From ebe8fee885a275816731d8d11d478bec7944f3a3 Mon Sep 17 00:00:00 2001 From: Avasam Date: Sat, 15 Jun 2024 15:18:12 -0400 Subject: [PATCH 1/5] Fix devices that throw ValueError: NULL pointer access --- .vscode/settings.json | 4 ++++ pyproject.toml | 2 +- .../VideoCaptureDeviceCaptureMethod.py | 16 +++++----------- src/capture_method/__init__.py | 11 +++++++++-- 4 files changed, 19 insertions(+), 14 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 815e9679..138878f4 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -62,6 +62,10 @@ "[json][jsonc]": { "editor.defaultFormatter": "vscode.json-language-features", }, + "[yaml]": { + "editor.defaultFormatter": "redhat.vscode-yaml" + }, + "yaml.format.printWidth": 100, "[python]": { // Ruff as a formatter doesn't fully satisfy our needs yet: https://github.com/astral-sh/ruff/discussions/7310 "editor.defaultFormatter": "ms-python.autopep8", diff --git a/pyproject.toml b/pyproject.toml index 41b4e3a5..bb3a68c7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,7 @@ ignore = [ "ERA001", # eradicate: commented-out-code # contextlib.suppress is roughly 3x slower than try/except "SIM105", # flake8-simplify: use-contextlib-suppress - # Negative performance impact + # Slower and more verbose https://github.com/astral-sh/ruff/issues/7871 "UP038", # non-pep604-isinstance # Checked by type-checker (pyright) "ANN", # flake-annotations diff --git a/src/capture_method/VideoCaptureDeviceCaptureMethod.py b/src/capture_method/VideoCaptureDeviceCaptureMethod.py index fc62e1fc..29606f95 100644 --- a/src/capture_method/VideoCaptureDeviceCaptureMethod.py +++ b/src/capture_method/VideoCaptureDeviceCaptureMethod.py @@ -1,4 +1,3 @@ -import sys from threading import Event, Thread from typing import TYPE_CHECKING @@ -8,13 +7,11 @@ from cv2.typing import MatLike from typing_extensions import override +from capture_method import get_input_device_resolution from capture_method.CaptureMethodBase import CaptureMethodBase from error_messages import CREATE_NEW_ISSUE_MESSAGE, exception_traceback from utils import ImageShape, is_valid_image -if sys.platform == "win32": - from pygrabber.dshow_graph import FilterGraph - if TYPE_CHECKING: from AutoSplit import AutoSplit @@ -101,14 +98,11 @@ def __init__(self, autosplit: "AutoSplit"): return # Ensure we're using the right camera size. And not OpenCV's default 640x480 - if sys.platform == "win32": - filter_graph = FilterGraph() - filter_graph.add_video_input_device(autosplit.settings_dict["capture_device_id"]) - width, height = filter_graph.get_input_device().get_current_format() - filter_graph.remove_filters() + resolution = get_input_device_resolution(autosplit.settings_dict["capture_device_id"]) + if resolution is not None: try: - self.capture_device.set(cv2.CAP_PROP_FRAME_WIDTH, width) - self.capture_device.set(cv2.CAP_PROP_FRAME_HEIGHT, height) + self.capture_device.set(cv2.CAP_PROP_FRAME_WIDTH, resolution[0]) + self.capture_device.set(cv2.CAP_PROP_FRAME_HEIGHT, resolution[1]) except cv2.error: # Some cameras don't allow changing the resolution pass diff --git a/src/capture_method/__init__.py b/src/capture_method/__init__.py index 3c5c28b1..33dea47f 100644 --- a/src/capture_method/__init__.py +++ b/src/capture_method/__init__.py @@ -210,8 +210,15 @@ def get_input_device_resolution(index: int) -> tuple[int, int] | None: # https://github.com/Toufool/AutoSplit/issues/238 except COMError: return None - resolution = filter_graph.get_input_device().get_current_format() - filter_graph.remove_filters() + + try: + resolution = filter_graph.get_input_device().get_current_format() + # For unknown reasons, some devices can raise "ValueError: NULL pointer access". + # For instance, Oh_DeeR's AVerMedia HD Capture C985 Bus 12 + except ValueError: + return None + finally: + filter_graph.remove_filters() return resolution From 123f8f8c60c4a52ff327c7e9c2ddfac9541e1b40 Mon Sep 17 00:00:00 2001 From: Avasam Date: Sat, 15 Jun 2024 17:14:01 -0400 Subject: [PATCH 2/5] Lint fixes --- .github/workflows/lint-and-build.yml | 1 + scripts/lint.ps1 | 12 ++++++---- src/AutoSplit.py | 9 +++----- .../Screenshot using QT attempt.py | 7 +++--- src/capture_method/__init__.py | 22 ++++++++----------- src/menu_bar.py | 5 ++--- 6 files changed, 27 insertions(+), 29 deletions(-) diff --git a/.github/workflows/lint-and-build.yml b/.github/workflows/lint-and-build.yml index 85f116ab..1089c845 100644 --- a/.github/workflows/lint-and-build.yml +++ b/.github/workflows/lint-and-build.yml @@ -89,6 +89,7 @@ jobs: - name: Analysing the code with Pyright uses: jakebailey/pyright-action@v1 with: + version: "1.1.364" working-directory: src/ python-version: ${{ matrix.python-version }} Build: diff --git a/scripts/lint.ps1 b/scripts/lint.ps1 index 84dc6f8b..3e0eacb1 100644 --- a/scripts/lint.ps1 +++ b/scripts/lint.ps1 @@ -6,7 +6,7 @@ Write-Host "`nRunning formatting..." autopep8 src/ --recursive --in-place add-trailing-comma $(git ls-files '**.py*') -Write-Host "`nRunning Ruff..." +Write-Host "`nRunning Ruff ..." ruff check . --fix $exitCodes += $LastExitCode if ($LastExitCode -gt 0) { @@ -16,12 +16,16 @@ else { Write-Host "`Ruff passed" -ForegroundColor Green } -Write-Host "`nRunning Pyright..." -$Env:PYRIGHT_PYTHON_FORCE_VERSION = 'latest' -npx pyright@latest src/ +$pyrightVersion = '1.1.364' # Change this if latest has issues +Write-Host "`nRunning Pyright $pyrightVersion ..." +$Env:PYRIGHT_PYTHON_FORCE_VERSION = $pyrightVersion +npx -y pyright@$pyrightVersion src/ $exitCodes += $LastExitCode if ($LastExitCode -gt 0) { Write-Host "`Pyright failed ($LastExitCode)" -ForegroundColor Red + if ($pyrightVersion -eq 'latest') { + npx pyright@latest --version + } } else { Write-Host "`Pyright passed" -ForegroundColor Green diff --git a/src/AutoSplit.py b/src/AutoSplit.py index b24d97c8..9314101a 100644 --- a/src/AutoSplit.py +++ b/src/AutoSplit.py @@ -323,8 +323,7 @@ def __compare_capture_for_auto_start(self): start_image_similarity = self.start_image.compare_with_capture(self, capture) # If the similarity becomes higher than highest similarity, set it as such. - if start_image_similarity > self.highest_similarity: - self.highest_similarity = start_image_similarity + self.highest_similarity = max(start_image_similarity, self.highest_similarity) self.table_current_image_live_label.setText(decimal(start_image_similarity)) self.table_current_image_highest_label.setText(decimal(self.highest_similarity)) @@ -672,8 +671,7 @@ def __similarity_threshold_loop(self, number_of_split_images: int, dummy_splits_ self.table_current_image_live_label.setText(decimal(similarity)) # if the similarity becomes higher than highest similarity, set it as such. - if similarity > self.highest_similarity: - self.highest_similarity = similarity + self.highest_similarity = max(similarity, self.highest_similarity) # show live highest similarity if the checkbox is checked self.table_current_image_highest_label.setText(decimal(self.highest_similarity)) @@ -841,8 +839,7 @@ def __reset_if_should(self, capture: MatLike | None): self.table_reset_image_live_label.setText("paused") else: should_reset = similarity >= threshold - if similarity > self.reset_highest_similarity: - self.reset_highest_similarity = similarity + self.reset_highest_similarity = max(similarity, self.reset_highest_similarity) self.table_reset_image_highest_label.setText(decimal(self.reset_highest_similarity)) self.table_reset_image_live_label.setText(decimal(similarity)) diff --git a/src/capture_method/Screenshot using QT attempt.py b/src/capture_method/Screenshot using QT attempt.py index fa55e8d5..abb3d3af 100644 --- a/src/capture_method/Screenshot using QT attempt.py +++ b/src/capture_method/Screenshot using QT attempt.py @@ -1,17 +1,18 @@ -# flake8: noqa +# ruff: noqa: RET504 import sys if sys.platform != "linux": - raise OSError() + raise OSError from typing import cast import numpy as np from cv2.typing import MatLike from PySide6.QtCore import QBuffer, QIODeviceBase from PySide6.QtGui import QGuiApplication -from capture_method.CaptureMethodBase import CaptureMethodBase from typing_extensions import override +from capture_method.CaptureMethodBase import CaptureMethodBase + class QtCaptureMethod(CaptureMethodBase): _render_full_content = False diff --git a/src/capture_method/__init__.py b/src/capture_method/__init__.py index 33dea47f..7d8eba8f 100644 --- a/src/capture_method/__init__.py +++ b/src/capture_method/__init__.py @@ -1,4 +1,3 @@ -import asyncio import os import sys from collections import OrderedDict @@ -15,6 +14,7 @@ if sys.platform == "win32": from _ctypes import COMError # noqa: PLC2701 + from pygrabber.dshow_graph import FilterGraph from capture_method.BitBltCaptureMethod import BitBltCaptureMethod @@ -76,7 +76,7 @@ def __hash__(self): @override @staticmethod - def _generate_next_value_(name: "str | CaptureMethodEnum", *_): + def _generate_next_value_(name: str, start: int, count: int, last_values: list["str | CaptureMethodEnum"]): return name NONE = "" @@ -112,10 +112,11 @@ def get_method_by_index(self, index: int): # Disallow unsafe get w/o breaking it at runtime @override def __getitem__( # type:ignore[override] # pyright: ignore[reportIncompatibleMethodOverride] - self, - __key: Never, + self, + key: Never, + /, ) -> type[CaptureMethodBase]: - return super().__getitem__(__key) + return super().__getitem__(key) @override def get(self, key: CaptureMethodEnum, default: object = None, /): @@ -222,10 +223,10 @@ def get_input_device_resolution(index: int) -> tuple[int, int] | None: return resolution -async def get_all_video_capture_devices(): +def get_all_video_capture_devices(): named_video_inputs = get_input_devices() - async def get_camera_info(index: int, device_name: str): + def get_camera_info(index: int, device_name: str): backend = "" # Probing freezes some devices (like GV-USB2 and AverMedia) if already in use. See #169 # FIXME: Maybe offer the option to the user to obtain more info about their devices? @@ -252,9 +253,4 @@ async def get_camera_info(index: int, device_name: str): else None ) - return [ - camera_info - for camera_info - in await asyncio.gather(*starmap(get_camera_info, enumerate(named_video_inputs))) - if camera_info is not None - ] + return list(filter(None, starmap(get_camera_info, enumerate(named_video_inputs)))) diff --git a/src/menu_bar.py b/src/menu_bar.py index c47e880d..a01fe031 100644 --- a/src/menu_bar.py +++ b/src/menu_bar.py @@ -1,4 +1,3 @@ -import asyncio import json import sys import webbrowser @@ -135,7 +134,7 @@ def __init__(self, autosplit: "AutoSplit"): self.__video_capture_devices: list[CameraInfo] = [] """ Used to temporarily store the existing cameras, - we don't want to call `get_all_video_capture_devices` agains and possibly have a different result + we don't want to call `get_all_video_capture_devices` again and possibly have a different result """ self.setupUi(self) @@ -246,7 +245,7 @@ def __fps_limit_changed(self, value: int): @fire_and_forget def __set_all_capture_devices(self): - self.__video_capture_devices = asyncio.run(get_all_video_capture_devices()) + self.__video_capture_devices = get_all_video_capture_devices() if len(self.__video_capture_devices) > 0: for i in range(self.capture_device_combobox.count()): self.capture_device_combobox.removeItem(i) From 79eb7857237dd5ce59289928ccf354f829085137 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 15 Jun 2024 21:14:09 +0000 Subject: [PATCH 3/5] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/capture_method/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/capture_method/__init__.py b/src/capture_method/__init__.py index 7d8eba8f..91f50bce 100644 --- a/src/capture_method/__init__.py +++ b/src/capture_method/__init__.py @@ -14,7 +14,6 @@ if sys.platform == "win32": from _ctypes import COMError # noqa: PLC2701 - from pygrabber.dshow_graph import FilterGraph from capture_method.BitBltCaptureMethod import BitBltCaptureMethod From 158327f42b65a6dcab05d0be3944bc5a86200f7a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 15 Jun 2024 21:17:08 +0000 Subject: [PATCH 4/5] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/capture_method/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/capture_method/__init__.py b/src/capture_method/__init__.py index 91f50bce..7d8eba8f 100644 --- a/src/capture_method/__init__.py +++ b/src/capture_method/__init__.py @@ -14,6 +14,7 @@ if sys.platform == "win32": from _ctypes import COMError # noqa: PLC2701 + from pygrabber.dshow_graph import FilterGraph from capture_method.BitBltCaptureMethod import BitBltCaptureMethod From e1806ae2093951d0368a14bc44858c880a127599 Mon Sep 17 00:00:00 2001 From: Avasam Date: Sat, 15 Jun 2024 17:30:03 -0400 Subject: [PATCH 5/5] Fix Linux type-checking issues --- src/AutoSplit.py | 5 +++-- src/capture_method/XcbCaptureMethod.py | 2 +- src/capture_method/__init__.py | 3 ++- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/AutoSplit.py b/src/AutoSplit.py index 9314101a..20bbfab0 100644 --- a/src/AutoSplit.py +++ b/src/AutoSplit.py @@ -6,7 +6,7 @@ from copy import deepcopy from time import time from types import FunctionType -from typing import NoReturn +from typing import NoReturn, cast import cv2 from cv2.typing import MatLike @@ -946,7 +946,8 @@ def set_preview_image(qlabel: QLabel, image: MatLike | None): capture = image qimage = QtGui.QImage( - capture.data, + # Try to update PySide6, see https://bugreports.qt.io/browse/QTBUG-114635 + cast(bytes, capture.data) if sys.platform == "linux" else capture.data, width, height, width * channels, diff --git a/src/capture_method/XcbCaptureMethod.py b/src/capture_method/XcbCaptureMethod.py index 7f56f41f..7c957a1d 100644 --- a/src/capture_method/XcbCaptureMethod.py +++ b/src/capture_method/XcbCaptureMethod.py @@ -39,7 +39,7 @@ def get_frame(self): selection = self._autosplit_ref.settings_dict["capture_region"] x = selection["x"] + offset_x y = selection["y"] + offset_y - image = ImageGrab.grab( + image = ImageGrab.grab( # pyright: ignore[reportUnknownMemberType] # TODO: Fix upstream ( x, y, diff --git a/src/capture_method/__init__.py b/src/capture_method/__init__.py index 91f50bce..e099d444 100644 --- a/src/capture_method/__init__.py +++ b/src/capture_method/__init__.py @@ -14,6 +14,7 @@ if sys.platform == "win32": from _ctypes import COMError # noqa: PLC2701 + from pygrabber.dshow_graph import FilterGraph from capture_method.BitBltCaptureMethod import BitBltCaptureMethod @@ -148,7 +149,7 @@ def get(self, key: CaptureMethodEnum, default: object = None, /): CAPTURE_METHODS[CaptureMethodEnum.DESKTOP_DUPLICATION] = DesktopDuplicationCaptureMethod CAPTURE_METHODS[CaptureMethodEnum.PRINTWINDOW_RENDERFULLCONTENT] = ForceFullContentRenderingCaptureMethod elif sys.platform == "linux": - if features.check_feature(feature="xcb"): + if features.check_feature(feature="xcb"): # pyright: ignore[reportUnknownMemberType] # TODO: Fix upstream CAPTURE_METHODS[CaptureMethodEnum.XCB] = XcbCaptureMethod try: pyscreeze.screenshot()