diff --git a/.github/workflows/lint-and-build.yml b/.github/workflows/lint-and-build.yml index 2cc32c45..dfcbdc49 100644 --- a/.github/workflows/lint-and-build.yml +++ b/.github/workflows/lint-and-build.yml @@ -40,7 +40,7 @@ jobs: runs-on: windows-latest strategy: fail-fast: false - # Ruff is version and platform sensible + # Ruff is version sensible matrix: python-version: ["3.9", "3.10", "3.11"] steps: @@ -95,7 +95,7 @@ jobs: fail-fast: false # Only the Python version we plan on shipping matters. matrix: - python-version: ["3.10", "3.11"] + python-version: ["3.11"] steps: - name: Checkout ${{ github.repository }}/${{ github.ref }} uses: actions/checkout@v3 diff --git a/.vscode/settings.json b/.vscode/settings.json index d82bf1b4..8405b12c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -59,8 +59,7 @@ }, "[python]": { // Ruff is not yet a formatter: https://github.com/charliermarsh/ruff/issues/1904 - // Cannot use autotpep8 until https://github.com/microsoft/vscode-autopep8/issues/32 is fixed - "editor.defaultFormatter": "ms-python.python", + "editor.defaultFormatter": "ms-python.autopep8", "editor.tabSize": 4, "editor.rulers": [ 72, // PEP8-17 docstrings @@ -73,7 +72,6 @@ // Important to follow the config in pyrightconfig.json "python.analysis.useLibraryCodeForTypes": false, "python.analysis.diagnosticMode": "workspace", - "python.formatting.provider": "autopep8", "python.linting.enabled": true, "ruff.importStrategy": "fromEnvironment", // Use the Ruff extension instead @@ -84,6 +82,8 @@ "python.linting.pycodestyleEnabled": false, "python.linting.pylamaEnabled": false, "python.linting.pylintEnabled": false, + // Use the autopep8 extension instead + "python.formatting.provider": "none", // Use Pyright/Pylance instead "python.linting.mypyEnabled": false, "powershell.codeFormatting.pipelineIndentationStyle": "IncreaseIndentationForFirstPipeline", @@ -96,4 +96,18 @@ "terminal.integrated.defaultProfile.windows": "PowerShell", "xml.codeLens.enabled": true, "xml.format.spaceBeforeEmptyCloseTag": false, + "xml.format.preserveSpace": [ + // Default + "xsl:text", + "xsl:comment", + "xsl:processing-instruction", + "literallayout", + "programlisting", + "screen", + "synopsis", + "pre", + "xd:pre", + // Custom + "string" + ] } diff --git a/README.md b/README.md index 2b17ebc6..8a62ba64 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,7 @@ Refer to the [build instructions](build%20instructions.md) if you'd like to buil - Perceptual Hash: An explanation on pHash comparison can be found [here](http://www.hackerfactor.com/blog/index.php?/archives/432-Looks-Like-It.html). It is highly recommended to NOT use pHash if you use masked images, or it'll be very inaccurate. #### Capture Method + - **Windows Graphics Capture** (fast, most compatible, capped at 60fps) Only available in Windows 10.0.17134 and up. @@ -94,7 +95,8 @@ Refer to the [build instructions](build%20instructions.md) if you'd like to buil #### Capture Device -Select the Video Capture Device that you wanna use if selecting the `Video Capture Device` Capture Method. +Select the Video Capture Device that you wanna use if selecting the `Video Capture Device` Capture Method. + #### Show Live Similarity diff --git a/pyproject.toml b/pyproject.toml index f42167f0..a69cfa99 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -97,7 +97,6 @@ max-branches = 15 # https://github.com/hhatto/autopep8#more-advanced-usage [tool.autopep8] max_line_length = 120 -recursive = true aggressive = 3 ignore = [ "E124", # Closing bracket may not match multi-line method invocation style (enforced by add-trailing-comma) diff --git a/res/about.ui b/res/about.ui index 5aa56a75..329dac8c 100644 --- a/res/about.ui +++ b/res/about.ui @@ -32,9 +32,7 @@ - :/resources/icon.ico - :/resources/icon.ico - + :/resources/icon.ico:/resources/icon.ico @@ -115,7 +113,7 @@ Thank you! - 181 + 190 17 64 64 diff --git a/res/design.ui b/res/design.ui index afb56b1d..34f92d5a 100644 --- a/res/design.ui +++ b/res/design.ui @@ -6,20 +6,20 @@ 0 0 - 777 - 424 + 786 + 426 - 777 - 424 + 786 + 426 - 777 - 424 + 786 + 426 @@ -39,8 +39,8 @@ 11 - 143 - 44 + 140 + 49 20 @@ -56,7 +56,7 @@ 10 67 - 101 + 107 23 @@ -70,7 +70,7 @@ - 650 + 657 369 121 27 @@ -89,7 +89,7 @@ - 650 + 657 339 121 27 @@ -108,7 +108,7 @@ - 650 + 657 310 59 27 @@ -127,7 +127,7 @@ - 712 + 719 310 59 27 @@ -144,7 +144,7 @@ 10 - 253 + 270 53 23 @@ -163,8 +163,8 @@ 92 - 254 - 20 + 272 + 21 20 @@ -175,7 +175,7 @@ - 120 + 127 67 320 240 @@ -203,7 +203,7 @@ - 449 + 456 67 320 240 @@ -222,7 +222,7 @@ - 449 + 456 31 318 20 @@ -239,8 +239,8 @@ 11 - 183 - 44 + 190 + 49 20 @@ -255,8 +255,8 @@ 66 - 183 - 44 + 190 + 49 20 @@ -271,7 +271,7 @@ 65 - 254 + 272 26 20 @@ -284,8 +284,8 @@ 11 - 200 - 44 + 210 + 51 24 @@ -306,8 +306,8 @@ 66 - 200 - 44 + 210 + 51 24 @@ -327,7 +327,7 @@ - 120 + 127 31 318 20 @@ -343,7 +343,7 @@ - 477 + 484 49 264 20 @@ -360,8 +360,8 @@ 10 - 227 - 101 + 240 + 107 23 @@ -377,7 +377,7 @@ 11 160 - 44 + 51 24 @@ -405,7 +405,7 @@ 66 160 - 44 + 51 24 @@ -426,8 +426,8 @@ 66 - 143 - 44 + 140 + 49 20 @@ -443,7 +443,7 @@ 10 119 - 101 + 107 23 @@ -462,7 +462,7 @@ 10 93 - 101 + 107 23 @@ -478,7 +478,7 @@ 696 5 - 75 + 81 24 @@ -494,7 +494,7 @@ 10 9 - 98 + 111 16 @@ -505,9 +505,9 @@ - 119 + 127 6 - 574 + 561 22 @@ -518,7 +518,7 @@ - 120 + 127 49 318 20 @@ -534,9 +534,9 @@ - 451 + 458 313 - 67 + 81 20 @@ -547,7 +547,7 @@ - 120 + 127 312 321 84 @@ -798,9 +798,9 @@ - 450 + 457 369 - 121 + 131 27 @@ -814,10 +814,10 @@ - 449 + 458 344 - 101 - 16 + 121 + 20 @@ -827,10 +827,10 @@ - 560 + 577 344 - 98 - 16 + 81 + 20 @@ -840,9 +840,9 @@ - 520 + 537 313 - 131 + 121 20 @@ -856,7 +856,7 @@ - 448 + 455 49 27 18 @@ -878,7 +878,7 @@ - 743 + 750 49 27 18 @@ -936,7 +936,7 @@ 0 0 - 777 + 786 22 diff --git a/res/settings.ui b/res/settings.ui index ec00fb6b..8681dc44 100644 --- a/res/settings.ui +++ b/res/settings.ui @@ -6,20 +6,20 @@ 0 0 - 291 - 661 + 290 + 664 - 291 - 661 + 290 + 664 - 291 - 661 + 290 + 664 @@ -32,16 +32,14 @@ - :/resources/icon.ico - :/resources/icon.ico - + :/resources/icon.ico:/resources/icon.ico 10 200 - 271 + 270 181 @@ -51,10 +49,10 @@ - 138 - 25 + 150 + 24 51 - 22 + 24 @@ -75,7 +73,7 @@ 6 27 - 121 + 141 16 @@ -87,14 +85,11 @@ - - true - 6 49 - 129 + 141 20 @@ -166,8 +161,8 @@ 10 390 - 271 - 261 + 270 + 266 @@ -185,9 +180,9 @@ - 167 + 170 25 - 88 + 91 22 @@ -202,8 +197,8 @@ Histograms: An explanation on Histograms comparison can be found here https://mpatacchiola.github.io/blog/2016/11/12/the-simplest-classifier-histogram-intersection.html This is a great method to use if you are using several masked images. -> This algorithm is particular reliable when the colour is a strong predictor of the object identity. -> The histogram intersection [...] is robust to occluding objects in the foreground. +> This algorithm is particular reliable when the colour is a strong predictor of the object identity. +> The histogram intersection [...] is robust to occluding objects in the foreground. Perceptual Hash: An explanation on pHash comparison can be found here @@ -231,7 +226,7 @@ It is highly recommended to NOT use pHash if you use masked images, or it'll be 6 28 - 161 + 171 16 @@ -244,7 +239,7 @@ It is highly recommended to NOT use pHash if you use masked images, or it'll be 6 118 - 161 + 171 16 @@ -255,10 +250,10 @@ It is highly recommended to NOT use pHash if you use masked images, or it'll be - 167 + 170 115 - 87 - 22 + 91 + 24 @@ -285,7 +280,7 @@ It is highly recommended to NOT use pHash if you use masked images, or it'll be 6 58 - 151 + 171 16 @@ -296,10 +291,10 @@ It is highly recommended to NOT use pHash if you use masked images, or it'll be - 167 + 170 55 - 52 - 22 + 51 + 24 @@ -319,14 +314,11 @@ It is highly recommended to NOT use pHash if you use masked images, or it'll be - - true - 6 143 - 235 + 261 20 @@ -344,9 +336,9 @@ It is highly recommended to NOT use pHash if you use masked images, or it'll be 6 - 190 + 193 261 - 61 + 71 @@ -367,7 +359,7 @@ It is highly recommended to NOT use pHash if you use masked images, or it'll be 6 88 - 161 + 171 16 @@ -378,10 +370,10 @@ It is highly recommended to NOT use pHash if you use masked images, or it'll be - 167 + 170 85 - 87 - 22 + 91 + 24 @@ -398,13 +390,14 @@ It is highly recommended to NOT use pHash if you use masked images, or it'll be 140 - 210 + 218 71 31 + Segoe UI 8 true @@ -427,7 +420,7 @@ It is highly recommended to NOT use pHash if you use masked images, or it'll be 6 168 - 151 + 171 20 @@ -444,7 +437,7 @@ It is highly recommended to NOT use pHash if you use masked images, or it'll be 10 10 - 271 + 270 191 @@ -466,7 +459,7 @@ It is highly recommended to NOT use pHash if you use masked images, or it'll be 180 130 81 - 21 + 22 @@ -477,15 +470,12 @@ It is highly recommended to NOT use pHash if you use masked images, or it'll be - - true - - 76 + 80 30 94 - 20 + 22 @@ -498,10 +488,10 @@ It is highly recommended to NOT use pHash if you use masked images, or it'll be - 76 + 80 80 94 - 20 + 22 @@ -527,10 +517,10 @@ It is highly recommended to NOT use pHash if you use masked images, or it'll be - 76 + 80 55 94 - 20 + 22 @@ -546,7 +536,7 @@ It is highly recommended to NOT use pHash if you use masked images, or it'll be 180 80 81 - 21 + 22 @@ -591,7 +581,7 @@ It is highly recommended to NOT use pHash if you use masked images, or it'll be 180 30 81 - 21 + 22 @@ -617,10 +607,10 @@ It is highly recommended to NOT use pHash if you use masked images, or it'll be - 76 + 80 130 94 - 20 + 22 @@ -649,7 +639,7 @@ It is highly recommended to NOT use pHash if you use masked images, or it'll be 180 105 81 - 21 + 22 @@ -675,10 +665,10 @@ It is highly recommended to NOT use pHash if you use masked images, or it'll be - 76 + 80 105 94 - 20 + 22 @@ -694,7 +684,7 @@ It is highly recommended to NOT use pHash if you use masked images, or it'll be 180 155 81 - 21 + 22 @@ -721,10 +711,10 @@ reset image - 76 + 80 155 94 - 20 + 22 @@ -737,11 +727,11 @@ reset image - split_input - reset_input - undo_split_input - skip_split_input - pause_input + set_split_hotkey_button + set_reset_hotkey_button + set_undo_split_hotkey_button + set_skip_split_hotkey_button + set_pause_hotkey_button fps_limit_spinbox live_capture_region_checkbox capture_method_combobox diff --git a/res/update_checker.ui b/res/update_checker.ui index 75fbc852..6ff6ea85 100644 --- a/res/update_checker.ui +++ b/res/update_checker.ui @@ -9,20 +9,20 @@ 0 0 - 313 - 133 + 318 + 132 - 313 - 133 + 318 + 132 - 313 - 133 + 318 + 132 @@ -43,9 +43,9 @@ - 20 + 10 10 - 218 + 251 16 @@ -62,9 +62,9 @@ - 20 + 10 30 - 91 + 101 16 @@ -75,9 +75,9 @@ - 20 + 10 50 - 81 + 101 16 @@ -88,9 +88,9 @@ - 20 + 10 80 - 119 + 141 16 @@ -101,9 +101,9 @@ - 150 + 160 100 - 75 + 71 24 @@ -117,9 +117,9 @@ - 230 + 240 100 - 75 + 71 24 @@ -130,9 +130,9 @@ - 120 + 110 30 - 181 + 191 16 @@ -143,9 +143,9 @@ - 120 + 110 50 - 181 + 191 16 @@ -156,9 +156,9 @@ - 20 + 10 102 - 131 + 151 20 diff --git a/scripts/build.ps1 b/scripts/build.ps1 index 840db0c2..f9825aaf 100644 --- a/scripts/build.ps1 +++ b/scripts/build.ps1 @@ -1,10 +1,11 @@ & "$PSScriptRoot/compile_resources.ps1" $arguments = @( + "$PSScriptRoot/../src/AutoSplit.py", '--onefile', '--windowed', '--additional-hooks-dir=Pyinstaller/hooks', '--icon=res/icon.ico', '--splash=res/splash.png') -pyinstaller $arguments "$PSScriptRoot/../src/AutoSplit.py" +Start-Process -Wait -NoNewWindow pyinstaller -ArgumentList $arguments diff --git a/scripts/designer.ps1 b/scripts/designer.ps1 index 13d2450a..a6a159f6 100644 --- a/scripts/designer.ps1 +++ b/scripts/designer.ps1 @@ -1,4 +1,10 @@ -$qt6_applications_path = python3 -c 'import qt6_applications; print(qt6_applications.__path__[0])' +$qt6_applications_import = 'import qt6_applications; print(qt6_applications.__path__[0])' +$qt6_applications_path = python -c $qt6_applications_import +if ($null -eq $qt6_applications_path) { + Write-Host 'Designer not found, installing qt6_applications' + python -m pip install qt6_applications +} +$qt6_applications_path = python -c $qt6_applications_import & "$qt6_applications_path/Qt/bin/designer" ` "$PSScriptRoot/../res/design.ui" ` "$PSScriptRoot/../res/about.ui" ` diff --git a/scripts/install.ps1 b/scripts/install.ps1 index cac7c468..1a513d35 100644 --- a/scripts/install.ps1 +++ b/scripts/install.ps1 @@ -1,14 +1,7 @@ -# Alias python3 to python on Windows -If ($IsWindows) { - $python = (Get-Command python).Source - $python3 = "$((Get-Item $python).Directory.FullName)/python3.exe" - New-Item -ItemType SymbolicLink -Path $python3 -Target $python -ErrorAction SilentlyContinue -} - # Installing Python dependencies $dev = If ($Env:GITHUB_JOB -eq 'Build') { '' } Else { '-dev' } # Ensures installation tools are up to date. This also aliases pip to pip3 on MacOS. -python3 -m pip install wheel pip setuptools --upgrade +python -m pip install wheel pip setuptools --upgrade pip install -r "$PSScriptRoot/requirements$dev.txt" --upgrade # Don't compile resources on the Build CI job as it'll do so in build script diff --git a/scripts/requirements-dev.txt b/scripts/requirements-dev.txt index 8b02dc5f..3f8d5b76 100644 --- a/scripts/requirements-dev.txt +++ b/scripts/requirements-dev.txt @@ -15,12 +15,12 @@ ruff>=0.0.262 # Avoid N802 violations for @override + Avoid adding required impo # Can also be downloaded externally as a non-python package # qt6-applications # Types -types-d3dshot +types-D3DShot ; sys_platform == 'win32' types-keyboard types-Pillow types-psutil types-PyAutoGUI types-pyinstaller -types-pywin32 +types-pywin32 ; sys_platform == 'win32' types-requests types-toml diff --git a/scripts/requirements.txt b/scripts/requirements.txt index a611e047..2d7bb755 100644 --- a/scripts/requirements.txt +++ b/scripts/requirements.txt @@ -21,6 +21,7 @@ packaging Pillow>=9.2 # gnome-screeshot checks psutil PyAutoGUI +PyWinCtl>=0.0.42 # py.typed PySide6-Essentials>=6.5 # fixes https://bugreports.qt.io/browse/PYSIDE-2189 and https://bugreports.qt.io/browse/PYSIDE-1603 requests<=2.28.1 # 2.28.2 has issues with PyInstaller https://github.com/pyinstaller/pyinstaller-hooks-contrib/issues/534 toml diff --git a/scripts/start.ps1 b/scripts/start.ps1 index d1e8ec08..70d6fd8b 100644 --- a/scripts/start.ps1 +++ b/scripts/start.ps1 @@ -1,3 +1,3 @@ param ([string]$p1) & "$PSScriptRoot/compile_resources.ps1" -python3 "$PSScriptRoot/../src/AutoSplit.py" $p1 +python "$PSScriptRoot/../src/AutoSplit.py" $p1 diff --git a/src/AutoSplit.py b/src/AutoSplit.py index f245078f..e7cf0663 100644 --- a/src/AutoSplit.py +++ b/src/AutoSplit.py @@ -56,7 +56,7 @@ os.environ["REQUESTS_CA_BUNDLE"] = certifi.where() myappid = f"Toufool.AutoSplit.v{AUTOSPLIT_VERSION}" ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid) -# qt.qpa.window: SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2) failed: COM error 0x5: Access is denied. # noqa: E501 # pylint: disable=line-too-long +# qt.qpa.window: SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2) failed: COM error 0x5: Access is denied. # noqa: E501 # ctypes.windll.user32.SetProcessDpiAwarenessContext(2) @@ -432,7 +432,7 @@ def __is_current_split_out_of_range(self): or self.split_image_number > len(self.split_images_and_loop_number) - 1 def undo_split(self, navigate_image_only: bool = False): - """"Undo Split" and "Prev. Img." buttons connect to here.""" + """Undo Split" and "Prev. Img." buttons connect to here.""" # Can't undo until timer is started # or Undoing past the first image if not self.is_running \ @@ -454,7 +454,7 @@ def undo_split(self, navigate_image_only: bool = False): send_command(self, "undo") def skip_split(self, navigate_image_only: bool = False): - """"Skip Split" and "Next Img." buttons connect to here.""" + """Skip Split" and "Next Img." buttons connect to here.""" # Can't skip or split until timer is started # or Splitting/skipping when there are no images left if not self.is_running \ diff --git a/src/capture_method/BitBltCaptureMethod.py b/src/capture_method/BitBltCaptureMethod.py index eed9f739..b15c9fbd 100644 --- a/src/capture_method/BitBltCaptureMethod.py +++ b/src/capture_method/BitBltCaptureMethod.py @@ -22,6 +22,14 @@ class BitBltCaptureMethod(CaptureMethodBase): + name = "BitBlt" + short_description = "fastest, least compatible" + description = ( + "\nThe best option when compatible. But it cannot properly record " + + "\nOpenGL, Hardware Accelerated or Exclusive Fullscreen windows. " + + "\nThe smaller the selected region, the more efficient it is. " + ) + _render_full_content = False def get_frame(self, autosplit: AutoSplit) -> tuple[cv2.Mat | None, bool]: diff --git a/src/capture_method/CaptureMethodBase.py b/src/capture_method/CaptureMethodBase.py index 759946f1..b82dc9ca 100644 --- a/src/capture_method/CaptureMethodBase.py +++ b/src/capture_method/CaptureMethodBase.py @@ -11,6 +11,10 @@ class CaptureMethodBase(): + name = "None" + short_description = "" + description = "" + def __init__(self, autosplit: AutoSplit | None = None): # Some capture methods don't need an initialization process pass diff --git a/src/capture_method/DesktopDuplicationCaptureMethod.py b/src/capture_method/DesktopDuplicationCaptureMethod.py index 744480d6..a00ec7e9 100644 --- a/src/capture_method/DesktopDuplicationCaptureMethod.py +++ b/src/capture_method/DesktopDuplicationCaptureMethod.py @@ -9,13 +9,25 @@ from win32 import win32gui from capture_method.BitBltCaptureMethod import BitBltCaptureMethod -from utils import get_window_bounds +from utils import GITHUB_REPOSITORY, get_window_bounds if TYPE_CHECKING: from AutoSplit import AutoSplit class DesktopDuplicationCaptureMethod(BitBltCaptureMethod): + name = "Direct3D Desktop Duplication" + short_description = "slower, bound to display" + description = ( + "\nDuplicates the desktop using Direct3D. " + + "\nIt can record OpenGL and Hardware Accelerated windows. " + + "\nAbout 10-15x slower than BitBlt. Not affected by window size. " + + "\nOverlapping windows will show up and can't record across displays. " + + "\nThis option may not be available for hybrid GPU laptops, " + + "\nsee D3DDD-Note-Laptops.md for a solution. " + + f"\nhttps://www.github.com/{GITHUB_REPOSITORY}#capture-method " + ) + def __init__(self): super().__init__() # Must not set statically as some laptops will throw an error diff --git a/src/capture_method/ForceFullContentRenderingCaptureMethod.py b/src/capture_method/ForceFullContentRenderingCaptureMethod.py index 384ef027..6bbcd70e 100644 --- a/src/capture_method/ForceFullContentRenderingCaptureMethod.py +++ b/src/capture_method/ForceFullContentRenderingCaptureMethod.py @@ -4,4 +4,12 @@ class ForceFullContentRenderingCaptureMethod(BitBltCaptureMethod): + name = "Force Full Content Rendering" + short_description = "very slow, can affect rendering" + description = ( + "\nUses BitBlt behind the scene, but passes a special flag " + + "\nto PrintWindow to force rendering the entire desktop. " + + "\nAbout 10-15x slower than BitBlt based on original window size " + + "\nand can mess up some applications' rendering pipelines. " + ) _render_full_content = True diff --git a/src/capture_method/VideoCaptureDeviceCaptureMethod.py b/src/capture_method/VideoCaptureDeviceCaptureMethod.py index 427fb0e9..0addeb7b 100644 --- a/src/capture_method/VideoCaptureDeviceCaptureMethod.py +++ b/src/capture_method/VideoCaptureDeviceCaptureMethod.py @@ -25,6 +25,15 @@ def is_blank(image: cv2.Mat): class VideoCaptureDeviceCaptureMethod(CaptureMethodBase): + name = "Video Capture Device" + short_description = "see below" + description = ( + "\nUses a Video Capture Device, like a webcam, virtual cam, or capture card. " + + "\nYou can select one below. " + + "\nIf you want to use this with OBS' Virtual Camera, use the Virtualcam plugin instead " + + "\nhttps://github.com/Avasam/obs-virtual-cam/releases" + ) + capture_device: cv2.VideoCapture capture_thread: Thread | None stop_thread: Event @@ -114,8 +123,5 @@ def get_frame(self, autosplit: AutoSplit): ] return cv2.cvtColor(image, cv2.COLOR_BGR2BGRA), is_old_image - def recover_window(self, captured_window_title: str, autosplit: AutoSplit) -> bool: - raise NotImplementedError - def check_selected_region_exists(self, autosplit: AutoSplit): return bool(self.capture_device.isOpened()) diff --git a/src/capture_method/WindowsGraphicsCaptureMethod.py b/src/capture_method/WindowsGraphicsCaptureMethod.py index 68315f8e..e38f7957 100644 --- a/src/capture_method/WindowsGraphicsCaptureMethod.py +++ b/src/capture_method/WindowsGraphicsCaptureMethod.py @@ -13,15 +13,28 @@ from winsdk.windows.graphics.imaging import BitmapBufferAccessMode, SoftwareBitmap from capture_method.CaptureMethodBase import CaptureMethodBase -from utils import RGBA_CHANNEL_COUNT, WINDOWS_BUILD_NUMBER, get_direct3d_device, is_valid_hwnd +from utils import RGBA_CHANNEL_COUNT, WGC_MIN_BUILD, WINDOWS_BUILD_NUMBER, get_direct3d_device, is_valid_hwnd if TYPE_CHECKING: from AutoSplit import AutoSplit WGC_NO_BORDER_MIN_BUILD = 20348 +LEARNING_MODE_DEVICE_BUILD = 17763 +"""https://learn.microsoft.com/en-us/uwp/api/windows.ai.machinelearning.learningmodeldevice""" class WindowsGraphicsCaptureMethod(CaptureMethodBase): + name = "Windows Graphics Capture" + short_description = "fast, most compatible, capped at 60fps" + description = ( + f"\nOnly available in Windows 10.0.{WGC_MIN_BUILD} and up. " + + f"\nDue to current technical limitations, Windows versions below 10.0.0.{LEARNING_MODE_DEVICE_BUILD}" + + "\nrequire having at least one audio or video Capture Device connected and enabled." + + "\nAllows recording UWP apps, Hardware Accelerated and Exclusive Fullscreen windows. " + + "\nAdds a yellow border on Windows 10 (not on Windows 11)." + + "\nCaps at around 60 FPS. " + ) + size: SizeInt32 frame_pool: Direct3D11CaptureFramePool | None = None session: GraphicsCaptureSession | None = None diff --git a/src/capture_method/__init__.py b/src/capture_method/__init__.py index e179cd9e..1ee2401a 100644 --- a/src/capture_method/__init__.py +++ b/src/capture_method/__init__.py @@ -15,16 +15,11 @@ from capture_method.ForceFullContentRenderingCaptureMethod import ForceFullContentRenderingCaptureMethod from capture_method.VideoCaptureDeviceCaptureMethod import VideoCaptureDeviceCaptureMethod from capture_method.WindowsGraphicsCaptureMethod import WindowsGraphicsCaptureMethod -from utils import GITHUB_REPOSITORY, WINDOWS_BUILD_NUMBER, first, try_get_direct3d_device +from utils import WGC_MIN_BUILD, WINDOWS_BUILD_NUMBER, first, try_get_direct3d_device if TYPE_CHECKING: from AutoSplit import AutoSplit -WGC_MIN_BUILD = 17134 -"""https://docs.microsoft.com/en-us/uwp/api/windows.graphics.capture.graphicscapturepicker#applies-to""" -LEARNING_MODE_DEVICE_BUILD = 17763 -"""https://learn.microsoft.com/en-us/uwp/api/windows.ai.machinelearning.learningmodeldevice""" - class Region(TypedDict): x: int @@ -33,14 +28,6 @@ class Region(TypedDict): height: int -@dataclass -class CaptureMethodInfo(): - name: str - short_description: str - description: str - implementation: type[CaptureMethodBase] - - class CaptureMethodMeta(EnumMeta): # Allow checking if simple string is enum def __contains__(self, other: str): @@ -74,7 +61,7 @@ def __hash__(self): VIDEO_CAPTURE_DEVICE = "VIDEO_CAPTURE_DEVICE" -class CaptureMethodDict(OrderedDict[CaptureMethodEnum, CaptureMethodInfo]): +class CaptureMethodDict(OrderedDict[CaptureMethodEnum, type[CaptureMethodBase]]): def get_index(self, capture_method: str | CaptureMethodEnum): """Returns 0 if the capture_method is invalid or unsupported.""" try: @@ -99,99 +86,37 @@ def get_method_by_index(self, index: int): def get(self, __key: CaptureMethodEnum): """ - Returns the `CaptureMethodInfo` for `CaptureMethodEnum` if `CaptureMethodEnum` is available, + Returns the `CaptureMethodBase` subclass for `CaptureMethodEnum` if `CaptureMethodEnum` is available, else defaults to the first available `CaptureMethodEnum`. - Returns the `CaptureMethodBase` (default) implementation if there's no capture methods. + Returns `CaptureMethodBase` (default) directly if there's no capture methods. """ if __key == CaptureMethodEnum.NONE or len(self) <= 0: - return NONE_CAPTURE_METHOD + return CaptureMethodBase return super().get(__key, first(self.values())) -NONE_CAPTURE_METHOD = CaptureMethodInfo( - name="None", - short_description="", - description="", - implementation=CaptureMethodBase, -) - CAPTURE_METHODS = CaptureMethodDict() if ( # Windows Graphics Capture requires a minimum Windows Build WINDOWS_BUILD_NUMBER >= WGC_MIN_BUILD # Our current implementation of Windows Graphics Capture does not ensure we can get an ID3DDevice and try_get_direct3d_device() ): - CAPTURE_METHODS[CaptureMethodEnum.WINDOWS_GRAPHICS_CAPTURE] = CaptureMethodInfo( - name="Windows Graphics Capture", - short_description="fast, most compatible, capped at 60fps", - description=( - f"\nOnly available in Windows 10.0.{WGC_MIN_BUILD} and up. " - + f"\nDue to current technical limitations, Windows versions below 10.0.0.{LEARNING_MODE_DEVICE_BUILD}" - + "\nrequire having at least one audio or video Capture Device connected and enabled." - + "\nAllows recording UWP apps, Hardware Accelerated and Exclusive Fullscreen windows. " - + "\nAdds a yellow border on Windows 10 (not on Windows 11)." - + "\nCaps at around 60 FPS. " - ), - implementation=WindowsGraphicsCaptureMethod, - ) -CAPTURE_METHODS[CaptureMethodEnum.BITBLT] = CaptureMethodInfo( - name="BitBlt", - short_description="fastest, least compatible", - description=( - "\nThe best option when compatible. But it cannot properly record " - + "\nOpenGL, Hardware Accelerated or Exclusive Fullscreen windows. " - + "\nThe smaller the selected region, the more efficient it is. " - ), - - implementation=BitBltCaptureMethod, -) + CAPTURE_METHODS[CaptureMethodEnum.WINDOWS_GRAPHICS_CAPTURE] = WindowsGraphicsCaptureMethod +CAPTURE_METHODS[CaptureMethodEnum.BITBLT] = BitBltCaptureMethod try: import d3dshot d3dshot.create(capture_output="numpy") except (ModuleNotFoundError, COMError): pass else: - CAPTURE_METHODS[CaptureMethodEnum.DESKTOP_DUPLICATION] = CaptureMethodInfo( - name="Direct3D Desktop Duplication", - short_description="slower, bound to display", - description=( - "\nDuplicates the desktop using Direct3D. " - + "\nIt can record OpenGL and Hardware Accelerated windows. " - + "\nAbout 10-15x slower than BitBlt. Not affected by window size. " - + "\nOverlapping windows will show up and can't record across displays. " - + "\nThis option may not be available for hybrid GPU laptops, " - + "\nsee D3DDD-Note-Laptops.md for a solution. " - + f"\nhttps://www.github.com/{GITHUB_REPOSITORY}#capture-method " - ), - implementation=DesktopDuplicationCaptureMethod, - ) -CAPTURE_METHODS[CaptureMethodEnum.PRINTWINDOW_RENDERFULLCONTENT] = CaptureMethodInfo( - name="Force Full Content Rendering", - short_description="very slow, can affect rendering", - description=( - "\nUses BitBlt behind the scene, but passes a special flag " - + "\nto PrintWindow to force rendering the entire desktop. " - + "\nAbout 10-15x slower than BitBlt based on original window size " - + "\nand can mess up some applications' rendering pipelines. " - ), - implementation=ForceFullContentRenderingCaptureMethod, -) -CAPTURE_METHODS[CaptureMethodEnum.VIDEO_CAPTURE_DEVICE] = CaptureMethodInfo( - name="Video Capture Device", - short_description="see below", - description=( - "\nUses a Video Capture Device, like a webcam, virtual cam, or capture card. " - + "\nYou can select one below. " - + "\nIf you want to use this with OBS' Virtual Camera, use the Virtualcam plugin instead " - + "\nhttps://github.com/Avasam/obs-virtual-cam/releases" - ), - implementation=VideoCaptureDeviceCaptureMethod, -) + CAPTURE_METHODS[CaptureMethodEnum.DESKTOP_DUPLICATION] = DesktopDuplicationCaptureMethod +CAPTURE_METHODS[CaptureMethodEnum.PRINTWINDOW_RENDERFULLCONTENT] = ForceFullContentRenderingCaptureMethod +CAPTURE_METHODS[CaptureMethodEnum.VIDEO_CAPTURE_DEVICE] = VideoCaptureDeviceCaptureMethod def change_capture_method(selected_capture_method: CaptureMethodEnum, autosplit: AutoSplit): autosplit.capture_method.close(autosplit) - autosplit.capture_method = CAPTURE_METHODS.get(selected_capture_method).implementation(autosplit) + autosplit.capture_method = CAPTURE_METHODS.get(selected_capture_method)(autosplit) if selected_capture_method == CaptureMethodEnum.VIDEO_CAPTURE_DEVICE: autosplit.select_region_button.setDisabled(True) autosplit.select_window_button.setDisabled(True) @@ -209,6 +134,11 @@ class CameraInfo(): resolution: tuple[int, int] | None +def get_input_devices(): + # https://github.com/andreaschiavinato/python_grabber/pull/24 + return cast(list[str], FilterGraph().get_input_devices()) + + def get_input_device_resolution(index: int): filter_graph = FilterGraph() filter_graph.add_video_input_device(index) @@ -218,8 +148,7 @@ def get_input_device_resolution(index: int): async def get_all_video_capture_devices() -> list[CameraInfo]: - # TODO: Fix partially Unknown list upstream - named_video_inputs: list[str] = FilterGraph().get_input_devices() + named_video_inputs = get_input_devices() async def get_camera_info(index: int, device_name: str): backend = "" @@ -242,7 +171,8 @@ async def get_camera_info(index: int, device_name: str): return CameraInfo(index, device_name, False, backend, get_input_device_resolution(index)) - future = asyncio.gather( + # https://github.com/python/typeshed/issues/2652 + future: asyncio.Future[list[CameraInfo | None]] = asyncio.gather( *[ get_camera_info(index, name) for index, name in enumerate(named_video_inputs) diff --git a/src/hotkeys.py b/src/hotkeys.py index b7c95632..b0575467 100644 --- a/src/hotkeys.py +++ b/src/hotkeys.py @@ -105,7 +105,7 @@ def __validate_keypad(expected_key: str, keyboard_event: keyboard.KeyboardEvent) NOTE: This is a workaround very specific to numpads. Windows reports different physical keys with the same scan code. For example, "Home", "Num Home" and "Num 7" are all `71`. - See: https://github.com/boppreh/keyboard/issues/171#issuecomment-390437684. + See: https://github.com/boppreh/keyboard/issues/171#issuecomment-390437684 . Since we reuse the key string we set to send to LiveSplit, we can't use fake names like "num home". We're also trying to achieve the same hotkey behaviour as LiveSplit has. @@ -153,7 +153,7 @@ def __get_key_name(keyboard_event: keyboard.KeyboardEvent): def __get_hotkey_name(names: list[str]): """ Uses keyboard.get_hotkey_name but works with non-english modifiers and keypad - See: https://github.com/boppreh/keyboard/issues/516. + See: https://github.com/boppreh/keyboard/issues/516 . """ def sorting_key(key: str): return not keyboard.is_modifier(keyboard.key_to_scan_codes(key)[0]) diff --git a/src/region_selection.py b/src/region_selection.py index d0d73870..310b71f1 100644 --- a/src/region_selection.py +++ b/src/region_selection.py @@ -10,15 +10,16 @@ import numpy as np from PySide6 import QtCore, QtGui, QtWidgets from PySide6.QtTest import QTest +from pywinctl import getTopWindowAt from typing_extensions import override from win32 import win32gui from win32con import SM_CXVIRTUALSCREEN, SM_CYVIRTUALSCREEN, SM_XVIRTUALSCREEN, SM_YVIRTUALSCREEN -from winsdk._winrt import initialize_with_window # pylint: disable=no-name-in-module +from winsdk._winrt import initialize_with_window from winsdk.windows.foundation import AsyncStatus, IAsyncOperation from winsdk.windows.graphics.capture import GraphicsCaptureItem, GraphicsCapturePicker import error_messages -from utils import MAXBYTE, get_window_bounds, getTopWindowAt, is_valid_hwnd, is_valid_image +from utils import MAXBYTE, get_window_bounds, is_valid_hwnd, is_valid_image user32 = ctypes.windll.user32 @@ -327,7 +328,7 @@ def mouseReleaseEvent(self, event: QtGui.QMouseEvent): class SelectRegionWidget(BaseSelectWidget): """ Widget for dragging screen region - https://github.com/harupy/snipping-tool. + Originated from https://github.com/harupy/snipping-tool . """ _right: int = 0 diff --git a/src/user_profile.py b/src/user_profile.py index cfa3ee96..5d86ef8d 100644 --- a/src/user_profile.py +++ b/src/user_profile.py @@ -202,7 +202,9 @@ def load_check_for_updates_on_open(autosplit: AutoSplit): value = QtCore \ .QSettings("AutoSplit", "Check For Updates On Open") \ .value("check_for_updates_on_open", True, type=bool) - autosplit.action_check_for_updates_on_open.setChecked(value) # pyright: ignore[reportGeneralTypeIssues] # Type not infered by PySide6 # noqa: E501 # pylint: disable=line-too-long + # Type not infered by PySide6 + # TODO: Report this issue upstream + autosplit.action_check_for_updates_on_open.setChecked(value) # pyright: ignore[reportGeneralTypeIssues] def set_check_for_updates_on_open(design_window: design.Ui_MainWindow, value: bool): diff --git a/src/utils.py b/src/utils.py index 9989c263..4dd16158 100644 --- a/src/utils.py +++ b/src/utils.py @@ -141,7 +141,8 @@ def fire_and_forget(func: Callable[..., Any]): """ Runs synchronous function asynchronously without waiting for a response. - Uses threads on Windows because `RuntimeError: There is no current event loop in thread 'MainThread'.` + Uses threads on Windows because ~~`RuntimeError: There is no current event loop in thread 'MainThread'.`~~ + Because maybe asyncio has issues. Unsure. See alpha.5 and https://github.com/Avasam/AutoSplit/issues/36 Uses asyncio on Linux because of a `Segmentation fault (core dumped)` """ @@ -155,31 +156,12 @@ def wrapped(*args: Any, **kwargs: Any): return wrapped -def getTopWindowAt(x: int, y: int): # noqa: N802 - # Immitating PyWinCTL's function - class Win32Window(): - def __init__(self, hwnd: int) -> None: - self._hWnd = hwnd - - def getHandle(self): # noqa: N802 - return self._hWnd - - @property - def title(self): - return win32gui.GetWindowText(self._hWnd) - hwnd = win32gui.WindowFromPoint((x, y)) - - # Want to pull the parent window from the window handle - # By using GetAncestor we are able to get the parent window instead of the owner window. - while win32gui.IsChild(win32gui.GetParent(hwnd), hwnd): - hwnd = ctypes.windll.user32.GetAncestor(hwnd, 2) - return Win32Window(hwnd) if hwnd else None - - # Environment specifics WINDOWS_BUILD_NUMBER = int(version().split(".")[-1]) if sys.platform == "win32" else -1 FIRST_WIN_11_BUILD = 22000 """AutoSplit Version number""" +WGC_MIN_BUILD = 17134 +"""https://docs.microsoft.com/en-us/uwp/api/windows.graphics.capture.graphicscapturepicker#applies-to""" FROZEN = hasattr(sys, "frozen") """Running from build made by PyInstaller""" auto_split_directory = os.path.dirname(sys.executable if FROZEN else os.path.abspath(__file__)) diff --git a/typings/cv2/mat_wrapper/__init__.pyi b/typings/cv2/mat_wrapper/__init__.pyi index 15d667e9..fa91b2f9 100644 --- a/typings/cv2/mat_wrapper/__init__.pyi +++ b/typings/cv2/mat_wrapper/__init__.pyi @@ -1,5 +1,3 @@ -from __future__ import annotations - import numpy as np from typing_extensions import TypeAlias