Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
973d174a30 | ||
|
|
9f35690b95 | ||
|
|
d961f2bc13 | ||
|
|
10e8277044 | ||
|
|
b4c40d9b18 | ||
|
|
e1603f0808 | ||
|
|
d4c2350d63 | ||
|
|
5e5470ae12 | ||
|
|
6230a99360 | ||
|
|
801adb6af7 | ||
|
|
8b9b96f422 | ||
|
|
160684f23e | ||
|
|
88930a9de4 | ||
|
|
af6b8c9b88 | ||
|
|
bf52d3189d | ||
|
|
5d872e8b5b | ||
|
|
4832d8007b | ||
|
|
879e084383 | ||
|
|
1ae16bb657 | ||
|
|
2177bc68db | ||
|
|
c8b887fe32 | ||
|
|
7205242f38 | ||
|
|
be33b5b387 | ||
|
|
66a123f4b9 | ||
|
|
76a4ee22b3 | ||
|
|
3a6bf4d20b | ||
|
|
09cf105cdc | ||
|
|
ce02e086a3 | ||
|
|
e8d4313454 | ||
|
|
49046f88cf | ||
|
|
5e2f32e3eb | ||
|
|
b4a1393bf6 | ||
|
|
0ccee9d806 | ||
|
|
5700272dc4 | ||
|
|
7e5a3925b3 | ||
|
|
1febe32e73 | ||
|
|
6059c982cf | ||
|
|
de7b973239 | ||
|
|
741113a607 | ||
|
|
c20daa8867 | ||
|
|
5e57a7d5c0 | ||
|
|
7c8d27700e | ||
|
|
f8621be5ee | ||
|
|
8ed87e10b4 | ||
|
|
ca26fa2dd0 | ||
|
|
1669095fb1 | ||
|
|
13eed48877 | ||
|
|
07494e6eff | ||
|
|
41b1d72825 | ||
|
|
4d2965e74f | ||
|
|
12526bfca4 | ||
|
|
3e2c344140 | ||
|
|
33730dd584 | ||
|
|
1cb03b1882 | ||
|
|
2f0a6db414 | ||
|
|
a42328d971 | ||
|
|
96ff51d598 | ||
|
|
7c3d3af681 | ||
|
|
6942bca5a7 | ||
|
|
9eac9a190e | ||
|
|
40e4fbd9a6 | ||
|
|
7b6996ed80 | ||
|
|
ef2827a9d8 | ||
|
|
dbf98ce3a6 | ||
|
|
ee7cb5123d | ||
|
|
77e1d6ab8a | ||
|
|
1640bed7a5 | ||
|
|
c6fd5cbbce | ||
|
|
5288041da5 | ||
|
|
a848fb1dd1 | ||
|
|
172945434c | ||
|
|
a0967e0281 | ||
|
|
deab717c73 | ||
|
|
5db5a088ea | ||
|
|
5e106d2f19 | ||
|
|
940da6bc73 | ||
|
|
d110bff253 | ||
|
|
4caa329c36 | ||
|
|
7c7d279337 | ||
|
|
831e6ee76d | ||
|
|
b01842ad02 | ||
|
|
7acf91883a | ||
|
|
897f6fdd3e | ||
|
|
6f9c3fbc38 | ||
|
|
bcbd1a5b4b | ||
|
|
6468efd7e4 | ||
|
|
66863789c7 | ||
|
|
b30058fc29 | ||
|
|
a41cd0b070 | ||
|
|
ea8b905f02 | ||
|
|
16b301236b | ||
|
|
22162dda83 | ||
|
|
024af89be3 | ||
|
|
e1a697658e | ||
|
|
0a5051f959 | ||
|
|
82b6b60b31 | ||
|
|
06b3c3636a | ||
|
|
e5712a9474 | ||
|
|
6ff845c53b | ||
|
|
1c225ed04d | ||
|
|
612b738968 | ||
|
|
87356ef594 | ||
|
|
20a4dfa6d6 | ||
|
|
524cc97f51 | ||
|
|
fc849fb51a | ||
|
|
c88651939a | ||
|
|
01254f67f0 | ||
|
|
212ed33b21 | ||
|
|
bd3f8aef21 | ||
|
|
3785acea29 | ||
|
|
866e2f1762 | ||
|
|
0a705d6752 | ||
|
|
4f74fd5354 | ||
|
|
3df9a5d01c | ||
|
|
2682829bd3 | ||
|
|
30cfa2607d | ||
|
|
4467c2e6ad | ||
|
|
2c0e8a0ddf | ||
|
|
4c17f32a2e | ||
|
|
674220087f | ||
|
|
fc8d5d5799 | ||
|
|
4e4f04c174 | ||
|
|
65c21f3392 | ||
|
|
97fb30cdf1 | ||
|
|
c975279589 | ||
|
|
b3ee7a635f | ||
|
|
c2b12868bb | ||
|
|
bdbb03e16f | ||
|
|
53138d7c16 | ||
|
|
18d1d35104 | ||
|
|
99d9de35de | ||
|
|
e6a48481a5 | ||
|
|
21f2b1224a | ||
|
|
187f47a353 | ||
|
|
cb75b3d6d2 | ||
|
|
27708b9ecb | ||
|
|
5443165fce | ||
|
|
dfb006d264 | ||
|
|
4f00f62674 | ||
|
|
cec527ffd5 | ||
|
|
4f2fe5c988 | ||
|
|
2bf552f801 | ||
|
|
477fa8fedf | ||
|
|
f1c7c5f02c | ||
|
|
4e936193c7 | ||
|
|
f9895c22b6 | ||
|
|
6e46537112 | ||
|
|
92b3918b26 | ||
|
|
34c615a5fd | ||
|
|
fa29260e09 | ||
|
|
a05816b495 | ||
|
|
9c5d847dae | ||
|
|
082e330f9d | ||
|
|
8878c4a136 | ||
|
|
00926bac81 | ||
|
|
67b0907e52 | ||
|
|
dba479774b | ||
|
|
5c8a1e632b | ||
|
|
d65e29111f | ||
|
|
ed170b8092 | ||
|
|
ac36fef968 | ||
|
|
10f6b73cad | ||
|
|
13d933fc07 | ||
|
|
3ba98e86f2 | ||
|
|
1dfca525cf | ||
|
|
e51ad1a68a | ||
|
|
9c76700f74 | ||
|
|
773b1af6f1 | ||
|
|
b8b633a6cc | ||
|
|
ada2270bd1 | ||
|
|
25e5175c54 | ||
|
|
9dcd770df2 | ||
|
|
00a1948b73 | ||
|
|
859a5a5e0c | ||
|
|
6783cf57da | ||
|
|
a8dc5ac14a | ||
|
|
6182553d79 | ||
|
|
905a0cdac1 | ||
|
|
f20ddcf26d | ||
|
|
fe26098c2b | ||
|
|
2354a0a640 | ||
|
|
7c3f09c70c | ||
|
|
caf5350fe6 | ||
|
|
84084835ce | ||
|
|
7f38a67ede | ||
|
|
54383b84ec | ||
|
|
948910bfa2 | ||
|
|
ce5dac07d3 | ||
|
|
d4b0057423 | ||
|
|
de8b12f594 | ||
|
|
69e31a0a37 | ||
|
|
c383cd39f4 | ||
|
|
e88d2f0ab2 | ||
|
|
32535578a0 | ||
|
|
2e9af8fef0 | ||
|
|
0aa87aacc6 | ||
|
|
bc38c302ed | ||
|
|
c724902508 | ||
|
|
73604020d7 | ||
|
|
060a00b9c7 | ||
|
|
2cd32bf25a | ||
|
|
81ad1a18fa | ||
|
|
1d94903eef | ||
|
|
2d7dcb495c | ||
|
|
bc580c9aaa | ||
|
|
f8e2740532 | ||
|
|
a079ecd20b | ||
|
|
89c8725766 | ||
|
|
da7d532564 | ||
|
|
4752d4966d | ||
|
|
50f47f1ed5 | ||
|
|
26bdc3e1d5 | ||
|
|
764360c0f3 | ||
|
|
08482577eb | ||
|
|
cabbba2448 | ||
|
|
c48ed7bb0d | ||
|
|
fa4c0e82bd | ||
|
|
c0bf8b9c25 | ||
|
|
d2cff86c97 | ||
|
|
03c0be0720 | ||
|
|
a3361363e4 | ||
|
|
40e142c5c7 | ||
|
|
2703efedef | ||
|
|
c313f1a20b | ||
|
|
b7116657d9 | ||
|
|
2bf3f1b94b | ||
|
|
b78a7971d9 | ||
|
|
5079bc4976 | ||
|
|
555032c15c | ||
|
|
bcff327565 | ||
|
|
e95ff989b1 | ||
|
|
018e8999e2 | ||
|
|
f624a1817e | ||
|
|
202a643e5a | ||
|
|
c75b9c9221 | ||
|
|
9a6f5ba1a8 | ||
|
|
052194714a | ||
|
|
5ccb4a8b7f | ||
|
|
7c3cec0451 | ||
|
|
b79d3b31b3 | ||
|
|
bf5d43d0cf | ||
|
|
f2ac570949 | ||
|
|
37636ef1eb | ||
|
|
c540bec824 | ||
|
|
9288cfa684 | ||
|
|
b1de8b703b | ||
|
|
ccd0777ce9 | ||
|
|
49b67b3e18 | ||
|
|
bc97168dc7 | ||
|
|
0ec04df334 | ||
|
|
26d7946b4a | ||
|
|
292cc579cf | ||
|
|
8492482e55 | ||
|
|
ac8183feee | ||
|
|
8037fb76fc | ||
|
|
754a14941b | ||
|
|
e32bc1a0a3 | ||
|
|
e5a8b49c93 | ||
|
|
b6872456cd | ||
|
|
2a12b018da | ||
|
|
f64486c88f | ||
|
|
80a40ac5f9 | ||
|
|
a1956350b1 | ||
|
|
2c09875556 | ||
|
|
561eed4cbd | ||
|
|
a8f72df7f8 | ||
|
|
ab4127f940 | ||
|
|
f03d12b677 | ||
|
|
81c6202582 | ||
|
|
6cab2bec61 | ||
|
|
94ffda653c | ||
|
|
c2268b45f6 | ||
|
|
38b8a59fe5 | ||
|
|
640e3213ee | ||
|
|
b1799009b3 | ||
|
|
f8e693a060 | ||
|
|
aba806bc32 | ||
|
|
b1fb3f2391 | ||
|
|
041e901c61 | ||
|
|
bb72134992 | ||
|
|
f83853c2fb | ||
|
|
d5f7b20be3 | ||
|
|
b878d64460 | ||
|
|
5397e8ae1a | ||
|
|
46bd8ad83d | ||
|
|
4dac57cf8c | ||
|
|
95cb026446 | ||
|
|
76ea714257 | ||
|
|
5788fa7204 | ||
|
|
d077c6450f | ||
|
|
4499ed3878 | ||
|
|
feb5e3bc78 | ||
|
|
23f1060756 | ||
|
|
6ed512e762 | ||
|
|
947970e07b | ||
|
|
df567b405a | ||
|
|
7b0596d9df | ||
|
|
4b7200ae69 | ||
|
|
710648f3f4 | ||
|
|
d2002b6a62 | ||
|
|
8de42b3199 | ||
|
|
48e78d2cd3 | ||
|
|
1fd099db70 | ||
|
|
2986a40c5a | ||
|
|
8bb6d4d0d9 | ||
|
|
00f08578ee | ||
|
|
6a514c7f51 | ||
|
|
21aad643f3 | ||
|
|
8687448966 | ||
|
|
6fcc8b721b | ||
|
|
b4bbd58c77 | ||
|
|
1a45b8f07a | ||
|
|
4471a858db | ||
|
|
22e712af5a | ||
|
|
9799b82d53 | ||
|
|
78915e0b99 | ||
|
|
c9cc65aae7 | ||
|
|
a4edd9d202 | ||
|
|
7747624c80 | ||
|
|
aab7191a44 | ||
|
|
1f972b1828 | ||
|
|
58b77cf589 | ||
|
|
c064f87889 | ||
|
|
d6b4c8e7da | ||
|
|
49f0b45fa8 | ||
|
|
b19c4bfc5d | ||
|
|
e944c819d9 | ||
|
|
75f631599c | ||
|
|
e0bebeae25 | ||
|
|
32e7ddf272 | ||
|
|
7091e0324e | ||
|
|
8f8532921c | ||
|
|
e9936992ec | ||
|
|
6375055b3e | ||
|
|
88b1ea296c | ||
|
|
2a13a869f3 | ||
|
|
89c342bb6f | ||
|
|
067f5e33aa | ||
|
|
33695f1c6b | ||
|
|
52ef84873e | ||
|
|
c21ba5c808 | ||
|
|
6cf408f12b | ||
|
|
8bdc2d285d | ||
|
|
a66fbcd6be | ||
|
|
93402a62fc | ||
|
|
2a56fb6c70 | ||
|
|
8276c031e9 | ||
|
|
102e5f6de9 | ||
|
|
1f7bdbfbcb | ||
|
|
f2fdd048c7 | ||
|
|
5a2cfc15a6 | ||
|
|
11c7131875 | ||
|
|
34aacc90d7 | ||
|
|
e75dce2543 | ||
|
|
e2774309fb | ||
|
|
dfefdc93c5 | ||
|
|
d46ea28bba | ||
|
|
d35c5bcaba | ||
|
|
2b14856d7f | ||
|
|
3316fdb80e | ||
|
|
c5d70c48c9 | ||
|
|
82230ef2f5 | ||
|
|
dcfd3d3e48 | ||
|
|
cb1e2ac32e | ||
|
|
ec2c15946a | ||
|
|
8be73142f8 | ||
|
|
d1640e3bb8 | ||
|
|
8612e6726d | ||
|
|
3b4694e9f8 | ||
|
|
14cb2e1999 | ||
|
|
cfabaf334e | ||
|
|
c054536fab | ||
|
|
35aac18b4e | ||
|
|
de3928cb4d | ||
|
|
9bf612b800 | ||
|
|
d9a8d67e3d | ||
|
|
99b0a56a6a | ||
|
|
61d7aa0400 | ||
|
|
1e98594972 | ||
|
|
c6383f042c | ||
|
|
b4d8e0e551 | ||
|
|
d696536486 | ||
|
|
b46a3cf759 | ||
|
|
8a5e3cdb76 | ||
|
|
4fb39226ef | ||
|
|
c40174034c | ||
|
|
5029c855f0 | ||
|
|
2eee839eaa | ||
|
|
f42c7fec1e | ||
|
|
0ab07303c9 | ||
|
|
ad47990a87 | ||
|
|
1968461ce9 | ||
|
|
8aa3b5d3da | ||
|
|
23e77eefb0 | ||
|
|
094443cf26 | ||
|
|
b97758ef46 | ||
|
|
d1fe752028 | ||
|
|
b26df787cd | ||
|
|
4c5b254730 | ||
|
|
9a153b7170 | ||
|
|
065a6caee5 | ||
|
|
9c7df02941 | ||
|
|
c63c5100e2 | ||
|
|
bf05088d70 | ||
|
|
e7bb146f92 | ||
|
|
688c4a90d9 | ||
|
|
165ad96be1 | ||
|
|
115ced7ab4 | ||
|
|
ef15b5641a | ||
|
|
be50464619 | ||
|
|
d847b71c86 | ||
|
|
dfcc1ff899 | ||
|
|
65562937f3 | ||
|
|
7b5dc33a21 | ||
|
|
f64c13b9ec | ||
|
|
25f70e570e | ||
|
|
b27b64c25c | ||
|
|
bfc64c926a | ||
|
|
88ad983aa4 | ||
|
|
a95d2e76f5 | ||
|
|
037886ba5b | ||
|
|
011edfe2a0 | ||
|
|
1a5a6773ce | ||
|
|
dda22fd387 | ||
|
|
7254224347 | ||
|
|
789e52af57 | ||
|
|
b2fcd6c084 | ||
|
|
76e5794203 | ||
|
|
a81332dd23 | ||
|
|
e1fab22d98 | ||
|
|
cb5e58d465 |
No files matched your search
@@ -1,8 +1,10 @@
|
|||||||
---
|
---
|
||||||
name: Bug report
|
name: Bug report
|
||||||
about: Create a report to help us address broken functionality
|
about: Create a report to help us address issues in Moonlight
|
||||||
|
|
||||||
---
|
---
|
||||||
|
**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://discord.gg/MySTSdq
|
||||||
|
|
||||||
**Describe the bug**
|
**Describe the bug**
|
||||||
A clear and concise description of what the bug is.
|
A clear and concise description of what the bug is.
|
||||||
@@ -13,19 +15,31 @@ Any special steps that are required for the bug to appear.
|
|||||||
**Screenshots**
|
**Screenshots**
|
||||||
If applicable, add screenshots to help explain your problem. If the issue is related to video glitching or poor quality, please include screenshots.
|
If applicable, add screenshots to help explain your problem. If the issue is related to video glitching or poor quality, please include screenshots.
|
||||||
|
|
||||||
|
**Affected games**
|
||||||
|
List the games you've tried that exhibit the issue. To see if the issue is game-specific, try streaming Steam Big Picture with Moonlight and see if the issue persists there.
|
||||||
|
|
||||||
|
**Other Moonlight clients**
|
||||||
|
- Does the issue occur when using Moonlight on iOS or Android?
|
||||||
|
|
||||||
**Moonlight settings (please complete the following information)**
|
**Moonlight settings (please complete the following information)**
|
||||||
- Have any settings been adjusted from defaults?
|
- Have any settings been adjusted from defaults?
|
||||||
- If so, which settings have been changed?
|
- If so, which settings have been changed?
|
||||||
- Does the problem still occur after reverting settings back to default?
|
- Does the problem still occur after reverting settings back to default?
|
||||||
|
|
||||||
**Client PC details (please complete the following information)**
|
**Client PC details (please complete the following information)**
|
||||||
- OS: [e.g. Windows 10 1803]
|
- OS: [e.g. Windows 10 1809]
|
||||||
- Moonlight Version: [e.g. v0.2.0]
|
- Moonlight Version: [e.g. v0.9.0]
|
||||||
|
|
||||||
**Logs (please attach)**
|
**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]
|
||||||
|
|
||||||
|
**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.
|
- On Windows, `Moonlight-###.log` files can be found in `%TEMP%`. Simply type that into the File Explorer path field to navigate there.
|
||||||
- On macOS, `Moonlight-###.log` files can be found in `/tmp`. In Finder, press Cmd+Shift+G, then type `/tmp` to navigate there.
|
- On macOS, `Moonlight-###.log` files can be found in `/tmp`. In Finder, press Cmd+Shift+G, then type `/tmp` to navigate there.
|
||||||
- On Linux, logs print to the terminal when running via `flatpak run com.moonlight_stream.Moonlight`
|
- On Linux with the Flatpak, logs print to the terminal when running with the command: `flatpak run com.moonlight_stream.Moonlight`
|
||||||
|
- On Linux with the Snap, logs print to the terminal when running with the command: `moonlight`
|
||||||
|
|
||||||
**Additional context**
|
**Additional context**
|
||||||
Add any other context about the problem here.
|
Anything else you think may be relevant to the issue
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
issuesOpened: >
|
||||||
|
If this is a question about Moonlight or you need help troubleshooting a streaming problem, please use the help channels on our [Discord server](https://discord.gg/MySTSdq) instead of GitHub issues. There are many more people available on Discord to help you and answer your questions.<br /><br />
|
||||||
|
This issue tracker should only be used for specific bugs or feature requests.<br /><br />
|
||||||
|
Thank you, and happy streaming!
|
||||||
@@ -2,4 +2,6 @@
|
|||||||
|
|
||||||
|
|
||||||
**/.vs/
|
**/.vs/
|
||||||
build/
|
build/
|
||||||
|
config.tests/*/.qmake.stash
|
||||||
|
config.tests/*/Makefile
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
url = https://github.com/gabomdq/SDL_GameControllerDB.git
|
url = https://github.com/gabomdq/SDL_GameControllerDB.git
|
||||||
[submodule "soundio/libsoundio"]
|
[submodule "soundio/libsoundio"]
|
||||||
path = soundio/libsoundio
|
path = soundio/libsoundio
|
||||||
url = https://github.com/cgutman/libsoundio.git
|
url = https://github.com/andrewrk/libsoundio.git
|
||||||
[submodule "h264bitstream/h264bitstream"]
|
[submodule "h264bitstream/h264bitstream"]
|
||||||
path = h264bitstream/h264bitstream
|
path = h264bitstream/h264bitstream
|
||||||
url = https://github.com/aizvorski/h264bitstream.git
|
url = https://github.com/aizvorski/h264bitstream.git
|
||||||
@@ -6,45 +6,67 @@ git:
|
|||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- os: osx
|
- os: osx
|
||||||
|
osx_image: xcode10.2
|
||||||
compiler: clang
|
compiler: clang
|
||||||
|
env: TRAVIS_TARGET=osx
|
||||||
|
addons:
|
||||||
|
homebrew:
|
||||||
|
packages:
|
||||||
|
- qt
|
||||||
- os: linux
|
- os: linux
|
||||||
sudo: required
|
sudo: required
|
||||||
dist: xenial
|
dist: xenial
|
||||||
compiler: gcc
|
compiler: gcc
|
||||||
|
env: TRAVIS_TARGET=steamlink
|
||||||
|
addons:
|
||||||
|
apt:
|
||||||
|
packages:
|
||||||
|
- libc6:i386
|
||||||
|
- libstdc++6:i386
|
||||||
|
- os: linux
|
||||||
|
sudo: required
|
||||||
|
dist: bionic
|
||||||
|
compiler: gcc
|
||||||
|
env: TRAVIS_TARGET=linux
|
||||||
addons:
|
addons:
|
||||||
apt:
|
apt:
|
||||||
sources:
|
sources:
|
||||||
- sourceline: 'ppa:beineri/opt-qt596-xenial'
|
- sourceline: 'ppa:savoury1/multimedia'
|
||||||
- sourceline: 'ppa:jonathonf/ffmpeg-4'
|
- sourceline: 'ppa:savoury1/ffmpeg4'
|
||||||
packages:
|
packages:
|
||||||
- qt59base
|
- qt5-default
|
||||||
- qt59quickcontrols2
|
- qtbase5-dev
|
||||||
- qt59svg
|
- qtquickcontrols2-5-dev
|
||||||
- qt59multimedia
|
- libqt5svg5-dev
|
||||||
- libesd0-dev
|
- libgl1-mesa-dev
|
||||||
- libgl-dev
|
|
||||||
- libegl1-mesa-dev
|
- libegl1-mesa-dev
|
||||||
- libxkbcommon-dev
|
- libxkbcommon-dev
|
||||||
- wayland-protocols
|
- wayland-protocols
|
||||||
- libopus-dev
|
- libopus-dev
|
||||||
- libavcodec-dev
|
|
||||||
- libavutil-dev
|
|
||||||
- libva-dev
|
- libva-dev
|
||||||
- libvdpau-dev
|
- libvdpau-dev
|
||||||
|
- libsdl2-dev
|
||||||
|
- qtdeclarative5-dev
|
||||||
|
- libssl-dev
|
||||||
|
- libsdl2-ttf-dev
|
||||||
|
- libavcodec-dev
|
||||||
|
- libavutil-dev
|
||||||
|
|
||||||
install:
|
install:
|
||||||
- '[ "$TRAVIS_OS_NAME" != osx ] || brew install qt5'
|
- '[ "$TRAVIS_TARGET" != osx ] || brew link --force qt'
|
||||||
- '[ "$TRAVIS_OS_NAME" != osx ] || brew link --force qt5'
|
- if [[ "$TRAVIS_TARGET" = "osx" ]]; then nvm use node && npm install --global create-dmg; fi
|
||||||
- if [[ "$TRAVIS_OS_NAME" = "linux" ]]; then export SDL2_VER=2.0.8 && wget https://www.libsdl.org/release/SDL2-$SDL2_VER.tar.gz && tar -xvf SDL2-$SDL2_VER.tar.gz && cd SDL2-$SDL2_VER && ./configure && make && sudo make install && cd ..; fi
|
- if [[ "$TRAVIS_TARGET" = "steamlink" ]]; then git clone --depth=1 https://github.com/ValveSoftware/steamlink-sdk.git $HOME/steamlink-sdk; fi
|
||||||
|
|
||||||
before_script:
|
before_script:
|
||||||
- '[ "$TRAVIS_OS_NAME" != "linux" ] || source /opt/qt59/bin/qt59-env.sh'
|
- '[ "$TRAVIS_TARGET" = "steamlink" ] || qmake -v'
|
||||||
- qmake -v
|
- '[[ "$TRAVIS_TARGET" != linux || "$CXX" != clang++ ]] || export QMAKESPEC=linux-clang'
|
||||||
- '[[ "$TRAVIS_OS_NAME" != linux || "$CXX" != clang++ ]] || export QMAKESPEC=linux-clang'
|
- '[[ "$TRAVIS_TARGET" != linux || "$CXX" != g++ ]] || export QMAKESPEC=linux-g++'
|
||||||
- '[[ "$TRAVIS_OS_NAME" != linux || "$CXX" != g++ ]] || export QMAKESPEC=linux-g++'
|
- '[ "$TRAVIS_TARGET" != osx ] || export QMAKESPEC=macx-clang'
|
||||||
- '[ "$TRAVIS_OS_NAME" != osx ] || export QMAKESPEC=macx-clang'
|
- '[ "$TRAVIS_TARGET" != "steamlink" ] || export STEAMLINK_SDK_PATH=$HOME/steamlink-sdk'
|
||||||
|
|
||||||
script:
|
script:
|
||||||
- qmake moonlight-qt.pro -spec $QMAKESPEC
|
- '[ "$TRAVIS_TARGET" != "linux" ] || qmake moonlight-qt.pro -spec $QMAKESPEC'
|
||||||
- make debug
|
- '[ "$TRAVIS_TARGET" != "linux" ] || make -j$(nproc) debug'
|
||||||
- make release
|
- '[ "$TRAVIS_TARGET" != "linux" ] || make -j$(nproc) release'
|
||||||
|
- '[ "$TRAVIS_TARGET" != "osx" ] || scripts/generate-dmg.sh Release'
|
||||||
|
- '[ "$TRAVIS_TARGET" != "steamlink" ] || scripts/build-steamlink-app.sh'
|
||||||
@@ -3,11 +3,7 @@ QT -= core gui
|
|||||||
TARGET = AntiHooking
|
TARGET = AntiHooking
|
||||||
TEMPLATE = lib
|
TEMPLATE = lib
|
||||||
|
|
||||||
# Support debug and release builds from command line for CI
|
include(../globaldefs.pri)
|
||||||
CONFIG += debug_and_release
|
|
||||||
|
|
||||||
# Ensure symbols are always generated
|
|
||||||
CONFIG += force_debug_info
|
|
||||||
|
|
||||||
INCLUDEPATH += $$PWD/../libs/windows/include
|
INCLUDEPATH += $$PWD/../libs/windows/include
|
||||||
contains(QT_ARCH, i386) {
|
contains(QT_ARCH, i386) {
|
||||||
|
|||||||
@@ -134,7 +134,46 @@ private:
|
|||||||
// This DLL shipped with ASUS Sonic Radar 3 improperly handles
|
// This DLL shipped with ASUS Sonic Radar 3 improperly handles
|
||||||
// D3D9 exclusive fullscreen in a way that causes CreateDeviceEx()
|
// D3D9 exclusive fullscreen in a way that causes CreateDeviceEx()
|
||||||
// to deadlock. https://github.com/moonlight-stream/moonlight-qt/issues/102
|
// to deadlock. https://github.com/moonlight-stream/moonlight-qt/issues/102
|
||||||
L"NahimicOSD.dll"
|
L"NahimicOSD.dll",
|
||||||
|
|
||||||
|
// This DLL has been seen in several crash reports. Some Googling
|
||||||
|
// suggests it's highly unstable and causes issues in many games.
|
||||||
|
L"EZFRD32.dll",
|
||||||
|
L"EZFRD64.dll",
|
||||||
|
|
||||||
|
// These are the dList DLLs for Optimus hybrid graphics DDI.
|
||||||
|
// https://docs.microsoft.com/en-us/windows-hardware/drivers/display/hybrid-system-ddi
|
||||||
|
//
|
||||||
|
// We forcefully block them from loading because Optimus has a bug that
|
||||||
|
// deadlocks DXVA2 when we present with D3DPRESENT_DONOTWAIT. This will prevent
|
||||||
|
// Optimus from ever using the dGPU even if the user has requested it.
|
||||||
|
// https://github.com/moonlight-stream/moonlight-qt/issues/240
|
||||||
|
// https://github.com/moonlight-stream/moonlight-qt/issues/235
|
||||||
|
L"nvdlist.dll",
|
||||||
|
L"nvdlistx.dll",
|
||||||
|
|
||||||
|
// In some unknown circumstances, RTSS tries to hook in the middle of an instruction, leaving garbage
|
||||||
|
// code inside d3d9.dll that causes a crash when executed:
|
||||||
|
//
|
||||||
|
// 0:000> u
|
||||||
|
// d3d9!D3D9GetCurrentOwnershipMode+0x5d:
|
||||||
|
// 00007ff8`95b95861 9b wait
|
||||||
|
// 00007ff8`95b95862 a7 cmps dword ptr [rsi],dword ptr [rdi] <--- crash happens here
|
||||||
|
// 00007ff8`95b95863 ff ???
|
||||||
|
// 00007ff8`95b95864 bfe8ca8a00 mov edi,8ACAE8h
|
||||||
|
// 00007ff8`95b95869 00eb add bl,ch
|
||||||
|
// 00007ff8`95b9586b f1 ???
|
||||||
|
// 00007ff8`95b9586c b808000000 mov eax,8
|
||||||
|
// 00007ff8`95b95871 ebe6 jmp d3d9!D3D9GetCurrentOwnershipMode+0x55 (00007ff8`95b95859)
|
||||||
|
//
|
||||||
|
// Disassembling starting at the exact address of the attempted hook yields the intended jmp instruction
|
||||||
|
//
|
||||||
|
// 0:000> u d3d9!D3D9GetCurrentOwnershipMode+0x5c:
|
||||||
|
// 00007ff8`95b95860 e99ba7ffbf jmp 00007ff8`55b90000
|
||||||
|
//
|
||||||
|
// Since the RTSS OSD doesn't even work with DXVA2, we'll just block the hooks entirely.
|
||||||
|
L"RTSSHooks.dll",
|
||||||
|
L"RTSSHooks64.dll",
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
# Moonlight PC
|
# Moonlight PC
|
||||||
|
|
||||||
[Moonlight PC](http://moonlight-stream.com) is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield, but built to run on Windows, Mac, and Linux. This client is the successor to [Moonlight Chrome](https://github.com/moonlight-stream/moonlight-chrome) for streaming on PC.
|
[Moonlight PC](https://moonlight-stream.org) is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield, but built to run on Windows, Mac, and Linux. This client is the successor to [Moonlight Chrome](https://github.com/moonlight-stream/moonlight-chrome) for streaming on PC.
|
||||||
|
|
||||||
Moonlight also has mobile versions for [Android](https://github.com/moonlight-stream/moonlight-android) and [iOS](https://github.com/moonlight-stream/moonlight-ios).
|
Moonlight also has mobile versions for [Android](https://github.com/moonlight-stream/moonlight-android) and [iOS](https://github.com/moonlight-stream/moonlight-ios).
|
||||||
|
|
||||||
See the [releases page](https://github.com/moonlight-stream/moonlight-qt/releases) for download links for Windows and Mac. For Linux, we're on [Flathub](https://flathub.org/apps/details/com.moonlight_stream.Moonlight).
|
|
||||||
|
|
||||||
You can follow development on our [Discord server](https://discord.gg/6ERtzFY).
|
You can follow development on our [Discord server](https://discord.gg/6ERtzFY).
|
||||||
|
|
||||||
[](https://ci.appveyor.com/project/cgutman/moonlight-qt/branch/master)
|
[](https://ci.appveyor.com/project/cgutman/moonlight-qt/branch/master)
|
||||||
[](https://travis-ci.org/moonlight-stream/moonlight-qt)
|
[](https://travis-ci.org/moonlight-stream/moonlight-qt)
|
||||||
|
[](https://github.com/moonlight-stream/moonlight-qt/releases)
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
- Hardware accelerated video decoding on Windows, Mac, and Linux
|
- Hardware accelerated video decoding on Windows, Mac, and Linux
|
||||||
@@ -18,41 +17,55 @@ You can follow development on our [Discord server](https://discord.gg/6ERtzFY).
|
|||||||
- 5.1 surround sound audio
|
- 5.1 surround sound audio
|
||||||
- HEVC support for better image quality at reduced bandwidth
|
- HEVC support for better image quality at reduced bandwidth
|
||||||
- Keyboard and mouse support
|
- Keyboard and mouse support
|
||||||
- Gamepad support with SDL gamepad mappings
|
- Gamepad support with force feedback for up to 4 players
|
||||||
|
|
||||||
|
## Downloads
|
||||||
|
- [Windows, macOS, and Steam Link](https://github.com/moonlight-stream/moonlight-qt/releases)
|
||||||
|
- [Snap (for Ubuntu-based Linux distros)](https://snapcraft.io/moonlight)
|
||||||
|
- [Flatpak (for other Linux distros)](https://flathub.org/apps/details/com.moonlight_stream.Moonlight)
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
### General Build Requirements
|
### General Build Requirements
|
||||||
* Qt 5.9 SDK or later
|
* Qt 5.9 SDK or later
|
||||||
|
|
||||||
### Windows-specific Requirements
|
### Windows-specific Build Requirements
|
||||||
* Windows 7 or later
|
* Windows 7 or later
|
||||||
* Visual Studio 2015 or later
|
* [Visual Studio](https://visualstudio.microsoft.com/downloads/) 2017 (Community edition is fine)
|
||||||
* Select MSVC Desktop toolchain during Qt installation
|
* Select MSVC Desktop toolchain during Qt installation
|
||||||
* 7-Zip (only if building installers for non-development PCs)
|
* [DirectX SDK](https://www.microsoft.com/en-us/download/details.aspx?id=6812)
|
||||||
* WiX Toolset v3.11 or later (only if building installers for non-development PCs)
|
* [7-Zip](https://www.7-zip.org/) (only if building installers for non-development PCs)
|
||||||
|
* [WiX Toolset](http://wixtoolset.org/releases/) v3.11 or later (only if building installers for non-development PCs)
|
||||||
|
|
||||||
### Mac-specific Requirements
|
### Mac-specific Build Requirements
|
||||||
* macOS El Capitan (10.11) or later
|
* macOS Sierra (10.12) or later
|
||||||
* Xcode with High Sierra (10.13) SDK (Mojave SDK not yet supported until Qt 5.12 later this year)
|
* Xcode 10
|
||||||
* [create-dmg](https://github.com/sindresorhus/create-dmg) (only if building DMGs for use on non-development Macs)
|
* [create-dmg](https://github.com/sindresorhus/create-dmg) (only if building DMGs for use on non-development Macs)
|
||||||
|
|
||||||
### Linux-specific Requirements
|
### Linux-specific Build Requirements
|
||||||
* GCC or Clang
|
* GCC or Clang
|
||||||
* Install your distro equivalents of: `openssl-devel qt5-devel SDL2-devel ffmpeg-devel qt5-qtquickcontrols2-devel libva-devel libvdpau-devel opus-devel`
|
* Install your distro equivalents of: `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`
|
||||||
* FFmpeg 4.0 is required to build. If your distro doesn't package FFmpeg 4.0, you can build and install it from source on http://ffmpeg.org/
|
* FFmpeg 4.0 is required to build. If your distro doesn't package FFmpeg 4.0, you can build and install it from source on http://ffmpeg.org/
|
||||||
|
|
||||||
|
### Steam Link-specific Build Requirements
|
||||||
|
* [Steam Link SDK](https://github.com/ValveSoftware/steamlink-sdk) cloned on your build system
|
||||||
|
* STEAMLINK_SDK_PATH environment variable set to the Steam Link SDK path
|
||||||
|
|
||||||
### Build Setup Steps
|
### Build Setup Steps
|
||||||
1. Install the latest Qt SDK (and optionally, the Qt Creator IDE) from https://www.qt.io/download
|
1. Install the latest Qt SDK (and optionally, the Qt Creator IDE) from https://www.qt.io/download
|
||||||
* You may also use Homebrew on macOS or your Linux distro's package manager for the Qt SDK as long as the packages are Qt 5.9 or later.
|
* You can install Qt via Homebrew on macOS, but you will need to use `brew install qt --with-debug` to be able to create debug builds of Moonlight.
|
||||||
|
* You may also use your Linux distro's package manager for the Qt SDK as long as the packages are Qt 5.9 or later.
|
||||||
|
* This step is not required for building on Steam Link, because the Steam Link SDK includes Qt 5.9.
|
||||||
2. Run `git submodule update --init --recursive` from within `moonlight-qt/`
|
2. Run `git submodule update --init --recursive` from within `moonlight-qt/`
|
||||||
3. Open the project in Qt Creator or build from qmake on the command line.
|
3. Open the project in Qt Creator or build from qmake on the command line.
|
||||||
* To build a binary for use on non-development machines, use the scripts in the `scripts` folder.
|
* To build a binary for use on non-development machines, use the scripts in the `scripts` folder.
|
||||||
* For Windows builds, use `scripts\generate-installers.bat`. Execute this script from the root of the repository within a Qt command prompt. Ensure WiX and 7-Zip binary directories are in your `%PATH%`.
|
* For Windows builds, use `scripts\generate-installers.bat`. Execute this script from the root of the repository within a Qt command prompt. Ensure WiX and 7-Zip binary directories are in your `%PATH%`.
|
||||||
* For macOS builds, use `scripts/generate-dmg.sh`. Execute this script from the root of the repository and ensure Qt's `bin` folder is in your `$PATH`.
|
* For 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`
|
||||||
|
|
||||||
## Contribute
|
## Contribute
|
||||||
1. Fork us
|
1. Fork us
|
||||||
2. Write code
|
2. Write code
|
||||||
3. Send Pull Requests
|
3. Send Pull Requests
|
||||||
|
|
||||||
Check out our [website](http://moonlight-stream.com) for project links and information.
|
Check out our [website](https://moonlight-stream.org) for project links and information.
|
||||||
@@ -23,9 +23,9 @@
|
|||||||
<key>NSSupportsAutomaticGraphicsSwitching</key>
|
<key>NSSupportsAutomaticGraphicsSwitching</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>0.6.2</string>
|
<string>VERSION</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>0.6.2</string>
|
<string>VERSION</string>
|
||||||
<key>CFBundleDisplayName</key>
|
<key>CFBundleDisplayName</key>
|
||||||
<string>Moonlight</string>
|
<string>Moonlight</string>
|
||||||
</dict>
|
</dict>
|
||||||
|
|||||||
@@ -8,17 +8,15 @@ unix:!macx {
|
|||||||
TARGET = Moonlight
|
TARGET = Moonlight
|
||||||
}
|
}
|
||||||
|
|
||||||
# Support debug and release builds from command line for CI
|
include(../globaldefs.pri)
|
||||||
CONFIG += debug_and_release
|
|
||||||
|
|
||||||
# Ensure symbols are always generated
|
|
||||||
CONFIG += force_debug_info
|
|
||||||
|
|
||||||
# Precompile QML files to avoid writing qmlcache on portable versions.
|
# Precompile QML files to avoid writing qmlcache on portable versions.
|
||||||
# Since this binds the app against the Qt runtime version, we will only
|
# Since this binds the app against the Qt runtime version, we will only
|
||||||
# do this for Windows and Mac, since they ship with the Qt runtime.
|
# do this for Windows and Mac, since they ship with the Qt runtime.
|
||||||
win32|macx {
|
win32|macx {
|
||||||
CONFIG += qtquickcompiler
|
CONFIG(release, debug|release) {
|
||||||
|
CONFIG += qtquickcompiler
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TEMPLATE = app
|
TEMPLATE = app
|
||||||
@@ -35,32 +33,44 @@ DEFINES += QT_DEPRECATED_WARNINGS
|
|||||||
DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
|
DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
|
||||||
|
|
||||||
win32 {
|
win32 {
|
||||||
INCLUDEPATH += $$PWD/../libs/windows/include
|
INCLUDEPATH += \
|
||||||
|
$$PWD/../libs/windows/include \
|
||||||
|
$$(DXSDK_DIR)/Include
|
||||||
|
|
||||||
contains(QT_ARCH, i386) {
|
contains(QT_ARCH, i386) {
|
||||||
LIBS += -L$$PWD/../libs/windows/lib/x86
|
LIBS += -L$$PWD/../libs/windows/lib/x86
|
||||||
|
LIBS += -L$$(DXSDK_DIR)/Lib/x86
|
||||||
}
|
}
|
||||||
contains(QT_ARCH, x86_64) {
|
contains(QT_ARCH, x86_64) {
|
||||||
LIBS += -L$$PWD/../libs/windows/lib/x64
|
LIBS += -L$$PWD/../libs/windows/lib/x64
|
||||||
|
LIBS += -L$$(DXSDK_DIR)/Lib/x64
|
||||||
}
|
}
|
||||||
|
|
||||||
LIBS += ws2_32.lib winmm.lib dxva2.lib ole32.lib gdi32.lib user32.lib d3d9.lib dwmapi.lib dbghelp.lib
|
LIBS += ws2_32.lib winmm.lib dxva2.lib ole32.lib gdi32.lib user32.lib d3d9.lib dwmapi.lib dbghelp.lib
|
||||||
}
|
}
|
||||||
macx {
|
macx {
|
||||||
INCLUDEPATH += $$PWD/../libs/mac/include $$PWD/../libs/mac/Frameworks/SDL2.framework/Versions/A/Headers
|
INCLUDEPATH += $$PWD/../libs/mac/include
|
||||||
|
INCLUDEPATH += $$PWD/../libs/mac/Frameworks/SDL2.framework/Versions/A/Headers
|
||||||
|
INCLUDEPATH += $$PWD/../libs/mac/Frameworks/SDL2_ttf.framework/Versions/A/Headers
|
||||||
LIBS += -L$$PWD/../libs/mac/lib -F$$PWD/../libs/mac/Frameworks
|
LIBS += -L$$PWD/../libs/mac/lib -F$$PWD/../libs/mac/Frameworks
|
||||||
}
|
}
|
||||||
|
|
||||||
unix:!macx {
|
unix:!macx {
|
||||||
CONFIG += link_pkgconfig
|
CONFIG += link_pkgconfig
|
||||||
PKGCONFIG += openssl sdl2 opus
|
PKGCONFIG += openssl sdl2 SDL2_ttf opus
|
||||||
|
|
||||||
# For libsoundio
|
# SLAudio is used on Steam Link
|
||||||
packagesExist(libpulse) {
|
!config_SL {
|
||||||
PKGCONFIG += libpulse
|
CONFIG += soundio
|
||||||
}
|
}
|
||||||
packagesExist(alsa) {
|
|
||||||
PKGCONFIG += alsa
|
soundio {
|
||||||
|
packagesExist(libpulse) {
|
||||||
|
PKGCONFIG += libpulse
|
||||||
|
}
|
||||||
|
packagesExist(alsa) {
|
||||||
|
PKGCONFIG += alsa
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
packagesExist(libavcodec) {
|
packagesExist(libavcodec) {
|
||||||
@@ -74,37 +84,56 @@ unix:!macx {
|
|||||||
packagesExist(libva-wayland) {
|
packagesExist(libva-wayland) {
|
||||||
CONFIG += libva-wayland
|
CONFIG += libva-wayland
|
||||||
}
|
}
|
||||||
|
packagesExist(libva-drm) {
|
||||||
|
CONFIG += libva-drm
|
||||||
|
}
|
||||||
CONFIG += libva
|
CONFIG += libva
|
||||||
}
|
}
|
||||||
|
|
||||||
packagesExist(vdpau) {
|
packagesExist(vdpau) {
|
||||||
CONFIG += libvdpau
|
CONFIG += libvdpau
|
||||||
}
|
}
|
||||||
|
|
||||||
|
packagesExist(mmal) {
|
||||||
|
PKGCONFIG += mmal
|
||||||
|
CONFIG += mmal
|
||||||
|
}
|
||||||
|
|
||||||
|
packagesExist(libdrm) {
|
||||||
|
PKGCONFIG += libdrm
|
||||||
|
CONFIG += libdrm
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
win32 {
|
win32 {
|
||||||
LIBS += -llibssl -llibcrypto -lSDL2 -lavcodec -lavutil -lopus
|
LIBS += -llibssl -llibcrypto -lSDL2 -lSDL2_ttf -lavcodec -lavutil -lopus -ld3dx9
|
||||||
CONFIG += ffmpeg soundio
|
CONFIG += ffmpeg
|
||||||
|
}
|
||||||
|
win32:!winrt {
|
||||||
|
CONFIG += soundio discord-rpc
|
||||||
}
|
}
|
||||||
macx {
|
macx {
|
||||||
LIBS += -lssl -lcrypto -lavcodec.58 -lavutil.56 -lopus -framework SDL2
|
LIBS += -lssl -lcrypto -lavcodec.58 -lavutil.56 -lopus -framework SDL2 -framework SDL2_ttf
|
||||||
LIBS += -lobjc -framework VideoToolbox -framework AVFoundation -framework CoreVideo -framework CoreGraphics -framework CoreMedia -framework AppKit
|
LIBS += -lobjc -framework VideoToolbox -framework AVFoundation -framework CoreVideo -framework CoreGraphics -framework CoreMedia -framework AppKit -framework Metal
|
||||||
|
|
||||||
# For libsoundio
|
# For libsoundio
|
||||||
LIBS += -framework CoreAudio -framework AudioUnit
|
LIBS += -framework CoreAudio -framework AudioUnit
|
||||||
|
|
||||||
CONFIG += ffmpeg soundio
|
CONFIG += ffmpeg soundio discord-rpc
|
||||||
}
|
}
|
||||||
|
|
||||||
SOURCES += \
|
SOURCES += \
|
||||||
main.cpp \
|
main.cpp \
|
||||||
|
backend/computerseeker.cpp \
|
||||||
backend/identitymanager.cpp \
|
backend/identitymanager.cpp \
|
||||||
backend/nvcomputer.cpp \
|
backend/nvcomputer.cpp \
|
||||||
backend/nvhttp.cpp \
|
backend/nvhttp.cpp \
|
||||||
backend/nvpairingmanager.cpp \
|
backend/nvpairingmanager.cpp \
|
||||||
backend/computermanager.cpp \
|
backend/computermanager.cpp \
|
||||||
backend/boxartmanager.cpp \
|
backend/boxartmanager.cpp \
|
||||||
|
backend/richpresencemanager.cpp \
|
||||||
cli/commandlineparser.cpp \
|
cli/commandlineparser.cpp \
|
||||||
|
cli/quitstream.cpp \
|
||||||
cli/startstream.cpp \
|
cli/startstream.cpp \
|
||||||
settings/streamingpreferences.cpp \
|
settings/streamingpreferences.cpp \
|
||||||
streaming/input.cpp \
|
streaming/input.cpp \
|
||||||
@@ -117,17 +146,22 @@ SOURCES += \
|
|||||||
backend/autoupdatechecker.cpp \
|
backend/autoupdatechecker.cpp \
|
||||||
path.cpp \
|
path.cpp \
|
||||||
settings/mappingmanager.cpp \
|
settings/mappingmanager.cpp \
|
||||||
gui/sdlgamepadkeynavigation.cpp
|
gui/sdlgamepadkeynavigation.cpp \
|
||||||
|
streaming/video/overlaymanager.cpp \
|
||||||
|
backend/systemproperties.cpp
|
||||||
|
|
||||||
HEADERS += \
|
HEADERS += \
|
||||||
utils.h \
|
utils.h \
|
||||||
|
backend/computerseeker.h \
|
||||||
backend/identitymanager.h \
|
backend/identitymanager.h \
|
||||||
backend/nvcomputer.h \
|
backend/nvcomputer.h \
|
||||||
backend/nvhttp.h \
|
backend/nvhttp.h \
|
||||||
backend/nvpairingmanager.h \
|
backend/nvpairingmanager.h \
|
||||||
backend/computermanager.h \
|
backend/computermanager.h \
|
||||||
backend/boxartmanager.h \
|
backend/boxartmanager.h \
|
||||||
|
backend/richpresencemanager.h \
|
||||||
cli/commandlineparser.h \
|
cli/commandlineparser.h \
|
||||||
|
cli/quitstream.h \
|
||||||
cli/startstream.h \
|
cli/startstream.h \
|
||||||
settings/streamingpreferences.h \
|
settings/streamingpreferences.h \
|
||||||
streaming/input.h \
|
streaming/input.h \
|
||||||
@@ -141,7 +175,9 @@ HEADERS += \
|
|||||||
backend/autoupdatechecker.h \
|
backend/autoupdatechecker.h \
|
||||||
path.h \
|
path.h \
|
||||||
settings/mappingmanager.h \
|
settings/mappingmanager.h \
|
||||||
gui/sdlgamepadkeynavigation.h
|
gui/sdlgamepadkeynavigation.h \
|
||||||
|
streaming/video/overlaymanager.h \
|
||||||
|
backend/systemproperties.h
|
||||||
|
|
||||||
# Platform-specific renderers and decoders
|
# Platform-specific renderers and decoders
|
||||||
ffmpeg {
|
ffmpeg {
|
||||||
@@ -151,13 +187,15 @@ ffmpeg {
|
|||||||
SOURCES += \
|
SOURCES += \
|
||||||
streaming/video/ffmpeg.cpp \
|
streaming/video/ffmpeg.cpp \
|
||||||
streaming/video/ffmpeg-renderers/sdlvid.cpp \
|
streaming/video/ffmpeg-renderers/sdlvid.cpp \
|
||||||
|
streaming/video/ffmpeg-renderers/cuda.cpp \
|
||||||
streaming/video/ffmpeg-renderers/pacer/pacer.cpp \
|
streaming/video/ffmpeg-renderers/pacer/pacer.cpp \
|
||||||
streaming/video/ffmpeg-renderers/pacer/nullthreadedvsyncsource.cpp
|
streaming/video/ffmpeg-renderers/pacer/nullthreadedvsyncsource.cpp
|
||||||
|
|
||||||
|
|
||||||
HEADERS += \
|
HEADERS += \
|
||||||
streaming/video/ffmpeg.h \
|
streaming/video/ffmpeg.h \
|
||||||
streaming/video/ffmpeg-renderers/renderer.h \
|
streaming/video/ffmpeg-renderers/renderer.h \
|
||||||
|
streaming/video/ffmpeg-renderers/sdlvid.h \
|
||||||
|
streaming/video/ffmpeg-renderers/cuda.h \
|
||||||
streaming/video/ffmpeg-renderers/pacer/pacer.h \
|
streaming/video/ffmpeg-renderers/pacer/pacer.h \
|
||||||
streaming/video/ffmpeg-renderers/pacer/nullthreadedvsyncsource.h
|
streaming/video/ffmpeg-renderers/pacer/nullthreadedvsyncsource.h
|
||||||
}
|
}
|
||||||
@@ -181,6 +219,12 @@ libva-wayland {
|
|||||||
PKGCONFIG += libva-wayland
|
PKGCONFIG += libva-wayland
|
||||||
DEFINES += HAVE_LIBVA_WAYLAND
|
DEFINES += HAVE_LIBVA_WAYLAND
|
||||||
}
|
}
|
||||||
|
libva-wayland {
|
||||||
|
message(VAAPI DRM support enabled)
|
||||||
|
|
||||||
|
PKGCONFIG += libva-drm
|
||||||
|
DEFINES += HAVE_LIBVA_DRM
|
||||||
|
}
|
||||||
libvdpau {
|
libvdpau {
|
||||||
message(VDPAU renderer selected)
|
message(VDPAU renderer selected)
|
||||||
|
|
||||||
@@ -188,16 +232,34 @@ libvdpau {
|
|||||||
SOURCES += streaming/video/ffmpeg-renderers/vdpau.cpp
|
SOURCES += streaming/video/ffmpeg-renderers/vdpau.cpp
|
||||||
HEADERS += streaming/video/ffmpeg-renderers/vdpau.h
|
HEADERS += streaming/video/ffmpeg-renderers/vdpau.h
|
||||||
}
|
}
|
||||||
config_SLVideo {
|
mmal {
|
||||||
message(SLVideo decoder/renderer selected)
|
message(MMAL renderer selected)
|
||||||
|
|
||||||
DEFINES += HAVE_SLVIDEO
|
DEFINES += HAVE_MMAL
|
||||||
LIBS += -lSLVideo
|
SOURCES += streaming/video/ffmpeg-renderers/mmal.cpp
|
||||||
|
HEADERS += streaming/video/ffmpeg-renderers/mmal.h
|
||||||
SOURCES += streaming/video/sl.cpp
|
|
||||||
HEADERS += streaming/video/sl.h
|
|
||||||
}
|
}
|
||||||
win32 {
|
libdrm {
|
||||||
|
message(DRM renderer selected)
|
||||||
|
|
||||||
|
DEFINES += HAVE_DRM
|
||||||
|
SOURCES += streaming/video/ffmpeg-renderers/drm.cpp
|
||||||
|
HEADERS += streaming/video/ffmpeg-renderers/drm.h
|
||||||
|
}
|
||||||
|
config_SL {
|
||||||
|
message(Steam Link build configuration selected)
|
||||||
|
|
||||||
|
DEFINES += STEAM_LINK HAVE_SLVIDEO HAVE_SLAUDIO
|
||||||
|
LIBS += -lSLVideo -lSLAudio
|
||||||
|
|
||||||
|
SOURCES += \
|
||||||
|
streaming/video/slvid.cpp \
|
||||||
|
streaming/audio/renderers/slaud.cpp
|
||||||
|
HEADERS += \
|
||||||
|
streaming/video/slvid.h \
|
||||||
|
streaming/audio/renderers/slaud.h
|
||||||
|
}
|
||||||
|
win32:!winrt {
|
||||||
message(DXVA2 renderer selected)
|
message(DXVA2 renderer selected)
|
||||||
|
|
||||||
SOURCES += \
|
SOURCES += \
|
||||||
@@ -212,12 +274,10 @@ macx {
|
|||||||
message(VideoToolbox renderer selected)
|
message(VideoToolbox renderer selected)
|
||||||
|
|
||||||
SOURCES += \
|
SOURCES += \
|
||||||
streaming/video/ffmpeg-renderers/vt.mm \
|
streaming/video/ffmpeg-renderers/vt.mm
|
||||||
streaming/video/ffmpeg-renderers/pacer/displaylinkvsyncsource.mm
|
|
||||||
|
|
||||||
HEADERS += \
|
HEADERS += \
|
||||||
streaming/video/ffmpeg-renderers/vt.h \
|
streaming/video/ffmpeg-renderers/vt.h
|
||||||
streaming/video/ffmpeg-renderers/pacer/displaylinkvsyncsource.h
|
|
||||||
}
|
}
|
||||||
soundio {
|
soundio {
|
||||||
message(libsoundio audio renderer selected)
|
message(libsoundio audio renderer selected)
|
||||||
@@ -226,6 +286,12 @@ soundio {
|
|||||||
SOURCES += streaming/audio/renderers/soundioaudiorenderer.cpp
|
SOURCES += streaming/audio/renderers/soundioaudiorenderer.cpp
|
||||||
HEADERS += streaming/audio/renderers/soundioaudiorenderer.h
|
HEADERS += streaming/audio/renderers/soundioaudiorenderer.h
|
||||||
}
|
}
|
||||||
|
discord-rpc {
|
||||||
|
message(Discord integration enabled)
|
||||||
|
|
||||||
|
LIBS += -ldiscord-rpc
|
||||||
|
DEFINES += HAVE_DISCORD
|
||||||
|
}
|
||||||
|
|
||||||
RESOURCES += \
|
RESOURCES += \
|
||||||
resources.qrc \
|
resources.qrc \
|
||||||
@@ -251,12 +317,14 @@ else:unix: LIBS += -L$$OUT_PWD/../qmdnsengine/ -lqmdnsengine
|
|||||||
INCLUDEPATH += $$PWD/../qmdnsengine/qmdnsengine/src/include $$PWD/../qmdnsengine
|
INCLUDEPATH += $$PWD/../qmdnsengine/qmdnsengine/src/include $$PWD/../qmdnsengine
|
||||||
DEPENDPATH += $$PWD/../qmdnsengine/qmdnsengine/src/include $$PWD/../qmdnsengine
|
DEPENDPATH += $$PWD/../qmdnsengine/qmdnsengine/src/include $$PWD/../qmdnsengine
|
||||||
|
|
||||||
win32:CONFIG(release, debug|release): LIBS += -L$$OUT_PWD/../soundio/release/ -lsoundio
|
soundio {
|
||||||
else:win32:CONFIG(debug, debug|release): LIBS += -L$$OUT_PWD/../soundio/debug/ -lsoundio
|
win32:CONFIG(release, debug|release): LIBS += -L$$OUT_PWD/../soundio/release/ -lsoundio
|
||||||
else:unix: LIBS += -L$$OUT_PWD/../soundio/ -lsoundio
|
else:win32:CONFIG(debug, debug|release): LIBS += -L$$OUT_PWD/../soundio/debug/ -lsoundio
|
||||||
|
else:unix: LIBS += -L$$OUT_PWD/../soundio/ -lsoundio
|
||||||
|
|
||||||
INCLUDEPATH += $$PWD/../soundio/libsoundio
|
INCLUDEPATH += $$PWD/../soundio/libsoundio
|
||||||
DEPENDPATH += $$PWD/../soundio/libsoundio
|
DEPENDPATH += $$PWD/../soundio/libsoundio
|
||||||
|
}
|
||||||
|
|
||||||
win32:CONFIG(release, debug|release): LIBS += -L$$OUT_PWD/../h264bitstream/release/ -lh264bitstream
|
win32:CONFIG(release, debug|release): LIBS += -L$$OUT_PWD/../h264bitstream/release/ -lh264bitstream
|
||||||
else:win32:CONFIG(debug, debug|release): LIBS += -L$$OUT_PWD/../h264bitstream/debug/ -lh264bitstream
|
else:win32:CONFIG(debug, debug|release): LIBS += -L$$OUT_PWD/../h264bitstream/debug/ -lh264bitstream
|
||||||
@@ -265,11 +333,13 @@ else:unix: LIBS += -L$$OUT_PWD/../h264bitstream/ -lh264bitstream
|
|||||||
INCLUDEPATH += $$PWD/../h264bitstream/h264bitstream
|
INCLUDEPATH += $$PWD/../h264bitstream/h264bitstream
|
||||||
DEPENDPATH += $$PWD/../h264bitstream/h264bitstream
|
DEPENDPATH += $$PWD/../h264bitstream/h264bitstream
|
||||||
|
|
||||||
win32:CONFIG(release, debug|release): LIBS += -L$$OUT_PWD/../AntiHooking/release/ -lAntiHooking
|
!winrt {
|
||||||
else:win32:CONFIG(debug, debug|release): LIBS += -L$$OUT_PWD/../AntiHooking/debug/ -lAntiHooking
|
win32:CONFIG(release, debug|release): LIBS += -L$$OUT_PWD/../AntiHooking/release/ -lAntiHooking
|
||||||
|
else:win32:CONFIG(debug, debug|release): LIBS += -L$$OUT_PWD/../AntiHooking/debug/ -lAntiHooking
|
||||||
|
|
||||||
INCLUDEPATH += $$PWD/../AntiHooking
|
INCLUDEPATH += $$PWD/../AntiHooking
|
||||||
DEPENDPATH += $$PWD/../AntiHooking
|
DEPENDPATH += $$PWD/../AntiHooking
|
||||||
|
}
|
||||||
|
|
||||||
unix:!macx: {
|
unix:!macx: {
|
||||||
isEmpty(PREFIX) {
|
isEmpty(PREFIX) {
|
||||||
@@ -293,10 +363,7 @@ unix:!macx: {
|
|||||||
appstream.files = deploy/linux/com.moonlight_stream.Moonlight.appdata.xml
|
appstream.files = deploy/linux/com.moonlight_stream.Moonlight.appdata.xml
|
||||||
appstream.path = $$PREFIX/$$DATADIR/metainfo/
|
appstream.path = $$PREFIX/$$DATADIR/metainfo/
|
||||||
|
|
||||||
appdata.files = SDL_GameControllerDB/gamecontrollerdb.txt
|
INSTALLS += target desktop icons appstream
|
||||||
appdata.path = "$$PREFIX/$$DATADIR/Moonlight Game Streaming Project/Moonlight/"
|
|
||||||
|
|
||||||
INSTALLS += target appdata desktop icons appstream
|
|
||||||
}
|
}
|
||||||
win32 {
|
win32 {
|
||||||
RC_ICONS = moonlight.ico
|
RC_ICONS = moonlight.ico
|
||||||
@@ -308,12 +375,22 @@ win32 {
|
|||||||
QMAKE_LFLAGS += /MANIFEST:embed /MANIFESTINPUT:$${PWD}/Moonlight.exe.manifest
|
QMAKE_LFLAGS += /MANIFEST:embed /MANIFESTINPUT:$${PWD}/Moonlight.exe.manifest
|
||||||
}
|
}
|
||||||
macx {
|
macx {
|
||||||
QMAKE_INFO_PLIST = $$PWD/Info.plist
|
# Create Info.plist in object dir with the correct version string
|
||||||
|
system(cp $$PWD/Info.plist $$OUT_PWD/Info.plist)
|
||||||
|
system(sed -i -e 's/VERSION/$$cat(version.txt)/g' $$OUT_PWD/Info.plist)
|
||||||
|
|
||||||
APP_BUNDLE_RESOURCES.files = moonlight.icns SDL_GameControllerDB/gamecontrollerdb.txt
|
QMAKE_INFO_PLIST = $$OUT_PWD/Info.plist
|
||||||
|
|
||||||
|
APP_BUNDLE_RESOURCES.files = moonlight.icns
|
||||||
APP_BUNDLE_RESOURCES.path = Contents/Resources
|
APP_BUNDLE_RESOURCES.path = Contents/Resources
|
||||||
QMAKE_BUNDLE_DATA += APP_BUNDLE_RESOURCES
|
|
||||||
|
APP_BUNDLE_FRAMEWORKS.files = $$files(../libs/mac/Frameworks/*.framework, true) $$files(../libs/mac/lib/*.dylib, true)
|
||||||
|
APP_BUNDLE_FRAMEWORKS.path = Contents/Frameworks
|
||||||
|
|
||||||
|
QMAKE_BUNDLE_DATA += APP_BUNDLE_RESOURCES APP_BUNDLE_FRAMEWORKS
|
||||||
|
|
||||||
|
QMAKE_RPATHDIR += @executable_path/../Frameworks
|
||||||
}
|
}
|
||||||
|
|
||||||
VERSION = 0.6.2
|
VERSION = "$$cat(version.txt)"
|
||||||
DEFINES += VERSION_STR=\\\"0.6.2\\\"
|
DEFINES += VERSION_STR=\\\"$$cat(version.txt)\\\"
|
||||||
@@ -12,6 +12,9 @@ AutoUpdateChecker::AutoUpdateChecker(QObject *parent) :
|
|||||||
// Never communicate over HTTP
|
// Never communicate over HTTP
|
||||||
m_Nam.setStrictTransportSecurityEnabled(true);
|
m_Nam.setStrictTransportSecurityEnabled(true);
|
||||||
|
|
||||||
|
// Allow HTTP redirects
|
||||||
|
m_Nam.setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy);
|
||||||
|
|
||||||
connect(&m_Nam, SIGNAL(finished(QNetworkReply*)),
|
connect(&m_Nam, SIGNAL(finished(QNetworkReply*)),
|
||||||
this, SLOT(handleUpdateCheckRequestFinished(QNetworkReply*)));
|
this, SLOT(handleUpdateCheckRequestFinished(QNetworkReply*)));
|
||||||
|
|
||||||
@@ -25,9 +28,9 @@ AutoUpdateChecker::AutoUpdateChecker(QObject *parent) :
|
|||||||
|
|
||||||
void AutoUpdateChecker::start()
|
void AutoUpdateChecker::start()
|
||||||
{
|
{
|
||||||
#if defined(Q_OS_WIN32) || defined(Q_OS_DARWIN) // Only run update checker on platforms without auto-update
|
#if defined(Q_OS_WIN32) || defined(Q_OS_DARWIN) || defined(STEAM_LINK) // Only run update checker on platforms without auto-update
|
||||||
// We'll get a callback when this is finished
|
// We'll get a callback when this is finished
|
||||||
QUrl url("https://moonlight-stream.com/updates/qt.json");
|
QUrl url("https://moonlight-stream.org/updates/qt.json");
|
||||||
m_Nam.get(QNetworkRequest(url));
|
m_Nam.get(QNetworkRequest(url));
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
@@ -40,6 +43,15 @@ void AutoUpdateChecker::parseStringToVersionQuad(QString& string, QVector<int>&
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QString AutoUpdateChecker::getPlatform()
|
||||||
|
{
|
||||||
|
#ifdef STEAM_LINK
|
||||||
|
return QStringLiteral("steamlink");
|
||||||
|
#else
|
||||||
|
return QSysInfo::productType();
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
void AutoUpdateChecker::handleUpdateCheckRequestFinished(QNetworkReply* reply)
|
void AutoUpdateChecker::handleUpdateCheckRequestFinished(QNetworkReply* reply)
|
||||||
{
|
{
|
||||||
Q_ASSERT(reply->isFinished());
|
Q_ASSERT(reply->isFinished());
|
||||||
@@ -85,7 +97,7 @@ void AutoUpdateChecker::handleUpdateCheckRequestFinished(QNetworkReply* reply)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (updateObj["arch"] == QSysInfo::buildCpuArchitecture() &&
|
if (updateObj["arch"] == QSysInfo::buildCpuArchitecture() &&
|
||||||
updateObj["platform"] == QSysInfo::productType()) {
|
updateObj["platform"] == getPlatform()) {
|
||||||
qDebug() << "Found update manifest match for current platform";
|
qDebug() << "Found update manifest match for current platform";
|
||||||
|
|
||||||
QString latestVersion = updateObj["version"].toString();
|
QString latestVersion = updateObj["version"].toString();
|
||||||
@@ -113,7 +125,8 @@ void AutoUpdateChecker::handleUpdateCheckRequestFinished(QNetworkReply* reply)
|
|||||||
if (currentVer < latestVer) {
|
if (currentVer < latestVer) {
|
||||||
qDebug() << "Update available";
|
qDebug() << "Update available";
|
||||||
|
|
||||||
emit onUpdateAvailable(updateObj["browser_url"].toString());
|
emit onUpdateAvailable(updateObj["version"].toString(),
|
||||||
|
updateObj["browser_url"].toString());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
else if (currentVer > latestVer) {
|
else if (currentVer > latestVer) {
|
||||||
@@ -133,7 +146,7 @@ void AutoUpdateChecker::handleUpdateCheckRequestFinished(QNetworkReply* reply)
|
|||||||
}
|
}
|
||||||
|
|
||||||
qWarning() << "No entry in update manifest found for current platform: "
|
qWarning() << "No entry in update manifest found for current platform: "
|
||||||
<< QSysInfo::buildCpuArchitecture() << QSysInfo::productType();
|
<< QSysInfo::buildCpuArchitecture() << getPlatform();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
qWarning() << "Update checking failed with error: " << reply->error();
|
qWarning() << "Update checking failed with error: " << reply->error();
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ public:
|
|||||||
Q_INVOKABLE void start();
|
Q_INVOKABLE void start();
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void onUpdateAvailable(QString url);
|
void onUpdateAvailable(QString newVersion, QString url);
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void handleUpdateCheckRequestFinished(QNetworkReply* reply);
|
void handleUpdateCheckRequestFinished(QNetworkReply* reply);
|
||||||
@@ -20,6 +20,8 @@ private slots:
|
|||||||
private:
|
private:
|
||||||
void parseStringToVersionQuad(QString& string, QVector<int>& version);
|
void parseStringToVersionQuad(QString& string, QVector<int>& version);
|
||||||
|
|
||||||
|
QString getPlatform();
|
||||||
|
|
||||||
QVector<int> m_CurrentVersionQuad;
|
QVector<int> m_CurrentVersionQuad;
|
||||||
QNetworkAccessManager m_Nam;
|
QNetworkAccessManager m_Nam;
|
||||||
};
|
};
|
||||||
@@ -89,15 +89,14 @@ QUrl BoxArtManager::loadBoxArt(NvComputer* computer, NvApp& app)
|
|||||||
|
|
||||||
void BoxArtManager::handleBoxArtLoadComplete(NvComputer* computer, NvApp app, QUrl image)
|
void BoxArtManager::handleBoxArtLoadComplete(NvComputer* computer, NvApp app, QUrl image)
|
||||||
{
|
{
|
||||||
if (image.isEmpty()) {
|
if (!image.isEmpty()) {
|
||||||
image = QUrl("qrc:/res/no_app_image.png");
|
emit boxArtLoadComplete(computer, app, image);
|
||||||
}
|
}
|
||||||
emit boxArtLoadComplete(computer, app, image);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QUrl BoxArtManager::loadBoxArtFromNetwork(NvComputer* computer, int appId)
|
QUrl BoxArtManager::loadBoxArtFromNetwork(NvComputer* computer, int appId)
|
||||||
{
|
{
|
||||||
NvHTTP http(computer->activeAddress);
|
NvHTTP http(computer->activeAddress, computer->serverCert);
|
||||||
|
|
||||||
QString cachePath = getFilePathForBoxArt(computer, appId);
|
QString cachePath = getFilePathForBoxArt(computer, appId);
|
||||||
QImage image;
|
QImage image;
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
#include <QThread>
|
#include <QThread>
|
||||||
#include <QThreadPool>
|
#include <QThreadPool>
|
||||||
|
#include <QCoreApplication>
|
||||||
|
|
||||||
#define SER_HOSTS "hosts"
|
#define SER_HOSTS "hosts"
|
||||||
|
|
||||||
@@ -27,16 +28,16 @@ public:
|
|||||||
private:
|
private:
|
||||||
bool tryPollComputer(QString address, bool& changed)
|
bool tryPollComputer(QString address, bool& changed)
|
||||||
{
|
{
|
||||||
NvHTTP http(address);
|
NvHTTP http(address, m_Computer->serverCert);
|
||||||
|
|
||||||
QString serverInfo;
|
QString serverInfo;
|
||||||
try {
|
try {
|
||||||
serverInfo = http.getServerInfo(NvHTTP::NvLogLevel::NONE);
|
serverInfo = http.getServerInfo(NvHTTP::NvLogLevel::NVLL_NONE);
|
||||||
} catch (...) {
|
} catch (...) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
NvComputer newState(address, serverInfo);
|
NvComputer newState(address, serverInfo, QSslCertificate());
|
||||||
|
|
||||||
// Ensure the machine that responded is the one we intended to contact
|
// Ensure the machine that responded is the one we intended to contact
|
||||||
if (m_Computer->uuid != newState.uuid) {
|
if (m_Computer->uuid != newState.uuid) {
|
||||||
@@ -52,7 +53,7 @@ private:
|
|||||||
{
|
{
|
||||||
Q_ASSERT(m_Computer->activeAddress != nullptr);
|
Q_ASSERT(m_Computer->activeAddress != nullptr);
|
||||||
|
|
||||||
NvHTTP http(m_Computer->activeAddress);
|
NvHTTP http(m_Computer->activeAddress, m_Computer->serverCert);
|
||||||
|
|
||||||
QVector<NvApp> appList;
|
QVector<NvApp> appList;
|
||||||
|
|
||||||
@@ -130,8 +131,12 @@ private:
|
|||||||
emit computerStateChanged(m_Computer);
|
emit computerStateChanged(m_Computer);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait a bit to poll again
|
// Wait a bit to poll again, but do it in 100 ms chunks
|
||||||
QThread::sleep(3);
|
// so we can be interrupted reasonably quickly.
|
||||||
|
// FIXME: QWaitCondition would be better.
|
||||||
|
for (int i = 0; i < 30 && !isInterruptionRequested(); i++) {
|
||||||
|
QThread::msleep(100);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,6 +162,13 @@ ComputerManager::ComputerManager(QObject *parent)
|
|||||||
m_KnownHosts[computer->uuid] = computer;
|
m_KnownHosts[computer->uuid] = computer;
|
||||||
}
|
}
|
||||||
settings.endArray();
|
settings.endArray();
|
||||||
|
|
||||||
|
// 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()));
|
||||||
}
|
}
|
||||||
|
|
||||||
ComputerManager::~ComputerManager()
|
ComputerManager::~ComputerManager()
|
||||||
@@ -174,38 +186,19 @@ ComputerManager::~ComputerManager()
|
|||||||
delete m_MdnsBrowser;
|
delete m_MdnsBrowser;
|
||||||
m_MdnsBrowser = nullptr;
|
m_MdnsBrowser = nullptr;
|
||||||
|
|
||||||
{
|
// Interrupt polling
|
||||||
QMutableMapIterator<QString, QThread*> i(m_PollThreads);
|
for (ComputerPollingEntry* entry : m_PollEntries) {
|
||||||
|
entry->interrupt();
|
||||||
// Interrupt all polling threads
|
|
||||||
while (i.hasNext()) {
|
|
||||||
i.next();
|
|
||||||
i.value()->requestInterruption();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for all threads to terminate before destroying
|
|
||||||
// the NvComputer objects.
|
|
||||||
i.toFront();
|
|
||||||
while (i.hasNext()) {
|
|
||||||
i.next();
|
|
||||||
|
|
||||||
QThread* thread = i.value();
|
|
||||||
thread->wait();
|
|
||||||
delete thread;
|
|
||||||
|
|
||||||
i.remove();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
// Delete all polling entries (and associated threads)
|
||||||
QMutableMapIterator<QString, NvComputer*> i(m_KnownHosts);
|
for (ComputerPollingEntry* entry : m_PollEntries) {
|
||||||
|
delete entry;
|
||||||
|
}
|
||||||
|
|
||||||
// Destroy all NvComputer objects
|
// Destroy all NvComputer objects now that polling is halted
|
||||||
while (i.hasNext()) {
|
for (NvComputer* computer : m_KnownHosts) {
|
||||||
i.next();
|
delete computer;
|
||||||
delete i.value();
|
|
||||||
i.remove();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -218,11 +211,47 @@ void ComputerManager::saveHosts()
|
|||||||
settings.beginWriteArray(SER_HOSTS);
|
settings.beginWriteArray(SER_HOSTS);
|
||||||
for (int i = 0; i < m_KnownHosts.count(); i++) {
|
for (int i = 0; i < m_KnownHosts.count(); i++) {
|
||||||
settings.setArrayIndex(i);
|
settings.setArrayIndex(i);
|
||||||
m_KnownHosts[m_KnownHosts.keys()[i]]->serialize(settings);
|
m_KnownHosts.value(m_KnownHosts.keys()[i])->serialize(settings);
|
||||||
}
|
}
|
||||||
settings.endArray();
|
settings.endArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QHostAddress ComputerManager::getBestGlobalAddressV6(QVector<QHostAddress> &addresses)
|
||||||
|
{
|
||||||
|
for (const QHostAddress& address : addresses) {
|
||||||
|
if (address.protocol() == QAbstractSocket::IPv6Protocol) {
|
||||||
|
if (address.isInSubnet(QHostAddress("fe80::"), 10)) {
|
||||||
|
// Link-local
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (address.isInSubnet(QHostAddress("fec0::"), 10)) {
|
||||||
|
qInfo() << "Ignoring site-local address:" << address;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (address.isInSubnet(QHostAddress("fc00::"), 7)) {
|
||||||
|
qInfo() << "Ignoring ULA:" << address;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (address.isInSubnet(QHostAddress("2002::"), 16)) {
|
||||||
|
qInfo() << "Ignoring 6to4 address:" << address;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (address.isInSubnet(QHostAddress("2001::"), 32)) {
|
||||||
|
qInfo() << "Ignoring Teredo address:" << address;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return address;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return QHostAddress();
|
||||||
|
}
|
||||||
|
|
||||||
void ComputerManager::startPolling()
|
void ComputerManager::startPolling()
|
||||||
{
|
{
|
||||||
QWriteLocker lock(&m_Lock);
|
QWriteLocker lock(&m_Lock);
|
||||||
@@ -240,9 +269,9 @@ void ComputerManager::startPolling()
|
|||||||
this, [this](const QMdnsEngine::Service& service) {
|
this, [this](const QMdnsEngine::Service& service) {
|
||||||
qInfo() << "Discovered mDNS host:" << service.hostname();
|
qInfo() << "Discovered mDNS host:" << service.hostname();
|
||||||
|
|
||||||
MdnsPendingComputer* pendingComputer = new MdnsPendingComputer(&m_MdnsServer, &m_MdnsCache, service);
|
MdnsPendingComputer* pendingComputer = new MdnsPendingComputer(&m_MdnsServer, service);
|
||||||
connect(pendingComputer, SIGNAL(resolvedv4(MdnsPendingComputer*,QHostAddress)),
|
connect(pendingComputer, &MdnsPendingComputer::resolvedHost,
|
||||||
this, SLOT(handleMdnsServiceResolved(MdnsPendingComputer*,QHostAddress)));
|
this, &ComputerManager::handleMdnsServiceResolved);
|
||||||
m_PendingResolution.append(pendingComputer);
|
m_PendingResolution.append(pendingComputer);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -265,24 +294,58 @@ void ComputerManager::startPollingComputer(NvComputer* computer)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (m_PollThreads.contains(computer->uuid)) {
|
ComputerPollingEntry* pollingEntry;
|
||||||
Q_ASSERT(m_PollThreads[computer->uuid]->isRunning());
|
|
||||||
return;
|
if (!m_PollEntries.contains(computer->uuid)) {
|
||||||
|
pollingEntry = m_PollEntries[computer->uuid] = new ComputerPollingEntry();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
pollingEntry = m_PollEntries[computer->uuid];
|
||||||
}
|
}
|
||||||
|
|
||||||
PcMonitorThread* thread = new PcMonitorThread(computer);
|
if (!pollingEntry->isActive()) {
|
||||||
connect(thread, SIGNAL(computerStateChanged(NvComputer*)),
|
PcMonitorThread* thread = new PcMonitorThread(computer);
|
||||||
this, SLOT(handleComputerStateChanged(NvComputer*)));
|
connect(thread, SIGNAL(computerStateChanged(NvComputer*)),
|
||||||
m_PollThreads[computer->uuid] = thread;
|
this, SLOT(handleComputerStateChanged(NvComputer*)));
|
||||||
thread->start();
|
pollingEntry->setActiveThread(thread);
|
||||||
|
thread->start();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ComputerManager::handleMdnsServiceResolved(MdnsPendingComputer* computer,
|
void ComputerManager::handleMdnsServiceResolved(MdnsPendingComputer* computer,
|
||||||
const QHostAddress& address)
|
QVector<QHostAddress>& addresses)
|
||||||
{
|
{
|
||||||
qInfo() << "Resolved" << computer->hostname() << "to" << address.toString();
|
QHostAddress v6Global = getBestGlobalAddressV6(addresses);
|
||||||
|
bool added = false;
|
||||||
|
|
||||||
addNewHost(address.toString(), true);
|
// Add the host using the IPv4 address
|
||||||
|
for (const QHostAddress& address : addresses) {
|
||||||
|
if (address.protocol() == QAbstractSocket::IPv4Protocol) {
|
||||||
|
// NB: We don't just call addNewHost() here with v6Global because the IPv6
|
||||||
|
// 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);
|
||||||
|
added = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!added) {
|
||||||
|
// If we get here, there wasn't an IPv4 address so we'll do it v6-only
|
||||||
|
for (const QHostAddress& address : addresses) {
|
||||||
|
if (address.protocol() == QAbstractSocket::IPv6Protocol) {
|
||||||
|
// Use a link-local or site-local address for the "local address"
|
||||||
|
if (address.isInSubnet(QHostAddress("fe80::"), 10) ||
|
||||||
|
address.isInSubnet(QHostAddress("fec0::"), 10) ||
|
||||||
|
address.isInSubnet(QHostAddress("fc00::"), 7)) {
|
||||||
|
addNewHost(address.toString(), true, v6Global);
|
||||||
|
added = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
m_PendingResolution.removeOne(computer);
|
m_PendingResolution.removeOne(computer);
|
||||||
computer->deleteLater();
|
computer->deleteLater();
|
||||||
@@ -317,34 +380,23 @@ public:
|
|||||||
|
|
||||||
void run()
|
void run()
|
||||||
{
|
{
|
||||||
QThread* pollingThread;
|
ComputerPollingEntry* pollingEntry;
|
||||||
|
|
||||||
// Only do the minimum amount of work while holding the writer lock.
|
// Only do the minimum amount of work while holding the writer lock.
|
||||||
// We must release it before calling saveHosts().
|
// We must release it before calling saveHosts().
|
||||||
{
|
{
|
||||||
QWriteLocker lock(&m_ComputerManager->m_Lock);
|
QWriteLocker lock(&m_ComputerManager->m_Lock);
|
||||||
|
|
||||||
pollingThread = m_ComputerManager->m_PollThreads[m_Computer->uuid];
|
pollingEntry = m_ComputerManager->m_PollEntries.take(m_Computer->uuid);
|
||||||
|
|
||||||
m_ComputerManager->m_PollThreads.remove(m_Computer->uuid);
|
|
||||||
m_ComputerManager->m_KnownHosts.remove(m_Computer->uuid);
|
m_ComputerManager->m_KnownHosts.remove(m_Computer->uuid);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Persist the new host list
|
// Persist the new host list
|
||||||
m_ComputerManager->saveHosts();
|
m_ComputerManager->saveHosts();
|
||||||
|
|
||||||
// Delete the polling thread
|
// Delete the polling entry first. This will stop all polling threads too.
|
||||||
if (pollingThread != nullptr) {
|
delete pollingEntry;
|
||||||
pollingThread->requestInterruption();
|
|
||||||
|
|
||||||
// We must wait here because we're going to delete computer
|
|
||||||
// and we can't do that out from underneath the poller.
|
|
||||||
pollingThread->wait();
|
|
||||||
|
|
||||||
// The thread is safe to delete now
|
|
||||||
Q_ASSERT(pollingThread->isFinished());
|
|
||||||
delete pollingThread;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Finally, delete the computer itself. This must be done
|
// Finally, delete the computer itself. This must be done
|
||||||
// last because the polling thread might be using it.
|
// last because the polling thread might be using it.
|
||||||
@@ -363,6 +415,17 @@ void ComputerManager::deleteHost(NvComputer* computer)
|
|||||||
QThreadPool::globalInstance()->start(new DeferredHostDeletionTask(this, computer));
|
QThreadPool::globalInstance()->start(new DeferredHostDeletionTask(this, computer));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ComputerManager::handleAboutToQuit()
|
||||||
|
{
|
||||||
|
QWriteLocker lock(&m_Lock);
|
||||||
|
|
||||||
|
// Interrupt polling threads immediately, so they
|
||||||
|
// avoid making additional requests while quitting
|
||||||
|
for (ComputerPollingEntry* entry : m_PollEntries) {
|
||||||
|
entry->interrupt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class PendingPairingTask : public QObject, public QRunnable
|
class PendingPairingTask : public QObject, public QRunnable
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
@@ -385,7 +448,7 @@ private:
|
|||||||
NvPairingManager pairingManager(m_Computer->activeAddress);
|
NvPairingManager pairingManager(m_Computer->activeAddress);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
NvPairingManager::PairState result = pairingManager.pair(m_Computer->appVersion, m_Pin);
|
NvPairingManager::PairState result = pairingManager.pair(m_Computer->appVersion, m_Pin, m_Computer->serverCert);
|
||||||
switch (result)
|
switch (result)
|
||||||
{
|
{
|
||||||
case NvPairingManager::PairState::PIN_WRONG:
|
case NvPairingManager::PairState::PIN_WRONG:
|
||||||
@@ -402,7 +465,7 @@ private:
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} catch (const GfeHttpResponseException& e) {
|
} catch (const GfeHttpResponseException& e) {
|
||||||
emit pairingCompleted(m_Computer, e.toQString());
|
emit pairingCompleted(m_Computer, "GeForce Experience returned error: " + e.toQString());
|
||||||
} catch (const QtNetworkReplyException& e) {
|
} catch (const QtNetworkReplyException& e) {
|
||||||
emit pairingCompleted(m_Computer, e.toQString());
|
emit pairingCompleted(m_Computer, e.toQString());
|
||||||
}
|
}
|
||||||
@@ -438,7 +501,7 @@ signals:
|
|||||||
private:
|
private:
|
||||||
void run()
|
void run()
|
||||||
{
|
{
|
||||||
NvHTTP http(m_Computer->activeAddress);
|
NvHTTP http(m_Computer->activeAddress, m_Computer->serverCert);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (m_Computer->currentGameId != 0) {
|
if (m_Computer->currentGameId != 0) {
|
||||||
@@ -499,20 +562,8 @@ void ComputerManager::stopPollingAsync()
|
|||||||
m_MdnsBrowser = nullptr;
|
m_MdnsBrowser = nullptr;
|
||||||
|
|
||||||
// Interrupt all threads, but don't wait for them to terminate
|
// Interrupt all threads, but don't wait for them to terminate
|
||||||
QMutableMapIterator<QString, QThread*> i(m_PollThreads);
|
for (ComputerPollingEntry* entry : m_PollEntries) {
|
||||||
while (i.hasNext()) {
|
entry->interrupt();
|
||||||
i.next();
|
|
||||||
|
|
||||||
QThread* thread = i.value();
|
|
||||||
|
|
||||||
// Let this thread delete itself when it's finished
|
|
||||||
connect(thread, SIGNAL(finished()),
|
|
||||||
thread, SLOT(deleteLater()));
|
|
||||||
|
|
||||||
// The threads will delete themselves when they terminate,
|
|
||||||
// but we remove them from the polling threads map here.
|
|
||||||
i.value()->requestInterruption();
|
|
||||||
i.remove();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -521,9 +572,10 @@ class PendingAddTask : public QObject, public QRunnable
|
|||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
PendingAddTask(ComputerManager* computerManager, QString address, bool mdns)
|
PendingAddTask(ComputerManager* computerManager, QString address, QHostAddress mdnsIpv6Address, bool mdns)
|
||||||
: m_ComputerManager(computerManager),
|
: m_ComputerManager(computerManager),
|
||||||
m_Address(address),
|
m_Address(address),
|
||||||
|
m_MdnsIpv6Address(mdnsIpv6Address),
|
||||||
m_Mdns(mdns)
|
m_Mdns(mdns)
|
||||||
{
|
{
|
||||||
connect(this, &PendingAddTask::computerAddCompleted,
|
connect(this, &PendingAddTask::computerAddCompleted,
|
||||||
@@ -538,12 +590,8 @@ signals:
|
|||||||
void computerStateChanged(NvComputer* computer);
|
void computerStateChanged(NvComputer* computer);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void run()
|
QString fetchServerInfo(NvHTTP& http)
|
||||||
{
|
{
|
||||||
NvHTTP http(m_Address);
|
|
||||||
|
|
||||||
qInfo() << "Processing new PC at" << m_Address << "from" << (m_Mdns ? "mDNS" : "user");
|
|
||||||
|
|
||||||
QString serverInfo;
|
QString serverInfo;
|
||||||
try {
|
try {
|
||||||
// There's a race condition between GameStream servers reporting presence over
|
// There's a race condition between GameStream servers reporting presence over
|
||||||
@@ -551,12 +599,12 @@ private:
|
|||||||
// around this issue, we will issue the request again after a few seconds if
|
// around this issue, we will issue the request again after a few seconds if
|
||||||
// we see a ServiceUnavailableError error.
|
// we see a ServiceUnavailableError error.
|
||||||
try {
|
try {
|
||||||
serverInfo = http.getServerInfo(NvHTTP::NvLogLevel::VERBOSE);
|
serverInfo = http.getServerInfo(NvHTTP::NVLL_VERBOSE);
|
||||||
} catch (const QtNetworkReplyException& e) {
|
} catch (const QtNetworkReplyException& e) {
|
||||||
if (e.getError() == QNetworkReply::ServiceUnavailableError) {
|
if (e.getError() == QNetworkReply::ServiceUnavailableError) {
|
||||||
qWarning() << "Retrying request in 5 seconds after ServiceUnavailableError";
|
qWarning() << "Retrying request in 5 seconds after ServiceUnavailableError";
|
||||||
QThread::sleep(5);
|
QThread::sleep(5);
|
||||||
serverInfo = http.getServerInfo(NvHTTP::NvLogLevel::VERBOSE);
|
serverInfo = http.getServerInfo(NvHTTP::NVLL_VERBOSE);
|
||||||
qInfo() << "Retry successful";
|
qInfo() << "Retry successful";
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -564,85 +612,142 @@ private:
|
|||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return serverInfo;
|
||||||
} catch (...) {
|
} catch (...) {
|
||||||
if (!m_Mdns) {
|
if (!m_Mdns) {
|
||||||
emit computerAddCompleted(false);
|
emit computerAddCompleted(false);
|
||||||
}
|
}
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void run()
|
||||||
|
{
|
||||||
|
NvHTTP http(m_Address, QSslCertificate());
|
||||||
|
|
||||||
|
qInfo() << "Processing new PC at" << m_Address << "from" << (m_Mdns ? "mDNS" : "user") << m_MdnsIpv6Address;
|
||||||
|
|
||||||
|
// 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());
|
||||||
|
serverInfo = fetchServerInfo(http);
|
||||||
|
}
|
||||||
|
if (serverInfo.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
NvComputer* newComputer = new NvComputer(m_Address, serverInfo);
|
// Create initial newComputer using HTTP serverinfo with no pinned cert
|
||||||
|
NvComputer* newComputer = new NvComputer(http.address(), serverInfo, QSslCertificate());
|
||||||
|
|
||||||
|
// Check if we have a record of this host UUID to pull the pinned cert
|
||||||
|
NvComputer* existingComputer;
|
||||||
|
{
|
||||||
|
QReadLocker lock(&m_ComputerManager->m_Lock);
|
||||||
|
existingComputer = m_ComputerManager->m_KnownHosts.value(newComputer->uuid);
|
||||||
|
if (existingComputer != nullptr) {
|
||||||
|
http.setServerCert(existingComputer->serverCert);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch serverinfo again over HTTPS with the pinned cert
|
||||||
|
if (existingComputer != nullptr) {
|
||||||
|
serverInfo = fetchServerInfo(http);
|
||||||
|
if (serverInfo.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the polled computer with the HTTPS information
|
||||||
|
NvComputer httpsComputer(http.address(), serverInfo, QSslCertificate());
|
||||||
|
newComputer->update(httpsComputer);
|
||||||
|
}
|
||||||
|
|
||||||
// Update addresses depending on the context
|
// Update addresses depending on the context
|
||||||
if (m_Mdns) {
|
if (m_Mdns) {
|
||||||
newComputer->localAddress = m_Address;
|
// Only update local address if we actually reached the PC via this address.
|
||||||
|
// If we reached it via the IPv6 address after the local address failed,
|
||||||
// Get the WAN IP address using STUN if we're on mDNS
|
// don't store the non-working local address.
|
||||||
quint32 addr;
|
if (http.address() == m_Address) {
|
||||||
int err = LiFindExternalAddressIP4("stun.stunprotocol.org", 3478, &addr);
|
newComputer->localAddress = m_Address;
|
||||||
if (err == 0) {
|
|
||||||
newComputer->remoteAddress = QHostAddress(qFromBigEndian(addr)).toString();
|
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
qWarning() << "STUN failed to get WAN address:" << err;
|
// Get the WAN IP address using STUN if we're on mDNS over IPv4
|
||||||
|
if (QHostAddress(newComputer->localAddress).protocol() == QAbstractSocket::IPv4Protocol) {
|
||||||
|
quint32 addr;
|
||||||
|
int err = LiFindExternalAddressIP4("stun.moonlight-stream.org", 3478, &addr);
|
||||||
|
if (err == 0) {
|
||||||
|
newComputer->remoteAddress = QHostAddress(qFromBigEndian(addr)).toString();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
qWarning() << "STUN failed to get WAN address:" << err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!m_MdnsIpv6Address.isNull()) {
|
||||||
|
Q_ASSERT(m_MdnsIpv6Address.protocol() == QAbstractSocket::IPv6Protocol);
|
||||||
|
newComputer->ipv6Address = m_MdnsIpv6Address.toString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
newComputer->manualAddress = m_Address;
|
newComputer->manualAddress = m_Address;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if this PC already exists
|
{
|
||||||
QWriteLocker lock(&m_ComputerManager->m_Lock);
|
// Check if this PC already exists
|
||||||
NvComputer* existingComputer = m_ComputerManager->m_KnownHosts[newComputer->uuid];
|
QWriteLocker lock(&m_ComputerManager->m_Lock);
|
||||||
if (existingComputer != nullptr) {
|
NvComputer* existingComputer = m_ComputerManager->m_KnownHosts.value(newComputer->uuid);
|
||||||
// Fold it into the existing PC
|
if (existingComputer != nullptr) {
|
||||||
bool changed = existingComputer->update(*newComputer);
|
// Fold it into the existing PC
|
||||||
delete newComputer;
|
bool changed = existingComputer->update(*newComputer);
|
||||||
|
delete newComputer;
|
||||||
|
|
||||||
// Drop the lock before notifying
|
// Drop the lock before notifying
|
||||||
lock.unlock();
|
lock.unlock();
|
||||||
|
|
||||||
// For non-mDNS clients, let them know it succeeded
|
// For non-mDNS clients, let them know it succeeded
|
||||||
if (!m_Mdns) {
|
if (!m_Mdns) {
|
||||||
emit computerAddCompleted(true);
|
emit computerAddCompleted(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tell our client if something changed
|
||||||
|
if (changed) {
|
||||||
|
qInfo() << existingComputer->name << "is now at" << existingComputer->activeAddress;
|
||||||
|
emit computerStateChanged(existingComputer);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
// Store this in our active sets
|
||||||
|
m_ComputerManager->m_KnownHosts[newComputer->uuid] = newComputer;
|
||||||
|
|
||||||
// Tell our client if something changed
|
// Start polling if enabled (write lock required)
|
||||||
if (changed) {
|
m_ComputerManager->startPollingComputer(newComputer);
|
||||||
qInfo() << existingComputer->name << "is now at" << existingComputer->activeAddress;
|
|
||||||
emit computerStateChanged(existingComputer);
|
// Drop the lock before notifying
|
||||||
|
lock.unlock();
|
||||||
|
|
||||||
|
// For non-mDNS clients, let them know it succeeded
|
||||||
|
if (!m_Mdns) {
|
||||||
|
emit computerAddCompleted(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tell our client about this new PC
|
||||||
|
emit computerStateChanged(newComputer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
// Store this in our active sets
|
|
||||||
m_ComputerManager->m_KnownHosts[newComputer->uuid] = newComputer;
|
|
||||||
|
|
||||||
// Start polling if enabled (write lock required)
|
|
||||||
m_ComputerManager->startPollingComputer(newComputer);
|
|
||||||
|
|
||||||
// Drop the lock before notifying
|
|
||||||
lock.unlock();
|
|
||||||
|
|
||||||
// For non-mDNS clients, let them know it succeeded
|
|
||||||
if (!m_Mdns) {
|
|
||||||
emit computerAddCompleted(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tell our client about this new PC
|
|
||||||
emit computerStateChanged(newComputer);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ComputerManager* m_ComputerManager;
|
ComputerManager* m_ComputerManager;
|
||||||
QString m_Address;
|
QString m_Address;
|
||||||
|
QHostAddress m_MdnsIpv6Address;
|
||||||
bool m_Mdns;
|
bool m_Mdns;
|
||||||
};
|
};
|
||||||
|
|
||||||
void ComputerManager::addNewHost(QString address, bool mdns)
|
void ComputerManager::addNewHost(QString address, bool mdns, QHostAddress mdnsIpv6Address)
|
||||||
{
|
{
|
||||||
// Punt to a worker thread to avoid stalling the
|
// Punt to a worker thread to avoid stalling the
|
||||||
// UI while waiting for serverinfo query to complete
|
// UI while waiting for serverinfo query to complete
|
||||||
PendingAddTask* addTask = new PendingAddTask(this, address, mdns);
|
PendingAddTask* addTask = new PendingAddTask(this, address, mdnsIpv6Address, mdns);
|
||||||
QThreadPool::globalInstance()->start(addTask);
|
QThreadPool::globalInstance()->start(addTask);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
#include <QReadWriteLock>
|
#include <QReadWriteLock>
|
||||||
#include <QSettings>
|
#include <QSettings>
|
||||||
#include <QRunnable>
|
#include <QRunnable>
|
||||||
|
#include <QTimer>
|
||||||
|
|
||||||
class MdnsPendingComputer : public QObject
|
class MdnsPendingComputer : public QObject
|
||||||
{
|
{
|
||||||
@@ -20,13 +21,18 @@ class MdnsPendingComputer : public QObject
|
|||||||
|
|
||||||
public:
|
public:
|
||||||
explicit MdnsPendingComputer(QMdnsEngine::Server* server,
|
explicit MdnsPendingComputer(QMdnsEngine::Server* server,
|
||||||
QMdnsEngine::Cache* cache,
|
|
||||||
const QMdnsEngine::Service& service)
|
const QMdnsEngine::Service& service)
|
||||||
: m_Hostname(service.hostname()),
|
: m_Hostname(service.hostname()),
|
||||||
m_Resolver(server, m_Hostname, cache)
|
m_Server(server),
|
||||||
|
m_Resolver(nullptr)
|
||||||
{
|
{
|
||||||
connect(&m_Resolver, SIGNAL(resolved(QHostAddress)),
|
// Start resolving
|
||||||
this, SLOT(handleResolved(QHostAddress)));
|
resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual ~MdnsPendingComputer()
|
||||||
|
{
|
||||||
|
delete m_Resolver;
|
||||||
}
|
}
|
||||||
|
|
||||||
QString hostname()
|
QString hostname()
|
||||||
@@ -35,20 +41,115 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void handleResolved(const QHostAddress& address)
|
void handleResolvedTimeout()
|
||||||
{
|
{
|
||||||
if (address.protocol() == QAbstractSocket::IPv4Protocol) {
|
if (m_Addresses.isEmpty()) {
|
||||||
m_Resolver.disconnect();
|
// Try again
|
||||||
emit resolvedv4(this, address);
|
qInfo() << "Resolving" << hostname() << "timed out. Retrying...";
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Q_ASSERT(!m_Addresses.isEmpty());
|
||||||
|
emit resolvedHost(this, m_Addresses);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void handleResolvedAddress(const QHostAddress& address)
|
||||||
|
{
|
||||||
|
qInfo() << "Resolved" << hostname() << "to" << address;
|
||||||
|
m_Addresses.push_back(address);
|
||||||
|
}
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void resolvedv4(MdnsPendingComputer*, const QHostAddress&);
|
void resolvedHost(MdnsPendingComputer*,QVector<QHostAddress>&);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
void resolve()
|
||||||
|
{
|
||||||
|
delete m_Resolver;
|
||||||
|
m_Resolver = new QMdnsEngine::Resolver(m_Server, m_Hostname);
|
||||||
|
connect(m_Resolver, &QMdnsEngine::Resolver::resolved,
|
||||||
|
this, &MdnsPendingComputer::handleResolvedAddress);
|
||||||
|
QTimer::singleShot(2000, this, SLOT(handleResolvedTimeout()));
|
||||||
|
}
|
||||||
|
|
||||||
QByteArray m_Hostname;
|
QByteArray m_Hostname;
|
||||||
QMdnsEngine::Resolver m_Resolver;
|
QMdnsEngine::Server* m_Server;
|
||||||
|
QMdnsEngine::Resolver* m_Resolver;
|
||||||
|
QVector<QHostAddress> m_Addresses;
|
||||||
|
};
|
||||||
|
|
||||||
|
class ComputerPollingEntry
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
ComputerPollingEntry()
|
||||||
|
: m_ActiveThread(nullptr)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual ~ComputerPollingEntry()
|
||||||
|
{
|
||||||
|
interrupt();
|
||||||
|
|
||||||
|
// interrupt() should have taken care of this
|
||||||
|
Q_ASSERT(m_ActiveThread == nullptr);
|
||||||
|
|
||||||
|
for (QThread* thread : m_InactiveList) {
|
||||||
|
thread->wait();
|
||||||
|
delete thread;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isActive()
|
||||||
|
{
|
||||||
|
cleanInactiveList();
|
||||||
|
|
||||||
|
return m_ActiveThread != nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setActiveThread(QThread* thread)
|
||||||
|
{
|
||||||
|
cleanInactiveList();
|
||||||
|
|
||||||
|
Q_ASSERT(!isActive());
|
||||||
|
m_ActiveThread = thread;
|
||||||
|
}
|
||||||
|
|
||||||
|
void interrupt()
|
||||||
|
{
|
||||||
|
cleanInactiveList();
|
||||||
|
|
||||||
|
if (m_ActiveThread != nullptr) {
|
||||||
|
// Interrupt the active thread
|
||||||
|
m_ActiveThread->requestInterruption();
|
||||||
|
|
||||||
|
// Place it on the inactive list awaiting death
|
||||||
|
m_InactiveList.append(m_ActiveThread);
|
||||||
|
|
||||||
|
m_ActiveThread = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
void cleanInactiveList()
|
||||||
|
{
|
||||||
|
QMutableListIterator<QThread*> i(m_InactiveList);
|
||||||
|
|
||||||
|
// Reap any threads that have finished
|
||||||
|
while (i.hasNext()) {
|
||||||
|
i.next();
|
||||||
|
|
||||||
|
QThread* thread = i.value();
|
||||||
|
if (thread->isFinished()) {
|
||||||
|
delete thread;
|
||||||
|
i.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QThread* m_ActiveThread;
|
||||||
|
QList<QThread*> m_InactiveList;
|
||||||
};
|
};
|
||||||
|
|
||||||
class ComputerManager : public QObject
|
class ComputerManager : public QObject
|
||||||
@@ -67,7 +168,7 @@ public:
|
|||||||
|
|
||||||
Q_INVOKABLE void stopPollingAsync();
|
Q_INVOKABLE void stopPollingAsync();
|
||||||
|
|
||||||
Q_INVOKABLE void addNewHost(QString address, bool mdns);
|
Q_INVOKABLE void addNewHost(QString address, bool mdns, QHostAddress mdnsIpv6Address = QHostAddress());
|
||||||
|
|
||||||
void pairHost(NvComputer* computer, QString pin);
|
void pairHost(NvComputer* computer, QString pin);
|
||||||
|
|
||||||
@@ -88,19 +189,23 @@ signals:
|
|||||||
void quitAppCompleted(QVariant error);
|
void quitAppCompleted(QVariant error);
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
|
void handleAboutToQuit();
|
||||||
|
|
||||||
void handleComputerStateChanged(NvComputer* computer);
|
void handleComputerStateChanged(NvComputer* computer);
|
||||||
|
|
||||||
void handleMdnsServiceResolved(MdnsPendingComputer* computer, const QHostAddress& address);
|
void handleMdnsServiceResolved(MdnsPendingComputer* computer, QVector<QHostAddress>& addresses);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void saveHosts();
|
void saveHosts();
|
||||||
|
|
||||||
|
QHostAddress getBestGlobalAddressV6(QVector<QHostAddress>& addresses);
|
||||||
|
|
||||||
void startPollingComputer(NvComputer* computer);
|
void startPollingComputer(NvComputer* computer);
|
||||||
|
|
||||||
int m_PollingRef;
|
int m_PollingRef;
|
||||||
QReadWriteLock m_Lock;
|
QReadWriteLock m_Lock;
|
||||||
QMap<QString, NvComputer*> m_KnownHosts;
|
QMap<QString, NvComputer*> m_KnownHosts;
|
||||||
QMap<QString, QThread*> m_PollThreads;
|
QMap<QString, ComputerPollingEntry*> m_PollEntries;
|
||||||
QMdnsEngine::Server m_MdnsServer;
|
QMdnsEngine::Server m_MdnsServer;
|
||||||
QMdnsEngine::Browser* m_MdnsBrowser;
|
QMdnsEngine::Browser* m_MdnsBrowser;
|
||||||
QMdnsEngine::Cache m_MdnsCache;
|
QMdnsEngine::Cache m_MdnsCache;
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
#include "computerseeker.h"
|
||||||
|
#include "computermanager.h"
|
||||||
|
#include <QTimer>
|
||||||
|
|
||||||
|
ComputerSeeker::ComputerSeeker(ComputerManager *manager, QString computerName, QObject *parent)
|
||||||
|
: QObject(parent), m_ComputerManager(manager), m_ComputerName(computerName),
|
||||||
|
m_TimeoutTimer(new QTimer(this))
|
||||||
|
{
|
||||||
|
m_TimeoutTimer->setSingleShot(true);
|
||||||
|
connect(m_TimeoutTimer, &QTimer::timeout,
|
||||||
|
this, &ComputerSeeker::onTimeout);
|
||||||
|
connect(m_ComputerManager, &ComputerManager::computerStateChanged,
|
||||||
|
this, &ComputerSeeker::onComputerUpdated);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ComputerSeeker::start(int timeout)
|
||||||
|
{
|
||||||
|
m_TimeoutTimer->start(timeout);
|
||||||
|
// Seek desired computer by both connecting to it directly (this may fail
|
||||||
|
// 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->startPolling();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ComputerSeeker::onComputerUpdated(NvComputer *computer)
|
||||||
|
{
|
||||||
|
if (!m_TimeoutTimer->isActive()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (matchComputer(computer) && isOnline(computer)) {
|
||||||
|
m_ComputerManager->stopPollingAsync();
|
||||||
|
m_TimeoutTimer->stop();
|
||||||
|
emit computerFound(computer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ComputerSeeker::matchComputer(NvComputer *computer) const
|
||||||
|
{
|
||||||
|
QString value = m_ComputerName.toLower();
|
||||||
|
|
||||||
|
if (computer->name.toLower() == value || computer->uuid.toLower() == value) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const QString& addr : computer->uniqueAddresses()) {
|
||||||
|
if (addr.toLower() == value) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ComputerSeeker::isOnline(NvComputer *computer) const
|
||||||
|
{
|
||||||
|
return computer->state == NvComputer::CS_ONLINE;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ComputerSeeker::onTimeout()
|
||||||
|
{
|
||||||
|
m_TimeoutTimer->stop();
|
||||||
|
m_ComputerManager->stopPollingAsync();
|
||||||
|
emit errorTimeout();
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
|
||||||
|
class ComputerManager;
|
||||||
|
class NvComputer;
|
||||||
|
class QTimer;
|
||||||
|
|
||||||
|
class ComputerSeeker : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
explicit ComputerSeeker(ComputerManager *manager, QString computerName, QObject *parent = nullptr);
|
||||||
|
|
||||||
|
void start(int timeout);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void computerFound(NvComputer *computer);
|
||||||
|
void errorTimeout();
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void onComputerUpdated(NvComputer *computer);
|
||||||
|
void onTimeout();
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool matchComputer(NvComputer *computer) const;
|
||||||
|
bool isOnline(NvComputer *computer) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
ComputerManager *m_ComputerManager;
|
||||||
|
QString m_ComputerName;
|
||||||
|
QTimer *m_TimeoutTimer;
|
||||||
|
};
|
||||||
@@ -75,7 +75,7 @@ void IdentityManager::createCredentials(QSettings& settings)
|
|||||||
-1, -1, 0);
|
-1, -1, 0);
|
||||||
X509_set_issuer_name(cert, name);
|
X509_set_issuer_name(cert, name);
|
||||||
|
|
||||||
X509_sign(cert, pk, EVP_sha1());
|
X509_sign(cert, pk, EVP_sha256());
|
||||||
|
|
||||||
BIO* biokey = BIO_new(BIO_s_mem());
|
BIO* biokey = BIO_new(BIO_s_mem());
|
||||||
THROW_BAD_ALLOC_IF_NULL(biokey);
|
THROW_BAD_ALLOC_IF_NULL(biokey);
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
#include <QUdpSocket>
|
#include <QUdpSocket>
|
||||||
#include <QHostInfo>
|
#include <QHostInfo>
|
||||||
|
#include <QNetworkInterface>
|
||||||
|
#include <QNetworkProxy>
|
||||||
|
|
||||||
#define SER_NAME "hostname"
|
#define SER_NAME "hostname"
|
||||||
#define SER_UUID "uuid"
|
#define SER_UUID "uuid"
|
||||||
@@ -9,7 +11,9 @@
|
|||||||
#define SER_LOCALADDR "localaddress"
|
#define SER_LOCALADDR "localaddress"
|
||||||
#define SER_REMOTEADDR "remoteaddress"
|
#define SER_REMOTEADDR "remoteaddress"
|
||||||
#define SER_MANUALADDR "manualaddress"
|
#define SER_MANUALADDR "manualaddress"
|
||||||
|
#define SER_IPV6ADDR "ipv6address"
|
||||||
#define SER_APPLIST "apps"
|
#define SER_APPLIST "apps"
|
||||||
|
#define SER_SRVCERT "srvcert"
|
||||||
|
|
||||||
#define SER_APPNAME "name"
|
#define SER_APPNAME "name"
|
||||||
#define SER_APPID "id"
|
#define SER_APPID "id"
|
||||||
@@ -22,7 +26,9 @@ NvComputer::NvComputer(QSettings& settings)
|
|||||||
this->macAddress = settings.value(SER_MAC).toByteArray();
|
this->macAddress = settings.value(SER_MAC).toByteArray();
|
||||||
this->localAddress = settings.value(SER_LOCALADDR).toString();
|
this->localAddress = settings.value(SER_LOCALADDR).toString();
|
||||||
this->remoteAddress = settings.value(SER_REMOTEADDR).toString();
|
this->remoteAddress = settings.value(SER_REMOTEADDR).toString();
|
||||||
|
this->ipv6Address = settings.value(SER_IPV6ADDR).toString();
|
||||||
this->manualAddress = settings.value(SER_MANUALADDR).toString();
|
this->manualAddress = settings.value(SER_MANUALADDR).toString();
|
||||||
|
this->serverCert = QSslCertificate(settings.value(SER_SRVCERT).toByteArray());
|
||||||
|
|
||||||
int appCount = settings.beginReadArray(SER_APPLIST);
|
int appCount = settings.beginReadArray(SER_APPLIST);
|
||||||
for (int i = 0; i < appCount; i++) {
|
for (int i = 0; i < appCount; i++) {
|
||||||
@@ -60,7 +66,9 @@ void NvComputer::serialize(QSettings& settings)
|
|||||||
settings.setValue(SER_MAC, macAddress);
|
settings.setValue(SER_MAC, macAddress);
|
||||||
settings.setValue(SER_LOCALADDR, localAddress);
|
settings.setValue(SER_LOCALADDR, localAddress);
|
||||||
settings.setValue(SER_REMOTEADDR, remoteAddress);
|
settings.setValue(SER_REMOTEADDR, remoteAddress);
|
||||||
|
settings.setValue(SER_IPV6ADDR, ipv6Address);
|
||||||
settings.setValue(SER_MANUALADDR, manualAddress);
|
settings.setValue(SER_MANUALADDR, manualAddress);
|
||||||
|
settings.setValue(SER_SRVCERT, serverCert.toPem());
|
||||||
|
|
||||||
// Avoid deleting an existing applist if we couldn't get one
|
// Avoid deleting an existing applist if we couldn't get one
|
||||||
if (!appList.isEmpty()) {
|
if (!appList.isEmpty()) {
|
||||||
@@ -84,8 +92,10 @@ void NvComputer::sortAppList()
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
NvComputer::NvComputer(QString address, QString serverInfo)
|
NvComputer::NvComputer(QString address, QString serverInfo, QSslCertificate serverCert)
|
||||||
{
|
{
|
||||||
|
this->serverCert = serverCert;
|
||||||
|
|
||||||
this->name = NvHTTP::getXmlString(serverInfo, "hostname");
|
this->name = NvHTTP::getXmlString(serverInfo, "hostname");
|
||||||
if (this->name.isEmpty()) {
|
if (this->name.isEmpty()) {
|
||||||
this->name = "UNKNOWN";
|
this->name = "UNKNOWN";
|
||||||
@@ -123,7 +133,12 @@ NvComputer::NvComputer(QString address, QString serverInfo)
|
|||||||
mode2.width * mode2.height * mode2.refreshRate;
|
mode2.width * mode2.height * mode2.refreshRate;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// We can get an IPv4 loopback address if we're using the GS IPv6 Forwarder
|
||||||
this->localAddress = NvHTTP::getXmlString(serverInfo, "LocalIP");
|
this->localAddress = NvHTTP::getXmlString(serverInfo, "LocalIP");
|
||||||
|
if (this->localAddress.startsWith("127.")) {
|
||||||
|
this->localAddress = QString();
|
||||||
|
}
|
||||||
|
|
||||||
this->remoteAddress = NvHTTP::getXmlString(serverInfo, "ExternalIP");
|
this->remoteAddress = NvHTTP::getXmlString(serverInfo, "ExternalIP");
|
||||||
this->pairState = NvHTTP::getXmlString(serverInfo, "PairStatus") == "1" ?
|
this->pairState = NvHTTP::getXmlString(serverInfo, "PairStatus") == "1" ?
|
||||||
PS_PAIRED : PS_NOT_PAIRED;
|
PS_PAIRED : PS_NOT_PAIRED;
|
||||||
@@ -150,7 +165,7 @@ bool NvComputer::wake()
|
|||||||
|
|
||||||
const quint16 WOL_PORTS[] = {
|
const quint16 WOL_PORTS[] = {
|
||||||
7, 9, // Standard WOL ports
|
7, 9, // Standard WOL ports
|
||||||
47998, 47999, 48000, // Ports opened by GFE
|
47998, 47999, 48000, 48002, 48010, // Ports opened by GFE
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create the WoL payload
|
// Create the WoL payload
|
||||||
@@ -167,6 +182,32 @@ bool NvComputer::wake()
|
|||||||
QVector<QString> addressList = uniqueAddresses();
|
QVector<QString> addressList = uniqueAddresses();
|
||||||
addressList.append("255.255.255.255");
|
addressList.append("255.255.255.255");
|
||||||
|
|
||||||
|
// Try to broadcast on all available NICs
|
||||||
|
for (const QNetworkInterface& nic : QNetworkInterface::allInterfaces()) {
|
||||||
|
// Ensure the interface is up and skip the loopback adapter
|
||||||
|
if ((nic.flags() & QNetworkInterface::IsUp) == 0 ||
|
||||||
|
(nic.flags() & QNetworkInterface::IsLoopBack) != 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
QHostAddress allNodesMulticast("FF02::1");
|
||||||
|
for (const QNetworkAddressEntry& addr : nic.addressEntries()) {
|
||||||
|
// Store the scope ID for this NIC if IPv6 is enabled
|
||||||
|
if (!addr.ip().scopeId().isEmpty()) {
|
||||||
|
allNodesMulticast.setScopeId(addr.ip().scopeId());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip IPv6 which doesn't support broadcast
|
||||||
|
if (!addr.broadcast().isNull()) {
|
||||||
|
addressList.append(addr.broadcast().toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!allNodesMulticast.scopeId().isEmpty()) {
|
||||||
|
addressList.append(allNodesMulticast.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Try all unique address strings or host names
|
// Try all unique address strings or host names
|
||||||
bool success = false;
|
bool success = false;
|
||||||
for (QString& addressString : addressList) {
|
for (QString& addressString : addressList) {
|
||||||
@@ -181,16 +222,14 @@ bool NvComputer::wake()
|
|||||||
for (QHostAddress& address : hostInfo.addresses()) {
|
for (QHostAddress& address : hostInfo.addresses()) {
|
||||||
QUdpSocket sock;
|
QUdpSocket sock;
|
||||||
|
|
||||||
// Bind to any address on the correct protocol
|
// Send to all ports
|
||||||
if (sock.bind(address.protocol() == QUdpSocket::IPv4Protocol ?
|
for (quint16 port : WOL_PORTS) {
|
||||||
QHostAddress::AnyIPv4 : QHostAddress::AnyIPv6)) {
|
if (sock.writeDatagram(wolPayload, address, port)) {
|
||||||
|
qInfo().nospace().noquote() << "Sent WoL packet to " << name << " via " << address.toString() << ":" << port;
|
||||||
// Send to all ports
|
success = true;
|
||||||
for (quint16 port : WOL_PORTS) {
|
}
|
||||||
if (sock.writeDatagram(wolPayload, address, port)) {
|
else {
|
||||||
qInfo().nospace().noquote() << "Send WoL packet to " << name << " via " << address.toString() << ":" << port;
|
qWarning() << "Send failed:" << sock.error();
|
||||||
success = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -199,6 +238,81 @@ bool NvComputer::wake()
|
|||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool NvComputer::isReachableOverVpn()
|
||||||
|
{
|
||||||
|
if (activeAddress.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QTcpSocket s;
|
||||||
|
|
||||||
|
s.setProxy(QNetworkProxy::NoProxy);
|
||||||
|
s.connectToHost(activeAddress, 47984);
|
||||||
|
if (s.waitForConnected(3000)) {
|
||||||
|
Q_ASSERT(!s.localAddress().isNull());
|
||||||
|
|
||||||
|
for (const QNetworkInterface& nic : QNetworkInterface::allInterfaces()) {
|
||||||
|
// Ensure the interface is up
|
||||||
|
if ((nic.flags() & QNetworkInterface::IsUp) == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const QNetworkAddressEntry& addr : nic.addressEntries()) {
|
||||||
|
if (addr.ip() == s.localAddress()) {
|
||||||
|
qInfo() << "Found matching interface:" << nic.humanReadableName() << nic.hardwareAddress() << nic.flags();
|
||||||
|
|
||||||
|
#if QT_VERSION >= QT_VERSION_CHECK(5, 11, 0)
|
||||||
|
qInfo() << "Interface Type:" << nic.type();
|
||||||
|
qInfo() << "Interface MTU:" << nic.maximumTransmissionUnit();
|
||||||
|
|
||||||
|
if (nic.type() == QNetworkInterface::Virtual ||
|
||||||
|
nic.type() == QNetworkInterface::Ppp) {
|
||||||
|
// Treat PPP and virtual interfaces as likely VPNs
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nic.maximumTransmissionUnit() != 0 && nic.maximumTransmissionUnit() < 1500) {
|
||||||
|
// Treat MTUs under 1500 as likely VPNs
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if (nic.flags() & QNetworkInterface::IsPointToPoint) {
|
||||||
|
// Treat point-to-point links as likely VPNs.
|
||||||
|
// This check detects OpenVPN on Unix-like OSes.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nic.hardwareAddress().startsWith("00:FF", Qt::CaseInsensitive)) {
|
||||||
|
// OpenVPN TAP interfaces have a MAC address starting with 00:FF on Windows
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nic.humanReadableName().startsWith("ZeroTier")) {
|
||||||
|
// ZeroTier interfaces always start with "ZeroTier"
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nic.humanReadableName().contains("VPN")) {
|
||||||
|
// This one is just a final heuristic if all else fails
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Didn't meet any of our VPN heuristics
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
qWarning() << "No match found for address:" << s.localAddress();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// If we fail to connect, just pretend that it's not a VPN
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
QVector<QString> NvComputer::uniqueAddresses()
|
QVector<QString> NvComputer::uniqueAddresses()
|
||||||
{
|
{
|
||||||
QVector<QString> uniqueAddressList;
|
QVector<QString> uniqueAddressList;
|
||||||
@@ -207,6 +321,7 @@ QVector<QString> NvComputer::uniqueAddresses()
|
|||||||
uniqueAddressList.append(activeAddress);
|
uniqueAddressList.append(activeAddress);
|
||||||
uniqueAddressList.append(localAddress);
|
uniqueAddressList.append(localAddress);
|
||||||
uniqueAddressList.append(remoteAddress);
|
uniqueAddressList.append(remoteAddress);
|
||||||
|
uniqueAddressList.append(ipv6Address);
|
||||||
uniqueAddressList.append(manualAddress);
|
uniqueAddressList.append(manualAddress);
|
||||||
|
|
||||||
// Prune duplicates (always giving precedence to the first)
|
// Prune duplicates (always giving precedence to the first)
|
||||||
@@ -255,10 +370,18 @@ bool NvComputer::update(NvComputer& that)
|
|||||||
changed = true; \
|
changed = true; \
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#define ASSIGN_IF_CHANGED_AND_NONNULL(field) \
|
||||||
|
if (!that.field.isNull() && \
|
||||||
|
this->field != that.field) { \
|
||||||
|
this->field = that.field; \
|
||||||
|
changed = true; \
|
||||||
|
}
|
||||||
|
|
||||||
ASSIGN_IF_CHANGED(name);
|
ASSIGN_IF_CHANGED(name);
|
||||||
ASSIGN_IF_CHANGED_AND_NONEMPTY(macAddress);
|
ASSIGN_IF_CHANGED_AND_NONEMPTY(macAddress);
|
||||||
ASSIGN_IF_CHANGED_AND_NONEMPTY(localAddress);
|
ASSIGN_IF_CHANGED_AND_NONEMPTY(localAddress);
|
||||||
ASSIGN_IF_CHANGED_AND_NONEMPTY(remoteAddress);
|
ASSIGN_IF_CHANGED_AND_NONEMPTY(remoteAddress);
|
||||||
|
ASSIGN_IF_CHANGED_AND_NONEMPTY(ipv6Address);
|
||||||
ASSIGN_IF_CHANGED_AND_NONEMPTY(manualAddress);
|
ASSIGN_IF_CHANGED_AND_NONEMPTY(manualAddress);
|
||||||
ASSIGN_IF_CHANGED(pairState);
|
ASSIGN_IF_CHANGED(pairState);
|
||||||
ASSIGN_IF_CHANGED(serverCodecModeSupport);
|
ASSIGN_IF_CHANGED(serverCodecModeSupport);
|
||||||
@@ -269,6 +392,7 @@ bool NvComputer::update(NvComputer& that)
|
|||||||
ASSIGN_IF_CHANGED(appVersion);
|
ASSIGN_IF_CHANGED(appVersion);
|
||||||
ASSIGN_IF_CHANGED(maxLumaPixelsHEVC);
|
ASSIGN_IF_CHANGED(maxLumaPixelsHEVC);
|
||||||
ASSIGN_IF_CHANGED(gpuModel);
|
ASSIGN_IF_CHANGED(gpuModel);
|
||||||
|
ASSIGN_IF_CHANGED_AND_NONNULL(serverCert);
|
||||||
ASSIGN_IF_CHANGED_AND_NONEMPTY(appList);
|
ASSIGN_IF_CHANGED_AND_NONEMPTY(appList);
|
||||||
ASSIGN_IF_CHANGED_AND_NONEMPTY(displayModes);
|
ASSIGN_IF_CHANGED_AND_NONEMPTY(displayModes);
|
||||||
return changed;
|
return changed;
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ private:
|
|||||||
bool pendingQuit;
|
bool pendingQuit;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit NvComputer(QString address, QString serverInfo);
|
explicit NvComputer(QString address, QString serverInfo, QSslCertificate serverCert);
|
||||||
|
|
||||||
explicit NvComputer(QSettings& settings);
|
explicit NvComputer(QSettings& settings);
|
||||||
|
|
||||||
@@ -29,6 +29,9 @@ public:
|
|||||||
bool
|
bool
|
||||||
wake();
|
wake();
|
||||||
|
|
||||||
|
bool
|
||||||
|
isReachableOverVpn();
|
||||||
|
|
||||||
QVector<QString>
|
QVector<QString>
|
||||||
uniqueAddresses();
|
uniqueAddresses();
|
||||||
|
|
||||||
@@ -64,10 +67,12 @@ public:
|
|||||||
// Persisted traits
|
// Persisted traits
|
||||||
QString localAddress;
|
QString localAddress;
|
||||||
QString remoteAddress;
|
QString remoteAddress;
|
||||||
|
QString ipv6Address;
|
||||||
QString manualAddress;
|
QString manualAddress;
|
||||||
QByteArray macAddress;
|
QByteArray macAddress;
|
||||||
QString name;
|
QString name;
|
||||||
QString uuid;
|
QString uuid;
|
||||||
|
QSslCertificate serverCert;
|
||||||
QVector<NvApp> appList;
|
QVector<NvApp> appList;
|
||||||
|
|
||||||
// Synchronization
|
// Synchronization
|
||||||
|
|||||||
@@ -10,20 +10,48 @@
|
|||||||
#include <QSslKey>
|
#include <QSslKey>
|
||||||
#include <QImageReader>
|
#include <QImageReader>
|
||||||
#include <QtEndian>
|
#include <QtEndian>
|
||||||
|
#include <QNetworkProxy>
|
||||||
|
|
||||||
#define REQUEST_TIMEOUT_MS 5000
|
#define REQUEST_TIMEOUT_MS 5000
|
||||||
|
#define LAUNCH_TIMEOUT_MS 120000
|
||||||
|
#define RESUME_TIMEOUT_MS 30000
|
||||||
|
#define QUIT_TIMEOUT_MS 30000
|
||||||
|
|
||||||
NvHTTP::NvHTTP(QString address) :
|
NvHTTP::NvHTTP(QString address, QSslCertificate serverCert) :
|
||||||
m_Address(address)
|
m_ServerCert(serverCert)
|
||||||
|
{
|
||||||
|
m_BaseUrlHttp.setScheme("http");
|
||||||
|
m_BaseUrlHttps.setScheme("https");
|
||||||
|
m_BaseUrlHttp.setPort(47989);
|
||||||
|
m_BaseUrlHttps.setPort(47984);
|
||||||
|
|
||||||
|
setAddress(address);
|
||||||
|
|
||||||
|
// Never use a proxy server
|
||||||
|
QNetworkProxy noProxy(QNetworkProxy::NoProxy);
|
||||||
|
m_Nam.setProxy(noProxy);
|
||||||
|
|
||||||
|
connect(&m_Nam, &QNetworkAccessManager::sslErrors, this, &NvHTTP::handleSslErrors);
|
||||||
|
}
|
||||||
|
|
||||||
|
void NvHTTP::setServerCert(QSslCertificate serverCert)
|
||||||
|
{
|
||||||
|
m_ServerCert = serverCert;
|
||||||
|
}
|
||||||
|
|
||||||
|
void NvHTTP::setAddress(QString address)
|
||||||
{
|
{
|
||||||
Q_ASSERT(!address.isEmpty());
|
Q_ASSERT(!address.isEmpty());
|
||||||
|
|
||||||
m_BaseUrlHttp.setScheme("http");
|
m_Address = address;
|
||||||
m_BaseUrlHttps.setScheme("https");
|
|
||||||
m_BaseUrlHttp.setHost(address);
|
m_BaseUrlHttp.setHost(address);
|
||||||
m_BaseUrlHttps.setHost(address);
|
m_BaseUrlHttps.setHost(address);
|
||||||
m_BaseUrlHttp.setPort(47989);
|
}
|
||||||
m_BaseUrlHttps.setPort(47984);
|
|
||||||
|
QString NvHTTP::address()
|
||||||
|
{
|
||||||
|
return m_Address;
|
||||||
}
|
}
|
||||||
|
|
||||||
QVector<int>
|
QVector<int>
|
||||||
@@ -68,36 +96,50 @@ NvHTTP::getServerInfo(NvLogLevel logLevel)
|
|||||||
{
|
{
|
||||||
QString serverInfo;
|
QString serverInfo;
|
||||||
|
|
||||||
try
|
// Check if we have a pinned cert for this host yet
|
||||||
|
if (!m_ServerCert.isNull())
|
||||||
{
|
{
|
||||||
// Always try HTTPS first, since it properly reports
|
try
|
||||||
// pairing status (and a few other attributes).
|
|
||||||
serverInfo = openConnectionToString(m_BaseUrlHttps,
|
|
||||||
"serverinfo",
|
|
||||||
nullptr,
|
|
||||||
true,
|
|
||||||
logLevel);
|
|
||||||
// Throws if the request failed
|
|
||||||
verifyResponseStatus(serverInfo);
|
|
||||||
}
|
|
||||||
catch (const GfeHttpResponseException& e)
|
|
||||||
{
|
|
||||||
if (e.getStatusCode() == 401)
|
|
||||||
{
|
{
|
||||||
// Certificate validation error, fallback to HTTP
|
// Always try HTTPS first, since it properly reports
|
||||||
serverInfo = openConnectionToString(m_BaseUrlHttp,
|
// pairing status (and a few other attributes).
|
||||||
|
serverInfo = openConnectionToString(m_BaseUrlHttps,
|
||||||
"serverinfo",
|
"serverinfo",
|
||||||
nullptr,
|
nullptr,
|
||||||
true,
|
REQUEST_TIMEOUT_MS,
|
||||||
logLevel);
|
logLevel);
|
||||||
|
// Throws if the request failed
|
||||||
verifyResponseStatus(serverInfo);
|
verifyResponseStatus(serverInfo);
|
||||||
}
|
}
|
||||||
else
|
catch (const GfeHttpResponseException& e)
|
||||||
{
|
{
|
||||||
// Rethrow real errors
|
if (e.getStatusCode() == 401)
|
||||||
throw e;
|
{
|
||||||
|
// Certificate validation error, fallback to HTTP
|
||||||
|
serverInfo = openConnectionToString(m_BaseUrlHttp,
|
||||||
|
"serverinfo",
|
||||||
|
nullptr,
|
||||||
|
REQUEST_TIMEOUT_MS,
|
||||||
|
logLevel);
|
||||||
|
verifyResponseStatus(serverInfo);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Rethrow real errors
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Only use HTTP prior to pairing
|
||||||
|
serverInfo = openConnectionToString(m_BaseUrlHttp,
|
||||||
|
"serverinfo",
|
||||||
|
nullptr,
|
||||||
|
REQUEST_TIMEOUT_MS,
|
||||||
|
logLevel);
|
||||||
|
verifyResponseStatus(serverInfo);
|
||||||
|
}
|
||||||
|
|
||||||
return serverInfo;
|
return serverInfo;
|
||||||
}
|
}
|
||||||
@@ -144,7 +186,10 @@ NvHTTP::launchApp(int appId,
|
|||||||
"appid="+QString::number(appId)+
|
"appid="+QString::number(appId)+
|
||||||
"&mode="+QString::number(streamConfig->width)+"x"+
|
"&mode="+QString::number(streamConfig->width)+"x"+
|
||||||
QString::number(streamConfig->height)+"x"+
|
QString::number(streamConfig->height)+"x"+
|
||||||
QString::number(streamConfig->fps)+
|
// Using an FPS value over 60 causes SOPS to default to 720p60,
|
||||||
|
// so force it to 60 when starting. This won't impact our ability
|
||||||
|
// to get > 60 FPS while actually streaming though.
|
||||||
|
QString::number(streamConfig->fps > 60 ? 60 : streamConfig->fps)+
|
||||||
"&additionalStates=1&sops="+QString::number(sops ? 1 : 0)+
|
"&additionalStates=1&sops="+QString::number(sops ? 1 : 0)+
|
||||||
"&rikey="+QByteArray(streamConfig->remoteInputAesKey, sizeof(streamConfig->remoteInputAesKey)).toHex()+
|
"&rikey="+QByteArray(streamConfig->remoteInputAesKey, sizeof(streamConfig->remoteInputAesKey)).toHex()+
|
||||||
"&rikeyid="+QString::number(riKeyId)+
|
"&rikeyid="+QString::number(riKeyId)+
|
||||||
@@ -155,7 +200,7 @@ NvHTTP::launchApp(int appId,
|
|||||||
"&surroundAudioInfo="+getSurroundAudioInfoString(streamConfig->audioConfiguration)+
|
"&surroundAudioInfo="+getSurroundAudioInfoString(streamConfig->audioConfiguration)+
|
||||||
"&remoteControllersBitmap="+QString::number(gamepadMask)+
|
"&remoteControllersBitmap="+QString::number(gamepadMask)+
|
||||||
"&gcmap="+QString::number(gamepadMask),
|
"&gcmap="+QString::number(gamepadMask),
|
||||||
false);
|
LAUNCH_TIMEOUT_MS);
|
||||||
|
|
||||||
// Throws if the request failed
|
// Throws if the request failed
|
||||||
verifyResponseStatus(response);
|
verifyResponseStatus(response);
|
||||||
@@ -175,7 +220,7 @@ NvHTTP::resumeApp(PSTREAM_CONFIGURATION streamConfig)
|
|||||||
"rikey="+QString(QByteArray(streamConfig->remoteInputAesKey, sizeof(streamConfig->remoteInputAesKey)).toHex())+
|
"rikey="+QString(QByteArray(streamConfig->remoteInputAesKey, sizeof(streamConfig->remoteInputAesKey)).toHex())+
|
||||||
"&rikeyid="+QString::number(riKeyId)+
|
"&rikeyid="+QString::number(riKeyId)+
|
||||||
"&surroundAudioInfo="+getSurroundAudioInfoString(streamConfig->audioConfiguration),
|
"&surroundAudioInfo="+getSurroundAudioInfoString(streamConfig->audioConfiguration),
|
||||||
false);
|
RESUME_TIMEOUT_MS);
|
||||||
|
|
||||||
// Throws if the request failed
|
// Throws if the request failed
|
||||||
verifyResponseStatus(response);
|
verifyResponseStatus(response);
|
||||||
@@ -188,14 +233,14 @@ NvHTTP::quitApp()
|
|||||||
openConnectionToString(m_BaseUrlHttps,
|
openConnectionToString(m_BaseUrlHttps,
|
||||||
"cancel",
|
"cancel",
|
||||||
nullptr,
|
nullptr,
|
||||||
false);
|
QUIT_TIMEOUT_MS);
|
||||||
|
|
||||||
// Throws if the request failed
|
// Throws if the request failed
|
||||||
verifyResponseStatus(response);
|
verifyResponseStatus(response);
|
||||||
|
|
||||||
// Newer GFE versions will just return success even if quitting fails
|
// Newer GFE versions will just return success even if quitting fails
|
||||||
// if we're not the original requestor.
|
// if we're not the original requestor.
|
||||||
if (getCurrentGame(getServerInfo(NvHTTP::ERROR)) != 0) {
|
if (getCurrentGame(getServerInfo(NvHTTP::NVLL_ERROR)) != 0) {
|
||||||
// Generate a synthetic GfeResponseException letting the caller know
|
// Generate a synthetic GfeResponseException letting the caller know
|
||||||
// that they can't kill someone else's stream.
|
// that they can't kill someone else's stream.
|
||||||
throw GfeHttpResponseException(599, "");
|
throw GfeHttpResponseException(599, "");
|
||||||
@@ -235,8 +280,8 @@ NvHTTP::getAppList()
|
|||||||
QString appxml = openConnectionToString(m_BaseUrlHttps,
|
QString appxml = openConnectionToString(m_BaseUrlHttps,
|
||||||
"applist",
|
"applist",
|
||||||
nullptr,
|
nullptr,
|
||||||
true,
|
REQUEST_TIMEOUT_MS,
|
||||||
NvLogLevel::ERROR);
|
NvLogLevel::NVLL_ERROR);
|
||||||
verifyResponseStatus(appxml);
|
verifyResponseStatus(appxml);
|
||||||
|
|
||||||
QXmlStreamReader xmlReader(appxml);
|
QXmlStreamReader xmlReader(appxml);
|
||||||
@@ -303,8 +348,8 @@ NvHTTP::getBoxArt(int appId)
|
|||||||
"appasset",
|
"appasset",
|
||||||
"appid="+QString::number(appId)+
|
"appid="+QString::number(appId)+
|
||||||
"&AssetType=2&AssetIdx=0",
|
"&AssetType=2&AssetIdx=0",
|
||||||
true,
|
REQUEST_TIMEOUT_MS,
|
||||||
NvLogLevel::VERBOSE);
|
NvLogLevel::NVLL_VERBOSE);
|
||||||
QImage image = QImageReader(reply).read();
|
QImage image = QImageReader(reply).read();
|
||||||
delete reply;
|
delete reply;
|
||||||
|
|
||||||
@@ -346,14 +391,36 @@ NvHTTP::getXmlString(QString xml,
|
|||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void NvHTTP::handleSslErrors(QNetworkReply* reply, const QList<QSslError>& errors)
|
||||||
|
{
|
||||||
|
bool ignoreErrors = true;
|
||||||
|
|
||||||
|
if (m_ServerCert.isNull()) {
|
||||||
|
// We should never make an HTTPS request without a cert
|
||||||
|
Q_ASSERT(!m_ServerCert.isNull());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto error : errors) {
|
||||||
|
if (m_ServerCert != error.certificate()) {
|
||||||
|
ignoreErrors = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ignoreErrors) {
|
||||||
|
reply->ignoreSslErrors(errors);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
QString
|
QString
|
||||||
NvHTTP::openConnectionToString(QUrl baseUrl,
|
NvHTTP::openConnectionToString(QUrl baseUrl,
|
||||||
QString command,
|
QString command,
|
||||||
QString arguments,
|
QString arguments,
|
||||||
bool enableTimeout,
|
int timeoutMs,
|
||||||
NvLogLevel logLevel)
|
NvLogLevel logLevel)
|
||||||
{
|
{
|
||||||
QNetworkReply* reply = openConnection(baseUrl, command, arguments, enableTimeout, logLevel);
|
QNetworkReply* reply = openConnection(baseUrl, command, arguments, timeoutMs, logLevel);
|
||||||
QString ret;
|
QString ret;
|
||||||
|
|
||||||
QTextStream stream(reply);
|
QTextStream stream(reply);
|
||||||
@@ -368,34 +435,35 @@ QNetworkReply*
|
|||||||
NvHTTP::openConnection(QUrl baseUrl,
|
NvHTTP::openConnection(QUrl baseUrl,
|
||||||
QString command,
|
QString command,
|
||||||
QString arguments,
|
QString arguments,
|
||||||
bool enableTimeout,
|
int timeoutMs,
|
||||||
NvLogLevel logLevel)
|
NvLogLevel logLevel)
|
||||||
{
|
{
|
||||||
// Build a URL for the request
|
// Build a URL for the request
|
||||||
QUrl url(baseUrl);
|
QUrl url(baseUrl);
|
||||||
url.setPath("/" + command);
|
url.setPath("/" + command);
|
||||||
url.setQuery("uniqueid=" + IdentityManager::get()->getUniqueId() +
|
|
||||||
"&uuid=" + QUuid::createUuid().toRfc4122().toHex() +
|
// Use a common UID for Moonlight clients to allow them to quit
|
||||||
|
// games for each other (otherwise GFE gets screwed up and it requires
|
||||||
|
// manual intervention to solve).
|
||||||
|
url.setQuery("uniqueid=0123456789ABCDEF&uuid=" +
|
||||||
|
QUuid::createUuid().toRfc4122().toHex() +
|
||||||
((arguments != nullptr) ? ("&" + arguments) : ""));
|
((arguments != nullptr) ? ("&" + arguments) : ""));
|
||||||
|
|
||||||
QNetworkRequest request = QNetworkRequest(url);
|
QNetworkRequest request(url);
|
||||||
|
|
||||||
// Add our client certificate
|
// Add our client certificate
|
||||||
request.setSslConfiguration(IdentityManager::get()->getSslConfig());
|
request.setSslConfiguration(IdentityManager::get()->getSslConfig());
|
||||||
|
|
||||||
QNetworkReply* reply = m_Nam.get(request);
|
QNetworkReply* reply = m_Nam.get(request);
|
||||||
|
|
||||||
// Ignore self-signed certificate errors (since GFE uses them)
|
|
||||||
reply->ignoreSslErrors();
|
|
||||||
|
|
||||||
// Run the request with a timeout if requested
|
// Run the request with a timeout if requested
|
||||||
QEventLoop loop;
|
QEventLoop loop;
|
||||||
QObject::connect(reply, SIGNAL(finished()), &loop, SLOT(quit()));
|
connect(reply, SIGNAL(finished()), &loop, SLOT(quit()));
|
||||||
if (enableTimeout)
|
connect(QCoreApplication::instance(), SIGNAL(aboutToQuit()), &loop, SLOT(quit()));
|
||||||
{
|
if (timeoutMs) {
|
||||||
QTimer::singleShot(REQUEST_TIMEOUT_MS, &loop, SLOT(quit()));
|
QTimer::singleShot(timeoutMs, &loop, SLOT(quit()));
|
||||||
}
|
}
|
||||||
if (logLevel >= NvLogLevel::VERBOSE) {
|
if (logLevel >= NvLogLevel::NVLL_VERBOSE) {
|
||||||
qInfo() << "Executing request:" << url.toString();
|
qInfo() << "Executing request:" << url.toString();
|
||||||
}
|
}
|
||||||
loop.exec(QEventLoop::ExcludeUserInputEvents);
|
loop.exec(QEventLoop::ExcludeUserInputEvents);
|
||||||
@@ -403,7 +471,7 @@ NvHTTP::openConnection(QUrl baseUrl,
|
|||||||
// Abort the request if it timed out
|
// Abort the request if it timed out
|
||||||
if (!reply->isFinished())
|
if (!reply->isFinished())
|
||||||
{
|
{
|
||||||
if (logLevel >= NvLogLevel::ERROR) {
|
if (logLevel >= NvLogLevel::NVLL_ERROR) {
|
||||||
qWarning() << "Aborting timed out request for" << url.toString();
|
qWarning() << "Aborting timed out request for" << url.toString();
|
||||||
}
|
}
|
||||||
reply->abort();
|
reply->abort();
|
||||||
@@ -416,12 +484,27 @@ NvHTTP::openConnection(QUrl baseUrl,
|
|||||||
// Handle error
|
// Handle error
|
||||||
if (reply->error() != QNetworkReply::NoError)
|
if (reply->error() != QNetworkReply::NoError)
|
||||||
{
|
{
|
||||||
if (logLevel >= NvLogLevel::ERROR) {
|
if (logLevel >= NvLogLevel::NVLL_ERROR) {
|
||||||
qWarning() << command << " request failed with error " << reply->error();
|
qWarning() << command << " request failed with error " << reply->error();
|
||||||
}
|
}
|
||||||
QtNetworkReplyException exception(reply->error(), reply->errorString());
|
|
||||||
delete reply;
|
if (reply->error() == QNetworkReply::SslHandshakeFailedError) {
|
||||||
throw exception;
|
// This will trigger falling back to HTTP for the serverinfo query
|
||||||
|
// then pairing again to get the updated certificate.
|
||||||
|
GfeHttpResponseException exception(401, "Server certificate mismatch");
|
||||||
|
delete reply;
|
||||||
|
throw exception;
|
||||||
|
}
|
||||||
|
else if (reply->error() == QNetworkReply::OperationCanceledError) {
|
||||||
|
QtNetworkReplyException exception(QNetworkReply::TimeoutError, "Request timed out");
|
||||||
|
delete reply;
|
||||||
|
throw exception;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
QtNetworkReplyException exception(reply->error(), reply->errorString());
|
||||||
|
delete reply;
|
||||||
|
throw exception;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return reply;
|
return reply;
|
||||||
|
|||||||
@@ -113,16 +113,18 @@ private:
|
|||||||
QString m_ErrorText;
|
QString m_ErrorText;
|
||||||
};
|
};
|
||||||
|
|
||||||
class NvHTTP
|
class NvHTTP : public QObject
|
||||||
{
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
enum NvLogLevel {
|
enum NvLogLevel {
|
||||||
NONE,
|
NVLL_NONE,
|
||||||
ERROR,
|
NVLL_ERROR,
|
||||||
VERBOSE
|
NVLL_VERBOSE
|
||||||
};
|
};
|
||||||
|
|
||||||
explicit NvHTTP(QString address);
|
explicit NvHTTP(QString address, QSslCertificate serverCert);
|
||||||
|
|
||||||
static
|
static
|
||||||
int
|
int
|
||||||
@@ -149,8 +151,14 @@ public:
|
|||||||
openConnectionToString(QUrl baseUrl,
|
openConnectionToString(QUrl baseUrl,
|
||||||
QString command,
|
QString command,
|
||||||
QString arguments,
|
QString arguments,
|
||||||
bool enableTimeout,
|
int timeoutMs,
|
||||||
NvLogLevel logLevel = NvLogLevel::VERBOSE);
|
NvLogLevel logLevel = NvLogLevel::NVLL_VERBOSE);
|
||||||
|
|
||||||
|
void setServerCert(QSslCertificate serverCert);
|
||||||
|
|
||||||
|
void setAddress(QString address);
|
||||||
|
|
||||||
|
QString address();
|
||||||
|
|
||||||
static
|
static
|
||||||
QVector<int>
|
QVector<int>
|
||||||
@@ -182,13 +190,17 @@ public:
|
|||||||
QUrl m_BaseUrlHttp;
|
QUrl m_BaseUrlHttp;
|
||||||
QUrl m_BaseUrlHttps;
|
QUrl m_BaseUrlHttps;
|
||||||
private:
|
private:
|
||||||
|
void
|
||||||
|
handleSslErrors(QNetworkReply* reply, const QList<QSslError>& errors);
|
||||||
|
|
||||||
QNetworkReply*
|
QNetworkReply*
|
||||||
openConnection(QUrl baseUrl,
|
openConnection(QUrl baseUrl,
|
||||||
QString command,
|
QString command,
|
||||||
QString arguments,
|
QString arguments,
|
||||||
bool enableTimeout,
|
int timeoutMs,
|
||||||
NvLogLevel logLevel);
|
NvLogLevel logLevel);
|
||||||
|
|
||||||
QString m_Address;
|
QString m_Address;
|
||||||
QNetworkAccessManager m_Nam;
|
QNetworkAccessManager m_Nam;
|
||||||
|
QSslCertificate m_ServerCert;
|
||||||
};
|
};
|
||||||
@@ -8,8 +8,10 @@
|
|||||||
#include <openssl/x509.h>
|
#include <openssl/x509.h>
|
||||||
#include <openssl/evp.h>
|
#include <openssl/evp.h>
|
||||||
|
|
||||||
|
#define REQUEST_TIMEOUT_MS 5000
|
||||||
|
|
||||||
NvPairingManager::NvPairingManager(QString address) :
|
NvPairingManager::NvPairingManager(QString address) :
|
||||||
m_Http(address)
|
m_Http(address, QSslCertificate())
|
||||||
{
|
{
|
||||||
QByteArray cert = IdentityManager::get()->getCertificate();
|
QByteArray cert = IdentityManager::get()->getCertificate();
|
||||||
BIO *bio = BIO_new_mem_buf(cert.data(), -1);
|
BIO *bio = BIO_new_mem_buf(cert.data(), -1);
|
||||||
@@ -161,7 +163,7 @@ NvPairingManager::saltPin(QByteArray salt, QString pin)
|
|||||||
}
|
}
|
||||||
|
|
||||||
NvPairingManager::PairState
|
NvPairingManager::PairState
|
||||||
NvPairingManager::pair(QString appVersion, QString pin)
|
NvPairingManager::pair(QString appVersion, QString pin, QSslCertificate& serverCert)
|
||||||
{
|
{
|
||||||
int serverMajorVersion = NvHTTP::parseQuad(appVersion).at(0);
|
int serverMajorVersion = NvHTTP::parseQuad(appVersion).at(0);
|
||||||
qInfo() << "Pairing with server generation:" << serverMajorVersion;
|
qInfo() << "Pairing with server generation:" << serverMajorVersion;
|
||||||
@@ -192,7 +194,7 @@ NvPairingManager::pair(QString appVersion, QString pin)
|
|||||||
"pair",
|
"pair",
|
||||||
"devicename=roth&updateState=1&phrase=getservercert&salt=" +
|
"devicename=roth&updateState=1&phrase=getservercert&salt=" +
|
||||||
salt.toHex() + "&clientcert=" + IdentityManager::get()->getCertificate().toHex(),
|
salt.toHex() + "&clientcert=" + IdentityManager::get()->getCertificate().toHex(),
|
||||||
false);
|
0);
|
||||||
NvHTTP::verifyResponseStatus(getCert);
|
NvHTTP::verifyResponseStatus(getCert);
|
||||||
if (NvHTTP::getXmlString(getCert, "paired") != "1")
|
if (NvHTTP::getXmlString(getCert, "paired") != "1")
|
||||||
{
|
{
|
||||||
@@ -200,26 +202,38 @@ NvPairingManager::pair(QString appVersion, QString pin)
|
|||||||
return PairState::FAILED;
|
return PairState::FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
QByteArray serverCert = NvHTTP::getXmlStringFromHex(getCert, "plaincert");
|
QByteArray serverCertStr = NvHTTP::getXmlStringFromHex(getCert, "plaincert");
|
||||||
if (serverCert == nullptr)
|
if (serverCertStr == nullptr)
|
||||||
{
|
{
|
||||||
qCritical() << "Server likely already pairing";
|
qCritical() << "Server likely already pairing";
|
||||||
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
|
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, REQUEST_TIMEOUT_MS);
|
||||||
return PairState::ALREADY_IN_PROGRESS;
|
return PairState::ALREADY_IN_PROGRESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
serverCert = QSslCertificate(serverCertStr);
|
||||||
|
if (serverCert.isNull()) {
|
||||||
|
Q_ASSERT(!serverCert.isNull());
|
||||||
|
|
||||||
|
qCritical() << "Failed to parse plaincert";
|
||||||
|
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, REQUEST_TIMEOUT_MS);
|
||||||
|
return PairState::FAILED;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pin this cert for TLS
|
||||||
|
m_Http.setServerCert(serverCert);
|
||||||
|
|
||||||
QByteArray randomChallenge = generateRandomBytes(16);
|
QByteArray randomChallenge = generateRandomBytes(16);
|
||||||
QByteArray encryptedChallenge = encrypt(randomChallenge, &encKey);
|
QByteArray encryptedChallenge = encrypt(randomChallenge, &encKey);
|
||||||
QString challengeXml = m_Http.openConnectionToString(m_Http.m_BaseUrlHttp,
|
QString challengeXml = m_Http.openConnectionToString(m_Http.m_BaseUrlHttp,
|
||||||
"pair",
|
"pair",
|
||||||
"devicename=roth&updateState=1&clientchallenge=" +
|
"devicename=roth&updateState=1&clientchallenge=" +
|
||||||
encryptedChallenge.toHex(),
|
encryptedChallenge.toHex(),
|
||||||
true);
|
REQUEST_TIMEOUT_MS);
|
||||||
NvHTTP::verifyResponseStatus(challengeXml);
|
NvHTTP::verifyResponseStatus(challengeXml);
|
||||||
if (NvHTTP::getXmlString(challengeXml, "paired") != "1")
|
if (NvHTTP::getXmlString(challengeXml, "paired") != "1")
|
||||||
{
|
{
|
||||||
qCritical() << "Failed pairing at stage #2";
|
qCritical() << "Failed pairing at stage #2";
|
||||||
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
|
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, REQUEST_TIMEOUT_MS);
|
||||||
return PairState::FAILED;
|
return PairState::FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,17 +256,19 @@ NvPairingManager::pair(QString appVersion, QString pin)
|
|||||||
challengeResponse.append(reinterpret_cast<char*>(asnSignature->data), asnSignature->length);
|
challengeResponse.append(reinterpret_cast<char*>(asnSignature->data), asnSignature->length);
|
||||||
challengeResponse.append(clientSecretData);
|
challengeResponse.append(clientSecretData);
|
||||||
|
|
||||||
QByteArray encryptedChallengeResponseHash = encrypt(QCryptographicHash::hash(challengeResponse, hashAlgo), &encKey);
|
QByteArray paddedHash = QCryptographicHash::hash(challengeResponse, hashAlgo);
|
||||||
|
paddedHash.resize(32);
|
||||||
|
QByteArray encryptedChallengeResponseHash = encrypt(paddedHash, &encKey);
|
||||||
QString respXml = m_Http.openConnectionToString(m_Http.m_BaseUrlHttp,
|
QString respXml = m_Http.openConnectionToString(m_Http.m_BaseUrlHttp,
|
||||||
"pair",
|
"pair",
|
||||||
"devicename=roth&updateState=1&serverchallengeresp=" +
|
"devicename=roth&updateState=1&serverchallengeresp=" +
|
||||||
encryptedChallengeResponseHash.toHex(),
|
encryptedChallengeResponseHash.toHex(),
|
||||||
true);
|
REQUEST_TIMEOUT_MS);
|
||||||
NvHTTP::verifyResponseStatus(respXml);
|
NvHTTP::verifyResponseStatus(respXml);
|
||||||
if (NvHTTP::getXmlString(respXml, "paired") != "1")
|
if (NvHTTP::getXmlString(respXml, "paired") != "1")
|
||||||
{
|
{
|
||||||
qCritical() << "Failed pairing at stage #3";
|
qCritical() << "Failed pairing at stage #3";
|
||||||
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
|
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, REQUEST_TIMEOUT_MS);
|
||||||
return PairState::FAILED;
|
return PairState::FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -262,21 +278,21 @@ NvPairingManager::pair(QString appVersion, QString pin)
|
|||||||
|
|
||||||
if (!verifySignature(serverSecret,
|
if (!verifySignature(serverSecret,
|
||||||
serverSignature,
|
serverSignature,
|
||||||
serverCert))
|
serverCertStr))
|
||||||
{
|
{
|
||||||
qCritical() << "MITM detected";
|
qCritical() << "MITM detected";
|
||||||
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
|
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, REQUEST_TIMEOUT_MS);
|
||||||
return PairState::FAILED;
|
return PairState::FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
QByteArray expectedResponseData;
|
QByteArray expectedResponseData;
|
||||||
expectedResponseData.append(randomChallenge);
|
expectedResponseData.append(randomChallenge);
|
||||||
expectedResponseData.append(getSignatureFromPemCert(serverCert));
|
expectedResponseData.append(getSignatureFromPemCert(serverCertStr));
|
||||||
expectedResponseData.append(serverSecret);
|
expectedResponseData.append(serverSecret);
|
||||||
if (QCryptographicHash::hash(expectedResponseData, hashAlgo) != serverResponse)
|
if (QCryptographicHash::hash(expectedResponseData, hashAlgo) != serverResponse)
|
||||||
{
|
{
|
||||||
qCritical() << "Incorrect PIN";
|
qCritical() << "Incorrect PIN";
|
||||||
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
|
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, REQUEST_TIMEOUT_MS);
|
||||||
return PairState::PIN_WRONG;
|
return PairState::PIN_WRONG;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -288,24 +304,24 @@ NvPairingManager::pair(QString appVersion, QString pin)
|
|||||||
"pair",
|
"pair",
|
||||||
"devicename=roth&updateState=1&clientpairingsecret=" +
|
"devicename=roth&updateState=1&clientpairingsecret=" +
|
||||||
clientPairingSecret.toHex(),
|
clientPairingSecret.toHex(),
|
||||||
true);
|
REQUEST_TIMEOUT_MS);
|
||||||
NvHTTP::verifyResponseStatus(secretRespXml);
|
NvHTTP::verifyResponseStatus(secretRespXml);
|
||||||
if (NvHTTP::getXmlString(secretRespXml, "paired") != "1")
|
if (NvHTTP::getXmlString(secretRespXml, "paired") != "1")
|
||||||
{
|
{
|
||||||
qCritical() << "Failed pairing at stage #4";
|
qCritical() << "Failed pairing at stage #4";
|
||||||
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
|
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, REQUEST_TIMEOUT_MS);
|
||||||
return PairState::FAILED;
|
return PairState::FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
QString pairChallengeXml = m_Http.openConnectionToString(m_Http.m_BaseUrlHttps,
|
QString pairChallengeXml = m_Http.openConnectionToString(m_Http.m_BaseUrlHttps,
|
||||||
"pair",
|
"pair",
|
||||||
"devicename=roth&updateState=1&phrase=pairchallenge",
|
"devicename=roth&updateState=1&phrase=pairchallenge",
|
||||||
true);
|
REQUEST_TIMEOUT_MS);
|
||||||
NvHTTP::verifyResponseStatus(pairChallengeXml);
|
NvHTTP::verifyResponseStatus(pairChallengeXml);
|
||||||
if (NvHTTP::getXmlString(pairChallengeXml, "paired") != "1")
|
if (NvHTTP::getXmlString(pairChallengeXml, "paired") != "1")
|
||||||
{
|
{
|
||||||
qCritical() << "Failed pairing at stage #5";
|
qCritical() << "Failed pairing at stage #5";
|
||||||
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
|
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, REQUEST_TIMEOUT_MS);
|
||||||
return PairState::FAILED;
|
return PairState::FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ public:
|
|||||||
~NvPairingManager();
|
~NvPairingManager();
|
||||||
|
|
||||||
PairState
|
PairState
|
||||||
pair(QString appVersion, QString pin);
|
pair(QString appVersion, QString pin, QSslCertificate& serverCert);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QByteArray
|
QByteArray
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
#include "richpresencemanager.h"
|
||||||
|
|
||||||
|
#include <QDebug>
|
||||||
|
|
||||||
|
RichPresenceManager::RichPresenceManager(StreamingPreferences& prefs, QString gameName)
|
||||||
|
: m_DiscordActive(false)
|
||||||
|
{
|
||||||
|
#ifdef HAVE_DISCORD
|
||||||
|
if (prefs.richPresence) {
|
||||||
|
DiscordEventHandlers handlers = {};
|
||||||
|
handlers.ready = discordReady;
|
||||||
|
handlers.disconnected = discordDisconnected;
|
||||||
|
handlers.errored = discordErrored;
|
||||||
|
Discord_Initialize("594668102021677159", &handlers, 0, nullptr);
|
||||||
|
m_DiscordActive = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_DiscordActive) {
|
||||||
|
QByteArray stateStr = (QString("Streaming ") + gameName).toUtf8();
|
||||||
|
|
||||||
|
DiscordRichPresence discordPresence = {};
|
||||||
|
discordPresence.state = stateStr.data();
|
||||||
|
discordPresence.startTimestamp = time(nullptr);
|
||||||
|
discordPresence.largeImageKey = "icon";
|
||||||
|
Discord_UpdatePresence(&discordPresence);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
RichPresenceManager::~RichPresenceManager()
|
||||||
|
{
|
||||||
|
#ifdef HAVE_DISCORD
|
||||||
|
if (m_DiscordActive) {
|
||||||
|
Discord_ClearPresence();
|
||||||
|
Discord_Shutdown();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void RichPresenceManager::runCallbacks()
|
||||||
|
{
|
||||||
|
#ifdef HAVE_DISCORD
|
||||||
|
if (m_DiscordActive) {
|
||||||
|
Discord_RunCallbacks();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef HAVE_DISCORD
|
||||||
|
void RichPresenceManager::discordReady(const DiscordUser* request)
|
||||||
|
{
|
||||||
|
qInfo() << "Discord integration ready for user:" << request->username;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RichPresenceManager::discordDisconnected(int errorCode, const char *message)
|
||||||
|
{
|
||||||
|
qInfo() << "Discord integration disconnected:" << errorCode << message;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RichPresenceManager::discordErrored(int errorCode, const char *message)
|
||||||
|
{
|
||||||
|
qWarning() << "Discord integration error:" << errorCode << message;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "settings/streamingpreferences.h"
|
||||||
|
|
||||||
|
#ifdef HAVE_DISCORD
|
||||||
|
#include <discord_rpc.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
class RichPresenceManager
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
RichPresenceManager(StreamingPreferences& prefs, QString gameName);
|
||||||
|
~RichPresenceManager();
|
||||||
|
|
||||||
|
void runCallbacks();
|
||||||
|
|
||||||
|
private:
|
||||||
|
#ifdef HAVE_DISCORD
|
||||||
|
static void discordReady(const DiscordUser* request);
|
||||||
|
static void discordDisconnected(int errorCode, const char* message);
|
||||||
|
static void discordErrored(int errorCode, const char* message);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
bool m_DiscordActive;
|
||||||
|
};
|
||||||
|
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
#include "systemproperties.h"
|
||||||
|
|
||||||
|
#include <QGuiApplication>
|
||||||
|
|
||||||
|
#include "streaming/session.h"
|
||||||
|
#include "streaming/streamutils.h"
|
||||||
|
|
||||||
|
SystemProperties::SystemProperties()
|
||||||
|
{
|
||||||
|
isRunningWayland = qgetenv("XDG_SESSION_TYPE") == "wayland";
|
||||||
|
isRunningXWayland = qgetenv("XDG_SESSION_TYPE") == "wayland" && QGuiApplication::platformName() == "xcb";
|
||||||
|
|
||||||
|
#ifdef Q_OS_WIN32
|
||||||
|
isWow64 = QSysInfo::currentCpuArchitecture() != QSysInfo::buildCpuArchitecture();
|
||||||
|
#else
|
||||||
|
isWow64 = false;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef STEAM_LINK
|
||||||
|
hasBrowser = true;
|
||||||
|
#else
|
||||||
|
hasBrowser = false;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef HAVE_DISCORD
|
||||||
|
hasDiscordIntegration = true;
|
||||||
|
#else
|
||||||
|
hasDiscordIntegration = false;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
unmappedGamepads = SdlInputHandler::getUnmappedGamepads();
|
||||||
|
|
||||||
|
// Populate data that requires talking to SDL. We do it all in one shot
|
||||||
|
// and cache the results to speed up future queries on this data.
|
||||||
|
querySdlVideoInfo();
|
||||||
|
|
||||||
|
Q_ASSERT(maximumStreamingFrameRate >= 60);
|
||||||
|
Q_ASSERT(!monitorDesktopResolutions.isEmpty());
|
||||||
|
Q_ASSERT(!monitorNativeResolutions.isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
QRect SystemProperties::getDesktopResolution(int displayIndex)
|
||||||
|
{
|
||||||
|
// Returns default constructed QRect if out of bounds
|
||||||
|
return monitorDesktopResolutions.value(displayIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
QRect SystemProperties::getNativeResolution(int displayIndex)
|
||||||
|
{
|
||||||
|
// Returns default constructed QRect if out of bounds
|
||||||
|
return monitorNativeResolutions.value(displayIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SystemProperties::querySdlVideoInfo()
|
||||||
|
{
|
||||||
|
monitorDesktopResolutions.clear();
|
||||||
|
monitorNativeResolutions.clear();
|
||||||
|
hasHardwareAcceleration = false;
|
||||||
|
|
||||||
|
// Never let the maximum drop below 60 FPS
|
||||||
|
maximumStreamingFrameRate = 60;
|
||||||
|
|
||||||
|
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_DisplayMode bestMode;
|
||||||
|
for (int displayIndex = 0; displayIndex < SDL_GetNumVideoDisplays(); displayIndex++) {
|
||||||
|
SDL_DisplayMode desktopMode;
|
||||||
|
int err;
|
||||||
|
|
||||||
|
err = SDL_GetDesktopDisplayMode(displayIndex, &desktopMode);
|
||||||
|
if (err == 0) {
|
||||||
|
monitorDesktopResolutions.insert(displayIndex, QRect(0, 0, desktopMode.w, desktopMode.h));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"SDL_GetDesktopDisplayMode() failed: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (StreamUtils::getRealDesktopMode(displayIndex, &desktopMode)) {
|
||||||
|
monitorNativeResolutions.insert(displayIndex, QRect(0, 0, desktopMode.w, desktopMode.h));
|
||||||
|
|
||||||
|
// Start at desktop mode and work our way up
|
||||||
|
bestMode = desktopMode;
|
||||||
|
for (int i = 0; i < SDL_GetNumDisplayModes(displayIndex); i++) {
|
||||||
|
SDL_DisplayMode mode;
|
||||||
|
if (SDL_GetDisplayMode(displayIndex, i, &mode) == 0) {
|
||||||
|
if (mode.w == desktopMode.w && mode.h == desktopMode.h) {
|
||||||
|
if (mode.refresh_rate > bestMode.refresh_rate) {
|
||||||
|
bestMode = mode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
maximumStreamingFrameRate = qMax(maximumStreamingFrameRate, bestMode.refresh_rate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_Window* testWindow = SDL_CreateWindow("", 0, 0, 1280, 720, SDL_WINDOW_HIDDEN);
|
||||||
|
if (!testWindow) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Failed to create window for hardware decode test: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasHardwareAcceleration =
|
||||||
|
Session::isHardwareDecodeAvailable(testWindow,
|
||||||
|
StreamingPreferences::VDS_AUTO,
|
||||||
|
VIDEO_FORMAT_H264,
|
||||||
|
1920, 1080, 60);
|
||||||
|
|
||||||
|
SDL_DestroyWindow(testWindow);
|
||||||
|
|
||||||
|
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QRect>
|
||||||
|
|
||||||
|
class SystemProperties : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
SystemProperties();
|
||||||
|
|
||||||
|
Q_PROPERTY(bool hasHardwareAcceleration MEMBER hasHardwareAcceleration CONSTANT)
|
||||||
|
Q_PROPERTY(bool isRunningWayland MEMBER isRunningWayland CONSTANT)
|
||||||
|
Q_PROPERTY(bool isRunningXWayland MEMBER isRunningXWayland CONSTANT)
|
||||||
|
Q_PROPERTY(bool isWow64 MEMBER isWow64 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_INVOKABLE QRect getDesktopResolution(int displayIndex);
|
||||||
|
Q_INVOKABLE QRect getNativeResolution(int displayIndex);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void unmappedGamepadsChanged();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void querySdlVideoInfo();
|
||||||
|
|
||||||
|
bool hasHardwareAcceleration;
|
||||||
|
bool isRunningWayland;
|
||||||
|
bool isRunningXWayland;
|
||||||
|
bool isWow64;
|
||||||
|
bool hasBrowser;
|
||||||
|
bool hasDiscordIntegration;
|
||||||
|
QString unmappedGamepads;
|
||||||
|
int maximumStreamingFrameRate;
|
||||||
|
QList<QRect> monitorDesktopResolutions;
|
||||||
|
QList<QRect> monitorNativeResolutions;
|
||||||
|
};
|
||||||
|
|
||||||
@@ -59,7 +59,7 @@ public:
|
|||||||
|
|
||||||
void showMessage(QString message, MessageType type) const
|
void showMessage(QString message, MessageType type) const
|
||||||
{
|
{
|
||||||
#if defined(Q_OS_WIN)
|
#if defined(Q_OS_WIN32)
|
||||||
UINT flags = MB_OK | MB_TOPMOST | MB_SETFOREGROUND;
|
UINT flags = MB_OK | MB_TOPMOST | MB_SETFOREGROUND;
|
||||||
flags |= (type == Info ? MB_ICONINFORMATION : MB_ICONERROR);
|
flags |= (type == Info ? MB_ICONINFORMATION : MB_ICONERROR);
|
||||||
QString title = "Moonlight";
|
QString title = "Moonlight";
|
||||||
@@ -164,6 +164,7 @@ GlobalCommandLineParser::ParseResult GlobalCommandLineParser::parse(const QStrin
|
|||||||
"Starts Moonlight normally if no arguments are given.\n"
|
"Starts Moonlight normally if no arguments are given.\n"
|
||||||
"\n"
|
"\n"
|
||||||
"Available actions:\n"
|
"Available actions:\n"
|
||||||
|
" quit Quit the currently running app\n"
|
||||||
" stream Start streaming an app\n"
|
" stream Start streaming an app\n"
|
||||||
"\n"
|
"\n"
|
||||||
"See 'moonlight <action> --help' for help of specific action."
|
"See 'moonlight <action> --help' for help of specific action."
|
||||||
@@ -179,6 +180,8 @@ GlobalCommandLineParser::ParseResult GlobalCommandLineParser::parse(const QStrin
|
|||||||
parser.handleHelpAndVersionOptions();
|
parser.handleHelpAndVersionOptions();
|
||||||
parser.handleUnknownOptions();
|
parser.handleUnknownOptions();
|
||||||
return NormalStartRequested;
|
return NormalStartRequested;
|
||||||
|
} else if (action == "quit") {
|
||||||
|
return QuitRequested;
|
||||||
} else if (action == "stream") {
|
} else if (action == "stream") {
|
||||||
return StreamRequested;
|
return StreamRequested;
|
||||||
} else {
|
} else {
|
||||||
@@ -186,6 +189,48 @@ GlobalCommandLineParser::ParseResult GlobalCommandLineParser::parse(const QStrin
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QuitCommandLineParser::QuitCommandLineParser()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
QuitCommandLineParser::~QuitCommandLineParser()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void QuitCommandLineParser::parse(const QStringList &args)
|
||||||
|
{
|
||||||
|
CommandLineParser parser;
|
||||||
|
parser.setupCommonOptions();
|
||||||
|
parser.setApplicationDescription(
|
||||||
|
"\n"
|
||||||
|
"Quit the currently running app on the given host."
|
||||||
|
);
|
||||||
|
parser.addPositionalArgument("quit", "quit running app");
|
||||||
|
parser.addPositionalArgument("host", "Host computer name, UUID, or IP address", "<host>");
|
||||||
|
|
||||||
|
if (!parser.parse(args)) {
|
||||||
|
parser.showError(parser.errorText());
|
||||||
|
}
|
||||||
|
|
||||||
|
parser.handleUnknownOptions();
|
||||||
|
|
||||||
|
// This method will not return and terminates the process if --version or
|
||||||
|
// --help is specified
|
||||||
|
parser.handleHelpAndVersionOptions();
|
||||||
|
|
||||||
|
// Verify that host has been provided
|
||||||
|
auto posArgs = parser.positionalArguments();
|
||||||
|
if (posArgs.length() < 2) {
|
||||||
|
parser.showError("Host not provided");
|
||||||
|
}
|
||||||
|
m_Host = parser.positionalArguments().at(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString QuitCommandLineParser::getHost() const
|
||||||
|
{
|
||||||
|
return m_Host;
|
||||||
|
}
|
||||||
|
|
||||||
StreamCommandLineParser::StreamCommandLineParser()
|
StreamCommandLineParser::StreamCommandLineParser()
|
||||||
{
|
{
|
||||||
m_WindowModeMap = {
|
m_WindowModeMap = {
|
||||||
@@ -238,9 +283,11 @@ void StreamCommandLineParser::parse(const QStringList &args, StreamingPreference
|
|||||||
parser.addChoiceOption("display-mode", "display mode", m_WindowModeMap.keys());
|
parser.addChoiceOption("display-mode", "display mode", m_WindowModeMap.keys());
|
||||||
parser.addChoiceOption("audio-config", "audio config", m_AudioConfigMap.keys());
|
parser.addChoiceOption("audio-config", "audio config", m_AudioConfigMap.keys());
|
||||||
parser.addToggleOption("multi-controller", "multiple controller support");
|
parser.addToggleOption("multi-controller", "multiple controller support");
|
||||||
|
parser.addToggleOption("quit-after", "quit app after session");
|
||||||
parser.addToggleOption("mouse-acceleration", "mouse acceleration");
|
parser.addToggleOption("mouse-acceleration", "mouse acceleration");
|
||||||
parser.addToggleOption("game-optimization", "game optimizations");
|
parser.addToggleOption("game-optimization", "game optimizations");
|
||||||
parser.addToggleOption("audio-on-host", "audio on host PC");
|
parser.addToggleOption("audio-on-host", "audio on host PC");
|
||||||
|
parser.addToggleOption("frame-pacing", "frame pacing");
|
||||||
parser.addChoiceOption("video-codec", "video codec", m_VideoCodecMap.keys());
|
parser.addChoiceOption("video-codec", "video codec", m_VideoCodecMap.keys());
|
||||||
parser.addChoiceOption("video-decoder", "video decoder", m_VideoDecoderMap.keys());
|
parser.addChoiceOption("video-decoder", "video decoder", m_VideoDecoderMap.keys());
|
||||||
|
|
||||||
@@ -314,6 +361,9 @@ void StreamCommandLineParser::parse(const QStringList &args, StreamingPreference
|
|||||||
// Resolve --multi-controller and --no-multi-controller options
|
// Resolve --multi-controller and --no-multi-controller options
|
||||||
preferences->multiController = parser.getToggleOptionValue("multi-controller", preferences->multiController);
|
preferences->multiController = parser.getToggleOptionValue("multi-controller", preferences->multiController);
|
||||||
|
|
||||||
|
// Resolve --quit-after and --no-quit-after options
|
||||||
|
preferences->quitAppAfter = parser.getToggleOptionValue("quit-after", preferences->quitAppAfter);
|
||||||
|
|
||||||
// Resolve --mouse-acceleration and --no-mouse-acceleration options
|
// Resolve --mouse-acceleration and --no-mouse-acceleration options
|
||||||
preferences->mouseAcceleration = parser.getToggleOptionValue("mouse-acceleration", preferences->mouseAcceleration);
|
preferences->mouseAcceleration = parser.getToggleOptionValue("mouse-acceleration", preferences->mouseAcceleration);
|
||||||
|
|
||||||
@@ -323,6 +373,9 @@ void StreamCommandLineParser::parse(const QStringList &args, StreamingPreference
|
|||||||
// Resolve --audio-on-host and --no-audio-on-host options
|
// Resolve --audio-on-host and --no-audio-on-host options
|
||||||
preferences->playAudioOnHost = parser.getToggleOptionValue("audio-on-host", preferences->playAudioOnHost);
|
preferences->playAudioOnHost = parser.getToggleOptionValue("audio-on-host", preferences->playAudioOnHost);
|
||||||
|
|
||||||
|
// Resolve --frame-pacing and --no-frame-pacing options
|
||||||
|
preferences->framePacing = parser.getToggleOptionValue("frame-pacing", preferences->framePacing);
|
||||||
|
|
||||||
// Resolve --video-codec option
|
// Resolve --video-codec option
|
||||||
if (parser.isSet("video-codec")) {
|
if (parser.isSet("video-codec")) {
|
||||||
preferences->videoCodecConfig = mapValue(m_VideoCodecMap, parser.getChoiceOptionValue("video-codec"));
|
preferences->videoCodecConfig = mapValue(m_VideoCodecMap, parser.getChoiceOptionValue("video-codec"));
|
||||||
@@ -359,4 +412,3 @@ QString StreamCommandLineParser::getAppName() const
|
|||||||
{
|
{
|
||||||
return m_AppName;
|
return m_AppName;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -11,6 +11,7 @@ public:
|
|||||||
enum ParseResult {
|
enum ParseResult {
|
||||||
NormalStartRequested,
|
NormalStartRequested,
|
||||||
StreamRequested,
|
StreamRequested,
|
||||||
|
QuitRequested,
|
||||||
};
|
};
|
||||||
|
|
||||||
GlobalCommandLineParser();
|
GlobalCommandLineParser();
|
||||||
@@ -20,6 +21,20 @@ public:
|
|||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
class QuitCommandLineParser
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
QuitCommandLineParser();
|
||||||
|
virtual ~QuitCommandLineParser();
|
||||||
|
|
||||||
|
void parse(const QStringList &args);
|
||||||
|
|
||||||
|
QString getHost() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
QString m_Host;
|
||||||
|
};
|
||||||
|
|
||||||
class StreamCommandLineParser
|
class StreamCommandLineParser
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
|||||||
@@ -0,0 +1,171 @@
|
|||||||
|
#include "quitstream.h"
|
||||||
|
|
||||||
|
#include "backend/computermanager.h"
|
||||||
|
#include "backend/computerseeker.h"
|
||||||
|
#include "streaming/session.h"
|
||||||
|
|
||||||
|
#include <QCoreApplication>
|
||||||
|
#include <QTimer>
|
||||||
|
|
||||||
|
#define COMPUTER_SEEK_TIMEOUT 10000
|
||||||
|
|
||||||
|
namespace CliQuitStream
|
||||||
|
{
|
||||||
|
|
||||||
|
enum State {
|
||||||
|
StateInit,
|
||||||
|
StateSeekComputer,
|
||||||
|
StateQuitApp,
|
||||||
|
StateFailure,
|
||||||
|
};
|
||||||
|
|
||||||
|
class Event
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
enum Type {
|
||||||
|
AppQuitCompleted,
|
||||||
|
ComputerFound,
|
||||||
|
ComputerSeekTimedout,
|
||||||
|
Executed,
|
||||||
|
};
|
||||||
|
|
||||||
|
Event(Type type)
|
||||||
|
: type(type), computerManager(nullptr), computer(nullptr) {}
|
||||||
|
|
||||||
|
Type type;
|
||||||
|
ComputerManager *computerManager;
|
||||||
|
NvComputer *computer;
|
||||||
|
QString errorMessage;
|
||||||
|
};
|
||||||
|
|
||||||
|
class LauncherPrivate
|
||||||
|
{
|
||||||
|
Q_DECLARE_PUBLIC(Launcher)
|
||||||
|
|
||||||
|
public:
|
||||||
|
LauncherPrivate(Launcher *q) : q_ptr(q) {}
|
||||||
|
|
||||||
|
void handleEvent(Event event)
|
||||||
|
{
|
||||||
|
Q_Q(Launcher);
|
||||||
|
NvApp app;
|
||||||
|
|
||||||
|
switch (event.type) {
|
||||||
|
// Occurs when CliQuitStreamSegue becomes visible and the UI calls launcher's execute()
|
||||||
|
case Event::Executed:
|
||||||
|
if (m_State == StateInit) {
|
||||||
|
m_State = StateSeekComputer;
|
||||||
|
m_ComputerManager = event.computerManager;
|
||||||
|
q->connect(m_ComputerManager, &ComputerManager::quitAppCompleted,
|
||||||
|
q, &Launcher::onQuitAppCompleted);
|
||||||
|
|
||||||
|
m_ComputerSeeker = new ComputerSeeker(m_ComputerManager, m_ComputerName, q);
|
||||||
|
q->connect(m_ComputerSeeker, &ComputerSeeker::computerFound,
|
||||||
|
q, &Launcher::onComputerFound);
|
||||||
|
q->connect(m_ComputerSeeker, &ComputerSeeker::errorTimeout,
|
||||||
|
q, &Launcher::onComputerSeekTimeout);
|
||||||
|
m_ComputerSeeker->start(COMPUTER_SEEK_TIMEOUT);
|
||||||
|
|
||||||
|
emit q->searchingComputer();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
// Occurs when computer search timed out
|
||||||
|
case Event::ComputerSeekTimedout:
|
||||||
|
if (m_State == StateSeekComputer) {
|
||||||
|
m_State = StateFailure;
|
||||||
|
emit q->failed(QString("Failed to connect to %1").arg(m_ComputerName));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
// Occurs when searched computer is found
|
||||||
|
case Event::ComputerFound:
|
||||||
|
if (m_State == StateSeekComputer) {
|
||||||
|
if (event.computer->pairState == NvComputer::PS_PAIRED) {
|
||||||
|
m_State = StateQuitApp;
|
||||||
|
emit q->quittingApp();
|
||||||
|
m_ComputerManager->quitRunningApp(event.computer);
|
||||||
|
} else {
|
||||||
|
m_State = StateFailure;
|
||||||
|
QString msg = QString("Computer %1 has not been paired. "
|
||||||
|
"Please open Moonlight to pair before streaming.")
|
||||||
|
.arg(event.computer->name);
|
||||||
|
emit q->failed(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
// Occurs when app quit completed (error message is set if failed)
|
||||||
|
case Event::AppQuitCompleted:
|
||||||
|
if (m_State == StateQuitApp) {
|
||||||
|
if (event.errorMessage.isEmpty()) {
|
||||||
|
QCoreApplication::exit(0);
|
||||||
|
} else {
|
||||||
|
m_State = StateFailure;
|
||||||
|
emit q->failed(QString("Quitting app failed, reason: %1").arg(event.errorMessage));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Launcher *q_ptr;
|
||||||
|
ComputerManager *m_ComputerManager;
|
||||||
|
QString m_ComputerName;
|
||||||
|
ComputerSeeker *m_ComputerSeeker;
|
||||||
|
State m_State;
|
||||||
|
QTimer *m_TimeoutTimer;
|
||||||
|
};
|
||||||
|
|
||||||
|
Launcher::Launcher(QString computer, QObject *parent)
|
||||||
|
: QObject(parent),
|
||||||
|
m_DPtr(new LauncherPrivate(this))
|
||||||
|
{
|
||||||
|
Q_D(Launcher);
|
||||||
|
d->m_ComputerName = computer;
|
||||||
|
d->m_State = StateInit;
|
||||||
|
d->m_TimeoutTimer = new QTimer(this);
|
||||||
|
d->m_TimeoutTimer->setSingleShot(true);
|
||||||
|
connect(d->m_TimeoutTimer, &QTimer::timeout,
|
||||||
|
this, &Launcher::onComputerSeekTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
Launcher::~Launcher()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void Launcher::execute(ComputerManager *manager)
|
||||||
|
{
|
||||||
|
Q_D(Launcher);
|
||||||
|
Event event(Event::Executed);
|
||||||
|
event.computerManager = manager;
|
||||||
|
d->handleEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Launcher::isExecuted() const
|
||||||
|
{
|
||||||
|
Q_D(const Launcher);
|
||||||
|
return d->m_State != StateInit;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Launcher::onComputerFound(NvComputer *computer)
|
||||||
|
{
|
||||||
|
Q_D(Launcher);
|
||||||
|
Event event(Event::ComputerFound);
|
||||||
|
event.computer = computer;
|
||||||
|
d->handleEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Launcher::onComputerSeekTimeout()
|
||||||
|
{
|
||||||
|
Q_D(Launcher);
|
||||||
|
Event event(Event::ComputerSeekTimedout);
|
||||||
|
d->handleEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Launcher::onQuitAppCompleted(QVariant error)
|
||||||
|
{
|
||||||
|
Q_D(Launcher);
|
||||||
|
Event event(Event::AppQuitCompleted);
|
||||||
|
event.errorMessage = error.toString();
|
||||||
|
d->handleEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QVariant>
|
||||||
|
|
||||||
|
class ComputerManager;
|
||||||
|
class NvComputer;
|
||||||
|
|
||||||
|
namespace CliQuitStream
|
||||||
|
{
|
||||||
|
|
||||||
|
class LauncherPrivate;
|
||||||
|
|
||||||
|
class Launcher : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
Q_DECLARE_PRIVATE_D(m_DPtr, Launcher)
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit Launcher(QString computer, QObject *parent = nullptr);
|
||||||
|
~Launcher();
|
||||||
|
|
||||||
|
Q_INVOKABLE void execute(ComputerManager *manager);
|
||||||
|
Q_INVOKABLE bool isExecuted() const;
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void searchingComputer();
|
||||||
|
void quittingApp();
|
||||||
|
void failed(QString text);
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void onComputerFound(NvComputer *computer);
|
||||||
|
void onComputerSeekTimeout();
|
||||||
|
void onQuitAppCompleted(QVariant error);
|
||||||
|
|
||||||
|
private:
|
||||||
|
QScopedPointer<LauncherPrivate> m_DPtr;
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
#include "startstream.h"
|
#include "startstream.h"
|
||||||
#include "backend/computermanager.h"
|
#include "backend/computermanager.h"
|
||||||
|
#include "backend/computerseeker.h"
|
||||||
#include "streaming/session.h"
|
#include "streaming/session.h"
|
||||||
|
|
||||||
|
#include <QCoreApplication>
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
|
|
||||||
#define COMPUTER_SEEK_TIMEOUT 10000
|
#define COMPUTER_SEEK_TIMEOUT 10000
|
||||||
@@ -22,6 +24,9 @@ class Event
|
|||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
enum Type {
|
enum Type {
|
||||||
|
AppQuitCompleted,
|
||||||
|
AppQuitRequested,
|
||||||
|
ComputerFound,
|
||||||
ComputerUpdated,
|
ComputerUpdated,
|
||||||
Executed,
|
Executed,
|
||||||
Timedout,
|
Timedout,
|
||||||
@@ -33,6 +38,7 @@ public:
|
|||||||
Type type;
|
Type type;
|
||||||
ComputerManager *computerManager;
|
ComputerManager *computerManager;
|
||||||
NvComputer *computer;
|
NvComputer *computer;
|
||||||
|
QString errorMessage;
|
||||||
};
|
};
|
||||||
|
|
||||||
class LauncherPrivate
|
class LauncherPrivate
|
||||||
@@ -49,58 +55,78 @@ public:
|
|||||||
NvApp app;
|
NvApp app;
|
||||||
|
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
|
// Occurs when CliStartStreamSegue becomes visible and the UI calls launcher's execute()
|
||||||
case Event::Executed:
|
case Event::Executed:
|
||||||
if (m_State == StateInit) {
|
if (m_State == StateInit) {
|
||||||
m_State = StateSeekComputer;
|
m_State = StateSeekComputer;
|
||||||
m_TimeoutTimer->start(COMPUTER_SEEK_TIMEOUT);
|
|
||||||
m_ComputerManager = event.computerManager;
|
m_ComputerManager = event.computerManager;
|
||||||
|
|
||||||
|
m_ComputerSeeker = new ComputerSeeker(m_ComputerManager, m_ComputerName, q);
|
||||||
|
q->connect(m_ComputerSeeker, &ComputerSeeker::computerFound,
|
||||||
|
q, &Launcher::onComputerFound);
|
||||||
|
q->connect(m_ComputerSeeker, &ComputerSeeker::errorTimeout,
|
||||||
|
q, &Launcher::onTimeout);
|
||||||
|
m_ComputerSeeker->start(COMPUTER_SEEK_TIMEOUT);
|
||||||
|
|
||||||
q->connect(m_ComputerManager, &ComputerManager::computerStateChanged,
|
q->connect(m_ComputerManager, &ComputerManager::computerStateChanged,
|
||||||
q, &Launcher::onComputerUpdated);
|
q, &Launcher::onComputerUpdated);
|
||||||
// Seek desired computer by both connecting to it directly (this may fail
|
q->connect(m_ComputerManager, &ComputerManager::quitAppCompleted,
|
||||||
// if m_ComputerName is UUID, or the name that doesn't resolve to an IP
|
q, &Launcher::onQuitAppCompleted);
|
||||||
// address) and by polling it using mDNS, hopefully one of these methods
|
|
||||||
// would find the host
|
|
||||||
m_ComputerManager->addNewHost(m_ComputerName, false);
|
|
||||||
m_ComputerManager->startPolling();
|
|
||||||
emit q->searchingComputer();
|
emit q->searchingComputer();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case Event::ComputerUpdated:
|
// Occurs when searched computer is found
|
||||||
|
case Event::ComputerFound:
|
||||||
if (m_State == StateSeekComputer) {
|
if (m_State == StateSeekComputer) {
|
||||||
if (matchComputer(event.computer) && isOnline(event.computer)) {
|
if (event.computer->pairState == NvComputer::PS_PAIRED) {
|
||||||
if (isPaired(event.computer)) {
|
m_State = StateSeekApp;
|
||||||
m_State = StateSeekApp;
|
m_Computer = event.computer;
|
||||||
m_TimeoutTimer->start(APP_SEEK_TIMEOUT);
|
m_TimeoutTimer->start(APP_SEEK_TIMEOUT);
|
||||||
m_Computer = event.computer;
|
emit q->searchingApp();
|
||||||
m_ComputerManager->stopPollingAsync();
|
} else {
|
||||||
emit q->searchingApp();
|
m_State = StateFailure;
|
||||||
} else {
|
QString msg = QString("Computer %1 has not been paired. "
|
||||||
m_State = StateFailure;
|
"Please open Moonlight to pair before streaming.")
|
||||||
QString msg = QString("Computer %1 has not been paired. "
|
.arg(event.computer->name);
|
||||||
"Please open Moonlight to pair before streaming.")
|
emit q->failed(msg);
|
||||||
.arg(event.computer->name);
|
|
||||||
emit q->failed(msg);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
|
// Occurs when a computer is updated
|
||||||
|
case Event::ComputerUpdated:
|
||||||
if (m_State == StateSeekApp) {
|
if (m_State == StateSeekApp) {
|
||||||
int index = getAppIndex();
|
int index = getAppIndex();
|
||||||
if (-1 != index) {
|
if (-1 != index) {
|
||||||
app = m_Computer->appList[index];
|
app = m_Computer->appList[index];
|
||||||
|
m_TimeoutTimer->stop();
|
||||||
if (isNotStreaming() || isStreamingApp(app)) {
|
if (isNotStreaming() || isStreamingApp(app)) {
|
||||||
m_State = StateStartSession;
|
m_State = StateStartSession;
|
||||||
m_TimeoutTimer->stop();
|
|
||||||
session = new Session(m_Computer, app, m_Preferences);
|
session = new Session(m_Computer, app, m_Preferences);
|
||||||
emit q->sessionCreated(app.name, session);
|
emit q->sessionCreated(app.name, session);
|
||||||
} else {
|
} else {
|
||||||
m_State = StateFailure;
|
emit q->appQuitRequired(getCurrentAppName());
|
||||||
QString msg = QString("%1 is already running. Please quit %1 to stream %2.")
|
|
||||||
.arg(getCurrentAppName(), app.name);
|
|
||||||
emit q->failed(msg);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
// Occurs when there was another app running on computer and user accepted quit
|
||||||
|
// confirmation dialog
|
||||||
|
case Event::AppQuitRequested:
|
||||||
|
if (m_State == StateSeekApp) {
|
||||||
|
m_ComputerManager->quitRunningApp(m_Computer);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
// Occurs when the previous app quit has been completed, handles quitting errors if any
|
||||||
|
// happended. ComputerUpdated event's handler handles session start when previous app has
|
||||||
|
// quit.
|
||||||
|
case Event::AppQuitCompleted:
|
||||||
|
if (m_State == StateSeekApp && !event.errorMessage.isEmpty()) {
|
||||||
|
m_State = StateFailure;
|
||||||
|
emit q->failed(QString("Quitting app failed, reason: %1").arg(event.errorMessage));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
// Occurs when computer or app search timed out
|
||||||
case Event::Timedout:
|
case Event::Timedout:
|
||||||
if (m_State == StateSeekComputer) {
|
if (m_State == StateSeekComputer) {
|
||||||
m_State = StateFailure;
|
m_State = StateFailure;
|
||||||
@@ -114,26 +140,6 @@ public:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool matchComputer(NvComputer *computer) const
|
|
||||||
{
|
|
||||||
QString value = m_ComputerName.toLower();
|
|
||||||
return computer->name.toLower() == value ||
|
|
||||||
computer->localAddress.toLower() == value ||
|
|
||||||
computer->remoteAddress.toLower() == value ||
|
|
||||||
computer->manualAddress.toLower() == value ||
|
|
||||||
computer->uuid.toLower() == value;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool isOnline(NvComputer *computer) const
|
|
||||||
{
|
|
||||||
return computer->state == NvComputer::CS_ONLINE;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool isPaired(NvComputer *computer) const
|
|
||||||
{
|
|
||||||
return computer->pairState == NvComputer::PS_PAIRED;
|
|
||||||
}
|
|
||||||
|
|
||||||
int getAppIndex() const
|
int getAppIndex() const
|
||||||
{
|
{
|
||||||
for (int i = 0; i < m_Computer->appList.length(); i++) {
|
for (int i = 0; i < m_Computer->appList.length(); i++) {
|
||||||
@@ -169,6 +175,7 @@ public:
|
|||||||
QString m_AppName;
|
QString m_AppName;
|
||||||
StreamingPreferences *m_Preferences;
|
StreamingPreferences *m_Preferences;
|
||||||
ComputerManager *m_ComputerManager;
|
ComputerManager *m_ComputerManager;
|
||||||
|
ComputerSeeker *m_ComputerSeeker;
|
||||||
NvComputer *m_Computer;
|
NvComputer *m_Computer;
|
||||||
State m_State;
|
State m_State;
|
||||||
QTimer *m_TimeoutTimer;
|
QTimer *m_TimeoutTimer;
|
||||||
@@ -202,6 +209,27 @@ void Launcher::execute(ComputerManager *manager)
|
|||||||
d->handleEvent(event);
|
d->handleEvent(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Launcher::quitRunningApp()
|
||||||
|
{
|
||||||
|
Q_D(Launcher);
|
||||||
|
Event event(Event::AppQuitRequested);
|
||||||
|
d->handleEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Launcher::isExecuted() const
|
||||||
|
{
|
||||||
|
Q_D(const Launcher);
|
||||||
|
return d->m_State != StateInit;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Launcher::onComputerFound(NvComputer *computer)
|
||||||
|
{
|
||||||
|
Q_D(Launcher);
|
||||||
|
Event event(Event::ComputerFound);
|
||||||
|
event.computer = computer;
|
||||||
|
d->handleEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
void Launcher::onComputerUpdated(NvComputer *computer)
|
void Launcher::onComputerUpdated(NvComputer *computer)
|
||||||
{
|
{
|
||||||
Q_D(Launcher);
|
Q_D(Launcher);
|
||||||
@@ -217,4 +245,12 @@ void Launcher::onTimeout()
|
|||||||
d->handleEvent(event);
|
d->handleEvent(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Launcher::onQuitAppCompleted(QVariant error)
|
||||||
|
{
|
||||||
|
Q_D(Launcher);
|
||||||
|
Event event(Event::AppQuitCompleted);
|
||||||
|
event.errorMessage = error.toString();
|
||||||
|
d->handleEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
|
#include <QVariant>
|
||||||
|
|
||||||
class ComputerManager;
|
class ComputerManager;
|
||||||
class NvComputer;
|
class NvComputer;
|
||||||
@@ -24,16 +25,21 @@ public:
|
|||||||
QObject *parent = nullptr);
|
QObject *parent = nullptr);
|
||||||
~Launcher();
|
~Launcher();
|
||||||
Q_INVOKABLE void execute(ComputerManager *manager);
|
Q_INVOKABLE void execute(ComputerManager *manager);
|
||||||
|
Q_INVOKABLE void quitRunningApp();
|
||||||
|
Q_INVOKABLE bool isExecuted() const;
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void searchingComputer();
|
void searchingComputer();
|
||||||
void searchingApp();
|
void searchingApp();
|
||||||
void sessionCreated(QString appName, Session *session);
|
void sessionCreated(QString appName, Session *session);
|
||||||
void failed(QString text);
|
void failed(QString text);
|
||||||
|
void appQuitRequired(QString appName);
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
|
void onComputerFound(NvComputer *computer);
|
||||||
void onComputerUpdated(NvComputer *computer);
|
void onComputerUpdated(NvComputer *computer);
|
||||||
void onTimeout();
|
void onTimeout();
|
||||||
|
void onQuitAppCompleted(QVariant error);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QScopedPointer<LauncherPrivate> m_DPtr;
|
QScopedPointer<LauncherPrivate> m_DPtr;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<metadata_license>CC0-1.0</metadata_license>
|
<metadata_license>CC0-1.0</metadata_license>
|
||||||
<project_license>GPL-3.0+</project_license>
|
<project_license>GPL-3.0+</project_license>
|
||||||
<name>Moonlight</name>
|
<name>Moonlight</name>
|
||||||
<summary>Stream games from your NVIDIA GameStream-enabled PC</summary>
|
<summary>Play games remotely from your NVIDIA GameStream-enabled PC</summary>
|
||||||
|
|
||||||
<description>
|
<description>
|
||||||
<p>Moonlight can stream games and other applications from a PC with an NVIDIA GeForce GTX 600-series or higher GPU and GeForce Experience installed.</p>
|
<p>Moonlight can stream games and other applications from a PC with an NVIDIA GeForce GTX 600-series or higher GPU and GeForce Experience installed.</p>
|
||||||
@@ -12,27 +12,243 @@
|
|||||||
<ul>
|
<ul>
|
||||||
<li>Streaming at up to 4K resolution</li>
|
<li>Streaming at up to 4K resolution</li>
|
||||||
<li>Support for up to 120 FPS streaming (high refresh rate monitor recommended)</li>
|
<li>Support for up to 120 FPS streaming (high refresh rate monitor recommended)</li>
|
||||||
<li>Hardware accelerated video decoding with VAAPI and VDPAU support</li>
|
<li>Hardware accelerated video decoding with VAAPI, VDPAU, and NVDEC support</li>
|
||||||
<li>5.1 surround sound audio support</li>
|
<li>5.1 surround sound audio support</li>
|
||||||
<li>HEVC support for better video compression efficiency</li>
|
<li>HEVC support for better video compression efficiency</li>
|
||||||
<li>Gamepad support with SDL mapping compatibility</li>
|
<li>Gamepad support with force feedback for up to 4 players</li>
|
||||||
</ul>
|
</ul>
|
||||||
</description>
|
</description>
|
||||||
|
|
||||||
<url type="homepage">https://moonlight-stream.com</url>
|
<url type="homepage">https://moonlight-stream.org</url>
|
||||||
<url type="bugtracker">https://github.com/moonlight-stream/moonlight-qt/issues</url>
|
<url type="bugtracker">https://github.com/moonlight-stream/moonlight-qt/issues</url>
|
||||||
<url type="help">https://github.com/moonlight-stream/moonlight-docs/wiki/Setup-Guide</url>
|
<url type="help">https://github.com/moonlight-stream/moonlight-docs/wiki/Setup-Guide</url>
|
||||||
|
|
||||||
<screenshots>
|
<screenshots>
|
||||||
<screenshot type="default">
|
<screenshot type="default">
|
||||||
<image>https://moonlight-stream.com/appstream_resources/moonlight_screenshot_1.png</image>
|
<image>https://moonlight-stream.org/appstream_resources/moonlight_screenshot_1.png</image>
|
||||||
</screenshot>
|
</screenshot>
|
||||||
<screenshot>
|
<screenshot>
|
||||||
<image>https://moonlight-stream.com/appstream_resources/moonlight_screenshot_2.png</image>
|
<image>https://moonlight-stream.org/appstream_resources/moonlight_screenshot_2.png</image>
|
||||||
</screenshot>
|
</screenshot>
|
||||||
</screenshots>
|
</screenshots>
|
||||||
|
|
||||||
<releases>
|
<releases>
|
||||||
|
<release version="1.2.0" date="2019-12-17">
|
||||||
|
<description>
|
||||||
|
<p>Bugfixes:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Improved video encoding efficiency</li>
|
||||||
|
<li>Improved performance when streaming over a VPN</li>
|
||||||
|
<li>Fixed RTSP handshake error on certain networks</li>
|
||||||
|
<li>Fixed rumble not working on certain gamepads</li>
|
||||||
|
<li>Fixed pairing error when double-clicking on an unpaired PC</li>
|
||||||
|
<li>Updated included SDL gamepad mappings</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release version="1.1.1" date="2019-09-29">
|
||||||
|
<description>
|
||||||
|
<p>Bugfixes:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Improved reliability of public IP address detection</li>
|
||||||
|
<li>Fixed connecting to a host PC with multiple network connections</li>
|
||||||
|
<li>Fixed a hang after exiting a full-screen stream with GNOME on Wayland</li>
|
||||||
|
<li>Fixed gamepad hotplugging</li>
|
||||||
|
<li>Updated to SDL 2.0.10</li>
|
||||||
|
<li>Updated to Qt 5.13.1</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release version="1.1.0" date="2019-08-05">
|
||||||
|
<description>
|
||||||
|
<p>New features:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Added support for NVDEC for NVIDIA GPUs on Wayland</li>
|
||||||
|
<li>Added an option to disable gamepad mouse mode for games that require holding the Start button</li>
|
||||||
|
<li>Added automatic detection of IPv6 addresses for Internet streaming over IPv6 networks</li>
|
||||||
|
</ul>
|
||||||
|
<p>Bugfixes:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Improved speed and reliability of automatic PC detection on the local network</li>
|
||||||
|
<li>Fixed audio latency increasing under certain network conditions</li>
|
||||||
|
<li>Fixed 5.1 surround sound sometimes causing no audio to play when streaming over the Internet</li>
|
||||||
|
<li>Fixed graceful termination of games not being detected properly on GFE 3.19</li>
|
||||||
|
<li>Fixed several audio issues on Linux by switching back to SDL for audio</li>
|
||||||
|
<li>Changed client certificate signature algorithm from SHA-1 to SHA-256 for Debian Buster compatibility</li>
|
||||||
|
<li>Updated included gamepad mappings</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release version="1.0.1" date="2019-05-27">
|
||||||
|
<description>
|
||||||
|
<p>Bugfixes:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Fixed broken video on GeForce Experience 3.19</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release version="1.0.0" date="2019-05-21">
|
||||||
|
<description>
|
||||||
|
<p>New features:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Mouse mode for gamepads (Press and hold Start to toggle)</li>
|
||||||
|
<li>Improved gamepad UI navigation</li>
|
||||||
|
</ul>
|
||||||
|
<p>Bugfixes:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Fixed duplicate non-working gamepads appearing on the host</li>
|
||||||
|
<li>Fixed excessive frame queue delay on macOS</li>
|
||||||
|
<li>Fixed excessive audio latency on Steam Link</li>
|
||||||
|
<li>Fixed hiding mouse cursor after streaming on Steam Link</li>
|
||||||
|
<li>Fixed incorrect receive time stats on 32-bit platforms</li>
|
||||||
|
<li>Added UDP ports 48002 and 48010 for Wake-on-LAN</li>
|
||||||
|
<li>Updated included gamepad mappings</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release version="0.10.1" date="2019-04-28">
|
||||||
|
<description>
|
||||||
|
<p>New features:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Video performance optimizations</li>
|
||||||
|
<li>VAAPI support on Wayland</li>
|
||||||
|
</ul>
|
||||||
|
<p>Bugfixes:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Fixed a crash when attempting to dismiss the connection terminated dialog</li>
|
||||||
|
<li>Fixed a crash when using the overlay on the software decoder</li>
|
||||||
|
<li>Fixed software decoder not displaying output in some cases</li>
|
||||||
|
<li>Fixed some minor UI issues on the PC grid</li>
|
||||||
|
<li>Improved visibility of toolbar highlights</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release version="0.10.0" date="2019-04-07">
|
||||||
|
<description>
|
||||||
|
<p>New features:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Redesigned App Grid with larger game artwork and less blank space</li>
|
||||||
|
<li>Redesigned UI dialogs using Material Design</li>
|
||||||
|
<li>Added a connection warning overlay (which can be disabled in settings)</li>
|
||||||
|
<li>Streaming window no longer minimizes when it loses focus in full-screen mode</li>
|
||||||
|
<li>The Steam Link device is now a supported platform</li>
|
||||||
|
</ul>
|
||||||
|
<p>Bugfixes:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Fixed several bugs that could cause UI hangs or lag</li>
|
||||||
|
<li>Fixed rumble support on gamepads with limited rumble motor control</li>
|
||||||
|
<li>Fixed increased memory usage if the software renderer gets delayed</li>
|
||||||
|
<li>Fixed crashes on certain desktop configurations when loading the UI</li>
|
||||||
|
<li>Fixed handling of pairing certificates with a bogus date</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release version="0.9.1" date="2019-02-17">
|
||||||
|
<description>
|
||||||
|
<p>New features:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Added gamepad rumble support</li>
|
||||||
|
<li>Added a dedicated quit button to the running app tile</li>
|
||||||
|
<li>Reduced video latency on Windows and Linux</li>
|
||||||
|
<li>Added a performance overlay for software decoding</li>
|
||||||
|
</ul>
|
||||||
|
<p>Bugfixes:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Fixed delayed first video frame with GFE 3.17</li>
|
||||||
|
<li>Fixed erroneously setting host resolution to 720p in certain configurations</li>
|
||||||
|
<li>The connection termination dialog is no longer displayed for intentional termination</li>
|
||||||
|
<li>Fixed some gamepads only functioning for UI navigation and not in game</li>
|
||||||
|
<li>Fixed audio latency increasing when switching audio devices</li>
|
||||||
|
<li>Fixed VDPAU scaling behavior on AMD cards</li>
|
||||||
|
<li>Fixed several bugs in gamepad navigation behavior</li>
|
||||||
|
<li>Work around a bug in QGnomePlatform that breaks dialog rendering on Linux</li>
|
||||||
|
<li>Updated included SDL gamepad mappings</li>
|
||||||
|
<li>Fixed hotkey behavior on non-latin keyboards</li>
|
||||||
|
<li>Fixed incorrect frame drop percentages in debug stats</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release version="0.8.1" date="2019-01-08">
|
||||||
|
<description>
|
||||||
|
<p>New features:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Stricter adherence to the specified bitrate when streaming over the Internet</li>
|
||||||
|
</ul>
|
||||||
|
<p>Bugfixes:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Fixed audio crackling that impacted certain systems</li>
|
||||||
|
<li>Fixed Wake-on-LAN packets failing to send on some systems</li>
|
||||||
|
<li>Reduced audio latency when resuming a session where audio is already playing</li>
|
||||||
|
<li>Added option to enable the old SDL audio renderer by setting ML_AUDIO=SDL</li>
|
||||||
|
<li>Updated to use the new Qt 5.12 Flatpak runtime</li>
|
||||||
|
<li>Updated to FFmpeg 4.1</li>
|
||||||
|
<li>Minor UI tweaks</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release version="0.8.0" date="2018-12-25">
|
||||||
|
<description>
|
||||||
|
<p>New features:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Improved security of pairing - After this update, you must re-pair with your PC!</li>
|
||||||
|
<li>Enabled quitting games started by other Moonlight clients also running this update</li>
|
||||||
|
</ul>
|
||||||
|
<p>Bugfixes:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Fixed the video bitrate slider not working above certain values on GFE 3.16</li>
|
||||||
|
<li>Fixed broken Bluetooth audio with PulseAudio</li>
|
||||||
|
<li>Reduced audio latency</li>
|
||||||
|
<li>Fixed some non-gamepad devices triggering the unmapped gamepad dialog box</li>
|
||||||
|
<li>Minor UI tweaks</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release version="0.7.0" date="2018-12-05">
|
||||||
|
<description>
|
||||||
|
<p>New features:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Added option to quit the app after stopping the stream</li>
|
||||||
|
<li>Added CLI option to quit the running app</li>
|
||||||
|
</ul>
|
||||||
|
<p>Bugfixes:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Switched to libsoundio backend to address audio glitches and reduce latency</li>
|
||||||
|
<li>Fixed CLI version output</li>
|
||||||
|
<li>Fixed streaming error messages not showing up when launching from CLI</li>
|
||||||
|
<li>Updated included SDL gamepad mappings</li>
|
||||||
|
<li>Various minor UI tweaks</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release version="0.6.4" date="2018-11-20">
|
||||||
|
<description>
|
||||||
|
<p>New features:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Added an option to match client display refresh rate when FPS is unlocked</li>
|
||||||
|
</ul>
|
||||||
|
<p>Bugfixes:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Fixed multiple gamepads not being detected on the host in some scenarios</li>
|
||||||
|
<li>Fixed an incorrect warning when trying to stream at 4K</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release version="0.6.3" date="2018-11-16">
|
||||||
|
<description>
|
||||||
|
<p>New features:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Added support for GeForce Experience 3.16</li>
|
||||||
|
<li>Added an option to force Moonlight to start in windowed mode</li>
|
||||||
|
</ul>
|
||||||
|
<p>Bugfixes:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Fixed scrolling not working on the settings page</li>
|
||||||
|
<li>Skipped using proxy servers when attempting to stream</li>
|
||||||
|
<li>Fixed a couple possible crashes</li>
|
||||||
|
<li>Renamed the mouse acceleration option to be more clear</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
<release version="0.6.2" date="2018-10-28">
|
<release version="0.6.2" date="2018-10-28">
|
||||||
<description>
|
<description>
|
||||||
<p>New features:</p>
|
<p>New features:</p>
|
||||||
@@ -185,7 +401,7 @@
|
|||||||
</categories>
|
</categories>
|
||||||
|
|
||||||
<developer_name>Moonlight Game Streaming Team</developer_name>
|
<developer_name>Moonlight Game Streaming Team</developer_name>
|
||||||
<update_contact>cameron@moonlight-stream.com</update_contact>
|
<update_contact>cameron@moonlight-stream.org</update_contact>
|
||||||
|
|
||||||
<launchable type="desktop-id">com.moonlight_stream.Moonlight.desktop</launchable>
|
<launchable type="desktop-id">com.moonlight_stream.Moonlight.desktop</launchable>
|
||||||
|
|
||||||
|
|||||||
|
After Width: | Height: | Size: 2.9 KiB |
@@ -0,0 +1,13 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# The default HOME is not persistent, so override
|
||||||
|
# it to a path on the onboard flash. Otherwise our
|
||||||
|
# pairing data will be lost each reboot.
|
||||||
|
HOME=/usr/local/moonlight
|
||||||
|
|
||||||
|
# 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
|
||||||
|
exec nice -n -10 ./bin/moonlight > /tmp/moonlight.log
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
name=Moonlight
|
||||||
|
icon=moonlight.png
|
||||||
|
run=moonlight.sh
|
||||||
@@ -1,29 +1,21 @@
|
|||||||
import QtQuick 2.9
|
import QtQuick 2.9
|
||||||
import QtQuick.Dialogs 1.2
|
|
||||||
import QtQuick.Controls 2.2
|
import QtQuick.Controls 2.2
|
||||||
|
|
||||||
import AppModel 1.0
|
import AppModel 1.0
|
||||||
import ComputerManager 1.0
|
import ComputerManager 1.0
|
||||||
import SdlGamepadKeyNavigation 1.0
|
import SdlGamepadKeyNavigation 1.0
|
||||||
|
|
||||||
GridView {
|
CenteredGridView {
|
||||||
property int computerIndex
|
property int computerIndex
|
||||||
property AppModel appModel : createModel()
|
property AppModel appModel : createModel()
|
||||||
|
property bool activated
|
||||||
|
|
||||||
id: appGrid
|
id: appGrid
|
||||||
focus: true
|
focus: true
|
||||||
activeFocusOnTab: true
|
activeFocusOnTab: true
|
||||||
anchors.fill: parent
|
topMargin: 20
|
||||||
anchors.leftMargin: (parent.width % (cellWidth + anchors.rightMargin)) / 2
|
bottomMargin: 5
|
||||||
anchors.topMargin: 20
|
cellWidth: 230; cellHeight: 297;
|
||||||
anchors.rightMargin: 5
|
|
||||||
anchors.bottomMargin: 5
|
|
||||||
cellWidth: 225; cellHeight: 385;
|
|
||||||
|
|
||||||
// The StackView will trigger a visibility change when
|
|
||||||
// we're pushed onto it, causing our onVisibleChanged
|
|
||||||
// routine to run, but only if we start as invisible
|
|
||||||
visible: false
|
|
||||||
|
|
||||||
function computerLost()
|
function computerLost()
|
||||||
{
|
{
|
||||||
@@ -32,23 +24,25 @@ GridView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
// Don't show any highlighted item until interacting with them
|
// Don't show any highlighted item until interacting with them.
|
||||||
|
// We do this here instead of onActivated to avoid losing the user's
|
||||||
|
// selection when backing out of a different page of the app.
|
||||||
currentIndex = -1
|
currentIndex = -1
|
||||||
}
|
}
|
||||||
|
|
||||||
SdlGamepadKeyNavigation {
|
StackView.onActivated: {
|
||||||
id: gamepadKeyNav
|
appModel.computerLost.connect(computerLost)
|
||||||
|
activated = true
|
||||||
|
|
||||||
|
// Highlight the first item if a gamepad is connected
|
||||||
|
if (currentIndex == -1 && SdlGamepadKeyNavigation.getConnectedGamepads() > 0) {
|
||||||
|
currentIndex = 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onVisibleChanged: {
|
StackView.onDeactivating: {
|
||||||
if (visible) {
|
appModel.computerLost.disconnect(computerLost)
|
||||||
appModel.computerLost.connect(computerLost)
|
activated = false
|
||||||
gamepadKeyNav.enable()
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
appModel.computerLost.disconnect(computerLost)
|
|
||||||
gamepadKeyNav.disable()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function createModel()
|
function createModel()
|
||||||
@@ -61,51 +55,112 @@ GridView {
|
|||||||
model: appModel
|
model: appModel
|
||||||
|
|
||||||
delegate: NavigableItemDelegate {
|
delegate: NavigableItemDelegate {
|
||||||
width: 200; height: 335;
|
width: 220; height: 287;
|
||||||
grid: appGrid
|
grid: appGrid
|
||||||
|
|
||||||
Image {
|
Image {
|
||||||
|
property bool isPlaceholder: false
|
||||||
|
|
||||||
id: appIcon
|
id: appIcon
|
||||||
anchors.horizontalCenter: parent.horizontalCenter;
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
y: 20
|
y: 10
|
||||||
source: model.boxart
|
source: model.boxart
|
||||||
sourceSize {
|
|
||||||
width: 150
|
|
||||||
height: 200
|
|
||||||
}
|
|
||||||
width: sourceSize.width
|
|
||||||
height: sourceSize.height
|
|
||||||
fillMode: Image.Pad
|
|
||||||
}
|
|
||||||
|
|
||||||
Image {
|
onSourceSizeChanged: {
|
||||||
id: runningIcon
|
if ((sourceSize.width == 130 && sourceSize.height == 180) || // GFE 2.0 placeholder image
|
||||||
anchors.centerIn: appIcon
|
(sourceSize.width == 628 && sourceSize.height == 888) || // GFE 3.0 placeholder image
|
||||||
visible: model.running
|
(sourceSize.width == 200 && sourceSize.height == 266)) // Our no_app_image.png
|
||||||
source: "qrc:/res/baseline-play_circle_filled_white-48px.svg"
|
{
|
||||||
sourceSize {
|
isPlaceholder = true
|
||||||
width: 75
|
}
|
||||||
height: 75
|
else
|
||||||
}
|
{
|
||||||
}
|
isPlaceholder = false
|
||||||
|
}
|
||||||
|
|
||||||
Text {
|
width = 200
|
||||||
id: appNameText
|
height = 267
|
||||||
text: model.name
|
}
|
||||||
color: "white"
|
|
||||||
width: parent.width
|
|
||||||
height: 125
|
|
||||||
anchors.top: appIcon.bottom
|
|
||||||
font.pointSize: 22
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
|
||||||
wrapMode: Text.Wrap
|
|
||||||
elide: Text.ElideRight
|
|
||||||
|
|
||||||
// Display a tooltip with the full name if it's truncated
|
// Display a tooltip with the full name if it's truncated
|
||||||
ToolTip.text: model.name
|
ToolTip.text: model.name
|
||||||
ToolTip.delay: 1000
|
ToolTip.delay: 1000
|
||||||
ToolTip.timeout: 5000
|
ToolTip.timeout: 5000
|
||||||
ToolTip.visible: (parent.hovered || parent.highlighted) && truncated
|
ToolTip.visible: (parent.hovered || parent.highlighted) && (!appNameText.visible || 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
|
||||||
|
|
||||||
|
Image {
|
||||||
|
source: "qrc:/res/baseline-play_circle_filled_white-48px.svg"
|
||||||
|
anchors.centerIn: parent
|
||||||
|
sourceSize {
|
||||||
|
width: 75
|
||||||
|
height: 75
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
launchOrResumeSelectedApp()
|
||||||
|
}
|
||||||
|
|
||||||
|
ToolTip.text: "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
|
||||||
|
|
||||||
|
Image {
|
||||||
|
source: "qrc:/res/baseline-cancel-24px.svg"
|
||||||
|
anchors.centerIn: parent
|
||||||
|
sourceSize {
|
||||||
|
width: 75
|
||||||
|
height: 75
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
doQuitGame()
|
||||||
|
}
|
||||||
|
|
||||||
|
ToolTip.text: "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
|
||||||
}
|
}
|
||||||
|
|
||||||
function launchOrResumeSelectedApp()
|
function launchOrResumeSelectedApp()
|
||||||
@@ -126,55 +181,64 @@ GridView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
// Nothing is running or this app is running
|
// Only allow clicking on the box art for non-running games.
|
||||||
launchOrResumeSelectedApp()
|
// For running games, buttons will appear to resume or quit which
|
||||||
|
// will handle starting the game and clicks on the box art will
|
||||||
|
// be ignored.
|
||||||
|
if (!model.running) {
|
||||||
|
launchOrResumeSelectedApp()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MouseArea {
|
Keys.onReturnPressed: {
|
||||||
anchors.fill: parent
|
// Open the app context menu if activated via the gamepad or keyboard
|
||||||
acceptedButtons: Qt.RightButton
|
// for running games. If the game isn't running, the above onClicked
|
||||||
onClicked: {
|
// method will handle the launch.
|
||||||
// popup() ensures the menu appears under the mouse cursor
|
if (model.running) {
|
||||||
appContextMenu.popup()
|
// This will be keyboard/gamepad driven so use
|
||||||
|
// open() instead of popup()
|
||||||
|
appContextMenu.open()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Keys.onMenuPressed: {
|
Keys.onMenuPressed: {
|
||||||
// We must use open() here so the menu is positioned on
|
if (model.running) {
|
||||||
// the ItemDelegate and not where the mouse cursor is
|
// This will be keyboard/gamepad driven so use
|
||||||
appContextMenu.open()
|
// open() instead of popup()
|
||||||
|
appContextMenu.open()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Menu {
|
function doQuitGame() {
|
||||||
|
quitAppDialog.appName = appModel.getRunningAppName()
|
||||||
|
quitAppDialog.segueToStream = false
|
||||||
|
quitAppDialog.open()
|
||||||
|
}
|
||||||
|
|
||||||
|
NavigableMenu {
|
||||||
id: appContextMenu
|
id: appContextMenu
|
||||||
NavigableMenuItem {
|
NavigableMenuItem {
|
||||||
|
parentMenu: appContextMenu
|
||||||
text: model.running ? "Resume Game" : "Launch Game"
|
text: model.running ? "Resume Game" : "Launch Game"
|
||||||
onTriggered: {
|
onTriggered: launchOrResumeSelectedApp()
|
||||||
appContextMenu.close()
|
|
||||||
launchOrResumeSelectedApp()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
NavigableMenuItem {
|
NavigableMenuItem {
|
||||||
|
parentMenu: appContextMenu
|
||||||
text: "Quit Game"
|
text: "Quit Game"
|
||||||
onTriggered: {
|
onTriggered: doQuitGame()
|
||||||
quitAppDialog.appName = appModel.getRunningAppName()
|
|
||||||
quitAppDialog.segueToStream = false
|
|
||||||
quitAppDialog.open()
|
|
||||||
}
|
|
||||||
visible: model.running
|
visible: model.running
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MessageDialog {
|
NavigableMessageDialog {
|
||||||
id: quitAppDialog
|
id: quitAppDialog
|
||||||
modality:Qt.WindowModal
|
|
||||||
property string appName : ""
|
property string appName : ""
|
||||||
property bool segueToStream : false
|
property bool segueToStream : false
|
||||||
property string nextAppName: ""
|
property string nextAppName: ""
|
||||||
property int nextAppIndex: 0
|
property int nextAppIndex: 0
|
||||||
text:"Are you sure you want to quit " + appName +"? Any unsaved progress will be lost."
|
text:"Are you sure you want to quit " + appName +"? Any unsaved progress will be lost."
|
||||||
standardButtons: StandardButton.Yes | StandardButton.No
|
standardButtons: Dialog.Yes | Dialog.No
|
||||||
|
|
||||||
function quitApp() {
|
function quitApp() {
|
||||||
var component = Qt.createComponent("QuitSegue.qml")
|
var component = Qt.createComponent("QuitSegue.qml")
|
||||||
@@ -196,20 +260,21 @@ GridView {
|
|||||||
appModel.quitRunningApp()
|
appModel.quitRunningApp()
|
||||||
}
|
}
|
||||||
|
|
||||||
onYes: quitApp()
|
|
||||||
|
|
||||||
// For keyboard/gamepad navigation
|
|
||||||
onAccepted: quitApp()
|
onAccepted: quitApp()
|
||||||
}
|
}
|
||||||
|
|
||||||
ScrollBar.vertical: ScrollBar {
|
ScrollBar.vertical: ScrollBar {
|
||||||
|
// Manually hide the scrollbar to prevent it from being drawn on top
|
||||||
|
// of the StreamSegue during the transition. It can sometimes get stuck
|
||||||
|
// there since we're not consistently pumping the event loop while
|
||||||
|
// starting the stream.
|
||||||
|
visible: activated
|
||||||
|
|
||||||
parent: appGrid.parent
|
parent: appGrid.parent
|
||||||
anchors {
|
anchors {
|
||||||
top: appGrid.top
|
top: parent.top
|
||||||
left: appGrid.right
|
right: parent.right
|
||||||
bottom: appGrid.bottom
|
bottom: parent.bottom
|
||||||
|
|
||||||
leftMargin: -10
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import QtQuick 2.9
|
import QtQuick 2.9
|
||||||
import QtQuick.Controls 2.2
|
import QtQuick.Controls 2.2
|
||||||
|
|
||||||
|
import SdlGamepadKeyNavigation 1.0
|
||||||
|
|
||||||
// https://stackoverflow.com/questions/45029968/how-do-i-set-the-combobox-width-to-fit-the-largest-item
|
// https://stackoverflow.com/questions/45029968/how-do-i-set-the-combobox-width-to-fit-the-largest-item
|
||||||
ComboBox {
|
ComboBox {
|
||||||
property int textWidth
|
property int textWidth
|
||||||
@@ -28,4 +30,13 @@ ComboBox {
|
|||||||
textWidth = Math.max(popupMetrics.width, textWidth)
|
textWidth = Math.max(popupMetrics.width, textWidth)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
popup.onAboutToShow: {
|
||||||
|
// Switch to normal navigation for combo boxes
|
||||||
|
SdlGamepadKeyNavigation.setUiNavMode(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
popup.onAboutToHide: {
|
||||||
|
SdlGamepadKeyNavigation.setUiNavMode(true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import QtQuick 2.9
|
||||||
|
import QtQuick.Controls 2.2
|
||||||
|
|
||||||
|
GridView {
|
||||||
|
// Detect Qt 5.11 or earlier using presence of synchronousDrag.
|
||||||
|
// Prior to 5.12, the leftMargin and rightMargin values did not work.
|
||||||
|
property bool hasBrokenMargins: this.synchronousDrag === undefined
|
||||||
|
|
||||||
|
property int minMargin: 10
|
||||||
|
property real availableWidth: (parent.width - 2 * minMargin)
|
||||||
|
property int itemsPerRow: availableWidth / cellWidth
|
||||||
|
property real horizontalMargin: itemsPerRow < count && availableWidth >= cellWidth ?
|
||||||
|
(availableWidth % cellWidth) / 2 : minMargin
|
||||||
|
|
||||||
|
function updateMargins() {
|
||||||
|
leftMargin = horizontalMargin
|
||||||
|
rightMargin = horizontalMargin
|
||||||
|
|
||||||
|
if (hasBrokenMargins) {
|
||||||
|
anchors.leftMargin = leftMargin
|
||||||
|
anchors.rightMargin = rightMargin
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onHorizontalMarginChanged: {
|
||||||
|
updateMargins()
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
if (hasBrokenMargins) {
|
||||||
|
// This will cause an anchor conflict with the parent StackView
|
||||||
|
// which breaks animation, but otherwise the grid will not be
|
||||||
|
// centered in the window.
|
||||||
|
anchors.fill = parent
|
||||||
|
anchors.topMargin = topMargin
|
||||||
|
anchors.bottomMargin = bottomMargin
|
||||||
|
}
|
||||||
|
|
||||||
|
updateMargins()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import QtQuick 2.0
|
||||||
|
import QtQuick.Controls 2.2
|
||||||
|
|
||||||
|
import ComputerManager 1.0
|
||||||
|
import Session 1.0
|
||||||
|
|
||||||
|
Item {
|
||||||
|
function onSearchingComputer() {
|
||||||
|
stageLabel.text = "Establishing connection to PC..."
|
||||||
|
}
|
||||||
|
|
||||||
|
function onQuittingApp() {
|
||||||
|
stageLabel.text = "Quitting app..."
|
||||||
|
}
|
||||||
|
|
||||||
|
function onFailure(message) {
|
||||||
|
errorDialog.text = message
|
||||||
|
errorDialog.open()
|
||||||
|
}
|
||||||
|
|
||||||
|
StackView.onActivated: {
|
||||||
|
if (!launcher.isExecuted()) {
|
||||||
|
toolBar.visible = false
|
||||||
|
launcher.searchingComputer.connect(onSearchingComputer)
|
||||||
|
launcher.quittingApp.connect(onQuittingApp)
|
||||||
|
launcher.failed.connect(onFailure)
|
||||||
|
launcher.execute(ComputerManager)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: 5
|
||||||
|
|
||||||
|
BusyIndicator {
|
||||||
|
id: stageSpinner
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
id: stageLabel
|
||||||
|
height: stageSpinner.height
|
||||||
|
text: stageText
|
||||||
|
font.pointSize: 20
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ErrorMessageDialog {
|
||||||
|
id: errorDialog
|
||||||
|
|
||||||
|
onClosed: {
|
||||||
|
Qt.quit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,9 @@
|
|||||||
import QtQuick 2.0
|
import QtQuick 2.0
|
||||||
import QtQuick.Controls 2.2
|
import QtQuick.Controls 2.2
|
||||||
import QtQuick.Dialogs 1.2
|
|
||||||
|
|
||||||
import ComputerManager 1.0
|
import ComputerManager 1.0
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
visible: false
|
|
||||||
|
|
||||||
function onSearchingComputer() {
|
function onSearchingComputer() {
|
||||||
stageLabel.text = "Establishing connection to PC..."
|
stageLabel.text = "Establishing connection to PC..."
|
||||||
}
|
}
|
||||||
@@ -30,13 +27,19 @@ Item {
|
|||||||
errorDialog.open()
|
errorDialog.open()
|
||||||
}
|
}
|
||||||
|
|
||||||
onVisibleChanged: {
|
function onAppQuitRequired(appName) {
|
||||||
if (visible) {
|
quitAppDialog.appName = appName
|
||||||
|
quitAppDialog.open()
|
||||||
|
}
|
||||||
|
|
||||||
|
StackView.onActivated: {
|
||||||
|
if (!launcher.isExecuted()) {
|
||||||
toolBar.visible = false
|
toolBar.visible = false
|
||||||
launcher.searchingComputer.connect(onSearchingComputer)
|
launcher.searchingComputer.connect(onSearchingComputer)
|
||||||
launcher.searchingApp.connect(onSearchingApp)
|
launcher.searchingApp.connect(onSearchingApp)
|
||||||
launcher.sessionCreated.connect(onSessionCreated)
|
launcher.sessionCreated.connect(onSessionCreated)
|
||||||
launcher.failed.connect(onLaunchFailed)
|
launcher.failed.connect(onLaunchFailed)
|
||||||
|
launcher.appQuitRequired.connect(onAppQuitRequired)
|
||||||
launcher.execute(ComputerManager)
|
launcher.execute(ComputerManager)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -56,23 +59,32 @@ Item {
|
|||||||
verticalAlignment: Text.AlignVCenter
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
|
||||||
wrapMode: Text.Wrap
|
wrapMode: Text.Wrap
|
||||||
color: "white"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MessageDialog {
|
ErrorMessageDialog {
|
||||||
id: errorDialog
|
id: errorDialog
|
||||||
modality:Qt.WindowModal
|
|
||||||
icon: StandardIcon.Critical
|
onClosed: {
|
||||||
standardButtons: StandardButton.Ok | StandardButton.Help
|
Qt.quit();
|
||||||
onHelp: {
|
|
||||||
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Troubleshooting");
|
|
||||||
}
|
|
||||||
onVisibleChanged: {
|
|
||||||
if (!visible) {
|
|
||||||
Qt.quit();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
NavigableMessageDialog {
|
||||||
|
id: quitAppDialog
|
||||||
|
text:"Are you sure you want to quit " + appName +"? Any unsaved progress will be lost."
|
||||||
|
standardButtons: Dialog.Yes | Dialog.No
|
||||||
|
property string appName : ""
|
||||||
|
|
||||||
|
function quitApp() {
|
||||||
|
var component = Qt.createComponent("QuitSegue.qml")
|
||||||
|
var params = {"appName": appName}
|
||||||
|
stackView.push(component.createObject(stackView, params))
|
||||||
|
// Trigger the quit after pushing the quit segue on screen
|
||||||
|
launcher.quitRunningApp()
|
||||||
|
}
|
||||||
|
|
||||||
|
onAccepted: quitApp()
|
||||||
|
onRejected: Qt.quit()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import QtQuick 2.0
|
||||||
|
import QtQuick.Controls 2.2
|
||||||
|
|
||||||
|
import SystemProperties 1.0
|
||||||
|
|
||||||
|
NavigableMessageDialog {
|
||||||
|
standardButtons: Dialog.Ok | (SystemProperties.hasBrowser ? Dialog.Help : 0)
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import QtQuick 2.0
|
||||||
|
import QtQuick.Controls 2.2
|
||||||
|
|
||||||
|
Dialog {
|
||||||
|
parent: overlay
|
||||||
|
|
||||||
|
x: Math.round((parent.width - width) / 2)
|
||||||
|
y: Math.round((parent.height - height) / 2)
|
||||||
|
|
||||||
|
onAboutToHide: {
|
||||||
|
// We must force focus back to the last item for platforms without
|
||||||
|
// support for more than one active window like Steam Link. If
|
||||||
|
// we don't, gamepad and keyboard navigation will break after a
|
||||||
|
// dialog appears.
|
||||||
|
stackView.forceActiveFocus()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import QtQuick 2.0
|
||||||
|
import QtQuick.Controls 2.2
|
||||||
|
|
||||||
|
Menu {
|
||||||
|
onOpened: {
|
||||||
|
itemAt(0).forceActiveFocus(Qt.TabFocusReason)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,11 +2,27 @@ import QtQuick 2.0
|
|||||||
import QtQuick.Controls 2.2
|
import QtQuick.Controls 2.2
|
||||||
|
|
||||||
MenuItem {
|
MenuItem {
|
||||||
|
// Qt 5.10 has a menu property, but we need to support 5.9
|
||||||
|
// so we must make our own.
|
||||||
|
property Menu parentMenu
|
||||||
|
|
||||||
// Ensure focus can't be given to an invisible item
|
// Ensure focus can't be given to an invisible item
|
||||||
enabled: visible
|
enabled: visible
|
||||||
height: visible ? implicitHeight : 0
|
height: visible ? implicitHeight : 0
|
||||||
|
focusPolicy: visible ? Qt.TabFocus : Qt.NoFocus
|
||||||
|
|
||||||
|
onTriggered: {
|
||||||
|
// We must close the context menu first or
|
||||||
|
// it can steal focus from any dialogs that
|
||||||
|
// onTriggered may spawn.
|
||||||
|
parentMenu.close()
|
||||||
|
}
|
||||||
|
|
||||||
Keys.onReturnPressed: {
|
Keys.onReturnPressed: {
|
||||||
triggered()
|
triggered()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Keys.onEscapePressed: {
|
||||||
|
parentMenu.close()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import QtQuick 2.0
|
||||||
|
import QtQuick.Controls 2.2
|
||||||
|
import QtQuick.Layouts 1.2
|
||||||
|
|
||||||
|
NavigableDialog {
|
||||||
|
id: dialog
|
||||||
|
|
||||||
|
property alias text: dialogLabel.dialogText
|
||||||
|
|
||||||
|
property string helpText
|
||||||
|
property string helpUrl : "https://github.com/moonlight-stream/moonlight-docs/wiki/Troubleshooting"
|
||||||
|
property string helpTextSeparator : " "
|
||||||
|
|
||||||
|
onOpened: {
|
||||||
|
// Force keyboard focus on the label so keyboard navigation works
|
||||||
|
dialogLabel.forceActiveFocus()
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
spacing: 10
|
||||||
|
|
||||||
|
Image {
|
||||||
|
source: (standardButtons & Dialog.Yes) ?
|
||||||
|
"qrc:/res/baseline-help_outline-24px.svg" :
|
||||||
|
"qrc:/res/baseline-error_outline-24px.svg"
|
||||||
|
sourceSize {
|
||||||
|
// The icon should be square so use the height as the width too
|
||||||
|
width: 50
|
||||||
|
height: 50
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
property string dialogText
|
||||||
|
|
||||||
|
id: dialogLabel
|
||||||
|
text: dialogText + ((helpText && (standardButtons & Dialog.Help)) ? (helpTextSeparator + helpText) : "")
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
elide: Label.ElideRight
|
||||||
|
|
||||||
|
// Cap the width so the dialog doesn't grow horizontally forever. This
|
||||||
|
// will cause word wrap to kick in.
|
||||||
|
Layout.maximumWidth: 400
|
||||||
|
Layout.maximumHeight: 400
|
||||||
|
|
||||||
|
Keys.onReturnPressed: {
|
||||||
|
accept()
|
||||||
|
}
|
||||||
|
|
||||||
|
Keys.onEscapePressed: {
|
||||||
|
reject()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
footer: DialogButtonBox {
|
||||||
|
id: dialogButtonBox
|
||||||
|
standardButtons: dialog.standardButtons
|
||||||
|
|
||||||
|
onHelpRequested: {
|
||||||
|
Qt.openUrlExternally(helpUrl)
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,8 +2,27 @@ import QtQuick 2.0
|
|||||||
import QtQuick.Controls 2.2
|
import QtQuick.Controls 2.2
|
||||||
|
|
||||||
ToolButton {
|
ToolButton {
|
||||||
|
property string iconSource
|
||||||
|
|
||||||
activeFocusOnTab: true
|
activeFocusOnTab: true
|
||||||
|
|
||||||
|
// FIXME: We're using an Image here rather than icon.source because
|
||||||
|
// icons don't work on Qt 5.9 LTS.
|
||||||
|
Image {
|
||||||
|
id: image
|
||||||
|
source: iconSource
|
||||||
|
anchors.centerIn: parent.background
|
||||||
|
sourceSize {
|
||||||
|
width: parent.background.width * 1.10
|
||||||
|
height: parent.background.height * 1.10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
|
||||||
Keys.onReturnPressed: {
|
Keys.onReturnPressed: {
|
||||||
clicked()
|
clicked()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
import QtQuick 2.9
|
import QtQuick 2.9
|
||||||
import QtQuick.Controls 2.2
|
import QtQuick.Controls 2.2
|
||||||
import QtQuick.Dialogs 1.2
|
|
||||||
import QtQuick.Layouts 1.3
|
|
||||||
import QtQuick.Window 2.2
|
|
||||||
|
|
||||||
import ComputerModel 1.0
|
import ComputerModel 1.0
|
||||||
|
|
||||||
@@ -10,48 +7,41 @@ import ComputerManager 1.0
|
|||||||
import StreamingPreferences 1.0
|
import StreamingPreferences 1.0
|
||||||
import SdlGamepadKeyNavigation 1.0
|
import SdlGamepadKeyNavigation 1.0
|
||||||
|
|
||||||
GridView {
|
CenteredGridView {
|
||||||
property ComputerModel computerModel : createModel()
|
property ComputerModel computerModel : createModel()
|
||||||
|
|
||||||
id: pcGrid
|
id: pcGrid
|
||||||
focus: true
|
focus: true
|
||||||
activeFocusOnTab: true
|
activeFocusOnTab: true
|
||||||
anchors.fill: parent
|
topMargin: 20
|
||||||
anchors.leftMargin: (parent.width % (cellWidth + anchors.rightMargin)) / 2
|
bottomMargin: 5
|
||||||
anchors.topMargin: 20
|
cellWidth: 310; cellHeight: 330;
|
||||||
anchors.rightMargin: 5
|
|
||||||
anchors.bottomMargin: 5
|
|
||||||
cellWidth: 350; cellHeight: 350;
|
|
||||||
objectName: "Computers"
|
objectName: "Computers"
|
||||||
|
|
||||||
// The StackView will trigger a visibility change when
|
|
||||||
// we're pushed onto it, causing our onVisibleChanged
|
|
||||||
// routine to run, but only if we start as invisible
|
|
||||||
visible: false
|
|
||||||
|
|
||||||
StreamingPreferences {
|
|
||||||
id: prefs
|
|
||||||
}
|
|
||||||
|
|
||||||
SdlGamepadKeyNavigation {
|
|
||||||
id: gamepadKeyNav
|
|
||||||
}
|
|
||||||
|
|
||||||
onVisibleChanged: {
|
|
||||||
if (visible) {
|
|
||||||
gamepadKeyNav.enable()
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
gamepadKeyNav.disable()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
|
// Don't show any highlighted item until interacting with them.
|
||||||
|
// We do this here instead of onActivated to avoid losing the user's
|
||||||
|
// selection when backing out of a different page of the app.
|
||||||
|
currentIndex = -1
|
||||||
|
}
|
||||||
|
|
||||||
|
StackView.onActivated: {
|
||||||
// Setup signals on CM
|
// Setup signals on CM
|
||||||
ComputerManager.computerAddCompleted.connect(addComplete)
|
ComputerManager.computerAddCompleted.connect(addComplete)
|
||||||
|
|
||||||
// Don't show any highlighted item until interacting with them
|
// This is a bit of a hack to do this here as opposed to main.qml, but
|
||||||
currentIndex = -1
|
// we need it enabled before calling getConnectedGamepads() and PcView
|
||||||
|
// is never destroyed, so it should be okay.
|
||||||
|
SdlGamepadKeyNavigation.enable()
|
||||||
|
|
||||||
|
// Highlight the first item if a gamepad is connected
|
||||||
|
if (currentIndex == -1 && SdlGamepadKeyNavigation.getConnectedGamepads() > 0) {
|
||||||
|
currentIndex = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StackView.onDeactivating: {
|
||||||
|
ComputerManager.computerAddCompleted.disconnect(addComplete)
|
||||||
}
|
}
|
||||||
|
|
||||||
function pairingComplete(error)
|
function pairingComplete(error)
|
||||||
@@ -62,6 +52,7 @@ GridView {
|
|||||||
// Display a failed dialog if we got an error
|
// Display a failed dialog if we got an error
|
||||||
if (error !== undefined) {
|
if (error !== undefined) {
|
||||||
errorDialog.text = error
|
errorDialog.text = error
|
||||||
|
errorDialog.helpText = ""
|
||||||
errorDialog.open()
|
errorDialog.open()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -69,7 +60,8 @@ GridView {
|
|||||||
function addComplete(success)
|
function addComplete(success)
|
||||||
{
|
{
|
||||||
if (!success) {
|
if (!success) {
|
||||||
errorDialog.text = "Unable to connect to the specified PC. Click the Help button for possible solutions."
|
errorDialog.text = "Unable to connect to the specified PC."
|
||||||
|
errorDialog.helpText = "Click the Help button for possible solutions."
|
||||||
errorDialog.open()
|
errorDialog.open()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -82,18 +74,37 @@ GridView {
|
|||||||
return model
|
return model
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: 5
|
||||||
|
visible: pcGrid.count === 0
|
||||||
|
|
||||||
|
BusyIndicator {
|
||||||
|
id: searchSpinner
|
||||||
|
visible: StreamingPreferences.enableMdns
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
height: searchSpinner.height
|
||||||
|
elide: Label.ElideRight
|
||||||
|
text: StreamingPreferences.enableMdns ? "Searching for PCs with NVIDIA GameStream enabled..."
|
||||||
|
: "Automatic PC discovery is disabled. Add your PC manually."
|
||||||
|
font.pointSize: 20
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
model: computerModel
|
model: computerModel
|
||||||
|
|
||||||
delegate: NavigableItemDelegate {
|
delegate: NavigableItemDelegate {
|
||||||
width: 300; height: 300;
|
width: 300; height: 320;
|
||||||
grid: pcGrid
|
grid: pcGrid
|
||||||
|
|
||||||
Image {
|
Image {
|
||||||
id: pcIcon
|
id: pcIcon
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
source: {
|
source: "qrc:/res/desktop_windows-48px.svg"
|
||||||
model.addPc ? "qrc:/res/ic_add_to_queue_white_48px.svg" : "qrc:/res/ic_tv_white_48px.svg"
|
|
||||||
}
|
|
||||||
sourceSize {
|
sourceSize {
|
||||||
width: 200
|
width: 200
|
||||||
height: 200
|
height: 200
|
||||||
@@ -105,8 +116,8 @@ GridView {
|
|||||||
id: stateIcon
|
id: stateIcon
|
||||||
anchors.horizontalCenter: pcIcon.horizontalCenter
|
anchors.horizontalCenter: pcIcon.horizontalCenter
|
||||||
anchors.verticalCenter: pcIcon.verticalCenter
|
anchors.verticalCenter: pcIcon.verticalCenter
|
||||||
anchors.verticalCenterOffset: -10
|
anchors.verticalCenterOffset: -15
|
||||||
visible: !model.addPc && !model.statusUnknown && (!model.online || !model.paired)
|
visible: !model.statusUnknown && (!model.online || !model.paired)
|
||||||
source: !model.online ? "qrc:/res/baseline-warning-24px.svg" : "qrc:/res/baseline-lock-24px.svg"
|
source: !model.online ? "qrc:/res/baseline-warning-24px.svg" : "qrc:/res/baseline-lock-24px.svg"
|
||||||
sourceSize {
|
sourceSize {
|
||||||
width: 75
|
width: 75
|
||||||
@@ -118,32 +129,29 @@ GridView {
|
|||||||
id: statusUnknownSpinner
|
id: statusUnknownSpinner
|
||||||
anchors.horizontalCenter: pcIcon.horizontalCenter
|
anchors.horizontalCenter: pcIcon.horizontalCenter
|
||||||
anchors.verticalCenter: pcIcon.verticalCenter
|
anchors.verticalCenter: pcIcon.verticalCenter
|
||||||
anchors.verticalCenterOffset: -10
|
anchors.verticalCenterOffset: -15
|
||||||
width: 75
|
width: 75
|
||||||
height: 75
|
height: 75
|
||||||
visible: !model.addPc && model.statusUnknown
|
visible: model.statusUnknown
|
||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Label {
|
||||||
id: pcNameText
|
id: pcNameText
|
||||||
text: model.name
|
text: model.name
|
||||||
color: "white"
|
|
||||||
|
|
||||||
width: parent.width
|
width: parent.width
|
||||||
anchors.top: pcIcon.bottom
|
anchors.top: pcIcon.bottom
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
font.pointSize: 36
|
font.pointSize: 36
|
||||||
horizontalAlignment: Text.AlignHCenter
|
horizontalAlignment: Text.AlignHCenter
|
||||||
wrapMode: Text.Wrap
|
wrapMode: Text.Wrap
|
||||||
|
elide: Text.ElideRight
|
||||||
}
|
}
|
||||||
|
|
||||||
Menu {
|
NavigableMenu {
|
||||||
id: pcContextMenu
|
id: pcContextMenu
|
||||||
NavigableMenuItem {
|
NavigableMenuItem {
|
||||||
text: "Wake PC"
|
parentMenu: pcContextMenu
|
||||||
onTriggered: computerModel.wakeComputer(index)
|
|
||||||
visible: !model.addPc && !model.online && model.wakeable
|
|
||||||
}
|
|
||||||
NavigableMenuItem {
|
|
||||||
text: "Delete PC"
|
text: "Delete PC"
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
deletePcDialog.pcIndex = index
|
deletePcDialog.pcIndex = index
|
||||||
@@ -151,13 +159,16 @@ GridView {
|
|||||||
deletePcDialog.open()
|
deletePcDialog.open()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
NavigableMenuItem {
|
||||||
|
parentMenu: pcContextMenu
|
||||||
|
text: "Wake PC"
|
||||||
|
onTriggered: computerModel.wakeComputer(index)
|
||||||
|
visible: !model.online && model.wakeable
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if (model.addPc) {
|
if (model.online) {
|
||||||
addPcDialog.open()
|
|
||||||
}
|
|
||||||
else if (model.online) {
|
|
||||||
if (model.paired) {
|
if (model.paired) {
|
||||||
// go to game view
|
// go to game view
|
||||||
var component = Qt.createComponent("AppView.qml")
|
var component = Qt.createComponent("AppView.qml")
|
||||||
@@ -177,7 +188,8 @@ GridView {
|
|||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// cannot pair while something is streaming or attempting to pair
|
// cannot pair while something is streaming or attempting to pair
|
||||||
errorDialog.text = "This PC is currently busy. Make sure to quit any running games and try again."
|
errorDialog.text = "You cannot pair while a previous session is still running on the host PC. Quit any running games or reboot the host PC, then try pairing again."
|
||||||
|
errorDialog.helpText = ""
|
||||||
errorDialog.open()
|
errorDialog.open()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -187,111 +199,78 @@ GridView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onPressAndHold: {
|
||||||
|
// popup() ensures the menu appears under the mouse cursor
|
||||||
|
if (pcContextMenu.popup) {
|
||||||
|
pcContextMenu.popup()
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Qt 5.9 doesn't have popup()
|
||||||
|
pcContextMenu.open()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
acceptedButtons: Qt.RightButton;
|
acceptedButtons: Qt.RightButton;
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if (!model.addPc) {
|
parent.onPressAndHold()
|
||||||
// popup() ensures the menu appears under the mouse cursor
|
|
||||||
pcContextMenu.popup()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Keys.onMenuPressed: {
|
Keys.onMenuPressed: {
|
||||||
if (!model.addPc) {
|
// We must use open() here so the menu is positioned on
|
||||||
// We must use open() here so the menu is positioned on
|
// the ItemDelegate and not where the mouse cursor is
|
||||||
// the ItemDelegate and not where the mouse cursor is
|
pcContextMenu.open()
|
||||||
pcContextMenu.open()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MessageDialog {
|
ErrorMessageDialog {
|
||||||
id: errorDialog
|
id: errorDialog
|
||||||
// don't allow edits to the rest of the window while open
|
|
||||||
modality:Qt.WindowModal
|
// Using Setup-Guide here instead of Troubleshooting because it's likely that users
|
||||||
icon: StandardIcon.Critical
|
// will arrive here by forgetting to enable GameStream or not forwarding ports.
|
||||||
standardButtons: StandardButton.Ok | StandardButton.Help
|
helpUrl: "https://github.com/moonlight-stream/moonlight-docs/wiki/Setup-Guide"
|
||||||
onHelp: {
|
|
||||||
// Using Setup-Guide here instead of Troubleshooting because it's likely that users
|
|
||||||
// will arrive here by forgetting to enable GameStream or not forwarding ports.
|
|
||||||
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Setup-Guide");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
MessageDialog {
|
NavigableMessageDialog {
|
||||||
id: pairDialog
|
id: pairDialog
|
||||||
|
|
||||||
|
// Pairing dialog must be modal to prevent double-clicks from triggering
|
||||||
|
// pairing twice
|
||||||
|
modal: true
|
||||||
|
closePolicy: Popup.CloseOnEscape
|
||||||
|
|
||||||
// don't allow edits to the rest of the window while open
|
// don't allow edits to the rest of the window while open
|
||||||
modality:Qt.WindowModal
|
|
||||||
property string pin : "0000"
|
property string pin : "0000"
|
||||||
text:"Please enter " + pin + " on your GameStream PC. This dialog will close when pairing is completed."
|
text:"Please enter " + pin + " on your GameStream PC. This dialog will close when pairing is completed."
|
||||||
standardButtons: StandardButton.Cancel
|
standardButtons: Dialog.Cancel
|
||||||
onRejected: {
|
onRejected: {
|
||||||
// FIXME: We should interrupt pairing here
|
// FIXME: We should interrupt pairing here
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MessageDialog {
|
NavigableMessageDialog {
|
||||||
id: deletePcDialog
|
id: deletePcDialog
|
||||||
// don't allow edits to the rest of the window while open
|
// don't allow edits to the rest of the window while open
|
||||||
modality:Qt.WindowModal
|
|
||||||
property int pcIndex : -1;
|
property int pcIndex : -1;
|
||||||
text:"Are you sure you want to remove this PC?"
|
text:"Are you sure you want to remove this PC?"
|
||||||
standardButtons: StandardButton.Yes | StandardButton.No
|
standardButtons: Dialog.Yes | Dialog.No
|
||||||
|
|
||||||
function deletePc() {
|
function deletePc() {
|
||||||
console.log("deleting PC pairing for PC at index: " + pcIndex)
|
console.log("deleting PC pairing for PC at index: " + pcIndex)
|
||||||
computerModel.deleteComputer(pcIndex);
|
computerModel.deleteComputer(pcIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
onYes: deletePc()
|
|
||||||
|
|
||||||
// For keyboard/gamepad activation
|
|
||||||
onAccepted: deletePc()
|
onAccepted: deletePc()
|
||||||
}
|
}
|
||||||
|
|
||||||
Dialog {
|
|
||||||
id: addPcDialog
|
|
||||||
property string label: "Enter the IP address of your GameStream PC"
|
|
||||||
property string hint: "192.168.1.100"
|
|
||||||
property alias editText : editTextItem
|
|
||||||
|
|
||||||
standardButtons: StandardButton.Ok | StandardButton.Cancel
|
|
||||||
onVisibleChanged: {
|
|
||||||
editTextItem.focus = true
|
|
||||||
editTextItem.selectAll()
|
|
||||||
}
|
|
||||||
onAccepted: {
|
|
||||||
ComputerManager.addNewHost(editText.text, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
Text {
|
|
||||||
id: addPcDialogLabel
|
|
||||||
text: addPcDialog.label
|
|
||||||
}
|
|
||||||
Rectangle {
|
|
||||||
implicitWidth: parent.parent.width
|
|
||||||
height: editTextItem.height
|
|
||||||
|
|
||||||
TextInput {
|
|
||||||
id: editTextItem
|
|
||||||
inputMethodHints: Qt.ImhPreferUppercase
|
|
||||||
text: addPcDialog.hint
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ScrollBar.vertical: ScrollBar {
|
ScrollBar.vertical: ScrollBar {
|
||||||
parent: pcGrid.parent
|
parent: pcGrid.parent
|
||||||
anchors {
|
anchors {
|
||||||
top: pcGrid.top
|
top: parent.top
|
||||||
left: pcGrid.right
|
right: parent.right
|
||||||
bottom: pcGrid.bottom
|
bottom: parent.bottom
|
||||||
|
|
||||||
leftMargin: -10
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import QtQuick 2.0
|
import QtQuick 2.0
|
||||||
import QtQuick.Controls 2.2
|
import QtQuick.Controls 2.2
|
||||||
import QtQuick.Dialogs 1.2
|
|
||||||
|
|
||||||
import ComputerManager 1.0
|
import ComputerManager 1.0
|
||||||
import Session 1.0
|
import Session 1.0
|
||||||
@@ -12,13 +11,6 @@ Item {
|
|||||||
|
|
||||||
property string stageText : "Quitting " + appName + "..."
|
property string stageText : "Quitting " + appName + "..."
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
|
|
||||||
// The StackView will trigger a visibility change when
|
|
||||||
// we're pushed onto it, causing our onVisibleChanged
|
|
||||||
// routine to run, but only if we start as invisible
|
|
||||||
visible: false
|
|
||||||
|
|
||||||
function quitAppCompleted(error)
|
function quitAppCompleted(error)
|
||||||
{
|
{
|
||||||
// Display a failed dialog if we got an error
|
// Display a failed dialog if we got an error
|
||||||
@@ -27,26 +19,32 @@ Item {
|
|||||||
errorDialog.open()
|
errorDialog.open()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Exit this view
|
|
||||||
stackView.pop()
|
|
||||||
|
|
||||||
// If we're supposed to launch another game after this, do so now
|
// If we're supposed to launch another game after this, do so now
|
||||||
if (error === undefined && nextSession !== null) {
|
if (error === undefined && nextSession !== null) {
|
||||||
var component = Qt.createComponent("StreamSegue.qml")
|
var component = Qt.createComponent("StreamSegue.qml")
|
||||||
var segue = component.createObject(stackView, {"appName": nextAppName, "session": nextSession})
|
var segue = component.createObject(stackView, {"appName": nextAppName, "session": nextSession})
|
||||||
stackView.push(segue)
|
stackView.replace(segue)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Exit this view
|
||||||
|
stackView.pop()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onVisibleChanged: {
|
StackView.onActivated: {
|
||||||
if (visible) {
|
// Hide the toolbar before we start loading
|
||||||
// Connect the quit completion signal
|
toolBar.visible = false
|
||||||
ComputerManager.quitAppCompleted.connect(quitAppCompleted)
|
|
||||||
}
|
// Connect the quit completion signal
|
||||||
else {
|
ComputerManager.quitAppCompleted.connect(quitAppCompleted)
|
||||||
// Disconnect the signal
|
}
|
||||||
ComputerManager.quitAppCompleted.disconnect(quitAppCompleted)
|
|
||||||
}
|
StackView.onDeactivating: {
|
||||||
|
// Show the toolbar again
|
||||||
|
toolBar.visible = true
|
||||||
|
|
||||||
|
// Disconnect the signal
|
||||||
|
ComputerManager.quitAppCompleted.disconnect(quitAppCompleted)
|
||||||
}
|
}
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
@@ -65,14 +63,10 @@ Item {
|
|||||||
verticalAlignment: Text.AlignVCenter
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
|
||||||
wrapMode: Text.Wrap
|
wrapMode: Text.Wrap
|
||||||
color: "white"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MessageDialog {
|
ErrorMessageDialog {
|
||||||
id: errorDialog
|
id: errorDialog
|
||||||
modality:Qt.WindowModal
|
|
||||||
icon: StandardIcon.Critical
|
|
||||||
standardButtons: StandardButton.Ok
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,36 +4,42 @@ import QtQuick.Controls 2.2
|
|||||||
import StreamingPreferences 1.0
|
import StreamingPreferences 1.0
|
||||||
import ComputerManager 1.0
|
import ComputerManager 1.0
|
||||||
import SdlGamepadKeyNavigation 1.0
|
import SdlGamepadKeyNavigation 1.0
|
||||||
|
import SystemProperties 1.0
|
||||||
|
|
||||||
ScrollView {
|
Flickable {
|
||||||
id: settingsPage
|
id: settingsPage
|
||||||
objectName: "Settings"
|
objectName: "Settings"
|
||||||
|
|
||||||
StreamingPreferences {
|
contentWidth: settingsColumn1.width > settingsColumn2.width ? settingsColumn1.width : settingsColumn2.width
|
||||||
id: prefs
|
contentHeight: settingsColumn1.height > settingsColumn2.height ? settingsColumn1.height : settingsColumn2.height
|
||||||
}
|
|
||||||
|
|
||||||
// The StackView will trigger a visibility change when
|
ScrollBar.vertical: ScrollBar {
|
||||||
// we're pushed onto it, causing our onVisibleChanged
|
parent: settingsPage.parent
|
||||||
// routine to run, but only if we start as invisible
|
anchors {
|
||||||
visible: false
|
top: settingsPage.top
|
||||||
|
left: settingsPage.right
|
||||||
|
bottom: settingsPage.bottom
|
||||||
|
|
||||||
SdlGamepadKeyNavigation {
|
leftMargin: -10
|
||||||
id: gamepadKeyNav
|
|
||||||
}
|
|
||||||
|
|
||||||
onVisibleChanged: {
|
|
||||||
if (visible) {
|
|
||||||
gamepadKeyNav.setSettingsMode(true)
|
|
||||||
gamepadKeyNav.enable()
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
gamepadKeyNav.disable()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Component.onDestruction: {
|
StackView.onActivated: {
|
||||||
prefs.save()
|
// This enables Tab and BackTab based navigation rather than arrow keys.
|
||||||
|
// It is required to shift focus between controls on the settings page.
|
||||||
|
SdlGamepadKeyNavigation.setUiNavMode(true)
|
||||||
|
|
||||||
|
// Highlight the first item if a gamepad is connected
|
||||||
|
if (SdlGamepadKeyNavigation.getConnectedGamepads() > 0) {
|
||||||
|
resolutionComboBox.forceActiveFocus(Qt.TabFocus)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StackView.onDeactivating: {
|
||||||
|
SdlGamepadKeyNavigation.setUiNavMode(false)
|
||||||
|
|
||||||
|
// Save the prefs so the Session can observe the changes
|
||||||
|
StreamingPreferences.save()
|
||||||
}
|
}
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
@@ -57,17 +63,15 @@ ScrollView {
|
|||||||
id: resFPStitle
|
id: resFPStitle
|
||||||
text: qsTr("Resolution and FPS")
|
text: qsTr("Resolution and FPS")
|
||||||
font.pointSize: 12
|
font.pointSize: 12
|
||||||
wrapMode: Text.Wrap
|
wrapMode: Text.Wrap
|
||||||
color: "white"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
id: resFPSdesc
|
id: resFPSdesc
|
||||||
text: qsTr("Setting values too high for your PC may cause lag, stuttering, or errors.")
|
text: qsTr("Setting values too high for your PC or network connection may cause lag, stuttering, or errors.")
|
||||||
font.pointSize: 9
|
font.pointSize: 9
|
||||||
wrapMode: Text.Wrap
|
wrapMode: Text.Wrap
|
||||||
color: "white"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
@@ -85,10 +89,10 @@ ScrollView {
|
|||||||
// Some platforms have different desktop resolutions
|
// Some platforms have different desktop resolutions
|
||||||
// and native resolutions (like macOS with Retina displays)
|
// and native resolutions (like macOS with Retina displays)
|
||||||
if (displayResIndex == 0) {
|
if (displayResIndex == 0) {
|
||||||
screenRect = prefs.getDesktopResolution(displayIndex)
|
screenRect = SystemProperties.getDesktopResolution(displayIndex)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
screenRect = prefs.getNativeResolution(displayIndex)
|
screenRect = SystemProperties.getNativeResolution(displayIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (screenRect.width === 0) {
|
if (screenRect.width === 0) {
|
||||||
@@ -127,8 +131,8 @@ ScrollView {
|
|||||||
|
|
||||||
// load the saved width/height, and iterate through the ComboBox until a match is found
|
// load the saved width/height, and iterate through the ComboBox until a match is found
|
||||||
// and set it to that index.
|
// and set it to that index.
|
||||||
var saved_width = prefs.width
|
var saved_width = StreamingPreferences.width
|
||||||
var saved_height = prefs.height
|
var saved_height = StreamingPreferences.height
|
||||||
currentIndex = 0
|
currentIndex = 0
|
||||||
for (var i = 0; i < resolutionListModel.count; i++) {
|
for (var i = 0; i < resolutionListModel.count; i++) {
|
||||||
var el_width = parseInt(resolutionListModel.get(i).video_width);
|
var el_width = parseInt(resolutionListModel.get(i).video_width);
|
||||||
@@ -177,12 +181,14 @@ ScrollView {
|
|||||||
var selectedHeight = parseInt(resolutionListModel.get(currentIndex).video_height)
|
var selectedHeight = parseInt(resolutionListModel.get(currentIndex).video_height)
|
||||||
|
|
||||||
// Only modify the bitrate if the values actually changed
|
// Only modify the bitrate if the values actually changed
|
||||||
if (prefs.width !== selectedWidth || prefs.height !== selectedHeight) {
|
if (StreamingPreferences.width !== selectedWidth || StreamingPreferences.height !== selectedHeight) {
|
||||||
prefs.width = selectedWidth
|
StreamingPreferences.width = selectedWidth
|
||||||
prefs.height = selectedHeight
|
StreamingPreferences.height = selectedHeight
|
||||||
|
|
||||||
prefs.bitrateKbps = prefs.getDefaultBitrate(prefs.width, prefs.height, prefs.fps);
|
StreamingPreferences.bitrateKbps = StreamingPreferences.getDefaultBitrate(StreamingPreferences.width,
|
||||||
slider.value = prefs.bitrateKbps
|
StreamingPreferences.height,
|
||||||
|
StreamingPreferences.fps);
|
||||||
|
slider.value = StreamingPreferences.bitrateKbps
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -191,15 +197,14 @@ ScrollView {
|
|||||||
function createModel() {
|
function createModel() {
|
||||||
var fpsListModel = Qt.createQmlObject('import QtQuick 2.0; ListModel {}', parent, '')
|
var fpsListModel = Qt.createQmlObject('import QtQuick 2.0; ListModel {}', parent, '')
|
||||||
|
|
||||||
// Get the max supported FPS on this system
|
var max_fps = SystemProperties.maximumStreamingFrameRate
|
||||||
var max_fps = prefs.getMaximumStreamingFrameRate();
|
|
||||||
|
|
||||||
// Default entries
|
// Default entries
|
||||||
fpsListModel.append({"text": "30 FPS", "video_fps": "30"})
|
fpsListModel.append({"text": "30 FPS", "video_fps": "30"})
|
||||||
fpsListModel.append({"text": "60 FPS", "video_fps": "60"})
|
fpsListModel.append({"text": "60 FPS", "video_fps": "60"})
|
||||||
|
|
||||||
// Add unsupported FPS values that come before the display max FPS
|
// Add unsupported FPS values that come before the display max FPS
|
||||||
if (prefs.unsupportedFps) {
|
if (StreamingPreferences.unsupportedFps) {
|
||||||
if (max_fps > 90) {
|
if (max_fps > 90) {
|
||||||
fpsListModel.append({"text": "90 FPS (Unsupported)", "video_fps": "90"})
|
fpsListModel.append({"text": "90 FPS (Unsupported)", "video_fps": "90"})
|
||||||
}
|
}
|
||||||
@@ -211,11 +216,20 @@ ScrollView {
|
|||||||
// Use 64 as the cutoff for adding a separate option to
|
// Use 64 as the cutoff for adding a separate option to
|
||||||
// handle wonky displays that report just over 60 Hz.
|
// handle wonky displays that report just over 60 Hz.
|
||||||
if (max_fps > 64) {
|
if (max_fps > 64) {
|
||||||
fpsListModel.append({"text": max_fps+" FPS", "video_fps": ""+max_fps})
|
// Mark any FPS value greater than 120 as unsupported
|
||||||
|
if (StreamingPreferences.unsupportedFps && max_fps > 120) {
|
||||||
|
fpsListModel.append({"text": max_fps+" FPS (Unsupported)", "video_fps": ""+max_fps})
|
||||||
|
}
|
||||||
|
else if (max_fps > 120) {
|
||||||
|
fpsListModel.append({"text": "120 FPS", "video_fps": "120"})
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
fpsListModel.append({"text": max_fps+" FPS", "video_fps": ""+max_fps})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add unsupported FPS values that come after the display max FPS
|
// Add unsupported FPS values that come after the display max FPS
|
||||||
if (prefs.unsupportedFps) {
|
if (StreamingPreferences.unsupportedFps) {
|
||||||
if (max_fps < 90) {
|
if (max_fps < 90) {
|
||||||
fpsListModel.append({"text": "90 FPS (Unsupported)", "video_fps": "90"})
|
fpsListModel.append({"text": "90 FPS (Unsupported)", "video_fps": "90"})
|
||||||
}
|
}
|
||||||
@@ -230,7 +244,7 @@ ScrollView {
|
|||||||
function reinitialize() {
|
function reinitialize() {
|
||||||
model = createModel()
|
model = createModel()
|
||||||
|
|
||||||
var saved_fps = prefs.fps
|
var saved_fps = StreamingPreferences.fps
|
||||||
currentIndex = 0
|
currentIndex = 0
|
||||||
for (var i = 0; i < model.count; i++) {
|
for (var i = 0; i < model.count; i++) {
|
||||||
var el_fps = parseInt(model.get(i).video_fps);
|
var el_fps = parseInt(model.get(i).video_fps);
|
||||||
@@ -257,11 +271,13 @@ ScrollView {
|
|||||||
var selectedFps = parseInt(model.get(currentIndex).video_fps)
|
var selectedFps = parseInt(model.get(currentIndex).video_fps)
|
||||||
|
|
||||||
// Only modify the bitrate if the values actually changed
|
// Only modify the bitrate if the values actually changed
|
||||||
if (prefs.fps !== selectedFps) {
|
if (StreamingPreferences.fps !== selectedFps) {
|
||||||
prefs.fps = selectedFps
|
StreamingPreferences.fps = selectedFps
|
||||||
|
|
||||||
prefs.bitrateKbps = prefs.getDefaultBitrate(prefs.width, prefs.height, prefs.fps);
|
StreamingPreferences.bitrateKbps = StreamingPreferences.getDefaultBitrate(StreamingPreferences.width,
|
||||||
slider.value = prefs.bitrateKbps
|
StreamingPreferences.height,
|
||||||
|
StreamingPreferences.fps);
|
||||||
|
slider.value = StreamingPreferences.bitrateKbps
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -273,23 +289,20 @@ ScrollView {
|
|||||||
text: qsTr("Video bitrate: ")
|
text: qsTr("Video bitrate: ")
|
||||||
font.pointSize: 12
|
font.pointSize: 12
|
||||||
wrapMode: Text.Wrap
|
wrapMode: Text.Wrap
|
||||||
color: "white"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
id: bitrateDesc
|
id: bitrateDesc
|
||||||
text: qsTr("Lower bitrate to reduce lag and stuttering. Raise bitrate to increase image quality.")
|
text: qsTr("Lower the bitrate on slower connections. Raise the bitrate to increase image quality.")
|
||||||
font.pointSize: 9
|
font.pointSize: 9
|
||||||
wrapMode: Text.Wrap
|
wrapMode: Text.Wrap
|
||||||
color: "white"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Slider {
|
Slider {
|
||||||
id: slider
|
id: slider
|
||||||
wheelEnabled: true
|
|
||||||
|
|
||||||
value: prefs.bitrateKbps
|
value: StreamingPreferences.bitrateKbps
|
||||||
|
|
||||||
stepSize: 500
|
stepSize: 500
|
||||||
from : 500
|
from : 500
|
||||||
@@ -300,7 +313,7 @@ ScrollView {
|
|||||||
|
|
||||||
onValueChanged: {
|
onValueChanged: {
|
||||||
bitrateTitle.text = "Video bitrate: " + (value / 1000.0) + " Mbps"
|
bitrateTitle.text = "Video bitrate: " + (value / 1000.0) + " Mbps"
|
||||||
prefs.bitrateKbps = value
|
StreamingPreferences.bitrateKbps = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -310,13 +323,22 @@ ScrollView {
|
|||||||
text: qsTr("Display mode")
|
text: qsTr("Display mode")
|
||||||
font.pointSize: 12
|
font.pointSize: 12
|
||||||
wrapMode: Text.Wrap
|
wrapMode: Text.Wrap
|
||||||
color: "white"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
AutoResizingComboBox {
|
AutoResizingComboBox {
|
||||||
// ignore setting the index at first, and actually set it when the component is loaded
|
// ignore setting the index at first, and actually set it when the component is loaded
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
var savedWm = prefs.windowMode
|
// Set the recommended option based on the OS
|
||||||
|
for (var i = 0; i < windowModeListModel.count; i++) {
|
||||||
|
var thisWm = windowModeListModel.get(i).val;
|
||||||
|
if (thisWm === StreamingPreferences.recommendedFullScreenMode) {
|
||||||
|
windowModeListModel.get(i).text += " (Recommended)"
|
||||||
|
windowModeListModel.move(i, 0, 1);
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var savedWm = StreamingPreferences.windowMode
|
||||||
currentIndex = 0
|
currentIndex = 0
|
||||||
for (var i = 0; i < windowModeListModel.count; i++) {
|
for (var i = 0; i < windowModeListModel.count; i++) {
|
||||||
var thisWm = windowModeListModel.get(i).val;
|
var thisWm = windowModeListModel.get(i).val;
|
||||||
@@ -329,11 +351,12 @@ ScrollView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
id: windowModeComboBox
|
id: windowModeComboBox
|
||||||
|
hoverEnabled: true
|
||||||
textRole: "text"
|
textRole: "text"
|
||||||
model: ListModel {
|
model: ListModel {
|
||||||
id: windowModeListModel
|
id: windowModeListModel
|
||||||
ListElement {
|
ListElement {
|
||||||
text: "Full-screen (Recommended)"
|
text: "Full-screen"
|
||||||
val: StreamingPreferences.WM_FULLSCREEN
|
val: StreamingPreferences.WM_FULLSCREEN
|
||||||
}
|
}
|
||||||
ListElement {
|
ListElement {
|
||||||
@@ -346,18 +369,45 @@ ScrollView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
onActivated: {
|
onActivated: {
|
||||||
prefs.windowMode = windowModeListModel.get(currentIndex).val
|
StreamingPreferences.windowMode = windowModeListModel.get(currentIndex).val
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ToolTip.delay: 1000
|
||||||
|
ToolTip.timeout: 5000
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
ToolTip.text: "Full-screen generally provides the best performance, but borderless windowed may work better with features like macOS Spaces, Alt+Tab, screenshot tools, on-screen overlays, etc."
|
||||||
}
|
}
|
||||||
|
|
||||||
CheckBox {
|
CheckBox {
|
||||||
id: vsyncCheck
|
id: vsyncCheck
|
||||||
text: "<font color=\"white\">Enable V-Sync</font>"
|
hoverEnabled: true
|
||||||
|
text: "V-Sync"
|
||||||
font.pointSize: 12
|
font.pointSize: 12
|
||||||
checked: prefs.enableVsync
|
checked: StreamingPreferences.enableVsync
|
||||||
onCheckedChanged: {
|
onCheckedChanged: {
|
||||||
prefs.enableVsync = checked
|
StreamingPreferences.enableVsync = checked
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ToolTip.delay: 1000
|
||||||
|
ToolTip.timeout: 5000
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
ToolTip.text: "Disabling V-Sync allows sub-frame rendering latency, but it can display visible tearing"
|
||||||
|
}
|
||||||
|
|
||||||
|
CheckBox {
|
||||||
|
id: framePacingCheck
|
||||||
|
hoverEnabled: true
|
||||||
|
text: "Frame pacing"
|
||||||
|
font.pointSize: 12
|
||||||
|
enabled: StreamingPreferences.enableVsync
|
||||||
|
checked: StreamingPreferences.enableVsync && StreamingPreferences.framePacing
|
||||||
|
onCheckedChanged: {
|
||||||
|
StreamingPreferences.framePacing = checked
|
||||||
|
}
|
||||||
|
ToolTip.delay: 1000
|
||||||
|
ToolTip.timeout: 5000
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
ToolTip.text: "Frame pacing reduces micro-stutter by delaying frames that come in too early"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -380,13 +430,12 @@ ScrollView {
|
|||||||
text: qsTr("Audio configuration")
|
text: qsTr("Audio configuration")
|
||||||
font.pointSize: 12
|
font.pointSize: 12
|
||||||
wrapMode: Text.Wrap
|
wrapMode: Text.Wrap
|
||||||
color: "white"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
AutoResizingComboBox {
|
AutoResizingComboBox {
|
||||||
// ignore setting the index at first, and actually set it when the component is loaded
|
// ignore setting the index at first, and actually set it when the component is loaded
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
var saved_audio = prefs.audioConfig
|
var saved_audio = StreamingPreferences.audioConfig
|
||||||
currentIndex = 0
|
currentIndex = 0
|
||||||
for (var i = 0; i < audioListModel.count; i++) {
|
for (var i = 0; i < audioListModel.count; i++) {
|
||||||
var el_audio = audioListModel.get(i).val;
|
var el_audio = audioListModel.get(i).val;
|
||||||
@@ -413,12 +462,61 @@ ScrollView {
|
|||||||
}
|
}
|
||||||
// ::onActivated must be used, as it only listens for when the index is changed by a human
|
// ::onActivated must be used, as it only listens for when the index is changed by a human
|
||||||
onActivated : {
|
onActivated : {
|
||||||
prefs.audioConfig = audioListModel.get(currentIndex).val
|
StreamingPreferences.audioConfig = audioListModel.get(currentIndex).val
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
GroupBox {
|
||||||
|
id: uiSettingsGroupBox
|
||||||
|
width: (parent.width - 2 * parent.padding)
|
||||||
|
padding: 12
|
||||||
|
title: "<font color=\"skyblue\">UI Settings</font>"
|
||||||
|
font.pointSize: 12
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.fill: parent
|
||||||
|
spacing: 5
|
||||||
|
|
||||||
|
CheckBox {
|
||||||
|
id: startMaximizedCheck
|
||||||
|
text: "Maximize Moonlight window on startup"
|
||||||
|
font.pointSize: 12
|
||||||
|
checked: !StreamingPreferences.startWindowed
|
||||||
|
onCheckedChanged: {
|
||||||
|
StreamingPreferences.startWindowed = !checked
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CheckBox {
|
||||||
|
id: connectionWarningsCheck
|
||||||
|
text: "Show connection quality warnings"
|
||||||
|
font.pointSize: 12
|
||||||
|
checked: StreamingPreferences.connectionWarnings
|
||||||
|
onCheckedChanged: {
|
||||||
|
StreamingPreferences.connectionWarnings = checked
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CheckBox {
|
||||||
|
visible: SystemProperties.hasDiscordIntegration
|
||||||
|
id: discordPresenceCheck
|
||||||
|
text: "Discord Rich Presence integration"
|
||||||
|
font.pointSize: 12
|
||||||
|
checked: StreamingPreferences.richPresence
|
||||||
|
onCheckedChanged: {
|
||||||
|
StreamingPreferences.richPresence = checked
|
||||||
|
}
|
||||||
|
|
||||||
|
ToolTip.delay: 1000
|
||||||
|
ToolTip.timeout: 5000
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
ToolTip.text: "Updates your Discord status to display the name of the game you're streaming."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
@@ -434,28 +532,56 @@ ScrollView {
|
|||||||
title: "<font color=\"skyblue\">Input Settings</font>"
|
title: "<font color=\"skyblue\">Input Settings</font>"
|
||||||
font.pointSize: 12
|
font.pointSize: 12
|
||||||
|
|
||||||
Row {
|
Column {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
spacing: 5
|
spacing: 5
|
||||||
|
|
||||||
CheckBox {
|
CheckBox {
|
||||||
id: multiControllerCheck
|
id: singleControllerCheck
|
||||||
text: "<font color=\"white\">Multiple controller support</font>"
|
text: "Force gamepad #1 always present"
|
||||||
font.pointSize: 12
|
font.pointSize: 12
|
||||||
checked: prefs.multiController
|
checked: !StreamingPreferences.multiController
|
||||||
onCheckedChanged: {
|
onCheckedChanged: {
|
||||||
prefs.multiController = checked
|
StreamingPreferences.multiController = !checked
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ToolTip.delay: 1000
|
||||||
|
ToolTip.timeout: 5000
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
ToolTip.text: "Forces a single gamepad to always stay connected to the host, even if no gamepads are actually connected to this PC.\n" +
|
||||||
|
"Only enable this option when streaming a game that doesn't support gamepads being connected after startup."
|
||||||
}
|
}
|
||||||
|
|
||||||
CheckBox {
|
CheckBox {
|
||||||
id: mouseAccelerationCheck
|
id: rawInputCheck
|
||||||
text: "<font color=\"white\">Enable mouse acceleration</font>"
|
hoverEnabled: true
|
||||||
|
text: "Raw mouse input"
|
||||||
font.pointSize: 12
|
font.pointSize: 12
|
||||||
checked: prefs.mouseAcceleration
|
checked: !StreamingPreferences.mouseAcceleration
|
||||||
onCheckedChanged: {
|
onCheckedChanged: {
|
||||||
prefs.mouseAcceleration = checked
|
StreamingPreferences.mouseAcceleration = !checked
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ToolTip.delay: 1000
|
||||||
|
ToolTip.timeout: 3000
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
ToolTip.text: "When checked, mouse input is not accelerated or scaled by the OS before passing to Moonlight"
|
||||||
|
}
|
||||||
|
|
||||||
|
CheckBox {
|
||||||
|
id: gamepadMouseCheck
|
||||||
|
hoverEnabled: true
|
||||||
|
text: "Gamepad mouse mode support"
|
||||||
|
font.pointSize: 12
|
||||||
|
checked: StreamingPreferences.gamepadMouse
|
||||||
|
onCheckedChanged: {
|
||||||
|
StreamingPreferences.gamepadMouse = checked
|
||||||
|
}
|
||||||
|
|
||||||
|
ToolTip.delay: 1000
|
||||||
|
ToolTip.timeout: 3000
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
ToolTip.text: "When enabled, holding the Start button will toggle mouse mode"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -467,29 +593,27 @@ ScrollView {
|
|||||||
title: "<font color=\"skyblue\">Host Settings</font>"
|
title: "<font color=\"skyblue\">Host Settings</font>"
|
||||||
font.pointSize: 12
|
font.pointSize: 12
|
||||||
|
|
||||||
Row {
|
Column {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
spacing: 5
|
spacing: 5
|
||||||
|
|
||||||
CheckBox {
|
CheckBox {
|
||||||
id: optimizeGameSettingsCheck
|
id: optimizeGameSettingsCheck
|
||||||
text: "<font color=\"white\">Optimize game settings</font>"
|
text: "Optimize game settings"
|
||||||
// HACK: Match width of the other checkbox to make the UI not look bad
|
|
||||||
width: multiControllerCheck.width
|
|
||||||
font.pointSize: 12
|
font.pointSize: 12
|
||||||
checked: prefs.gameOptimizations
|
checked: StreamingPreferences.gameOptimizations
|
||||||
onCheckedChanged: {
|
onCheckedChanged: {
|
||||||
prefs.gameOptimizations = checked
|
StreamingPreferences.gameOptimizations = checked
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
CheckBox {
|
CheckBox {
|
||||||
id: audioPcCheck
|
id: audioPcCheck
|
||||||
text: "<font color=\"white\">Play audio on host PC</font>"
|
text: "Play audio on host PC"
|
||||||
font.pointSize: 12
|
font.pointSize: 12
|
||||||
checked: prefs.playAudioOnHost
|
checked: StreamingPreferences.playAudioOnHost
|
||||||
onCheckedChanged: {
|
onCheckedChanged: {
|
||||||
prefs.playAudioOnHost = checked
|
StreamingPreferences.playAudioOnHost = checked
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -512,13 +636,12 @@ ScrollView {
|
|||||||
text: qsTr("Video decoder")
|
text: qsTr("Video decoder")
|
||||||
font.pointSize: 12
|
font.pointSize: 12
|
||||||
wrapMode: Text.Wrap
|
wrapMode: Text.Wrap
|
||||||
color: "white"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
AutoResizingComboBox {
|
AutoResizingComboBox {
|
||||||
// ignore setting the index at first, and actually set it when the component is loaded
|
// ignore setting the index at first, and actually set it when the component is loaded
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
var saved_vds = prefs.videoDecoderSelection
|
var saved_vds = StreamingPreferences.videoDecoderSelection
|
||||||
currentIndex = 0
|
currentIndex = 0
|
||||||
for (var i = 0; i < decoderListModel.count; i++) {
|
for (var i = 0; i < decoderListModel.count; i++) {
|
||||||
var el_vds = decoderListModel.get(i).val;
|
var el_vds = decoderListModel.get(i).val;
|
||||||
@@ -549,7 +672,7 @@ ScrollView {
|
|||||||
}
|
}
|
||||||
// ::onActivated must be used, as it only listens for when the index is changed by a human
|
// ::onActivated must be used, as it only listens for when the index is changed by a human
|
||||||
onActivated : {
|
onActivated : {
|
||||||
prefs.videoDecoderSelection = decoderListModel.get(currentIndex).val
|
StreamingPreferences.videoDecoderSelection = decoderListModel.get(currentIndex).val
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -559,13 +682,12 @@ ScrollView {
|
|||||||
text: qsTr("Video codec")
|
text: qsTr("Video codec")
|
||||||
font.pointSize: 12
|
font.pointSize: 12
|
||||||
wrapMode: Text.Wrap
|
wrapMode: Text.Wrap
|
||||||
color: "white"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
AutoResizingComboBox {
|
AutoResizingComboBox {
|
||||||
// ignore setting the index at first, and actually set it when the component is loaded
|
// ignore setting the index at first, and actually set it when the component is loaded
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
var saved_vcc = prefs.videoCodecConfig
|
var saved_vcc = StreamingPreferences.videoCodecConfig
|
||||||
currentIndex = 0
|
currentIndex = 0
|
||||||
for(var i = 0; i < codecListModel.count; i++) {
|
for(var i = 0; i < codecListModel.count; i++) {
|
||||||
var el_vcc = codecListModel.get(i).val;
|
var el_vcc = codecListModel.get(i).val;
|
||||||
@@ -603,43 +725,61 @@ ScrollView {
|
|||||||
}
|
}
|
||||||
// ::onActivated must be used, as it only listens for when the index is changed by a human
|
// ::onActivated must be used, as it only listens for when the index is changed by a human
|
||||||
onActivated : {
|
onActivated : {
|
||||||
prefs.videoCodecConfig = codecListModel.get(currentIndex).val
|
StreamingPreferences.videoCodecConfig = codecListModel.get(currentIndex).val
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
CheckBox {
|
CheckBox {
|
||||||
id: unlockUnsupportedFps
|
id: unlockUnsupportedFps
|
||||||
text: "<font color=\"white\">Unlock unsupported FPS options</font>"
|
text: "Unlock unsupported FPS options"
|
||||||
font.pointSize: 12
|
font.pointSize: 12
|
||||||
checked: prefs.unsupportedFps
|
checked: StreamingPreferences.unsupportedFps
|
||||||
onCheckedChanged: {
|
onCheckedChanged: {
|
||||||
prefs.unsupportedFps = checked
|
// This is called on init, so only do the work if we've
|
||||||
|
// actually changed the value.
|
||||||
|
if (StreamingPreferences.unsupportedFps != checked) {
|
||||||
|
StreamingPreferences.unsupportedFps = checked
|
||||||
|
|
||||||
// The selectable FPS values depend on whether
|
// The selectable FPS values depend on whether
|
||||||
// this option is enabled or not
|
// this option is enabled or not
|
||||||
fpsComboBox.reinitialize()
|
fpsComboBox.reinitialize()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
CheckBox {
|
CheckBox {
|
||||||
id: enableMdns
|
id: enableMdns
|
||||||
text: "<font color=\"white\">Automatically find PCs on the local network (Recommended)</font>"
|
text: "Automatically find PCs on the local network (Recommended)"
|
||||||
font.pointSize: 12
|
font.pointSize: 12
|
||||||
checked: prefs.enableMdns
|
checked: StreamingPreferences.enableMdns
|
||||||
onCheckedChanged: {
|
onCheckedChanged: {
|
||||||
prefs.enableMdns = checked
|
// This is called on init, so only do the work if we've
|
||||||
|
// actually changed the value.
|
||||||
|
if (StreamingPreferences.enableMdns != checked) {
|
||||||
|
StreamingPreferences.enableMdns = checked
|
||||||
|
|
||||||
// We must save the updated preference to ensure
|
// We must save the updated preference to ensure
|
||||||
// ComputerManager can observe the change internally.
|
// ComputerManager can observe the change internally.
|
||||||
prefs.save()
|
StreamingPreferences.save()
|
||||||
|
|
||||||
// Restart polling so the mDNS change takes effect
|
// Restart polling so the mDNS change takes effect
|
||||||
if (window.pollingActive) {
|
if (window.pollingActive) {
|
||||||
ComputerManager.stopPollingAsync()
|
ComputerManager.stopPollingAsync()
|
||||||
ComputerManager.startPolling()
|
ComputerManager.startPolling()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CheckBox {
|
||||||
|
id: quitAppAfter
|
||||||
|
text: "Quit app after quitting session"
|
||||||
|
font.pointSize: 12
|
||||||
|
checked: StreamingPreferences.quitAppAfter
|
||||||
|
onCheckedChanged: {
|
||||||
|
StreamingPreferences.quitAppAfter = checked
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import QtQuick 2.0
|
import QtQuick 2.0
|
||||||
import QtQuick.Controls 2.2
|
import QtQuick.Controls 2.2
|
||||||
import QtQuick.Dialogs 1.2
|
|
||||||
import QtQuick.Window 2.2
|
import QtQuick.Window 2.2
|
||||||
|
|
||||||
import SdlGamepadKeyNavigation 1.0
|
import SdlGamepadKeyNavigation 1.0
|
||||||
@@ -12,13 +11,6 @@ Item {
|
|||||||
property string stageText : "Starting " + appName + "..."
|
property string stageText : "Starting " + appName + "..."
|
||||||
property bool quitAfter : false
|
property bool quitAfter : false
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
|
|
||||||
// The StackView will trigger a visibility change when
|
|
||||||
// we're pushed onto it, causing our onVisibleChanged
|
|
||||||
// routine to run, but only if we start as invisible
|
|
||||||
visible: false
|
|
||||||
|
|
||||||
function stageStarting(stage)
|
function stageStarting(stage)
|
||||||
{
|
{
|
||||||
// Update the spinner text
|
// Update the spinner text
|
||||||
@@ -27,8 +19,8 @@ Item {
|
|||||||
|
|
||||||
function stageFailed(stage, errorCode)
|
function stageFailed(stage, errorCode)
|
||||||
{
|
{
|
||||||
errorDialog.text = "Starting " + stage + " failed: Error " + errorCode
|
// Display the error dialog after Session::exec() returns
|
||||||
errorDialog.open()
|
streamSegueErrorDialog.text = "Starting " + stage + " failed: Error " + errorCode
|
||||||
}
|
}
|
||||||
|
|
||||||
function connectionStarted()
|
function connectionStarted()
|
||||||
@@ -37,6 +29,7 @@ Item {
|
|||||||
// see them briefly when we pop off the StackView
|
// see them briefly when we pop off the StackView
|
||||||
stageSpinner.visible = false
|
stageSpinner.visible = false
|
||||||
stageLabel.visible = false
|
stageLabel.visible = false
|
||||||
|
hintText.visible = false
|
||||||
|
|
||||||
// Hide the window now that streaming has begun
|
// Hide the window now that streaming has begun
|
||||||
window.visible = false
|
window.visible = false
|
||||||
@@ -45,7 +38,7 @@ Item {
|
|||||||
function displayLaunchError(text)
|
function displayLaunchError(text)
|
||||||
{
|
{
|
||||||
// Display the error dialog after Session::exec() returns
|
// Display the error dialog after Session::exec() returns
|
||||||
errorDialog.text = text
|
streamSegueErrorDialog.text = text
|
||||||
}
|
}
|
||||||
|
|
||||||
function displayLaunchWarning(text)
|
function displayLaunchWarning(text)
|
||||||
@@ -53,64 +46,102 @@ Item {
|
|||||||
// This toast appears for 3 seconds, just shorter than how long
|
// This toast appears for 3 seconds, just shorter than how long
|
||||||
// Session will wait for it to be displayed. This gives it time
|
// Session will wait for it to be displayed. This gives it time
|
||||||
// to transition to invisible before continuing.
|
// to transition to invisible before continuing.
|
||||||
var toast = Qt.createQmlObject('import QtQuick.Controls 2.3; ToolTip {}', parent, '')
|
var toast = Qt.createQmlObject('import QtQuick.Controls 2.2; ToolTip {}', parent, '')
|
||||||
toast.text = text
|
toast.text = text
|
||||||
toast.timeout = 3000
|
toast.timeout = 3000
|
||||||
toast.visible = true
|
toast.visible = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// It's important that we don't call enable() here
|
function quitStarting()
|
||||||
// or it may interfere with the Session instance
|
{
|
||||||
// getting notified of initial connected gamepads.
|
// Avoid the push transition animation
|
||||||
SdlGamepadKeyNavigation {
|
var component = Qt.createComponent("QuitSegue.qml")
|
||||||
id: gamepadKeyNav
|
stackView.replace(stackView.currentItem, component.createObject(stackView, {"appName": appName}), StackView.Immediate)
|
||||||
|
|
||||||
|
// Show the Qt window again to show quit segue
|
||||||
|
window.visible = true
|
||||||
}
|
}
|
||||||
|
|
||||||
onVisibleChanged: {
|
function sessionFinished()
|
||||||
if (visible) {
|
{
|
||||||
// Hide the toolbar before we start loading
|
// Enable GUI gamepad usage now
|
||||||
toolBar.visible = false
|
SdlGamepadKeyNavigation.enable()
|
||||||
|
|
||||||
|
if (quitAfter) {
|
||||||
|
if (streamSegueErrorDialog.text) {
|
||||||
|
// Quit when the error dialog is acknowledged
|
||||||
|
streamSegueErrorDialog.quitAfter = quitAfter
|
||||||
|
streamSegueErrorDialog.open()
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Quit immediately
|
||||||
|
Qt.quit()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Exit this view
|
||||||
|
stackView.pop()
|
||||||
|
|
||||||
|
// Show the Qt window again after streaming
|
||||||
|
window.visible = true
|
||||||
|
|
||||||
|
// Display any launch errors. We do this after
|
||||||
|
// the Qt UI is visible again to prevent losing
|
||||||
|
// focus on the dialog which would impact gamepad
|
||||||
|
// users.
|
||||||
|
if (streamSegueErrorDialog.text) {
|
||||||
|
streamSegueErrorDialog.quitAfter = quitAfter
|
||||||
|
streamSegueErrorDialog.open()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StackView.onDeactivating: {
|
||||||
|
// Show the toolbar again when popped off the stack
|
||||||
|
toolBar.visible = true
|
||||||
|
|
||||||
|
// Enable GUI gamepad usage now
|
||||||
|
SdlGamepadKeyNavigation.enable()
|
||||||
|
}
|
||||||
|
|
||||||
|
StackView.onActivated: {
|
||||||
|
// Hide the toolbar before we start loading
|
||||||
|
toolBar.visible = false
|
||||||
|
|
||||||
|
// Hook up our signals
|
||||||
|
session.stageStarting.connect(stageStarting)
|
||||||
|
session.stageFailed.connect(stageFailed)
|
||||||
|
session.connectionStarted.connect(connectionStarted)
|
||||||
|
session.displayLaunchError.connect(displayLaunchError)
|
||||||
|
session.displayLaunchWarning.connect(displayLaunchWarning)
|
||||||
|
session.quitStarting.connect(quitStarting)
|
||||||
|
session.sessionFinished.connect(sessionFinished)
|
||||||
|
|
||||||
|
// Kick off the stream
|
||||||
|
streamLoader.active = true
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: streamLoader
|
||||||
|
active: false
|
||||||
|
asynchronous: true
|
||||||
|
|
||||||
|
onLoaded: {
|
||||||
// Set the hint text. We do this here rather than
|
// Set the hint text. We do this here rather than
|
||||||
// in the hintText control itself to synchronize
|
// in the hintText control itself to synchronize
|
||||||
// with Session.exec() which requires no concurrent
|
// with Session.exec() which requires no concurrent
|
||||||
// gamepad usage.
|
// gamepad usage.
|
||||||
hintText.text = gamepadKeyNav.getConnectedGamepads() > 0 ?
|
hintText.text = SdlGamepadKeyNavigation.getConnectedGamepads() > 0 ?
|
||||||
"Tip: Press Start+Select+L1+R1 to disconnect your session" :
|
"Tip: Press Start+Select+L1+R1 to disconnect your session" :
|
||||||
"Tip: Press Ctrl+Alt+Shift+Q to disconnect your session"
|
"Tip: Press Ctrl+Alt+Shift+Q to disconnect your session"
|
||||||
|
|
||||||
// Hook up our signals
|
// Stop GUI gamepad usage now
|
||||||
session.stageStarting.connect(stageStarting)
|
SdlGamepadKeyNavigation.disable()
|
||||||
session.stageFailed.connect(stageFailed)
|
|
||||||
session.connectionStarted.connect(connectionStarted)
|
|
||||||
session.displayLaunchError.connect(displayLaunchError)
|
|
||||||
session.displayLaunchWarning.connect(displayLaunchWarning)
|
|
||||||
|
|
||||||
// Run the streaming session to completion
|
// Run the streaming session to completion
|
||||||
session.exec(Screen.virtualX, Screen.virtualY)
|
session.exec(Screen.virtualX, Screen.virtualY)
|
||||||
|
|
||||||
if (quitAfter) {
|
|
||||||
Qt.quit()
|
|
||||||
} else {
|
|
||||||
// Show the Qt window again after streaming
|
|
||||||
window.visible = true
|
|
||||||
|
|
||||||
// Exit this view
|
|
||||||
stackView.pop()
|
|
||||||
|
|
||||||
// Display any launch errors. We do this after
|
|
||||||
// the Qt UI is visible again to prevent losing
|
|
||||||
// focus on the dialog which would impact gamepad
|
|
||||||
// users.
|
|
||||||
if (errorDialog.text) {
|
|
||||||
errorDialog.open()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Show the toolbar again when we become hidden
|
|
||||||
toolBar.visible = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sourceComponent: Item {}
|
||||||
}
|
}
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
@@ -129,7 +160,6 @@ Item {
|
|||||||
verticalAlignment: Text.AlignVCenter
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
|
||||||
wrapMode: Text.Wrap
|
wrapMode: Text.Wrap
|
||||||
color: "white"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,16 +172,5 @@ Item {
|
|||||||
verticalAlignment: Text.AlignVCenter
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
|
||||||
wrapMode: Text.Wrap
|
wrapMode: Text.Wrap
|
||||||
color: "white"
|
|
||||||
}
|
|
||||||
|
|
||||||
MessageDialog {
|
|
||||||
id: errorDialog
|
|
||||||
modality:Qt.WindowModal
|
|
||||||
icon: StandardIcon.Critical
|
|
||||||
standardButtons: StandardButton.Ok | StandardButton.Help
|
|
||||||
onHelp: {
|
|
||||||
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Troubleshooting");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -22,18 +22,6 @@ QVariant ComputerModel::data(const QModelIndex& index, int role) const
|
|||||||
return QVariant();
|
return QVariant();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (index.row() == m_Computers.count()) {
|
|
||||||
// We insert a synthetic item at the end for the Add PC option
|
|
||||||
switch (role) {
|
|
||||||
case NameRole:
|
|
||||||
return "Add PC";
|
|
||||||
case AddPcRole:
|
|
||||||
return true;
|
|
||||||
default:
|
|
||||||
return QVariant();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Q_ASSERT(index.row() < m_Computers.count());
|
Q_ASSERT(index.row() < m_Computers.count());
|
||||||
|
|
||||||
NvComputer* computer = m_Computers[index.row()];
|
NvComputer* computer = m_Computers[index.row()];
|
||||||
@@ -50,8 +38,6 @@ QVariant ComputerModel::data(const QModelIndex& index, int role) const
|
|||||||
return computer->currentGameId != 0;
|
return computer->currentGameId != 0;
|
||||||
case WakeableRole:
|
case WakeableRole:
|
||||||
return !computer->macAddress.isEmpty();
|
return !computer->macAddress.isEmpty();
|
||||||
case AddPcRole:
|
|
||||||
return false;
|
|
||||||
case StatusUnknownRole:
|
case StatusUnknownRole:
|
||||||
return computer->state == NvComputer::CS_UNKNOWN;
|
return computer->state == NvComputer::CS_UNKNOWN;
|
||||||
default:
|
default:
|
||||||
@@ -67,8 +53,7 @@ int ComputerModel::rowCount(const QModelIndex& parent) const
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add PC placeholder counts as 1
|
return m_Computers.count();
|
||||||
return m_Computers.count() + 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QHash<int, QByteArray> ComputerModel::roleNames() const
|
QHash<int, QByteArray> ComputerModel::roleNames() const
|
||||||
@@ -79,7 +64,6 @@ QHash<int, QByteArray> ComputerModel::roleNames() const
|
|||||||
names[OnlineRole] = "online";
|
names[OnlineRole] = "online";
|
||||||
names[PairedRole] = "paired";
|
names[PairedRole] = "paired";
|
||||||
names[BusyRole] = "busy";
|
names[BusyRole] = "busy";
|
||||||
names[AddPcRole] = "addPc";
|
|
||||||
names[WakeableRole] = "wakeable";
|
names[WakeableRole] = "wakeable";
|
||||||
names[StatusUnknownRole] = "statusUnknown";
|
names[StatusUnknownRole] = "statusUnknown";
|
||||||
|
|
||||||
|
|||||||
@@ -14,8 +14,7 @@ class ComputerModel : public QAbstractListModel
|
|||||||
PairedRole,
|
PairedRole,
|
||||||
BusyRole,
|
BusyRole,
|
||||||
WakeableRole,
|
WakeableRole,
|
||||||
StatusUnknownRole,
|
StatusUnknownRole
|
||||||
AddPcRole
|
|
||||||
};
|
};
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
import QtQuick 2.9
|
import QtQuick 2.9
|
||||||
import QtQuick.Controls 2.2
|
import QtQuick.Controls 2.2
|
||||||
import QtQuick.Dialogs 1.2
|
|
||||||
import QtQuick.Layouts 1.3
|
import QtQuick.Layouts 1.3
|
||||||
import QtQuick.Window 2.2
|
import QtQuick.Window 2.2
|
||||||
|
|
||||||
import QtQuick.Controls.Material 2.1
|
|
||||||
|
|
||||||
import ComputerManager 1.0
|
import ComputerManager 1.0
|
||||||
import AutoUpdateChecker 1.0
|
import AutoUpdateChecker 1.0
|
||||||
import StreamingPreferences 1.0
|
import StreamingPreferences 1.0
|
||||||
|
import SystemProperties 1.0
|
||||||
|
import SdlGamepadKeyNavigation 1.0
|
||||||
|
|
||||||
ApplicationWindow {
|
ApplicationWindow {
|
||||||
property bool pollingActive: false
|
property bool pollingActive: false
|
||||||
@@ -18,18 +17,7 @@ ApplicationWindow {
|
|||||||
width: 1280
|
width: 1280
|
||||||
height: 600
|
height: 600
|
||||||
|
|
||||||
// Maximize the window by default when the stream is configured
|
visibility: StreamingPreferences.startWindowed ? "Windowed" : "Maximized"
|
||||||
// for full-screen or borderless windowed. This is ideal for TV
|
|
||||||
// setups where the user doesn't want a tiny window in the middle
|
|
||||||
// of their screen when starting Moonlight.
|
|
||||||
visibility: prefs.windowMode != StreamingPreferences.WM_WINDOWED ? "Maximized" : "Windowed"
|
|
||||||
|
|
||||||
Material.theme: Material.Dark
|
|
||||||
Material.accent: Material.Purple
|
|
||||||
|
|
||||||
StreamingPreferences {
|
|
||||||
id: prefs
|
|
||||||
}
|
|
||||||
|
|
||||||
StackView {
|
StackView {
|
||||||
id: stackView
|
id: stackView
|
||||||
@@ -141,23 +129,27 @@ ApplicationWindow {
|
|||||||
// Doing it earlier can lead to the dialog appearing behind
|
// Doing it earlier can lead to the dialog appearing behind
|
||||||
// the window or otherwise without input focus.
|
// the window or otherwise without input focus.
|
||||||
if (!initialized) {
|
if (!initialized) {
|
||||||
if (prefs.isRunningWayland()) {
|
// Set initialized before calling anything else, because
|
||||||
waylandDialog.open()
|
// pumping the event loop can cause us to get another
|
||||||
}
|
// onAfterRendering call and potentially reenter this code.
|
||||||
else if (prefs.isWow64()) {
|
initialized = true;
|
||||||
|
|
||||||
|
if (SystemProperties.isWow64) {
|
||||||
wow64Dialog.open()
|
wow64Dialog.open()
|
||||||
}
|
}
|
||||||
else if (!prefs.hasAnyHardwareAcceleration()) {
|
else if (!SystemProperties.hasHardwareAcceleration) {
|
||||||
noHwDecoderDialog.open()
|
if (SystemProperties.isRunningXWayland) {
|
||||||
|
xWaylandDialog.open()
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
noHwDecoderDialog.open()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var unmappedGamepads = prefs.getUnmappedGamepads()
|
if (SystemProperties.unmappedGamepads) {
|
||||||
if (unmappedGamepads) {
|
unmappedGamepadDialog.unmappedGamepads = SystemProperties.unmappedGamepads
|
||||||
unmappedGamepadDialog.unmappedGamepads = unmappedGamepads
|
|
||||||
unmappedGamepadDialog.open()
|
unmappedGamepadDialog.open()
|
||||||
}
|
}
|
||||||
|
|
||||||
initialized = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,28 +171,21 @@ ApplicationWindow {
|
|||||||
|
|
||||||
header: ToolBar {
|
header: ToolBar {
|
||||||
id: toolBar
|
id: toolBar
|
||||||
|
height: 60
|
||||||
anchors.topMargin: 5
|
anchors.topMargin: 5
|
||||||
anchors.bottomMargin: 5
|
anchors.bottomMargin: 5
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
spacing: 20
|
spacing: 20
|
||||||
|
anchors.leftMargin: 10
|
||||||
|
anchors.rightMargin: 10
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
|
||||||
NavigableToolButton {
|
NavigableToolButton {
|
||||||
// Only make the button visible if the user has navigated somewhere.
|
// Only make the button visible if the user has navigated somewhere.
|
||||||
visible: stackView.depth > 1
|
visible: stackView.depth > 1
|
||||||
|
|
||||||
// FIXME: We're using an Image here rather than icon.source because
|
iconSource: "qrc:/res/arrow_left.svg"
|
||||||
// icons don't work on Qt 5.9 LTS.
|
|
||||||
Image {
|
|
||||||
source: "qrc:/res/arrow_left.svg"
|
|
||||||
anchors.centerIn: parent
|
|
||||||
sourceSize {
|
|
||||||
// The icon should be square so use the height as the width too
|
|
||||||
width: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
|
|
||||||
height: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onClicked: stackView.pop()
|
onClicked: stackView.pop()
|
||||||
|
|
||||||
@@ -220,32 +205,55 @@ ApplicationWindow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
NavigableToolButton {
|
NavigableToolButton {
|
||||||
property string browserUrl: ""
|
id: addPcButton
|
||||||
|
visible: stackView.currentItem.objectName === "Computers"
|
||||||
|
|
||||||
id: updateButton
|
iconSource: "qrc:/res/ic_add_to_queue_white_48px.svg"
|
||||||
|
|
||||||
Image {
|
|
||||||
source: "qrc:/res/update.svg"
|
|
||||||
anchors.centerIn: parent
|
|
||||||
sourceSize {
|
|
||||||
width: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
|
|
||||||
height: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ToolTip.delay: 1000
|
ToolTip.delay: 1000
|
||||||
ToolTip.timeout: 3000
|
ToolTip.timeout: 3000
|
||||||
ToolTip.visible: hovered
|
ToolTip.visible: hovered
|
||||||
ToolTip.text: "Update available for Moonlight"
|
ToolTip.text: "Add PC manually" + (newPcShortcut.nativeText ? (" ("+newPcShortcut.nativeText+")") : "")
|
||||||
|
|
||||||
|
Shortcut {
|
||||||
|
id: newPcShortcut
|
||||||
|
sequence: StandardKey.New
|
||||||
|
onActivated: addPcButton.clicked()
|
||||||
|
}
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
addPcDialog.open()
|
||||||
|
}
|
||||||
|
|
||||||
|
Keys.onDownPressed: {
|
||||||
|
stackView.currentItem.forceActiveFocus(Qt.TabFocus)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NavigableToolButton {
|
||||||
|
property string browserUrl: ""
|
||||||
|
|
||||||
|
id: updateButton
|
||||||
|
|
||||||
|
iconSource: "qrc:/res/update.svg"
|
||||||
|
|
||||||
|
ToolTip.delay: 1000
|
||||||
|
ToolTip.timeout: 3000
|
||||||
|
ToolTip.visible: hovered || visible
|
||||||
|
|
||||||
// Invisible until we get a callback notifying us that
|
// Invisible until we get a callback notifying us that
|
||||||
// an update is available
|
// an update is available
|
||||||
visible: false
|
visible: false
|
||||||
|
|
||||||
onClicked: Qt.openUrlExternally(browserUrl);
|
onClicked: {
|
||||||
|
if (SystemProperties.hasBrowser) {
|
||||||
|
Qt.openUrlExternally(browserUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function updateAvailable(url)
|
function updateAvailable(version, url)
|
||||||
{
|
{
|
||||||
|
ToolTip.text = "Update available for Moonlight: Version " + version
|
||||||
updateButton.browserUrl = url
|
updateButton.browserUrl = url
|
||||||
updateButton.visible = true
|
updateButton.visible = true
|
||||||
}
|
}
|
||||||
@@ -262,15 +270,9 @@ ApplicationWindow {
|
|||||||
|
|
||||||
NavigableToolButton {
|
NavigableToolButton {
|
||||||
id: helpButton
|
id: helpButton
|
||||||
|
visible: SystemProperties.hasBrowser
|
||||||
|
|
||||||
Image {
|
iconSource: "qrc:/res/question_mark.svg"
|
||||||
source: "qrc:/res/question_mark.svg"
|
|
||||||
anchors.centerIn: parent
|
|
||||||
sourceSize {
|
|
||||||
width: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
|
|
||||||
height: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ToolTip.delay: 1000
|
ToolTip.delay: 1000
|
||||||
ToolTip.timeout: 3000
|
ToolTip.timeout: 3000
|
||||||
@@ -300,14 +302,7 @@ ApplicationWindow {
|
|||||||
ToolTip.visible: hovered
|
ToolTip.visible: hovered
|
||||||
ToolTip.text: "Gamepad Mapper"
|
ToolTip.text: "Gamepad Mapper"
|
||||||
|
|
||||||
Image {
|
iconSource: "qrc:/res/ic_videogame_asset_white_48px.svg"
|
||||||
source: "qrc:/res/ic_videogame_asset_white_48px.svg"
|
|
||||||
anchors.centerIn: parent
|
|
||||||
sourceSize {
|
|
||||||
width: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
|
|
||||||
height: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onClicked: navigateTo("qrc:/gui/GamepadMapper.qml", "Gamepad Mapping")
|
onClicked: navigateTo("qrc:/gui/GamepadMapper.qml", "Gamepad Mapping")
|
||||||
|
|
||||||
@@ -319,14 +314,7 @@ ApplicationWindow {
|
|||||||
NavigableToolButton {
|
NavigableToolButton {
|
||||||
id: settingsButton
|
id: settingsButton
|
||||||
|
|
||||||
Image {
|
iconSource: "qrc:/res/settings.svg"
|
||||||
source: "qrc:/res/settings.svg"
|
|
||||||
anchors.centerIn: parent
|
|
||||||
sourceSize {
|
|
||||||
width: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
|
|
||||||
height: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onClicked: navigateTo("qrc:/gui/SettingsView.qml", "Settings")
|
onClicked: navigateTo("qrc:/gui/SettingsView.qml", "Settings")
|
||||||
|
|
||||||
@@ -348,67 +336,104 @@ ApplicationWindow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MessageDialog {
|
ErrorMessageDialog {
|
||||||
id: noHwDecoderDialog
|
id: noHwDecoderDialog
|
||||||
modality:Qt.WindowModal
|
|
||||||
icon: StandardIcon.Warning
|
|
||||||
standardButtons: StandardButton.Ok | StandardButton.Help
|
|
||||||
text: "No functioning hardware accelerated H.264 video decoder was detected by Moonlight. " +
|
text: "No functioning hardware accelerated H.264 video decoder was detected by Moonlight. " +
|
||||||
"Your streaming performance may be severely degraded in this configuration. " +
|
"Your streaming performance may be severely degraded in this configuration."
|
||||||
"Click the Help button for more information on solving this problem."
|
helpText: "Click the Help button for more information on solving this problem."
|
||||||
onHelp: {
|
helpUrl: "https://github.com/moonlight-stream/moonlight-docs/wiki/Fixing-Hardware-Decoding-Problems"
|
||||||
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Fixing-Hardware-Decoding-Problems");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
MessageDialog {
|
ErrorMessageDialog {
|
||||||
id: waylandDialog
|
id: xWaylandDialog
|
||||||
modality:Qt.WindowModal
|
text: "Hardware acceleration doesn't work on XWayland. Continuing on XWayland may result in poor streaming performance. " +
|
||||||
icon: StandardIcon.Warning
|
"Try running with QT_QPA_PLATFORM=wayland or switch to X11."
|
||||||
standardButtons: StandardButton.Ok | StandardButton.Help
|
helpText: "Click the Help button for more information."
|
||||||
text: "Moonlight does not support hardware acceleration on Wayland. Continuing on Wayland may result in poor streaming performance. " +
|
helpUrl: "https://github.com/moonlight-stream/moonlight-docs/wiki/Fixing-Hardware-Decoding-Problems"
|
||||||
"Please switch to an X session for optimal performance."
|
|
||||||
onHelp: {
|
|
||||||
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Fixing-Hardware-Decoding-Problems");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
MessageDialog {
|
NavigableMessageDialog {
|
||||||
id: wow64Dialog
|
id: wow64Dialog
|
||||||
modality:Qt.WindowModal
|
standardButtons: Dialog.Ok | Dialog.Cancel
|
||||||
icon: StandardIcon.Warning
|
|
||||||
standardButtons: StandardButton.Ok | StandardButton.Cancel
|
|
||||||
text: "This PC is running a 64-bit version of Windows. Please download the x64 version of Moonlight for the best streaming performance."
|
text: "This PC is running a 64-bit version of Windows. Please download the x64 version of Moonlight for the best streaming performance."
|
||||||
onAccepted: {
|
onAccepted: {
|
||||||
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-qt/releases");
|
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-qt/releases");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MessageDialog {
|
ErrorMessageDialog {
|
||||||
id: unmappedGamepadDialog
|
id: unmappedGamepadDialog
|
||||||
property string unmappedGamepads : ""
|
property string unmappedGamepads : ""
|
||||||
modality:Qt.WindowModal
|
text: "Moonlight detected gamepads without a mapping:\n" + unmappedGamepads
|
||||||
icon: StandardIcon.Warning
|
helpTextSeparator: "\n\n"
|
||||||
standardButtons: StandardButton.Ok | StandardButton.Help
|
helpText: "Click the Help button for information on how to map your gamepads."
|
||||||
text: "Moonlight detected gamepads without a proper mapping. " +
|
helpUrl: "https://github.com/moonlight-stream/moonlight-docs/wiki/Gamepad-Mapping"
|
||||||
"The following gamepads will not function until this is resolved: " + unmappedGamepads + "\n\n" +
|
|
||||||
"Click the Help button for information on how to map your gamepads."
|
|
||||||
onHelp: {
|
|
||||||
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Gamepad-Mapping");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// This dialog appears when quitting via keyboard or gamepad button
|
// This dialog appears when quitting via keyboard or gamepad button
|
||||||
MessageDialog {
|
NavigableMessageDialog {
|
||||||
id: quitConfirmationDialog
|
id: quitConfirmationDialog
|
||||||
modality:Qt.WindowModal
|
standardButtons: Dialog.Yes | Dialog.No
|
||||||
icon: StandardIcon.Warning
|
|
||||||
standardButtons: StandardButton.Yes | StandardButton.No
|
|
||||||
text: "Are you sure you want to quit?"
|
text: "Are you sure you want to quit?"
|
||||||
|
|
||||||
onYes: Qt.quit()
|
|
||||||
|
|
||||||
// For keyboard/gamepad navigation
|
// For keyboard/gamepad navigation
|
||||||
onAccepted: Qt.quit()
|
onAccepted: Qt.quit()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HACK: This belongs in StreamSegue but keeping a dialog around after the parent
|
||||||
|
// dies can trigger bugs in Qt 5.12 that cause the app to crash. For now, we will
|
||||||
|
// host this dialog in a QML component that is never destroyed.
|
||||||
|
//
|
||||||
|
// To repro: Start a stream, cut the network connection to trigger the "Connection
|
||||||
|
// terminated" dialog, wait until the app grid times out back to the PC grid, then
|
||||||
|
// try to dismiss the dialog.
|
||||||
|
ErrorMessageDialog {
|
||||||
|
id: streamSegueErrorDialog
|
||||||
|
|
||||||
|
property bool quitAfter: false
|
||||||
|
|
||||||
|
onClosed: {
|
||||||
|
if (quitAfter) {
|
||||||
|
Qt.quit()
|
||||||
|
}
|
||||||
|
|
||||||
|
// StreamSegue assumes its dialog will be re-created each time we
|
||||||
|
// start streaming, so fake it by wiping out the text each time.
|
||||||
|
text = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NavigableDialog {
|
||||||
|
id: addPcDialog
|
||||||
|
property string label: "Enter the IP address of your GameStream PC:"
|
||||||
|
|
||||||
|
standardButtons: Dialog.Ok | Dialog.Cancel
|
||||||
|
|
||||||
|
onOpened: {
|
||||||
|
// Force keyboard focus on the textbox so keyboard navigation works
|
||||||
|
editText.forceActiveFocus()
|
||||||
|
}
|
||||||
|
|
||||||
|
onAccepted: {
|
||||||
|
if (editText.text) {
|
||||||
|
ComputerManager.addNewHost(editText.text, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
Label {
|
||||||
|
text: addPcDialog.label
|
||||||
|
font.bold: true
|
||||||
|
}
|
||||||
|
|
||||||
|
TextField {
|
||||||
|
id: editText
|
||||||
|
Layout.fillWidth: true
|
||||||
|
focus: true
|
||||||
|
|
||||||
|
Keys.onReturnPressed: {
|
||||||
|
addPcDialog.accept()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
SdlGamepadKeyNavigation::SdlGamepadKeyNavigation()
|
SdlGamepadKeyNavigation::SdlGamepadKeyNavigation()
|
||||||
: m_Enabled(false),
|
: m_Enabled(false),
|
||||||
m_SettingsMode(false),
|
m_UiNavMode(false),
|
||||||
m_LastAxisNavigationEventTime(0)
|
m_LastAxisNavigationEventTime(0)
|
||||||
{
|
{
|
||||||
m_PollingTimer = new QTimer(this);
|
m_PollingTimer = new QTimer(this);
|
||||||
@@ -91,6 +91,12 @@ void SdlGamepadKeyNavigation::onPollingTimerFired()
|
|||||||
|
|
||||||
while (SDL_PollEvent(&event)) {
|
while (SDL_PollEvent(&event)) {
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
|
case SDL_QUIT:
|
||||||
|
// SDL may send us a quit event since we initialize
|
||||||
|
// the video subsystem on startup. If we get one,
|
||||||
|
// forward it on for Qt to take care of.
|
||||||
|
QCoreApplication::instance()->quit();
|
||||||
|
break;
|
||||||
case SDL_CONTROLLERBUTTONDOWN:
|
case SDL_CONTROLLERBUTTONDOWN:
|
||||||
case SDL_CONTROLLERBUTTONUP:
|
case SDL_CONTROLLERBUTTONUP:
|
||||||
{
|
{
|
||||||
@@ -100,7 +106,7 @@ void SdlGamepadKeyNavigation::onPollingTimerFired()
|
|||||||
|
|
||||||
switch (event.cbutton.button) {
|
switch (event.cbutton.button) {
|
||||||
case SDL_CONTROLLER_BUTTON_DPAD_UP:
|
case SDL_CONTROLLER_BUTTON_DPAD_UP:
|
||||||
if (m_SettingsMode) {
|
if (m_UiNavMode) {
|
||||||
// Back-tab
|
// Back-tab
|
||||||
sendKey(type, Qt::Key_Tab, Qt::ShiftModifier);
|
sendKey(type, Qt::Key_Tab, Qt::ShiftModifier);
|
||||||
}
|
}
|
||||||
@@ -109,7 +115,7 @@ void SdlGamepadKeyNavigation::onPollingTimerFired()
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case SDL_CONTROLLER_BUTTON_DPAD_DOWN:
|
case SDL_CONTROLLER_BUTTON_DPAD_DOWN:
|
||||||
if (m_SettingsMode) {
|
if (m_UiNavMode) {
|
||||||
sendKey(type, Qt::Key_Tab);
|
sendKey(type, Qt::Key_Tab);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -118,24 +124,12 @@ void SdlGamepadKeyNavigation::onPollingTimerFired()
|
|||||||
break;
|
break;
|
||||||
case SDL_CONTROLLER_BUTTON_DPAD_LEFT:
|
case SDL_CONTROLLER_BUTTON_DPAD_LEFT:
|
||||||
sendKey(type, Qt::Key_Left);
|
sendKey(type, Qt::Key_Left);
|
||||||
if (m_SettingsMode) {
|
|
||||||
// Some settings controls respond to left/right (like the slider)
|
|
||||||
// and others respond to up/down (like combo boxes). They seem to
|
|
||||||
// be mutually exclusive though so let's just send both.
|
|
||||||
sendKey(type, Qt::Key_Up);
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case SDL_CONTROLLER_BUTTON_DPAD_RIGHT:
|
case SDL_CONTROLLER_BUTTON_DPAD_RIGHT:
|
||||||
sendKey(type, Qt::Key_Right);
|
sendKey(type, Qt::Key_Right);
|
||||||
if (m_SettingsMode) {
|
|
||||||
// Some settings controls respond to left/right (like the slider)
|
|
||||||
// and others respond to up/down (like combo boxes). They seem to
|
|
||||||
// be mutually exclusive though so let's just send both.
|
|
||||||
sendKey(type, Qt::Key_Down);
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case SDL_CONTROLLER_BUTTON_A:
|
case SDL_CONTROLLER_BUTTON_A:
|
||||||
if (m_SettingsMode) {
|
if (m_UiNavMode) {
|
||||||
sendKey(type, Qt::Key_Space);
|
sendKey(type, Qt::Key_Space);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -177,7 +171,7 @@ void SdlGamepadKeyNavigation::onPollingTimerFired()
|
|||||||
// Do nothing
|
// Do nothing
|
||||||
}
|
}
|
||||||
else if (leftY < -30000) {
|
else if (leftY < -30000) {
|
||||||
if (m_SettingsMode) {
|
if (m_UiNavMode) {
|
||||||
// Back-tab
|
// Back-tab
|
||||||
sendKey(QEvent::Type::KeyPress, Qt::Key_Tab, Qt::ShiftModifier);
|
sendKey(QEvent::Type::KeyPress, Qt::Key_Tab, Qt::ShiftModifier);
|
||||||
sendKey(QEvent::Type::KeyRelease, Qt::Key_Tab, Qt::ShiftModifier);
|
sendKey(QEvent::Type::KeyRelease, Qt::Key_Tab, Qt::ShiftModifier);
|
||||||
@@ -190,7 +184,7 @@ void SdlGamepadKeyNavigation::onPollingTimerFired()
|
|||||||
m_LastAxisNavigationEventTime = SDL_GetTicks();
|
m_LastAxisNavigationEventTime = SDL_GetTicks();
|
||||||
}
|
}
|
||||||
else if (leftY > 30000) {
|
else if (leftY > 30000) {
|
||||||
if (m_SettingsMode) {
|
if (m_UiNavMode) {
|
||||||
sendKey(QEvent::Type::KeyPress, Qt::Key_Tab);
|
sendKey(QEvent::Type::KeyPress, Qt::Key_Tab);
|
||||||
sendKey(QEvent::Type::KeyRelease, Qt::Key_Tab);
|
sendKey(QEvent::Type::KeyRelease, Qt::Key_Tab);
|
||||||
}
|
}
|
||||||
@@ -204,27 +198,11 @@ void SdlGamepadKeyNavigation::onPollingTimerFired()
|
|||||||
else if (leftX < -30000) {
|
else if (leftX < -30000) {
|
||||||
sendKey(QEvent::Type::KeyPress, Qt::Key_Left);
|
sendKey(QEvent::Type::KeyPress, Qt::Key_Left);
|
||||||
sendKey(QEvent::Type::KeyRelease, Qt::Key_Left);
|
sendKey(QEvent::Type::KeyRelease, Qt::Key_Left);
|
||||||
if (m_SettingsMode) {
|
|
||||||
// Some settings controls respond to left/right (like the slider)
|
|
||||||
// and others respond to up/down (like combo boxes). They seem to
|
|
||||||
// be mutually exclusive though so let's just send both.
|
|
||||||
sendKey(QEvent::Type::KeyPress, Qt::Key_Up);
|
|
||||||
sendKey(QEvent::Type::KeyRelease, Qt::Key_Up);
|
|
||||||
}
|
|
||||||
|
|
||||||
m_LastAxisNavigationEventTime = SDL_GetTicks();
|
m_LastAxisNavigationEventTime = SDL_GetTicks();
|
||||||
}
|
}
|
||||||
else if (leftX > 30000) {
|
else if (leftX > 30000) {
|
||||||
sendKey(QEvent::Type::KeyPress, Qt::Key_Right);
|
sendKey(QEvent::Type::KeyPress, Qt::Key_Right);
|
||||||
sendKey(QEvent::Type::KeyRelease, Qt::Key_Right);
|
sendKey(QEvent::Type::KeyRelease, Qt::Key_Right);
|
||||||
if (m_SettingsMode) {
|
|
||||||
// Some settings controls respond to left/right (like the slider)
|
|
||||||
// and others respond to up/down (like combo boxes). They seem to
|
|
||||||
// be mutually exclusive though so let's just send both.
|
|
||||||
sendKey(QEvent::Type::KeyPress, Qt::Key_Down);
|
|
||||||
sendKey(QEvent::Type::KeyRelease, Qt::Key_Down);
|
|
||||||
}
|
|
||||||
|
|
||||||
m_LastAxisNavigationEventTime = SDL_GetTicks();
|
m_LastAxisNavigationEventTime = SDL_GetTicks();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -240,19 +218,14 @@ void SdlGamepadKeyNavigation::sendKey(QEvent::Type type, Qt::Key key, Qt::Keyboa
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void SdlGamepadKeyNavigation::setSettingsMode(bool settingsMode)
|
void SdlGamepadKeyNavigation::setUiNavMode(bool uiNavMode)
|
||||||
{
|
{
|
||||||
m_SettingsMode = settingsMode;
|
m_UiNavMode = uiNavMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
int SdlGamepadKeyNavigation::getConnectedGamepads()
|
int SdlGamepadKeyNavigation::getConnectedGamepads()
|
||||||
{
|
{
|
||||||
if (SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) != 0) {
|
Q_ASSERT(m_Enabled);
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) failed: %s",
|
|
||||||
SDL_GetError());
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
int count = 0;
|
int count = 0;
|
||||||
for (int i = 0; i < SDL_NumJoysticks(); i++) {
|
for (int i = 0; i < SDL_NumJoysticks(); i++) {
|
||||||
@@ -261,6 +234,5 @@ int SdlGamepadKeyNavigation::getConnectedGamepads()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER);
|
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
@@ -18,7 +18,7 @@ public:
|
|||||||
|
|
||||||
Q_INVOKABLE void disable();
|
Q_INVOKABLE void disable();
|
||||||
|
|
||||||
Q_INVOKABLE void setSettingsMode(bool settingsMode);
|
Q_INVOKABLE void setUiNavMode(bool settingsMode);
|
||||||
|
|
||||||
Q_INVOKABLE int getConnectedGamepads();
|
Q_INVOKABLE int getConnectedGamepads();
|
||||||
|
|
||||||
@@ -32,6 +32,6 @@ private:
|
|||||||
QTimer* m_PollingTimer;
|
QTimer* m_PollingTimer;
|
||||||
QList<SDL_GameController*> m_Gamepads;
|
QList<SDL_GameController*> m_Gamepads;
|
||||||
bool m_Enabled;
|
bool m_Enabled;
|
||||||
bool m_SettingsMode;
|
bool m_UiNavMode;
|
||||||
Uint32 m_LastAxisNavigationEventTime;
|
Uint32 m_LastAxisNavigationEventTime;
|
||||||
};
|
};
|
||||||
@@ -5,6 +5,11 @@
|
|||||||
#include <QQuickStyle>
|
#include <QQuickStyle>
|
||||||
#include <QMutex>
|
#include <QMutex>
|
||||||
#include <QtDebug>
|
#include <QtDebug>
|
||||||
|
#include <QNetworkProxyFactory>
|
||||||
|
#include <QPalette>
|
||||||
|
#include <QFont>
|
||||||
|
#include <QCursor>
|
||||||
|
#include <QElapsedTimer>
|
||||||
|
|
||||||
// Don't let SDL hook our main function, since Qt is already
|
// Don't let SDL hook our main function, since Qt is already
|
||||||
// doing the same thing. This needs to be before any headers
|
// doing the same thing. This needs to be before any headers
|
||||||
@@ -20,12 +25,14 @@
|
|||||||
#include "antihookingprotection.h"
|
#include "antihookingprotection.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#include "cli/quitstream.h"
|
||||||
#include "cli/startstream.h"
|
#include "cli/startstream.h"
|
||||||
#include "cli/commandlineparser.h"
|
#include "cli/commandlineparser.h"
|
||||||
#include "path.h"
|
#include "path.h"
|
||||||
#include "gui/computermodel.h"
|
#include "gui/computermodel.h"
|
||||||
#include "gui/appmodel.h"
|
#include "gui/appmodel.h"
|
||||||
#include "backend/autoupdatechecker.h"
|
#include "backend/autoupdatechecker.h"
|
||||||
|
#include "backend/systemproperties.h"
|
||||||
#include "streaming/session.h"
|
#include "streaming/session.h"
|
||||||
#include "settings/streamingpreferences.h"
|
#include "settings/streamingpreferences.h"
|
||||||
#include "gui/sdlgamepadkeynavigation.h"
|
#include "gui/sdlgamepadkeynavigation.h"
|
||||||
@@ -46,7 +53,7 @@
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_CUSTOM_LOGGER
|
#ifdef USE_CUSTOM_LOGGER
|
||||||
static QTime s_LoggerTime;
|
static QElapsedTimer s_LoggerTime;
|
||||||
static QTextStream s_LoggerStream(stdout);
|
static QTextStream s_LoggerStream(stdout);
|
||||||
static QMutex s_LoggerLock;
|
static QMutex s_LoggerLock;
|
||||||
#ifdef LOG_TO_FILE
|
#ifdef LOG_TO_FILE
|
||||||
@@ -221,6 +228,11 @@ LONG WINAPI UnhandledExceptionHandler(struct _EXCEPTION_POINTERS *ExceptionInfo)
|
|||||||
|
|
||||||
int main(int argc, char *argv[])
|
int main(int argc, char *argv[])
|
||||||
{
|
{
|
||||||
|
SDL_SetMainReady();
|
||||||
|
|
||||||
|
// Set the app version for the QCommandLineParser's showVersion() command
|
||||||
|
QCoreApplication::setApplicationVersion(VERSION_STR);
|
||||||
|
|
||||||
// Set these here to allow us to use the default QSettings constructor.
|
// Set these here to allow us to use the default QSettings constructor.
|
||||||
// These also ensure that our cache directory is named correctly. As such,
|
// These also ensure that our cache directory is named correctly. As such,
|
||||||
// it is critical that these be called before Path::initialize().
|
// it is critical that these be called before Path::initialize().
|
||||||
@@ -273,11 +285,17 @@ int main(int argc, char *argv[])
|
|||||||
AntiHookingDummyImport();
|
AntiHookingDummyImport();
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
// Enable High DPI support
|
||||||
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
|
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
|
||||||
|
|
||||||
|
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
|
||||||
|
// Enable fractional High DPI scaling on Qt 5.14 and later
|
||||||
|
QGuiApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::PassThrough);
|
||||||
|
#endif
|
||||||
|
|
||||||
// This avoids using the default keychain for SSL, which may cause
|
// This avoids using the default keychain for SSL, which may cause
|
||||||
// password prompts on macOS.
|
// password prompts on macOS.
|
||||||
qputenv("QT_SSL_USE_TEMPORARY_KEYCHAIN", QByteArray("1"));
|
qputenv("QT_SSL_USE_TEMPORARY_KEYCHAIN", "1");
|
||||||
|
|
||||||
#ifdef Q_OS_WIN32
|
#ifdef Q_OS_WIN32
|
||||||
if (!qEnvironmentVariableIsSet("QT_OPENGL")) {
|
if (!qEnvironmentVariableIsSet("QT_OPENGL")) {
|
||||||
@@ -288,18 +306,104 @@ int main(int argc, char *argv[])
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
// We don't want system proxies to apply to us
|
||||||
|
QNetworkProxyFactory::setUseSystemConfiguration(false);
|
||||||
|
|
||||||
|
// Clear any default application proxy
|
||||||
|
QNetworkProxy noProxy(QNetworkProxy::NoProxy);
|
||||||
|
QNetworkProxy::setApplicationProxy(noProxy);
|
||||||
|
|
||||||
// Register custom metatypes for use in signals
|
// Register custom metatypes for use in signals
|
||||||
qRegisterMetaType<NvApp>("NvApp");
|
qRegisterMetaType<NvApp>("NvApp");
|
||||||
|
|
||||||
|
SDL_version compileVersion;
|
||||||
|
SDL_VERSION(&compileVersion);
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Compiled with SDL %d.%d.%d",
|
||||||
|
compileVersion.major, compileVersion.minor, compileVersion.patch);
|
||||||
|
|
||||||
|
SDL_version runtimeVersion;
|
||||||
|
SDL_GetVersion(&runtimeVersion);
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Running with SDL %d.%d.%d",
|
||||||
|
runtimeVersion.major, runtimeVersion.minor, runtimeVersion.patch);
|
||||||
|
|
||||||
|
// Allow the display to sleep by default. We will manually use SDL_DisableScreenSaver()
|
||||||
|
// and SDL_EnableScreenSaver() when appropriate. This hint must be set before
|
||||||
|
// initializing the SDL video subsystem to have any effect.
|
||||||
|
SDL_SetHint(SDL_HINT_VIDEO_ALLOW_SCREENSAVER, "1");
|
||||||
|
|
||||||
|
if (SDL_InitSubSystem(SDL_INIT_TIMER) != 0) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"SDL_InitSubSystem(SDL_INIT_TIMER) failed: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef STEAM_LINK
|
||||||
|
// Steam Link requires that we initialize video before creating our
|
||||||
|
// QGuiApplication in order to configure the framebuffer correctly.
|
||||||
|
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Use atexit() to ensure SDL_Quit() is called. This avoids
|
||||||
|
// racing with object destruction where SDL may be used.
|
||||||
|
atexit(SDL_Quit);
|
||||||
|
|
||||||
|
// Avoid the default behavior of changing the timer resolution to 1 ms.
|
||||||
|
// We don't want this all the time that Moonlight is open. We will set
|
||||||
|
// it manually when we start streaming.
|
||||||
|
SDL_SetHint(SDL_HINT_TIMER_RESOLUTION, "0");
|
||||||
|
|
||||||
|
// Disable minimize on focus loss by default. Users seem to want this off by default.
|
||||||
|
SDL_SetHint(SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS, "0");
|
||||||
|
|
||||||
|
#ifdef QT_DEBUG
|
||||||
|
// Allow thread naming using exceptions on debug builds. SDL doesn't use SEH
|
||||||
|
// when throwing the exceptions, so we don't enable it for release builds out
|
||||||
|
// of caution.
|
||||||
|
SDL_SetHint(SDL_HINT_WINDOWS_DISABLE_THREAD_NAMING, "0");
|
||||||
|
#endif
|
||||||
|
|
||||||
QGuiApplication app(argc, argv);
|
QGuiApplication app(argc, argv);
|
||||||
|
|
||||||
|
// After the QGuiApplication is created, the platform stuff will be initialized
|
||||||
|
// and we can set the SDL video driver to match Qt.
|
||||||
|
if (qgetenv("XDG_SESSION_TYPE") == "wayland" && QGuiApplication::platformName() == "xcb") {
|
||||||
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Detected XWayland. This will probably break hardware decoding! Try running with QT_QPA_PLATFORM=wayland or switch to X11.");
|
||||||
|
}
|
||||||
|
else if (QGuiApplication::platformName().startsWith("wayland")) {
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Detected Wayland. Performance may be worse than X11.");
|
||||||
|
qputenv("SDL_VIDEODRIVER", "wayland");
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef STEAM_LINK
|
||||||
|
// Qt 5.9 from the Steam Link SDK is not able to load any fonts
|
||||||
|
// since the Steam Link doesn't include any of the ones it looks
|
||||||
|
// for. We know it has NotoSans so we will explicitly ask for that.
|
||||||
|
if (app.font().family().isEmpty()) {
|
||||||
|
qWarning() << "SL HACK: No default font - using NotoSans";
|
||||||
|
|
||||||
|
QFont fon("NotoSans");
|
||||||
|
app.setFont(fon);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move the mouse to the bottom right so it's invisible when using
|
||||||
|
// gamepad-only navigation.
|
||||||
|
QCursor().setPos(0xFFFF, 0xFFFF);
|
||||||
|
#endif
|
||||||
|
|
||||||
app.setWindowIcon(QIcon(":/res/moonlight.svg"));
|
app.setWindowIcon(QIcon(":/res/moonlight.svg"));
|
||||||
|
|
||||||
// Register our C++ types for QML
|
// Register our C++ types for QML
|
||||||
qmlRegisterType<ComputerModel>("ComputerModel", 1, 0, "ComputerModel");
|
qmlRegisterType<ComputerModel>("ComputerModel", 1, 0, "ComputerModel");
|
||||||
qmlRegisterType<AppModel>("AppModel", 1, 0, "AppModel");
|
qmlRegisterType<AppModel>("AppModel", 1, 0, "AppModel");
|
||||||
qmlRegisterType<StreamingPreferences>("StreamingPreferences", 1, 0, "StreamingPreferences");
|
|
||||||
qmlRegisterType<SdlGamepadKeyNavigation>("SdlGamepadKeyNavigation", 1, 0, "SdlGamepadKeyNavigation");
|
|
||||||
qmlRegisterUncreatableType<Session>("Session", 1, 0, "Session", "Session cannot be created from QML");
|
qmlRegisterUncreatableType<Session>("Session", 1, 0, "Session", "Session cannot be created from QML");
|
||||||
qmlRegisterSingletonType<ComputerManager>("ComputerManager", 1, 0,
|
qmlRegisterSingletonType<ComputerManager>("ComputerManager", 1, 0,
|
||||||
"ComputerManager",
|
"ComputerManager",
|
||||||
@@ -311,8 +415,45 @@ int main(int argc, char *argv[])
|
|||||||
[](QQmlEngine*, QJSEngine*) -> QObject* {
|
[](QQmlEngine*, QJSEngine*) -> QObject* {
|
||||||
return new AutoUpdateChecker();
|
return new AutoUpdateChecker();
|
||||||
});
|
});
|
||||||
|
qmlRegisterSingletonType<SystemProperties>("SystemProperties", 1, 0,
|
||||||
|
"SystemProperties",
|
||||||
|
[](QQmlEngine*, QJSEngine*) -> QObject* {
|
||||||
|
return new SystemProperties();
|
||||||
|
});
|
||||||
|
qmlRegisterSingletonType<SdlGamepadKeyNavigation>("SdlGamepadKeyNavigation", 1, 0,
|
||||||
|
"SdlGamepadKeyNavigation",
|
||||||
|
[](QQmlEngine*, QJSEngine*) -> QObject* {
|
||||||
|
return new SdlGamepadKeyNavigation();
|
||||||
|
});
|
||||||
|
qmlRegisterSingletonType<StreamingPreferences>("StreamingPreferences", 1, 0,
|
||||||
|
"StreamingPreferences",
|
||||||
|
[](QQmlEngine*, QJSEngine*) -> QObject* {
|
||||||
|
return new StreamingPreferences();
|
||||||
|
});
|
||||||
|
|
||||||
QQuickStyle::setStyle("Material");
|
#ifndef Q_OS_WINRT
|
||||||
|
// Use the dense material dark theme by default
|
||||||
|
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_STYLE")) {
|
||||||
|
qputenv("QT_QUICK_CONTROLS_STYLE", "Material");
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
// Use universal dark on WinRT
|
||||||
|
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_STYLE")) {
|
||||||
|
qputenv("QT_QUICK_CONTROLS_STYLE", "Universal");
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_MATERIAL_THEME")) {
|
||||||
|
qputenv("QT_QUICK_CONTROLS_MATERIAL_THEME", "Dark");
|
||||||
|
}
|
||||||
|
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_MATERIAL_ACCENT")) {
|
||||||
|
qputenv("QT_QUICK_CONTROLS_MATERIAL_ACCENT", "Purple");
|
||||||
|
}
|
||||||
|
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_MATERIAL_VARIANT")) {
|
||||||
|
qputenv("QT_QUICK_CONTROLS_MATERIAL_VARIANT", "Dense");
|
||||||
|
}
|
||||||
|
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_UNIVERSAL_THEME")) {
|
||||||
|
qputenv("QT_QUICK_CONTROLS_UNIVERSAL_THEME", "Dark");
|
||||||
|
}
|
||||||
|
|
||||||
QQmlApplicationEngine engine;
|
QQmlApplicationEngine engine;
|
||||||
QString initialView;
|
QString initialView;
|
||||||
@@ -320,11 +461,11 @@ int main(int argc, char *argv[])
|
|||||||
GlobalCommandLineParser parser;
|
GlobalCommandLineParser parser;
|
||||||
switch (parser.parse(app.arguments())) {
|
switch (parser.parse(app.arguments())) {
|
||||||
case GlobalCommandLineParser::NormalStartRequested:
|
case GlobalCommandLineParser::NormalStartRequested:
|
||||||
initialView = "PcView.qml";
|
initialView = "qrc:/gui/PcView.qml";
|
||||||
break;
|
break;
|
||||||
case GlobalCommandLineParser::StreamRequested:
|
case GlobalCommandLineParser::StreamRequested:
|
||||||
{
|
{
|
||||||
initialView = "CliStartStreamSegue.qml";
|
initialView = "qrc:/gui/CliStartStreamSegue.qml";
|
||||||
StreamingPreferences* preferences = new StreamingPreferences(&app);
|
StreamingPreferences* preferences = new StreamingPreferences(&app);
|
||||||
StreamCommandLineParser streamParser;
|
StreamCommandLineParser streamParser;
|
||||||
streamParser.parse(app.arguments(), preferences);
|
streamParser.parse(app.arguments(), preferences);
|
||||||
@@ -334,6 +475,15 @@ int main(int argc, char *argv[])
|
|||||||
engine.rootContext()->setContextProperty("launcher", launcher);
|
engine.rootContext()->setContextProperty("launcher", launcher);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case GlobalCommandLineParser::QuitRequested:
|
||||||
|
{
|
||||||
|
initialView = "qrc:/gui/CliQuitStreamSegue.qml";
|
||||||
|
QuitCommandLineParser quitParser;
|
||||||
|
quitParser.parse(app.arguments());
|
||||||
|
auto launcher = new CliQuitStream::Launcher(quitParser.getHost(), &app);
|
||||||
|
engine.rootContext()->setContextProperty("launcher", launcher);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
engine.rootContext()->setContextProperty("initialView", initialView);
|
engine.rootContext()->setContextProperty("initialView", initialView);
|
||||||
@@ -343,23 +493,11 @@ int main(int argc, char *argv[])
|
|||||||
if (engine.rootObjects().isEmpty())
|
if (engine.rootObjects().isEmpty())
|
||||||
return -1;
|
return -1;
|
||||||
|
|
||||||
SDL_SetMainReady();
|
|
||||||
if (SDL_Init(0) != 0) {
|
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"SDL_Init() failed: %s",
|
|
||||||
SDL_GetError());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use atexit() to ensure SDL_Quit() is called. This avoids
|
|
||||||
// racing with object destruction where SDL may be used.
|
|
||||||
atexit(SDL_Quit);
|
|
||||||
|
|
||||||
// Avoid the default behavior of changing the timer resolution to 1 ms.
|
|
||||||
// We don't want this all the time that Moonlight is open. We will set
|
|
||||||
// it manually when we start streaming.
|
|
||||||
SDL_SetHint(SDL_HINT_TIMER_RESOLUTION, "0");
|
|
||||||
|
|
||||||
int err = app.exec();
|
int err = app.exec();
|
||||||
|
|
||||||
|
// Give worker tasks time to properly exit. Fixes PendingQuitTask
|
||||||
|
// sometimes freezing and blocking process exit.
|
||||||
|
QThreadPool::globalInstance()->waitForDone(30000);
|
||||||
|
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
|
Before Width: | Height: | Size: 345 KiB After Width: | Height: | Size: 328 KiB |
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 2.6 KiB |
@@ -8,7 +8,6 @@
|
|||||||
|
|
||||||
QString Path::s_LogDir;
|
QString Path::s_LogDir;
|
||||||
QString Path::s_BoxArtCacheDir;
|
QString Path::s_BoxArtCacheDir;
|
||||||
const QString Path::k_GameControllerMappingFile("gamecontrollerdb.txt");
|
|
||||||
|
|
||||||
QString Path::getLogDir()
|
QString Path::getLogDir()
|
||||||
{
|
{
|
||||||
@@ -22,40 +21,42 @@ QString Path::getBoxArtCacheDir()
|
|||||||
return s_BoxArtCacheDir;
|
return s_BoxArtCacheDir;
|
||||||
}
|
}
|
||||||
|
|
||||||
QString Path::getGamepadMappingFile()
|
QByteArray Path::readDataFile(QString fileName)
|
||||||
|
{
|
||||||
|
QFile dataFile(getDataFilePath(fileName));
|
||||||
|
dataFile.open(QIODevice::ReadOnly);
|
||||||
|
return dataFile.readAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString Path::getDataFilePath(QString fileName)
|
||||||
{
|
{
|
||||||
QString candidatePath;
|
QString candidatePath;
|
||||||
|
|
||||||
// Check the current directory first
|
// Check the current directory first
|
||||||
candidatePath = QDir(QDir::currentPath()).absoluteFilePath(k_GameControllerMappingFile);
|
candidatePath = QDir(QDir::currentPath()).absoluteFilePath(fileName);
|
||||||
if (QFile::exists(candidatePath)) {
|
if (QFile::exists(candidatePath)) {
|
||||||
|
qInfo() << "Found" << fileName << "at" << candidatePath;
|
||||||
return candidatePath;
|
return candidatePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now check the data directories (for Linux, in particular)
|
// Now check the data directories (for Linux, in particular)
|
||||||
candidatePath = QStandardPaths::locate(QStandardPaths::DataLocation, k_GameControllerMappingFile);
|
candidatePath = QStandardPaths::locate(QStandardPaths::DataLocation, fileName);
|
||||||
if (!candidatePath.isEmpty() && QFile::exists(candidatePath)) {
|
if (!candidatePath.isEmpty() && QFile::exists(candidatePath)) {
|
||||||
|
qInfo() << "Found" << fileName << "at" << candidatePath;
|
||||||
return candidatePath;
|
return candidatePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now try the directory of our app installation (for Windows, if current dir doesn't find it)
|
// Now try the directory of our app installation (for Windows, if current dir doesn't find it)
|
||||||
candidatePath = QDir(QCoreApplication::applicationDirPath()).absoluteFilePath(k_GameControllerMappingFile);
|
candidatePath = QDir(QCoreApplication::applicationDirPath()).absoluteFilePath(fileName);
|
||||||
if (QFile::exists(candidatePath)) {
|
if (QFile::exists(candidatePath)) {
|
||||||
|
qInfo() << "Found" << fileName << "at" << candidatePath;
|
||||||
return candidatePath;
|
return candidatePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now try the Resources folder in our app bundle for macOS
|
// Return the QRC embedded copy
|
||||||
QDir dir = QDir(QCoreApplication::applicationDirPath());
|
candidatePath = ":/data/" + fileName;
|
||||||
dir.cdUp();
|
qInfo() << "Found" << fileName << "at" << candidatePath;
|
||||||
dir.cd("Resources");
|
return QString(candidatePath);
|
||||||
dir.filePath(k_GameControllerMappingFile);
|
|
||||||
candidatePath = dir.absoluteFilePath(k_GameControllerMappingFile);
|
|
||||||
if (QFile::exists(candidatePath)) {
|
|
||||||
return candidatePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Couldn't find it
|
|
||||||
return QString();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Path::initialize(bool portable)
|
void Path::initialize(bool portable)
|
||||||
|
|||||||
@@ -9,12 +9,14 @@ public:
|
|||||||
|
|
||||||
static QString getBoxArtCacheDir();
|
static QString getBoxArtCacheDir();
|
||||||
|
|
||||||
static QString getGamepadMappingFile();
|
static QByteArray readDataFile(QString fileName);
|
||||||
|
|
||||||
|
// Only safe to use directly for Qt classes
|
||||||
|
static QString getDataFilePath(QString fileName);
|
||||||
|
|
||||||
static void initialize(bool portable);
|
static void initialize(bool portable);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static QString s_LogDir;
|
static QString s_LogDir;
|
||||||
static QString s_BoxArtCacheDir;
|
static QString s_BoxArtCacheDir;
|
||||||
static const QString k_GameControllerMappingFile;
|
|
||||||
};
|
};
|
||||||
@@ -10,7 +10,13 @@
|
|||||||
<file>gui/NavigableToolButton.qml</file>
|
<file>gui/NavigableToolButton.qml</file>
|
||||||
<file>gui/NavigableItemDelegate.qml</file>
|
<file>gui/NavigableItemDelegate.qml</file>
|
||||||
<file>gui/NavigableMenuItem.qml</file>
|
<file>gui/NavigableMenuItem.qml</file>
|
||||||
|
<file>gui/CliQuitStreamSegue.qml</file>
|
||||||
<file>gui/CliStartStreamSegue.qml</file>
|
<file>gui/CliStartStreamSegue.qml</file>
|
||||||
<file>gui/AutoResizingComboBox.qml</file>
|
<file>gui/AutoResizingComboBox.qml</file>
|
||||||
|
<file>gui/NavigableMenu.qml</file>
|
||||||
|
<file>gui/ErrorMessageDialog.qml</file>
|
||||||
|
<file>gui/NavigableMessageDialog.qml</file>
|
||||||
|
<file>gui/NavigableDialog.qml</file>
|
||||||
|
<file>gui/CenteredGridView.qml</file>
|
||||||
</qresource>
|
</qresource>
|
||||||
</RCC>
|
</RCC>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="#fff"><path d="M12 2C6.47 2 2 6.47 2 12s4.47 10 10 10 10-4.47 10-10S17.53 2 12 2zm5 13.59L15.59 17 12 13.41 8.41 17 7 15.59 10.59 12 7 8.41 8.41 7 12 10.59 15.59 7 17 8.41 13.41 12 17 15.59z"/><path d="M0 0h24v24H0z" fill="none"/></svg>
|
||||||
|
After Width: | Height: | Size: 325 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="#FFFFFF"><path fill="none" d="M0 0h24v24H0V0z"/><path d="M11 15h2v2h-2zm0-8h2v6h-2zm.99-5C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"/></svg>
|
||||||
|
After Width: | Height: | Size: 323 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="#FFFFFF"><path fill="none" d="M0 0h24v24H0z"/><path d="M11 18h2v-2h-2v2zm1-16C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm0-14c-2.21 0-4 1.79-4 4h2c0-1.1.9-2 2-2s2 .9 2 2c0 2-3 1.75-3 5h2c0-2.25 3-2.5 3-5 0-2.21-1.79-4-4-4z"/></svg>
|
||||||
|
After Width: | Height: | Size: 397 B |
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="#FFFFFF" width="48" height="48" viewBox="0 0 24 24">
|
||||||
|
<path fill="none" d="M0 0h24v24H0V0z"/>
|
||||||
|
<path d="M21 2H3c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h7v2H8v2h8v-2h-2v-2h7c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 14H3V4h18v12z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 276 B |
@@ -1,4 +0,0 @@
|
|||||||
<svg fill="#FFFFFF" height="48" viewBox="0 0 24 24" width="48" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M0 0h24v24H0z" fill="none"/>
|
|
||||||
<path d="M7 11v2h10v-2H7zm5-9C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 303 B |
@@ -1,9 +0,0 @@
|
|||||||
<svg fill="#FFFFFF" height="48" viewBox="0 0 24 24" width="48" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
||||||
<defs>
|
|
||||||
<path d="M0 0h24v24H0V0z" id="a"/>
|
|
||||||
</defs>
|
|
||||||
<clipPath id="b">
|
|
||||||
<use overflow="visible" xlink:href="#a"/>
|
|
||||||
</clipPath>
|
|
||||||
<path clip-path="url(#b)" d="M21 3H3c-1.11 0-2 .89-2 2v12c0 1.1.89 2 2 2h5v2h8v-2h5c1.1 0 1.99-.9 1.99-2L23 5c0-1.11-.9-2-2-2zm0 14H3V5h18v12zm-5-7v2H8v-2h8z"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 467 B |
@@ -1,4 +0,0 @@
|
|||||||
<svg fill="#FFFFFF" height="48" viewBox="0 0 24 24" width="48" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M0 0h24v24H0z" fill="none"/>
|
|
||||||
<path d="M21 3H3c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h5v2h8v-2h5c1.1 0 1.99-.9 1.99-2L23 5c0-1.1-.9-2-2-2zm0 14H3V5h18v12z"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 273 B |
@@ -1,69 +1 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
<?xml version="1.0" standalone="no"?><!-- Generator: Gravit.io --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="isolation:isolate" viewBox="0 0 256 256" width="256" height="256"><g><circle vector-effect="non-scaling-stroke" cx="128" cy="128" r="128" fill="rgb(86,92,100)"/><circle vector-effect="non-scaling-stroke" cx="128.00000000000003" cy="128.00000000000003" r="96" fill="rgb(255,255,255)"/><path d=" M 137.66 132 L 224 132 L 224 124 L 137.66 124 L 198.71 62.95 L 193.05 57.29 L 132 118.34 L 132 32 L 124 32 L 124 118.34 L 62.95 57.29 L 57.29 62.95 L 118.34 124 L 32 124 L 32 132 L 118.34 132 L 57.29 193.05 L 62.95 198.71 L 124 137.66 L 124 224 L 132 224 L 132 137.66 L 193.05 198.71 L 198.71 193.05 L 137.66 132 Z " fill-rule="evenodd" fill="rgb(86,92,100)"/></g></svg>
|
||||||
<svg
|
|
||||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
|
||||||
xmlns:cc="http://creativecommons.org/ns#"
|
|
||||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
id="svg3761"
|
|
||||||
viewBox="0 0 680 680"
|
|
||||||
height="680"
|
|
||||||
width="680"
|
|
||||||
version="1.0">
|
|
||||||
<metadata
|
|
||||||
id="metadata3767">
|
|
||||||
<rdf:RDF>
|
|
||||||
<cc:Work
|
|
||||||
rdf:about="">
|
|
||||||
<dc:format>image/svg+xml</dc:format>
|
|
||||||
<dc:type
|
|
||||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
|
||||||
<dc:title></dc:title>
|
|
||||||
</cc:Work>
|
|
||||||
</rdf:RDF>
|
|
||||||
</metadata>
|
|
||||||
<defs
|
|
||||||
id="defs3765" />
|
|
||||||
<g
|
|
||||||
id="g3759"
|
|
||||||
transform="translate(-135,491) scale(0.100000,-0.100000)">
|
|
||||||
<circle
|
|
||||||
id="circle3741"
|
|
||||||
fill="#565C64"
|
|
||||||
r="3400"
|
|
||||||
cy="1505"
|
|
||||||
cx="4755" />
|
|
||||||
<path
|
|
||||||
id="path3743"
|
|
||||||
style="fill:#ffffff"
|
|
||||||
d="M4550 4230 c-582 -48 -1123 -271 -1568 -646 l-84 -70 869 -869 c477 -477 871 -868 876 -868 4 0 6 552 5 1228 l-3 1227 -25 1 c-14 0 -45 -1 -70 -3z" />
|
|
||||||
<path
|
|
||||||
id="path3745"
|
|
||||||
style="fill:#ffffff"
|
|
||||||
d="M4867 4230 c-10 -9 -9 -2453 1 -2453 4 0 398 391 875 868 l869 869 -84 70 c-344 290 -749 492 -1178 587 -166 36 -469 74 -483 59z" />
|
|
||||||
<path
|
|
||||||
id="path3747"
|
|
||||||
style="fill:#ffffff"
|
|
||||||
d="M2673 3275 c-241 -285 -412 -595 -526 -953 -63 -196 -111 -458 -119 -640 l-3 -70 1228 -3 c675 -1 1227 1 1227 5 0 5 -391 399 -868 876 l-869 869 -70 -84z" />
|
|
||||||
<path
|
|
||||||
id="path3749"
|
|
||||||
style="fill:#ffffff"
|
|
||||||
d="M5898 2490 c-478 -477 -868 -871 -868 -876 0 -4 552 -6 1228 -5 l1227 3 -3 70 c-14 326 -124 729 -287 1055 -91 182 -259 430 -387 573 l-43 48 -867 -868z" />
|
|
||||||
<path
|
|
||||||
id="path3751"
|
|
||||||
style="fill:#ffffff"
|
|
||||||
d="M2026 1388 c-7 -11 10 -198 30 -321 76 -486 291 -952 617 -1338 l70 -84 869 869 c477 477 868 871 868 875 0 11 -2448 10 -2454 -1z" />
|
|
||||||
<path
|
|
||||||
id="path3753"
|
|
||||||
style="fill:#ffffff"
|
|
||||||
d="M5030 1390 c0 -5 391 -399 868 -876 l869 -869 70 84 c241 285 412 595 526 953 63 196 111 458 119 640 l3 70 -1227 3 c-676 1 -1228 -1 -1228 -5z" />
|
|
||||||
<path
|
|
||||||
id="path3755"
|
|
||||||
style="fill:#ffffff"
|
|
||||||
d="M3767 359 l-869 -869 84 -70 c285 -241 595 -412 953 -526 196 -63 458 -111 640 -119 l70 -3 3 1228 c1 675 -1 1227 -5 1227 -5 0 -399 -391 -876 -868z" />
|
|
||||||
<path
|
|
||||||
id="path3757"
|
|
||||||
style="fill:#ffffff"
|
|
||||||
d="M4862 0 l3 -1228 70 3 c182 8 444 56 640 119 358 114 668 285 953 526 l84 70 -869 869 c-477 477 -871 868 -876 868 -4 0 -6 -552 -5 -1227z" />
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 820 B |
@@ -1,3 +1,3 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="24" height="24" viewBox="0 0 24 24" fill="#0BF200">
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="24" height="24" viewBox="0 0 24 24" fill="#FF0000">
|
||||||
<path d="M21,10.12H14.22L16.96,7.3C14.23,4.6 9.81,4.5 7.08,7.2C4.35,9.91 4.35,14.28 7.08,17C9.81,19.7 14.23,19.7 16.96,17C18.32,15.65 19,14.08 19,12.1H21C21,14.08 20.12,16.65 18.36,18.39C14.85,21.87 9.15,21.87 5.64,18.39C2.14,14.92 2.11,9.28 5.62,5.81C9.13,2.34 14.76,2.34 18.27,5.81L21,3V10.12M12.5,8V12.25L16,14.33L15.28,15.54L11,13V8H12.5Z" />
|
<path d="M21,10.12H14.22L16.96,7.3C14.23,4.6 9.81,4.5 7.08,7.2C4.35,9.91 4.35,14.28 7.08,17C9.81,19.7 14.23,19.7 16.96,17C18.32,15.65 19,14.08 19,12.1H21C21,14.08 20.12,16.65 18.36,18.39C14.85,21.87 9.15,21.87 5.64,18.39C2.14,14.92 2.11,9.28 5.62,5.81C9.13,2.34 14.76,2.34 18.27,5.81L21,3V10.12M12.5,8V12.25L16,14.33L15.28,15.54L11,13V8H12.5Z" />
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 499 B After Width: | Height: | Size: 499 B |
@@ -1,18 +1,23 @@
|
|||||||
<RCC>
|
<RCC>
|
||||||
<qresource prefix="/">
|
<qresource prefix="/">
|
||||||
<file>res/ic_remove_circle_outline_white_48px.svg</file>
|
|
||||||
<file>res/ic_remove_from_queue_white_48px.svg</file>
|
|
||||||
<file>res/ic_videogame_asset_white_48px.svg</file>
|
<file>res/ic_videogame_asset_white_48px.svg</file>
|
||||||
<file>res/ic_tv_white_48px.svg</file>
|
<file>res/desktop_windows-48px.svg</file>
|
||||||
<file>res/ic_add_to_queue_white_48px.svg</file>
|
<file>res/ic_add_to_queue_white_48px.svg</file>
|
||||||
<file>res/baseline-lock-24px.svg</file>
|
<file>res/baseline-lock-24px.svg</file>
|
||||||
<file>res/baseline-play_circle_filled_white-48px.svg</file>
|
<file>res/baseline-play_circle_filled_white-48px.svg</file>
|
||||||
<file>res/baseline-warning-24px.svg</file>
|
<file>res/baseline-warning-24px.svg</file>
|
||||||
|
<file>res/baseline-cancel-24px.svg</file>
|
||||||
<file>res/no_app_image.png</file>
|
<file>res/no_app_image.png</file>
|
||||||
<file>res/settings.svg</file>
|
<file>res/settings.svg</file>
|
||||||
<file>res/arrow_left.svg</file>
|
<file>res/arrow_left.svg</file>
|
||||||
<file>res/question_mark.svg</file>
|
<file>res/question_mark.svg</file>
|
||||||
<file>res/moonlight.svg</file>
|
<file>res/moonlight.svg</file>
|
||||||
<file>res/update.svg</file>
|
<file>res/update.svg</file>
|
||||||
|
<file>res/baseline-help_outline-24px.svg</file>
|
||||||
|
<file>res/baseline-error_outline-24px.svg</file>
|
||||||
|
</qresource>
|
||||||
|
<qresource prefix="/data">
|
||||||
|
<file alias="gamecontrollerdb.txt">SDL_GameControllerDB/gamecontrollerdb.txt</file>
|
||||||
|
<file alias="ModeSeven.ttf">ModeSeven.ttf</file>
|
||||||
</qresource>
|
</qresource>
|
||||||
</RCC>
|
</RCC>
|
||||||
@@ -54,15 +54,10 @@ void MappingManager::save()
|
|||||||
|
|
||||||
void MappingManager::applyMappings()
|
void MappingManager::applyMappings()
|
||||||
{
|
{
|
||||||
QString mappingFile = Path::getGamepadMappingFile();
|
QByteArray mappingData = Path::readDataFile("gamecontrollerdb.txt");
|
||||||
if (!mappingFile.isEmpty()) {
|
if (!mappingData.isEmpty()) {
|
||||||
std::string mappingFileNative = QDir::toNativeSeparators(mappingFile).toStdString();
|
int newMappings = SDL_GameControllerAddMappingsFromRW(
|
||||||
|
SDL_RWFromConstMem(mappingData.constData(), mappingData.size()), 1);
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Loading gamepad mappings from: %s",
|
|
||||||
mappingFileNative.c_str());
|
|
||||||
|
|
||||||
int newMappings = SDL_GameControllerAddMappingsFromFile(mappingFileNative.c_str());
|
|
||||||
if (newMappings < 0) {
|
if (newMappings < 0) {
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"Error loading gamepad mappings: %s",
|
"Error loading gamepad mappings: %s",
|
||||||
@@ -75,8 +70,8 @@ void MappingManager::applyMappings()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"No gamepad mapping file found");
|
"Unable to load gamepad mapping file");
|
||||||
}
|
}
|
||||||
|
|
||||||
QList<SdlGamepadMapping> mappings = m_Mappings.values();
|
QList<SdlGamepadMapping> mappings = m_Mappings.values();
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
#include "streamingpreferences.h"
|
#include "streamingpreferences.h"
|
||||||
#include "streaming/session.h"
|
|
||||||
#include "streaming/streamutils.h"
|
|
||||||
|
|
||||||
#include <QSettings>
|
#include <QSettings>
|
||||||
|
|
||||||
@@ -20,7 +18,16 @@
|
|||||||
#define SER_WINDOWMODE "windowmode"
|
#define SER_WINDOWMODE "windowmode"
|
||||||
#define SER_UNSUPPORTEDFPS "unsupportedfps"
|
#define SER_UNSUPPORTEDFPS "unsupportedfps"
|
||||||
#define SER_MDNS "mdns"
|
#define SER_MDNS "mdns"
|
||||||
|
#define SER_QUITAPPAFTER "quitAppAfter"
|
||||||
#define SER_MOUSEACCELERATION "mouseacceleration"
|
#define SER_MOUSEACCELERATION "mouseacceleration"
|
||||||
|
#define SER_STARTWINDOWED "startwindowed"
|
||||||
|
#define SER_FRAMEPACING "framepacing"
|
||||||
|
#define SER_CONNWARNINGS "connwarnings"
|
||||||
|
#define SER_RICHPRESENCE "richpresence"
|
||||||
|
#define SER_GAMEPADMOUSE "gamepadmouse"
|
||||||
|
#define SER_DEFAULTVER "defaultver"
|
||||||
|
|
||||||
|
#define CURRENT_DEFAULT_VER 1
|
||||||
|
|
||||||
StreamingPreferences::StreamingPreferences(QObject *parent)
|
StreamingPreferences::StreamingPreferences(QObject *parent)
|
||||||
: QObject(parent)
|
: QObject(parent)
|
||||||
@@ -32,6 +39,14 @@ void StreamingPreferences::reload()
|
|||||||
{
|
{
|
||||||
QSettings settings;
|
QSettings settings;
|
||||||
|
|
||||||
|
int defaultVer = settings.value(SER_DEFAULTVER, 0).toInt();
|
||||||
|
|
||||||
|
#ifdef Q_OS_DARWIN
|
||||||
|
recommendedFullScreenMode = WindowMode::WM_FULLSCREEN_DESKTOP;
|
||||||
|
#else
|
||||||
|
recommendedFullScreenMode = WindowMode::WM_FULLSCREEN;
|
||||||
|
#endif
|
||||||
|
|
||||||
width = settings.value(SER_WIDTH, 1280).toInt();
|
width = settings.value(SER_WIDTH, 1280).toInt();
|
||||||
height = settings.value(SER_HEIGHT, 720).toInt();
|
height = settings.value(SER_HEIGHT, 720).toInt();
|
||||||
fps = settings.value(SER_FPS, 60).toInt();
|
fps = settings.value(SER_FPS, 60).toInt();
|
||||||
@@ -42,7 +57,13 @@ void StreamingPreferences::reload()
|
|||||||
multiController = settings.value(SER_MULTICONT, true).toBool();
|
multiController = settings.value(SER_MULTICONT, true).toBool();
|
||||||
unsupportedFps = settings.value(SER_UNSUPPORTEDFPS, false).toBool();
|
unsupportedFps = settings.value(SER_UNSUPPORTEDFPS, false).toBool();
|
||||||
enableMdns = settings.value(SER_MDNS, true).toBool();
|
enableMdns = settings.value(SER_MDNS, true).toBool();
|
||||||
|
quitAppAfter = settings.value(SER_QUITAPPAFTER, false).toBool();
|
||||||
mouseAcceleration = settings.value(SER_MOUSEACCELERATION, false).toBool();
|
mouseAcceleration = settings.value(SER_MOUSEACCELERATION, false).toBool();
|
||||||
|
startWindowed = settings.value(SER_STARTWINDOWED, false).toBool();
|
||||||
|
framePacing = settings.value(SER_FRAMEPACING, false).toBool();
|
||||||
|
connectionWarnings = settings.value(SER_CONNWARNINGS, true).toBool();
|
||||||
|
richPresence = settings.value(SER_RICHPRESENCE, true).toBool();
|
||||||
|
gamepadMouse = settings.value(SER_GAMEPADMOUSE, true).toBool();
|
||||||
audioConfig = static_cast<AudioConfig>(settings.value(SER_AUDIOCFG,
|
audioConfig = static_cast<AudioConfig>(settings.value(SER_AUDIOCFG,
|
||||||
static_cast<int>(AudioConfig::AC_STEREO)).toInt());
|
static_cast<int>(AudioConfig::AC_STEREO)).toInt());
|
||||||
videoCodecConfig = static_cast<VideoCodecConfig>(settings.value(SER_VIDEOCFG,
|
videoCodecConfig = static_cast<VideoCodecConfig>(settings.value(SER_VIDEOCFG,
|
||||||
@@ -52,7 +73,17 @@ void StreamingPreferences::reload()
|
|||||||
windowMode = static_cast<WindowMode>(settings.value(SER_WINDOWMODE,
|
windowMode = static_cast<WindowMode>(settings.value(SER_WINDOWMODE,
|
||||||
// Try to load from the old preference value too
|
// Try to load from the old preference value too
|
||||||
static_cast<int>(settings.value(SER_FULLSCREEN, true).toBool() ?
|
static_cast<int>(settings.value(SER_FULLSCREEN, true).toBool() ?
|
||||||
WindowMode::WM_FULLSCREEN : WindowMode::WM_WINDOWED)).toInt());
|
recommendedFullScreenMode : WindowMode::WM_WINDOWED)).toInt());
|
||||||
|
|
||||||
|
// Perform default settings updates as required based on last default version
|
||||||
|
if (defaultVer == 0) {
|
||||||
|
#ifdef Q_OS_DARWIN
|
||||||
|
// Update window mode setting on macOS from full-screen (old default) to borderless windowed (new default)
|
||||||
|
if (windowMode == WindowMode::WM_FULLSCREEN) {
|
||||||
|
windowMode = WindowMode::WM_FULLSCREEN_DESKTOP;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void StreamingPreferences::save()
|
void StreamingPreferences::save()
|
||||||
@@ -69,134 +100,18 @@ void StreamingPreferences::save()
|
|||||||
settings.setValue(SER_MULTICONT, multiController);
|
settings.setValue(SER_MULTICONT, multiController);
|
||||||
settings.setValue(SER_UNSUPPORTEDFPS, unsupportedFps);
|
settings.setValue(SER_UNSUPPORTEDFPS, unsupportedFps);
|
||||||
settings.setValue(SER_MDNS, enableMdns);
|
settings.setValue(SER_MDNS, enableMdns);
|
||||||
|
settings.setValue(SER_QUITAPPAFTER, quitAppAfter);
|
||||||
settings.setValue(SER_MOUSEACCELERATION, mouseAcceleration);
|
settings.setValue(SER_MOUSEACCELERATION, mouseAcceleration);
|
||||||
|
settings.setValue(SER_STARTWINDOWED, startWindowed);
|
||||||
|
settings.setValue(SER_FRAMEPACING, framePacing);
|
||||||
|
settings.setValue(SER_CONNWARNINGS, connectionWarnings);
|
||||||
|
settings.setValue(SER_RICHPRESENCE, richPresence);
|
||||||
|
settings.setValue(SER_GAMEPADMOUSE, gamepadMouse);
|
||||||
settings.setValue(SER_AUDIOCFG, static_cast<int>(audioConfig));
|
settings.setValue(SER_AUDIOCFG, static_cast<int>(audioConfig));
|
||||||
settings.setValue(SER_VIDEOCFG, static_cast<int>(videoCodecConfig));
|
settings.setValue(SER_VIDEOCFG, static_cast<int>(videoCodecConfig));
|
||||||
settings.setValue(SER_VIDEODEC, static_cast<int>(videoDecoderSelection));
|
settings.setValue(SER_VIDEODEC, static_cast<int>(videoDecoderSelection));
|
||||||
settings.setValue(SER_WINDOWMODE, static_cast<int>(windowMode));
|
settings.setValue(SER_WINDOWMODE, static_cast<int>(windowMode));
|
||||||
}
|
settings.setValue(SER_DEFAULTVER, CURRENT_DEFAULT_VER);
|
||||||
|
|
||||||
bool StreamingPreferences::hasAnyHardwareAcceleration()
|
|
||||||
{
|
|
||||||
// Always use VDS_AUTO to avoid spamming the user with warnings
|
|
||||||
// if they've forced software decoding.
|
|
||||||
return Session::isHardwareDecodeAvailable(VDS_AUTO,
|
|
||||||
VIDEO_FORMAT_H264,
|
|
||||||
1920, 1080, 60);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool StreamingPreferences::isRunningWayland()
|
|
||||||
{
|
|
||||||
return qgetenv("XDG_SESSION_TYPE") == QByteArray("wayland");
|
|
||||||
}
|
|
||||||
|
|
||||||
bool StreamingPreferences::isWow64()
|
|
||||||
{
|
|
||||||
#ifdef Q_OS_WIN32
|
|
||||||
return QSysInfo::currentCpuArchitecture() != QSysInfo::buildCpuArchitecture();
|
|
||||||
#else
|
|
||||||
return false;
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
QString StreamingPreferences::getUnmappedGamepads()
|
|
||||||
{
|
|
||||||
return SdlInputHandler::getUnmappedGamepads();
|
|
||||||
}
|
|
||||||
|
|
||||||
int StreamingPreferences::getMaximumStreamingFrameRate()
|
|
||||||
{
|
|
||||||
// Never let the maximum drop below 60 FPS
|
|
||||||
int maxFrameRate = 60;
|
|
||||||
|
|
||||||
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
|
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s",
|
|
||||||
SDL_GetError());
|
|
||||||
return maxFrameRate;
|
|
||||||
}
|
|
||||||
|
|
||||||
SDL_DisplayMode mode, bestMode, desktopMode;
|
|
||||||
for (int displayIndex = 0; displayIndex < SDL_GetNumVideoDisplays(); displayIndex++) {
|
|
||||||
// Get the native desktop resolution
|
|
||||||
if (StreamUtils::getRealDesktopMode(displayIndex, &desktopMode)) {
|
|
||||||
|
|
||||||
// Start at desktop mode and work our way up
|
|
||||||
bestMode = desktopMode;
|
|
||||||
for (int i = 0; i < SDL_GetNumDisplayModes(displayIndex); i++) {
|
|
||||||
if (SDL_GetDisplayMode(displayIndex, i, &mode) == 0) {
|
|
||||||
if (mode.w == desktopMode.w && mode.h == desktopMode.h) {
|
|
||||||
if (mode.refresh_rate > bestMode.refresh_rate) {
|
|
||||||
bestMode = mode;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cap the frame rate at 120 FPS. Past this, the encoders start
|
|
||||||
// to max out and drop frames.
|
|
||||||
maxFrameRate = qMax(maxFrameRate, qMin(120, bestMode.refresh_rate));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
|
||||||
|
|
||||||
return maxFrameRate;
|
|
||||||
}
|
|
||||||
|
|
||||||
QRect StreamingPreferences::getDesktopResolution(int displayIndex)
|
|
||||||
{
|
|
||||||
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
|
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s",
|
|
||||||
SDL_GetError());
|
|
||||||
return QRect();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (displayIndex >= SDL_GetNumVideoDisplays()) {
|
|
||||||
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
|
||||||
return QRect();
|
|
||||||
}
|
|
||||||
|
|
||||||
SDL_DisplayMode mode;
|
|
||||||
int err = SDL_GetDesktopDisplayMode(displayIndex, &mode);
|
|
||||||
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
|
||||||
|
|
||||||
if (err == 0) {
|
|
||||||
return QRect(0, 0, mode.w, mode.h);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"SDL_GetDesktopDisplayMode() failed: %s",
|
|
||||||
SDL_GetError());
|
|
||||||
return QRect();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QRect StreamingPreferences::getNativeResolution(int displayIndex)
|
|
||||||
{
|
|
||||||
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
|
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s",
|
|
||||||
SDL_GetError());
|
|
||||||
return QRect();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (displayIndex >= SDL_GetNumVideoDisplays()) {
|
|
||||||
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
|
||||||
return QRect();
|
|
||||||
}
|
|
||||||
|
|
||||||
SDL_DisplayMode mode;
|
|
||||||
bool success = StreamUtils::getRealDesktopMode(displayIndex, &mode);
|
|
||||||
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
|
||||||
|
|
||||||
if (success) {
|
|
||||||
return QRect(0, 0, mode.w, mode.h);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return QRect();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int StreamingPreferences::getDefaultBitrate(int width, int height, int fps)
|
int StreamingPreferences::getDefaultBitrate(int width, int height, int fps)
|
||||||
|
|||||||
@@ -15,20 +15,6 @@ public:
|
|||||||
|
|
||||||
Q_INVOKABLE void save();
|
Q_INVOKABLE void save();
|
||||||
|
|
||||||
Q_INVOKABLE static bool hasAnyHardwareAcceleration();
|
|
||||||
|
|
||||||
Q_INVOKABLE static bool isRunningWayland();
|
|
||||||
|
|
||||||
Q_INVOKABLE static bool isWow64();
|
|
||||||
|
|
||||||
Q_INVOKABLE static int getMaximumStreamingFrameRate();
|
|
||||||
|
|
||||||
Q_INVOKABLE QRect getDesktopResolution(int displayIndex);
|
|
||||||
|
|
||||||
Q_INVOKABLE QRect getNativeResolution(int displayIndex);
|
|
||||||
|
|
||||||
Q_INVOKABLE QString getUnmappedGamepads();
|
|
||||||
|
|
||||||
void reload();
|
void reload();
|
||||||
|
|
||||||
enum AudioConfig
|
enum AudioConfig
|
||||||
@@ -73,11 +59,18 @@ public:
|
|||||||
Q_PROPERTY(bool multiController MEMBER multiController NOTIFY multiControllerChanged)
|
Q_PROPERTY(bool multiController MEMBER multiController NOTIFY multiControllerChanged)
|
||||||
Q_PROPERTY(bool unsupportedFps MEMBER unsupportedFps NOTIFY unsupportedFpsChanged)
|
Q_PROPERTY(bool unsupportedFps MEMBER unsupportedFps NOTIFY unsupportedFpsChanged)
|
||||||
Q_PROPERTY(bool enableMdns MEMBER enableMdns NOTIFY enableMdnsChanged)
|
Q_PROPERTY(bool enableMdns MEMBER enableMdns NOTIFY enableMdnsChanged)
|
||||||
|
Q_PROPERTY(bool quitAppAfter MEMBER quitAppAfter NOTIFY quitAppAfterChanged)
|
||||||
Q_PROPERTY(bool mouseAcceleration MEMBER mouseAcceleration NOTIFY mouseAccelerationChanged)
|
Q_PROPERTY(bool mouseAcceleration MEMBER mouseAcceleration NOTIFY mouseAccelerationChanged)
|
||||||
|
Q_PROPERTY(bool startWindowed MEMBER startWindowed NOTIFY startWindowedChanged)
|
||||||
|
Q_PROPERTY(bool framePacing MEMBER framePacing NOTIFY framePacingChanged)
|
||||||
|
Q_PROPERTY(bool connectionWarnings MEMBER connectionWarnings NOTIFY connectionWarningsChanged)
|
||||||
|
Q_PROPERTY(bool richPresence MEMBER richPresence NOTIFY richPresenceChanged)
|
||||||
|
Q_PROPERTY(bool gamepadMouse MEMBER gamepadMouse NOTIFY gamepadMouseChanged)
|
||||||
Q_PROPERTY(AudioConfig audioConfig MEMBER audioConfig NOTIFY audioConfigChanged)
|
Q_PROPERTY(AudioConfig audioConfig MEMBER audioConfig NOTIFY audioConfigChanged)
|
||||||
Q_PROPERTY(VideoCodecConfig videoCodecConfig MEMBER videoCodecConfig NOTIFY videoCodecConfigChanged)
|
Q_PROPERTY(VideoCodecConfig videoCodecConfig MEMBER videoCodecConfig NOTIFY videoCodecConfigChanged)
|
||||||
Q_PROPERTY(VideoDecoderSelection videoDecoderSelection MEMBER videoDecoderSelection NOTIFY videoDecoderSelectionChanged)
|
Q_PROPERTY(VideoDecoderSelection videoDecoderSelection MEMBER videoDecoderSelection NOTIFY videoDecoderSelectionChanged)
|
||||||
Q_PROPERTY(WindowMode windowMode MEMBER windowMode NOTIFY windowModeChanged)
|
Q_PROPERTY(WindowMode windowMode MEMBER windowMode NOTIFY windowModeChanged)
|
||||||
|
Q_PROPERTY(WindowMode recommendedFullScreenMode MEMBER recommendedFullScreenMode CONSTANT)
|
||||||
|
|
||||||
// Directly accessible members for preferences
|
// Directly accessible members for preferences
|
||||||
int width;
|
int width;
|
||||||
@@ -90,11 +83,18 @@ public:
|
|||||||
bool multiController;
|
bool multiController;
|
||||||
bool unsupportedFps;
|
bool unsupportedFps;
|
||||||
bool enableMdns;
|
bool enableMdns;
|
||||||
|
bool quitAppAfter;
|
||||||
bool mouseAcceleration;
|
bool mouseAcceleration;
|
||||||
|
bool startWindowed;
|
||||||
|
bool framePacing;
|
||||||
|
bool connectionWarnings;
|
||||||
|
bool richPresence;
|
||||||
|
bool gamepadMouse;
|
||||||
AudioConfig audioConfig;
|
AudioConfig audioConfig;
|
||||||
VideoCodecConfig videoCodecConfig;
|
VideoCodecConfig videoCodecConfig;
|
||||||
VideoDecoderSelection videoDecoderSelection;
|
VideoDecoderSelection videoDecoderSelection;
|
||||||
WindowMode windowMode;
|
WindowMode windowMode;
|
||||||
|
WindowMode recommendedFullScreenMode;
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void displayModeChanged();
|
void displayModeChanged();
|
||||||
@@ -105,10 +105,16 @@ signals:
|
|||||||
void multiControllerChanged();
|
void multiControllerChanged();
|
||||||
void unsupportedFpsChanged();
|
void unsupportedFpsChanged();
|
||||||
void enableMdnsChanged();
|
void enableMdnsChanged();
|
||||||
|
void quitAppAfterChanged();
|
||||||
void mouseAccelerationChanged();
|
void mouseAccelerationChanged();
|
||||||
void audioConfigChanged();
|
void audioConfigChanged();
|
||||||
void videoCodecConfigChanged();
|
void videoCodecConfigChanged();
|
||||||
void videoDecoderSelectionChanged();
|
void videoDecoderSelectionChanged();
|
||||||
void windowModeChanged();
|
void windowModeChanged();
|
||||||
|
void startWindowedChanged();
|
||||||
|
void framePacingChanged();
|
||||||
|
void connectionWarningsChanged();
|
||||||
|
void richPresenceChanged();
|
||||||
|
void gamepadMouseChanged();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -3,34 +3,116 @@
|
|||||||
|
|
||||||
#ifdef HAVE_SOUNDIO
|
#ifdef HAVE_SOUNDIO
|
||||||
#include "renderers/soundioaudiorenderer.h"
|
#include "renderers/soundioaudiorenderer.h"
|
||||||
#else
|
|
||||||
#include "renderers/sdl.h"
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifdef HAVE_SLAUDIO
|
||||||
|
#include "renderers/slaud.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include "renderers/sdl.h"
|
||||||
|
|
||||||
#include <Limelight.h>
|
#include <Limelight.h>
|
||||||
|
|
||||||
IAudioRenderer* Session::createAudioRenderer()
|
#define TRY_INIT_RENDERER(renderer, opusConfig) \
|
||||||
{
|
{ \
|
||||||
#ifdef HAVE_SOUNDIO
|
IAudioRenderer* __renderer = new renderer(); \
|
||||||
return new SoundIoAudioRenderer();
|
if (__renderer->prepareForPlayback(opusConfig)) \
|
||||||
#else
|
return __renderer; \
|
||||||
return new SdlAudioRenderer();
|
delete __renderer; \
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Session::testAudio(int audioConfiguration)
|
IAudioRenderer* Session::createAudioRenderer(const POPUS_MULTISTREAM_CONFIGURATION opusConfig)
|
||||||
{
|
{
|
||||||
IAudioRenderer* audioRenderer;
|
// Handle explicit ML_AUDIO setting and fail if the requested backend fails
|
||||||
|
QString mlAudio = qgetenv("ML_AUDIO").toLower();
|
||||||
audioRenderer = createAudioRenderer();
|
if (mlAudio == "sdl") {
|
||||||
if (audioRenderer == nullptr) {
|
TRY_INIT_RENDERER(SdlAudioRenderer, opusConfig)
|
||||||
return false;
|
return nullptr;
|
||||||
|
}
|
||||||
|
#ifdef HAVE_SOUNDIO
|
||||||
|
else if (mlAudio == "libsoundio") {
|
||||||
|
TRY_INIT_RENDERER(SoundIoAudioRenderer, opusConfig)
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
#if defined(HAVE_SLAUDIO)
|
||||||
|
else if (mlAudio == "slaudio") {
|
||||||
|
TRY_INIT_RENDERER(SLAudioRenderer, opusConfig)
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
else if (!mlAudio.isEmpty()) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Unknown audio backend: %s",
|
||||||
|
SDL_getenv("ML_AUDIO"));
|
||||||
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -------------- Automatic backend selection below this line ---------------
|
||||||
|
|
||||||
|
#if defined(HAVE_SLAUDIO)
|
||||||
|
// Steam Link should always have SLAudio
|
||||||
|
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.
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
int Session::getAudioRendererCapabilities(int audioConfiguration)
|
||||||
|
{
|
||||||
// Build a fake OPUS_MULTISTREAM_CONFIGURATION to give
|
// Build a fake OPUS_MULTISTREAM_CONFIGURATION to give
|
||||||
// the renderer the channel count and sample rate.
|
// the renderer the channel count and sample rate.
|
||||||
OPUS_MULTISTREAM_CONFIGURATION opusConfig = {};
|
OPUS_MULTISTREAM_CONFIGURATION opusConfig = {};
|
||||||
opusConfig.sampleRate = 48000;
|
opusConfig.sampleRate = 48000;
|
||||||
|
opusConfig.samplesPerFrame = 240;
|
||||||
|
|
||||||
|
switch (audioConfiguration)
|
||||||
|
{
|
||||||
|
case AUDIO_CONFIGURATION_STEREO:
|
||||||
|
opusConfig.channelCount = 2;
|
||||||
|
break;
|
||||||
|
case AUDIO_CONFIGURATION_51_SURROUND:
|
||||||
|
opusConfig.channelCount = 6;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
SDL_assert(false);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
IAudioRenderer* audioRenderer = createAudioRenderer(&opusConfig);
|
||||||
|
if (audioRenderer == nullptr) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int caps = audioRenderer->getCapabilities();
|
||||||
|
|
||||||
|
delete audioRenderer;
|
||||||
|
|
||||||
|
return caps;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Session::testAudio(int audioConfiguration)
|
||||||
|
{
|
||||||
|
// Build a fake OPUS_MULTISTREAM_CONFIGURATION to give
|
||||||
|
// the renderer the channel count and sample rate.
|
||||||
|
OPUS_MULTISTREAM_CONFIGURATION opusConfig = {};
|
||||||
|
opusConfig.sampleRate = 48000;
|
||||||
|
opusConfig.samplesPerFrame = 240;
|
||||||
|
|
||||||
switch (audioConfiguration)
|
switch (audioConfiguration)
|
||||||
{
|
{
|
||||||
@@ -45,11 +127,14 @@ bool Session::testAudio(int audioConfiguration)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ret = audioRenderer->prepareForPlayback(&opusConfig);
|
IAudioRenderer* audioRenderer = createAudioRenderer(&opusConfig);
|
||||||
|
if (audioRenderer == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
delete audioRenderer;
|
delete audioRenderer;
|
||||||
|
|
||||||
return ret;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
int Session::arInit(int /* audioConfiguration */,
|
int Session::arInit(int /* audioConfiguration */,
|
||||||
@@ -60,30 +145,34 @@ int Session::arInit(int /* audioConfiguration */,
|
|||||||
|
|
||||||
SDL_memcpy(&s_ActiveSession->m_AudioConfig, opusConfig, sizeof(*opusConfig));
|
SDL_memcpy(&s_ActiveSession->m_AudioConfig, opusConfig, sizeof(*opusConfig));
|
||||||
|
|
||||||
|
s_ActiveSession->m_AudioRenderer = s_ActiveSession->createAudioRenderer(&s_ActiveSession->m_AudioConfig);
|
||||||
|
if (s_ActiveSession->m_AudioRenderer == nullptr) {
|
||||||
|
return -2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow the chosen renderer to remap Opus channels as needed to ensure proper output
|
||||||
|
s_ActiveSession->m_AudioRenderer->remapChannels(&s_ActiveSession->m_AudioConfig);
|
||||||
|
|
||||||
|
// Create the Opus decoder with the renderer's preferred channel mapping
|
||||||
s_ActiveSession->m_OpusDecoder =
|
s_ActiveSession->m_OpusDecoder =
|
||||||
opus_multistream_decoder_create(opusConfig->sampleRate,
|
opus_multistream_decoder_create(s_ActiveSession->m_AudioConfig.sampleRate,
|
||||||
opusConfig->channelCount,
|
s_ActiveSession->m_AudioConfig.channelCount,
|
||||||
opusConfig->streams,
|
s_ActiveSession->m_AudioConfig.streams,
|
||||||
opusConfig->coupledStreams,
|
s_ActiveSession->m_AudioConfig.coupledStreams,
|
||||||
opusConfig->mapping,
|
s_ActiveSession->m_AudioConfig.mapping,
|
||||||
&error);
|
&error);
|
||||||
if (s_ActiveSession->m_OpusDecoder == NULL) {
|
if (s_ActiveSession->m_OpusDecoder == NULL) {
|
||||||
|
delete s_ActiveSession->m_AudioRenderer;
|
||||||
|
s_ActiveSession->m_AudioRenderer = nullptr;
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"Failed to create decoder: %d",
|
"Failed to create decoder: %d",
|
||||||
error);
|
error);
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
s_ActiveSession->m_AudioRenderer = s_ActiveSession->createAudioRenderer();
|
|
||||||
if (!s_ActiveSession->m_AudioRenderer->prepareForPlayback(opusConfig)) {
|
|
||||||
delete s_ActiveSession->m_AudioRenderer;
|
|
||||||
opus_multistream_decoder_destroy(s_ActiveSession->m_OpusDecoder);
|
|
||||||
return -2;
|
|
||||||
}
|
|
||||||
|
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"Audio stream has %d channels",
|
"Audio stream has %d channels",
|
||||||
opusConfig->channelCount);
|
s_ActiveSession->m_AudioConfig.channelCount);
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -101,8 +190,11 @@ void Session::arDecodeAndPlaySample(char* sampleData, int sampleLength)
|
|||||||
{
|
{
|
||||||
int samplesDecoded;
|
int samplesDecoded;
|
||||||
|
|
||||||
// Set this thread to high priority to reduce
|
#ifndef STEAM_LINK
|
||||||
// the chance of missing our sample delivery time
|
// Set this thread to high priority to reduce the chance of missing
|
||||||
|
// our sample delivery time. On Steam Link, this causes starvation
|
||||||
|
// of other threads due to severely restricted CPU time available,
|
||||||
|
// so we will skip it on that platform.
|
||||||
if (s_ActiveSession->m_AudioSampleCount == 0) {
|
if (s_ActiveSession->m_AudioSampleCount == 0) {
|
||||||
if (SDL_SetThreadPriority(SDL_THREAD_PRIORITY_HIGH) < 0) {
|
if (SDL_SetThreadPriority(SDL_THREAD_PRIORITY_HIGH) < 0) {
|
||||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
@@ -110,29 +202,54 @@ void Session::arDecodeAndPlaySample(char* sampleData, int sampleLength)
|
|||||||
SDL_GetError());
|
SDL_GetError());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// See if we need to drop this sample
|
||||||
|
if (s_ActiveSession->m_DropAudioEndTime != 0) {
|
||||||
|
if (SDL_TICKS_PASSED(SDL_GetTicks(), s_ActiveSession->m_DropAudioEndTime)) {
|
||||||
|
// Avoid calling SDL_GetTicks() now
|
||||||
|
s_ActiveSession->m_DropAudioEndTime = 0;
|
||||||
|
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Audio drop window has ended");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// We're still in the drop window
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
s_ActiveSession->m_AudioSampleCount++;
|
s_ActiveSession->m_AudioSampleCount++;
|
||||||
|
|
||||||
if (s_ActiveSession->m_AudioRenderer != nullptr) {
|
if (s_ActiveSession->m_AudioRenderer != nullptr) {
|
||||||
|
int desiredSize = sizeof(short) * s_ActiveSession->m_AudioConfig.samplesPerFrame * s_ActiveSession->m_AudioConfig.channelCount;
|
||||||
|
void* buffer = s_ActiveSession->m_AudioRenderer->getAudioBuffer(&desiredSize);
|
||||||
|
if (buffer == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
samplesDecoded = opus_multistream_decode(s_ActiveSession->m_OpusDecoder,
|
samplesDecoded = opus_multistream_decode(s_ActiveSession->m_OpusDecoder,
|
||||||
(unsigned char*)sampleData,
|
(unsigned char*)sampleData,
|
||||||
sampleLength,
|
sampleLength,
|
||||||
s_ActiveSession->m_OpusDecodeBuffer,
|
(short*)buffer,
|
||||||
SAMPLES_PER_FRAME,
|
desiredSize / sizeof(short) / s_ActiveSession->m_AudioConfig.channelCount,
|
||||||
0);
|
0);
|
||||||
|
|
||||||
|
// Update desiredSize with the number of bytes actually populated by the decoding operation
|
||||||
if (samplesDecoded > 0) {
|
if (samplesDecoded > 0) {
|
||||||
if (!s_ActiveSession->m_AudioRenderer->submitAudio(s_ActiveSession->m_OpusDecodeBuffer,
|
SDL_assert(desiredSize >= sizeof(short) * samplesDecoded * s_ActiveSession->m_AudioConfig.channelCount);
|
||||||
static_cast<int>(
|
desiredSize = sizeof(short) * samplesDecoded * s_ActiveSession->m_AudioConfig.channelCount;
|
||||||
sizeof(short) *
|
}
|
||||||
samplesDecoded *
|
else {
|
||||||
s_ActiveSession->m_AudioConfig.channelCount))) {
|
desiredSize = 0;
|
||||||
|
}
|
||||||
|
|
||||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
if (!s_ActiveSession->m_AudioRenderer->submitAudio(desiredSize)) {
|
||||||
"Reinitializing audio renderer after failure");
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Reinitializing audio renderer after failure");
|
||||||
|
|
||||||
delete s_ActiveSession->m_AudioRenderer;
|
delete s_ActiveSession->m_AudioRenderer;
|
||||||
s_ActiveSession->m_AudioRenderer = nullptr;
|
s_ActiveSession->m_AudioRenderer = nullptr;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,10 +258,18 @@ void Session::arDecodeAndPlaySample(char* sampleData, int sampleLength)
|
|||||||
// safe to reinitialize here because we can't be torn down while
|
// safe to reinitialize here because we can't be torn down while
|
||||||
// the audio decoder/playback thread is still alive.
|
// the audio decoder/playback thread is still alive.
|
||||||
if (s_ActiveSession->m_AudioRenderer == nullptr && (s_ActiveSession->m_AudioSampleCount % 200) == 0) {
|
if (s_ActiveSession->m_AudioRenderer == nullptr && (s_ActiveSession->m_AudioSampleCount % 200) == 0) {
|
||||||
s_ActiveSession->m_AudioRenderer = s_ActiveSession->createAudioRenderer();
|
// Since we're doing this inline and audio initialization takes time, we need
|
||||||
if (!s_ActiveSession->m_AudioRenderer->prepareForPlayback(&s_ActiveSession->m_AudioConfig)) {
|
// to drop samples to account for the time we've spent blocking audio rendering
|
||||||
delete s_ActiveSession->m_AudioRenderer;
|
// so we return to real-time playback and don't accumulate latency.
|
||||||
s_ActiveSession->m_AudioRenderer = nullptr;
|
Uint32 audioReinitStartTime = SDL_GetTicks();
|
||||||
}
|
|
||||||
|
s_ActiveSession->m_AudioRenderer = s_ActiveSession->createAudioRenderer(&s_ActiveSession->m_AudioConfig);
|
||||||
|
|
||||||
|
Uint32 audioReinitStopTime = SDL_GetTicks();
|
||||||
|
|
||||||
|
s_ActiveSession->m_DropAudioEndTime = audioReinitStopTime + (audioReinitStopTime - audioReinitStartTime);
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Audio reinitialization took %d ms - starting drop window",
|
||||||
|
audioReinitStopTime - audioReinitStartTime);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,9 +2,6 @@
|
|||||||
|
|
||||||
#include <Limelight.h>
|
#include <Limelight.h>
|
||||||
|
|
||||||
#define MAX_CHANNELS 6
|
|
||||||
#define SAMPLES_PER_FRAME 240
|
|
||||||
|
|
||||||
class IAudioRenderer
|
class IAudioRenderer
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
@@ -12,6 +9,20 @@ public:
|
|||||||
|
|
||||||
virtual bool prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig) = 0;
|
virtual bool prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig) = 0;
|
||||||
|
|
||||||
|
virtual void* getAudioBuffer(int* size) = 0;
|
||||||
|
|
||||||
// Return false if an unrecoverable error has occurred and the renderer must be reinitialized
|
// Return false if an unrecoverable error has occurred and the renderer must be reinitialized
|
||||||
virtual bool submitAudio(short* audioBuffer, int audioSize) = 0;
|
virtual bool submitAudio(int bytesWritten) = 0;
|
||||||
|
|
||||||
|
virtual int getCapabilities() = 0;
|
||||||
|
|
||||||
|
virtual void remapChannels(POPUS_MULTISTREAM_CONFIGURATION) {
|
||||||
|
// Use default channel mapping:
|
||||||
|
// 0 - Front Left
|
||||||
|
// 1 - Front Right
|
||||||
|
// 2 - Center
|
||||||
|
// 3 - LFE
|
||||||
|
// 4 - Surround Left
|
||||||
|
// 5 - Surround Right
|
||||||
|
}
|
||||||
};
|
};
|
||||||
@@ -12,13 +12,14 @@ public:
|
|||||||
|
|
||||||
virtual bool prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig);
|
virtual bool prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig);
|
||||||
|
|
||||||
virtual bool submitAudio(short* audioBuffer, int audioSize);
|
virtual void* getAudioBuffer(int* size);
|
||||||
|
|
||||||
|
virtual bool submitAudio(int bytesWritten);
|
||||||
|
|
||||||
|
virtual int getCapabilities();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
SDL_AudioDeviceID m_AudioDevice;
|
SDL_AudioDeviceID m_AudioDevice;
|
||||||
Uint32 m_ChannelCount;
|
void* m_AudioBuffer;
|
||||||
Uint32 m_PendingDrops;
|
int m_FrameSize;
|
||||||
Uint32 m_PendingHardDrops;
|
|
||||||
Uint32 m_SampleIndex;
|
|
||||||
Uint32 m_BaselinePendingData;
|
|
||||||
};
|
};
|
||||||
@@ -5,18 +5,9 @@
|
|||||||
|
|
||||||
#include <QtGlobal>
|
#include <QtGlobal>
|
||||||
|
|
||||||
#define MIN_QUEUED_FRAMES 2
|
|
||||||
#define MAX_QUEUED_FRAMES 4
|
|
||||||
#define STOP_THE_WORLD_LIMIT 20
|
|
||||||
#define DROP_RATIO_DENOM 32
|
|
||||||
|
|
||||||
SdlAudioRenderer::SdlAudioRenderer()
|
SdlAudioRenderer::SdlAudioRenderer()
|
||||||
: m_AudioDevice(0),
|
: m_AudioDevice(0),
|
||||||
m_ChannelCount(0),
|
m_AudioBuffer(nullptr)
|
||||||
m_PendingDrops(0),
|
|
||||||
m_PendingHardDrops(0),
|
|
||||||
m_SampleIndex(0),
|
|
||||||
m_BaselinePendingData(0)
|
|
||||||
{
|
{
|
||||||
SDL_assert(!SDL_WasInit(SDL_INIT_AUDIO));
|
SDL_assert(!SDL_WasInit(SDL_INIT_AUDIO));
|
||||||
if (SDL_InitSubSystem(SDL_INIT_AUDIO) != 0) {
|
if (SDL_InitSubSystem(SDL_INIT_AUDIO) != 0) {
|
||||||
@@ -40,14 +31,22 @@ bool SdlAudioRenderer::prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION*
|
|||||||
// frames contain a non-power of 2 number of samples,
|
// frames contain a non-power of 2 number of samples,
|
||||||
// so the slop would require buffering another full frame.
|
// so the slop would require buffering another full frame.
|
||||||
// Specifying non-Po2 seems to work for our supported platforms.
|
// Specifying non-Po2 seems to work for our supported platforms.
|
||||||
want.samples = SAMPLES_PER_FRAME;
|
want.samples = opusConfig->samplesPerFrame;
|
||||||
|
|
||||||
|
m_FrameSize = opusConfig->samplesPerFrame * sizeof(short) * opusConfig->channelCount;
|
||||||
|
|
||||||
m_AudioDevice = SDL_OpenAudioDevice(NULL, 0, &want, &have, 0);
|
m_AudioDevice = SDL_OpenAudioDevice(NULL, 0, &want, &have, 0);
|
||||||
if (m_AudioDevice == 0) {
|
if (m_AudioDevice == 0) {
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"Failed to open audio device: %s",
|
"Failed to open audio device: %s",
|
||||||
SDL_GetError());
|
SDL_GetError());
|
||||||
SDL_QuitSubSystem(SDL_INIT_AUDIO);
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_AudioBuffer = malloc(m_FrameSize);
|
||||||
|
if (m_AudioBuffer == nullptr) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Failed to allocate audio buffer");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,27 +60,6 @@ bool SdlAudioRenderer::prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION*
|
|||||||
have.samples,
|
have.samples,
|
||||||
have.size);
|
have.size);
|
||||||
|
|
||||||
// SDL counts pending samples in the queued
|
|
||||||
// audio size using the WASAPI backend. This
|
|
||||||
// includes silence, which can throw off our
|
|
||||||
// pending data count. Get a baseline so we
|
|
||||||
// can exclude that data.
|
|
||||||
m_BaselinePendingData = 0;
|
|
||||||
#ifdef Q_OS_WIN32
|
|
||||||
for (int i = 0; i < 100; i++) {
|
|
||||||
m_BaselinePendingData = qMax(m_BaselinePendingData, SDL_GetQueuedAudioSize(m_AudioDevice));
|
|
||||||
SDL_Delay(10);
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
m_BaselinePendingData *= 2;
|
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Baseline pending audio data: %d bytes",
|
|
||||||
m_BaselinePendingData);
|
|
||||||
|
|
||||||
m_ChannelCount = opusConfig->channelCount;
|
|
||||||
m_SampleIndex = 0;
|
|
||||||
m_PendingDrops = m_PendingHardDrops = 0;
|
|
||||||
|
|
||||||
// Start playback
|
// Start playback
|
||||||
SDL_PauseAudioDevice(m_AudioDevice, 0);
|
SDL_PauseAudioDevice(m_AudioDevice, 0);
|
||||||
|
|
||||||
@@ -96,60 +74,39 @@ SdlAudioRenderer::~SdlAudioRenderer()
|
|||||||
SDL_CloseAudioDevice(m_AudioDevice);
|
SDL_CloseAudioDevice(m_AudioDevice);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (m_AudioBuffer != nullptr) {
|
||||||
|
free(m_AudioBuffer);
|
||||||
|
}
|
||||||
|
|
||||||
SDL_QuitSubSystem(SDL_INIT_AUDIO);
|
SDL_QuitSubSystem(SDL_INIT_AUDIO);
|
||||||
SDL_assert(!SDL_WasInit(SDL_INIT_AUDIO));
|
SDL_assert(!SDL_WasInit(SDL_INIT_AUDIO));
|
||||||
}
|
}
|
||||||
|
|
||||||
bool SdlAudioRenderer::submitAudio(short* audioBuffer, int audioSize)
|
void* SdlAudioRenderer::getAudioBuffer(int*)
|
||||||
{
|
{
|
||||||
m_SampleIndex++;
|
return m_AudioBuffer;
|
||||||
|
}
|
||||||
|
|
||||||
Uint32 queuedAudio = SDL_GetQueuedAudioSize(m_AudioDevice);
|
bool SdlAudioRenderer::submitAudio(int bytesWritten)
|
||||||
if (queuedAudio > m_BaselinePendingData) {
|
{
|
||||||
queuedAudio -= m_BaselinePendingData;
|
if (bytesWritten == 0) {
|
||||||
}
|
// Nothing to do
|
||||||
else {
|
|
||||||
queuedAudio = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
Uint32 framesQueued = queuedAudio / (SAMPLES_PER_FRAME * m_ChannelCount * sizeof(short));
|
|
||||||
|
|
||||||
// We must check this prior to the below checks to ensure we don't
|
|
||||||
// underflow if framesQueued - m_PendingHardDrops < 0.
|
|
||||||
if (framesQueued <= MIN_QUEUED_FRAMES) {
|
|
||||||
m_PendingDrops = m_PendingHardDrops = 0;
|
|
||||||
}
|
|
||||||
// Pend enough drops to get us back to MIN_QUEUED_FRAMES, checking first
|
|
||||||
// to ensure we don't underflow.
|
|
||||||
else if (framesQueued > m_PendingHardDrops &&
|
|
||||||
framesQueued - m_PendingHardDrops > STOP_THE_WORLD_LIMIT) {
|
|
||||||
m_PendingHardDrops = framesQueued - MIN_QUEUED_FRAMES;
|
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Pending hard drop of %u audio frames",
|
|
||||||
m_PendingHardDrops);
|
|
||||||
}
|
|
||||||
// If we're under the stop the world limit, we can drop samples
|
|
||||||
// gracefully over the next little while.
|
|
||||||
else if (framesQueued > m_PendingHardDrops + m_PendingDrops &&
|
|
||||||
framesQueued - m_PendingHardDrops - m_PendingDrops > MAX_QUEUED_FRAMES) {
|
|
||||||
m_PendingDrops = framesQueued - MIN_QUEUED_FRAMES;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine if this frame should be dropped
|
|
||||||
if (m_PendingHardDrops != 0) {
|
|
||||||
// Hard drops happen all at once to forcefully
|
|
||||||
// resync with the source.
|
|
||||||
m_PendingHardDrops--;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
else if (m_PendingDrops != 0 && m_SampleIndex % DROP_RATIO_DENOM == 0) {
|
|
||||||
// Normal drops are interspersed with the audio data
|
|
||||||
// to hide the glitches.
|
|
||||||
m_PendingDrops--;
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (SDL_QueueAudio(m_AudioDevice, audioBuffer, audioSize) < 0) {
|
// Don't queue if there's already more than 30 ms of audio data waiting
|
||||||
|
// in Moonlight's audio queue.
|
||||||
|
if (LiGetPendingAudioDuration() > 30) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provide backpressure on the queue to ensure too many frames don't build up
|
||||||
|
// in SDL's audio queue.
|
||||||
|
while (SDL_GetQueuedAudioSize(m_AudioDevice) / m_FrameSize > 10) {
|
||||||
|
SDL_Delay(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SDL_QueueAudio(m_AudioDevice, m_AudioBuffer, bytesWritten) < 0) {
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"Failed to queue audio sample: %s",
|
"Failed to queue audio sample: %s",
|
||||||
SDL_GetError());
|
SDL_GetError());
|
||||||
@@ -157,3 +114,9 @@ bool SdlAudioRenderer::submitAudio(short* audioBuffer, int audioSize)
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int SdlAudioRenderer::getCapabilities()
|
||||||
|
{
|
||||||
|
// Direct submit can't be used because we use LiGetPendingAudioDuration()
|
||||||
|
return CAPABILITY_SUPPORTS_ARBITRARY_AUDIO_DURATION;
|
||||||
|
}
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
#include "slaud.h"
|
||||||
|
|
||||||
|
#include <SDL.h>
|
||||||
|
|
||||||
|
SLAudioRenderer::SLAudioRenderer()
|
||||||
|
: m_AudioContext(nullptr),
|
||||||
|
m_AudioStream(nullptr),
|
||||||
|
m_AudioBuffer(nullptr)
|
||||||
|
{
|
||||||
|
SLAudio_SetLogFunction(SLAudioRenderer::slLogCallback, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SLAudioRenderer::prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig)
|
||||||
|
{
|
||||||
|
m_AudioContext = SLAudio_CreateContext();
|
||||||
|
if (m_AudioContext == nullptr) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"SLAudio_CreateContext() failed");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This number is pretty conservative (especially for surround), but
|
||||||
|
// it's hard to avoid since we get crushed by CPU limitations.
|
||||||
|
m_MaxQueuedAudioMs = 40 * opusConfig->channelCount / 2;
|
||||||
|
|
||||||
|
m_AudioBufferSize = opusConfig->samplesPerFrame * sizeof(short) * opusConfig->channelCount;
|
||||||
|
m_AudioStream = SLAudio_CreateStream(m_AudioContext,
|
||||||
|
opusConfig->sampleRate,
|
||||||
|
opusConfig->channelCount,
|
||||||
|
m_AudioBufferSize,
|
||||||
|
1);
|
||||||
|
if (m_AudioStream == nullptr) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"SLAudio_CreateStream() failed");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Using SLAudio renderer with %d samples per frame",
|
||||||
|
opusConfig->samplesPerFrame);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
#define SWAP_CHANNEL(i, j) \
|
||||||
|
tmp = opusConfig->mapping[i]; \
|
||||||
|
opusConfig->mapping[i] = opusConfig->mapping[j]; \
|
||||||
|
opusConfig->mapping[j] = tmp
|
||||||
|
|
||||||
|
void SLAudioRenderer::remapChannels(POPUS_MULTISTREAM_CONFIGURATION opusConfig) {
|
||||||
|
unsigned char tmp;
|
||||||
|
|
||||||
|
if (opusConfig->channelCount == 6) {
|
||||||
|
// The Moonlight's default channel order is FL,FR,C,LFE,RL,RR
|
||||||
|
// SLAudio expects FL,C,FR,RL,RR,LFE so we swap the channels around to match
|
||||||
|
|
||||||
|
// Swap FR and C - now FL,C,FR,LFE,RL,RR
|
||||||
|
SWAP_CHANNEL(1, 2);
|
||||||
|
|
||||||
|
// Swap LFE and RR - now FL,C,FR,RR,RL,LFE
|
||||||
|
SWAP_CHANNEL(3, 5);
|
||||||
|
|
||||||
|
// Swap RR and RL - now FL,C,FR,RL,RR,LFE
|
||||||
|
SWAP_CHANNEL(3, 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void* SLAudioRenderer::getAudioBuffer(int* size)
|
||||||
|
{
|
||||||
|
SDL_assert(*size == m_AudioBufferSize);
|
||||||
|
|
||||||
|
if (m_AudioBuffer == nullptr) {
|
||||||
|
m_AudioBuffer = SLAudio_BeginFrame(m_AudioStream);
|
||||||
|
}
|
||||||
|
|
||||||
|
return m_AudioBuffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
SLAudioRenderer::~SLAudioRenderer()
|
||||||
|
{
|
||||||
|
if (m_AudioBuffer != nullptr) {
|
||||||
|
memset(m_AudioBuffer, 0, m_AudioBufferSize);
|
||||||
|
SLAudio_SubmitFrame(m_AudioStream);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_AudioStream != nullptr) {
|
||||||
|
SLAudio_FreeStream(m_AudioStream);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_AudioContext != nullptr) {
|
||||||
|
SLAudio_FreeContext(m_AudioContext);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SLAudioRenderer::submitAudio(int bytesWritten)
|
||||||
|
{
|
||||||
|
if (bytesWritten == 0) {
|
||||||
|
// This buffer will be reused next time
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (LiGetPendingAudioDuration() < m_MaxQueuedAudioMs) {
|
||||||
|
SLAudio_SubmitFrame(m_AudioStream);
|
||||||
|
m_AudioBuffer = nullptr;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Too many queued audio frames: %d",
|
||||||
|
LiGetPendingAudioFrames());
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int SLAudioRenderer::getCapabilities()
|
||||||
|
{
|
||||||
|
return CAPABILITY_SLOW_OPUS_DECODER | CAPABILITY_SUPPORTS_ARBITRARY_AUDIO_DURATION;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SLAudioRenderer::slLogCallback(void*, ESLAudioLog logLevel, const char *message)
|
||||||
|
{
|
||||||
|
SDL_LogPriority priority;
|
||||||
|
|
||||||
|
switch (logLevel)
|
||||||
|
{
|
||||||
|
case k_ESLAudioLogError:
|
||||||
|
priority = SDL_LOG_PRIORITY_ERROR;
|
||||||
|
break;
|
||||||
|
case k_ESLAudioLogWarning:
|
||||||
|
priority = SDL_LOG_PRIORITY_WARN;
|
||||||
|
break;
|
||||||
|
case k_ESLAudioLogInfo:
|
||||||
|
priority = SDL_LOG_PRIORITY_INFO;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
case k_ESLAudioLogDebug:
|
||||||
|
priority = SDL_LOG_PRIORITY_DEBUG;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_LogMessage(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
priority,
|
||||||
|
"SLAudio: %s",
|
||||||
|
message);
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "renderer.h"
|
||||||
|
#include <SLAudio.h>
|
||||||
|
|
||||||
|
class SLAudioRenderer : public IAudioRenderer
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
SLAudioRenderer();
|
||||||
|
|
||||||
|
virtual ~SLAudioRenderer();
|
||||||
|
|
||||||
|
virtual bool prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig);
|
||||||
|
|
||||||
|
virtual void* getAudioBuffer(int* size);
|
||||||
|
|
||||||
|
virtual bool submitAudio(int bytesWritten);
|
||||||
|
|
||||||
|
virtual int getCapabilities();
|
||||||
|
|
||||||
|
virtual void remapChannels(POPUS_MULTISTREAM_CONFIGURATION opusConfig);
|
||||||
|
|
||||||
|
private:
|
||||||
|
static void slLogCallback(void* context, ESLAudioLog logLevel, const char* message);
|
||||||
|
|
||||||
|
CSLAudioContext* m_AudioContext;
|
||||||
|
CSLAudioStream* m_AudioStream;
|
||||||
|
|
||||||
|
void* m_AudioBuffer;
|
||||||
|
int m_AudioBufferSize;
|
||||||
|
int m_MaxQueuedAudioMs;
|
||||||
|
};
|
||||||
@@ -10,6 +10,8 @@ SoundIoAudioRenderer::SoundIoAudioRenderer()
|
|||||||
m_Device(nullptr),
|
m_Device(nullptr),
|
||||||
m_OutputStream(nullptr),
|
m_OutputStream(nullptr),
|
||||||
m_RingBuffer(nullptr),
|
m_RingBuffer(nullptr),
|
||||||
|
m_AudioPacketDuration(0),
|
||||||
|
m_Latency(0),
|
||||||
m_Errored(false)
|
m_Errored(false)
|
||||||
{
|
{
|
||||||
|
|
||||||
@@ -17,6 +19,10 @@ SoundIoAudioRenderer::SoundIoAudioRenderer()
|
|||||||
|
|
||||||
SoundIoAudioRenderer::~SoundIoAudioRenderer()
|
SoundIoAudioRenderer::~SoundIoAudioRenderer()
|
||||||
{
|
{
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Audio latency: %f",
|
||||||
|
m_Latency);
|
||||||
|
|
||||||
if (m_OutputStream != nullptr) {
|
if (m_OutputStream != nullptr) {
|
||||||
soundio_outstream_destroy(m_OutputStream);
|
soundio_outstream_destroy(m_OutputStream);
|
||||||
}
|
}
|
||||||
@@ -150,19 +156,16 @@ bool SoundIoAudioRenderer::prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATI
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
m_AudioPacketDuration = (opusConfig->samplesPerFrame / (opusConfig->sampleRate / 1000)) / 1000.0;
|
||||||
|
|
||||||
m_OutputStream->format = SoundIoFormatS16NE;
|
m_OutputStream->format = SoundIoFormatS16NE;
|
||||||
m_OutputStream->sample_rate = opusConfig->sampleRate;
|
m_OutputStream->sample_rate = opusConfig->sampleRate;
|
||||||
|
m_OutputStream->software_latency = m_AudioPacketDuration;
|
||||||
m_OutputStream->name = "Moonlight";
|
m_OutputStream->name = "Moonlight";
|
||||||
m_OutputStream->userdata = this;
|
m_OutputStream->userdata = this;
|
||||||
m_OutputStream->error_callback = sioErrorCallback;
|
m_OutputStream->error_callback = sioErrorCallback;
|
||||||
m_OutputStream->write_callback = sioWriteCallback;
|
m_OutputStream->write_callback = sioWriteCallback;
|
||||||
|
|
||||||
// This determines the size of the buffers we'll
|
|
||||||
// get from CoreAudio. Since GFE sends us packets
|
|
||||||
// in 5 ms chunks, we'll give them to the OS in
|
|
||||||
// buffers of the same size.
|
|
||||||
m_OutputStream->software_latency = 0.005;
|
|
||||||
|
|
||||||
SoundIoChannelLayout bestLayout = m_Device->current_layout;
|
SoundIoChannelLayout bestLayout = m_Device->current_layout;
|
||||||
for (int i = 0; i < m_Device->layout_count; i++) {
|
for (int i = 0; i < m_Device->layout_count; i++) {
|
||||||
if (scoreChannelLayout(&bestLayout, opusConfig) <
|
if (scoreChannelLayout(&bestLayout, opusConfig) <
|
||||||
@@ -218,12 +221,29 @@ bool SoundIoAudioRenderer::prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATI
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Buffer up to 6 packets of audio (30 ms) to smooth
|
int packetsToBuffer;
|
||||||
// out network packet delivery jitter
|
|
||||||
|
if (m_SoundIo->current_backend == SoundIoBackendWasapi) {
|
||||||
|
// 15 ms buffer seems to be fine for WASAPI
|
||||||
|
packetsToBuffer = (int)ceil(0.015 / m_AudioPacketDuration);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// 30 ms buffer on CoreAudio to avoid glitching on macOS
|
||||||
|
packetsToBuffer = (int)ceil(0.030 / m_AudioPacketDuration);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always buffer at least 2 packets
|
||||||
|
packetsToBuffer = qMax(2, packetsToBuffer);
|
||||||
|
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Audio buffer size: %f seconds",
|
||||||
|
packetsToBuffer * m_AudioPacketDuration);
|
||||||
|
|
||||||
m_RingBuffer = soundio_ring_buffer_create(m_SoundIo,
|
m_RingBuffer = soundio_ring_buffer_create(m_SoundIo,
|
||||||
m_OutputStream->bytes_per_sample *
|
m_OutputStream->bytes_per_sample *
|
||||||
m_OpusChannelCount *
|
m_OpusChannelCount *
|
||||||
SAMPLES_PER_FRAME * 6);
|
opusConfig->samplesPerFrame *
|
||||||
|
packetsToBuffer);
|
||||||
if (m_RingBuffer == nullptr) {
|
if (m_RingBuffer == nullptr) {
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"soundio_ring_buffer_create() failed");
|
"soundio_ring_buffer_create() failed");
|
||||||
@@ -238,18 +258,16 @@ bool SoundIoAudioRenderer::prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATI
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HACK: For some reason, a constant latency hangs around in the audio pipeline
|
||||||
|
// unless we wait for the audio stream to drain before actually submitting any samples.
|
||||||
|
// This is a gross hack, but it works remarkably well.
|
||||||
|
SDL_Delay(500);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool SoundIoAudioRenderer::submitAudio(short* audioBuffer, int audioSize)
|
void* SoundIoAudioRenderer::getAudioBuffer(int* size)
|
||||||
{
|
{
|
||||||
if (m_Errored) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Flush events to update with new device arrivals
|
|
||||||
soundio_flush_events(m_SoundIo);
|
|
||||||
|
|
||||||
// We must always write a full frame of audio. If we don't,
|
// We must always write a full frame of audio. If we don't,
|
||||||
// the reader will get out of sync with the writer and our
|
// the reader will get out of sync with the writer and our
|
||||||
// channels will get all mixed up. To ensure this is always
|
// channels will get all mixed up. To ensure this is always
|
||||||
@@ -257,13 +275,36 @@ bool SoundIoAudioRenderer::submitAudio(short* audioBuffer, int audioSize)
|
|||||||
// of our frame size.
|
// of our frame size.
|
||||||
int bytesFree = soundio_ring_buffer_free_count(m_RingBuffer);
|
int bytesFree = soundio_ring_buffer_free_count(m_RingBuffer);
|
||||||
int bytesPerFrame = m_OpusChannelCount * m_OutputStream->bytes_per_sample;
|
int bytesPerFrame = m_OpusChannelCount * m_OutputStream->bytes_per_sample;
|
||||||
int bytesToWrite = qMin(audioSize, (bytesFree / bytesPerFrame) * bytesPerFrame);
|
*size = qMin(*size, (bytesFree / bytesPerFrame) * bytesPerFrame);
|
||||||
memcpy(soundio_ring_buffer_write_ptr(m_RingBuffer), audioBuffer, bytesToWrite);
|
return soundio_ring_buffer_write_ptr(m_RingBuffer);
|
||||||
soundio_ring_buffer_advance_write_ptr(m_RingBuffer, bytesToWrite);
|
}
|
||||||
|
|
||||||
|
bool SoundIoAudioRenderer::submitAudio(int bytesWritten)
|
||||||
|
{
|
||||||
|
if (m_Errored) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bytesWritten == 0) {
|
||||||
|
// Nothing to do
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flush events to update with new device arrivals
|
||||||
|
soundio_flush_events(m_SoundIo);
|
||||||
|
|
||||||
|
// Advance the write pointer
|
||||||
|
soundio_ring_buffer_advance_write_ptr(m_RingBuffer, bytesWritten);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int SoundIoAudioRenderer::getCapabilities()
|
||||||
|
{
|
||||||
|
// TODO: Tweak buffer sizes then re-enable arbitrary audio duration
|
||||||
|
return CAPABILITY_DIRECT_SUBMIT /* | CAPABILITY_SUPPORTS_ARBITRARY_AUDIO_DURATION */;
|
||||||
|
}
|
||||||
|
|
||||||
void SoundIoAudioRenderer::sioErrorCallback(SoundIoOutStream* stream, int err)
|
void SoundIoAudioRenderer::sioErrorCallback(SoundIoOutStream* stream, int err)
|
||||||
{
|
{
|
||||||
auto me = reinterpret_cast<SoundIoAudioRenderer*>(stream->userdata);
|
auto me = reinterpret_cast<SoundIoAudioRenderer*>(stream->userdata);
|
||||||
@@ -328,34 +369,22 @@ void SoundIoAudioRenderer::sioWriteCallback(SoundIoOutStream* stream, int frameC
|
|||||||
(me->m_OpusChannelCount * stream->bytes_per_sample);
|
(me->m_OpusChannelCount * stream->bytes_per_sample);
|
||||||
int bytesRead = 0;
|
int bytesRead = 0;
|
||||||
|
|
||||||
|
// Ensure we always write at least a buffer, even if it's silence, to avoid
|
||||||
|
// busy looping when no audio data is available while libsoundio tries to keep
|
||||||
|
// us from starving the output device.
|
||||||
|
frameCountMin = qMax(frameCountMin, (int)(stream->sample_rate * me->m_AudioPacketDuration));
|
||||||
|
|
||||||
|
// Clamp frameCountMax to at least 2 packets or 20 ms to stop our latency from growing if audio packets lag.
|
||||||
|
// This makes sure that we never increase our latency far beyond what the sink is consuming.
|
||||||
|
frameCountMax = qMin(frameCountMax, (int)(stream->sample_rate * qMax(me->m_AudioPacketDuration * 2, 0.020)));
|
||||||
|
frameCountMin = qMin(frameCountMin, frameCountMax);
|
||||||
|
|
||||||
// Clamp framesLeft to frameCountMax
|
// Clamp framesLeft to frameCountMax
|
||||||
framesLeft = qMin(framesLeft, frameCountMax);
|
framesLeft = qMin(framesLeft, frameCountMax);
|
||||||
|
|
||||||
// Place an upper-bound on audio stream latency to
|
// Track latency on queueing-based backends
|
||||||
// avoid accumulating packets in queue-based backends
|
if (me->m_SoundIo->current_backend != SoundIoBackendCoreAudio && me->m_SoundIo->current_backend != SoundIoBackendJack) {
|
||||||
// like WASAPI. This bound was set by testing on several
|
soundio_outstream_get_latency(stream, &me->m_Latency);
|
||||||
// Windows machines. The highest latency was found on
|
|
||||||
// a XPS 9343 running Windows 7 in Steam Big Picture
|
|
||||||
// and the 5.1 audio test clip from Fraunhofer.
|
|
||||||
if (me->m_SoundIo->current_backend == SoundIoBackendWasapi) {
|
|
||||||
double latency;
|
|
||||||
if (soundio_outstream_get_latency(stream, &latency) == SoundIoErrorNone) {
|
|
||||||
if (latency > 0.050) {
|
|
||||||
// If our latency is higher than desired, drop these samples to gracefully lower
|
|
||||||
// the latency without glitching too much. Dropping the whole buffer causes
|
|
||||||
// a much more noticeable glitch. This approach also ensures that we don't
|
|
||||||
// accidentally underflow if the driver/kernel side is delayed and isn't
|
|
||||||
// consuming data fast enough. Dropping a frame at a time and re-evaluating
|
|
||||||
// each time ensures that we'll stop dropping if latency returns to normal.
|
|
||||||
readPtr += framesLeft * stream->bytes_per_sample * me->m_OpusChannelCount;
|
|
||||||
bytesRead += framesLeft * stream->bytes_per_sample * me->m_OpusChannelCount;
|
|
||||||
framesLeft = 0;
|
|
||||||
|
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Latency exceeded drop cap: %f",
|
|
||||||
latency);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (;;) {
|
for (;;) {
|
||||||
|
|||||||
@@ -13,7 +13,11 @@ public:
|
|||||||
|
|
||||||
virtual bool prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig);
|
virtual bool prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig);
|
||||||
|
|
||||||
virtual bool submitAudio(short* audioBuffer, int audioSize);
|
virtual void* getAudioBuffer(int* size);
|
||||||
|
|
||||||
|
virtual bool submitAudio(int bytesWritten);
|
||||||
|
|
||||||
|
virtual int getCapabilities();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
int scoreChannelLayout(const struct SoundIoChannelLayout* layout, const OPUS_MULTISTREAM_CONFIGURATION* opusConfig);
|
int scoreChannelLayout(const struct SoundIoChannelLayout* layout, const OPUS_MULTISTREAM_CONFIGURATION* opusConfig);
|
||||||
@@ -32,5 +36,7 @@ private:
|
|||||||
struct SoundIoOutStream* m_OutputStream;
|
struct SoundIoOutStream* m_OutputStream;
|
||||||
struct SoundIoRingBuffer* m_RingBuffer;
|
struct SoundIoRingBuffer* m_RingBuffer;
|
||||||
struct SoundIoChannelLayout m_EffectiveLayout;
|
struct SoundIoChannelLayout m_EffectiveLayout;
|
||||||
|
double m_AudioPacketDuration;
|
||||||
|
double m_Latency;
|
||||||
bool m_Errored;
|
bool m_Errored;
|
||||||
};
|
};
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
#include "path.h"
|
#include "path.h"
|
||||||
|
|
||||||
#include <QtGlobal>
|
#include <QtGlobal>
|
||||||
|
#include <QtMath>
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
|
|
||||||
#define VK_0 0x30
|
#define VK_0 0x30
|
||||||
@@ -17,6 +18,8 @@
|
|||||||
#define VK_NUMPAD0 0x60
|
#define VK_NUMPAD0 0x60
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#define MOUSE_POLLING_INTERVAL 5
|
||||||
|
|
||||||
// How long the mouse button will be pressed for a tap to click gesture
|
// How long the mouse button will be pressed for a tap to click gesture
|
||||||
#define TAP_BUTTON_RELEASE_DELAY 100
|
#define TAP_BUTTON_RELEASE_DELAY 100
|
||||||
|
|
||||||
@@ -26,6 +29,22 @@
|
|||||||
// How far the finger can move before it cancels a drag or tap
|
// How far the finger can move before it cancels a drag or tap
|
||||||
#define DEAD_ZONE_DELTA 0.1f
|
#define DEAD_ZONE_DELTA 0.1f
|
||||||
|
|
||||||
|
// How long the Start button must be pressed to toggle mouse emulation
|
||||||
|
#define MOUSE_EMULATION_LONG_PRESS_TIME 750
|
||||||
|
|
||||||
|
// How long between polling the gamepad to send virtual mouse input
|
||||||
|
#define MOUSE_EMULATION_POLLING_INTERVAL 50
|
||||||
|
|
||||||
|
// Determines how fast the mouse will move each interval
|
||||||
|
#define MOUSE_EMULATION_MOTION_MULTIPLIER 4
|
||||||
|
|
||||||
|
// Determines the maximum motion amount before allowing movement
|
||||||
|
#define MOUSE_EMULATION_DEADZONE 2
|
||||||
|
|
||||||
|
// Haptic capabilities (in addition to those from SDL_HapticQuery())
|
||||||
|
#define ML_HAPTIC_GC_RUMBLE (1U << 16)
|
||||||
|
#define ML_HAPTIC_SIMPLE_RUMBLE (1U << 17)
|
||||||
|
|
||||||
const int SdlInputHandler::k_ButtonMap[] = {
|
const int SdlInputHandler::k_ButtonMap[] = {
|
||||||
A_FLAG, B_FLAG, X_FLAG, Y_FLAG,
|
A_FLAG, B_FLAG, X_FLAG, Y_FLAG,
|
||||||
BACK_FLAG, SPECIAL_FLAG, PLAY_FLAG,
|
BACK_FLAG, SPECIAL_FLAG, PLAY_FLAG,
|
||||||
@@ -34,10 +53,11 @@ const int SdlInputHandler::k_ButtonMap[] = {
|
|||||||
UP_FLAG, DOWN_FLAG, LEFT_FLAG, RIGHT_FLAG
|
UP_FLAG, DOWN_FLAG, LEFT_FLAG, RIGHT_FLAG
|
||||||
};
|
};
|
||||||
|
|
||||||
SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, NvComputer* computer, int streamWidth, int streamHeight)
|
SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, NvComputer*, int streamWidth, int streamHeight)
|
||||||
: m_LastMouseMotionTime(0),
|
: m_MultiController(prefs.multiController),
|
||||||
m_MultiController(prefs.multiController),
|
m_GamepadMouse(prefs.gamepadMouse),
|
||||||
m_NeedsInputDelay(false),
|
m_MouseMoveTimer(0),
|
||||||
|
m_FakeCaptureActive(false),
|
||||||
m_LeftButtonReleaseTimer(0),
|
m_LeftButtonReleaseTimer(0),
|
||||||
m_RightButtonReleaseTimer(0),
|
m_RightButtonReleaseTimer(0),
|
||||||
m_DragTimer(0),
|
m_DragTimer(0),
|
||||||
@@ -53,8 +73,39 @@ SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, NvComputer* comput
|
|||||||
// is via normal motion events that are influenced by mouse acceleration).
|
// is via normal motion events that are influenced by mouse acceleration).
|
||||||
// Otherwise, we'll use raw input capture which is straight from the device
|
// Otherwise, we'll use raw input capture which is straight from the device
|
||||||
// without modification by the OS.
|
// without modification by the OS.
|
||||||
SDL_SetHint(SDL_HINT_MOUSE_RELATIVE_MODE_WARP,
|
SDL_SetHintWithPriority(SDL_HINT_MOUSE_RELATIVE_MODE_WARP,
|
||||||
prefs.mouseAcceleration ? "1" : "0");
|
prefs.mouseAcceleration ? "1" : "0",
|
||||||
|
SDL_HINT_OVERRIDE);
|
||||||
|
|
||||||
|
#if defined(Q_OS_DARWIN) && !SDL_VERSION_ATLEAST(2, 0, 10)
|
||||||
|
// SDL 2.0.9 on macOS has a broken HIDAPI mapping for the older Xbox One S
|
||||||
|
// firmware, so we have to disable HIDAPI for Xbox gamepads on macOS until
|
||||||
|
// SDL 2.0.10 where the bug is fixed.
|
||||||
|
// https://github.com/moonlight-stream/moonlight-qt/issues/133
|
||||||
|
// https://bugzilla.libsdl.org/show_bug.cgi?id=4395
|
||||||
|
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_XBOX, "0");
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// We must initialize joystick explicitly before gamecontroller in order
|
||||||
|
// to ensure we receive gamecontroller attach events for gamepads where
|
||||||
|
// SDL doesn't have a built-in mapping. By starting joystick first, we
|
||||||
|
// can allow mapping manager to update the mappings before GC attach
|
||||||
|
// events are generated.
|
||||||
|
SDL_assert(!SDL_WasInit(SDL_INIT_JOYSTICK));
|
||||||
|
if (SDL_InitSubSystem(SDL_INIT_JOYSTICK) != 0) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"SDL_InitSubSystem(SDL_INIT_JOYSTICK) failed: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
}
|
||||||
|
|
||||||
|
MappingManager mappingManager;
|
||||||
|
mappingManager.applyMappings();
|
||||||
|
|
||||||
|
// Flush gamepad arrival and departure events which may be queued before
|
||||||
|
// starting the gamecontroller subsystem again. This prevents us from
|
||||||
|
// receiving duplicate arrival and departure events for the same gamepad.
|
||||||
|
SDL_FlushEvent(SDL_CONTROLLERDEVICEADDED);
|
||||||
|
SDL_FlushEvent(SDL_CONTROLLERDEVICEREMOVED);
|
||||||
|
|
||||||
// We need to reinit this each time, since you only get
|
// We need to reinit this each time, since you only get
|
||||||
// an initial set of gamepad arrival events once per init.
|
// an initial set of gamepad arrival events once per init.
|
||||||
@@ -65,50 +116,73 @@ SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, NvComputer* comput
|
|||||||
SDL_GetError());
|
SDL_GetError());
|
||||||
}
|
}
|
||||||
|
|
||||||
MappingManager mappingManager;
|
#if !SDL_VERSION_ATLEAST(2, 0, 9)
|
||||||
mappingManager.applyMappings();
|
SDL_assert(!SDL_WasInit(SDL_INIT_HAPTIC));
|
||||||
|
if (SDL_InitSubSystem(SDL_INIT_HAPTIC) != 0) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"SDL_InitSubSystem(SDL_INIT_HAPTIC) failed: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
if (!m_MultiController) {
|
// Initialize the gamepad mask with currently attached gamepads to avoid
|
||||||
// Player 1 is always present in non-MC mode
|
// causing gamepads to unexpectedly disappear and reappear on the host
|
||||||
m_GamepadMask = 0x1;
|
// during stream startup as we detect currently attached gamepads one at a time.
|
||||||
}
|
m_GamepadMask = getAttachedGamepadMask();
|
||||||
else {
|
|
||||||
// Otherwise, detect gamepads on the fly
|
|
||||||
m_GamepadMask = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prior to GFE 3.14.1, sending too many mouse motion events can cause
|
|
||||||
// GFE to choke and input latency to increase significantly. We will
|
|
||||||
// artificially throttle them to avoid this situation.
|
|
||||||
QVector<int> gfeVersion = NvHTTP::parseQuad(computer->gfeVersion);
|
|
||||||
if (gfeVersion.isEmpty() || // Very old versions don't have GfeVersion at all
|
|
||||||
gfeVersion[0] < 3 ||
|
|
||||||
(gfeVersion[0] == 3 && gfeVersion[1] < 14) ||
|
|
||||||
(gfeVersion[0] == 3 && gfeVersion[1] == 14 && gfeVersion[2] < 1)) {
|
|
||||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"This older version of GFE requires input delay hack");
|
|
||||||
m_NeedsInputDelay = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
SDL_zero(m_GamepadState);
|
SDL_zero(m_GamepadState);
|
||||||
SDL_zero(m_TouchDownEvent);
|
SDL_zero(m_TouchDownEvent);
|
||||||
SDL_zero(m_CumulativeDelta);
|
SDL_zero(m_CumulativeDelta);
|
||||||
|
|
||||||
|
SDL_AtomicSet(&m_MouseDeltaX, 0);
|
||||||
|
SDL_AtomicSet(&m_MouseDeltaY, 0);
|
||||||
|
|
||||||
|
m_MouseMoveTimer = SDL_AddTimer(MOUSE_POLLING_INTERVAL, SdlInputHandler::mouseMoveTimerCallback, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
SdlInputHandler::~SdlInputHandler()
|
SdlInputHandler::~SdlInputHandler()
|
||||||
{
|
{
|
||||||
for (int i = 0; i < MAX_GAMEPADS; i++) {
|
for (int i = 0; i < MAX_GAMEPADS; i++) {
|
||||||
|
if (m_GamepadState[i].mouseEmulationTimer != 0) {
|
||||||
|
Session::get()->notifyMouseEmulationMode(false);
|
||||||
|
SDL_RemoveTimer(m_GamepadState[i].mouseEmulationTimer);
|
||||||
|
}
|
||||||
|
#if !SDL_VERSION_ATLEAST(2, 0, 9)
|
||||||
|
if (m_GamepadState[i].haptic != nullptr) {
|
||||||
|
SDL_HapticClose(m_GamepadState[i].haptic);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
if (m_GamepadState[i].controller != nullptr) {
|
if (m_GamepadState[i].controller != nullptr) {
|
||||||
SDL_GameControllerClose(m_GamepadState[i].controller);
|
SDL_GameControllerClose(m_GamepadState[i].controller);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SDL_RemoveTimer(m_MouseMoveTimer);
|
||||||
SDL_RemoveTimer(m_LeftButtonReleaseTimer);
|
SDL_RemoveTimer(m_LeftButtonReleaseTimer);
|
||||||
SDL_RemoveTimer(m_RightButtonReleaseTimer);
|
SDL_RemoveTimer(m_RightButtonReleaseTimer);
|
||||||
SDL_RemoveTimer(m_DragTimer);
|
SDL_RemoveTimer(m_DragTimer);
|
||||||
|
|
||||||
|
#if !SDL_VERSION_ATLEAST(2, 0, 9)
|
||||||
|
SDL_QuitSubSystem(SDL_INIT_HAPTIC);
|
||||||
|
SDL_assert(!SDL_WasInit(SDL_INIT_HAPTIC));
|
||||||
|
#endif
|
||||||
|
|
||||||
SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER);
|
SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER);
|
||||||
SDL_assert(!SDL_WasInit(SDL_INIT_GAMECONTROLLER));
|
SDL_assert(!SDL_WasInit(SDL_INIT_GAMECONTROLLER));
|
||||||
|
|
||||||
|
SDL_QuitSubSystem(SDL_INIT_JOYSTICK);
|
||||||
|
SDL_assert(!SDL_WasInit(SDL_INIT_JOYSTICK));
|
||||||
|
|
||||||
|
// Return background event handling to off
|
||||||
|
SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "0");
|
||||||
|
|
||||||
|
#ifdef STEAM_LINK
|
||||||
|
// Hide SDL's cursor on Steam Link after quitting the stream.
|
||||||
|
// FIXME: We should also do this for other situations where SDL
|
||||||
|
// and Qt will draw their own mouse cursors like KMSDRM or RPi
|
||||||
|
// video backends.
|
||||||
|
SDL_ShowCursor(SDL_DISABLE);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
|
void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
|
||||||
@@ -121,10 +195,21 @@ void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
|
|||||||
(event->keysym.mod & KMOD_CTRL) &&
|
(event->keysym.mod & KMOD_CTRL) &&
|
||||||
(event->keysym.mod & KMOD_ALT) &&
|
(event->keysym.mod & KMOD_ALT) &&
|
||||||
(event->keysym.mod & KMOD_SHIFT)) {
|
(event->keysym.mod & KMOD_SHIFT)) {
|
||||||
|
// First we test the SDLK combos for matches,
|
||||||
|
// that way we ensure that latin keyboard users
|
||||||
|
// can match to the key they see on their keyboards.
|
||||||
|
// If nothing matches that, we'll then go on to
|
||||||
|
// checking scancodes so non-latin keyboard users
|
||||||
|
// can have working hotkeys (though possibly in
|
||||||
|
// odd positions). We must do all SDLK tests before
|
||||||
|
// any scancode tests to avoid issues in cases
|
||||||
|
// where the SDLK for one shortcut collides with
|
||||||
|
// the scancode of another.
|
||||||
|
|
||||||
// Check for quit combo (Ctrl+Alt+Shift+Q)
|
// Check for quit combo (Ctrl+Alt+Shift+Q)
|
||||||
if (event->keysym.sym == SDLK_q) {
|
if (event->keysym.sym == SDLK_q) {
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"Detected quit key combo");
|
"Detected quit key combo (SDLK)");
|
||||||
|
|
||||||
// Push a quit event to the main loop
|
// Push a quit event to the main loop
|
||||||
SDL_Event event;
|
SDL_Event event;
|
||||||
@@ -136,10 +221,58 @@ void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
|
|||||||
// Check for the unbind combo (Ctrl+Alt+Shift+Z)
|
// Check for the unbind combo (Ctrl+Alt+Shift+Z)
|
||||||
else if (event->keysym.sym == SDLK_z) {
|
else if (event->keysym.sym == SDLK_z) {
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"Detected mouse capture toggle combo");
|
"Detected mouse capture toggle combo (SDLK)");
|
||||||
|
|
||||||
// Stop handling future input
|
// Stop handling future input
|
||||||
SDL_SetRelativeMouseMode((SDL_bool)!SDL_GetRelativeMouseMode());
|
setCaptureActive(!isCaptureActive());
|
||||||
|
|
||||||
|
// Force raise all keys to ensure they aren't stuck,
|
||||||
|
// since we won't get their key up events.
|
||||||
|
raiseAllKeys();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else if (event->keysym.sym == SDLK_x) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
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 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) {
|
||||||
|
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,
|
// Force raise all keys to ensure they aren't stuck,
|
||||||
// since we won't get their key up events.
|
// since we won't get their key up events.
|
||||||
@@ -147,11 +280,24 @@ void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Check for the full-screen combo (Ctrl+Alt+Shift+X)
|
// Check for the full-screen combo (Ctrl+Alt+Shift+X)
|
||||||
else if (event->keysym.sym == SDLK_x) {
|
else if (event->keysym.scancode == SDL_SCANCODE_X) {
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"Detected full-screen toggle combo");
|
"Detected full-screen toggle combo (scancode)");
|
||||||
Session::s_ActiveSession->toggleFullscreen();
|
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;
|
||||||
|
}
|
||||||
|
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
|
// Force raise all keys just be safe across this full-screen/windowed
|
||||||
// transition just in case key events get lost.
|
// transition just in case key events get lost.
|
||||||
raiseAllKeys();
|
raiseAllKeys();
|
||||||
@@ -159,7 +305,7 @@ void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!SDL_GetRelativeMouseMode()) {
|
if (!isCaptureActive()) {
|
||||||
// Not capturing
|
// Not capturing
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -405,11 +551,11 @@ void SdlInputHandler::handleMouseButtonEvent(SDL_MouseButtonEvent* event)
|
|||||||
// the pressed event was consumed by this code).
|
// the pressed event was consumed by this code).
|
||||||
if (event->button == SDL_BUTTON_LEFT &&
|
if (event->button == SDL_BUTTON_LEFT &&
|
||||||
event->state == SDL_RELEASED &&
|
event->state == SDL_RELEASED &&
|
||||||
!SDL_GetRelativeMouseMode()) {
|
!isCaptureActive()) {
|
||||||
SDL_SetRelativeMouseMode(SDL_TRUE);
|
setCaptureActive(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
else if (!SDL_GetRelativeMouseMode()) {
|
else if (!isCaptureActive()) {
|
||||||
// Not capturing
|
// Not capturing
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -450,7 +596,7 @@ void SdlInputHandler::handleMouseButtonEvent(SDL_MouseButtonEvent* event)
|
|||||||
|
|
||||||
void SdlInputHandler::handleMouseMotionEvent(SDL_MouseMotionEvent* event)
|
void SdlInputHandler::handleMouseMotionEvent(SDL_MouseMotionEvent* event)
|
||||||
{
|
{
|
||||||
if (!SDL_GetRelativeMouseMode()) {
|
if (!isCaptureActive()) {
|
||||||
// Not capturing
|
// Not capturing
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -459,45 +605,15 @@ void SdlInputHandler::handleMouseMotionEvent(SDL_MouseMotionEvent* event)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
short xdelta = (short)event->xrel;
|
// Batch until the next mouse polling window or we'll get awful
|
||||||
short ydelta = (short)event->yrel;
|
// input lag everything except GFE 3.14 and 3.15.
|
||||||
|
SDL_AtomicAdd(&m_MouseDeltaX, event->xrel);
|
||||||
// If we're sending more than one motion event per millisecond,
|
SDL_AtomicAdd(&m_MouseDeltaY, event->yrel);
|
||||||
// delay for 1 ms to allow batching of mouse move events. On older
|
|
||||||
// versions of GFE, we will unconditionally wait this 1 ms to
|
|
||||||
// work around an input processing issue that causes massive mouse latency.
|
|
||||||
Uint32 currentTime = SDL_GetTicks();
|
|
||||||
if (m_NeedsInputDelay || !SDL_TICKS_PASSED(currentTime, m_LastMouseMotionTime + 1)) {
|
|
||||||
SDL_Delay(1);
|
|
||||||
currentTime = SDL_GetTicks();
|
|
||||||
}
|
|
||||||
m_LastMouseMotionTime = currentTime;
|
|
||||||
|
|
||||||
// Pump even if we didn't delay since we might get some extra events
|
|
||||||
SDL_PumpEvents();
|
|
||||||
|
|
||||||
// Batch all of the pending mouse motion events
|
|
||||||
SDL_Event nextEvent;
|
|
||||||
while (SDL_PeepEvents(&nextEvent,
|
|
||||||
1,
|
|
||||||
SDL_GETEVENT,
|
|
||||||
SDL_MOUSEMOTION,
|
|
||||||
SDL_MOUSEMOTION) == 1) {
|
|
||||||
// In theory, these can overflow but in practice
|
|
||||||
// it should be highly unlikely since it would require
|
|
||||||
// moving 64K pixels in 1 ms.
|
|
||||||
xdelta += nextEvent.motion.xrel;
|
|
||||||
ydelta += nextEvent.motion.yrel;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (xdelta != 0 || ydelta != 0) {
|
|
||||||
LiSendMouseMoveEvent(xdelta, ydelta);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void SdlInputHandler::handleMouseWheelEvent(SDL_MouseWheelEvent* event)
|
void SdlInputHandler::handleMouseWheelEvent(SDL_MouseWheelEvent* event)
|
||||||
{
|
{
|
||||||
if (!SDL_GetRelativeMouseMode()) {
|
if (!isCaptureActive()) {
|
||||||
// Not capturing
|
// Not capturing
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -572,46 +688,117 @@ Uint32 SdlInputHandler::dragTimerCallback(Uint32, void *param)
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Uint32 SdlInputHandler::mouseMoveTimerCallback(Uint32 interval, void *param)
|
||||||
|
{
|
||||||
|
auto me = reinterpret_cast<SdlInputHandler*>(param);
|
||||||
|
|
||||||
|
short deltaX = (short)SDL_AtomicSet(&me->m_MouseDeltaX, 0);
|
||||||
|
short deltaY = (short)SDL_AtomicSet(&me->m_MouseDeltaY, 0);
|
||||||
|
|
||||||
|
if (deltaX != 0 || deltaY != 0) {
|
||||||
|
LiSendMouseMoveEvent(deltaX, deltaY);
|
||||||
|
}
|
||||||
|
|
||||||
|
return interval;
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint32 SdlInputHandler::mouseEmulationTimerCallback(Uint32 interval, void *param)
|
||||||
|
{
|
||||||
|
auto gamepad = reinterpret_cast<GamepadState*>(param);
|
||||||
|
|
||||||
|
short rawX;
|
||||||
|
short rawY;
|
||||||
|
|
||||||
|
// Determine which analog stick is currently receiving the strongest input
|
||||||
|
if ((uint32_t)qAbs(gamepad->lsX) + qAbs(gamepad->lsY) > (uint32_t)qAbs(gamepad->rsX) + qAbs(gamepad->rsY)) {
|
||||||
|
rawX = gamepad->lsX;
|
||||||
|
rawY = -gamepad->lsY;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
rawX = gamepad->rsX;
|
||||||
|
rawY = -gamepad->rsY;
|
||||||
|
}
|
||||||
|
|
||||||
|
float deltaX;
|
||||||
|
float deltaY;
|
||||||
|
|
||||||
|
// Produce a base vector for mouse movement with increased speed as we deviate further from center
|
||||||
|
deltaX = qPow(rawX / 32766.0f * MOUSE_EMULATION_MOTION_MULTIPLIER, 3);
|
||||||
|
deltaY = qPow(rawY / 32766.0f * MOUSE_EMULATION_MOTION_MULTIPLIER, 3);
|
||||||
|
|
||||||
|
// Enforce deadzones
|
||||||
|
deltaX = qAbs(deltaX) > MOUSE_EMULATION_DEADZONE ? deltaX - MOUSE_EMULATION_DEADZONE : 0;
|
||||||
|
deltaY = qAbs(deltaY) > MOUSE_EMULATION_DEADZONE ? deltaY - MOUSE_EMULATION_DEADZONE : 0;
|
||||||
|
|
||||||
|
if (deltaX != 0 || deltaY != 0) {
|
||||||
|
LiSendMouseMoveEvent((short)deltaX, (short)deltaY);
|
||||||
|
}
|
||||||
|
|
||||||
|
return interval;
|
||||||
|
}
|
||||||
|
|
||||||
void SdlInputHandler::handleControllerAxisEvent(SDL_ControllerAxisEvent* event)
|
void SdlInputHandler::handleControllerAxisEvent(SDL_ControllerAxisEvent* event)
|
||||||
{
|
{
|
||||||
GamepadState* state = findStateForGamepad(event->which);
|
SDL_JoystickID gameControllerId = event->which;
|
||||||
|
GamepadState* state = findStateForGamepad(gameControllerId);
|
||||||
if (state == NULL) {
|
if (state == NULL) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (event->axis)
|
// Batch all pending axis motion events for this gamepad to save CPU time
|
||||||
{
|
SDL_Event nextEvent;
|
||||||
case SDL_CONTROLLER_AXIS_LEFTX:
|
for (;;) {
|
||||||
state->lsX = event->value;
|
switch (event->axis)
|
||||||
|
{
|
||||||
|
case SDL_CONTROLLER_AXIS_LEFTX:
|
||||||
|
state->lsX = event->value;
|
||||||
|
break;
|
||||||
|
case SDL_CONTROLLER_AXIS_LEFTY:
|
||||||
|
// Signed values have one more negative value than
|
||||||
|
// positive value, so inverting the sign on -32768
|
||||||
|
// could actually cause the value to overflow and
|
||||||
|
// wrap around to be negative again. Avoid that by
|
||||||
|
// capping the value at 32767.
|
||||||
|
state->lsY = -qMax(event->value, (short)-32767);
|
||||||
|
break;
|
||||||
|
case SDL_CONTROLLER_AXIS_RIGHTX:
|
||||||
|
state->rsX = event->value;
|
||||||
|
break;
|
||||||
|
case SDL_CONTROLLER_AXIS_RIGHTY:
|
||||||
|
state->rsY = -qMax(event->value, (short)-32767);
|
||||||
|
break;
|
||||||
|
case SDL_CONTROLLER_AXIS_TRIGGERLEFT:
|
||||||
|
state->lt = (unsigned char)(event->value * 255UL / 32767);
|
||||||
|
break;
|
||||||
|
case SDL_CONTROLLER_AXIS_TRIGGERRIGHT:
|
||||||
|
state->rt = (unsigned char)(event->value * 255UL / 32767);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Unhandled controller axis: %d",
|
||||||
|
event->axis);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for another event to batch with
|
||||||
|
if (SDL_PeepEvents(&nextEvent, 1, SDL_PEEKEVENT, SDL_CONTROLLERAXISMOTION, SDL_CONTROLLERAXISMOTION) <= 0) {
|
||||||
break;
|
break;
|
||||||
case SDL_CONTROLLER_AXIS_LEFTY:
|
}
|
||||||
// Signed values have one more negative value than
|
|
||||||
// positive value, so inverting the sign on -32768
|
event = &nextEvent.caxis;
|
||||||
// could actually cause the value to overflow and
|
if (event->which != gameControllerId) {
|
||||||
// wrap around to be negative again. Avoid that by
|
// Stop batching if a different gamepad interrupts us
|
||||||
// capping the value at 32767.
|
|
||||||
state->lsY = -qMax(event->value, (short)-32767);
|
|
||||||
break;
|
break;
|
||||||
case SDL_CONTROLLER_AXIS_RIGHTX:
|
}
|
||||||
state->rsX = event->value;
|
|
||||||
break;
|
// Remove the next event to batch
|
||||||
case SDL_CONTROLLER_AXIS_RIGHTY:
|
SDL_PeepEvents(&nextEvent, 1, SDL_GETEVENT, SDL_CONTROLLERAXISMOTION, SDL_CONTROLLERAXISMOTION);
|
||||||
state->rsY = -qMax(event->value, (short)-32767);
|
|
||||||
break;
|
|
||||||
case SDL_CONTROLLER_AXIS_TRIGGERLEFT:
|
|
||||||
state->lt = (unsigned char)(event->value * 255UL / 32767);
|
|
||||||
break;
|
|
||||||
case SDL_CONTROLLER_AXIS_TRIGGERRIGHT:
|
|
||||||
state->rt = (unsigned char)(event->value * 255UL / 32767);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Unhandled controller axis: %d",
|
|
||||||
event->axis);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sendGamepadState(state);
|
// Only send the gamepad state to the host if it's not in mouse emulation mode
|
||||||
|
if (state->mouseEmulationTimer == 0) {
|
||||||
|
sendGamepadState(state);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void SdlInputHandler::handleControllerButtonEvent(SDL_ControllerButtonEvent* event)
|
void SdlInputHandler::handleControllerButtonEvent(SDL_ControllerButtonEvent* event)
|
||||||
@@ -623,9 +810,76 @@ void SdlInputHandler::handleControllerButtonEvent(SDL_ControllerButtonEvent* eve
|
|||||||
|
|
||||||
if (event->state == SDL_PRESSED) {
|
if (event->state == SDL_PRESSED) {
|
||||||
state->buttons |= k_ButtonMap[event->button];
|
state->buttons |= k_ButtonMap[event->button];
|
||||||
|
|
||||||
|
if (event->button == SDL_CONTROLLER_BUTTON_START) {
|
||||||
|
state->lastStartDownTime = SDL_GetTicks();
|
||||||
|
}
|
||||||
|
else if (state->mouseEmulationTimer != 0) {
|
||||||
|
if (event->button == SDL_CONTROLLER_BUTTON_A) {
|
||||||
|
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT);
|
||||||
|
}
|
||||||
|
else if (event->button == SDL_CONTROLLER_BUTTON_B) {
|
||||||
|
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_RIGHT);
|
||||||
|
}
|
||||||
|
else if (event->button == SDL_CONTROLLER_BUTTON_X) {
|
||||||
|
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_MIDDLE);
|
||||||
|
}
|
||||||
|
else if (event->button == SDL_CONTROLLER_BUTTON_LEFTSHOULDER) {
|
||||||
|
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_X1);
|
||||||
|
}
|
||||||
|
else if (event->button == SDL_CONTROLLER_BUTTON_RIGHTSHOULDER) {
|
||||||
|
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_X2);
|
||||||
|
}
|
||||||
|
else if (event->button == SDL_CONTROLLER_BUTTON_DPAD_UP) {
|
||||||
|
LiSendScrollEvent(1);
|
||||||
|
}
|
||||||
|
else if (event->button == SDL_CONTROLLER_BUTTON_DPAD_DOWN) {
|
||||||
|
LiSendScrollEvent(-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
state->buttons &= ~k_ButtonMap[event->button];
|
state->buttons &= ~k_ButtonMap[event->button];
|
||||||
|
|
||||||
|
if (event->button == SDL_CONTROLLER_BUTTON_START) {
|
||||||
|
if (SDL_GetTicks() - state->lastStartDownTime > MOUSE_EMULATION_LONG_PRESS_TIME) {
|
||||||
|
if (state->mouseEmulationTimer != 0) {
|
||||||
|
SDL_RemoveTimer(state->mouseEmulationTimer);
|
||||||
|
state->mouseEmulationTimer = 0;
|
||||||
|
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Mouse emulation deactivated");
|
||||||
|
Session::get()->notifyMouseEmulationMode(false);
|
||||||
|
}
|
||||||
|
else if (m_GamepadMouse) {
|
||||||
|
// Send the start button up event to the host, since we won't do it below
|
||||||
|
sendGamepadState(state);
|
||||||
|
|
||||||
|
state->mouseEmulationTimer = SDL_AddTimer(MOUSE_EMULATION_POLLING_INTERVAL, SdlInputHandler::mouseEmulationTimerCallback, state);
|
||||||
|
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Mouse emulation active");
|
||||||
|
Session::get()->notifyMouseEmulationMode(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (state->mouseEmulationTimer != 0) {
|
||||||
|
if (event->button == SDL_CONTROLLER_BUTTON_A) {
|
||||||
|
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
|
||||||
|
}
|
||||||
|
else if (event->button == SDL_CONTROLLER_BUTTON_B) {
|
||||||
|
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_RIGHT);
|
||||||
|
}
|
||||||
|
else if (event->button == SDL_CONTROLLER_BUTTON_X) {
|
||||||
|
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_MIDDLE);
|
||||||
|
}
|
||||||
|
else if (event->button == SDL_CONTROLLER_BUTTON_LEFTSHOULDER) {
|
||||||
|
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_X1);
|
||||||
|
}
|
||||||
|
else if (event->button == SDL_CONTROLLER_BUTTON_RIGHTSHOULDER) {
|
||||||
|
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_X2);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle Start+Select+L1+R1 as a gamepad quit combo
|
// Handle Start+Select+L1+R1 as a gamepad quit combo
|
||||||
@@ -645,7 +899,10 @@ void SdlInputHandler::handleControllerButtonEvent(SDL_ControllerButtonEvent* eve
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
sendGamepadState(state);
|
// Only send the gamepad state to the host if it's not in mouse emulation mode
|
||||||
|
if (state->mouseEmulationTimer == 0) {
|
||||||
|
sendGamepadState(state);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void SdlInputHandler::handleControllerDeviceEvent(SDL_ControllerDeviceEvent* event)
|
void SdlInputHandler::handleControllerDeviceEvent(SDL_ControllerDeviceEvent* event)
|
||||||
@@ -655,8 +912,28 @@ void SdlInputHandler::handleControllerDeviceEvent(SDL_ControllerDeviceEvent* eve
|
|||||||
if (event->type == SDL_CONTROLLERDEVICEADDED) {
|
if (event->type == SDL_CONTROLLERDEVICEADDED) {
|
||||||
int i;
|
int i;
|
||||||
const char* name;
|
const char* name;
|
||||||
|
SDL_GameController* controller;
|
||||||
|
const char* mapping;
|
||||||
|
char guidStr[33];
|
||||||
|
uint32_t hapticCaps;
|
||||||
|
|
||||||
for (i = 0; i < MAX_GAMEPADS; i++) {
|
controller = SDL_GameControllerOpen(event->which);
|
||||||
|
if (controller == NULL) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Failed to open gamepad: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We used to use SDL_GameControllerGetPlayerIndex() here but that
|
||||||
|
// can lead to strange issues due to bugs in Windows where an Xbox
|
||||||
|
// controller will join as player 2, even though no player 1 controller
|
||||||
|
// is connected at all. This pretty much screws any attempt to use
|
||||||
|
// the gamepad in single player games, so just assign them in order from 0.
|
||||||
|
i = 0;
|
||||||
|
|
||||||
|
for (; i < MAX_GAMEPADS; i++) {
|
||||||
|
SDL_assert(m_GamepadState[i].controller != controller);
|
||||||
if (m_GamepadState[i].controller == NULL) {
|
if (m_GamepadState[i].controller == NULL) {
|
||||||
// Found an empty slot
|
// Found an empty slot
|
||||||
break;
|
break;
|
||||||
@@ -666,6 +943,7 @@ void SdlInputHandler::handleControllerDeviceEvent(SDL_ControllerDeviceEvent* eve
|
|||||||
if (i == MAX_GAMEPADS) {
|
if (i == MAX_GAMEPADS) {
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"No open gamepad slots found!");
|
"No open gamepad slots found!");
|
||||||
|
SDL_GameControllerClose(controller);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -677,26 +955,64 @@ void SdlInputHandler::handleControllerDeviceEvent(SDL_ControllerDeviceEvent* eve
|
|||||||
// Always player 1 in single controller mode
|
// Always player 1 in single controller mode
|
||||||
state->index = 0;
|
state->index = 0;
|
||||||
}
|
}
|
||||||
state->controller = SDL_GameControllerOpen(event->which);
|
|
||||||
if (state->controller == NULL) {
|
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Failed to open gamepad: %s",
|
|
||||||
SDL_GetError());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
state->controller = controller;
|
||||||
state->jsId = SDL_JoystickInstanceID(SDL_GameControllerGetJoystick(state->controller));
|
state->jsId = SDL_JoystickInstanceID(SDL_GameControllerGetJoystick(state->controller));
|
||||||
|
|
||||||
|
#if SDL_VERSION_ATLEAST(2, 0, 9)
|
||||||
|
// Perform a no-op rumble to see if haptics are supported
|
||||||
|
hapticCaps = SDL_GameControllerRumble(controller, 0, 0, 0) == 0 ?
|
||||||
|
ML_HAPTIC_GC_RUMBLE : 0;
|
||||||
|
#else
|
||||||
|
state->haptic = SDL_HapticOpenFromJoystick(SDL_GameControllerGetJoystick(state->controller));
|
||||||
|
state->hapticEffectId = -1;
|
||||||
|
state->hapticMethod = GAMEPAD_HAPTIC_METHOD_NONE;
|
||||||
|
if (state->haptic != nullptr) {
|
||||||
|
// Query for supported haptic effects
|
||||||
|
hapticCaps = SDL_HapticQuery(state->haptic);
|
||||||
|
hapticCaps |= SDL_HapticRumbleSupported(state->haptic) ?
|
||||||
|
ML_HAPTIC_SIMPLE_RUMBLE : 0;
|
||||||
|
|
||||||
|
if ((SDL_HapticQuery(state->haptic) & SDL_HAPTIC_LEFTRIGHT) == 0) {
|
||||||
|
if (SDL_HapticRumbleSupported(state->haptic)) {
|
||||||
|
if (SDL_HapticRumbleInit(state->haptic) == 0) {
|
||||||
|
state->hapticMethod = GAMEPAD_HAPTIC_METHOD_SIMPLERUMBLE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (state->hapticMethod == GAMEPAD_HAPTIC_METHOD_NONE) {
|
||||||
|
SDL_HapticClose(state->haptic);
|
||||||
|
state->haptic = nullptr;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
state->hapticMethod = GAMEPAD_HAPTIC_METHOD_LEFTRIGHT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
hapticCaps = 0;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
SDL_JoystickGetGUIDString(SDL_JoystickGetGUID(SDL_GameControllerGetJoystick(state->controller)),
|
||||||
|
guidStr, sizeof(guidStr));
|
||||||
|
mapping = SDL_GameControllerMapping(state->controller);
|
||||||
name = SDL_GameControllerName(state->controller);
|
name = SDL_GameControllerName(state->controller);
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"Gamepad %d (player %d) is: %s",
|
"Gamepad %d (player %d) is: %s (haptic capabilities: 0x%x) (mapping: %s -> %s)",
|
||||||
i,
|
i,
|
||||||
state->index,
|
state->index,
|
||||||
name != NULL ? name : "<null>");
|
name != nullptr ? name : "<null>",
|
||||||
|
hapticCaps,
|
||||||
|
guidStr,
|
||||||
|
mapping != nullptr ? mapping : "<null>");
|
||||||
|
if (mapping != nullptr) {
|
||||||
|
SDL_free((void*)mapping);
|
||||||
|
}
|
||||||
|
|
||||||
// Add this gamepad to the gamepad mask
|
// Add this gamepad to the gamepad mask
|
||||||
if (m_MultiController) {
|
if (m_MultiController) {
|
||||||
SDL_assert(!(m_GamepadMask & (1 << state->index)));
|
// NB: Don't assert that it's unset here because we will already
|
||||||
|
// have the mask set for initially attached gamepads to avoid confusing
|
||||||
|
// apps running on the host.
|
||||||
m_GamepadMask |= (1 << state->index);
|
m_GamepadMask |= (1 << state->index);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -709,8 +1025,19 @@ void SdlInputHandler::handleControllerDeviceEvent(SDL_ControllerDeviceEvent* eve
|
|||||||
else if (event->type == SDL_CONTROLLERDEVICEREMOVED) {
|
else if (event->type == SDL_CONTROLLERDEVICEREMOVED) {
|
||||||
state = findStateForGamepad(event->which);
|
state = findStateForGamepad(event->which);
|
||||||
if (state != NULL) {
|
if (state != NULL) {
|
||||||
|
if (state->mouseEmulationTimer != 0) {
|
||||||
|
Session::get()->notifyMouseEmulationMode(false);
|
||||||
|
SDL_RemoveTimer(state->mouseEmulationTimer);
|
||||||
|
}
|
||||||
|
|
||||||
SDL_GameControllerClose(state->controller);
|
SDL_GameControllerClose(state->controller);
|
||||||
|
|
||||||
|
#if !SDL_VERSION_ATLEAST(2, 0, 9)
|
||||||
|
if (state->haptic != nullptr) {
|
||||||
|
SDL_HapticClose(state->haptic);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
// Remove this from the gamepad mask in MC-mode
|
// Remove this from the gamepad mask in MC-mode
|
||||||
if (m_MultiController) {
|
if (m_MultiController) {
|
||||||
SDL_assert(m_GamepadMask & (1 << state->index));
|
SDL_assert(m_GamepadMask & (1 << state->index));
|
||||||
@@ -763,10 +1090,83 @@ void SdlInputHandler::handleJoystickArrivalEvent(SDL_JoyDeviceEvent* event)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SdlInputHandler::rumble(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor)
|
||||||
|
{
|
||||||
|
// Make sure the controller number is within our supported count
|
||||||
|
if (controllerNumber >= MAX_GAMEPADS) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
#if SDL_VERSION_ATLEAST(2, 0, 9)
|
||||||
|
if (m_GamepadState[controllerNumber].controller != nullptr) {
|
||||||
|
SDL_GameControllerRumble(m_GamepadState[controllerNumber].controller, lowFreqMotor, highFreqMotor, 30000);
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
// Check if the controller supports haptics (and if the controller exists at all)
|
||||||
|
SDL_Haptic* haptic = m_GamepadState[controllerNumber].haptic;
|
||||||
|
if (haptic == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop the last effect we played
|
||||||
|
if (m_GamepadState[controllerNumber].hapticMethod == GAMEPAD_HAPTIC_METHOD_LEFTRIGHT) {
|
||||||
|
if (m_GamepadState[controllerNumber].hapticEffectId >= 0) {
|
||||||
|
SDL_HapticDestroyEffect(haptic, m_GamepadState[controllerNumber].hapticEffectId);
|
||||||
|
}
|
||||||
|
} else if (m_GamepadState[controllerNumber].hapticMethod == GAMEPAD_HAPTIC_METHOD_SIMPLERUMBLE) {
|
||||||
|
SDL_HapticRumbleStop(haptic);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this callback is telling us to stop both motors, don't bother queuing a new effect
|
||||||
|
if (lowFreqMotor == 0 && highFreqMotor == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_GamepadState[controllerNumber].hapticMethod == GAMEPAD_HAPTIC_METHOD_LEFTRIGHT) {
|
||||||
|
SDL_HapticEffect effect;
|
||||||
|
SDL_memset(&effect, 0, sizeof(effect));
|
||||||
|
effect.type = SDL_HAPTIC_LEFTRIGHT;
|
||||||
|
|
||||||
|
// The effect should last until we are instructed to stop or change it
|
||||||
|
effect.leftright.length = SDL_HAPTIC_INFINITY;
|
||||||
|
|
||||||
|
// SDL haptics range from 0-32767 but XInput uses 0-65535, so divide by 2 to correct for SDL's scaling
|
||||||
|
effect.leftright.large_magnitude = lowFreqMotor / 2;
|
||||||
|
effect.leftright.small_magnitude = highFreqMotor / 2;
|
||||||
|
|
||||||
|
// Play the new effect
|
||||||
|
m_GamepadState[controllerNumber].hapticEffectId = SDL_HapticNewEffect(haptic, &effect);
|
||||||
|
if (m_GamepadState[controllerNumber].hapticEffectId >= 0) {
|
||||||
|
SDL_HapticRunEffect(haptic, m_GamepadState[controllerNumber].hapticEffectId, 1);
|
||||||
|
}
|
||||||
|
} else if (m_GamepadState[controllerNumber].hapticMethod == GAMEPAD_HAPTIC_METHOD_SIMPLERUMBLE) {
|
||||||
|
SDL_HapticRumblePlay(haptic,
|
||||||
|
std::min(1.0, (GAMEPAD_HAPTIC_SIMPLE_HIFREQ_MOTOR_WEIGHT*highFreqMotor +
|
||||||
|
GAMEPAD_HAPTIC_SIMPLE_LOWFREQ_MOTOR_WEIGHT*lowFreqMotor) / 65535.0),
|
||||||
|
SDL_HAPTIC_INFINITY);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
void SdlInputHandler::handleTouchFingerEvent(SDL_TouchFingerEvent* event)
|
void SdlInputHandler::handleTouchFingerEvent(SDL_TouchFingerEvent* event)
|
||||||
{
|
{
|
||||||
int fingerIndex = -1;
|
int fingerIndex = -1;
|
||||||
|
|
||||||
|
#if SDL_VERSION_ATLEAST(2, 0, 10)
|
||||||
|
if (SDL_GetTouchDeviceType(event->touchId) != SDL_TOUCH_DEVICE_DIRECT) {
|
||||||
|
// Ignore anything that isn't a touchscreen. We may get callbacks
|
||||||
|
// for trackpads, but we want to handle those in the mouse path.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
#elif defined(Q_OS_DARWIN)
|
||||||
|
// SDL2 sends touch events from trackpads by default on
|
||||||
|
// macOS. This totally screws our actual mouse handling,
|
||||||
|
// so we must explicitly ignore touch events on macOS
|
||||||
|
// until SDL 2.0.10 where we have SDL_GetTouchDeviceType()
|
||||||
|
// to tell them apart.
|
||||||
|
return;
|
||||||
|
#endif
|
||||||
|
|
||||||
// Observations on Windows 10: x and y appear to be relative to 0,0 of the window client area.
|
// Observations on Windows 10: x and y appear to be relative to 0,0 of the window client area.
|
||||||
// Although SDL documentation states they are 0.0 - 1.0 float values, they can actually be higher
|
// Although SDL documentation states they are 0.0 - 1.0 float values, they can actually be higher
|
||||||
// or lower than those values as touch events continue for touches started within the client area that
|
// or lower than those values as touch events continue for touches started within the client area that
|
||||||
@@ -935,6 +1335,35 @@ void SdlInputHandler::raiseAllKeys()
|
|||||||
m_KeysDown.clear();
|
m_KeysDown.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool SdlInputHandler::isCaptureActive()
|
||||||
|
{
|
||||||
|
if (SDL_GetRelativeMouseMode()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Some platforms don't support SDL_SetRelativeMouseMode
|
||||||
|
return m_FakeCaptureActive;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SdlInputHandler::setCaptureActive(bool active)
|
||||||
|
{
|
||||||
|
if (active) {
|
||||||
|
// Try to activate SDL's relative mouse mode
|
||||||
|
if (SDL_SetRelativeMouseMode(SDL_TRUE) < 0) {
|
||||||
|
// Relative mouse mode didn't work, so we'll use fake capture
|
||||||
|
SDL_ShowCursor(SDL_DISABLE);
|
||||||
|
m_FakeCaptureActive = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (m_FakeCaptureActive) {
|
||||||
|
SDL_ShowCursor(SDL_ENABLE);
|
||||||
|
m_FakeCaptureActive = false;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
SDL_SetRelativeMouseMode(SDL_FALSE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
QString SdlInputHandler::getUnmappedGamepads()
|
QString SdlInputHandler::getUnmappedGamepads()
|
||||||
{
|
{
|
||||||
QString ret;
|
QString ret;
|
||||||
@@ -968,7 +1397,7 @@ QString SdlInputHandler::getUnmappedGamepads()
|
|||||||
"Number of axes: %d | Number of buttons: %d | Number of hats: %d",
|
"Number of axes: %d | Number of buttons: %d | Number of hats: %d",
|
||||||
numAxes, numButtons, numHats);
|
numAxes, numButtons, numHats);
|
||||||
|
|
||||||
if (numAxes >= 4 && numButtons >= 8 && numHats <= 1) {
|
if ((numAxes >= 4 && numAxes <= 8) && numButtons >= 8 && numHats <= 1) {
|
||||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"Joystick likely to be an unmapped game controller");
|
"Joystick likely to be an unmapped game controller");
|
||||||
if (!ret.isEmpty()) {
|
if (!ret.isEmpty()) {
|
||||||
|
|||||||
@@ -10,6 +10,15 @@ struct GamepadState {
|
|||||||
SDL_JoystickID jsId;
|
SDL_JoystickID jsId;
|
||||||
short index;
|
short index;
|
||||||
|
|
||||||
|
#if !SDL_VERSION_ATLEAST(2, 0, 9)
|
||||||
|
SDL_Haptic* haptic;
|
||||||
|
int hapticMethod;
|
||||||
|
int hapticEffectId;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
SDL_TimerID mouseEmulationTimer;
|
||||||
|
uint32_t lastStartDownTime;
|
||||||
|
|
||||||
short buttons;
|
short buttons;
|
||||||
short lsX, lsY;
|
short lsX, lsY;
|
||||||
short rsX, rsY;
|
short rsX, rsY;
|
||||||
@@ -19,6 +28,13 @@ struct GamepadState {
|
|||||||
#define MAX_GAMEPADS 4
|
#define MAX_GAMEPADS 4
|
||||||
#define MAX_FINGERS 2
|
#define MAX_FINGERS 2
|
||||||
|
|
||||||
|
#define GAMEPAD_HAPTIC_METHOD_NONE 0
|
||||||
|
#define GAMEPAD_HAPTIC_METHOD_LEFTRIGHT 1
|
||||||
|
#define GAMEPAD_HAPTIC_METHOD_SIMPLERUMBLE 2
|
||||||
|
|
||||||
|
#define GAMEPAD_HAPTIC_SIMPLE_HIFREQ_MOTOR_WEIGHT 0.33
|
||||||
|
#define GAMEPAD_HAPTIC_SIMPLE_LOWFREQ_MOTOR_WEIGHT 0.8
|
||||||
|
|
||||||
class SdlInputHandler
|
class SdlInputHandler
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
@@ -43,12 +59,18 @@ public:
|
|||||||
|
|
||||||
void handleJoystickArrivalEvent(SDL_JoyDeviceEvent* event);
|
void handleJoystickArrivalEvent(SDL_JoyDeviceEvent* event);
|
||||||
|
|
||||||
|
void rumble(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor);
|
||||||
|
|
||||||
void handleTouchFingerEvent(SDL_TouchFingerEvent* event);
|
void handleTouchFingerEvent(SDL_TouchFingerEvent* event);
|
||||||
|
|
||||||
int getAttachedGamepadMask();
|
int getAttachedGamepadMask();
|
||||||
|
|
||||||
void raiseAllKeys();
|
void raiseAllKeys();
|
||||||
|
|
||||||
|
bool isCaptureActive();
|
||||||
|
|
||||||
|
void setCaptureActive(bool active);
|
||||||
|
|
||||||
static
|
static
|
||||||
QString getUnmappedGamepads();
|
QString getUnmappedGamepads();
|
||||||
|
|
||||||
@@ -67,12 +89,21 @@ private:
|
|||||||
static
|
static
|
||||||
Uint32 dragTimerCallback(Uint32 interval, void* param);
|
Uint32 dragTimerCallback(Uint32 interval, void* param);
|
||||||
|
|
||||||
Uint32 m_LastMouseMotionTime;
|
static
|
||||||
|
Uint32 mouseMoveTimerCallback(Uint32 interval, void* param);
|
||||||
|
|
||||||
|
static
|
||||||
|
Uint32 mouseEmulationTimerCallback(Uint32 interval, void* param);
|
||||||
|
|
||||||
bool m_MultiController;
|
bool m_MultiController;
|
||||||
bool m_NeedsInputDelay;
|
bool m_GamepadMouse;
|
||||||
|
SDL_TimerID m_MouseMoveTimer;
|
||||||
|
SDL_atomic_t m_MouseDeltaX;
|
||||||
|
SDL_atomic_t m_MouseDeltaY;
|
||||||
int m_GamepadMask;
|
int m_GamepadMask;
|
||||||
GamepadState m_GamepadState[MAX_GAMEPADS];
|
GamepadState m_GamepadState[MAX_GAMEPADS];
|
||||||
QSet<short> m_KeysDown;
|
QSet<short> m_KeysDown;
|
||||||
|
bool m_FakeCaptureActive;
|
||||||
|
|
||||||
SDL_TouchFingerEvent m_TouchDownEvent[MAX_FINGERS];
|
SDL_TouchFingerEvent m_TouchDownEvent[MAX_FINGERS];
|
||||||
float m_CumulativeDelta[MAX_FINGERS];
|
float m_CumulativeDelta[MAX_FINGERS];
|
||||||
|
|||||||
@@ -1,23 +1,18 @@
|
|||||||
#include "session.h"
|
#include "session.h"
|
||||||
#include "settings/streamingpreferences.h"
|
#include "settings/streamingpreferences.h"
|
||||||
#include "streaming/streamutils.h"
|
#include "streaming/streamutils.h"
|
||||||
|
#include "backend/richpresencemanager.h"
|
||||||
|
|
||||||
#include <Limelight.h>
|
#include <Limelight.h>
|
||||||
#include <SDL.h>
|
#include <SDL.h>
|
||||||
#include "utils.h"
|
#include "utils.h"
|
||||||
|
|
||||||
// HACK: Need to call SetThreadExecutionState() because SDL doesn't
|
|
||||||
#ifdef Q_OS_WIN32
|
|
||||||
#define WIN32_LEAN_AND_MEAN
|
|
||||||
#include <Windows.h>
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef HAVE_FFMPEG
|
#ifdef HAVE_FFMPEG
|
||||||
#include "video/ffmpeg.h"
|
#include "video/ffmpeg.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef HAVE_SLVIDEO
|
#ifdef HAVE_SLVIDEO
|
||||||
#include "video/sl.h"
|
#include "video/slvid.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef Q_OS_WIN32
|
#ifdef Q_OS_WIN32
|
||||||
@@ -44,16 +39,9 @@ CONNECTION_LISTENER_CALLBACKS Session::k_ConnCallbacks = {
|
|||||||
Session::clConnectionTerminated,
|
Session::clConnectionTerminated,
|
||||||
nullptr,
|
nullptr,
|
||||||
nullptr,
|
nullptr,
|
||||||
Session::clLogMessage
|
Session::clLogMessage,
|
||||||
};
|
Session::clRumble,
|
||||||
|
Session::clConnectionStatusUpdate
|
||||||
AUDIO_RENDERER_CALLBACKS Session::k_AudioCallbacks = {
|
|
||||||
Session::arInit,
|
|
||||||
nullptr,
|
|
||||||
nullptr,
|
|
||||||
Session::arCleanup,
|
|
||||||
Session::arDecodeAndPlaySample,
|
|
||||||
CAPABILITY_DIRECT_SUBMIT
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Session* Session::s_ActiveSession;
|
Session* Session::s_ActiveSession;
|
||||||
@@ -79,7 +67,11 @@ void Session::clStageFailed(int stage, long errorCode)
|
|||||||
|
|
||||||
void Session::clConnectionTerminated(long errorCode)
|
void Session::clConnectionTerminated(long errorCode)
|
||||||
{
|
{
|
||||||
emit s_ActiveSession->displayLaunchError("Connection terminated");
|
// Display the termination dialog if this was not intended
|
||||||
|
if (errorCode != 0) {
|
||||||
|
s_ActiveSession->m_UnexpectedTermination = true;
|
||||||
|
emit s_ActiveSession->displayLaunchError("Connection terminated");
|
||||||
|
}
|
||||||
|
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"Connection terminated: %ld",
|
"Connection terminated: %ld",
|
||||||
@@ -104,15 +96,72 @@ void Session::clLogMessage(const char* format, ...)
|
|||||||
va_end(ap);
|
va_end(ap);
|
||||||
}
|
}
|
||||||
|
|
||||||
#define CALL_INITIALIZE(dec) (dec)->initialize(vds, window, videoFormat, width, height, frameRate, enableVsync)
|
void Session::clRumble(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor)
|
||||||
|
{
|
||||||
|
// The input handler can be closed during the stream if LiStopConnection() hasn't completed yet
|
||||||
|
// but the stream has been stopped by the user. In this case, just discard the rumble.
|
||||||
|
SDL_AtomicLock(&s_ActiveSession->m_InputHandlerLock);
|
||||||
|
if (s_ActiveSession->m_InputHandler != nullptr) {
|
||||||
|
s_ActiveSession->m_InputHandler->rumble(controllerNumber, lowFreqMotor, highFreqMotor);
|
||||||
|
}
|
||||||
|
SDL_AtomicUnlock(&s_ActiveSession->m_InputHandlerLock);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Session::clConnectionStatusUpdate(int connectionStatus)
|
||||||
|
{
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Connection status update: %d",
|
||||||
|
connectionStatus);
|
||||||
|
|
||||||
|
if (!s_ActiveSession->m_Preferences->connectionWarnings) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (s_ActiveSession->m_MouseEmulationRefCount > 0) {
|
||||||
|
// Don't display the overlay if mouse emulation is already using it
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (connectionStatus)
|
||||||
|
{
|
||||||
|
case CONN_STATUS_POOR:
|
||||||
|
if (s_ActiveSession->m_StreamConfig.bitrate > 5000) {
|
||||||
|
strcpy(s_ActiveSession->m_OverlayManager.getOverlayText(Overlay::OverlayStatusUpdate), "Slow connection to PC\nReduce your bitrate");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
strcpy(s_ActiveSession->m_OverlayManager.getOverlayText(Overlay::OverlayStatusUpdate), "Poor connection to PC");
|
||||||
|
}
|
||||||
|
s_ActiveSession->m_OverlayManager.setOverlayTextUpdated(Overlay::OverlayStatusUpdate);
|
||||||
|
s_ActiveSession->m_OverlayManager.setOverlayState(Overlay::OverlayStatusUpdate, true);
|
||||||
|
break;
|
||||||
|
case CONN_STATUS_OKAY:
|
||||||
|
s_ActiveSession->m_OverlayManager.setOverlayState(Overlay::OverlayStatusUpdate, false);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bool Session::chooseDecoder(StreamingPreferences::VideoDecoderSelection vds,
|
bool Session::chooseDecoder(StreamingPreferences::VideoDecoderSelection vds,
|
||||||
SDL_Window* window, int videoFormat, int width, int height,
|
SDL_Window* window, int videoFormat, int width, int height,
|
||||||
int frameRate, bool enableVsync, IVideoDecoder*& chosenDecoder)
|
int frameRate, bool enableVsync, bool enableFramePacing, bool testOnly, IVideoDecoder*& chosenDecoder)
|
||||||
{
|
{
|
||||||
|
DECODER_PARAMETERS params;
|
||||||
|
|
||||||
|
params.width = width;
|
||||||
|
params.height = height;
|
||||||
|
params.frameRate = frameRate;
|
||||||
|
params.videoFormat = videoFormat;
|
||||||
|
params.window = window;
|
||||||
|
params.enableVsync = enableVsync;
|
||||||
|
params.enableFramePacing = enableFramePacing;
|
||||||
|
params.vds = vds;
|
||||||
|
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"V-sync %s",
|
||||||
|
enableVsync ? "enabled" : "disabled");
|
||||||
|
|
||||||
#ifdef HAVE_SLVIDEO
|
#ifdef HAVE_SLVIDEO
|
||||||
chosenDecoder = new SLVideoDecoder();
|
chosenDecoder = new SLVideoDecoder(testOnly);
|
||||||
if (CALL_INITIALIZE(chosenDecoder)) {
|
if (chosenDecoder->initialize(¶ms)) {
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"SLVideo video decoder chosen");
|
"SLVideo video decoder chosen");
|
||||||
return true;
|
return true;
|
||||||
@@ -126,8 +175,8 @@ bool Session::chooseDecoder(StreamingPreferences::VideoDecoderSelection vds,
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef HAVE_FFMPEG
|
#ifdef HAVE_FFMPEG
|
||||||
chosenDecoder = new FFmpegVideoDecoder();
|
chosenDecoder = new FFmpegVideoDecoder(testOnly);
|
||||||
if (CALL_INITIALIZE(chosenDecoder)) {
|
if (chosenDecoder->initialize(¶ms)) {
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"FFmpeg-based video decoder chosen");
|
"FFmpeg-based video decoder chosen");
|
||||||
return true;
|
return true;
|
||||||
@@ -201,78 +250,45 @@ int Session::drSubmitDecodeUnit(PDECODE_UNIT du)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Session::isHardwareDecodeAvailable(StreamingPreferences::VideoDecoderSelection vds,
|
bool Session::isHardwareDecodeAvailable(SDL_Window* window,
|
||||||
|
StreamingPreferences::VideoDecoderSelection vds,
|
||||||
int videoFormat, int width, int height, int frameRate)
|
int videoFormat, int width, int height, int frameRate)
|
||||||
{
|
{
|
||||||
IVideoDecoder* decoder;
|
IVideoDecoder* decoder;
|
||||||
|
|
||||||
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
|
if (!chooseDecoder(vds, window, videoFormat, width, height, frameRate, true, false, true, decoder)) {
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s",
|
|
||||||
SDL_GetError());
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
SDL_Window* window = SDL_CreateWindow("", 0, 0, width, height, SDL_WINDOW_HIDDEN);
|
|
||||||
if (!window) {
|
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Failed to create window for hardware decode test: %s",
|
|
||||||
SDL_GetError());
|
|
||||||
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!chooseDecoder(vds, window, videoFormat, width, height, frameRate, true, decoder)) {
|
|
||||||
SDL_DestroyWindow(window);
|
|
||||||
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
SDL_DestroyWindow(window);
|
|
||||||
|
|
||||||
bool ret = decoder->isHardwareAccelerated();
|
bool ret = decoder->isHardwareAccelerated();
|
||||||
|
|
||||||
delete decoder;
|
delete decoder;
|
||||||
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
int Session::getDecoderCapabilities(StreamingPreferences::VideoDecoderSelection vds,
|
bool Session::populateDecoderProperties(SDL_Window* window)
|
||||||
int videoFormat, int width, int height, int frameRate)
|
|
||||||
{
|
{
|
||||||
IVideoDecoder* decoder;
|
IVideoDecoder* decoder;
|
||||||
|
|
||||||
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
|
if (!chooseDecoder(m_Preferences->videoDecoderSelection,
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
window,
|
||||||
"SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s",
|
m_StreamConfig.enableHdr ? VIDEO_FORMAT_H265_MAIN10 :
|
||||||
SDL_GetError());
|
(m_StreamConfig.supportsHevc ? VIDEO_FORMAT_H265 : VIDEO_FORMAT_H264),
|
||||||
|
m_StreamConfig.width,
|
||||||
|
m_StreamConfig.height,
|
||||||
|
m_StreamConfig.fps,
|
||||||
|
true, false, true, decoder)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
SDL_Window* window = SDL_CreateWindow("", 0, 0, width, height, SDL_WINDOW_HIDDEN);
|
m_VideoCallbacks.capabilities = decoder->getDecoderCapabilities();
|
||||||
if (!window) {
|
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Failed to create window for hardware decode test: %s",
|
|
||||||
SDL_GetError());
|
|
||||||
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!chooseDecoder(vds, window, videoFormat, width, height, frameRate, true, decoder)) {
|
m_StreamConfig.colorSpace = decoder->getDecoderColorspace();
|
||||||
SDL_DestroyWindow(window);
|
|
||||||
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
SDL_DestroyWindow(window);
|
|
||||||
|
|
||||||
int caps = decoder->getDecoderCapabilities();
|
|
||||||
|
|
||||||
delete decoder;
|
delete decoder;
|
||||||
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
|
||||||
|
|
||||||
return caps;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Session::Session(NvComputer* computer, NvApp& app, StreamingPreferences *preferences)
|
Session::Session(NvComputer* computer, NvApp& app, StreamingPreferences *preferences)
|
||||||
@@ -287,14 +303,46 @@ Session::Session(NvComputer* computer, NvApp& app, StreamingPreferences *prefere
|
|||||||
m_DisplayOriginX(0),
|
m_DisplayOriginX(0),
|
||||||
m_DisplayOriginY(0),
|
m_DisplayOriginY(0),
|
||||||
m_PendingWindowedTransition(false),
|
m_PendingWindowedTransition(false),
|
||||||
|
m_UnexpectedTermination(true), // Failure prior to streaming is unexpected
|
||||||
|
m_InputHandler(nullptr),
|
||||||
|
m_InputHandlerLock(0),
|
||||||
|
m_MouseEmulationRefCount(0),
|
||||||
m_OpusDecoder(nullptr),
|
m_OpusDecoder(nullptr),
|
||||||
m_AudioRenderer(nullptr),
|
m_AudioRenderer(nullptr),
|
||||||
m_AudioSampleCount(0)
|
m_AudioSampleCount(0),
|
||||||
|
m_DropAudioEndTime(0)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
void Session::initialize()
|
// NB: This may not get destroyed for a long time! Don't put any vital cleanup here.
|
||||||
|
// Use Session::exec() or DeferredSessionCleanupTask instead.
|
||||||
|
Session::~Session()
|
||||||
{
|
{
|
||||||
|
// Acquire session semaphore to ensure all cleanup is done before the destructor returns
|
||||||
|
// and the object is deallocated.
|
||||||
|
s_ActiveSessionSemaphore.acquire();
|
||||||
|
s_ActiveSessionSemaphore.release();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Session::initialize()
|
||||||
|
{
|
||||||
|
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a hidden window to use for decoder initialization tests
|
||||||
|
SDL_Window* testWindow = SDL_CreateWindow("", 0, 0, 1280, 720, SDL_WINDOW_HIDDEN);
|
||||||
|
if (!testWindow) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Failed to create window for hardware decode test: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
qInfo() << "Server GPU:" << m_Computer->gpuModel;
|
qInfo() << "Server GPU:" << m_Computer->gpuModel;
|
||||||
qInfo() << "Server GFE version:" << m_Computer->gfeVersion;
|
qInfo() << "Server GFE version:" << m_Computer->gfeVersion;
|
||||||
|
|
||||||
@@ -302,13 +350,6 @@ void Session::initialize()
|
|||||||
m_VideoCallbacks.setup = drSetup;
|
m_VideoCallbacks.setup = drSetup;
|
||||||
m_VideoCallbacks.submitDecodeUnit = drSubmitDecodeUnit;
|
m_VideoCallbacks.submitDecodeUnit = drSubmitDecodeUnit;
|
||||||
|
|
||||||
// Slice up to 4 times for parallel decode, once slice per core
|
|
||||||
int slices = qMin(MAX_SLICES, SDL_GetCPUCount());
|
|
||||||
m_VideoCallbacks.capabilities |= CAPABILITY_SLICES_PER_FRAME(slices);
|
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Encoder configured for %d slices per frame",
|
|
||||||
slices);
|
|
||||||
|
|
||||||
LiInitializeStreamConfiguration(&m_StreamConfig);
|
LiInitializeStreamConfiguration(&m_StreamConfig);
|
||||||
m_StreamConfig.width = m_Preferences->width;
|
m_StreamConfig.width = m_Preferences->width;
|
||||||
m_StreamConfig.height = m_Preferences->height;
|
m_StreamConfig.height = m_Preferences->height;
|
||||||
@@ -336,6 +377,12 @@ void Session::initialize()
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LiInitializeAudioCallbacks(&m_AudioCallbacks);
|
||||||
|
m_AudioCallbacks.init = arInit;
|
||||||
|
m_AudioCallbacks.cleanup = arCleanup;
|
||||||
|
m_AudioCallbacks.decodeAndPlaySample = arDecodeAndPlaySample;
|
||||||
|
m_AudioCallbacks.capabilities = getAudioRendererCapabilities(m_StreamConfig.audioConfiguration);
|
||||||
|
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"Audio configuration: %d",
|
"Audio configuration: %d",
|
||||||
m_StreamConfig.audioConfiguration);
|
m_StreamConfig.audioConfiguration);
|
||||||
@@ -345,7 +392,8 @@ void Session::initialize()
|
|||||||
case StreamingPreferences::VCC_AUTO:
|
case StreamingPreferences::VCC_AUTO:
|
||||||
// TODO: Determine if HEVC is better depending on the decoder
|
// TODO: Determine if HEVC is better depending on the decoder
|
||||||
m_StreamConfig.supportsHevc =
|
m_StreamConfig.supportsHevc =
|
||||||
isHardwareDecodeAvailable(m_Preferences->videoDecoderSelection,
|
isHardwareDecodeAvailable(testWindow,
|
||||||
|
m_Preferences->videoDecoderSelection,
|
||||||
VIDEO_FORMAT_H265,
|
VIDEO_FORMAT_H265,
|
||||||
m_StreamConfig.width,
|
m_StreamConfig.width,
|
||||||
m_StreamConfig.height,
|
m_StreamConfig.height,
|
||||||
@@ -382,30 +430,35 @@ void Session::initialize()
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (m_Computer->activeAddress == m_Computer->remoteAddress) {
|
|
||||||
m_StreamConfig.streamingRemotely = 1;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
m_StreamConfig.streamingRemotely = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_Computer->activeAddress == m_Computer->localAddress) {
|
|
||||||
m_StreamConfig.packetSize = 1392;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
m_StreamConfig.packetSize = 1024;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (m_Preferences->windowMode)
|
switch (m_Preferences->windowMode)
|
||||||
{
|
{
|
||||||
|
default:
|
||||||
case StreamingPreferences::WM_FULLSCREEN_DESKTOP:
|
case StreamingPreferences::WM_FULLSCREEN_DESKTOP:
|
||||||
m_FullScreenFlag = SDL_WINDOW_FULLSCREEN_DESKTOP;
|
m_FullScreenFlag = SDL_WINDOW_FULLSCREEN_DESKTOP;
|
||||||
break;
|
break;
|
||||||
case StreamingPreferences::WM_FULLSCREEN:
|
case StreamingPreferences::WM_FULLSCREEN:
|
||||||
default:
|
|
||||||
m_FullScreenFlag = SDL_WINDOW_FULLSCREEN;
|
m_FullScreenFlag = SDL_WINDOW_FULLSCREEN;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for validation errors/warnings and emit
|
||||||
|
// signals for them, if appropriate
|
||||||
|
bool ret = validateLaunch(testWindow);
|
||||||
|
|
||||||
|
if (ret) {
|
||||||
|
// Populate decoder-dependent properties.
|
||||||
|
// Must be done after validateLaunch() since m_StreamConfig is finalized.
|
||||||
|
ret = populateDecoderProperties(testWindow);
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_DestroyWindow(testWindow);
|
||||||
|
|
||||||
|
if (!ret) {
|
||||||
|
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Session::emitLaunchWarning(QString text)
|
void Session::emitLaunchWarning(QString text)
|
||||||
@@ -424,7 +477,7 @@ void Session::emitLaunchWarning(QString text)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Session::validateLaunch()
|
bool Session::validateLaunch(SDL_Window* testWindow)
|
||||||
{
|
{
|
||||||
QStringList warningList;
|
QStringList warningList;
|
||||||
|
|
||||||
@@ -444,12 +497,14 @@ bool Session::validateLaunch()
|
|||||||
bool hevcForced = m_Preferences->videoCodecConfig == StreamingPreferences::VCC_FORCE_HEVC ||
|
bool hevcForced = m_Preferences->videoCodecConfig == StreamingPreferences::VCC_FORCE_HEVC ||
|
||||||
m_Preferences->videoCodecConfig == StreamingPreferences::VCC_FORCE_HEVC_HDR;
|
m_Preferences->videoCodecConfig == StreamingPreferences::VCC_FORCE_HEVC_HDR;
|
||||||
|
|
||||||
if (!isHardwareDecodeAvailable(m_Preferences->videoDecoderSelection,
|
if (m_Preferences->videoDecoderSelection == StreamingPreferences::VDS_AUTO && // Force hardware decoding checked below
|
||||||
VIDEO_FORMAT_H265,
|
m_Preferences->videoCodecConfig != StreamingPreferences::VCC_AUTO && // Already checked in initialize()
|
||||||
m_StreamConfig.width,
|
!isHardwareDecodeAvailable(testWindow,
|
||||||
m_StreamConfig.height,
|
m_Preferences->videoDecoderSelection,
|
||||||
m_StreamConfig.fps) &&
|
VIDEO_FORMAT_H265,
|
||||||
m_Preferences->videoDecoderSelection == StreamingPreferences::VDS_AUTO) {
|
m_StreamConfig.width,
|
||||||
|
m_StreamConfig.height,
|
||||||
|
m_StreamConfig.fps)) {
|
||||||
if (hevcForced) {
|
if (hevcForced) {
|
||||||
emitLaunchWarning("Using software decoding due to your selection to force HEVC without GPU support. This may cause poor streaming performance.");
|
emitLaunchWarning("Using software decoding due to your selection to force HEVC without GPU support. This may cause poor streaming performance.");
|
||||||
}
|
}
|
||||||
@@ -485,7 +540,8 @@ bool Session::validateLaunch()
|
|||||||
emitLaunchWarning("Your host PC GPU doesn't support HDR streaming. "
|
emitLaunchWarning("Your host PC GPU doesn't support HDR streaming. "
|
||||||
"A GeForce GTX 1000-series (Pascal) or later GPU is required for HDR streaming.");
|
"A GeForce GTX 1000-series (Pascal) or later GPU is required for HDR streaming.");
|
||||||
}
|
}
|
||||||
else if (!isHardwareDecodeAvailable(m_Preferences->videoDecoderSelection,
|
else if (!isHardwareDecodeAvailable(testWindow,
|
||||||
|
m_Preferences->videoDecoderSelection,
|
||||||
VIDEO_FORMAT_H265_MAIN10,
|
VIDEO_FORMAT_H265_MAIN10,
|
||||||
m_StreamConfig.width,
|
m_StreamConfig.width,
|
||||||
m_StreamConfig.height,
|
m_StreamConfig.height,
|
||||||
@@ -505,15 +561,6 @@ bool Session::validateLaunch()
|
|||||||
if (m_Computer->gfeVersion.isEmpty() || m_Computer->gfeVersion.startsWith("2.")) {
|
if (m_Computer->gfeVersion.isEmpty() || m_Computer->gfeVersion.startsWith("2.")) {
|
||||||
emitLaunchWarning("GeForce Experience 3.0 or higher is required for 4K streaming.");
|
emitLaunchWarning("GeForce Experience 3.0 or higher is required for 4K streaming.");
|
||||||
|
|
||||||
m_StreamConfig.width = 1920;
|
|
||||||
m_StreamConfig.height = 1080;
|
|
||||||
}
|
|
||||||
// This list is sorted from least to greatest
|
|
||||||
else if (m_Computer->displayModes.last().width < 3840 ||
|
|
||||||
(m_Computer->displayModes.last().refreshRate < 60 && m_StreamConfig.fps >= 60)) {
|
|
||||||
emitLaunchWarning("Your host PC GPU doesn't support 4K streaming. "
|
|
||||||
"A GeForce GTX 900-series (Maxwell) or later GPU is required for 4K streaming.");
|
|
||||||
|
|
||||||
m_StreamConfig.width = 1920;
|
m_StreamConfig.width = 1920;
|
||||||
m_StreamConfig.height = 1080;
|
m_StreamConfig.height = 1080;
|
||||||
}
|
}
|
||||||
@@ -543,7 +590,9 @@ bool Session::validateLaunch()
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (m_Preferences->videoDecoderSelection == StreamingPreferences::VDS_FORCE_HARDWARE &&
|
if (m_Preferences->videoDecoderSelection == StreamingPreferences::VDS_FORCE_HARDWARE &&
|
||||||
!isHardwareDecodeAvailable(m_Preferences->videoDecoderSelection,
|
!m_StreamConfig.enableHdr && // HEVC Main10 was already checked for hardware decode support above
|
||||||
|
!isHardwareDecodeAvailable(testWindow,
|
||||||
|
m_Preferences->videoDecoderSelection,
|
||||||
m_StreamConfig.supportsHevc ? VIDEO_FORMAT_H265 : VIDEO_FORMAT_H264,
|
m_StreamConfig.supportsHevc ? VIDEO_FORMAT_H265 : VIDEO_FORMAT_H264,
|
||||||
m_StreamConfig.width,
|
m_StreamConfig.width,
|
||||||
m_StreamConfig.height,
|
m_StreamConfig.height,
|
||||||
@@ -559,38 +608,71 @@ bool Session::validateLaunch()
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the capability flags from the chosen decoder/renderer
|
|
||||||
m_VideoCallbacks.capabilities |= getDecoderCapabilities(m_Preferences->videoDecoderSelection,
|
|
||||||
m_StreamConfig.supportsHevc ? VIDEO_FORMAT_H265 : VIDEO_FORMAT_H264,
|
|
||||||
m_StreamConfig.width,
|
|
||||||
m_StreamConfig.height,
|
|
||||||
m_StreamConfig.fps);
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class DeferredSessionCleanupTask : public QRunnable
|
class DeferredSessionCleanupTask : public QRunnable
|
||||||
{
|
{
|
||||||
void run() override
|
public:
|
||||||
{
|
DeferredSessionCleanupTask(Session* session) :
|
||||||
// Finish cleanup of the connection state
|
m_Session(session) {}
|
||||||
LiStopConnection();
|
|
||||||
|
|
||||||
|
private:
|
||||||
|
virtual ~DeferredSessionCleanupTask() override
|
||||||
|
{
|
||||||
// Allow another session to start now that we're cleaned up
|
// Allow another session to start now that we're cleaned up
|
||||||
Session::s_ActiveSession = nullptr;
|
Session::s_ActiveSession = nullptr;
|
||||||
Session::s_ActiveSessionSemaphore.release();
|
Session::s_ActiveSessionSemaphore.release();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void run() override
|
||||||
|
{
|
||||||
|
// Only quit the running app if our session terminated gracefully
|
||||||
|
bool shouldQuit =
|
||||||
|
!m_Session->m_UnexpectedTermination &&
|
||||||
|
m_Session->m_Preferences->quitAppAfter;
|
||||||
|
|
||||||
|
// Notify the UI
|
||||||
|
if (shouldQuit) {
|
||||||
|
emit m_Session->quitStarting();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
emit m_Session->sessionFinished();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
// Logging is already done inside NvHTTP
|
||||||
|
try {
|
||||||
|
http.quitApp();
|
||||||
|
} catch (const GfeHttpResponseException&) {
|
||||||
|
} catch (const QtNetworkReplyException&) {
|
||||||
|
}
|
||||||
|
|
||||||
|
// Session is finished now
|
||||||
|
emit m_Session->sessionFinished();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Session* m_Session;
|
||||||
};
|
};
|
||||||
|
|
||||||
void Session::getWindowDimensions(int& x, int& y,
|
void Session::getWindowDimensions(int& x, int& y,
|
||||||
int& width, int& height)
|
int& width, int& height)
|
||||||
{
|
{
|
||||||
int displayIndex = 0;
|
int displayIndex = 0;
|
||||||
|
bool fullScreen;
|
||||||
|
|
||||||
if (m_Window != nullptr) {
|
if (m_Window != nullptr) {
|
||||||
displayIndex = SDL_GetWindowDisplayIndex(m_Window);
|
displayIndex = SDL_GetWindowDisplayIndex(m_Window);
|
||||||
SDL_assert(displayIndex >= 0);
|
SDL_assert(displayIndex >= 0);
|
||||||
|
fullScreen = (SDL_GetWindowFlags(m_Window) & SDL_WINDOW_FULLSCREEN);
|
||||||
}
|
}
|
||||||
// Create our window on the same display that Qt's UI
|
// Create our window on the same display that Qt's UI
|
||||||
// was being displayed on.
|
// was being displayed on.
|
||||||
@@ -617,10 +699,16 @@ void Session::getWindowDimensions(int& x, int& y,
|
|||||||
i, SDL_GetError());
|
i, SDL_GetError());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fullScreen = (m_Preferences->windowMode != StreamingPreferences::WM_WINDOWED);
|
||||||
}
|
}
|
||||||
|
|
||||||
SDL_Rect usableBounds;
|
SDL_Rect usableBounds;
|
||||||
if (SDL_GetDisplayUsableBounds(displayIndex, &usableBounds) == 0) {
|
if (fullScreen && SDL_GetDisplayBounds(displayIndex, &usableBounds) == 0) {
|
||||||
|
width = usableBounds.w;
|
||||||
|
height = usableBounds.h;
|
||||||
|
}
|
||||||
|
else if (SDL_GetDisplayUsableBounds(displayIndex, &usableBounds) == 0) {
|
||||||
width = usableBounds.w;
|
width = usableBounds.w;
|
||||||
height = usableBounds.h;
|
height = usableBounds.h;
|
||||||
|
|
||||||
@@ -733,6 +821,22 @@ void Session::toggleFullscreen()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Session::notifyMouseEmulationMode(bool enabled)
|
||||||
|
{
|
||||||
|
m_MouseEmulationRefCount += enabled ? 1 : -1;
|
||||||
|
SDL_assert(m_MouseEmulationRefCount >= 0);
|
||||||
|
|
||||||
|
// We re-use the status update overlay for mouse mode notification
|
||||||
|
if (m_MouseEmulationRefCount > 0) {
|
||||||
|
strcpy(m_OverlayManager.getOverlayText(Overlay::OverlayStatusUpdate), "Gamepad mouse mode active\nLong press Start to deactivate");
|
||||||
|
m_OverlayManager.setOverlayTextUpdated(Overlay::OverlayStatusUpdate);
|
||||||
|
m_OverlayManager.setOverlayState(Overlay::OverlayStatusUpdate, true);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
m_OverlayManager.setOverlayState(Overlay::OverlayStatusUpdate, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void Session::exec(int displayOriginX, int displayOriginY)
|
void Session::exec(int displayOriginX, int displayOriginY)
|
||||||
{
|
{
|
||||||
m_DisplayOriginX = displayOriginX;
|
m_DisplayOriginX = displayOriginX;
|
||||||
@@ -741,11 +845,8 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
|||||||
// Complete initialization in this deferred context to avoid
|
// Complete initialization in this deferred context to avoid
|
||||||
// calling expensive functions in the constructor (during the
|
// calling expensive functions in the constructor (during the
|
||||||
// process of loading the StreamSegue).
|
// process of loading the StreamSegue).
|
||||||
initialize();
|
if (!initialize()) {
|
||||||
|
emit sessionFinished();
|
||||||
// Check for validation errors/warnings and emit
|
|
||||||
// signals for them, if appropriate
|
|
||||||
if (!validateLaunch()) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -766,15 +867,18 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
|||||||
|
|
||||||
// Initialize the gamepad code with our preferences
|
// Initialize the gamepad code with our preferences
|
||||||
StreamingPreferences prefs;
|
StreamingPreferences prefs;
|
||||||
SdlInputHandler inputHandler(prefs, m_Computer,
|
m_InputHandler = new SdlInputHandler(prefs, m_Computer,
|
||||||
m_StreamConfig.width,
|
m_StreamConfig.width,
|
||||||
m_StreamConfig.height);
|
m_StreamConfig.height);
|
||||||
|
|
||||||
// The UI should have ensured the old game was already quit
|
// The UI should have ensured the old game was already quit
|
||||||
// if we decide to stream a different game.
|
// if we decide to stream a different game.
|
||||||
Q_ASSERT(m_Computer->currentGameId == 0 ||
|
Q_ASSERT(m_Computer->currentGameId == 0 ||
|
||||||
m_Computer->currentGameId == m_App.id);
|
m_Computer->currentGameId == m_App.id);
|
||||||
|
|
||||||
|
// SOPS will set all settings to 720p60 if it doesn't recognize
|
||||||
|
// the chosen resolution. Avoid that by disabling SOPS when it
|
||||||
|
// is not streaming a supported resolution.
|
||||||
bool enableGameOptimizations = false;
|
bool enableGameOptimizations = false;
|
||||||
for (const NvDisplayMode &mode : m_Computer->displayModes) {
|
for (const NvDisplayMode &mode : m_Computer->displayModes) {
|
||||||
if (mode.width == m_StreamConfig.width &&
|
if (mode.width == m_StreamConfig.width &&
|
||||||
@@ -788,7 +892,7 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
NvHTTP http(m_Computer->activeAddress);
|
NvHTTP http(m_Computer->activeAddress, m_Computer->serverCert);
|
||||||
if (m_Computer->currentGameId != 0) {
|
if (m_Computer->currentGameId != 0) {
|
||||||
http.resumeApp(&m_StreamConfig);
|
http.resumeApp(&m_StreamConfig);
|
||||||
}
|
}
|
||||||
@@ -796,25 +900,21 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
|||||||
http.launchApp(m_App.id, &m_StreamConfig,
|
http.launchApp(m_App.id, &m_StreamConfig,
|
||||||
enableGameOptimizations,
|
enableGameOptimizations,
|
||||||
prefs.playAudioOnHost,
|
prefs.playAudioOnHost,
|
||||||
inputHandler.getAttachedGamepadMask());
|
m_InputHandler->getAttachedGamepadMask());
|
||||||
}
|
}
|
||||||
} catch (const GfeHttpResponseException& e) {
|
} catch (const GfeHttpResponseException& e) {
|
||||||
emit displayLaunchError(e.toQString());
|
delete m_InputHandler;
|
||||||
s_ActiveSessionSemaphore.release();
|
m_InputHandler = nullptr;
|
||||||
|
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||||
|
emit displayLaunchError("GeForce Experience returned error: " + e.toQString());
|
||||||
|
QThreadPool::globalInstance()->start(new DeferredSessionCleanupTask(this));
|
||||||
return;
|
return;
|
||||||
} catch (const QtNetworkReplyException& e) {
|
} catch (const QtNetworkReplyException& e) {
|
||||||
|
delete m_InputHandler;
|
||||||
|
m_InputHandler = nullptr;
|
||||||
|
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||||
emit displayLaunchError(e.toQString());
|
emit displayLaunchError(e.toQString());
|
||||||
s_ActiveSessionSemaphore.release();
|
QThreadPool::globalInstance()->start(new DeferredSessionCleanupTask(this));
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
SDL_assert(!SDL_WasInit(SDL_INIT_VIDEO));
|
|
||||||
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
|
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s",
|
|
||||||
SDL_GetError());
|
|
||||||
emit displayLaunchError(QString::fromLocal8Bit(SDL_GetError()));
|
|
||||||
s_ActiveSessionSemaphore.release();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -834,15 +934,30 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
|||||||
hostInfo.serverInfoGfeVersion = siGfeVersion.data();
|
hostInfo.serverInfoGfeVersion = siGfeVersion.data();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isReachableOverVpn() does network I/O, so we only attempt to check
|
||||||
|
// VPN reachability if we've already contacted the PC successfully
|
||||||
|
if (m_Computer->isReachableOverVpn()) {
|
||||||
|
// It looks like our route to this PC is over a VPN.
|
||||||
|
// Treat it as remote even if the target address is in RFC 1918 address space.
|
||||||
|
m_StreamConfig.streamingRemotely = STREAM_CFG_REMOTE;
|
||||||
|
m_StreamConfig.packetSize = 1024;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
m_StreamConfig.streamingRemotely = STREAM_CFG_AUTO;
|
||||||
|
m_StreamConfig.packetSize = 1392;
|
||||||
|
}
|
||||||
|
|
||||||
int err = LiStartConnection(&hostInfo, &m_StreamConfig, &k_ConnCallbacks,
|
int err = LiStartConnection(&hostInfo, &m_StreamConfig, &k_ConnCallbacks,
|
||||||
&m_VideoCallbacks,
|
&m_VideoCallbacks,
|
||||||
m_AudioDisabled ? nullptr : &k_AudioCallbacks,
|
m_AudioDisabled ? nullptr : &m_AudioCallbacks,
|
||||||
NULL, 0, NULL, 0);
|
NULL, 0, NULL, 0);
|
||||||
if (err != 0) {
|
if (err != 0) {
|
||||||
// We already displayed an error dialog in the stage failure
|
// We already displayed an error dialog in the stage failure
|
||||||
// listener.
|
// listener.
|
||||||
|
delete m_InputHandler;
|
||||||
|
m_InputHandler = nullptr;
|
||||||
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||||
s_ActiveSessionSemaphore.release();
|
QThreadPool::globalInstance()->start(new DeferredSessionCleanupTask(this));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -863,8 +978,10 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
|||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"SDL_CreateWindow() failed: %s",
|
"SDL_CreateWindow() failed: %s",
|
||||||
SDL_GetError());
|
SDL_GetError());
|
||||||
|
delete m_InputHandler;
|
||||||
|
m_InputHandler = nullptr;
|
||||||
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||||
s_ActiveSessionSemaphore.release();
|
QThreadPool::globalInstance()->start(new DeferredSessionCleanupTask(this));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -917,7 +1034,11 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
|||||||
// This prevents the mouse from becoming trapped inside
|
// This prevents the mouse from becoming trapped inside
|
||||||
// Moonlight when it's halted at a debug break.
|
// Moonlight when it's halted at a debug break.
|
||||||
if (m_Preferences->windowMode != StreamingPreferences::WM_WINDOWED) {
|
if (m_Preferences->windowMode != StreamingPreferences::WM_WINDOWED) {
|
||||||
SDL_SetRelativeMouseMode(SDL_TRUE);
|
// HACK: This doesn't work on Wayland until we render a frame, so
|
||||||
|
// just don't do it for now.
|
||||||
|
if (strcmp(SDL_GetCurrentVideoDriver(), "wayland") != 0) {
|
||||||
|
m_InputHandler->setCaptureActive(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -930,27 +1051,20 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
|||||||
// Disable the screen saver
|
// Disable the screen saver
|
||||||
SDL_DisableScreenSaver();
|
SDL_DisableScreenSaver();
|
||||||
|
|
||||||
#ifdef Q_OS_WIN32
|
|
||||||
// HACK: SDL doesn't call this, so we must do so to disable the
|
|
||||||
// screensaver when we're in windowed mode. DirectX will disable
|
|
||||||
// it for us when we're in full-screen exclusive mode.
|
|
||||||
SetThreadExecutionState(ES_CONTINUOUS | ES_DISPLAY_REQUIRED);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Set timer resolution to 1 ms on Windows for greater
|
// Set timer resolution to 1 ms on Windows for greater
|
||||||
// sleep precision and more accurate callback timing.
|
// sleep precision and more accurate callback timing.
|
||||||
SDL_SetHint(SDL_HINT_TIMER_RESOLUTION, "1");
|
SDL_SetHint(SDL_HINT_TIMER_RESOLUTION, "1");
|
||||||
|
|
||||||
// Raise the priority of the main thread, since it handles
|
|
||||||
// time-sensitive video rendering
|
|
||||||
if (SDL_SetThreadPriority(SDL_THREAD_PRIORITY_HIGH) < 0) {
|
|
||||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Unable to set main thread to high priority: %s",
|
|
||||||
SDL_GetError());
|
|
||||||
}
|
|
||||||
|
|
||||||
int currentDisplayIndex = SDL_GetWindowDisplayIndex(m_Window);
|
int currentDisplayIndex = SDL_GetWindowDisplayIndex(m_Window);
|
||||||
|
|
||||||
|
// Now that we're about to stream, any SDL_QUIT event is expected
|
||||||
|
// unless it comes from the connection termination callback where
|
||||||
|
// (m_UnexpectedTermination is set back to true).
|
||||||
|
m_UnexpectedTermination = false;
|
||||||
|
|
||||||
|
// Start rich presence to indicate we're in game
|
||||||
|
RichPresenceManager presence(prefs, m_App.name);
|
||||||
|
|
||||||
// Hijack this thread to be the SDL main thread. We have to do this
|
// 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.
|
// because we want to suspend all Qt processing until the stream is over.
|
||||||
SDL_Event event;
|
SDL_Event event;
|
||||||
@@ -960,7 +1074,14 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
|||||||
// blocks this thread too long for high polling rate mice and high
|
// blocks this thread too long for high polling rate mice and high
|
||||||
// refresh rate displays.
|
// refresh rate displays.
|
||||||
if (!SDL_PollEvent(&event)) {
|
if (!SDL_PollEvent(&event)) {
|
||||||
|
#ifndef STEAM_LINK
|
||||||
SDL_Delay(1);
|
SDL_Delay(1);
|
||||||
|
#else
|
||||||
|
// Waking every 1 ms to process input is too much for the low performance
|
||||||
|
// ARM core in the Steam Link, so we will wait 10 ms instead.
|
||||||
|
SDL_Delay(10);
|
||||||
|
#endif
|
||||||
|
presence.runCallbacks();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
@@ -968,25 +1089,11 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
|||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"Quit event received");
|
"Quit event received");
|
||||||
goto DispatchDeferredCleanup;
|
goto DispatchDeferredCleanup;
|
||||||
case SDL_USEREVENT: {
|
|
||||||
SDL_Event nextEvent;
|
|
||||||
|
|
||||||
|
case SDL_USEREVENT:
|
||||||
SDL_assert(event.user.code == SDL_CODE_FRAME_READY);
|
SDL_assert(event.user.code == SDL_CODE_FRAME_READY);
|
||||||
|
m_VideoDecoder->renderFrameOnMainThread();
|
||||||
// Drop any earlier frames
|
|
||||||
while (SDL_PeepEvents(&nextEvent,
|
|
||||||
1,
|
|
||||||
SDL_GETEVENT,
|
|
||||||
SDL_USEREVENT,
|
|
||||||
SDL_USEREVENT) == 1) {
|
|
||||||
m_VideoDecoder->dropFrame(&event.user);
|
|
||||||
event = nextEvent;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Render the last frame
|
|
||||||
m_VideoDecoder->renderFrame(&event.user);
|
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
|
|
||||||
case SDL_WINDOWEVENT:
|
case SDL_WINDOWEVENT:
|
||||||
// Capture mouse cursor when user actives the window by clicking on
|
// Capture mouse cursor when user actives the window by clicking on
|
||||||
@@ -1008,25 +1115,24 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
|||||||
SDL_GetWindowPosition(m_Window, &x, &y);
|
SDL_GetWindowPosition(m_Window, &x, &y);
|
||||||
SDL_GetWindowSize(m_Window, &width, &height);
|
SDL_GetWindowSize(m_Window, &width, &height);
|
||||||
if (mouseX > x && mouseX < x+width && mouseY > y && mouseY < y+height) {
|
if (mouseX > x && mouseX < x+width && mouseY > y && mouseY < y+height) {
|
||||||
SDL_SetRelativeMouseMode(SDL_TRUE);
|
m_InputHandler->setCaptureActive(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|
||||||
if (event.window.event == SDL_WINDOWEVENT_FOCUS_LOST) {
|
if (event.window.event == SDL_WINDOWEVENT_FOCUS_LOST) {
|
||||||
// Release mouse cursor when another window is activated (e.g. by using ALT+TAB).
|
// Release mouse cursor when another window is activated (e.g. by using ALT+TAB).
|
||||||
// This lets user to interact with our window's title bar and with the buttons in it.
|
// This lets user to interact with our window's title bar and with the buttons in it.
|
||||||
// Doing this while the window is full-screen breaks the transition out of FS
|
// Doing this while the window is full-screen breaks the transition out of FS
|
||||||
// (desktop and exclusive), so we must check for that before releasing mouse capture.
|
// (desktop and exclusive), so we must check for that before releasing mouse capture.
|
||||||
if (!(SDL_GetWindowFlags(m_Window) & SDL_WINDOW_FULLSCREEN)) {
|
if (!(SDL_GetWindowFlags(m_Window) & SDL_WINDOW_FULLSCREEN)) {
|
||||||
SDL_SetRelativeMouseMode(SDL_FALSE);
|
m_InputHandler->setCaptureActive(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Raise all keys that are currently pressed. If we don't do this, certain keys
|
// Raise all keys that are currently pressed. If we don't do this, certain keys
|
||||||
// used in shortcuts that cause focus loss (such as Alt+Tab) may get stuck down.
|
// used in shortcuts that cause focus loss (such as Alt+Tab) may get stuck down.
|
||||||
inputHandler.raiseAllKeys();
|
m_InputHandler->raiseAllKeys();
|
||||||
}
|
}
|
||||||
|
|
||||||
// We want to recreate the decoder for resizes (full-screen toggles) and the initial shown event.
|
// We want to recreate the decoder for resizes (full-screen toggles) and the initial shown event.
|
||||||
@@ -1098,6 +1204,8 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
|||||||
m_Window, m_ActiveVideoFormat, m_ActiveVideoWidth,
|
m_Window, m_ActiveVideoFormat, m_ActiveVideoWidth,
|
||||||
m_ActiveVideoHeight, m_ActiveVideoFrameRate,
|
m_ActiveVideoHeight, m_ActiveVideoFrameRate,
|
||||||
enableVsync,
|
enableVsync,
|
||||||
|
enableVsync && m_Preferences->framePacing,
|
||||||
|
false,
|
||||||
s_ActiveSession->m_VideoDecoder)) {
|
s_ActiveSession->m_VideoDecoder)) {
|
||||||
SDL_AtomicUnlock(&m_DecoderLock);
|
SDL_AtomicUnlock(&m_DecoderLock);
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
@@ -1115,60 +1223,58 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
|||||||
|
|
||||||
case SDL_KEYUP:
|
case SDL_KEYUP:
|
||||||
case SDL_KEYDOWN:
|
case SDL_KEYDOWN:
|
||||||
inputHandler.handleKeyEvent(&event.key);
|
m_InputHandler->handleKeyEvent(&event.key);
|
||||||
break;
|
break;
|
||||||
case SDL_MOUSEBUTTONDOWN:
|
case SDL_MOUSEBUTTONDOWN:
|
||||||
case SDL_MOUSEBUTTONUP:
|
case SDL_MOUSEBUTTONUP:
|
||||||
inputHandler.handleMouseButtonEvent(&event.button);
|
m_InputHandler->handleMouseButtonEvent(&event.button);
|
||||||
break;
|
break;
|
||||||
case SDL_MOUSEMOTION:
|
case SDL_MOUSEMOTION:
|
||||||
inputHandler.handleMouseMotionEvent(&event.motion);
|
m_InputHandler->handleMouseMotionEvent(&event.motion);
|
||||||
break;
|
break;
|
||||||
case SDL_MOUSEWHEEL:
|
case SDL_MOUSEWHEEL:
|
||||||
inputHandler.handleMouseWheelEvent(&event.wheel);
|
m_InputHandler->handleMouseWheelEvent(&event.wheel);
|
||||||
break;
|
break;
|
||||||
case SDL_CONTROLLERAXISMOTION:
|
case SDL_CONTROLLERAXISMOTION:
|
||||||
inputHandler.handleControllerAxisEvent(&event.caxis);
|
m_InputHandler->handleControllerAxisEvent(&event.caxis);
|
||||||
break;
|
break;
|
||||||
case SDL_CONTROLLERBUTTONDOWN:
|
case SDL_CONTROLLERBUTTONDOWN:
|
||||||
case SDL_CONTROLLERBUTTONUP:
|
case SDL_CONTROLLERBUTTONUP:
|
||||||
inputHandler.handleControllerButtonEvent(&event.cbutton);
|
m_InputHandler->handleControllerButtonEvent(&event.cbutton);
|
||||||
break;
|
break;
|
||||||
case SDL_CONTROLLERDEVICEADDED:
|
case SDL_CONTROLLERDEVICEADDED:
|
||||||
case SDL_CONTROLLERDEVICEREMOVED:
|
case SDL_CONTROLLERDEVICEREMOVED:
|
||||||
inputHandler.handleControllerDeviceEvent(&event.cdevice);
|
m_InputHandler->handleControllerDeviceEvent(&event.cdevice);
|
||||||
break;
|
break;
|
||||||
case SDL_JOYDEVICEADDED:
|
case SDL_JOYDEVICEADDED:
|
||||||
inputHandler.handleJoystickArrivalEvent(&event.jdevice);
|
m_InputHandler->handleJoystickArrivalEvent(&event.jdevice);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// SDL2 sends touch events from trackpads by default on
|
|
||||||
// macOS. This totally screws our actual mouse handling,
|
|
||||||
// so we must explicitly ignore touch events on macOS.
|
|
||||||
#ifndef Q_OS_DARWIN
|
|
||||||
case SDL_FINGERDOWN:
|
case SDL_FINGERDOWN:
|
||||||
case SDL_FINGERMOTION:
|
case SDL_FINGERMOTION:
|
||||||
case SDL_FINGERUP:
|
case SDL_FINGERUP:
|
||||||
inputHandler.handleTouchFingerEvent(&event.tfinger);
|
m_InputHandler->handleTouchFingerEvent(&event.tfinger);
|
||||||
break;
|
break;
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DispatchDeferredCleanup:
|
DispatchDeferredCleanup:
|
||||||
#ifdef Q_OS_WIN32
|
|
||||||
// HACK: See comment above
|
|
||||||
SetThreadExecutionState(ES_CONTINUOUS);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Uncapture the mouse and hide the window immediately,
|
// Uncapture the mouse and hide the window immediately,
|
||||||
// so we can return to the Qt GUI ASAP.
|
// so we can return to the Qt GUI ASAP.
|
||||||
SDL_SetRelativeMouseMode(SDL_FALSE);
|
m_InputHandler->setCaptureActive(false);
|
||||||
SDL_EnableScreenSaver();
|
SDL_EnableScreenSaver();
|
||||||
SDL_SetHint(SDL_HINT_TIMER_RESOLUTION, "0");
|
SDL_SetHint(SDL_HINT_TIMER_RESOLUTION, "0");
|
||||||
|
|
||||||
// Raise any keys that are still down
|
// Raise any keys that are still down
|
||||||
inputHandler.raiseAllKeys();
|
m_InputHandler->raiseAllKeys();
|
||||||
|
|
||||||
|
// Destroy the input handler now. Any rumble callbacks that
|
||||||
|
// occur after this point will be discarded. This must be destroyed
|
||||||
|
// before allow the UI to continue execution or it could interfere
|
||||||
|
// with SDLGamepadKeyNavigation.
|
||||||
|
SDL_AtomicLock(&m_InputHandlerLock);
|
||||||
|
delete m_InputHandler;
|
||||||
|
m_InputHandler = nullptr;
|
||||||
|
SDL_AtomicUnlock(&m_InputHandlerLock);
|
||||||
|
|
||||||
// Destroy the decoder, since this must be done on the main thread
|
// Destroy the decoder, since this must be done on the main thread
|
||||||
SDL_AtomicLock(&m_DecoderLock);
|
SDL_AtomicLock(&m_DecoderLock);
|
||||||
@@ -1176,16 +1282,38 @@ DispatchDeferredCleanup:
|
|||||||
m_VideoDecoder = nullptr;
|
m_VideoDecoder = nullptr;
|
||||||
SDL_AtomicUnlock(&m_DecoderLock);
|
SDL_AtomicUnlock(&m_DecoderLock);
|
||||||
|
|
||||||
|
if (strcmp(SDL_GetCurrentVideoDriver(), "wayland") == 0) {
|
||||||
|
// HACK: SDL (as of 2.0.10) has a bug that causes Mutter not to destroy the window
|
||||||
|
// surface when in full-screen unless we render more frames after we request
|
||||||
|
// to exit full-screen. The amount of frames required is variable but 500 ms
|
||||||
|
// of frames seems sufficient in my testing.
|
||||||
|
SDL_SetWindowFullscreen(m_Window, 0);
|
||||||
|
SDL_Renderer* renderer = SDL_CreateRenderer(m_Window, -1, SDL_RENDERER_PRESENTVSYNC);
|
||||||
|
if (renderer != nullptr) {
|
||||||
|
SDL_SetRenderDrawColor(renderer, 0, 0, 0, SDL_ALPHA_OPAQUE);
|
||||||
|
for (int i = 0; i < 10; i++)
|
||||||
|
{
|
||||||
|
SDL_RenderClear(renderer);
|
||||||
|
SDL_RenderPresent(renderer);
|
||||||
|
SDL_Delay(50);
|
||||||
|
}
|
||||||
|
SDL_DestroyRenderer(renderer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This must be called after the decoder is deleted, because
|
||||||
|
// the renderer may want to interact with the window
|
||||||
SDL_DestroyWindow(m_Window);
|
SDL_DestroyWindow(m_Window);
|
||||||
|
|
||||||
if (iconSurface != nullptr) {
|
if (iconSurface != nullptr) {
|
||||||
SDL_FreeSurface(iconSurface);
|
SDL_FreeSurface(iconSurface);
|
||||||
}
|
}
|
||||||
|
|
||||||
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||||
SDL_assert(!SDL_WasInit(SDL_INIT_VIDEO));
|
|
||||||
|
|
||||||
// Cleanup can take a while, so dispatch it to a worker thread.
|
// Cleanup can take a while, so dispatch it to a worker thread.
|
||||||
// When it is complete, it will release our s_ActiveSessionSemaphore
|
// When it is complete, it will release our s_ActiveSessionSemaphore
|
||||||
// reference.
|
// reference.
|
||||||
QThreadPool::globalInstance()->start(new DeferredSessionCleanupTask());
|
QThreadPool::globalInstance()->start(new DeferredSessionCleanupTask(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4,11 +4,11 @@
|
|||||||
|
|
||||||
#include <Limelight.h>
|
#include <Limelight.h>
|
||||||
#include <opus_multistream.h>
|
#include <opus_multistream.h>
|
||||||
#include "backend/computermanager.h"
|
|
||||||
#include "settings/streamingpreferences.h"
|
#include "settings/streamingpreferences.h"
|
||||||
#include "input.h"
|
#include "input.h"
|
||||||
#include "video/decoder.h"
|
#include "video/decoder.h"
|
||||||
#include "audio/renderers/renderer.h"
|
#include "audio/renderers/renderer.h"
|
||||||
|
#include "video/overlaymanager.h"
|
||||||
|
|
||||||
class Session : public QObject
|
class Session : public QObject
|
||||||
{
|
{
|
||||||
@@ -20,15 +20,24 @@ class Session : public QObject
|
|||||||
public:
|
public:
|
||||||
explicit Session(NvComputer* computer, NvApp& app, StreamingPreferences *preferences = nullptr);
|
explicit Session(NvComputer* computer, NvApp& app, StreamingPreferences *preferences = nullptr);
|
||||||
|
|
||||||
|
virtual ~Session();
|
||||||
|
|
||||||
Q_INVOKABLE void exec(int displayOriginX, int displayOriginY);
|
Q_INVOKABLE void exec(int displayOriginX, int displayOriginY);
|
||||||
|
|
||||||
static
|
static
|
||||||
bool isHardwareDecodeAvailable(StreamingPreferences::VideoDecoderSelection vds,
|
bool isHardwareDecodeAvailable(SDL_Window* window,
|
||||||
|
StreamingPreferences::VideoDecoderSelection vds,
|
||||||
int videoFormat, int width, int height, int frameRate);
|
int videoFormat, int width, int height, int frameRate);
|
||||||
|
|
||||||
static
|
static Session* get()
|
||||||
int getDecoderCapabilities(StreamingPreferences::VideoDecoderSelection vds,
|
{
|
||||||
int videoFormat, int width, int height, int frameRate);
|
return s_ActiveSession;
|
||||||
|
}
|
||||||
|
|
||||||
|
Overlay::OverlayManager& getOverlayManager()
|
||||||
|
{
|
||||||
|
return m_OverlayManager;
|
||||||
|
}
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void stageStarting(QString stage);
|
void stageStarting(QString stage);
|
||||||
@@ -41,32 +50,40 @@ signals:
|
|||||||
|
|
||||||
void displayLaunchWarning(QString text);
|
void displayLaunchWarning(QString text);
|
||||||
|
|
||||||
private:
|
void quitStarting();
|
||||||
void initialize();
|
|
||||||
|
|
||||||
bool validateLaunch();
|
void sessionFinished();
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool initialize();
|
||||||
|
|
||||||
|
bool validateLaunch(SDL_Window* testWindow);
|
||||||
|
|
||||||
void emitLaunchWarning(QString text);
|
void emitLaunchWarning(QString text);
|
||||||
|
|
||||||
int getDecoderCapabilities();
|
bool populateDecoderProperties(SDL_Window* window);
|
||||||
|
|
||||||
IAudioRenderer* createAudioRenderer();
|
IAudioRenderer* createAudioRenderer(const POPUS_MULTISTREAM_CONFIGURATION opusConfig);
|
||||||
|
|
||||||
int detectAudioConfiguration();
|
|
||||||
|
|
||||||
bool testAudio(int audioConfiguration);
|
bool testAudio(int audioConfiguration);
|
||||||
|
|
||||||
|
int getAudioRendererCapabilities(int audioConfiguration);
|
||||||
|
|
||||||
void getWindowDimensions(int& x, int& y,
|
void getWindowDimensions(int& x, int& y,
|
||||||
int& width, int& height);
|
int& width, int& height);
|
||||||
|
|
||||||
void toggleFullscreen();
|
void toggleFullscreen();
|
||||||
|
|
||||||
|
void notifyMouseEmulationMode(bool enabled);
|
||||||
|
|
||||||
void updateOptimalWindowDisplayMode();
|
void updateOptimalWindowDisplayMode();
|
||||||
|
|
||||||
static
|
static
|
||||||
bool chooseDecoder(StreamingPreferences::VideoDecoderSelection vds,
|
bool chooseDecoder(StreamingPreferences::VideoDecoderSelection vds,
|
||||||
SDL_Window* window, int videoFormat, int width, int height,
|
SDL_Window* window, int videoFormat, int width, int height,
|
||||||
int frameRate, bool enableVsync, IVideoDecoder*& chosenDecoder);
|
int frameRate, bool enableVsync, bool enableFramePacing,
|
||||||
|
bool testOnly,
|
||||||
|
IVideoDecoder*& chosenDecoder);
|
||||||
|
|
||||||
static
|
static
|
||||||
void clStageStarting(int stage);
|
void clStageStarting(int stage);
|
||||||
@@ -80,6 +97,12 @@ private:
|
|||||||
static
|
static
|
||||||
void clLogMessage(const char* format, ...);
|
void clLogMessage(const char* format, ...);
|
||||||
|
|
||||||
|
static
|
||||||
|
void clRumble(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor);
|
||||||
|
|
||||||
|
static
|
||||||
|
void clConnectionStatusUpdate(int connectionStatus);
|
||||||
|
|
||||||
static
|
static
|
||||||
int arInit(int audioConfiguration,
|
int arInit(int audioConfiguration,
|
||||||
const POPUS_MULTISTREAM_CONFIGURATION opusConfig,
|
const POPUS_MULTISTREAM_CONFIGURATION opusConfig,
|
||||||
@@ -103,6 +126,7 @@ private:
|
|||||||
StreamingPreferences* m_Preferences;
|
StreamingPreferences* m_Preferences;
|
||||||
STREAM_CONFIGURATION m_StreamConfig;
|
STREAM_CONFIGURATION m_StreamConfig;
|
||||||
DECODER_RENDERER_CALLBACKS m_VideoCallbacks;
|
DECODER_RENDERER_CALLBACKS m_VideoCallbacks;
|
||||||
|
AUDIO_RENDERER_CALLBACKS m_AudioCallbacks;
|
||||||
NvComputer* m_Computer;
|
NvComputer* m_Computer;
|
||||||
NvApp m_App;
|
NvApp m_App;
|
||||||
SDL_Window* m_Window;
|
SDL_Window* m_Window;
|
||||||
@@ -114,6 +138,10 @@ private:
|
|||||||
int m_DisplayOriginX;
|
int m_DisplayOriginX;
|
||||||
int m_DisplayOriginY;
|
int m_DisplayOriginY;
|
||||||
bool m_PendingWindowedTransition;
|
bool m_PendingWindowedTransition;
|
||||||
|
bool m_UnexpectedTermination;
|
||||||
|
SdlInputHandler* m_InputHandler;
|
||||||
|
SDL_SpinLock m_InputHandlerLock;
|
||||||
|
int m_MouseEmulationRefCount;
|
||||||
|
|
||||||
int m_ActiveVideoFormat;
|
int m_ActiveVideoFormat;
|
||||||
int m_ActiveVideoWidth;
|
int m_ActiveVideoWidth;
|
||||||
@@ -121,12 +149,13 @@ private:
|
|||||||
int m_ActiveVideoFrameRate;
|
int m_ActiveVideoFrameRate;
|
||||||
|
|
||||||
OpusMSDecoder* m_OpusDecoder;
|
OpusMSDecoder* m_OpusDecoder;
|
||||||
short m_OpusDecodeBuffer[MAX_CHANNELS * SAMPLES_PER_FRAME];
|
|
||||||
IAudioRenderer* m_AudioRenderer;
|
IAudioRenderer* m_AudioRenderer;
|
||||||
OPUS_MULTISTREAM_CONFIGURATION m_AudioConfig;
|
OPUS_MULTISTREAM_CONFIGURATION m_AudioConfig;
|
||||||
int m_AudioSampleCount;
|
int m_AudioSampleCount;
|
||||||
|
Uint32 m_DropAudioEndTime;
|
||||||
|
|
||||||
|
Overlay::OverlayManager m_OverlayManager;
|
||||||
|
|
||||||
static AUDIO_RENDERER_CALLBACKS k_AudioCallbacks;
|
|
||||||
static CONNECTION_LISTENER_CALLBACKS k_ConnCallbacks;
|
static CONNECTION_LISTENER_CALLBACKS k_ConnCallbacks;
|
||||||
static Session* s_ActiveSession;
|
static Session* s_ActiveSession;
|
||||||
static QSemaphore s_ActiveSessionSemaphore;
|
static QSemaphore s_ActiveSessionSemaphore;
|
||||||
|
|||||||
@@ -12,43 +12,39 @@ typedef struct _VIDEO_STATS {
|
|||||||
uint32_t receivedFrames;
|
uint32_t receivedFrames;
|
||||||
uint32_t decodedFrames;
|
uint32_t decodedFrames;
|
||||||
uint32_t renderedFrames;
|
uint32_t renderedFrames;
|
||||||
|
uint32_t totalFrames;
|
||||||
uint32_t networkDroppedFrames;
|
uint32_t networkDroppedFrames;
|
||||||
uint32_t pacerDroppedFrames;
|
uint32_t pacerDroppedFrames;
|
||||||
uint32_t totalReassemblyTime;
|
uint32_t totalReassemblyTime;
|
||||||
uint32_t totalDecodeTime;
|
uint32_t totalDecodeTime;
|
||||||
|
uint32_t totalPacerTime;
|
||||||
uint32_t totalRenderTime;
|
uint32_t totalRenderTime;
|
||||||
|
float totalFps;
|
||||||
float receivedFps;
|
float receivedFps;
|
||||||
float decodedFps;
|
float decodedFps;
|
||||||
float renderedFps;
|
float renderedFps;
|
||||||
uint32_t measurementStartTimestamp;
|
uint32_t measurementStartTimestamp;
|
||||||
} VIDEO_STATS, *PVIDEO_STATS;
|
} VIDEO_STATS, *PVIDEO_STATS;
|
||||||
|
|
||||||
|
typedef struct _DECODER_PARAMETERS {
|
||||||
|
SDL_Window* window;
|
||||||
|
StreamingPreferences::VideoDecoderSelection vds;
|
||||||
|
|
||||||
|
int videoFormat;
|
||||||
|
int width;
|
||||||
|
int height;
|
||||||
|
int frameRate;
|
||||||
|
bool enableVsync;
|
||||||
|
bool enableFramePacing;
|
||||||
|
} DECODER_PARAMETERS, *PDECODER_PARAMETERS;
|
||||||
|
|
||||||
class IVideoDecoder {
|
class IVideoDecoder {
|
||||||
public:
|
public:
|
||||||
virtual ~IVideoDecoder() {}
|
virtual ~IVideoDecoder() {}
|
||||||
virtual bool initialize(StreamingPreferences::VideoDecoderSelection vds,
|
virtual bool initialize(PDECODER_PARAMETERS params) = 0;
|
||||||
SDL_Window* window,
|
|
||||||
int videoFormat,
|
|
||||||
int width,
|
|
||||||
int height,
|
|
||||||
int frameRate,
|
|
||||||
bool enableVsync) = 0;
|
|
||||||
virtual bool isHardwareAccelerated() = 0;
|
virtual bool isHardwareAccelerated() = 0;
|
||||||
virtual int getDecoderCapabilities() = 0;
|
virtual int getDecoderCapabilities() = 0;
|
||||||
|
virtual int getDecoderColorspace() = 0;
|
||||||
virtual int submitDecodeUnit(PDECODE_UNIT du) = 0;
|
virtual int submitDecodeUnit(PDECODE_UNIT du) = 0;
|
||||||
virtual void renderFrame(SDL_UserEvent* event) = 0;
|
virtual void renderFrameOnMainThread() = 0;
|
||||||
virtual void dropFrame(SDL_UserEvent* event) = 0;
|
|
||||||
|
|
||||||
virtual void queueFrame(void* data1 = nullptr,
|
|
||||||
void* data2 = nullptr)
|
|
||||||
{
|
|
||||||
SDL_Event event;
|
|
||||||
|
|
||||||
event.type = SDL_USEREVENT;
|
|
||||||
event.user.code = SDL_CODE_FRAME_READY;
|
|
||||||
event.user.data1 = data1;
|
|
||||||
event.user.data2 = data2;
|
|
||||||
|
|
||||||
SDL_PushEvent(&event);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
#include "cuda.h"
|
||||||
|
|
||||||
|
CUDARenderer::CUDARenderer()
|
||||||
|
: m_HwContext(nullptr)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
CUDARenderer::~CUDARenderer()
|
||||||
|
{
|
||||||
|
if (m_HwContext != nullptr) {
|
||||||
|
av_buffer_unref(&m_HwContext);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CUDARenderer::initialize(PDECODER_PARAMETERS)
|
||||||
|
{
|
||||||
|
int err;
|
||||||
|
|
||||||
|
err = av_hwdevice_ctx_create(&m_HwContext, AV_HWDEVICE_TYPE_CUDA, nullptr, nullptr, 0);
|
||||||
|
if (err != 0) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"av_hwdevice_ctx_create(CUDA) failed: %d",
|
||||||
|
err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CUDARenderer::prepareDecoderContext(AVCodecContext* context)
|
||||||
|
{
|
||||||
|
context->hw_device_ctx = av_buffer_ref(m_HwContext);
|
||||||
|
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Using CUDA accelerated decoder");
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CUDARenderer::renderFrame(AVFrame*)
|
||||||
|
{
|
||||||
|
// We only support indirect rendering
|
||||||
|
SDL_assert(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CUDARenderer::needsTestFrame()
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CUDARenderer::isDirectRenderingSupported()
|
||||||
|
{
|
||||||
|
// We only support rendering via SDL read-back
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "renderer.h"
|
||||||
|
|
||||||
|
class CUDARenderer : public IFFmpegRenderer {
|
||||||
|
public:
|
||||||
|
CUDARenderer();
|
||||||
|
virtual ~CUDARenderer() override;
|
||||||
|
virtual bool initialize(PDECODER_PARAMETERS) override;
|
||||||
|
virtual bool prepareDecoderContext(AVCodecContext* context) override;
|
||||||
|
virtual void renderFrame(AVFrame* frame) override;
|
||||||
|
virtual bool needsTestFrame() override;
|
||||||
|
virtual bool isDirectRenderingSupported() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
AVBufferRef* m_HwContext;
|
||||||
|
};
|
||||||
|
|
||||||
@@ -0,0 +1,268 @@
|
|||||||
|
#include "drm.h"
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
#include <libavutil/hwcontext_drm.h>
|
||||||
|
}
|
||||||
|
|
||||||
|
#include <libdrm/drm_fourcc.h>
|
||||||
|
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
|
||||||
|
#include "streaming/streamutils.h"
|
||||||
|
#include "streaming/session.h"
|
||||||
|
|
||||||
|
#include <Limelight.h>
|
||||||
|
|
||||||
|
DrmRenderer::DrmRenderer()
|
||||||
|
: m_DrmFd(-1),
|
||||||
|
m_CrtcId(0),
|
||||||
|
m_PlaneId(0),
|
||||||
|
m_CurrentFbId(0)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
DrmRenderer::~DrmRenderer()
|
||||||
|
{
|
||||||
|
if (m_CurrentFbId != 0) {
|
||||||
|
drmModeRmFB(m_DrmFd, m_CurrentFbId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_DrmFd != -1) {
|
||||||
|
close(m_DrmFd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DrmRenderer::prepareDecoderContext(AVCodecContext*)
|
||||||
|
{
|
||||||
|
/* Nothing to do */
|
||||||
|
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Using DRM renderer");
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DrmRenderer::initialize(PDECODER_PARAMETERS)
|
||||||
|
{
|
||||||
|
const char* device = SDL_getenv("DRM_DEV");
|
||||||
|
int i;
|
||||||
|
|
||||||
|
if (device == nullptr) {
|
||||||
|
device = "/dev/dri/card0";
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Opening DRM device: %s",
|
||||||
|
device);
|
||||||
|
|
||||||
|
m_DrmFd = open(device, O_RDWR | O_CLOEXEC);
|
||||||
|
if (m_DrmFd < 0) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Failed to open DRM device: %d",
|
||||||
|
errno);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
drmModeRes* resources = drmModeGetResources(m_DrmFd);
|
||||||
|
if (resources == nullptr) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"drmModeGetResources() failed: %d",
|
||||||
|
errno);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look for a connected connector and get the associated encoder
|
||||||
|
uint32_t encoderId = 0;
|
||||||
|
for (i = 0; i < resources->count_connectors && encoderId == 0; i++) {
|
||||||
|
drmModeConnector* connector = drmModeGetConnector(m_DrmFd, resources->connectors[i]);
|
||||||
|
if (connector != nullptr) {
|
||||||
|
if (connector->connection == DRM_MODE_CONNECTED && connector->count_modes > 0) {
|
||||||
|
encoderId = connector->encoder_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
drmModeFreeConnector(connector);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (encoderId == 0) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"No connected displays found!");
|
||||||
|
drmModeFreeResources(resources);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now find the CRTC from the encoder
|
||||||
|
m_CrtcId = 0;
|
||||||
|
for (i = 0; i < resources->count_encoders && m_CrtcId == 0; i++) {
|
||||||
|
drmModeEncoder* encoder = drmModeGetEncoder(m_DrmFd, resources->encoders[i]);
|
||||||
|
if (encoder != nullptr) {
|
||||||
|
if (encoder->encoder_id == encoderId) {
|
||||||
|
m_CrtcId = encoder->crtc_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
drmModeFreeEncoder(encoder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_CrtcId == 0) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"DRM encoder not found!");
|
||||||
|
drmModeFreeResources(resources);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int crtcIndex = -1;
|
||||||
|
for (int i = 0; i < resources->count_crtcs; i++) {
|
||||||
|
if (resources->crtcs[i] == m_CrtcId) {
|
||||||
|
drmModeCrtc* crtc = drmModeGetCrtc(m_DrmFd, resources->crtcs[i]);
|
||||||
|
crtcIndex = i;
|
||||||
|
m_OutputRect.x = m_OutputRect.y = 0;
|
||||||
|
m_OutputRect.w = crtc->width;
|
||||||
|
m_OutputRect.h = crtc->height;
|
||||||
|
drmModeFreeCrtc(crtc);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
drmModeFreeResources(resources);
|
||||||
|
|
||||||
|
if (crtcIndex == -1) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Failed to get CRTC!");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
drmSetClientCap(m_DrmFd, DRM_CLIENT_CAP_UNIVERSAL_PLANES, 1);
|
||||||
|
|
||||||
|
drmModePlaneRes* planeRes = drmModeGetPlaneResources(m_DrmFd);
|
||||||
|
if (planeRes == nullptr) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"drmGetPlaneResources() failed: %d",
|
||||||
|
errno);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find an NV12 overlay plane to render on
|
||||||
|
m_PlaneId = 0;
|
||||||
|
for (uint32_t i = 0; i < planeRes->count_planes && m_PlaneId == 0; i++) {
|
||||||
|
drmModePlane* plane = drmModeGetPlane(m_DrmFd, planeRes->planes[i]);
|
||||||
|
if (plane != nullptr) {
|
||||||
|
bool matchingFormat = false;
|
||||||
|
for (uint32_t j = 0; j < plane->count_formats; j++) {
|
||||||
|
if (plane->formats[j] == DRM_FORMAT_NV12) {
|
||||||
|
matchingFormat = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matchingFormat == false) {
|
||||||
|
drmModeFreePlane(plane);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((plane->possible_crtcs & (1 << crtcIndex)) && plane->crtc_id == 0) {
|
||||||
|
drmModeObjectPropertiesPtr props = drmModeObjectGetProperties(m_DrmFd, planeRes->planes[i], DRM_MODE_OBJECT_PLANE);
|
||||||
|
if (props != nullptr) {
|
||||||
|
for (uint32_t j = 0; j < props->count_props && m_PlaneId == 0; j++) {
|
||||||
|
drmModePropertyPtr prop = drmModeGetProperty(m_DrmFd, props->props[j]);
|
||||||
|
if (prop != nullptr) {
|
||||||
|
if (!strcmp(prop->name, "type") && props->prop_values[j] == DRM_PLANE_TYPE_OVERLAY) {
|
||||||
|
m_PlaneId = plane->plane_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
drmModeFreeProperty(prop);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
drmModeFreeObjectProperties(props);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
drmModeFreePlane(plane);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
drmModeFreePlaneResources(planeRes);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum AVPixelFormat DrmRenderer::getPreferredPixelFormat(int)
|
||||||
|
{
|
||||||
|
// DRM PRIME buffers
|
||||||
|
return AV_PIX_FMT_DRM_PRIME;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DrmRenderer::renderFrame(AVFrame* frame)
|
||||||
|
{
|
||||||
|
AVDRMFrameDescriptor* drmFrame = (AVDRMFrameDescriptor*)frame->data[0];
|
||||||
|
int err;
|
||||||
|
uint32_t primeHandle;
|
||||||
|
uint32_t handles[4] = {};
|
||||||
|
uint32_t pitches[4] = {};
|
||||||
|
uint32_t offsets[4] = {};
|
||||||
|
|
||||||
|
SDL_Rect src, dst;
|
||||||
|
|
||||||
|
src.x = src.y = 0;
|
||||||
|
src.w = frame->width;
|
||||||
|
src.h = frame->height;
|
||||||
|
dst = m_OutputRect;
|
||||||
|
|
||||||
|
StreamUtils::scaleSourceToDestinationSurface(&src, &dst);
|
||||||
|
|
||||||
|
// Convert the FD in the AVDRMFrameDescriptor to a PRIME handle
|
||||||
|
// that can be used in drmModeAddFB2()
|
||||||
|
SDL_assert(drmFrame->nb_objects == 1);
|
||||||
|
err = drmPrimeFDToHandle(m_DrmFd, drmFrame->objects[0].fd, &primeHandle);
|
||||||
|
if (err < 0) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"drmPrimeFDToHandle() failed: %d",
|
||||||
|
errno);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_assert(drmFrame->nb_layers == 1);
|
||||||
|
SDL_assert(drmFrame->layers[0].nb_planes == 2);
|
||||||
|
for (int i = 0; i < drmFrame->layers[0].nb_planes; i++) {
|
||||||
|
handles[i] = primeHandle;
|
||||||
|
pitches[i] = drmFrame->layers[0].planes[i].pitch;
|
||||||
|
offsets[i] = drmFrame->layers[0].planes[i].offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remember the last FB object we created so we can free it
|
||||||
|
// when we are finished rendering this one (if successful).
|
||||||
|
uint32_t lastFbId = m_CurrentFbId;
|
||||||
|
|
||||||
|
// Create a frame buffer object from the PRIME buffer
|
||||||
|
err = drmModeAddFB2(m_DrmFd, frame->width, frame->height,
|
||||||
|
drmFrame->layers[0].format,
|
||||||
|
handles, pitches, offsets, &m_CurrentFbId, 0);
|
||||||
|
if (err < 0) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"drmModeAddFB2() failed: %d",
|
||||||
|
errno);
|
||||||
|
m_CurrentFbId = lastFbId;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the overlay
|
||||||
|
err = drmModeSetPlane(m_DrmFd, m_PlaneId, m_CrtcId, m_CurrentFbId, 0,
|
||||||
|
dst.x, dst.y,
|
||||||
|
dst.w, dst.h,
|
||||||
|
0, 0,
|
||||||
|
frame->width << 16,
|
||||||
|
frame->height << 16);
|
||||||
|
if (err < 0) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"drmModeSetPlane() failed: %d",
|
||||||
|
errno);
|
||||||
|
drmModeRmFB(m_DrmFd, m_CurrentFbId);
|
||||||
|
m_CurrentFbId = lastFbId;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Free the previous FB object which has now been superseded
|
||||||
|
drmModeRmFB(m_DrmFd, lastFbId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "renderer.h"
|
||||||
|
|
||||||
|
#include <xf86drm.h>
|
||||||
|
#include <xf86drmMode.h>
|
||||||
|
|
||||||
|
class DrmRenderer : public IFFmpegRenderer {
|
||||||
|
public:
|
||||||
|
DrmRenderer();
|
||||||
|
virtual ~DrmRenderer() override;
|
||||||
|
virtual bool initialize(PDECODER_PARAMETERS params) override;
|
||||||
|
virtual bool prepareDecoderContext(AVCodecContext* context) override;
|
||||||
|
virtual void renderFrame(AVFrame* frame) override;
|
||||||
|
virtual enum AVPixelFormat getPreferredPixelFormat(int videoFormat) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
int m_DrmFd;
|
||||||
|
uint32_t m_CrtcId;
|
||||||
|
uint32_t m_PlaneId;
|
||||||
|
uint32_t m_CurrentFbId;
|
||||||
|
SDL_Rect m_OutputRect;
|
||||||
|
};
|
||||||
|
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
#include "dxva2.h"
|
#include "dxva2.h"
|
||||||
#include "../ffmpeg.h"
|
#include "../ffmpeg.h"
|
||||||
#include <streaming/streamutils.h>
|
#include <streaming/streamutils.h>
|
||||||
|
#include <streaming/session.h>
|
||||||
|
|
||||||
#include <SDL_syswm.h>
|
#include <SDL_syswm.h>
|
||||||
|
|
||||||
@@ -25,7 +26,10 @@ DXVA2Renderer::DXVA2Renderer() :
|
|||||||
m_RenderTarget(nullptr),
|
m_RenderTarget(nullptr),
|
||||||
m_ProcService(nullptr),
|
m_ProcService(nullptr),
|
||||||
m_Processor(nullptr),
|
m_Processor(nullptr),
|
||||||
m_FrameIndex(0)
|
m_FrameIndex(0),
|
||||||
|
m_DebugOverlayFont(nullptr),
|
||||||
|
m_StatusOverlayFont(nullptr),
|
||||||
|
m_BlockingPresent(false)
|
||||||
{
|
{
|
||||||
RtlZeroMemory(m_DecSurfaces, sizeof(m_DecSurfaces));
|
RtlZeroMemory(m_DecSurfaces, sizeof(m_DecSurfaces));
|
||||||
RtlZeroMemory(&m_DXVAContext, sizeof(m_DXVAContext));
|
RtlZeroMemory(&m_DXVAContext, sizeof(m_DXVAContext));
|
||||||
@@ -44,6 +48,8 @@ DXVA2Renderer::~DXVA2Renderer()
|
|||||||
SAFE_COM_RELEASE(m_RenderTarget);
|
SAFE_COM_RELEASE(m_RenderTarget);
|
||||||
SAFE_COM_RELEASE(m_ProcService);
|
SAFE_COM_RELEASE(m_ProcService);
|
||||||
SAFE_COM_RELEASE(m_Processor);
|
SAFE_COM_RELEASE(m_Processor);
|
||||||
|
SAFE_COM_RELEASE(m_DebugOverlayFont);
|
||||||
|
SAFE_COM_RELEASE(m_StatusOverlayFont);
|
||||||
|
|
||||||
for (int i = 0; i < ARRAYSIZE(m_DecSurfaces); i++) {
|
for (int i = 0; i < ARRAYSIZE(m_DecSurfaces); i++) {
|
||||||
SAFE_COM_RELEASE(m_DecSurfaces[i]);
|
SAFE_COM_RELEASE(m_DecSurfaces[i]);
|
||||||
@@ -101,7 +107,7 @@ bool DXVA2Renderer::prepareDecoderContext(AVCodecContext* context)
|
|||||||
|
|
||||||
int DXVA2Renderer::ffGetBuffer2(AVCodecContext* context, AVFrame* frame, int)
|
int DXVA2Renderer::ffGetBuffer2(AVCodecContext* context, AVFrame* frame, int)
|
||||||
{
|
{
|
||||||
DXVA2Renderer* me = (DXVA2Renderer*)((FFmpegVideoDecoder*)context->opaque)->getRenderer();
|
DXVA2Renderer* me = (DXVA2Renderer*)((FFmpegVideoDecoder*)context->opaque)->getBackendRenderer();
|
||||||
|
|
||||||
frame->buf[0] = av_buffer_pool_get(me->m_Pool);
|
frame->buf[0] = av_buffer_pool_get(me->m_Pool);
|
||||||
if (!frame->buf[0]) {
|
if (!frame->buf[0]) {
|
||||||
@@ -256,76 +262,131 @@ bool DXVA2Renderer::initializeRenderer()
|
|||||||
m_DisplayWidth = renderTargetDesc.Width;
|
m_DisplayWidth = renderTargetDesc.Width;
|
||||||
m_DisplayHeight = renderTargetDesc.Height;
|
m_DisplayHeight = renderTargetDesc.Height;
|
||||||
|
|
||||||
hr = DXVA2CreateVideoService(m_Device, IID_IDirectXVideoProcessorService,
|
if (!isDXVideoProcessorAPIBlacklisted()) {
|
||||||
reinterpret_cast<void**>(&m_ProcService));
|
hr = DXVA2CreateVideoService(m_Device, IID_IDirectXVideoProcessorService,
|
||||||
|
reinterpret_cast<void**>(&m_ProcService));
|
||||||
|
|
||||||
if (FAILED(hr)) {
|
if (FAILED(hr)) {
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"DXVA2CreateVideoService(IID_IDirectXVideoProcessorService) failed: %x",
|
"DXVA2CreateVideoService(IID_IDirectXVideoProcessorService) failed: %x",
|
||||||
hr);
|
hr);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
DXVA2_VideoProcessorCaps caps;
|
DXVA2_VideoProcessorCaps caps;
|
||||||
hr = m_ProcService->GetVideoProcessorCaps(DXVA2_VideoProcProgressiveDevice, &m_Desc, renderTargetDesc.Format, &caps);
|
hr = m_ProcService->GetVideoProcessorCaps(DXVA2_VideoProcProgressiveDevice, &m_Desc, renderTargetDesc.Format, &caps);
|
||||||
if (FAILED(hr)) {
|
if (FAILED(hr)) {
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"GetVideoProcessorCaps() failed for DXVA2_VideoProcProgressiveDevice: %x",
|
"GetVideoProcessorCaps() failed for DXVA2_VideoProcProgressiveDevice: %x",
|
||||||
hr);
|
hr);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!(caps.DeviceCaps & DXVA2_VPDev_HardwareDevice)) {
|
if (!(caps.DeviceCaps & DXVA2_VPDev_HardwareDevice)) {
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"DXVA2_VideoProcProgressiveDevice is not hardware: %x",
|
"DXVA2_VideoProcProgressiveDevice is not hardware: %x",
|
||||||
caps.DeviceCaps);
|
caps.DeviceCaps);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
else if (!(caps.VideoProcessorOperations & DXVA2_VideoProcess_YUV2RGB) &&
|
else if (!(caps.VideoProcessorOperations & DXVA2_VideoProcess_YUV2RGB) &&
|
||||||
!(caps.VideoProcessorOperations & DXVA2_VideoProcess_YUV2RGBExtended)) {
|
!(caps.VideoProcessorOperations & DXVA2_VideoProcess_YUV2RGBExtended)) {
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"DXVA2_VideoProcProgressiveDevice can't convert YUV2RGB: %x",
|
"DXVA2_VideoProcProgressiveDevice can't convert YUV2RGB: %x",
|
||||||
caps.VideoProcessorOperations);
|
caps.VideoProcessorOperations);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
else if (!(caps.VideoProcessorOperations & DXVA2_VideoProcess_StretchX) ||
|
else if (!(caps.VideoProcessorOperations & DXVA2_VideoProcess_StretchX) ||
|
||||||
!(caps.VideoProcessorOperations & DXVA2_VideoProcess_StretchY)) {
|
!(caps.VideoProcessorOperations & DXVA2_VideoProcess_StretchY)) {
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"DXVA2_VideoProcProgressiveDevice can't stretch video: %x",
|
"DXVA2_VideoProcProgressiveDevice can't stretch video: %x",
|
||||||
caps.VideoProcessorOperations);
|
caps.VideoProcessorOperations);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (caps.DeviceCaps & DXVA2_VPDev_EmulatedDXVA1) {
|
if (caps.DeviceCaps & DXVA2_VPDev_EmulatedDXVA1) {
|
||||||
// DXVA2 over DXVA1 may have bad performance
|
// DXVA2 over DXVA1 may have bad performance
|
||||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"DXVA2_VideoProcProgressiveDevice is DXVA1");
|
"DXVA2_VideoProcProgressiveDevice is DXVA1");
|
||||||
}
|
}
|
||||||
|
|
||||||
m_ProcService->GetProcAmpRange(DXVA2_VideoProcProgressiveDevice, &m_Desc, renderTargetDesc.Format, DXVA2_ProcAmp_Brightness, &m_BrightnessRange);
|
m_ProcService->GetProcAmpRange(DXVA2_VideoProcProgressiveDevice, &m_Desc, renderTargetDesc.Format, DXVA2_ProcAmp_Brightness, &m_BrightnessRange);
|
||||||
m_ProcService->GetProcAmpRange(DXVA2_VideoProcProgressiveDevice, &m_Desc, renderTargetDesc.Format, DXVA2_ProcAmp_Contrast, &m_ContrastRange);
|
m_ProcService->GetProcAmpRange(DXVA2_VideoProcProgressiveDevice, &m_Desc, renderTargetDesc.Format, DXVA2_ProcAmp_Contrast, &m_ContrastRange);
|
||||||
m_ProcService->GetProcAmpRange(DXVA2_VideoProcProgressiveDevice, &m_Desc, renderTargetDesc.Format, DXVA2_ProcAmp_Hue, &m_HueRange);
|
m_ProcService->GetProcAmpRange(DXVA2_VideoProcProgressiveDevice, &m_Desc, renderTargetDesc.Format, DXVA2_ProcAmp_Hue, &m_HueRange);
|
||||||
m_ProcService->GetProcAmpRange(DXVA2_VideoProcProgressiveDevice, &m_Desc, renderTargetDesc.Format, DXVA2_ProcAmp_Saturation, &m_SaturationRange);
|
m_ProcService->GetProcAmpRange(DXVA2_VideoProcProgressiveDevice, &m_Desc, renderTargetDesc.Format, DXVA2_ProcAmp_Saturation, &m_SaturationRange);
|
||||||
|
|
||||||
hr = m_ProcService->CreateVideoProcessor(DXVA2_VideoProcProgressiveDevice, &m_Desc, renderTargetDesc.Format, 0, &m_Processor);
|
hr = m_ProcService->CreateVideoProcessor(DXVA2_VideoProcProgressiveDevice, &m_Desc, renderTargetDesc.Format, 0, &m_Processor);
|
||||||
if (FAILED(hr)) {
|
if (FAILED(hr)) {
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"CreateVideoProcessor() failed for DXVA2_VideoProcProgressiveDevice: %x",
|
"CreateVideoProcessor() failed for DXVA2_VideoProcProgressiveDevice: %x",
|
||||||
hr);
|
hr);
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool DXVA2Renderer::isDXVideoProcessorAPIBlacklisted()
|
||||||
|
{
|
||||||
|
IDirect3D9* d3d9;
|
||||||
|
HRESULT hr;
|
||||||
|
bool result = false;
|
||||||
|
|
||||||
|
if (qgetenv("DXVA2_DISABLE_VIDPROC_BLACKLIST") == "1") {
|
||||||
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"IDirectXVideoProcessor blacklist is disabled");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr = m_Device->GetDirect3D(&d3d9);
|
||||||
|
if (SUCCEEDED(hr)) {
|
||||||
|
D3DCAPS9 caps;
|
||||||
|
|
||||||
|
hr = m_Device->GetDeviceCaps(&caps);
|
||||||
|
if (SUCCEEDED(hr)) {
|
||||||
|
D3DADAPTER_IDENTIFIER9 id;
|
||||||
|
|
||||||
|
hr = d3d9->GetAdapterIdentifier(caps.AdapterOrdinal, 0, &id);
|
||||||
|
if (SUCCEEDED(hr) && id.VendorId == 0x8086) {
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Avoiding IDirectXVideoProcessor API on Intel GPU");
|
||||||
|
|
||||||
|
// On Intel GPUs, we can get unwanted video "enhancements" due to post-processing
|
||||||
|
// effects that the GPU driver forces on us. In many cases, this makes the video
|
||||||
|
// actually look worse. We can avoid these by using StretchRect() instead on these
|
||||||
|
// platforms.
|
||||||
|
result = true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
result = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"GetDeviceCaps() failed: %x", hr);
|
||||||
|
}
|
||||||
|
|
||||||
|
d3d9->Release();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"GetDirect3D() failed: %x", hr);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
bool DXVA2Renderer::isDecoderBlacklisted()
|
bool DXVA2Renderer::isDecoderBlacklisted()
|
||||||
{
|
{
|
||||||
IDirect3D9* d3d9;
|
IDirect3D9* d3d9;
|
||||||
HRESULT hr;
|
HRESULT hr;
|
||||||
bool result = false;
|
bool result = false;
|
||||||
|
|
||||||
// TODO: Update for HEVC Main10
|
if (qgetenv("DXVA2_DISABLE_DECODER_BLACKLIST") == "1") {
|
||||||
SDL_assert(m_VideoFormat != VIDEO_FORMAT_H265_MAIN10);
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"DXVA2 decoder blacklist is disabled");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
hr = m_Device->GetDirect3D(&d3d9);
|
hr = m_Device->GetDirect3D(&d3d9);
|
||||||
if (SUCCEEDED(hr)) {
|
if (SUCCEEDED(hr)) {
|
||||||
@@ -368,6 +429,15 @@ bool DXVA2Renderer::isDecoderBlacklisted()
|
|||||||
"GPU blacklisted for HEVC due to hybrid decode");
|
"GPU blacklisted for HEVC due to hybrid decode");
|
||||||
result = (m_VideoFormat & VIDEO_FORMAT_MASK_H265) != 0;
|
result = (m_VideoFormat & VIDEO_FORMAT_MASK_H265) != 0;
|
||||||
break;
|
break;
|
||||||
|
case 0x1900: // Skylake
|
||||||
|
// Blacklist these for HEVC Main10 to avoid hybrid decode.
|
||||||
|
// Regular HEVC Main is fine though.
|
||||||
|
if (m_VideoFormat == VIDEO_FORMAT_H265_MAIN10) {
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"GPU blacklisted for HEVC Main10 due to hybrid decode");
|
||||||
|
result = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
// Intel drivers from before late-2017 had a bug that caused some strange artifacts
|
// Intel drivers from before late-2017 had a bug that caused some strange artifacts
|
||||||
// when decoding HEVC. Avoid HEVC on drivers prior to build 4836 which I confirmed
|
// when decoding HEVC. Avoid HEVC on drivers prior to build 4836 which I confirmed
|
||||||
@@ -481,6 +551,8 @@ bool DXVA2Renderer::initializeDevice(SDL_Window* window, bool enableVsync)
|
|||||||
d3dpp.BackBufferWidth = currentMode.Width;
|
d3dpp.BackBufferWidth = currentMode.Width;
|
||||||
d3dpp.BackBufferHeight = currentMode.Height;
|
d3dpp.BackBufferHeight = currentMode.Height;
|
||||||
d3dpp.FullScreen_RefreshRateInHz = currentMode.RefreshRate;
|
d3dpp.FullScreen_RefreshRateInHz = currentMode.RefreshRate;
|
||||||
|
|
||||||
|
// TODO: Need to select 10-bit color format for HDR
|
||||||
d3dpp.BackBufferFormat = currentMode.Format;
|
d3dpp.BackBufferFormat = currentMode.Format;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -512,6 +584,8 @@ bool DXVA2Renderer::initializeDevice(SDL_Window* window, bool enableVsync)
|
|||||||
d3dpp.BackBufferCount = 1;
|
d3dpp.BackBufferCount = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
m_BlockingPresent = false;
|
||||||
|
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"Windowed mode with DWM running");
|
"Windowed mode with DWM running");
|
||||||
}
|
}
|
||||||
@@ -521,6 +595,7 @@ bool DXVA2Renderer::initializeDevice(SDL_Window* window, bool enableVsync)
|
|||||||
d3dpp.PresentationInterval = D3DPRESENT_INTERVAL_ONE;
|
d3dpp.PresentationInterval = D3DPRESENT_INTERVAL_ONE;
|
||||||
d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
|
d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
|
||||||
d3dpp.BackBufferCount = 1;
|
d3dpp.BackBufferCount = 1;
|
||||||
|
m_BlockingPresent = true;
|
||||||
|
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"V-Sync enabled");
|
"V-Sync enabled");
|
||||||
@@ -531,6 +606,7 @@ bool DXVA2Renderer::initializeDevice(SDL_Window* window, bool enableVsync)
|
|||||||
d3dpp.PresentationInterval = D3DPRESENT_INTERVAL_IMMEDIATE;
|
d3dpp.PresentationInterval = D3DPRESENT_INTERVAL_IMMEDIATE;
|
||||||
d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
|
d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
|
||||||
d3dpp.BackBufferCount = 1;
|
d3dpp.BackBufferCount = 1;
|
||||||
|
m_BlockingPresent = false;
|
||||||
|
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"V-Sync disabled in tearing mode");
|
"V-Sync disabled in tearing mode");
|
||||||
@@ -581,19 +657,23 @@ bool DXVA2Renderer::initializeDevice(SDL_Window* window, bool enableVsync)
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool DXVA2Renderer::initialize(SDL_Window* window, int videoFormat, int width, int height, int, bool enableVsync)
|
bool DXVA2Renderer::initialize(PDECODER_PARAMETERS params)
|
||||||
{
|
{
|
||||||
m_VideoFormat = videoFormat;
|
m_VideoFormat = params->videoFormat;
|
||||||
m_VideoWidth = width;
|
m_VideoWidth = params->width;
|
||||||
m_VideoHeight = height;
|
m_VideoHeight = params->height;
|
||||||
|
|
||||||
RtlZeroMemory(&m_Desc, sizeof(m_Desc));
|
RtlZeroMemory(&m_Desc, sizeof(m_Desc));
|
||||||
|
|
||||||
int alignment;
|
int alignment;
|
||||||
|
|
||||||
// HEVC using DXVA requires 128B alignment
|
// HEVC using DXVA requires 128 pixel alignment, however Intel GPUs decoding HEVC
|
||||||
|
// using StretchRect() to render draw a translucent green line at the top of
|
||||||
|
// the screen in full-screen mode at 720p/1080p unless we use 32 pixel alignment.
|
||||||
|
// This appears to work without issues on AMD and Nvidia GPUs too, so we will
|
||||||
|
// do it unconditionally for now.
|
||||||
if (m_VideoFormat & VIDEO_FORMAT_MASK_H265) {
|
if (m_VideoFormat & VIDEO_FORMAT_MASK_H265) {
|
||||||
alignment = 128;
|
alignment = 32;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
alignment = 16;
|
alignment = 16;
|
||||||
@@ -608,9 +688,15 @@ bool DXVA2Renderer::initialize(SDL_Window* window, int videoFormat, int width, i
|
|||||||
m_Desc.SampleFormat.VideoPrimaries = DXVA2_VideoPrimaries_Unknown;
|
m_Desc.SampleFormat.VideoPrimaries = DXVA2_VideoPrimaries_Unknown;
|
||||||
m_Desc.SampleFormat.VideoTransferFunction = DXVA2_VideoTransFunc_Unknown;
|
m_Desc.SampleFormat.VideoTransferFunction = DXVA2_VideoTransFunc_Unknown;
|
||||||
m_Desc.SampleFormat.SampleFormat = DXVA2_SampleProgressiveFrame;
|
m_Desc.SampleFormat.SampleFormat = DXVA2_SampleProgressiveFrame;
|
||||||
m_Desc.Format = (D3DFORMAT)MAKEFOURCC('N','V','1','2');
|
|
||||||
|
|
||||||
if (!initializeDevice(window, enableVsync)) {
|
if (m_VideoFormat == VIDEO_FORMAT_H265_MAIN10) {
|
||||||
|
m_Desc.Format = (D3DFORMAT)MAKEFOURCC('P','0','1','0');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
m_Desc.Format = (D3DFORMAT)MAKEFOURCC('N','V','1','2');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!initializeDevice(params->window, params->enableVsync)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -627,29 +713,84 @@ bool DXVA2Renderer::initialize(SDL_Window* window, int videoFormat, int width, i
|
|||||||
// input focus. This glitches out the renderer and a bunch of other stuff.
|
// input focus. This glitches out the renderer and a bunch of other stuff.
|
||||||
// Direct3D9Ex itself seems to have this minimize on focus loss behavior on its
|
// Direct3D9Ex itself seems to have this minimize on focus loss behavior on its
|
||||||
// own, so just disable SDL's handling of the focus loss event.
|
// own, so just disable SDL's handling of the focus loss event.
|
||||||
SDL_SetHint(SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS, "0");
|
SDL_SetHintWithPriority(SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS, "0", SDL_HINT_OVERRIDE);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool DXVA2Renderer::needsTestFrame()
|
void DXVA2Renderer::notifyOverlayUpdated(Overlay::OverlayType type)
|
||||||
{
|
{
|
||||||
// We validate the DXVA2 profiles are supported
|
HRESULT hr;
|
||||||
// in initialize() so no test frame is required
|
|
||||||
return false;
|
switch (type)
|
||||||
|
{
|
||||||
|
case Overlay::OverlayDebug:
|
||||||
|
if (m_DebugOverlayFont == nullptr) {
|
||||||
|
hr = D3DXCreateFontA(m_Device,
|
||||||
|
Session::get()->getOverlayManager().getOverlayFontSize(Overlay::OverlayDebug),
|
||||||
|
0,
|
||||||
|
FW_HEAVY,
|
||||||
|
1,
|
||||||
|
false,
|
||||||
|
ANSI_CHARSET,
|
||||||
|
OUT_DEFAULT_PRECIS,
|
||||||
|
DEFAULT_QUALITY,
|
||||||
|
DEFAULT_PITCH | FF_DONTCARE,
|
||||||
|
"",
|
||||||
|
&m_DebugOverlayFont);
|
||||||
|
if (FAILED(hr)) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"D3DXCreateFontA() failed: %x",
|
||||||
|
hr);
|
||||||
|
m_DebugOverlayFont = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Overlay::OverlayStatusUpdate:
|
||||||
|
if (m_StatusOverlayFont == nullptr) {
|
||||||
|
hr = D3DXCreateFontA(m_Device,
|
||||||
|
Session::get()->getOverlayManager().getOverlayFontSize(Overlay::OverlayStatusUpdate),
|
||||||
|
0,
|
||||||
|
FW_HEAVY,
|
||||||
|
1,
|
||||||
|
false,
|
||||||
|
ANSI_CHARSET,
|
||||||
|
OUT_DEFAULT_PRECIS,
|
||||||
|
DEFAULT_QUALITY,
|
||||||
|
DEFAULT_PITCH | FF_DONTCARE,
|
||||||
|
"",
|
||||||
|
&m_StatusOverlayFont);
|
||||||
|
if (FAILED(hr)) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"D3DXCreateFontA() failed: %x",
|
||||||
|
hr);
|
||||||
|
m_StatusOverlayFont = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
SDL_assert(false);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int DXVA2Renderer::getDecoderCapabilities()
|
int DXVA2Renderer::getDecoderColorspace()
|
||||||
{
|
{
|
||||||
return 0;
|
if (isDXVideoProcessorAPIBlacklisted()) {
|
||||||
|
// StretchRect() assumes Rec 601 on Intel GPUs
|
||||||
|
return COLORSPACE_REC_601;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// VideoProcessBlt() *should* properly handle whatever, since
|
||||||
|
// we provide colorspace information. However, AMD GPUs seem to
|
||||||
|
// always assume Rec 709, so we'll use that as our default.
|
||||||
|
return COLORSPACE_REC_709;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
IFFmpegRenderer::VSyncConstraint DXVA2Renderer::getVsyncConstraint()
|
void DXVA2Renderer::renderFrame(AVFrame *frame)
|
||||||
{
|
|
||||||
return VSYNC_ANY;
|
|
||||||
}
|
|
||||||
|
|
||||||
void DXVA2Renderer::renderFrameAtVsync(AVFrame *frame)
|
|
||||||
{
|
{
|
||||||
IDirect3DSurface9* surface = reinterpret_cast<IDirect3DSurface9*>(frame->data[3]);
|
IDirect3DSurface9* surface = reinterpret_cast<IDirect3DSurface9*>(frame->data[3]);
|
||||||
HRESULT hr;
|
HRESULT hr;
|
||||||
@@ -785,12 +926,10 @@ void DXVA2Renderer::renderFrameAtVsync(AVFrame *frame)
|
|||||||
|
|
||||||
bltParams.Alpha = DXVA2_Fixed32OpaqueAlpha();
|
bltParams.Alpha = DXVA2_Fixed32OpaqueAlpha();
|
||||||
|
|
||||||
m_Device->Clear(0, nullptr, D3DCLEAR_TARGET, D3DCOLOR_ARGB(255, 0, 0, 0), 0.0f, 0);
|
hr = m_Device->Clear(0, nullptr, D3DCLEAR_TARGET, D3DCOLOR_ARGB(255, 0, 0, 0), 0.0f, 0);
|
||||||
|
|
||||||
hr = m_Processor->VideoProcessBlt(m_RenderTarget, &bltParams, &sample, 1, nullptr);
|
|
||||||
if (FAILED(hr)) {
|
if (FAILED(hr)) {
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"VideoProcessBlt() failed: %x",
|
"Clear() failed: %x",
|
||||||
hr);
|
hr);
|
||||||
SDL_Event event;
|
SDL_Event event;
|
||||||
event.type = SDL_RENDER_TARGETS_RESET;
|
event.type = SDL_RENDER_TARGETS_RESET;
|
||||||
@@ -798,7 +937,85 @@ void DXVA2Renderer::renderFrameAtVsync(AVFrame *frame)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
hr = m_Device->PresentEx(nullptr, nullptr, nullptr, nullptr, 0);
|
hr = m_Device->BeginScene();
|
||||||
|
if (FAILED(hr)) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"BeginScene() failed: %x",
|
||||||
|
hr);
|
||||||
|
SDL_Event event;
|
||||||
|
event.type = SDL_RENDER_TARGETS_RESET;
|
||||||
|
SDL_PushEvent(&event);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_Processor) {
|
||||||
|
hr = m_Processor->VideoProcessBlt(m_RenderTarget, &bltParams, &sample, 1, nullptr);
|
||||||
|
if (FAILED(hr)) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"VideoProcessBlt() failed, falling back to StretchRect(): %x",
|
||||||
|
hr);
|
||||||
|
m_Processor->Release();
|
||||||
|
m_Processor = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!m_Processor) {
|
||||||
|
// This function doesn't trigger any of Intel's garbage video "enhancements"
|
||||||
|
hr = m_Device->StretchRect(surface, &sample.SrcRect, m_RenderTarget, &sample.DstRect, D3DTEXF_NONE);
|
||||||
|
if (FAILED(hr)) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"StretchRect() failed: %x",
|
||||||
|
hr);
|
||||||
|
SDL_Event event;
|
||||||
|
event.type = SDL_RENDER_TARGETS_RESET;
|
||||||
|
SDL_PushEvent(&event);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_DebugOverlayFont != nullptr) {
|
||||||
|
if (Session::get()->getOverlayManager().isOverlayEnabled(Overlay::OverlayDebug)) {
|
||||||
|
SDL_Color color = Session::get()->getOverlayManager().getOverlayColor(Overlay::OverlayDebug);
|
||||||
|
m_DebugOverlayFont->DrawTextA(nullptr,
|
||||||
|
Session::get()->getOverlayManager().getOverlayText(Overlay::OverlayDebug),
|
||||||
|
-1,
|
||||||
|
&sample.DstRect,
|
||||||
|
DT_LEFT | DT_NOCLIP,
|
||||||
|
D3DCOLOR_ARGB(color.a, color.r, color.g, color.b));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_StatusOverlayFont != nullptr) {
|
||||||
|
if (Session::get()->getOverlayManager().isOverlayEnabled(Overlay::OverlayStatusUpdate)) {
|
||||||
|
SDL_Color color = Session::get()->getOverlayManager().getOverlayColor(Overlay::OverlayStatusUpdate);
|
||||||
|
m_StatusOverlayFont->DrawTextA(nullptr,
|
||||||
|
Session::get()->getOverlayManager().getOverlayText(Overlay::OverlayStatusUpdate),
|
||||||
|
-1,
|
||||||
|
&sample.DstRect,
|
||||||
|
DT_RIGHT | DT_NOCLIP,
|
||||||
|
D3DCOLOR_ARGB(color.a, color.r, color.g, color.b));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hr = m_Device->EndScene();
|
||||||
|
if (FAILED(hr)) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"EndScene() failed: %x",
|
||||||
|
hr);
|
||||||
|
SDL_Event event;
|
||||||
|
event.type = SDL_RENDER_TARGETS_RESET;
|
||||||
|
SDL_PushEvent(&event);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
// Use D3DPRESENT_DONOTWAIT if present may block in order to avoid holding the giant
|
||||||
|
// lock around this D3D device for excessive lengths of time (blocking concurrent decoding tasks).
|
||||||
|
hr = m_Device->PresentEx(nullptr, nullptr, nullptr, nullptr, m_BlockingPresent ? D3DPRESENT_DONOTWAIT : 0);
|
||||||
|
if (hr == D3DERR_WASSTILLDRAWING) {
|
||||||
|
SDL_Delay(1);
|
||||||
|
}
|
||||||
|
} while (hr == D3DERR_WASSTILLDRAWING);
|
||||||
if (FAILED(hr)) {
|
if (FAILED(hr)) {
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"PresentEx() failed: %x",
|
"PresentEx() failed: %x",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include "pacer/pacer.h"
|
#include "pacer/pacer.h"
|
||||||
|
|
||||||
#include <d3d9.h>
|
#include <d3d9.h>
|
||||||
|
#include <d3dx9.h>
|
||||||
#include <dxva2api.h>
|
#include <dxva2api.h>
|
||||||
|
|
||||||
extern "C" {
|
extern "C" {
|
||||||
@@ -14,24 +15,19 @@ class DXVA2Renderer : public IFFmpegRenderer
|
|||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
DXVA2Renderer();
|
DXVA2Renderer();
|
||||||
virtual ~DXVA2Renderer();
|
virtual ~DXVA2Renderer() override;
|
||||||
virtual bool initialize(SDL_Window* window,
|
virtual bool initialize(PDECODER_PARAMETERS params) override;
|
||||||
int videoFormat,
|
virtual bool prepareDecoderContext(AVCodecContext* context) override;
|
||||||
int width,
|
virtual void renderFrame(AVFrame* frame) override;
|
||||||
int height,
|
virtual void notifyOverlayUpdated(Overlay::OverlayType) override;
|
||||||
int maxFps,
|
virtual int getDecoderColorspace() override;
|
||||||
bool enableVsync);
|
|
||||||
virtual bool prepareDecoderContext(AVCodecContext* context);
|
|
||||||
virtual void renderFrameAtVsync(AVFrame* frame);
|
|
||||||
virtual bool needsTestFrame();
|
|
||||||
virtual int getDecoderCapabilities();
|
|
||||||
virtual VSyncConstraint getVsyncConstraint();
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
bool initializeDecoder();
|
bool initializeDecoder();
|
||||||
bool initializeRenderer();
|
bool initializeRenderer();
|
||||||
bool initializeDevice(SDL_Window* window, bool enableVsync);
|
bool initializeDevice(SDL_Window* window, bool enableVsync);
|
||||||
bool isDecoderBlacklisted();
|
bool isDecoderBlacklisted();
|
||||||
|
bool isDXVideoProcessorAPIBlacklisted();
|
||||||
|
|
||||||
static
|
static
|
||||||
AVBufferRef* ffPoolAlloc(void* opaque, int size);
|
AVBufferRef* ffPoolAlloc(void* opaque, int size);
|
||||||
@@ -67,4 +63,7 @@ private:
|
|||||||
DXVA2_ValueRange m_SaturationRange;
|
DXVA2_ValueRange m_SaturationRange;
|
||||||
DXVA2_VideoDesc m_Desc;
|
DXVA2_VideoDesc m_Desc;
|
||||||
REFERENCE_TIME m_FrameIndex;
|
REFERENCE_TIME m_FrameIndex;
|
||||||
|
LPD3DXFONT m_DebugOverlayFont;
|
||||||
|
LPD3DXFONT m_StatusOverlayFont;
|
||||||
|
bool m_BlockingPresent;
|
||||||
};
|
};
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
#include "mmal.h"
|
||||||
|
|
||||||
|
#include "streaming/streamutils.h"
|
||||||
|
#include "streaming/session.h"
|
||||||
|
|
||||||
|
#include <Limelight.h>
|
||||||
|
|
||||||
|
MmalRenderer::MmalRenderer()
|
||||||
|
: m_Renderer(nullptr),
|
||||||
|
m_InputPort(nullptr)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
MmalRenderer::~MmalRenderer()
|
||||||
|
{
|
||||||
|
if (m_InputPort != nullptr) {
|
||||||
|
mmal_port_disable(m_InputPort);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_Renderer != nullptr) {
|
||||||
|
mmal_component_destroy(m_Renderer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MmalRenderer::prepareDecoderContext(AVCodecContext*)
|
||||||
|
{
|
||||||
|
/* Nothing to do */
|
||||||
|
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Using MMAL renderer");
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MmalRenderer::initialize(PDECODER_PARAMETERS params)
|
||||||
|
{
|
||||||
|
MMAL_STATUS_T status;
|
||||||
|
|
||||||
|
status = mmal_component_create(MMAL_COMPONENT_DEFAULT_VIDEO_RENDERER, &m_Renderer);
|
||||||
|
if (status != MMAL_SUCCESS) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"mmal_component_create() failed: %x (%s)",
|
||||||
|
status, mmal_status_to_string(status));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_InputPort = m_Renderer->input[0];
|
||||||
|
|
||||||
|
m_InputPort->format->encoding = MMAL_ENCODING_OPAQUE;
|
||||||
|
m_InputPort->format->es->video.width = params->width;
|
||||||
|
m_InputPort->format->es->video.height = params->height;
|
||||||
|
m_InputPort->format->es->video.crop.x = 0;
|
||||||
|
m_InputPort->format->es->video.crop.y = 0;
|
||||||
|
m_InputPort->format->es->video.crop.width = params->width;
|
||||||
|
m_InputPort->format->es->video.crop.height = params->height;
|
||||||
|
status = mmal_port_format_commit(m_InputPort);
|
||||||
|
if (status != MMAL_SUCCESS) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"mmal_port_format_commit() failed: %x (%s)",
|
||||||
|
status, mmal_status_to_string(status));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
status = mmal_component_enable(m_Renderer);
|
||||||
|
if (status != MMAL_SUCCESS) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"mmal_component_enable() failed: %x (%s)",
|
||||||
|
status, mmal_status_to_string(status));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
MMAL_DISPLAYREGION_T dr;
|
||||||
|
|
||||||
|
dr.hdr.id = MMAL_PARAMETER_DISPLAYREGION;
|
||||||
|
dr.hdr.size = sizeof(MMAL_DISPLAYREGION_T);
|
||||||
|
|
||||||
|
dr.set = MMAL_DISPLAY_SET_LAYER;
|
||||||
|
dr.layer = 128;
|
||||||
|
|
||||||
|
dr.set |= MMAL_DISPLAY_SET_ALPHA;
|
||||||
|
dr.alpha = 255;
|
||||||
|
|
||||||
|
dr.set |= MMAL_DISPLAY_SET_FULLSCREEN;
|
||||||
|
dr.fullscreen = (SDL_GetWindowFlags(params->window) & SDL_WINDOW_FULLSCREEN) != 0;
|
||||||
|
|
||||||
|
{
|
||||||
|
SDL_Rect src, dst;
|
||||||
|
src.x = src.y = 0;
|
||||||
|
src.w = params->width;
|
||||||
|
src.h = params->height;
|
||||||
|
dst.x = dst.y = 0;
|
||||||
|
SDL_GetWindowSize(params->window, &dst.w, &dst.h);
|
||||||
|
|
||||||
|
StreamUtils::scaleSourceToDestinationSurface(&src, &dst);
|
||||||
|
|
||||||
|
dr.set |= MMAL_DISPLAY_SET_DEST_RECT;
|
||||||
|
dr.dest_rect.x = dst.x;
|
||||||
|
dr.dest_rect.y = dst.y;
|
||||||
|
dr.dest_rect.width = dst.w;
|
||||||
|
dr.dest_rect.height = dst.h;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
status = mmal_port_enable(m_InputPort, InputPortCallback);
|
||||||
|
if (status != MMAL_SUCCESS) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"mmal_port_enable() failed: %x (%s)",
|
||||||
|
status, mmal_status_to_string(status));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void MmalRenderer::InputPortCallback(MMAL_PORT_T*, MMAL_BUFFER_HEADER_T* buffer)
|
||||||
|
{
|
||||||
|
mmal_buffer_header_release(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
enum AVPixelFormat MmalRenderer::getPreferredPixelFormat(int videoFormat)
|
||||||
|
{
|
||||||
|
// Opaque MMAL buffers
|
||||||
|
SDL_assert(videoFormat == VIDEO_FORMAT_H264);
|
||||||
|
return AV_PIX_FMT_MMAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MmalRenderer::needsTestFrame()
|
||||||
|
{
|
||||||
|
// We won't be able to decode if the GPU memory is 64 MB or lower,
|
||||||
|
// so we must test before allowing the decoder to be used.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void MmalRenderer::renderFrame(AVFrame* frame)
|
||||||
|
{
|
||||||
|
MMAL_BUFFER_HEADER_T* buffer = (MMAL_BUFFER_HEADER_T*)frame->data[3];
|
||||||
|
MMAL_STATUS_T status;
|
||||||
|
|
||||||
|
status = mmal_port_send_buffer(m_InputPort, buffer);
|
||||||
|
if (status != MMAL_SUCCESS) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"mmal_port_send_buffer() failed: %x (%s)",
|
||||||
|
status, mmal_status_to_string(status));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Prevent the buffer from being freed during av_frame_free()
|
||||||
|
// until rendering is complete. The reference is dropped in
|
||||||
|
// InputPortCallback().
|
||||||
|
mmal_buffer_header_acquire(buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||