mirror of
https://github.com/moonlight-stream/moonlight-qt.git
synced 2026-10-13 12:04:07 +00:00
Compare commits
304
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
60fadcbf36 | ||
|
|
f374e88bac | ||
|
|
3cb169ea26 | ||
|
|
58f87e39a9 | ||
|
|
b1c8005d1a | ||
|
|
f8a2e22a18 | ||
|
|
7ea118fbae | ||
|
|
2149024c35 | ||
|
|
f5d71c8c9a | ||
|
|
e108a05895 | ||
|
|
ea8bccd508 | ||
|
|
bca919c48c | ||
|
|
511ffe03b5 | ||
|
|
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 |
@@ -1,6 +1,6 @@
|
||||
# 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).
|
||||
|
||||
@@ -12,15 +12,13 @@ You can follow development on our [Discord server](https://moonlight-stream.org/
|
||||
|
||||
## Features
|
||||
- Hardware accelerated video decoding on Windows, Mac, and Linux
|
||||
- Supports streaming at up to 120 FPS (high refresh rate monitor recommended)
|
||||
- Supports streaming at 720p, 1080p, 1440p, 4K, and the client PC's native screen resolution
|
||||
- H.264, HEVC, and AV1 codec support (AV1 requires Sunshine and a supported host GPU)
|
||||
- HDR streaming 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 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
|
||||
- [Windows, macOS, and Steam Link](https://github.com/moonlight-stream/moonlight-qt/releases)
|
||||
@@ -28,37 +26,36 @@ You can follow development on our [Discord server](https://moonlight-stream.org/
|
||||
- [Flatpak (for other Linux distros)](https://flathub.org/apps/details/com.moonlight_stream.Moonlight)
|
||||
- [AppImage](https://github.com/moonlight-stream/moonlight-qt/releases)
|
||||
- [Raspberry Pi 4](https://github.com/moonlight-stream/moonlight-docs/wiki/Installing-Moonlight-Qt-on-Raspberry-Pi-4)
|
||||
- [Generic ARM SBC packages for Debian](https://github.com/moonlight-stream/moonlight-docs/wiki/Installing-Moonlight-Qt-on-ARM%E2%80%90based-Single-Board-Computers) (not for Raspberry Pi)
|
||||
- [Experimental RISC-V SBC packages for Debian](https://github.com/moonlight-stream/moonlight-docs/wiki/Installing-Moonlight-Qt-on-RISC%E2%80%90V-Single-Board-Computers)
|
||||
- [NVIDIA Jetson and Nintendo Switch (Ubuntu L4T)](https://github.com/moonlight-stream/moonlight-docs/wiki/Installing-Moonlight-Qt-on-Linux4Tegra-(L4T)-Ubuntu)
|
||||
|
||||
#### Special Thanks
|
||||
|
||||
[](https://cloudsmith.com)
|
||||
|
||||
Hosting for Moonlight's Raspberry Pi and L4T package repositories is graciously provided for free by [Cloudsmith](https://cloudsmith.com).
|
||||
Hosting for Moonlight's Debian and L4T package repositories is graciously provided for free by [Cloudsmith](https://cloudsmith.com).
|
||||
|
||||
## Building
|
||||
|
||||
### Windows Build Requirements
|
||||
* Qt 5.15 SDK or later
|
||||
* Windows 7 or later
|
||||
* [Visual Studio 2019](https://visualstudio.microsoft.com/downloads/) (Community edition is fine)
|
||||
* Select **MSVC 2019** option during Qt installation. MinGW is not supported.
|
||||
* Qt 5.15 SDK or later. Qt 6 is also supported for x64 and ARM64 builds.
|
||||
* [Visual Studio 2022](https://visualstudio.microsoft.com/downloads/) (Community edition is fine)
|
||||
* 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)
|
||||
* [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
|
||||
* Qt 6.2 SDK or later
|
||||
* macOS Mojave (10.14) or later
|
||||
* Xcode 13
|
||||
* Qt 6.4 SDK or later
|
||||
* Xcode 13 or later
|
||||
* [create-dmg](https://github.com/sindresorhus/create-dmg) (only if building DMGs for use on non-development Macs)
|
||||
|
||||
### Linux/Unix Build Requirements
|
||||
* Qt 5.9 SDK or later
|
||||
* Qt 5.9 SDK or later. Qt 6 is also supported.
|
||||
* GCC or Clang
|
||||
* 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`
|
||||
* 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 SDK](https://github.com/ValveSoftware/steamlink-sdk) cloned on your build system
|
||||
@@ -68,16 +65,17 @@ Hosting for Moonlight's Raspberry Pi and L4T package repositories is graciously
|
||||
1. Install the latest Qt SDK (and optionally, the Qt Creator IDE) from https://www.qt.io/download
|
||||
* You can install Qt via Homebrew on macOS, but you will need to use `brew install qt --with-debug` to be able to create debug builds of Moonlight.
|
||||
* You may also use your Linux distro's package manager for the Qt SDK as long as the packages are Qt 5.9 or later.
|
||||
* This step is not required for building on Steam Link, because the Steam Link SDK includes Qt 5.9.
|
||||
* This step is not required for building on Steam Link, because the Steam Link SDK includes Qt 5.14.
|
||||
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.
|
||||
* 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 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.
|
||||
* 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
|
||||
1. Fork us
|
||||
|
||||
+17
-1
@@ -5,7 +5,7 @@
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>Moonlight</string>
|
||||
<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>
|
||||
<string>moonlight</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
@@ -22,6 +22,22 @@
|
||||
<string>NSApplication</string>
|
||||
<key>NSSupportsAutomaticGraphicsSwitching</key>
|
||||
<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>
|
||||
<string>VERSION</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
|
||||
+1
-1
Submodule app/SDL_GameControllerDB updated: ff26eb04d0...dbcf31a670
+103
-47
@@ -48,6 +48,9 @@ win32 {
|
||||
|
||||
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
|
||||
|
||||
# Work around a conflict with math.h inclusion between SDL and Qt 6
|
||||
DEFINES += _USE_MATH_DEFINES
|
||||
}
|
||||
macx {
|
||||
INCLUDEPATH += $$PWD/../libs/mac/include
|
||||
@@ -65,51 +68,73 @@ unix:!macx {
|
||||
CONFIG += link_pkgconfig
|
||||
PKGCONFIG += openssl sdl2 SDL2_ttf opus
|
||||
|
||||
packagesExist(libavcodec) {
|
||||
PKGCONFIG += libavcodec libavutil
|
||||
CONFIG += ffmpeg
|
||||
!disable-ffmpeg {
|
||||
packagesExist(libavcodec) {
|
||||
PKGCONFIG += libavcodec libavutil
|
||||
CONFIG += ffmpeg
|
||||
|
||||
packagesExist(libva) {
|
||||
packagesExist(libva-x11) {
|
||||
CONFIG += libva-x11
|
||||
!disable-libva {
|
||||
packagesExist(libva) {
|
||||
!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) {
|
||||
CONFIG += libvdpau
|
||||
!disable-wayland {
|
||||
packagesExist(wayland-client) {
|
||||
CONFIG += wayland
|
||||
PKGCONFIG += wayland-client
|
||||
}
|
||||
}
|
||||
|
||||
packagesExist(mmal) {
|
||||
PKGCONFIG += mmal
|
||||
CONFIG += mmal
|
||||
!disable-x11 {
|
||||
packagesExist(x11) {
|
||||
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 {
|
||||
@@ -120,7 +145,7 @@ win32:!winrt {
|
||||
CONFIG += soundio discord-rpc
|
||||
}
|
||||
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
|
||||
|
||||
# For libsoundio
|
||||
@@ -212,15 +237,15 @@ ffmpeg {
|
||||
SOURCES += \
|
||||
streaming/video/ffmpeg.cpp \
|
||||
streaming/video/ffmpeg-renderers/sdlvid.cpp \
|
||||
streaming/video/ffmpeg-renderers/pacer/pacer.cpp \
|
||||
streaming/video/ffmpeg-renderers/pacer/nullthreadedvsyncsource.cpp
|
||||
streaming/video/ffmpeg-renderers/swframemapper.cpp \
|
||||
streaming/video/ffmpeg-renderers/pacer/pacer.cpp
|
||||
|
||||
HEADERS += \
|
||||
streaming/video/ffmpeg.h \
|
||||
streaming/video/ffmpeg-renderers/renderer.h \
|
||||
streaming/video/ffmpeg-renderers/sdlvid.h \
|
||||
streaming/video/ffmpeg-renderers/pacer/pacer.h \
|
||||
streaming/video/ffmpeg-renderers/pacer/nullthreadedvsyncsource.h
|
||||
streaming/video/ffmpeg-renderers/swframemapper.h \
|
||||
streaming/video/ffmpeg-renderers/pacer/pacer.h
|
||||
}
|
||||
libva {
|
||||
message(VAAPI renderer selected)
|
||||
@@ -242,7 +267,7 @@ libva-wayland {
|
||||
PKGCONFIG += libva-wayland
|
||||
DEFINES += HAVE_LIBVA_WAYLAND
|
||||
}
|
||||
libva-wayland {
|
||||
libva-drm {
|
||||
message(VAAPI DRM support enabled)
|
||||
|
||||
PKGCONFIG += libva-drm
|
||||
@@ -261,6 +286,18 @@ mmal {
|
||||
DEFINES += HAVE_MMAL
|
||||
SOURCES += streaming/video/ffmpeg-renderers/mmal.cpp
|
||||
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 {
|
||||
message(DRM renderer selected)
|
||||
@@ -281,6 +318,9 @@ cuda {
|
||||
DEFINES += HAVE_CUDA
|
||||
SOURCES += streaming/video/ffmpeg-renderers/cuda.cpp
|
||||
HEADERS += streaming/video/ffmpeg-renderers/cuda.h
|
||||
|
||||
# ffnvcodec uses libdl in cuda_load_functions()/cuda_free_functions()
|
||||
LIBS += -ldl
|
||||
}
|
||||
config_EGL {
|
||||
message(EGL renderer selected)
|
||||
@@ -289,8 +329,11 @@ config_EGL {
|
||||
DEFINES += HAVE_EGL
|
||||
SOURCES += \
|
||||
streaming/video/ffmpeg-renderers/eglvid.cpp \
|
||||
streaming/video/ffmpeg-renderers/egl_extensions.cpp
|
||||
HEADERS += streaming/video/ffmpeg-renderers/eglvid.h
|
||||
streaming/video/ffmpeg-renderers/egl_extensions.cpp \
|
||||
streaming/video/ffmpeg-renderers/eglimagefactory.cpp
|
||||
HEADERS += \
|
||||
streaming/video/ffmpeg-renderers/eglvid.h \
|
||||
streaming/video/ffmpeg-renderers/eglimagefactory.h
|
||||
}
|
||||
config_SL {
|
||||
message(Steam Link build configuration selected)
|
||||
@@ -348,13 +391,25 @@ embedded {
|
||||
|
||||
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.qrc \
|
||||
qml.qrc
|
||||
|
||||
TRANSLATIONS += \
|
||||
languages/qml_zh_cn.ts \
|
||||
languages/qml_zh_CN.ts \
|
||||
languages/qml_de.ts \
|
||||
languages/qml_fr.ts \
|
||||
languages/qml_nb_NO.ts \
|
||||
@@ -369,14 +424,15 @@ TRANSLATIONS += \
|
||||
languages/qml_sv.ts \
|
||||
languages/qml_tr.ts \
|
||||
languages/qml_uk.ts \
|
||||
languages/qml_zh_Hant.ts \
|
||||
languages/qml_zh_TW.ts \
|
||||
languages/qml_el.ts \
|
||||
languages/qml_hi.ts \
|
||||
languages/qml_it.ts \
|
||||
languages/qml_pt.ts \
|
||||
languages/qml_pt_BR.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
|
||||
QML_IMPORT_PATH =
|
||||
|
||||
@@ -6,16 +6,17 @@
|
||||
#include <QJsonObject>
|
||||
|
||||
AutoUpdateChecker::AutoUpdateChecker(QObject *parent) :
|
||||
QObject(parent),
|
||||
m_Nam(this)
|
||||
QObject(parent)
|
||||
{
|
||||
m_Nam = new QNetworkAccessManager(this);
|
||||
|
||||
// Never communicate over HTTP
|
||||
m_Nam.setStrictTransportSecurityEnabled(true);
|
||||
m_Nam->setStrictTransportSecurityEnabled(true);
|
||||
|
||||
// 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);
|
||||
|
||||
QString currentVersion(VERSION_STR);
|
||||
@@ -28,12 +29,17 @@ AutoUpdateChecker::AutoUpdateChecker(QObject *parent) :
|
||||
|
||||
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 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)
|
||||
QT_WARNING_PUSH
|
||||
QT_WARNING_DISABLE_DEPRECATED
|
||||
m_Nam.setNetworkAccessible(QNetworkAccessManager::Accessible);
|
||||
m_Nam->setNetworkAccessible(QNetworkAccessManager::Accessible);
|
||||
QT_WARNING_POP
|
||||
#endif
|
||||
|
||||
@@ -45,7 +51,7 @@ void AutoUpdateChecker::start()
|
||||
#else
|
||||
request.setAttribute(QNetworkRequest::HTTP2AllowedAttribute, true);
|
||||
#endif
|
||||
m_Nam.get(request);
|
||||
m_Nam->get(request);
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -102,6 +108,11 @@ void AutoUpdateChecker::handleUpdateCheckRequestFinished(QNetworkReply* reply)
|
||||
{
|
||||
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) {
|
||||
QTextStream stream(reply);
|
||||
|
||||
|
||||
@@ -25,5 +25,5 @@ private:
|
||||
QString getPlatform();
|
||||
|
||||
QVector<int> m_CurrentVersionQuad;
|
||||
QNetworkAccessManager m_Nam;
|
||||
QNetworkAccessManager* m_Nam;
|
||||
};
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include <random>
|
||||
|
||||
#define SER_HOSTS "hosts"
|
||||
#define SER_HOSTS_BACKUP "hostsbackup"
|
||||
|
||||
class PcMonitorThread : public QThread
|
||||
{
|
||||
@@ -152,12 +153,21 @@ ComputerManager::ComputerManager(QObject *parent)
|
||||
{
|
||||
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
|
||||
int hosts = settings.beginReadArray(SER_HOSTS);
|
||||
for (int i = 0; i < hosts; i++) {
|
||||
settings.setArrayIndex(i);
|
||||
NvComputer* computer = new NvComputer(settings);
|
||||
m_KnownHosts[computer->uuid] = computer;
|
||||
m_LastSerializedHosts[computer->uuid] = *computer;
|
||||
}
|
||||
settings.endArray();
|
||||
|
||||
@@ -169,8 +179,8 @@ ComputerManager::ComputerManager(QObject *parent)
|
||||
m_DelayedFlushThread->start();
|
||||
|
||||
// To quit in a timely manner, we must block additional requests
|
||||
// after we receive the aboutToQuit() signal. This is neccessary
|
||||
// because NvHTTP uses aboutToQuit() to abort requests in progres
|
||||
// after we receive the aboutToQuit() signal. This is necessary
|
||||
// because NvHTTP uses aboutToQuit() to abort requests in progress
|
||||
// while quitting, however this is a one time signal - additional
|
||||
// requests would not be aborted and block termination.
|
||||
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
|
||||
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
|
||||
{
|
||||
QReadLocker lock(&m_ComputerManager->m_Lock);
|
||||
|
||||
QSettings settings;
|
||||
settings.remove(SER_HOSTS);
|
||||
settings.beginWriteArray(SER_HOSTS);
|
||||
int i = 0;
|
||||
for (const NvComputer* computer : m_ComputerManager->m_KnownHosts) {
|
||||
settings.setArrayIndex(i++);
|
||||
computer->serialize(settings);
|
||||
|
||||
// First, write to the backup location
|
||||
settings.beginWriteArray(SER_HOSTS_BACKUP);
|
||||
{
|
||||
QReadLocker lock(&m_ComputerManager->m_Lock);
|
||||
int i = 0;
|
||||
for (const NvComputer* computer : m_ComputerManager->m_KnownHosts) {
|
||||
settings.setArrayIndex(i++);
|
||||
computer->serialize(settings, false);
|
||||
}
|
||||
}
|
||||
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) {
|
||||
// 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,
|
||||
this, [this](const QMdnsEngine::Service& service) {
|
||||
qInfo() << "Discovered mDNS host:" << service.hostname();
|
||||
|
||||
MdnsPendingComputer* pendingComputer = new MdnsPendingComputer(&m_MdnsServer, service);
|
||||
MdnsPendingComputer* pendingComputer = new MdnsPendingComputer(m_MdnsServer, service);
|
||||
connect(pendingComputer, &MdnsPendingComputer::resolvedHost,
|
||||
this, &ComputerManager::handleMdnsServiceResolved);
|
||||
m_PendingResolution.append(pendingComputer);
|
||||
@@ -405,6 +443,19 @@ void ComputerManager::handleMdnsServiceResolved(MdnsPendingComputer* computer,
|
||||
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)
|
||||
{
|
||||
emit computerStateChanged(computer);
|
||||
@@ -414,15 +465,20 @@ void ComputerManager::handleComputerStateChanged(NvComputer* computer)
|
||||
emit quitAppCompleted(QVariant());
|
||||
}
|
||||
|
||||
// Save updated hosts to QSettings
|
||||
saveHosts();
|
||||
// Save updates to this host
|
||||
saveHost(computer);
|
||||
}
|
||||
|
||||
QVector<NvComputer*> ComputerManager::getComputers()
|
||||
{
|
||||
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
|
||||
@@ -446,7 +502,7 @@ public:
|
||||
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();
|
||||
|
||||
// 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)
|
||||
{
|
||||
// Persist the change
|
||||
saveHosts();
|
||||
|
||||
// Notify the UI of the state change
|
||||
handleComputerStateChanged(computer);
|
||||
}
|
||||
@@ -547,7 +600,7 @@ private:
|
||||
break;
|
||||
case NvPairingManager::PairState::PAIRED:
|
||||
// Persist the newly pinned server certificate for this host
|
||||
m_ComputerManager->saveHosts();
|
||||
m_ComputerManager->saveHost(m_Computer);
|
||||
|
||||
emit pairingCompleted(m_Computer, nullptr);
|
||||
break;
|
||||
@@ -646,9 +699,10 @@ void ComputerManager::stopPollingAsync()
|
||||
m_PendingResolution.removeFirst();
|
||||
}
|
||||
|
||||
// Delete the browser to stop discovery
|
||||
// Delete the browser and server to stop discovery and refresh polling
|
||||
delete m_MdnsBrowser;
|
||||
m_MdnsBrowser = nullptr;
|
||||
m_MdnsServer.reset();
|
||||
|
||||
// Interrupt all threads, but don't wait for them to terminate
|
||||
for (ComputerPollingEntry* entry : m_PollEntries) {
|
||||
@@ -658,8 +712,8 @@ void ComputerManager::stopPollingAsync()
|
||||
|
||||
void ComputerManager::addNewHostManually(QString address)
|
||||
{
|
||||
QUrl url = QUrl::fromUserInput(address);
|
||||
if (url.isValid() && !url.host().isEmpty()) {
|
||||
QUrl url = QUrl::fromUserInput("moonlight://" + address);
|
||||
if (url.isValid() && !url.host().isEmpty() && url.scheme() == "moonlight") {
|
||||
// If there wasn't a port specified, use the default
|
||||
addNewHost(NvAddress(url.host(), url.port(DEFAULT_HTTP_PORT)), false);
|
||||
}
|
||||
|
||||
@@ -42,11 +42,11 @@ class MdnsPendingComputer : public QObject
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit MdnsPendingComputer(QMdnsEngine::Server* server,
|
||||
explicit MdnsPendingComputer(const QSharedPointer<QMdnsEngine::Server> server,
|
||||
const QMdnsEngine::Service& service)
|
||||
: m_Hostname(service.hostname()),
|
||||
m_Port(service.port()),
|
||||
m_Server(server),
|
||||
m_ServerWeak(server),
|
||||
m_Resolver(nullptr)
|
||||
{
|
||||
// Start resolving
|
||||
@@ -94,8 +94,21 @@ signals:
|
||||
private:
|
||||
void resolve()
|
||||
{
|
||||
// Delete our resolver, so we're guaranteed that nothing is referencing m_Server.
|
||||
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,
|
||||
this, &MdnsPendingComputer::handleResolvedAddress);
|
||||
QTimer::singleShot(2000, this, &MdnsPendingComputer::handleResolvedTimeout);
|
||||
@@ -103,7 +116,8 @@ private:
|
||||
|
||||
QByteArray m_Hostname;
|
||||
uint16_t m_Port;
|
||||
QMdnsEngine::Server* m_Server;
|
||||
QWeakPointer<QMdnsEngine::Server> m_ServerWeak;
|
||||
QSharedPointer<QMdnsEngine::Server> m_Server;
|
||||
QMdnsEngine::Resolver* m_Resolver;
|
||||
QVector<QHostAddress> m_Addresses;
|
||||
};
|
||||
@@ -237,6 +251,8 @@ private slots:
|
||||
private:
|
||||
void saveHosts();
|
||||
|
||||
void saveHost(NvComputer* computer);
|
||||
|
||||
QHostAddress getBestGlobalAddressV6(QVector<QHostAddress>& addresses);
|
||||
|
||||
void startPollingComputer(NvComputer* computer);
|
||||
@@ -245,13 +261,13 @@ private:
|
||||
QReadWriteLock m_Lock;
|
||||
QMap<QString, NvComputer*> m_KnownHosts;
|
||||
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::Cache m_MdnsCache;
|
||||
QVector<MdnsPendingComputer*> m_PendingResolution;
|
||||
CompatFetcher m_CompatFetcher;
|
||||
DelayedFlushThread* m_DelayedFlushThread;
|
||||
QMutex m_DelayedFlushMutex;
|
||||
QMutex m_DelayedFlushMutex; // Lock ordering: Must never be acquired while holding NvComputer lock
|
||||
QWaitCondition m_DelayedFlushCondition;
|
||||
bool m_NeedsDelayedFlush;
|
||||
};
|
||||
|
||||
+81
-22
@@ -74,7 +74,7 @@ void NvComputer::setRemoteAddress(QHostAddress address)
|
||||
this->remoteAddress = NvAddress(address, this->externalPort);
|
||||
}
|
||||
|
||||
void NvComputer::serialize(QSettings& settings) const
|
||||
void NvComputer::serialize(QSettings& settings, bool serializeApps) const
|
||||
{
|
||||
QReadLocker lock(&this->lock);
|
||||
|
||||
@@ -94,7 +94,7 @@ void NvComputer::serialize(QSettings& settings) const
|
||||
settings.setValue(SER_NVIDIASOFTWARE, isNvidiaServerSoftware);
|
||||
|
||||
// Avoid deleting an existing applist if we couldn't get one
|
||||
if (!appList.isEmpty()) {
|
||||
if (!appList.isEmpty() && serializeApps) {
|
||||
settings.remove(SER_APPLIST);
|
||||
settings.beginWriteArray(SER_APPLIST);
|
||||
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()
|
||||
{
|
||||
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();
|
||||
}
|
||||
else {
|
||||
this->serverCodecModeSupport = 0;
|
||||
// Assume H.264 is always supported
|
||||
this->serverCodecModeSupport = SCM_H264;
|
||||
}
|
||||
|
||||
QString maxLumaPixelsHEVC = NvHTTP::getXmlString(serverInfo, "MaxLumaPixelsHEVC");
|
||||
@@ -150,8 +166,8 @@ NvComputer::NvComputer(NvHTTP& http, QString serverInfo)
|
||||
this->displayModes = NvHTTP::getDisplayModeList(serverInfo);
|
||||
std::stable_sort(this->displayModes.begin(), this->displayModes.end(),
|
||||
[](const NvDisplayMode& mode1, const NvDisplayMode& mode2) {
|
||||
return mode1.width * mode1.height * mode1.refreshRate <
|
||||
mode2.width * mode2.height * mode2.refreshRate;
|
||||
return (uint64_t)mode1.width * mode1.height * mode1.refreshRate <
|
||||
(uint64_t)mode2.width * mode2.height * mode2.refreshRate;
|
||||
});
|
||||
|
||||
// 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.
|
||||
QString remotePortStr = NvHTTP::getXmlString(serverInfo, "ExternalPort");
|
||||
if (remotePortStr.isEmpty() || (this->externalPort = remotePortStr.toUShort()) == 0) {
|
||||
this->externalPort = DEFAULT_HTTP_PORT;
|
||||
this->externalPort = http.httpPort();
|
||||
}
|
||||
|
||||
QString remoteAddress = NvHTTP::getXmlString(serverInfo, "ExternalIP");
|
||||
@@ -219,23 +235,30 @@ bool NvComputer::wake() const
|
||||
for (int i = 0; i < 16; i++) {
|
||||
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)
|
||||
47998, 47999, 48000, 48002, 48010, // Ports opened by GFE
|
||||
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
|
||||
// and broadcast addresses for this link just in
|
||||
// case the host has timed out in ARP entries.
|
||||
QVector<QString> addressList;
|
||||
QMap<QString, quint16> addressMap;
|
||||
QSet<quint16> basePortSet;
|
||||
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
|
||||
for (const QNetworkInterface& nic : QNetworkInterface::allInterfaces()) {
|
||||
@@ -254,31 +277,42 @@ bool NvComputer::wake() const
|
||||
|
||||
// Skip IPv6 which doesn't support broadcast
|
||||
if (!addr.broadcast().isNull()) {
|
||||
addressList.append(addr.broadcast().toString());
|
||||
addressMap.insert(addr.broadcast().toString(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
if (!allNodesMulticast.scopeId().isEmpty()) {
|
||||
addressList.append(allNodesMulticast.toString());
|
||||
addressMap.insert(allNodesMulticast.toString(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Try all unique address strings or host names
|
||||
bool success = false;
|
||||
for (QString& addressString : addressList) {
|
||||
QHostInfo hostInfo = QHostInfo::fromName(addressString);
|
||||
for (auto i = addressMap.constBegin(); i != addressMap.constEnd(); i++) {
|
||||
QHostAddress literalAddress;
|
||||
QList<QHostAddress> addressList;
|
||||
|
||||
if (hostInfo.error() != QHostInfo::NoError) {
|
||||
qWarning() << "Error resolving" << addressString << ":" << hostInfo.errorString();
|
||||
continue;
|
||||
// If this is an IPv4/IPv6 literal, don't use QHostInfo::fromName() because that will
|
||||
// try to perform a reverse DNS lookup that leads to delays sending WoL packets.
|
||||
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
|
||||
for (QHostAddress& address : hostInfo.addresses()) {
|
||||
for (QHostAddress& address : addressList) {
|
||||
QUdpSocket sock;
|
||||
|
||||
// Send to all ports
|
||||
for (quint16 port : WOL_PORTS) {
|
||||
// Send to all static ports
|
||||
for (quint16 port : STATIC_WOL_PORTS) {
|
||||
if (sock.writeDatagram(wolPayload, address, port)) {
|
||||
qInfo().nospace().noquote() << "Sent WoL packet to " << name << " via " << address.toString() << ":" << port;
|
||||
success = true;
|
||||
@@ -287,6 +321,31 @@ bool NvComputer::wake() const
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,16 @@
|
||||
#include <QSettings>
|
||||
#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
|
||||
{
|
||||
friend class PcMonitorThread;
|
||||
@@ -22,6 +32,14 @@ private:
|
||||
bool pendingQuit;
|
||||
|
||||
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(QSettings& settings);
|
||||
@@ -49,7 +67,11 @@ public:
|
||||
uniqueAddresses() const;
|
||||
|
||||
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
|
||||
{
|
||||
@@ -91,9 +113,10 @@ public:
|
||||
QSslCertificate serverCert;
|
||||
QVector<NvApp> appList;
|
||||
bool isNvidiaServerSoftware;
|
||||
// Remember to update isEqualSerialized() when adding fields here!
|
||||
|
||||
// Synchronization
|
||||
mutable QReadWriteLock lock;
|
||||
mutable CopySafeReadWriteLock lock;
|
||||
|
||||
private:
|
||||
uint16_t externalPort;
|
||||
|
||||
+17
-37
@@ -187,12 +187,15 @@ NvHTTP::getServerInfo(NvLogLevel logLevel, bool fastFail)
|
||||
}
|
||||
|
||||
void
|
||||
NvHTTP::launchApp(int appId,
|
||||
PSTREAM_CONFIGURATION streamConfig,
|
||||
bool sops,
|
||||
bool localAudio,
|
||||
int gamepadMask,
|
||||
QString& rtspSessionUrl)
|
||||
NvHTTP::startApp(QString verb,
|
||||
bool isGfe,
|
||||
int appId,
|
||||
PSTREAM_CONFIGURATION streamConfig,
|
||||
bool sops,
|
||||
bool localAudio,
|
||||
int gamepadMask,
|
||||
bool persistGameControllersOnDisconnect,
|
||||
QString& rtspSessionUrl)
|
||||
{
|
||||
int riKeyId;
|
||||
|
||||
@@ -201,25 +204,26 @@ NvHTTP::launchApp(int appId,
|
||||
|
||||
QString response =
|
||||
openConnectionToString(m_BaseUrlHttps,
|
||||
"launch",
|
||||
verb,
|
||||
"appid="+QString::number(appId)+
|
||||
"&mode="+QString::number(streamConfig->width)+"x"+
|
||||
QString::number(streamConfig->height)+"x"+
|
||||
// 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
|
||||
// used to use 60 here but that locked the frame rate to 60 FPS
|
||||
// on GFE 3.20.3.
|
||||
QString::number(streamConfig->fps > 60 ? 0 : streamConfig->fps)+
|
||||
// on GFE 3.20.3. We don't need this hack for Sunshine.
|
||||
QString::number((streamConfig->fps > 60 && isGfe) ? 0 : streamConfig->fps)+
|
||||
"&additionalStates=1&sops="+QString::number(sops ? 1 : 0)+
|
||||
"&rikey="+QByteArray(streamConfig->remoteInputAesKey, sizeof(streamConfig->remoteInputAesKey)).toHex()+
|
||||
"&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" :
|
||||
"")+
|
||||
"&localAudioPlayMode="+QString::number(localAudio ? 1 : 0)+
|
||||
"&surroundAudioInfo="+QString::number(SURROUNDAUDIOINFO_FROM_AUDIO_CONFIGURATION(streamConfig->audioConfiguration))+
|
||||
"&remoteControllersBitmap="+QString::number(gamepadMask)+
|
||||
"&gcmap="+QString::number(gamepadMask),
|
||||
"&gcmap="+QString::number(gamepadMask)+
|
||||
"&gcpersist="+QString::number(persistGameControllersOnDisconnect ? 1 : 0),
|
||||
LAUNCH_TIMEOUT_MS);
|
||||
|
||||
qInfo() << "Launch response:" << response;
|
||||
@@ -230,30 +234,6 @@ NvHTTP::launchApp(int appId,
|
||||
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
|
||||
NvHTTP::quitApp()
|
||||
{
|
||||
@@ -269,7 +249,7 @@ NvHTTP::quitApp()
|
||||
verifyResponseStatus(response);
|
||||
|
||||
// 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) {
|
||||
// Generate a synthetic GfeResponseException letting the caller know
|
||||
// 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
|
||||
|
||||
+9
-10
@@ -162,16 +162,15 @@ public:
|
||||
quitApp();
|
||||
|
||||
void
|
||||
resumeApp(PSTREAM_CONFIGURATION streamConfig,
|
||||
QString& rtspSessionUrl);
|
||||
|
||||
void
|
||||
launchApp(int appId,
|
||||
PSTREAM_CONFIGURATION streamConfig,
|
||||
bool sops,
|
||||
bool localAudio,
|
||||
int gamepadMask,
|
||||
QString& rtspSessionUrl);
|
||||
startApp(QString verb,
|
||||
bool isGfe,
|
||||
int appId,
|
||||
PSTREAM_CONFIGURATION streamConfig,
|
||||
bool sops,
|
||||
bool localAudio,
|
||||
int gamepadMask,
|
||||
bool persistGameControllersOnDisconnect,
|
||||
QString& rtspSessionUrl);
|
||||
|
||||
QVector<NvApp>
|
||||
getAppList();
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "utils.h"
|
||||
|
||||
#include <QGuiApplication>
|
||||
#include <QLibraryInfo>
|
||||
|
||||
#include "streaming/session.h"
|
||||
#include "streaming/streamutils.h"
|
||||
@@ -17,6 +18,7 @@ SystemProperties::SystemProperties()
|
||||
hasDesktopEnvironment = WMUtils::isRunningDesktopEnvironment();
|
||||
isRunningWayland = WMUtils::isRunningWayland();
|
||||
isRunningXWayland = isRunningWayland && QGuiApplication::platformName() == "xcb";
|
||||
usesMaterial3Theme = QLibraryInfo::version() >= QVersionNumber(6, 5, 0);
|
||||
QString nativeArch = QSysInfo::currentCpuArchitecture();
|
||||
|
||||
#ifdef Q_OS_WIN32
|
||||
@@ -70,7 +72,7 @@ SystemProperties::SystemProperties()
|
||||
// and cache the results to speed up future queries on this data.
|
||||
querySdlVideoInfo();
|
||||
|
||||
Q_ASSERT(maximumStreamingFrameRate >= 60);
|
||||
Q_ASSERT(!monitorRefreshRates.isEmpty());
|
||||
Q_ASSERT(!monitorNativeResolutions.isEmpty());
|
||||
}
|
||||
|
||||
@@ -80,6 +82,12 @@ QRect SystemProperties::getNativeResolution(int displayIndex)
|
||||
return monitorNativeResolutions.value(displayIndex);
|
||||
}
|
||||
|
||||
int SystemProperties::getRefreshRate(int displayIndex)
|
||||
{
|
||||
// Returns 0 if out of bounds
|
||||
return monitorRefreshRates.value(displayIndex);
|
||||
}
|
||||
|
||||
class QuerySdlVideoThread : public QThread
|
||||
{
|
||||
public:
|
||||
@@ -188,9 +196,6 @@ void SystemProperties::refreshDisplaysInternal()
|
||||
|
||||
monitorNativeResolutions.clear();
|
||||
|
||||
// Never let the maximum drop below 60 FPS
|
||||
maximumStreamingFrameRate = 60;
|
||||
|
||||
SDL_DisplayMode bestMode;
|
||||
for (int displayIndex = 0; displayIndex < SDL_GetNumVideoDisplays(); displayIndex++) {
|
||||
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 hasDiscordIntegration MEMBER hasDiscordIntegration CONSTANT)
|
||||
Q_PROPERTY(QString unmappedGamepads MEMBER unmappedGamepads NOTIFY unmappedGamepadsChanged)
|
||||
Q_PROPERTY(int maximumStreamingFrameRate MEMBER maximumStreamingFrameRate CONSTANT)
|
||||
Q_PROPERTY(QSize maximumResolution MEMBER maximumResolution CONSTANT)
|
||||
Q_PROPERTY(QString versionString MEMBER versionString CONSTANT)
|
||||
Q_PROPERTY(bool supportsHdr MEMBER supportsHdr CONSTANT)
|
||||
Q_PROPERTY(bool usesMaterial3Theme MEMBER usesMaterial3Theme CONSTANT)
|
||||
|
||||
Q_INVOKABLE void refreshDisplays();
|
||||
Q_INVOKABLE QRect getNativeResolution(int displayIndex);
|
||||
Q_INVOKABLE int getRefreshRate(int displayIndex);
|
||||
|
||||
signals:
|
||||
void unmappedGamepadsChanged();
|
||||
@@ -49,10 +50,11 @@ private:
|
||||
bool hasBrowser;
|
||||
bool hasDiscordIntegration;
|
||||
QString unmappedGamepads;
|
||||
int maximumStreamingFrameRate;
|
||||
QSize maximumResolution;
|
||||
QList<QRect> monitorNativeResolutions;
|
||||
QList<int> monitorRefreshRates;
|
||||
QString versionString;
|
||||
bool supportsHdr;
|
||||
bool usesMaterial3Theme;
|
||||
};
|
||||
|
||||
|
||||
@@ -118,7 +118,7 @@ public:
|
||||
}
|
||||
break;
|
||||
// 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.
|
||||
case Event::AppQuitCompleted:
|
||||
if (m_State == StateSeekApp && !event.errorMessage.isEmpty()) {
|
||||
|
||||
@@ -4,21 +4,19 @@
|
||||
<metadata_license>CC0-1.0</metadata_license>
|
||||
<project_license>GPL-3.0+</project_license>
|
||||
<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>
|
||||
<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>
|
||||
<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>H.264, HEVC, and AV1 codec 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 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>
|
||||
</description>
|
||||
|
||||
@@ -36,6 +34,65 @@
|
||||
</screenshots>
|
||||
|
||||
<releases>
|
||||
<release version="5.0.1" date="2023-11-02">
|
||||
<description>
|
||||
<p>Bugfixes:</p>
|
||||
<ul>
|
||||
<li>Fixed connection issue streaming from some hosts running GeForce Experience or Sunshine v0.20 and earlier</li>
|
||||
<li>Updated community-contributed translations from Weblate</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<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>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
[Desktop Entry]
|
||||
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
|
||||
Icon=moonlight
|
||||
Terminal=false
|
||||
Type=Application
|
||||
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.Controls 2.2
|
||||
import QtQuick.Controls.Material 2.2
|
||||
|
||||
import AppModel 1.0
|
||||
import ComputerManager 1.0
|
||||
@@ -89,7 +90,7 @@ CenteredGridView {
|
||||
|
||||
onSourceSizeChanged: {
|
||||
// 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
|
||||
// supported games all have box art, so this check is not required.
|
||||
if (!model.isAppCollectorGame &&
|
||||
@@ -121,15 +122,15 @@ CenteredGridView {
|
||||
anchors.fill: appIcon
|
||||
|
||||
sourceComponent: Item {
|
||||
ToolButton {
|
||||
RoundButton {
|
||||
anchors.horizontalCenterOffset: appIcon.isPlaceholder ? -47 : 0
|
||||
anchors.verticalCenterOffset: appIcon.isPlaceholder ? -75 : -60
|
||||
anchors.centerIn: parent
|
||||
implicitWidth: 125
|
||||
implicitHeight: 125
|
||||
implicitWidth: 85
|
||||
implicitHeight: 85
|
||||
|
||||
Image {
|
||||
source: "qrc:/res/baseline-play_circle_filled_white-48px.svg"
|
||||
source: "qrc:/res/play_arrow_FILL1_wght700_GRAD200_opsz48.svg"
|
||||
anchors.centerIn: parent
|
||||
sourceSize {
|
||||
width: 75
|
||||
@@ -145,17 +146,19 @@ CenteredGridView {
|
||||
ToolTip.delay: 1000
|
||||
ToolTip.timeout: 3000
|
||||
ToolTip.visible: hovered
|
||||
|
||||
Material.background: "#D0808080"
|
||||
}
|
||||
|
||||
ToolButton {
|
||||
RoundButton {
|
||||
anchors.horizontalCenterOffset: appIcon.isPlaceholder ? 47 : 0
|
||||
anchors.verticalCenterOffset: appIcon.isPlaceholder ? -75 : 60
|
||||
anchors.centerIn: parent
|
||||
implicitWidth: 125
|
||||
implicitHeight: 125
|
||||
implicitWidth: 85
|
||||
implicitHeight: 85
|
||||
|
||||
Image {
|
||||
source: "qrc:/res/baseline-cancel-24px.svg"
|
||||
source: "qrc:/res/stop_FILL1_wght700_GRAD200_opsz48.svg"
|
||||
anchors.centerIn: parent
|
||||
sourceSize {
|
||||
width: 75
|
||||
@@ -171,6 +174,8 @@ CenteredGridView {
|
||||
ToolTip.delay: 1000
|
||||
ToolTip.timeout: 3000
|
||||
ToolTip.visible: hovered
|
||||
|
||||
Material.background: "#D0808080"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -345,7 +350,7 @@ CenteredGridView {
|
||||
|
||||
function quitApp() {
|
||||
var component = Qt.createComponent("QuitSegue.qml")
|
||||
var params = {"appName": appName}
|
||||
var params = {"appName": appName, "quitRunningAppFn": appModel.quitRunningApp}
|
||||
if (segueToStream) {
|
||||
// Store the session and app name if we're going to stream after
|
||||
// successfully quitting the old app.
|
||||
@@ -358,9 +363,6 @@ CenteredGridView {
|
||||
}
|
||||
|
||||
stackView.push(component.createObject(stackView, params))
|
||||
|
||||
// Trigger the quit after pushing the quit segue on screen
|
||||
appModel.quitRunningApp()
|
||||
}
|
||||
|
||||
onAccepted: quitApp()
|
||||
|
||||
@@ -2,6 +2,7 @@ import QtQuick 2.9
|
||||
import QtQuick.Controls 2.2
|
||||
|
||||
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
|
||||
ComboBox {
|
||||
@@ -38,6 +39,12 @@ ComboBox {
|
||||
popup.onAboutToShow: {
|
||||
// Switch to normal navigation for combo boxes
|
||||
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: {
|
||||
|
||||
@@ -26,6 +26,7 @@ Item {
|
||||
function onLaunchFailed(message) {
|
||||
errorDialog.text = message
|
||||
errorDialog.open()
|
||||
console.error(message)
|
||||
}
|
||||
|
||||
function onAppQuitRequired(appName) {
|
||||
@@ -84,10 +85,8 @@ Item {
|
||||
|
||||
function quitApp() {
|
||||
var component = Qt.createComponent("QuitSegue.qml")
|
||||
var params = {"appName": appName}
|
||||
var params = {"appName": appName, "quitRunningAppFn": launcher.quitRunningApp}
|
||||
stackView.push(component.createObject(stackView, params))
|
||||
// Trigger the quit after pushing the quit segue on screen
|
||||
launcher.quitRunningApp()
|
||||
}
|
||||
|
||||
onAccepted: quitApp()
|
||||
|
||||
+20
-15
@@ -99,7 +99,7 @@ CenteredGridView {
|
||||
Label {
|
||||
height: searchSpinner.height
|
||||
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.")
|
||||
font.pointSize: 20
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
@@ -130,12 +130,12 @@ CenteredGridView {
|
||||
id: stateIcon
|
||||
anchors.horizontalCenter: pcIcon.horizontalCenter
|
||||
anchors.verticalCenter: pcIcon.verticalCenter
|
||||
anchors.verticalCenterOffset: -15
|
||||
anchors.verticalCenterOffset: !model.online ? -18 : -16
|
||||
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 {
|
||||
width: 75
|
||||
height: 75
|
||||
width: !model.online ? 75 : 70
|
||||
height: !model.online ? 75 : 70
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,7 +211,7 @@ CenteredGridView {
|
||||
text: qsTr("Delete PC")
|
||||
onTriggered: {
|
||||
deletePcDialog.pcIndex = index
|
||||
// get confirmation first, actual closing is called from the dialog
|
||||
deletePcDialog.pcName = model.name
|
||||
deletePcDialog.open()
|
||||
}
|
||||
}
|
||||
@@ -271,6 +271,12 @@ CenteredGridView {
|
||||
// the ItemDelegate and not where the mouse cursor is
|
||||
pcContextMenu.open()
|
||||
}
|
||||
|
||||
Keys.onDeletePressed: {
|
||||
deletePcDialog.pcIndex = index
|
||||
deletePcDialog.pcName = model.name
|
||||
deletePcDialog.open()
|
||||
}
|
||||
}
|
||||
|
||||
ErrorMessageDialog {
|
||||
@@ -291,7 +297,8 @@ CenteredGridView {
|
||||
|
||||
// don't allow edits to the rest of the window while open
|
||||
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
|
||||
onRejected: {
|
||||
// FIXME: We should interrupt pairing here
|
||||
@@ -301,16 +308,14 @@ CenteredGridView {
|
||||
NavigableMessageDialog {
|
||||
id: deletePcDialog
|
||||
// don't allow edits to the rest of the window while open
|
||||
property int pcIndex : -1;
|
||||
text:qsTr("Are you sure you want to remove this PC?")
|
||||
property int pcIndex : -1
|
||||
property string pcName : ""
|
||||
text: qsTr("Are you sure you want to remove '%1'?").arg(pcName)
|
||||
standardButtons: Dialog.Yes | Dialog.No
|
||||
|
||||
function deletePc() {
|
||||
console.log("deleting PC pairing for PC at index: " + pcIndex)
|
||||
computerModel.deleteComputer(pcIndex);
|
||||
onAccepted: {
|
||||
computerModel.deleteComputer(pcIndex)
|
||||
}
|
||||
|
||||
onAccepted: deletePc()
|
||||
}
|
||||
|
||||
NavigableMessageDialog {
|
||||
@@ -319,7 +324,7 @@ CenteredGridView {
|
||||
standardButtons: Dialog.Ok
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import Session 1.0
|
||||
|
||||
Item {
|
||||
property string appName
|
||||
property var quitRunningAppFn
|
||||
property Session nextSession : null
|
||||
property string nextAppName : ""
|
||||
|
||||
@@ -17,6 +18,7 @@ Item {
|
||||
if (error !== undefined) {
|
||||
errorDialog.text = error
|
||||
errorDialog.open()
|
||||
console.error(error)
|
||||
}
|
||||
|
||||
// If we're supposed to launch another game after this, do so now
|
||||
@@ -37,6 +39,11 @@ Item {
|
||||
|
||||
// Connect the quit completion signal
|
||||
ComputerManager.quitAppCompleted.connect(quitAppCompleted)
|
||||
|
||||
// Start the quit operation if requested
|
||||
if (quitRunningAppFn) {
|
||||
quitRunningAppFn()
|
||||
}
|
||||
}
|
||||
|
||||
StackView.onDeactivating: {
|
||||
|
||||
+242
-85
@@ -1,6 +1,7 @@
|
||||
import QtQuick 2.9
|
||||
import QtQuick.Controls 2.2
|
||||
import QtQuick.Layouts 1.2
|
||||
import QtQuick.Window 2.2
|
||||
|
||||
import StreamingPreferences 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: {
|
||||
// This enables Tab and BackTab based navigation rather than arrow keys.
|
||||
// It is required to shift focus between controls on the settings page.
|
||||
@@ -388,69 +432,190 @@ Flickable {
|
||||
}
|
||||
|
||||
AutoResizingComboBox {
|
||||
function createModel() {
|
||||
var fpsListModel = Qt.createQmlObject('import QtQuick 2.0; ListModel {}', parent, '')
|
||||
property int lastIndexValue
|
||||
|
||||
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
|
||||
fpsListModel.append({"text": qsTr("%1 FPS").arg("30"), "video_fps": "30"})
|
||||
fpsListModel.append({"text": qsTr("%1 FPS").arg("60"), "video_fps": "60"})
|
||||
StreamingPreferences.bitrateKbps = StreamingPreferences.getDefaultBitrate(StreamingPreferences.width,
|
||||
StreamingPreferences.height,
|
||||
StreamingPreferences.fps);
|
||||
slider.value = StreamingPreferences.bitrateKbps
|
||||
}
|
||||
|
||||
// Add unsupported FPS values that come before the display max FPS
|
||||
if (StreamingPreferences.unsupportedFps) {
|
||||
if (max_fps > 90) {
|
||||
fpsListModel.append({"text": qsTr("%1 FPS (Unsupported)").arg("90"), "video_fps": "90"})
|
||||
lastIndexValue = currentIndex
|
||||
}
|
||||
|
||||
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
|
||||
// handle wonky displays that report just over 60 Hz.
|
||||
if (max_fps > 64) {
|
||||
// Mark any FPS value greater than 120 as unsupported
|
||||
if (StreamingPreferences.unsupportedFps && max_fps > 120) {
|
||||
fpsListModel.append({"text": qsTr("%1 FPS (Unsupported)").arg(max_fps), "video_fps": ""+max_fps})
|
||||
onClosed: {
|
||||
fpsField.clear()
|
||||
}
|
||||
|
||||
onRejected: {
|
||||
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"})
|
||||
}
|
||||
else {
|
||||
fpsListModel.append({"text": qsTr("%1 FPS").arg(max_fps), "video_fps": ""+max_fps})
|
||||
|
||||
var fps = fpsField.text ? fpsField.text : fpsField.placeholderText
|
||||
|
||||
// Find and update the custom entry
|
||||
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
|
||||
if (StreamingPreferences.unsupportedFps) {
|
||||
if (max_fps < 90) {
|
||||
fpsListModel.append({"text":qsTr("%1 FPS (Unsupported)").arg("90"), "video_fps": "90"})
|
||||
ColumnLayout {
|
||||
Label {
|
||||
text: qsTr("Enter a custom frame rate:")
|
||||
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() {
|
||||
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
|
||||
currentIndex = 0
|
||||
var found = false
|
||||
for (var i = 0; i < model.count; i++) {
|
||||
var el_fps = parseInt(model.get(i).video_fps);
|
||||
|
||||
// Pick the highest value lesser or equal to the saved FPS
|
||||
if (saved_fps >= el_fps) {
|
||||
// Look for a matching frame rate
|
||||
if (saved_fps === el_fps) {
|
||||
currentIndex = i
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Persist the selected value
|
||||
activated(currentIndex)
|
||||
// If we didn't find one, add a custom frame rate for the current value
|
||||
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
|
||||
@@ -459,21 +624,31 @@ Flickable {
|
||||
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
|
||||
maximumWidth: parent.width / 2
|
||||
textRole: "text"
|
||||
// ::onActivated must be used, as it only listens for when the index is changed by a human
|
||||
onActivated : {
|
||||
var selectedFps = parseInt(model.get(currentIndex).video_fps)
|
||||
|
||||
// Only modify the bitrate if the values actually changed
|
||||
if (StreamingPreferences.fps !== selectedFps) {
|
||||
StreamingPreferences.fps = selectedFps
|
||||
|
||||
StreamingPreferences.bitrateKbps = StreamingPreferences.getDefaultBitrate(StreamingPreferences.width,
|
||||
StreamingPreferences.height,
|
||||
StreamingPreferences.fps);
|
||||
slider.value = StreamingPreferences.bitrateKbps
|
||||
if (model.get(currentIndex).is_custom) {
|
||||
customFpsDialog.open()
|
||||
}
|
||||
else {
|
||||
updateBitrateForSelection()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -511,6 +686,11 @@ Flickable {
|
||||
bitrateTitle.text = qsTr("Video bitrate: %1 Mbps").arg(value / 1000.0)
|
||||
StreamingPreferences.bitrateKbps = value
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
// Refresh the text after translations change
|
||||
languageChanged.connect(onValueChanged)
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
@@ -546,7 +726,7 @@ Flickable {
|
||||
for (var i = 0; i < model.count; i++) {
|
||||
var thisWm = model.get(i).val;
|
||||
if (thisWm === StreamingPreferences.recommendedFullScreenMode) {
|
||||
model.get(i).text += qsTr(" (Recommended)")
|
||||
model.get(i).text += " " + qsTr("(Recommended)")
|
||||
model.move(i, 0, 1)
|
||||
break
|
||||
}
|
||||
@@ -837,8 +1017,8 @@ Flickable {
|
||||
val: StreamingPreferences.LANG_UK
|
||||
} */
|
||||
ListElement {
|
||||
text: "繁体字" // Traditional Chinese
|
||||
val: StreamingPreferences.LANG_ZH_HANT
|
||||
text: "繁體中文" // Traditional Chinese
|
||||
val: StreamingPreferences.LANG_ZH_TW
|
||||
}
|
||||
ListElement {
|
||||
text: "Português" // Portuguese
|
||||
@@ -868,6 +1048,10 @@ Flickable {
|
||||
text: "Čeština" // Czech
|
||||
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 : {
|
||||
@@ -1375,7 +1559,6 @@ Flickable {
|
||||
|
||||
id: codecComboBox
|
||||
textRole: "text"
|
||||
enabled: !enableHdr.checked
|
||||
model: ListModel {
|
||||
id: codecListModel
|
||||
ListElement {
|
||||
@@ -1390,6 +1573,10 @@ Flickable {
|
||||
text: qsTr("HEVC (H.265)")
|
||||
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 : {
|
||||
@@ -1397,21 +1584,6 @@ Flickable {
|
||||
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 {
|
||||
@@ -1419,8 +1591,12 @@ Flickable {
|
||||
width: parent.width
|
||||
text: qsTr("Enable HDR (Experimental)")
|
||||
font.pointSize: 12
|
||||
|
||||
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
|
||||
|
||||
@@ -1433,25 +1609,6 @@ Flickable {
|
||||
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 {
|
||||
id: enableMdns
|
||||
width: parent.width
|
||||
|
||||
@@ -45,6 +45,7 @@ Item {
|
||||
{
|
||||
// Display the error dialog after Session::exec() returns
|
||||
streamSegueErrorDialog.text = text
|
||||
console.error(text)
|
||||
}
|
||||
|
||||
function displayLaunchWarning(text)
|
||||
@@ -56,6 +57,7 @@ Item {
|
||||
toast.text = text
|
||||
toast.timeout = 3000
|
||||
toast.visible = true
|
||||
console.warn(text)
|
||||
}
|
||||
|
||||
function quitStarting()
|
||||
|
||||
+10
-11
@@ -186,19 +186,18 @@ void ComputerModel::handlePairingCompleted(NvComputer*, QString error)
|
||||
|
||||
void ComputerModel::handleComputerStateChanged(NvComputer* computer)
|
||||
{
|
||||
// If this is an existing computer, we can report the data changed
|
||||
int index = m_Computers.indexOf(computer);
|
||||
if (index >= 0) {
|
||||
// Let the view know that this specific computer changed
|
||||
emit dataChanged(createIndex(index, 0), createIndex(index, 0));
|
||||
QVector<NvComputer*> newComputerList = m_ComputerManager->getComputers();
|
||||
|
||||
// Reset the model if the structural layout of the list has changed
|
||||
if (m_Computers != newComputerList) {
|
||||
beginResetModel();
|
||||
m_Computers = newComputerList;
|
||||
endResetModel();
|
||||
}
|
||||
else {
|
||||
// This is a new PC which may be inserted at an arbitrary point
|
||||
// in our computer list (since it comes from CM's QMap). Reload
|
||||
// the whole model state to ensure it stays consistent.
|
||||
beginResetModel();
|
||||
m_Computers = m_ComputerManager->getComputers();
|
||||
endResetModel();
|
||||
// Let the view know that this specific computer changed
|
||||
int index = m_Computers.indexOf(computer);
|
||||
emit dataChanged(createIndex(index, 0), createIndex(index, 0));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+11
-1
@@ -2,6 +2,7 @@ import QtQuick 2.9
|
||||
import QtQuick.Controls 2.2
|
||||
import QtQuick.Layouts 1.3
|
||||
import QtQuick.Window 2.2
|
||||
import QtQuick.Controls.Material 2.2
|
||||
|
||||
import ComputerManager 1.0
|
||||
import AutoUpdateChecker 1.0
|
||||
@@ -22,6 +23,15 @@ ApplicationWindow {
|
||||
width: 1280
|
||||
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: {
|
||||
if (SystemProperties.hasDesktopEnvironment) {
|
||||
if (StreamingPreferences.uiDisplayMode == StreamingPreferences.UI_WINDOWED) return "Windowed"
|
||||
@@ -488,7 +498,7 @@ ApplicationWindow {
|
||||
|
||||
NavigableDialog {
|
||||
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
|
||||
|
||||
|
||||
@@ -191,7 +191,17 @@ void SdlGamepadKeyNavigation::onPollingTimerFired()
|
||||
case SDL_CONTROLLERDEVICEADDED:
|
||||
SDL_GameController* gc = SDL_GameControllerOpen(event.cdevice.which);
|
||||
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;
|
||||
}
|
||||
@@ -239,31 +249,6 @@ void SdlGamepadKeyNavigation::onPollingTimerFired()
|
||||
sendKey(QEvent::Type::KeyRelease, Qt::Key_Right);
|
||||
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)
|
||||
{
|
||||
m_UiNavMode = uiNavMode;
|
||||
|
||||
@@ -24,7 +24,6 @@ public:
|
||||
|
||||
private:
|
||||
void sendKey(QEvent::Type type, Qt::Key key, Qt::KeyboardModifiers modifiers = Qt::NoModifier);
|
||||
void sendWheel(QPoint& angleDelta);
|
||||
|
||||
private slots:
|
||||
void onPollingTimerFired();
|
||||
|
||||
Binary file not shown.
+292
-292
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.
+305
-309
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+313
-352
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.
+661
-633
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+304
-308
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.
+291
-291
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.
+304
-304
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.
+452
-432
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.
+33
-17
@@ -301,6 +301,11 @@ int main(int argc, char *argv[])
|
||||
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 LOG_TO_FILE
|
||||
QDir tempDir(Path::getLogDir());
|
||||
@@ -326,6 +331,15 @@ int main(int argc, char *argv[])
|
||||
SetUnhandledExceptionFilter(UnhandledExceptionHandler);
|
||||
#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)
|
||||
// Force AntiHooking.dll to be statically imported and loaded
|
||||
// by ntdll on Win32 platforms by calling a dummy function.
|
||||
@@ -371,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.";
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
@@ -531,6 +550,13 @@ int main(int argc, char *argv[])
|
||||
StreamingPreferences prefs;
|
||||
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
|
||||
// and we can set the SDL video driver to match Qt.
|
||||
if (WMUtils::isRunningWayland() && QGuiApplication::platformName() == "xcb") {
|
||||
@@ -614,29 +640,19 @@ int main(int argc, char *argv[])
|
||||
// Create the identity manager on the main thread
|
||||
IdentityManager::get();
|
||||
|
||||
#ifndef Q_OS_WINRT
|
||||
// Use the dense material dark theme by default
|
||||
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_STYLE")) {
|
||||
qputenv("QT_QUICK_CONTROLS_STYLE", "Material");
|
||||
}
|
||||
#else
|
||||
// Use universal dark on WinRT
|
||||
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");
|
||||
}
|
||||
// We require the Material theme
|
||||
QQuickStyle::setStyle("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");
|
||||
|
||||
// These are defaults that we allow the user to override
|
||||
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_MATERIAL_ACCENT")) {
|
||||
qputenv("QT_QUICK_CONTROLS_MATERIAL_ACCENT", "Purple");
|
||||
}
|
||||
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_MATERIAL_VARIANT")) {
|
||||
qputenv("QT_QUICK_CONTROLS_MATERIAL_VARIANT", "Dense");
|
||||
}
|
||||
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_UNIVERSAL_THEME")) {
|
||||
qputenv("QT_QUICK_CONTROLS_UNIVERSAL_THEME", "Dark");
|
||||
}
|
||||
|
||||
QQmlApplicationEngine engine;
|
||||
QString initialView;
|
||||
|
||||
+3
-3
@@ -34,7 +34,7 @@
|
||||
|
||||
// Qt's DRM master FD grabbed by our hook
|
||||
int g_QtDrmMasterFd = -1;
|
||||
struct stat64 g_DrmMasterStat;
|
||||
struct stat g_DrmMasterStat;
|
||||
|
||||
// The DRM master FD created for SDL
|
||||
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.
|
||||
if (g_QtDrmMasterFd == -1 && drmIsMaster(fd)) {
|
||||
g_QtDrmMasterFd = fd;
|
||||
fstat64(g_QtDrmMasterFd, &g_DrmMasterStat);
|
||||
fstat(g_QtDrmMasterFd, &g_DrmMasterStat);
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Captured Qt EGLFS DRM master fd (legacy): %d",
|
||||
g_QtDrmMasterFd);
|
||||
@@ -73,7 +73,7 @@ int drmModeAtomicCommit(int fd, drmModeAtomicReqPtr req,
|
||||
// EGLFS backend's DRM FD, on which we will call drmDropMaster() later.
|
||||
if (g_QtDrmMasterFd == -1 && drmIsMaster(fd)) {
|
||||
g_QtDrmMasterFd = fd;
|
||||
fstat64(g_QtDrmMasterFd, &g_DrmMasterStat);
|
||||
fstat(g_QtDrmMasterFd, &g_DrmMasterStat);
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Captured Qt EGLFS DRM master fd (atomic): %d",
|
||||
g_QtDrmMasterFd);
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
#if SDL_VERSION_ATLEAST(2, 0, 15)
|
||||
|
||||
extern int g_QtDrmMasterFd;
|
||||
extern struct stat64 g_DrmMasterStat;
|
||||
extern struct stat g_DrmMasterStat;
|
||||
extern int g_SdlDrmMasterFd;
|
||||
|
||||
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 (strncmp(pathname, "/dev/dri/card", 13) == 0) {
|
||||
// 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 &&
|
||||
g_DrmMasterStat.st_ino == fdstat.st_ino) {
|
||||
// It is our device. Time to do the magic!
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
QString Path::s_CacheDir;
|
||||
QString Path::s_LogDir;
|
||||
QString Path::s_BoxArtCacheDir;
|
||||
QString Path::s_QmlCacheDir;
|
||||
|
||||
QString Path::getLogDir()
|
||||
{
|
||||
@@ -22,6 +23,12 @@ QString Path::getBoxArtCacheDir()
|
||||
return s_BoxArtCacheDir;
|
||||
}
|
||||
|
||||
QString Path::getQmlCacheDir()
|
||||
{
|
||||
Q_ASSERT(!s_QmlCacheDir.isEmpty());
|
||||
return s_QmlCacheDir;
|
||||
}
|
||||
|
||||
QByteArray Path::readDataFile(QString fileName)
|
||||
{
|
||||
QFile dataFile(getDataFilePath(fileName));
|
||||
@@ -97,6 +104,7 @@ void Path::initialize(bool portable)
|
||||
if (portable) {
|
||||
s_LogDir = QDir::currentPath();
|
||||
s_BoxArtCacheDir = QDir::currentPath() + "/boxart";
|
||||
s_QmlCacheDir = QDir::currentPath() + "/qmlcache";
|
||||
|
||||
// In order for the If-Modified-Since logic to work in MappingFetcher,
|
||||
// the cache directory must be different than the current directory.
|
||||
@@ -112,5 +120,6 @@ void Path::initialize(bool portable)
|
||||
#endif
|
||||
s_CacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation);
|
||||
s_BoxArtCacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/boxart";
|
||||
s_QmlCacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/qmlcache";
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -7,8 +7,8 @@ class Path
|
||||
{
|
||||
public:
|
||||
static QString getLogDir();
|
||||
|
||||
static QString getBoxArtCacheDir();
|
||||
static QString getQmlCacheDir();
|
||||
|
||||
static QByteArray readDataFile(QString fileName);
|
||||
static void writeCacheFile(QString fileName, QByteArray data);
|
||||
@@ -24,4 +24,5 @@ private:
|
||||
static QString s_CacheDir;
|
||||
static QString s_LogDir;
|
||||
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/ic_add_to_queue_white_48px.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-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/settings.svg</file>
|
||||
<file>res/arrow_left.svg</file>
|
||||
@@ -21,8 +22,8 @@
|
||||
<file>languages/qml_de.qm</file>
|
||||
<file>languages/qml_fr.ts</file>
|
||||
<file>languages/qml_fr.qm</file>
|
||||
<file>languages/qml_zh_cn.ts</file>
|
||||
<file>languages/qml_zh_cn.qm</file>
|
||||
<file>languages/qml_zh_CN.ts</file>
|
||||
<file>languages/qml_zh_CN.qm</file>
|
||||
<file>languages/qml_nb_NO.ts</file>
|
||||
<file>languages/qml_nb_NO.qm</file>
|
||||
<file>languages/qml_ru.ts</file>
|
||||
@@ -47,8 +48,8 @@
|
||||
<file>languages/qml_tr.qm</file>
|
||||
<file>languages/qml_uk.ts</file>
|
||||
<file>languages/qml_uk.qm</file>
|
||||
<file>languages/qml_zh_Hant.ts</file>
|
||||
<file>languages/qml_zh_Hant.qm</file>
|
||||
<file>languages/qml_zh_TW.ts</file>
|
||||
<file>languages/qml_zh_TW.qm</file>
|
||||
<file>languages/qml_el.ts</file>
|
||||
<file>languages/qml_el.qm</file>
|
||||
<file>languages/qml_hi.ts</file>
|
||||
@@ -61,6 +62,8 @@
|
||||
<file>languages/qml_pl.qm</file>
|
||||
<file>languages/qml_cs.ts</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 -->
|
||||
<!--file>languages/qml_pt_BR.qm</file-->
|
||||
<!--file>languages/qml_pt_BR.ts</file-->
|
||||
|
||||
@@ -8,26 +8,32 @@
|
||||
#define COMPAT_KEY "latestsupportedversion-"
|
||||
|
||||
CompatFetcher::CompatFetcher(QObject *parent) :
|
||||
QObject(parent),
|
||||
m_Nam(this)
|
||||
QObject(parent)
|
||||
{
|
||||
m_Nam = new QNetworkAccessManager(this);
|
||||
|
||||
// Never communicate over HTTP
|
||||
m_Nam.setStrictTransportSecurityEnabled(true);
|
||||
m_Nam->setStrictTransportSecurityEnabled(true);
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
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)
|
||||
// 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_DISABLE_DEPRECATED
|
||||
m_Nam.setNetworkAccessible(QNetworkAccessManager::Accessible);
|
||||
m_Nam->setNetworkAccessible(QNetworkAccessManager::Accessible);
|
||||
QT_WARNING_POP
|
||||
#endif
|
||||
|
||||
@@ -41,7 +47,7 @@ void CompatFetcher::start()
|
||||
#endif
|
||||
|
||||
// We'll get a callback when this is finished
|
||||
m_Nam.get(request);
|
||||
m_Nam->get(request);
|
||||
}
|
||||
|
||||
bool CompatFetcher::isGfeVersionSupported(QString gfeVersion)
|
||||
@@ -119,6 +125,11 @@ void CompatFetcher::handleCompatInfoFetched(QNetworkReply* reply)
|
||||
{
|
||||
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) {
|
||||
// Queue the reply for deletion
|
||||
reply->deleteLater();
|
||||
|
||||
@@ -18,5 +18,5 @@ private slots:
|
||||
void handleCompatInfoFetched(QNetworkReply* reply);
|
||||
|
||||
private:
|
||||
QNetworkAccessManager m_Nam;
|
||||
QNetworkAccessManager* m_Nam;
|
||||
};
|
||||
|
||||
@@ -4,26 +4,32 @@
|
||||
#include <QNetworkReply>
|
||||
|
||||
MappingFetcher::MappingFetcher(QObject *parent) :
|
||||
QObject(parent),
|
||||
m_Nam(this)
|
||||
QObject(parent)
|
||||
{
|
||||
m_Nam = new QNetworkAccessManager(this);
|
||||
|
||||
// Never communicate over HTTP
|
||||
m_Nam.setStrictTransportSecurityEnabled(true);
|
||||
m_Nam->setStrictTransportSecurityEnabled(true);
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
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)
|
||||
// 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_DISABLE_DEPRECATED
|
||||
m_Nam.setNetworkAccessible(QNetworkAccessManager::Accessible);
|
||||
m_Nam->setNetworkAccessible(QNetworkAccessManager::Accessible);
|
||||
QT_WARNING_POP
|
||||
#endif
|
||||
|
||||
@@ -57,13 +63,18 @@ void MappingFetcher::start()
|
||||
#endif
|
||||
|
||||
// We'll get a callback when this is finished
|
||||
m_Nam.get(request);
|
||||
m_Nam->get(request);
|
||||
}
|
||||
|
||||
void MappingFetcher::handleMappingListFetched(QNetworkReply* reply)
|
||||
{
|
||||
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) {
|
||||
// Queue the reply for deletion
|
||||
reply->deleteLater();
|
||||
|
||||
@@ -16,5 +16,5 @@ private slots:
|
||||
void handleMappingListFetched(QNetworkReply* reply);
|
||||
|
||||
private:
|
||||
QNetworkAccessManager m_Nam;
|
||||
QNetworkAccessManager* m_Nam;
|
||||
};
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <QTranslator>
|
||||
#include <QCoreApplication>
|
||||
#include <QLocale>
|
||||
#include <QtMath>
|
||||
|
||||
#include <QtDebug>
|
||||
|
||||
@@ -20,9 +21,9 @@
|
||||
#define SER_MULTICONT "multicontroller"
|
||||
#define SER_AUDIOCFG "audiocfg"
|
||||
#define SER_VIDEOCFG "videocfg"
|
||||
#define SER_HDR "hdr"
|
||||
#define SER_VIDEODEC "videodec"
|
||||
#define SER_WINDOWMODE "windowmode"
|
||||
#define SER_UNSUPPORTEDFPS "unsupportedfps"
|
||||
#define SER_MDNS "mdns"
|
||||
#define SER_QUITAPPAFTER "quitAppAfter"
|
||||
#define SER_ABSMOUSEMODE "mouseacceleration"
|
||||
@@ -87,7 +88,6 @@ void StreamingPreferences::reload()
|
||||
gameOptimizations = settings.value(SER_GAMEOPTS, true).toBool();
|
||||
playAudioOnHost = settings.value(SER_HOSTAUDIO, false).toBool();
|
||||
multiController = settings.value(SER_MULTICONT, true).toBool();
|
||||
unsupportedFps = settings.value(SER_UNSUPPORTEDFPS, false).toBool();
|
||||
enableMdns = settings.value(SER_MDNS, true).toBool();
|
||||
quitAppAfter = settings.value(SER_QUITAPPAFTER, false).toBool();
|
||||
absoluteMouseMode = settings.value(SER_ABSMOUSEMODE, false).toBool();
|
||||
@@ -104,6 +104,7 @@ void StreamingPreferences::reload()
|
||||
reverseScrollDirection = settings.value(SER_REVERSESCROLL, false).toBool();
|
||||
swapFaceButtons = settings.value(SER_SWAPFACEBUTTONS, false).toBool();
|
||||
keepAwake = settings.value(SER_KEEPAWAKE, true).toBool();
|
||||
enableHdr = settings.value(SER_HDR, false).toBool();
|
||||
captureSysKeysMode = static_cast<CaptureSysKeysMode>(settings.value(SER_CAPTURESYSKEYS,
|
||||
static_cast<int>(CaptureSysKeysMode::CSK_OFF)).toInt());
|
||||
audioConfig = static_cast<AudioConfig>(settings.value(SER_AUDIOCFG,
|
||||
@@ -137,6 +138,12 @@ void StreamingPreferences::reload()
|
||||
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()
|
||||
@@ -202,7 +209,7 @@ QString StreamingPreferences::getSuffixFromLanguage(StreamingPreferences::Langua
|
||||
case LANG_FR:
|
||||
return "fr";
|
||||
case LANG_ZH_CN:
|
||||
return "zh_cn";
|
||||
return "zh_CN";
|
||||
case LANG_NB_NO:
|
||||
return "nb_NO";
|
||||
case LANG_RU:
|
||||
@@ -227,8 +234,8 @@ QString StreamingPreferences::getSuffixFromLanguage(StreamingPreferences::Langua
|
||||
return "tr";
|
||||
case LANG_UK:
|
||||
return "uk";
|
||||
case LANG_ZH_HANT:
|
||||
return "zh_Hant";
|
||||
case LANG_ZH_TW:
|
||||
return "zh_TW";
|
||||
case LANG_PT:
|
||||
return "pt";
|
||||
case LANG_PT_BR:
|
||||
@@ -243,6 +250,8 @@ QString StreamingPreferences::getSuffixFromLanguage(StreamingPreferences::Langua
|
||||
return "pl";
|
||||
case LANG_CS:
|
||||
return "cs";
|
||||
case LANG_HE:
|
||||
return "he";
|
||||
case LANG_AUTO:
|
||||
default:
|
||||
return QLocale::system().name();
|
||||
@@ -261,7 +270,6 @@ void StreamingPreferences::save()
|
||||
settings.setValue(SER_GAMEOPTS, gameOptimizations);
|
||||
settings.setValue(SER_HOSTAUDIO, playAudioOnHost);
|
||||
settings.setValue(SER_MULTICONT, multiController);
|
||||
settings.setValue(SER_UNSUPPORTEDFPS, unsupportedFps);
|
||||
settings.setValue(SER_MDNS, enableMdns);
|
||||
settings.setValue(SER_QUITAPPAFTER, quitAppAfter);
|
||||
settings.setValue(SER_ABSMOUSEMODE, absoluteMouseMode);
|
||||
@@ -273,6 +281,7 @@ void StreamingPreferences::save()
|
||||
settings.setValue(SER_PACKETSIZE, packetSize);
|
||||
settings.setValue(SER_DETECTNETBLOCKING, detectNetworkBlocking);
|
||||
settings.setValue(SER_AUDIOCFG, static_cast<int>(audioConfig));
|
||||
settings.setValue(SER_HDR, enableHdr);
|
||||
settings.setValue(SER_VIDEOCFG, static_cast<int>(videoCodecConfig));
|
||||
settings.setValue(SER_VIDEODEC, static_cast<int>(videoDecoderSelection));
|
||||
settings.setValue(SER_WINDOWMODE, static_cast<int>(windowMode));
|
||||
@@ -290,28 +299,51 @@ void StreamingPreferences::save()
|
||||
|
||||
int StreamingPreferences::getDefaultBitrate(int width, int height, int fps)
|
||||
{
|
||||
// This table prefers 16:10 resolutions because they are
|
||||
// only slightly more pixels than the 16:9 equivalents, so
|
||||
// we don't want to bump those 16:10 resolutions up to the
|
||||
// next 16:9 slot.
|
||||
// Don't scale bitrate linearly beyond 60 FPS. It's definitely not a linear
|
||||
// bitrate increase for frame rate once we get to values that high.
|
||||
float frameRateFactor = (fps <= 60 ? fps : (qSqrt(fps / 60.f) * 60.f)) / 30.f;
|
||||
|
||||
if (width * height <= 640 * 360) {
|
||||
return static_cast<int>(1000 * (fps / 30.0));
|
||||
}
|
||||
else if (width * height <= 854 * 480) {
|
||||
return static_cast<int>(1500 * (fps / 30.0));
|
||||
}
|
||||
// This covers 1280x720 and 1280x800 too
|
||||
else if (width * height <= 1366 * 768) {
|
||||
return static_cast<int>(5000 * (fps / 30.0));
|
||||
}
|
||||
else if (width * height <= 1920 * 1200) {
|
||||
return static_cast<int>(10000 * (fps / 30.0));
|
||||
}
|
||||
else if (width * height <= 2560 * 1600) {
|
||||
return static_cast<int>(20000 * (fps / 30.0));
|
||||
}
|
||||
else /* if (width * height <= 3840 * 2160) */ {
|
||||
return static_cast<int>(40000 * (fps / 30.0));
|
||||
// TODO: Collect some empirical data to see if these defaults make sense.
|
||||
// We're just using the values that the Shield used, as we have for years.
|
||||
static const struct resTable {
|
||||
int pixels;
|
||||
int factor;
|
||||
} resTable[] {
|
||||
{ 640 * 360, 1 },
|
||||
{ 854 * 480, 2 },
|
||||
{ 1280 * 720, 5 },
|
||||
{ 1920 * 1080, 10 },
|
||||
{ 2560 * 1440, 20 },
|
||||
{ 3840 * 2160, 40 },
|
||||
{ -1, -1 },
|
||||
};
|
||||
|
||||
// Calculate the resolution factor by linear interpolation of the resolution table
|
||||
float resolutionFactor;
|
||||
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_FORCE_H264,
|
||||
VCC_FORCE_HEVC,
|
||||
VCC_FORCE_HEVC_HDR
|
||||
VCC_FORCE_HEVC_HDR_DEPRECATED, // Kept for backwards compatibility
|
||||
VCC_FORCE_AV1
|
||||
};
|
||||
Q_ENUM(VideoCodecConfig)
|
||||
|
||||
@@ -82,7 +83,7 @@ public:
|
||||
LANG_SV,
|
||||
LANG_TR,
|
||||
LANG_UK,
|
||||
LANG_ZH_HANT,
|
||||
LANG_ZH_TW,
|
||||
LANG_PT,
|
||||
LANG_PT_BR,
|
||||
LANG_EL,
|
||||
@@ -90,6 +91,7 @@ public:
|
||||
LANG_HI,
|
||||
LANG_PL,
|
||||
LANG_CS,
|
||||
LANG_HE,
|
||||
};
|
||||
Q_ENUM(Language);
|
||||
|
||||
@@ -109,7 +111,6 @@ public:
|
||||
Q_PROPERTY(bool gameOptimizations MEMBER gameOptimizations NOTIFY gameOptimizationsChanged)
|
||||
Q_PROPERTY(bool playAudioOnHost MEMBER playAudioOnHost NOTIFY playAudioOnHostChanged)
|
||||
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 quitAppAfter MEMBER quitAppAfter NOTIFY quitAppAfterChanged)
|
||||
Q_PROPERTY(bool absoluteMouseMode MEMBER absoluteMouseMode NOTIFY absoluteMouseModeChanged)
|
||||
@@ -118,9 +119,10 @@ public:
|
||||
Q_PROPERTY(bool connectionWarnings MEMBER connectionWarnings NOTIFY connectionWarningsChanged)
|
||||
Q_PROPERTY(bool richPresence MEMBER richPresence NOTIFY richPresenceChanged)
|
||||
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(VideoCodecConfig videoCodecConfig MEMBER videoCodecConfig NOTIFY videoCodecConfigChanged)
|
||||
Q_PROPERTY(bool enableHdr MEMBER enableHdr NOTIFY enableHdrChanged)
|
||||
Q_PROPERTY(VideoDecoderSelection videoDecoderSelection MEMBER videoDecoderSelection NOTIFY videoDecoderSelectionChanged)
|
||||
Q_PROPERTY(WindowMode windowMode MEMBER windowMode NOTIFY windowModeChanged)
|
||||
Q_PROPERTY(WindowMode recommendedFullScreenMode MEMBER recommendedFullScreenMode CONSTANT)
|
||||
@@ -145,7 +147,6 @@ public:
|
||||
bool gameOptimizations;
|
||||
bool playAudioOnHost;
|
||||
bool multiController;
|
||||
bool unsupportedFps;
|
||||
bool enableMdns;
|
||||
bool quitAppAfter;
|
||||
bool absoluteMouseMode;
|
||||
@@ -164,6 +165,7 @@ public:
|
||||
int packetSize;
|
||||
AudioConfig audioConfig;
|
||||
VideoCodecConfig videoCodecConfig;
|
||||
bool enableHdr;
|
||||
VideoDecoderSelection videoDecoderSelection;
|
||||
WindowMode windowMode;
|
||||
WindowMode recommendedFullScreenMode;
|
||||
@@ -185,6 +187,7 @@ signals:
|
||||
void absoluteTouchModeChanged();
|
||||
void audioConfigChanged();
|
||||
void videoCodecConfigChanged();
|
||||
void enableHdrChanged();
|
||||
void videoDecoderSelectionChanged();
|
||||
void uiDisplayModeChanged();
|
||||
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
|
||||
// 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);
|
||||
#endif
|
||||
|
||||
m_FrameSize = opusConfig->samplesPerFrame * sizeof(short) * opusConfig->channelCount;
|
||||
|
||||
@@ -58,6 +67,10 @@ bool SdlAudioRenderer::prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION*
|
||||
have.samples,
|
||||
have.size);
|
||||
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"SDL audio driver: %s",
|
||||
SDL_GetCurrentAudioDriver());
|
||||
|
||||
// Start playback
|
||||
SDL_PauseAudioDevice(m_AudioDevice, 0);
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <Limelight.h>
|
||||
#include <SDL.h>
|
||||
#include <SDL_syswm.h>
|
||||
#include "streaming/streamutils.h"
|
||||
|
||||
#include <QtMath>
|
||||
@@ -27,7 +28,103 @@ Uint32 SdlInputHandler::longPressTimerCallback(Uint32, void*)
|
||||
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)
|
||||
{
|
||||
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.
|
||||
// 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
|
||||
|
||||
// Haptic capabilities (in addition to those from SDL_HapticQuery())
|
||||
#define ML_HAPTIC_GC_RUMBLE (1U << 16)
|
||||
#define ML_HAPTIC_SIMPLE_RUMBLE (1U << 17)
|
||||
#define ML_HAPTIC_GC_RUMBLE (1U << 16)
|
||||
#define ML_HAPTIC_SIMPLE_RUMBLE (1U << 17)
|
||||
#define ML_HAPTIC_GC_TRIGGER_RUMBLE (1U << 18)
|
||||
|
||||
const int SdlInputHandler::k_ButtonMap[] = {
|
||||
A_FLAG, B_FLAG, X_FLAG, Y_FLAG,
|
||||
BACK_FLAG, SPECIAL_FLAG, PLAY_FLAG,
|
||||
LS_CLK_FLAG, RS_CLK_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*
|
||||
@@ -42,17 +46,31 @@ SdlInputHandler::findStateForGamepad(SDL_JoystickID id)
|
||||
}
|
||||
}
|
||||
|
||||
// This should only happen with > 4 gamepads
|
||||
SDL_assert(SDL_NumJoysticks() > 4);
|
||||
// We can get a spurious removal event if the device is removed
|
||||
// before or during SDL_GameControllerOpen(). This is fine to ignore.
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void SdlInputHandler::sendGamepadState(GamepadState* state)
|
||||
{
|
||||
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,
|
||||
m_GamepadMask,
|
||||
state->buttons,
|
||||
buttons,
|
||||
state->lt,
|
||||
state->rt,
|
||||
state->lsX,
|
||||
@@ -61,6 +79,47 @@ void SdlInputHandler::sendGamepadState(GamepadState* state)
|
||||
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)
|
||||
{
|
||||
auto gamepad = reinterpret_cast<GamepadState*>(param);
|
||||
@@ -162,6 +221,13 @@ void SdlInputHandler::handleControllerAxisEvent(SDL_ControllerAxisEvent* 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);
|
||||
if (state == NULL) {
|
||||
return;
|
||||
@@ -212,6 +278,12 @@ void SdlInputHandler::handleControllerButtonEvent(SDL_ControllerButtonEvent* eve
|
||||
else if (event->button == SDL_CONTROLLER_BUTTON_DPAD_DOWN) {
|
||||
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 {
|
||||
@@ -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)
|
||||
{
|
||||
GamepadState* state;
|
||||
@@ -316,6 +466,20 @@ void SdlInputHandler::handleControllerDeviceEvent(SDL_ControllerDeviceEvent* eve
|
||||
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
|
||||
// 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
|
||||
@@ -338,6 +502,17 @@ void SdlInputHandler::handleControllerDeviceEvent(SDL_ControllerDeviceEvent* eve
|
||||
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];
|
||||
if (m_MultiController) {
|
||||
state->index = i;
|
||||
@@ -357,14 +532,18 @@ void SdlInputHandler::handleControllerDeviceEvent(SDL_ControllerDeviceEvent* eve
|
||||
state->controller = controller;
|
||||
state->jsId = SDL_JoystickInstanceID(SDL_GameControllerGetJoystick(state->controller));
|
||||
|
||||
hapticCaps = 0;
|
||||
#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)
|
||||
// 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
|
||||
// and we'll get a (potentially false) success value returned.
|
||||
hapticCaps = SDL_GameControllerRumble(controller, 1, 1, 1) == 0 ?
|
||||
ML_HAPTIC_GC_RUMBLE : 0;
|
||||
hapticCaps |= SDL_GameControllerRumble(controller, 1, 1, 1) == 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
|
||||
state->haptic = SDL_HapticOpenFromJoystick(SDL_GameControllerGetJoystick(state->controller));
|
||||
state->hapticEffectId = -1;
|
||||
@@ -394,15 +573,18 @@ void SdlInputHandler::handleControllerDeviceEvent(SDL_ControllerDeviceEvent* eve
|
||||
}
|
||||
#endif
|
||||
|
||||
SDL_JoystickGetGUIDString(SDL_JoystickGetGUID(SDL_GameControllerGetJoystick(state->controller)),
|
||||
guidStr, sizeof(guidStr));
|
||||
mapping = SDL_GameControllerMapping(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,
|
||||
"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,
|
||||
state->index,
|
||||
name != nullptr ? name : "<null>",
|
||||
vendorId,
|
||||
productId,
|
||||
hapticCaps,
|
||||
guidStr,
|
||||
mapping != nullptr ? mapping : "<null>");
|
||||
@@ -421,8 +603,89 @@ void SdlInputHandler::handleControllerDeviceEvent(SDL_ControllerDeviceEvent* eve
|
||||
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
|
||||
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) {
|
||||
state = findStateForGamepad(event->which);
|
||||
@@ -550,6 +813,60 @@ void SdlInputHandler::rumble(unsigned short controllerNumber, unsigned short low
|
||||
#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 ret;
|
||||
@@ -621,7 +938,14 @@ int SdlInputHandler::getAttachedGamepadMask()
|
||||
count = mask = 0;
|
||||
for (int i = 0; i < SDL_NumJoysticks(); 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 <QGuiApplication>
|
||||
|
||||
#define MOUSE_POLLING_INTERVAL 5
|
||||
|
||||
SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, NvComputer*, int streamWidth, int streamHeight)
|
||||
SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, int streamWidth, int streamHeight)
|
||||
: m_MultiController(prefs.multiController),
|
||||
m_GamepadMouse(prefs.gamepadMouse),
|
||||
m_SwapMouseButtons(prefs.swapMouseButtons),
|
||||
m_ReverseScrollDirection(prefs.reverseScrollDirection),
|
||||
m_SwapFaceButtons(prefs.swapFaceButtons),
|
||||
m_MouseMoveTimer(0),
|
||||
m_MousePositionLock(0),
|
||||
m_MouseWasInVideoRegion(false),
|
||||
m_PendingMouseButtonsAllUpOnVideoRegionLeave(false),
|
||||
m_PointerRegionLockActive(false),
|
||||
@@ -31,6 +27,7 @@ SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, NvComputer*, int s
|
||||
m_StreamHeight(streamHeight),
|
||||
m_AbsoluteMouseMode(prefs.absoluteMouseMode),
|
||||
m_AbsoluteTouchMode(prefs.absoluteTouchMode),
|
||||
m_DisabledTouchFeedback(false),
|
||||
m_LeftButtonReleaseTimer(0),
|
||||
m_RightButtonReleaseTimer(0),
|
||||
m_DragTimer(0),
|
||||
@@ -133,6 +130,20 @@ SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, NvComputer*, int s
|
||||
}
|
||||
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
|
||||
// and STREAM_GAMECONTROLLER_IGNORE_DEVICES while streaming. STREAM_GAMECONTROLLER_IGNORE_DEVICES_EXCEPT
|
||||
// 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_LastTouchUpEvent);
|
||||
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()
|
||||
@@ -223,7 +217,6 @@ SdlInputHandler::~SdlInputHandler()
|
||||
}
|
||||
}
|
||||
|
||||
SDL_RemoveTimer(m_MouseMoveTimer);
|
||||
SDL_RemoveTimer(m_LongPressTimer);
|
||||
SDL_RemoveTimer(m_LeftButtonReleaseTimer);
|
||||
SDL_RemoveTimer(m_RightButtonReleaseTimer);
|
||||
@@ -402,7 +395,14 @@ void SdlInputHandler::setCaptureActive(bool active)
|
||||
mouseY -= windowY;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user