Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ignore devices that throw ValueError: NULL pointer access #286

Merged
merged 8 commits into from
Jun 15, 2024
Merged
1 change: 1 addition & 0 deletions .github/workflows/lint-and-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
4 changes: 4 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 8 additions & 4 deletions scripts/lint.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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
Expand Down
5 changes: 3 additions & 2 deletions src/AutoSplit.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
7 changes: 4 additions & 3 deletions src/capture_method/Screenshot using QT attempt.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
16 changes: 5 additions & 11 deletions src/capture_method/VideoCaptureDeviceCaptureMethod.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import sys
from threading import Event, Thread
from typing import TYPE_CHECKING

Expand All @@ -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

Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/capture_method/XcbCaptureMethod.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
34 changes: 18 additions & 16 deletions src/capture_method/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import asyncio
import os
import sys
from collections import OrderedDict
Expand Down Expand Up @@ -77,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 = ""
Expand Down Expand Up @@ -113,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, /):
Expand Down Expand Up @@ -149,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()
Expand Down Expand Up @@ -211,15 +211,22 @@ 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


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?
Expand All @@ -246,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))))
5 changes: 2 additions & 3 deletions src/menu_bar.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import asyncio
import json
import sys
import webbrowser
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
Loading