mirror of
https://github.com/moonlight-stream/moonlight-qt.git
synced 2026-10-22 16:17:20 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
413993ab6f | ||
|
|
948b9c818a | ||
|
|
cf615ea88d | ||
|
|
2d1e16fe50 | ||
|
|
089c3d3ad3 | ||
|
|
63e3fa6609 | ||
|
|
8a7fa7e559 | ||
|
|
989a90a0f7 | ||
|
|
a707720577 | ||
|
|
eef270de26 | ||
|
|
0ac765bc32 | ||
|
|
e76551cb64 | ||
|
|
4fd902a0d3 | ||
|
|
9cba449092 | ||
|
|
905b2ecf9e | ||
|
|
53c2c612c9 | ||
|
|
8ffadde9cc | ||
|
|
f569bf633c | ||
|
|
6eb5e07007 | ||
|
|
f07201baa9 | ||
|
|
9120fd01a8 | ||
|
|
c14cc70a6a | ||
|
|
a66f91ae3c | ||
|
|
7e4fddbe62 | ||
|
|
0ff9f8c55a | ||
|
|
6f710faa8e | ||
|
|
2127e1d90b | ||
|
|
9d73aa6b4a | ||
|
|
a2f23b7053 | ||
|
|
5974980aff | ||
|
|
6fa53691aa | ||
|
|
49e0679864 | ||
|
|
0976bc39b3 | ||
|
|
facd6e4e56 | ||
|
|
d2dc0aa1b1 | ||
|
|
aa74e6930b | ||
|
|
b7811bf196 | ||
|
|
18130fd8de | ||
|
|
b6d4f97e96 | ||
|
|
0dd0112518 | ||
|
|
5711e6064e | ||
|
|
517b684f85 | ||
|
|
39465f6d8a | ||
|
|
738813cbac | ||
|
|
c2778e040a | ||
|
|
1efdeeb9dc | ||
|
|
b01dfea343 | ||
|
|
e143293e91 | ||
|
|
f420dc740a | ||
|
|
2e532224b7 | ||
|
|
cb8366a7e5 | ||
|
|
fee54a9d76 | ||
|
|
dc556d3c9b | ||
|
|
4fed3802aa | ||
|
|
f7bb4611e9 | ||
|
|
57791eec62 | ||
|
|
e1e3d8e710 | ||
|
|
654be492a0 | ||
|
|
1df0da4d4a | ||
|
|
1dd6cdb567 | ||
|
|
edfb3cfdc8 | ||
|
|
c6a8f44640 | ||
|
|
b7301d4415 | ||
|
|
4d668702b5 | ||
|
|
f4b0f4d78f | ||
|
|
7100262e09 | ||
|
|
e516943d53 | ||
|
|
fd41226cdd | ||
|
|
47b04eee3e | ||
|
|
24ee6f96e1 | ||
|
|
6e34c21175 | ||
|
|
64789ea8bc | ||
|
|
2ac7176787 | ||
|
|
b968886594 | ||
|
|
2f9c44103b | ||
|
|
e9ed4940cd | ||
|
|
85b5845ebb | ||
|
|
6f4db913aa | ||
|
|
2d20c711a6 | ||
|
|
2e829bc85f | ||
|
|
acddc3cabd | ||
|
|
a16647f16c | ||
|
|
3e3f384218 | ||
|
|
45a98a391a | ||
|
|
c9992201ce | ||
|
|
80062d262e | ||
|
|
652f7a7710 | ||
|
|
7e7e50ada2 | ||
|
|
87eba9bb89 | ||
|
|
da1fcd49fc | ||
|
|
c98289a9e7 | ||
|
|
1d0b65f798 | ||
|
|
3094185cd6 | ||
|
|
aaff04167b | ||
|
|
21bba02e6f | ||
|
|
f60e724167 | ||
|
|
7551e90899 | ||
|
|
715429c13c | ||
|
|
7976189d56 | ||
|
|
47d1eff45d | ||
|
|
073df32b01 | ||
|
|
e8ec7efa5c | ||
|
|
f08cdae2b4 | ||
|
|
1389d29d8c | ||
|
|
d4bfde7906 | ||
|
|
fe73c36958 | ||
|
|
a97f6d90f4 | ||
|
|
0735f164a8 | ||
|
|
9de0c215ba | ||
|
|
56474822f7 | ||
|
|
98b18d89eb | ||
|
|
78ea79b95a | ||
|
|
acbe503c12 | ||
|
|
0261c41574 | ||
|
|
c8182dfab3 | ||
|
|
2eee9c019f | ||
|
|
9ed40b5d9f | ||
|
|
ebc4c71c72 | ||
|
|
dec5a6370e | ||
|
|
519858b708 | ||
|
|
8fcc4cf7fe | ||
|
|
2f78350696 | ||
|
|
85d4037a89 | ||
|
|
d165bf7498 | ||
|
|
94d821a4a9 | ||
|
|
6a854f7a64 | ||
|
|
d76675f4b1 | ||
|
|
4a7e43ccb6 | ||
|
|
b53333d1ba | ||
|
|
f1f3c8bfb9 | ||
|
|
e580e7c672 | ||
|
|
a62fd96269 | ||
|
|
a1d592bb87 | ||
|
|
83f5f6cefc | ||
|
|
9166e70524 | ||
|
|
41f8fa95fe | ||
|
|
b140c21ed9 | ||
|
|
863712cb6e | ||
|
|
43f0aa7f06 | ||
|
|
12535d8142 | ||
|
|
8cf6a8bc98 | ||
|
|
7073dd26de | ||
|
|
66bfd2fad8 | ||
|
|
bb444f306a | ||
|
|
626fba4a8f | ||
|
|
9dc96bda58 | ||
|
|
1a2f1ed620 | ||
|
|
50255dd896 | ||
|
|
c8b9fda258 | ||
|
|
bb41732e59 | ||
|
|
7d9b984747 | ||
|
|
7404e5fce2 | ||
|
|
dc5414bd0d | ||
|
|
d147d36867 | ||
|
|
7763ec3c5c | ||
|
|
c61083ccd9 | ||
|
|
841aeccd6b | ||
|
|
bfcd373eec | ||
|
|
202ef41894 | ||
|
|
f112edc3c3 | ||
|
|
05fd7b18b9 | ||
|
|
9764174070 | ||
|
|
6b0370f0bc | ||
|
|
2e4313e7c3 | ||
|
|
2684d1874a | ||
|
|
882539a700 | ||
|
|
42960b7bf1 | ||
|
|
333f978186 | ||
|
|
e7e9df3261 | ||
|
|
24dbc96fe5 | ||
|
|
abc391f32a | ||
|
|
a589e8e3de | ||
|
|
39fa9a91b8 | ||
|
|
e10d98fb79 | ||
|
|
d17cdbda93 | ||
|
|
2252abefbf | ||
|
|
ce60a146dc | ||
|
|
cd4a6f3e17 | ||
|
|
fc5e1ed5d8 | ||
|
|
d70b98075e | ||
|
|
a4d9952061 | ||
|
|
45e4dfe09c | ||
|
|
34ba9063ad | ||
|
|
d5e2d59fe9 | ||
|
|
d39bdb28d6 | ||
|
|
8bf5622d21 | ||
|
|
9cafd5e099 | ||
|
|
d1b3bfd700 | ||
|
|
e60caf06f9 | ||
|
|
a243c6bb90 | ||
|
|
10523a7e27 | ||
|
|
ea06ec0133 | ||
|
|
e287ebcded | ||
|
|
2ee90289a1 | ||
|
|
193df861d1 | ||
|
|
9055496a19 | ||
|
|
5424576f73 | ||
|
|
a81c6a1c5e | ||
|
|
53a572a4a4 | ||
|
|
df283d80c6 | ||
|
|
b6d203b6dd | ||
|
|
ef988cc8af | ||
|
|
ba46905243 | ||
|
|
df0c4c8208 | ||
|
|
e5aaa1fc53 | ||
|
|
8b6df34d8b | ||
|
|
7a719aa886 | ||
|
|
9af58af5e4 | ||
|
|
b945c8c2dc | ||
|
|
2165f56aee | ||
|
|
e42de9d155 | ||
|
|
7fd95b9de2 | ||
|
|
a42c85802e | ||
|
|
cb3730100d | ||
|
|
7a04d2ad2c | ||
|
|
1f93976919 | ||
|
|
0b7505f109 | ||
|
|
392ca2d8b7 | ||
|
|
68daa90b7e | ||
|
|
fb39ad896e | ||
|
|
cb199054e3 | ||
|
|
7cf77211b0 | ||
|
|
a3d1e9a117 | ||
|
|
755c7c9895 | ||
|
|
45b5136233 | ||
|
|
f16b5cd337 | ||
|
|
21cda8141d | ||
|
|
90f26dfa45 | ||
|
|
ba507d8046 | ||
|
|
5639bcc0f5 | ||
|
|
4cd030582d | ||
|
|
7908076b39 | ||
|
|
ca91f9b702 | ||
|
|
0e86af1c9b | ||
|
|
c5ad84e2ee | ||
|
|
bfb5d6e8fe | ||
|
|
d63b0b5978 | ||
|
|
5e6b60bae0 | ||
|
|
65b6885c61 | ||
|
|
e8ceaa4085 | ||
|
|
db2b120b68 | ||
|
|
5f315d0093 | ||
|
|
016b212ff7 | ||
|
|
bccffe09e2 | ||
|
|
a0365c8e1c | ||
|
|
38156c9f7f | ||
|
|
640db09a64 | ||
|
|
5baa8c680a | ||
|
|
e1c4a488ed | ||
|
|
332d4433c4 | ||
|
|
f5d535b1ef | ||
|
|
35e226273c | ||
|
|
90e25e60d6 | ||
|
|
80659160d3 | ||
|
|
5de29928cd | ||
|
|
c65cea0f66 | ||
|
|
b3cfdbb835 | ||
|
|
9c15cceee7 | ||
|
|
8a0142bd0f | ||
|
|
3d6e05d8c0 | ||
|
|
ddf0e6a935 | ||
|
|
51852f044e | ||
|
|
17cad6b3ca | ||
|
|
8912869077 | ||
|
|
a2697634ba | ||
|
|
1bfb798107 | ||
|
|
cf60568f53 | ||
|
|
ddd18fdaf6 | ||
|
|
b04bb30930 | ||
|
|
e41aaf980b | ||
|
|
46df6601a1 | ||
|
|
5eb8b97469 | ||
|
|
8cd0536fb1 | ||
|
|
99a6789720 | ||
|
|
92670cc051 | ||
|
|
89cbbfc291 | ||
|
|
31869a7da2 | ||
|
|
68c6d6c932 | ||
|
|
e4e140fa53 | ||
|
|
b9709d0825 | ||
|
|
324e3dffa0 | ||
|
|
dd9de3fcb4 | ||
|
|
6710d0561a | ||
|
|
a30f97db0d | ||
|
|
cae666b834 | ||
|
|
5970ca49e9 | ||
|
|
6ae6218043 | ||
|
|
8e3e19a7f7 | ||
|
|
e3d51fd7f7 | ||
|
|
95f5c1d104 | ||
|
|
02a14069a3 | ||
|
|
394f28339e | ||
|
|
411998d4e3 | ||
|
|
371906445e | ||
|
|
34d91e58c0 | ||
|
|
f3f25bec7a | ||
|
|
08d74bea75 | ||
|
|
63f917b317 | ||
|
|
448633ec3c | ||
|
|
09ce3c9ce4 | ||
|
|
8fcc98102b | ||
|
|
e14075b464 | ||
|
|
ca89d14ec8 | ||
|
|
3f51998065 | ||
|
|
6e5b6a1a5c | ||
|
|
660f6c5bbf |
No files matched your search
@@ -1,6 +1,6 @@
|
|||||||
# Moonlight PC
|
# Moonlight PC
|
||||||
|
|
||||||
[Moonlight PC](https://moonlight-stream.org) is an open source PC client for NVIDIA GameStream, as used by the NVIDIA Shield.
|
[Moonlight PC](https://moonlight-stream.org) is an open source PC client for NVIDIA GameStream and [Sunshine](https://github.com/LizardByte/Sunshine).
|
||||||
|
|
||||||
Moonlight also has mobile versions for [Android](https://github.com/moonlight-stream/moonlight-android) and [iOS](https://github.com/moonlight-stream/moonlight-ios).
|
Moonlight also has mobile versions for [Android](https://github.com/moonlight-stream/moonlight-android) and [iOS](https://github.com/moonlight-stream/moonlight-ios).
|
||||||
|
|
||||||
@@ -12,15 +12,13 @@ You can follow development on our [Discord server](https://moonlight-stream.org/
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
- Hardware accelerated video decoding on Windows, Mac, and Linux
|
- Hardware accelerated video decoding on Windows, Mac, and Linux
|
||||||
- Supports streaming at up to 120 FPS (high refresh rate monitor recommended)
|
- H.264, HEVC, and AV1 codec support (AV1 requires Sunshine and a supported host GPU)
|
||||||
- Supports streaming at 720p, 1080p, 1440p, 4K, and the client PC's native screen resolution
|
|
||||||
- HDR streaming support
|
- HDR streaming support
|
||||||
- 7.1 surround sound audio support
|
- 7.1 surround sound audio support
|
||||||
|
- 10-point multitouch support (Sunshine only)
|
||||||
|
- Gamepad support with force feedback and motion controls for up to 16 players
|
||||||
- Support for both pointer capture (for games) and direct mouse control (for remote desktop)
|
- Support for both pointer capture (for games) and direct mouse control (for remote desktop)
|
||||||
- Support for passing system-wide keyboard shortcuts like Alt+Tab to the host
|
- Support for passing system-wide keyboard shortcuts like Alt+Tab to the host
|
||||||
- Direct touchscreen mouse control
|
|
||||||
- HEVC support for better image quality at reduced bandwidth
|
|
||||||
- Gamepad support with force feedback for up to 4 players
|
|
||||||
|
|
||||||
## Downloads
|
## Downloads
|
||||||
- [Windows, macOS, and Steam Link](https://github.com/moonlight-stream/moonlight-qt/releases)
|
- [Windows, macOS, and Steam Link](https://github.com/moonlight-stream/moonlight-qt/releases)
|
||||||
@@ -41,15 +39,14 @@ Hosting for Moonlight's Raspberry Pi and L4T package repositories is graciously
|
|||||||
### Windows Build Requirements
|
### Windows Build Requirements
|
||||||
* Qt 5.15 SDK or later
|
* Qt 5.15 SDK or later
|
||||||
* Windows 7 or later
|
* Windows 7 or later
|
||||||
* [Visual Studio 2019](https://visualstudio.microsoft.com/downloads/) (Community edition is fine)
|
* [Visual Studio 2022](https://visualstudio.microsoft.com/downloads/) (Community edition is fine)
|
||||||
* Select **MSVC 2019** option during Qt installation. MinGW is not supported.
|
* Select **MSVC** option during Qt installation. MinGW is not supported.
|
||||||
* [7-Zip](https://www.7-zip.org/) (only if building installers for non-development PCs)
|
* [7-Zip](https://www.7-zip.org/) (only if building installers for non-development PCs)
|
||||||
* [WiX Toolset](https://wixtoolset.org/releases/v3-14-0-5722/) v3.14.0.5722 or later (only if building installers for non-development PCs)
|
|
||||||
|
|
||||||
### macOS Build Requirements
|
### macOS Build Requirements
|
||||||
* Qt 6.2 SDK or later
|
* Qt 6.2 SDK or later
|
||||||
* macOS Mojave (10.14) or later
|
* macOS Mojave (10.14) or later
|
||||||
* Xcode 13
|
* Xcode 13 or later
|
||||||
* [create-dmg](https://github.com/sindresorhus/create-dmg) (only if building DMGs for use on non-development Macs)
|
* [create-dmg](https://github.com/sindresorhus/create-dmg) (only if building DMGs for use on non-development Macs)
|
||||||
|
|
||||||
### Linux/Unix Build Requirements
|
### Linux/Unix Build Requirements
|
||||||
@@ -58,7 +55,7 @@ Hosting for Moonlight's Raspberry Pi and L4T package repositories is graciously
|
|||||||
* Install the required packages:
|
* Install the required packages:
|
||||||
* Debian/Ubuntu: `libegl1-mesa-dev libgl1-mesa-dev libopus-dev libqt5svg5-dev libsdl2-dev libsdl2-ttf-dev libssl-dev libavcodec-dev libva-dev libvdpau-dev libxkbcommon-dev qtwayland5 qt5-qmake qtbase5-dev qtdeclarative5-dev qtquickcontrols2-5-dev wayland-protocols qml-module-qtquick-controls2 qml-module-qtquick-layouts qml-module-qtquick-window2 qml-module-qtquick2`
|
* Debian/Ubuntu: `libegl1-mesa-dev libgl1-mesa-dev libopus-dev libqt5svg5-dev libsdl2-dev libsdl2-ttf-dev libssl-dev libavcodec-dev libva-dev libvdpau-dev libxkbcommon-dev qtwayland5 qt5-qmake qtbase5-dev qtdeclarative5-dev qtquickcontrols2-5-dev wayland-protocols qml-module-qtquick-controls2 qml-module-qtquick-layouts qml-module-qtquick-window2 qml-module-qtquick2`
|
||||||
* RedHat/Fedora: `openssl-devel SDL2-devel SDL2_ttf-devel ffmpeg-devel qt5-qtsvg-devel qt5-qtquickcontrols2-devel libva-devel libvdpau-devel opus-devel pulseaudio-libs-devel alsa-lib-devel`
|
* RedHat/Fedora: `openssl-devel SDL2-devel SDL2_ttf-devel ffmpeg-devel qt5-qtsvg-devel qt5-qtquickcontrols2-devel libva-devel libvdpau-devel opus-devel pulseaudio-libs-devel alsa-lib-devel`
|
||||||
* FFmpeg 4.0+ is required to build. If your distro doesn't package FFmpeg 4.0 or later, you can build and install it from source on https://ffmpeg.org/
|
* FFmpeg 4.0 or later is required to build. If your distro doesn't package FFmpeg 4.0 or later, you can build and install it from source on https://ffmpeg.org/
|
||||||
|
|
||||||
### Steam Link Build Requirements
|
### Steam Link Build Requirements
|
||||||
* [Steam Link SDK](https://github.com/ValveSoftware/steamlink-sdk) cloned on your build system
|
* [Steam Link SDK](https://github.com/ValveSoftware/steamlink-sdk) cloned on your build system
|
||||||
@@ -72,12 +69,13 @@ Hosting for Moonlight's Raspberry Pi and L4T package repositories is graciously
|
|||||||
2. Run `git submodule update --init --recursive` from within `moonlight-qt/`
|
2. Run `git submodule update --init --recursive` from within `moonlight-qt/`
|
||||||
3. Open the project in Qt Creator or build from qmake on the command line.
|
3. Open the project in Qt Creator or build from qmake on the command line.
|
||||||
* To build a binary for use on non-development machines, use the scripts in the `scripts` folder.
|
* To build a binary for use on non-development machines, use the scripts in the `scripts` folder.
|
||||||
* For Windows builds, use `scripts\build-arch.bat` and `scripts\generate-bundle.bat`. Execute these scripts from the root of the repository within a Qt command prompt. Ensure WiX and 7-Zip binary directories are in your `%PATH%`.
|
* For Windows builds, use `scripts\build-arch.bat` and `scripts\generate-bundle.bat`. Execute these scripts from the root of the repository within a Qt command prompt. Ensure 7-Zip binary directory is on your `%PATH%`.
|
||||||
* For macOS builds, use `scripts/generate-dmg.sh`. Execute this script from the root of the repository and ensure Qt's `bin` folder is in your `$PATH`.
|
* For macOS builds, use `scripts/generate-dmg.sh`. Execute this script from the root of the repository and ensure Qt's `bin` folder is in your `$PATH`.
|
||||||
* For Steam Link builds, run `scripts/build-steamlink-app.sh` from the root of the repository.
|
* For Steam Link builds, run `scripts/build-steamlink-app.sh` from the root of the repository.
|
||||||
* To build from the command line for development use, run `qmake moonlight-qt.pro` then `make debug` or `make release`
|
* To build from the command line for development use on macOS or Linux, run `qmake moonlight-qt.pro` then `make debug` or `make release`
|
||||||
* To create an embedded build for a single-purpose device, use `qmake "CONFIG+=embedded" moonlight-qt.pro` and build normally.
|
* To create an embedded build for a single-purpose device, use `qmake "CONFIG+=embedded" moonlight-qt.pro` and build normally.
|
||||||
* This build will lack windowed mode, Discord/Help links, and other features that don't make sense on an embedded device.
|
* This build will lack windowed mode, Discord/Help links, and other features that don't make sense on an embedded device.
|
||||||
|
* For platforms with poor GL performance, add `"CONFIG+=glslow"` to prefer direct KMSDRM rendering over EGL/GLES renderers. Direct KMSDRM rendering can use dedicated YUV/RGB conversion and scaling hardware rather than slower GPU shaders for these operations.
|
||||||
|
|
||||||
## Contribute
|
## Contribute
|
||||||
1. Fork us
|
1. Fork us
|
||||||
|
|||||||
+17
-1
@@ -5,7 +5,7 @@
|
|||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
<string>Moonlight</string>
|
<string>Moonlight</string>
|
||||||
<key>CFBundleGetInfoString</key>
|
<key>CFBundleGetInfoString</key>
|
||||||
<string>Stream games from your NVIDIA GameStream-enabled PC</string>
|
<string>Stream games and other applications from another PC</string>
|
||||||
<key>CFBundleIconFile</key>
|
<key>CFBundleIconFile</key>
|
||||||
<string>moonlight</string>
|
<string>moonlight</string>
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
@@ -22,6 +22,22 @@
|
|||||||
<string>NSApplication</string>
|
<string>NSApplication</string>
|
||||||
<key>NSSupportsAutomaticGraphicsSwitching</key>
|
<key>NSSupportsAutomaticGraphicsSwitching</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
<key>GCSupportedGameControllers</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>ProfileName</key>
|
||||||
|
<string>ExtendedGamepad</string>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
<key>GCSupportsControllerUserInteraction</key>
|
||||||
|
<true/>
|
||||||
|
<key>LSApplicationCategoryType</key>
|
||||||
|
<string>public.app-category.games</string>
|
||||||
|
<key>NSAppTransportSecurity</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSAllowsArbitraryLoads</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>VERSION</string>
|
<string>VERSION</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
|
|||||||
+1
-1
Submodule app/SDL_GameControllerDB updated: ff26eb04d0...dbcf31a670.
+99
-46
@@ -48,6 +48,9 @@ win32 {
|
|||||||
|
|
||||||
INCLUDEPATH += $$PWD/../libs/windows/include
|
INCLUDEPATH += $$PWD/../libs/windows/include
|
||||||
LIBS += ws2_32.lib winmm.lib dxva2.lib ole32.lib gdi32.lib user32.lib d3d9.lib dwmapi.lib dbghelp.lib
|
LIBS += ws2_32.lib winmm.lib dxva2.lib ole32.lib gdi32.lib user32.lib d3d9.lib dwmapi.lib dbghelp.lib
|
||||||
|
|
||||||
|
# Work around a conflict with math.h inclusion between SDL and Qt 6
|
||||||
|
DEFINES += _USE_MATH_DEFINES
|
||||||
}
|
}
|
||||||
macx {
|
macx {
|
||||||
INCLUDEPATH += $$PWD/../libs/mac/include
|
INCLUDEPATH += $$PWD/../libs/mac/include
|
||||||
@@ -65,51 +68,73 @@ unix:!macx {
|
|||||||
CONFIG += link_pkgconfig
|
CONFIG += link_pkgconfig
|
||||||
PKGCONFIG += openssl sdl2 SDL2_ttf opus
|
PKGCONFIG += openssl sdl2 SDL2_ttf opus
|
||||||
|
|
||||||
packagesExist(libavcodec) {
|
!disable-ffmpeg {
|
||||||
PKGCONFIG += libavcodec libavutil
|
packagesExist(libavcodec) {
|
||||||
CONFIG += ffmpeg
|
PKGCONFIG += libavcodec libavutil
|
||||||
|
CONFIG += ffmpeg
|
||||||
|
|
||||||
packagesExist(libva) {
|
!disable-libva {
|
||||||
packagesExist(libva-x11) {
|
packagesExist(libva) {
|
||||||
CONFIG += libva-x11
|
!disable-x11 {
|
||||||
|
packagesExist(libva-x11) {
|
||||||
|
CONFIG += libva-x11
|
||||||
|
}
|
||||||
|
}
|
||||||
|
!disable-wayland {
|
||||||
|
packagesExist(libva-wayland) {
|
||||||
|
CONFIG += libva-wayland
|
||||||
|
}
|
||||||
|
}
|
||||||
|
!disable-libdrm {
|
||||||
|
packagesExist(libva-drm) {
|
||||||
|
CONFIG += libva-drm
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CONFIG += libva
|
||||||
|
}
|
||||||
}
|
}
|
||||||
packagesExist(libva-wayland) {
|
|
||||||
CONFIG += libva-wayland
|
!disable-libvdpau {
|
||||||
|
packagesExist(vdpau) {
|
||||||
|
CONFIG += libvdpau
|
||||||
|
}
|
||||||
}
|
}
|
||||||
packagesExist(libva-drm) {
|
|
||||||
CONFIG += libva-drm
|
!disable-mmal {
|
||||||
|
packagesExist(mmal) {
|
||||||
|
PKGCONFIG += mmal
|
||||||
|
CONFIG += mmal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
!disable-libdrm {
|
||||||
|
packagesExist(libdrm) {
|
||||||
|
PKGCONFIG += libdrm
|
||||||
|
CONFIG += libdrm
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
!disable-cuda {
|
||||||
|
packagesExist(ffnvcodec) {
|
||||||
|
PKGCONFIG += ffnvcodec
|
||||||
|
CONFIG += cuda
|
||||||
|
}
|
||||||
}
|
}
|
||||||
CONFIG += libva
|
|
||||||
}
|
}
|
||||||
|
|
||||||
packagesExist(vdpau) {
|
!disable-wayland {
|
||||||
CONFIG += libvdpau
|
packagesExist(wayland-client) {
|
||||||
|
CONFIG += wayland
|
||||||
|
PKGCONFIG += wayland-client
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
packagesExist(mmal) {
|
!disable-x11 {
|
||||||
PKGCONFIG += mmal
|
packagesExist(x11) {
|
||||||
CONFIG += mmal
|
DEFINES += HAS_X11
|
||||||
|
PKGCONFIG += x11
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
packagesExist(libdrm) {
|
|
||||||
PKGCONFIG += libdrm
|
|
||||||
CONFIG += libdrm
|
|
||||||
}
|
|
||||||
|
|
||||||
packagesExist(ffnvcodec) {
|
|
||||||
PKGCONFIG += ffnvcodec
|
|
||||||
CONFIG += cuda
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
packagesExist(wayland-client) {
|
|
||||||
DEFINES += HAS_WAYLAND
|
|
||||||
PKGCONFIG += wayland-client
|
|
||||||
}
|
|
||||||
|
|
||||||
packagesExist(x11) {
|
|
||||||
DEFINES += HAS_X11
|
|
||||||
PKGCONFIG += x11
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
win32 {
|
win32 {
|
||||||
@@ -120,7 +145,7 @@ win32:!winrt {
|
|||||||
CONFIG += soundio discord-rpc
|
CONFIG += soundio discord-rpc
|
||||||
}
|
}
|
||||||
macx {
|
macx {
|
||||||
LIBS += -lssl -lcrypto -lavcodec.59 -lavutil.57 -lopus -framework SDL2 -framework SDL2_ttf
|
LIBS += -lssl -lcrypto -lavcodec.60 -lavutil.58 -lopus -framework SDL2 -framework SDL2_ttf
|
||||||
LIBS += -lobjc -framework VideoToolbox -framework AVFoundation -framework CoreVideo -framework CoreGraphics -framework CoreMedia -framework AppKit -framework Metal
|
LIBS += -lobjc -framework VideoToolbox -framework AVFoundation -framework CoreVideo -framework CoreGraphics -framework CoreMedia -framework AppKit -framework Metal
|
||||||
|
|
||||||
# For libsoundio
|
# For libsoundio
|
||||||
@@ -212,15 +237,15 @@ ffmpeg {
|
|||||||
SOURCES += \
|
SOURCES += \
|
||||||
streaming/video/ffmpeg.cpp \
|
streaming/video/ffmpeg.cpp \
|
||||||
streaming/video/ffmpeg-renderers/sdlvid.cpp \
|
streaming/video/ffmpeg-renderers/sdlvid.cpp \
|
||||||
streaming/video/ffmpeg-renderers/pacer/pacer.cpp \
|
streaming/video/ffmpeg-renderers/swframemapper.cpp \
|
||||||
streaming/video/ffmpeg-renderers/pacer/nullthreadedvsyncsource.cpp
|
streaming/video/ffmpeg-renderers/pacer/pacer.cpp
|
||||||
|
|
||||||
HEADERS += \
|
HEADERS += \
|
||||||
streaming/video/ffmpeg.h \
|
streaming/video/ffmpeg.h \
|
||||||
streaming/video/ffmpeg-renderers/renderer.h \
|
streaming/video/ffmpeg-renderers/renderer.h \
|
||||||
streaming/video/ffmpeg-renderers/sdlvid.h \
|
streaming/video/ffmpeg-renderers/sdlvid.h \
|
||||||
streaming/video/ffmpeg-renderers/pacer/pacer.h \
|
streaming/video/ffmpeg-renderers/swframemapper.h \
|
||||||
streaming/video/ffmpeg-renderers/pacer/nullthreadedvsyncsource.h
|
streaming/video/ffmpeg-renderers/pacer/pacer.h
|
||||||
}
|
}
|
||||||
libva {
|
libva {
|
||||||
message(VAAPI renderer selected)
|
message(VAAPI renderer selected)
|
||||||
@@ -261,6 +286,18 @@ mmal {
|
|||||||
DEFINES += HAVE_MMAL
|
DEFINES += HAVE_MMAL
|
||||||
SOURCES += streaming/video/ffmpeg-renderers/mmal.cpp
|
SOURCES += streaming/video/ffmpeg-renderers/mmal.cpp
|
||||||
HEADERS += streaming/video/ffmpeg-renderers/mmal.h
|
HEADERS += streaming/video/ffmpeg-renderers/mmal.h
|
||||||
|
|
||||||
|
# We suppress EGL usage when MMAL is available because MMAL has
|
||||||
|
# significantly better performance than EGL on the Pi. Setting
|
||||||
|
# this option allows EGL usage even if built with MMAL support.
|
||||||
|
#
|
||||||
|
# It is highly recommended to also build with 'glslow' to avoid
|
||||||
|
# EGL being preferred if direct DRM rendering is available.
|
||||||
|
allow-egl-with-mmal {
|
||||||
|
message(Allowing EGL usage with MMAL enabled)
|
||||||
|
|
||||||
|
DEFINES += ALLOW_EGL_WITH_MMAL
|
||||||
|
}
|
||||||
}
|
}
|
||||||
libdrm {
|
libdrm {
|
||||||
message(DRM renderer selected)
|
message(DRM renderer selected)
|
||||||
@@ -289,8 +326,11 @@ config_EGL {
|
|||||||
DEFINES += HAVE_EGL
|
DEFINES += HAVE_EGL
|
||||||
SOURCES += \
|
SOURCES += \
|
||||||
streaming/video/ffmpeg-renderers/eglvid.cpp \
|
streaming/video/ffmpeg-renderers/eglvid.cpp \
|
||||||
streaming/video/ffmpeg-renderers/egl_extensions.cpp
|
streaming/video/ffmpeg-renderers/egl_extensions.cpp \
|
||||||
HEADERS += streaming/video/ffmpeg-renderers/eglvid.h
|
streaming/video/ffmpeg-renderers/eglimagefactory.cpp
|
||||||
|
HEADERS += \
|
||||||
|
streaming/video/ffmpeg-renderers/eglvid.h \
|
||||||
|
streaming/video/ffmpeg-renderers/eglimagefactory.h
|
||||||
}
|
}
|
||||||
config_SL {
|
config_SL {
|
||||||
message(Steam Link build configuration selected)
|
message(Steam Link build configuration selected)
|
||||||
@@ -348,13 +388,25 @@ embedded {
|
|||||||
|
|
||||||
DEFINES += EMBEDDED_BUILD
|
DEFINES += EMBEDDED_BUILD
|
||||||
}
|
}
|
||||||
|
glslow {
|
||||||
|
message(GL slow build)
|
||||||
|
|
||||||
|
DEFINES += GL_IS_SLOW
|
||||||
|
}
|
||||||
|
wayland {
|
||||||
|
message(Wayland extensions enabled)
|
||||||
|
|
||||||
|
DEFINES += HAS_WAYLAND
|
||||||
|
SOURCES += streaming/video/ffmpeg-renderers/pacer/waylandvsyncsource.cpp
|
||||||
|
HEADERS += streaming/video/ffmpeg-renderers/pacer/waylandvsyncsource.h
|
||||||
|
}
|
||||||
|
|
||||||
RESOURCES += \
|
RESOURCES += \
|
||||||
resources.qrc \
|
resources.qrc \
|
||||||
qml.qrc
|
qml.qrc
|
||||||
|
|
||||||
TRANSLATIONS += \
|
TRANSLATIONS += \
|
||||||
languages/qml_zh_cn.ts \
|
languages/qml_zh_CN.ts \
|
||||||
languages/qml_de.ts \
|
languages/qml_de.ts \
|
||||||
languages/qml_fr.ts \
|
languages/qml_fr.ts \
|
||||||
languages/qml_nb_NO.ts \
|
languages/qml_nb_NO.ts \
|
||||||
@@ -369,14 +421,15 @@ TRANSLATIONS += \
|
|||||||
languages/qml_sv.ts \
|
languages/qml_sv.ts \
|
||||||
languages/qml_tr.ts \
|
languages/qml_tr.ts \
|
||||||
languages/qml_uk.ts \
|
languages/qml_uk.ts \
|
||||||
languages/qml_zh_Hant.ts \
|
languages/qml_zh_TW.ts \
|
||||||
languages/qml_el.ts \
|
languages/qml_el.ts \
|
||||||
languages/qml_hi.ts \
|
languages/qml_hi.ts \
|
||||||
languages/qml_it.ts \
|
languages/qml_it.ts \
|
||||||
languages/qml_pt.ts \
|
languages/qml_pt.ts \
|
||||||
languages/qml_pt_BR.ts \
|
languages/qml_pt_BR.ts \
|
||||||
languages/qml_pl.ts \
|
languages/qml_pl.ts \
|
||||||
languages/qml_cs.ts
|
languages/qml_cs.ts \
|
||||||
|
languages/qml_he.ts
|
||||||
|
|
||||||
# Additional import path used to resolve QML modules in Qt Creator's code model
|
# Additional import path used to resolve QML modules in Qt Creator's code model
|
||||||
QML_IMPORT_PATH =
|
QML_IMPORT_PATH =
|
||||||
|
|||||||
@@ -6,16 +6,17 @@
|
|||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
|
|
||||||
AutoUpdateChecker::AutoUpdateChecker(QObject *parent) :
|
AutoUpdateChecker::AutoUpdateChecker(QObject *parent) :
|
||||||
QObject(parent),
|
QObject(parent)
|
||||||
m_Nam(this)
|
|
||||||
{
|
{
|
||||||
|
m_Nam = new QNetworkAccessManager(this);
|
||||||
|
|
||||||
// Never communicate over HTTP
|
// Never communicate over HTTP
|
||||||
m_Nam.setStrictTransportSecurityEnabled(true);
|
m_Nam->setStrictTransportSecurityEnabled(true);
|
||||||
|
|
||||||
// Allow HTTP redirects
|
// Allow HTTP redirects
|
||||||
m_Nam.setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy);
|
m_Nam->setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy);
|
||||||
|
|
||||||
connect(&m_Nam, &QNetworkAccessManager::finished,
|
connect(m_Nam, &QNetworkAccessManager::finished,
|
||||||
this, &AutoUpdateChecker::handleUpdateCheckRequestFinished);
|
this, &AutoUpdateChecker::handleUpdateCheckRequestFinished);
|
||||||
|
|
||||||
QString currentVersion(VERSION_STR);
|
QString currentVersion(VERSION_STR);
|
||||||
@@ -28,12 +29,17 @@ AutoUpdateChecker::AutoUpdateChecker(QObject *parent) :
|
|||||||
|
|
||||||
void AutoUpdateChecker::start()
|
void AutoUpdateChecker::start()
|
||||||
{
|
{
|
||||||
|
if (!m_Nam) {
|
||||||
|
Q_ASSERT(m_Nam);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
#if defined(Q_OS_WIN32) || defined(Q_OS_DARWIN) || defined(STEAM_LINK) || defined(APP_IMAGE) // Only run update checker on platforms without auto-update
|
#if defined(Q_OS_WIN32) || defined(Q_OS_DARWIN) || defined(STEAM_LINK) || defined(APP_IMAGE) // Only run update checker on platforms without auto-update
|
||||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) && QT_VERSION < QT_VERSION_CHECK(5, 15, 1) && !defined(QT_NO_BEARERMANAGEMENT)
|
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) && QT_VERSION < QT_VERSION_CHECK(5, 15, 1) && !defined(QT_NO_BEARERMANAGEMENT)
|
||||||
// HACK: Set network accessibility to work around QTBUG-80947 (introduced in Qt 5.14.0 and fixed in Qt 5.15.1)
|
// HACK: Set network accessibility to work around QTBUG-80947 (introduced in Qt 5.14.0 and fixed in Qt 5.15.1)
|
||||||
QT_WARNING_PUSH
|
QT_WARNING_PUSH
|
||||||
QT_WARNING_DISABLE_DEPRECATED
|
QT_WARNING_DISABLE_DEPRECATED
|
||||||
m_Nam.setNetworkAccessible(QNetworkAccessManager::Accessible);
|
m_Nam->setNetworkAccessible(QNetworkAccessManager::Accessible);
|
||||||
QT_WARNING_POP
|
QT_WARNING_POP
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -45,7 +51,7 @@ void AutoUpdateChecker::start()
|
|||||||
#else
|
#else
|
||||||
request.setAttribute(QNetworkRequest::HTTP2AllowedAttribute, true);
|
request.setAttribute(QNetworkRequest::HTTP2AllowedAttribute, true);
|
||||||
#endif
|
#endif
|
||||||
m_Nam.get(request);
|
m_Nam->get(request);
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,6 +108,11 @@ void AutoUpdateChecker::handleUpdateCheckRequestFinished(QNetworkReply* reply)
|
|||||||
{
|
{
|
||||||
Q_ASSERT(reply->isFinished());
|
Q_ASSERT(reply->isFinished());
|
||||||
|
|
||||||
|
// Delete the QNetworkAccessManager to free resources and
|
||||||
|
// prevent the bearer plugin from polling in the background.
|
||||||
|
m_Nam->deleteLater();
|
||||||
|
m_Nam = nullptr;
|
||||||
|
|
||||||
if (reply->error() == QNetworkReply::NoError) {
|
if (reply->error() == QNetworkReply::NoError) {
|
||||||
QTextStream stream(reply);
|
QTextStream stream(reply);
|
||||||
|
|
||||||
@@ -201,7 +212,7 @@ void AutoUpdateChecker::handleUpdateCheckRequestFinished(QNetworkReply* reply)
|
|||||||
<< QSysInfo::buildCpuArchitecture() << getPlatform() << QSysInfo::kernelVersion();
|
<< QSysInfo::buildCpuArchitecture() << getPlatform() << QSysInfo::kernelVersion();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
qWarning() << "Update checking failed with error: " << reply->error();
|
qWarning() << "Update checking failed with error:" << reply->error();
|
||||||
reply->deleteLater();
|
reply->deleteLater();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -25,5 +25,5 @@ private:
|
|||||||
QString getPlatform();
|
QString getPlatform();
|
||||||
|
|
||||||
QVector<int> m_CurrentVersionQuad;
|
QVector<int> m_CurrentVersionQuad;
|
||||||
QNetworkAccessManager m_Nam;
|
QNetworkAccessManager* m_Nam;
|
||||||
};
|
};
|
||||||
@@ -13,6 +13,7 @@
|
|||||||
#include <random>
|
#include <random>
|
||||||
|
|
||||||
#define SER_HOSTS "hosts"
|
#define SER_HOSTS "hosts"
|
||||||
|
#define SER_HOSTS_BACKUP "hostsbackup"
|
||||||
|
|
||||||
class PcMonitorThread : public QThread
|
class PcMonitorThread : public QThread
|
||||||
{
|
{
|
||||||
@@ -44,7 +45,7 @@ private:
|
|||||||
|
|
||||||
// Ensure the machine that responded is the one we intended to contact
|
// Ensure the machine that responded is the one we intended to contact
|
||||||
if (m_Computer->uuid != newState.uuid) {
|
if (m_Computer->uuid != newState.uuid) {
|
||||||
qInfo() << "Found unexpected PC " << newState.name << " looking for " << m_Computer->name;
|
qInfo() << "Found unexpected PC" << newState.name << "looking for" << m_Computer->name;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,12 +153,21 @@ ComputerManager::ComputerManager(QObject *parent)
|
|||||||
{
|
{
|
||||||
QSettings settings;
|
QSettings settings;
|
||||||
|
|
||||||
|
// If there's a hosts backup copy, we must have failed to commit
|
||||||
|
// a previous update before exiting. Restore the backup now.
|
||||||
|
int hosts = settings.beginReadArray(SER_HOSTS_BACKUP);
|
||||||
|
if (hosts == 0) {
|
||||||
|
// If there's no host backup, read from the primary location.
|
||||||
|
settings.endArray();
|
||||||
|
hosts = settings.beginReadArray(SER_HOSTS);
|
||||||
|
}
|
||||||
|
|
||||||
// Inflate our hosts from QSettings
|
// Inflate our hosts from QSettings
|
||||||
int hosts = settings.beginReadArray(SER_HOSTS);
|
|
||||||
for (int i = 0; i < hosts; i++) {
|
for (int i = 0; i < hosts; i++) {
|
||||||
settings.setArrayIndex(i);
|
settings.setArrayIndex(i);
|
||||||
NvComputer* computer = new NvComputer(settings);
|
NvComputer* computer = new NvComputer(settings);
|
||||||
m_KnownHosts[computer->uuid] = computer;
|
m_KnownHosts[computer->uuid] = computer;
|
||||||
|
m_LastSerializedHosts[computer->uuid] = *computer;
|
||||||
}
|
}
|
||||||
settings.endArray();
|
settings.endArray();
|
||||||
|
|
||||||
@@ -169,8 +179,8 @@ ComputerManager::ComputerManager(QObject *parent)
|
|||||||
m_DelayedFlushThread->start();
|
m_DelayedFlushThread->start();
|
||||||
|
|
||||||
// To quit in a timely manner, we must block additional requests
|
// To quit in a timely manner, we must block additional requests
|
||||||
// after we receive the aboutToQuit() signal. This is neccessary
|
// after we receive the aboutToQuit() signal. This is necessary
|
||||||
// because NvHTTP uses aboutToQuit() to abort requests in progres
|
// because NvHTTP uses aboutToQuit() to abort requests in progress
|
||||||
// while quitting, however this is a one time signal - additional
|
// while quitting, however this is a one time signal - additional
|
||||||
// requests would not be aborted and block termination.
|
// requests would not be aborted and block termination.
|
||||||
connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this, &ComputerManager::handleAboutToQuit);
|
connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this, &ComputerManager::handleAboutToQuit);
|
||||||
@@ -241,21 +251,48 @@ void DelayedFlushThread::run() {
|
|||||||
|
|
||||||
// Reset the delayed flush flag to ensure any racing saveHosts() call will set it again
|
// Reset the delayed flush flag to ensure any racing saveHosts() call will set it again
|
||||||
m_ComputerManager->m_NeedsDelayedFlush = false;
|
m_ComputerManager->m_NeedsDelayedFlush = false;
|
||||||
|
|
||||||
|
// Update the last serialized hosts map under the delayed flush mutex
|
||||||
|
m_ComputerManager->m_LastSerializedHosts.clear();
|
||||||
|
for (const NvComputer* computer : m_ComputerManager->m_KnownHosts) {
|
||||||
|
// Copy the current state of the NvComputer to allow us to check later if we need
|
||||||
|
// to serialize it again when attribute updates occur.
|
||||||
|
QReadLocker computerLock(&computer->lock);
|
||||||
|
m_ComputerManager->m_LastSerializedHosts[computer->uuid] = *computer;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Perform the flush
|
// Perform the flush
|
||||||
{
|
{
|
||||||
QReadLocker lock(&m_ComputerManager->m_Lock);
|
|
||||||
|
|
||||||
QSettings settings;
|
QSettings settings;
|
||||||
settings.remove(SER_HOSTS);
|
|
||||||
settings.beginWriteArray(SER_HOSTS);
|
// First, write to the backup location
|
||||||
int i = 0;
|
settings.beginWriteArray(SER_HOSTS_BACKUP);
|
||||||
for (const NvComputer* computer : m_ComputerManager->m_KnownHosts) {
|
{
|
||||||
settings.setArrayIndex(i++);
|
QReadLocker lock(&m_ComputerManager->m_Lock);
|
||||||
computer->serialize(settings);
|
int i = 0;
|
||||||
|
for (const NvComputer* computer : m_ComputerManager->m_KnownHosts) {
|
||||||
|
settings.setArrayIndex(i++);
|
||||||
|
computer->serialize(settings, false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
settings.endArray();
|
settings.endArray();
|
||||||
|
|
||||||
|
// Next, write to the primary location
|
||||||
|
settings.remove(SER_HOSTS);
|
||||||
|
settings.beginWriteArray(SER_HOSTS);
|
||||||
|
{
|
||||||
|
QReadLocker lock(&m_ComputerManager->m_Lock);
|
||||||
|
int i = 0;
|
||||||
|
for (const NvComputer* computer : m_ComputerManager->m_KnownHosts) {
|
||||||
|
settings.setArrayIndex(i++);
|
||||||
|
computer->serialize(settings, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
settings.endArray();
|
||||||
|
|
||||||
|
// Finally, delete the backup copy
|
||||||
|
settings.remove(SER_HOSTS_BACKUP);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -319,12 +356,13 @@ void ComputerManager::startPolling()
|
|||||||
|
|
||||||
if (prefs.enableMdns) {
|
if (prefs.enableMdns) {
|
||||||
// Start an MDNS query for GameStream hosts
|
// Start an MDNS query for GameStream hosts
|
||||||
m_MdnsBrowser = new QMdnsEngine::Browser(&m_MdnsServer, "_nvstream._tcp.local.", &m_MdnsCache);
|
m_MdnsServer.reset(new QMdnsEngine::Server());
|
||||||
|
m_MdnsBrowser = new QMdnsEngine::Browser(m_MdnsServer.data(), "_nvstream._tcp.local.");
|
||||||
connect(m_MdnsBrowser, &QMdnsEngine::Browser::serviceAdded,
|
connect(m_MdnsBrowser, &QMdnsEngine::Browser::serviceAdded,
|
||||||
this, [this](const QMdnsEngine::Service& service) {
|
this, [this](const QMdnsEngine::Service& service) {
|
||||||
qInfo() << "Discovered mDNS host:" << service.hostname();
|
qInfo() << "Discovered mDNS host:" << service.hostname();
|
||||||
|
|
||||||
MdnsPendingComputer* pendingComputer = new MdnsPendingComputer(&m_MdnsServer, service);
|
MdnsPendingComputer* pendingComputer = new MdnsPendingComputer(m_MdnsServer, service);
|
||||||
connect(pendingComputer, &MdnsPendingComputer::resolvedHost,
|
connect(pendingComputer, &MdnsPendingComputer::resolvedHost,
|
||||||
this, &ComputerManager::handleMdnsServiceResolved);
|
this, &ComputerManager::handleMdnsServiceResolved);
|
||||||
m_PendingResolution.append(pendingComputer);
|
m_PendingResolution.append(pendingComputer);
|
||||||
@@ -405,6 +443,19 @@ void ComputerManager::handleMdnsServiceResolved(MdnsPendingComputer* computer,
|
|||||||
computer->deleteLater();
|
computer->deleteLater();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ComputerManager::saveHost(NvComputer *computer)
|
||||||
|
{
|
||||||
|
// If no serializable properties changed, don't bother saving hosts
|
||||||
|
QMutexLocker lock(&m_DelayedFlushMutex);
|
||||||
|
QReadLocker computerLock(&computer->lock);
|
||||||
|
if (!m_LastSerializedHosts.value(computer->uuid).isEqualSerialized(*computer)) {
|
||||||
|
// Queue a request for a delayed flush to QSettings outside of the lock
|
||||||
|
computerLock.unlock();
|
||||||
|
lock.unlock();
|
||||||
|
saveHosts();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void ComputerManager::handleComputerStateChanged(NvComputer* computer)
|
void ComputerManager::handleComputerStateChanged(NvComputer* computer)
|
||||||
{
|
{
|
||||||
emit computerStateChanged(computer);
|
emit computerStateChanged(computer);
|
||||||
@@ -414,15 +465,20 @@ void ComputerManager::handleComputerStateChanged(NvComputer* computer)
|
|||||||
emit quitAppCompleted(QVariant());
|
emit quitAppCompleted(QVariant());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save updated hosts to QSettings
|
// Save updates to this host
|
||||||
saveHosts();
|
saveHost(computer);
|
||||||
}
|
}
|
||||||
|
|
||||||
QVector<NvComputer*> ComputerManager::getComputers()
|
QVector<NvComputer*> ComputerManager::getComputers()
|
||||||
{
|
{
|
||||||
QReadLocker lock(&m_Lock);
|
QReadLocker lock(&m_Lock);
|
||||||
|
|
||||||
return QVector<NvComputer*>::fromList(m_KnownHosts.values());
|
// Return a sorted host list
|
||||||
|
auto hosts = QVector<NvComputer*>::fromList(m_KnownHosts.values());
|
||||||
|
std::stable_sort(hosts.begin(), hosts.end(), [](const NvComputer* host1, const NvComputer* host2) {
|
||||||
|
return host1->name.toLower() < host2->name.toLower();
|
||||||
|
});
|
||||||
|
return hosts;
|
||||||
}
|
}
|
||||||
|
|
||||||
class DeferredHostDeletionTask : public QRunnable
|
class DeferredHostDeletionTask : public QRunnable
|
||||||
@@ -446,7 +502,7 @@ public:
|
|||||||
m_ComputerManager->m_KnownHosts.remove(m_Computer->uuid);
|
m_ComputerManager->m_KnownHosts.remove(m_Computer->uuid);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Persist the new host list
|
// Persist the new host list with this computer deleted
|
||||||
m_ComputerManager->saveHosts();
|
m_ComputerManager->saveHosts();
|
||||||
|
|
||||||
// Delete the polling entry first. This will stop all polling threads too.
|
// Delete the polling entry first. This will stop all polling threads too.
|
||||||
@@ -487,9 +543,6 @@ void ComputerManager::renameHost(NvComputer* computer, QString name)
|
|||||||
|
|
||||||
void ComputerManager::clientSideAttributeUpdated(NvComputer* computer)
|
void ComputerManager::clientSideAttributeUpdated(NvComputer* computer)
|
||||||
{
|
{
|
||||||
// Persist the change
|
|
||||||
saveHosts();
|
|
||||||
|
|
||||||
// Notify the UI of the state change
|
// Notify the UI of the state change
|
||||||
handleComputerStateChanged(computer);
|
handleComputerStateChanged(computer);
|
||||||
}
|
}
|
||||||
@@ -547,7 +600,7 @@ private:
|
|||||||
break;
|
break;
|
||||||
case NvPairingManager::PairState::PAIRED:
|
case NvPairingManager::PairState::PAIRED:
|
||||||
// Persist the newly pinned server certificate for this host
|
// Persist the newly pinned server certificate for this host
|
||||||
m_ComputerManager->saveHosts();
|
m_ComputerManager->saveHost(m_Computer);
|
||||||
|
|
||||||
emit pairingCompleted(m_Computer, nullptr);
|
emit pairingCompleted(m_Computer, nullptr);
|
||||||
break;
|
break;
|
||||||
@@ -646,9 +699,10 @@ void ComputerManager::stopPollingAsync()
|
|||||||
m_PendingResolution.removeFirst();
|
m_PendingResolution.removeFirst();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete the browser to stop discovery
|
// Delete the browser and server to stop discovery and refresh polling
|
||||||
delete m_MdnsBrowser;
|
delete m_MdnsBrowser;
|
||||||
m_MdnsBrowser = nullptr;
|
m_MdnsBrowser = nullptr;
|
||||||
|
m_MdnsServer.reset();
|
||||||
|
|
||||||
// Interrupt all threads, but don't wait for them to terminate
|
// Interrupt all threads, but don't wait for them to terminate
|
||||||
for (ComputerPollingEntry* entry : m_PollEntries) {
|
for (ComputerPollingEntry* entry : m_PollEntries) {
|
||||||
@@ -658,8 +712,8 @@ void ComputerManager::stopPollingAsync()
|
|||||||
|
|
||||||
void ComputerManager::addNewHostManually(QString address)
|
void ComputerManager::addNewHostManually(QString address)
|
||||||
{
|
{
|
||||||
QUrl url = QUrl::fromUserInput(address);
|
QUrl url = QUrl::fromUserInput("moonlight://" + address);
|
||||||
if (url.isValid() && !url.host().isEmpty()) {
|
if (url.isValid() && !url.host().isEmpty() && url.scheme() == "moonlight") {
|
||||||
// If there wasn't a port specified, use the default
|
// If there wasn't a port specified, use the default
|
||||||
addNewHost(NvAddress(url.host(), url.port(DEFAULT_HTTP_PORT)), false);
|
addNewHost(NvAddress(url.host(), url.port(DEFAULT_HTTP_PORT)), false);
|
||||||
}
|
}
|
||||||
@@ -878,7 +932,7 @@ private:
|
|||||||
|
|
||||||
// If this wasn't added via mDNS but it is a RFC 1918 IPv4 address and not a VPN,
|
// If this wasn't added via mDNS but it is a RFC 1918 IPv4 address and not a VPN,
|
||||||
// go ahead and do the STUN request now to populate an external address.
|
// go ahead and do the STUN request now to populate an external address.
|
||||||
if (!m_Mdns && addressIsSiteLocalV4 && !newComputer->isReachableOverVpn()) {
|
if (!m_Mdns && addressIsSiteLocalV4 && newComputer->getActiveAddressReachability() != NvComputer::RI_VPN) {
|
||||||
quint32 addr;
|
quint32 addr;
|
||||||
int err = LiFindExternalAddressIP4("stun.moonlight-stream.org", 3478, &addr);
|
int err = LiFindExternalAddressIP4("stun.moonlight-stream.org", 3478, &addr);
|
||||||
if (err == 0) {
|
if (err == 0) {
|
||||||
|
|||||||
@@ -42,11 +42,11 @@ class MdnsPendingComputer : public QObject
|
|||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit MdnsPendingComputer(QMdnsEngine::Server* server,
|
explicit MdnsPendingComputer(const QSharedPointer<QMdnsEngine::Server> server,
|
||||||
const QMdnsEngine::Service& service)
|
const QMdnsEngine::Service& service)
|
||||||
: m_Hostname(service.hostname()),
|
: m_Hostname(service.hostname()),
|
||||||
m_Port(service.port()),
|
m_Port(service.port()),
|
||||||
m_Server(server),
|
m_ServerWeak(server),
|
||||||
m_Resolver(nullptr)
|
m_Resolver(nullptr)
|
||||||
{
|
{
|
||||||
// Start resolving
|
// Start resolving
|
||||||
@@ -94,8 +94,21 @@ signals:
|
|||||||
private:
|
private:
|
||||||
void resolve()
|
void resolve()
|
||||||
{
|
{
|
||||||
|
// Delete our resolver, so we're guaranteed that nothing is referencing m_Server.
|
||||||
delete m_Resolver;
|
delete m_Resolver;
|
||||||
m_Resolver = new QMdnsEngine::Resolver(m_Server, m_Hostname);
|
m_Resolver = nullptr;
|
||||||
|
|
||||||
|
// Now delete our strong reference that we held on behalf of m_Resolver.
|
||||||
|
// The server may be destroyed after we make this call.
|
||||||
|
m_Server.reset();
|
||||||
|
|
||||||
|
// Re-acquire a strong reference if the server still exists.
|
||||||
|
m_Server = m_ServerWeak.toStrongRef();
|
||||||
|
if (!m_Server) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_Resolver = new QMdnsEngine::Resolver(m_Server.data(), m_Hostname);
|
||||||
connect(m_Resolver, &QMdnsEngine::Resolver::resolved,
|
connect(m_Resolver, &QMdnsEngine::Resolver::resolved,
|
||||||
this, &MdnsPendingComputer::handleResolvedAddress);
|
this, &MdnsPendingComputer::handleResolvedAddress);
|
||||||
QTimer::singleShot(2000, this, &MdnsPendingComputer::handleResolvedTimeout);
|
QTimer::singleShot(2000, this, &MdnsPendingComputer::handleResolvedTimeout);
|
||||||
@@ -103,7 +116,8 @@ private:
|
|||||||
|
|
||||||
QByteArray m_Hostname;
|
QByteArray m_Hostname;
|
||||||
uint16_t m_Port;
|
uint16_t m_Port;
|
||||||
QMdnsEngine::Server* m_Server;
|
QWeakPointer<QMdnsEngine::Server> m_ServerWeak;
|
||||||
|
QSharedPointer<QMdnsEngine::Server> m_Server;
|
||||||
QMdnsEngine::Resolver* m_Resolver;
|
QMdnsEngine::Resolver* m_Resolver;
|
||||||
QVector<QHostAddress> m_Addresses;
|
QVector<QHostAddress> m_Addresses;
|
||||||
};
|
};
|
||||||
@@ -237,6 +251,8 @@ private slots:
|
|||||||
private:
|
private:
|
||||||
void saveHosts();
|
void saveHosts();
|
||||||
|
|
||||||
|
void saveHost(NvComputer* computer);
|
||||||
|
|
||||||
QHostAddress getBestGlobalAddressV6(QVector<QHostAddress>& addresses);
|
QHostAddress getBestGlobalAddressV6(QVector<QHostAddress>& addresses);
|
||||||
|
|
||||||
void startPollingComputer(NvComputer* computer);
|
void startPollingComputer(NvComputer* computer);
|
||||||
@@ -245,13 +261,13 @@ private:
|
|||||||
QReadWriteLock m_Lock;
|
QReadWriteLock m_Lock;
|
||||||
QMap<QString, NvComputer*> m_KnownHosts;
|
QMap<QString, NvComputer*> m_KnownHosts;
|
||||||
QMap<QString, ComputerPollingEntry*> m_PollEntries;
|
QMap<QString, ComputerPollingEntry*> m_PollEntries;
|
||||||
QMdnsEngine::Server m_MdnsServer;
|
QHash<QString, NvComputer> m_LastSerializedHosts; // Protected by m_DelayedFlushMutex
|
||||||
|
QSharedPointer<QMdnsEngine::Server> m_MdnsServer;
|
||||||
QMdnsEngine::Browser* m_MdnsBrowser;
|
QMdnsEngine::Browser* m_MdnsBrowser;
|
||||||
QMdnsEngine::Cache m_MdnsCache;
|
|
||||||
QVector<MdnsPendingComputer*> m_PendingResolution;
|
QVector<MdnsPendingComputer*> m_PendingResolution;
|
||||||
CompatFetcher m_CompatFetcher;
|
CompatFetcher m_CompatFetcher;
|
||||||
DelayedFlushThread* m_DelayedFlushThread;
|
DelayedFlushThread* m_DelayedFlushThread;
|
||||||
QMutex m_DelayedFlushMutex;
|
QMutex m_DelayedFlushMutex; // Lock ordering: Must never be acquired while holding NvComputer lock
|
||||||
QWaitCondition m_DelayedFlushCondition;
|
QWaitCondition m_DelayedFlushCondition;
|
||||||
bool m_NeedsDelayedFlush;
|
bool m_NeedsDelayedFlush;
|
||||||
};
|
};
|
||||||
+101
-35
@@ -74,7 +74,7 @@ void NvComputer::setRemoteAddress(QHostAddress address)
|
|||||||
this->remoteAddress = NvAddress(address, this->externalPort);
|
this->remoteAddress = NvAddress(address, this->externalPort);
|
||||||
}
|
}
|
||||||
|
|
||||||
void NvComputer::serialize(QSettings& settings) const
|
void NvComputer::serialize(QSettings& settings, bool serializeApps) const
|
||||||
{
|
{
|
||||||
QReadLocker lock(&this->lock);
|
QReadLocker lock(&this->lock);
|
||||||
|
|
||||||
@@ -94,7 +94,7 @@ void NvComputer::serialize(QSettings& settings) const
|
|||||||
settings.setValue(SER_NVIDIASOFTWARE, isNvidiaServerSoftware);
|
settings.setValue(SER_NVIDIASOFTWARE, isNvidiaServerSoftware);
|
||||||
|
|
||||||
// Avoid deleting an existing applist if we couldn't get one
|
// Avoid deleting an existing applist if we couldn't get one
|
||||||
if (!appList.isEmpty()) {
|
if (!appList.isEmpty() && serializeApps) {
|
||||||
settings.remove(SER_APPLIST);
|
settings.remove(SER_APPLIST);
|
||||||
settings.beginWriteArray(SER_APPLIST);
|
settings.beginWriteArray(SER_APPLIST);
|
||||||
for (int i = 0; i < appList.count(); i++) {
|
for (int i = 0; i < appList.count(); i++) {
|
||||||
@@ -105,6 +105,21 @@ void NvComputer::serialize(QSettings& settings) const
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool NvComputer::isEqualSerialized(const NvComputer &that) const
|
||||||
|
{
|
||||||
|
return this->name == that.name &&
|
||||||
|
this->hasCustomName == that.hasCustomName &&
|
||||||
|
this->uuid == that.uuid &&
|
||||||
|
this->macAddress == that.macAddress &&
|
||||||
|
this->localAddress == that.localAddress &&
|
||||||
|
this->remoteAddress == that.remoteAddress &&
|
||||||
|
this->ipv6Address == that.ipv6Address &&
|
||||||
|
this->manualAddress == that.manualAddress &&
|
||||||
|
this->serverCert == that.serverCert &&
|
||||||
|
this->isNvidiaServerSoftware == that.isNvidiaServerSoftware &&
|
||||||
|
this->appList == that.appList;
|
||||||
|
}
|
||||||
|
|
||||||
void NvComputer::sortAppList()
|
void NvComputer::sortAppList()
|
||||||
{
|
{
|
||||||
std::stable_sort(appList.begin(), appList.end(), [](const NvApp& app1, const NvApp& app2) {
|
std::stable_sort(appList.begin(), appList.end(), [](const NvApp& app1, const NvApp& app2) {
|
||||||
@@ -136,7 +151,8 @@ NvComputer::NvComputer(NvHTTP& http, QString serverInfo)
|
|||||||
this->serverCodecModeSupport = codecSupport.toInt();
|
this->serverCodecModeSupport = codecSupport.toInt();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
this->serverCodecModeSupport = 0;
|
// Assume H.264 is always supported
|
||||||
|
this->serverCodecModeSupport = SCM_H264;
|
||||||
}
|
}
|
||||||
|
|
||||||
QString maxLumaPixelsHEVC = NvHTTP::getXmlString(serverInfo, "MaxLumaPixelsHEVC");
|
QString maxLumaPixelsHEVC = NvHTTP::getXmlString(serverInfo, "MaxLumaPixelsHEVC");
|
||||||
@@ -150,8 +166,8 @@ NvComputer::NvComputer(NvHTTP& http, QString serverInfo)
|
|||||||
this->displayModes = NvHTTP::getDisplayModeList(serverInfo);
|
this->displayModes = NvHTTP::getDisplayModeList(serverInfo);
|
||||||
std::stable_sort(this->displayModes.begin(), this->displayModes.end(),
|
std::stable_sort(this->displayModes.begin(), this->displayModes.end(),
|
||||||
[](const NvDisplayMode& mode1, const NvDisplayMode& mode2) {
|
[](const NvDisplayMode& mode1, const NvDisplayMode& mode2) {
|
||||||
return mode1.width * mode1.height * mode1.refreshRate <
|
return (uint64_t)mode1.width * mode1.height * mode1.refreshRate <
|
||||||
mode2.width * mode2.height * mode2.refreshRate;
|
(uint64_t)mode2.width * mode2.height * mode2.refreshRate;
|
||||||
});
|
});
|
||||||
|
|
||||||
// We can get an IPv4 loopback address if we're using the GS IPv6 Forwarder
|
// We can get an IPv4 loopback address if we're using the GS IPv6 Forwarder
|
||||||
@@ -169,7 +185,7 @@ NvComputer::NvComputer(NvHTTP& http, QString serverInfo)
|
|||||||
// to support dynamic HTTP WAN ports without requiring the user to manually enter the port.
|
// to support dynamic HTTP WAN ports without requiring the user to manually enter the port.
|
||||||
QString remotePortStr = NvHTTP::getXmlString(serverInfo, "ExternalPort");
|
QString remotePortStr = NvHTTP::getXmlString(serverInfo, "ExternalPort");
|
||||||
if (remotePortStr.isEmpty() || (this->externalPort = remotePortStr.toUShort()) == 0) {
|
if (remotePortStr.isEmpty() || (this->externalPort = remotePortStr.toUShort()) == 0) {
|
||||||
this->externalPort = DEFAULT_HTTP_PORT;
|
this->externalPort = http.httpPort();
|
||||||
}
|
}
|
||||||
|
|
||||||
QString remoteAddress = NvHTTP::getXmlString(serverInfo, "ExternalIP");
|
QString remoteAddress = NvHTTP::getXmlString(serverInfo, "ExternalIP");
|
||||||
@@ -219,23 +235,30 @@ bool NvComputer::wake() const
|
|||||||
for (int i = 0; i < 16; i++) {
|
for (int i = 0; i < 16; i++) {
|
||||||
wolPayload.append(macAddress);
|
wolPayload.append(macAddress);
|
||||||
}
|
}
|
||||||
Q_ASSERT(wolPayload.count() == 102);
|
Q_ASSERT(wolPayload.size() == 102);
|
||||||
}
|
}
|
||||||
|
|
||||||
const quint16 WOL_PORTS[] = {
|
// Ports used as-is
|
||||||
|
const quint16 STATIC_WOL_PORTS[] = {
|
||||||
9, // Standard WOL port (privileged port)
|
9, // Standard WOL port (privileged port)
|
||||||
47998, 47999, 48000, 48002, 48010, // Ports opened by GFE
|
|
||||||
47009, // Port opened by Moonlight Internet Hosting Tool for WoL (non-privileged port)
|
47009, // Port opened by Moonlight Internet Hosting Tool for WoL (non-privileged port)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Ports offset by the HTTP base port for hosts using alternate ports
|
||||||
|
const quint16 DYNAMIC_WOL_PORTS[] = {
|
||||||
|
47998, 47999, 48000, 48002, 48010, // Ports opened by GFE
|
||||||
|
};
|
||||||
|
|
||||||
// Add the addresses that we know this host to be
|
// Add the addresses that we know this host to be
|
||||||
// and broadcast addresses for this link just in
|
// and broadcast addresses for this link just in
|
||||||
// case the host has timed out in ARP entries.
|
// case the host has timed out in ARP entries.
|
||||||
QVector<QString> addressList;
|
QMap<QString, quint16> addressMap;
|
||||||
|
QSet<quint16> basePortSet;
|
||||||
for (const NvAddress& addr : uniqueAddresses()) {
|
for (const NvAddress& addr : uniqueAddresses()) {
|
||||||
addressList.append(addr.address());
|
addressMap.insert(addr.address(), addr.port());
|
||||||
|
basePortSet.insert(addr.port());
|
||||||
}
|
}
|
||||||
addressList.append("255.255.255.255");
|
addressMap.insert("255.255.255.255", 0);
|
||||||
|
|
||||||
// Try to broadcast on all available NICs
|
// Try to broadcast on all available NICs
|
||||||
for (const QNetworkInterface& nic : QNetworkInterface::allInterfaces()) {
|
for (const QNetworkInterface& nic : QNetworkInterface::allInterfaces()) {
|
||||||
@@ -254,31 +277,42 @@ bool NvComputer::wake() const
|
|||||||
|
|
||||||
// Skip IPv6 which doesn't support broadcast
|
// Skip IPv6 which doesn't support broadcast
|
||||||
if (!addr.broadcast().isNull()) {
|
if (!addr.broadcast().isNull()) {
|
||||||
addressList.append(addr.broadcast().toString());
|
addressMap.insert(addr.broadcast().toString(), 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!allNodesMulticast.scopeId().isEmpty()) {
|
if (!allNodesMulticast.scopeId().isEmpty()) {
|
||||||
addressList.append(allNodesMulticast.toString());
|
addressMap.insert(allNodesMulticast.toString(), 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try all unique address strings or host names
|
// Try all unique address strings or host names
|
||||||
bool success = false;
|
bool success = false;
|
||||||
for (QString& addressString : addressList) {
|
for (auto i = addressMap.constBegin(); i != addressMap.constEnd(); i++) {
|
||||||
QHostInfo hostInfo = QHostInfo::fromName(addressString);
|
QHostAddress literalAddress;
|
||||||
|
QList<QHostAddress> addressList;
|
||||||
|
|
||||||
if (hostInfo.error() != QHostInfo::NoError) {
|
// If this is an IPv4/IPv6 literal, don't use QHostInfo::fromName() because that will
|
||||||
qWarning() << "Error resolving" << addressString << ":" << hostInfo.errorString();
|
// try to perform a reverse DNS lookup that leads to delays sending WoL packets.
|
||||||
continue;
|
if (literalAddress.setAddress(i.key())) {
|
||||||
|
addressList.append(literalAddress);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
QHostInfo hostInfo = QHostInfo::fromName(i.key());
|
||||||
|
if (hostInfo.error() != QHostInfo::NoError) {
|
||||||
|
qWarning() << "Error resolving" << i.key() << ":" << hostInfo.errorString();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
addressList.append(hostInfo.addresses());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try all IP addresses that this string resolves to
|
// Try all IP addresses that this string resolves to
|
||||||
for (QHostAddress& address : hostInfo.addresses()) {
|
for (QHostAddress& address : addressList) {
|
||||||
QUdpSocket sock;
|
QUdpSocket sock;
|
||||||
|
|
||||||
// Send to all ports
|
// Send to all static ports
|
||||||
for (quint16 port : WOL_PORTS) {
|
for (quint16 port : STATIC_WOL_PORTS) {
|
||||||
if (sock.writeDatagram(wolPayload, address, port)) {
|
if (sock.writeDatagram(wolPayload, address, port)) {
|
||||||
qInfo().nospace().noquote() << "Sent WoL packet to " << name << " via " << address.toString() << ":" << port;
|
qInfo().nospace().noquote() << "Sent WoL packet to " << name << " via " << address.toString() << ":" << port;
|
||||||
success = true;
|
success = true;
|
||||||
@@ -287,13 +321,38 @@ bool NvComputer::wake() const
|
|||||||
qWarning() << "Send failed:" << sock.error();
|
qWarning() << "Send failed:" << sock.error();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QList<quint16> basePorts;
|
||||||
|
if (i.value() != 0) {
|
||||||
|
// If we have a known base port for this address, use only that port
|
||||||
|
basePorts.append(i.value());
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// If this is a broadcast address without a known HTTP port, try all of them
|
||||||
|
basePorts.append(basePortSet.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send to all dynamic ports using the HTTP port offset(s) for this address
|
||||||
|
for (quint16 basePort : basePorts) {
|
||||||
|
for (quint16 port : DYNAMIC_WOL_PORTS) {
|
||||||
|
port = (port - 47989) + basePort;
|
||||||
|
|
||||||
|
if (sock.writeDatagram(wolPayload, address, port)) {
|
||||||
|
qInfo().nospace().noquote() << "Sent WoL packet to " << name << " via " << address.toString() << ":" << port;
|
||||||
|
success = true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
qWarning() << "Send failed:" << sock.error();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool NvComputer::isReachableOverVpn() const
|
NvComputer::ReachabilityType NvComputer::getActiveAddressReachability() const
|
||||||
{
|
{
|
||||||
NvAddress copyOfActiveAddress;
|
NvAddress copyOfActiveAddress;
|
||||||
|
|
||||||
@@ -301,7 +360,7 @@ bool NvComputer::isReachableOverVpn() const
|
|||||||
QReadLocker readLocker(&lock);
|
QReadLocker readLocker(&lock);
|
||||||
|
|
||||||
if (activeAddress.isNull()) {
|
if (activeAddress.isNull()) {
|
||||||
return false;
|
return ReachabilityType::RI_UNKNOWN;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Grab a copy of the active address to avoid having to hold
|
// Grab a copy of the active address to avoid having to hold
|
||||||
@@ -332,48 +391,55 @@ bool NvComputer::isReachableOverVpn() const
|
|||||||
if (nic.type() == QNetworkInterface::Virtual ||
|
if (nic.type() == QNetworkInterface::Virtual ||
|
||||||
nic.type() == QNetworkInterface::Ppp) {
|
nic.type() == QNetworkInterface::Ppp) {
|
||||||
// Treat PPP and virtual interfaces as likely VPNs
|
// Treat PPP and virtual interfaces as likely VPNs
|
||||||
return true;
|
return ReachabilityType::RI_VPN;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nic.maximumTransmissionUnit() != 0 && nic.maximumTransmissionUnit() < 1500) {
|
if (nic.maximumTransmissionUnit() != 0 && nic.maximumTransmissionUnit() < 1500) {
|
||||||
// Treat MTUs under 1500 as likely VPNs
|
// Treat MTUs under 1500 as likely VPNs
|
||||||
return true;
|
return ReachabilityType::RI_VPN;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
if (nic.flags() & QNetworkInterface::IsPointToPoint) {
|
if (nic.flags() & QNetworkInterface::IsPointToPoint) {
|
||||||
// Treat point-to-point links as likely VPNs.
|
// Treat point-to-point links as likely VPNs.
|
||||||
// This check detects OpenVPN on Unix-like OSes.
|
// This check detects OpenVPN on Unix-like OSes.
|
||||||
return true;
|
return ReachabilityType::RI_VPN;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nic.hardwareAddress().startsWith("00:FF", Qt::CaseInsensitive)) {
|
if (nic.hardwareAddress().startsWith("00:FF", Qt::CaseInsensitive)) {
|
||||||
// OpenVPN TAP interfaces have a MAC address starting with 00:FF on Windows
|
// OpenVPN TAP interfaces have a MAC address starting with 00:FF on Windows
|
||||||
return true;
|
return ReachabilityType::RI_VPN;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nic.humanReadableName().startsWith("ZeroTier")) {
|
if (nic.humanReadableName().startsWith("ZeroTier")) {
|
||||||
// ZeroTier interfaces always start with "ZeroTier"
|
// ZeroTier interfaces always start with "ZeroTier"
|
||||||
return true;
|
return ReachabilityType::RI_VPN;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nic.humanReadableName().contains("VPN")) {
|
if (nic.humanReadableName().contains("VPN")) {
|
||||||
// This one is just a final heuristic if all else fails
|
// This one is just a final VPN heuristic if all else fails
|
||||||
return true;
|
return ReachabilityType::RI_VPN;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Didn't meet any of our VPN heuristics
|
// Didn't meet any of our VPN heuristics. Let's see if it's on-link.
|
||||||
return false;
|
Q_ASSERT(addr.prefixLength() >= 0);
|
||||||
|
if (addr.prefixLength() >= 0 && s.localAddress().isInSubnet(addr.ip(), addr.prefixLength())) {
|
||||||
|
return ReachabilityType::RI_LAN;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default to unknown if nothing else matched
|
||||||
|
return ReachabilityType::RI_UNKNOWN;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
qWarning() << "No match found for address:" << s.localAddress();
|
qWarning() << "No match found for address:" << s.localAddress();
|
||||||
return false;
|
return ReachabilityType::RI_UNKNOWN;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// If we fail to connect, just pretend that it's not a VPN
|
// If we fail to connect, just pretend that it's not a VPN
|
||||||
return false;
|
qWarning() << "Unable to check for reachability within 3 seconds";
|
||||||
|
return ReachabilityType::RI_UNKNOWN;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,16 @@
|
|||||||
#include <QSettings>
|
#include <QSettings>
|
||||||
#include <QRunnable>
|
#include <QRunnable>
|
||||||
|
|
||||||
|
class CopySafeReadWriteLock : public QReadWriteLock
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
CopySafeReadWriteLock() = default;
|
||||||
|
|
||||||
|
// Don't actually copy the QReadWriteLock
|
||||||
|
CopySafeReadWriteLock(const CopySafeReadWriteLock&) : QReadWriteLock() {}
|
||||||
|
CopySafeReadWriteLock& operator=(const CopySafeReadWriteLock &) { return *this; }
|
||||||
|
};
|
||||||
|
|
||||||
class NvComputer
|
class NvComputer
|
||||||
{
|
{
|
||||||
friend class PcMonitorThread;
|
friend class PcMonitorThread;
|
||||||
@@ -22,6 +32,14 @@ private:
|
|||||||
bool pendingQuit;
|
bool pendingQuit;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
NvComputer() = default;
|
||||||
|
|
||||||
|
// Caller is responsible for synchronizing read access to the other host
|
||||||
|
NvComputer(const NvComputer&) = default;
|
||||||
|
|
||||||
|
// Caller is responsible for synchronizing read access to the other host
|
||||||
|
NvComputer& operator=(const NvComputer &) = default;
|
||||||
|
|
||||||
explicit NvComputer(NvHTTP& http, QString serverInfo);
|
explicit NvComputer(NvHTTP& http, QString serverInfo);
|
||||||
|
|
||||||
explicit NvComputer(QSettings& settings);
|
explicit NvComputer(QSettings& settings);
|
||||||
@@ -35,14 +53,25 @@ public:
|
|||||||
bool
|
bool
|
||||||
wake() const;
|
wake() const;
|
||||||
|
|
||||||
bool
|
enum ReachabilityType
|
||||||
isReachableOverVpn() const;
|
{
|
||||||
|
RI_UNKNOWN,
|
||||||
|
RI_LAN,
|
||||||
|
RI_VPN,
|
||||||
|
};
|
||||||
|
|
||||||
|
ReachabilityType
|
||||||
|
getActiveAddressReachability() const;
|
||||||
|
|
||||||
QVector<NvAddress>
|
QVector<NvAddress>
|
||||||
uniqueAddresses() const;
|
uniqueAddresses() const;
|
||||||
|
|
||||||
void
|
void
|
||||||
serialize(QSettings& settings) const;
|
serialize(QSettings& settings, bool serializeApps) const;
|
||||||
|
|
||||||
|
// Caller is responsible for synchronizing read access to both hosts
|
||||||
|
bool
|
||||||
|
isEqualSerialized(const NvComputer& that) const;
|
||||||
|
|
||||||
enum PairState
|
enum PairState
|
||||||
{
|
{
|
||||||
@@ -84,9 +113,10 @@ public:
|
|||||||
QSslCertificate serverCert;
|
QSslCertificate serverCert;
|
||||||
QVector<NvApp> appList;
|
QVector<NvApp> appList;
|
||||||
bool isNvidiaServerSoftware;
|
bool isNvidiaServerSoftware;
|
||||||
|
// Remember to update isEqualSerialized() when adding fields here!
|
||||||
|
|
||||||
// Synchronization
|
// Synchronization
|
||||||
mutable QReadWriteLock lock;
|
mutable CopySafeReadWriteLock lock;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
uint16_t externalPort;
|
uint16_t externalPort;
|
||||||
|
|||||||
+18
-38
@@ -187,12 +187,15 @@ NvHTTP::getServerInfo(NvLogLevel logLevel, bool fastFail)
|
|||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
NvHTTP::launchApp(int appId,
|
NvHTTP::startApp(QString verb,
|
||||||
PSTREAM_CONFIGURATION streamConfig,
|
bool isGfe,
|
||||||
bool sops,
|
int appId,
|
||||||
bool localAudio,
|
PSTREAM_CONFIGURATION streamConfig,
|
||||||
int gamepadMask,
|
bool sops,
|
||||||
QString& rtspSessionUrl)
|
bool localAudio,
|
||||||
|
int gamepadMask,
|
||||||
|
bool persistGameControllersOnDisconnect,
|
||||||
|
QString& rtspSessionUrl)
|
||||||
{
|
{
|
||||||
int riKeyId;
|
int riKeyId;
|
||||||
|
|
||||||
@@ -201,25 +204,26 @@ NvHTTP::launchApp(int appId,
|
|||||||
|
|
||||||
QString response =
|
QString response =
|
||||||
openConnectionToString(m_BaseUrlHttps,
|
openConnectionToString(m_BaseUrlHttps,
|
||||||
"launch",
|
verb,
|
||||||
"appid="+QString::number(appId)+
|
"appid="+QString::number(appId)+
|
||||||
"&mode="+QString::number(streamConfig->width)+"x"+
|
"&mode="+QString::number(streamConfig->width)+"x"+
|
||||||
QString::number(streamConfig->height)+"x"+
|
QString::number(streamConfig->height)+"x"+
|
||||||
// Using an FPS value over 60 causes SOPS to default to 720p60,
|
// Using an FPS value over 60 causes SOPS to default to 720p60,
|
||||||
// so force it to 0 to ensure the correct resolution is set. We
|
// so force it to 0 to ensure the correct resolution is set. We
|
||||||
// used to use 60 here but that locked the frame rate to 60 FPS
|
// used to use 60 here but that locked the frame rate to 60 FPS
|
||||||
// on GFE 3.20.3.
|
// on GFE 3.20.3. We don't need this hack for Sunshine.
|
||||||
QString::number(streamConfig->fps > 60 ? 0 : streamConfig->fps)+
|
QString::number((streamConfig->fps > 60 && isGfe) ? 0 : streamConfig->fps)+
|
||||||
"&additionalStates=1&sops="+QString::number(sops ? 1 : 0)+
|
"&additionalStates=1&sops="+QString::number(sops ? 1 : 0)+
|
||||||
"&rikey="+QByteArray(streamConfig->remoteInputAesKey, sizeof(streamConfig->remoteInputAesKey)).toHex()+
|
"&rikey="+QByteArray(streamConfig->remoteInputAesKey, sizeof(streamConfig->remoteInputAesKey)).toHex()+
|
||||||
"&rikeyid="+QString::number(riKeyId)+
|
"&rikeyid="+QString::number(riKeyId)+
|
||||||
(streamConfig->enableHdr ?
|
((streamConfig->supportedVideoFormats & VIDEO_FORMAT_MASK_10BIT) ?
|
||||||
"&hdrMode=1&clientHdrCapVersion=0&clientHdrCapSupportedFlagsInUint32=0&clientHdrCapMetaDataId=NV_STATIC_METADATA_TYPE_1&clientHdrCapDisplayData=0x0x0x0x0x0x0x0x0x0x0" :
|
"&hdrMode=1&clientHdrCapVersion=0&clientHdrCapSupportedFlagsInUint32=0&clientHdrCapMetaDataId=NV_STATIC_METADATA_TYPE_1&clientHdrCapDisplayData=0x0x0x0x0x0x0x0x0x0x0" :
|
||||||
"")+
|
"")+
|
||||||
"&localAudioPlayMode="+QString::number(localAudio ? 1 : 0)+
|
"&localAudioPlayMode="+QString::number(localAudio ? 1 : 0)+
|
||||||
"&surroundAudioInfo="+QString::number(SURROUNDAUDIOINFO_FROM_AUDIO_CONFIGURATION(streamConfig->audioConfiguration))+
|
"&surroundAudioInfo="+QString::number(SURROUNDAUDIOINFO_FROM_AUDIO_CONFIGURATION(streamConfig->audioConfiguration))+
|
||||||
"&remoteControllersBitmap="+QString::number(gamepadMask)+
|
"&remoteControllersBitmap="+QString::number(gamepadMask)+
|
||||||
"&gcmap="+QString::number(gamepadMask),
|
"&gcmap="+QString::number(gamepadMask)+
|
||||||
|
"&gcpersist="+QString::number(persistGameControllersOnDisconnect ? 1 : 0),
|
||||||
LAUNCH_TIMEOUT_MS);
|
LAUNCH_TIMEOUT_MS);
|
||||||
|
|
||||||
qInfo() << "Launch response:" << response;
|
qInfo() << "Launch response:" << response;
|
||||||
@@ -230,30 +234,6 @@ NvHTTP::launchApp(int appId,
|
|||||||
rtspSessionUrl = getXmlString(response, "sessionUrl0");
|
rtspSessionUrl = getXmlString(response, "sessionUrl0");
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
|
||||||
NvHTTP::resumeApp(PSTREAM_CONFIGURATION streamConfig, QString& rtspSessionUrl)
|
|
||||||
{
|
|
||||||
int riKeyId;
|
|
||||||
|
|
||||||
memcpy(&riKeyId, streamConfig->remoteInputAesIv, sizeof(riKeyId));
|
|
||||||
riKeyId = qFromBigEndian(riKeyId);
|
|
||||||
|
|
||||||
QString response =
|
|
||||||
openConnectionToString(m_BaseUrlHttps,
|
|
||||||
"resume",
|
|
||||||
"rikey="+QString(QByteArray(streamConfig->remoteInputAesKey, sizeof(streamConfig->remoteInputAesKey)).toHex())+
|
|
||||||
"&rikeyid="+QString::number(riKeyId)+
|
|
||||||
"&surroundAudioInfo="+QString::number(SURROUNDAUDIOINFO_FROM_AUDIO_CONFIGURATION(streamConfig->audioConfiguration)),
|
|
||||||
RESUME_TIMEOUT_MS);
|
|
||||||
|
|
||||||
qInfo() << "Resume response:" << response;
|
|
||||||
|
|
||||||
// Throws if the request failed
|
|
||||||
verifyResponseStatus(response);
|
|
||||||
|
|
||||||
rtspSessionUrl = getXmlString(response, "sessionUrl0");
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
void
|
||||||
NvHTTP::quitApp()
|
NvHTTP::quitApp()
|
||||||
{
|
{
|
||||||
@@ -269,7 +249,7 @@ NvHTTP::quitApp()
|
|||||||
verifyResponseStatus(response);
|
verifyResponseStatus(response);
|
||||||
|
|
||||||
// Newer GFE versions will just return success even if quitting fails
|
// Newer GFE versions will just return success even if quitting fails
|
||||||
// if we're not the original requestor.
|
// if we're not the original requester.
|
||||||
if (getCurrentGame(getServerInfo(NvHTTP::NVLL_ERROR)) != 0) {
|
if (getCurrentGame(getServerInfo(NvHTTP::NVLL_ERROR)) != 0) {
|
||||||
// Generate a synthetic GfeResponseException letting the caller know
|
// Generate a synthetic GfeResponseException letting the caller know
|
||||||
// that they can't kill someone else's stream.
|
// that they can't kill someone else's stream.
|
||||||
@@ -382,7 +362,7 @@ NvHTTP::verifyResponseStatus(QString xml)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
throw GfeHttpResponseException(-1, "Malformed GFE XML (missing root element)");
|
throw GfeHttpResponseException(-1, "Malformed XML (missing root element)");
|
||||||
}
|
}
|
||||||
|
|
||||||
QImage
|
QImage
|
||||||
@@ -551,7 +531,7 @@ NvHTTP::openConnection(QUrl baseUrl,
|
|||||||
if (reply->error() != QNetworkReply::NoError)
|
if (reply->error() != QNetworkReply::NoError)
|
||||||
{
|
{
|
||||||
if (logLevel >= NvLogLevel::NVLL_ERROR) {
|
if (logLevel >= NvLogLevel::NVLL_ERROR) {
|
||||||
qWarning() << command << " request failed with error " << reply->error();
|
qWarning() << command << "request failed with error:" << reply->error();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (reply->error() == QNetworkReply::SslHandshakeFailedError) {
|
if (reply->error() == QNetworkReply::SslHandshakeFailedError) {
|
||||||
|
|||||||
+9
-10
@@ -162,16 +162,15 @@ public:
|
|||||||
quitApp();
|
quitApp();
|
||||||
|
|
||||||
void
|
void
|
||||||
resumeApp(PSTREAM_CONFIGURATION streamConfig,
|
startApp(QString verb,
|
||||||
QString& rtspSessionUrl);
|
bool isGfe,
|
||||||
|
int appId,
|
||||||
void
|
PSTREAM_CONFIGURATION streamConfig,
|
||||||
launchApp(int appId,
|
bool sops,
|
||||||
PSTREAM_CONFIGURATION streamConfig,
|
bool localAudio,
|
||||||
bool sops,
|
int gamepadMask,
|
||||||
bool localAudio,
|
bool persistGameControllersOnDisconnect,
|
||||||
int gamepadMask,
|
QString& rtspSessionUrl);
|
||||||
QString& rtspSessionUrl);
|
|
||||||
|
|
||||||
QVector<NvApp>
|
QVector<NvApp>
|
||||||
getAppList();
|
getAppList();
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
#include "utils.h"
|
#include "utils.h"
|
||||||
|
|
||||||
#include <QGuiApplication>
|
#include <QGuiApplication>
|
||||||
|
#include <QLibraryInfo>
|
||||||
|
|
||||||
#include "streaming/session.h"
|
#include "streaming/session.h"
|
||||||
#include "streaming/streamutils.h"
|
#include "streaming/streamutils.h"
|
||||||
@@ -17,6 +18,7 @@ SystemProperties::SystemProperties()
|
|||||||
hasDesktopEnvironment = WMUtils::isRunningDesktopEnvironment();
|
hasDesktopEnvironment = WMUtils::isRunningDesktopEnvironment();
|
||||||
isRunningWayland = WMUtils::isRunningWayland();
|
isRunningWayland = WMUtils::isRunningWayland();
|
||||||
isRunningXWayland = isRunningWayland && QGuiApplication::platformName() == "xcb";
|
isRunningXWayland = isRunningWayland && QGuiApplication::platformName() == "xcb";
|
||||||
|
usesMaterial3Theme = QLibraryInfo::version() >= QVersionNumber(6, 5, 0);
|
||||||
QString nativeArch = QSysInfo::currentCpuArchitecture();
|
QString nativeArch = QSysInfo::currentCpuArchitecture();
|
||||||
|
|
||||||
#ifdef Q_OS_WIN32
|
#ifdef Q_OS_WIN32
|
||||||
@@ -70,7 +72,7 @@ SystemProperties::SystemProperties()
|
|||||||
// and cache the results to speed up future queries on this data.
|
// and cache the results to speed up future queries on this data.
|
||||||
querySdlVideoInfo();
|
querySdlVideoInfo();
|
||||||
|
|
||||||
Q_ASSERT(maximumStreamingFrameRate >= 60);
|
Q_ASSERT(!monitorRefreshRates.isEmpty());
|
||||||
Q_ASSERT(!monitorNativeResolutions.isEmpty());
|
Q_ASSERT(!monitorNativeResolutions.isEmpty());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,6 +82,12 @@ QRect SystemProperties::getNativeResolution(int displayIndex)
|
|||||||
return monitorNativeResolutions.value(displayIndex);
|
return monitorNativeResolutions.value(displayIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int SystemProperties::getRefreshRate(int displayIndex)
|
||||||
|
{
|
||||||
|
// Returns 0 if out of bounds
|
||||||
|
return monitorRefreshRates.value(displayIndex);
|
||||||
|
}
|
||||||
|
|
||||||
class QuerySdlVideoThread : public QThread
|
class QuerySdlVideoThread : public QThread
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
@@ -188,9 +196,6 @@ void SystemProperties::refreshDisplaysInternal()
|
|||||||
|
|
||||||
monitorNativeResolutions.clear();
|
monitorNativeResolutions.clear();
|
||||||
|
|
||||||
// Never let the maximum drop below 60 FPS
|
|
||||||
maximumStreamingFrameRate = 60;
|
|
||||||
|
|
||||||
SDL_DisplayMode bestMode;
|
SDL_DisplayMode bestMode;
|
||||||
for (int displayIndex = 0; displayIndex < SDL_GetNumVideoDisplays(); displayIndex++) {
|
for (int displayIndex = 0; displayIndex < SDL_GetNumVideoDisplays(); displayIndex++) {
|
||||||
SDL_DisplayMode desktopMode;
|
SDL_DisplayMode desktopMode;
|
||||||
@@ -218,7 +223,17 @@ void SystemProperties::refreshDisplaysInternal()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
maximumStreamingFrameRate = qMax(maximumStreamingFrameRate, bestMode.refresh_rate);
|
// Try to normalize values around our our standard refresh rates.
|
||||||
|
// Some displays/OSes report values that are slightly off.
|
||||||
|
if (bestMode.refresh_rate >= 58 && bestMode.refresh_rate <= 62) {
|
||||||
|
monitorRefreshRates.append(60);
|
||||||
|
}
|
||||||
|
else if (bestMode.refresh_rate >= 28 && bestMode.refresh_rate <= 32) {
|
||||||
|
monitorRefreshRates.append(30);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
monitorRefreshRates.append(bestMode.refresh_rate);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,13 +23,14 @@ public:
|
|||||||
Q_PROPERTY(bool hasBrowser MEMBER hasBrowser CONSTANT)
|
Q_PROPERTY(bool hasBrowser MEMBER hasBrowser CONSTANT)
|
||||||
Q_PROPERTY(bool hasDiscordIntegration MEMBER hasDiscordIntegration CONSTANT)
|
Q_PROPERTY(bool hasDiscordIntegration MEMBER hasDiscordIntegration CONSTANT)
|
||||||
Q_PROPERTY(QString unmappedGamepads MEMBER unmappedGamepads NOTIFY unmappedGamepadsChanged)
|
Q_PROPERTY(QString unmappedGamepads MEMBER unmappedGamepads NOTIFY unmappedGamepadsChanged)
|
||||||
Q_PROPERTY(int maximumStreamingFrameRate MEMBER maximumStreamingFrameRate CONSTANT)
|
|
||||||
Q_PROPERTY(QSize maximumResolution MEMBER maximumResolution CONSTANT)
|
Q_PROPERTY(QSize maximumResolution MEMBER maximumResolution CONSTANT)
|
||||||
Q_PROPERTY(QString versionString MEMBER versionString CONSTANT)
|
Q_PROPERTY(QString versionString MEMBER versionString CONSTANT)
|
||||||
Q_PROPERTY(bool supportsHdr MEMBER supportsHdr CONSTANT)
|
Q_PROPERTY(bool supportsHdr MEMBER supportsHdr CONSTANT)
|
||||||
|
Q_PROPERTY(bool usesMaterial3Theme MEMBER usesMaterial3Theme CONSTANT)
|
||||||
|
|
||||||
Q_INVOKABLE void refreshDisplays();
|
Q_INVOKABLE void refreshDisplays();
|
||||||
Q_INVOKABLE QRect getNativeResolution(int displayIndex);
|
Q_INVOKABLE QRect getNativeResolution(int displayIndex);
|
||||||
|
Q_INVOKABLE int getRefreshRate(int displayIndex);
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void unmappedGamepadsChanged();
|
void unmappedGamepadsChanged();
|
||||||
@@ -49,10 +50,11 @@ private:
|
|||||||
bool hasBrowser;
|
bool hasBrowser;
|
||||||
bool hasDiscordIntegration;
|
bool hasDiscordIntegration;
|
||||||
QString unmappedGamepads;
|
QString unmappedGamepads;
|
||||||
int maximumStreamingFrameRate;
|
|
||||||
QSize maximumResolution;
|
QSize maximumResolution;
|
||||||
QList<QRect> monitorNativeResolutions;
|
QList<QRect> monitorNativeResolutions;
|
||||||
|
QList<int> monitorRefreshRates;
|
||||||
QString versionString;
|
QString versionString;
|
||||||
bool supportsHdr;
|
bool supportsHdr;
|
||||||
|
bool usesMaterial3Theme;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -118,7 +118,7 @@ public:
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
// Occurs when the previous app quit has been completed, handles quitting errors if any
|
// Occurs when the previous app quit has been completed, handles quitting errors if any
|
||||||
// happended. ComputerUpdated event's handler handles session start when previous app has
|
// happened. ComputerUpdated event's handler handles session start when previous app has
|
||||||
// quit.
|
// quit.
|
||||||
case Event::AppQuitCompleted:
|
case Event::AppQuitCompleted:
|
||||||
if (m_State == StateSeekApp && !event.errorMessage.isEmpty()) {
|
if (m_State == StateSeekApp && !event.errorMessage.isEmpty()) {
|
||||||
|
|||||||
@@ -4,21 +4,19 @@
|
|||||||
<metadata_license>CC0-1.0</metadata_license>
|
<metadata_license>CC0-1.0</metadata_license>
|
||||||
<project_license>GPL-3.0+</project_license>
|
<project_license>GPL-3.0+</project_license>
|
||||||
<name>Moonlight</name>
|
<name>Moonlight</name>
|
||||||
<summary>Play games remotely from your NVIDIA GameStream-enabled PC</summary>
|
<summary>Stream games and other applications from another PC running Sunshine or GeForce Experience</summary>
|
||||||
|
|
||||||
<description>
|
<description>
|
||||||
<p>Moonlight can stream games and other applications from a PC with an NVIDIA GeForce GTX/RTX or NVIDIA Quadro GPU and GeForce/Quadro Experience installed.</p>
|
<p>Moonlight can stream games and other applications from another PC with Sunshine or GeForce Experience software installed.</p>
|
||||||
<p>Features include:</p>
|
<p>Features include:</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>Streaming at up to 4K resolution</li>
|
|
||||||
<li>Support for up to 120 FPS streaming (high refresh rate monitor recommended)</li>
|
|
||||||
<li>Hardware accelerated video decoding with VAAPI, VDPAU, and NVDEC support</li>
|
<li>Hardware accelerated video decoding with VAAPI, VDPAU, and NVDEC support</li>
|
||||||
|
<li>H.264, HEVC, and AV1 codec support</li>
|
||||||
<li>7.1 surround sound audio support</li>
|
<li>7.1 surround sound audio support</li>
|
||||||
|
<li>10-point multitouch support</li>
|
||||||
|
<li>Gamepad support with force feedback for up to 16 players</li>
|
||||||
<li>Support for both pointer capture (for games) and direct mouse control (for remote desktop)</li>
|
<li>Support for both pointer capture (for games) and direct mouse control (for remote desktop)</li>
|
||||||
<li>Support for passing system-wide keyboard shortcuts like Alt+Tab to the host</li>
|
<li>Support for passing system-wide keyboard shortcuts like Alt+Tab to the host</li>
|
||||||
<li>Direct touchscreen mouse control</li>
|
|
||||||
<li>HEVC support for better video compression efficiency</li>
|
|
||||||
<li>Gamepad support with force feedback for up to 4 players</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</description>
|
</description>
|
||||||
|
|
||||||
@@ -36,6 +34,67 @@
|
|||||||
</screenshots>
|
</screenshots>
|
||||||
|
|
||||||
<releases>
|
<releases>
|
||||||
|
<release version="5.0.0" date="2023-10-18">
|
||||||
|
<description>
|
||||||
|
<p>New features:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Custom FPS values</li>
|
||||||
|
<li>Streaming using the AV1 codec from Sunshine hosts</li>
|
||||||
|
<li>Horizontal scrolling with Sunshine hosts</li>
|
||||||
|
<li>Native multi-touch input with Sunshine hosts</li>
|
||||||
|
<li>Controller type information is passed to Sunshine for controller type autoselection</li>
|
||||||
|
<li>Extended features of PS4/PS5 controllers (touchpad, battery state, gyro/accelerometer motion, and RGB LED) are natively passed to Sunshine hosts</li>
|
||||||
|
<li>Support for up to 16 gamepads with Sunshine hosts</li>
|
||||||
|
<li>Frame pacing support on Wayland</li>
|
||||||
|
<li>Host-side processing latency is now displayed in the performance overlay with Sunshine hosts</li>
|
||||||
|
<li>Window title now includes the PC name when streaming</li>
|
||||||
|
<li>Alphabetical sorting of the PC list</li>
|
||||||
|
<li>Updated Play and Stop button design to improve contrast with app box art</li>
|
||||||
|
<li>The settings page automatically scrolls to the selected element when using keyboard or gamepad navigation</li>
|
||||||
|
<li>Pressing Share+PS buttons on PS4/PS5 controllers can be used to emulate a clickpad press</li>
|
||||||
|
<li>Improved audio quality when streaming with a high bitrate from remote hosts</li>
|
||||||
|
<li>Increased mouse polling rate</li>
|
||||||
|
</ul>
|
||||||
|
<p>Behavior Changes:</p>
|
||||||
|
<ul>
|
||||||
|
<li>IPv6 addresses must be provided in URL-escaped form. Example: [2001:db8::1234:5678] or [2001:db8::1234:5678]:47989</li>
|
||||||
|
<li>Default bitrate logic has been adjusted to behave better with custom resolutions and frame rates</li>
|
||||||
|
</ul>
|
||||||
|
<p>Bugfixes:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Reduced the frequency of audio underruns (clicks and pops) by increasing audio buffer size</li>
|
||||||
|
<li>Fixed "Quitting App" message never dismissing in some cases</li>
|
||||||
|
<li>Fixed parsing IPv6 literals with port numbers</li>
|
||||||
|
<li>Fixed unnecessary WiFi activity causing stream disruptions</li>
|
||||||
|
<li>Fixed paired hosts being forgotten if Moonlight terminates while writing hosts to disk</li>
|
||||||
|
<li>Fixed freeze when closing the stream on Windows with some software that uses DX11 hooks</li>
|
||||||
|
<li>Fixed delayed PC wake when activating Wake-On-LAN feature</li>
|
||||||
|
<li>Fixed Wake-on-LAN when Sunshine is running on alternate ports</li>
|
||||||
|
<li>Fixed leak of /dev/dri fds when trying to initialize VAAPI decoder</li>
|
||||||
|
<li>Fixed support for the Applications/Menu key</li>
|
||||||
|
<li>Fixed automatic language detection for Simplified Chinese and Traditional Chinese</li>
|
||||||
|
<li>Fixed streaming from Sunshine when the client PC has multiple network interfaces or IPv6 privacy extensions enabled</li>
|
||||||
|
<li>Fixed small receive buffer causing video packet loss on some systems</li>
|
||||||
|
<li>Fixed theme overrides causing the UI to be unusable on some systems</li>
|
||||||
|
<li>Fixed tearing with V-Sync enabled on macOS and Linux when using the SDL renderer (software decoding and some hardware decoders)</li>
|
||||||
|
<li>Fixed stateful V4L2 hardware decoding using h264_v4l2m2m and hevc_v4l2m2m decoders</li>
|
||||||
|
<li>Fixed CVE-2023-42801</li>
|
||||||
|
<li>Updated included gamepad mappings</li>
|
||||||
|
<li>Updated community-contributed translations from Weblate</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release version="4.3.1" date="2022-11-03">
|
||||||
|
<description>
|
||||||
|
<p>Changes:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Fixed crash during specific patterns of audio packet loss</li>
|
||||||
|
<li>Fixed detection of local streaming when using IPv6 addresses</li>
|
||||||
|
<li>Windowed mode streaming now starts maximized if the GUI window was maximized</li>
|
||||||
|
<li>Updated community-contributed translations from Weblate</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
<release version="4.3.0" date="2022-10-28">
|
<release version="4.3.0" date="2022-10-28">
|
||||||
<description>
|
<description>
|
||||||
<p>New features:</p>
|
<p>New features:</p>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
[Desktop Entry]
|
[Desktop Entry]
|
||||||
Name=Moonlight
|
Name=Moonlight
|
||||||
Comment=Stream games from your NVIDIA GameStream-enabled PC
|
Comment=Stream games and other applications from another PC running Sunshine or GeForce Experience
|
||||||
Exec=moonlight
|
Exec=moonlight
|
||||||
Icon=moonlight
|
Icon=moonlight
|
||||||
Terminal=false
|
Terminal=false
|
||||||
Type=Application
|
Type=Application
|
||||||
Categories=Qt;Game;
|
Categories=Qt;Game;
|
||||||
Keywords=nvidia;gamestream;stream;
|
Keywords=nvidia;gamestream;stream;sunshine;remote play;
|
||||||
+15
-13
@@ -1,5 +1,6 @@
|
|||||||
import QtQuick 2.9
|
import QtQuick 2.9
|
||||||
import QtQuick.Controls 2.2
|
import QtQuick.Controls 2.2
|
||||||
|
import QtQuick.Controls.Material 2.2
|
||||||
|
|
||||||
import AppModel 1.0
|
import AppModel 1.0
|
||||||
import ComputerManager 1.0
|
import ComputerManager 1.0
|
||||||
@@ -89,7 +90,7 @@ CenteredGridView {
|
|||||||
|
|
||||||
onSourceSizeChanged: {
|
onSourceSizeChanged: {
|
||||||
// Nearly all of Nvidia's official box art does not match the dimensions of placeholder
|
// Nearly all of Nvidia's official box art does not match the dimensions of placeholder
|
||||||
// images, however the one known exeception is Overcooked. Therefore, we only execute
|
// images, however the one known exception is Overcooked. Therefore, we only execute
|
||||||
// the image size checks if this is not an app collector game. We know the officially
|
// the image size checks if this is not an app collector game. We know the officially
|
||||||
// supported games all have box art, so this check is not required.
|
// supported games all have box art, so this check is not required.
|
||||||
if (!model.isAppCollectorGame &&
|
if (!model.isAppCollectorGame &&
|
||||||
@@ -121,15 +122,15 @@ CenteredGridView {
|
|||||||
anchors.fill: appIcon
|
anchors.fill: appIcon
|
||||||
|
|
||||||
sourceComponent: Item {
|
sourceComponent: Item {
|
||||||
ToolButton {
|
RoundButton {
|
||||||
anchors.horizontalCenterOffset: appIcon.isPlaceholder ? -47 : 0
|
anchors.horizontalCenterOffset: appIcon.isPlaceholder ? -47 : 0
|
||||||
anchors.verticalCenterOffset: appIcon.isPlaceholder ? -75 : -60
|
anchors.verticalCenterOffset: appIcon.isPlaceholder ? -75 : -60
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
implicitWidth: 125
|
implicitWidth: 85
|
||||||
implicitHeight: 125
|
implicitHeight: 85
|
||||||
|
|
||||||
Image {
|
Image {
|
||||||
source: "qrc:/res/baseline-play_circle_filled_white-48px.svg"
|
source: "qrc:/res/play_arrow_FILL1_wght700_GRAD200_opsz48.svg"
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
sourceSize {
|
sourceSize {
|
||||||
width: 75
|
width: 75
|
||||||
@@ -145,17 +146,19 @@ CenteredGridView {
|
|||||||
ToolTip.delay: 1000
|
ToolTip.delay: 1000
|
||||||
ToolTip.timeout: 3000
|
ToolTip.timeout: 3000
|
||||||
ToolTip.visible: hovered
|
ToolTip.visible: hovered
|
||||||
|
|
||||||
|
Material.background: "#D0808080"
|
||||||
}
|
}
|
||||||
|
|
||||||
ToolButton {
|
RoundButton {
|
||||||
anchors.horizontalCenterOffset: appIcon.isPlaceholder ? 47 : 0
|
anchors.horizontalCenterOffset: appIcon.isPlaceholder ? 47 : 0
|
||||||
anchors.verticalCenterOffset: appIcon.isPlaceholder ? -75 : 60
|
anchors.verticalCenterOffset: appIcon.isPlaceholder ? -75 : 60
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
implicitWidth: 125
|
implicitWidth: 85
|
||||||
implicitHeight: 125
|
implicitHeight: 85
|
||||||
|
|
||||||
Image {
|
Image {
|
||||||
source: "qrc:/res/baseline-cancel-24px.svg"
|
source: "qrc:/res/stop_FILL1_wght700_GRAD200_opsz48.svg"
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
sourceSize {
|
sourceSize {
|
||||||
width: 75
|
width: 75
|
||||||
@@ -171,6 +174,8 @@ CenteredGridView {
|
|||||||
ToolTip.delay: 1000
|
ToolTip.delay: 1000
|
||||||
ToolTip.timeout: 3000
|
ToolTip.timeout: 3000
|
||||||
ToolTip.visible: hovered
|
ToolTip.visible: hovered
|
||||||
|
|
||||||
|
Material.background: "#D0808080"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -345,7 +350,7 @@ CenteredGridView {
|
|||||||
|
|
||||||
function quitApp() {
|
function quitApp() {
|
||||||
var component = Qt.createComponent("QuitSegue.qml")
|
var component = Qt.createComponent("QuitSegue.qml")
|
||||||
var params = {"appName": appName}
|
var params = {"appName": appName, "quitRunningAppFn": appModel.quitRunningApp}
|
||||||
if (segueToStream) {
|
if (segueToStream) {
|
||||||
// Store the session and app name if we're going to stream after
|
// Store the session and app name if we're going to stream after
|
||||||
// successfully quitting the old app.
|
// successfully quitting the old app.
|
||||||
@@ -358,9 +363,6 @@ CenteredGridView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
stackView.push(component.createObject(stackView, params))
|
stackView.push(component.createObject(stackView, params))
|
||||||
|
|
||||||
// Trigger the quit after pushing the quit segue on screen
|
|
||||||
appModel.quitRunningApp()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onAccepted: quitApp()
|
onAccepted: quitApp()
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import QtQuick 2.9
|
|||||||
import QtQuick.Controls 2.2
|
import QtQuick.Controls 2.2
|
||||||
|
|
||||||
import SdlGamepadKeyNavigation 1.0
|
import SdlGamepadKeyNavigation 1.0
|
||||||
|
import SystemProperties 1.0
|
||||||
|
|
||||||
// https://stackoverflow.com/questions/45029968/how-do-i-set-the-combobox-width-to-fit-the-largest-item
|
// https://stackoverflow.com/questions/45029968/how-do-i-set-the-combobox-width-to-fit-the-largest-item
|
||||||
ComboBox {
|
ComboBox {
|
||||||
@@ -38,6 +39,12 @@ ComboBox {
|
|||||||
popup.onAboutToShow: {
|
popup.onAboutToShow: {
|
||||||
// Switch to normal navigation for combo boxes
|
// Switch to normal navigation for combo boxes
|
||||||
SdlGamepadKeyNavigation.setUiNavMode(false)
|
SdlGamepadKeyNavigation.setUiNavMode(false)
|
||||||
|
|
||||||
|
// Override the popup color to improve contrast with the overridden
|
||||||
|
// Material 2 background color set in main.qml.
|
||||||
|
if (SystemProperties.usesMaterial3Theme) {
|
||||||
|
popup.background.color = "#424242"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
popup.onAboutToHide: {
|
popup.onAboutToHide: {
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ Item {
|
|||||||
function onLaunchFailed(message) {
|
function onLaunchFailed(message) {
|
||||||
errorDialog.text = message
|
errorDialog.text = message
|
||||||
errorDialog.open()
|
errorDialog.open()
|
||||||
|
console.error(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
function onAppQuitRequired(appName) {
|
function onAppQuitRequired(appName) {
|
||||||
@@ -84,10 +85,8 @@ Item {
|
|||||||
|
|
||||||
function quitApp() {
|
function quitApp() {
|
||||||
var component = Qt.createComponent("QuitSegue.qml")
|
var component = Qt.createComponent("QuitSegue.qml")
|
||||||
var params = {"appName": appName}
|
var params = {"appName": appName, "quitRunningAppFn": launcher.quitRunningApp}
|
||||||
stackView.push(component.createObject(stackView, params))
|
stackView.push(component.createObject(stackView, params))
|
||||||
// Trigger the quit after pushing the quit segue on screen
|
|
||||||
launcher.quitRunningApp()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onAccepted: quitApp()
|
onAccepted: quitApp()
|
||||||
|
|||||||
+20
-15
@@ -99,7 +99,7 @@ CenteredGridView {
|
|||||||
Label {
|
Label {
|
||||||
height: searchSpinner.height
|
height: searchSpinner.height
|
||||||
elide: Label.ElideRight
|
elide: Label.ElideRight
|
||||||
text: StreamingPreferences.enableMdns ? qsTr("Searching for PCs on your local network with NVIDIA GameStream enabled...")
|
text: StreamingPreferences.enableMdns ? qsTr("Searching for compatible hosts on your local network...")
|
||||||
: qsTr("Automatic PC discovery is disabled. Add your PC manually.")
|
: qsTr("Automatic PC discovery is disabled. Add your PC manually.")
|
||||||
font.pointSize: 20
|
font.pointSize: 20
|
||||||
verticalAlignment: Text.AlignVCenter
|
verticalAlignment: Text.AlignVCenter
|
||||||
@@ -130,12 +130,12 @@ CenteredGridView {
|
|||||||
id: stateIcon
|
id: stateIcon
|
||||||
anchors.horizontalCenter: pcIcon.horizontalCenter
|
anchors.horizontalCenter: pcIcon.horizontalCenter
|
||||||
anchors.verticalCenter: pcIcon.verticalCenter
|
anchors.verticalCenter: pcIcon.verticalCenter
|
||||||
anchors.verticalCenterOffset: -15
|
anchors.verticalCenterOffset: !model.online ? -18 : -16
|
||||||
visible: !model.statusUnknown && (!model.online || !model.paired)
|
visible: !model.statusUnknown && (!model.online || !model.paired)
|
||||||
source: !model.online ? "qrc:/res/baseline-warning-24px.svg" : "qrc:/res/baseline-lock-24px.svg"
|
source: !model.online ? "qrc:/res/warning_FILL1_wght300_GRAD200_opsz24.svg" : "qrc:/res/baseline-lock-24px.svg"
|
||||||
sourceSize {
|
sourceSize {
|
||||||
width: 75
|
width: !model.online ? 75 : 70
|
||||||
height: 75
|
height: !model.online ? 75 : 70
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,7 +211,7 @@ CenteredGridView {
|
|||||||
text: qsTr("Delete PC")
|
text: qsTr("Delete PC")
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
deletePcDialog.pcIndex = index
|
deletePcDialog.pcIndex = index
|
||||||
// get confirmation first, actual closing is called from the dialog
|
deletePcDialog.pcName = model.name
|
||||||
deletePcDialog.open()
|
deletePcDialog.open()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -271,6 +271,12 @@ CenteredGridView {
|
|||||||
// the ItemDelegate and not where the mouse cursor is
|
// the ItemDelegate and not where the mouse cursor is
|
||||||
pcContextMenu.open()
|
pcContextMenu.open()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Keys.onDeletePressed: {
|
||||||
|
deletePcDialog.pcIndex = index
|
||||||
|
deletePcDialog.pcName = model.name
|
||||||
|
deletePcDialog.open()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ErrorMessageDialog {
|
ErrorMessageDialog {
|
||||||
@@ -291,7 +297,8 @@ CenteredGridView {
|
|||||||
|
|
||||||
// don't allow edits to the rest of the window while open
|
// don't allow edits to the rest of the window while open
|
||||||
property string pin : "0000"
|
property string pin : "0000"
|
||||||
text:qsTr("Please enter %1 on your GameStream PC. This dialog will close when pairing is completed.").arg(pin)
|
text:qsTr("Please enter %1 on your host PC. This dialog will close when pairing is completed.").arg(pin)+"\n\n"+
|
||||||
|
qsTr("If your host PC is running Sunshine, navigate to the Sunshine web UI to enter the PIN.")
|
||||||
standardButtons: Dialog.Cancel
|
standardButtons: Dialog.Cancel
|
||||||
onRejected: {
|
onRejected: {
|
||||||
// FIXME: We should interrupt pairing here
|
// FIXME: We should interrupt pairing here
|
||||||
@@ -301,16 +308,14 @@ CenteredGridView {
|
|||||||
NavigableMessageDialog {
|
NavigableMessageDialog {
|
||||||
id: deletePcDialog
|
id: deletePcDialog
|
||||||
// don't allow edits to the rest of the window while open
|
// don't allow edits to the rest of the window while open
|
||||||
property int pcIndex : -1;
|
property int pcIndex : -1
|
||||||
text:qsTr("Are you sure you want to remove this PC?")
|
property string pcName : ""
|
||||||
|
text: qsTr("Are you sure you want to remove '%1'?").arg(pcName)
|
||||||
standardButtons: Dialog.Yes | Dialog.No
|
standardButtons: Dialog.Yes | Dialog.No
|
||||||
|
|
||||||
function deletePc() {
|
onAccepted: {
|
||||||
console.log("deleting PC pairing for PC at index: " + pcIndex)
|
computerModel.deleteComputer(pcIndex)
|
||||||
computerModel.deleteComputer(pcIndex);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onAccepted: deletePc()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
NavigableMessageDialog {
|
NavigableMessageDialog {
|
||||||
@@ -319,7 +324,7 @@ CenteredGridView {
|
|||||||
standardButtons: Dialog.Ok
|
standardButtons: Dialog.Ok
|
||||||
|
|
||||||
onAboutToShow: {
|
onAboutToShow: {
|
||||||
testConnectionDialog.text = qsTr("Moonlight is testing your network connection to determine if NVIDIA GameStream is blocked.") + "\n\n" + qsTr("This may take a few seconds…")
|
testConnectionDialog.text = qsTr("Moonlight is testing your network connection to determine if any required ports are blocked.") + "\n\n" + qsTr("This may take a few seconds…")
|
||||||
showSpinner = true
|
showSpinner = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import Session 1.0
|
|||||||
|
|
||||||
Item {
|
Item {
|
||||||
property string appName
|
property string appName
|
||||||
|
property var quitRunningAppFn
|
||||||
property Session nextSession : null
|
property Session nextSession : null
|
||||||
property string nextAppName : ""
|
property string nextAppName : ""
|
||||||
|
|
||||||
@@ -17,6 +18,7 @@ Item {
|
|||||||
if (error !== undefined) {
|
if (error !== undefined) {
|
||||||
errorDialog.text = error
|
errorDialog.text = error
|
||||||
errorDialog.open()
|
errorDialog.open()
|
||||||
|
console.error(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we're supposed to launch another game after this, do so now
|
// If we're supposed to launch another game after this, do so now
|
||||||
@@ -37,6 +39,11 @@ Item {
|
|||||||
|
|
||||||
// Connect the quit completion signal
|
// Connect the quit completion signal
|
||||||
ComputerManager.quitAppCompleted.connect(quitAppCompleted)
|
ComputerManager.quitAppCompleted.connect(quitAppCompleted)
|
||||||
|
|
||||||
|
// Start the quit operation if requested
|
||||||
|
if (quitRunningAppFn) {
|
||||||
|
quitRunningAppFn()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
StackView.onDeactivating: {
|
StackView.onDeactivating: {
|
||||||
|
|||||||
+242
-85
@@ -1,6 +1,7 @@
|
|||||||
import QtQuick 2.9
|
import QtQuick 2.9
|
||||||
import QtQuick.Controls 2.2
|
import QtQuick.Controls 2.2
|
||||||
import QtQuick.Layouts 1.2
|
import QtQuick.Layouts 1.2
|
||||||
|
import QtQuick.Window 2.2
|
||||||
|
|
||||||
import StreamingPreferences 1.0
|
import StreamingPreferences 1.0
|
||||||
import ComputerManager 1.0
|
import ComputerManager 1.0
|
||||||
@@ -25,6 +26,49 @@ Flickable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isChildOfFlickable(item) {
|
||||||
|
while (item) {
|
||||||
|
if (item.parent === contentItem) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
item = item.parent
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
NumberAnimation on contentY {
|
||||||
|
id: autoScrollAnimation
|
||||||
|
duration: 100
|
||||||
|
}
|
||||||
|
|
||||||
|
Window.onActiveFocusItemChanged: {
|
||||||
|
var item = Window.activeFocusItem
|
||||||
|
if (item) {
|
||||||
|
// Ignore non-child elements like the toolbar buttons
|
||||||
|
if (!isChildOfFlickable(item)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map the focus item's position into our content item's coordinate space
|
||||||
|
var pos = item.mapToItem(contentItem, 0, 0)
|
||||||
|
|
||||||
|
// Ensure some extra space is visible around the element we're scrolling to
|
||||||
|
var scrollMargin = height > 100 ? 50 : 0
|
||||||
|
|
||||||
|
if (pos.y - scrollMargin < contentY) {
|
||||||
|
autoScrollAnimation.from = contentY
|
||||||
|
autoScrollAnimation.to = Math.max(pos.y - scrollMargin, 0)
|
||||||
|
autoScrollAnimation.start()
|
||||||
|
}
|
||||||
|
else if (pos.y + item.height + scrollMargin > contentY + height) {
|
||||||
|
autoScrollAnimation.from = contentY
|
||||||
|
autoScrollAnimation.to = Math.min(pos.y + item.height + scrollMargin - height, contentHeight - height)
|
||||||
|
autoScrollAnimation.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
StackView.onActivated: {
|
StackView.onActivated: {
|
||||||
// This enables Tab and BackTab based navigation rather than arrow keys.
|
// This enables Tab and BackTab based navigation rather than arrow keys.
|
||||||
// It is required to shift focus between controls on the settings page.
|
// It is required to shift focus between controls on the settings page.
|
||||||
@@ -388,69 +432,190 @@ Flickable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
AutoResizingComboBox {
|
AutoResizingComboBox {
|
||||||
function createModel() {
|
property int lastIndexValue
|
||||||
var fpsListModel = Qt.createQmlObject('import QtQuick 2.0; ListModel {}', parent, '')
|
|
||||||
|
|
||||||
var max_fps = SystemProperties.maximumStreamingFrameRate
|
function updateBitrateForSelection() {
|
||||||
|
// Only modify the bitrate if the values actually changed
|
||||||
|
var selectedFps = parseInt(model.get(fpsComboBox.currentIndex).video_fps)
|
||||||
|
if (StreamingPreferences.fps !== selectedFps) {
|
||||||
|
StreamingPreferences.fps = selectedFps
|
||||||
|
|
||||||
// Default entries
|
StreamingPreferences.bitrateKbps = StreamingPreferences.getDefaultBitrate(StreamingPreferences.width,
|
||||||
fpsListModel.append({"text": qsTr("%1 FPS").arg("30"), "video_fps": "30"})
|
StreamingPreferences.height,
|
||||||
fpsListModel.append({"text": qsTr("%1 FPS").arg("60"), "video_fps": "60"})
|
StreamingPreferences.fps);
|
||||||
|
slider.value = StreamingPreferences.bitrateKbps
|
||||||
|
}
|
||||||
|
|
||||||
// Add unsupported FPS values that come before the display max FPS
|
lastIndexValue = currentIndex
|
||||||
if (StreamingPreferences.unsupportedFps) {
|
}
|
||||||
if (max_fps > 90) {
|
|
||||||
fpsListModel.append({"text": qsTr("%1 FPS (Unsupported)").arg("90"), "video_fps": "90"})
|
NavigableDialog {
|
||||||
|
function isInputValid() {
|
||||||
|
// If we have text that isn't valid, reject the input.
|
||||||
|
if (!fpsField.acceptableInput && fpsField.text) {
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
if (max_fps > 120) {
|
|
||||||
fpsListModel.append({"text": qsTr("%1 FPS (Unsupported)").arg("120"), "video_fps": "120"})
|
// The textbox needs to have text or placeholder text
|
||||||
|
if (!fpsField.text && !fpsField.placeholderText) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
id: customFpsDialog
|
||||||
|
standardButtons: Dialog.Ok | Dialog.Cancel
|
||||||
|
onOpened: {
|
||||||
|
// Force keyboard focus on the textbox so keyboard navigation works
|
||||||
|
fpsField.forceActiveFocus()
|
||||||
|
|
||||||
|
// standardButton() was added in Qt 5.10, so we must check for it first
|
||||||
|
if (customFpsDialog.standardButton) {
|
||||||
|
customFpsDialog.standardButton(Dialog.Ok).enabled = customFpsDialog.isInputValid()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use 64 as the cutoff for adding a separate option to
|
onClosed: {
|
||||||
// handle wonky displays that report just over 60 Hz.
|
fpsField.clear()
|
||||||
if (max_fps > 64) {
|
}
|
||||||
// Mark any FPS value greater than 120 as unsupported
|
|
||||||
if (StreamingPreferences.unsupportedFps && max_fps > 120) {
|
onRejected: {
|
||||||
fpsListModel.append({"text": qsTr("%1 FPS (Unsupported)").arg(max_fps), "video_fps": ""+max_fps})
|
fpsComboBox.currentIndex = fpsComboBox.lastIndexValue
|
||||||
|
}
|
||||||
|
|
||||||
|
onAccepted: {
|
||||||
|
// Reject if there's invalid input
|
||||||
|
if (!isInputValid()) {
|
||||||
|
reject()
|
||||||
|
return
|
||||||
}
|
}
|
||||||
else if (max_fps > 120) {
|
|
||||||
fpsListModel.append({"text": qsTr("%1 FPS").arg("120"), "video_fps": "120"})
|
var fps = fpsField.text ? fpsField.text : fpsField.placeholderText
|
||||||
}
|
|
||||||
else {
|
// Find and update the custom entry
|
||||||
fpsListModel.append({"text": qsTr("%1 FPS").arg(max_fps), "video_fps": ""+max_fps})
|
for (var i = 0; i < fpsListModel.count; i++) {
|
||||||
|
if (fpsListModel.get(i).is_custom) {
|
||||||
|
fpsListModel.setProperty(i, "video_fps", fps)
|
||||||
|
fpsListModel.setProperty(i, "text", qsTr("Custom (%1 FPS)").arg(fps))
|
||||||
|
|
||||||
|
// Now update the bitrate using the custom resolution
|
||||||
|
fpsComboBox.currentIndex = i
|
||||||
|
fpsComboBox.updateBitrateForSelection()
|
||||||
|
|
||||||
|
// Update the combobox width too
|
||||||
|
fpsComboBox.recalculateWidth()
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add unsupported FPS values that come after the display max FPS
|
ColumnLayout {
|
||||||
if (StreamingPreferences.unsupportedFps) {
|
Label {
|
||||||
if (max_fps < 90) {
|
text: qsTr("Enter a custom frame rate:")
|
||||||
fpsListModel.append({"text":qsTr("%1 FPS (Unsupported)").arg("90"), "video_fps": "90"})
|
font.bold: true
|
||||||
}
|
}
|
||||||
if (max_fps < 120) {
|
|
||||||
fpsListModel.append({"text":qsTr("%1 FPS (Unsupported)").arg("120"), "video_fps": "120"})
|
RowLayout {
|
||||||
|
TextField {
|
||||||
|
id: fpsField
|
||||||
|
maximumLength: 4
|
||||||
|
inputMethodHints: Qt.ImhDigitsOnly
|
||||||
|
placeholderText: fpsListModel.get(fpsComboBox.currentIndex).video_fps
|
||||||
|
validator: IntValidator{bottom:10; top:9999}
|
||||||
|
focus: true
|
||||||
|
|
||||||
|
onTextChanged: {
|
||||||
|
// standardButton() was added in Qt 5.10, so we must check for it first
|
||||||
|
if (customFpsDialog.standardButton) {
|
||||||
|
customFpsDialog.standardButton(Dialog.Ok).enabled = customFpsDialog.isInputValid()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Keys.onReturnPressed: {
|
||||||
|
customFpsDialog.accept()
|
||||||
|
}
|
||||||
|
|
||||||
|
Keys.onEnterPressed: {
|
||||||
|
customFpsDialog.accept()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addRefreshRateOrdered(fpsListModel, refreshRate, description, custom) {
|
||||||
|
var indexToAdd = 0
|
||||||
|
for (var j = 0; j < fpsListModel.count; j++) {
|
||||||
|
var existing_fps = parseInt(fpsListModel.get(j).video_fps);
|
||||||
|
|
||||||
|
if (refreshRate === existing_fps || (custom && fpsListModel.get(j).is_custom)) {
|
||||||
|
// Duplicate entry, skip
|
||||||
|
indexToAdd = -1
|
||||||
|
break
|
||||||
|
}
|
||||||
|
else if (refreshRate > existing_fps) {
|
||||||
|
// Candidate entrypoint after this entry
|
||||||
|
indexToAdd = j + 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return fpsListModel
|
// Insert this frame rate if it's not a duplicate
|
||||||
|
if (indexToAdd >= 0) {
|
||||||
|
// Custom values always go at the end of the list
|
||||||
|
if (custom) {
|
||||||
|
indexToAdd = fpsListModel.count
|
||||||
|
}
|
||||||
|
|
||||||
|
fpsListModel.insert(indexToAdd,
|
||||||
|
{
|
||||||
|
"text": description,
|
||||||
|
"video_fps": ""+refreshRate,
|
||||||
|
"is_custom": custom
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return indexToAdd
|
||||||
}
|
}
|
||||||
|
|
||||||
function reinitialize() {
|
function reinitialize() {
|
||||||
model = createModel()
|
// Add native refresh rate for all attached displays
|
||||||
|
var done = false
|
||||||
|
for (var displayIndex = 0; !done; displayIndex++) {
|
||||||
|
var refreshRate = SystemProperties.getRefreshRate(displayIndex);
|
||||||
|
if (refreshRate === 0) {
|
||||||
|
// Exceeded max count of displays
|
||||||
|
done = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
addRefreshRateOrdered(fpsListModel, refreshRate, qsTr("%1 FPS").arg(refreshRate), false)
|
||||||
|
}
|
||||||
|
|
||||||
var saved_fps = StreamingPreferences.fps
|
var saved_fps = StreamingPreferences.fps
|
||||||
currentIndex = 0
|
var found = false
|
||||||
for (var i = 0; i < model.count; i++) {
|
for (var i = 0; i < model.count; i++) {
|
||||||
var el_fps = parseInt(model.get(i).video_fps);
|
var el_fps = parseInt(model.get(i).video_fps);
|
||||||
|
|
||||||
// Pick the highest value lesser or equal to the saved FPS
|
// Look for a matching frame rate
|
||||||
if (saved_fps >= el_fps) {
|
if (saved_fps === el_fps) {
|
||||||
currentIndex = i
|
currentIndex = i
|
||||||
|
found = true
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Persist the selected value
|
// If we didn't find one, add a custom frame rate for the current value
|
||||||
activated(currentIndex)
|
if (!found) {
|
||||||
|
currentIndex = addRefreshRateOrdered(model, saved_fps, qsTr("Custom (%1 FPS)").arg(saved_fps), true)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
addRefreshRateOrdered(model, "", qsTr("Custom"), true)
|
||||||
|
}
|
||||||
|
|
||||||
|
recalculateWidth()
|
||||||
|
|
||||||
|
lastIndexValue = currentIndex
|
||||||
}
|
}
|
||||||
|
|
||||||
// ignore setting the index at first, and actually set it when the component is loaded
|
// ignore setting the index at first, and actually set it when the component is loaded
|
||||||
@@ -459,21 +624,31 @@ Flickable {
|
|||||||
languageChanged.connect(reinitialize)
|
languageChanged.connect(reinitialize)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model: ListModel {
|
||||||
|
id: fpsListModel
|
||||||
|
// Other elements may be added at runtime
|
||||||
|
ListElement {
|
||||||
|
text: qsTr("30 FPS")
|
||||||
|
video_fps: "30"
|
||||||
|
is_custom: false
|
||||||
|
}
|
||||||
|
ListElement {
|
||||||
|
text: qsTr("60 FPS")
|
||||||
|
video_fps: "60"
|
||||||
|
is_custom: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
id: fpsComboBox
|
id: fpsComboBox
|
||||||
maximumWidth: parent.width / 2
|
maximumWidth: parent.width / 2
|
||||||
textRole: "text"
|
textRole: "text"
|
||||||
// ::onActivated must be used, as it only listens for when the index is changed by a human
|
// ::onActivated must be used, as it only listens for when the index is changed by a human
|
||||||
onActivated : {
|
onActivated : {
|
||||||
var selectedFps = parseInt(model.get(currentIndex).video_fps)
|
if (model.get(currentIndex).is_custom) {
|
||||||
|
customFpsDialog.open()
|
||||||
// Only modify the bitrate if the values actually changed
|
}
|
||||||
if (StreamingPreferences.fps !== selectedFps) {
|
else {
|
||||||
StreamingPreferences.fps = selectedFps
|
updateBitrateForSelection()
|
||||||
|
|
||||||
StreamingPreferences.bitrateKbps = StreamingPreferences.getDefaultBitrate(StreamingPreferences.width,
|
|
||||||
StreamingPreferences.height,
|
|
||||||
StreamingPreferences.fps);
|
|
||||||
slider.value = StreamingPreferences.bitrateKbps
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -511,6 +686,11 @@ Flickable {
|
|||||||
bitrateTitle.text = qsTr("Video bitrate: %1 Mbps").arg(value / 1000.0)
|
bitrateTitle.text = qsTr("Video bitrate: %1 Mbps").arg(value / 1000.0)
|
||||||
StreamingPreferences.bitrateKbps = value
|
StreamingPreferences.bitrateKbps = value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
// Refresh the text after translations change
|
||||||
|
languageChanged.connect(onValueChanged)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
@@ -546,7 +726,7 @@ Flickable {
|
|||||||
for (var i = 0; i < model.count; i++) {
|
for (var i = 0; i < model.count; i++) {
|
||||||
var thisWm = model.get(i).val;
|
var thisWm = model.get(i).val;
|
||||||
if (thisWm === StreamingPreferences.recommendedFullScreenMode) {
|
if (thisWm === StreamingPreferences.recommendedFullScreenMode) {
|
||||||
model.get(i).text += qsTr(" (Recommended)")
|
model.get(i).text += " " + qsTr("(Recommended)")
|
||||||
model.move(i, 0, 1)
|
model.move(i, 0, 1)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -837,8 +1017,8 @@ Flickable {
|
|||||||
val: StreamingPreferences.LANG_UK
|
val: StreamingPreferences.LANG_UK
|
||||||
} */
|
} */
|
||||||
ListElement {
|
ListElement {
|
||||||
text: "繁体字" // Traditional Chinese
|
text: "繁體中文" // Traditional Chinese
|
||||||
val: StreamingPreferences.LANG_ZH_HANT
|
val: StreamingPreferences.LANG_ZH_TW
|
||||||
}
|
}
|
||||||
ListElement {
|
ListElement {
|
||||||
text: "Português" // Portuguese
|
text: "Português" // Portuguese
|
||||||
@@ -868,6 +1048,10 @@ Flickable {
|
|||||||
text: "Čeština" // Czech
|
text: "Čeština" // Czech
|
||||||
val: StreamingPreferences.LANG_CS
|
val: StreamingPreferences.LANG_CS
|
||||||
}
|
}
|
||||||
|
/* ListElement {
|
||||||
|
text: "עִבְרִית" // Hebrew
|
||||||
|
val: StreamingPreferences.LANG_HE
|
||||||
|
} */
|
||||||
}
|
}
|
||||||
// ::onActivated must be used, as it only listens for when the index is changed by a human
|
// ::onActivated must be used, as it only listens for when the index is changed by a human
|
||||||
onActivated : {
|
onActivated : {
|
||||||
@@ -1375,7 +1559,6 @@ Flickable {
|
|||||||
|
|
||||||
id: codecComboBox
|
id: codecComboBox
|
||||||
textRole: "text"
|
textRole: "text"
|
||||||
enabled: !enableHdr.checked
|
|
||||||
model: ListModel {
|
model: ListModel {
|
||||||
id: codecListModel
|
id: codecListModel
|
||||||
ListElement {
|
ListElement {
|
||||||
@@ -1390,6 +1573,10 @@ Flickable {
|
|||||||
text: qsTr("HEVC (H.265)")
|
text: qsTr("HEVC (H.265)")
|
||||||
val: StreamingPreferences.VCC_FORCE_HEVC
|
val: StreamingPreferences.VCC_FORCE_HEVC
|
||||||
}
|
}
|
||||||
|
ListElement {
|
||||||
|
text: qsTr("AV1 (Experimental)")
|
||||||
|
val: StreamingPreferences.VCC_FORCE_AV1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// ::onActivated must be used, as it only listens for when the index is changed by a human
|
// ::onActivated must be used, as it only listens for when the index is changed by a human
|
||||||
onActivated : {
|
onActivated : {
|
||||||
@@ -1397,21 +1584,6 @@ Flickable {
|
|||||||
StreamingPreferences.videoCodecConfig = codecListModel.get(currentIndex).val
|
StreamingPreferences.videoCodecConfig = codecListModel.get(currentIndex).val
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// This handles the state of the enableHdr checkbox changing
|
|
||||||
onEnabledChanged: {
|
|
||||||
if (enabled) {
|
|
||||||
StreamingPreferences.videoCodecConfig = codecListModel.get(currentIndex).val
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
StreamingPreferences.videoCodecConfig = StreamingPreferences.VCC_FORCE_HEVC_HDR
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ToolTip.delay: 1000
|
|
||||||
ToolTip.timeout: 5000
|
|
||||||
ToolTip.visible: hovered && !enabled
|
|
||||||
ToolTip.text: qsTr("Enabling HDR overrides manual codec selections.")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
CheckBox {
|
CheckBox {
|
||||||
@@ -1419,8 +1591,12 @@ Flickable {
|
|||||||
width: parent.width
|
width: parent.width
|
||||||
text: qsTr("Enable HDR (Experimental)")
|
text: qsTr("Enable HDR (Experimental)")
|
||||||
font.pointSize: 12
|
font.pointSize: 12
|
||||||
|
|
||||||
enabled: SystemProperties.supportsHdr
|
enabled: SystemProperties.supportsHdr
|
||||||
checked: enabled && StreamingPreferences.videoCodecConfig == StreamingPreferences.VCC_FORCE_HEVC_HDR
|
checked: enabled && StreamingPreferences.enableHdr
|
||||||
|
onCheckedChanged: {
|
||||||
|
StreamingPreferences.enableHdr = checked
|
||||||
|
}
|
||||||
|
|
||||||
// Updating StreamingPreferences.videoCodecConfig is handled above
|
// Updating StreamingPreferences.videoCodecConfig is handled above
|
||||||
|
|
||||||
@@ -1433,25 +1609,6 @@ Flickable {
|
|||||||
qsTr("HDR streaming is not supported on this PC.")
|
qsTr("HDR streaming is not supported on this PC.")
|
||||||
}
|
}
|
||||||
|
|
||||||
CheckBox {
|
|
||||||
id: unlockUnsupportedFps
|
|
||||||
width: parent.width
|
|
||||||
text: qsTr("Unlock unsupported FPS options")
|
|
||||||
font.pointSize: 12
|
|
||||||
checked: StreamingPreferences.unsupportedFps
|
|
||||||
onCheckedChanged: {
|
|
||||||
// This is called on init, so only do the work if we've
|
|
||||||
// actually changed the value.
|
|
||||||
if (StreamingPreferences.unsupportedFps != checked) {
|
|
||||||
StreamingPreferences.unsupportedFps = checked
|
|
||||||
|
|
||||||
// The selectable FPS values depend on whether
|
|
||||||
// this option is enabled or not
|
|
||||||
fpsComboBox.reinitialize()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CheckBox {
|
CheckBox {
|
||||||
id: enableMdns
|
id: enableMdns
|
||||||
width: parent.width
|
width: parent.width
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ Item {
|
|||||||
{
|
{
|
||||||
// Display the error dialog after Session::exec() returns
|
// Display the error dialog after Session::exec() returns
|
||||||
streamSegueErrorDialog.text = text
|
streamSegueErrorDialog.text = text
|
||||||
|
console.error(text)
|
||||||
}
|
}
|
||||||
|
|
||||||
function displayLaunchWarning(text)
|
function displayLaunchWarning(text)
|
||||||
@@ -56,6 +57,7 @@ Item {
|
|||||||
toast.text = text
|
toast.text = text
|
||||||
toast.timeout = 3000
|
toast.timeout = 3000
|
||||||
toast.visible = true
|
toast.visible = true
|
||||||
|
console.warn(text)
|
||||||
}
|
}
|
||||||
|
|
||||||
function quitStarting()
|
function quitStarting()
|
||||||
|
|||||||
+10
-11
@@ -186,19 +186,18 @@ void ComputerModel::handlePairingCompleted(NvComputer*, QString error)
|
|||||||
|
|
||||||
void ComputerModel::handleComputerStateChanged(NvComputer* computer)
|
void ComputerModel::handleComputerStateChanged(NvComputer* computer)
|
||||||
{
|
{
|
||||||
// If this is an existing computer, we can report the data changed
|
QVector<NvComputer*> newComputerList = m_ComputerManager->getComputers();
|
||||||
int index = m_Computers.indexOf(computer);
|
|
||||||
if (index >= 0) {
|
// Reset the model if the structural layout of the list has changed
|
||||||
// Let the view know that this specific computer changed
|
if (m_Computers != newComputerList) {
|
||||||
emit dataChanged(createIndex(index, 0), createIndex(index, 0));
|
beginResetModel();
|
||||||
|
m_Computers = newComputerList;
|
||||||
|
endResetModel();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// This is a new PC which may be inserted at an arbitrary point
|
// Let the view know that this specific computer changed
|
||||||
// in our computer list (since it comes from CM's QMap). Reload
|
int index = m_Computers.indexOf(computer);
|
||||||
// the whole model state to ensure it stays consistent.
|
emit dataChanged(createIndex(index, 0), createIndex(index, 0));
|
||||||
beginResetModel();
|
|
||||||
m_Computers = m_ComputerManager->getComputers();
|
|
||||||
endResetModel();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+11
-1
@@ -2,6 +2,7 @@ import QtQuick 2.9
|
|||||||
import QtQuick.Controls 2.2
|
import QtQuick.Controls 2.2
|
||||||
import QtQuick.Layouts 1.3
|
import QtQuick.Layouts 1.3
|
||||||
import QtQuick.Window 2.2
|
import QtQuick.Window 2.2
|
||||||
|
import QtQuick.Controls.Material 2.2
|
||||||
|
|
||||||
import ComputerManager 1.0
|
import ComputerManager 1.0
|
||||||
import AutoUpdateChecker 1.0
|
import AutoUpdateChecker 1.0
|
||||||
@@ -22,6 +23,15 @@ ApplicationWindow {
|
|||||||
width: 1280
|
width: 1280
|
||||||
height: 600
|
height: 600
|
||||||
|
|
||||||
|
// Override the background color to Material 2 colors for Qt 6.5+
|
||||||
|
// in order to improve contrast between GFE's placeholder box art
|
||||||
|
// and the background of the app grid.
|
||||||
|
Component.onCompleted: {
|
||||||
|
if (SystemProperties.usesMaterial3Theme) {
|
||||||
|
Material.background = "#303030"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
visibility: {
|
visibility: {
|
||||||
if (SystemProperties.hasDesktopEnvironment) {
|
if (SystemProperties.hasDesktopEnvironment) {
|
||||||
if (StreamingPreferences.uiDisplayMode == StreamingPreferences.UI_WINDOWED) return "Windowed"
|
if (StreamingPreferences.uiDisplayMode == StreamingPreferences.UI_WINDOWED) return "Windowed"
|
||||||
@@ -488,7 +498,7 @@ ApplicationWindow {
|
|||||||
|
|
||||||
NavigableDialog {
|
NavigableDialog {
|
||||||
id: addPcDialog
|
id: addPcDialog
|
||||||
property string label: qsTr("Enter the IP address of your GameStream PC:")
|
property string label: qsTr("Enter the IP address of your host PC:")
|
||||||
|
|
||||||
standardButtons: Dialog.Ok | Dialog.Cancel
|
standardButtons: Dialog.Ok | Dialog.Cancel
|
||||||
|
|
||||||
|
|||||||
@@ -191,7 +191,17 @@ void SdlGamepadKeyNavigation::onPollingTimerFired()
|
|||||||
case SDL_CONTROLLERDEVICEADDED:
|
case SDL_CONTROLLERDEVICEADDED:
|
||||||
SDL_GameController* gc = SDL_GameControllerOpen(event.cdevice.which);
|
SDL_GameController* gc = SDL_GameControllerOpen(event.cdevice.which);
|
||||||
if (gc != nullptr) {
|
if (gc != nullptr) {
|
||||||
m_Gamepads.append(gc);
|
// SDL_CONTROLLERDEVICEADDED can be reported multiple times for the same
|
||||||
|
// gamepad in rare cases, because SDL doesn't fixup the device index in
|
||||||
|
// the SDL_CONTROLLERDEVICEADDED event if an unopened gamepad disappears
|
||||||
|
// before we've processed the add event.
|
||||||
|
if (!m_Gamepads.contains(gc)) {
|
||||||
|
m_Gamepads.append(gc);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// We already have this game controller open
|
||||||
|
SDL_GameControllerClose(gc);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -239,31 +249,6 @@ void SdlGamepadKeyNavigation::onPollingTimerFired()
|
|||||||
sendKey(QEvent::Type::KeyRelease, Qt::Key_Right);
|
sendKey(QEvent::Type::KeyRelease, Qt::Key_Right);
|
||||||
m_LastAxisNavigationEventTime = SDL_GetTicks();
|
m_LastAxisNavigationEventTime = SDL_GetTicks();
|
||||||
}
|
}
|
||||||
|
|
||||||
// In UI navigation mode (settings page), use the right stick to scroll
|
|
||||||
if (m_UiNavMode) {
|
|
||||||
short rightX = SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_RIGHTX);
|
|
||||||
short rightY = SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_RIGHTY);
|
|
||||||
|
|
||||||
QPoint wheelDelta;
|
|
||||||
if (rightX > 30000) {
|
|
||||||
wheelDelta.setX(30);
|
|
||||||
}
|
|
||||||
else if (rightX < -30000) {
|
|
||||||
wheelDelta.setX(-30);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rightY > 30000) {
|
|
||||||
wheelDelta.setY(-30);
|
|
||||||
}
|
|
||||||
else if (rightY < -30000) {
|
|
||||||
wheelDelta.setY(30);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!wheelDelta.isNull()) {
|
|
||||||
sendWheel(wheelDelta);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -277,22 +262,6 @@ void SdlGamepadKeyNavigation::sendKey(QEvent::Type type, Qt::Key key, Qt::Keyboa
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void SdlGamepadKeyNavigation::sendWheel(QPoint& angleDelta)
|
|
||||||
{
|
|
||||||
QGuiApplication* app = static_cast<QGuiApplication*>(QGuiApplication::instance());
|
|
||||||
QWindow* focusWindow = app->focusWindow();
|
|
||||||
if (focusWindow != nullptr) {
|
|
||||||
QPoint mousePos(focusWindow->width() / 2, focusWindow->height() / 2);
|
|
||||||
QPoint globalPos(focusWindow->mapToGlobal(mousePos));
|
|
||||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 12, 0)
|
|
||||||
QWheelEvent wheelEvent(mousePos, globalPos, QPoint(), angleDelta, Qt::NoButton, Qt::NoModifier, Qt::NoScrollPhase, false, Qt::MouseEventSynthesizedByApplication);
|
|
||||||
#else
|
|
||||||
QWheelEvent wheelEvent(mousePos, globalPos, QPoint(), angleDelta, angleDelta.y(), Qt::Vertical, Qt::NoButton, Qt::NoModifier, Qt::NoScrollPhase, Qt::MouseEventSynthesizedByApplication, false);
|
|
||||||
#endif
|
|
||||||
app->sendEvent(focusWindow, &wheelEvent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void SdlGamepadKeyNavigation::setUiNavMode(bool uiNavMode)
|
void SdlGamepadKeyNavigation::setUiNavMode(bool uiNavMode)
|
||||||
{
|
{
|
||||||
m_UiNavMode = uiNavMode;
|
m_UiNavMode = uiNavMode;
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ public:
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
void sendKey(QEvent::Type type, Qt::Key key, Qt::KeyboardModifiers modifiers = Qt::NoModifier);
|
void sendKey(QEvent::Type type, Qt::Key key, Qt::KeyboardModifiers modifiers = Qt::NoModifier);
|
||||||
void sendWheel(QPoint& angleDelta);
|
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void onPollingTimerFired();
|
void onPollingTimerFired();
|
||||||
|
|||||||
Binary file not shown.
+304
-304
File diff suppressed because it is too large.
Load diff
Binary file not shown.
+290
-290
File diff suppressed because it is too large.
Load diff
Binary file not shown.
+289
-293
File diff suppressed because it is too large.
Load diff
Binary file not shown.
+303
-307
File diff suppressed because it is too large.
Load diff
Binary file not shown.
+306
-345
File diff suppressed because it is too large.
Load diff
Binary file not shown.
File diff suppressed because it is too large.
Load diff
Binary file not shown.
+290
-286
File diff suppressed because it is too large.
Load diff
Binary file not shown.
+676
-648
File diff suppressed because it is too large.
Load diff
Binary file not shown.
+302
-306
File diff suppressed because it is too large.
Load diff
Binary file not shown.
+296
-296
File diff suppressed because it is too large.
Load diff
Binary file not shown.
+303
-303
File diff suppressed because it is too large.
Load diff
Binary file not shown.
+289
-293
File diff suppressed because it is too large.
Load diff
Binary file not shown.
+290
-294
File diff suppressed because it is too large.
Load diff
Binary file not shown.
+302
-302
File diff suppressed because it is too large.
Load diff
Binary file not shown.
+297
-301
File diff suppressed because it is too large.
Load diff
Binary file not shown.
+296
-288
File diff suppressed because it is too large.
Load diff
Binary file not shown.
+290
-294
File diff suppressed because it is too large.
Load diff
Binary file not shown.
+461
-441
File diff suppressed because it is too large.
Load diff
Binary file not shown.
+290
-286
File diff suppressed because it is too large.
Load diff
Binary file not shown.
+291
-267
File diff suppressed because it is too large.
Load diff
Binary file not shown.
+310
-290
File diff suppressed because it is too large.
Load diff
Binary file not shown.
+290
-294
File diff suppressed because it is too large.
Load diff
Binary file not shown.
File diff suppressed because it is too large.
Load diff
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large.
Load diff
Binary file not shown.
+39
-22
@@ -65,8 +65,9 @@ static QTextStream s_LoggerStream(stdout);
|
|||||||
static QMutex s_LoggerLock;
|
static QMutex s_LoggerLock;
|
||||||
static bool s_SuppressVerboseOutput;
|
static bool s_SuppressVerboseOutput;
|
||||||
#ifdef LOG_TO_FILE
|
#ifdef LOG_TO_FILE
|
||||||
#define MAX_LOG_LINES 10000
|
// Max log file size of 10 MB
|
||||||
static int s_LogLinesWritten = 0;
|
#define MAX_LOG_SIZE_BYTES (10 * 1024 * 1024)
|
||||||
|
static int s_LogBytesWritten = 0;
|
||||||
static bool s_LogLimitReached = false;
|
static bool s_LogLimitReached = false;
|
||||||
static QFile* s_LoggerFile;
|
static QFile* s_LoggerFile;
|
||||||
#endif
|
#endif
|
||||||
@@ -79,7 +80,7 @@ void logToLoggerStream(QString& message)
|
|||||||
if (s_LogLimitReached) {
|
if (s_LogLimitReached) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
else if (s_LogLinesWritten == MAX_LOG_LINES) {
|
else if (s_LogBytesWritten >= MAX_LOG_SIZE_BYTES) {
|
||||||
s_LoggerStream << "Log size limit reached!";
|
s_LoggerStream << "Log size limit reached!";
|
||||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
|
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
|
||||||
s_LoggerStream << Qt::endl;
|
s_LoggerStream << Qt::endl;
|
||||||
@@ -90,7 +91,7 @@ void logToLoggerStream(QString& message)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
s_LogLinesWritten++;
|
s_LogBytesWritten += message.size();
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -300,11 +301,16 @@ int main(int argc, char *argv[])
|
|||||||
Path::initialize(false);
|
Path::initialize(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Override the default QML cache directory with the one we chose
|
||||||
|
if (qEnvironmentVariableIsEmpty("QML_DISK_CACHE_PATH")) {
|
||||||
|
qputenv("QML_DISK_CACHE_PATH", Path::getQmlCacheDir().toUtf8());
|
||||||
|
}
|
||||||
|
|
||||||
#ifdef USE_CUSTOM_LOGGER
|
#ifdef USE_CUSTOM_LOGGER
|
||||||
#ifdef LOG_TO_FILE
|
#ifdef LOG_TO_FILE
|
||||||
QDir tempDir(Path::getLogDir());
|
QDir tempDir(Path::getLogDir());
|
||||||
s_LoggerFile = new QFile(tempDir.filePath(QString("Moonlight-%1.log").arg(QDateTime::currentSecsSinceEpoch())));
|
s_LoggerFile = new QFile(tempDir.filePath(QString("Moonlight-%1.log").arg(QDateTime::currentSecsSinceEpoch())));
|
||||||
if (s_LoggerFile->open(QIODevice::WriteOnly)) {
|
if (s_LoggerFile->open(QIODevice::WriteOnly | QIODevice::Text)) {
|
||||||
QTextStream(stderr) << "Redirecting log output to " << s_LoggerFile->fileName() << Qt::endl;
|
QTextStream(stderr) << "Redirecting log output to " << s_LoggerFile->fileName() << Qt::endl;
|
||||||
s_LoggerStream.setDevice(s_LoggerFile);
|
s_LoggerStream.setDevice(s_LoggerFile);
|
||||||
}
|
}
|
||||||
@@ -325,6 +331,15 @@ int main(int argc, char *argv[])
|
|||||||
SetUnhandledExceptionFilter(UnhandledExceptionHandler);
|
SetUnhandledExceptionFilter(UnhandledExceptionHandler);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifdef LOG_TO_FILE
|
||||||
|
// Prune the oldest existing logs if there are more than 10
|
||||||
|
QStringList existingLogNames = tempDir.entryList(QStringList("Moonlight-*.log"), QDir::NoFilter, QDir::SortFlag::Time);
|
||||||
|
for (int i = 10; i < existingLogNames.size(); i++) {
|
||||||
|
qInfo() << "Removing old log file:" << existingLogNames.at(i);
|
||||||
|
QFile(tempDir.filePath(existingLogNames.at(i))).remove();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
#if defined(Q_OS_WIN32)
|
#if defined(Q_OS_WIN32)
|
||||||
// Force AntiHooking.dll to be statically imported and loaded
|
// Force AntiHooking.dll to be statically imported and loaded
|
||||||
// by ntdll on Win32 platforms by calling a dummy function.
|
// by ntdll on Win32 platforms by calling a dummy function.
|
||||||
@@ -370,6 +385,11 @@ int main(int argc, char *argv[])
|
|||||||
qWarning() << "On the Raspberry Pi, you must enable the 'fake KMS' driver in raspi-config to use Moonlight outside of the GUI environment.";
|
qWarning() << "On the Raspberry Pi, you must enable the 'fake KMS' driver in raspi-config to use Moonlight outside of the GUI environment.";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EGLFS uses OpenGLES 2.0, so we will too. Some embedded platforms may not
|
||||||
|
// even have working OpenGL implementations, so GLES is the only option.
|
||||||
|
// See https://github.com/moonlight-stream/moonlight-qt/issues/868
|
||||||
|
SDL_SetHint(SDL_HINT_RENDER_DRIVER, "opengles2");
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -530,6 +550,13 @@ int main(int argc, char *argv[])
|
|||||||
StreamingPreferences prefs;
|
StreamingPreferences prefs;
|
||||||
prefs.retranslate();
|
prefs.retranslate();
|
||||||
|
|
||||||
|
// Trickily declare the translation for dialog buttons
|
||||||
|
QCoreApplication::translate("QPlatformTheme", "&Yes");
|
||||||
|
QCoreApplication::translate("QPlatformTheme", "&No");
|
||||||
|
QCoreApplication::translate("QPlatformTheme", "OK");
|
||||||
|
QCoreApplication::translate("QPlatformTheme", "Help");
|
||||||
|
QCoreApplication::translate("QPlatformTheme", "Cancel");
|
||||||
|
|
||||||
// After the QGuiApplication is created, the platform stuff will be initialized
|
// After the QGuiApplication is created, the platform stuff will be initialized
|
||||||
// and we can set the SDL video driver to match Qt.
|
// and we can set the SDL video driver to match Qt.
|
||||||
if (WMUtils::isRunningWayland() && QGuiApplication::platformName() == "xcb") {
|
if (WMUtils::isRunningWayland() && QGuiApplication::platformName() == "xcb") {
|
||||||
@@ -613,29 +640,19 @@ int main(int argc, char *argv[])
|
|||||||
// Create the identity manager on the main thread
|
// Create the identity manager on the main thread
|
||||||
IdentityManager::get();
|
IdentityManager::get();
|
||||||
|
|
||||||
#ifndef Q_OS_WINRT
|
// We require the Material theme
|
||||||
// Use the dense material dark theme by default
|
QQuickStyle::setStyle("Material");
|
||||||
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_STYLE")) {
|
|
||||||
qputenv("QT_QUICK_CONTROLS_STYLE", "Material");
|
// Our icons are styled for a dark theme, so we do not allow the user to override this
|
||||||
}
|
qputenv("QT_QUICK_CONTROLS_MATERIAL_THEME", "Dark");
|
||||||
#else
|
|
||||||
// Use universal dark on WinRT
|
// These are defaults that we allow the user to override
|
||||||
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_STYLE")) {
|
|
||||||
qputenv("QT_QUICK_CONTROLS_STYLE", "Universal");
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_MATERIAL_THEME")) {
|
|
||||||
qputenv("QT_QUICK_CONTROLS_MATERIAL_THEME", "Dark");
|
|
||||||
}
|
|
||||||
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_MATERIAL_ACCENT")) {
|
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_MATERIAL_ACCENT")) {
|
||||||
qputenv("QT_QUICK_CONTROLS_MATERIAL_ACCENT", "Purple");
|
qputenv("QT_QUICK_CONTROLS_MATERIAL_ACCENT", "Purple");
|
||||||
}
|
}
|
||||||
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_MATERIAL_VARIANT")) {
|
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_MATERIAL_VARIANT")) {
|
||||||
qputenv("QT_QUICK_CONTROLS_MATERIAL_VARIANT", "Dense");
|
qputenv("QT_QUICK_CONTROLS_MATERIAL_VARIANT", "Dense");
|
||||||
}
|
}
|
||||||
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_UNIVERSAL_THEME")) {
|
|
||||||
qputenv("QT_QUICK_CONTROLS_UNIVERSAL_THEME", "Dark");
|
|
||||||
}
|
|
||||||
|
|
||||||
QQmlApplicationEngine engine;
|
QQmlApplicationEngine engine;
|
||||||
QString initialView;
|
QString initialView;
|
||||||
|
|||||||
+3
-3
@@ -34,7 +34,7 @@
|
|||||||
|
|
||||||
// Qt's DRM master FD grabbed by our hook
|
// Qt's DRM master FD grabbed by our hook
|
||||||
int g_QtDrmMasterFd = -1;
|
int g_QtDrmMasterFd = -1;
|
||||||
struct stat64 g_DrmMasterStat;
|
struct stat g_DrmMasterStat;
|
||||||
|
|
||||||
// The DRM master FD created for SDL
|
// The DRM master FD created for SDL
|
||||||
int g_SdlDrmMasterFd = -1;
|
int g_SdlDrmMasterFd = -1;
|
||||||
@@ -55,7 +55,7 @@ int drmModeSetCrtc(int fd, uint32_t crtcId, uint32_t bufferId,
|
|||||||
// EGLFS backend's DRM FD, on which we will call drmDropMaster() later.
|
// EGLFS backend's DRM FD, on which we will call drmDropMaster() later.
|
||||||
if (g_QtDrmMasterFd == -1 && drmIsMaster(fd)) {
|
if (g_QtDrmMasterFd == -1 && drmIsMaster(fd)) {
|
||||||
g_QtDrmMasterFd = fd;
|
g_QtDrmMasterFd = fd;
|
||||||
fstat64(g_QtDrmMasterFd, &g_DrmMasterStat);
|
fstat(g_QtDrmMasterFd, &g_DrmMasterStat);
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"Captured Qt EGLFS DRM master fd (legacy): %d",
|
"Captured Qt EGLFS DRM master fd (legacy): %d",
|
||||||
g_QtDrmMasterFd);
|
g_QtDrmMasterFd);
|
||||||
@@ -73,7 +73,7 @@ int drmModeAtomicCommit(int fd, drmModeAtomicReqPtr req,
|
|||||||
// EGLFS backend's DRM FD, on which we will call drmDropMaster() later.
|
// EGLFS backend's DRM FD, on which we will call drmDropMaster() later.
|
||||||
if (g_QtDrmMasterFd == -1 && drmIsMaster(fd)) {
|
if (g_QtDrmMasterFd == -1 && drmIsMaster(fd)) {
|
||||||
g_QtDrmMasterFd = fd;
|
g_QtDrmMasterFd = fd;
|
||||||
fstat64(g_QtDrmMasterFd, &g_DrmMasterStat);
|
fstat(g_QtDrmMasterFd, &g_DrmMasterStat);
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"Captured Qt EGLFS DRM master fd (atomic): %d",
|
"Captured Qt EGLFS DRM master fd (atomic): %d",
|
||||||
g_QtDrmMasterFd);
|
g_QtDrmMasterFd);
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
#if SDL_VERSION_ATLEAST(2, 0, 15)
|
#if SDL_VERSION_ATLEAST(2, 0, 15)
|
||||||
|
|
||||||
extern int g_QtDrmMasterFd;
|
extern int g_QtDrmMasterFd;
|
||||||
extern struct stat64 g_DrmMasterStat;
|
extern struct stat g_DrmMasterStat;
|
||||||
extern int g_SdlDrmMasterFd;
|
extern int g_SdlDrmMasterFd;
|
||||||
|
|
||||||
int openHook(const char *funcname, const char *pathname, int flags, va_list va)
|
int openHook(const char *funcname, const char *pathname, int flags, va_list va)
|
||||||
@@ -50,9 +50,9 @@ int openHook(const char *funcname, const char *pathname, int flags, va_list va)
|
|||||||
if (fd >= 0 && g_QtDrmMasterFd != -1) {
|
if (fd >= 0 && g_QtDrmMasterFd != -1) {
|
||||||
if (strncmp(pathname, "/dev/dri/card", 13) == 0) {
|
if (strncmp(pathname, "/dev/dri/card", 13) == 0) {
|
||||||
// It's a DRM device, but is it _our_ DRM device?
|
// It's a DRM device, but is it _our_ DRM device?
|
||||||
struct stat64 fdstat;
|
struct stat fdstat;
|
||||||
|
|
||||||
fstat64(fd, &fdstat);
|
fstat(fd, &fdstat);
|
||||||
if (g_DrmMasterStat.st_dev == fdstat.st_dev &&
|
if (g_DrmMasterStat.st_dev == fdstat.st_dev &&
|
||||||
g_DrmMasterStat.st_ino == fdstat.st_ino) {
|
g_DrmMasterStat.st_ino == fdstat.st_ino) {
|
||||||
// It is our device. Time to do the magic!
|
// It is our device. Time to do the magic!
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
QString Path::s_CacheDir;
|
QString Path::s_CacheDir;
|
||||||
QString Path::s_LogDir;
|
QString Path::s_LogDir;
|
||||||
QString Path::s_BoxArtCacheDir;
|
QString Path::s_BoxArtCacheDir;
|
||||||
|
QString Path::s_QmlCacheDir;
|
||||||
|
|
||||||
QString Path::getLogDir()
|
QString Path::getLogDir()
|
||||||
{
|
{
|
||||||
@@ -22,6 +23,12 @@ QString Path::getBoxArtCacheDir()
|
|||||||
return s_BoxArtCacheDir;
|
return s_BoxArtCacheDir;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QString Path::getQmlCacheDir()
|
||||||
|
{
|
||||||
|
Q_ASSERT(!s_QmlCacheDir.isEmpty());
|
||||||
|
return s_QmlCacheDir;
|
||||||
|
}
|
||||||
|
|
||||||
QByteArray Path::readDataFile(QString fileName)
|
QByteArray Path::readDataFile(QString fileName)
|
||||||
{
|
{
|
||||||
QFile dataFile(getDataFilePath(fileName));
|
QFile dataFile(getDataFilePath(fileName));
|
||||||
@@ -97,6 +104,7 @@ void Path::initialize(bool portable)
|
|||||||
if (portable) {
|
if (portable) {
|
||||||
s_LogDir = QDir::currentPath();
|
s_LogDir = QDir::currentPath();
|
||||||
s_BoxArtCacheDir = QDir::currentPath() + "/boxart";
|
s_BoxArtCacheDir = QDir::currentPath() + "/boxart";
|
||||||
|
s_QmlCacheDir = QDir::currentPath() + "/qmlcache";
|
||||||
|
|
||||||
// In order for the If-Modified-Since logic to work in MappingFetcher,
|
// In order for the If-Modified-Since logic to work in MappingFetcher,
|
||||||
// the cache directory must be different than the current directory.
|
// the cache directory must be different than the current directory.
|
||||||
@@ -112,5 +120,6 @@ void Path::initialize(bool portable)
|
|||||||
#endif
|
#endif
|
||||||
s_CacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation);
|
s_CacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation);
|
||||||
s_BoxArtCacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/boxart";
|
s_BoxArtCacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/boxart";
|
||||||
|
s_QmlCacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/qmlcache";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+2
-1
@@ -7,8 +7,8 @@ class Path
|
|||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
static QString getLogDir();
|
static QString getLogDir();
|
||||||
|
|
||||||
static QString getBoxArtCacheDir();
|
static QString getBoxArtCacheDir();
|
||||||
|
static QString getQmlCacheDir();
|
||||||
|
|
||||||
static QByteArray readDataFile(QString fileName);
|
static QByteArray readDataFile(QString fileName);
|
||||||
static void writeCacheFile(QString fileName, QByteArray data);
|
static void writeCacheFile(QString fileName, QByteArray data);
|
||||||
@@ -24,4 +24,5 @@ private:
|
|||||||
static QString s_CacheDir;
|
static QString s_CacheDir;
|
||||||
static QString s_LogDir;
|
static QString s_LogDir;
|
||||||
static QString s_BoxArtCacheDir;
|
static QString s_BoxArtCacheDir;
|
||||||
|
static QString s_QmlCacheDir;
|
||||||
};
|
};
|
||||||
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="#fff"><path d="M12 2C6.47 2 2 6.47 2 12s4.47 10 10 10 10-4.47 10-10S17.53 2 12 2zm5 13.59L15.59 17 12 13.41 8.41 17 7 15.59 10.59 12 7 8.41 8.41 7 12 10.59 15.59 7 17 8.41 13.41 12 17 15.59z"/><path d="M0 0h24v24H0z" fill="none"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 325 B |
@@ -1,4 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="#fff">
|
|
||||||
<path d="M0 0h48v48H0z" fill="none"/>
|
|
||||||
<path d="M24 4C12.95 4 4 12.95 4 24s8.95 20 20 20 20-8.95 20-20S35.05 4 24 4zm-4 29V15l12 9-12 9z"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 250 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48" fill="#FFFFFF"><path d="M370-206q-26 14-49 1t-23-40v-477q0-26 23-39t49 1l372 239q23 14 23 38.5T742-445L370-206Z"/></svg>
|
||||||
|
After Width: | Height: | Size: 208 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48" fill="#FFFFFF"><path d="M309-218q-38.425 0-64.712-26.288Q218-270.575 218-309v-342q0-38.838 26.288-65.419Q270.575-743 309-743h342q38.838 0 65.419 26.581Q743-689.838 743-651v342q0 38.425-26.581 64.712Q689.838-218 651-218H309Z"/></svg>
|
||||||
|
After Width: | Height: | Size: 320 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24" fill="white"><path d="M108.018-98.001q-14.17 0-25.267-7.472-11.097-7.471-17.595-17.605-6.68-10.077-7.147-23-.468-12.923 7.085-26.165l372.43-640.512q7.553-13.242 18.399-18.665 10.846-5.424 24.077-5.424 13.231 0 24.077 5.424 10.846 5.423 18.399 18.665l372.43 640.512q7.553 13.242 7.085 26.165-.468 12.923-7.147 23-6.498 10.134-17.595 17.605-11.097 7.472-25.267 7.472H108.018ZM481-220.693q18.731 0 32.019-13.288 13.288-13.288 13.288-32.019 0-18.731-13.288-32.019-13.288-13.288-32.019-13.288-18.731 0-32.019 13.288-13.288 13.288-13.288 32.019 0 18.731 13.288 32.019 13.288 13.288 32.019 13.288Zm.013-127.614q17.756 0 30.371-12.625 12.615-12.625 12.615-30.374v-118.002q0-17.75-12.628-30.374-12.629-12.625-30.384-12.625-17.756 0-30.371 12.625-12.615 12.624-12.615 30.374v118.002q0 17.749 12.628 30.374 12.629 12.625 30.384 12.625Z"/></svg>
|
||||||
|
After Width: | Height: | Size: 921 B |
+9
-6
@@ -4,9 +4,10 @@
|
|||||||
<file>res/desktop_windows-48px.svg</file>
|
<file>res/desktop_windows-48px.svg</file>
|
||||||
<file>res/ic_add_to_queue_white_48px.svg</file>
|
<file>res/ic_add_to_queue_white_48px.svg</file>
|
||||||
<file>res/baseline-lock-24px.svg</file>
|
<file>res/baseline-lock-24px.svg</file>
|
||||||
<file>res/baseline-play_circle_filled_white-48px.svg</file>
|
<file>res/play_arrow_FILL1_wght700_GRAD200_opsz48.svg</file>
|
||||||
<file>res/baseline-warning-24px.svg</file>
|
<file>res/baseline-warning-24px.svg</file>
|
||||||
<file>res/baseline-cancel-24px.svg</file>
|
<file>res/warning_FILL1_wght300_GRAD200_opsz24.svg</file>
|
||||||
|
<file>res/stop_FILL1_wght700_GRAD200_opsz48.svg</file>
|
||||||
<file>res/no_app_image.png</file>
|
<file>res/no_app_image.png</file>
|
||||||
<file>res/settings.svg</file>
|
<file>res/settings.svg</file>
|
||||||
<file>res/arrow_left.svg</file>
|
<file>res/arrow_left.svg</file>
|
||||||
@@ -21,8 +22,8 @@
|
|||||||
<file>languages/qml_de.qm</file>
|
<file>languages/qml_de.qm</file>
|
||||||
<file>languages/qml_fr.ts</file>
|
<file>languages/qml_fr.ts</file>
|
||||||
<file>languages/qml_fr.qm</file>
|
<file>languages/qml_fr.qm</file>
|
||||||
<file>languages/qml_zh_cn.ts</file>
|
<file>languages/qml_zh_CN.ts</file>
|
||||||
<file>languages/qml_zh_cn.qm</file>
|
<file>languages/qml_zh_CN.qm</file>
|
||||||
<file>languages/qml_nb_NO.ts</file>
|
<file>languages/qml_nb_NO.ts</file>
|
||||||
<file>languages/qml_nb_NO.qm</file>
|
<file>languages/qml_nb_NO.qm</file>
|
||||||
<file>languages/qml_ru.ts</file>
|
<file>languages/qml_ru.ts</file>
|
||||||
@@ -47,8 +48,8 @@
|
|||||||
<file>languages/qml_tr.qm</file>
|
<file>languages/qml_tr.qm</file>
|
||||||
<file>languages/qml_uk.ts</file>
|
<file>languages/qml_uk.ts</file>
|
||||||
<file>languages/qml_uk.qm</file>
|
<file>languages/qml_uk.qm</file>
|
||||||
<file>languages/qml_zh_Hant.ts</file>
|
<file>languages/qml_zh_TW.ts</file>
|
||||||
<file>languages/qml_zh_Hant.qm</file>
|
<file>languages/qml_zh_TW.qm</file>
|
||||||
<file>languages/qml_el.ts</file>
|
<file>languages/qml_el.ts</file>
|
||||||
<file>languages/qml_el.qm</file>
|
<file>languages/qml_el.qm</file>
|
||||||
<file>languages/qml_hi.ts</file>
|
<file>languages/qml_hi.ts</file>
|
||||||
@@ -61,6 +62,8 @@
|
|||||||
<file>languages/qml_pl.qm</file>
|
<file>languages/qml_pl.qm</file>
|
||||||
<file>languages/qml_cs.ts</file>
|
<file>languages/qml_cs.ts</file>
|
||||||
<file>languages/qml_cs.qm</file>
|
<file>languages/qml_cs.qm</file>
|
||||||
|
<file>languages/qml_he.ts</file>
|
||||||
|
<file>languages/qml_he.qm</file>
|
||||||
<!-- Don't include pt_BR until it is more complete -->
|
<!-- Don't include pt_BR until it is more complete -->
|
||||||
<!--file>languages/qml_pt_BR.qm</file-->
|
<!--file>languages/qml_pt_BR.qm</file-->
|
||||||
<!--file>languages/qml_pt_BR.ts</file-->
|
<!--file>languages/qml_pt_BR.ts</file-->
|
||||||
|
|||||||
@@ -8,26 +8,32 @@
|
|||||||
#define COMPAT_KEY "latestsupportedversion-"
|
#define COMPAT_KEY "latestsupportedversion-"
|
||||||
|
|
||||||
CompatFetcher::CompatFetcher(QObject *parent) :
|
CompatFetcher::CompatFetcher(QObject *parent) :
|
||||||
QObject(parent),
|
QObject(parent)
|
||||||
m_Nam(this)
|
|
||||||
{
|
{
|
||||||
|
m_Nam = new QNetworkAccessManager(this);
|
||||||
|
|
||||||
// Never communicate over HTTP
|
// Never communicate over HTTP
|
||||||
m_Nam.setStrictTransportSecurityEnabled(true);
|
m_Nam->setStrictTransportSecurityEnabled(true);
|
||||||
|
|
||||||
// Allow HTTP redirects
|
// Allow HTTP redirects
|
||||||
m_Nam.setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy);
|
m_Nam->setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy);
|
||||||
|
|
||||||
connect(&m_Nam, &QNetworkAccessManager::finished,
|
connect(m_Nam, &QNetworkAccessManager::finished,
|
||||||
this, &CompatFetcher::handleCompatInfoFetched);
|
this, &CompatFetcher::handleCompatInfoFetched);
|
||||||
}
|
}
|
||||||
|
|
||||||
void CompatFetcher::start()
|
void CompatFetcher::start()
|
||||||
{
|
{
|
||||||
|
if (!m_Nam) {
|
||||||
|
Q_ASSERT(m_Nam);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) && QT_VERSION < QT_VERSION_CHECK(5, 15, 1) && !defined(QT_NO_BEARERMANAGEMENT)
|
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) && QT_VERSION < QT_VERSION_CHECK(5, 15, 1) && !defined(QT_NO_BEARERMANAGEMENT)
|
||||||
// HACK: Set network accessibility to work around QTBUG-80947 (introduced in Qt 5.14.0 and fixed in Qt 5.15.1)
|
// HACK: Set network accessibility to work around QTBUG-80947 (introduced in Qt 5.14.0 and fixed in Qt 5.15.1)
|
||||||
QT_WARNING_PUSH
|
QT_WARNING_PUSH
|
||||||
QT_WARNING_DISABLE_DEPRECATED
|
QT_WARNING_DISABLE_DEPRECATED
|
||||||
m_Nam.setNetworkAccessible(QNetworkAccessManager::Accessible);
|
m_Nam->setNetworkAccessible(QNetworkAccessManager::Accessible);
|
||||||
QT_WARNING_POP
|
QT_WARNING_POP
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -41,7 +47,7 @@ void CompatFetcher::start()
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
// We'll get a callback when this is finished
|
// We'll get a callback when this is finished
|
||||||
m_Nam.get(request);
|
m_Nam->get(request);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool CompatFetcher::isGfeVersionSupported(QString gfeVersion)
|
bool CompatFetcher::isGfeVersionSupported(QString gfeVersion)
|
||||||
@@ -119,6 +125,11 @@ void CompatFetcher::handleCompatInfoFetched(QNetworkReply* reply)
|
|||||||
{
|
{
|
||||||
Q_ASSERT(reply->isFinished());
|
Q_ASSERT(reply->isFinished());
|
||||||
|
|
||||||
|
// Delete the QNetworkAccessManager to free resources and
|
||||||
|
// prevent the bearer plugin from polling in the background.
|
||||||
|
m_Nam->deleteLater();
|
||||||
|
m_Nam = nullptr;
|
||||||
|
|
||||||
if (reply->error() == QNetworkReply::NoError) {
|
if (reply->error() == QNetworkReply::NoError) {
|
||||||
// Queue the reply for deletion
|
// Queue the reply for deletion
|
||||||
reply->deleteLater();
|
reply->deleteLater();
|
||||||
|
|||||||
@@ -18,5 +18,5 @@ private slots:
|
|||||||
void handleCompatInfoFetched(QNetworkReply* reply);
|
void handleCompatInfoFetched(QNetworkReply* reply);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QNetworkAccessManager m_Nam;
|
QNetworkAccessManager* m_Nam;
|
||||||
};
|
};
|
||||||
@@ -4,26 +4,32 @@
|
|||||||
#include <QNetworkReply>
|
#include <QNetworkReply>
|
||||||
|
|
||||||
MappingFetcher::MappingFetcher(QObject *parent) :
|
MappingFetcher::MappingFetcher(QObject *parent) :
|
||||||
QObject(parent),
|
QObject(parent)
|
||||||
m_Nam(this)
|
|
||||||
{
|
{
|
||||||
|
m_Nam = new QNetworkAccessManager(this);
|
||||||
|
|
||||||
// Never communicate over HTTP
|
// Never communicate over HTTP
|
||||||
m_Nam.setStrictTransportSecurityEnabled(true);
|
m_Nam->setStrictTransportSecurityEnabled(true);
|
||||||
|
|
||||||
// Allow HTTP redirects
|
// Allow HTTP redirects
|
||||||
m_Nam.setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy);
|
m_Nam->setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy);
|
||||||
|
|
||||||
connect(&m_Nam, &QNetworkAccessManager::finished,
|
connect(m_Nam, &QNetworkAccessManager::finished,
|
||||||
this, &MappingFetcher::handleMappingListFetched);
|
this, &MappingFetcher::handleMappingListFetched);
|
||||||
}
|
}
|
||||||
|
|
||||||
void MappingFetcher::start()
|
void MappingFetcher::start()
|
||||||
{
|
{
|
||||||
|
if (!m_Nam) {
|
||||||
|
Q_ASSERT(m_Nam);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) && QT_VERSION < QT_VERSION_CHECK(5, 15, 1) && !defined(QT_NO_BEARERMANAGEMENT)
|
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) && QT_VERSION < QT_VERSION_CHECK(5, 15, 1) && !defined(QT_NO_BEARERMANAGEMENT)
|
||||||
// HACK: Set network accessibility to work around QTBUG-80947 (introduced in Qt 5.14.0 and fixed in Qt 5.15.1)
|
// HACK: Set network accessibility to work around QTBUG-80947 (introduced in Qt 5.14.0 and fixed in Qt 5.15.1)
|
||||||
QT_WARNING_PUSH
|
QT_WARNING_PUSH
|
||||||
QT_WARNING_DISABLE_DEPRECATED
|
QT_WARNING_DISABLE_DEPRECATED
|
||||||
m_Nam.setNetworkAccessible(QNetworkAccessManager::Accessible);
|
m_Nam->setNetworkAccessible(QNetworkAccessManager::Accessible);
|
||||||
QT_WARNING_POP
|
QT_WARNING_POP
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -57,13 +63,18 @@ void MappingFetcher::start()
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
// We'll get a callback when this is finished
|
// We'll get a callback when this is finished
|
||||||
m_Nam.get(request);
|
m_Nam->get(request);
|
||||||
}
|
}
|
||||||
|
|
||||||
void MappingFetcher::handleMappingListFetched(QNetworkReply* reply)
|
void MappingFetcher::handleMappingListFetched(QNetworkReply* reply)
|
||||||
{
|
{
|
||||||
Q_ASSERT(reply->isFinished());
|
Q_ASSERT(reply->isFinished());
|
||||||
|
|
||||||
|
// Delete the QNetworkAccessManager to free resources and
|
||||||
|
// prevent the bearer plugin from polling in the background.
|
||||||
|
m_Nam->deleteLater();
|
||||||
|
m_Nam = nullptr;
|
||||||
|
|
||||||
if (reply->error() == QNetworkReply::NoError) {
|
if (reply->error() == QNetworkReply::NoError) {
|
||||||
// Queue the reply for deletion
|
// Queue the reply for deletion
|
||||||
reply->deleteLater();
|
reply->deleteLater();
|
||||||
|
|||||||
@@ -16,5 +16,5 @@ private slots:
|
|||||||
void handleMappingListFetched(QNetworkReply* reply);
|
void handleMappingListFetched(QNetworkReply* reply);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QNetworkAccessManager m_Nam;
|
QNetworkAccessManager* m_Nam;
|
||||||
};
|
};
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
#include <QTranslator>
|
#include <QTranslator>
|
||||||
#include <QCoreApplication>
|
#include <QCoreApplication>
|
||||||
#include <QLocale>
|
#include <QLocale>
|
||||||
|
#include <QtMath>
|
||||||
|
|
||||||
#include <QtDebug>
|
#include <QtDebug>
|
||||||
|
|
||||||
@@ -20,9 +21,9 @@
|
|||||||
#define SER_MULTICONT "multicontroller"
|
#define SER_MULTICONT "multicontroller"
|
||||||
#define SER_AUDIOCFG "audiocfg"
|
#define SER_AUDIOCFG "audiocfg"
|
||||||
#define SER_VIDEOCFG "videocfg"
|
#define SER_VIDEOCFG "videocfg"
|
||||||
|
#define SER_HDR "hdr"
|
||||||
#define SER_VIDEODEC "videodec"
|
#define SER_VIDEODEC "videodec"
|
||||||
#define SER_WINDOWMODE "windowmode"
|
#define SER_WINDOWMODE "windowmode"
|
||||||
#define SER_UNSUPPORTEDFPS "unsupportedfps"
|
|
||||||
#define SER_MDNS "mdns"
|
#define SER_MDNS "mdns"
|
||||||
#define SER_QUITAPPAFTER "quitAppAfter"
|
#define SER_QUITAPPAFTER "quitAppAfter"
|
||||||
#define SER_ABSMOUSEMODE "mouseacceleration"
|
#define SER_ABSMOUSEMODE "mouseacceleration"
|
||||||
@@ -87,7 +88,6 @@ void StreamingPreferences::reload()
|
|||||||
gameOptimizations = settings.value(SER_GAMEOPTS, true).toBool();
|
gameOptimizations = settings.value(SER_GAMEOPTS, true).toBool();
|
||||||
playAudioOnHost = settings.value(SER_HOSTAUDIO, false).toBool();
|
playAudioOnHost = settings.value(SER_HOSTAUDIO, false).toBool();
|
||||||
multiController = settings.value(SER_MULTICONT, true).toBool();
|
multiController = settings.value(SER_MULTICONT, true).toBool();
|
||||||
unsupportedFps = settings.value(SER_UNSUPPORTEDFPS, false).toBool();
|
|
||||||
enableMdns = settings.value(SER_MDNS, true).toBool();
|
enableMdns = settings.value(SER_MDNS, true).toBool();
|
||||||
quitAppAfter = settings.value(SER_QUITAPPAFTER, false).toBool();
|
quitAppAfter = settings.value(SER_QUITAPPAFTER, false).toBool();
|
||||||
absoluteMouseMode = settings.value(SER_ABSMOUSEMODE, false).toBool();
|
absoluteMouseMode = settings.value(SER_ABSMOUSEMODE, false).toBool();
|
||||||
@@ -104,6 +104,7 @@ void StreamingPreferences::reload()
|
|||||||
reverseScrollDirection = settings.value(SER_REVERSESCROLL, false).toBool();
|
reverseScrollDirection = settings.value(SER_REVERSESCROLL, false).toBool();
|
||||||
swapFaceButtons = settings.value(SER_SWAPFACEBUTTONS, false).toBool();
|
swapFaceButtons = settings.value(SER_SWAPFACEBUTTONS, false).toBool();
|
||||||
keepAwake = settings.value(SER_KEEPAWAKE, true).toBool();
|
keepAwake = settings.value(SER_KEEPAWAKE, true).toBool();
|
||||||
|
enableHdr = settings.value(SER_HDR, false).toBool();
|
||||||
captureSysKeysMode = static_cast<CaptureSysKeysMode>(settings.value(SER_CAPTURESYSKEYS,
|
captureSysKeysMode = static_cast<CaptureSysKeysMode>(settings.value(SER_CAPTURESYSKEYS,
|
||||||
static_cast<int>(CaptureSysKeysMode::CSK_OFF)).toInt());
|
static_cast<int>(CaptureSysKeysMode::CSK_OFF)).toInt());
|
||||||
audioConfig = static_cast<AudioConfig>(settings.value(SER_AUDIOCFG,
|
audioConfig = static_cast<AudioConfig>(settings.value(SER_AUDIOCFG,
|
||||||
@@ -137,6 +138,12 @@ void StreamingPreferences::reload()
|
|||||||
windowMode = WindowMode::WM_FULLSCREEN_DESKTOP;
|
windowMode = WindowMode::WM_FULLSCREEN_DESKTOP;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fixup VCC value to the new settings format with codec and HDR separate
|
||||||
|
if (videoCodecConfig == VCC_FORCE_HEVC_HDR_DEPRECATED) {
|
||||||
|
videoCodecConfig = VCC_AUTO;
|
||||||
|
enableHdr = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool StreamingPreferences::retranslate()
|
bool StreamingPreferences::retranslate()
|
||||||
@@ -163,13 +170,13 @@ bool StreamingPreferences::retranslate()
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (newTranslator->load(QString(":/languages/qml_") + languageSuffix)) {
|
if (newTranslator->load(QString(":/languages/qml_") + languageSuffix)) {
|
||||||
qInfo() << "Successfully loaded translation for " << languageSuffix;
|
qInfo() << "Successfully loaded translation for" << languageSuffix;
|
||||||
|
|
||||||
translator = newTranslator;
|
translator = newTranslator;
|
||||||
QCoreApplication::installTranslator(translator);
|
QCoreApplication::installTranslator(translator);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
qInfo() << "No translation available for " << languageSuffix;
|
qInfo() << "No translation available for" << languageSuffix;
|
||||||
delete newTranslator;
|
delete newTranslator;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,7 +209,7 @@ QString StreamingPreferences::getSuffixFromLanguage(StreamingPreferences::Langua
|
|||||||
case LANG_FR:
|
case LANG_FR:
|
||||||
return "fr";
|
return "fr";
|
||||||
case LANG_ZH_CN:
|
case LANG_ZH_CN:
|
||||||
return "zh_cn";
|
return "zh_CN";
|
||||||
case LANG_NB_NO:
|
case LANG_NB_NO:
|
||||||
return "nb_NO";
|
return "nb_NO";
|
||||||
case LANG_RU:
|
case LANG_RU:
|
||||||
@@ -227,8 +234,8 @@ QString StreamingPreferences::getSuffixFromLanguage(StreamingPreferences::Langua
|
|||||||
return "tr";
|
return "tr";
|
||||||
case LANG_UK:
|
case LANG_UK:
|
||||||
return "uk";
|
return "uk";
|
||||||
case LANG_ZH_HANT:
|
case LANG_ZH_TW:
|
||||||
return "zh_Hant";
|
return "zh_TW";
|
||||||
case LANG_PT:
|
case LANG_PT:
|
||||||
return "pt";
|
return "pt";
|
||||||
case LANG_PT_BR:
|
case LANG_PT_BR:
|
||||||
@@ -243,6 +250,8 @@ QString StreamingPreferences::getSuffixFromLanguage(StreamingPreferences::Langua
|
|||||||
return "pl";
|
return "pl";
|
||||||
case LANG_CS:
|
case LANG_CS:
|
||||||
return "cs";
|
return "cs";
|
||||||
|
case LANG_HE:
|
||||||
|
return "he";
|
||||||
case LANG_AUTO:
|
case LANG_AUTO:
|
||||||
default:
|
default:
|
||||||
return QLocale::system().name();
|
return QLocale::system().name();
|
||||||
@@ -261,7 +270,6 @@ void StreamingPreferences::save()
|
|||||||
settings.setValue(SER_GAMEOPTS, gameOptimizations);
|
settings.setValue(SER_GAMEOPTS, gameOptimizations);
|
||||||
settings.setValue(SER_HOSTAUDIO, playAudioOnHost);
|
settings.setValue(SER_HOSTAUDIO, playAudioOnHost);
|
||||||
settings.setValue(SER_MULTICONT, multiController);
|
settings.setValue(SER_MULTICONT, multiController);
|
||||||
settings.setValue(SER_UNSUPPORTEDFPS, unsupportedFps);
|
|
||||||
settings.setValue(SER_MDNS, enableMdns);
|
settings.setValue(SER_MDNS, enableMdns);
|
||||||
settings.setValue(SER_QUITAPPAFTER, quitAppAfter);
|
settings.setValue(SER_QUITAPPAFTER, quitAppAfter);
|
||||||
settings.setValue(SER_ABSMOUSEMODE, absoluteMouseMode);
|
settings.setValue(SER_ABSMOUSEMODE, absoluteMouseMode);
|
||||||
@@ -273,6 +281,7 @@ void StreamingPreferences::save()
|
|||||||
settings.setValue(SER_PACKETSIZE, packetSize);
|
settings.setValue(SER_PACKETSIZE, packetSize);
|
||||||
settings.setValue(SER_DETECTNETBLOCKING, detectNetworkBlocking);
|
settings.setValue(SER_DETECTNETBLOCKING, detectNetworkBlocking);
|
||||||
settings.setValue(SER_AUDIOCFG, static_cast<int>(audioConfig));
|
settings.setValue(SER_AUDIOCFG, static_cast<int>(audioConfig));
|
||||||
|
settings.setValue(SER_HDR, enableHdr);
|
||||||
settings.setValue(SER_VIDEOCFG, static_cast<int>(videoCodecConfig));
|
settings.setValue(SER_VIDEOCFG, static_cast<int>(videoCodecConfig));
|
||||||
settings.setValue(SER_VIDEODEC, static_cast<int>(videoDecoderSelection));
|
settings.setValue(SER_VIDEODEC, static_cast<int>(videoDecoderSelection));
|
||||||
settings.setValue(SER_WINDOWMODE, static_cast<int>(windowMode));
|
settings.setValue(SER_WINDOWMODE, static_cast<int>(windowMode));
|
||||||
@@ -290,28 +299,51 @@ void StreamingPreferences::save()
|
|||||||
|
|
||||||
int StreamingPreferences::getDefaultBitrate(int width, int height, int fps)
|
int StreamingPreferences::getDefaultBitrate(int width, int height, int fps)
|
||||||
{
|
{
|
||||||
// This table prefers 16:10 resolutions because they are
|
// Don't scale bitrate linearly beyond 60 FPS. It's definitely not a linear
|
||||||
// only slightly more pixels than the 16:9 equivalents, so
|
// bitrate increase for frame rate once we get to values that high.
|
||||||
// we don't want to bump those 16:10 resolutions up to the
|
float frameRateFactor = (fps <= 60 ? fps : (qSqrt(fps / 60.f) * 60.f)) / 30.f;
|
||||||
// next 16:9 slot.
|
|
||||||
|
|
||||||
if (width * height <= 640 * 360) {
|
// TODO: Collect some empirical data to see if these defaults make sense.
|
||||||
return static_cast<int>(1000 * (fps / 30.0));
|
// We're just using the values that the Shield used, as we have for years.
|
||||||
}
|
static const struct resTable {
|
||||||
else if (width * height <= 854 * 480) {
|
int pixels;
|
||||||
return static_cast<int>(1500 * (fps / 30.0));
|
int factor;
|
||||||
}
|
} resTable[] {
|
||||||
// This covers 1280x720 and 1280x800 too
|
{ 640 * 360, 1 },
|
||||||
else if (width * height <= 1366 * 768) {
|
{ 854 * 480, 2 },
|
||||||
return static_cast<int>(5000 * (fps / 30.0));
|
{ 1280 * 720, 5 },
|
||||||
}
|
{ 1920 * 1080, 10 },
|
||||||
else if (width * height <= 1920 * 1200) {
|
{ 2560 * 1440, 20 },
|
||||||
return static_cast<int>(10000 * (fps / 30.0));
|
{ 3840 * 2160, 40 },
|
||||||
}
|
{ -1, -1 },
|
||||||
else if (width * height <= 2560 * 1600) {
|
};
|
||||||
return static_cast<int>(20000 * (fps / 30.0));
|
|
||||||
}
|
// Calculate the resolution factor by linear interpolation of the resolution table
|
||||||
else /* if (width * height <= 3840 * 2160) */ {
|
float resolutionFactor;
|
||||||
return static_cast<int>(40000 * (fps / 30.0));
|
int pixels = width * height;
|
||||||
|
for (int i = 0;; i++) {
|
||||||
|
if (pixels == resTable[i].pixels) {
|
||||||
|
// We can bail immediately for exact matches
|
||||||
|
resolutionFactor = resTable[i].factor;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else if (pixels < resTable[i].pixels) {
|
||||||
|
if (i == 0) {
|
||||||
|
// Never go below the lowest resolution entry
|
||||||
|
resolutionFactor = resTable[i].factor;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Interpolate between the entry greater than the chosen resolution (i) and the entry less than the chosen resolution (i-1)
|
||||||
|
resolutionFactor = ((float)(pixels - resTable[i-1].pixels) / (resTable[i].pixels - resTable[i-1].pixels)) * (resTable[i].factor - resTable[i-1].factor) + resTable[i-1].factor;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else if (resTable[i].pixels == -1) {
|
||||||
|
// Never go above the highest resolution entry
|
||||||
|
resolutionFactor = resTable[i-1].factor;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return qRound(resolutionFactor * frameRateFactor) * 1000;
|
||||||
}
|
}
|
||||||
@@ -32,7 +32,8 @@ public:
|
|||||||
VCC_AUTO,
|
VCC_AUTO,
|
||||||
VCC_FORCE_H264,
|
VCC_FORCE_H264,
|
||||||
VCC_FORCE_HEVC,
|
VCC_FORCE_HEVC,
|
||||||
VCC_FORCE_HEVC_HDR
|
VCC_FORCE_HEVC_HDR_DEPRECATED, // Kept for backwards compatibility
|
||||||
|
VCC_FORCE_AV1
|
||||||
};
|
};
|
||||||
Q_ENUM(VideoCodecConfig)
|
Q_ENUM(VideoCodecConfig)
|
||||||
|
|
||||||
@@ -82,7 +83,7 @@ public:
|
|||||||
LANG_SV,
|
LANG_SV,
|
||||||
LANG_TR,
|
LANG_TR,
|
||||||
LANG_UK,
|
LANG_UK,
|
||||||
LANG_ZH_HANT,
|
LANG_ZH_TW,
|
||||||
LANG_PT,
|
LANG_PT,
|
||||||
LANG_PT_BR,
|
LANG_PT_BR,
|
||||||
LANG_EL,
|
LANG_EL,
|
||||||
@@ -90,6 +91,7 @@ public:
|
|||||||
LANG_HI,
|
LANG_HI,
|
||||||
LANG_PL,
|
LANG_PL,
|
||||||
LANG_CS,
|
LANG_CS,
|
||||||
|
LANG_HE,
|
||||||
};
|
};
|
||||||
Q_ENUM(Language);
|
Q_ENUM(Language);
|
||||||
|
|
||||||
@@ -109,7 +111,6 @@ public:
|
|||||||
Q_PROPERTY(bool gameOptimizations MEMBER gameOptimizations NOTIFY gameOptimizationsChanged)
|
Q_PROPERTY(bool gameOptimizations MEMBER gameOptimizations NOTIFY gameOptimizationsChanged)
|
||||||
Q_PROPERTY(bool playAudioOnHost MEMBER playAudioOnHost NOTIFY playAudioOnHostChanged)
|
Q_PROPERTY(bool playAudioOnHost MEMBER playAudioOnHost NOTIFY playAudioOnHostChanged)
|
||||||
Q_PROPERTY(bool multiController MEMBER multiController NOTIFY multiControllerChanged)
|
Q_PROPERTY(bool multiController MEMBER multiController NOTIFY multiControllerChanged)
|
||||||
Q_PROPERTY(bool unsupportedFps MEMBER unsupportedFps NOTIFY unsupportedFpsChanged)
|
|
||||||
Q_PROPERTY(bool enableMdns MEMBER enableMdns NOTIFY enableMdnsChanged)
|
Q_PROPERTY(bool enableMdns MEMBER enableMdns NOTIFY enableMdnsChanged)
|
||||||
Q_PROPERTY(bool quitAppAfter MEMBER quitAppAfter NOTIFY quitAppAfterChanged)
|
Q_PROPERTY(bool quitAppAfter MEMBER quitAppAfter NOTIFY quitAppAfterChanged)
|
||||||
Q_PROPERTY(bool absoluteMouseMode MEMBER absoluteMouseMode NOTIFY absoluteMouseModeChanged)
|
Q_PROPERTY(bool absoluteMouseMode MEMBER absoluteMouseMode NOTIFY absoluteMouseModeChanged)
|
||||||
@@ -118,9 +119,10 @@ public:
|
|||||||
Q_PROPERTY(bool connectionWarnings MEMBER connectionWarnings NOTIFY connectionWarningsChanged)
|
Q_PROPERTY(bool connectionWarnings MEMBER connectionWarnings NOTIFY connectionWarningsChanged)
|
||||||
Q_PROPERTY(bool richPresence MEMBER richPresence NOTIFY richPresenceChanged)
|
Q_PROPERTY(bool richPresence MEMBER richPresence NOTIFY richPresenceChanged)
|
||||||
Q_PROPERTY(bool gamepadMouse MEMBER gamepadMouse NOTIFY gamepadMouseChanged)
|
Q_PROPERTY(bool gamepadMouse MEMBER gamepadMouse NOTIFY gamepadMouseChanged)
|
||||||
Q_PROPERTY(bool detectNetworkBlocking MEMBER detectNetworkBlocking NOTIFY detectNetworkBlockingChanged);
|
Q_PROPERTY(bool detectNetworkBlocking MEMBER detectNetworkBlocking NOTIFY detectNetworkBlockingChanged)
|
||||||
Q_PROPERTY(AudioConfig audioConfig MEMBER audioConfig NOTIFY audioConfigChanged)
|
Q_PROPERTY(AudioConfig audioConfig MEMBER audioConfig NOTIFY audioConfigChanged)
|
||||||
Q_PROPERTY(VideoCodecConfig videoCodecConfig MEMBER videoCodecConfig NOTIFY videoCodecConfigChanged)
|
Q_PROPERTY(VideoCodecConfig videoCodecConfig MEMBER videoCodecConfig NOTIFY videoCodecConfigChanged)
|
||||||
|
Q_PROPERTY(bool enableHdr MEMBER enableHdr NOTIFY enableHdrChanged)
|
||||||
Q_PROPERTY(VideoDecoderSelection videoDecoderSelection MEMBER videoDecoderSelection NOTIFY videoDecoderSelectionChanged)
|
Q_PROPERTY(VideoDecoderSelection videoDecoderSelection MEMBER videoDecoderSelection NOTIFY videoDecoderSelectionChanged)
|
||||||
Q_PROPERTY(WindowMode windowMode MEMBER windowMode NOTIFY windowModeChanged)
|
Q_PROPERTY(WindowMode windowMode MEMBER windowMode NOTIFY windowModeChanged)
|
||||||
Q_PROPERTY(WindowMode recommendedFullScreenMode MEMBER recommendedFullScreenMode CONSTANT)
|
Q_PROPERTY(WindowMode recommendedFullScreenMode MEMBER recommendedFullScreenMode CONSTANT)
|
||||||
@@ -145,7 +147,6 @@ public:
|
|||||||
bool gameOptimizations;
|
bool gameOptimizations;
|
||||||
bool playAudioOnHost;
|
bool playAudioOnHost;
|
||||||
bool multiController;
|
bool multiController;
|
||||||
bool unsupportedFps;
|
|
||||||
bool enableMdns;
|
bool enableMdns;
|
||||||
bool quitAppAfter;
|
bool quitAppAfter;
|
||||||
bool absoluteMouseMode;
|
bool absoluteMouseMode;
|
||||||
@@ -164,6 +165,7 @@ public:
|
|||||||
int packetSize;
|
int packetSize;
|
||||||
AudioConfig audioConfig;
|
AudioConfig audioConfig;
|
||||||
VideoCodecConfig videoCodecConfig;
|
VideoCodecConfig videoCodecConfig;
|
||||||
|
bool enableHdr;
|
||||||
VideoDecoderSelection videoDecoderSelection;
|
VideoDecoderSelection videoDecoderSelection;
|
||||||
WindowMode windowMode;
|
WindowMode windowMode;
|
||||||
WindowMode recommendedFullScreenMode;
|
WindowMode recommendedFullScreenMode;
|
||||||
@@ -185,6 +187,7 @@ signals:
|
|||||||
void absoluteTouchModeChanged();
|
void absoluteTouchModeChanged();
|
||||||
void audioConfigChanged();
|
void audioConfigChanged();
|
||||||
void videoCodecConfigChanged();
|
void videoCodecConfigChanged();
|
||||||
|
void enableHdrChanged();
|
||||||
void videoDecoderSelectionChanged();
|
void videoDecoderSelectionChanged();
|
||||||
void uiDisplayModeChanged();
|
void uiDisplayModeChanged();
|
||||||
void windowModeChanged();
|
void windowModeChanged();
|
||||||
|
|||||||
@@ -28,8 +28,17 @@ bool SdlAudioRenderer::prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION*
|
|||||||
|
|
||||||
// On PulseAudio systems, setting a value too small can cause underruns for other
|
// On PulseAudio systems, setting a value too small can cause underruns for other
|
||||||
// applications sharing this output device. We impose a floor of 480 samples (10 ms)
|
// applications sharing this output device. We impose a floor of 480 samples (10 ms)
|
||||||
// to mitigate this issue.
|
// to mitigate this issue. Otherwise, we will buffer up to 3 frames of audio which
|
||||||
|
// is 15 ms at regular 5 ms frames and 30 ms at 10 ms frames for slow connections.
|
||||||
|
// The buffering helps avoid audio underruns due to network jitter.
|
||||||
|
#ifndef Q_OS_DARWIN
|
||||||
|
want.samples = SDL_max(480, opusConfig->samplesPerFrame * 3);
|
||||||
|
#else
|
||||||
|
// HACK: Changing the buffer size can lead to Bluetooth HFP
|
||||||
|
// audio issues on macOS, so we're leaving this alone.
|
||||||
|
// https://github.com/moonlight-stream/moonlight-qt/issues/1071
|
||||||
want.samples = SDL_max(480, opusConfig->samplesPerFrame);
|
want.samples = SDL_max(480, opusConfig->samplesPerFrame);
|
||||||
|
#endif
|
||||||
|
|
||||||
m_FrameSize = opusConfig->samplesPerFrame * sizeof(short) * opusConfig->channelCount;
|
m_FrameSize = opusConfig->samplesPerFrame * sizeof(short) * opusConfig->channelCount;
|
||||||
|
|
||||||
@@ -58,6 +67,10 @@ bool SdlAudioRenderer::prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION*
|
|||||||
have.samples,
|
have.samples,
|
||||||
have.size);
|
have.size);
|
||||||
|
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"SDL audio driver: %s",
|
||||||
|
SDL_GetCurrentAudioDriver());
|
||||||
|
|
||||||
// Start playback
|
// Start playback
|
||||||
SDL_PauseAudioDevice(m_AudioDevice, 0);
|
SDL_PauseAudioDevice(m_AudioDevice, 0);
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include <Limelight.h>
|
#include <Limelight.h>
|
||||||
#include <SDL.h>
|
#include <SDL.h>
|
||||||
|
#include <SDL_syswm.h>
|
||||||
#include "streaming/streamutils.h"
|
#include "streaming/streamutils.h"
|
||||||
|
|
||||||
#include <QtMath>
|
#include <QtMath>
|
||||||
@@ -27,7 +28,103 @@ Uint32 SdlInputHandler::longPressTimerCallback(Uint32, void*)
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SdlInputHandler::disableTouchFeedback()
|
||||||
|
{
|
||||||
|
SDL_SysWMinfo info;
|
||||||
|
|
||||||
|
SDL_VERSION(&info.version);
|
||||||
|
SDL_GetWindowWMInfo(m_Window, &info);
|
||||||
|
|
||||||
|
#ifdef Q_OS_WIN32
|
||||||
|
if (info.subsystem == SDL_SYSWM_WINDOWS) {
|
||||||
|
auto fnSetWindowFeedbackSetting = (decltype(SetWindowFeedbackSetting)*)GetProcAddress(GetModuleHandleW(L"user32.dll"), "SetWindowFeedbackSetting");
|
||||||
|
if (fnSetWindowFeedbackSetting) {
|
||||||
|
constexpr FEEDBACK_TYPE feedbackTypes[] = {
|
||||||
|
FEEDBACK_TOUCH_CONTACTVISUALIZATION,
|
||||||
|
FEEDBACK_PEN_BARRELVISUALIZATION,
|
||||||
|
FEEDBACK_PEN_TAP,
|
||||||
|
FEEDBACK_PEN_DOUBLETAP,
|
||||||
|
FEEDBACK_PEN_PRESSANDHOLD,
|
||||||
|
FEEDBACK_PEN_RIGHTTAP,
|
||||||
|
FEEDBACK_TOUCH_TAP,
|
||||||
|
FEEDBACK_TOUCH_DOUBLETAP,
|
||||||
|
FEEDBACK_TOUCH_PRESSANDHOLD,
|
||||||
|
FEEDBACK_TOUCH_RIGHTTAP,
|
||||||
|
FEEDBACK_GESTURE_PRESSANDTAP,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (FEEDBACK_TYPE ft : feedbackTypes) {
|
||||||
|
BOOL val = FALSE;
|
||||||
|
fnSetWindowFeedbackSetting(info.info.win.window, ft, 0, sizeof(val), &val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
void SdlInputHandler::handleAbsoluteFingerEvent(SDL_TouchFingerEvent* event)
|
void SdlInputHandler::handleAbsoluteFingerEvent(SDL_TouchFingerEvent* event)
|
||||||
|
{
|
||||||
|
SDL_Rect src, dst;
|
||||||
|
int windowWidth, windowHeight;
|
||||||
|
|
||||||
|
SDL_GetWindowSize(m_Window, &windowWidth, &windowHeight);
|
||||||
|
|
||||||
|
src.x = src.y = 0;
|
||||||
|
src.w = m_StreamWidth;
|
||||||
|
src.h = m_StreamHeight;
|
||||||
|
|
||||||
|
dst.x = dst.y = 0;
|
||||||
|
dst.w = windowWidth;
|
||||||
|
dst.h = windowHeight;
|
||||||
|
|
||||||
|
// Scale window-relative events to be video-relative and clamp to video region
|
||||||
|
StreamUtils::scaleSourceToDestinationSurface(&src, &dst);
|
||||||
|
float vidrelx = qMin(qMax((int)(event->x * windowWidth), dst.x), dst.x + dst.w) - dst.x;
|
||||||
|
float vidrely = qMin(qMax((int)(event->y * windowHeight), dst.y), dst.y + dst.h) - dst.y;
|
||||||
|
|
||||||
|
uint8_t eventType;
|
||||||
|
switch (event->type) {
|
||||||
|
case SDL_FINGERDOWN:
|
||||||
|
eventType = LI_TOUCH_EVENT_DOWN;
|
||||||
|
break;
|
||||||
|
case SDL_FINGERMOTION:
|
||||||
|
eventType = LI_TOUCH_EVENT_MOVE;
|
||||||
|
break;
|
||||||
|
case SDL_FINGERUP:
|
||||||
|
eventType = LI_TOUCH_EVENT_UP;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t pointerId;
|
||||||
|
|
||||||
|
// If the pointer ID is larger than we can fit, just CRC it and use that as the ID.
|
||||||
|
if ((uint64_t)event->fingerId > UINT32_MAX) {
|
||||||
|
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||||
|
QByteArrayView bav((char*)&event->fingerId, sizeof(event->fingerId));
|
||||||
|
pointerId = qChecksum(bav);
|
||||||
|
#else
|
||||||
|
pointerId = qChecksum((char*)&event->fingerId, sizeof(event->fingerId));
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
pointerId = (uint32_t)event->fingerId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to send it as a native touch event, otherwise fall back to our touch emulation
|
||||||
|
if (LiSendTouchEvent(eventType, pointerId, vidrelx / dst.w, vidrely / dst.h, event->pressure,
|
||||||
|
0.0f, 0.0f, LI_ROT_UNKNOWN) == LI_ERR_UNSUPPORTED) {
|
||||||
|
emulateAbsoluteFingerEvent(event);
|
||||||
|
}
|
||||||
|
else if (!m_DisabledTouchFeedback) {
|
||||||
|
// Disable touch feedback when passing touch natively
|
||||||
|
disableTouchFeedback();
|
||||||
|
m_DisabledTouchFeedback = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SdlInputHandler::emulateAbsoluteFingerEvent(SDL_TouchFingerEvent* event)
|
||||||
{
|
{
|
||||||
// Observations on Windows 10: x and y appear to be relative to 0,0 of the window client area.
|
// Observations on Windows 10: x and y appear to be relative to 0,0 of the window client area.
|
||||||
// Although SDL documentation states they are 0.0 - 1.0 float values, they can actually be higher
|
// Although SDL documentation states they are 0.0 - 1.0 float values, they can actually be higher
|
||||||
|
|||||||
+338
-14
@@ -19,15 +19,19 @@
|
|||||||
#define MOUSE_EMULATION_DEADZONE 2
|
#define MOUSE_EMULATION_DEADZONE 2
|
||||||
|
|
||||||
// Haptic capabilities (in addition to those from SDL_HapticQuery())
|
// Haptic capabilities (in addition to those from SDL_HapticQuery())
|
||||||
#define ML_HAPTIC_GC_RUMBLE (1U << 16)
|
#define ML_HAPTIC_GC_RUMBLE (1U << 16)
|
||||||
#define ML_HAPTIC_SIMPLE_RUMBLE (1U << 17)
|
#define ML_HAPTIC_SIMPLE_RUMBLE (1U << 17)
|
||||||
|
#define ML_HAPTIC_GC_TRIGGER_RUMBLE (1U << 18)
|
||||||
|
|
||||||
const int SdlInputHandler::k_ButtonMap[] = {
|
const int SdlInputHandler::k_ButtonMap[] = {
|
||||||
A_FLAG, B_FLAG, X_FLAG, Y_FLAG,
|
A_FLAG, B_FLAG, X_FLAG, Y_FLAG,
|
||||||
BACK_FLAG, SPECIAL_FLAG, PLAY_FLAG,
|
BACK_FLAG, SPECIAL_FLAG, PLAY_FLAG,
|
||||||
LS_CLK_FLAG, RS_CLK_FLAG,
|
LS_CLK_FLAG, RS_CLK_FLAG,
|
||||||
LB_FLAG, RB_FLAG,
|
LB_FLAG, RB_FLAG,
|
||||||
UP_FLAG, DOWN_FLAG, LEFT_FLAG, RIGHT_FLAG
|
UP_FLAG, DOWN_FLAG, LEFT_FLAG, RIGHT_FLAG,
|
||||||
|
MISC_FLAG,
|
||||||
|
PADDLE1_FLAG, PADDLE2_FLAG, PADDLE3_FLAG, PADDLE4_FLAG,
|
||||||
|
TOUCHPAD_FLAG,
|
||||||
};
|
};
|
||||||
|
|
||||||
GamepadState*
|
GamepadState*
|
||||||
@@ -42,17 +46,31 @@ SdlInputHandler::findStateForGamepad(SDL_JoystickID id)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// This should only happen with > 4 gamepads
|
// We can get a spurious removal event if the device is removed
|
||||||
SDL_assert(SDL_NumJoysticks() > 4);
|
// before or during SDL_GameControllerOpen(). This is fine to ignore.
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
void SdlInputHandler::sendGamepadState(GamepadState* state)
|
void SdlInputHandler::sendGamepadState(GamepadState* state)
|
||||||
{
|
{
|
||||||
SDL_assert(m_GamepadMask == 0x1 || m_MultiController);
|
SDL_assert(m_GamepadMask == 0x1 || m_MultiController);
|
||||||
|
|
||||||
|
// Handle Select+PS as the clickpad button on PS4/5 controllers without a clickpad mapping
|
||||||
|
int buttons = state->buttons;
|
||||||
|
if (state->clickpadButtonEmulationEnabled) {
|
||||||
|
if (state->buttons == (BACK_FLAG | SPECIAL_FLAG)) {
|
||||||
|
buttons = MISC_FLAG;
|
||||||
|
state->emulatedClickpadButtonDown = true;
|
||||||
|
}
|
||||||
|
else if (state->emulatedClickpadButtonDown) {
|
||||||
|
buttons &= ~MISC_FLAG;
|
||||||
|
state->emulatedClickpadButtonDown = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
LiSendMultiControllerEvent(state->index,
|
LiSendMultiControllerEvent(state->index,
|
||||||
m_GamepadMask,
|
m_GamepadMask,
|
||||||
state->buttons,
|
buttons,
|
||||||
state->lt,
|
state->lt,
|
||||||
state->rt,
|
state->rt,
|
||||||
state->lsX,
|
state->lsX,
|
||||||
@@ -61,6 +79,47 @@ void SdlInputHandler::sendGamepadState(GamepadState* state)
|
|||||||
state->rsY);
|
state->rsY);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SdlInputHandler::sendGamepadBatteryState(GamepadState* state, SDL_JoystickPowerLevel level)
|
||||||
|
{
|
||||||
|
uint8_t batteryPercentage;
|
||||||
|
uint8_t batteryState;
|
||||||
|
|
||||||
|
// SDL's battery reporting capabilities are quite limited. Notably, we cannot
|
||||||
|
// tell the battery level while charging (or even if a battery is present).
|
||||||
|
// We also cannot tell the percentage of charge exactly in any case.
|
||||||
|
switch (level)
|
||||||
|
{
|
||||||
|
case SDL_JOYSTICK_POWER_UNKNOWN:
|
||||||
|
batteryState = LI_BATTERY_STATE_UNKNOWN;
|
||||||
|
batteryPercentage = LI_BATTERY_PERCENTAGE_UNKNOWN;
|
||||||
|
break;
|
||||||
|
case SDL_JOYSTICK_POWER_WIRED:
|
||||||
|
batteryState = LI_BATTERY_STATE_CHARGING;
|
||||||
|
batteryPercentage = LI_BATTERY_PERCENTAGE_UNKNOWN;
|
||||||
|
break;
|
||||||
|
case SDL_JOYSTICK_POWER_EMPTY:
|
||||||
|
batteryState = LI_BATTERY_STATE_DISCHARGING;
|
||||||
|
batteryPercentage = 5;
|
||||||
|
break;
|
||||||
|
case SDL_JOYSTICK_POWER_LOW:
|
||||||
|
batteryState = LI_BATTERY_STATE_DISCHARGING;
|
||||||
|
batteryPercentage = 20;
|
||||||
|
break;
|
||||||
|
case SDL_JOYSTICK_POWER_MEDIUM:
|
||||||
|
batteryState = LI_BATTERY_STATE_DISCHARGING;
|
||||||
|
batteryPercentage = 50;
|
||||||
|
break;
|
||||||
|
case SDL_JOYSTICK_POWER_FULL:
|
||||||
|
batteryState = LI_BATTERY_STATE_DISCHARGING;
|
||||||
|
batteryPercentage = 90;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LiSendControllerBatteryEvent(state->index, batteryState, batteryPercentage);
|
||||||
|
}
|
||||||
|
|
||||||
Uint32 SdlInputHandler::mouseEmulationTimerCallback(Uint32 interval, void *param)
|
Uint32 SdlInputHandler::mouseEmulationTimerCallback(Uint32 interval, void *param)
|
||||||
{
|
{
|
||||||
auto gamepad = reinterpret_cast<GamepadState*>(param);
|
auto gamepad = reinterpret_cast<GamepadState*>(param);
|
||||||
@@ -162,6 +221,13 @@ void SdlInputHandler::handleControllerAxisEvent(SDL_ControllerAxisEvent* event)
|
|||||||
|
|
||||||
void SdlInputHandler::handleControllerButtonEvent(SDL_ControllerButtonEvent* event)
|
void SdlInputHandler::handleControllerButtonEvent(SDL_ControllerButtonEvent* event)
|
||||||
{
|
{
|
||||||
|
if (event->button >= SDL_arraysize(k_ButtonMap)) {
|
||||||
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"No mapping for gamepad button: %u",
|
||||||
|
event->button);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
GamepadState* state = findStateForGamepad(event->which);
|
GamepadState* state = findStateForGamepad(event->which);
|
||||||
if (state == NULL) {
|
if (state == NULL) {
|
||||||
return;
|
return;
|
||||||
@@ -212,6 +278,12 @@ void SdlInputHandler::handleControllerButtonEvent(SDL_ControllerButtonEvent* eve
|
|||||||
else if (event->button == SDL_CONTROLLER_BUTTON_DPAD_DOWN) {
|
else if (event->button == SDL_CONTROLLER_BUTTON_DPAD_DOWN) {
|
||||||
LiSendScrollEvent(-1);
|
LiSendScrollEvent(-1);
|
||||||
}
|
}
|
||||||
|
else if (event->button == SDL_CONTROLLER_BUTTON_DPAD_RIGHT) {
|
||||||
|
LiSendHScrollEvent(1);
|
||||||
|
}
|
||||||
|
else if (event->button == SDL_CONTROLLER_BUTTON_DPAD_LEFT) {
|
||||||
|
LiSendHScrollEvent(-1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -296,6 +368,84 @@ void SdlInputHandler::handleControllerButtonEvent(SDL_ControllerButtonEvent* eve
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if SDL_VERSION_ATLEAST(2, 0, 14)
|
||||||
|
|
||||||
|
void SdlInputHandler::handleControllerSensorEvent(SDL_ControllerSensorEvent* event)
|
||||||
|
{
|
||||||
|
GamepadState* state = findStateForGamepad(event->which);
|
||||||
|
if (state == NULL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (event->sensor) {
|
||||||
|
case SDL_SENSOR_ACCEL:
|
||||||
|
if (state->accelReportPeriodMs &&
|
||||||
|
SDL_TICKS_PASSED(event->timestamp, state->lastAccelEventTime + state->accelReportPeriodMs) &&
|
||||||
|
memcmp(event->data, state->lastAccelEventData, sizeof(event->data)) != 0) {
|
||||||
|
memcpy(state->lastAccelEventData, event->data, sizeof(event->data));
|
||||||
|
state->lastAccelEventTime = event->timestamp;
|
||||||
|
|
||||||
|
LiSendControllerMotionEvent((uint8_t)state->index, LI_MOTION_TYPE_ACCEL, event->data[0], event->data[1], event->data[2]);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case SDL_SENSOR_GYRO:
|
||||||
|
if (state->gyroReportPeriodMs &&
|
||||||
|
SDL_TICKS_PASSED(event->timestamp, state->lastGyroEventTime + state->gyroReportPeriodMs) &&
|
||||||
|
memcmp(event->data, state->lastGyroEventData, sizeof(event->data)) != 0) {
|
||||||
|
memcpy(state->lastGyroEventData, event->data, sizeof(event->data));
|
||||||
|
state->lastGyroEventTime = event->timestamp;
|
||||||
|
|
||||||
|
// Convert rad/s to deg/s
|
||||||
|
LiSendControllerMotionEvent((uint8_t)state->index, LI_MOTION_TYPE_GYRO,
|
||||||
|
event->data[0] * 57.2957795f,
|
||||||
|
event->data[1] * 57.2957795f,
|
||||||
|
event->data[2] * 57.2957795f);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SdlInputHandler::handleControllerTouchpadEvent(SDL_ControllerTouchpadEvent* event)
|
||||||
|
{
|
||||||
|
GamepadState* state = findStateForGamepad(event->which);
|
||||||
|
if (state == NULL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t eventType;
|
||||||
|
switch (event->type) {
|
||||||
|
case SDL_CONTROLLERTOUCHPADDOWN:
|
||||||
|
eventType = LI_TOUCH_EVENT_DOWN;
|
||||||
|
break;
|
||||||
|
case SDL_CONTROLLERTOUCHPADUP:
|
||||||
|
eventType = LI_TOUCH_EVENT_UP;
|
||||||
|
break;
|
||||||
|
case SDL_CONTROLLERTOUCHPADMOTION:
|
||||||
|
eventType = LI_TOUCH_EVENT_MOVE;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LiSendControllerTouchEvent((uint8_t)state->index, eventType, event->finger, event->x, event->y, event->pressure);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if SDL_VERSION_ATLEAST(2, 24, 0)
|
||||||
|
|
||||||
|
void SdlInputHandler::handleJoystickBatteryEvent(SDL_JoyBatteryEvent* event)
|
||||||
|
{
|
||||||
|
GamepadState* state = findStateForGamepad(event->which);
|
||||||
|
if (state == NULL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sendGamepadBatteryState(state, event->level);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
void SdlInputHandler::handleControllerDeviceEvent(SDL_ControllerDeviceEvent* event)
|
void SdlInputHandler::handleControllerDeviceEvent(SDL_ControllerDeviceEvent* event)
|
||||||
{
|
{
|
||||||
GamepadState* state;
|
GamepadState* state;
|
||||||
@@ -316,6 +466,20 @@ void SdlInputHandler::handleControllerDeviceEvent(SDL_ControllerDeviceEvent* eve
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SDL_CONTROLLERDEVICEADDED can be reported multiple times for the same
|
||||||
|
// gamepad in rare cases, because SDL doesn't fixup the device index in
|
||||||
|
// the SDL_CONTROLLERDEVICEADDED event if an unopened gamepad disappears
|
||||||
|
// before we've processed the add event.
|
||||||
|
for (int i = 0; i < MAX_GAMEPADS; i++) {
|
||||||
|
if (m_GamepadState[i].controller == controller) {
|
||||||
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Received duplicate add event for controller index: %d",
|
||||||
|
event->which);
|
||||||
|
SDL_GameControllerClose(controller);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// We used to use SDL_GameControllerGetPlayerIndex() here but that
|
// We used to use SDL_GameControllerGetPlayerIndex() here but that
|
||||||
// can lead to strange issues due to bugs in Windows where an Xbox
|
// can lead to strange issues due to bugs in Windows where an Xbox
|
||||||
// controller will join as player 2, even though no player 1 controller
|
// controller will join as player 2, even though no player 1 controller
|
||||||
@@ -338,6 +502,17 @@ void SdlInputHandler::handleControllerDeviceEvent(SDL_ControllerDeviceEvent* eve
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SDL_JoystickGetGUIDString(SDL_JoystickGetGUID(SDL_GameControllerGetJoystick(controller)),
|
||||||
|
guidStr, sizeof(guidStr));
|
||||||
|
if (m_IgnoreDeviceGuids.contains(guidStr, Qt::CaseInsensitive))
|
||||||
|
{
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Skipping ignored device with GUID: %s",
|
||||||
|
guidStr);
|
||||||
|
SDL_GameControllerClose(controller);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
state = &m_GamepadState[i];
|
state = &m_GamepadState[i];
|
||||||
if (m_MultiController) {
|
if (m_MultiController) {
|
||||||
state->index = i;
|
state->index = i;
|
||||||
@@ -357,14 +532,18 @@ void SdlInputHandler::handleControllerDeviceEvent(SDL_ControllerDeviceEvent* eve
|
|||||||
state->controller = controller;
|
state->controller = controller;
|
||||||
state->jsId = SDL_JoystickInstanceID(SDL_GameControllerGetJoystick(state->controller));
|
state->jsId = SDL_JoystickInstanceID(SDL_GameControllerGetJoystick(state->controller));
|
||||||
|
|
||||||
|
hapticCaps = 0;
|
||||||
#if SDL_VERSION_ATLEAST(2, 0, 18)
|
#if SDL_VERSION_ATLEAST(2, 0, 18)
|
||||||
hapticCaps = SDL_GameControllerHasRumble(controller) ? ML_HAPTIC_GC_RUMBLE : 0;
|
hapticCaps |= SDL_GameControllerHasRumble(controller) ? ML_HAPTIC_GC_RUMBLE : 0;
|
||||||
|
hapticCaps |= SDL_GameControllerHasRumbleTriggers(controller) ? ML_HAPTIC_GC_TRIGGER_RUMBLE : 0;
|
||||||
#elif SDL_VERSION_ATLEAST(2, 0, 9)
|
#elif SDL_VERSION_ATLEAST(2, 0, 9)
|
||||||
// Perform a tiny rumble to see if haptics are supported.
|
// Perform a tiny rumbles to see if haptics are supported.
|
||||||
// NB: We cannot use zeros for rumble intensity or SDL will not actually call the JS driver
|
// NB: We cannot use zeros for rumble intensity or SDL will not actually call the JS driver
|
||||||
// and we'll get a (potentially false) success value returned.
|
// and we'll get a (potentially false) success value returned.
|
||||||
hapticCaps = SDL_GameControllerRumble(controller, 1, 1, 1) == 0 ?
|
hapticCaps |= SDL_GameControllerRumble(controller, 1, 1, 1) == 0 ? ML_HAPTIC_GC_RUMBLE : 0;
|
||||||
ML_HAPTIC_GC_RUMBLE : 0;
|
#if SDL_VERSION_ATLEAST(2, 0, 14)
|
||||||
|
hapticCaps |= SDL_GameControllerRumbleTriggers(controller, 1, 1, 1) == 0 ? ML_HAPTIC_GC_TRIGGER_RUMBLE : 0;
|
||||||
|
#endif
|
||||||
#else
|
#else
|
||||||
state->haptic = SDL_HapticOpenFromJoystick(SDL_GameControllerGetJoystick(state->controller));
|
state->haptic = SDL_HapticOpenFromJoystick(SDL_GameControllerGetJoystick(state->controller));
|
||||||
state->hapticEffectId = -1;
|
state->hapticEffectId = -1;
|
||||||
@@ -394,15 +573,18 @@ void SdlInputHandler::handleControllerDeviceEvent(SDL_ControllerDeviceEvent* eve
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
SDL_JoystickGetGUIDString(SDL_JoystickGetGUID(SDL_GameControllerGetJoystick(state->controller)),
|
|
||||||
guidStr, sizeof(guidStr));
|
|
||||||
mapping = SDL_GameControllerMapping(state->controller);
|
mapping = SDL_GameControllerMapping(state->controller);
|
||||||
name = SDL_GameControllerName(state->controller);
|
name = SDL_GameControllerName(state->controller);
|
||||||
|
|
||||||
|
uint16_t vendorId = SDL_GameControllerGetVendor(state->controller);
|
||||||
|
uint16_t productId = SDL_GameControllerGetProduct(state->controller);
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"Gamepad %d (player %d) is: %s (haptic capabilities: 0x%x) (mapping: %s -> %s)",
|
"Gamepad %d (player %d) is: %s (VID/PID: 0x%.4x/0x%.4x) (haptic capabilities: 0x%x) (mapping: %s -> %s)",
|
||||||
i,
|
i,
|
||||||
state->index,
|
state->index,
|
||||||
name != nullptr ? name : "<null>",
|
name != nullptr ? name : "<null>",
|
||||||
|
vendorId,
|
||||||
|
productId,
|
||||||
hapticCaps,
|
hapticCaps,
|
||||||
guidStr,
|
guidStr,
|
||||||
mapping != nullptr ? mapping : "<null>");
|
mapping != nullptr ? mapping : "<null>");
|
||||||
@@ -421,8 +603,89 @@ void SdlInputHandler::handleControllerDeviceEvent(SDL_ControllerDeviceEvent* eve
|
|||||||
SDL_assert(m_GamepadMask == 0x1);
|
SDL_assert(m_GamepadMask == 0x1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SDL_JoystickPowerLevel powerLevel = SDL_JoystickCurrentPowerLevel(SDL_GameControllerGetJoystick(state->controller));
|
||||||
|
|
||||||
|
#if SDL_VERSION_ATLEAST(2, 0, 14)
|
||||||
|
// On SDL 2.0.14 and later, we can provide enhanced controller information to the host PC
|
||||||
|
// for it to use as a hint for the type of controller to emulate.
|
||||||
|
uint32_t supportedButtonFlags = 0;
|
||||||
|
for (int i = 0; i < (int)SDL_arraysize(k_ButtonMap); i++) {
|
||||||
|
if (SDL_GameControllerHasButton(state->controller, (SDL_GameControllerButton)i)) {
|
||||||
|
supportedButtonFlags |= k_ButtonMap[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t capabilities = 0;
|
||||||
|
if (SDL_GameControllerGetBindForAxis(state->controller, SDL_CONTROLLER_AXIS_TRIGGERLEFT).bindType == SDL_CONTROLLER_BINDTYPE_AXIS ||
|
||||||
|
SDL_GameControllerGetBindForAxis(state->controller, SDL_CONTROLLER_AXIS_TRIGGERRIGHT).bindType == SDL_CONTROLLER_BINDTYPE_AXIS) {
|
||||||
|
// We assume these are analog triggers if the binding is to an axis rather than a button
|
||||||
|
capabilities |= LI_CCAP_ANALOG_TRIGGERS;
|
||||||
|
}
|
||||||
|
if (hapticCaps & ML_HAPTIC_GC_RUMBLE) {
|
||||||
|
capabilities |= LI_CCAP_RUMBLE;
|
||||||
|
}
|
||||||
|
if (hapticCaps & ML_HAPTIC_GC_TRIGGER_RUMBLE) {
|
||||||
|
capabilities |= LI_CCAP_TRIGGER_RUMBLE;
|
||||||
|
}
|
||||||
|
if (SDL_GameControllerGetNumTouchpads(state->controller) > 0) {
|
||||||
|
capabilities |= LI_CCAP_TOUCHPAD;
|
||||||
|
}
|
||||||
|
if (SDL_GameControllerHasSensor(state->controller, SDL_SENSOR_ACCEL)) {
|
||||||
|
capabilities |= LI_CCAP_ACCEL;
|
||||||
|
}
|
||||||
|
if (SDL_GameControllerHasSensor(state->controller, SDL_SENSOR_GYRO)) {
|
||||||
|
capabilities |= LI_CCAP_GYRO;
|
||||||
|
}
|
||||||
|
if (powerLevel != SDL_JOYSTICK_POWER_UNKNOWN || SDL_VERSION_ATLEAST(2, 24, 0)) {
|
||||||
|
capabilities |= LI_CCAP_BATTERY_STATE;
|
||||||
|
}
|
||||||
|
if (SDL_GameControllerHasLED(state->controller)) {
|
||||||
|
capabilities |= LI_CCAP_RGB_LED;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t type;
|
||||||
|
switch (SDL_GameControllerGetType(state->controller)) {
|
||||||
|
case SDL_CONTROLLER_TYPE_XBOX360:
|
||||||
|
case SDL_CONTROLLER_TYPE_XBOXONE:
|
||||||
|
type = LI_CTYPE_XBOX;
|
||||||
|
break;
|
||||||
|
case SDL_CONTROLLER_TYPE_PS3:
|
||||||
|
case SDL_CONTROLLER_TYPE_PS4:
|
||||||
|
case SDL_CONTROLLER_TYPE_PS5:
|
||||||
|
type = LI_CTYPE_PS;
|
||||||
|
break;
|
||||||
|
case SDL_CONTROLLER_TYPE_NINTENDO_SWITCH_PRO:
|
||||||
|
#if SDL_VERSION_ATLEAST(2, 24, 0)
|
||||||
|
case SDL_CONTROLLER_TYPE_NINTENDO_SWITCH_JOYCON_LEFT:
|
||||||
|
case SDL_CONTROLLER_TYPE_NINTENDO_SWITCH_JOYCON_RIGHT:
|
||||||
|
case SDL_CONTROLLER_TYPE_NINTENDO_SWITCH_JOYCON_PAIR:
|
||||||
|
#endif
|
||||||
|
type = LI_CTYPE_NINTENDO;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
type = LI_CTYPE_UNKNOWN;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this is a PlayStation controller that doesn't have a touchpad button mapped,
|
||||||
|
// we'll allow the Select+PS button combo to act as the touchpad.
|
||||||
|
state->clickpadButtonEmulationEnabled =
|
||||||
|
#if SDL_VERSION_ATLEAST(2, 0, 14)
|
||||||
|
SDL_GameControllerGetBindForButton(state->controller, SDL_CONTROLLER_BUTTON_TOUCHPAD).bindType == SDL_CONTROLLER_BINDTYPE_NONE &&
|
||||||
|
#endif
|
||||||
|
type == LI_CTYPE_PS;
|
||||||
|
|
||||||
|
LiSendControllerArrivalEvent(state->index, m_GamepadMask, type, supportedButtonFlags, capabilities);
|
||||||
|
#else
|
||||||
|
|
||||||
// Send an empty event to tell the PC we've arrived
|
// Send an empty event to tell the PC we've arrived
|
||||||
sendGamepadState(state);
|
sendGamepadState(state);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Send a power level if it's known at this time
|
||||||
|
if (powerLevel != SDL_JOYSTICK_POWER_UNKNOWN) {
|
||||||
|
sendGamepadBatteryState(state, powerLevel);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else if (event->type == SDL_CONTROLLERDEVICEREMOVED) {
|
else if (event->type == SDL_CONTROLLERDEVICEREMOVED) {
|
||||||
state = findStateForGamepad(event->which);
|
state = findStateForGamepad(event->which);
|
||||||
@@ -550,6 +813,60 @@ void SdlInputHandler::rumble(unsigned short controllerNumber, unsigned short low
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SdlInputHandler::rumbleTriggers(uint16_t controllerNumber, uint16_t leftTrigger, uint16_t rightTrigger)
|
||||||
|
{
|
||||||
|
// Make sure the controller number is within our supported count
|
||||||
|
if (controllerNumber >= MAX_GAMEPADS) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
#if SDL_VERSION_ATLEAST(2, 0, 14)
|
||||||
|
if (m_GamepadState[controllerNumber].controller != nullptr) {
|
||||||
|
SDL_GameControllerRumbleTriggers(m_GamepadState[controllerNumber].controller, leftTrigger, rightTrigger, 30000);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void SdlInputHandler::setMotionEventState(uint16_t controllerNumber, uint8_t motionType, uint16_t reportRateHz)
|
||||||
|
{
|
||||||
|
// Make sure the controller number is within our supported count
|
||||||
|
if (controllerNumber >= MAX_GAMEPADS) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
#if SDL_VERSION_ATLEAST(2, 0, 14)
|
||||||
|
if (m_GamepadState[controllerNumber].controller != nullptr) {
|
||||||
|
uint8_t reportPeriodMs = reportRateHz ? (1000 / reportRateHz) : 0;
|
||||||
|
|
||||||
|
switch (motionType) {
|
||||||
|
case LI_MOTION_TYPE_ACCEL:
|
||||||
|
m_GamepadState[controllerNumber].accelReportPeriodMs = reportPeriodMs;
|
||||||
|
SDL_GameControllerSetSensorEnabled(m_GamepadState[controllerNumber].controller, SDL_SENSOR_ACCEL, reportRateHz ? SDL_TRUE : SDL_FALSE);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case LI_MOTION_TYPE_GYRO:
|
||||||
|
m_GamepadState[controllerNumber].gyroReportPeriodMs = reportPeriodMs;
|
||||||
|
SDL_GameControllerSetSensorEnabled(m_GamepadState[controllerNumber].controller, SDL_SENSOR_GYRO, reportRateHz ? SDL_TRUE : SDL_FALSE);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void SdlInputHandler::setControllerLED(uint16_t controllerNumber, uint8_t r, uint8_t g, uint8_t b)
|
||||||
|
{
|
||||||
|
// Make sure the controller number is within our supported count
|
||||||
|
if (controllerNumber >= MAX_GAMEPADS) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
#if SDL_VERSION_ATLEAST(2, 0, 14)
|
||||||
|
if (m_GamepadState[controllerNumber].controller != nullptr) {
|
||||||
|
SDL_GameControllerSetLED(m_GamepadState[controllerNumber].controller, r, g, b);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
QString SdlInputHandler::getUnmappedGamepads()
|
QString SdlInputHandler::getUnmappedGamepads()
|
||||||
{
|
{
|
||||||
QString ret;
|
QString ret;
|
||||||
@@ -621,7 +938,14 @@ int SdlInputHandler::getAttachedGamepadMask()
|
|||||||
count = mask = 0;
|
count = mask = 0;
|
||||||
for (int i = 0; i < SDL_NumJoysticks(); i++) {
|
for (int i = 0; i < SDL_NumJoysticks(); i++) {
|
||||||
if (SDL_IsGameController(i)) {
|
if (SDL_IsGameController(i)) {
|
||||||
mask |= (1 << count++);
|
char guidStr[33];
|
||||||
|
SDL_JoystickGetGUIDString(SDL_JoystickGetDeviceGUID(i),
|
||||||
|
guidStr, sizeof(guidStr));
|
||||||
|
|
||||||
|
if (!m_IgnoreDeviceGuids.contains(guidStr, Qt::CaseInsensitive))
|
||||||
|
{
|
||||||
|
mask |= (1 << count++);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,16 +9,12 @@
|
|||||||
#include <QDir>
|
#include <QDir>
|
||||||
#include <QGuiApplication>
|
#include <QGuiApplication>
|
||||||
|
|
||||||
#define MOUSE_POLLING_INTERVAL 5
|
SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, int streamWidth, int streamHeight)
|
||||||
|
|
||||||
SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, NvComputer*, int streamWidth, int streamHeight)
|
|
||||||
: m_MultiController(prefs.multiController),
|
: m_MultiController(prefs.multiController),
|
||||||
m_GamepadMouse(prefs.gamepadMouse),
|
m_GamepadMouse(prefs.gamepadMouse),
|
||||||
m_SwapMouseButtons(prefs.swapMouseButtons),
|
m_SwapMouseButtons(prefs.swapMouseButtons),
|
||||||
m_ReverseScrollDirection(prefs.reverseScrollDirection),
|
m_ReverseScrollDirection(prefs.reverseScrollDirection),
|
||||||
m_SwapFaceButtons(prefs.swapFaceButtons),
|
m_SwapFaceButtons(prefs.swapFaceButtons),
|
||||||
m_MouseMoveTimer(0),
|
|
||||||
m_MousePositionLock(0),
|
|
||||||
m_MouseWasInVideoRegion(false),
|
m_MouseWasInVideoRegion(false),
|
||||||
m_PendingMouseButtonsAllUpOnVideoRegionLeave(false),
|
m_PendingMouseButtonsAllUpOnVideoRegionLeave(false),
|
||||||
m_PointerRegionLockActive(false),
|
m_PointerRegionLockActive(false),
|
||||||
@@ -31,6 +27,7 @@ SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, NvComputer*, int s
|
|||||||
m_StreamHeight(streamHeight),
|
m_StreamHeight(streamHeight),
|
||||||
m_AbsoluteMouseMode(prefs.absoluteMouseMode),
|
m_AbsoluteMouseMode(prefs.absoluteMouseMode),
|
||||||
m_AbsoluteTouchMode(prefs.absoluteTouchMode),
|
m_AbsoluteTouchMode(prefs.absoluteTouchMode),
|
||||||
|
m_DisabledTouchFeedback(false),
|
||||||
m_LeftButtonReleaseTimer(0),
|
m_LeftButtonReleaseTimer(0),
|
||||||
m_RightButtonReleaseTimer(0),
|
m_RightButtonReleaseTimer(0),
|
||||||
m_DragTimer(0),
|
m_DragTimer(0),
|
||||||
@@ -133,6 +130,20 @@ SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, NvComputer*, int s
|
|||||||
}
|
}
|
||||||
streamIgnoreDevices += m_OldIgnoreDevices;
|
streamIgnoreDevices += m_OldIgnoreDevices;
|
||||||
|
|
||||||
|
// STREAM_IGNORE_DEVICE_GUIDS allows to specify additional devices to be ignored when starting
|
||||||
|
// the stream in case the scope of STREAM_GAMECONTROLLER_IGNORE_DEVICES is too broad. One such
|
||||||
|
// case is "Steam Virtual Gamepad" where everything is under the same VID/PID, but different GUIDs.
|
||||||
|
// Multiple GUIDs can be provided, but need to be separated by commas:
|
||||||
|
//
|
||||||
|
// <GUID>,<GUID>,<GUID>,...
|
||||||
|
//
|
||||||
|
QString streamIgnoreDeviceGuids = qgetenv("STREAM_IGNORE_DEVICE_GUIDS");
|
||||||
|
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
|
||||||
|
m_IgnoreDeviceGuids = streamIgnoreDeviceGuids.split(',', Qt::SkipEmptyParts);
|
||||||
|
#else
|
||||||
|
m_IgnoreDeviceGuids = streamIgnoreDeviceGuids.split(',', QString::SkipEmptyParts);
|
||||||
|
#endif
|
||||||
|
|
||||||
// For SDL_HINT_GAMECONTROLLER_IGNORE_DEVICES, we use the union of SDL_GAMECONTROLLER_IGNORE_DEVICES
|
// For SDL_HINT_GAMECONTROLLER_IGNORE_DEVICES, we use the union of SDL_GAMECONTROLLER_IGNORE_DEVICES
|
||||||
// and STREAM_GAMECONTROLLER_IGNORE_DEVICES while streaming. STREAM_GAMECONTROLLER_IGNORE_DEVICES_EXCEPT
|
// and STREAM_GAMECONTROLLER_IGNORE_DEVICES while streaming. STREAM_GAMECONTROLLER_IGNORE_DEVICES_EXCEPT
|
||||||
// overrides SDL_GAMECONTROLLER_IGNORE_DEVICES_EXCEPT while streaming.
|
// overrides SDL_GAMECONTROLLER_IGNORE_DEVICES_EXCEPT while streaming.
|
||||||
@@ -187,23 +198,6 @@ SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, NvComputer*, int s
|
|||||||
SDL_zero(m_LastTouchDownEvent);
|
SDL_zero(m_LastTouchDownEvent);
|
||||||
SDL_zero(m_LastTouchUpEvent);
|
SDL_zero(m_LastTouchUpEvent);
|
||||||
SDL_zero(m_TouchDownEvent);
|
SDL_zero(m_TouchDownEvent);
|
||||||
SDL_zero(m_MousePositionReport);
|
|
||||||
|
|
||||||
SDL_AtomicSet(&m_MouseDeltaX, 0);
|
|
||||||
SDL_AtomicSet(&m_MouseDeltaY, 0);
|
|
||||||
SDL_AtomicSet(&m_MousePositionUpdated, 0);
|
|
||||||
|
|
||||||
Uint32 pollingInterval = QString(qgetenv("MOUSE_POLLING_INTERVAL")).toUInt();
|
|
||||||
if (pollingInterval == 0) {
|
|
||||||
pollingInterval = MOUSE_POLLING_INTERVAL;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Using custom mouse polling interval: %u ms",
|
|
||||||
pollingInterval);
|
|
||||||
}
|
|
||||||
|
|
||||||
m_MouseMoveTimer = SDL_AddTimer(pollingInterval, SdlInputHandler::mouseMoveTimerCallback, this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SdlInputHandler::~SdlInputHandler()
|
SdlInputHandler::~SdlInputHandler()
|
||||||
@@ -223,7 +217,6 @@ SdlInputHandler::~SdlInputHandler()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SDL_RemoveTimer(m_MouseMoveTimer);
|
|
||||||
SDL_RemoveTimer(m_LongPressTimer);
|
SDL_RemoveTimer(m_LongPressTimer);
|
||||||
SDL_RemoveTimer(m_LeftButtonReleaseTimer);
|
SDL_RemoveTimer(m_LeftButtonReleaseTimer);
|
||||||
SDL_RemoveTimer(m_RightButtonReleaseTimer);
|
SDL_RemoveTimer(m_RightButtonReleaseTimer);
|
||||||
@@ -402,7 +395,14 @@ void SdlInputHandler::setCaptureActive(bool active)
|
|||||||
mouseY -= windowY;
|
mouseY -= windowY;
|
||||||
|
|
||||||
if (isMouseInVideoRegion(mouseX, mouseY)) {
|
if (isMouseInVideoRegion(mouseX, mouseY)) {
|
||||||
updateMousePositionReport(mouseX, mouseY);
|
// Synthesize a mouse event to synchronize the cursor
|
||||||
|
SDL_MouseMotionEvent motionEvent = {};
|
||||||
|
motionEvent.type = SDL_MOUSEMOTION;
|
||||||
|
motionEvent.timestamp = SDL_GetTicks();
|
||||||
|
motionEvent.windowID = SDL_GetWindowID(m_Window);
|
||||||
|
motionEvent.x = mouseX;
|
||||||
|
motionEvent.y = mouseY;
|
||||||
|
handleMouseMotionEvent(&motionEvent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+43
-25
@@ -5,10 +5,6 @@
|
|||||||
|
|
||||||
#include <SDL.h>
|
#include <SDL.h>
|
||||||
|
|
||||||
#define SDL_CODE_HIDE_CURSOR 1
|
|
||||||
#define SDL_CODE_SHOW_CURSOR 2
|
|
||||||
#define SDL_CODE_UNCAPTURE_MOUSE 3
|
|
||||||
|
|
||||||
struct GamepadState {
|
struct GamepadState {
|
||||||
SDL_GameController* controller;
|
SDL_GameController* controller;
|
||||||
SDL_JoystickID jsId;
|
SDL_JoystickID jsId;
|
||||||
@@ -23,13 +19,28 @@ struct GamepadState {
|
|||||||
SDL_TimerID mouseEmulationTimer;
|
SDL_TimerID mouseEmulationTimer;
|
||||||
uint32_t lastStartDownTime;
|
uint32_t lastStartDownTime;
|
||||||
|
|
||||||
short buttons;
|
bool clickpadButtonEmulationEnabled;
|
||||||
|
bool emulatedClickpadButtonDown;
|
||||||
|
|
||||||
|
#if SDL_VERSION_ATLEAST(2, 0, 14)
|
||||||
|
uint8_t gyroReportPeriodMs;
|
||||||
|
float lastGyroEventData[SDL_arraysize(SDL_ControllerSensorEvent::data)];
|
||||||
|
uint32_t lastGyroEventTime;
|
||||||
|
|
||||||
|
uint8_t accelReportPeriodMs;
|
||||||
|
float lastAccelEventData[SDL_arraysize(SDL_ControllerSensorEvent::data)];
|
||||||
|
uint32_t lastAccelEventTime;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
int buttons;
|
||||||
short lsX, lsY;
|
short lsX, lsY;
|
||||||
short rsX, rsY;
|
short rsX, rsY;
|
||||||
unsigned char lt, rt;
|
unsigned char lt, rt;
|
||||||
};
|
};
|
||||||
|
|
||||||
#define MAX_GAMEPADS 4
|
// activeGamepadMask is a short, so we're bounded by the number of mask bits
|
||||||
|
#define MAX_GAMEPADS 16
|
||||||
|
|
||||||
#define MAX_FINGERS 2
|
#define MAX_FINGERS 2
|
||||||
|
|
||||||
#define GAMEPAD_HAPTIC_METHOD_NONE 0
|
#define GAMEPAD_HAPTIC_METHOD_NONE 0
|
||||||
@@ -42,8 +53,7 @@ struct GamepadState {
|
|||||||
class SdlInputHandler
|
class SdlInputHandler
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
explicit SdlInputHandler(StreamingPreferences& prefs, NvComputer* computer,
|
explicit SdlInputHandler(StreamingPreferences& prefs, int streamWidth, int streamHeight);
|
||||||
int streamWidth, int streamHeight);
|
|
||||||
|
|
||||||
~SdlInputHandler();
|
~SdlInputHandler();
|
||||||
|
|
||||||
@@ -63,11 +73,27 @@ public:
|
|||||||
|
|
||||||
void handleControllerDeviceEvent(SDL_ControllerDeviceEvent* event);
|
void handleControllerDeviceEvent(SDL_ControllerDeviceEvent* event);
|
||||||
|
|
||||||
|
#if SDL_VERSION_ATLEAST(2, 0, 14)
|
||||||
|
void handleControllerSensorEvent(SDL_ControllerSensorEvent* event);
|
||||||
|
|
||||||
|
void handleControllerTouchpadEvent(SDL_ControllerTouchpadEvent* event);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if SDL_VERSION_ATLEAST(2, 24, 0)
|
||||||
|
void handleJoystickBatteryEvent(SDL_JoyBatteryEvent* event);
|
||||||
|
#endif
|
||||||
|
|
||||||
void handleJoystickArrivalEvent(SDL_JoyDeviceEvent* event);
|
void handleJoystickArrivalEvent(SDL_JoyDeviceEvent* event);
|
||||||
|
|
||||||
void sendText(QString& string);
|
void sendText(QString& string);
|
||||||
|
|
||||||
void rumble(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor);
|
void rumble(uint16_t controllerNumber, uint16_t lowFreqMotor, uint16_t highFreqMotor);
|
||||||
|
|
||||||
|
void rumbleTriggers(uint16_t controllerNumber, uint16_t leftTrigger, uint16_t rightTrigger);
|
||||||
|
|
||||||
|
void setMotionEventState(uint16_t controllerNumber, uint8_t motionType, uint16_t reportRateHz);
|
||||||
|
|
||||||
|
void setControllerLED(uint16_t controllerNumber, uint8_t r, uint8_t g, uint8_t b);
|
||||||
|
|
||||||
void handleTouchFingerEvent(SDL_TouchFingerEvent* event);
|
void handleTouchFingerEvent(SDL_TouchFingerEvent* event);
|
||||||
|
|
||||||
@@ -87,10 +113,6 @@ public:
|
|||||||
|
|
||||||
bool isMouseInVideoRegion(int mouseX, int mouseY, int windowWidth = -1, int windowHeight = -1);
|
bool isMouseInVideoRegion(int mouseX, int mouseY, int windowWidth = -1, int windowHeight = -1);
|
||||||
|
|
||||||
void updateMousePositionReport(int mouseX, int mouseY);
|
|
||||||
|
|
||||||
void flushMousePositionUpdate();
|
|
||||||
|
|
||||||
void updateKeyboardGrabState();
|
void updateKeyboardGrabState();
|
||||||
|
|
||||||
void updatePointerRegionLock();
|
void updatePointerRegionLock();
|
||||||
@@ -117,8 +139,14 @@ private:
|
|||||||
|
|
||||||
void sendGamepadState(GamepadState* state);
|
void sendGamepadState(GamepadState* state);
|
||||||
|
|
||||||
|
void sendGamepadBatteryState(GamepadState* state, SDL_JoystickPowerLevel level);
|
||||||
|
|
||||||
void handleAbsoluteFingerEvent(SDL_TouchFingerEvent* event);
|
void handleAbsoluteFingerEvent(SDL_TouchFingerEvent* event);
|
||||||
|
|
||||||
|
void emulateAbsoluteFingerEvent(SDL_TouchFingerEvent* event);
|
||||||
|
|
||||||
|
void disableTouchFeedback();
|
||||||
|
|
||||||
void handleRelativeFingerEvent(SDL_TouchFingerEvent* event);
|
void handleRelativeFingerEvent(SDL_TouchFingerEvent* event);
|
||||||
|
|
||||||
void performSpecialKeyCombo(KeyCombo combo);
|
void performSpecialKeyCombo(KeyCombo combo);
|
||||||
@@ -126,9 +154,6 @@ private:
|
|||||||
static
|
static
|
||||||
Uint32 longPressTimerCallback(Uint32 interval, void* param);
|
Uint32 longPressTimerCallback(Uint32 interval, void* param);
|
||||||
|
|
||||||
static
|
|
||||||
Uint32 mouseMoveTimerCallback(Uint32 interval, void* param);
|
|
||||||
|
|
||||||
static
|
static
|
||||||
Uint32 mouseEmulationTimerCallback(Uint32 interval, void* param);
|
Uint32 mouseEmulationTimerCallback(Uint32 interval, void* param);
|
||||||
|
|
||||||
@@ -147,16 +172,7 @@ private:
|
|||||||
bool m_SwapMouseButtons;
|
bool m_SwapMouseButtons;
|
||||||
bool m_ReverseScrollDirection;
|
bool m_ReverseScrollDirection;
|
||||||
bool m_SwapFaceButtons;
|
bool m_SwapFaceButtons;
|
||||||
SDL_TimerID m_MouseMoveTimer;
|
|
||||||
SDL_atomic_t m_MouseDeltaX;
|
|
||||||
SDL_atomic_t m_MouseDeltaY;
|
|
||||||
|
|
||||||
SDL_SpinLock m_MousePositionLock;
|
|
||||||
struct {
|
|
||||||
int x, y;
|
|
||||||
int windowWidth, windowHeight;
|
|
||||||
} m_MousePositionReport;
|
|
||||||
SDL_atomic_t m_MousePositionUpdated;
|
|
||||||
bool m_MouseWasInVideoRegion;
|
bool m_MouseWasInVideoRegion;
|
||||||
bool m_PendingMouseButtonsAllUpOnVideoRegionLeave;
|
bool m_PendingMouseButtonsAllUpOnVideoRegionLeave;
|
||||||
bool m_PointerRegionLockActive;
|
bool m_PointerRegionLockActive;
|
||||||
@@ -168,6 +184,7 @@ private:
|
|||||||
bool m_FakeCaptureActive;
|
bool m_FakeCaptureActive;
|
||||||
QString m_OldIgnoreDevices;
|
QString m_OldIgnoreDevices;
|
||||||
QString m_OldIgnoreDevicesExcept;
|
QString m_OldIgnoreDevicesExcept;
|
||||||
|
QStringList m_IgnoreDeviceGuids;
|
||||||
StreamingPreferences::CaptureSysKeysMode m_CaptureSystemKeysMode;
|
StreamingPreferences::CaptureSysKeysMode m_CaptureSystemKeysMode;
|
||||||
int m_MouseCursorCapturedVisibilityState;
|
int m_MouseCursorCapturedVisibilityState;
|
||||||
|
|
||||||
@@ -185,6 +202,7 @@ private:
|
|||||||
int m_StreamHeight;
|
int m_StreamHeight;
|
||||||
bool m_AbsoluteMouseMode;
|
bool m_AbsoluteMouseMode;
|
||||||
bool m_AbsoluteTouchMode;
|
bool m_AbsoluteTouchMode;
|
||||||
|
bool m_DisabledTouchFeedback;
|
||||||
|
|
||||||
SDL_TouchFingerEvent m_TouchDownEvent[MAX_FINGERS];
|
SDL_TouchFingerEvent m_TouchDownEvent[MAX_FINGERS];
|
||||||
SDL_TimerID m_LeftButtonReleaseTimer;
|
SDL_TimerID m_LeftButtonReleaseTimer;
|
||||||
|
|||||||
Loaded 100 of 161 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user