Skip to content
Closed
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
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ See Git checking messages for full history.
- Linux: add the XShmGetImage backend with automatic XGetImage fallback and explicit status reporting (#431)
- Windows: improve error checking and messages for Win32 API calls (#448)
- Mac: fix memory leak (#450, #453)
- improve multithreading: allow multiple threads to use the same MSS object, allow multiple MSS objects to concurrently take screenshots, and document multithreading guarantees (#446, #452)
- :heart: contributors: @jholveck

## 10.1.0 (2025-08-16)
Expand All @@ -19,7 +20,7 @@ See Git checking messages for full history.
## 10.0.0 (2024-11-14)
- removed support for Python 3.8
- added support for Python 3.14
- Linux: fixed a threadding issue in `.close()` when calling `XCloseDisplay()` (#251)
- Linux: fixed a threading issue in `.close()` when calling `XCloseDisplay()` (#251)
- Linux: minor optimization when checking for a X extension status (#251)
- :heart: contributors: @kianmeng, @shravanasati, @mgorny

Expand Down
5 changes: 5 additions & 0 deletions docs/source/examples.rst
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,11 @@ You can get the bytes of the PNG image:
Advanced
========

.. _custom_cls_image:

Custom ScreenShot Subclass
--------------------------

You can handle data using a custom class:

.. literalinclude:: examples/custom_cls_image.py
Expand Down
29 changes: 27 additions & 2 deletions docs/source/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,37 @@ This is a much better usage, memory efficient::
Also, it is a good thing to save the MSS instance inside an attribute of your class and calling it when needed.


Multithreading
==============

MSS is thread-safe and can be used from multiple threads.

**Sharing one MSS object**: You can use the same MSS object from multiple threads. Calls to
:py:meth:`mss.base.MSSBase.grab` (and other capture methods) are serialized automatically, meaning only one thread
will capture at a time. This may be relaxed in a future version if it can be done safely.

**Using separate MSS objects**: You can also create different MSS objects in different threads. Whether these run
concurrently or are serialized by the OS depends on the platform.

Custom :py:class:`mss.screenshot.ScreenShot` classes (see :ref:`custom_cls_image`) must **not** call
:py:meth:`mss.base.MSSBase.grab` in their constructor.

.. danger::
These guarantees do not apply to the obsolete Xlib backend, :py:mod:`mss.linux.xlib`. That backend is only used
if you specifically request it, so you won't be caught off-guard.

.. versionadded:: 10.2.0
Prior to this version, on some operating systems, the MSS object could only be used on the thread on which it was
created.

.. _backends:

Backends
--------
========

Some platforms have multiple ways to take screenshots. In MSS, these are known as *backends*. The :py:func:`mss` functions will normally autodetect which one is appropriate for your situation, but you can override this if you want. For instance, you may know that your specific situation requires a particular backend.
Some platforms have multiple ways to take screenshots. In MSS, these are known as *backends*. The :py:func:`mss`
functions will normally autodetect which one is appropriate for your situation, but you can override this if you want.
For instance, you may know that your specific situation requires a particular backend.

If you want to choose a particular backend, you can use the :py:attr:`backend` keyword to :py:func:`mss`::

Expand Down
47 changes: 37 additions & 10 deletions src/mss/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@
#: Global lock protecting access to platform screenshot calls.
#:
#: .. versionadded:: 6.0.0
#:
#: .. deprecated:: 10.2.0
#: The global lock is no longer used, and will be removed in a future release.
#: MSS objects now have their own locks, which are not publicly-accessible.
lock = Lock()

OPAQUE = 255
Expand All @@ -55,7 +59,7 @@ class MSSBase(metaclass=ABCMeta):
``compression_level``, ``display``, ``max_displays``, and ``with_cursor`` keyword arguments.
"""

__slots__ = {"_closed", "_monitors", "cls_image", "compression_level", "with_cursor"}
__slots__ = {"_closed", "_lock", "_monitors", "cls_image", "compression_level", "with_cursor"}

def __init__(
self,
Expand All @@ -69,18 +73,33 @@ def __init__(
# Mac only
max_displays: int = 32, # noqa: ARG002
) -> None:
# The cls_image is only used atomically, so does not require locking.
self.cls_image: type[ScreenShot] = ScreenShot
# The compression level is only used atomically, so does not require locking.
#: PNG compression level used when saving the screenshot data into a file
#: (see :py:func:`zlib.compress()` for details).
#:
#: .. versionadded:: 3.2.0
self.compression_level = compression_level
# The with_cursor attribute is not meant to be changed after initialization.
#: Include the mouse cursor in screenshots.
#:
#: In some circumstances, it may not be possible to include the cursor. In that case, MSS will automatically
#: change this to False when the object is created.
#:
#: This should not be changed after creating the object.
#:
#: .. versionadded:: 8.0.0
self.with_cursor = with_cursor

# The attributes below are protected by self._lock. The attributes above are user-visible, so we don't
# control when they're modified. Currently, we only make sure that they're safe to modify while locked, or
# document that the user shouldn't change them. We could also use properties protect them against changes, or
# change them under the lock.
self._lock = Lock()
self._monitors: Monitors = []
self._closed = False

# If there isn't a factory that removed the "backend" argument, make sure that it was set to "default".
# Factories that do backend-specific dispatch should remove that argument.
if backend != "default":
Expand All @@ -97,24 +116,33 @@ def __exit__(self, *_: object) -> None:

@abstractmethod
def _cursor_impl(self) -> ScreenShot | None:
"""Retrieve all cursor data. Pixels have to be RGB."""
"""Retrieve all cursor data. Pixels have to be RGB.

The object lock will be held when this method is called.
"""

@abstractmethod
def _grab_impl(self, monitor: Monitor, /) -> ScreenShot:
"""Retrieve all pixels from a monitor. Pixels have to be RGB.
That method has to be run using a threading lock.

The object lock will be held when this method is called.
"""

@abstractmethod
def _monitors_impl(self) -> None:
"""Get positions of monitors (has to be run using a threading lock).
"""Get positions of monitors.

It must populate self._monitors.

The object lock will be held when this method is called.
"""

def _close_impl(self) -> None: # noqa:B027
"""Clean up.

This will be called at most once.

The object lock will be held when this method is called.
"""
# It's not necessary for subclasses to implement this if they have nothing to clean up.

Expand All @@ -133,7 +161,7 @@ def close(self) -> None:
with mss.mss() as sct:
...
"""
with lock:
with self._lock:
if self._closed:
return
self._close_impl()
Expand Down Expand Up @@ -165,7 +193,7 @@ def grab(self, monitor: Monitor | tuple[int, int, int, int], /) -> ScreenShot:
msg = f"Region has zero or negative size: {monitor!r}"
raise ScreenShotError(msg)

with lock:
with self._lock:
screenshot = self._grab_impl(monitor)
if self.with_cursor and (cursor := self._cursor_impl()):
return self._merge(screenshot, cursor)
Expand All @@ -190,11 +218,10 @@ def monitors(self) -> Monitors:
- ``width``: the width
- ``height``: the height
"""
if not self._monitors:
with lock:
with self._lock:
if not self._monitors:
self._monitors_impl()

return self._monitors
return self._monitors

def save(
self,
Expand Down
1 change: 1 addition & 0 deletions src/mss/darwin.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ class MSS(MSSBase):
def __init__(self, /, **kwargs: Any) -> None:
super().__init__(**kwargs)

# max_displays is only used by _monitors_impl, while the lock is held.
#: Maximum number of displays to handle.
self.max_displays = kwargs.get("max_displays", 32)

Expand Down
Loading