mirror of
https://github.com/moonlight-stream/moonlight-qt.git
synced 2026-10-14 20:44:19 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
719024ec01 | ||
|
|
f0659b4f3c | ||
|
|
a649f2736f | ||
|
|
046ccb1dee | ||
|
|
d340556c28 | ||
|
|
32250db126 | ||
|
|
cb8c30db2e | ||
|
|
dacd5a9e3d | ||
|
|
859d8b96a7 | ||
|
|
02981e3cee | ||
|
|
49892b3ce8 | ||
|
|
b223e7ec36 | ||
|
|
0926337c1d | ||
|
|
346e72a678 | ||
|
|
d612e47b62 | ||
|
|
c138cbf994 | ||
|
|
dfe275ab67 | ||
|
|
d6cfbdb273 | ||
|
|
8a27fa7bb5 | ||
|
|
b9a6fb1fe5 | ||
|
|
4b6194e15e | ||
|
|
e1b3f90f8b | ||
|
|
c5cc0b8c59 | ||
|
|
5c849ec666 | ||
|
|
c2a30cbe19 | ||
|
|
31eddd8da0 | ||
|
|
abf2a14ee6 | ||
|
|
e555874485 | ||
|
|
a7aef8f147 | ||
|
|
5b8c11f1a9 | ||
|
|
645040c943 | ||
|
|
fcc5ea348c | ||
|
|
3fbb45448f | ||
|
|
8a83289709 | ||
|
|
08175de069 | ||
|
|
808933cd70 | ||
|
|
384dd2adab | ||
|
|
4a37952b41 | ||
|
|
1046d264d8 | ||
|
|
bb235ab1ae | ||
|
|
ef88b83727 | ||
|
|
5612e9864e | ||
|
|
8302187dee | ||
|
|
77d07c7c4d | ||
|
|
b2265080fd | ||
|
|
0d70bd9ac0 | ||
|
|
8f63d59b81 | ||
|
|
0395c0cc95 | ||
|
|
72065412a3 | ||
|
|
7af2cdd0ef | ||
|
|
67c801c4e7 | ||
|
|
f7c8ad4c5a | ||
|
|
61092b34de | ||
|
|
1e9e96fb8c | ||
|
|
17d1ced07c | ||
|
|
a26ced04ff | ||
|
|
154b5b6ded | ||
|
|
8219dbef6f | ||
|
|
3da724e76a | ||
|
|
1616d237ae | ||
|
|
3d21facc9f | ||
|
|
fead2a08a3 | ||
|
|
d8acf24af0 | ||
|
|
97412b3c79 | ||
|
|
6145913068 | ||
|
|
e56bb6785f | ||
|
|
6d63d6c54a | ||
|
|
6e8a103e70 | ||
|
|
00d3530f64 | ||
|
|
1e6ca18193 | ||
|
|
5a32992497 | ||
|
|
79ba00a2f4 | ||
|
|
fdfaf8ec70 | ||
|
|
162c58b697 | ||
|
|
75959ca692 | ||
|
|
06fbcd19c8 | ||
|
|
36e0468a99 | ||
|
|
c4d85cf928 | ||
|
|
daf8495acb | ||
|
|
9999ded933 | ||
|
|
80128e8293 | ||
|
|
9579b2c85e | ||
|
|
eb7f184848 | ||
|
|
71b21920e2 | ||
|
|
91b787a5be | ||
|
|
11f659aadb | ||
|
|
0870dd15a8 | ||
|
|
7cd52041d8 | ||
|
|
f2bcb49478 | ||
|
|
60a4bda94f | ||
|
|
54cfc46500 | ||
|
|
c09559ade5 | ||
|
|
3e9aea1f7a | ||
|
|
f0c292f508 | ||
|
|
1444ba2c35 | ||
|
|
fac4c85133 | ||
|
|
97a09e0571 | ||
|
|
c7121516c1 | ||
|
|
56a5fbf63b | ||
|
|
80604a752b | ||
|
|
3fe6e18609 | ||
|
|
47f3f30d83 | ||
|
|
085a904970 | ||
|
|
0fa7723f51 | ||
|
|
84ea483b4c | ||
|
|
4de3aa96b6 | ||
|
|
1193943228 | ||
|
|
2959faca0c | ||
|
|
d0df4fa3a0 | ||
|
|
0a301dd2f9 | ||
|
|
43e3679297 | ||
|
|
0455063854 | ||
|
|
12d90d77c7 | ||
|
|
61d7749aaf | ||
|
|
69fb4a0c37 | ||
|
|
d2c9f058f8 | ||
|
|
acaa3afa0f | ||
|
|
3c58e21cbf | ||
|
|
9d3c5b9bc6 | ||
|
|
ce34358d46 | ||
|
|
278bcec238 | ||
|
|
128f458848 | ||
|
|
7ae2bcd6db | ||
|
|
130f252f97 | ||
|
|
d339e05a3b | ||
|
|
933f4813aa | ||
|
|
f14e71c1d6 | ||
|
|
d78dbd7b0f | ||
|
|
71a6ef6eba | ||
|
|
4873862d16 | ||
|
|
425efe3d72 | ||
|
|
666dc13c01 | ||
|
|
267f9f5759 | ||
|
|
c8d8c00f84 | ||
|
|
dcc567ebda | ||
|
|
a199a3ccc3 | ||
|
|
8159e07427 | ||
|
|
32d70758c5 | ||
|
|
3a9e284f29 | ||
|
|
c658daeac6 | ||
|
|
f605169b15 | ||
|
|
f82b495bcc | ||
|
|
33b44a87c5 | ||
|
|
a88d994fcb | ||
|
|
dd9224d9a6 | ||
|
|
48728efee0 | ||
|
|
043c55ae66 | ||
|
|
fa3f822cf1 | ||
|
|
5afab126b3 | ||
|
|
bee8378795 | ||
|
|
cbd42f4a58 | ||
|
|
3333fd7fc8 | ||
|
|
142258b4b5 | ||
|
|
01213b58ba | ||
|
|
51df7032bb | ||
|
|
c033ce5ecf | ||
|
|
bfaf57cfd0 | ||
|
|
6c523570e5 | ||
|
|
047e5bdd81 | ||
|
|
6b1559d397 | ||
|
|
a3167b8152 | ||
|
|
cbeb23f16a | ||
|
|
b4a6512784 | ||
|
|
1af2742013 | ||
|
|
8048938158 | ||
|
|
3689c7f982 | ||
|
|
96c7057fee | ||
|
|
df7c4c82a9 | ||
|
|
18d8db338b | ||
|
|
9637639e31 | ||
|
|
17abe8f164 | ||
|
|
badaaece04 | ||
|
|
56119eebae | ||
|
|
c17c8e2a2c | ||
|
|
e45a60f2ed | ||
|
|
ebe9356937 | ||
|
|
0094305299 | ||
|
|
b27050d3bf | ||
|
|
d941e596ad | ||
|
|
ddaec92da5 | ||
|
|
1c0a4c92a8 | ||
|
|
383c00afbd | ||
|
|
d3b2bff8ad | ||
|
|
77aaa6141d | ||
|
|
2bb1af61d0 | ||
|
|
40b29472b3 | ||
|
|
5d5029de59 | ||
|
|
cc96e6a222 | ||
|
|
581c9438ca | ||
|
|
54bd427f26 | ||
|
|
c55d184195 | ||
|
|
b279f94789 | ||
|
|
f3a897e99a | ||
|
|
392b1b5904 | ||
|
|
4daa8066a6 | ||
|
|
8a89951286 | ||
|
|
c41ed55291 | ||
|
|
caba0499d7 | ||
|
|
13deacfc3b | ||
|
|
7231085456 | ||
|
|
663bb4aa10 | ||
|
|
18721b4542 | ||
|
|
19232722e2 | ||
|
|
d2eca68484 | ||
|
|
091167478e | ||
|
|
547b8abc56 | ||
|
|
dbfac45d59 | ||
|
|
086f6de867 | ||
|
|
0f93091848 | ||
|
|
ac19f62ed9 | ||
|
|
8fcef63890 | ||
|
|
34cd2c1afa | ||
|
|
5d1a19d49c | ||
|
|
2fbb320539 | ||
|
|
b50e5ed7e6 | ||
|
|
26c82ca72c | ||
|
|
f8a8a431a8 | ||
|
|
fd27867195 | ||
|
|
5e2a627795 | ||
|
|
e081401c2d | ||
|
|
e62a268aac | ||
|
|
140a9e5191 | ||
|
|
51b5f8046f | ||
|
|
91b965d1aa | ||
|
|
f8faccee59 | ||
|
|
6ac46a36a7 | ||
|
|
27572ba4bc | ||
|
|
21be832161 | ||
|
|
5769632793 | ||
|
|
9efd959ed8 | ||
|
|
5c72afe17e | ||
|
|
dc24319333 | ||
|
|
1acbdf94aa | ||
|
|
44229dfe51 | ||
|
|
13206227d9 | ||
|
|
e3e8a57df6 | ||
|
|
7afff80984 | ||
|
|
500522418e | ||
|
|
5ccac5a5c3 | ||
|
|
69822f4401 | ||
|
|
f290cfbd35 | ||
|
|
fae30f59ce | ||
|
|
a15f303c0a | ||
|
|
5e85038d78 | ||
|
|
4e7b35c54e | ||
|
|
48dde310ae | ||
|
|
6b041e7657 | ||
|
|
69576ee1e1 | ||
|
|
bd3105f62e | ||
|
|
02e3f0e71b | ||
|
|
74bf981f3d | ||
|
|
32f0678692 | ||
|
|
e74753bec1 | ||
|
|
4a8c9ad17f | ||
|
|
0c6c882f50 | ||
|
|
579f25c1ba | ||
|
|
d5eb5c8816 | ||
|
|
26d04b5f9c | ||
|
|
a0b9684504 | ||
|
|
402b6d1f0c | ||
|
|
e87a5fa354 | ||
|
|
63574f643e | ||
|
|
1d6b1020c3 | ||
|
|
96337a3230 | ||
|
|
950c59d8b4 | ||
|
|
9cfd095281 | ||
|
|
996e049863 | ||
|
|
6e42817c3e | ||
|
|
7758344a57 | ||
|
|
b5df566b6b | ||
|
|
e21fd19a70 | ||
|
|
98672c8e44 | ||
|
|
2e92f0640b | ||
|
|
d6b314f573 | ||
|
|
d7a150ffa8 | ||
|
|
955da6be30 | ||
|
|
b5f8e6738a | ||
|
|
d7fa8c9795 | ||
|
|
acb7a5b3c8 | ||
|
|
783a57ef89 | ||
|
|
d33ccce683 | ||
|
|
d722bf10b9 | ||
|
|
41337475a2 | ||
|
|
f504a069d7 | ||
|
|
96f79688dd | ||
|
|
c330898aeb | ||
|
|
94952cf0a4 | ||
|
|
a3c249abe9 | ||
|
|
c26562fab8 | ||
|
|
b7cbd99212 | ||
|
|
b53ef35a85 | ||
|
|
1ebb5fefb1 | ||
|
|
164b3edd41 | ||
|
|
819ea36cca | ||
|
|
088c9b9e4e | ||
|
|
ab25a9b8ee | ||
|
|
b2aea9f8f0 | ||
|
|
5399b95936 | ||
|
|
1d5c120748 | ||
|
|
6be7301e69 | ||
|
|
fff9f2b17d | ||
|
|
87a7d2e45c | ||
|
|
58803ef40d | ||
|
|
b27ca993aa | ||
|
|
28ecc6bcbf | ||
|
|
30c5e3f237 | ||
|
|
6a25a3a3d0 | ||
|
|
9a84c0ae30 | ||
|
|
133cc5457d | ||
|
|
05e4c5b507 | ||
|
|
8372717a09 | ||
|
|
5f3f2a96fd | ||
|
|
b83c6f0c28 | ||
|
|
36dc0f3e3c | ||
|
|
b4edde6f90 | ||
|
|
395eb5581c | ||
|
|
6dab251055 | ||
|
|
3bd6a6a614 | ||
|
|
93d21a0d1d | ||
|
|
2c2a73e320 | ||
|
|
a88a3c9ea3 | ||
|
|
e4df70cd56 | ||
|
|
90f7d35150 | ||
|
|
75b19a60c1 | ||
|
|
9fc921f49f | ||
|
|
4b9e90ac72 | ||
|
|
36dd6c83e0 | ||
|
|
d790313221 | ||
|
|
c3895f06c0 | ||
|
|
76e81fa651 | ||
|
|
d63a1b0eb0 | ||
|
|
4dffac3c78 | ||
|
|
88391b0274 | ||
|
|
c3cea0238e | ||
|
|
5bf4266973 | ||
|
|
ed57c54d06 | ||
|
|
01936e000e | ||
|
|
80137848dc | ||
|
|
149575e0c4 | ||
|
|
6a66b462ce | ||
|
|
1e31f6fe77 | ||
|
|
f173b3ff09 | ||
|
|
baffab62ce | ||
|
|
d141cec2a8 | ||
|
|
df87ac3810 | ||
|
|
46960ce0a4 | ||
|
|
522206b0bc | ||
|
|
24d60dd3ab | ||
|
|
dbfdc2fd14 | ||
|
|
a97912e297 | ||
|
|
cedba501ee | ||
|
|
ac947d3bda | ||
|
|
243ba371b3 | ||
|
|
3af24eafbb | ||
|
|
69b6022afe | ||
|
|
1cd66419d4 | ||
|
|
5309a02f6d | ||
|
|
541784382a | ||
|
|
2a7fae2c82 | ||
|
|
ed3a5448b1 | ||
|
|
2eb4769bbc | ||
|
|
02019000df | ||
|
|
6262f51c96 | ||
|
|
d38ff5759a | ||
|
|
f6de2e4dd3 | ||
|
|
3959f447d4 |
@@ -1,10 +1,12 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Ask for help on Discord before reporting a bug
|
||||
about: Follow the troubleshooting guide before reporting a bug
|
||||
|
||||
---
|
||||
**READ ME FIRST!**
|
||||
This issue tracker is not the right place to ask for troubleshooting help if you're not able to connect or stream at all. For those requests, please use the #pc-help channel on our Discord server where there are many more people who can help (or direct you back here if it looks like a Moonlight bug after all). https://moonlight-stream.org/discord
|
||||
If you're here because something basic is not working (like gamepad input, video, or similar), it's probably something specific to your setup, so make sure you've gone through the Troubleshooting Guide first: https://github.com/moonlight-stream/moonlight-docs/wiki/Troubleshooting
|
||||
|
||||
If you still have trouble with basic functionality after following the guide, join our Discord server where there are many other volunteers who can help (or direct you back here if it looks like a Moonlight bug after all). https://moonlight-stream.org/discord
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
@@ -26,14 +28,21 @@ List the games you've tried that exhibit the issue. To see if the issue is game-
|
||||
- If so, which settings have been changed?
|
||||
- Does the problem still occur after reverting settings back to default?
|
||||
|
||||
**Gamepad-related issues (please complete if problem is gamepad-related)**
|
||||
- Do you have any gamepads connected to your host PC directly?
|
||||
- Does the problem still remain if you stream the desktop and use https://html5gamepad.com to test your gamepad?
|
||||
- Instructions for streaming the desktop can be found here: https://github.com/moonlight-stream/moonlight-docs/wiki/Setup-Guide
|
||||
|
||||
**Client PC details (please complete the following information)**
|
||||
- OS: [e.g. Windows 10 1809]
|
||||
- Moonlight Version: [e.g. v0.9.0]
|
||||
- GPU: [e.g. Intel HD Graphics 520]
|
||||
|
||||
**Server PC details (please complete the following information)**
|
||||
- OS: [e.g. Windows 10 1809]
|
||||
- GeForce Experience version: [e.g. 3.16.0.140]
|
||||
- Nvidia GPU driver: [e.g. 417.35]
|
||||
- Antivirus and firewall software: [e.g. Windows Defender and Windows Firewall]
|
||||
|
||||
**Moonlight Logs (please attach)**
|
||||
- On Windows, `Moonlight-###.log` files can be found in `%TEMP%`. Simply type that into the File Explorer path field to navigate there.
|
||||
|
||||
@@ -13,3 +13,7 @@
|
||||
[submodule "h264bitstream/h264bitstream"]
|
||||
path = h264bitstream/h264bitstream
|
||||
url = https://github.com/aizvorski/h264bitstream.git
|
||||
[submodule "libs"]
|
||||
path = libs
|
||||
url = https://github.com/cgutman/moonlight-qt-prebuilts.git
|
||||
shallow = true
|
||||
|
||||
@@ -139,7 +139,7 @@ private:
|
||||
L"SS2OSD.dll", // ASUS Sonic Studio 2
|
||||
L"Nahimic2OSD.dll",
|
||||
L"NahimicMSIOSD.dll",
|
||||
L"nhAsusPhoebusOSD.dll" // ASUS Phoebus
|
||||
L"nhAsusPhoebusOSD.dll", // ASUS Phoebus
|
||||
|
||||
// This DLL has been seen in several crash reports. Some Googling
|
||||
// suggests it's highly unstable and causes issues in many games.
|
||||
|
||||
@@ -4,10 +4,11 @@
|
||||
|
||||
Moonlight also has mobile versions for [Android](https://github.com/moonlight-stream/moonlight-android) and [iOS](https://github.com/moonlight-stream/moonlight-ios).
|
||||
|
||||
You can follow development on our [Discord server](https://moonlight-stream.org/discord).
|
||||
You can follow development on our [Discord server](https://moonlight-stream.org/discord) and help translate Moonlight into your language on [Weblate](https://hosted.weblate.org/projects/moonlight/moonlight-qt/).
|
||||
|
||||
[](https://ci.appveyor.com/project/cgutman/moonlight-qt/branch/master)
|
||||
[](https://github.com/moonlight-stream/moonlight-qt/releases)
|
||||
[](https://hosted.weblate.org/projects/moonlight/moonlight-qt/)
|
||||
|
||||
## Features
|
||||
- Hardware accelerated video decoding on Windows, Mac, and Linux
|
||||
@@ -15,7 +16,7 @@ You can follow development on our [Discord server](https://moonlight-stream.org/
|
||||
- Supports streaming at 720p, 1080p, 1440p, 4K, and the client PC's native screen resolution
|
||||
- 7.1 surround sound audio support
|
||||
- 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 in fullscreen mode
|
||||
- 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
|
||||
@@ -28,6 +29,12 @@ You can follow development on our [Discord server](https://moonlight-stream.org/
|
||||
- [Raspberry Pi 4](https://github.com/moonlight-stream/moonlight-docs/wiki/Installing-Moonlight-Qt-on-Raspberry-Pi-4)
|
||||
- [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).
|
||||
|
||||
## Building
|
||||
|
||||
### Windows Build Requirements
|
||||
@@ -36,12 +43,12 @@ You can follow development on our [Discord server](https://moonlight-stream.org/
|
||||
* [Visual Studio 2019](https://visualstudio.microsoft.com/downloads/) (Community edition is fine)
|
||||
* Select **MSVC 2019** 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.11 or later (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 5.15 SDK or later
|
||||
* macOS High Sierra (10.13) or later
|
||||
* Xcode 11
|
||||
* Qt 6.2 SDK or later
|
||||
* macOS Mojave (10.14) or later
|
||||
* Xcode 13
|
||||
* [create-dmg](https://github.com/sindresorhus/create-dmg) (only if building DMGs for use on non-development Macs)
|
||||
|
||||
### Linux/Unix Build Requirements
|
||||
@@ -49,7 +56,7 @@ You can follow development on our [Discord server](https://moonlight-stream.org/
|
||||
* 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 qt5-default 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 qt5-devel SDL2-devel ffmpeg-devel qt5-qtquickcontrols2-devel libva-devel libvdpau-devel opus-devel pulseaudio-libs-devel alsa-lib-devel SDL2_ttf-devel`
|
||||
* RedHat/Fedora: `openssl-devel SDL2-devel SDL2_ttf-devel ffmpeg-devel qt5-qtsvg-devel qt5-qtquickcontrols2-devel libva-devel libvdpau-devel opus-devel pulseaudio-libs-devel alsa-lib-devel`
|
||||
* FFmpeg 4.0+ is required to build. If your distro doesn't package FFmpeg 4.0 or later, you can build and install it from source on https://ffmpeg.org/
|
||||
|
||||
### Steam Link Build Requirements
|
||||
@@ -68,6 +75,8 @@ You can follow development on our [Discord server](https://moonlight-stream.org/
|
||||
* 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 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.
|
||||
|
||||
## Contribute
|
||||
1. Fork us
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>10.13</string>
|
||||
<string>10.14</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string>NSApplication</string>
|
||||
<key>NSSupportsAutomaticGraphicsSwitching</key>
|
||||
|
||||
+1
-1
Submodule app/SDL_GameControllerDB updated: f0ae9dea50...2924d8a562
+49
-7
@@ -33,19 +33,21 @@ DEFINES += QT_DEPRECATED_WARNINGS
|
||||
DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
|
||||
|
||||
win32 {
|
||||
INCLUDEPATH += $$PWD/../libs/windows/include
|
||||
|
||||
contains(QT_ARCH, i386) {
|
||||
LIBS += -L$$PWD/../libs/windows/lib/x86
|
||||
INCLUDEPATH += $$PWD/../libs/windows/include/x86
|
||||
}
|
||||
contains(QT_ARCH, x86_64) {
|
||||
LIBS += -L$$PWD/../libs/windows/lib/x64
|
||||
INCLUDEPATH += $$PWD/../libs/windows/include/x64
|
||||
}
|
||||
contains(QT_ARCH, arm64) {
|
||||
LIBS += -L$$PWD/../libs/windows/lib/arm64
|
||||
INCLUDEPATH += $$PWD/../libs/windows/include/arm64
|
||||
}
|
||||
|
||||
LIBS += ws2_32.lib winmm.lib dxva2.lib ole32.lib gdi32.lib user32.lib d3d9.lib dwmapi.lib dbghelp.lib qwave.lib
|
||||
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
|
||||
}
|
||||
macx {
|
||||
INCLUDEPATH += $$PWD/../libs/mac/include
|
||||
@@ -88,6 +90,11 @@ unix:!macx {
|
||||
PKGCONFIG += libdrm
|
||||
CONFIG += libdrm
|
||||
}
|
||||
|
||||
packagesExist(ffnvcodec) {
|
||||
PKGCONFIG += ffnvcodec
|
||||
CONFIG += cuda
|
||||
}
|
||||
}
|
||||
|
||||
packagesExist(wayland-client) {
|
||||
@@ -118,6 +125,7 @@ macx {
|
||||
}
|
||||
|
||||
SOURCES += \
|
||||
backend/nvaddress.cpp \
|
||||
backend/nvapp.cpp \
|
||||
main.cpp \
|
||||
backend/computerseeker.cpp \
|
||||
@@ -131,6 +139,7 @@ SOURCES += \
|
||||
cli/commandlineparser.cpp \
|
||||
cli/quitstream.cpp \
|
||||
cli/startstream.cpp \
|
||||
settings/compatfetcher.cpp \
|
||||
settings/mappingfetcher.cpp \
|
||||
settings/streamingpreferences.cpp \
|
||||
streaming/input/abstouch.cpp \
|
||||
@@ -154,7 +163,9 @@ SOURCES += \
|
||||
wm.cpp
|
||||
|
||||
HEADERS += \
|
||||
backend/nvaddress.h \
|
||||
backend/nvapp.h \
|
||||
settings/compatfetcher.h \
|
||||
settings/mappingfetcher.h \
|
||||
utils.h \
|
||||
backend/computerseeker.h \
|
||||
@@ -192,7 +203,6 @@ ffmpeg {
|
||||
SOURCES += \
|
||||
streaming/video/ffmpeg.cpp \
|
||||
streaming/video/ffmpeg-renderers/sdlvid.cpp \
|
||||
streaming/video/ffmpeg-renderers/cuda.cpp \
|
||||
streaming/video/ffmpeg-renderers/pacer/pacer.cpp \
|
||||
streaming/video/ffmpeg-renderers/pacer/nullthreadedvsyncsource.cpp
|
||||
|
||||
@@ -200,7 +210,6 @@ ffmpeg {
|
||||
streaming/video/ffmpeg.h \
|
||||
streaming/video/ffmpeg-renderers/renderer.h \
|
||||
streaming/video/ffmpeg-renderers/sdlvid.h \
|
||||
streaming/video/ffmpeg-renderers/cuda.h \
|
||||
streaming/video/ffmpeg-renderers/pacer/pacer.h \
|
||||
streaming/video/ffmpeg-renderers/pacer/nullthreadedvsyncsource.h
|
||||
}
|
||||
@@ -250,6 +259,19 @@ libdrm {
|
||||
DEFINES += HAVE_DRM
|
||||
SOURCES += streaming/video/ffmpeg-renderers/drm.cpp
|
||||
HEADERS += streaming/video/ffmpeg-renderers/drm.h
|
||||
|
||||
linux {
|
||||
message(Master hooks enabled)
|
||||
SOURCES += masterhook.c
|
||||
LIBS += -ldl
|
||||
}
|
||||
}
|
||||
cuda {
|
||||
message(CUDA support enabled)
|
||||
|
||||
DEFINES += HAVE_CUDA
|
||||
SOURCES += streaming/video/ffmpeg-renderers/cuda.cpp
|
||||
HEADERS += streaming/video/ffmpeg-renderers/cuda.h
|
||||
}
|
||||
config_EGL {
|
||||
message(EGL renderer selected)
|
||||
@@ -264,7 +286,7 @@ config_EGL {
|
||||
config_SL {
|
||||
message(Steam Link build configuration selected)
|
||||
|
||||
DEFINES += STEAM_LINK HAVE_SLVIDEO HAVE_SLAUDIO
|
||||
DEFINES += EMBEDDED_BUILD STEAM_LINK HAVE_SLVIDEO HAVE_SLAUDIO
|
||||
LIBS += -lSLVideo -lSLAudio
|
||||
|
||||
SOURCES += \
|
||||
@@ -307,13 +329,33 @@ discord-rpc {
|
||||
LIBS += -ldiscord-rpc
|
||||
DEFINES += HAVE_DISCORD
|
||||
}
|
||||
embedded {
|
||||
message(Embedded build)
|
||||
|
||||
DEFINES += EMBEDDED_BUILD
|
||||
}
|
||||
|
||||
RESOURCES += \
|
||||
resources.qrc \
|
||||
qml.qrc
|
||||
|
||||
TRANSLATIONS += \
|
||||
languages/qml_fr.ts
|
||||
languages/qml_zh_cn.ts \
|
||||
languages/qml_de.ts \
|
||||
languages/qml_fr.ts \
|
||||
languages/qml_nb_NO.ts \
|
||||
languages/qml_ru.ts \
|
||||
languages/qml_es.ts \
|
||||
languages/qml_ja.ts \
|
||||
languages/qml_vi.ts \
|
||||
languages/qml_th.ts \
|
||||
languages/qml_ko.ts \
|
||||
languages/qml_hu.ts \
|
||||
languages/qml_nl.ts \
|
||||
languages/qml_sv.ts \
|
||||
languages/qml_tr.ts \
|
||||
languages/qml_uk.ts \
|
||||
languages/qml_zh_Hant.ts
|
||||
|
||||
# Additional import path used to resolve QML modules in Qt Creator's code model
|
||||
QML_IMPORT_PATH =
|
||||
|
||||
@@ -15,8 +15,8 @@ AutoUpdateChecker::AutoUpdateChecker(QObject *parent) :
|
||||
// Allow HTTP redirects
|
||||
m_Nam.setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy);
|
||||
|
||||
connect(&m_Nam, SIGNAL(finished(QNetworkReply*)),
|
||||
this, SLOT(handleUpdateCheckRequestFinished(QNetworkReply*)));
|
||||
connect(&m_Nam, &QNetworkAccessManager::finished,
|
||||
this, &AutoUpdateChecker::handleUpdateCheckRequestFinished);
|
||||
|
||||
QString currentVersion(VERSION_STR);
|
||||
qDebug() << "Current Moonlight version:" << currentVersion;
|
||||
@@ -63,11 +63,41 @@ QString AutoUpdateChecker::getPlatform()
|
||||
return QStringLiteral("steamlink");
|
||||
#elif defined(APP_IMAGE)
|
||||
return QStringLiteral("appimage");
|
||||
#elif defined(Q_OS_DARWIN) && QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
// Qt 6 changed this from 'osx' to 'macos'. Use the old one
|
||||
// to be consistent (and not require another entry in the manifest).
|
||||
return QStringLiteral("osx");
|
||||
#else
|
||||
return QSysInfo::productType();
|
||||
#endif
|
||||
}
|
||||
|
||||
int AutoUpdateChecker::compareVersion(QVector<int>& version1, QVector<int>& version2) {
|
||||
for (int i = 0;; i++) {
|
||||
int v1Val = 0;
|
||||
int v2Val = 0;
|
||||
|
||||
// Treat missing decimal places as 0
|
||||
if (i < version1.count()) {
|
||||
v1Val = version1[i];
|
||||
}
|
||||
if (i < version2.count()) {
|
||||
v2Val = version2[i];
|
||||
}
|
||||
if (i >= version1.count() && i >= version2.count()) {
|
||||
// Equal versions
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (v1Val < v2Val) {
|
||||
return -1;
|
||||
}
|
||||
else if (v1Val > v2Val) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AutoUpdateChecker::handleUpdateCheckRequestFinished(QNetworkReply* reply)
|
||||
{
|
||||
Q_ASSERT(reply->isFinished());
|
||||
@@ -119,46 +149,47 @@ void AutoUpdateChecker::handleUpdateCheckRequestFinished(QNetworkReply* reply)
|
||||
|
||||
if (updateObj["arch"] == QSysInfo::buildCpuArchitecture() &&
|
||||
updateObj["platform"] == getPlatform()) {
|
||||
|
||||
// Check the kernel version minimum if one exists
|
||||
if (updateObj.contains("kernel_version_at_least") && updateObj["kernel_version_at_least"].isString()) {
|
||||
QVector<int> requiredVersionQuad;
|
||||
QVector<int> actualVersionQuad;
|
||||
|
||||
QString requiredVersion = updateObj["kernel_version_at_least"].toString();
|
||||
QString actualVersion = QSysInfo::kernelVersion();
|
||||
parseStringToVersionQuad(requiredVersion, requiredVersionQuad);
|
||||
parseStringToVersionQuad(actualVersion, actualVersionQuad);
|
||||
|
||||
if (compareVersion(actualVersionQuad, requiredVersionQuad) < 0) {
|
||||
qDebug() << "Skipping manifest entry due to kernel version (" << actualVersion << "<" << requiredVersion << ")";
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
qDebug() << "Found update manifest match for current platform";
|
||||
|
||||
QString latestVersion = updateObj["version"].toString();
|
||||
qDebug() << "Latest version of Moonlight for this platform is:" << latestVersion;
|
||||
|
||||
QVector<int> latestVersionQuad;
|
||||
|
||||
parseStringToVersionQuad(latestVersion, latestVersionQuad);
|
||||
|
||||
for (int i = 0;; i++) {
|
||||
int latestVer = 0;
|
||||
int currentVer = 0;
|
||||
|
||||
// Treat missing decimal places as 0
|
||||
if (i < latestVersionQuad.count()) {
|
||||
latestVer = latestVersionQuad[i];
|
||||
}
|
||||
if (i < m_CurrentVersionQuad.count()) {
|
||||
currentVer = m_CurrentVersionQuad[i];
|
||||
}
|
||||
if (i >= latestVersionQuad.count() && i >= m_CurrentVersionQuad.count()) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (currentVer < latestVer) {
|
||||
qDebug() << "Update available";
|
||||
|
||||
emit onUpdateAvailable(updateObj["version"].toString(),
|
||||
updateObj["browser_url"].toString());
|
||||
return;
|
||||
}
|
||||
else if (currentVer > latestVer) {
|
||||
qDebug() << "Update manifest version lower than current version";
|
||||
return;
|
||||
}
|
||||
int res = compareVersion(m_CurrentVersionQuad, latestVersionQuad);
|
||||
if (res < 0) {
|
||||
// m_CurrentVersionQuad < latestVersionQuad
|
||||
qDebug() << "Update available";
|
||||
emit onUpdateAvailable(updateObj["version"].toString(),
|
||||
updateObj["browser_url"].toString());
|
||||
return;
|
||||
}
|
||||
else if (res > 0) {
|
||||
qDebug() << "Update manifest version lower than current version";
|
||||
return;
|
||||
}
|
||||
else {
|
||||
qDebug() << "Update manifest version equal to current version";
|
||||
return;
|
||||
}
|
||||
|
||||
qDebug() << "Update manifest version equal to current version";
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
else {
|
||||
@@ -166,8 +197,8 @@ void AutoUpdateChecker::handleUpdateCheckRequestFinished(QNetworkReply* reply)
|
||||
}
|
||||
}
|
||||
|
||||
qWarning() << "No entry in update manifest found for current platform: "
|
||||
<< QSysInfo::buildCpuArchitecture() << getPlatform();
|
||||
qWarning() << "No entry in update manifest found for current platform:"
|
||||
<< QSysInfo::buildCpuArchitecture() << getPlatform() << QSysInfo::kernelVersion();
|
||||
}
|
||||
else {
|
||||
qWarning() << "Update checking failed with error: " << reply->error();
|
||||
|
||||
@@ -20,6 +20,8 @@ private slots:
|
||||
private:
|
||||
void parseStringToVersionQuad(QString& string, QVector<int>& version);
|
||||
|
||||
int compareVersion(QVector<int>& version1, QVector<int>& version2);
|
||||
|
||||
QString getPlatform();
|
||||
|
||||
QVector<int> m_CurrentVersionQuad;
|
||||
|
||||
@@ -46,8 +46,8 @@ public:
|
||||
m_Computer(computer),
|
||||
m_App(app)
|
||||
{
|
||||
connect(this, SIGNAL(boxArtFetchCompleted(NvComputer*,NvApp,QUrl)),
|
||||
boxArtManager, SLOT(handleBoxArtLoadComplete(NvComputer*,NvApp,QUrl)));
|
||||
connect(this, &NetworkBoxArtLoadTask::boxArtFetchCompleted,
|
||||
boxArtManager, &BoxArtManager::handleBoxArtLoadComplete);
|
||||
}
|
||||
|
||||
signals:
|
||||
@@ -106,7 +106,7 @@ void BoxArtManager::handleBoxArtLoadComplete(NvComputer* computer, NvApp app, QU
|
||||
|
||||
QUrl BoxArtManager::loadBoxArtFromNetwork(NvComputer* computer, int appId)
|
||||
{
|
||||
NvHTTP http(computer->activeAddress, computer->serverCert);
|
||||
NvHTTP http(computer);
|
||||
|
||||
QString cachePath = getFilePathForBoxArt(computer, appId);
|
||||
QImage image;
|
||||
|
||||
@@ -27,9 +27,9 @@ public:
|
||||
}
|
||||
|
||||
private:
|
||||
bool tryPollComputer(QString address, bool& changed)
|
||||
bool tryPollComputer(NvAddress address, bool& changed)
|
||||
{
|
||||
NvHTTP http(address, m_Computer->serverCert);
|
||||
NvHTTP http(address, 0, m_Computer->serverCert);
|
||||
|
||||
QString serverInfo;
|
||||
try {
|
||||
@@ -38,7 +38,7 @@ private:
|
||||
return false;
|
||||
}
|
||||
|
||||
NvComputer newState(address, serverInfo, QSslCertificate());
|
||||
NvComputer newState(http, serverInfo);
|
||||
|
||||
// Ensure the machine that responded is the one we intended to contact
|
||||
if (m_Computer->uuid != newState.uuid) {
|
||||
@@ -52,9 +52,7 @@ private:
|
||||
|
||||
bool updateAppList(bool& changed)
|
||||
{
|
||||
Q_ASSERT(m_Computer->activeAddress != nullptr);
|
||||
|
||||
NvHTTP http(m_Computer->activeAddress, m_Computer->serverCert);
|
||||
NvHTTP http(m_Computer);
|
||||
|
||||
QVector<NvApp> appList;
|
||||
|
||||
@@ -88,7 +86,7 @@ private:
|
||||
|
||||
if (tryPollComputer(address, stateChanged)) {
|
||||
if (!wasOnline) {
|
||||
qInfo() << m_Computer->name << "is now online at" << m_Computer->activeAddress;
|
||||
qInfo() << m_Computer->name << "is now online at" << m_Computer->activeAddress.toString();
|
||||
}
|
||||
online = true;
|
||||
break;
|
||||
@@ -146,7 +144,8 @@ private:
|
||||
ComputerManager::ComputerManager(QObject *parent)
|
||||
: QObject(parent),
|
||||
m_PollingRef(0),
|
||||
m_MdnsBrowser(nullptr)
|
||||
m_MdnsBrowser(nullptr),
|
||||
m_CompatFetcher(nullptr)
|
||||
{
|
||||
QSettings settings;
|
||||
|
||||
@@ -159,12 +158,15 @@ ComputerManager::ComputerManager(QObject *parent)
|
||||
}
|
||||
settings.endArray();
|
||||
|
||||
// Fetch latest compatibility data asynchronously
|
||||
m_CompatFetcher.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
|
||||
// while quitting, however this is a one time signal - additional
|
||||
// requests would not be aborted and block termination.
|
||||
connect(QCoreApplication::instance(), SIGNAL(aboutToQuit()), this, SLOT(handleAboutToQuit()));
|
||||
connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this, &ComputerManager::handleAboutToQuit);
|
||||
}
|
||||
|
||||
ComputerManager::~ComputerManager()
|
||||
@@ -302,8 +304,8 @@ void ComputerManager::startPollingComputer(NvComputer* computer)
|
||||
|
||||
if (!pollingEntry->isActive()) {
|
||||
PcMonitorThread* thread = new PcMonitorThread(computer);
|
||||
connect(thread, SIGNAL(computerStateChanged(NvComputer*)),
|
||||
this, SLOT(handleComputerStateChanged(NvComputer*)));
|
||||
connect(thread, &PcMonitorThread::computerStateChanged,
|
||||
this, &ComputerManager::handleComputerStateChanged);
|
||||
pollingEntry->setActiveThread(thread);
|
||||
thread->start();
|
||||
}
|
||||
@@ -322,7 +324,7 @@ void ComputerManager::handleMdnsServiceResolved(MdnsPendingComputer* computer,
|
||||
// address may not be reachable (if the user hasn't installed the IPv6 helper yet
|
||||
// or if this host lacks outbound IPv6 capability). We want to add IPv6 even if
|
||||
// it's not currently reachable.
|
||||
addNewHost(address.toString(), true, v6Global);
|
||||
addNewHost(NvAddress(address, computer->port()), true, NvAddress(v6Global, computer->port()));
|
||||
added = true;
|
||||
break;
|
||||
}
|
||||
@@ -336,8 +338,7 @@ void ComputerManager::handleMdnsServiceResolved(MdnsPendingComputer* computer,
|
||||
if (address.isInSubnet(QHostAddress("fe80::"), 10) ||
|
||||
address.isInSubnet(QHostAddress("fec0::"), 10) ||
|
||||
address.isInSubnet(QHostAddress("fc00::"), 7)) {
|
||||
addNewHost(address.toString(), true, v6Global);
|
||||
added = true;
|
||||
addNewHost(NvAddress(address, computer->port()), true, NvAddress(v6Global, computer->port()));
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -418,7 +419,7 @@ void ComputerManager::deleteHost(NvComputer* computer)
|
||||
void ComputerManager::renameHost(NvComputer* computer, QString name)
|
||||
{
|
||||
{
|
||||
QWriteLocker(&computer->lock);
|
||||
QWriteLocker lock(&computer->lock);
|
||||
|
||||
computer->name = name;
|
||||
computer->hasCustomName = true;
|
||||
@@ -467,7 +468,7 @@ signals:
|
||||
private:
|
||||
void run()
|
||||
{
|
||||
NvPairingManager pairingManager(m_Computer->activeAddress);
|
||||
NvPairingManager pairingManager(m_Computer);
|
||||
|
||||
try {
|
||||
NvPairingManager::PairState result = pairingManager.pair(m_Computer->appVersion, m_Pin, m_Computer->serverCert);
|
||||
@@ -523,7 +524,7 @@ signals:
|
||||
private:
|
||||
void run()
|
||||
{
|
||||
NvHTTP http(m_Computer->activeAddress, m_Computer->serverCert);
|
||||
NvHTTP http(m_Computer);
|
||||
|
||||
try {
|
||||
if (m_Computer->currentGameId != 0) {
|
||||
@@ -589,12 +590,24 @@ void ComputerManager::stopPollingAsync()
|
||||
}
|
||||
}
|
||||
|
||||
void ComputerManager::addNewHostManually(QString address)
|
||||
{
|
||||
QUrl url = QUrl::fromUserInput(address);
|
||||
if (url.isValid() && !url.host().isEmpty()) {
|
||||
// If there wasn't a port specified, use the default
|
||||
addNewHost(NvAddress(url.host(), url.port(DEFAULT_HTTP_PORT)), false);
|
||||
}
|
||||
else {
|
||||
emit computerAddCompleted(false, false);
|
||||
}
|
||||
}
|
||||
|
||||
class PendingAddTask : public QObject, public QRunnable
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
PendingAddTask(ComputerManager* computerManager, QString address, QHostAddress mdnsIpv6Address, bool mdns)
|
||||
PendingAddTask(ComputerManager* computerManager, NvAddress address, NvAddress mdnsIpv6Address, bool mdns)
|
||||
: m_ComputerManager(computerManager),
|
||||
m_Address(address),
|
||||
m_MdnsIpv6Address(mdnsIpv6Address),
|
||||
@@ -658,15 +671,15 @@ private:
|
||||
|
||||
void run()
|
||||
{
|
||||
NvHTTP http(m_Address, QSslCertificate());
|
||||
NvHTTP http(m_Address, 0, QSslCertificate());
|
||||
|
||||
qInfo() << "Processing new PC at" << m_Address << "from" << (m_Mdns ? "mDNS" : "user") << m_MdnsIpv6Address;
|
||||
qInfo() << "Processing new PC at" << m_Address.toString() << "from" << (m_Mdns ? "mDNS" : "user") << "with IPv6 address" << m_MdnsIpv6Address.toString();
|
||||
|
||||
// Perform initial serverinfo fetch over HTTP since we don't know which cert to use
|
||||
QString serverInfo = fetchServerInfo(http);
|
||||
if (serverInfo.isEmpty() && !m_MdnsIpv6Address.isNull()) {
|
||||
// Retry using the global IPv6 address if the IPv4 or link-local IPv6 address fails
|
||||
http.setAddress(m_MdnsIpv6Address.toString());
|
||||
http.setAddress(m_MdnsIpv6Address);
|
||||
serverInfo = fetchServerInfo(http);
|
||||
}
|
||||
if (serverInfo.isEmpty()) {
|
||||
@@ -674,7 +687,7 @@ private:
|
||||
}
|
||||
|
||||
// Create initial newComputer using HTTP serverinfo with no pinned cert
|
||||
NvComputer* newComputer = new NvComputer(http.address(), serverInfo, QSslCertificate());
|
||||
NvComputer* newComputer = new NvComputer(http, serverInfo);
|
||||
|
||||
// Check if we have a record of this host UUID to pull the pinned cert
|
||||
NvComputer* existingComputer;
|
||||
@@ -688,13 +701,14 @@ private:
|
||||
|
||||
// Fetch serverinfo again over HTTPS with the pinned cert
|
||||
if (existingComputer != nullptr) {
|
||||
Q_ASSERT(http.httpsPort() != 0);
|
||||
serverInfo = fetchServerInfo(http);
|
||||
if (serverInfo.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the polled computer with the HTTPS information
|
||||
NvComputer httpsComputer(http.address(), serverInfo, QSslCertificate());
|
||||
NvComputer httpsComputer(http, serverInfo);
|
||||
newComputer->update(httpsComputer);
|
||||
}
|
||||
|
||||
@@ -708,11 +722,11 @@ private:
|
||||
}
|
||||
|
||||
// Get the WAN IP address using STUN if we're on mDNS over IPv4
|
||||
if (QHostAddress(newComputer->localAddress).protocol() == QAbstractSocket::IPv4Protocol) {
|
||||
if (QHostAddress(newComputer->localAddress.address()).protocol() == QAbstractSocket::IPv4Protocol) {
|
||||
quint32 addr;
|
||||
int err = LiFindExternalAddressIP4("stun.moonlight-stream.org", 3478, &addr);
|
||||
if (err == 0) {
|
||||
newComputer->remoteAddress = QHostAddress(qFromBigEndian(addr)).toString();
|
||||
newComputer->setRemoteAddress(QHostAddress(qFromBigEndian(addr)));
|
||||
}
|
||||
else {
|
||||
qWarning() << "STUN failed to get WAN address:" << err;
|
||||
@@ -720,15 +734,15 @@ private:
|
||||
}
|
||||
|
||||
if (!m_MdnsIpv6Address.isNull()) {
|
||||
Q_ASSERT(m_MdnsIpv6Address.protocol() == QAbstractSocket::IPv6Protocol);
|
||||
newComputer->ipv6Address = m_MdnsIpv6Address.toString();
|
||||
Q_ASSERT(QHostAddress(m_MdnsIpv6Address.address()).protocol() == QAbstractSocket::IPv6Protocol);
|
||||
newComputer->ipv6Address = m_MdnsIpv6Address;
|
||||
}
|
||||
}
|
||||
else {
|
||||
newComputer->manualAddress = m_Address;
|
||||
}
|
||||
|
||||
QHostAddress hostAddress(m_Address);
|
||||
QHostAddress hostAddress(m_Address.address());
|
||||
bool addressIsSiteLocalV4 =
|
||||
hostAddress.isInSubnet(QHostAddress("10.0.0.0"), 8) ||
|
||||
hostAddress.isInSubnet(QHostAddress("172.16.0.0"), 12) ||
|
||||
@@ -753,7 +767,7 @@ private:
|
||||
|
||||
// Tell our client if something changed
|
||||
if (changed) {
|
||||
qInfo() << existingComputer->name << "is now at" << existingComputer->activeAddress;
|
||||
qInfo() << existingComputer->name << "is now at" << existingComputer->activeAddress.toString();
|
||||
emit computerStateChanged(existingComputer);
|
||||
}
|
||||
}
|
||||
@@ -774,7 +788,7 @@ private:
|
||||
int err = LiFindExternalAddressIP4("stun.moonlight-stream.org", 3478, &addr);
|
||||
if (err == 0) {
|
||||
lock.relock();
|
||||
newComputer->remoteAddress = QHostAddress(qFromBigEndian(addr)).toString();
|
||||
newComputer->setRemoteAddress(QHostAddress(qFromBigEndian(addr)));
|
||||
lock.unlock();
|
||||
}
|
||||
else {
|
||||
@@ -794,12 +808,12 @@ private:
|
||||
}
|
||||
|
||||
ComputerManager* m_ComputerManager;
|
||||
QString m_Address;
|
||||
QHostAddress m_MdnsIpv6Address;
|
||||
NvAddress m_Address;
|
||||
NvAddress m_MdnsIpv6Address;
|
||||
bool m_Mdns;
|
||||
};
|
||||
|
||||
void ComputerManager::addNewHost(QString address, bool mdns, QHostAddress mdnsIpv6Address)
|
||||
void ComputerManager::addNewHost(NvAddress address, bool mdns, NvAddress mdnsIpv6Address)
|
||||
{
|
||||
// Punt to a worker thread to avoid stalling the
|
||||
// UI while waiting for serverinfo query to complete
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "nvcomputer.h"
|
||||
#include "nvpairingmanager.h"
|
||||
#include "settings/compatfetcher.h"
|
||||
|
||||
#include <qmdnsengine/server.h>
|
||||
#include <qmdnsengine/cache.h>
|
||||
@@ -23,6 +24,7 @@ public:
|
||||
explicit MdnsPendingComputer(QMdnsEngine::Server* server,
|
||||
const QMdnsEngine::Service& service)
|
||||
: m_Hostname(service.hostname()),
|
||||
m_Port(service.port()),
|
||||
m_Server(server),
|
||||
m_Resolver(nullptr)
|
||||
{
|
||||
@@ -40,6 +42,11 @@ public:
|
||||
return m_Hostname;
|
||||
}
|
||||
|
||||
uint16_t port()
|
||||
{
|
||||
return m_Port;
|
||||
}
|
||||
|
||||
private slots:
|
||||
void handleResolvedTimeout()
|
||||
{
|
||||
@@ -70,10 +77,11 @@ private:
|
||||
m_Resolver = new QMdnsEngine::Resolver(m_Server, m_Hostname);
|
||||
connect(m_Resolver, &QMdnsEngine::Resolver::resolved,
|
||||
this, &MdnsPendingComputer::handleResolvedAddress);
|
||||
QTimer::singleShot(2000, this, SLOT(handleResolvedTimeout()));
|
||||
QTimer::singleShot(2000, this, &MdnsPendingComputer::handleResolvedTimeout);
|
||||
}
|
||||
|
||||
QByteArray m_Hostname;
|
||||
uint16_t m_Port;
|
||||
QMdnsEngine::Server* m_Server;
|
||||
QMdnsEngine::Resolver* m_Resolver;
|
||||
QVector<QHostAddress> m_Addresses;
|
||||
@@ -168,7 +176,9 @@ public:
|
||||
|
||||
Q_INVOKABLE void stopPollingAsync();
|
||||
|
||||
Q_INVOKABLE void addNewHost(QString address, bool mdns, QHostAddress mdnsIpv6Address = QHostAddress());
|
||||
Q_INVOKABLE void addNewHostManually(QString address);
|
||||
|
||||
void addNewHost(NvAddress address, bool mdns, NvAddress mdnsIpv6Address = NvAddress());
|
||||
|
||||
void pairHost(NvComputer* computer, QString pin);
|
||||
|
||||
@@ -214,4 +224,5 @@ private:
|
||||
QMdnsEngine::Browser* m_MdnsBrowser;
|
||||
QMdnsEngine::Cache m_MdnsCache;
|
||||
QVector<MdnsPendingComputer*> m_PendingResolution;
|
||||
CompatFetcher m_CompatFetcher;
|
||||
};
|
||||
|
||||
@@ -20,7 +20,7 @@ void ComputerSeeker::start(int timeout)
|
||||
// if m_ComputerName is UUID, or the name that doesn't resolve to an IP
|
||||
// address) and by polling it using mDNS, hopefully one of these methods
|
||||
// would find the host
|
||||
m_ComputerManager->addNewHost(m_ComputerName, false);
|
||||
m_ComputerManager->addNewHostManually(m_ComputerName);
|
||||
m_ComputerManager->startPolling();
|
||||
}
|
||||
|
||||
@@ -44,8 +44,8 @@ bool ComputerSeeker::matchComputer(NvComputer *computer) const
|
||||
return true;
|
||||
}
|
||||
|
||||
for (const QString& addr : computer->uniqueAddresses()) {
|
||||
if (addr.toLower() == value) {
|
||||
for (const NvAddress& addr : computer->uniqueAddresses()) {
|
||||
if (addr.address().toLower() == value || addr.toString().toLower() == value) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,7 +143,10 @@ QSslKey
|
||||
IdentityManager::getSslKey()
|
||||
{
|
||||
if (m_CachedSslKey.isNull()) {
|
||||
BIO* bio = BIO_new_mem_buf(m_CachedPrivateKey.data(), -1);
|
||||
// This seemingly useless const_cast is required for old OpenSSL headers
|
||||
// where BIO_new_mem_buf's parameter is not declared const like those on
|
||||
// the Steam Link hardware.
|
||||
BIO* bio = BIO_new_mem_buf(const_cast<char*>(m_CachedPrivateKey.constData()), -1);
|
||||
THROW_BAD_ALLOC_IF_NULL(bio);
|
||||
|
||||
EVP_PKEY* pk = PEM_read_bio_PrivateKey(bio, nullptr, nullptr, nullptr);
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
#include "nvaddress.h"
|
||||
|
||||
#include <QHostAddress>
|
||||
|
||||
NvAddress::NvAddress()
|
||||
{
|
||||
setAddress(nullptr);
|
||||
setPort(0);
|
||||
}
|
||||
|
||||
NvAddress::NvAddress(QString addr, uint16_t port)
|
||||
{
|
||||
setAddress(addr);
|
||||
setPort(port);
|
||||
}
|
||||
|
||||
NvAddress::NvAddress(QHostAddress addr, uint16_t port)
|
||||
{
|
||||
setAddress(addr);
|
||||
setPort(port);
|
||||
}
|
||||
|
||||
uint16_t NvAddress::port() const
|
||||
{
|
||||
return m_Port;
|
||||
}
|
||||
|
||||
QString NvAddress::address() const
|
||||
{
|
||||
return m_Address;
|
||||
}
|
||||
|
||||
void NvAddress::setPort(uint16_t port)
|
||||
{
|
||||
m_Port = port;
|
||||
}
|
||||
|
||||
void NvAddress::setAddress(QString addr)
|
||||
{
|
||||
m_Address = addr;
|
||||
}
|
||||
|
||||
void NvAddress::setAddress(QHostAddress addr)
|
||||
{
|
||||
m_Address = addr.toString();
|
||||
}
|
||||
|
||||
bool NvAddress::isNull() const
|
||||
{
|
||||
return m_Address.isEmpty();
|
||||
}
|
||||
|
||||
QString NvAddress::toString() const
|
||||
{
|
||||
if (m_Address.isEmpty()) {
|
||||
return "<NULL>";
|
||||
}
|
||||
|
||||
if (QHostAddress(m_Address).protocol() == QAbstractSocket::IPv6Protocol) {
|
||||
return QString("[%1]:%2").arg(m_Address).arg(m_Port);
|
||||
}
|
||||
else {
|
||||
return QString("%1:%2").arg(m_Address).arg(m_Port);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
#pragma once
|
||||
|
||||
#include <QHostAddress>
|
||||
|
||||
#define DEFAULT_HTTP_PORT 47989
|
||||
#define DEFAULT_HTTPS_PORT 47984
|
||||
|
||||
class NvAddress
|
||||
{
|
||||
public:
|
||||
NvAddress();
|
||||
explicit NvAddress(QString addr, uint16_t port);
|
||||
explicit NvAddress(QHostAddress addr, uint16_t port);
|
||||
|
||||
uint16_t port() const;
|
||||
void setPort(uint16_t port);
|
||||
|
||||
QString address() const;
|
||||
void setAddress(QString addr);
|
||||
void setAddress(QHostAddress addr);
|
||||
|
||||
bool isNull() const;
|
||||
QString toString() const;
|
||||
|
||||
bool operator==(const NvAddress& other) const
|
||||
{
|
||||
return m_Address == other.m_Address &&
|
||||
m_Port == other.m_Port;
|
||||
}
|
||||
|
||||
bool operator!=(const NvAddress& other) const
|
||||
{
|
||||
return !operator==(other);
|
||||
}
|
||||
|
||||
private:
|
||||
QString m_Address;
|
||||
uint16_t m_Port;
|
||||
};
|
||||
+76
-27
@@ -1,5 +1,6 @@
|
||||
#include "nvcomputer.h"
|
||||
#include "nvapp.h"
|
||||
#include "settings/compatfetcher.h"
|
||||
|
||||
#include <QUdpSocket>
|
||||
#include <QHostInfo>
|
||||
@@ -10,9 +11,13 @@
|
||||
#define SER_UUID "uuid"
|
||||
#define SER_MAC "mac"
|
||||
#define SER_LOCALADDR "localaddress"
|
||||
#define SER_LOCALPORT "localport"
|
||||
#define SER_REMOTEADDR "remoteaddress"
|
||||
#define SER_REMOTEPORT "remoteport"
|
||||
#define SER_MANUALADDR "manualaddress"
|
||||
#define SER_MANUALPORT "manualport"
|
||||
#define SER_IPV6ADDR "ipv6address"
|
||||
#define SER_IPV6PORT "ipv6port"
|
||||
#define SER_APPLIST "apps"
|
||||
#define SER_SRVCERT "srvcert"
|
||||
#define SER_CUSTOMNAME "customname"
|
||||
@@ -23,13 +28,18 @@ NvComputer::NvComputer(QSettings& settings)
|
||||
this->uuid = settings.value(SER_UUID).toString();
|
||||
this->hasCustomName = settings.value(SER_CUSTOMNAME).toBool();
|
||||
this->macAddress = settings.value(SER_MAC).toByteArray();
|
||||
this->localAddress = settings.value(SER_LOCALADDR).toString();
|
||||
this->remoteAddress = settings.value(SER_REMOTEADDR).toString();
|
||||
this->ipv6Address = settings.value(SER_IPV6ADDR).toString();
|
||||
this->manualAddress = settings.value(SER_MANUALADDR).toString();
|
||||
this->localAddress = NvAddress(settings.value(SER_LOCALADDR).toString(),
|
||||
settings.value(SER_LOCALPORT, QVariant(DEFAULT_HTTP_PORT)).toUInt());
|
||||
this->remoteAddress = NvAddress(settings.value(SER_REMOTEADDR).toString(),
|
||||
settings.value(SER_REMOTEPORT, QVariant(DEFAULT_HTTP_PORT)).toUInt());
|
||||
this->ipv6Address = NvAddress(settings.value(SER_IPV6ADDR).toString(),
|
||||
settings.value(SER_IPV6PORT, QVariant(DEFAULT_HTTP_PORT)).toUInt());
|
||||
this->manualAddress = NvAddress(settings.value(SER_MANUALADDR).toString(),
|
||||
settings.value(SER_MANUALPORT, QVariant(DEFAULT_HTTP_PORT)).toUInt());
|
||||
this->serverCert = QSslCertificate(settings.value(SER_SRVCERT).toByteArray());
|
||||
|
||||
int appCount = settings.beginReadArray(SER_APPLIST);
|
||||
this->appList.reserve(appCount);
|
||||
for (int i = 0; i < appCount; i++) {
|
||||
settings.setArrayIndex(i);
|
||||
|
||||
@@ -39,7 +49,6 @@ NvComputer::NvComputer(QSettings& settings)
|
||||
settings.endArray();
|
||||
sortAppList();
|
||||
|
||||
this->activeAddress = nullptr;
|
||||
this->currentGameId = 0;
|
||||
this->pairState = PS_UNKNOWN;
|
||||
this->state = CS_UNKNOWN;
|
||||
@@ -49,6 +58,16 @@ NvComputer::NvComputer(QSettings& settings)
|
||||
this->serverCodecModeSupport = 0;
|
||||
this->pendingQuit = false;
|
||||
this->gpuModel = nullptr;
|
||||
this->isSupportedServerVersion = true;
|
||||
this->externalPort = this->remoteAddress.port();
|
||||
this->activeHttpsPort = 0;
|
||||
}
|
||||
|
||||
void NvComputer::setRemoteAddress(QHostAddress address)
|
||||
{
|
||||
Q_ASSERT(this->externalPort != 0);
|
||||
|
||||
this->remoteAddress = NvAddress(address, this->externalPort);
|
||||
}
|
||||
|
||||
void NvComputer::serialize(QSettings& settings) const
|
||||
@@ -59,10 +78,14 @@ void NvComputer::serialize(QSettings& settings) const
|
||||
settings.setValue(SER_CUSTOMNAME, hasCustomName);
|
||||
settings.setValue(SER_UUID, uuid);
|
||||
settings.setValue(SER_MAC, macAddress);
|
||||
settings.setValue(SER_LOCALADDR, localAddress);
|
||||
settings.setValue(SER_REMOTEADDR, remoteAddress);
|
||||
settings.setValue(SER_IPV6ADDR, ipv6Address);
|
||||
settings.setValue(SER_MANUALADDR, manualAddress);
|
||||
settings.setValue(SER_LOCALADDR, localAddress.address());
|
||||
settings.setValue(SER_LOCALPORT, localAddress.port());
|
||||
settings.setValue(SER_REMOTEADDR, remoteAddress.address());
|
||||
settings.setValue(SER_REMOTEPORT, remoteAddress.port());
|
||||
settings.setValue(SER_IPV6ADDR, ipv6Address.address());
|
||||
settings.setValue(SER_IPV6PORT, ipv6Address.port());
|
||||
settings.setValue(SER_MANUALADDR, manualAddress.address());
|
||||
settings.setValue(SER_MANUALPORT, manualAddress.port());
|
||||
settings.setValue(SER_SRVCERT, serverCert.toPem());
|
||||
|
||||
// Avoid deleting an existing applist if we couldn't get one
|
||||
@@ -84,9 +107,9 @@ void NvComputer::sortAppList()
|
||||
});
|
||||
}
|
||||
|
||||
NvComputer::NvComputer(QString address, QString serverInfo, QSslCertificate serverCert)
|
||||
NvComputer::NvComputer(NvHTTP& http, QString serverInfo)
|
||||
{
|
||||
this->serverCert = serverCert;
|
||||
this->serverCert = http.serverCert();
|
||||
|
||||
this->hasCustomName = false;
|
||||
this->name = NvHTTP::getXmlString(serverInfo, "hostname");
|
||||
@@ -98,7 +121,7 @@ NvComputer::NvComputer(QString address, QString serverInfo, QSslCertificate serv
|
||||
QString newMacString = NvHTTP::getXmlString(serverInfo, "mac");
|
||||
if (newMacString != "00:00:00:00:00:00") {
|
||||
QStringList macOctets = newMacString.split(':');
|
||||
for (QString macOctet : macOctets) {
|
||||
for (const QString& macOctet : macOctets) {
|
||||
this->macAddress.append((char) macOctet.toInt(nullptr, 16));
|
||||
}
|
||||
}
|
||||
@@ -127,21 +150,41 @@ NvComputer::NvComputer(QString address, QString serverInfo, QSslCertificate serv
|
||||
});
|
||||
|
||||
// We can get an IPv4 loopback address if we're using the GS IPv6 Forwarder
|
||||
this->localAddress = NvHTTP::getXmlString(serverInfo, "LocalIP");
|
||||
if (this->localAddress.startsWith("127.")) {
|
||||
this->localAddress = QString();
|
||||
this->localAddress = NvAddress(NvHTTP::getXmlString(serverInfo, "LocalIP"), http.httpPort());
|
||||
if (this->localAddress.address().startsWith("127.")) {
|
||||
this->localAddress = NvAddress();
|
||||
}
|
||||
|
||||
QString httpsPort = NvHTTP::getXmlString(serverInfo, "HttpsPort");
|
||||
if (httpsPort.isEmpty() || (this->activeHttpsPort = httpsPort.toUShort()) == 0) {
|
||||
this->activeHttpsPort = DEFAULT_HTTPS_PORT;
|
||||
}
|
||||
|
||||
// This is an extension which is not present in GFE. It is present for Sunshine to be able
|
||||
// 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;
|
||||
}
|
||||
|
||||
QString remoteAddress = NvHTTP::getXmlString(serverInfo, "ExternalIP");
|
||||
if (!remoteAddress.isEmpty()) {
|
||||
this->remoteAddress = NvAddress(remoteAddress, this->externalPort);
|
||||
}
|
||||
else {
|
||||
this->remoteAddress = NvAddress();
|
||||
}
|
||||
|
||||
this->remoteAddress = NvHTTP::getXmlString(serverInfo, "ExternalIP");
|
||||
this->pairState = NvHTTP::getXmlString(serverInfo, "PairStatus") == "1" ?
|
||||
PS_PAIRED : PS_NOT_PAIRED;
|
||||
this->currentGameId = NvHTTP::getCurrentGame(serverInfo);
|
||||
this->appVersion = NvHTTP::getXmlString(serverInfo, "appversion");
|
||||
this->gfeVersion = NvHTTP::getXmlString(serverInfo, "GfeVersion");
|
||||
this->gpuModel = NvHTTP::getXmlString(serverInfo, "gputype");
|
||||
this->activeAddress = address;
|
||||
this->activeAddress = http.address();
|
||||
this->state = NvComputer::CS_ONLINE;
|
||||
this->pendingQuit = false;
|
||||
this->isSupportedServerVersion = CompatFetcher::isGfeVersionSupported(this->gfeVersion);
|
||||
}
|
||||
|
||||
bool NvComputer::wake()
|
||||
@@ -173,7 +216,10 @@ bool NvComputer::wake()
|
||||
// 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 = uniqueAddresses();
|
||||
QVector<QString> addressList;
|
||||
for (const NvAddress& addr : uniqueAddresses()) {
|
||||
addressList.append(addr.address());
|
||||
}
|
||||
addressList.append("255.255.255.255");
|
||||
|
||||
// Try to broadcast on all available NICs
|
||||
@@ -234,14 +280,14 @@ bool NvComputer::wake()
|
||||
|
||||
bool NvComputer::isReachableOverVpn()
|
||||
{
|
||||
if (activeAddress.isEmpty()) {
|
||||
if (activeAddress.isNull()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QTcpSocket s;
|
||||
|
||||
s.setProxy(QNetworkProxy::NoProxy);
|
||||
s.connectToHost(activeAddress, 47984);
|
||||
s.connectToHost(activeAddress.address(), activeAddress.port());
|
||||
if (s.waitForConnected(3000)) {
|
||||
Q_ASSERT(!s.localAddress().isNull());
|
||||
|
||||
@@ -327,9 +373,9 @@ bool NvComputer::updateAppList(QVector<NvApp> newAppList) {
|
||||
return true;
|
||||
}
|
||||
|
||||
QVector<QString> NvComputer::uniqueAddresses() const
|
||||
QVector<NvAddress> NvComputer::uniqueAddresses() const
|
||||
{
|
||||
QVector<QString> uniqueAddressList;
|
||||
QVector<NvAddress> uniqueAddressList;
|
||||
|
||||
// Start with addresses correctly ordered
|
||||
uniqueAddressList.append(activeAddress);
|
||||
@@ -340,7 +386,7 @@ QVector<QString> NvComputer::uniqueAddresses() const
|
||||
|
||||
// Prune duplicates (always giving precedence to the first)
|
||||
for (int i = 0; i < uniqueAddressList.count(); i++) {
|
||||
if (uniqueAddressList[i].isEmpty()) {
|
||||
if (uniqueAddressList[i].isNull()) {
|
||||
uniqueAddressList.remove(i);
|
||||
i--;
|
||||
continue;
|
||||
@@ -396,10 +442,12 @@ bool NvComputer::update(NvComputer& that)
|
||||
ASSIGN_IF_CHANGED(name);
|
||||
}
|
||||
ASSIGN_IF_CHANGED_AND_NONEMPTY(macAddress);
|
||||
ASSIGN_IF_CHANGED_AND_NONEMPTY(localAddress);
|
||||
ASSIGN_IF_CHANGED_AND_NONEMPTY(remoteAddress);
|
||||
ASSIGN_IF_CHANGED_AND_NONEMPTY(ipv6Address);
|
||||
ASSIGN_IF_CHANGED_AND_NONEMPTY(manualAddress);
|
||||
ASSIGN_IF_CHANGED_AND_NONNULL(localAddress);
|
||||
ASSIGN_IF_CHANGED_AND_NONNULL(remoteAddress);
|
||||
ASSIGN_IF_CHANGED_AND_NONNULL(ipv6Address);
|
||||
ASSIGN_IF_CHANGED_AND_NONNULL(manualAddress);
|
||||
ASSIGN_IF_CHANGED(activeHttpsPort);
|
||||
ASSIGN_IF_CHANGED(externalPort);
|
||||
ASSIGN_IF_CHANGED(pairState);
|
||||
ASSIGN_IF_CHANGED(serverCodecModeSupport);
|
||||
ASSIGN_IF_CHANGED(currentGameId);
|
||||
@@ -407,6 +455,7 @@ bool NvComputer::update(NvComputer& that)
|
||||
ASSIGN_IF_CHANGED(state);
|
||||
ASSIGN_IF_CHANGED(gfeVersion);
|
||||
ASSIGN_IF_CHANGED(appVersion);
|
||||
ASSIGN_IF_CHANGED(isSupportedServerVersion);
|
||||
ASSIGN_IF_CHANGED(maxLumaPixelsHEVC);
|
||||
ASSIGN_IF_CHANGED(gpuModel);
|
||||
ASSIGN_IF_CHANGED_AND_NONNULL(serverCert);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "nvhttp.h"
|
||||
#include "nvaddress.h"
|
||||
|
||||
#include <QThread>
|
||||
#include <QReadWriteLock>
|
||||
@@ -21,10 +22,13 @@ private:
|
||||
bool pendingQuit;
|
||||
|
||||
public:
|
||||
explicit NvComputer(QString address, QString serverInfo, QSslCertificate serverCert);
|
||||
explicit NvComputer(NvHTTP& http, QString serverInfo);
|
||||
|
||||
explicit NvComputer(QSettings& settings);
|
||||
|
||||
void
|
||||
setRemoteAddress(QHostAddress);
|
||||
|
||||
bool
|
||||
update(NvComputer& that);
|
||||
|
||||
@@ -34,7 +38,7 @@ public:
|
||||
bool
|
||||
isReachableOverVpn();
|
||||
|
||||
QVector<QString>
|
||||
QVector<NvAddress>
|
||||
uniqueAddresses() const;
|
||||
|
||||
void
|
||||
@@ -57,7 +61,8 @@ public:
|
||||
// Ephemeral traits
|
||||
ComputerState state;
|
||||
PairState pairState;
|
||||
QString activeAddress;
|
||||
NvAddress activeAddress;
|
||||
uint16_t activeHttpsPort;
|
||||
int currentGameId;
|
||||
QString gfeVersion;
|
||||
QString appVersion;
|
||||
@@ -65,12 +70,13 @@ public:
|
||||
int maxLumaPixelsHEVC;
|
||||
int serverCodecModeSupport;
|
||||
QString gpuModel;
|
||||
bool isSupportedServerVersion;
|
||||
|
||||
// Persisted traits
|
||||
QString localAddress;
|
||||
QString remoteAddress;
|
||||
QString ipv6Address;
|
||||
QString manualAddress;
|
||||
NvAddress localAddress;
|
||||
NvAddress remoteAddress;
|
||||
NvAddress ipv6Address;
|
||||
NvAddress manualAddress;
|
||||
QByteArray macAddress;
|
||||
QString name;
|
||||
bool hasCustomName;
|
||||
@@ -80,4 +86,7 @@ public:
|
||||
|
||||
// Synchronization
|
||||
mutable QReadWriteLock lock;
|
||||
|
||||
private:
|
||||
uint16_t externalPort;
|
||||
};
|
||||
|
||||
+81
-19
@@ -1,4 +1,4 @@
|
||||
#include "nvhttp.h"
|
||||
#include "nvcomputer.h"
|
||||
#include <Limelight.h>
|
||||
|
||||
#include <QDebug>
|
||||
@@ -18,15 +18,14 @@
|
||||
#define RESUME_TIMEOUT_MS 30000
|
||||
#define QUIT_TIMEOUT_MS 30000
|
||||
|
||||
NvHTTP::NvHTTP(QString address, QSslCertificate serverCert) :
|
||||
NvHTTP::NvHTTP(NvAddress address, uint16_t httpsPort, QSslCertificate serverCert) :
|
||||
m_ServerCert(serverCert)
|
||||
{
|
||||
m_BaseUrlHttp.setScheme("http");
|
||||
m_BaseUrlHttps.setScheme("https");
|
||||
m_BaseUrlHttp.setPort(47989);
|
||||
m_BaseUrlHttps.setPort(47984);
|
||||
|
||||
setAddress(address);
|
||||
setHttpsPort(httpsPort);
|
||||
|
||||
// Never use a proxy server
|
||||
QNetworkProxy noProxy(QNetworkProxy::NoProxy);
|
||||
@@ -35,26 +34,54 @@ NvHTTP::NvHTTP(QString address, QSslCertificate serverCert) :
|
||||
connect(&m_Nam, &QNetworkAccessManager::sslErrors, this, &NvHTTP::handleSslErrors);
|
||||
}
|
||||
|
||||
NvHTTP::NvHTTP(NvComputer* computer) :
|
||||
NvHTTP(computer->activeAddress, computer->activeHttpsPort, computer->serverCert)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
void NvHTTP::setServerCert(QSslCertificate serverCert)
|
||||
{
|
||||
m_ServerCert = serverCert;
|
||||
}
|
||||
|
||||
void NvHTTP::setAddress(QString address)
|
||||
void NvHTTP::setAddress(NvAddress address)
|
||||
{
|
||||
Q_ASSERT(!address.isEmpty());
|
||||
Q_ASSERT(!address.isNull());
|
||||
|
||||
m_Address = address;
|
||||
|
||||
m_BaseUrlHttp.setHost(address);
|
||||
m_BaseUrlHttps.setHost(address);
|
||||
m_BaseUrlHttp.setHost(address.address());
|
||||
m_BaseUrlHttps.setHost(address.address());
|
||||
|
||||
m_BaseUrlHttp.setPort(address.port());
|
||||
}
|
||||
|
||||
QString NvHTTP::address()
|
||||
void NvHTTP::setHttpsPort(uint16_t port)
|
||||
{
|
||||
m_BaseUrlHttps.setPort(port);
|
||||
}
|
||||
|
||||
NvAddress NvHTTP::address()
|
||||
{
|
||||
return m_Address;
|
||||
}
|
||||
|
||||
QSslCertificate NvHTTP::serverCert()
|
||||
{
|
||||
return m_ServerCert;
|
||||
}
|
||||
|
||||
uint16_t NvHTTP::httpPort()
|
||||
{
|
||||
return m_BaseUrlHttp.port();
|
||||
}
|
||||
|
||||
uint16_t NvHTTP::httpsPort()
|
||||
{
|
||||
return m_BaseUrlHttps.port();
|
||||
}
|
||||
|
||||
QVector<int>
|
||||
NvHTTP::parseQuad(QString quad)
|
||||
{
|
||||
@@ -67,7 +94,8 @@ NvHTTP::parseQuad(QString quad)
|
||||
}
|
||||
|
||||
QStringList parts = quad.split(".");
|
||||
for (int i = 0; i < 4; i++)
|
||||
ret.reserve(parts.length());
|
||||
for (int i = 0; i < parts.length(); i++)
|
||||
{
|
||||
ret.append(parts.at(i).toInt());
|
||||
}
|
||||
@@ -97,8 +125,8 @@ NvHTTP::getServerInfo(NvLogLevel logLevel, bool fastFail)
|
||||
{
|
||||
QString serverInfo;
|
||||
|
||||
// Check if we have a pinned cert for this host yet
|
||||
if (!m_ServerCert.isNull())
|
||||
// Check if we have a pinned cert and HTTPS port for this host yet
|
||||
if (!m_ServerCert.isNull() && httpsPort() != 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -133,13 +161,26 @@ NvHTTP::getServerInfo(NvLogLevel logLevel, bool fastFail)
|
||||
}
|
||||
else
|
||||
{
|
||||
// Only use HTTP prior to pairing
|
||||
// Only use HTTP prior to pairing or fetching HTTPS port
|
||||
serverInfo = openConnectionToString(m_BaseUrlHttp,
|
||||
"serverinfo",
|
||||
nullptr,
|
||||
fastFail ? FAST_FAIL_TIMEOUT_MS : REQUEST_TIMEOUT_MS,
|
||||
logLevel);
|
||||
verifyResponseStatus(serverInfo);
|
||||
|
||||
// Populate the HTTPS port
|
||||
uint16_t httpsPort = getXmlString(serverInfo, "HttpsPort").toUShort();
|
||||
if (httpsPort == 0) {
|
||||
httpsPort = DEFAULT_HTTPS_PORT;
|
||||
}
|
||||
setHttpsPort(httpsPort);
|
||||
|
||||
// If we just needed to determine the HTTPS port, we'll try again over
|
||||
// HTTPS now that we have the port number
|
||||
if (!m_ServerCert.isNull()) {
|
||||
return getServerInfo(logLevel, fastFail);
|
||||
}
|
||||
}
|
||||
|
||||
return serverInfo;
|
||||
@@ -150,7 +191,8 @@ NvHTTP::launchApp(int appId,
|
||||
PSTREAM_CONFIGURATION streamConfig,
|
||||
bool sops,
|
||||
bool localAudio,
|
||||
int gamepadMask)
|
||||
int gamepadMask,
|
||||
QString& rtspSessionUrl)
|
||||
{
|
||||
int riKeyId;
|
||||
|
||||
@@ -180,12 +222,16 @@ NvHTTP::launchApp(int appId,
|
||||
"&gcmap="+QString::number(gamepadMask),
|
||||
LAUNCH_TIMEOUT_MS);
|
||||
|
||||
qInfo() << "Launch response:" << response;
|
||||
|
||||
// Throws if the request failed
|
||||
verifyResponseStatus(response);
|
||||
|
||||
rtspSessionUrl = getXmlString(response, "sessionUrl0");
|
||||
}
|
||||
|
||||
void
|
||||
NvHTTP::resumeApp(PSTREAM_CONFIGURATION streamConfig)
|
||||
NvHTTP::resumeApp(PSTREAM_CONFIGURATION streamConfig, QString& rtspSessionUrl)
|
||||
{
|
||||
int riKeyId;
|
||||
|
||||
@@ -200,8 +246,12 @@ NvHTTP::resumeApp(PSTREAM_CONFIGURATION streamConfig)
|
||||
"&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
|
||||
@@ -213,6 +263,8 @@ NvHTTP::quitApp()
|
||||
nullptr,
|
||||
QUIT_TIMEOUT_MS);
|
||||
|
||||
qInfo() << "Quit response:" << response;
|
||||
|
||||
// Throws if the request failed
|
||||
verifyResponseStatus(response);
|
||||
|
||||
@@ -329,6 +381,8 @@ NvHTTP::verifyResponseStatus(QString xml)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw GfeHttpResponseException(-1, "Malformed GFE XML (missing root element)");
|
||||
}
|
||||
|
||||
QImage
|
||||
@@ -391,7 +445,7 @@ void NvHTTP::handleSslErrors(QNetworkReply* reply, const QList<QSslError>& error
|
||||
return;
|
||||
}
|
||||
|
||||
for (auto error : errors) {
|
||||
for (const QSslError& error : errors) {
|
||||
if (m_ServerCert != error.certificate()) {
|
||||
ignoreErrors = false;
|
||||
break;
|
||||
@@ -434,6 +488,9 @@ NvHTTP::openConnection(QUrl baseUrl,
|
||||
int timeoutMs,
|
||||
NvLogLevel logLevel)
|
||||
{
|
||||
// Port must be set
|
||||
Q_ASSERT(baseUrl.port(0) != 0);
|
||||
|
||||
// Build a URL for the request
|
||||
QUrl url(baseUrl);
|
||||
url.setPath("/" + command);
|
||||
@@ -450,6 +507,11 @@ NvHTTP::openConnection(QUrl baseUrl,
|
||||
// Add our client certificate
|
||||
request.setSslConfiguration(IdentityManager::get()->getSslConfig());
|
||||
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
// Disable HTTP/2 (GFE 3.22 doesn't like it) and Qt 6 enables it by default
|
||||
request.setAttribute(QNetworkRequest::Http2AllowedAttribute, false);
|
||||
#endif
|
||||
|
||||
#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
|
||||
@@ -462,10 +524,10 @@ NvHTTP::openConnection(QUrl baseUrl,
|
||||
|
||||
// Run the request with a timeout if requested
|
||||
QEventLoop loop;
|
||||
connect(reply, SIGNAL(finished()), &loop, SLOT(quit()));
|
||||
connect(QCoreApplication::instance(), SIGNAL(aboutToQuit()), &loop, SLOT(quit()));
|
||||
connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
|
||||
connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, &loop, &QEventLoop::quit);
|
||||
if (timeoutMs) {
|
||||
QTimer::singleShot(timeoutMs, &loop, SLOT(quit()));
|
||||
QTimer::singleShot(timeoutMs, &loop, &QEventLoop::quit);
|
||||
}
|
||||
if (logLevel >= NvLogLevel::NVLL_VERBOSE) {
|
||||
qInfo() << "Executing request:" << url.toString();
|
||||
|
||||
+21
-6
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "identitymanager.h"
|
||||
#include "nvapp.h"
|
||||
#include "nvaddress.h"
|
||||
|
||||
#include <Limelight.h>
|
||||
|
||||
@@ -9,6 +10,8 @@
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkReply>
|
||||
|
||||
class NvComputer;
|
||||
|
||||
class NvDisplayMode
|
||||
{
|
||||
public:
|
||||
@@ -23,6 +26,7 @@ public:
|
||||
int height;
|
||||
int refreshRate;
|
||||
};
|
||||
Q_DECLARE_TYPEINFO(NvDisplayMode, Q_PRIMITIVE_TYPE);
|
||||
|
||||
class GfeHttpResponseException : public std::exception
|
||||
{
|
||||
@@ -105,7 +109,9 @@ public:
|
||||
NVLL_VERBOSE
|
||||
};
|
||||
|
||||
explicit NvHTTP(QString address, QSslCertificate serverCert);
|
||||
explicit NvHTTP(NvAddress address, uint16_t httpsPort, QSslCertificate serverCert);
|
||||
|
||||
explicit NvHTTP(NvComputer* computer);
|
||||
|
||||
static
|
||||
int
|
||||
@@ -137,9 +143,16 @@ public:
|
||||
|
||||
void setServerCert(QSslCertificate serverCert);
|
||||
|
||||
void setAddress(QString address);
|
||||
void setAddress(NvAddress address);
|
||||
void setHttpsPort(uint16_t port);
|
||||
|
||||
QString address();
|
||||
NvAddress address();
|
||||
|
||||
QSslCertificate serverCert();
|
||||
|
||||
uint16_t httpPort();
|
||||
|
||||
uint16_t httpsPort();
|
||||
|
||||
static
|
||||
QVector<int>
|
||||
@@ -149,14 +162,16 @@ public:
|
||||
quitApp();
|
||||
|
||||
void
|
||||
resumeApp(PSTREAM_CONFIGURATION streamConfig);
|
||||
resumeApp(PSTREAM_CONFIGURATION streamConfig,
|
||||
QString& rtspSessionUrl);
|
||||
|
||||
void
|
||||
launchApp(int appId,
|
||||
PSTREAM_CONFIGURATION streamConfig,
|
||||
bool sops,
|
||||
bool localAudio,
|
||||
int gamepadMask);
|
||||
int gamepadMask,
|
||||
QString& rtspSessionUrl);
|
||||
|
||||
QVector<NvApp>
|
||||
getAppList();
|
||||
@@ -181,7 +196,7 @@ private:
|
||||
int timeoutMs,
|
||||
NvLogLevel logLevel);
|
||||
|
||||
QString m_Address;
|
||||
NvAddress m_Address;
|
||||
QNetworkAccessManager m_Nam;
|
||||
QSslCertificate m_ServerCert;
|
||||
};
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
|
||||
#define REQUEST_TIMEOUT_MS 5000
|
||||
|
||||
NvPairingManager::NvPairingManager(QString address) :
|
||||
m_Http(address, QSslCertificate())
|
||||
NvPairingManager::NvPairingManager(NvComputer* computer) :
|
||||
m_Http(computer)
|
||||
{
|
||||
QByteArray cert = IdentityManager::get()->getCertificate();
|
||||
BIO *bio = BIO_new_mem_buf(cert.data(), -1);
|
||||
@@ -167,11 +167,7 @@ NvPairingManager::signMessage(const QByteArray& message)
|
||||
EVP_MD_CTX *ctx = EVP_MD_CTX_create();
|
||||
THROW_BAD_ALLOC_IF_NULL(ctx);
|
||||
|
||||
const EVP_MD *md = EVP_get_digestbyname("SHA256");
|
||||
THROW_BAD_ALLOC_IF_NULL(md);
|
||||
|
||||
EVP_DigestInit_ex(ctx, md, NULL);
|
||||
EVP_DigestSignInit(ctx, NULL, md, NULL, m_PrivateKey);
|
||||
EVP_DigestSignInit(ctx, NULL, EVP_sha256(), NULL, m_PrivateKey);
|
||||
EVP_DigestSignUpdate(ctx, reinterpret_cast<unsigned char*>(const_cast<char*>(message.data())), message.length());
|
||||
|
||||
size_t signatureLength = 0;
|
||||
@@ -215,7 +211,7 @@ NvPairingManager::pair(QString appVersion, QString pin, QSslCertificate& serverC
|
||||
QByteArray salt = generateRandomBytes(16);
|
||||
QByteArray saltedPin = saltPin(salt, pin);
|
||||
|
||||
QByteArray aesKey = QCryptographicHash::hash(saltedPin, hashAlgo).data();
|
||||
QByteArray aesKey = QCryptographicHash::hash(saltedPin, hashAlgo).constData();
|
||||
aesKey.truncate(16);
|
||||
|
||||
QString getCert = m_Http.openConnectionToString(m_Http.m_BaseUrlHttp,
|
||||
|
||||
@@ -17,7 +17,7 @@ public:
|
||||
ALREADY_IN_PROGRESS
|
||||
};
|
||||
|
||||
explicit NvPairingManager(QString address);
|
||||
explicit NvPairingManager(NvComputer* computer);
|
||||
|
||||
~NvPairingManager();
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
SystemProperties::SystemProperties()
|
||||
{
|
||||
versionString = QString(VERSION_STR);
|
||||
hasWindowManager = WMUtils::isRunningWindowManager();
|
||||
hasDesktopEnvironment = WMUtils::isRunningDesktopEnvironment();
|
||||
isRunningWayland = WMUtils::isRunningWayland();
|
||||
isRunningXWayland = isRunningWayland && QGuiApplication::platformName() == "xcb";
|
||||
QString nativeArch = QSysInfo::currentCpuArchitecture();
|
||||
@@ -55,12 +55,8 @@ SystemProperties::SystemProperties()
|
||||
friendlyNativeArchName = nativeArch.toUpper();
|
||||
}
|
||||
|
||||
#ifndef STEAM_LINK
|
||||
// Assume we can probably launch a browser if we're in a GUI environment
|
||||
hasBrowser = hasWindowManager;
|
||||
#else
|
||||
hasBrowser = false;
|
||||
#endif
|
||||
hasBrowser = hasDesktopEnvironment;
|
||||
|
||||
#ifdef HAVE_DISCORD
|
||||
hasDiscordIntegration = true;
|
||||
@@ -68,6 +64,16 @@ SystemProperties::SystemProperties()
|
||||
hasDiscordIntegration = false;
|
||||
#endif
|
||||
|
||||
// TODO: Do something smarter than this. We should be able to query
|
||||
// this from the decoder (or just try VIDEO_FORMAT_H265_MAIN10 and
|
||||
// fail if we don't get a hardware accelerated decoder).
|
||||
#ifdef Q_OS_DARWIN
|
||||
// HDR is supported by the VideoToolbox renderer
|
||||
supportsHdr = true;
|
||||
#else
|
||||
supportsHdr = false;
|
||||
#endif
|
||||
|
||||
unmappedGamepads = SdlInputHandler::getUnmappedGamepads();
|
||||
|
||||
// Populate data that requires talking to SDL. We do it all in one shot
|
||||
@@ -91,7 +97,36 @@ QRect SystemProperties::getNativeResolution(int displayIndex)
|
||||
return monitorNativeResolutions.value(displayIndex);
|
||||
}
|
||||
|
||||
class QuerySdlVideoThread : public QThread
|
||||
{
|
||||
public:
|
||||
QuerySdlVideoThread(SystemProperties* me) :
|
||||
QThread(nullptr),
|
||||
m_Me(me) {}
|
||||
|
||||
void run() override
|
||||
{
|
||||
m_Me->querySdlVideoInfoInternal();
|
||||
}
|
||||
|
||||
SystemProperties* m_Me;
|
||||
};
|
||||
|
||||
void SystemProperties::querySdlVideoInfo()
|
||||
{
|
||||
if (WMUtils::isRunningX11() || WMUtils::isRunningWayland()) {
|
||||
// Use a separate thread to temporarily initialize SDL
|
||||
// video to avoid stomping on Qt's X11 and OGL state.
|
||||
QuerySdlVideoThread thread(this);
|
||||
thread.start();
|
||||
thread.wait();
|
||||
}
|
||||
else {
|
||||
querySdlVideoInfoInternal();
|
||||
}
|
||||
}
|
||||
|
||||
void SystemProperties::querySdlVideoInfoInternal()
|
||||
{
|
||||
hasHardwareAcceleration = false;
|
||||
|
||||
@@ -103,7 +138,8 @@ void SystemProperties::querySdlVideoInfo()
|
||||
}
|
||||
|
||||
// Update display related attributes (max FPS, native resolution, etc).
|
||||
refreshDisplays();
|
||||
// We call the internal variant because we're already in a safe thread context.
|
||||
refreshDisplaysInternal();
|
||||
|
||||
SDL_Window* testWindow = SDL_CreateWindow("", 0, 0, 1280, 720,
|
||||
SDL_WINDOW_HIDDEN | StreamUtils::getPlatformWindowFlags());
|
||||
@@ -129,7 +165,36 @@ void SystemProperties::querySdlVideoInfo()
|
||||
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||
}
|
||||
|
||||
class RefreshDisplaysThread : public QThread
|
||||
{
|
||||
public:
|
||||
RefreshDisplaysThread(SystemProperties* me) :
|
||||
QThread(nullptr),
|
||||
m_Me(me) {}
|
||||
|
||||
void run() override
|
||||
{
|
||||
m_Me->refreshDisplaysInternal();
|
||||
}
|
||||
|
||||
SystemProperties* m_Me;
|
||||
};
|
||||
|
||||
void SystemProperties::refreshDisplays()
|
||||
{
|
||||
if (WMUtils::isRunningX11() || WMUtils::isRunningWayland()) {
|
||||
// Use a separate thread to temporarily initialize SDL
|
||||
// video to avoid stomping on Qt's X11 and OGL state.
|
||||
RefreshDisplaysThread thread(this);
|
||||
thread.start();
|
||||
thread.wait();
|
||||
}
|
||||
else {
|
||||
refreshDisplaysInternal();
|
||||
}
|
||||
}
|
||||
|
||||
void SystemProperties::refreshDisplaysInternal()
|
||||
{
|
||||
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
|
||||
@@ -7,6 +7,9 @@ class SystemProperties : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
friend class QuerySdlVideoThread;
|
||||
friend class RefreshDisplaysThread;
|
||||
|
||||
public:
|
||||
SystemProperties();
|
||||
|
||||
@@ -16,13 +19,14 @@ public:
|
||||
Q_PROPERTY(bool isRunningXWayland MEMBER isRunningXWayland CONSTANT)
|
||||
Q_PROPERTY(bool isWow64 MEMBER isWow64 CONSTANT)
|
||||
Q_PROPERTY(QString friendlyNativeArchName MEMBER friendlyNativeArchName CONSTANT)
|
||||
Q_PROPERTY(bool hasWindowManager MEMBER hasWindowManager CONSTANT)
|
||||
Q_PROPERTY(bool hasDesktopEnvironment MEMBER hasDesktopEnvironment CONSTANT)
|
||||
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_INVOKABLE void refreshDisplays();
|
||||
Q_INVOKABLE QRect getDesktopResolution(int displayIndex);
|
||||
@@ -33,6 +37,8 @@ signals:
|
||||
|
||||
private:
|
||||
void querySdlVideoInfo();
|
||||
void querySdlVideoInfoInternal();
|
||||
void refreshDisplaysInternal();
|
||||
|
||||
bool hasHardwareAcceleration;
|
||||
bool rendererAlwaysFullScreen;
|
||||
@@ -40,7 +46,7 @@ private:
|
||||
bool isRunningXWayland;
|
||||
bool isWow64;
|
||||
QString friendlyNativeArchName;
|
||||
bool hasWindowManager;
|
||||
bool hasDesktopEnvironment;
|
||||
bool hasBrowser;
|
||||
bool hasDiscordIntegration;
|
||||
QString unmappedGamepads;
|
||||
@@ -49,5 +55,6 @@ private:
|
||||
QList<QRect> monitorDesktopResolutions;
|
||||
QList<QRect> monitorNativeResolutions;
|
||||
QString versionString;
|
||||
bool supportsHdr;
|
||||
};
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ public:
|
||||
|
||||
void handleUnknownOptions()
|
||||
{
|
||||
if (unknownOptionNames().length()) {
|
||||
if (!unknownOptionNames().isEmpty()) {
|
||||
showError(QString("Unknown options: %1").arg(unknownOptionNames().join(", ")));
|
||||
}
|
||||
}
|
||||
@@ -264,6 +264,11 @@ StreamCommandLineParser::StreamCommandLineParser()
|
||||
{"software", StreamingPreferences::VDS_FORCE_SOFTWARE},
|
||||
{"hardware", StreamingPreferences::VDS_FORCE_HARDWARE},
|
||||
};
|
||||
m_CaptureSysKeysModeMap = {
|
||||
{"never", StreamingPreferences::CSK_OFF},
|
||||
{"fullscreen", StreamingPreferences::CSK_FULLSCREEN},
|
||||
{"always", StreamingPreferences::CSK_ALWAYS},
|
||||
};
|
||||
}
|
||||
|
||||
StreamCommandLineParser::~StreamCommandLineParser()
|
||||
@@ -307,7 +312,7 @@ void StreamCommandLineParser::parse(const QStringList &args, StreamingPreference
|
||||
parser.addToggleOption("background-gamepad", "background gamepad input");
|
||||
parser.addToggleOption("reverse-scroll-direction", "inverted scroll direction");
|
||||
parser.addToggleOption("swap-gamepad-buttons", "swap A/B and X/Y gamepad buttons (Nintendo-style)");
|
||||
parser.addToggleOption("capture-system-keys", "capture system key combos in fullscreen mode");
|
||||
parser.addChoiceOption("capture-system-keys", "capture system key combos", m_CaptureSysKeysModeMap.keys());
|
||||
parser.addChoiceOption("video-codec", "video codec", m_VideoCodecMap.keys());
|
||||
parser.addChoiceOption("video-decoder", "video decoder", m_VideoDecoderMap.keys());
|
||||
|
||||
@@ -320,25 +325,21 @@ void StreamCommandLineParser::parse(const QStringList &args, StreamingPreference
|
||||
// Resolve display's width and height
|
||||
QRegularExpression resolutionRexExp("^(720|1080|1440|4K|resolution)$");
|
||||
QStringList resoOptions = parser.optionNames().filter(resolutionRexExp);
|
||||
bool displaySet = resoOptions.length();
|
||||
bool displaySet = !resoOptions.isEmpty();
|
||||
if (displaySet) {
|
||||
QString name = resoOptions.last();
|
||||
if (name == "720") {
|
||||
preferences->width = 1280;
|
||||
preferences->height = 720;
|
||||
displaySet = true;
|
||||
} else if (name == "1080") {
|
||||
preferences->width = 1920;
|
||||
preferences->height = 1080;
|
||||
displaySet = true;
|
||||
} else if (name == "1440") {
|
||||
preferences->width = 2560;
|
||||
preferences->height = 1440;
|
||||
displaySet = true;
|
||||
} else if (name == "4K") {
|
||||
preferences->width = 3840;
|
||||
preferences->height = 2160;
|
||||
displaySet = true;
|
||||
} else if (name == "resolution") {
|
||||
auto resolution = parser.getResolutionOptionValue(name);
|
||||
preferences->width = resolution.first;
|
||||
@@ -349,8 +350,8 @@ void StreamCommandLineParser::parse(const QStringList &args, StreamingPreference
|
||||
// Resolve --fps option
|
||||
if (parser.isSet("fps")) {
|
||||
preferences->fps = parser.getIntOption("fps");
|
||||
if (!inRange(preferences->fps, 30, 120)) {
|
||||
parser.showError("FPS must be in range: 30 - 120");
|
||||
if (!inRange(preferences->fps, 30, 240)) {
|
||||
parser.showError("FPS must be in range: 30 - 240");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -422,8 +423,10 @@ void StreamCommandLineParser::parse(const QStringList &args, StreamingPreference
|
||||
// Resolve --swap-gamepad-buttons and --no-swap-gamepad-buttons options
|
||||
preferences->swapFaceButtons = parser.getToggleOptionValue("swap-gamepad-buttons", preferences->swapFaceButtons);
|
||||
|
||||
// Resolve --capture-system-keys and --no-capture-system-keys options
|
||||
preferences->captureSysKeys = parser.getToggleOptionValue("capture-system-keys", preferences->captureSysKeys);
|
||||
// Resolve --capture-system-keys option
|
||||
if (parser.isSet("capture-system-keys")) {
|
||||
preferences->captureSysKeysMode = mapValue(m_CaptureSysKeysModeMap, parser.getChoiceOptionValue("capture-system-keys"));
|
||||
}
|
||||
|
||||
// Resolve --video-codec option
|
||||
if (parser.isSet("video-codec")) {
|
||||
|
||||
@@ -53,4 +53,5 @@ private:
|
||||
QMap<QString, StreamingPreferences::AudioConfig> m_AudioConfigMap;
|
||||
QMap<QString, StreamingPreferences::VideoCodecConfig> m_VideoCodecMap;
|
||||
QMap<QString, StreamingPreferences::VideoDecoderSelection> m_VideoDecoderMap;
|
||||
QMap<QString, StreamingPreferences::CaptureSysKeysMode> m_CaptureSysKeysModeMap;
|
||||
};
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<li>Hardware accelerated video decoding with VAAPI, VDPAU, and NVDEC support</li>
|
||||
<li>7.1 surround sound audio support</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 in fullscreen mode on X11</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>
|
||||
@@ -36,6 +36,90 @@
|
||||
</screenshots>
|
||||
|
||||
<releases>
|
||||
<release version="3.2.0" date="2022-01-18">
|
||||
<description>
|
||||
<p>New features:</p>
|
||||
<ul>
|
||||
<li>Reduced CPU usage while streaming</li>
|
||||
<li>Non-ASCII characters are now supported when pasting clipboard data to the host</li>
|
||||
<li>Added support for zero-copy NVDEC rendering for Nvidia Wayland systems</li>
|
||||
<li>Significantly improved performance with asynchronous decoders like V4L2M2M on Linux</li>
|
||||
<li>Added support for color compression planes on newer Intel systems when using VAAPI decoding</li>
|
||||
<li>Added support for copy-back rendering in non-default pixel formats (like the Raspberry Pi SAND format)</li>
|
||||
</ul>
|
||||
<p>Bugfixes:</p>
|
||||
<ul>
|
||||
<li>Fixed unexpected GUI input after using the gamepad quit combo</li>
|
||||
<li>Updated included gamepad mappings</li>
|
||||
<li>Updated community-contributed translations from Weblate</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="3.1.4" date="2021-06-25">
|
||||
<description>
|
||||
<p>Changes:</p>
|
||||
<ul>
|
||||
<li>Added network latency statistics to the performance overlay</li>
|
||||
<li>Improved audio quality on unreliable connections (requires GeForce Experience 3.19 or later)</li>
|
||||
<li>Reduced video frame drops on unreliable connections (requires GeForce Experience 3.22 or later)</li>
|
||||
<li>Updated community-contributed translations from Weblate</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="3.1.3" date="2021-05-07">
|
||||
<description>
|
||||
<p>Changes:</p>
|
||||
<ul>
|
||||
<li>Fixed an issue that could cause excessive audio latency on some connections</li>
|
||||
<li>Updated community-contributed translations from Weblate</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="3.1.2" date="2021-05-03">
|
||||
<description>
|
||||
<p>Changes:</p>
|
||||
<ul>
|
||||
<li>Improved connection reliability and performance with GeForce Experience 3.22</li>
|
||||
<li>Added support for audio encryption with GeForce Experience 3.22</li>
|
||||
<li>Added compatibility update checks for future versions of GeForce Experience</li>
|
||||
<li>Fixed keyboard capture on X11</li>
|
||||
<li>Updated community-contributed translations from Weblate</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="3.1.1" date="2021-04-09">
|
||||
<description>
|
||||
<p>Changes:</p>
|
||||
<ul>
|
||||
<li>Added support for streaming from GeForce Experience 3.22</li>
|
||||
<li>Added support for AMF-encoded HEVC streams from servers like Sunshine and OpenStream</li>
|
||||
<li>Added new community-contributed translations from Weblate</li>
|
||||
<li>Fixed KMSDRM rendering with buffer modifiers on embedded devices</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="3.1.0" date="2021-03-14">
|
||||
<description>
|
||||
<p>New features:</p>
|
||||
<ul>
|
||||
<li>System keyboard shortcuts (Alt+Tab, Super key, Ctrl+Shift+Esc, etc.) can now be (optionally) captured in windowed mode</li>
|
||||
<li>System keyboard shortcuts can now be captured under Wayland</li>
|
||||
<li>Added support for typing the client's clipboard text on the host by pressing Ctrl+Alt+Shift+V</li>
|
||||
<li>Added Ctrl+Alt+Shift+D key combo to minimize the streaming window</li>
|
||||
<li>Added Ctrl+Alt+Shift+C key combo to show the local cursor while streaming in remote desktop mouse mode</li>
|
||||
<li>Added Select+L1+R1+X gamepad button combo to toggle performance overlay</li>
|
||||
<li>Added translations for Simplified Chinese and German</li>
|
||||
<li>Added a language selector on the settings page</li>
|
||||
</ul>
|
||||
<p>Bugfixes:</p>
|
||||
<ul>
|
||||
<li>Improved performance of loading the app grid</li>
|
||||
<li>Fixed video scaling in High DPI mode on Wayland</li>
|
||||
<li>Fixed window icons on Wayland</li>
|
||||
<li>Fixed number pad enter key functionality in the UI</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="3.0.0" date="2021-01-17">
|
||||
<description>
|
||||
<p>New features:</p>
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 13 KiB |
@@ -5,19 +5,9 @@
|
||||
# pairing data will be lost each reboot.
|
||||
HOME=/usr/local/moonlight
|
||||
|
||||
# For a seamless transition across the Qt 5.9 -> Qt 5.14 boundary,
|
||||
# we bundle binaries built with both Qt 5.9 and Qt 5.14 and pick
|
||||
# the correct one at runtime.
|
||||
if [ -d "/usr/local/Qt-5.14.1" ]; then
|
||||
BIN="moonlight"
|
||||
else
|
||||
BIN="moonlight59"
|
||||
fi
|
||||
|
||||
# Renice PE_Single_CPU which seems to host A/V stuff
|
||||
renice -10 -p $(pidof PE_Single_CPU)
|
||||
|
||||
# Renice Moonlight itself to avoid preemption by background tasks
|
||||
# Write output to a logfile in /tmp
|
||||
echo Launching $BIN
|
||||
exec nice -n -10 ./bin/$BIN > /tmp/moonlight.log
|
||||
exec nice -n -10 ./bin/moonlight > /tmp/moonlight.log
|
||||
|
||||
+136
-100
@@ -73,6 +73,9 @@ CenteredGridView {
|
||||
width: 220; height: 287;
|
||||
grid: appGrid
|
||||
|
||||
property alias appContextMenu: appContextMenuLoader.item
|
||||
property alias appNameText: appNameTextLoader.item
|
||||
|
||||
// Dim the app if it's hidden
|
||||
opacity: model.hidden ? 0.4 : 1.0
|
||||
|
||||
@@ -85,9 +88,14 @@ CenteredGridView {
|
||||
source: model.boxart
|
||||
|
||||
onSourceSizeChanged: {
|
||||
if ((sourceSize.width == 130 && sourceSize.height == 180) || // GFE 2.0 placeholder image
|
||||
(sourceSize.width == 628 && sourceSize.height == 888) || // GFE 3.0 placeholder image
|
||||
(sourceSize.width == 200 && sourceSize.height == 266)) // Our no_app_image.png
|
||||
// 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
|
||||
// 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 &&
|
||||
((sourceSize.width == 130 && sourceSize.height == 180) || // GFE 2.0 placeholder image
|
||||
(sourceSize.width == 628 && sourceSize.height == 888) || // GFE 3.0 placeholder image
|
||||
(sourceSize.width == 200 && sourceSize.height == 266))) // Our no_app_image.png
|
||||
{
|
||||
isPlaceholder = true
|
||||
}
|
||||
@@ -104,81 +112,94 @@ CenteredGridView {
|
||||
ToolTip.text: model.name
|
||||
ToolTip.delay: 1000
|
||||
ToolTip.timeout: 5000
|
||||
ToolTip.visible: (parent.hovered || parent.highlighted) && (!appNameText.visible || appNameText.truncated)
|
||||
ToolTip.visible: (parent.hovered || parent.highlighted) && (!appNameText || appNameText.truncated)
|
||||
}
|
||||
|
||||
ToolButton {
|
||||
id: resumeButton
|
||||
anchors.horizontalCenterOffset: appIcon.isPlaceholder ? -47 : 0
|
||||
anchors.verticalCenterOffset: appIcon.isPlaceholder ? -75 : -60
|
||||
anchors.centerIn: appIcon
|
||||
visible: model.running
|
||||
implicitWidth: 125
|
||||
implicitHeight: 125
|
||||
Loader {
|
||||
active: model.running
|
||||
asynchronous: true
|
||||
anchors.fill: appIcon
|
||||
|
||||
Image {
|
||||
source: "qrc:/res/baseline-play_circle_filled_white-48px.svg"
|
||||
anchors.centerIn: parent
|
||||
sourceSize {
|
||||
width: 75
|
||||
height: 75
|
||||
sourceComponent: Item {
|
||||
ToolButton {
|
||||
anchors.horizontalCenterOffset: appIcon.isPlaceholder ? -47 : 0
|
||||
anchors.verticalCenterOffset: appIcon.isPlaceholder ? -75 : -60
|
||||
anchors.centerIn: parent
|
||||
implicitWidth: 125
|
||||
implicitHeight: 125
|
||||
|
||||
Image {
|
||||
source: "qrc:/res/baseline-play_circle_filled_white-48px.svg"
|
||||
anchors.centerIn: parent
|
||||
sourceSize {
|
||||
width: 75
|
||||
height: 75
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
launchOrResumeSelectedApp(true)
|
||||
}
|
||||
|
||||
ToolTip.text: qsTr("Resume Game")
|
||||
ToolTip.delay: 1000
|
||||
ToolTip.timeout: 3000
|
||||
ToolTip.visible: hovered
|
||||
}
|
||||
|
||||
ToolButton {
|
||||
anchors.horizontalCenterOffset: appIcon.isPlaceholder ? 47 : 0
|
||||
anchors.verticalCenterOffset: appIcon.isPlaceholder ? -75 : 60
|
||||
anchors.centerIn: parent
|
||||
implicitWidth: 125
|
||||
implicitHeight: 125
|
||||
|
||||
Image {
|
||||
source: "qrc:/res/baseline-cancel-24px.svg"
|
||||
anchors.centerIn: parent
|
||||
sourceSize {
|
||||
width: 75
|
||||
height: 75
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
doQuitGame()
|
||||
}
|
||||
|
||||
ToolTip.text: qsTr("Quit Game")
|
||||
ToolTip.delay: 1000
|
||||
ToolTip.timeout: 3000
|
||||
ToolTip.visible: hovered
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
launchOrResumeSelectedApp(true)
|
||||
}
|
||||
|
||||
ToolTip.text: qsTr("Resume Game")
|
||||
ToolTip.delay: 1000
|
||||
ToolTip.timeout: 3000
|
||||
ToolTip.visible: hovered
|
||||
}
|
||||
|
||||
ToolButton {
|
||||
id: quitButton
|
||||
anchors.horizontalCenterOffset: appIcon.isPlaceholder ? 47 : 0
|
||||
anchors.verticalCenterOffset: appIcon.isPlaceholder ? -75 : 60
|
||||
anchors.centerIn: appIcon
|
||||
visible: model.running
|
||||
implicitWidth: 125
|
||||
implicitHeight: 125
|
||||
Loader {
|
||||
id: appNameTextLoader
|
||||
active: appIcon.isPlaceholder
|
||||
|
||||
Image {
|
||||
source: "qrc:/res/baseline-cancel-24px.svg"
|
||||
anchors.centerIn: parent
|
||||
sourceSize {
|
||||
width: 75
|
||||
height: 75
|
||||
}
|
||||
}
|
||||
// This loader is not asynchronous to avoid noticeable differences
|
||||
// in the time in which the text loads for each game.
|
||||
|
||||
onClicked: {
|
||||
doQuitGame()
|
||||
}
|
||||
|
||||
ToolTip.text: qsTr("Quit Game")
|
||||
ToolTip.delay: 1000
|
||||
ToolTip.timeout: 3000
|
||||
ToolTip.visible: hovered
|
||||
}
|
||||
|
||||
Label {
|
||||
id: appNameText
|
||||
visible: appIcon.isPlaceholder
|
||||
text: model.name
|
||||
width: appIcon.width
|
||||
height: model.running ? 175 : appIcon.height
|
||||
leftPadding: 20
|
||||
rightPadding: 20
|
||||
|
||||
anchors.left: appIcon.left
|
||||
anchors.right: appIcon.right
|
||||
anchors.bottom: appIcon.bottom
|
||||
font.pointSize: 22
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
wrapMode: Text.Wrap
|
||||
elide: Text.ElideRight
|
||||
|
||||
sourceComponent: Label {
|
||||
id: appNameText
|
||||
text: model.name
|
||||
font.pointSize: 22
|
||||
leftPadding: 20
|
||||
rightPadding: 20
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
wrapMode: Text.Wrap
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
|
||||
function launchOrResumeSelectedApp(quitExistingApp)
|
||||
@@ -245,6 +266,17 @@ CenteredGridView {
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onEnterPressed: {
|
||||
// Open the app context menu if activated via the gamepad or keyboard
|
||||
// for running games. If the game isn't running, the above onClicked
|
||||
// method will handle the launch.
|
||||
if (model.running) {
|
||||
// This will be keyboard/gamepad driven so use
|
||||
// open() instead of popup()
|
||||
appContextMenu.open()
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onMenuPressed: {
|
||||
// This will be keyboard/gamepad driven so use open() instead of popup()
|
||||
appContextMenu.open()
|
||||
@@ -256,44 +288,48 @@ CenteredGridView {
|
||||
quitAppDialog.open()
|
||||
}
|
||||
|
||||
NavigableMenu {
|
||||
id: appContextMenu
|
||||
NavigableMenuItem {
|
||||
parentMenu: appContextMenu
|
||||
text: model.running ? qsTr("Resume Game") : qsTr("Launch Game")
|
||||
onTriggered: launchOrResumeSelectedApp(true)
|
||||
}
|
||||
NavigableMenuItem {
|
||||
parentMenu: appContextMenu
|
||||
text: qsTr("Quit Game")
|
||||
onTriggered: doQuitGame()
|
||||
visible: model.running
|
||||
}
|
||||
NavigableMenuItem {
|
||||
parentMenu: appContextMenu
|
||||
checkable: true
|
||||
checked: model.directLaunch
|
||||
text: qsTr("Direct Launch")
|
||||
onTriggered: appModel.setAppDirectLaunch(model.index, !model.directLaunch)
|
||||
enabled: !model.hidden
|
||||
Loader {
|
||||
id: appContextMenuLoader
|
||||
asynchronous: true
|
||||
sourceComponent: NavigableMenu {
|
||||
id: appContextMenu
|
||||
NavigableMenuItem {
|
||||
parentMenu: appContextMenu
|
||||
text: model.running ? qsTr("Resume Game") : qsTr("Launch Game")
|
||||
onTriggered: launchOrResumeSelectedApp(true)
|
||||
}
|
||||
NavigableMenuItem {
|
||||
parentMenu: appContextMenu
|
||||
text: qsTr("Quit Game")
|
||||
onTriggered: doQuitGame()
|
||||
visible: model.running
|
||||
}
|
||||
NavigableMenuItem {
|
||||
parentMenu: appContextMenu
|
||||
checkable: true
|
||||
checked: model.directLaunch
|
||||
text: qsTr("Direct Launch")
|
||||
onTriggered: appModel.setAppDirectLaunch(model.index, !model.directLaunch)
|
||||
enabled: !model.hidden
|
||||
|
||||
ToolTip.text: qsTr("Launch this app immediately when the host is selected, bypassing the app selection grid.")
|
||||
ToolTip.delay: 1000
|
||||
ToolTip.timeout: 3000
|
||||
ToolTip.visible: hovered
|
||||
}
|
||||
NavigableMenuItem {
|
||||
parentMenu: appContextMenu
|
||||
checkable: true
|
||||
checked: model.hidden
|
||||
text: qsTr("Hide Game")
|
||||
onTriggered: appModel.setAppHidden(model.index, !model.hidden)
|
||||
enabled: model.hidden || (!model.running && !model.directLaunch)
|
||||
ToolTip.text: qsTr("Launch this app immediately when the host is selected, bypassing the app selection grid.")
|
||||
ToolTip.delay: 1000
|
||||
ToolTip.timeout: 3000
|
||||
ToolTip.visible: hovered
|
||||
}
|
||||
NavigableMenuItem {
|
||||
parentMenu: appContextMenu
|
||||
checkable: true
|
||||
checked: model.hidden
|
||||
text: qsTr("Hide Game")
|
||||
onTriggered: appModel.setAppHidden(model.index, !model.hidden)
|
||||
enabled: model.hidden || (!model.running && !model.directLaunch)
|
||||
|
||||
ToolTip.text: qsTr("Hide this game from the app grid. To access hidden games, right-click on the host and choose %1.").arg(qsTr("View All Apps"))
|
||||
ToolTip.delay: 1000
|
||||
ToolTip.timeout: 5000
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.text: qsTr("Hide this game from the app grid. To access hidden games, right-click on the host and choose %1.").arg(qsTr("View All Apps"))
|
||||
ToolTip.delay: 1000
|
||||
ToolTip.timeout: 5000
|
||||
ToolTip.visible: hovered
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,4 +21,7 @@ ItemDelegate {
|
||||
Keys.onReturnPressed: {
|
||||
clicked()
|
||||
}
|
||||
Keys.onEnterPressed: {
|
||||
clicked()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,10 @@ MenuItem {
|
||||
triggered()
|
||||
}
|
||||
|
||||
Keys.onEnterPressed: {
|
||||
triggered()
|
||||
}
|
||||
|
||||
Keys.onEscapePressed: {
|
||||
parentMenu.close()
|
||||
}
|
||||
|
||||
@@ -56,6 +56,10 @@ NavigableDialog {
|
||||
accept()
|
||||
}
|
||||
|
||||
Keys.onEnterPressed: {
|
||||
accept()
|
||||
}
|
||||
|
||||
Keys.onEscapePressed: {
|
||||
reject()
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import QtQuick 2.0
|
||||
import QtQuick.Controls 2.2
|
||||
import QtQuick.Layouts 1.3
|
||||
|
||||
ToolButton {
|
||||
property string iconSource
|
||||
@@ -20,13 +21,16 @@ ToolButton {
|
||||
|
||||
// This determines the size of the Material highlight. We increase it
|
||||
// from the default because we use larger than normal icons for TV readability.
|
||||
background.width: (parent.height - parent.anchors.bottomMargin - parent.anchors.topMargin) * 0.60
|
||||
background.height: (parent.height - parent.anchors.bottomMargin - parent.anchors.topMargin) * 0.60
|
||||
Layout.preferredHeight: parent.height
|
||||
|
||||
Keys.onReturnPressed: {
|
||||
clicked()
|
||||
}
|
||||
|
||||
Keys.onEnterPressed: {
|
||||
clicked()
|
||||
}
|
||||
|
||||
Keys.onRightPressed: {
|
||||
nextItemInFocusChain(true).forceActiveFocus(Qt.TabFocus)
|
||||
}
|
||||
|
||||
+60
-45
@@ -113,6 +113,8 @@ CenteredGridView {
|
||||
width: 300; height: 320;
|
||||
grid: pcGrid
|
||||
|
||||
property alias pcContextMenu : pcContextMenuLoader.item
|
||||
|
||||
Image {
|
||||
id: pcIcon
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
@@ -160,61 +162,70 @@ CenteredGridView {
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
NavigableMenu {
|
||||
id: pcContextMenu
|
||||
MenuItem {
|
||||
text: qsTr("PC Status: %1").arg(model.online ? qsTr("Online") : qsTr("Offline"))
|
||||
font.bold: true
|
||||
enabled: false
|
||||
}
|
||||
NavigableMenuItem {
|
||||
parentMenu: pcContextMenu
|
||||
text: qsTr("View All Apps")
|
||||
onTriggered: {
|
||||
var component = Qt.createComponent("AppView.qml")
|
||||
var appView = component.createObject(stackView, {"computerIndex": index, "objectName": model.name, "showHiddenGames": true})
|
||||
stackView.push(appView)
|
||||
Loader {
|
||||
id: pcContextMenuLoader
|
||||
asynchronous: true
|
||||
sourceComponent: NavigableMenu {
|
||||
id: pcContextMenu
|
||||
MenuItem {
|
||||
text: qsTr("PC Status: %1").arg(model.online ? qsTr("Online") : qsTr("Offline"))
|
||||
font.bold: true
|
||||
enabled: false
|
||||
}
|
||||
visible: model.online && model.paired
|
||||
}
|
||||
NavigableMenuItem {
|
||||
parentMenu: pcContextMenu
|
||||
text: qsTr("Wake PC")
|
||||
onTriggered: computerModel.wakeComputer(index)
|
||||
visible: !model.online && model.wakeable
|
||||
}
|
||||
NavigableMenuItem {
|
||||
parentMenu: pcContextMenu
|
||||
text: qsTr("Test Network")
|
||||
onTriggered: {
|
||||
computerModel.testConnectionForComputer(index)
|
||||
testConnectionDialog.open()
|
||||
NavigableMenuItem {
|
||||
parentMenu: pcContextMenu
|
||||
text: qsTr("View All Apps")
|
||||
onTriggered: {
|
||||
var component = Qt.createComponent("AppView.qml")
|
||||
var appView = component.createObject(stackView, {"computerIndex": index, "objectName": model.name, "showHiddenGames": true})
|
||||
stackView.push(appView)
|
||||
}
|
||||
visible: model.online && model.paired
|
||||
}
|
||||
NavigableMenuItem {
|
||||
parentMenu: pcContextMenu
|
||||
text: qsTr("Wake PC")
|
||||
onTriggered: computerModel.wakeComputer(index)
|
||||
visible: !model.online && model.wakeable
|
||||
}
|
||||
NavigableMenuItem {
|
||||
parentMenu: pcContextMenu
|
||||
text: qsTr("Test Network")
|
||||
onTriggered: {
|
||||
computerModel.testConnectionForComputer(index)
|
||||
testConnectionDialog.open()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NavigableMenuItem {
|
||||
parentMenu: pcContextMenu
|
||||
text: qsTr("Rename PC")
|
||||
onTriggered: {
|
||||
renamePcDialog.pcIndex = index
|
||||
renamePcDialog.originalName = model.name
|
||||
renamePcDialog.open()
|
||||
NavigableMenuItem {
|
||||
parentMenu: pcContextMenu
|
||||
text: qsTr("Rename PC")
|
||||
onTriggered: {
|
||||
renamePcDialog.pcIndex = index
|
||||
renamePcDialog.originalName = model.name
|
||||
renamePcDialog.open()
|
||||
}
|
||||
}
|
||||
}
|
||||
NavigableMenuItem {
|
||||
parentMenu: pcContextMenu
|
||||
text: qsTr("Delete PC")
|
||||
onTriggered: {
|
||||
deletePcDialog.pcIndex = index
|
||||
// get confirmation first, actual closing is called from the dialog
|
||||
deletePcDialog.open()
|
||||
NavigableMenuItem {
|
||||
parentMenu: pcContextMenu
|
||||
text: qsTr("Delete PC")
|
||||
onTriggered: {
|
||||
deletePcDialog.pcIndex = index
|
||||
// get confirmation first, actual closing is called from the dialog
|
||||
deletePcDialog.open()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
if (model.online) {
|
||||
if (model.paired) {
|
||||
if (!model.serverSupported) {
|
||||
errorDialog.text = qsTr("The version of GeForce Experience on %1 is not supported by this build of Moonlight. You must update Moonlight to stream from %1.").arg(model.name)
|
||||
errorDialog.helpText = ""
|
||||
errorDialog.open()
|
||||
}
|
||||
else if (model.paired) {
|
||||
// go to game view
|
||||
var component = Qt.createComponent("AppView.qml")
|
||||
var appView = component.createObject(stackView, {"computerIndex": index, "objectName": model.name})
|
||||
@@ -379,6 +390,10 @@ CenteredGridView {
|
||||
Keys.onReturnPressed: {
|
||||
renamePcDialog.accept()
|
||||
}
|
||||
|
||||
Keys.onEnterPressed: {
|
||||
renamePcDialog.accept()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+236
-50
@@ -358,6 +358,10 @@ Flickable {
|
||||
Keys.onReturnPressed: {
|
||||
customResolutionDialog.accept()
|
||||
}
|
||||
|
||||
Keys.onEnterPressed: {
|
||||
customResolutionDialog.accept()
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
@@ -382,6 +386,10 @@ Flickable {
|
||||
Keys.onReturnPressed: {
|
||||
customResolutionDialog.accept()
|
||||
}
|
||||
|
||||
Keys.onEnterPressed: {
|
||||
customResolutionDialog.accept()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -519,12 +527,17 @@ Flickable {
|
||||
text: qsTr("Display mode")
|
||||
font.pointSize: 12
|
||||
wrapMode: Text.Wrap
|
||||
visible: SystemProperties.hasWindowManager
|
||||
visible: SystemProperties.hasDesktopEnvironment
|
||||
}
|
||||
|
||||
AutoResizingComboBox {
|
||||
// ignore setting the index at first, and actually set it when the component is loaded
|
||||
Component.onCompleted: {
|
||||
if (!visible) {
|
||||
// Do nothing if the control won't even be visible
|
||||
return
|
||||
}
|
||||
|
||||
// Set the recommended option based on the OS
|
||||
for (var i = 0; i < windowModeListModel.count; i++) {
|
||||
var thisWm = windowModeListModel.get(i).val;
|
||||
@@ -537,22 +550,20 @@ Flickable {
|
||||
|
||||
currentIndex = 0
|
||||
|
||||
if (SystemProperties.hasWindowManager && !SystemProperties.rendererAlwaysFullScreen) {
|
||||
var savedWm = StreamingPreferences.windowMode
|
||||
for (var i = 0; i < windowModeListModel.count; i++) {
|
||||
var thisWm = windowModeListModel.get(i).val;
|
||||
if (savedWm === thisWm) {
|
||||
currentIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
var savedWm = StreamingPreferences.windowMode
|
||||
for (var i = 0; i < windowModeListModel.count; i++) {
|
||||
var thisWm = windowModeListModel.get(i).val;
|
||||
if (savedWm === thisWm) {
|
||||
currentIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
activated(currentIndex)
|
||||
}
|
||||
|
||||
id: windowModeComboBox
|
||||
visible: SystemProperties.hasWindowManager
|
||||
visible: SystemProperties.hasDesktopEnvironment
|
||||
enabled: !SystemProperties.rendererAlwaysFullScreen
|
||||
hoverEnabled: true
|
||||
textRole: "text"
|
||||
@@ -697,7 +708,7 @@ Flickable {
|
||||
width: parent.width
|
||||
text: qsTr("Mute audio stream when Moonlight is not the active window")
|
||||
font.pointSize: 12
|
||||
visible: SystemProperties.hasWindowManager
|
||||
visible: SystemProperties.hasDesktopEnvironment
|
||||
checked: StreamingPreferences.muteOnFocusLoss
|
||||
onCheckedChanged: {
|
||||
StreamingPreferences.muteOnFocusLoss = checked
|
||||
@@ -724,37 +735,155 @@ Flickable {
|
||||
|
||||
Label {
|
||||
width: parent.width
|
||||
id: uiDisplayModeTitle
|
||||
text: qsTr("GUI display mode")
|
||||
id: languageTitle
|
||||
text: qsTr("Language")
|
||||
font.pointSize: 12
|
||||
wrapMode: Text.Wrap
|
||||
visible: SystemProperties.hasWindowManager
|
||||
}
|
||||
|
||||
AutoResizingComboBox {
|
||||
// ignore setting the index at first, and actually set it when the component is loaded
|
||||
Component.onCompleted: {
|
||||
if (SystemProperties.hasWindowManager) {
|
||||
var saved_uidisplaymode = StreamingPreferences.uiDisplayMode
|
||||
currentIndex = 0
|
||||
for (var i = 0; i < uiDisplayModeListModel.count; i++) {
|
||||
var el_uidisplaymode = uiDisplayModeListModel.get(i).val;
|
||||
if (saved_uidisplaymode === el_uidisplaymode) {
|
||||
currentIndex = i
|
||||
break
|
||||
}
|
||||
var saved_language = StreamingPreferences.language
|
||||
currentIndex = 0
|
||||
for (var i = 0; i < languageListModel.count; i++) {
|
||||
var el_language = languageListModel.get(i).val;
|
||||
if (saved_language === el_language) {
|
||||
currentIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Full-screen is always selected when there is no window manager
|
||||
currentIndex = 2
|
||||
|
||||
activated(currentIndex)
|
||||
}
|
||||
|
||||
id: languageComboBox
|
||||
textRole: "text"
|
||||
model: ListModel {
|
||||
id: languageListModel
|
||||
ListElement {
|
||||
text: qsTr("Automatic")
|
||||
val: StreamingPreferences.LANG_AUTO
|
||||
}
|
||||
ListElement {
|
||||
text: "Deutsch" // German
|
||||
val: StreamingPreferences.LANG_DE
|
||||
}
|
||||
ListElement {
|
||||
text: "English"
|
||||
val: StreamingPreferences.LANG_EN
|
||||
}
|
||||
ListElement {
|
||||
text: "Français" // French
|
||||
val: StreamingPreferences.LANG_FR
|
||||
}
|
||||
ListElement {
|
||||
text: "简体中文" // Simplified Chinese
|
||||
val: StreamingPreferences.LANG_ZH_CN
|
||||
}
|
||||
ListElement {
|
||||
text: "Norwegian Bokmål"
|
||||
val: StreamingPreferences.LANG_NB_NO
|
||||
}
|
||||
ListElement {
|
||||
text: "русский" // Russian
|
||||
val: StreamingPreferences.LANG_RU
|
||||
}
|
||||
ListElement {
|
||||
text: "Español" // Spanish
|
||||
val: StreamingPreferences.LANG_ES
|
||||
}
|
||||
ListElement {
|
||||
text: "日本語" // Japanese
|
||||
val: StreamingPreferences.LANG_JA
|
||||
}
|
||||
ListElement {
|
||||
text: "Tiếng Việt" // Vietnamese
|
||||
val: StreamingPreferences.LANG_VI
|
||||
}
|
||||
ListElement {
|
||||
text: "ภาษาไทย" // Thai
|
||||
val: StreamingPreferences.LANG_TH
|
||||
}
|
||||
ListElement {
|
||||
text: "한국어" // Korean
|
||||
val: StreamingPreferences.LANG_KO
|
||||
}
|
||||
/* ListElement {
|
||||
text: "Magyar" // Hungarian
|
||||
val: StreamingPreferences.LANG_HU
|
||||
} */
|
||||
ListElement {
|
||||
text: "Nederlands" // Dutch
|
||||
val: StreamingPreferences.LANG_NL
|
||||
}
|
||||
ListElement {
|
||||
text: "Svenska" // Swedish
|
||||
val: StreamingPreferences.LANG_SV
|
||||
}
|
||||
/* ListElement {
|
||||
text: "Türkçe" // Turkish
|
||||
val: StreamingPreferences.LANG_TR
|
||||
} */
|
||||
/* ListElement {
|
||||
text: "Українська" // Ukrainian
|
||||
val: StreamingPreferences.LANG_UK
|
||||
} */
|
||||
ListElement {
|
||||
text: "繁体字" // Traditional Chinese
|
||||
val: StreamingPreferences.LANG_ZH_HANT
|
||||
}
|
||||
}
|
||||
// ::onActivated must be used, as it only listens for when the index is changed by a human
|
||||
onActivated : {
|
||||
// Retranslating is expensive, so only do it if the language actually changed
|
||||
var new_language = languageListModel.get(currentIndex).val
|
||||
if (StreamingPreferences.language !== new_language) {
|
||||
StreamingPreferences.language = languageListModel.get(currentIndex).val
|
||||
if (!StreamingPreferences.retranslate()) {
|
||||
ToolTip.show(qsTr("You must restart Moonlight for this change to take effect"), 5000)
|
||||
}
|
||||
else {
|
||||
// Force the back operation to pop any AppView pages that exist.
|
||||
// The AppView stops working after retranslate() for some reason.
|
||||
window.clearOnBack = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
width: parent.width
|
||||
id: uiDisplayModeTitle
|
||||
text: qsTr("GUI display mode")
|
||||
font.pointSize: 12
|
||||
wrapMode: Text.Wrap
|
||||
visible: SystemProperties.hasDesktopEnvironment
|
||||
}
|
||||
|
||||
AutoResizingComboBox {
|
||||
// ignore setting the index at first, and actually set it when the component is loaded
|
||||
Component.onCompleted: {
|
||||
if (!visible) {
|
||||
// Do nothing if the control won't even be visible
|
||||
return
|
||||
}
|
||||
|
||||
var saved_uidisplaymode = StreamingPreferences.uiDisplayMode
|
||||
currentIndex = 0
|
||||
for (var i = 0; i < uiDisplayModeListModel.count; i++) {
|
||||
var el_uidisplaymode = uiDisplayModeListModel.get(i).val;
|
||||
if (saved_uidisplaymode === el_uidisplaymode) {
|
||||
currentIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
activated(currentIndex)
|
||||
}
|
||||
|
||||
id: uiDisplayModeComboBox
|
||||
visible: SystemProperties.hasWindowManager
|
||||
visible: SystemProperties.hasDesktopEnvironment
|
||||
textRole: "text"
|
||||
model: ListModel {
|
||||
id: uiDisplayModeListModel
|
||||
@@ -833,7 +962,6 @@ Flickable {
|
||||
width: parent.width
|
||||
text: qsTr("Optimize mouse for remote desktop instead of games")
|
||||
font.pointSize: 12
|
||||
visible: SystemProperties.hasWindowManager
|
||||
checked: StreamingPreferences.absoluteMouseMode
|
||||
onCheckedChanged: {
|
||||
StreamingPreferences.absoluteMouseMode = checked
|
||||
@@ -847,25 +975,79 @@ Flickable {
|
||||
qsTr("NOTE: Due to a bug in GeForce Experience, this option may not work properly if your host PC has multiple monitors.")
|
||||
}
|
||||
|
||||
CheckBox {
|
||||
id: captureSysKeysCheck
|
||||
hoverEnabled: true
|
||||
Row {
|
||||
spacing: 5
|
||||
width: parent.width
|
||||
text: qsTr("Capture system keyboard shortcuts ") +
|
||||
(!SystemProperties.isRunningWayland ? qsTr("while streaming in fullscreen") :
|
||||
qsTr("(unsupported on Wayland)"))
|
||||
font.pointSize: 12
|
||||
enabled: SystemProperties.hasWindowManager && !SystemProperties.isRunningWayland
|
||||
checked: (StreamingPreferences.captureSysKeys || !SystemProperties.hasWindowManager) && !SystemProperties.isRunningWayland
|
||||
onCheckedChanged: {
|
||||
StreamingPreferences.captureSysKeys = checked
|
||||
|
||||
CheckBox {
|
||||
id: captureSysKeysCheck
|
||||
hoverEnabled: true
|
||||
text: qsTr("Capture system keyboard shortcuts")
|
||||
font.pointSize: 12
|
||||
enabled: SystemProperties.hasDesktopEnvironment
|
||||
checked: StreamingPreferences.captureSysKeysMode !== StreamingPreferences.CSK_OFF || !SystemProperties.hasDesktopEnvironment
|
||||
|
||||
ToolTip.delay: 1000
|
||||
ToolTip.timeout: 10000
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.text: qsTr("This enables the capture of system-wide keyboard shortcuts like Alt+Tab that would normally be handled by the client OS while streaming.") + "\n\n" +
|
||||
qsTr("NOTE: Certain keyboard shortcuts like Ctrl+Alt+Del on Windows cannot be intercepted by any application, including Moonlight.")
|
||||
}
|
||||
|
||||
ToolTip.delay: 1000
|
||||
ToolTip.timeout: 10000
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.text: qsTr("This enables the capture of system-wide keyboard shortcuts like Alt+Tab that would normally be handled by the client OS while streaming in fullscreen.") + "\n\n" +
|
||||
qsTr("NOTE: Certain keyboard shortcuts like Ctrl+Alt+Del on Windows cannot be intercepted by any application, including Moonlight.")
|
||||
AutoResizingComboBox {
|
||||
// ignore setting the index at first, and actually set it when the component is loaded
|
||||
Component.onCompleted: {
|
||||
if (!visible) {
|
||||
// Do nothing if the control won't even be visible
|
||||
return
|
||||
}
|
||||
|
||||
var saved_syskeysmode = StreamingPreferences.captureSysKeysMode
|
||||
currentIndex = 0
|
||||
for (var i = 0; i < captureSysKeysModeListModel.count; i++) {
|
||||
var el_syskeysmode = captureSysKeysModeListModel.get(i).val;
|
||||
if (saved_syskeysmode === el_syskeysmode) {
|
||||
currentIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
activated(currentIndex)
|
||||
}
|
||||
|
||||
enabled: captureSysKeysCheck.checked && captureSysKeysCheck.enabled
|
||||
textRole: "text"
|
||||
model: ListModel {
|
||||
id: captureSysKeysModeListModel
|
||||
ListElement {
|
||||
text: qsTr("in fullscreen")
|
||||
val: StreamingPreferences.CSK_FULLSCREEN
|
||||
}
|
||||
ListElement {
|
||||
text: qsTr("always")
|
||||
val: StreamingPreferences.CSK_ALWAYS
|
||||
}
|
||||
}
|
||||
|
||||
function updatePref() {
|
||||
if (!enabled) {
|
||||
StreamingPreferences.captureSysKeysMode = StreamingPreferences.CSK_OFF
|
||||
}
|
||||
else {
|
||||
StreamingPreferences.captureSysKeysMode = captureSysKeysModeListModel.get(currentIndex).val
|
||||
}
|
||||
}
|
||||
|
||||
// ::onActivated must be used, as it only listens for when the index is changed by a human
|
||||
onActivated: {
|
||||
updatePref()
|
||||
}
|
||||
|
||||
// This handles transition of the checkbox state
|
||||
onEnabledChanged: {
|
||||
updatePref()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CheckBox {
|
||||
@@ -980,7 +1162,7 @@ Flickable {
|
||||
width: parent.width
|
||||
text: qsTr("Process gamepad input when Moonlight is in the background")
|
||||
font.pointSize: 12
|
||||
visible: SystemProperties.hasWindowManager
|
||||
visible: SystemProperties.hasDesktopEnvironment
|
||||
checked: StreamingPreferences.backgroundGamepad
|
||||
onCheckedChanged: {
|
||||
StreamingPreferences.backgroundGamepad = checked
|
||||
@@ -1102,6 +1284,13 @@ Flickable {
|
||||
AutoResizingComboBox {
|
||||
// ignore setting the index at first, and actually set it when the component is loaded
|
||||
Component.onCompleted: {
|
||||
if (SystemProperties.supportsHdr) {
|
||||
codecListModel.append({
|
||||
"text": qsTr("HEVC HDR (Experimental)"),
|
||||
"val": StreamingPreferences.VCC_FORCE_HEVC_HDR
|
||||
});
|
||||
}
|
||||
|
||||
var saved_vcc = StreamingPreferences.videoCodecConfig
|
||||
currentIndex = 0
|
||||
for(var i = 0; i < codecListModel.count; i++) {
|
||||
@@ -1130,10 +1319,7 @@ Flickable {
|
||||
text: qsTr("HEVC (H.265)")
|
||||
val: StreamingPreferences.VCC_FORCE_HEVC
|
||||
}
|
||||
/*ListElement {
|
||||
text: qsTr("HEVC HDR (Experimental)")
|
||||
val: StreamingPreferences.VCC_FORCE_HEVC_HDR
|
||||
}*/
|
||||
/* HEVC HDR will be appended here */
|
||||
}
|
||||
// ::onActivated must be used, as it only listens for when the index is changed by a human
|
||||
onActivated : {
|
||||
|
||||
@@ -91,6 +91,8 @@ QVariant AppModel::data(const QModelIndex &index, int role) const
|
||||
return app.id;
|
||||
case DirectLaunchRole:
|
||||
return app.directLaunch;
|
||||
case AppCollectorGameRole:
|
||||
return app.isAppCollectorGame;
|
||||
default:
|
||||
return QVariant();
|
||||
}
|
||||
@@ -106,6 +108,7 @@ QHash<int, QByteArray> AppModel::roleNames() const
|
||||
names[HiddenRole] = "hidden";
|
||||
names[AppIdRole] = "appid";
|
||||
names[DirectLaunchRole] = "directLaunch";
|
||||
names[AppCollectorGameRole] = "appCollectorGame";
|
||||
|
||||
return names;
|
||||
}
|
||||
|
||||
+2
-1
@@ -17,7 +17,8 @@ class AppModel : public QAbstractListModel
|
||||
BoxArtRole,
|
||||
HiddenRole,
|
||||
AppIdRole,
|
||||
DirectLaunchRole
|
||||
DirectLaunchRole,
|
||||
AppCollectorGameRole,
|
||||
};
|
||||
|
||||
public:
|
||||
|
||||
@@ -42,6 +42,8 @@ QVariant ComputerModel::data(const QModelIndex& index, int role) const
|
||||
return !computer->macAddress.isEmpty();
|
||||
case StatusUnknownRole:
|
||||
return computer->state == NvComputer::CS_UNKNOWN;
|
||||
case ServerSupportedRole:
|
||||
return computer->isSupportedServerVersion;
|
||||
default:
|
||||
return QVariant();
|
||||
}
|
||||
@@ -68,6 +70,7 @@ QHash<int, QByteArray> ComputerModel::roleNames() const
|
||||
names[BusyRole] = "busy";
|
||||
names[WakeableRole] = "wakeable";
|
||||
names[StatusUnknownRole] = "statusUnknown";
|
||||
names[ServerSupportedRole] = "serverSupported";
|
||||
|
||||
return names;
|
||||
}
|
||||
|
||||
@@ -14,7 +14,8 @@ class ComputerModel : public QAbstractListModel
|
||||
PairedRole,
|
||||
BusyRole,
|
||||
WakeableRole,
|
||||
StatusUnknownRole
|
||||
StatusUnknownRole,
|
||||
ServerSupportedRole
|
||||
};
|
||||
|
||||
public:
|
||||
|
||||
+27
-7
@@ -12,13 +12,18 @@ import SdlGamepadKeyNavigation 1.0
|
||||
ApplicationWindow {
|
||||
property bool pollingActive: false
|
||||
|
||||
// Set by SettingsView to force the back operation to pop all
|
||||
// pages except the initial view. This is required when doing
|
||||
// a retranslate() because AppView breaks for some reason.
|
||||
property bool clearOnBack: false
|
||||
|
||||
id: window
|
||||
visible: true
|
||||
width: 1280
|
||||
height: 600
|
||||
|
||||
visibility: {
|
||||
if (SystemProperties.hasWindowManager) {
|
||||
if (SystemProperties.hasDesktopEnvironment) {
|
||||
if (StreamingPreferences.uiDisplayMode == StreamingPreferences.UI_WINDOWED) return "Windowed"
|
||||
else if (StreamingPreferences.uiDisplayMode == StreamingPreferences.UI_MAXIMIZED) return "Maximized"
|
||||
else if (StreamingPreferences.uiDisplayMode == StreamingPreferences.UI_FULLSCREEN) return "FullScreen"
|
||||
@@ -31,6 +36,17 @@ ApplicationWindow {
|
||||
// it will never insert a line break and just extend on forever.
|
||||
ToolTip.toolTip.contentWidth: ToolTip.toolTip.implicitContentWidth < 400 ? ToolTip.toolTip.implicitContentWidth : 400
|
||||
|
||||
function goBack() {
|
||||
if (clearOnBack) {
|
||||
// Pop all items except the first one
|
||||
stackView.pop(null)
|
||||
clearOnBack = false
|
||||
}
|
||||
else {
|
||||
stackView.pop()
|
||||
}
|
||||
}
|
||||
|
||||
StackView {
|
||||
id: stackView
|
||||
initialItem: initialView
|
||||
@@ -46,7 +62,7 @@ ApplicationWindow {
|
||||
|
||||
Keys.onEscapePressed: {
|
||||
if (depth > 1) {
|
||||
stackView.pop()
|
||||
goBack()
|
||||
}
|
||||
else {
|
||||
quitConfirmationDialog.open()
|
||||
@@ -55,7 +71,7 @@ ApplicationWindow {
|
||||
|
||||
Keys.onBackPressed: {
|
||||
if (depth > 1) {
|
||||
stackView.pop()
|
||||
goBack()
|
||||
}
|
||||
else {
|
||||
quitConfirmationDialog.open()
|
||||
@@ -199,7 +215,7 @@ ApplicationWindow {
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: 20
|
||||
spacing: 10
|
||||
anchors.leftMargin: 10
|
||||
anchors.rightMargin: 10
|
||||
anchors.fill: parent
|
||||
@@ -210,7 +226,7 @@ ApplicationWindow {
|
||||
|
||||
iconSource: "qrc:/res/arrow_left.svg"
|
||||
|
||||
onClicked: stackView.pop()
|
||||
onClicked: goBack()
|
||||
|
||||
Keys.onDownPressed: {
|
||||
stackView.currentItem.forceActiveFocus(Qt.TabFocus)
|
||||
@@ -396,7 +412,7 @@ ApplicationWindow {
|
||||
|
||||
ErrorMessageDialog {
|
||||
id: noHwDecoderDialog
|
||||
text: qsTr("No functioning hardware accelerated H.264 video decoder was detected by Moonlight. " +
|
||||
text: qsTr("No functioning hardware accelerated video decoder was detected by Moonlight. " +
|
||||
"Your streaming performance may be severely degraded in this configuration.")
|
||||
helpText: qsTr("Click the Help button for more information on solving this problem.")
|
||||
helpUrl: "https://github.com/moonlight-stream/moonlight-docs/wiki/Fixing-Hardware-Decoding-Problems"
|
||||
@@ -477,7 +493,7 @@ ApplicationWindow {
|
||||
|
||||
onAccepted: {
|
||||
if (editText.text) {
|
||||
ComputerManager.addNewHost(editText.text.trim(), false)
|
||||
ComputerManager.addNewHostManually(editText.text.trim())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -495,6 +511,10 @@ ApplicationWindow {
|
||||
Keys.onReturnPressed: {
|
||||
addPcDialog.accept()
|
||||
}
|
||||
|
||||
Keys.onEnterPressed: {
|
||||
addPcDialog.accept()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,10 +12,11 @@
|
||||
SdlGamepadKeyNavigation::SdlGamepadKeyNavigation()
|
||||
: m_Enabled(false),
|
||||
m_UiNavMode(false),
|
||||
m_FirstPoll(false),
|
||||
m_LastAxisNavigationEventTime(0)
|
||||
{
|
||||
m_PollingTimer = new QTimer(this);
|
||||
connect(m_PollingTimer, SIGNAL(timeout()), this, SLOT(onPollingTimerFired()));
|
||||
connect(m_PollingTimer, &QTimer::timeout, this, &SdlGamepadKeyNavigation::onPollingTimerFired);
|
||||
}
|
||||
|
||||
SdlGamepadKeyNavigation::~SdlGamepadKeyNavigation()
|
||||
@@ -62,6 +63,9 @@ void SdlGamepadKeyNavigation::enable()
|
||||
}
|
||||
}
|
||||
|
||||
// Flush events on the first poll
|
||||
m_FirstPoll = true;
|
||||
|
||||
// Poll every 50 ms for a new joystick event
|
||||
m_PollingTimer->start(50);
|
||||
|
||||
@@ -91,6 +95,16 @@ void SdlGamepadKeyNavigation::onPollingTimerFired()
|
||||
SDL_Event event;
|
||||
StreamingPreferences prefs;
|
||||
|
||||
// Discard any pending button events on the first poll to avoid picking up
|
||||
// stale input data from the stream session (like the quit combo).
|
||||
if (m_FirstPoll) {
|
||||
SDL_PumpEvents();
|
||||
SDL_FlushEvent(SDL_CONTROLLERBUTTONDOWN);
|
||||
SDL_FlushEvent(SDL_CONTROLLERBUTTONUP);
|
||||
|
||||
m_FirstPoll = false;
|
||||
}
|
||||
|
||||
while (SDL_PollEvent(&event)) {
|
||||
switch (event.type) {
|
||||
case SDL_QUIT:
|
||||
|
||||
@@ -34,5 +34,6 @@ private:
|
||||
QList<SDL_GameController*> m_Gamepads;
|
||||
bool m_Enabled;
|
||||
bool m_UiNavMode;
|
||||
bool m_FirstPoll;
|
||||
Uint32 m_LastAxisNavigationEventTime;
|
||||
};
|
||||
|
||||
Binary file not shown.
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.
+475
-220
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.
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.
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.
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.
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.
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.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
+27
-16
@@ -11,7 +11,6 @@
|
||||
#include <QCursor>
|
||||
#include <QElapsedTimer>
|
||||
#include <QFile>
|
||||
#include<QTranslator>
|
||||
|
||||
// Don't let SDL hook our main function, since Qt is already
|
||||
// doing the same thing. This needs to be before any headers
|
||||
@@ -351,14 +350,16 @@ int main(int argc, char *argv[])
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(Q_OS_DARWIN) && QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
if (!qEnvironmentVariableIsSet("QSG_RHI_BACKEND")) {
|
||||
// The Metal backend in Qt 6.0.0 causes really strange issues transitioning to
|
||||
// full-screen in our SDL window (hangs in SDL's Cocoa_SetWindowFullscreenSpace())
|
||||
// and breaks drawing our status updates in the StreamSegue, so use OpenGL like
|
||||
// Qt 5 does until we figure out the cause of these issues.
|
||||
qputenv("QSG_RHI_BACKEND", "opengl");
|
||||
}
|
||||
#if !defined(Q_OS_WIN32) || QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
// Moonlight requires the non-threaded renderer because we depend
|
||||
// on being able to control the render thread by blocking in the
|
||||
// main thread (and pumping events from the main thread when needed).
|
||||
// That doesn't work with the threaded renderer which causes all
|
||||
// sorts of odd behavior depending on the platform.
|
||||
//
|
||||
// NB: Windows defaults to the "windows" non-threaded render loop on
|
||||
// Qt 5 and the threaded render loop on Qt 6.
|
||||
qputenv("QSG_RENDER_LOOP", "basic");
|
||||
#endif
|
||||
|
||||
// We don't want system proxies to apply to us
|
||||
@@ -394,6 +395,9 @@ int main(int argc, char *argv[])
|
||||
// future KMSDRM platforms that use SDL for rendering.
|
||||
SDL_SetHint(SDL_HINT_VIDEO_DOUBLE_BUFFER, "1");
|
||||
|
||||
// We use MMAL to render on Raspberry Pi, so we do not require DRM master.
|
||||
SDL_SetHint("SDL_KMSDRM_REQUIRE_DRM_MASTER", "0");
|
||||
|
||||
// Use Direct3D 9Ex to avoid a deadlock caused by the D3D device being reset when
|
||||
// the user triggers a UAC prompt. This option controls the software/SDL renderer.
|
||||
// The DXVA2 renderer uses Direct3D 9Ex itself directly.
|
||||
@@ -438,9 +442,11 @@ int main(int argc, char *argv[])
|
||||
// the mouse motion exactly how it was given to us.
|
||||
SDL_SetHint("SDL_MOUSE_RELATIVE_SCALING", "0");
|
||||
|
||||
// Set our app name for SDL to use with PulseAudio. This matches what we provide
|
||||
// as our app name to libsoundio too.
|
||||
// Set our app name for SDL to use with PulseAudio and PipeWire. This matches what we
|
||||
// provide as our app name to libsoundio too. On SDL 2.0.18+, SDL_APP_NAME is also used
|
||||
// for screensaver inhibitor reporting.
|
||||
SDL_SetHint("SDL_AUDIO_DEVICE_APP_NAME", "Moonlight");
|
||||
SDL_SetHint("SDL_APP_NAME", "Moonlight");
|
||||
|
||||
#ifdef QT_DEBUG
|
||||
// Allow thread naming using exceptions on debug builds. SDL doesn't use SEH
|
||||
@@ -451,9 +457,9 @@ int main(int argc, char *argv[])
|
||||
|
||||
QGuiApplication app(argc, argv);
|
||||
|
||||
QTranslator translator;
|
||||
qDebug() << "Translation loaded:" << translator.load(QString(":/languages/qml_") + QLocale::system().name());
|
||||
app.installTranslator(&translator);
|
||||
// Apply the initial translation based on user preference
|
||||
StreamingPreferences prefs;
|
||||
prefs.retranslate();
|
||||
|
||||
// After the QGuiApplication is created, the platform stuff will be initialized
|
||||
// and we can set the SDL video driver to match Qt.
|
||||
@@ -499,6 +505,11 @@ int main(int argc, char *argv[])
|
||||
app.setWindowIcon(QIcon(":/res/moonlight.svg"));
|
||||
#endif
|
||||
|
||||
// This is necessary to show our icon correctly on Wayland
|
||||
app.setDesktopFileName("com.moonlight_stream.Moonlight.desktop");
|
||||
qputenv("SDL_VIDEO_WAYLAND_WMCLASS", "com.moonlight_stream.Moonlight");
|
||||
qputenv("SDL_VIDEO_X11_WMCLASS", "com.moonlight_stream.Moonlight");
|
||||
|
||||
// Register our C++ types for QML
|
||||
qmlRegisterType<ComputerModel>("ComputerModel", 1, 0, "ComputerModel");
|
||||
qmlRegisterType<AppModel>("AppModel", 1, 0, "AppModel");
|
||||
@@ -525,8 +536,8 @@ int main(int argc, char *argv[])
|
||||
});
|
||||
qmlRegisterSingletonType<StreamingPreferences>("StreamingPreferences", 1, 0,
|
||||
"StreamingPreferences",
|
||||
[](QQmlEngine*, QJSEngine*) -> QObject* {
|
||||
return new StreamingPreferences();
|
||||
[](QQmlEngine* qmlEngine, QJSEngine*) -> QObject* {
|
||||
return new StreamingPreferences(qmlEngine);
|
||||
});
|
||||
|
||||
// Create the identity manager on the main thread
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
// This file contains hooks for several functions that allow
|
||||
// Qt and SDL to more or less share DRM master ownership.
|
||||
//
|
||||
// This technique requires Linux v5.8 or later, or for Moonlight
|
||||
// to run as root (with CAP_SYS_ADMIN). Prior to Linux v5.8,
|
||||
// DRM_IOCTL_DROP_MASTER required CAP_SYS_ADMIN, which prevents
|
||||
// our trick from working (without root, that is).
|
||||
//
|
||||
// The specific kernel change required to run without root is:
|
||||
// https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=45bc3d26c95a8fc63a7d8668ca9e57ef0883351c
|
||||
|
||||
#define _GNU_SOURCE
|
||||
|
||||
#include <SDL.h>
|
||||
#include <dlfcn.h>
|
||||
#include <unistd.h>
|
||||
#include <errno.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#include <xf86drm.h>
|
||||
#include <xf86drmMode.h>
|
||||
|
||||
// We require SDL 2.0.15+ to hook because it supports sharing
|
||||
// the DRM FD with our code. This avoids having multiple DRM FDs
|
||||
// in flight at the same time which would significantly complicate
|
||||
// the logic here because we'd need to figure out exactly which FD
|
||||
// should be the master at any given time. With the position of our
|
||||
// hooks, that is definitely not trivial.
|
||||
#if SDL_VERSION_ATLEAST(2, 0, 15)
|
||||
|
||||
// Qt's DRM master FD grabbed by our hook
|
||||
int g_QtDrmMasterFd = -1;
|
||||
struct stat64 g_DrmMasterStat;
|
||||
|
||||
// The DRM master FD created for SDL
|
||||
int g_SdlDrmMasterFd = -1;
|
||||
|
||||
// This hook will handle legacy DRM rendering
|
||||
int drmModeSetCrtc(int fd, uint32_t crtcId, uint32_t bufferId,
|
||||
uint32_t x, uint32_t y, uint32_t *connectors, int count,
|
||||
drmModeModeInfoPtr mode)
|
||||
{
|
||||
// Grab the first DRM FD that makes it in here. This will be the Qt
|
||||
// EGLFS backend's DRM FD, which we will need later.
|
||||
if (g_QtDrmMasterFd == -1) {
|
||||
g_QtDrmMasterFd = fd;
|
||||
fstat64(g_QtDrmMasterFd, &g_DrmMasterStat);
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Captured Qt EGLFS DRM master fd (legacy): %d",
|
||||
g_QtDrmMasterFd);
|
||||
}
|
||||
|
||||
// Call into the real thing
|
||||
return ((typeof(drmModeSetCrtc)*)dlsym(RTLD_NEXT, "drmModeSetCrtc"))(fd, crtcId, bufferId, x, y, connectors, count, mode);
|
||||
}
|
||||
|
||||
// This hook will handle atomic DRM rendering
|
||||
int drmModeAtomicCommit(int fd, drmModeAtomicReqPtr req,
|
||||
uint32_t flags, void *user_data)
|
||||
{
|
||||
// Grab the first DRM FD that makes it in here. This will be the Qt
|
||||
// EGLFS backend's DRM FD, which we will need later.
|
||||
if (g_QtDrmMasterFd == -1) {
|
||||
g_QtDrmMasterFd = fd;
|
||||
fstat64(g_QtDrmMasterFd, &g_DrmMasterStat);
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Captured Qt EGLFS DRM master fd (atomic): %d",
|
||||
g_QtDrmMasterFd);
|
||||
}
|
||||
|
||||
// Call into the real thing
|
||||
return ((typeof(drmModeAtomicCommit)*)dlsym(RTLD_NEXT, "drmModeAtomicCommit"))(fd, req, flags, user_data);
|
||||
}
|
||||
|
||||
// This hook will handle SDL's open() on the DRM device. We just need to
|
||||
// hook this variant of open(), since that's what SDL uses. When we see
|
||||
// the open a FD for the same card as the Qt DRM master FD, we'll drop
|
||||
// master on the Qt FD to allow the new FD to have master.
|
||||
int open(const char *pathname, int flags)
|
||||
{
|
||||
// Call the real thing to do the open operation
|
||||
int fd = ((typeof(open)*)dlsym(RTLD_NEXT, "open"))(pathname, flags);
|
||||
|
||||
// If the file was successfully opened and we have a DRM master FD,
|
||||
// check if the FD we just opened is a DRM device.
|
||||
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;
|
||||
|
||||
fstat64(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!
|
||||
|
||||
// This code assumes SDL only ever opens a single FD
|
||||
// for a given DRM device.
|
||||
SDL_assert(g_SdlDrmMasterFd == -1);
|
||||
|
||||
// Drop master on Qt's FD so we can pick it up for SDL.
|
||||
if (drmDropMaster(g_QtDrmMasterFd) < 0) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Failed to drop master on Qt DRM FD: %d",
|
||||
errno);
|
||||
// Hope for the best
|
||||
return fd;
|
||||
}
|
||||
|
||||
// We are not allowed to call drmSetMaster() without CAP_SYS_ADMIN,
|
||||
// but since we just dropped the master, we can become master by
|
||||
// simply creating a new FD. Let's do it.
|
||||
close(fd);
|
||||
fd = ((typeof(open)*)dlsym(RTLD_NEXT, "open"))(pathname, flags);
|
||||
g_SdlDrmMasterFd = fd;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fd;
|
||||
}
|
||||
|
||||
// Our close() hook handles restoring DRM master to the Qt FD
|
||||
// after SDL closes its DRM FD.
|
||||
int close(int fd)
|
||||
{
|
||||
// Call the real thing
|
||||
int ret = ((typeof(close)*)dlsym(RTLD_NEXT, "close"))(fd);
|
||||
if (ret == 0) {
|
||||
// If we just closed the SDL DRM master FD, restore master
|
||||
// to the Qt DRM FD. This works because the Qt DRM master FD
|
||||
// was master once before, so we can set it as master again
|
||||
// using drmSetMaster() without CAP_SYS_ADMIN.
|
||||
if (g_SdlDrmMasterFd != -1 && fd == g_SdlDrmMasterFd) {
|
||||
if (drmSetMaster(g_QtDrmMasterFd) < 0) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Failed to restore master to Qt DRM FD: %d",
|
||||
errno);
|
||||
}
|
||||
|
||||
g_SdlDrmMasterFd = -1;
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
#endif
|
||||
+34
-2
@@ -17,14 +17,46 @@
|
||||
<file>res/baseline-error_outline-24px.svg</file>
|
||||
<file>res/baseline-check_circle_outline-24px.svg</file>
|
||||
<file>res/Discord-Logo-White.svg</file>
|
||||
<file>languages/qml_de.ts</file>
|
||||
<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_nb_NO.ts</file>
|
||||
<file>languages/qml_nb_NO.qm</file>
|
||||
<file>languages/qml_ru.ts</file>
|
||||
<file>languages/qml_ru.qm</file>
|
||||
<file>languages/qml_es.ts</file>
|
||||
<file>languages/qml_es.qm</file>
|
||||
<file>languages/qml_ja.ts</file>
|
||||
<file>languages/qml_ja.qm</file>
|
||||
<file>languages/qml_vi.ts</file>
|
||||
<file>languages/qml_vi.qm</file>
|
||||
<file>languages/qml_th.ts</file>
|
||||
<file>languages/qml_th.qm</file>
|
||||
<file>languages/qml_ko.ts</file>
|
||||
<file>languages/qml_ko.qm</file>
|
||||
<file>languages/qml_hu.ts</file>
|
||||
<file>languages/qml_hu.qm</file>
|
||||
<file>languages/qml_nl.ts</file>
|
||||
<file>languages/qml_nl.qm</file>
|
||||
<file>languages/qml_sv.ts</file>
|
||||
<file>languages/qml_sv.qm</file>
|
||||
<file>languages/qml_tr.ts</file>
|
||||
<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>
|
||||
</qresource>
|
||||
<qresource prefix="/data">
|
||||
<file alias="gamecontrollerdb.txt">SDL_GameControllerDB/gamecontrollerdb.txt</file>
|
||||
<file alias="ModeSeven.ttf">ModeSeven.ttf</file>
|
||||
<file alias="egl.frag">shaders/egl.frag</file>
|
||||
<file alias="egl.vert">shaders/egl.vert</file>
|
||||
<file alias="egl_nv12.frag">shaders/egl_nv12.frag</file>
|
||||
<file alias="egl_nv12.vert">shaders/egl_nv12.vert</file>
|
||||
<file alias="egl_opaque.frag">shaders/egl_opaque.frag</file>
|
||||
<file alias="egl_opaque.vert">shaders/egl_opaque.vert</file>
|
||||
<file alias="egl_overlay.frag">shaders/egl_overlay.frag</file>
|
||||
<file alias="egl_overlay.vert">shaders/egl_overlay.vert</file>
|
||||
</qresource>
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
#include "compatfetcher.h"
|
||||
#include "path.h"
|
||||
|
||||
#include <QNetworkReply>
|
||||
#include <QSettings>
|
||||
|
||||
#define COMPAT_VERSION "v1"
|
||||
#define COMPAT_KEY "latestsupportedversion-"
|
||||
|
||||
CompatFetcher::CompatFetcher(QObject *parent) :
|
||||
QObject(parent),
|
||||
m_Nam(this)
|
||||
{
|
||||
// Never communicate over HTTP
|
||||
m_Nam.setStrictTransportSecurityEnabled(true);
|
||||
|
||||
// Allow HTTP redirects
|
||||
m_Nam.setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy);
|
||||
|
||||
connect(&m_Nam, &QNetworkAccessManager::finished,
|
||||
this, &CompatFetcher::handleCompatInfoFetched);
|
||||
}
|
||||
|
||||
void CompatFetcher::start()
|
||||
{
|
||||
#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);
|
||||
QT_WARNING_POP
|
||||
#endif
|
||||
|
||||
QUrl url("https://moonlight-stream.org/compatibility/" COMPAT_VERSION);
|
||||
QNetworkRequest request(url);
|
||||
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
|
||||
request.setAttribute(QNetworkRequest::Http2AllowedAttribute, true);
|
||||
#else
|
||||
request.setAttribute(QNetworkRequest::HTTP2AllowedAttribute, true);
|
||||
#endif
|
||||
|
||||
// We'll get a callback when this is finished
|
||||
m_Nam.get(request);
|
||||
}
|
||||
|
||||
bool CompatFetcher::isGfeVersionSupported(QString gfeVersion)
|
||||
{
|
||||
QSettings settings;
|
||||
|
||||
if (gfeVersion.trimmed().isEmpty()) {
|
||||
// If we don't have a GFE version, just allow it
|
||||
return true;
|
||||
}
|
||||
|
||||
QString latestSupportedVersion = settings.value(COMPAT_KEY COMPAT_VERSION).toString();
|
||||
if (latestSupportedVersion.isEmpty()) {
|
||||
// We don't have compat data yet, so just assume it's supported
|
||||
return true;
|
||||
}
|
||||
|
||||
QStringList latestSupportedVersionQuad = latestSupportedVersion.split('.');
|
||||
QStringList gfeVersionQuad = gfeVersion.split('.');
|
||||
|
||||
if (gfeVersionQuad.count() <= 1) {
|
||||
qWarning() << "Failed to parse GFE version:" << gfeVersion;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (latestSupportedVersionQuad.count() <= 1) {
|
||||
qWarning() << "Failed to parse latest supported version:" << latestSupportedVersion;
|
||||
return true;
|
||||
}
|
||||
|
||||
for (int i = 0;; i++) {
|
||||
int actualVerVal = 0;
|
||||
int latestSupportedVal = 0;
|
||||
|
||||
// Treat missing decimal places as 0
|
||||
if (i < gfeVersionQuad.count()) {
|
||||
bool ok;
|
||||
|
||||
actualVerVal = gfeVersionQuad[i].toInt(&ok);
|
||||
if (!ok || actualVerVal < 0) {
|
||||
// Return true to be safe
|
||||
qWarning() << "Failed to parse GFE version:" << gfeVersion;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (i < latestSupportedVersionQuad.count()) {
|
||||
bool ok;
|
||||
|
||||
latestSupportedVal = latestSupportedVersionQuad[i].toInt(&ok);
|
||||
if (!ok || latestSupportedVal < 0) {
|
||||
// Return true to be safe
|
||||
qWarning() << "Failed to parse latest supported version:" << latestSupportedVersion;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (i >= gfeVersionQuad.count() && i >= latestSupportedVersionQuad.count()) {
|
||||
// Equal versions - this is fine
|
||||
return true;
|
||||
}
|
||||
|
||||
if (actualVerVal < latestSupportedVal) {
|
||||
// Actual version is lower than latest supported - this is fine
|
||||
return true;
|
||||
}
|
||||
else if (actualVerVal > latestSupportedVal) {
|
||||
// Actual version is greater than latest supported - this is bad
|
||||
qWarning() << "GFE version" << gfeVersion << "is not supported by this version of Moonlight";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CompatFetcher::handleCompatInfoFetched(QNetworkReply* reply)
|
||||
{
|
||||
Q_ASSERT(reply->isFinished());
|
||||
|
||||
if (reply->error() == QNetworkReply::NoError) {
|
||||
// Queue the reply for deletion
|
||||
reply->deleteLater();
|
||||
|
||||
QString version = QString(reply->readAll()).trimmed();
|
||||
|
||||
QSettings settings;
|
||||
settings.setValue(COMPAT_KEY COMPAT_VERSION, version);
|
||||
|
||||
qInfo() << "Latest supported GFE server:" << version;
|
||||
}
|
||||
else {
|
||||
qWarning() << "Failed to download latest compatibility data:" << reply->error();
|
||||
reply->deleteLater();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QNetworkAccessManager>
|
||||
|
||||
class CompatFetcher : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit CompatFetcher(QObject *parent = nullptr);
|
||||
|
||||
void start();
|
||||
|
||||
static bool isGfeVersionSupported(QString gfeVersion);
|
||||
|
||||
private slots:
|
||||
void handleCompatInfoFetched(QNetworkReply* reply);
|
||||
|
||||
private:
|
||||
QNetworkAccessManager m_Nam;
|
||||
};
|
||||
@@ -13,8 +13,8 @@ MappingFetcher::MappingFetcher(QObject *parent) :
|
||||
// Allow HTTP redirects
|
||||
m_Nam.setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy);
|
||||
|
||||
connect(&m_Nam, SIGNAL(finished(QNetworkReply*)),
|
||||
this, SLOT(handleMappingListFetched(QNetworkReply*)));
|
||||
connect(&m_Nam, &QNetworkAccessManager::finished,
|
||||
this, &MappingFetcher::handleMappingListFetched);
|
||||
}
|
||||
|
||||
void MappingFetcher::start()
|
||||
|
||||
@@ -41,7 +41,7 @@ MappingManager::MappingManager()
|
||||
#else
|
||||
.split('\n', QString::SkipEmptyParts);
|
||||
#endif
|
||||
for (QString sdlMapping : sdlMappings) {
|
||||
for (const QString& sdlMapping : sdlMappings) {
|
||||
SdlGamepadMapping mapping(sdlMapping);
|
||||
addMapping(mapping);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
#include "streamingpreferences.h"
|
||||
|
||||
#include <QSettings>
|
||||
#include <QTranslator>
|
||||
#include <QCoreApplication>
|
||||
#include <QLocale>
|
||||
|
||||
#include <QtDebug>
|
||||
|
||||
#define SER_STREAMSETTINGS "streamsettings"
|
||||
#define SER_WIDTH "width"
|
||||
@@ -36,11 +41,20 @@
|
||||
#define SER_REVERSESCROLL "reversescroll"
|
||||
#define SER_SWAPFACEBUTTONS "swapfacebuttons"
|
||||
#define SER_CAPTURESYSKEYS "capturesyskeys"
|
||||
#define SER_LANGUAGE "language"
|
||||
|
||||
#define CURRENT_DEFAULT_VER 1
|
||||
|
||||
StreamingPreferences::StreamingPreferences(QObject *parent)
|
||||
: QObject(parent)
|
||||
: QObject(parent),
|
||||
m_QmlEngine(nullptr)
|
||||
{
|
||||
reload();
|
||||
}
|
||||
|
||||
StreamingPreferences::StreamingPreferences(QQmlEngine *qmlEngine, QObject *parent)
|
||||
: QObject(parent),
|
||||
m_QmlEngine(qmlEngine)
|
||||
{
|
||||
reload();
|
||||
}
|
||||
@@ -81,7 +95,8 @@ void StreamingPreferences::reload()
|
||||
backgroundGamepad = settings.value(SER_BACKGROUNDGAMEPAD, false).toBool();
|
||||
reverseScrollDirection = settings.value(SER_REVERSESCROLL, false).toBool();
|
||||
swapFaceButtons = settings.value(SER_SWAPFACEBUTTONS, false).toBool();
|
||||
captureSysKeys = settings.value(SER_CAPTURESYSKEYS, 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,
|
||||
static_cast<int>(AudioConfig::AC_STEREO)).toInt());
|
||||
videoCodecConfig = static_cast<VideoCodecConfig>(settings.value(SER_VIDEOCFG,
|
||||
@@ -95,6 +110,8 @@ void StreamingPreferences::reload()
|
||||
uiDisplayMode = static_cast<UIDisplayMode>(settings.value(SER_UIDISPLAYMODE,
|
||||
static_cast<int>(settings.value(SER_STARTWINDOWED, true).toBool() ? UIDisplayMode::UI_WINDOWED
|
||||
: UIDisplayMode::UI_MAXIMIZED)).toInt());
|
||||
language = static_cast<Language>(settings.value(SER_LANGUAGE,
|
||||
static_cast<int>(Language::LANG_AUTO)).toInt());
|
||||
|
||||
|
||||
// Perform default settings updates as required based on last default version
|
||||
@@ -108,6 +125,102 @@ void StreamingPreferences::reload()
|
||||
}
|
||||
}
|
||||
|
||||
bool StreamingPreferences::retranslate()
|
||||
{
|
||||
static QTranslator* translator = nullptr;
|
||||
|
||||
#if QT_VERSION < QT_VERSION_CHECK(5, 10, 0)
|
||||
if (m_QmlEngine != nullptr) {
|
||||
// Dynamic retranslation is not supported until Qt 5.10
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
QTranslator* newTranslator = new QTranslator();
|
||||
QString languageSuffix = getSuffixFromLanguage(language);
|
||||
|
||||
// Remove the old translator, even if we can't load a new one.
|
||||
// Otherwise we'll be stuck with the old translated values instead
|
||||
// of defaulting to English.
|
||||
if (translator != nullptr) {
|
||||
QCoreApplication::removeTranslator(translator);
|
||||
delete translator;
|
||||
translator = nullptr;
|
||||
}
|
||||
|
||||
if (newTranslator->load(QString(":/languages/qml_") + languageSuffix)) {
|
||||
qInfo() << "Successfully loaded translation for " << languageSuffix;
|
||||
|
||||
translator = newTranslator;
|
||||
QCoreApplication::installTranslator(translator);
|
||||
}
|
||||
else {
|
||||
qInfo() << "No translation available for " << languageSuffix;
|
||||
delete newTranslator;
|
||||
}
|
||||
|
||||
if (m_QmlEngine != nullptr) {
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
|
||||
// This is a dynamic retranslation from the settings page.
|
||||
// We have to kick the QML engine into reloading our text.
|
||||
m_QmlEngine->retranslate();
|
||||
#else
|
||||
// Unreachable below Qt 5.10 due to the check above
|
||||
Q_ASSERT(false);
|
||||
#endif
|
||||
}
|
||||
else {
|
||||
// This is a translation from a non-QML context, which means
|
||||
// it is probably app startup. There's nothing to refresh.
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
QString StreamingPreferences::getSuffixFromLanguage(StreamingPreferences::Language lang)
|
||||
{
|
||||
switch (lang)
|
||||
{
|
||||
case LANG_DE:
|
||||
return "de";
|
||||
case LANG_EN:
|
||||
return "en";
|
||||
case LANG_FR:
|
||||
return "fr";
|
||||
case LANG_ZH_CN:
|
||||
return "zh_cn";
|
||||
case LANG_NB_NO:
|
||||
return "nb_NO";
|
||||
case LANG_RU:
|
||||
return "ru";
|
||||
case LANG_ES:
|
||||
return "es";
|
||||
case LANG_JA:
|
||||
return "ja";
|
||||
case LANG_VI:
|
||||
return "vi";
|
||||
case LANG_TH:
|
||||
return "th";
|
||||
case LANG_KO:
|
||||
return "ko";
|
||||
case LANG_HU:
|
||||
return "hu";
|
||||
case LANG_NL:
|
||||
return "nl";
|
||||
case LANG_SV:
|
||||
return "sv";
|
||||
case LANG_TR:
|
||||
return "tr";
|
||||
case LANG_UK:
|
||||
return "uk";
|
||||
case LANG_ZH_HANT:
|
||||
return "zh_Hant";
|
||||
case LANG_AUTO:
|
||||
default:
|
||||
return QLocale::system().name();
|
||||
}
|
||||
}
|
||||
|
||||
void StreamingPreferences::save()
|
||||
{
|
||||
QSettings settings;
|
||||
@@ -136,13 +249,14 @@ void StreamingPreferences::save()
|
||||
settings.setValue(SER_VIDEODEC, static_cast<int>(videoDecoderSelection));
|
||||
settings.setValue(SER_WINDOWMODE, static_cast<int>(windowMode));
|
||||
settings.setValue(SER_UIDISPLAYMODE, static_cast<int>(uiDisplayMode));
|
||||
settings.setValue(SER_LANGUAGE, static_cast<int>(language));
|
||||
settings.setValue(SER_DEFAULTVER, CURRENT_DEFAULT_VER);
|
||||
settings.setValue(SER_SWAPMOUSEBUTTONS, swapMouseButtons);
|
||||
settings.setValue(SER_MUTEONFOCUSLOSS, muteOnFocusLoss);
|
||||
settings.setValue(SER_BACKGROUNDGAMEPAD, backgroundGamepad);
|
||||
settings.setValue(SER_REVERSESCROLL, reverseScrollDirection);
|
||||
settings.setValue(SER_SWAPFACEBUTTONS, swapFaceButtons);
|
||||
settings.setValue(SER_CAPTURESYSKEYS, captureSysKeys);
|
||||
settings.setValue(SER_CAPTURESYSKEYS, captureSysKeysMode);
|
||||
}
|
||||
|
||||
int StreamingPreferences::getDefaultBitrate(int width, int height, int fps)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <QObject>
|
||||
#include <QRect>
|
||||
#include <QQmlEngine>
|
||||
|
||||
class StreamingPreferences : public QObject
|
||||
{
|
||||
@@ -9,6 +10,7 @@ class StreamingPreferences : public QObject
|
||||
|
||||
public:
|
||||
StreamingPreferences(QObject *parent = nullptr);
|
||||
StreamingPreferences(QQmlEngine *qmlEngine, QObject *parent = nullptr);
|
||||
|
||||
Q_INVOKABLE static int
|
||||
getDefaultBitrate(int width, int height, int fps);
|
||||
@@ -58,6 +60,40 @@ public:
|
||||
};
|
||||
Q_ENUM(UIDisplayMode)
|
||||
|
||||
// New entries must go at the end of the enum
|
||||
// to avoid renumbering existing entries (which
|
||||
// would affect existing user preferences).
|
||||
enum Language
|
||||
{
|
||||
LANG_AUTO,
|
||||
LANG_EN,
|
||||
LANG_FR,
|
||||
LANG_ZH_CN,
|
||||
LANG_DE,
|
||||
LANG_NB_NO,
|
||||
LANG_RU,
|
||||
LANG_ES,
|
||||
LANG_JA,
|
||||
LANG_VI,
|
||||
LANG_TH,
|
||||
LANG_KO,
|
||||
LANG_HU,
|
||||
LANG_NL,
|
||||
LANG_SV,
|
||||
LANG_TR,
|
||||
LANG_UK,
|
||||
LANG_ZH_HANT
|
||||
};
|
||||
Q_ENUM(Language);
|
||||
|
||||
enum CaptureSysKeysMode
|
||||
{
|
||||
CSK_OFF,
|
||||
CSK_FULLSCREEN,
|
||||
CSK_ALWAYS,
|
||||
};
|
||||
Q_ENUM(CaptureSysKeysMode);
|
||||
|
||||
Q_PROPERTY(int width MEMBER width NOTIFY displayModeChanged)
|
||||
Q_PROPERTY(int height MEMBER height NOTIFY displayModeChanged)
|
||||
Q_PROPERTY(int fps MEMBER fps NOTIFY displayModeChanged)
|
||||
@@ -87,7 +123,10 @@ public:
|
||||
Q_PROPERTY(bool backgroundGamepad MEMBER backgroundGamepad NOTIFY backgroundGamepadChanged)
|
||||
Q_PROPERTY(bool reverseScrollDirection MEMBER reverseScrollDirection NOTIFY reverseScrollDirectionChanged)
|
||||
Q_PROPERTY(bool swapFaceButtons MEMBER swapFaceButtons NOTIFY swapFaceButtonsChanged)
|
||||
Q_PROPERTY(bool captureSysKeys MEMBER captureSysKeys NOTIFY captureSysKeysChanged)
|
||||
Q_PROPERTY(CaptureSysKeysMode captureSysKeysMode MEMBER captureSysKeysMode NOTIFY captureSysKeysModeChanged)
|
||||
Q_PROPERTY(Language language MEMBER language NOTIFY languageChanged);
|
||||
|
||||
Q_INVOKABLE bool retranslate();
|
||||
|
||||
// Directly accessible members for preferences
|
||||
int width;
|
||||
@@ -113,7 +152,6 @@ public:
|
||||
bool backgroundGamepad;
|
||||
bool reverseScrollDirection;
|
||||
bool swapFaceButtons;
|
||||
bool captureSysKeys;
|
||||
int packetSize;
|
||||
AudioConfig audioConfig;
|
||||
VideoCodecConfig videoCodecConfig;
|
||||
@@ -121,6 +159,8 @@ public:
|
||||
WindowMode windowMode;
|
||||
WindowMode recommendedFullScreenMode;
|
||||
UIDisplayMode uiDisplayMode;
|
||||
Language language;
|
||||
CaptureSysKeysMode captureSysKeysMode;
|
||||
|
||||
signals:
|
||||
void displayModeChanged();
|
||||
@@ -149,6 +189,12 @@ signals:
|
||||
void backgroundGamepadChanged();
|
||||
void reverseScrollDirectionChanged();
|
||||
void swapFaceButtonsChanged();
|
||||
void captureSysKeysChanged();
|
||||
void captureSysKeysModeChanged();
|
||||
void languageChanged();
|
||||
|
||||
private:
|
||||
QString getSuffixFromLanguage(Language lang);
|
||||
|
||||
QQmlEngine* m_QmlEngine;
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
#version 300 es
|
||||
#extension GL_OES_EGL_image_external : require
|
||||
precision mediump float;
|
||||
out vec4 FragColor;
|
||||
|
||||
in vec2 vTextCoord;
|
||||
|
||||
uniform samplerExternalOES uTexture;
|
||||
|
||||
void main() {
|
||||
FragColor = texture2D(uTexture, vTextCoord);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
#version 300 es
|
||||
|
||||
layout (location = 0) in vec2 aPosition; // 2D: X,Y
|
||||
layout (location = 1) in vec2 aTexCoord;
|
||||
out vec2 vTextCoord;
|
||||
|
||||
void main() {
|
||||
vTextCoord = aTexCoord;
|
||||
gl_Position = vec4(aPosition, 0, 1);
|
||||
}
|
||||
@@ -55,20 +55,11 @@ IAudioRenderer* Session::createAudioRenderer(const POPUS_MULTISTREAM_CONFIGURATI
|
||||
TRY_INIT_RENDERER(SLAudioRenderer, opusConfig)
|
||||
#endif
|
||||
|
||||
#if !defined(Q_OS_WIN32) && !defined(Q_OS_DARWIN)
|
||||
// Linux defaults to SDL due to persistent glitching issues under libsoundio.
|
||||
// Platforms that libsoundio doesn't support also default to SDL.
|
||||
// Default to SDL and use libsoundio as a fallback
|
||||
TRY_INIT_RENDERER(SdlAudioRenderer, opusConfig)
|
||||
#ifdef HAVE_SOUNDIO
|
||||
TRY_INIT_RENDERER(SoundIoAudioRenderer, opusConfig)
|
||||
#endif
|
||||
#else
|
||||
// Windows and macOS default to libsoundio and fall back to SDL
|
||||
#ifdef HAVE_SOUNDIO
|
||||
TRY_INIT_RENDERER(SoundIoAudioRenderer, opusConfig)
|
||||
#endif
|
||||
TRY_INIT_RENDERER(SdlAudioRenderer, opusConfig)
|
||||
#endif
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
#include "qtaud.h"
|
||||
|
||||
#include <QDebug>
|
||||
|
||||
QtAudioRenderer::QtAudioRenderer()
|
||||
: m_AudioOutput(nullptr),
|
||||
m_OutputDevice(nullptr)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
QtAudioRenderer::~QtAudioRenderer()
|
||||
{
|
||||
delete m_AudioOutput;
|
||||
}
|
||||
|
||||
bool QtAudioRenderer::prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig)
|
||||
{
|
||||
QAudioFormat format;
|
||||
|
||||
// testAudio() assumes this sample rate
|
||||
Q_ASSERT(opusConfig->sampleRate == 48000);
|
||||
|
||||
format.setSampleRate(opusConfig->sampleRate);
|
||||
format.setChannelCount(opusConfig->channelCount);
|
||||
format.setSampleSize(16);
|
||||
format.setSampleType(QAudioFormat::SignedInt);
|
||||
format.setCodec("audio/pcm");
|
||||
|
||||
m_AudioOutput = new QAudioOutput(format);
|
||||
|
||||
// Buffer 40 ms of audio data
|
||||
m_AudioOutput->setBufferSize(format.bytesForDuration(40000));
|
||||
|
||||
m_OutputDevice = m_AudioOutput->start();
|
||||
if (m_OutputDevice == nullptr) {
|
||||
qWarning() << "Audio start failed:" << m_AudioOutput->error();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check what buffer size was really used after starting
|
||||
qInfo() << "Audio stream buffer:" << m_AudioOutput->bufferSize() << "bytes";
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void QtAudioRenderer::submitAudio(short* audioBuffer, int audioSize)
|
||||
{
|
||||
m_OutputDevice->write((const char *)audioBuffer, audioSize);
|
||||
}
|
||||
|
||||
bool QtAudioRenderer::testAudio(int audioConfiguration)
|
||||
{
|
||||
QAudioFormat format;
|
||||
|
||||
format.setSampleRate(48000);
|
||||
format.setSampleSize(16);
|
||||
format.setSampleType(QAudioFormat::SignedInt);
|
||||
format.setCodec("audio/pcm");
|
||||
|
||||
switch (audioConfiguration) {
|
||||
case AUDIO_CONFIGURATION_STEREO:
|
||||
format.setChannelCount(2);
|
||||
break;
|
||||
case AUDIO_CONFIGURATION_51_SURROUND:
|
||||
format.setChannelCount(6);
|
||||
break;
|
||||
default:
|
||||
Q_ASSERT(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (QAudioDeviceInfo::defaultOutputDevice().isFormatSupported(format)) {
|
||||
qInfo() << "Audio test successful with" << format.channelCount() << "channels";
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
qWarning() << "Audio test failed with" << format.channelCount() << "channels";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
int QtAudioRenderer::detectAudioConfiguration()
|
||||
{
|
||||
int preferredChannelCount = QAudioDeviceInfo::defaultOutputDevice().preferredFormat().channelCount();
|
||||
|
||||
qInfo() << "Audio output device prefers" << preferredChannelCount << "channel configuration";
|
||||
|
||||
// We can better downmix 5.1 to quad than we can upmix stereo
|
||||
if (preferredChannelCount > 2) {
|
||||
return AUDIO_CONFIGURATION_51_SURROUND;
|
||||
}
|
||||
else {
|
||||
return AUDIO_CONFIGURATION_STEREO;
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "renderer.h"
|
||||
#include <QAudioOutput>
|
||||
|
||||
class QtAudioRenderer : public IAudioRenderer
|
||||
{
|
||||
public:
|
||||
QtAudioRenderer();
|
||||
|
||||
virtual ~QtAudioRenderer();
|
||||
|
||||
virtual bool prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig);
|
||||
|
||||
virtual void submitAudio(short* audioBuffer, int audioSize);
|
||||
|
||||
virtual bool testAudio(int audioConfiguration);
|
||||
|
||||
virtual int detectAudioConfiguration();
|
||||
|
||||
private:
|
||||
QAudioOutput* m_AudioOutput;
|
||||
QIODevice* m_OutputDevice;
|
||||
};
|
||||
@@ -269,7 +269,22 @@ void SdlInputHandler::handleControllerButtonEvent(SDL_ControllerButtonEvent* eve
|
||||
event.quit.timestamp = SDL_GetTicks();
|
||||
SDL_PushEvent(&event);
|
||||
|
||||
// Clear buttons down on this gameapd
|
||||
// Clear buttons down on this gamepad
|
||||
LiSendMultiControllerEvent(state->index, m_GamepadMask,
|
||||
0, 0, 0, 0, 0, 0, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle Select+L1+R1+X as a gamepad overlay combo
|
||||
if (state->buttons == (BACK_FLAG | LB_FLAG | RB_FLAG | X_FLAG)) {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Detected stats toggle gamepad combo");
|
||||
|
||||
// Toggle the stats overlay
|
||||
Session::get()->getOverlayManager().setOverlayState(Overlay::OverlayDebug,
|
||||
!Session::get()->getOverlayManager().isOverlayEnabled(Overlay::OverlayDebug));
|
||||
|
||||
// Clear buttons down on this gamepad
|
||||
LiSendMultiControllerEvent(state->index, m_GamepadMask,
|
||||
0, 0, 0, 0, 0, 0, 0);
|
||||
return;
|
||||
@@ -342,7 +357,9 @@ void SdlInputHandler::handleControllerDeviceEvent(SDL_ControllerDeviceEvent* eve
|
||||
state->controller = controller;
|
||||
state->jsId = SDL_JoystickInstanceID(SDL_GameControllerGetJoystick(state->controller));
|
||||
|
||||
#if SDL_VERSION_ATLEAST(2, 0, 9)
|
||||
#if SDL_VERSION_ATLEAST(2, 0, 18)
|
||||
hapticCaps = SDL_GameControllerHasRumble(controller) ? ML_HAPTIC_GC_RUMBLE : 0;
|
||||
#elif SDL_VERSION_ATLEAST(2, 0, 9)
|
||||
// Perform a tiny rumble 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.
|
||||
|
||||
+134
-207
@@ -7,13 +7,10 @@
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QDir>
|
||||
#include <QGuiApplication>
|
||||
|
||||
#define MOUSE_POLLING_INTERVAL 5
|
||||
|
||||
#ifdef Q_OS_WIN32
|
||||
HHOOK g_KeyboardHook;
|
||||
#endif
|
||||
|
||||
SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, NvComputer*, int streamWidth, int streamHeight)
|
||||
: m_MultiController(prefs.multiController),
|
||||
m_GamepadMouse(prefs.gamepadMouse),
|
||||
@@ -25,19 +22,24 @@ SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, NvComputer*, int s
|
||||
m_MouseWasInVideoRegion(false),
|
||||
m_PendingMouseButtonsAllUpOnVideoRegionLeave(false),
|
||||
m_FakeCaptureActive(false),
|
||||
m_CaptureSystemKeysEnabled(prefs.captureSysKeys || !WMUtils::isRunningWindowManager()),
|
||||
m_CaptureSystemKeysMode(prefs.captureSysKeysMode),
|
||||
m_MouseCursorCapturedVisibilityState(SDL_DISABLE),
|
||||
m_LongPressTimer(0),
|
||||
m_StreamWidth(streamWidth),
|
||||
m_StreamHeight(streamHeight),
|
||||
m_AbsoluteMouseMode(prefs.absoluteMouseMode),
|
||||
m_AbsoluteTouchMode(prefs.absoluteTouchMode),
|
||||
m_PendingMouseLeaveButtonUp(0),
|
||||
m_LeftButtonReleaseTimer(0),
|
||||
m_RightButtonReleaseTimer(0),
|
||||
m_DragTimer(0),
|
||||
m_DragButton(0),
|
||||
m_NumFingersDown(0)
|
||||
{
|
||||
// System keys are always captured when running without a DE
|
||||
if (!WMUtils::isRunningDesktopEnvironment()) {
|
||||
m_CaptureSystemKeysMode = StreamingPreferences::CSK_ALWAYS;
|
||||
}
|
||||
|
||||
// Allow gamepad input when the app doesn't have focus if requested
|
||||
SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, prefs.backgroundGamepad ? "1" : "0");
|
||||
|
||||
@@ -49,10 +51,15 @@ SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, NvComputer*, int s
|
||||
prefs.absoluteMouseMode ? "1" : "0",
|
||||
SDL_HINT_OVERRIDE);
|
||||
|
||||
// If we're grabbing system keys, enable the X11 keyboard grab in SDL
|
||||
#if !SDL_VERSION_ATLEAST(2, 0, 15)
|
||||
// For older versions of SDL (2.0.14 and earlier), use SDL_HINT_GRAB_KEYBOARD
|
||||
SDL_SetHintWithPriority(SDL_HINT_GRAB_KEYBOARD,
|
||||
m_CaptureSystemKeysEnabled ? "1" : "0",
|
||||
m_CaptureSystemKeysMode != StreamingPreferences::CSK_OFF ? "1" : "0",
|
||||
SDL_HINT_OVERRIDE);
|
||||
#endif
|
||||
|
||||
// Opt-out of SDL's built-in Alt+Tab handling while keyboard grab is enabled
|
||||
SDL_SetHint("SDL_ALLOW_ALT_TAB_WHILE_GRABBED", "0");
|
||||
|
||||
// Allow clicks to pass through to us when focusing the window. If we're in
|
||||
// absolute mouse mode, this will avoid the user having to click twice to
|
||||
@@ -67,6 +74,47 @@ SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, NvComputer*, int s
|
||||
SDL_SetHint("SDL_JOYSTICK_HIDAPI_PS4_RUMBLE", "1");
|
||||
SDL_SetHint("SDL_JOYSTICK_HIDAPI_PS5_RUMBLE", "1");
|
||||
|
||||
// Populate special key combo configuration
|
||||
m_SpecialKeyCombos[KeyComboQuit].keyCombo = KeyComboQuit;
|
||||
m_SpecialKeyCombos[KeyComboQuit].keyCode = SDLK_q;
|
||||
m_SpecialKeyCombos[KeyComboQuit].scanCode = SDL_SCANCODE_Q;
|
||||
m_SpecialKeyCombos[KeyComboQuit].enabled = true;
|
||||
|
||||
m_SpecialKeyCombos[KeyComboUngrabInput].keyCombo = KeyComboUngrabInput;
|
||||
m_SpecialKeyCombos[KeyComboUngrabInput].keyCode = SDLK_z;
|
||||
m_SpecialKeyCombos[KeyComboUngrabInput].scanCode = SDL_SCANCODE_Z;
|
||||
m_SpecialKeyCombos[KeyComboUngrabInput].enabled = QGuiApplication::platformName() != "eglfs";
|
||||
|
||||
m_SpecialKeyCombos[KeyComboToggleFullScreen].keyCombo = KeyComboToggleFullScreen;
|
||||
m_SpecialKeyCombos[KeyComboToggleFullScreen].keyCode = SDLK_x;
|
||||
m_SpecialKeyCombos[KeyComboToggleFullScreen].scanCode = SDL_SCANCODE_X;
|
||||
m_SpecialKeyCombos[KeyComboToggleFullScreen].enabled = QGuiApplication::platformName() != "eglfs";
|
||||
|
||||
m_SpecialKeyCombos[KeyComboToggleStatsOverlay].keyCombo = KeyComboToggleStatsOverlay;
|
||||
m_SpecialKeyCombos[KeyComboToggleStatsOverlay].keyCode = SDLK_s;
|
||||
m_SpecialKeyCombos[KeyComboToggleStatsOverlay].scanCode = SDL_SCANCODE_S;
|
||||
m_SpecialKeyCombos[KeyComboToggleStatsOverlay].enabled = true;
|
||||
|
||||
m_SpecialKeyCombos[KeyComboToggleMouseMode].keyCombo = KeyComboToggleMouseMode;
|
||||
m_SpecialKeyCombos[KeyComboToggleMouseMode].keyCode = SDLK_m;
|
||||
m_SpecialKeyCombos[KeyComboToggleMouseMode].scanCode = SDL_SCANCODE_M;
|
||||
m_SpecialKeyCombos[KeyComboToggleMouseMode].enabled = true;
|
||||
|
||||
m_SpecialKeyCombos[KeyComboToggleCursorHide].keyCombo = KeyComboToggleCursorHide;
|
||||
m_SpecialKeyCombos[KeyComboToggleCursorHide].keyCode = SDLK_c;
|
||||
m_SpecialKeyCombos[KeyComboToggleCursorHide].scanCode = SDL_SCANCODE_C;
|
||||
m_SpecialKeyCombos[KeyComboToggleCursorHide].enabled = true;
|
||||
|
||||
m_SpecialKeyCombos[KeyComboToggleMinimize].keyCombo = KeyComboToggleMinimize;
|
||||
m_SpecialKeyCombos[KeyComboToggleMinimize].keyCode = SDLK_d;
|
||||
m_SpecialKeyCombos[KeyComboToggleMinimize].scanCode = SDL_SCANCODE_D;
|
||||
m_SpecialKeyCombos[KeyComboToggleMinimize].enabled = QGuiApplication::platformName() != "eglfs";
|
||||
|
||||
m_SpecialKeyCombos[KeyComboPasteText].keyCombo = KeyComboPasteText;
|
||||
m_SpecialKeyCombos[KeyComboPasteText].keyCode = SDLK_v;
|
||||
m_SpecialKeyCombos[KeyComboPasteText].scanCode = SDL_SCANCODE_V;
|
||||
m_SpecialKeyCombos[KeyComboPasteText].enabled = true;
|
||||
|
||||
m_OldIgnoreDevices = SDL_GetHint(SDL_HINT_GAMECONTROLLER_IGNORE_DEVICES);
|
||||
m_OldIgnoreDevicesExcept = SDL_GetHint(SDL_HINT_GAMECONTROLLER_IGNORE_DEVICES_EXCEPT);
|
||||
|
||||
@@ -149,38 +197,10 @@ SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, NvComputer*, int s
|
||||
}
|
||||
|
||||
m_MouseMoveTimer = SDL_AddTimer(pollingInterval, SdlInputHandler::mouseMoveTimerCallback, this);
|
||||
|
||||
#ifdef Q_OS_WIN32
|
||||
if (m_CaptureSystemKeysEnabled) {
|
||||
// If system key capture is enabled, install the window hook required to intercept
|
||||
// these key presses and block them from the OS itself.
|
||||
g_KeyboardHook = SetWindowsHookEx(WH_KEYBOARD_LL, SdlInputHandler::keyboardHookProc, GetModuleHandle(NULL), 0);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
SdlInputHandler::~SdlInputHandler()
|
||||
{
|
||||
#ifdef Q_OS_WIN32
|
||||
if (g_KeyboardHook != nullptr) {
|
||||
UnhookWindowsHookEx(g_KeyboardHook);
|
||||
g_KeyboardHook = nullptr;
|
||||
|
||||
// If we're terminating because of the user pressing the quit combo,
|
||||
// we won't have a chance to inform SDL as the modifier keys raise.
|
||||
// This will leave stale modifier flags in the SDL_Keyboard global
|
||||
// inside SDL which will show up on our next key event if the user
|
||||
// streams again. Avoid this by explicitly zeroing mod state when
|
||||
// ending the stream.
|
||||
//
|
||||
// This is only needed for the case where we're hooking the keyboard
|
||||
// because we're generating synthetic SDL_KEYDOWN/SDL_KEYUP events
|
||||
// which don't properly maintain SDL's internal keyboard state,
|
||||
// so we're forced to invoke SDL_SetModState() ourselves to set mods.
|
||||
SDL_SetModState(KMOD_NONE);
|
||||
}
|
||||
#endif
|
||||
|
||||
for (int i = 0; i < MAX_GAMEPADS; i++) {
|
||||
if (m_GamepadState[i].mouseEmulationTimer != 0) {
|
||||
Session::get()->notifyMouseEmulationMode(false);
|
||||
@@ -234,157 +254,6 @@ void SdlInputHandler::setWindow(SDL_Window *window)
|
||||
m_Window = window;
|
||||
}
|
||||
|
||||
#ifdef Q_OS_WIN32
|
||||
LRESULT CALLBACK SdlInputHandler::keyboardHookProc(int nCode, WPARAM wParam, LPARAM lParam)
|
||||
{
|
||||
if (nCode < 0 || nCode != HC_ACTION) {
|
||||
return CallNextHookEx(g_KeyboardHook, nCode, wParam, lParam);
|
||||
}
|
||||
|
||||
bool keyCaptureActive;
|
||||
SDL_Event event = {};
|
||||
KBDLLHOOKSTRUCT* hookData = (KBDLLHOOKSTRUCT*)lParam;
|
||||
switch (hookData->vkCode) {
|
||||
case VK_LWIN:
|
||||
event.key.keysym.sym = SDLK_LGUI;
|
||||
event.key.keysym.scancode = SDL_SCANCODE_LGUI;
|
||||
break;
|
||||
case VK_RWIN:
|
||||
event.key.keysym.sym = SDLK_RGUI;
|
||||
event.key.keysym.scancode = SDL_SCANCODE_RGUI;
|
||||
break;
|
||||
case VK_LMENU:
|
||||
event.key.keysym.sym = SDLK_LALT;
|
||||
event.key.keysym.scancode = SDL_SCANCODE_LALT;
|
||||
break;
|
||||
case VK_RMENU:
|
||||
event.key.keysym.sym = SDLK_RALT;
|
||||
event.key.keysym.scancode = SDL_SCANCODE_RALT;
|
||||
break;
|
||||
case VK_LCONTROL:
|
||||
event.key.keysym.sym = SDLK_LCTRL;
|
||||
event.key.keysym.scancode = SDL_SCANCODE_LCTRL;
|
||||
break;
|
||||
case VK_RCONTROL:
|
||||
event.key.keysym.sym = SDLK_RCTRL;
|
||||
event.key.keysym.scancode = SDL_SCANCODE_RCTRL;
|
||||
break;
|
||||
default:
|
||||
// Bail quickly if it's not a key we care about
|
||||
goto NextHook;
|
||||
}
|
||||
|
||||
// Make sure we're in a state where we actually want to steal this event (and it is safe to do so)
|
||||
Session* session = Session::get();
|
||||
if (session == nullptr || session->m_InputHandler == nullptr) {
|
||||
goto NextHook;
|
||||
}
|
||||
|
||||
keyCaptureActive = session->m_InputHandler->isSystemKeyCaptureActive();
|
||||
|
||||
// If this is a key we're going to intercept, create the synthetic SDL event.
|
||||
// This is necessary because we are also hiding this key event from ourselves too.
|
||||
if (wParam == WM_KEYDOWN || wParam == WM_SYSKEYDOWN) {
|
||||
event.type = SDL_KEYDOWN;
|
||||
event.key.state = SDL_PRESSED;
|
||||
}
|
||||
else {
|
||||
event.type = SDL_KEYUP;
|
||||
event.key.state = SDL_RELEASED;
|
||||
}
|
||||
|
||||
// Drop the event if it's a key down and capture is not active.
|
||||
// For key up, we'll need to do a little more work to determine if we need to send this
|
||||
// event to SDL in order to keep its internal keyboard modifier state consistent.
|
||||
if (event.type == SDL_KEYDOWN && !keyCaptureActive) {
|
||||
goto NextHook;
|
||||
}
|
||||
|
||||
event.key.keysym.mod = SDL_GetModState();
|
||||
switch (hookData->vkCode) {
|
||||
case VK_LWIN:
|
||||
if (event.key.state == SDL_PRESSED) {
|
||||
event.key.keysym.mod |= KMOD_LGUI;
|
||||
}
|
||||
else {
|
||||
event.key.keysym.mod &= ~KMOD_LGUI;
|
||||
}
|
||||
break;
|
||||
case VK_RWIN:
|
||||
if (event.key.state == SDL_PRESSED) {
|
||||
event.key.keysym.mod |= KMOD_RGUI;
|
||||
}
|
||||
else {
|
||||
event.key.keysym.mod &= ~KMOD_RGUI;
|
||||
}
|
||||
break;
|
||||
case VK_LMENU:
|
||||
if (event.key.state == SDL_PRESSED) {
|
||||
event.key.keysym.mod |= KMOD_LALT;
|
||||
}
|
||||
else {
|
||||
event.key.keysym.mod &= ~KMOD_LALT;
|
||||
}
|
||||
break;
|
||||
case VK_RMENU:
|
||||
if (event.key.state == SDL_PRESSED) {
|
||||
event.key.keysym.mod |= KMOD_RALT;
|
||||
}
|
||||
else {
|
||||
event.key.keysym.mod &= ~KMOD_RALT;
|
||||
}
|
||||
break;
|
||||
case VK_LCONTROL:
|
||||
if (event.key.state == SDL_PRESSED) {
|
||||
event.key.keysym.mod |= KMOD_LCTRL;
|
||||
}
|
||||
else {
|
||||
event.key.keysym.mod &= ~KMOD_LCTRL;
|
||||
}
|
||||
break;
|
||||
case VK_RCONTROL:
|
||||
if (event.key.state == SDL_PRESSED) {
|
||||
event.key.keysym.mod |= KMOD_RCTRL;
|
||||
}
|
||||
else {
|
||||
event.key.keysym.mod &= ~KMOD_RCTRL;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// If the modifier state is unchanged and we're not capturing system keys,
|
||||
// drop the event. If the modifier state is changed, we need to send the
|
||||
// event to update SDL's state even if system key capture is now inactive
|
||||
// (due to focus loss, mouse capture toggled off, etc.) otherwise SDL won't
|
||||
// know that the modifier key has been lifted.
|
||||
if (event.key.keysym.mod == SDL_GetModState() && !keyCaptureActive) {
|
||||
goto NextHook;
|
||||
}
|
||||
|
||||
event.key.timestamp = SDL_GetTicks();
|
||||
event.key.windowID = SDL_GetWindowID(session->m_Window);
|
||||
|
||||
// NOTE: event.key.repeat is not populated in this path!
|
||||
SDL_PushEvent(&event);
|
||||
|
||||
// Synchronize SDL's modifier state with the current state.
|
||||
// SDL won't do this on its own because it will never see the
|
||||
// WM_KEYUP/WM_KEYDOWN events.
|
||||
SDL_SetModState((SDL_Keymod)event.key.keysym.mod);
|
||||
|
||||
// Eat the event only if key capture is active.
|
||||
// If capture is not active and we're just resyncing SDL's modifier state,
|
||||
// we need to ensure the key event is still delivered normally to the
|
||||
// window in focus.
|
||||
if (keyCaptureActive) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
NextHook:
|
||||
return CallNextHookEx(g_KeyboardHook, nCode, wParam, lParam);
|
||||
}
|
||||
#endif
|
||||
|
||||
void SdlInputHandler::raiseAllKeys()
|
||||
{
|
||||
if (m_KeysDown.isEmpty()) {
|
||||
@@ -404,20 +273,22 @@ void SdlInputHandler::raiseAllKeys()
|
||||
|
||||
void SdlInputHandler::notifyMouseLeave()
|
||||
{
|
||||
#ifdef Q_OS_WIN32
|
||||
// SDL on Windows doesn't send the mouse button up until the mouse re-enters the window
|
||||
// after leaving it. This breaks some of the Aero snap gestures, so we'll fake it here.
|
||||
// after leaving it. This breaks some of the Aero snap gestures, so we'll capture it to
|
||||
// allow us to receive the mouse button up events later.
|
||||
//
|
||||
// On macOS and X11, capturing the mouse allows us to receive mouse motion outside the
|
||||
// window (button up already worked without capture).
|
||||
if (m_AbsoluteMouseMode && isCaptureActive()) {
|
||||
// NB: Not using SDL_GetGlobalMouseState() because we want our state not the system's
|
||||
Uint32 mouseState = SDL_GetMouseState(nullptr, nullptr);
|
||||
for (Uint32 button = SDL_BUTTON_LEFT; button <= SDL_BUTTON_X2; button++) {
|
||||
if (mouseState & SDL_BUTTON(button)) {
|
||||
m_PendingMouseLeaveButtonUp = button;
|
||||
SDL_CaptureMouse(SDL_TRUE);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void SdlInputHandler::notifyFocusLost()
|
||||
@@ -445,9 +316,49 @@ bool SdlInputHandler::isCaptureActive()
|
||||
return m_FakeCaptureActive;
|
||||
}
|
||||
|
||||
void SdlInputHandler::updateKeyboardGrabState()
|
||||
{
|
||||
if (m_CaptureSystemKeysMode == StreamingPreferences::CSK_OFF) {
|
||||
return;
|
||||
}
|
||||
|
||||
bool shouldGrab = isCaptureActive();
|
||||
Uint32 windowFlags = SDL_GetWindowFlags(m_Window);
|
||||
if (m_CaptureSystemKeysMode == StreamingPreferences::CSK_FULLSCREEN &&
|
||||
!(windowFlags & SDL_WINDOW_FULLSCREEN)) {
|
||||
// Ungrab if it's fullscreen only and we left fullscreen
|
||||
shouldGrab = false;
|
||||
}
|
||||
|
||||
// Don't close the window on Alt+F4 when keyboard grab is enabled
|
||||
SDL_SetHint(SDL_HINT_WINDOWS_NO_CLOSE_ON_ALT_F4, shouldGrab ? "1" : "0");
|
||||
|
||||
if (shouldGrab) {
|
||||
#if SDL_VERSION_ATLEAST(2, 0, 15)
|
||||
// On SDL 2.0.15, we can get keyboard-only grab on Win32, X11, and Wayland.
|
||||
// This does nothing on macOS but it sets the SDL_WINDOW_KEYBOARD_GRABBED flag
|
||||
// that we look for to see if keyboard capture is enabled. We'll handle macOS
|
||||
// ourselves below using the private CGSSetGlobalHotKeyOperatingMode() API.
|
||||
SDL_SetWindowKeyboardGrab(m_Window, SDL_TRUE);
|
||||
#else
|
||||
// If we're in full-screen desktop mode and SDL doesn't have keyboard grab yet,
|
||||
// grab the cursor (will grab the keyboard too on X11).
|
||||
if (SDL_GetWindowFlags(m_Window) & SDL_WINDOW_FULLSCREEN) {
|
||||
SDL_SetWindowGrab(m_Window, SDL_TRUE);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
else {
|
||||
#if SDL_VERSION_ATLEAST(2, 0, 15)
|
||||
// Allow the keyboard to leave the window
|
||||
SDL_SetWindowKeyboardGrab(m_Window, SDL_FALSE);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
bool SdlInputHandler::isSystemKeyCaptureActive()
|
||||
{
|
||||
if (!m_CaptureSystemKeysEnabled) {
|
||||
if (m_CaptureSystemKeysMode == StreamingPreferences::CSK_OFF) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -456,33 +367,41 @@ bool SdlInputHandler::isSystemKeyCaptureActive()
|
||||
}
|
||||
|
||||
Uint32 windowFlags = SDL_GetWindowFlags(m_Window);
|
||||
return (windowFlags & SDL_WINDOW_INPUT_FOCUS) &&
|
||||
(windowFlags & SDL_WINDOW_INPUT_GRABBED) &&
|
||||
(windowFlags & SDL_WINDOW_FULLSCREEN_DESKTOP);
|
||||
if (!(windowFlags & SDL_WINDOW_INPUT_FOCUS)
|
||||
#if SDL_VERSION_ATLEAST(2, 0, 15)
|
||||
|| !(windowFlags & SDL_WINDOW_KEYBOARD_GRABBED)
|
||||
#else
|
||||
|| !(windowFlags & SDL_WINDOW_INPUT_GRABBED)
|
||||
#endif
|
||||
)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_CaptureSystemKeysMode == StreamingPreferences::CSK_FULLSCREEN &&
|
||||
!(windowFlags & SDL_WINDOW_FULLSCREEN)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void SdlInputHandler::setCaptureActive(bool active)
|
||||
{
|
||||
if (active) {
|
||||
// If we're in full-screen exclusive mode, grab the cursor so it can't accidentally leave our window.
|
||||
// If we're in full-screen desktop mode but system key capture is enabled, also grab the cursor (will grab the keyboard too on X11).
|
||||
if (((SDL_GetWindowFlags(m_Window) & SDL_WINDOW_FULLSCREEN_DESKTOP) != 0 && m_CaptureSystemKeysEnabled) ||
|
||||
(SDL_GetWindowFlags(m_Window) & SDL_WINDOW_FULLSCREEN_DESKTOP) == SDL_WINDOW_FULLSCREEN) {
|
||||
if ((SDL_GetWindowFlags(m_Window) & SDL_WINDOW_FULLSCREEN_DESKTOP) == SDL_WINDOW_FULLSCREEN) {
|
||||
#if SDL_VERSION_ATLEAST(2, 0, 15)
|
||||
SDL_SetWindowMouseGrab(m_Window, SDL_TRUE);
|
||||
#else
|
||||
SDL_SetWindowGrab(m_Window, SDL_TRUE);
|
||||
}
|
||||
|
||||
if (!m_AbsoluteMouseMode) {
|
||||
// If our window is occluded when mouse is captured, the mouse may
|
||||
// get stuck on top of the occluding window and not be properly
|
||||
// captured. We can avoid this by raising our window before we
|
||||
// capture the mouse.
|
||||
SDL_RaiseWindow(m_Window);
|
||||
#endif
|
||||
}
|
||||
|
||||
// If we're in relative mode, try to activate SDL's relative mouse mode
|
||||
if (m_AbsoluteMouseMode || SDL_SetRelativeMouseMode(SDL_TRUE) < 0) {
|
||||
// Relative mouse mode didn't work or was disabled, so we'll just hide the cursor
|
||||
SDL_ShowCursor(SDL_DISABLE);
|
||||
SDL_ShowCursor(m_MouseCursorCapturedVisibilityState);
|
||||
m_FakeCaptureActive = true;
|
||||
}
|
||||
|
||||
@@ -515,9 +434,17 @@ void SdlInputHandler::setCaptureActive(bool active)
|
||||
SDL_SetRelativeMouseMode(SDL_FALSE);
|
||||
}
|
||||
|
||||
#if SDL_VERSION_ATLEAST(2, 0, 15)
|
||||
// Allow the cursor to leave the bounds of our window again.
|
||||
SDL_SetWindowMouseGrab(m_Window, SDL_FALSE);
|
||||
#else
|
||||
// Allow the cursor to leave the bounds of our window again.
|
||||
SDL_SetWindowGrab(m_Window, SDL_FALSE);
|
||||
#endif
|
||||
}
|
||||
|
||||
// Now update the keyboard grab
|
||||
updateKeyboardGrabState();
|
||||
}
|
||||
|
||||
void SdlInputHandler::handleTouchFingerEvent(SDL_TouchFingerEvent* event)
|
||||
|
||||
+28
-12
@@ -5,13 +5,9 @@
|
||||
|
||||
#include <SDL.h>
|
||||
|
||||
#ifdef Q_OS_WIN32
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <Windows.h>
|
||||
#endif
|
||||
|
||||
#define SDL_CODE_HIDE_CURSOR 1
|
||||
#define SDL_CODE_SHOW_CURSOR 2
|
||||
#define SDL_CODE_UNCAPTURE_MOUSE 3
|
||||
|
||||
struct GamepadState {
|
||||
SDL_GameController* controller;
|
||||
@@ -69,6 +65,8 @@ public:
|
||||
|
||||
void handleJoystickArrivalEvent(SDL_JoyDeviceEvent* event);
|
||||
|
||||
void sendText(QString& string);
|
||||
|
||||
void rumble(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor);
|
||||
|
||||
void handleTouchFingerEvent(SDL_TouchFingerEvent* event);
|
||||
@@ -93,10 +91,24 @@ public:
|
||||
|
||||
void flushMousePositionUpdate();
|
||||
|
||||
void updateKeyboardGrabState();
|
||||
|
||||
static
|
||||
QString getUnmappedGamepads();
|
||||
|
||||
private:
|
||||
enum KeyCombo {
|
||||
KeyComboQuit,
|
||||
KeyComboUngrabInput,
|
||||
KeyComboToggleFullScreen,
|
||||
KeyComboToggleStatsOverlay,
|
||||
KeyComboToggleMouseMode,
|
||||
KeyComboToggleCursorHide,
|
||||
KeyComboToggleMinimize,
|
||||
KeyComboPasteText,
|
||||
KeyComboMax
|
||||
};
|
||||
|
||||
GamepadState*
|
||||
findStateForGamepad(SDL_JoystickID id);
|
||||
|
||||
@@ -106,6 +118,8 @@ private:
|
||||
|
||||
void handleRelativeFingerEvent(SDL_TouchFingerEvent* event);
|
||||
|
||||
void performSpecialKeyCombo(KeyCombo combo);
|
||||
|
||||
static
|
||||
Uint32 longPressTimerCallback(Uint32 interval, void* param);
|
||||
|
||||
@@ -124,11 +138,6 @@ private:
|
||||
static
|
||||
Uint32 dragTimerCallback(Uint32 interval, void* param);
|
||||
|
||||
#ifdef Q_OS_WIN32
|
||||
static
|
||||
LRESULT CALLBACK keyboardHookProc(int nCode, WPARAM wParam, LPARAM lParam);
|
||||
#endif
|
||||
|
||||
SDL_Window* m_Window;
|
||||
bool m_MultiController;
|
||||
bool m_GamepadMouse;
|
||||
@@ -154,7 +163,15 @@ private:
|
||||
bool m_FakeCaptureActive;
|
||||
QString m_OldIgnoreDevices;
|
||||
QString m_OldIgnoreDevicesExcept;
|
||||
bool m_CaptureSystemKeysEnabled;
|
||||
StreamingPreferences::CaptureSysKeysMode m_CaptureSystemKeysMode;
|
||||
int m_MouseCursorCapturedVisibilityState;
|
||||
|
||||
struct {
|
||||
KeyCombo keyCombo;
|
||||
SDL_Keycode keyCode;
|
||||
SDL_Scancode scanCode;
|
||||
bool enabled;
|
||||
} m_SpecialKeyCombos[KeyComboMax];
|
||||
|
||||
SDL_TouchFingerEvent m_LastTouchDownEvent;
|
||||
SDL_TouchFingerEvent m_LastTouchUpEvent;
|
||||
@@ -163,7 +180,6 @@ private:
|
||||
int m_StreamHeight;
|
||||
bool m_AbsoluteMouseMode;
|
||||
bool m_AbsoluteTouchMode;
|
||||
Uint32 m_PendingMouseLeaveButtonUp;
|
||||
|
||||
SDL_TouchFingerEvent m_TouchDownEvent[MAX_FINGERS];
|
||||
SDL_TimerID m_LeftButtonReleaseTimer;
|
||||
|
||||
+134
-161
@@ -3,8 +3,6 @@
|
||||
#include <Limelight.h>
|
||||
#include <SDL.h>
|
||||
|
||||
#include <QGuiApplication>
|
||||
|
||||
#define VK_0 0x30
|
||||
#define VK_A 0x41
|
||||
|
||||
@@ -15,11 +13,135 @@
|
||||
#define VK_NUMPAD0 0x60
|
||||
#endif
|
||||
|
||||
void SdlInputHandler::performSpecialKeyCombo(KeyCombo combo)
|
||||
{
|
||||
switch (combo) {
|
||||
case KeyComboQuit:
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Detected quit key combo");
|
||||
|
||||
// Push a quit event to the main loop
|
||||
SDL_Event event;
|
||||
event.type = SDL_QUIT;
|
||||
event.quit.timestamp = SDL_GetTicks();
|
||||
SDL_PushEvent(&event);
|
||||
break;
|
||||
|
||||
case KeyComboUngrabInput:
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Detected mouse capture toggle combo");
|
||||
|
||||
// Stop handling future input
|
||||
setCaptureActive(!isCaptureActive());
|
||||
|
||||
// Force raise all keys to ensure they aren't stuck,
|
||||
// since we won't get their key up events.
|
||||
raiseAllKeys();
|
||||
break;
|
||||
|
||||
case KeyComboToggleFullScreen:
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Detected full-screen toggle combo");
|
||||
Session::s_ActiveSession->toggleFullscreen();
|
||||
|
||||
// Force raise all keys just be safe across this full-screen/windowed
|
||||
// transition just in case key events get lost.
|
||||
raiseAllKeys();
|
||||
break;
|
||||
|
||||
case KeyComboToggleStatsOverlay:
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Detected stats toggle combo");
|
||||
|
||||
// Toggle the stats overlay
|
||||
Session::get()->getOverlayManager().setOverlayState(Overlay::OverlayDebug,
|
||||
!Session::get()->getOverlayManager().isOverlayEnabled(Overlay::OverlayDebug));
|
||||
break;
|
||||
|
||||
case KeyComboToggleMouseMode:
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Detected mouse mode toggle combo");
|
||||
|
||||
// Uncapture input
|
||||
setCaptureActive(false);
|
||||
|
||||
// Toggle mouse mode
|
||||
m_AbsoluteMouseMode = !m_AbsoluteMouseMode;
|
||||
|
||||
// Recapture input
|
||||
setCaptureActive(true);
|
||||
break;
|
||||
|
||||
case KeyComboToggleCursorHide:
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Detected show mouse combo");
|
||||
|
||||
if (!SDL_GetRelativeMouseMode()) {
|
||||
m_MouseCursorCapturedVisibilityState = !m_MouseCursorCapturedVisibilityState;
|
||||
SDL_ShowCursor(m_MouseCursorCapturedVisibilityState);
|
||||
}
|
||||
else {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Cursor can only be shown in remote desktop mouse mode");
|
||||
}
|
||||
break;
|
||||
|
||||
case KeyComboToggleMinimize:
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Detected minimize combo");
|
||||
SDL_MinimizeWindow(m_Window);
|
||||
break;
|
||||
|
||||
case KeyComboPasteText:
|
||||
{
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Detected type clipboard text combo");
|
||||
|
||||
// Force raise all keys to ensure that none of them interfere
|
||||
// with the text we're going to type.
|
||||
raiseAllKeys();
|
||||
|
||||
char* text;
|
||||
if (SDL_HasClipboardText() && (text = SDL_GetClipboardText()) != nullptr) {
|
||||
// Sending both CR and LF will lead to two newlines in the destination for
|
||||
// each newline in the source, so we fix up any CRLFs into just a single LF.
|
||||
for (char* c = text; *c != 0; c++) {
|
||||
if (*c == '\r' && *(c + 1) == '\n') {
|
||||
// We're using strlen() rather than strlen() - 1 since we need to add 1
|
||||
// to copy the null terminator which is not included in strlen()'s count.
|
||||
memmove(c, c + 1, strlen(c));
|
||||
}
|
||||
}
|
||||
|
||||
// Send this text to the PC
|
||||
LiSendUtf8TextEvent(text, strlen(text));
|
||||
|
||||
// SDL_GetClipboardText() allocates, so we must free
|
||||
SDL_free((void*)text);
|
||||
}
|
||||
else {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"No text in clipboard to paste!");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
Q_UNREACHABLE();
|
||||
}
|
||||
}
|
||||
|
||||
void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
|
||||
{
|
||||
short keyCode;
|
||||
char modifiers;
|
||||
|
||||
if (event->repeat) {
|
||||
// Ignore repeat key down events
|
||||
SDL_assert(event->state == SDL_PRESSED);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for our special key combos
|
||||
if ((event->state == SDL_PRESSED) &&
|
||||
(event->keysym.mod & KMOD_CTRL) &&
|
||||
@@ -36,168 +158,19 @@ void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
|
||||
// where the SDLK for one shortcut collides with
|
||||
// the scancode of another.
|
||||
|
||||
// Check for quit combo (Ctrl+Alt+Shift+Q)
|
||||
if (event->keysym.sym == SDLK_q) {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Detected quit key combo (SDLK)");
|
||||
|
||||
// Push a quit event to the main loop
|
||||
SDL_Event event;
|
||||
event.type = SDL_QUIT;
|
||||
event.quit.timestamp = SDL_GetTicks();
|
||||
SDL_PushEvent(&event);
|
||||
return;
|
||||
}
|
||||
// Check for the unbind combo (Ctrl+Alt+Shift+Z) unless on EGLFS which has no window manager
|
||||
else if (event->keysym.sym == SDLK_z && QGuiApplication::platformName() != "eglfs") {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Detected mouse capture toggle combo (SDLK)");
|
||||
|
||||
// Stop handling future input
|
||||
setCaptureActive(!isCaptureActive());
|
||||
|
||||
// Force raise all keys to ensure they aren't stuck,
|
||||
// since we won't get their key up events.
|
||||
raiseAllKeys();
|
||||
return;
|
||||
}
|
||||
// Check for the mouse mode combo (Ctrl+Alt+Shift+M) unless on EGLFS which has no window manager
|
||||
else if (event->keysym.sym == SDLK_m && QGuiApplication::platformName() != "eglfs") {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Detected mouse mode toggle combo (SDLK)");
|
||||
|
||||
// Uncapture input
|
||||
setCaptureActive(false);
|
||||
|
||||
// Toggle mouse mode
|
||||
m_AbsoluteMouseMode = !m_AbsoluteMouseMode;
|
||||
|
||||
// Recapture input
|
||||
setCaptureActive(true);
|
||||
return;
|
||||
}
|
||||
else if (event->keysym.sym == SDLK_x && QGuiApplication::platformName() != "eglfs") {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Detected full-screen toggle combo (SDLK)");
|
||||
Session::s_ActiveSession->toggleFullscreen();
|
||||
|
||||
// Force raise all keys just be safe across this full-screen/windowed
|
||||
// transition just in case key events get lost.
|
||||
raiseAllKeys();
|
||||
return;
|
||||
}
|
||||
// Check for overlay combo (Ctrl+Alt+Shift+S)
|
||||
else if (event->keysym.sym == SDLK_s) {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Detected stats toggle combo (SDLK)");
|
||||
|
||||
// Toggle the stats overlay
|
||||
Session::get()->getOverlayManager().setOverlayState(Overlay::OverlayDebug,
|
||||
!Session::get()->getOverlayManager().isOverlayEnabled(Overlay::OverlayDebug));
|
||||
|
||||
// Force raise all keys just be safe across this full-screen/windowed
|
||||
// transition just in case key events get lost.
|
||||
raiseAllKeys();
|
||||
return;
|
||||
}
|
||||
// Check for mouse show combo (Ctrl+Alt+Shift+C)
|
||||
else if (event->keysym.sym == SDLK_c) {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Detected show mouse combo (SDLK)");
|
||||
|
||||
if (!SDL_GetRelativeMouseMode()) {
|
||||
SDL_ShowCursor(!SDL_ShowCursor(SDL_QUERY));
|
||||
for (int i = 0; i < KeyComboMax; i++) {
|
||||
if (m_SpecialKeyCombos[i].enabled && event->keysym.sym == m_SpecialKeyCombos[i].keyCode) {
|
||||
performSpecialKeyCombo(m_SpecialKeyCombos[i].keyCombo);
|
||||
return;
|
||||
}
|
||||
else {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Cursor can only be shown in remote desktop mouse mode");
|
||||
}
|
||||
|
||||
for (int i = 0; i < KeyComboMax; i++) {
|
||||
if (m_SpecialKeyCombos[i].enabled && event->keysym.scancode == m_SpecialKeyCombos[i].scanCode) {
|
||||
performSpecialKeyCombo(m_SpecialKeyCombos[i].keyCombo);
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Check for quit combo (Ctrl+Alt+Shift+Q)
|
||||
else if (event->keysym.scancode == SDL_SCANCODE_Q) {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Detected quit key combo (scancode)");
|
||||
|
||||
// Push a quit event to the main loop
|
||||
SDL_Event event;
|
||||
event.type = SDL_QUIT;
|
||||
event.quit.timestamp = SDL_GetTicks();
|
||||
SDL_PushEvent(&event);
|
||||
return;
|
||||
}
|
||||
// Check for the unbind combo (Ctrl+Alt+Shift+Z)
|
||||
else if (event->keysym.scancode == SDL_SCANCODE_Z && QGuiApplication::platformName() != "eglfs") {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Detected mouse capture toggle combo (scancode)");
|
||||
|
||||
// Stop handling future input
|
||||
setCaptureActive(!isCaptureActive());
|
||||
|
||||
// Force raise all keys to ensure they aren't stuck,
|
||||
// since we won't get their key up events.
|
||||
raiseAllKeys();
|
||||
return;
|
||||
}
|
||||
// Check for the full-screen combo (Ctrl+Alt+Shift+X) unless on EGLFS which has no window manager
|
||||
else if (event->keysym.scancode == SDL_SCANCODE_X && QGuiApplication::platformName() != "eglfs") {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Detected full-screen toggle combo (scancode)");
|
||||
Session::s_ActiveSession->toggleFullscreen();
|
||||
|
||||
// Force raise all keys just be safe across this full-screen/windowed
|
||||
// transition just in case key events get lost.
|
||||
raiseAllKeys();
|
||||
return;
|
||||
}
|
||||
// Check for the mouse mode toggle combo (Ctrl+Alt+Shift+M) unless on EGLFS which has no window manager
|
||||
else if (event->keysym.scancode == SDL_SCANCODE_M && QGuiApplication::platformName() != "eglfs") {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Detected mouse mode toggle combo (scancode)");
|
||||
|
||||
// Uncapture input
|
||||
setCaptureActive(false);
|
||||
|
||||
// Toggle mouse mode
|
||||
m_AbsoluteMouseMode = !m_AbsoluteMouseMode;
|
||||
|
||||
// Recapture input
|
||||
setCaptureActive(true);
|
||||
return;
|
||||
}
|
||||
else if (event->keysym.scancode == SDL_SCANCODE_S) {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Detected stats toggle combo (scancode)");
|
||||
|
||||
// Toggle the stats overlay
|
||||
Session::get()->getOverlayManager().setOverlayState(Overlay::OverlayDebug,
|
||||
!Session::get()->getOverlayManager().isOverlayEnabled(Overlay::OverlayDebug));
|
||||
|
||||
// Force raise all keys just be safe across this full-screen/windowed
|
||||
// transition just in case key events get lost.
|
||||
raiseAllKeys();
|
||||
return;
|
||||
}
|
||||
// Check for mouse show combo (Ctrl+Alt+Shift+C)
|
||||
else if (event->keysym.sym == SDL_SCANCODE_C) {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Detected show mouse combo (scancode)");
|
||||
|
||||
if (!SDL_GetRelativeMouseMode()) {
|
||||
SDL_ShowCursor(!SDL_ShowCursor(SDL_QUERY));
|
||||
}
|
||||
else {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Cursor can only be shown in remote desktop mouse mode");
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (event->repeat) {
|
||||
// Ignore repeat key down events
|
||||
SDL_assert(event->state == SDL_PRESSED);
|
||||
return;
|
||||
}
|
||||
|
||||
// Set modifier flags
|
||||
|
||||
@@ -133,7 +133,15 @@ void SdlInputHandler::flushMousePositionUpdate()
|
||||
// c) a mouse button is still down from before the cursor left the video region (to allow smooth dragging)
|
||||
Uint32 buttonState = SDL_GetMouseState(nullptr, nullptr);
|
||||
if (buttonState == 0) {
|
||||
m_PendingMouseButtonsAllUpOnVideoRegionLeave = false;
|
||||
if (m_PendingMouseButtonsAllUpOnVideoRegionLeave) {
|
||||
// Tell the main thread to stop capturing the mouse now
|
||||
SDL_Event event;
|
||||
event.type = SDL_USEREVENT;
|
||||
event.user.code = SDL_CODE_UNCAPTURE_MOUSE;
|
||||
SDL_PushEvent(&event);
|
||||
|
||||
m_PendingMouseButtonsAllUpOnVideoRegionLeave = false;
|
||||
}
|
||||
}
|
||||
if (mouseInVideoRegion || m_MouseWasInVideoRegion || m_PendingMouseButtonsAllUpOnVideoRegionLeave) {
|
||||
LiSendMousePositionEvent(x, y, dst.w, dst.h);
|
||||
@@ -146,7 +154,8 @@ void SdlInputHandler::flushMousePositionUpdate()
|
||||
// executes.
|
||||
SDL_Event event;
|
||||
event.type = SDL_USEREVENT;
|
||||
event.user.code = mouseInVideoRegion ? SDL_CODE_HIDE_CURSOR : SDL_CODE_SHOW_CURSOR;
|
||||
event.user.code = (mouseInVideoRegion && m_MouseCursorCapturedVisibilityState == SDL_DISABLE) ?
|
||||
SDL_CODE_HIDE_CURSOR : SDL_CODE_SHOW_CURSOR;
|
||||
SDL_PushEvent(&event);
|
||||
|
||||
if (!mouseInVideoRegion && buttonState != 0) {
|
||||
@@ -203,6 +212,16 @@ void SdlInputHandler::handleMouseWheelEvent(SDL_MouseWheelEvent* event)
|
||||
}
|
||||
}
|
||||
|
||||
#if SDL_VERSION_ATLEAST(2, 0, 18)
|
||||
if (event->preciseY != 0.0f) {
|
||||
// Invert the scroll direction if needed
|
||||
if (m_ReverseScrollDirection) {
|
||||
event->preciseY = -event->preciseY;
|
||||
}
|
||||
|
||||
LiSendHighResScrollEvent((short)(event->preciseY * 120)); // WHEEL_DELTA
|
||||
}
|
||||
#else
|
||||
if (event->y != 0) {
|
||||
// Invert the scroll direction if needed
|
||||
if (m_ReverseScrollDirection) {
|
||||
@@ -211,6 +230,7 @@ void SdlInputHandler::handleMouseWheelEvent(SDL_MouseWheelEvent* event)
|
||||
|
||||
LiSendScrollEvent((signed char)event->y);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
bool SdlInputHandler::isMouseInVideoRegion(int mouseX, int mouseY, int windowWidth, int windowHeight)
|
||||
@@ -250,33 +270,5 @@ Uint32 SdlInputHandler::mouseMoveTimerCallback(Uint32 interval, void *param)
|
||||
// Send mouse position updates if applicable
|
||||
me->flushMousePositionUpdate();
|
||||
|
||||
#ifdef Q_OS_WIN32
|
||||
// See comment in SdlInputHandler::notifyMouseLeave()
|
||||
if (me->m_AbsoluteMouseMode && me->m_PendingMouseLeaveButtonUp != 0 && me->isCaptureActive()) {
|
||||
int mouseX, mouseY;
|
||||
int windowX, windowY;
|
||||
Uint32 mouseState = SDL_GetGlobalMouseState(&mouseX, &mouseY);
|
||||
SDL_GetWindowPosition(me->m_Window, &windowX, &windowY);
|
||||
|
||||
// If the button is now up, send the synthetic mouse up event
|
||||
if ((mouseState & SDL_BUTTON(me->m_PendingMouseLeaveButtonUp)) == 0) {
|
||||
SDL_Event event;
|
||||
|
||||
event.button.type = SDL_MOUSEBUTTONUP;
|
||||
event.button.timestamp = SDL_GetTicks();
|
||||
event.button.windowID = SDL_GetWindowID(me->m_Window);
|
||||
event.button.which = 0;
|
||||
event.button.button = me->m_PendingMouseLeaveButtonUp;
|
||||
event.button.state = SDL_RELEASED;
|
||||
event.button.clicks = 1;
|
||||
event.button.x = mouseX - windowX;
|
||||
event.button.y = mouseY - windowY;
|
||||
SDL_PushEvent(&event);
|
||||
|
||||
me->m_PendingMouseLeaveButtonUp = 0;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
return interval;
|
||||
}
|
||||
|
||||
+273
-96
@@ -37,14 +37,6 @@
|
||||
|
||||
#define CONN_TEST_SERVER "qt.conntest.moonlight-stream.org"
|
||||
|
||||
// Running the connection process asynchronously seems to reliably
|
||||
// cause a crash in QSGRenderThread on Wayland and strange crashes
|
||||
// elsewhere. Until these are figured out, avoid the async connect
|
||||
// thread on Linux and BSDs.
|
||||
#if !defined(Q_OS_UNIX) || defined(Q_OS_DARWIN)
|
||||
#define USE_ASYNC_CONNECT_THREAD 1
|
||||
#endif
|
||||
|
||||
CONNECTION_LISTENER_CALLBACKS Session::k_ConnCallbacks = {
|
||||
Session::clStageStarting,
|
||||
nullptr,
|
||||
@@ -65,11 +57,6 @@ void Session::clStageStarting(int stage)
|
||||
// which happens to be the main thread, so it's cool to interact
|
||||
// with the GUI in these callbacks.
|
||||
emit s_ActiveSession->stageStarting(QString::fromLocal8Bit(LiGetStageName(stage)));
|
||||
|
||||
#ifndef USE_ASYNC_CONNECT_THREAD
|
||||
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
|
||||
QCoreApplication::sendPostedEvents();
|
||||
#endif
|
||||
}
|
||||
|
||||
void Session::clStageFailed(int stage, int errorCode)
|
||||
@@ -81,11 +68,6 @@ void Session::clStageFailed(int stage, int errorCode)
|
||||
char failingPorts[128];
|
||||
LiStringifyPortFlags(portFlags, ", ", failingPorts, sizeof(failingPorts));
|
||||
emit s_ActiveSession->stageFailed(QString::fromLocal8Bit(LiGetStageName(stage)), errorCode, QString(failingPorts));
|
||||
|
||||
#ifndef USE_ASYNC_CONNECT_THREAD
|
||||
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
|
||||
QCoreApplication::sendPostedEvents();
|
||||
#endif
|
||||
}
|
||||
|
||||
void Session::clConnectionTerminated(int errorCode)
|
||||
@@ -199,6 +181,11 @@ bool Session::chooseDecoder(StreamingPreferences::VideoDecoderSelection vds,
|
||||
{
|
||||
DECODER_PARAMETERS params;
|
||||
|
||||
// We should never have vsync enabled for test-mode.
|
||||
// It introduces unnecessary delay for renderers that may
|
||||
// block while waiting for a backbuffer swap.
|
||||
SDL_assert(!enableVsync || !testOnly);
|
||||
|
||||
params.width = width;
|
||||
params.height = height;
|
||||
params.frameRate = frameRate;
|
||||
@@ -277,14 +264,12 @@ int Session::drSubmitDecodeUnit(PDECODE_UNIT du)
|
||||
// safely return DR_OK and wait for m_NeedsIdr to be set by
|
||||
// the decoder reinitialization code.
|
||||
|
||||
if (SDL_AtomicTryLock(&s_ActiveSession->m_DecoderLock)) {
|
||||
if (s_ActiveSession->m_NeedsIdr) {
|
||||
// If we reset our decoder, we'll need to request an IDR frame
|
||||
s_ActiveSession->m_NeedsIdr = false;
|
||||
SDL_AtomicUnlock(&s_ActiveSession->m_DecoderLock);
|
||||
return DR_NEED_IDR;
|
||||
}
|
||||
if (s_ActiveSession->getAndClearPendingIdrFrameStatus()) {
|
||||
// If we reset our decoder, we'll need to request an IDR frame
|
||||
return DR_NEED_IDR;
|
||||
}
|
||||
|
||||
if (SDL_AtomicTryLock(&s_ActiveSession->m_DecoderLock)) {
|
||||
IVideoDecoder* decoder = s_ActiveSession->m_VideoDecoder;
|
||||
if (decoder != nullptr) {
|
||||
int ret = decoder->submitDecodeUnit(du);
|
||||
@@ -310,7 +295,7 @@ void Session::getDecoderInfo(SDL_Window* window,
|
||||
|
||||
if (!chooseDecoder(StreamingPreferences::VDS_AUTO,
|
||||
window, VIDEO_FORMAT_H264, 1920, 1080, 60,
|
||||
true, false, true, decoder)) {
|
||||
false, false, true, decoder)) {
|
||||
isHardwareAccelerated = isFullScreenOnly = false;
|
||||
return;
|
||||
}
|
||||
@@ -320,6 +305,23 @@ void Session::getDecoderInfo(SDL_Window* window,
|
||||
maxResolution = decoder->getDecoderMaxResolution();
|
||||
|
||||
delete decoder;
|
||||
|
||||
// If we don't get back a hardware H.264 decoder, see if we have a hardware
|
||||
// HEVC decoder. This can be the case on the Raspberry Pi with Full KMS
|
||||
// when not running in X11. Everything since Maxwell in 2014 can encode HEVC,
|
||||
// so we probably don't need to be concerned about a lack of fast H.264
|
||||
// decoding enough to bug the user about it every launch.
|
||||
if (!isHardwareAccelerated) {
|
||||
if (chooseDecoder(StreamingPreferences::VDS_FORCE_HARDWARE,
|
||||
window, VIDEO_FORMAT_H265, 1920, 1080, 60,
|
||||
false, false, true, decoder)) {
|
||||
isHardwareAccelerated = decoder->isHardwareAccelerated();
|
||||
isFullScreenOnly = decoder->isAlwaysFullScreen();
|
||||
maxResolution = decoder->getDecoderMaxResolution();
|
||||
|
||||
delete decoder;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool Session::isHardwareDecodeAvailable(SDL_Window* window,
|
||||
@@ -328,7 +330,7 @@ bool Session::isHardwareDecodeAvailable(SDL_Window* window,
|
||||
{
|
||||
IVideoDecoder* decoder;
|
||||
|
||||
if (!chooseDecoder(vds, window, videoFormat, width, height, frameRate, true, false, true, decoder)) {
|
||||
if (!chooseDecoder(vds, window, videoFormat, width, height, frameRate, false, false, true, decoder)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -350,14 +352,25 @@ bool Session::populateDecoderProperties(SDL_Window* window)
|
||||
m_StreamConfig.width,
|
||||
m_StreamConfig.height,
|
||||
m_StreamConfig.fps,
|
||||
true, false, true, decoder)) {
|
||||
false, false, true, decoder)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
m_VideoCallbacks.capabilities = decoder->getDecoderCapabilities();
|
||||
if (m_VideoCallbacks.capabilities & CAPABILITY_PULL_RENDERER) {
|
||||
// It is an error to pass a push callback when in pull mode
|
||||
m_VideoCallbacks.submitDecodeUnit = nullptr;
|
||||
}
|
||||
else {
|
||||
m_VideoCallbacks.submitDecodeUnit = drSubmitDecodeUnit;
|
||||
}
|
||||
|
||||
m_StreamConfig.colorSpace = decoder->getDecoderColorspace();
|
||||
|
||||
if (decoder->isAlwaysFullScreen()) {
|
||||
m_IsFullScreen = true;
|
||||
}
|
||||
|
||||
delete decoder;
|
||||
|
||||
return true;
|
||||
@@ -365,12 +378,12 @@ bool Session::populateDecoderProperties(SDL_Window* window)
|
||||
|
||||
Session::Session(NvComputer* computer, NvApp& app, StreamingPreferences *preferences)
|
||||
: m_Preferences(preferences ? preferences : new StreamingPreferences(this)),
|
||||
m_IsFullScreen(m_Preferences->windowMode != StreamingPreferences::WM_WINDOWED || !WMUtils::isRunningDesktopEnvironment()),
|
||||
m_Computer(computer),
|
||||
m_App(app),
|
||||
m_Window(nullptr),
|
||||
m_VideoDecoder(nullptr),
|
||||
m_DecoderLock(0),
|
||||
m_NeedsIdr(false),
|
||||
m_AudioDisabled(false),
|
||||
m_AudioMuted(false),
|
||||
m_DisplayOriginX(0),
|
||||
@@ -388,6 +401,7 @@ Session::Session(NvComputer* computer, NvApp& app, StreamingPreferences *prefere
|
||||
m_AudioSampleCount(0),
|
||||
m_DropAudioEndTime(0)
|
||||
{
|
||||
SDL_AtomicSet(&m_NeedsIdr, 0);
|
||||
}
|
||||
|
||||
// NB: This may not get destroyed for a long time! Don't put any vital cleanup here.
|
||||
@@ -432,7 +446,6 @@ bool Session::initialize()
|
||||
|
||||
LiInitializeVideoCallbacks(&m_VideoCallbacks);
|
||||
m_VideoCallbacks.setup = drSetup;
|
||||
m_VideoCallbacks.submitDecodeUnit = drSubmitDecodeUnit;
|
||||
|
||||
LiInitializeStreamConfiguration(&m_StreamConfig);
|
||||
m_StreamConfig.width = m_Preferences->width;
|
||||
@@ -441,6 +454,12 @@ bool Session::initialize()
|
||||
m_StreamConfig.bitrate = m_Preferences->bitrateKbps;
|
||||
m_StreamConfig.hevcBitratePercentageMultiplier = 75;
|
||||
|
||||
#ifndef STEAM_LINK
|
||||
// Enable audio encryption as long as we're not on Steam Link.
|
||||
// That hardware can hardly handle Opus decoding at all.
|
||||
m_StreamConfig.encryptionFlags = ENCFLG_AUDIO;
|
||||
#endif
|
||||
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Video bitrate: %d kbps",
|
||||
m_StreamConfig.bitrate);
|
||||
@@ -524,22 +543,17 @@ bool Session::initialize()
|
||||
{
|
||||
default:
|
||||
case StreamingPreferences::WM_FULLSCREEN_DESKTOP:
|
||||
m_FullScreenFlag = SDL_WINDOW_FULLSCREEN_DESKTOP;
|
||||
break;
|
||||
// Only use full-screen desktop mode if we're running a desktop environment
|
||||
if (WMUtils::isRunningDesktopEnvironment()) {
|
||||
m_FullScreenFlag = SDL_WINDOW_FULLSCREEN_DESKTOP;
|
||||
break;
|
||||
}
|
||||
// Fall-through
|
||||
case StreamingPreferences::WM_FULLSCREEN:
|
||||
m_FullScreenFlag = SDL_WINDOW_FULLSCREEN;
|
||||
break;
|
||||
}
|
||||
|
||||
#ifdef Q_OS_DARWIN
|
||||
// macOS behaves as if we're capturing system keys when
|
||||
// we enter "real" full-screen mode, so use standard
|
||||
// full-screen as our full-screen flag when capturing keys.
|
||||
if (m_Preferences->captureSysKeys) {
|
||||
m_FullScreenFlag = SDL_WINDOW_FULLSCREEN;
|
||||
}
|
||||
#endif
|
||||
|
||||
#if !SDL_VERSION_ATLEAST(2, 0, 11)
|
||||
// HACK: Using a full-screen window breaks mouse capture on the Pi's LXDE
|
||||
// GUI environment. Force the session to use windowed mode (which won't
|
||||
@@ -581,10 +595,13 @@ void Session::emitLaunchWarning(QString text)
|
||||
// to allow it to transition off the screen before continuing.
|
||||
uint32_t start = SDL_GetTicks();
|
||||
while (!SDL_TICKS_PASSED(SDL_GetTicks(), start + 3500)) {
|
||||
// Pump the UI loop while we wait
|
||||
SDL_Delay(5);
|
||||
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
|
||||
QCoreApplication::sendPostedEvents();
|
||||
|
||||
if (!m_ThreadedExec) {
|
||||
// Pump the UI loop while we wait if we're on the main thread
|
||||
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
|
||||
QCoreApplication::sendPostedEvents();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -592,6 +609,11 @@ bool Session::validateLaunch(SDL_Window* testWindow)
|
||||
{
|
||||
QStringList warningList;
|
||||
|
||||
if (!m_Computer->isSupportedServerVersion) {
|
||||
emit displayLaunchError(tr("The version of GeForce Experience on %1 is not supported by this build of Moonlight. You must update Moonlight to stream from %1.").arg(m_Computer->name));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_Preferences->absoluteMouseMode && !m_App.isAppCollectorGame) {
|
||||
emitLaunchWarning(tr("Your selection to enable remote desktop mouse mode may cause problems in games."));
|
||||
}
|
||||
@@ -618,32 +640,53 @@ bool Session::validateLaunch(SDL_Window* testWindow)
|
||||
bool hevcForced = m_Preferences->videoCodecConfig == StreamingPreferences::VCC_FORCE_HEVC ||
|
||||
m_Preferences->videoCodecConfig == StreamingPreferences::VCC_FORCE_HEVC_HDR;
|
||||
|
||||
if (m_Preferences->videoDecoderSelection == StreamingPreferences::VDS_AUTO && // Force hardware decoding checked below
|
||||
m_Preferences->videoCodecConfig != StreamingPreferences::VCC_AUTO && // Already checked in initialize()
|
||||
if (m_Computer->maxLumaPixelsHEVC == 0) {
|
||||
if (hevcForced) {
|
||||
emitLaunchWarning(tr("Your host PC GPU doesn't support HEVC. "
|
||||
"A GeForce GTX 900-series (Maxwell) or later GPU is required for HEVC streaming."));
|
||||
}
|
||||
|
||||
// Moonlight-common-c will handle this case already, but we want
|
||||
// to set this explicitly here so we can do our hardware acceleration
|
||||
// check below.
|
||||
m_StreamConfig.supportsHevc = false;
|
||||
}
|
||||
else if (m_Preferences->videoDecoderSelection == StreamingPreferences::VDS_AUTO && // Force hardware decoding checked below
|
||||
hevcForced && // Auto VCC is already checked in initialize()
|
||||
!isHardwareDecodeAvailable(testWindow,
|
||||
m_Preferences->videoDecoderSelection,
|
||||
VIDEO_FORMAT_H265,
|
||||
m_StreamConfig.width,
|
||||
m_StreamConfig.height,
|
||||
m_StreamConfig.fps)) {
|
||||
if (hevcForced) {
|
||||
emitLaunchWarning(tr("Using software decoding due to your selection to force HEVC without GPU support. This may cause poor streaming performance."));
|
||||
emitLaunchWarning(tr("Using software decoding due to your selection to force HEVC without GPU support. This may cause poor streaming performance."));
|
||||
}
|
||||
}
|
||||
|
||||
if (!m_StreamConfig.supportsHevc &&
|
||||
m_Preferences->videoDecoderSelection == StreamingPreferences::VDS_AUTO &&
|
||||
!isHardwareDecodeAvailable(testWindow,
|
||||
m_Preferences->videoDecoderSelection,
|
||||
VIDEO_FORMAT_H264,
|
||||
m_StreamConfig.width,
|
||||
m_StreamConfig.height,
|
||||
m_StreamConfig.fps)) {
|
||||
|
||||
if (m_Preferences->videoCodecConfig == StreamingPreferences::VCC_FORCE_H264) {
|
||||
emitLaunchWarning(tr("Using software decoding due to your selection to force H.264 without GPU support. This may cause poor streaming performance."));
|
||||
}
|
||||
else {
|
||||
if (m_Computer->maxLumaPixelsHEVC == 0 &&
|
||||
isHardwareDecodeAvailable(testWindow,
|
||||
m_Preferences->videoDecoderSelection,
|
||||
VIDEO_FORMAT_H265,
|
||||
m_StreamConfig.width,
|
||||
m_StreamConfig.height,
|
||||
m_StreamConfig.fps)) {
|
||||
emitLaunchWarning(tr("Your host PC and client PC don't support the same video codecs. This may cause poor streaming performance."));
|
||||
}
|
||||
else {
|
||||
emitLaunchWarning(tr("This PC's GPU doesn't support HEVC decoding."));
|
||||
m_StreamConfig.supportsHevc = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (hevcForced) {
|
||||
if (m_Computer->maxLumaPixelsHEVC == 0) {
|
||||
emitLaunchWarning(tr("Your host PC GPU doesn't support HEVC. "
|
||||
"A GeForce GTX 900-series (Maxwell) or later GPU is required for HEVC streaming."));
|
||||
|
||||
// Moonlight-common-c will handle this case already, but we want
|
||||
// to set this explicitly here so we can do our hardware acceleration
|
||||
// check below.
|
||||
m_StreamConfig.supportsHevc = false;
|
||||
emitLaunchWarning(tr("Your client GPU doesn't support H.264 decoding. This may cause poor streaming performance."));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -776,12 +819,17 @@ private:
|
||||
emit m_Session->sessionFinished(m_Session->m_PortTestResults);
|
||||
}
|
||||
|
||||
// The video decoder must already be destroyed, since it could
|
||||
// try to interact with APIs that can only be called between
|
||||
// LiStartConnection() and LiStopConnection().
|
||||
SDL_assert(m_Session->m_VideoDecoder == nullptr);
|
||||
|
||||
// Finish cleanup of the connection state
|
||||
LiStopConnection();
|
||||
|
||||
// Perform a best-effort app quit
|
||||
if (shouldQuit) {
|
||||
NvHTTP http(m_Session->m_Computer->activeAddress, m_Session->m_Computer->serverCert);
|
||||
NvHTTP http(m_Session->m_Computer);
|
||||
|
||||
// Logging is already done inside NvHTTP
|
||||
try {
|
||||
@@ -835,7 +883,7 @@ void Session::getWindowDimensions(int& x, int& y,
|
||||
}
|
||||
}
|
||||
|
||||
fullScreen = (m_Preferences->windowMode != StreamingPreferences::WM_WINDOWED);
|
||||
fullScreen = m_IsFullScreen;
|
||||
}
|
||||
|
||||
SDL_Rect usableBounds;
|
||||
@@ -918,6 +966,24 @@ void Session::updateOptimalWindowDisplayMode()
|
||||
}
|
||||
}
|
||||
|
||||
// For KMSDRM backends where we exclusively own the display, we should opt
|
||||
// to change the resolution if that allows us to hit the desired frame rate
|
||||
if (bestMode.refresh_rate == 0 && strcmp(SDL_GetCurrentVideoDriver(), "KMSDRM") == 0) {
|
||||
for (int i = 0; i < SDL_GetNumDisplayModes(displayIndex); i++) {
|
||||
if (SDL_GetDisplayMode(displayIndex, i, &mode) == 0) {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Detected display mode: %dx%dx%d",
|
||||
mode.w, mode.h, mode.refresh_rate);
|
||||
if (mode.w >= m_ActiveVideoWidth && mode.h >= m_ActiveVideoHeight &&
|
||||
mode.refresh_rate % m_StreamConfig.fps == 0) {
|
||||
if (mode.refresh_rate > bestMode.refresh_rate) {
|
||||
bestMode = mode;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (bestMode.refresh_rate == 0) {
|
||||
// We may find no match if the user has moved a 120 FPS
|
||||
// stream onto a 60 Hz monitor (since no refresh rate can
|
||||
@@ -944,10 +1010,8 @@ void Session::toggleFullscreen()
|
||||
bool fullScreen = !(SDL_GetWindowFlags(m_Window) & m_FullScreenFlag);
|
||||
|
||||
if (fullScreen) {
|
||||
if ((m_FullScreenFlag == SDL_WINDOW_FULLSCREEN || m_Preferences->captureSysKeys) && m_InputHandler->isCaptureActive()) {
|
||||
if (m_FullScreenFlag == SDL_WINDOW_FULLSCREEN && m_InputHandler->isCaptureActive()) {
|
||||
// Confine the cursor to the window if we're capturing input while transitioning to full screen.
|
||||
// We also need to grab if we're capturing system keys, because SDL requires window grab to
|
||||
// capture the keyboard on X11.
|
||||
SDL_SetWindowGrab(m_Window, SDL_TRUE);
|
||||
}
|
||||
|
||||
@@ -964,6 +1028,9 @@ void Session::toggleFullscreen()
|
||||
// Reposition the window when the resize is complete
|
||||
m_PendingWindowedTransition = true;
|
||||
}
|
||||
|
||||
// Input handler might need to start/stop keyboard grab after changing modes
|
||||
m_InputHandler->updateKeyboardGrabState();
|
||||
}
|
||||
|
||||
void Session::notifyMouseEmulationMode(bool enabled)
|
||||
@@ -987,7 +1054,10 @@ class AsyncConnectionStartThread : public QThread
|
||||
public:
|
||||
AsyncConnectionStartThread(Session* session) :
|
||||
QThread(nullptr),
|
||||
m_Session(session) {}
|
||||
m_Session(session)
|
||||
{
|
||||
setObjectName("Async Conn Start");
|
||||
}
|
||||
|
||||
void run() override
|
||||
{
|
||||
@@ -1002,17 +1072,7 @@ bool Session::startConnectionAsync()
|
||||
{
|
||||
// Wait 1.5 seconds before connecting to let the user
|
||||
// have time to read any messages present on the segue
|
||||
#ifdef USE_ASYNC_CONNECT_THREAD
|
||||
SDL_Delay(1500);
|
||||
#else
|
||||
uint32_t start = SDL_GetTicks();
|
||||
while (!SDL_TICKS_PASSED(SDL_GetTicks(), start + 1500)) {
|
||||
// Pump the UI loop while we wait since we're not async
|
||||
SDL_Delay(5);
|
||||
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
|
||||
QCoreApplication::sendPostedEvents();
|
||||
}
|
||||
#endif
|
||||
|
||||
// The UI should have ensured the old game was already quit
|
||||
// if we decide to stream a different game.
|
||||
@@ -1034,16 +1094,19 @@ bool Session::startConnectionAsync()
|
||||
}
|
||||
}
|
||||
|
||||
QString rtspSessionUrl;
|
||||
|
||||
try {
|
||||
NvHTTP http(m_Computer->activeAddress, m_Computer->serverCert);
|
||||
NvHTTP http(m_Computer);
|
||||
if (m_Computer->currentGameId != 0) {
|
||||
http.resumeApp(&m_StreamConfig);
|
||||
http.resumeApp(&m_StreamConfig, rtspSessionUrl);
|
||||
}
|
||||
else {
|
||||
http.launchApp(m_App.id, &m_StreamConfig,
|
||||
enableGameOptimizations,
|
||||
m_Preferences->playAudioOnHost,
|
||||
m_InputHandler->getAttachedGamepadMask());
|
||||
m_InputHandler->getAttachedGamepadMask(),
|
||||
rtspSessionUrl);
|
||||
}
|
||||
} catch (const GfeHttpResponseException& e) {
|
||||
emit displayLaunchError(tr("GeForce Experience returned error: %1").arg(e.toQString()));
|
||||
@@ -1053,7 +1116,7 @@ bool Session::startConnectionAsync()
|
||||
return false;
|
||||
}
|
||||
|
||||
QByteArray hostnameStr = m_Computer->activeAddress.toLatin1();
|
||||
QByteArray hostnameStr = m_Computer->activeAddress.address().toLatin1();
|
||||
QByteArray siAppVersion = m_Computer->appVersion.toLatin1();
|
||||
|
||||
SERVER_INFORMATION hostInfo;
|
||||
@@ -1069,6 +1132,13 @@ bool Session::startConnectionAsync()
|
||||
hostInfo.serverInfoGfeVersion = siGfeVersion.data();
|
||||
}
|
||||
|
||||
// Older GFE and Sunshine versions didn't have this field
|
||||
QByteArray rtspSessionUrlStr;
|
||||
if (!rtspSessionUrl.isEmpty()) {
|
||||
rtspSessionUrlStr = rtspSessionUrl.toLatin1();
|
||||
hostInfo.rtspSessionUrl = rtspSessionUrlStr.data();
|
||||
}
|
||||
|
||||
if (m_Preferences->packetSize != 0) {
|
||||
// Override default packet size and remote streaming detection
|
||||
// NB: Using STREAM_CFG_AUTO will cap our packet size at 1024 for remote hosts.
|
||||
@@ -1124,11 +1194,66 @@ void Session::flushWindowEvents()
|
||||
SDL_PushEvent(&flushEvent);
|
||||
}
|
||||
|
||||
bool Session::getAndClearPendingIdrFrameStatus()
|
||||
{
|
||||
return SDL_AtomicSet(&m_NeedsIdr, 0);
|
||||
}
|
||||
|
||||
class ExecThread : public QThread
|
||||
{
|
||||
public:
|
||||
ExecThread(Session* session) :
|
||||
QThread(nullptr),
|
||||
m_Session(session)
|
||||
{
|
||||
setObjectName("Session Exec");
|
||||
}
|
||||
|
||||
void run() override
|
||||
{
|
||||
m_Session->execInternal();
|
||||
}
|
||||
|
||||
Session* m_Session;
|
||||
};
|
||||
|
||||
void Session::exec(int displayOriginX, int displayOriginY)
|
||||
{
|
||||
m_DisplayOriginX = displayOriginX;
|
||||
m_DisplayOriginY = displayOriginY;
|
||||
|
||||
// Use a separate thread for the streaming session on X11 or Wayland
|
||||
// to ensure we don't stomp on Qt's GL context. This breaks when using
|
||||
// the Qt EGLFS backend, so we will restrict this to X11
|
||||
m_ThreadedExec = WMUtils::isRunningX11() || WMUtils::isRunningWayland();
|
||||
|
||||
if (m_ThreadedExec) {
|
||||
// Run the streaming session on a separate thread for Linux/BSD
|
||||
ExecThread execThread(this);
|
||||
execThread.start();
|
||||
|
||||
// Until the SDL streaming window is created, we should continue
|
||||
// to update the Qt UI to allow warning messages to display and
|
||||
// make sure that the Qt window can hide itself.
|
||||
while (!execThread.wait(10) && m_Window == nullptr) {
|
||||
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
|
||||
QCoreApplication::sendPostedEvents();
|
||||
}
|
||||
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
|
||||
QCoreApplication::sendPostedEvents();
|
||||
|
||||
// SDL is in charge now. Wait until the streaming thread exits
|
||||
// to further update the Qt window.
|
||||
execThread.wait();
|
||||
}
|
||||
else {
|
||||
// Run the streaming session on the main thread for Windows and macOS
|
||||
execInternal();
|
||||
}
|
||||
}
|
||||
|
||||
void Session::execInternal()
|
||||
{
|
||||
// Complete initialization in this deferred context to avoid
|
||||
// calling expensive functions in the constructor (during the
|
||||
// process of loading the StreamSegue).
|
||||
@@ -1152,22 +1277,26 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
||||
m_StreamConfig.width,
|
||||
m_StreamConfig.height);
|
||||
|
||||
// Kick off the async connection thread while we sit here and pump the event loop
|
||||
AsyncConnectionStartThread asyncConnThread(this);
|
||||
#ifdef USE_ASYNC_CONNECT_THREAD
|
||||
asyncConnThread.start();
|
||||
while (!asyncConnThread.wait(10)) {
|
||||
if (!m_ThreadedExec) {
|
||||
// Kick off the async connection thread while we sit here and pump the event loop
|
||||
asyncConnThread.start();
|
||||
while (!asyncConnThread.wait(10)) {
|
||||
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
|
||||
QCoreApplication::sendPostedEvents();
|
||||
}
|
||||
|
||||
// Pump the event loop one last time to ensure we pick up any events from
|
||||
// the thread that happened while it was in the final successful QThread::wait().
|
||||
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
|
||||
QCoreApplication::sendPostedEvents();
|
||||
}
|
||||
#else
|
||||
asyncConnThread.run();
|
||||
#endif
|
||||
|
||||
// Pump the event loop one last time to ensure we pick up any events from
|
||||
// the thread that happened while it was in the final successful QThread::wait().
|
||||
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
|
||||
QCoreApplication::sendPostedEvents();
|
||||
else {
|
||||
// We're already in a separate thread so run the connection operations
|
||||
// synchronously and don't pump the event loop. The main thread is already
|
||||
// pumping the event loop for us.
|
||||
asyncConnThread.run();
|
||||
}
|
||||
|
||||
// If the connection failed, clean up and abort the connection.
|
||||
if (!m_AsyncConnectionSuccess) {
|
||||
@@ -1244,7 +1373,7 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
||||
// For non-full screen windows, call getWindowDimensions()
|
||||
// again after creating a window to allow it to account
|
||||
// for window chrome size.
|
||||
if (m_Preferences->windowMode == StreamingPreferences::WM_WINDOWED) {
|
||||
if (!m_IsFullScreen) {
|
||||
getWindowDimensions(x, y, width, height);
|
||||
|
||||
// We must set the size before the position because centering
|
||||
@@ -1266,6 +1395,7 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
||||
|
||||
bool needsFirstEnterCapture = false;
|
||||
bool needsPostDecoderCreationCapture = false;
|
||||
bool ignoreDuplicateResizes = false;
|
||||
|
||||
// HACK: For Wayland, we wait until we get the first SDL_WINDOWEVENT_ENTER
|
||||
// event where it seems to work consistently on GNOME. For other platforms,
|
||||
@@ -1274,6 +1404,13 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
||||
if (strcmp(SDL_GetCurrentVideoDriver(), "wayland") == 0) {
|
||||
// Native Wayland: Capture on SDL_WINDOWEVENT_ENTER
|
||||
needsFirstEnterCapture = true;
|
||||
|
||||
// As of SDL 2.0.20, we get duplicate SDL_WINDOWEVENT_SIZE_CHANGED events
|
||||
// when focus is lost and gained under GNOME Wayland. This causes us to
|
||||
// recreate our renderer needlessly. This is fixed in SDL 2.0.21.
|
||||
#if !SDL_VERSION_ATLEAST(2, 0, 21)
|
||||
ignoreDuplicateResizes = true;
|
||||
#endif
|
||||
}
|
||||
else {
|
||||
// X11/XWayland: Capture after decoder creation
|
||||
@@ -1311,7 +1448,23 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
||||
// Hijack this thread to be the SDL main thread. We have to do this
|
||||
// because we want to suspend all Qt processing until the stream is over.
|
||||
SDL_Event event;
|
||||
Sint32 lastResizeW = -1, lastResizeH = -1;
|
||||
for (;;) {
|
||||
#if SDL_VERSION_ATLEAST(2, 0, 18) && !defined(STEAM_LINK)
|
||||
// SDL 2.0.18 has a proper wait event implementation that uses platform
|
||||
// support to block on events rather than polling on Windows, macOS, X11,
|
||||
// and Wayland. It will fall back to 1 ms polling if a joystick is
|
||||
// connected, so we don't use it for STEAM_LINK to ensure we only poll
|
||||
// every 10 ms.
|
||||
//
|
||||
// NB: This behavior was introduced in SDL 2.0.16, but had a few critical
|
||||
// issues that could cause indefinite timeouts, delayed joystick detection,
|
||||
// and other problems.
|
||||
if (!SDL_WaitEventTimeout(&event, 1000)) {
|
||||
presence.runCallbacks();
|
||||
continue;
|
||||
}
|
||||
#else
|
||||
// We explicitly use SDL_PollEvent() and SDL_Delay() because
|
||||
// SDL_WaitEvent() has an internal SDL_Delay(10) inside which
|
||||
// blocks this thread too long for high polling rate mice and high
|
||||
@@ -1327,6 +1480,7 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
||||
presence.runCallbacks();
|
||||
continue;
|
||||
}
|
||||
#endif
|
||||
switch (event.type) {
|
||||
case SDL_QUIT:
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
@@ -1344,6 +1498,9 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
||||
case SDL_CODE_SHOW_CURSOR:
|
||||
SDL_ShowCursor(SDL_ENABLE);
|
||||
break;
|
||||
case SDL_CODE_UNCAPTURE_MOUSE:
|
||||
SDL_CaptureMouse(SDL_FALSE);
|
||||
break;
|
||||
case SDL_CODE_FLUSH_WINDOW_EVENT_BARRIER:
|
||||
m_FlushingWindowEventsRef--;
|
||||
break;
|
||||
@@ -1371,6 +1528,8 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
||||
break;
|
||||
}
|
||||
|
||||
presence.runCallbacks();
|
||||
|
||||
// Capture the mouse on SDL_WINDOWEVENT_ENTER if needed
|
||||
if (needsFirstEnterCapture && event.window.event == SDL_WINDOWEVENT_ENTER) {
|
||||
m_InputHandler->setCaptureActive(true);
|
||||
@@ -1413,6 +1572,19 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
||||
break;
|
||||
}
|
||||
|
||||
if (event.window.event == SDL_WINDOWEVENT_SIZE_CHANGED) {
|
||||
if (ignoreDuplicateResizes && lastResizeW == event.window.data1 && lastResizeH == event.window.data2) {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Dropping duplicate size changed event: %d %d",
|
||||
event.window.data1,
|
||||
event.window.data2);
|
||||
break;
|
||||
}
|
||||
|
||||
lastResizeW = event.window.data1;
|
||||
lastResizeH = event.window.data2;
|
||||
}
|
||||
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Recreating renderer for window event: %d (%d %d)",
|
||||
event.window.event,
|
||||
@@ -1483,17 +1655,19 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
||||
}
|
||||
|
||||
// Request an IDR frame to complete the reset
|
||||
m_NeedsIdr = true;
|
||||
SDL_AtomicSet(&m_NeedsIdr, 1);
|
||||
|
||||
SDL_AtomicUnlock(&m_DecoderLock);
|
||||
break;
|
||||
|
||||
case SDL_KEYUP:
|
||||
case SDL_KEYDOWN:
|
||||
presence.runCallbacks();
|
||||
m_InputHandler->handleKeyEvent(&event.key);
|
||||
break;
|
||||
case SDL_MOUSEBUTTONDOWN:
|
||||
case SDL_MOUSEBUTTONUP:
|
||||
presence.runCallbacks();
|
||||
m_InputHandler->handleMouseButtonEvent(&event.button);
|
||||
break;
|
||||
case SDL_MOUSEMOTION:
|
||||
@@ -1507,6 +1681,7 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
||||
break;
|
||||
case SDL_CONTROLLERBUTTONDOWN:
|
||||
case SDL_CONTROLLERBUTTONUP:
|
||||
presence.runCallbacks();
|
||||
m_InputHandler->handleControllerButtonEvent(&event.cbutton);
|
||||
break;
|
||||
case SDL_CONTROLLERDEVICEADDED:
|
||||
@@ -1547,6 +1722,8 @@ DispatchDeferredCleanup:
|
||||
SDL_AtomicUnlock(&m_InputHandlerLock);
|
||||
|
||||
// Destroy the decoder, since this must be done on the main thread
|
||||
// NB: This must happen before LiStopConnection() for pull-based
|
||||
// decoders.
|
||||
SDL_AtomicLock(&m_DecoderLock);
|
||||
delete m_VideoDecoder;
|
||||
m_VideoDecoder = nullptr;
|
||||
|
||||
@@ -17,6 +17,7 @@ class Session : public QObject
|
||||
friend class SdlInputHandler;
|
||||
friend class DeferredSessionCleanupTask;
|
||||
friend class AsyncConnectionStartThread;
|
||||
friend class ExecThread;
|
||||
|
||||
public:
|
||||
explicit Session(NvComputer* computer, NvApp& app, StreamingPreferences *preferences = nullptr);
|
||||
@@ -41,6 +42,8 @@ public:
|
||||
|
||||
void flushWindowEvents();
|
||||
|
||||
bool getAndClearPendingIdrFrameStatus();
|
||||
|
||||
signals:
|
||||
void stageStarting(QString stage);
|
||||
|
||||
@@ -57,6 +60,8 @@ signals:
|
||||
void sessionFinished(int portTestResult);
|
||||
|
||||
private:
|
||||
void execInternal();
|
||||
|
||||
bool initialize();
|
||||
|
||||
bool startConnectionAsync();
|
||||
@@ -133,6 +138,7 @@ private:
|
||||
int drSubmitDecodeUnit(PDECODE_UNIT du);
|
||||
|
||||
StreamingPreferences* m_Preferences;
|
||||
bool m_IsFullScreen;
|
||||
STREAM_CONFIGURATION m_StreamConfig;
|
||||
DECODER_RENDERER_CALLBACKS m_VideoCallbacks;
|
||||
AUDIO_RENDERER_CALLBACKS m_AudioCallbacks;
|
||||
@@ -141,12 +147,13 @@ private:
|
||||
SDL_Window* m_Window;
|
||||
IVideoDecoder* m_VideoDecoder;
|
||||
SDL_SpinLock m_DecoderLock;
|
||||
bool m_NeedsIdr;
|
||||
SDL_atomic_t m_NeedsIdr;
|
||||
bool m_AudioDisabled;
|
||||
bool m_AudioMuted;
|
||||
Uint32 m_FullScreenFlag;
|
||||
int m_DisplayOriginX;
|
||||
int m_DisplayOriginY;
|
||||
bool m_ThreadedExec;
|
||||
bool m_PendingWindowedTransition;
|
||||
bool m_UnexpectedTermination;
|
||||
SdlInputHandler* m_InputHandler;
|
||||
|
||||
@@ -19,6 +19,8 @@ typedef struct _VIDEO_STATS {
|
||||
uint32_t totalDecodeTime;
|
||||
uint32_t totalPacerTime;
|
||||
uint32_t totalRenderTime;
|
||||
uint32_t lastRtt;
|
||||
uint32_t lastRttVariance;
|
||||
float totalFps;
|
||||
float receivedFps;
|
||||
float decodedFps;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#include "cuda.h"
|
||||
|
||||
#include <SDL_opengl.h>
|
||||
|
||||
CUDARenderer::CUDARenderer()
|
||||
: m_HwContext(nullptr)
|
||||
{
|
||||
@@ -55,3 +57,157 @@ bool CUDARenderer::isDirectRenderingSupported()
|
||||
return false;
|
||||
}
|
||||
|
||||
CUDAGLInteropHelper::CUDAGLInteropHelper(AVHWDeviceContext* context)
|
||||
: m_Funcs(nullptr),
|
||||
m_Context((AVCUDADeviceContext*)context->hwctx)
|
||||
{
|
||||
memset(m_Resources, 0, sizeof(m_Resources));
|
||||
|
||||
// One-time init of CUDA library
|
||||
cuda_load_functions(&m_Funcs, nullptr);
|
||||
if (m_Funcs == nullptr) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to initialize CUDA library");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
CUDAGLInteropHelper::~CUDAGLInteropHelper()
|
||||
{
|
||||
unregisterTextures();
|
||||
|
||||
if (m_Funcs != nullptr) {
|
||||
cuda_free_functions(&m_Funcs);
|
||||
}
|
||||
}
|
||||
|
||||
bool CUDAGLInteropHelper::registerBoundTextures()
|
||||
{
|
||||
int err;
|
||||
|
||||
if (m_Funcs == nullptr) {
|
||||
// Already logged in constructor
|
||||
return false;
|
||||
}
|
||||
|
||||
// Push FFmpeg's CUDA context to use for our CUDA operations
|
||||
err = m_Funcs->cuCtxPushCurrent(m_Context->cuda_ctx);
|
||||
if (err != CUDA_SUCCESS) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "cuCtxPushCurrent() failed: %d", err);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Register each plane as a separate resource
|
||||
for (int i = 0; i < NV12_PLANES; i++) {
|
||||
GLint tex;
|
||||
|
||||
// Get the ID of this plane's texture
|
||||
glActiveTexture(GL_TEXTURE0 + i);
|
||||
glGetIntegerv(GL_TEXTURE_BINDING_2D, &tex);
|
||||
|
||||
// Register it with CUDA
|
||||
err = m_Funcs->cuGraphicsGLRegisterImage(&m_Resources[i], tex, GL_TEXTURE_2D, CU_GRAPHICS_REGISTER_FLAGS_WRITE_DISCARD);
|
||||
if (err != CUDA_SUCCESS) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "cuGraphicsGLRegisterImage() failed: %d", err);
|
||||
m_Resources[i] = 0;
|
||||
unregisterTextures();
|
||||
goto Exit;
|
||||
}
|
||||
}
|
||||
|
||||
Exit:
|
||||
{
|
||||
CUcontext dummy;
|
||||
m_Funcs->cuCtxPopCurrent(&dummy);
|
||||
}
|
||||
return err == CUDA_SUCCESS;
|
||||
}
|
||||
|
||||
void CUDAGLInteropHelper::unregisterTextures()
|
||||
{
|
||||
int err;
|
||||
|
||||
if (m_Funcs == nullptr) {
|
||||
// Already logged in constructor
|
||||
return;
|
||||
}
|
||||
|
||||
// Push FFmpeg's CUDA context to use for our CUDA operations
|
||||
err = m_Funcs->cuCtxPushCurrent(m_Context->cuda_ctx);
|
||||
if (err != CUDA_SUCCESS) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "cuCtxPushCurrent() failed: %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < NV12_PLANES; i++) {
|
||||
if (m_Resources[i] != 0) {
|
||||
m_Funcs->cuGraphicsUnregisterResource(m_Resources[i]);
|
||||
m_Resources[i] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
CUcontext dummy;
|
||||
m_Funcs->cuCtxPopCurrent(&dummy);
|
||||
}
|
||||
}
|
||||
|
||||
bool CUDAGLInteropHelper::copyCudaFrameToTextures(AVFrame* frame)
|
||||
{
|
||||
int err;
|
||||
|
||||
if (m_Funcs == nullptr) {
|
||||
// Already logged in constructor
|
||||
return false;
|
||||
}
|
||||
|
||||
// Push FFmpeg's CUDA context to use for our CUDA operations
|
||||
err = m_Funcs->cuCtxPushCurrent(m_Context->cuda_ctx);
|
||||
if (err != CUDA_SUCCESS) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "cuCtxPushCurrent() failed: %d", err);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Map our resources
|
||||
err = m_Funcs->cuGraphicsMapResources(NV12_PLANES, m_Resources, m_Context->stream);
|
||||
if (err != CUDA_SUCCESS) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "cuGraphicsMapResources() failed: %d", err);
|
||||
goto PopCtxExit;
|
||||
}
|
||||
|
||||
for (int i = 0; i < NV12_PLANES; i++) {
|
||||
CUarray cudaArray;
|
||||
|
||||
// Get a pointer to the mapped array for this plane
|
||||
err = m_Funcs->cuGraphicsSubResourceGetMappedArray(&cudaArray, m_Resources[i], 0, 0);
|
||||
if (err != CUDA_SUCCESS) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "cuGraphicsSubResourceGetMappedArray() failed: %d", err);
|
||||
goto UnmapExit;
|
||||
}
|
||||
|
||||
// Do the copy
|
||||
CUDA_MEMCPY2D cu2d = {
|
||||
.srcMemoryType = CU_MEMORYTYPE_DEVICE,
|
||||
.srcDevice = (CUdeviceptr)frame->data[i],
|
||||
.srcPitch = (size_t)frame->linesize[i],
|
||||
.dstMemoryType = CU_MEMORYTYPE_ARRAY,
|
||||
.dstArray = cudaArray,
|
||||
.dstPitch = (size_t)frame->width >> i,
|
||||
.WidthInBytes = (size_t)frame->width,
|
||||
.Height = (size_t)frame->height >> i
|
||||
};
|
||||
err = m_Funcs->cuMemcpy2D(&cu2d);
|
||||
if (err != CUDA_SUCCESS) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "cuMemcpy2D() failed: %d", err);
|
||||
goto UnmapExit;
|
||||
}
|
||||
}
|
||||
|
||||
UnmapExit:
|
||||
m_Funcs->cuGraphicsUnmapResources(NV12_PLANES, m_Resources, m_Context->stream);
|
||||
PopCtxExit:
|
||||
{
|
||||
CUcontext dummy;
|
||||
m_Funcs->cuCtxPopCurrent(&dummy);
|
||||
}
|
||||
return err == CUDA_SUCCESS;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user