Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,14 @@ Run tests:
python -m pytest
```

When adding, removing, or renaming a packaged file, update the explicit expected file list in
`src/tests/test_setup.py::test_sdist` and, for files included in binary distributions, in
`src/tests/test_setup.py::test_wheel`. Run the focused packaging tests:

```shell
python -m pytest src/tests/test_setup.py::test_sdist src/tests/test_setup.py::test_wheel
```

On headless GNU/Linux environments, run tests with a virtual display:

```shell
Expand Down
16 changes: 10 additions & 6 deletions demos/video-capture-simple.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,17 +72,21 @@ def main() -> None:
monitor = sct.monitors[1]

# Because of how H.264 video stores color information, libx264 requires the video size to be a multiple of
# two.
monitor["width"] = (monitor["width"] // 2) * 2
monitor["height"] = (monitor["height"] // 2) * 2
# two. Keep the monitor description unchanged and capture a slightly smaller region when necessary.
capture_region = {
"left": monitor.left,
"top": monitor.top,
"width": (monitor.width // 2) * 2,
"height": (monitor.height // 2) * 2,
}

with av.open(FILENAME, "w") as avmux:
# The "avmux" object we get back from "av.open" represents the MP4 file. That's a container that holds
# the video, as well as possibly audio and more. These are each called "streams". We only create one
# stream here, since we're just recording video.
video_stream = avmux.add_stream(CODEC, rate=FPS, options=CODEC_OPTIONS)
video_stream.width = monitor["width"]
video_stream.height = monitor["height"]
video_stream.width = capture_region["width"]
video_stream.height = capture_region["height"]
# There are more options you can set on the video stream; the full demo uses some of those.

# Count how many frames we're capturing, so we can log the FPS later.
Expand Down Expand Up @@ -121,7 +125,7 @@ def main() -> None:
print(".", end="", flush=True)

# Grab a screenshot.
screenshot = sct.grab(monitor)
screenshot = sct.grab(capture_region)
frame_count += 1

# There are a few ways to get the screenshot into a VideoFrame. The highest-performance way isn't
Expand Down
20 changes: 10 additions & 10 deletions demos/video-capture.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,12 +122,11 @@

# Install the necessary libraries with "pip install av mss numpy si-prefix".
import av
import numpy as np
from si_prefix import si_format

import mss
from common.pipeline import Mailbox, PipelineStage

from common.pipeline import Mailbox, PipelineStage

# These are the options you'd give to ffmpeg that it sends to the video codec. Because ffmpeg and PyAV both use the
# libav libraries, you can get the list of available flags with `ffmpeg -help encoder=libx264`, or whatever encoder
Expand Down Expand Up @@ -165,7 +164,7 @@
def video_capture(
fps: int,
sct: mss.MSS,
monitor: mss.models.Monitor,
capture_region: dict[str, int],
shutdown_requested: Event,
) -> Generator[tuple[mss.screenshot.ScreenShot, float], None, None]:
# Keep track of the time when we want to get the next frame. We limit the frame time this way instead of sleeping
Expand All @@ -183,7 +182,7 @@ def video_capture(
time.sleep(next_frame_at - now)

# Capture a frame, and send it to the next processing stage.
screenshot = sct.grab(monitor)
screenshot = sct.grab(capture_region)
yield screenshot, now

# We try to keep the capture rate at the desired fps on average. If we can't quite keep up for a moment (such
Expand Down Expand Up @@ -436,14 +435,15 @@ def main() -> None:
with mss.MSS() as sct:
if args.region:
left, top, right, bottom = args.region
monitor = {
capture_region = {
"left": left,
"top": top,
"width": right - left,
"height": bottom - top,
}
else:
monitor = sct.monitors[args.monitor]
capture_region = monitor.as_capture_region()

# Some codecs, such as libx264, require the region to be a multiple of 2, to get the chroma subsampling right.
# Others, such as h264_nvenc, do not; they'll pad to get the subsampling region, and add flags to the stream
Expand All @@ -453,8 +453,8 @@ def main() -> None:
# it (at least, when using 4:2:0 subsampling).
region_crop_to_multiple_of_two = codec in {"libx264", "libx265"}
if region_crop_to_multiple_of_two:
monitor["width"] = (monitor["width"] // 2) * 2
monitor["height"] = (monitor["height"] // 2) * 2
capture_region["width"] = (capture_region["width"] // 2) * 2
capture_region["height"] = (capture_region["height"] // 2) * 2

# We don't pass the container format to av.open here, so it will choose it based on the extension: .mp4, .mkv,
# etc.
Expand Down Expand Up @@ -493,8 +493,8 @@ def main() -> None:
# so some video encoders will tag it as AVCOL_TRC_BT709 (1) instead.
video_stream.color_trc = 13

video_stream.width = monitor["width"]
video_stream.height = monitor["height"]
video_stream.width = capture_region["width"]
video_stream.height = capture_region["height"]
# There are multiple time bases in play (stream, codec context, per-frame). Depending on the container
# and codec, some of these might be ignored or overridden. We set the desired time base consistently
# everywhere, so that the saved timestamps are correct regardless of what format we're saving to.
Expand Down Expand Up @@ -535,7 +535,7 @@ def main() -> None:
video_capture,
fps,
sct,
monitor,
capture_region,
shutdown_requested,
),
out_mailbox=mailbox_screenshot,
Expand Down
6 changes: 3 additions & 3 deletions docs/source/examples/custom_cls_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from typing import Any

import mss
from mss.models import Monitor
from mss.models import CaptureRegion
from mss.screenshot import ScreenShot


Expand All @@ -17,9 +17,9 @@ class SimpleScreenShot(ScreenShot):
or add new methods.
"""

def __init__(self, data: bytearray, monitor: Monitor, **_: Any) -> None:
def __init__(self, data: bytearray, region: CaptureRegion, **_: Any) -> None:
self.data = data
self.monitor = monitor
self.region = region


with mss.MSS() as sct:
Expand Down
4 changes: 2 additions & 2 deletions docs/source/examples/from_pil_tuple.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
monitor = sct.monitors[1]

# Capture a bbox using percent values
left = monitor["left"] + monitor["width"] * 5 // 100 # 5% from the left
top = monitor["top"] + monitor["height"] * 5 // 100 # 5% from the top
left = monitor.left + monitor.width * 5 // 100 # 5% from the left
top = monitor.top + monitor.height * 5 // 100 # 5% from the top
right = left + 400 # 400px width
lower = top + 400 # 400px height
bbox = (left, top, right, lower)
Expand Down
4 changes: 2 additions & 2 deletions docs/source/examples/part_of_screen_monitor_2.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@

# The screen part to capture
monitor = {
"top": mon["top"] + 100, # 100px from the top
"left": mon["left"] + 100, # 100px from the left
"top": mon.top + 100, # 100px from the top
"left": mon.left + 100, # 100px from the left
"width": 160,
"height": 135,
"mon": monitor_number,
Expand Down
10 changes: 9 additions & 1 deletion docs/source/release-history/v10.2.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ In 11.0, monitor dictionaries will become a dedicated **`Monitor` class**.

To maintain compatibility:

- dictionary-style access will continue to work
- string-key access will temporarily continue to work

```python
monitor["left"]
Expand All @@ -269,6 +269,14 @@ monitor["top"]

- `grab()` will continue accepting dictionaries

The compatibility access does not make `Monitor` a complete mapping. Migrate dictionary methods, membership tests, and
unpacking to attribute access:

```python
monitor.left
monitor.top
```

If you use type annotations, you can switch to the provided `Monitor` type:

```python
Expand Down
22 changes: 22 additions & 0 deletions docs/source/release-history/v11.0.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,28 @@ The {py:attr}`mss.ScreenShot.bgra` and {py:attr}`mss.ScreenShot.rgb` properties
{py:class}`memoryview` objects, rather than {py:class}`bytes` or {py:class}`bytearray` objects. For practical use
cases, this should not be noticeable. This change allows faster access to screenshot data, with fewer memory copies.

#### Immutable monitor objects

{py:attr}`mss.MSS.monitors` now returns frozen, slotted {py:class}`mss.models.Monitor` objects instead of dictionaries.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why frozen? (I was going to ask about that in your latest proposal.)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have added documentation to explain why. It is important that customers cannot modify the monitors because they are owned by MSS and should be immutable.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To clarify: I hid interpreted "frozen" in the sense of C++'s final, or possibly making some sort of future compatibility guarantee, but now that I see your latest commit, I see you meant it in the sense of "immutable".

Might that be a better term?

(I'm also not sure on why it matters to users that they're slotted, but that's not a problem.)

Monitor entries are cached display-configuration snapshots; keeping them immutable prevents application code from
accidentally changing the geometry or metadata held by an {py:class}`mss.MSS` instance.
Use attributes for geometry and metadata:

```python
monitor = sct.monitors[1]
print(monitor.left, monitor.top, monitor.width, monitor.height)
print(monitor.is_primary, monitor.name, monitor.unique_id, monitor.output)
region = monitor.as_capture_region()
```

The four geometry attributes are always present. Metadata attributes are `None` when unavailable. String-key access
such as `monitor["width"]` remains temporarily available for migration, but `Monitor` is not a mapping: dictionary
methods, membership tests, and `**monitor` unpacking are not supported.
Use {py:meth}`mss.models.Monitor.as_capture_region` when a mutable capture-region dictionary is needed.

Existing regions can still be passed to {py:meth}`mss.MSS.grab` as dictionaries or PIL-style
`(left, top, right, bottom)` tuples.

### Python 3.9 EOL

Python 3.9 reached [end-of-life](https://devguide.python.org/developer-workflow/development-cycle/index.html#end-of-life-branches) on [October 31, 2025](https://devguide.python.org/versions/). It is no longer receiving any updates, even security updates.
Expand Down
15 changes: 14 additions & 1 deletion docs/source/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,19 @@ list of all the monitors, starting from index 1, as well as the full virtual scr
The primary monitor, the one that holds the taskbar or similar system UI, is available as
:py:attr:`mss.MSS.primary_monitor`.

Each entry is an immutable :py:class:`mss.models.Monitor`. Its geometry is available through ``left``, ``top``,
``width``, and ``height`` attributes. The ``is_primary``, ``name``, ``unique_id``, and ``output`` metadata attributes
are ``None`` when unavailable::

monitor = sct.monitors[1]
print(monitor.width, monitor.height)

Call ``monitor.as_capture_region()`` when you need its geometry as a mutable capture-region dictionary.

For migration from MSS 10, string-key access such as ``monitor["width"]`` is temporarily supported. ``Monitor`` is not
a mapping, so dictionary methods, membership tests, and ``**monitor`` unpacking are not supported. New code should use
attributes.

For capturing a specific region, you can pass :py:meth:`mss.MSS.grab` a dictionary with the keys ``top``, ``left``,
``width``, and ``height``. For instance, to capture a 100x100 pixel region starting at the top-left corner of the
screen, you could use :python:`{"top": 0, "left": 0, "width": 100, "height": 100}`. You can also use a PIL-style box,
Expand Down Expand Up @@ -323,4 +336,4 @@ Or via direct call from Python::

.. versionadded:: 11.0.0
``--coordinates`` now accepts coordinates in the traditional X11 style (WIDTHxHEIGHT+LEFT+TOP), as well as negative
left or top values (in either style).
left or top values (in either style).
4 changes: 2 additions & 2 deletions src/mss/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,9 +155,9 @@ def _capture_and_save(
"""Capture screenshots and write output files."""
if coordinates is not None:
if coordinates["top"] < 0:
coordinates["top"] = sct.monitors[monitor_index]["height"] + coordinates["top"]
coordinates["top"] = sct.monitors[monitor_index].height + coordinates["top"]
if coordinates["left"] < 0:
coordinates["left"] = sct.monitors[monitor_index]["width"] + coordinates["left"]
coordinates["left"] = sct.monitors[monitor_index].width + coordinates["left"]
output = output_template.format(**coordinates)
sct_img = sct.grab(coordinates)
to_png(sct_img.rgb, sct_img.size, level=options.level, output=output)
Expand Down
Loading