mirror of
https://github.com/moonlight-stream/moonlight-qt.git
synced 2026-10-17 22:14:16 +00:00
Compare commits
2244
Commits
v1.2.1
...
linuxdeploy
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6fa83ea330 | ||
|
|
c21affd15a | ||
|
|
7de4e74829 | ||
|
|
9d6218d1ff | ||
|
|
8cc3b30642 | ||
|
|
d77e2cc913 | ||
|
|
453435f916 | ||
|
|
f222aa7985 | ||
|
|
5faa5fece8 | ||
|
|
51f86caac3 | ||
|
|
5d813ed3df | ||
|
|
d17575d47c | ||
|
|
0e50a9d457 | ||
|
|
ed029f194f | ||
|
|
569153220a | ||
|
|
e0367d05ae | ||
|
|
53e0c2534d | ||
|
|
a766c9cafc | ||
|
|
c685021f21 | ||
|
|
af03f57e08 | ||
|
|
1fc1122c65 | ||
|
|
94d47e95fe | ||
|
|
1cffa7b350 | ||
|
|
fae1cd3dee | ||
|
|
2160f029d2 | ||
|
|
1ec1a3d7ad | ||
|
|
eb3d8ad92c | ||
|
|
07b00a6987 | ||
|
|
4ec682dc72 | ||
|
|
a9a1ea0a0b | ||
|
|
c8d18f8a8e | ||
|
|
9c11dca454 | ||
|
|
9496c25271 | ||
|
|
6ff3309677 | ||
|
|
e4c211b987 | ||
|
|
cac3bb28d7 | ||
|
|
78bc2141f6 | ||
|
|
effac744c7 | ||
|
|
5676a076fd | ||
|
|
b6407492c7 | ||
|
|
91ce7a41e5 | ||
|
|
89c8462e9e | ||
|
|
53a7680a38 | ||
|
|
88b4a17fe1 | ||
|
|
8326223220 | ||
|
|
4aa5ad71c0 | ||
|
|
e785be03c2 | ||
|
|
db294fe4ae | ||
|
|
2e9fbecfea | ||
|
|
7c066485f3 | ||
|
|
11d51e121a | ||
|
|
09675bf56d | ||
|
|
8f994dd113 | ||
|
|
e596c2dcdc | ||
|
|
209511b2dc | ||
|
|
efa67fec1e | ||
|
|
be0aeb6cc2 | ||
|
|
8d5072533f | ||
|
|
4c14a24960 | ||
|
|
e3702dbab7 | ||
|
|
e4325b8736 | ||
|
|
eb294e63f4 | ||
|
|
9d597a666b | ||
|
|
33e0b08699 | ||
|
|
31898bbab4 | ||
|
|
34c9e01a96 | ||
|
|
ddc6675dc8 | ||
|
|
f0626e8cae | ||
|
|
6f4ced5ed6 | ||
|
|
0a2b134d49 | ||
|
|
f6a9e7694a | ||
|
|
8795fb54f4 | ||
|
|
092eb1b117 | ||
|
|
179857f17d | ||
|
|
d878447413 | ||
|
|
60fd840d7a | ||
|
|
5020fc6f48 | ||
|
|
a2122e17c6 | ||
|
|
6bb7c8bf2a | ||
|
|
2dda65078f | ||
|
|
2914ff678d | ||
|
|
a56efe5c36 | ||
|
|
b7a05d73b9 | ||
|
|
6d5b9e4706 | ||
|
|
9813932c1b | ||
|
|
d4d4ab5b06 | ||
|
|
402ac59390 | ||
|
|
c33c49e6fa | ||
|
|
e1bbf8144e | ||
|
|
f4712718cb | ||
|
|
d040bd24d1 | ||
|
|
6d40c61850 | ||
|
|
b4a5d21f97 | ||
|
|
792f762c15 | ||
|
|
8c9c799f81 | ||
|
|
4eaf037c75 | ||
|
|
b732b58807 | ||
|
|
d484ec3ac8 | ||
|
|
f5f06ae44e | ||
|
|
d2fa488979 | ||
|
|
cd9aceb62c | ||
|
|
d5b7ef0c58 | ||
|
|
e4be57db5d | ||
|
|
1b0c657864 | ||
|
|
64fea80ac9 | ||
|
|
990804fe6b | ||
|
|
05ef938e2c | ||
|
|
1e825c85e5 | ||
|
|
a0a4c1ea83 | ||
|
|
b5e7dec378 | ||
|
|
adbd19e6fa | ||
|
|
02a86167da | ||
|
|
a739c1cef8 | ||
|
|
f6e08f8a43 | ||
|
|
218ffc2d55 | ||
|
|
0f49dca4c0 | ||
|
|
30274f3ae8 | ||
|
|
c696ae7a7e | ||
|
|
66a30c66f3 | ||
|
|
198a208b44 | ||
|
|
21ce23e377 | ||
|
|
cd4ae1c0ed | ||
|
|
d865c77975 | ||
|
|
cd13910e6c | ||
|
|
b8e9262b1b | ||
|
|
c14ebb1ad7 | ||
|
|
d3957b3cbb | ||
|
|
a8e8efb17b | ||
|
|
75adc22d05 | ||
|
|
2549efc8b3 | ||
|
|
6f8e517ce1 | ||
|
|
09f8e7117b | ||
|
|
88719cc8bf | ||
|
|
3ea62c0800 | ||
|
|
7643cc929e | ||
|
|
a58d3cfe39 | ||
|
|
b41c4022ea | ||
|
|
788675ad59 | ||
|
|
be9f465008 | ||
|
|
8b6ad55c9b | ||
|
|
92b68a40d1 | ||
|
|
a931161923 | ||
|
|
37b9b6ed9e | ||
|
|
fad197fdce | ||
|
|
cda65ab58d | ||
|
|
9ffe5218d5 | ||
|
|
ed98f256e8 | ||
|
|
8aec35a650 | ||
|
|
a3237b741c | ||
|
|
377409f313 | ||
|
|
80ddd5ba13 | ||
|
|
4a547eb05e | ||
|
|
1c6ed8f390 | ||
|
|
0a58af4fa1 | ||
|
|
50a094c917 | ||
|
|
d14cf984f4 | ||
|
|
13d9d79fe3 | ||
|
|
70d4f24455 | ||
|
|
705cd3ddc4 | ||
|
|
ea5b062da3 | ||
|
|
e6f41f5574 | ||
|
|
9af6222039 | ||
|
|
476414ea71 | ||
|
|
83d770cdba | ||
|
|
25e2d39e26 | ||
|
|
bd32370b6d | ||
|
|
8726c1c7d0 | ||
|
|
34881599f5 | ||
|
|
de0fb72424 | ||
|
|
46c76534bc | ||
|
|
67cba9e397 | ||
|
|
2ab72375c6 | ||
|
|
d3118bd677 | ||
|
|
b8c3658c42 | ||
|
|
d3fb52c5a0 | ||
|
|
7924a5b6a7 | ||
|
|
2972022ced | ||
|
|
c0846155c3 | ||
|
|
ac6dadffc6 | ||
|
|
745ac34b15 | ||
|
|
5f89636ed7 | ||
|
|
46d58e55a3 | ||
|
|
be460970a8 | ||
|
|
d50ba06321 | ||
|
|
b108684edc | ||
|
|
b6d10a34c8 | ||
|
|
232ff0b3c6 | ||
|
|
c5960c3e15 | ||
|
|
b48cbd76f5 | ||
|
|
ceef792f04 | ||
|
|
f3f1d56e8d | ||
|
|
39e24e9c8c | ||
|
|
c26e437f40 | ||
|
|
e89f63b8fb | ||
|
|
cf8251b9e6 | ||
|
|
d6c9beffc1 | ||
|
|
93dc6d6b60 | ||
|
|
8ee82421b3 | ||
|
|
31c65e0863 | ||
|
|
3e5aa9b127 | ||
|
|
085480e9f4 | ||
|
|
c3ce0918b3 | ||
|
|
c5b7a9c679 | ||
|
|
f1451a0db3 | ||
|
|
8a5f34fb01 | ||
|
|
d1f43ca258 | ||
|
|
249c29b74c | ||
|
|
1cf207de07 | ||
|
|
4f08d52754 | ||
|
|
41ad3c0938 | ||
|
|
21fb2832e1 | ||
|
|
f0bb1031d8 | ||
|
|
b80d2a00c0 | ||
|
|
85ea28287e | ||
|
|
a6f8901a18 | ||
|
|
d501a627f0 | ||
|
|
526d815930 | ||
|
|
b448e5bbd7 | ||
|
|
741e360024 | ||
|
|
7edbede95d | ||
|
|
7f54662160 | ||
|
|
f66285ddba | ||
|
|
bf250a4edb | ||
|
|
bdfadb1cdc | ||
|
|
63a381a882 | ||
|
|
4a591069ac | ||
|
|
c71b513bc4 | ||
|
|
700655c051 | ||
|
|
3425fec33d | ||
|
|
de844f2a0c | ||
|
|
54163e30d0 | ||
|
|
bd6a1d9a9b | ||
|
|
b25801ff7d | ||
|
|
bc5e9917cd | ||
|
|
d8eb817ed9 | ||
|
|
c1e5b0bf66 | ||
|
|
26f96a20a9 | ||
|
|
dbcc6a9089 | ||
|
|
9c6d2bab19 | ||
|
|
c92dc2495e | ||
|
|
1c24aada0a | ||
|
|
58f9b2a83b | ||
|
|
5feb331c46 | ||
|
|
9d5f25f828 | ||
|
|
d3c23b55dc | ||
|
|
80b7a69cd2 | ||
|
|
296387345d | ||
|
|
42bf87753f | ||
|
|
d262e5d1f6 | ||
|
|
f0821d9543 | ||
|
|
4164a222ae | ||
|
|
245cd7f1a8 | ||
|
|
62816a6b2a | ||
|
|
f5ef201905 | ||
|
|
01288be937 | ||
|
|
f57257cbfd | ||
|
|
d40999ce06 | ||
|
|
cb6d770b92 | ||
|
|
4d6196c3d3 | ||
|
|
54283ce0a1 | ||
|
|
6313129603 | ||
|
|
84d7a22638 | ||
|
|
f0aff0b0fb | ||
|
|
be358e9503 | ||
|
|
379d5ca376 | ||
|
|
8ed7144751 | ||
|
|
66b96e75af | ||
|
|
1160f40655 | ||
|
|
d5abf568c6 | ||
|
|
8324f61db0 | ||
|
|
9106f2c420 | ||
|
|
a2670dd568 | ||
|
|
c54e6e4459 | ||
|
|
dd6f1c5873 | ||
|
|
7fab5007a8 | ||
|
|
7f7cc89e61 | ||
|
|
7116efd8de | ||
|
|
229f5e4cea | ||
|
|
af37002a60 | ||
|
|
181dba5864 | ||
|
|
fd314db27e | ||
|
|
f67272b153 | ||
|
|
9b3050514b | ||
|
|
2c12ad297f | ||
|
|
50223dbdb8 | ||
|
|
beadf7819b | ||
|
|
7d829ac0c4 | ||
|
|
2f6e7608f8 | ||
|
|
5244313b30 | ||
|
|
c13e6b08c1 | ||
|
|
151f5dd678 | ||
|
|
a34d40e3ee | ||
|
|
68aa04926e | ||
|
|
7d544c1ce4 | ||
|
|
ac7696ea8f | ||
|
|
e79bf664dd | ||
|
|
4d303cebee | ||
|
|
bd6235efba | ||
|
|
b1232e0ed4 | ||
|
|
c35f7086a0 | ||
|
|
c7bc76325f | ||
|
|
ccaca68570 | ||
|
|
57db20016a | ||
|
|
bdb6d02dac | ||
|
|
9bcc6291be | ||
|
|
4ec549650d | ||
|
|
14027f3c74 | ||
|
|
fb9a164111 | ||
|
|
200cab9d17 | ||
|
|
c52a57f0ec | ||
|
|
b4dc7ca7cb | ||
|
|
00c1dd0d0d | ||
|
|
ecfc24c5d4 | ||
|
|
e6e91ca88b | ||
|
|
260a0e0ae2 | ||
|
|
3f8f4744c5 | ||
|
|
4bbd02fb2d | ||
|
|
c5ca672865 | ||
|
|
4688f3650c | ||
|
|
2550f416f4 | ||
|
|
579ad25a01 | ||
|
|
1144dbccb3 | ||
|
|
ff81f74391 | ||
|
|
c0d38ee78f | ||
|
|
c9cb64f90b | ||
|
|
93e597a93f | ||
|
|
82b33c033a | ||
|
|
be266d3349 | ||
|
|
c618a0b5df | ||
|
|
a20d429bc1 | ||
|
|
2b3e0803de | ||
|
|
4189903233 | ||
|
|
1a0dd56bb9 | ||
|
|
3aa3bfa403 | ||
|
|
aa9a716bd5 | ||
|
|
6fd2b73e6b | ||
|
|
06b5c4631f | ||
|
|
997c4aa0ae | ||
|
|
f4343c5f29 | ||
|
|
3cdea6b039 | ||
|
|
ac2e10c712 | ||
|
|
61fa45ad21 | ||
|
|
f362e76127 | ||
|
|
749c69fc76 | ||
|
|
5dca30def1 | ||
|
|
502783a86b | ||
|
|
45989fdd6e | ||
|
|
490aa5082f | ||
|
|
ae1c65805c | ||
|
|
9cb4105aec | ||
|
|
1fd545ae1f | ||
|
|
3194cb09d8 | ||
|
|
f9bb45579b | ||
|
|
f7b2edc8e3 | ||
|
|
c9a3946d80 | ||
|
|
0c8354336b | ||
|
|
1bf86f52d3 | ||
|
|
504d42865d | ||
|
|
65c04fd560 | ||
|
|
11dc244857 | ||
|
|
b9cab4cac5 | ||
|
|
65647a32f4 | ||
|
|
777502b495 | ||
|
|
ce4309694f | ||
|
|
ccc64a5629 | ||
|
|
25f6c020b6 | ||
|
|
6a95742a92 | ||
|
|
1dbdcb5279 | ||
|
|
9ce0537587 | ||
|
|
76d03f015d | ||
|
|
edbc24fa3e | ||
|
|
d4fd50b973 | ||
|
|
d5ab795b36 | ||
|
|
c5fc5220f3 | ||
|
|
28dea31533 | ||
|
|
8ce6a9585f | ||
|
|
47452371db | ||
|
|
4001b05fca | ||
|
|
75e917622e | ||
|
|
75359bb1c4 | ||
|
|
c93f7e7385 | ||
|
|
48bcd725c9 | ||
|
|
e807a52cfa | ||
|
|
9c9bfd8428 | ||
|
|
29b1304337 | ||
|
|
cc0b574bb1 | ||
|
|
7a769172a1 | ||
|
|
d9c7a245ef | ||
|
|
469a5c78f1 | ||
|
|
fabb4fdadc | ||
|
|
9491884cb4 | ||
|
|
bf51577787 | ||
|
|
4c5bcee8dc | ||
|
|
fd70865026 | ||
|
|
351aaa6759 | ||
|
|
59bc625cc7 | ||
|
|
b2f765e8ef | ||
|
|
bdd9a3a994 | ||
|
|
2ecafabcab | ||
|
|
dd2a99a96b | ||
|
|
9ff2ac0974 | ||
|
|
5760d08c33 | ||
|
|
edd7a134d8 | ||
|
|
ffa87c5f01 | ||
|
|
ff7e61c6d9 | ||
|
|
6ad96fba42 | ||
|
|
85856114b2 | ||
|
|
7decfae792 | ||
|
|
b6008b15dc | ||
|
|
260ec3d80d | ||
|
|
7ec2e50334 | ||
|
|
e532b9167a | ||
|
|
52d5890372 | ||
|
|
f34d11994f | ||
|
|
359c92340d | ||
|
|
15e337fff8 | ||
|
|
707dd3cb83 | ||
|
|
98f6a09991 | ||
|
|
f1d0e97681 | ||
|
|
103f988dbf | ||
|
|
2257cb0cef | ||
|
|
b6a3369243 | ||
|
|
4af9623727 | ||
|
|
515db03fe5 | ||
|
|
e44d097683 | ||
|
|
208d048358 | ||
|
|
9936085aee | ||
|
|
3279d9c3f6 | ||
|
|
e571d5833c | ||
|
|
3531fe0a4f | ||
|
|
286676e5c1 | ||
|
|
6ce02616f0 | ||
|
|
13880353d8 | ||
|
|
ec69dad8d7 | ||
|
|
72ae324d71 | ||
|
|
901cbd255c | ||
|
|
2a63ad53d7 | ||
|
|
9b3d4c1ad7 | ||
|
|
054e334066 | ||
|
|
6d023c2dfa | ||
|
|
0e2d5bf441 | ||
|
|
023b6b2772 | ||
|
|
6f39d120cb | ||
|
|
9cf305865b | ||
|
|
6d47287b60 | ||
|
|
6b11f43302 | ||
|
|
60fb6881b2 | ||
|
|
b79c116bd9 | ||
|
|
895d0a6bf3 | ||
|
|
5a1ef55767 | ||
|
|
76deafbd7b | ||
|
|
ae2693a860 | ||
|
|
0783b28ba6 | ||
|
|
f786e94c7b | ||
|
|
1caee721e8 | ||
|
|
accd30176f | ||
|
|
7721fa6d7c | ||
|
|
8e295aab00 | ||
|
|
4838a75c58 | ||
|
|
c5ec8c0fdb | ||
|
|
b266f61ba6 | ||
|
|
d1dffdc34c | ||
|
|
268f8db26b | ||
|
|
de28eda266 | ||
|
|
3e9e497203 | ||
|
|
96fb6ee5e9 | ||
|
|
02853e74ba | ||
|
|
46910cf774 | ||
|
|
b1c77ff80e | ||
|
|
92ee4a3046 | ||
|
|
6ec0c79899 | ||
|
|
1c281ee3fc | ||
|
|
e2b7cbfe62 | ||
|
|
67e89d9e16 | ||
|
|
bbf6d9722a | ||
|
|
34549864ad | ||
|
|
1303a700e0 | ||
|
|
25132a1f7b | ||
|
|
7074463d0f | ||
|
|
99311403fa | ||
|
|
8b50eea485 | ||
|
|
302dca6c0c | ||
|
|
f756be87ff | ||
|
|
9d99ecbca6 | ||
|
|
1475bdfbba | ||
|
|
69a3406d4a | ||
|
|
7e17d82ee5 | ||
|
|
153db55519 | ||
|
|
4290a54ffa | ||
|
|
a6e549471c | ||
|
|
04edfdc4ca | ||
|
|
db30faf602 | ||
|
|
cdb610f121 | ||
|
|
e404722e7c | ||
|
|
1840c47751 | ||
|
|
3bd571bc90 | ||
|
|
224153e230 | ||
|
|
ef12850867 | ||
|
|
caf322d934 | ||
|
|
9043e8f663 | ||
|
|
76fd502262 | ||
|
|
9186feca80 | ||
|
|
df814fef4a | ||
|
|
d2b3bc962f | ||
|
|
876375f7e9 | ||
|
|
e662e93a53 | ||
|
|
7c6954b5f6 | ||
|
|
416003248b | ||
|
|
6d6cd6fc35 | ||
|
|
17448c02b0 | ||
|
|
f3a75e8e76 | ||
|
|
7da085480c | ||
|
|
bed3a6ecd8 | ||
|
|
e01c42153c | ||
|
|
9df65cb814 | ||
|
|
fddb4881fb | ||
|
|
5765c254cd | ||
|
|
ea724a05a6 | ||
|
|
369f614b59 | ||
|
|
8606b2c95e | ||
|
|
fafddddfe0 | ||
|
|
1bb16be183 | ||
|
|
8e2aa87c4f | ||
|
|
2aea070d93 | ||
|
|
37ace0060e | ||
|
|
28b4272123 | ||
|
|
19660174b7 | ||
|
|
2beaf10ea5 | ||
|
|
665352ec95 | ||
|
|
ab791cf4c8 | ||
|
|
ef7dff32aa | ||
|
|
9227ebfec9 | ||
|
|
f138827cdf | ||
|
|
e25919e0f9 | ||
|
|
99749d4730 | ||
|
|
9e811f54f1 | ||
|
|
0bb0d27d64 | ||
|
|
ede5ab8671 | ||
|
|
6c6f808365 | ||
|
|
dd9569913b | ||
|
|
778eb07c5f | ||
|
|
e2ffeae3f6 | ||
|
|
eb6d16fdcf | ||
|
|
e548697a36 | ||
|
|
c707dab70a | ||
|
|
da0244c538 | ||
|
|
ff332d45f8 | ||
|
|
9e92c07cb7 | ||
|
|
7f009a4b8e | ||
|
|
8ac378f467 | ||
|
|
82ec773119 | ||
|
|
3580286807 | ||
|
|
e226091c19 | ||
|
|
074b4520e5 | ||
|
|
de30eeaa66 | ||
|
|
fe9282e7d9 | ||
|
|
45ccd1a811 | ||
|
|
52583f5c71 | ||
|
|
952ebcd0d2 | ||
|
|
d5a198b764 | ||
|
|
b5b2731d5f | ||
|
|
d085722911 | ||
|
|
2e29ef8d74 | ||
|
|
34fa7167b1 | ||
|
|
27b173b76b | ||
|
|
d73df12367 | ||
|
|
725e51f9b3 | ||
|
|
0c6689a5b4 | ||
|
|
32115c639e | ||
|
|
17a2548480 | ||
|
|
753deb915f | ||
|
|
7d6f4135d5 | ||
|
|
622ec843ff | ||
|
|
5c18bf1608 | ||
|
|
d184730a82 | ||
|
|
335ed0e8e6 | ||
|
|
0abb9fd7c2 | ||
|
|
1a1ce05959 | ||
|
|
93408386ae | ||
|
|
415b512c75 | ||
|
|
d389f9a6e9 | ||
|
|
ba01558bc5 | ||
|
|
dff02eab7d | ||
|
|
ecfcedad58 | ||
|
|
e76780e105 | ||
|
|
1d1fa0577b | ||
|
|
a9d7c8e495 | ||
|
|
ffdf683597 | ||
|
|
ac0e1098b9 | ||
|
|
35695642d5 | ||
|
|
e458682a23 | ||
|
|
fbe5e6f01f | ||
|
|
52756e7f45 | ||
|
|
0f57abf6f8 | ||
|
|
94943d2865 | ||
|
|
c3bd7edc4e | ||
|
|
b6bb96223d | ||
|
|
f07f3c843a | ||
|
|
7712a2dce4 | ||
|
|
d86e13a3a7 | ||
|
|
3aaa09bb7d | ||
|
|
377abf2155 | ||
|
|
d3219ae24a | ||
|
|
bd60b873ec | ||
|
|
3abf925810 | ||
|
|
d5cbcc8199 | ||
|
|
640ac3f9fe | ||
|
|
83811e2a07 | ||
|
|
31641f5246 | ||
|
|
d7bc735edc | ||
|
|
b59de38e0b | ||
|
|
17af71fe7a | ||
|
|
a0c77d0ad8 | ||
|
|
ec7137e693 | ||
|
|
1b56f28fd4 | ||
|
|
486c46781b | ||
|
|
6397031e46 | ||
|
|
e7a2d9f44d | ||
|
|
ee5d0acc1f | ||
|
|
6a5a69cabd | ||
|
|
cd7391f908 | ||
|
|
d6d28f1afd | ||
|
|
5a6620a9ae | ||
|
|
5a47ddca33 | ||
|
|
b58f021fc8 | ||
|
|
12b93337d1 | ||
|
|
807fc6b31c | ||
|
|
959080aa95 | ||
|
|
18a772aeba | ||
|
|
9bdd17f1c3 | ||
|
|
3d1967bd1e | ||
|
|
469bd32bde | ||
|
|
351b4edf0c | ||
|
|
a0a8b70bc1 | ||
|
|
e07f069222 | ||
|
|
47ccd1e481 | ||
|
|
96fba43186 | ||
|
|
9e7c212c6d | ||
|
|
e67d83e097 | ||
|
|
4474f815af | ||
|
|
e130a15037 | ||
|
|
845ef147c9 | ||
|
|
fb17c15cfd | ||
|
|
bb74cedf59 | ||
|
|
9af6906380 | ||
|
|
c1ae3cceda | ||
|
|
e1e3bd512f | ||
|
|
cf3ff32340 | ||
|
|
2c98deeae3 | ||
|
|
a572681737 | ||
|
|
e78b389bc0 | ||
|
|
83ec929758 | ||
|
|
9117f6565e | ||
|
|
aa33432c81 | ||
|
|
e25d9b0da2 | ||
|
|
0dfa8a7b53 | ||
|
|
85a9f85c54 | ||
|
|
ed68f920f1 | ||
|
|
f9a4498f9b | ||
|
|
4d1acf2e30 | ||
|
|
db4739b82d | ||
|
|
38bf833708 | ||
|
|
364093aef8 | ||
|
|
6d220a9062 | ||
|
|
abec8b3fe1 | ||
|
|
93ed985043 | ||
|
|
23c737cab9 | ||
|
|
cb850f013a | ||
|
|
6bd964a094 | ||
|
|
419c788ab5 | ||
|
|
a412100a11 | ||
|
|
0531666f38 | ||
|
|
c096238998 | ||
|
|
211fec0ff0 | ||
|
|
58e27875ae | ||
|
|
954e6608a1 | ||
|
|
1cd588f25c | ||
|
|
ebfe035a18 | ||
|
|
6f22ed13c1 | ||
|
|
4aa2b8570c | ||
|
|
fe6562e31c | ||
|
|
654e386263 | ||
|
|
76d0eb6b63 | ||
|
|
02d867fe09 | ||
|
|
76ff53f09f | ||
|
|
37d92dee16 | ||
|
|
afbc49e39a | ||
|
|
d1ccd19fcc | ||
|
|
ebe270bec5 | ||
|
|
784234c857 | ||
|
|
6a85996b4a | ||
|
|
c6710f93dc | ||
|
|
5e46c4ec2b | ||
|
|
011feab6ce | ||
|
|
f8c5d3c0ce | ||
|
|
74d452bf35 | ||
|
|
fd2ff61a40 | ||
|
|
8a87a09947 | ||
|
|
cf544a8703 | ||
|
|
0e1190a059 | ||
|
|
f2535f1e6e | ||
|
|
cefa567f95 | ||
|
|
9be782ae8e | ||
|
|
610e07d4bd | ||
|
|
75eb921a04 | ||
|
|
97bd515b11 | ||
|
|
a45695a22a | ||
|
|
0d67fd1ff0 | ||
|
|
a093a0ae59 | ||
|
|
c9ad8ffa69 | ||
|
|
9987914ebb | ||
|
|
fbfd6fb462 | ||
|
|
bec053e3cf | ||
|
|
d364b24378 | ||
|
|
8c67586cd0 | ||
|
|
89a628a0dd | ||
|
|
4fb9681b7f | ||
|
|
c13128ca1d | ||
|
|
ceca83346b | ||
|
|
fc6c51c75a | ||
|
|
161d91acf8 | ||
|
|
06ef10a2ad | ||
|
|
7b29b22192 | ||
|
|
289a7165b3 | ||
|
|
62011c37f4 | ||
|
|
919e44b29b | ||
|
|
32be1b158b | ||
|
|
f635860afd | ||
|
|
3b078f095b | ||
|
|
72fc7ea31a | ||
|
|
f4edd1e63a | ||
|
|
6011384afe | ||
|
|
896456a8ac | ||
|
|
8959ddbeb6 | ||
|
|
767215fce5 | ||
|
|
3ef05f3a35 | ||
|
|
12624bc5c4 | ||
|
|
5bd0dea85d | ||
|
|
80242f2e90 | ||
|
|
cf3e328f9d | ||
|
|
150002d4d4 | ||
|
|
b972a22459 | ||
|
|
db06239018 | ||
|
|
bdc7d53515 | ||
|
|
7f83b79c19 | ||
|
|
47b3aff352 | ||
|
|
679f237c1b | ||
|
|
3d8de56f25 | ||
|
|
801cebbbc7 | ||
|
|
66fb0465c4 | ||
|
|
8bbc9b2c57 | ||
|
|
be7694dd8b | ||
|
|
ce8b24dfa7 | ||
|
|
75ddac54c5 | ||
|
|
b01a83ff39 | ||
|
|
c3e886fbcc | ||
|
|
e20d56041e | ||
|
|
114074bfd8 | ||
|
|
e44e29e7eb | ||
|
|
03663d5552 | ||
|
|
dce2a857f7 | ||
|
|
61f71a318e | ||
|
|
646ce2bf66 | ||
|
|
2bbae90e2c | ||
|
|
bb8e229814 | ||
|
|
9ed8252cc6 | ||
|
|
2cef09471b | ||
|
|
2d15382e31 | ||
|
|
770872238d | ||
|
|
2a05b890d8 | ||
|
|
481f23b6e9 | ||
|
|
4e7107a09e | ||
|
|
0223394725 | ||
|
|
058739321c | ||
|
|
708bec2937 | ||
|
|
c939537c5c | ||
|
|
9497657c83 | ||
|
|
d4050f3a4f | ||
|
|
c74c4ef041 | ||
|
|
d75663052d | ||
|
|
8f2db99406 | ||
|
|
6a58dbb05e | ||
|
|
8aeb976128 | ||
|
|
7d51a4b67d | ||
|
|
f7d412e3bf | ||
|
|
4469c7fae3 | ||
|
|
ee95efc0f1 | ||
|
|
3ddc8ad68a | ||
|
|
333d97160e | ||
|
|
aac5ad3fd0 | ||
|
|
1ef083c952 | ||
|
|
c63b918bb2 | ||
|
|
4dba4ad538 | ||
|
|
83fcad6bd1 | ||
|
|
e08178474d | ||
|
|
f374e88bac | ||
|
|
3cb169ea26 | ||
|
|
58f87e39a9 | ||
|
|
b1c8005d1a | ||
|
|
f8a2e22a18 | ||
|
|
7ea118fbae | ||
|
|
2149024c35 | ||
|
|
f5d71c8c9a | ||
|
|
e108a05895 | ||
|
|
ea8bccd508 | ||
|
|
bca919c48c | ||
|
|
511ffe03b5 | ||
|
|
413993ab6f | ||
|
|
948b9c818a | ||
|
|
cf615ea88d | ||
|
|
2d1e16fe50 | ||
|
|
089c3d3ad3 | ||
|
|
63e3fa6609 | ||
|
|
8a7fa7e559 | ||
|
|
989a90a0f7 | ||
|
|
a707720577 | ||
|
|
eef270de26 | ||
|
|
0ac765bc32 | ||
|
|
e76551cb64 | ||
|
|
4fd902a0d3 | ||
|
|
9cba449092 | ||
|
|
905b2ecf9e | ||
|
|
53c2c612c9 | ||
|
|
8ffadde9cc | ||
|
|
f569bf633c | ||
|
|
6eb5e07007 | ||
|
|
f07201baa9 | ||
|
|
9120fd01a8 | ||
|
|
c14cc70a6a | ||
|
|
a66f91ae3c | ||
|
|
7e4fddbe62 | ||
|
|
0ff9f8c55a | ||
|
|
6f710faa8e | ||
|
|
2127e1d90b | ||
|
|
9d73aa6b4a | ||
|
|
a2f23b7053 | ||
|
|
5974980aff | ||
|
|
6fa53691aa | ||
|
|
49e0679864 | ||
|
|
0976bc39b3 | ||
|
|
facd6e4e56 | ||
|
|
d2dc0aa1b1 | ||
|
|
aa74e6930b | ||
|
|
b7811bf196 | ||
|
|
18130fd8de | ||
|
|
b6d4f97e96 | ||
|
|
0dd0112518 | ||
|
|
5711e6064e | ||
|
|
517b684f85 | ||
|
|
39465f6d8a | ||
|
|
738813cbac | ||
|
|
c2778e040a | ||
|
|
1efdeeb9dc | ||
|
|
b01dfea343 | ||
|
|
e143293e91 | ||
|
|
f420dc740a | ||
|
|
2e532224b7 | ||
|
|
cb8366a7e5 | ||
|
|
fee54a9d76 | ||
|
|
dc556d3c9b | ||
|
|
4fed3802aa | ||
|
|
f7bb4611e9 | ||
|
|
57791eec62 | ||
|
|
e1e3d8e710 | ||
|
|
654be492a0 | ||
|
|
1df0da4d4a | ||
|
|
1dd6cdb567 | ||
|
|
edfb3cfdc8 | ||
|
|
c6a8f44640 | ||
|
|
b7301d4415 | ||
|
|
4d668702b5 | ||
|
|
f4b0f4d78f | ||
|
|
7100262e09 | ||
|
|
e516943d53 | ||
|
|
fd41226cdd | ||
|
|
47b04eee3e | ||
|
|
24ee6f96e1 | ||
|
|
6e34c21175 | ||
|
|
64789ea8bc | ||
|
|
2ac7176787 | ||
|
|
b968886594 | ||
|
|
2f9c44103b | ||
|
|
e9ed4940cd | ||
|
|
85b5845ebb | ||
|
|
6f4db913aa | ||
|
|
2d20c711a6 | ||
|
|
2e829bc85f | ||
|
|
acddc3cabd | ||
|
|
a16647f16c | ||
|
|
3e3f384218 | ||
|
|
45a98a391a | ||
|
|
c9992201ce | ||
|
|
80062d262e | ||
|
|
652f7a7710 | ||
|
|
7e7e50ada2 | ||
|
|
87eba9bb89 | ||
|
|
da1fcd49fc | ||
|
|
c98289a9e7 | ||
|
|
1d0b65f798 | ||
|
|
3094185cd6 | ||
|
|
aaff04167b | ||
|
|
21bba02e6f | ||
|
|
f60e724167 | ||
|
|
7551e90899 | ||
|
|
715429c13c | ||
|
|
7976189d56 | ||
|
|
47d1eff45d | ||
|
|
073df32b01 | ||
|
|
e8ec7efa5c | ||
|
|
f08cdae2b4 | ||
|
|
1389d29d8c | ||
|
|
d4bfde7906 | ||
|
|
fe73c36958 | ||
|
|
a97f6d90f4 | ||
|
|
0735f164a8 | ||
|
|
9de0c215ba | ||
|
|
56474822f7 | ||
|
|
98b18d89eb | ||
|
|
78ea79b95a | ||
|
|
acbe503c12 | ||
|
|
0261c41574 | ||
|
|
c8182dfab3 | ||
|
|
2eee9c019f | ||
|
|
9ed40b5d9f | ||
|
|
ebc4c71c72 | ||
|
|
dec5a6370e | ||
|
|
519858b708 | ||
|
|
8fcc4cf7fe | ||
|
|
2f78350696 | ||
|
|
85d4037a89 | ||
|
|
d165bf7498 | ||
|
|
94d821a4a9 | ||
|
|
6a854f7a64 | ||
|
|
d76675f4b1 | ||
|
|
4a7e43ccb6 | ||
|
|
b53333d1ba | ||
|
|
f1f3c8bfb9 | ||
|
|
e580e7c672 | ||
|
|
a62fd96269 | ||
|
|
a1d592bb87 | ||
|
|
83f5f6cefc | ||
|
|
9166e70524 | ||
|
|
41f8fa95fe | ||
|
|
b140c21ed9 | ||
|
|
863712cb6e | ||
|
|
43f0aa7f06 | ||
|
|
12535d8142 | ||
|
|
8cf6a8bc98 | ||
|
|
7073dd26de | ||
|
|
66bfd2fad8 | ||
|
|
bb444f306a | ||
|
|
626fba4a8f | ||
|
|
9dc96bda58 | ||
|
|
1a2f1ed620 | ||
|
|
50255dd896 | ||
|
|
c8b9fda258 | ||
|
|
bb41732e59 | ||
|
|
7d9b984747 | ||
|
|
7404e5fce2 | ||
|
|
dc5414bd0d | ||
|
|
d147d36867 | ||
|
|
7763ec3c5c | ||
|
|
c61083ccd9 | ||
|
|
841aeccd6b | ||
|
|
bfcd373eec | ||
|
|
202ef41894 | ||
|
|
f112edc3c3 | ||
|
|
05fd7b18b9 | ||
|
|
9764174070 | ||
|
|
6b0370f0bc | ||
|
|
2e4313e7c3 | ||
|
|
2684d1874a | ||
|
|
882539a700 | ||
|
|
42960b7bf1 | ||
|
|
333f978186 | ||
|
|
e7e9df3261 | ||
|
|
24dbc96fe5 | ||
|
|
abc391f32a | ||
|
|
a589e8e3de | ||
|
|
39fa9a91b8 | ||
|
|
e10d98fb79 | ||
|
|
d17cdbda93 | ||
|
|
2252abefbf | ||
|
|
ce60a146dc | ||
|
|
cd4a6f3e17 | ||
|
|
fc5e1ed5d8 | ||
|
|
d70b98075e | ||
|
|
a4d9952061 | ||
|
|
45e4dfe09c | ||
|
|
34ba9063ad | ||
|
|
d5e2d59fe9 | ||
|
|
d39bdb28d6 | ||
|
|
8bf5622d21 | ||
|
|
9cafd5e099 | ||
|
|
d1b3bfd700 | ||
|
|
e60caf06f9 | ||
|
|
a243c6bb90 | ||
|
|
10523a7e27 | ||
|
|
ea06ec0133 | ||
|
|
e287ebcded | ||
|
|
2ee90289a1 | ||
|
|
193df861d1 | ||
|
|
9055496a19 | ||
|
|
5424576f73 | ||
|
|
a81c6a1c5e | ||
|
|
53a572a4a4 | ||
|
|
df283d80c6 | ||
|
|
b6d203b6dd | ||
|
|
ef988cc8af | ||
|
|
ba46905243 | ||
|
|
df0c4c8208 | ||
|
|
e5aaa1fc53 | ||
|
|
8b6df34d8b | ||
|
|
7a719aa886 | ||
|
|
9af58af5e4 | ||
|
|
b945c8c2dc | ||
|
|
2165f56aee | ||
|
|
e42de9d155 | ||
|
|
7fd95b9de2 | ||
|
|
a42c85802e | ||
|
|
cb3730100d | ||
|
|
7a04d2ad2c | ||
|
|
1f93976919 | ||
|
|
0b7505f109 | ||
|
|
392ca2d8b7 | ||
|
|
68daa90b7e | ||
|
|
fb39ad896e | ||
|
|
cb199054e3 | ||
|
|
7cf77211b0 | ||
|
|
a3d1e9a117 | ||
|
|
755c7c9895 | ||
|
|
45b5136233 | ||
|
|
f16b5cd337 | ||
|
|
21cda8141d | ||
|
|
90f26dfa45 | ||
|
|
ba507d8046 | ||
|
|
5639bcc0f5 | ||
|
|
4cd030582d | ||
|
|
7908076b39 | ||
|
|
ca91f9b702 | ||
|
|
0e86af1c9b | ||
|
|
c5ad84e2ee | ||
|
|
bfb5d6e8fe | ||
|
|
d63b0b5978 | ||
|
|
5e6b60bae0 | ||
|
|
65b6885c61 | ||
|
|
e8ceaa4085 | ||
|
|
db2b120b68 | ||
|
|
5f315d0093 | ||
|
|
016b212ff7 | ||
|
|
bccffe09e2 | ||
|
|
a0365c8e1c | ||
|
|
38156c9f7f | ||
|
|
640db09a64 | ||
|
|
5baa8c680a | ||
|
|
e1c4a488ed | ||
|
|
332d4433c4 | ||
|
|
f5d535b1ef | ||
|
|
35e226273c | ||
|
|
90e25e60d6 | ||
|
|
80659160d3 | ||
|
|
5de29928cd | ||
|
|
c65cea0f66 | ||
|
|
b3cfdbb835 | ||
|
|
9c15cceee7 | ||
|
|
8a0142bd0f | ||
|
|
3d6e05d8c0 | ||
|
|
ddf0e6a935 | ||
|
|
51852f044e | ||
|
|
17cad6b3ca | ||
|
|
8912869077 | ||
|
|
a2697634ba | ||
|
|
1bfb798107 | ||
|
|
cf60568f53 | ||
|
|
ddd18fdaf6 | ||
|
|
b04bb30930 | ||
|
|
e41aaf980b | ||
|
|
46df6601a1 | ||
|
|
5eb8b97469 | ||
|
|
8cd0536fb1 | ||
|
|
99a6789720 | ||
|
|
92670cc051 | ||
|
|
89cbbfc291 | ||
|
|
31869a7da2 | ||
|
|
68c6d6c932 | ||
|
|
e4e140fa53 | ||
|
|
b9709d0825 | ||
|
|
324e3dffa0 | ||
|
|
dd9de3fcb4 | ||
|
|
6710d0561a | ||
|
|
a30f97db0d | ||
|
|
cae666b834 | ||
|
|
5970ca49e9 | ||
|
|
6ae6218043 | ||
|
|
8e3e19a7f7 | ||
|
|
e3d51fd7f7 | ||
|
|
95f5c1d104 | ||
|
|
02a14069a3 | ||
|
|
394f28339e | ||
|
|
411998d4e3 | ||
|
|
371906445e | ||
|
|
34d91e58c0 | ||
|
|
f3f25bec7a | ||
|
|
08d74bea75 | ||
|
|
63f917b317 | ||
|
|
448633ec3c | ||
|
|
09ce3c9ce4 | ||
|
|
8fcc98102b | ||
|
|
e14075b464 | ||
|
|
ca89d14ec8 | ||
|
|
3f51998065 | ||
|
|
6e5b6a1a5c | ||
|
|
660f6c5bbf | ||
|
|
5034bd72ed | ||
|
|
5b7c144796 | ||
|
|
e45b6b9b85 | ||
|
|
245f242fb8 | ||
|
|
a6fccf93d1 | ||
|
|
3dcab90752 | ||
|
|
30a5f405f5 | ||
|
|
bad1c9628e | ||
|
|
1ab7bca43f | ||
|
|
7d0a53773c | ||
|
|
789236ee8f | ||
|
|
21cba0b080 | ||
|
|
31eb0891e5 | ||
|
|
a13f8e7937 | ||
|
|
5bfe56472f | ||
|
|
16a9ed682e | ||
|
|
59cae610d2 | ||
|
|
29ac7893d7 | ||
|
|
c8aeca8ace | ||
|
|
3da505c4c6 | ||
|
|
06bc05b708 | ||
|
|
a3dc9593c2 | ||
|
|
18a271a0ec | ||
|
|
cebd3bf751 | ||
|
|
cb04925c6e | ||
|
|
b7a340bd00 | ||
|
|
2a0f837120 | ||
|
|
d00bdff495 | ||
|
|
7e9c977b3d | ||
|
|
1629151528 | ||
|
|
24295197bd | ||
|
|
807dd84daa | ||
|
|
17848c097c | ||
|
|
563b760d31 | ||
|
|
8465a27a97 | ||
|
|
255cbd1e7e | ||
|
|
7717749b72 | ||
|
|
9e4337babf | ||
|
|
6771877a84 | ||
|
|
a860bd8dd5 | ||
|
|
704ce4ff0c | ||
|
|
21edcc8113 | ||
|
|
125738f13d | ||
|
|
02c781b8f4 | ||
|
|
453fe6aea0 | ||
|
|
e0fb7dfd14 | ||
|
|
c4012225c2 | ||
|
|
1750243537 | ||
|
|
84fc5f3008 | ||
|
|
8a224fbae4 | ||
|
|
e6c4332445 | ||
|
|
0928d1fab2 | ||
|
|
40dd588bef | ||
|
|
472e8ee92e | ||
|
|
c55af7fff3 | ||
|
|
7a39daf179 | ||
|
|
8d087ba5b1 | ||
|
|
c7a30d8728 | ||
|
|
1f678cafba | ||
|
|
579e0bc464 | ||
|
|
7f36dc5aad | ||
|
|
a7c2026128 | ||
|
|
7b0d67ebbe | ||
|
|
63f0910eac | ||
|
|
b16cf9fcc5 | ||
|
|
5501d7bc6b | ||
|
|
413ef42a34 | ||
|
|
a1694ab951 | ||
|
|
fbbbd5e0b8 | ||
|
|
f8c7e2ee41 | ||
|
|
2a4af0bc2c | ||
|
|
d24db13e9d | ||
|
|
bc68aad67b | ||
|
|
9e8a4cab7b | ||
|
|
3679964cf8 | ||
|
|
225ec1a87f | ||
|
|
071c76c06a | ||
|
|
d77a255721 | ||
|
|
26ee1aee25 | ||
|
|
b047395938 | ||
|
|
483a601867 | ||
|
|
c4bc2f6adf | ||
|
|
439e8fe33f | ||
|
|
99885f5b4b | ||
|
|
78b522ec1d | ||
|
|
dbfa5a9358 | ||
|
|
3910b3862a | ||
|
|
3b8e3b7d5c | ||
|
|
6129000dde | ||
|
|
65b274d695 | ||
|
|
a13dd0076b | ||
|
|
cc026c995c | ||
|
|
81cfacaa3d | ||
|
|
fabebc05d2 | ||
|
|
07a97bab03 | ||
|
|
69ac810c82 | ||
|
|
3f4609dc6e | ||
|
|
1ddb529d26 | ||
|
|
ab6b88073c | ||
|
|
ad3ed97472 | ||
|
|
07e902fe95 | ||
|
|
eb781ce689 | ||
|
|
555a65c8ea | ||
|
|
58429f4172 | ||
|
|
e2692a0abd | ||
|
|
d218c43edf | ||
|
|
fae9813127 | ||
|
|
adc56d4ee5 | ||
|
|
f982e33fab | ||
|
|
9ab4479fcf | ||
|
|
3e330b7929 | ||
|
|
ecddada150 | ||
|
|
0c828cbb37 | ||
|
|
84a32f8c16 | ||
|
|
4ee36fd405 | ||
|
|
0802609fd8 | ||
|
|
de88176995 | ||
|
|
aca82f400a | ||
|
|
54bb6fe527 | ||
|
|
c1f663cec1 | ||
|
|
aaf59c2d01 | ||
|
|
692942e5be | ||
|
|
f4acd629a1 | ||
|
|
0ff8a44ec3 | ||
|
|
c6d8f6e618 | ||
|
|
1baa23cc43 | ||
|
|
3be696e6bb | ||
|
|
3cfcb01603 | ||
|
|
d39a29759c | ||
|
|
bf4332b9e7 | ||
|
|
feaae7b684 | ||
|
|
be7852dfc0 | ||
|
|
b0804ce048 | ||
|
|
aa7d5fa924 | ||
|
|
b4cab12e9d | ||
|
|
133dda2de0 | ||
|
|
ef5605ae39 | ||
|
|
9b29b2702e | ||
|
|
abafa270e5 | ||
|
|
15852ccf81 | ||
|
|
a427ecd029 | ||
|
|
77228dcb2c | ||
|
|
3948cfe495 | ||
|
|
36218b54f8 | ||
|
|
be2f4433db | ||
|
|
dd5d36cf07 | ||
|
|
685136d98f | ||
|
|
852937421e | ||
|
|
a5389b7be5 | ||
|
|
1b96b18bc0 | ||
|
|
e9edad6f8c | ||
|
|
f8a6012c3a | ||
|
|
e103f6c9b6 | ||
|
|
9daf7b5a36 | ||
|
|
d6142e1af0 | ||
|
|
b0491210ff | ||
|
|
91d8550290 | ||
|
|
dfaaa7d72a | ||
|
|
5174a4f58a | ||
|
|
b9c73df488 | ||
|
|
f99f1f6441 | ||
|
|
1ec1f5d3be | ||
|
|
713db06a05 | ||
|
|
a0adcbd90b | ||
|
|
b94776421c | ||
|
|
79c1043063 | ||
|
|
cfabae17c0 | ||
|
|
71a1d25e90 | ||
|
|
c2d7b835fb | ||
|
|
f72888ce6f | ||
|
|
30f474d253 | ||
|
|
216f274a92 | ||
|
|
5ea79efe4b | ||
|
|
197c1ba21b | ||
|
|
95c4a24d99 | ||
|
|
f5f23a7c22 | ||
|
|
c469576460 | ||
|
|
0001bb0e4c | ||
|
|
56f184393f | ||
|
|
85777e85fa | ||
|
|
c750ec3cc7 | ||
|
|
c75b781d3f | ||
|
|
8aa42ff1d9 | ||
|
|
63a5bc5e12 | ||
|
|
e0b7b6dc3d | ||
|
|
3d7ecf98c2 | ||
|
|
ad5581c620 | ||
|
|
69af991772 | ||
|
|
2bb2745f91 | ||
|
|
5f682bb45f | ||
|
|
48bcb6b9a6 | ||
|
|
092c0a7392 | ||
|
|
4e8618c512 | ||
|
|
c51a69c0dc | ||
|
|
2480849f39 | ||
|
|
6bfe40607d | ||
|
|
a1c350a537 | ||
|
|
f732b1978e | ||
|
|
d3a542bac4 | ||
|
|
b12724db02 | ||
|
|
f8f5e8e869 | ||
|
|
0733f06103 | ||
|
|
6fdaa6ee8a | ||
|
|
f10b547a49 | ||
|
|
38396b2691 | ||
|
|
220f50ebe4 | ||
|
|
c989133d27 | ||
|
|
6c339551eb | ||
|
|
ef23a80b16 | ||
|
|
a521093731 | ||
|
|
747d3eedf1 | ||
|
|
fd563726f0 | ||
|
|
62136a1a96 | ||
|
|
cf16c31d38 | ||
|
|
0f5d4aa72f | ||
|
|
d8dbe77427 | ||
|
|
0e5670d50d | ||
|
|
22924a1e92 | ||
|
|
e3a7b54f90 | ||
|
|
81d5e7f014 | ||
|
|
9790c218dc | ||
|
|
6d3d51553b | ||
|
|
474591c6a5 | ||
|
|
5de0dc2713 | ||
|
|
57a34a7abb | ||
|
|
54517bda1b | ||
|
|
4291e15b60 | ||
|
|
940ae6406b | ||
|
|
6a0cf4bfd3 | ||
|
|
fee6f75223 | ||
|
|
8c93b2d54c | ||
|
|
dbbb1bd8a1 | ||
|
|
5c838b2fc5 | ||
|
|
3a094d4ba8 | ||
|
|
5d11c1ea6a | ||
|
|
a7e2e15eac | ||
|
|
585a0ca1f4 | ||
|
|
09ed82db65 | ||
|
|
a56cb0e88a | ||
|
|
9acf5ff0ef | ||
|
|
64a7790aed | ||
|
|
9a5134ffcf | ||
|
|
1d2c4d0843 | ||
|
|
efad34be62 | ||
|
|
52761ade87 | ||
|
|
e6b816e776 | ||
|
|
6d3bcba5d0 | ||
|
|
2e7ffa606b | ||
|
|
055cae90b8 | ||
|
|
23acf92803 | ||
|
|
91ee7d76eb | ||
|
|
570888e2b8 | ||
|
|
282f27f0c8 | ||
|
|
98376ade45 | ||
|
|
9682968b55 | ||
|
|
877cb5f1b0 | ||
|
|
6972573a6e | ||
|
|
742f1b1283 | ||
|
|
0b127a2f3b | ||
|
|
a6057835a8 | ||
|
|
c32794477e | ||
|
|
999ac60bda | ||
|
|
bca4b832a8 | ||
|
|
2fc1dda232 | ||
|
|
99495cff90 | ||
|
|
795962a4ba | ||
|
|
7bde9f447a | ||
|
|
b45c3e3c7f | ||
|
|
ce2a01fc59 | ||
|
|
edeea65c3e | ||
|
|
6e8ee94b91 | ||
|
|
97d8274911 | ||
|
|
73d84dc13f | ||
|
|
f32501d498 | ||
|
|
9c289c3fe9 | ||
|
|
2a610584ee | ||
|
|
8e06421567 | ||
|
|
57dc3725f5 | ||
|
|
d6ef8945f5 | ||
|
|
e078a8b7b6 | ||
|
|
a6395b9025 | ||
|
|
6c0983d77c | ||
|
|
4ae39f2bd5 | ||
|
|
71fc701809 | ||
|
|
d418921e4b | ||
|
|
4b83b022d3 | ||
|
|
8b13e5b47e | ||
|
|
0bfd565bda | ||
|
|
2224a558ee | ||
|
|
93c02fb43a | ||
|
|
93ee51cabb | ||
|
|
cd6ba554a4 | ||
|
|
89aa2be358 | ||
|
|
da261246b9 | ||
|
|
4d3e09f4a0 | ||
|
|
9add6b3696 | ||
|
|
ef037e18d0 | ||
|
|
36db791516 | ||
|
|
25d2ea5e94 | ||
|
|
34f2d18f27 | ||
|
|
4935c846aa | ||
|
|
b1f980c216 | ||
|
|
738f64c903 | ||
|
|
7235663447 | ||
|
|
9d78c416f7 | ||
|
|
9a64c026ea | ||
|
|
be2af1d17a | ||
|
|
b21131a466 | ||
|
|
41c5126472 | ||
|
|
b96cb1abaf | ||
|
|
20143606e2 | ||
|
|
1d79bcc657 | ||
|
|
e8b4494fc3 | ||
|
|
2518208c5c | ||
|
|
918fea7d4b | ||
|
|
4dc07bf63f | ||
|
|
04a0aca221 | ||
|
|
1942e72474 | ||
|
|
f256407789 | ||
|
|
67612f607e | ||
|
|
7274c42126 | ||
|
|
e288402e88 | ||
|
|
76bd4a1c7b | ||
|
|
13dbad6f35 | ||
|
|
cf089fdfe9 | ||
|
|
9755e31fa2 | ||
|
|
3fcf31d009 | ||
|
|
1cbdd0e675 | ||
|
|
e0fd84d6f5 | ||
|
|
443720c3de | ||
|
|
254526a213 | ||
|
|
ad0afb5fc9 | ||
|
|
e64b15d2a7 | ||
|
|
cdc3352318 | ||
|
|
2416618b34 | ||
|
|
c951d77a52 | ||
|
|
759ddc0b4b | ||
|
|
a0709bec51 | ||
|
|
f5bc4e7a24 | ||
|
|
b4665b6e5a | ||
|
|
b85d5b8822 | ||
|
|
12ad75a6b0 | ||
|
|
af92fe6ad1 | ||
|
|
fcf7ed4faa | ||
|
|
a58649fd10 | ||
|
|
ee5918cf4e | ||
|
|
d7de3301ed | ||
|
|
bd1988c775 | ||
|
|
474010b31d | ||
|
|
dd941154d6 | ||
|
|
cbb53745f6 | ||
|
|
3dff62e0cf | ||
|
|
719024ec01 | ||
|
|
f0659b4f3c | ||
|
|
a649f2736f | ||
|
|
046ccb1dee | ||
|
|
d340556c28 | ||
|
|
32250db126 | ||
|
|
cb8c30db2e | ||
|
|
dacd5a9e3d | ||
|
|
859d8b96a7 | ||
|
|
02981e3cee | ||
|
|
49892b3ce8 | ||
|
|
b223e7ec36 | ||
|
|
0926337c1d | ||
|
|
346e72a678 | ||
|
|
d612e47b62 | ||
|
|
c138cbf994 | ||
|
|
dfe275ab67 | ||
|
|
d6cfbdb273 | ||
|
|
8a27fa7bb5 | ||
|
|
b9a6fb1fe5 | ||
|
|
4b6194e15e | ||
|
|
e1b3f90f8b | ||
|
|
c5cc0b8c59 | ||
|
|
5c849ec666 | ||
|
|
c2a30cbe19 | ||
|
|
31eddd8da0 | ||
|
|
abf2a14ee6 | ||
|
|
e555874485 | ||
|
|
a7aef8f147 | ||
|
|
5b8c11f1a9 | ||
|
|
645040c943 | ||
|
|
fcc5ea348c | ||
|
|
3fbb45448f | ||
|
|
8a83289709 | ||
|
|
08175de069 | ||
|
|
808933cd70 | ||
|
|
384dd2adab | ||
|
|
4a37952b41 | ||
|
|
1046d264d8 | ||
|
|
bb235ab1ae | ||
|
|
ef88b83727 | ||
|
|
5612e9864e | ||
|
|
8302187dee | ||
|
|
77d07c7c4d | ||
|
|
b2265080fd | ||
|
|
0d70bd9ac0 | ||
|
|
8f63d59b81 | ||
|
|
0395c0cc95 | ||
|
|
72065412a3 | ||
|
|
7af2cdd0ef | ||
|
|
67c801c4e7 | ||
|
|
f7c8ad4c5a | ||
|
|
61092b34de | ||
|
|
1e9e96fb8c | ||
|
|
17d1ced07c | ||
|
|
a26ced04ff | ||
|
|
154b5b6ded | ||
|
|
8219dbef6f | ||
|
|
3da724e76a | ||
|
|
1616d237ae | ||
|
|
3d21facc9f | ||
|
|
fead2a08a3 | ||
|
|
d8acf24af0 | ||
|
|
97412b3c79 | ||
|
|
6145913068 | ||
|
|
e56bb6785f | ||
|
|
6d63d6c54a | ||
|
|
6e8a103e70 | ||
|
|
00d3530f64 | ||
|
|
1e6ca18193 | ||
|
|
5a32992497 | ||
|
|
79ba00a2f4 | ||
|
|
fdfaf8ec70 | ||
|
|
162c58b697 | ||
|
|
75959ca692 | ||
|
|
06fbcd19c8 | ||
|
|
36e0468a99 | ||
|
|
c4d85cf928 | ||
|
|
daf8495acb | ||
|
|
9999ded933 | ||
|
|
80128e8293 | ||
|
|
9579b2c85e | ||
|
|
eb7f184848 | ||
|
|
71b21920e2 | ||
|
|
91b787a5be | ||
|
|
11f659aadb | ||
|
|
0870dd15a8 | ||
|
|
7cd52041d8 | ||
|
|
f2bcb49478 | ||
|
|
60a4bda94f | ||
|
|
54cfc46500 | ||
|
|
c09559ade5 | ||
|
|
3e9aea1f7a | ||
|
|
f0c292f508 | ||
|
|
1444ba2c35 | ||
|
|
fac4c85133 | ||
|
|
97a09e0571 | ||
|
|
c7121516c1 | ||
|
|
56a5fbf63b | ||
|
|
80604a752b | ||
|
|
3fe6e18609 | ||
|
|
47f3f30d83 | ||
|
|
085a904970 | ||
|
|
0fa7723f51 | ||
|
|
84ea483b4c | ||
|
|
4de3aa96b6 | ||
|
|
1193943228 | ||
|
|
2959faca0c | ||
|
|
d0df4fa3a0 | ||
|
|
0a301dd2f9 | ||
|
|
43e3679297 | ||
|
|
0455063854 | ||
|
|
12d90d77c7 | ||
|
|
61d7749aaf | ||
|
|
69fb4a0c37 | ||
|
|
d2c9f058f8 | ||
|
|
acaa3afa0f | ||
|
|
3c58e21cbf | ||
|
|
9d3c5b9bc6 | ||
|
|
ce34358d46 | ||
|
|
278bcec238 | ||
|
|
128f458848 | ||
|
|
7ae2bcd6db | ||
|
|
130f252f97 | ||
|
|
d339e05a3b | ||
|
|
933f4813aa | ||
|
|
f14e71c1d6 | ||
|
|
d78dbd7b0f | ||
|
|
71a6ef6eba | ||
|
|
4873862d16 | ||
|
|
425efe3d72 | ||
|
|
666dc13c01 | ||
|
|
267f9f5759 | ||
|
|
c8d8c00f84 | ||
|
|
dcc567ebda | ||
|
|
a199a3ccc3 | ||
|
|
8159e07427 | ||
|
|
32d70758c5 | ||
|
|
3a9e284f29 | ||
|
|
c658daeac6 | ||
|
|
f605169b15 | ||
|
|
f82b495bcc | ||
|
|
33b44a87c5 | ||
|
|
a88d994fcb | ||
|
|
dd9224d9a6 | ||
|
|
48728efee0 | ||
|
|
043c55ae66 | ||
|
|
fa3f822cf1 | ||
|
|
5afab126b3 | ||
|
|
bee8378795 | ||
|
|
cbd42f4a58 | ||
|
|
3333fd7fc8 | ||
|
|
142258b4b5 | ||
|
|
01213b58ba | ||
|
|
51df7032bb | ||
|
|
c033ce5ecf | ||
|
|
bfaf57cfd0 | ||
|
|
6c523570e5 | ||
|
|
047e5bdd81 | ||
|
|
6b1559d397 | ||
|
|
a3167b8152 | ||
|
|
cbeb23f16a | ||
|
|
b4a6512784 | ||
|
|
1af2742013 | ||
|
|
8048938158 | ||
|
|
3689c7f982 | ||
|
|
96c7057fee | ||
|
|
df7c4c82a9 | ||
|
|
18d8db338b | ||
|
|
9637639e31 | ||
|
|
17abe8f164 | ||
|
|
badaaece04 | ||
|
|
56119eebae | ||
|
|
c17c8e2a2c | ||
|
|
e45a60f2ed | ||
|
|
ebe9356937 | ||
|
|
0094305299 | ||
|
|
b27050d3bf | ||
|
|
d941e596ad | ||
|
|
ddaec92da5 | ||
|
|
1c0a4c92a8 | ||
|
|
383c00afbd | ||
|
|
d3b2bff8ad | ||
|
|
77aaa6141d | ||
|
|
2bb1af61d0 | ||
|
|
40b29472b3 | ||
|
|
5d5029de59 | ||
|
|
cc96e6a222 | ||
|
|
581c9438ca | ||
|
|
54bd427f26 | ||
|
|
c55d184195 | ||
|
|
b279f94789 | ||
|
|
f3a897e99a | ||
|
|
392b1b5904 | ||
|
|
4daa8066a6 | ||
|
|
8a89951286 | ||
|
|
c41ed55291 | ||
|
|
caba0499d7 | ||
|
|
13deacfc3b | ||
|
|
7231085456 | ||
|
|
663bb4aa10 | ||
|
|
18721b4542 | ||
|
|
19232722e2 | ||
|
|
d2eca68484 | ||
|
|
091167478e | ||
|
|
547b8abc56 | ||
|
|
dbfac45d59 | ||
|
|
086f6de867 | ||
|
|
0f93091848 | ||
|
|
ac19f62ed9 | ||
|
|
8fcef63890 | ||
|
|
34cd2c1afa | ||
|
|
5d1a19d49c | ||
|
|
2fbb320539 | ||
|
|
b50e5ed7e6 | ||
|
|
26c82ca72c | ||
|
|
f8a8a431a8 | ||
|
|
fd27867195 | ||
|
|
5e2a627795 | ||
|
|
e081401c2d | ||
|
|
e62a268aac | ||
|
|
140a9e5191 | ||
|
|
51b5f8046f | ||
|
|
91b965d1aa | ||
|
|
f8faccee59 | ||
|
|
6ac46a36a7 | ||
|
|
27572ba4bc | ||
|
|
21be832161 | ||
|
|
5769632793 | ||
|
|
9efd959ed8 | ||
|
|
5c72afe17e | ||
|
|
dc24319333 | ||
|
|
1acbdf94aa | ||
|
|
44229dfe51 | ||
|
|
13206227d9 | ||
|
|
e3e8a57df6 | ||
|
|
7afff80984 | ||
|
|
500522418e | ||
|
|
5ccac5a5c3 | ||
|
|
69822f4401 | ||
|
|
f290cfbd35 | ||
|
|
fae30f59ce | ||
|
|
a15f303c0a | ||
|
|
5e85038d78 | ||
|
|
4e7b35c54e | ||
|
|
48dde310ae | ||
|
|
6b041e7657 | ||
|
|
69576ee1e1 | ||
|
|
bd3105f62e | ||
|
|
02e3f0e71b | ||
|
|
74bf981f3d | ||
|
|
32f0678692 | ||
|
|
e74753bec1 | ||
|
|
4a8c9ad17f | ||
|
|
0c6c882f50 | ||
|
|
579f25c1ba | ||
|
|
d5eb5c8816 | ||
|
|
26d04b5f9c | ||
|
|
a0b9684504 | ||
|
|
402b6d1f0c | ||
|
|
e87a5fa354 | ||
|
|
63574f643e | ||
|
|
1d6b1020c3 | ||
|
|
96337a3230 | ||
|
|
950c59d8b4 | ||
|
|
9cfd095281 | ||
|
|
996e049863 | ||
|
|
6e42817c3e | ||
|
|
7758344a57 | ||
|
|
b5df566b6b | ||
|
|
e21fd19a70 | ||
|
|
98672c8e44 | ||
|
|
2e92f0640b | ||
|
|
d6b314f573 | ||
|
|
d7a150ffa8 | ||
|
|
955da6be30 | ||
|
|
b5f8e6738a | ||
|
|
d7fa8c9795 | ||
|
|
acb7a5b3c8 | ||
|
|
783a57ef89 | ||
|
|
d33ccce683 | ||
|
|
d722bf10b9 | ||
|
|
41337475a2 | ||
|
|
f504a069d7 | ||
|
|
96f79688dd | ||
|
|
c330898aeb | ||
|
|
94952cf0a4 | ||
|
|
a3c249abe9 | ||
|
|
c26562fab8 | ||
|
|
b7cbd99212 | ||
|
|
b53ef35a85 | ||
|
|
1ebb5fefb1 | ||
|
|
164b3edd41 | ||
|
|
819ea36cca | ||
|
|
088c9b9e4e | ||
|
|
ab25a9b8ee | ||
|
|
b2aea9f8f0 | ||
|
|
5399b95936 | ||
|
|
1d5c120748 | ||
|
|
6be7301e69 | ||
|
|
fff9f2b17d | ||
|
|
87a7d2e45c | ||
|
|
58803ef40d | ||
|
|
b27ca993aa | ||
|
|
28ecc6bcbf | ||
|
|
30c5e3f237 | ||
|
|
6a25a3a3d0 | ||
|
|
9a84c0ae30 | ||
|
|
133cc5457d | ||
|
|
05e4c5b507 | ||
|
|
8372717a09 | ||
|
|
5f3f2a96fd | ||
|
|
b83c6f0c28 | ||
|
|
36dc0f3e3c | ||
|
|
b4edde6f90 | ||
|
|
395eb5581c | ||
|
|
6dab251055 | ||
|
|
3bd6a6a614 | ||
|
|
93d21a0d1d | ||
|
|
2c2a73e320 | ||
|
|
a88a3c9ea3 | ||
|
|
e4df70cd56 | ||
|
|
90f7d35150 | ||
|
|
75b19a60c1 | ||
|
|
9fc921f49f | ||
|
|
4b9e90ac72 | ||
|
|
36dd6c83e0 | ||
|
|
d790313221 | ||
|
|
c3895f06c0 | ||
|
|
76e81fa651 | ||
|
|
d63a1b0eb0 | ||
|
|
4dffac3c78 | ||
|
|
88391b0274 | ||
|
|
c3cea0238e | ||
|
|
5bf4266973 | ||
|
|
ed57c54d06 | ||
|
|
01936e000e | ||
|
|
80137848dc | ||
|
|
149575e0c4 | ||
|
|
6a66b462ce | ||
|
|
1e31f6fe77 | ||
|
|
f173b3ff09 | ||
|
|
baffab62ce | ||
|
|
d141cec2a8 | ||
|
|
df87ac3810 | ||
|
|
46960ce0a4 | ||
|
|
522206b0bc | ||
|
|
24d60dd3ab | ||
|
|
dbfdc2fd14 | ||
|
|
a97912e297 | ||
|
|
cedba501ee | ||
|
|
ac947d3bda | ||
|
|
243ba371b3 | ||
|
|
3af24eafbb | ||
|
|
69b6022afe | ||
|
|
1cd66419d4 | ||
|
|
5309a02f6d | ||
|
|
541784382a | ||
|
|
2a7fae2c82 | ||
|
|
ed3a5448b1 | ||
|
|
2eb4769bbc | ||
|
|
02019000df | ||
|
|
6262f51c96 | ||
|
|
d38ff5759a | ||
|
|
f6de2e4dd3 | ||
|
|
3959f447d4 | ||
|
|
14a6a69dc0 | ||
|
|
3191182009 | ||
|
|
e210dc748f | ||
|
|
724dcb557f | ||
|
|
11d274ff07 | ||
|
|
ae246b1005 | ||
|
|
ca0250280a | ||
|
|
f49d6e15a4 | ||
|
|
7172cfd889 | ||
|
|
89e9652075 | ||
|
|
53098edc85 | ||
|
|
e9968fbab3 | ||
|
|
bc10fd0aba | ||
|
|
76b4922bbe | ||
|
|
8e790c4745 | ||
|
|
c331b180cb | ||
|
|
30e3b02867 | ||
|
|
bedea4a093 | ||
|
|
5372cfba20 | ||
|
|
19dac306d1 | ||
|
|
e9a98a3402 | ||
|
|
7020749987 | ||
|
|
2d62d090df | ||
|
|
e6f59062fa | ||
|
|
0d3092d62c | ||
|
|
af0921c5b2 | ||
|
|
137454beff | ||
|
|
a0cc93b813 | ||
|
|
ff516f63b4 | ||
|
|
68acb78ae6 | ||
|
|
949aaaff2a | ||
|
|
390544cd9a | ||
|
|
766a923f86 | ||
|
|
202ce14afa | ||
|
|
8ff9d8123d | ||
|
|
2715367411 | ||
|
|
44b61d9252 | ||
|
|
565bba6e55 | ||
|
|
2ecfb7af64 | ||
|
|
998d3b2cc1 | ||
|
|
c923d643b9 | ||
|
|
bc39c721e3 | ||
|
|
5096ff6496 | ||
|
|
a11f623b17 | ||
|
|
f7ffb30bc6 | ||
|
|
c7ddb54e67 | ||
|
|
c4108feb15 | ||
|
|
47cad71fcd | ||
|
|
0b56b1685a | ||
|
|
c414e1a168 | ||
|
|
22bebb6bbc | ||
|
|
c3f9742bb6 | ||
|
|
3defccb6e2 | ||
|
|
1197b17509 | ||
|
|
265e28e7c3 | ||
|
|
9c758f1628 | ||
|
|
5f4975ca5c | ||
|
|
e5bcc793e5 | ||
|
|
cd5b0e74e3 | ||
|
|
3162d2c17d | ||
|
|
6a54fb649c | ||
|
|
8031ed7fe1 | ||
|
|
dcbf5affda | ||
|
|
027c8dcd41 | ||
|
|
94b46a2173 | ||
|
|
b66f7f6c8b | ||
|
|
355e4a5c67 | ||
|
|
1e7cb7f13e | ||
|
|
d58837421f | ||
|
|
19d5358b97 | ||
|
|
a6551c320d | ||
|
|
59a73817c8 | ||
|
|
1231f3eeb1 | ||
|
|
cd40c1dacd | ||
|
|
33286a5bde | ||
|
|
b61c35aff9 | ||
|
|
2a3e1ba62e | ||
|
|
d95f3277b2 | ||
|
|
e0a2c8cb8a | ||
|
|
0aa46a57fe | ||
|
|
53e3cd143e | ||
|
|
8381039f97 | ||
|
|
a91c61e5e2 | ||
|
|
f90efd7e38 | ||
|
|
fab142dd38 | ||
|
|
125d546cea | ||
|
|
179f7ecc23 | ||
|
|
0fc30a70ca | ||
|
|
f81509a892 | ||
|
|
fc4488f17f | ||
|
|
2748f3d22e | ||
|
|
e0d42cbe9c | ||
|
|
1780d9417f | ||
|
|
5f182b1e62 | ||
|
|
80cee8367f | ||
|
|
7325d5657d | ||
|
|
72182c7caa | ||
|
|
d7ca3801be | ||
|
|
2ef714f87b | ||
|
|
46940eb15a | ||
|
|
3485983553 | ||
|
|
861ebc151a | ||
|
|
ae5df938b6 | ||
|
|
e224a7f0c7 | ||
|
|
aa4684077d | ||
|
|
193e196c46 | ||
|
|
6e7a42065c | ||
|
|
2c987d2912 | ||
|
|
19f5dc12fb | ||
|
|
38b59aa1a9 | ||
|
|
201b59cd4c | ||
|
|
de3afe27c1 | ||
|
|
7bee8e73f0 | ||
|
|
17e0fbbaf8 | ||
|
|
ec486e89e4 | ||
|
|
ec3b000264 | ||
|
|
90a513dc3d | ||
|
|
32a74e3416 | ||
|
|
1d90bf9230 | ||
|
|
81943b12c0 | ||
|
|
18fa7c2832 | ||
|
|
dd02bf809e | ||
|
|
2f3a1a5d79 | ||
|
|
18654cb011 | ||
|
|
5954f80645 | ||
|
|
3e36357315 | ||
|
|
a06a454ade | ||
|
|
c0fab4b41f | ||
|
|
80b79f3636 | ||
|
|
b97ae25f93 | ||
|
|
4722ea6096 | ||
|
|
3e9781033a | ||
|
|
62b296cf85 | ||
|
|
dcbaa52683 | ||
|
|
1e29f61656 | ||
|
|
89319dc7aa | ||
|
|
5755afc4ea | ||
|
|
d00c434ccc | ||
|
|
4ada59ba48 | ||
|
|
14ccc38f4c | ||
|
|
a8be299bbd | ||
|
|
2e87f5ff85 | ||
|
|
829ebba622 | ||
|
|
81019fa2a8 | ||
|
|
1bc3675548 | ||
|
|
678ec979c9 | ||
|
|
d1a48ae641 | ||
|
|
eded86bc10 | ||
|
|
5a7a49d1d5 | ||
|
|
d3de8049bf | ||
|
|
d09dd90a73 | ||
|
|
78203eb9be | ||
|
|
31c7d4e36f | ||
|
|
d3949806f5 | ||
|
|
28ff15c99e | ||
|
|
bd2c74fe90 | ||
|
|
fb6995db94 | ||
|
|
8c2c4d3282 | ||
|
|
b81392b818 | ||
|
|
ad08440023 | ||
|
|
ec17623400 | ||
|
|
288eca7849 | ||
|
|
b5f202150e | ||
|
|
539bf0cb30 | ||
|
|
9385d62c89 | ||
|
|
897f916552 | ||
|
|
914cf6753e | ||
|
|
392208057e | ||
|
|
8d2cbf806d | ||
|
|
ab5025efbf | ||
|
|
912e51d863 | ||
|
|
d1c4ca5eae | ||
|
|
16bb4a148e | ||
|
|
d7071cb67f | ||
|
|
a13fb3d7c5 | ||
|
|
85558904a4 | ||
|
|
88ed72fdc1 | ||
|
|
586a93d7aa | ||
|
|
a446699c6c | ||
|
|
f1c459fd6c | ||
|
|
752f0a988b | ||
|
|
719b266691 | ||
|
|
dcba5762c7 | ||
|
|
0757717bea | ||
|
|
a9f762aa96 | ||
|
|
db45f46d9e | ||
|
|
f00653964e | ||
|
|
e0154f9314 | ||
|
|
4408a3a354 | ||
|
|
5abf5a157d | ||
|
|
d55d5f60a5 | ||
|
|
feacf2c359 | ||
|
|
72ea1601e9 | ||
|
|
02cfd8d978 | ||
|
|
728377cba3 | ||
|
|
f2283b56b9 | ||
|
|
b515f2a599 | ||
|
|
4354b49496 | ||
|
|
994312c212 | ||
|
|
5f25e0d4e2 | ||
|
|
4bbe571f5f | ||
|
|
c58d21375c | ||
|
|
ed128c558f | ||
|
|
828932d95c | ||
|
|
42ced309d3 | ||
|
|
1fcd306879 | ||
|
|
26c9ad7f06 | ||
|
|
4b7b01ce0a | ||
|
|
6632154906 | ||
|
|
ca67c7ef43 | ||
|
|
c4039a929c | ||
|
|
a1b74e1220 | ||
|
|
01f6188824 | ||
|
|
283f31a734 | ||
|
|
b800ce9ebe | ||
|
|
eb9d54465c | ||
|
|
eb9e1f81d4 | ||
|
|
ddfbd70b55 | ||
|
|
51e9ab83ad | ||
|
|
5ed7e03b2e | ||
|
|
0cb8d5b12f | ||
|
|
b4548c1af2 | ||
|
|
0a396f3112 | ||
|
|
2aa18dc88f | ||
|
|
8d89983eeb | ||
|
|
573c470965 | ||
|
|
daa0c751e2 | ||
|
|
e1babefc80 | ||
|
|
ce99b0e162 | ||
|
|
4c3a23b1bd | ||
|
|
12016af326 | ||
|
|
fc9871b075 | ||
|
|
3cb3c1c6d8 | ||
|
|
8fe3474589 | ||
|
|
dc3c565ec0 | ||
|
|
350c7d7081 | ||
|
|
0428bc8fe3 | ||
|
|
514a8575a2 | ||
|
|
fc11377552 | ||
|
|
fbaa70a2ae | ||
|
|
f30c11ddf4 | ||
|
|
51c4ecc879 | ||
|
|
080421f2fb | ||
|
|
8cc61ca8b8 | ||
|
|
400754c5b3 | ||
|
|
05e82c24f8 | ||
|
|
97c15c7876 | ||
|
|
36e7cc8e89 | ||
|
|
ad2ce4de53 | ||
|
|
2a28f7c4d2 | ||
|
|
0cdbe080a6 | ||
|
|
96fe1a7da1 | ||
|
|
4f73af2920 | ||
|
|
7ca4e49bf5 | ||
|
|
dfa63297fc | ||
|
|
402e8c1897 | ||
|
|
33e814122e | ||
|
|
f28a4df5ee | ||
|
|
417aa2815c | ||
|
|
2fdabe37aa | ||
|
|
d8270dfb6a | ||
|
|
b75f662c41 | ||
|
|
0d9d0845f5 | ||
|
|
5d66a1f298 | ||
|
|
9bf52eb63a | ||
|
|
0c9d72a9a1 | ||
|
|
c2c2bd2df6 | ||
|
|
78fc594d84 | ||
|
|
53b1e7c573 | ||
|
|
c14404a7c1 | ||
|
|
96e6750f9c | ||
|
|
dbafd05a4e | ||
|
|
d7fd578cc3 | ||
|
|
0e8572e1db | ||
|
|
98a997bd8e | ||
|
|
6fbc95cc41 | ||
|
|
1852f74e8b | ||
|
|
55e3e16860 | ||
|
|
7cf2a55b43 | ||
|
|
c41cbbd2da | ||
|
|
7283bb046b | ||
|
|
dd2df6cbcf | ||
|
|
01f1cac811 | ||
|
|
aaa112c3f3 | ||
|
|
dc0fa9c64d | ||
|
|
f749b38aa2 | ||
|
|
43d79673e1 | ||
|
|
fc841f572c | ||
|
|
df9f117a08 | ||
|
|
bedc46a1be | ||
|
|
a7c6095be5 | ||
|
|
f2d5cf785e | ||
|
|
f926ad9b5e | ||
|
|
68acf969f2 | ||
|
|
65d53e800b | ||
|
|
e44d8ac7e9 | ||
|
|
d1823dd6e1 | ||
|
|
80bfd3b8e9 | ||
|
|
07128cc7fe | ||
|
|
0892f0b0bb | ||
|
|
8cf7f3ac08 | ||
|
|
ab2bb51879 | ||
|
|
8cb98b5365 | ||
|
|
782c5f8a34 | ||
|
|
d6b5c5f63e | ||
|
|
ef6dfa6d91 | ||
|
|
3840ee8ee7 | ||
|
|
f47e40fa2a | ||
|
|
49a4a61876 | ||
|
|
669c1a6f0d | ||
|
|
15342190fa | ||
|
|
c98ef6e8a9 | ||
|
|
d36d5936da | ||
|
|
70de75ae3d | ||
|
|
ea62e33e51 | ||
|
|
524e92c81a | ||
|
|
d5a26b1d70 | ||
|
|
cd7649a666 | ||
|
|
847cc0a38a | ||
|
|
3588eb9bf8 | ||
|
|
296e22d3ca | ||
|
|
923e590da1 | ||
|
|
e56329d05a | ||
|
|
2452e80e57 | ||
|
|
b9019831a9 | ||
|
|
b3b49b440e | ||
|
|
cbedf0f74f | ||
|
|
6c980eba68 | ||
|
|
e9f766904e | ||
|
|
62d76c006d | ||
|
|
615b78a612 | ||
|
|
aaf0340d6c | ||
|
|
93ea437760 | ||
|
|
fe6a1244ed | ||
|
|
953858b0e7 | ||
|
|
f3da46833f | ||
|
|
ba2488b6e2 | ||
|
|
4e7e0d67e5 | ||
|
|
faa16b207c | ||
|
|
2d9f62761c | ||
|
|
60689f531d | ||
|
|
3f97692311 | ||
|
|
81221336f4 | ||
|
|
ecfc758ce4 | ||
|
|
2cb3a132c6 | ||
|
|
519473e498 | ||
|
|
9b271d918a | ||
|
|
5b4d874851 | ||
|
|
9dc1dbc002 | ||
|
|
7ee323f0d5 | ||
|
|
4bb6f711b9 | ||
|
|
816cec8f5c | ||
|
|
2394dfe88a | ||
|
|
dfc0d2303b | ||
|
|
598a48ec50 | ||
|
|
9f41e98e3f | ||
|
|
489891ff3c | ||
|
|
a44ed0b047 | ||
|
|
1aaa97b91d | ||
|
|
13d68e789f | ||
|
|
10dae7482c | ||
|
|
979de190dc | ||
|
|
46bcbda972 | ||
|
|
43bdc353fb | ||
|
|
6314676b9c | ||
|
|
9f77f3d968 | ||
|
|
f4733dbbe8 | ||
|
|
61fc0e4893 | ||
|
|
652ad86cad | ||
|
|
60f308bd76 | ||
|
|
b2113ff95b | ||
|
|
b8188a46a9 | ||
|
|
e790b0761e | ||
|
|
fa3317f6cd | ||
|
|
b080f7a81c | ||
|
|
57a1c5eb76 | ||
|
|
5b7e2521cc | ||
|
|
fced653c1c | ||
|
|
9253e6ac5e | ||
|
|
aff2ed22b9 | ||
|
|
5520a52379 | ||
|
|
505050e936 | ||
|
|
0437835bde | ||
|
|
77386f8dfc | ||
|
|
529fdf0341 | ||
|
|
fa74700ad9 | ||
|
|
caca491943 | ||
|
|
4794f44a6d | ||
|
|
f0119b1231 | ||
|
|
4c579aa826 | ||
|
|
b9463b3c0e | ||
|
|
cede6ce8a9 | ||
|
|
956e6e3638 | ||
|
|
b282c7d815 | ||
|
|
5b6142e2d9 | ||
|
|
58313ad8fc | ||
|
|
7aff148e9f | ||
|
|
ee5c61fb74 | ||
|
|
91be6c8028 | ||
|
|
2e0bd2ec28 | ||
|
|
a0151fbc04 | ||
|
|
d853782fea | ||
|
|
c6638a7154 | ||
|
|
4abebe3aec | ||
|
|
5aed8f928a | ||
|
|
afe6cfc383 | ||
|
|
174983ce1e | ||
|
|
1313c7b3bf | ||
|
|
bee622bcad | ||
|
|
8cefdeae49 | ||
|
|
7cf70b9607 | ||
|
|
add4b582b5 | ||
|
|
b419d21940 |
@@ -1,10 +1,12 @@
|
|||||||
---
|
---
|
||||||
name: Bug report
|
name: Bug report
|
||||||
about: Create a report to help us address issues in Moonlight
|
about: Follow the troubleshooting guide before reporting a bug
|
||||||
|
|
||||||
---
|
---
|
||||||
**READ ME FIRST!**
|
**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
|
If you're here because something basic is not working (like gamepad input, video, or similar), it's probably something specific to your setup, so make sure you've gone through the Troubleshooting Guide first: https://github.com/moonlight-stream/moonlight-docs/wiki/Troubleshooting
|
||||||
|
|
||||||
|
If you still have trouble with basic functionality after following the guide, join our Discord server where there are many other volunteers who can help (or direct you back here if it looks like a Moonlight bug after all). https://moonlight-stream.org/discord
|
||||||
|
|
||||||
**Describe the bug**
|
**Describe the bug**
|
||||||
A clear and concise description of what the bug is.
|
A clear and concise description of what the bug is.
|
||||||
@@ -26,14 +28,22 @@ List the games you've tried that exhibit the issue. To see if the issue is game-
|
|||||||
- 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?
|
||||||
|
|
||||||
|
**Gamepad-related issues (please complete if problem is gamepad-related)**
|
||||||
|
- Do you have any gamepads connected to your host PC directly?
|
||||||
|
- Does the problem still remain if you stream the desktop and use https://html5gamepad.com to test your gamepad?
|
||||||
|
- Instructions for streaming the desktop can be found here: https://github.com/moonlight-stream/moonlight-docs/wiki/Setup-Guide
|
||||||
|
|
||||||
**Client PC details (please complete the following information)**
|
**Client PC details (please complete the following information)**
|
||||||
- OS: [e.g. Windows 10 1809]
|
- OS: [e.g. Windows 10 1809]
|
||||||
- Moonlight Version: [e.g. v0.9.0]
|
- Moonlight Version: [e.g. v0.9.0]
|
||||||
|
- GPU: [e.g. Intel HD Graphics 520]
|
||||||
|
- Linux package type (if applicable): [e.g. Flatpak]
|
||||||
|
|
||||||
**Server PC details (please complete the following information)**
|
**Server PC details (please complete the following information)**
|
||||||
- OS: [e.g. Windows 10 1809]
|
- OS: [e.g. Windows 10 1809]
|
||||||
- GeForce Experience version: [e.g. 3.16.0.140]
|
- Sunshine or GeForce Experience version: [e.g. Sunshine v0.21.0]
|
||||||
- Nvidia GPU driver: [e.g. 417.35]
|
- GPU: [e.g. AMD Radeon RX 7900 XT]
|
||||||
|
- GPU driver: [e.g. 24.1.1]
|
||||||
|
|
||||||
**Moonlight Logs (please attach)**
|
**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.
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
blank_issues_enabled: false
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
issuesOpened: >
|
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 />
|
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://moonlight-stream.org/discord) 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 />
|
This issue tracker should only be used for specific bugs or feature requests.<br /><br />
|
||||||
Thank you, and happy streaming!
|
Thank you, and happy streaming!
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# Please see the documentation for all configuration options:
|
||||||
|
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
|
||||||
|
|
||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "gitsubmodule"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "daily"
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
---
|
||||||
|
name: Build - Linux AppImage
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
ci_version:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
env:
|
||||||
|
CI_VERSION: ${{ inputs.ci_version }}
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout Repository
|
||||||
|
uses: actions/checkout@v5
|
||||||
|
with:
|
||||||
|
submodules: 'recursive'
|
||||||
|
fetch-depth: 1
|
||||||
|
|
||||||
|
- name: Setup environment
|
||||||
|
run: |
|
||||||
|
wget -qO - https://packages.lunarg.com/lunarg-signing-key-pub.asc | sudo apt-key add -
|
||||||
|
sudo wget -qO /etc/apt/sources.list.d/lunarg-vulkan-1.4.313-jammy.list https://packages.lunarg.com/vulkan/1.4.313/lunarg-vulkan-1.4.313-jammy.list
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install -y qt6-base-dev qt6-declarative-dev libqt6svg6-dev qt6-wayland qml6-module-qtquick-controls qml6-module-qtquick-templates qml6-module-qtquick-layouts \
|
||||||
|
qml6-module-qtqml-workerscript qml6-module-qtquick-window qml6-module-qtquick python3-pip nasm libgbm-dev libdrm-dev libfreetype-dev libasound2-dev \
|
||||||
|
libdbus-1-dev libegl1-mesa-dev libgl1-mesa-dev libgles2-mesa-dev libglu1-mesa-dev libibus-1.0-dev libpulse-dev libudev-dev libx11-dev libxcursor-dev \
|
||||||
|
libxext-dev libxi-dev libxinerama-dev libxkbcommon-dev libxrandr-dev libxss-dev libxt-dev libxv-dev libxxf86vm-dev libxcb-dri3-dev libx11-xcb-dev \
|
||||||
|
libxfixes-dev libxtst-dev wayland-protocols libopus-dev libvdpau-dev libgl-dev libpipewire-0.3-dev liburing-dev vulkan-sdk cmake
|
||||||
|
sudo pip3 install meson
|
||||||
|
mkdir -p dep_root/{bin,include,lib}
|
||||||
|
echo "DEP_ROOT=$PWD/dep_root" >> "${GITHUB_ENV}"
|
||||||
|
echo "$PWD/dep_root/bin" >> "${GITHUB_PATH}"
|
||||||
|
|
||||||
|
- name: Checkout SDL3
|
||||||
|
uses: actions/checkout@v5
|
||||||
|
with:
|
||||||
|
repository: libsdl-org/SDL
|
||||||
|
ref: release-3.4.8
|
||||||
|
path: deps/SDL
|
||||||
|
|
||||||
|
- name: Build SDL3
|
||||||
|
working-directory: deps/SDL
|
||||||
|
run: |
|
||||||
|
cmake -DSDL_TEST_LIBRARY=OFF -DSDL_INSTALL_DOCS=OFF -S . -B build
|
||||||
|
cmake --build build -j
|
||||||
|
sudo cmake --install build
|
||||||
|
|
||||||
|
- name: Checkout sdl2-compat
|
||||||
|
uses: actions/checkout@v5
|
||||||
|
with:
|
||||||
|
repository: libsdl-org/sdl2-compat
|
||||||
|
ref: release-2.32.68
|
||||||
|
path: deps/sdl2-compat
|
||||||
|
|
||||||
|
- name: Build sdl2-compat
|
||||||
|
working-directory: deps/sdl2-compat
|
||||||
|
run: |
|
||||||
|
cmake -DSDL2COMPAT_TESTS=OFF -S . -B build
|
||||||
|
cmake --build build -j
|
||||||
|
sudo cmake --install build
|
||||||
|
|
||||||
|
- name: Checkout SDL_ttf
|
||||||
|
uses: actions/checkout@v5
|
||||||
|
with:
|
||||||
|
repository: libsdl-org/SDL_ttf
|
||||||
|
ref: a883e490e30fb44a5336ea3dcb990c6982c5216f
|
||||||
|
path: deps/SDL_ttf
|
||||||
|
submodules: 'recursive'
|
||||||
|
|
||||||
|
- name: Build SDL_ttf
|
||||||
|
working-directory: deps/SDL_ttf
|
||||||
|
run: |
|
||||||
|
./autogen.sh
|
||||||
|
./configure
|
||||||
|
make -j$(nproc)
|
||||||
|
sudo make install
|
||||||
|
|
||||||
|
- name: Checkout libva
|
||||||
|
uses: actions/checkout@v5
|
||||||
|
with:
|
||||||
|
repository: intel/libva
|
||||||
|
ref: 2.23.0
|
||||||
|
path: deps/libva
|
||||||
|
|
||||||
|
- name: Build libva
|
||||||
|
working-directory: deps/libva
|
||||||
|
run: |
|
||||||
|
./autogen.sh
|
||||||
|
./configure --enable-x11
|
||||||
|
make -j$(nproc)
|
||||||
|
sudo make install
|
||||||
|
|
||||||
|
- name: Checkout libplacebo
|
||||||
|
uses: actions/checkout@v5
|
||||||
|
with:
|
||||||
|
repository: haasn/libplacebo
|
||||||
|
ref: v7.360.1
|
||||||
|
path: deps/libplacebo
|
||||||
|
submodules: 'recursive'
|
||||||
|
|
||||||
|
- name: Build libplacebo
|
||||||
|
working-directory: deps/libplacebo
|
||||||
|
run: |
|
||||||
|
meson setup build -Dvulkan=enabled -Dopengl=disabled -Ddemos=false
|
||||||
|
ninja -C build
|
||||||
|
sudo ninja install -C build
|
||||||
|
|
||||||
|
- name: Build dav1d
|
||||||
|
working-directory: deps
|
||||||
|
env:
|
||||||
|
DAV1D_VER: 1.5.3
|
||||||
|
run: |
|
||||||
|
git clone --branch $DAV1D_VER --depth 1 https://code.videolan.org/videolan/dav1d.git
|
||||||
|
pushd dav1d
|
||||||
|
meson setup build -Ddefault_library=static -Dbuildtype=release -Denable_tools=false -Denable_tests=false
|
||||||
|
ninja -C build
|
||||||
|
sudo ninja install -C build
|
||||||
|
popd
|
||||||
|
|
||||||
|
- name: Checkout FFmpeg
|
||||||
|
uses: actions/checkout@v5
|
||||||
|
with:
|
||||||
|
repository: FFmpeg/FFmpeg
|
||||||
|
ref: n8.1.1
|
||||||
|
path: deps/FFmpeg
|
||||||
|
|
||||||
|
- name: Build FFmpeg
|
||||||
|
working-directory: deps/FFmpeg
|
||||||
|
run: |
|
||||||
|
./configure --enable-pic --disable-static --enable-shared --disable-all --disable-autodetect --enable-avcodec --enable-avformat --enable-swscale \
|
||||||
|
--enable-decoder=h264 --enable-decoder=hevc --enable-decoder=av1 \
|
||||||
|
--enable-vaapi --enable-hwaccel=h264_vaapi --enable-hwaccel=hevc_vaapi --enable-hwaccel=av1_vaapi \
|
||||||
|
--enable-vdpau --enable-hwaccel=h264_vdpau --enable-hwaccel=hevc_vdpau --enable-hwaccel=av1_vdpau \
|
||||||
|
--enable-libdrm --enable-vulkan --enable-hwaccel=h264_vulkan --enable-hwaccel=hevc_vulkan --enable-hwaccel=av1_vulkan \
|
||||||
|
--enable-libdav1d --enable-decoder=libdav1d
|
||||||
|
make -j$(nproc)
|
||||||
|
sudo make install
|
||||||
|
|
||||||
|
- name: Install linuxdeploy
|
||||||
|
working-directory: dep_root/bin
|
||||||
|
run: |
|
||||||
|
wget https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage
|
||||||
|
wget https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-plugin-qt-x86_64.AppImage
|
||||||
|
chmod +x linuxdeploy*.AppImage
|
||||||
|
|
||||||
|
- name: Build Binaries
|
||||||
|
run: |
|
||||||
|
sudo ldconfig
|
||||||
|
scripts/build-appimage.sh
|
||||||
|
|
||||||
|
- name: Upload Binaries
|
||||||
|
uses: actions/upload-artifact@v6
|
||||||
|
with:
|
||||||
|
name: Moonlight-LinuxAppImage-${{ env.CI_VERSION }}
|
||||||
|
path: build/installer-release/Moonlight-${{ env.CI_VERSION }}-x86_64.AppImage
|
||||||
|
compression-level: 0
|
||||||
|
if-no-files-found: error
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
---
|
||||||
|
name: Build - Steam Link
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
ci_version:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
env:
|
||||||
|
CI_VERSION: ${{ inputs.ci_version }}
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout Repository
|
||||||
|
uses: actions/checkout@v5
|
||||||
|
with:
|
||||||
|
submodules: 'recursive'
|
||||||
|
fetch-depth: 1
|
||||||
|
|
||||||
|
- name: Checkout Steam Link SDK
|
||||||
|
uses: actions/checkout@v5
|
||||||
|
with:
|
||||||
|
repository: ValveSoftware/steamlink-sdk
|
||||||
|
path: steamlink-sdk
|
||||||
|
fetch-depth: 1
|
||||||
|
|
||||||
|
- name: Build binaries
|
||||||
|
run: STEAMLINK_SDK_PATH=$PWD/steamlink-sdk scripts/build-steamlink-app.sh
|
||||||
|
|
||||||
|
- name: Upload Binaries
|
||||||
|
uses: actions/upload-artifact@v6
|
||||||
|
with:
|
||||||
|
name: Moonlight-SteamLink-${{ env.CI_VERSION }}
|
||||||
|
path: build/deploy-release/*
|
||||||
|
if-no-files-found: error
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
---
|
||||||
|
name: Build - Windows and macOS
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
ci_version:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
env:
|
||||||
|
CI_VERSION: ${{ inputs.ci_version }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
# Windows
|
||||||
|
- os: windows-2025
|
||||||
|
qt_version: 6.11.0
|
||||||
|
# macOS
|
||||||
|
- os: macos-15
|
||||||
|
qt_version: 6.11.0
|
||||||
|
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout Repository
|
||||||
|
uses: actions/checkout@v5
|
||||||
|
with:
|
||||||
|
submodules: 'recursive'
|
||||||
|
fetch-depth: 1
|
||||||
|
|
||||||
|
- name: Download dependencies (Windows)
|
||||||
|
if: runner.os == 'Windows'
|
||||||
|
run: powershell ./setup-deps.ps1
|
||||||
|
|
||||||
|
- name: Install create-dmg (macOS)
|
||||||
|
if: runner.os == 'macOS'
|
||||||
|
run: |
|
||||||
|
brew install node
|
||||||
|
npm install --global create-dmg
|
||||||
|
|
||||||
|
- name: Install Qt
|
||||||
|
uses: jurplel/install-qt-action@v4
|
||||||
|
with:
|
||||||
|
cache: true
|
||||||
|
set-env: ${{ runner.os != 'Windows' }}
|
||||||
|
add-tools-to-path: ${{ runner.os != 'Windows' }}
|
||||||
|
version: ${{ matrix.qt_version }}
|
||||||
|
aqtsource: 'git+https://github.com/miurahr/aqtinstall.git@073e34d7c2ab4ae6961ed7cca690b3abd5ba5a7e'
|
||||||
|
|
||||||
|
- name: Install Qt ARM64 (Windows)
|
||||||
|
if: runner.os == 'Windows'
|
||||||
|
uses: jurplel/install-qt-action@v4
|
||||||
|
with:
|
||||||
|
cache: true
|
||||||
|
set-env: false
|
||||||
|
add-tools-to-path: false
|
||||||
|
arch: win64_msvc2022_arm64_cross_compiled
|
||||||
|
version: ${{ matrix.qt_version }}
|
||||||
|
|
||||||
|
- name: Build x64 binaries (Windows)
|
||||||
|
if: runner.os == 'Windows'
|
||||||
|
shell: cmd
|
||||||
|
run: |
|
||||||
|
set "PATH=%QT_PATH%;%PATH%"
|
||||||
|
scripts\build-arch.bat Release x64
|
||||||
|
env:
|
||||||
|
QT_PATH: ${{ runner.workspace }}\Qt\${{ matrix.qt_version }}\msvc2022_64\bin
|
||||||
|
|
||||||
|
- name: Build ARM64 binaries (Windows)
|
||||||
|
if: runner.os == 'Windows'
|
||||||
|
shell: cmd
|
||||||
|
run: |
|
||||||
|
set "PATH=%QT_PATH%;%PATH%"
|
||||||
|
scripts\build-arch.bat Release arm64
|
||||||
|
env:
|
||||||
|
QT_PATH: ${{ runner.workspace }}\Qt\${{ matrix.qt_version }}\msvc2022_arm64\bin
|
||||||
|
|
||||||
|
- name: Build Package
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
if ($Env:RUNNER_OS -eq "Windows") {
|
||||||
|
cmd /c scripts\generate-bundle.bat Release
|
||||||
|
} else {
|
||||||
|
bash scripts/generate-dmg.sh Release
|
||||||
|
}
|
||||||
|
|
||||||
|
- name: Upload Binaries (Windows x64)
|
||||||
|
uses: actions/upload-artifact@v6
|
||||||
|
if: runner.os == 'Windows'
|
||||||
|
with:
|
||||||
|
name: Moonlight-${{ runner.os }}-x64-${{ env.CI_VERSION }}
|
||||||
|
path: build/deploy-x64-release/*
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
- name: Upload Binaries (Windows arm64)
|
||||||
|
uses: actions/upload-artifact@v6
|
||||||
|
if: runner.os == 'Windows'
|
||||||
|
with:
|
||||||
|
name: Moonlight-${{ runner.os }}-arm64-${{ env.CI_VERSION }}
|
||||||
|
path: build/deploy-arm64-release/*
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
- name: Upload Binaries (macOS)
|
||||||
|
uses: actions/upload-artifact@v6
|
||||||
|
if: runner.os == 'macOS'
|
||||||
|
with:
|
||||||
|
name: Moonlight-${{ runner.os }}-${{ env.CI_VERSION }}
|
||||||
|
path: build/installer-Release/*.dmg
|
||||||
|
compression-level: 0
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
- name: Upload Symbols
|
||||||
|
uses: actions/upload-artifact@v6
|
||||||
|
with:
|
||||||
|
name: zDevDbgSyms-${{ runner.os }}-${{ env.CI_VERSION }}
|
||||||
|
path: |
|
||||||
|
build/installer-*/*.dsym/*
|
||||||
|
build/symbols-*/*.pdb
|
||||||
|
if-no-files-found: error
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
---
|
||||||
|
name: Build
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: build-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
setup:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
ci_version: ${{ steps.set-ci-version.outputs.ci_version }}
|
||||||
|
steps:
|
||||||
|
- name: Set CI version
|
||||||
|
id: set-ci-version
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
CI_VERSION=`echo ${GITHUB_SHA} | cut -c1-6`
|
||||||
|
echo "ci_version=${CI_VERSION}" >> "${GITHUB_OUTPUT}"
|
||||||
|
|
||||||
|
build-appimage:
|
||||||
|
needs: setup
|
||||||
|
uses: ./.github/workflows/build-appimage.yml
|
||||||
|
with:
|
||||||
|
ci_version: ${{ needs.setup.outputs.ci_version }}
|
||||||
|
|
||||||
|
build-steamlink:
|
||||||
|
needs: setup
|
||||||
|
uses: ./.github/workflows/build-steamlink.yml
|
||||||
|
with:
|
||||||
|
ci_version: ${{ needs.setup.outputs.ci_version }}
|
||||||
|
|
||||||
|
build-windows-macos:
|
||||||
|
needs: setup
|
||||||
|
uses: ./.github/workflows/build-win-mac.yml
|
||||||
|
with:
|
||||||
|
ci_version: ${{ needs.setup.outputs.ci_version }}
|
||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
|
|
||||||
**/.vs/
|
**/.vs/
|
||||||
|
.vscode/
|
||||||
build/
|
build/
|
||||||
|
libs/
|
||||||
config.tests/*/.qmake.stash
|
config.tests/*/.qmake.stash
|
||||||
config.tests/*/Makefile
|
config.tests/*/Makefile
|
||||||
|
|||||||
@@ -7,9 +7,6 @@
|
|||||||
[submodule "app/SDL_GameControllerDB"]
|
[submodule "app/SDL_GameControllerDB"]
|
||||||
path = app/SDL_GameControllerDB
|
path = app/SDL_GameControllerDB
|
||||||
url = https://github.com/gabomdq/SDL_GameControllerDB.git
|
url = https://github.com/gabomdq/SDL_GameControllerDB.git
|
||||||
[submodule "soundio/libsoundio"]
|
|
||||||
path = soundio/libsoundio
|
|
||||||
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
|
||||||
|
|||||||
-72
@@ -1,72 +0,0 @@
|
|||||||
language: cpp
|
|
||||||
|
|
||||||
git:
|
|
||||||
depth: 1
|
|
||||||
|
|
||||||
matrix:
|
|
||||||
include:
|
|
||||||
- os: osx
|
|
||||||
osx_image: xcode10.2
|
|
||||||
compiler: clang
|
|
||||||
env: TRAVIS_TARGET=osx
|
|
||||||
addons:
|
|
||||||
homebrew:
|
|
||||||
packages:
|
|
||||||
- qt
|
|
||||||
- os: linux
|
|
||||||
sudo: required
|
|
||||||
dist: xenial
|
|
||||||
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:
|
|
||||||
apt:
|
|
||||||
sources:
|
|
||||||
- sourceline: 'ppa:savoury1/multimedia'
|
|
||||||
- sourceline: 'ppa:savoury1/ffmpeg4'
|
|
||||||
packages:
|
|
||||||
- qt5-default
|
|
||||||
- qtbase5-dev
|
|
||||||
- qtquickcontrols2-5-dev
|
|
||||||
- libqt5svg5-dev
|
|
||||||
- libgl1-mesa-dev
|
|
||||||
- libegl1-mesa-dev
|
|
||||||
- libxkbcommon-dev
|
|
||||||
- wayland-protocols
|
|
||||||
- libopus-dev
|
|
||||||
- libva-dev
|
|
||||||
- libvdpau-dev
|
|
||||||
- libsdl2-dev
|
|
||||||
- qtdeclarative5-dev
|
|
||||||
- libssl-dev
|
|
||||||
- libsdl2-ttf-dev
|
|
||||||
- libavcodec-dev
|
|
||||||
- libavutil-dev
|
|
||||||
|
|
||||||
install:
|
|
||||||
- '[ "$TRAVIS_TARGET" != osx ] || brew link --force qt'
|
|
||||||
- if [[ "$TRAVIS_TARGET" = "osx" ]]; then nvm use node && npm install --global create-dmg; fi
|
|
||||||
- if [[ "$TRAVIS_TARGET" = "steamlink" ]]; then git clone --depth=1 https://github.com/ValveSoftware/steamlink-sdk.git $HOME/steamlink-sdk; fi
|
|
||||||
|
|
||||||
before_script:
|
|
||||||
- '[ "$TRAVIS_TARGET" = "steamlink" ] || qmake -v'
|
|
||||||
- '[[ "$TRAVIS_TARGET" != linux || "$CXX" != clang++ ]] || export QMAKESPEC=linux-clang'
|
|
||||||
- '[[ "$TRAVIS_TARGET" != linux || "$CXX" != g++ ]] || export QMAKESPEC=linux-g++'
|
|
||||||
- '[ "$TRAVIS_TARGET" != osx ] || export QMAKESPEC=macx-clang'
|
|
||||||
- '[ "$TRAVIS_TARGET" != "steamlink" ] || export STEAMLINK_SDK_PATH=$HOME/steamlink-sdk'
|
|
||||||
|
|
||||||
script:
|
|
||||||
- '[ "$TRAVIS_TARGET" != "linux" ] || qmake moonlight-qt.pro -spec $QMAKESPEC'
|
|
||||||
- '[ "$TRAVIS_TARGET" != "linux" ] || make -j$(nproc) debug'
|
|
||||||
- '[ "$TRAVIS_TARGET" != "linux" ] || make -j$(nproc) release'
|
|
||||||
- '[ "$TRAVIS_TARGET" != "osx" ] || scripts/generate-dmg.sh Release'
|
|
||||||
- '[ "$TRAVIS_TARGET" != "steamlink" ] || scripts/build-steamlink-app.sh'
|
|
||||||
@@ -12,8 +12,11 @@ contains(QT_ARCH, i386) {
|
|||||||
contains(QT_ARCH, x86_64) {
|
contains(QT_ARCH, x86_64) {
|
||||||
LIBS += -L$$PWD/../libs/windows/lib/x64
|
LIBS += -L$$PWD/../libs/windows/lib/x64
|
||||||
}
|
}
|
||||||
|
contains(QT_ARCH, arm64) {
|
||||||
|
LIBS += -L$$PWD/../libs/windows/lib/arm64
|
||||||
|
}
|
||||||
|
|
||||||
LIBS += -lNktHookLib
|
LIBS += -ldetours
|
||||||
DEFINES += ANTIHOOKING_LIBRARY
|
DEFINES += ANTIHOOKING_LIBRARY
|
||||||
SOURCES += antihookingprotection.cpp
|
SOURCES += antihookingprotection.cpp
|
||||||
HEADERS += antihookingprotection.h
|
HEADERS += antihookingprotection.h
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
#include "antihookingprotection.h"
|
#include "antihookingprotection.h"
|
||||||
|
|
||||||
#include <NktHookLib.h>
|
#define WIN32_LEAN_AND_MEAN
|
||||||
|
#include <Windows.h>
|
||||||
|
|
||||||
|
#include <detours.h>
|
||||||
|
|
||||||
typedef HMODULE (WINAPI *LoadLibraryAFunc)(LPCSTR lpLibFileName);
|
typedef HMODULE (WINAPI *LoadLibraryAFunc)(LPCSTR lpLibFileName);
|
||||||
typedef HMODULE (WINAPI *LoadLibraryWFunc)(LPCWSTR lpLibFileName);
|
typedef HMODULE (WINAPI *LoadLibraryWFunc)(LPCWSTR lpLibFileName);
|
||||||
@@ -12,25 +15,35 @@ class AntiHookingProtection
|
|||||||
public:
|
public:
|
||||||
static void enable()
|
static void enable()
|
||||||
{
|
{
|
||||||
#ifdef QT_DEBUG
|
DetourTransactionBegin();
|
||||||
s_HookManager.SetEnableDebugOutput(true);
|
DetourUpdateThread(GetCurrentThread());
|
||||||
#endif
|
|
||||||
|
|
||||||
HINSTANCE kernel32Handle = NktHookLibHelpers::GetModuleBaseAddress(L"kernel32.dll");
|
s_RealLoadLibraryA = LoadLibraryA;
|
||||||
SIZE_T hookId;
|
DetourAttach(&(PVOID&)s_RealLoadLibraryA, LoadLibraryAHook);
|
||||||
|
|
||||||
s_HookManager.Hook(&hookId, (LPVOID*)&s_RealLoadLibraryA,
|
s_RealLoadLibraryW = LoadLibraryW;
|
||||||
NktHookLibHelpers::GetProcedureAddress(kernel32Handle, "LoadLibraryA"),
|
DetourAttach(&(PVOID&)s_RealLoadLibraryW, LoadLibraryWHook);
|
||||||
(LPVOID)AntiHookingProtection::LoadLibraryAHook);
|
|
||||||
s_HookManager.Hook(&hookId, (LPVOID*)&s_RealLoadLibraryW,
|
s_RealLoadLibraryExA = LoadLibraryExA;
|
||||||
NktHookLibHelpers::GetProcedureAddress(kernel32Handle, "LoadLibraryW"),
|
DetourAttach(&(PVOID&)s_RealLoadLibraryExA, LoadLibraryExAHook);
|
||||||
(LPVOID)AntiHookingProtection::LoadLibraryWHook);
|
|
||||||
s_HookManager.Hook(&hookId, (LPVOID*)&s_RealLoadLibraryExA,
|
s_RealLoadLibraryExW = LoadLibraryExW;
|
||||||
NktHookLibHelpers::GetProcedureAddress(kernel32Handle, "LoadLibraryExA"),
|
DetourAttach(&(PVOID&)s_RealLoadLibraryExW, LoadLibraryExWHook);
|
||||||
(LPVOID)AntiHookingProtection::LoadLibraryExAHook);
|
|
||||||
s_HookManager.Hook(&hookId, (LPVOID*)&s_RealLoadLibraryExW,
|
DetourTransactionCommit();
|
||||||
NktHookLibHelpers::GetProcedureAddress(kernel32Handle, "LoadLibraryExW"),
|
}
|
||||||
(LPVOID)AntiHookingProtection::LoadLibraryExWHook);
|
|
||||||
|
static void disable()
|
||||||
|
{
|
||||||
|
DetourTransactionBegin();
|
||||||
|
DetourUpdateThread(GetCurrentThread());
|
||||||
|
|
||||||
|
DetourDetach(&(PVOID&)s_RealLoadLibraryA, LoadLibraryAHook);
|
||||||
|
DetourDetach(&(PVOID&)s_RealLoadLibraryW, LoadLibraryWHook);
|
||||||
|
DetourDetach(&(PVOID&)s_RealLoadLibraryExA, LoadLibraryExAHook);
|
||||||
|
DetourDetach(&(PVOID&)s_RealLoadLibraryExW, LoadLibraryExWHook);
|
||||||
|
|
||||||
|
DetourTransactionCommit();
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
@@ -124,33 +137,53 @@ private:
|
|||||||
return s_RealLoadLibraryExW(lpLibFileName, hFile, dwFlags);
|
return s_RealLoadLibraryExW(lpLibFileName, hFile, dwFlags);
|
||||||
}
|
}
|
||||||
|
|
||||||
static CNktHookLib s_HookManager;
|
|
||||||
static LoadLibraryAFunc s_RealLoadLibraryA;
|
static LoadLibraryAFunc s_RealLoadLibraryA;
|
||||||
static LoadLibraryWFunc s_RealLoadLibraryW;
|
static LoadLibraryWFunc s_RealLoadLibraryW;
|
||||||
static LoadLibraryExAFunc s_RealLoadLibraryExA;
|
static LoadLibraryExAFunc s_RealLoadLibraryExA;
|
||||||
static LoadLibraryExWFunc s_RealLoadLibraryExW;
|
static LoadLibraryExWFunc s_RealLoadLibraryExW;
|
||||||
|
|
||||||
static constexpr LPCWSTR k_BlacklistedDlls[] = {
|
static constexpr LPCWSTR k_BlacklistedDlls[] = {
|
||||||
// This DLL shipped with ASUS Sonic Radar 3 improperly handles
|
// These A-Volute DLLs shipped with various audio driver packages improperly handle
|
||||||
// D3D9 exclusive fullscreen in a way that causes CreateDeviceEx()
|
// D3D9 exclusive fullscreen in a way that causes CreateDeviceEx() to deadlock.
|
||||||
// to deadlock. https://github.com/moonlight-stream/moonlight-qt/issues/102
|
// https://github.com/moonlight-stream/moonlight-qt/issues/102
|
||||||
L"NahimicOSD.dll",
|
L"NahimicOSD.dll", // ASUS Sonic Radar 3
|
||||||
|
L"SSAudioOSD.dll", // SteelSeries headsets
|
||||||
|
L"SS2OSD.dll", // ASUS Sonic Studio 2
|
||||||
|
L"Nahimic2OSD.dll",
|
||||||
|
L"NahimicMSIOSD.dll",
|
||||||
|
L"nhAsusPhoebusOSD.dll", // ASUS Phoebus
|
||||||
|
|
||||||
// This DLL has been seen in several crash reports. Some Googling
|
// This DLL has been seen in several crash reports. Some Googling
|
||||||
// suggests it's highly unstable and causes issues in many games.
|
// suggests it's highly unstable and causes issues in many games.
|
||||||
L"EZFRD32.dll",
|
L"EZFRD32.dll",
|
||||||
L"EZFRD64.dll",
|
L"EZFRD64.dll",
|
||||||
|
|
||||||
// These are the dList DLLs for Optimus hybrid graphics DDI.
|
// These are the older dList/AppInit DLLs for Optimus hybrid graphics DDI.
|
||||||
// https://docs.microsoft.com/en-us/windows-hardware/drivers/display/hybrid-system-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
|
// These seem to cause a crash in PresentEx() in full-screen exclusive mode.
|
||||||
// deadlocks DXVA2 when we present with D3DPRESENT_DONOTWAIT. This will prevent
|
// This block will prevent Optimus from ever using the dGPU even if the user has requested it.
|
||||||
// Optimus from ever using the dGPU even if the user has requested it.
|
// https://github.com/moonlight-stream/moonlight-qt/issues/386
|
||||||
// https://github.com/moonlight-stream/moonlight-qt/issues/240
|
//
|
||||||
// https://github.com/moonlight-stream/moonlight-qt/issues/235
|
// d3d9!CSwapChain::BltToHybridPrimary+0x200:
|
||||||
L"nvdlist.dll",
|
// 00007ffa`23f37e58 488b01 mov rax,qword ptr [rcx] ds:00000000`00000038=????????????????
|
||||||
L"nvdlistx.dll",
|
// 00 0000004e`496ff4e0 00007ffa`23f39e2c d3d9!CSwapChain::BltToHybridPrimary+0x200
|
||||||
|
// 01 0000004e`496ff880 00007ffa`23ee39ce d3d9!CSwapChain::FlipToSurface+0x15c
|
||||||
|
// 02 0000004e`496ff900 00007ffa`23f4dd75 d3d9!CSwapChain::PresentMain+0x3e13e
|
||||||
|
// 03 0000004e`496ffab0 00007ffa`23f4dccd d3d9!CBaseDevice::PresentMain+0x9d
|
||||||
|
// 04 0000004e`496ffb00 00007ff7`8e31016f d3d9!CBaseDevice::PresentEx+0xbd
|
||||||
|
// 05 0000004e`496ffb50 00007ff7`8e30df1e Moonlight!DXVA2Renderer::renderFrame+0x61f [C:\moonlight-qt\app\streaming\video\ffmpeg-renderers\dxva2.cpp @ 1035]
|
||||||
|
// 06 0000004e`496ffd50 00007ff7`8e30e46a Moonlight!Pacer::renderFrame+0x3e [C:\moonlight-qt\app\streaming\video\ffmpeg-renderers\pacer\pacer.cpp @ 265]
|
||||||
|
// 07 (Inline Function) --------`-------- Moonlight!Pacer::renderLastFrameAndUnlock+0x197 [C:\moonlight-qt\app\streaming\video\ffmpeg-renderers\pacer\pacer.cpp @ 156]
|
||||||
|
// 08 0000004e`496ffda0 00007ffa`14476978 Moonlight!Pacer::renderThread+0x1ca [C:\moonlight-qt\app\streaming\video\ffmpeg-renderers\pacer\pacer.cpp @ 88]
|
||||||
|
// 09 0000004e`496ffde0 00007ffa`14476ee2 SDL2!SDL_RunThread+0x38 [C:\Users\camer\SDL\src\thread\SDL_thread.c @ 276]
|
||||||
|
// 0a 0000004e`496ffe10 00007ffa`2aae0e82 SDL2!RunThread+0x12 [C:\Users\camer\SDL\src\thread\windows\SDL_systhread.c @ 83]
|
||||||
|
// 0b 0000004e`496ffe40 00007ffa`2d627bd4 ucrtbase!thread_start<unsigned int (__cdecl*)(void *),1>+0x42
|
||||||
|
// 0c 0000004e`496ffe70 00007ffa`2da4ce51 kernel32!BaseThreadInitThunk+0x14
|
||||||
|
// 0d 0000004e`496ffea0 00000000`00000000 ntdll!RtlUserThreadStart+0x21
|
||||||
|
//
|
||||||
|
L"nvinit.dll",
|
||||||
|
L"nvinitx.dll",
|
||||||
|
|
||||||
// In some unknown circumstances, RTSS tries to hook in the middle of an instruction, leaving garbage
|
// 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:
|
// code inside d3d9.dll that causes a crash when executed:
|
||||||
@@ -174,10 +207,31 @@ private:
|
|||||||
// Since the RTSS OSD doesn't even work with DXVA2, we'll just block the hooks entirely.
|
// Since the RTSS OSD doesn't even work with DXVA2, we'll just block the hooks entirely.
|
||||||
L"RTSSHooks.dll",
|
L"RTSSHooks.dll",
|
||||||
L"RTSSHooks64.dll",
|
L"RTSSHooks64.dll",
|
||||||
|
|
||||||
|
// Bandicam's Vulkan layer DLL crashes during destruction of the Vulkan swapchain
|
||||||
|
// bdcamvk64+0x10b73:
|
||||||
|
// 00007ffa`be3b0b73 498b5630 mov rdx,qword ptr [r14+30h] ds:00000000`00000030=????????????????
|
||||||
|
//
|
||||||
|
// bdcamvk64+0x10b73
|
||||||
|
// libplacebo_357!vk_sw_destroy+0x14f [D:\a\moonlight-deps\moonlight-deps\libplacebo\src\vulkan\swapchain.c @ 460]
|
||||||
|
// libplacebo_357!pl_swapchain_destroy+0x1a [D:\a\moonlight-deps\moonlight-deps\libplacebo\src\swapchain.c @ 30]
|
||||||
|
// Moonlight!PlVkRenderer::~PlVkRenderer+0x159 [D:\a\moonlight-qt\app\streaming\video\ffmpeg-renderers\plvk.cpp @ 140]
|
||||||
|
// Moonlight!PlVkRenderer::`scalar deleting destructor'+0x17
|
||||||
|
// Moonlight!FFmpegVideoDecoder::reset+0x203 [D:\a\moonlight-qt\app\streaming\video\ffmpeg.cpp @ 301]
|
||||||
|
// Moonlight!FFmpegVideoDecoder::~FFmpegVideoDecoder+0x22 [D:\a\moonlight-qt\app\streaming\video\ffmpeg.cpp @ 257]
|
||||||
|
// Moonlight!FFmpegVideoDecoder::`scalar deleting destructor'+0x17
|
||||||
|
// Moonlight!Session::getDecoderInfo+0x19a [D:\a\moonlight-qt\app\streaming\session.cpp @ 415]
|
||||||
|
// Moonlight!SystemProperties::querySdlVideoInfoInternal+0x134 [D:\a\moonlight-qt\app\backend\systemproperties.cpp @ 161]
|
||||||
|
// Moonlight!SystemProperties::querySdlVideoInfo+0x97 [D:\a\moonlight-qt\app\backend\systemproperties.cpp @ 123]
|
||||||
|
// Moonlight!SystemProperties::SystemProperties+0x4bf [D:\a\moonlight-qt\app\backend\systemproperties.cpp @ 75]
|
||||||
|
//
|
||||||
|
// https://github.com/moonlight-stream/moonlight-qt/issues/1425
|
||||||
|
// https://github.com/moonlight-stream/moonlight-qt/issues/1579
|
||||||
|
L"bdcamvk32.dll",
|
||||||
|
L"bdcamvk64.dll",
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
CNktHookLib AntiHookingProtection::s_HookManager;
|
|
||||||
LoadLibraryAFunc AntiHookingProtection::s_RealLoadLibraryA;
|
LoadLibraryAFunc AntiHookingProtection::s_RealLoadLibraryA;
|
||||||
LoadLibraryWFunc AntiHookingProtection::s_RealLoadLibraryW;
|
LoadLibraryWFunc AntiHookingProtection::s_RealLoadLibraryW;
|
||||||
LoadLibraryExAFunc AntiHookingProtection::s_RealLoadLibraryExA;
|
LoadLibraryExAFunc AntiHookingProtection::s_RealLoadLibraryExA;
|
||||||
@@ -186,14 +240,26 @@ LoadLibraryExWFunc AntiHookingProtection::s_RealLoadLibraryExW;
|
|||||||
AH_EXPORT void AntiHookingDummyImport() {}
|
AH_EXPORT void AntiHookingDummyImport() {}
|
||||||
|
|
||||||
extern "C"
|
extern "C"
|
||||||
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID)
|
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
|
||||||
{
|
{
|
||||||
|
if (DetourIsHelperProcess()) {
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
switch (fdwReason)
|
switch (fdwReason)
|
||||||
{
|
{
|
||||||
case DLL_PROCESS_ATTACH:
|
case DLL_PROCESS_ATTACH:
|
||||||
|
DetourRestoreAfterWith();
|
||||||
AntiHookingProtection::enable();
|
AntiHookingProtection::enable();
|
||||||
DisableThreadLibraryCalls(hinstDLL);
|
DisableThreadLibraryCalls(hinstDLL);
|
||||||
break;
|
break;
|
||||||
|
case DLL_PROCESS_DETACH:
|
||||||
|
// Ignore DLL_PROCESS_DETACH on process exit. No need to waste time
|
||||||
|
// unhooking everything if the whole process is being destroyed.
|
||||||
|
if (lpvReserved == NULL) {
|
||||||
|
AntiHookingProtection::disable();
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return TRUE;
|
return TRUE;
|
||||||
|
|||||||
@@ -1,67 +1,110 @@
|
|||||||
# Moonlight PC
|
# Moonlight 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 PC](https://moonlight-stream.org) is an open source PC client for NVIDIA GameStream and [Sunshine](https://github.com/LizardByte/Sunshine).
|
||||||
|
|
||||||
Moonlight also has mobile versions for [Android](https://github.com/moonlight-stream/moonlight-android) and [iOS](https://github.com/moonlight-stream/moonlight-ios).
|
Moonlight also has mobile versions for [Android](https://github.com/moonlight-stream/moonlight-android) and [iOS](https://github.com/moonlight-stream/moonlight-ios).
|
||||||
|
|
||||||
You can follow development on our [Discord server](https://discord.gg/6ERtzFY).
|
You can follow development on our [Discord server](https://moonlight-stream.org/discord) and help translate Moonlight into your language on [Weblate](https://hosted.weblate.org/projects/moonlight/moonlight-qt/).
|
||||||
|
|
||||||
[](https://ci.appveyor.com/project/cgutman/moonlight-qt/branch/master)
|
[](https://github.com/moonlight-stream/moonlight-qt/actions/workflows/build.yml?query=branch%3Amaster)
|
||||||
[](https://travis-ci.org/moonlight-stream/moonlight-qt)
|
|
||||||
[](https://github.com/moonlight-stream/moonlight-qt/releases)
|
[](https://github.com/moonlight-stream/moonlight-qt/releases)
|
||||||
|
[](https://hosted.weblate.org/projects/moonlight/moonlight-qt/)
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
- Hardware accelerated video decoding on Windows, Mac, and Linux
|
- Hardware accelerated video decoding on Windows, Mac, and Linux
|
||||||
- Supports streaming at up to 120 FPS (high refresh rate monitor recommended)
|
- H.264, HEVC, and AV1 codec support (AV1 requires Sunshine and a supported host GPU)
|
||||||
- Supports streaming at 720p, 1080p, 1440p, 4K, and the client PC's native screen resolution
|
- YUV 4:4:4 support (Sunshine only)
|
||||||
- 5.1 surround sound audio
|
- HDR streaming support
|
||||||
- HEVC support for better image quality at reduced bandwidth
|
- 7.1 surround sound audio support
|
||||||
- Keyboard and mouse support
|
- 10-point multitouch support (Sunshine only)
|
||||||
- Gamepad support with force feedback for up to 4 players
|
- Gamepad support with force feedback and motion controls for up to 16 players
|
||||||
|
- Support for both pointer capture (for games) and direct mouse control (for remote desktop)
|
||||||
|
- Support for passing system-wide keyboard shortcuts like Alt+Tab to the host
|
||||||
|
|
||||||
## Downloads
|
## Downloads
|
||||||
- [Windows, macOS, and Steam Link](https://github.com/moonlight-stream/moonlight-qt/releases)
|
- [Windows, macOS, and Steam Link](https://github.com/moonlight-stream/moonlight-qt/releases)
|
||||||
- [Snap (for Ubuntu-based Linux distros)](https://snapcraft.io/moonlight)
|
- [Snap (for Ubuntu-based Linux distros)](https://snapcraft.io/moonlight)
|
||||||
- [Flatpak (for other Linux distros)](https://flathub.org/apps/details/com.moonlight_stream.Moonlight)
|
- [Flatpak (for other Linux distros)](https://flathub.org/apps/details/com.moonlight_stream.Moonlight)
|
||||||
|
- [AppImage](https://github.com/moonlight-stream/moonlight-qt/releases)
|
||||||
|
- [Raspberry Pi 4 and 5](https://github.com/moonlight-stream/moonlight-docs/wiki/Installing-Moonlight-Qt-on-Raspberry-Pi-4)
|
||||||
|
- [Generic ARM 32-bit and 64-bit Debian packages](https://github.com/moonlight-stream/moonlight-docs/wiki/Installing-Moonlight-Qt-on-ARM%E2%80%90based-Single-Board-Computers) (not for Raspberry Pi)
|
||||||
|
- [Experimental RISC-V Debian packages](https://github.com/moonlight-stream/moonlight-docs/wiki/Installing-Moonlight-Qt-on-RISC%E2%80%90V-Single-Board-Computers)
|
||||||
|
- [NVIDIA Jetson and Nintendo Switch (Ubuntu L4T)](https://github.com/moonlight-stream/moonlight-docs/wiki/Installing-Moonlight-Qt-on-Linux4Tegra-(L4T)-Ubuntu)
|
||||||
|
|
||||||
|
### Nightly Builds
|
||||||
|
- [Downloads](https://nightly.link/moonlight-stream/moonlight-qt/workflows/build/master)
|
||||||
|
|
||||||
|
#### Special Thanks
|
||||||
|
|
||||||
|
[](https://cloudsmith.com)
|
||||||
|
|
||||||
|
Hosting for Moonlight's Debian and L4T package repositories is graciously provided for free by [Cloudsmith](https://cloudsmith.com).
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
### General Build Requirements
|
|
||||||
* Qt 5.9 SDK or later
|
|
||||||
|
|
||||||
### Windows-specific Build Requirements
|
### Windows Build Requirements
|
||||||
* Windows 7 or later
|
* Qt 6.7 SDK or later (earlier versions may work but are not officially supported)
|
||||||
* [Visual Studio](https://visualstudio.microsoft.com/downloads/) 2017 (Community edition is fine)
|
* [Visual Studio 2022](https://visualstudio.microsoft.com/downloads/) (Community edition is fine)
|
||||||
* Select MSVC Desktop toolchain during Qt installation
|
* Select **MSVC** option during Qt installation. MinGW is not supported.
|
||||||
* [DirectX SDK](https://www.microsoft.com/en-us/download/details.aspx?id=6812)
|
|
||||||
* [7-Zip](https://www.7-zip.org/) (only if building installers for non-development PCs)
|
* [7-Zip](https://www.7-zip.org/) (only if building installers for non-development PCs)
|
||||||
* [WiX Toolset](http://wixtoolset.org/releases/) v3.11 or later (only if building installers for non-development PCs)
|
* Graphics Tools (only if running debug builds)
|
||||||
|
* Install "Graphics Tools" in the Optional Features page of the Windows Settings app.
|
||||||
|
* Alternatively, run `dism /online /add-capability /capabilityname:Tools.Graphics.DirectX~~~~0.0.1.0` and reboot.
|
||||||
|
|
||||||
### Mac-specific Build Requirements
|
### macOS Build Requirements
|
||||||
* macOS Sierra (10.12) or later
|
* Qt 6.7 SDK or later (earlier versions may work but are not officially supported)
|
||||||
* Xcode 10
|
* Xcode 14 or later (earlier versions may work but are not officially supported)
|
||||||
* [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 Build Requirements
|
### Linux/Unix Build Requirements
|
||||||
|
* Qt 6 is recommended, but Qt 5.12 or later is also supported (replace `qmake6` with `qmake` when using Qt 5).
|
||||||
* 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 pulseaudio-libs-devel alsa-lib-devel SDL2_ttf-devel`
|
* FFmpeg 4.0 or later
|
||||||
* 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/
|
* Install the required packages:
|
||||||
|
* Debian/Ubuntu:
|
||||||
|
* Base Requirements: `libegl1-mesa-dev libgl1-mesa-dev libopus-dev libsdl2-dev libsdl2-ttf-dev libssl-dev libavcodec-dev libavformat-dev libswscale-dev libva-dev libvdpau-dev libxkbcommon-dev wayland-protocols libdrm-dev`
|
||||||
|
* Qt 6 (Recommended): `qt6-base-dev qt6-declarative-dev libqt6svg6-dev qt6-wayland qml6-module-qtquick-controls qml6-module-qtquick-templates qml6-module-qtquick-layouts qml6-module-qtqml-workerscript qml6-module-qtquick-window qml6-module-qtquick`
|
||||||
|
* Qt 5: `qtbase5-dev qt5-qmake qtdeclarative5-dev qtquickcontrols2-5-dev qml-module-qtquick-controls2 qml-module-qtquick-layouts qml-module-qtquick-window2 qml-module-qtquick2 qtwayland5`
|
||||||
|
* RedHat/Fedora (RPM Fusion repo required):
|
||||||
|
* Base Requirements: `openssl-devel SDL2-devel SDL2_ttf-devel ffmpeg-devel libva-devel libvdpau-devel opus-devel pulseaudio-libs-devel alsa-lib-devel libdrm-devel`
|
||||||
|
* Qt 6 (Recommended): `qt6-qtsvg-devel qt6-qtdeclarative-devel`
|
||||||
|
* Qt 5: `qt5-qtsvg-devel qt5-qtquickcontrols2-devel`
|
||||||
|
* Building the Vulkan renderer requires a `libplacebo-dev`/`libplacebo-devel` version of at least v7.349.0 and FFmpeg 6.1 or later.
|
||||||
|
|
||||||
### Steam Link-specific Build Requirements
|
### Steam Link Build Requirements
|
||||||
* [Steam Link SDK](https://github.com/ValveSoftware/steamlink-sdk) cloned on your build system
|
* [Steam Link SDK](https://github.com/ValveSoftware/steamlink-sdk) cloned on your build system
|
||||||
* STEAMLINK_SDK_PATH environment variable set to the Steam Link SDK path
|
* STEAMLINK_SDK_PATH environment variable set to the Steam Link SDK path
|
||||||
|
|
||||||
|
**Steam Link Hardware Limitations**
|
||||||
|
Moonlight builds for Steam Link are subject to hardware limitations of the Steam Link device:
|
||||||
|
* Maximum resolution: **1080p (1920x1080)**
|
||||||
|
* Maximum framerate: **60 FPS**
|
||||||
|
* Maximum video bitrate: **40 Mbps**
|
||||||
|
* **HDR streaming is not supported** on the original hardware
|
||||||
|
|
||||||
|
### Docker containers
|
||||||
|
If you want to use Docker for building, look at [this repo](https://github.com/cgutman/moonlight-packaging) containing canonical containers
|
||||||
|
for different architectures, which handle building deps and extra linking for you.
|
||||||
|
|
||||||
### 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 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 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.
|
* You may also use your Linux distro's package manager for the Qt SDK as long as the packages are Qt 5.12 or later.
|
||||||
* This step is not required for building on Steam Link, because the Steam Link SDK includes Qt 5.9.
|
* This step is not required for building on Steam Link, because the Steam Link SDK includes Qt 5.14.
|
||||||
2. Run `git submodule update --init --recursive` from within `moonlight-qt/`
|
2. Download submodules and dependencies
|
||||||
|
* Run `git submodule update --init --recursive` from within `moonlight-qt/`.
|
||||||
|
* On Windows and macOS, you must also run `setup-deps.ps1` (Windows) or `setup-deps.py` (macOS).
|
||||||
|
* Perform these steps each time you pull new changes from the Git repository.
|
||||||
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\build-arch.bat` and `scripts\generate-bundle.bat`. Execute these scripts from the root of the repository within a Qt command prompt. Ensure 7-Zip binary directory is on your `%PATH%`.
|
||||||
* For macOS builds, use `scripts/generate-dmg.sh`. Execute this script from the root of the repository and ensure Qt's `bin` folder is in your `$PATH`.
|
* For macOS builds, use `scripts/generate-dmg.sh`. Execute this script from the root of the repository and ensure Qt's `bin` folder is in your `$PATH`.
|
||||||
* For Steam Link builds, run `scripts/build-steamlink-app.sh` from the root of the repository.
|
* For Steam Link builds, run `scripts/build-steamlink-app.sh` from the root of the repository.
|
||||||
* To build from the command line for development use, run `qmake moonlight-qt.pro` then `make debug` or `make release`
|
* To build from the command line for development use on macOS or Linux, run `qmake6 moonlight-qt.pro` then `make debug` or `make release`.
|
||||||
|
* The final binary will be placed in `app/moonlight`.
|
||||||
|
* To create an embedded build for a single-purpose device, use `qmake6 "CONFIG+=embedded" moonlight-qt.pro` and build normally.
|
||||||
|
* This build will lack windowed mode, Discord/Help links, and other features that don't make sense on an embedded device.
|
||||||
|
* For platforms with poor GPU performance, add `"CONFIG+=gpuslow"` to prefer direct KMSDRM rendering over GL/Vulkan renderers. Direct KMSDRM rendering can use dedicated YUV/RGB conversion and scaling hardware rather than slower GPU shaders for these operations.
|
||||||
|
|
||||||
## Contribute
|
## Contribute
|
||||||
1. Fork us
|
1. Fork us
|
||||||
|
|||||||
+24
-2
@@ -2,10 +2,14 @@
|
|||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
|
<key>GCSupportsGameMode</key>
|
||||||
|
<true/>
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
<string>Moonlight</string>
|
<string>Moonlight</string>
|
||||||
|
<key>LSSupportsGameMode</key>
|
||||||
|
<true/>
|
||||||
<key>CFBundleGetInfoString</key>
|
<key>CFBundleGetInfoString</key>
|
||||||
<string>Stream games from your NVIDIA GameStream-enabled PC</string>
|
<string>Stream games and other applications from another PC</string>
|
||||||
<key>CFBundleIconFile</key>
|
<key>CFBundleIconFile</key>
|
||||||
<string>moonlight</string>
|
<string>moonlight</string>
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
@@ -17,16 +21,34 @@
|
|||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>LSMinimumSystemVersion</key>
|
<key>LSMinimumSystemVersion</key>
|
||||||
<string>10.11</string>
|
<string>11.0.0</string>
|
||||||
<key>NSPrincipalClass</key>
|
<key>NSPrincipalClass</key>
|
||||||
<string>NSApplication</string>
|
<string>NSApplication</string>
|
||||||
<key>NSSupportsAutomaticGraphicsSwitching</key>
|
<key>NSSupportsAutomaticGraphicsSwitching</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
<key>GCSupportedGameControllers</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>ProfileName</key>
|
||||||
|
<string>ExtendedGamepad</string>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
<key>GCSupportsControllerUserInteraction</key>
|
||||||
|
<true/>
|
||||||
|
<key>NSAppTransportSecurity</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSAllowsArbitraryLoads</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
<key>NSLocalNetworkUsageDescription</key>
|
||||||
|
<string>Moonlight uses the local network to connect to your gaming PC for streaming.</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>VERSION</string>
|
<string>VERSION</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>VERSION</string>
|
<string>VERSION</string>
|
||||||
<key>CFBundleDisplayName</key>
|
<key>CFBundleDisplayName</key>
|
||||||
<string>Moonlight</string>
|
<string>Moonlight</string>
|
||||||
|
<key>LSApplicationCategoryType</key>
|
||||||
|
<string>public.app-category.games</string>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
+1
-1
Submodule app/SDL_GameControllerDB updated: aeebc86f9f...1fc89ca578
@@ -0,0 +1,67 @@
|
|||||||
|
//
|
||||||
|
// Compatibility header for older version of SDL.
|
||||||
|
// Include this instead of SDL.h directly.
|
||||||
|
//
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL.h>
|
||||||
|
|
||||||
|
// SDL_FRect wasn't added until 2.0.10
|
||||||
|
#if !SDL_VERSION_ATLEAST(2, 0, 10)
|
||||||
|
typedef struct SDL_FRect
|
||||||
|
{
|
||||||
|
float x;
|
||||||
|
float y;
|
||||||
|
float w;
|
||||||
|
float h;
|
||||||
|
} SDL_FRect;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef SDL_HINT_VIDEO_X11_FORCE_EGL
|
||||||
|
#define SDL_HINT_VIDEO_X11_FORCE_EGL "SDL_VIDEO_X11_FORCE_EGL"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef SDL_HINT_KMSDRM_REQUIRE_DRM_MASTER
|
||||||
|
#define SDL_HINT_KMSDRM_REQUIRE_DRM_MASTER "SDL_KMSDRM_REQUIRE_DRM_MASTER"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef SDL_HINT_ALLOW_ALT_TAB_WHILE_GRABBED
|
||||||
|
#define SDL_HINT_ALLOW_ALT_TAB_WHILE_GRABBED "SDL_ALLOW_ALT_TAB_WHILE_GRABBED"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef SDL_HINT_JOYSTICK_HIDAPI_PS4_RUMBLE
|
||||||
|
#define SDL_HINT_JOYSTICK_HIDAPI_PS4_RUMBLE "SDL_JOYSTICK_HIDAPI_PS4_RUMBLE"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef SDL_HINT_JOYSTICK_HIDAPI_PS5_RUMBLE
|
||||||
|
#define SDL_HINT_JOYSTICK_HIDAPI_PS5_RUMBLE "SDL_JOYSTICK_HIDAPI_PS5_RUMBLE"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef SDL_HINT_WINDOWS_USE_D3D9EX
|
||||||
|
#define SDL_HINT_WINDOWS_USE_D3D9EX "SDL_WINDOWS_USE_D3D9EX"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef SDL_HINT_GAMECONTROLLER_USE_BUTTON_LABELS
|
||||||
|
#define SDL_HINT_GAMECONTROLLER_USE_BUTTON_LABELS "SDL_GAMECONTROLLER_USE_BUTTON_LABELS"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef SDL_HINT_MOUSE_RELATIVE_SCALING
|
||||||
|
#define SDL_HINT_MOUSE_RELATIVE_SCALING "SDL_MOUSE_RELATIVE_SCALING"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef SDL_HINT_AUDIO_DEVICE_APP_NAME
|
||||||
|
#define SDL_HINT_AUDIO_DEVICE_APP_NAME "SDL_AUDIO_DEVICE_APP_NAME"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef SDL_HINT_APP_NAME
|
||||||
|
#define SDL_HINT_APP_NAME "SDL_APP_NAME"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef SDL_HINT_MOUSE_AUTO_CAPTURE
|
||||||
|
#define SDL_HINT_MOUSE_AUTO_CAPTURE "SDL_MOUSE_AUTO_CAPTURE"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef SDL_HINT_VIDEO_WAYLAND_EMULATE_MOUSE_WARP
|
||||||
|
#define SDL_HINT_VIDEO_WAYLAND_EMULATE_MOUSE_WARP "SDL_VIDEO_WAYLAND_EMULATE_MOUSE_WARP"
|
||||||
|
#endif
|
||||||
+277
-95
@@ -1,5 +1,5 @@
|
|||||||
QT += core quick network quickcontrols2 svg
|
QT += core quick network quickcontrols2 svg
|
||||||
CONFIG += c++11
|
CONFIG += c++17
|
||||||
|
|
||||||
unix:!macx {
|
unix:!macx {
|
||||||
TARGET = moonlight
|
TARGET = moonlight
|
||||||
@@ -12,10 +12,13 @@ include(../globaldefs.pri)
|
|||||||
|
|
||||||
# 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 (when disable-prebuilts is not defined),
|
||||||
win32|macx {
|
# since they always ship with the matching build of the Qt runtime.
|
||||||
CONFIG(release, debug|release) {
|
!disable-prebuilts {
|
||||||
CONFIG += qtquickcompiler
|
win32|macx {
|
||||||
|
CONFIG(release, debug|release) {
|
||||||
|
CONFIG += qtquickcompiler
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,96 +36,139 @@ 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 += \
|
!exists($$PWD/../libs/windows) {
|
||||||
$$PWD/../libs/windows/include \
|
error("Missing dependencies. Please run 'powershell .\setup-deps.ps1' to fetch prebuilt libraries.")
|
||||||
$$(DXSDK_DIR)/Include
|
|
||||||
|
|
||||||
contains(QT_ARCH, i386) {
|
|
||||||
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
|
INCLUDEPATH += $$PWD/../libs/windows/include/x64 $$PWD/../libs/windows/include/x64/SDL2
|
||||||
|
}
|
||||||
|
contains(QT_ARCH, arm64) {
|
||||||
|
LIBS += -L$$PWD/../libs/windows/lib/arm64
|
||||||
|
INCLUDEPATH += $$PWD/../libs/windows/include/arm64 $$PWD/../libs/windows/include/arm64/SDL2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
INCLUDEPATH += $$PWD/../libs/windows/include
|
||||||
LIBS += ws2_32.lib winmm.lib dxva2.lib ole32.lib gdi32.lib user32.lib d3d9.lib dwmapi.lib dbghelp.lib
|
LIBS += ws2_32.lib winmm.lib dxva2.lib ole32.lib gdi32.lib user32.lib d3d9.lib dwmapi.lib dbghelp.lib
|
||||||
}
|
}
|
||||||
macx {
|
macx:!disable-prebuilts {
|
||||||
INCLUDEPATH += $$PWD/../libs/mac/include
|
!exists($$PWD/../libs/mac) {
|
||||||
INCLUDEPATH += $$PWD/../libs/mac/Frameworks/SDL2.framework/Versions/A/Headers
|
error("Missing dependencies. Please run 'python3 setup-deps.py' to fetch prebuilt libraries.")
|
||||||
INCLUDEPATH += $$PWD/../libs/mac/Frameworks/SDL2_ttf.framework/Versions/A/Headers
|
}
|
||||||
LIBS += -L$$PWD/../libs/mac/lib -F$$PWD/../libs/mac/Frameworks
|
|
||||||
|
INCLUDEPATH += $$PWD/../libs/mac/include $$PWD/../libs/mac/include/SDL2
|
||||||
|
LIBS += -L$$PWD/../libs/mac/lib
|
||||||
}
|
}
|
||||||
|
|
||||||
unix:!macx {
|
unix:if(!macx|disable-prebuilts) {
|
||||||
CONFIG += link_pkgconfig
|
CONFIG += link_pkgconfig
|
||||||
PKGCONFIG += openssl sdl2 SDL2_ttf opus
|
PKGCONFIG += openssl sdl2 SDL2_ttf
|
||||||
|
|
||||||
# SLAudio is used on Steam Link
|
# We have our own optimized libopus.a for Steam Link
|
||||||
!config_SL {
|
if(!config_SL|disable-prebuilts) {
|
||||||
CONFIG += soundio
|
PKGCONFIG += opus
|
||||||
}
|
}
|
||||||
|
|
||||||
soundio {
|
!disable-ffmpeg {
|
||||||
packagesExist(libpulse) {
|
packagesExist(libavcodec) {
|
||||||
PKGCONFIG += libpulse
|
PKGCONFIG += libavcodec libavutil libswscale
|
||||||
}
|
CONFIG += ffmpeg
|
||||||
packagesExist(alsa) {
|
|
||||||
PKGCONFIG += alsa
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
packagesExist(libavcodec) {
|
!disable-libva {
|
||||||
PKGCONFIG += libavcodec libavutil
|
packagesExist(libva) {
|
||||||
CONFIG += ffmpeg
|
!disable-x11 {
|
||||||
|
packagesExist(libva-x11) {
|
||||||
packagesExist(libva) {
|
CONFIG += libva-x11
|
||||||
packagesExist(libva-x11) {
|
}
|
||||||
CONFIG += libva-x11
|
}
|
||||||
|
!disable-wayland {
|
||||||
|
packagesExist(libva-wayland) {
|
||||||
|
CONFIG += libva-wayland
|
||||||
|
}
|
||||||
|
}
|
||||||
|
!disable-libdrm {
|
||||||
|
packagesExist(libva-drm) {
|
||||||
|
CONFIG += libva-drm
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CONFIG += libva
|
||||||
|
}
|
||||||
}
|
}
|
||||||
packagesExist(libva-wayland) {
|
|
||||||
CONFIG += libva-wayland
|
!disable-libvdpau {
|
||||||
|
packagesExist(vdpau) {
|
||||||
|
CONFIG += libvdpau
|
||||||
|
}
|
||||||
}
|
}
|
||||||
packagesExist(libva-drm) {
|
|
||||||
CONFIG += libva-drm
|
!disable-mmal {
|
||||||
|
packagesExist(mmal) {
|
||||||
|
PKGCONFIG += mmal
|
||||||
|
CONFIG += mmal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
!disable-libdrm {
|
||||||
|
packagesExist(libdrm) {
|
||||||
|
PKGCONFIG += libdrm
|
||||||
|
CONFIG += libdrm
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Disabled by default due to reliability issues. See #1314.
|
||||||
|
# CUDA interop is superseded by VDPAU and Vulkan Video.
|
||||||
|
enable-cuda {
|
||||||
|
packagesExist(ffnvcodec) {
|
||||||
|
PKGCONFIG += ffnvcodec
|
||||||
|
CONFIG += cuda
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
!disable-libplacebo {
|
||||||
|
packagesExist(libplacebo) {
|
||||||
|
PKGCONFIG += libplacebo
|
||||||
|
CONFIG += libplacebo
|
||||||
|
}
|
||||||
}
|
}
|
||||||
CONFIG += libva
|
|
||||||
}
|
}
|
||||||
|
|
||||||
packagesExist(vdpau) {
|
!disable-wayland {
|
||||||
CONFIG += libvdpau
|
packagesExist(wayland-client) {
|
||||||
|
CONFIG += wayland
|
||||||
|
PKGCONFIG += wayland-client
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
packagesExist(mmal) {
|
!disable-x11 {
|
||||||
PKGCONFIG += mmal
|
packagesExist(x11) {
|
||||||
CONFIG += mmal
|
DEFINES += HAS_X11
|
||||||
}
|
PKGCONFIG += x11
|
||||||
|
}
|
||||||
packagesExist(libdrm) {
|
|
||||||
PKGCONFIG += libdrm
|
|
||||||
CONFIG += libdrm
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
win32 {
|
win32 {
|
||||||
LIBS += -llibssl -llibcrypto -lSDL2 -lSDL2_ttf -lavcodec -lavutil -lopus -ld3dx9
|
LIBS += -llibssl -llibcrypto -lSDL2 -lSDL2_ttf -lavcodec -lavutil -lswscale -lopus -ldxgi -ld3d11 -llibplacebo
|
||||||
CONFIG += ffmpeg
|
CONFIG += ffmpeg libplacebo
|
||||||
}
|
}
|
||||||
win32:!winrt {
|
win32:!winrt {
|
||||||
CONFIG += soundio discord-rpc
|
CONFIG += discord-rpc
|
||||||
}
|
}
|
||||||
macx {
|
macx {
|
||||||
LIBS += -lssl -lcrypto -lavcodec.58 -lavutil.56 -lopus -framework SDL2 -framework SDL2_ttf
|
!disable-prebuilts {
|
||||||
LIBS += -lobjc -framework VideoToolbox -framework AVFoundation -framework CoreVideo -framework CoreGraphics -framework CoreMedia -framework AppKit -framework Metal
|
LIBS += -lssl.3 -lcrypto.3 -lavcodec.62 -lavutil.60 -lswscale.9 -lopus -lSDL2 -lSDL2_ttf
|
||||||
|
CONFIG += discord-rpc
|
||||||
|
}
|
||||||
|
|
||||||
# For libsoundio
|
LIBS += -lobjc -framework VideoToolbox -framework AVFoundation -framework CoreVideo -framework CoreGraphics -framework CoreMedia -framework AppKit -framework Metal -framework QuartzCore
|
||||||
LIBS += -framework CoreAudio -framework AudioUnit
|
CONFIG += ffmpeg
|
||||||
|
|
||||||
CONFIG += ffmpeg soundio discord-rpc
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SOURCES += \
|
SOURCES += \
|
||||||
|
backend/nvaddress.cpp \
|
||||||
|
backend/nvapp.cpp \
|
||||||
|
cli/pair.cpp \
|
||||||
main.cpp \
|
main.cpp \
|
||||||
backend/computerseeker.cpp \
|
backend/computerseeker.cpp \
|
||||||
backend/identitymanager.cpp \
|
backend/identitymanager.cpp \
|
||||||
@@ -133,24 +179,40 @@ SOURCES += \
|
|||||||
backend/boxartmanager.cpp \
|
backend/boxartmanager.cpp \
|
||||||
backend/richpresencemanager.cpp \
|
backend/richpresencemanager.cpp \
|
||||||
cli/commandlineparser.cpp \
|
cli/commandlineparser.cpp \
|
||||||
|
cli/listapps.cpp \
|
||||||
cli/quitstream.cpp \
|
cli/quitstream.cpp \
|
||||||
cli/startstream.cpp \
|
cli/startstream.cpp \
|
||||||
|
settings/compatfetcher.cpp \
|
||||||
|
settings/mappingfetcher.cpp \
|
||||||
settings/streamingpreferences.cpp \
|
settings/streamingpreferences.cpp \
|
||||||
streaming/input.cpp \
|
streaming/input/abstouch.cpp \
|
||||||
|
streaming/input/gamepad.cpp \
|
||||||
|
streaming/input/input.cpp \
|
||||||
|
streaming/input/keyboard.cpp \
|
||||||
|
streaming/input/mouse.cpp \
|
||||||
|
streaming/input/reltouch.cpp \
|
||||||
streaming/session.cpp \
|
streaming/session.cpp \
|
||||||
streaming/audio/audio.cpp \
|
streaming/audio/audio.cpp \
|
||||||
streaming/audio/renderers/sdlaud.cpp \
|
streaming/audio/renderers/sdlaud.cpp \
|
||||||
gui/computermodel.cpp \
|
gui/computermodel.cpp \
|
||||||
gui/appmodel.cpp \
|
gui/appmodel.cpp \
|
||||||
|
streaming/bandwidth.cpp \
|
||||||
streaming/streamutils.cpp \
|
streaming/streamutils.cpp \
|
||||||
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 \
|
streaming/video/overlaymanager.cpp \
|
||||||
backend/systemproperties.cpp
|
backend/systemproperties.cpp \
|
||||||
|
wm.cpp
|
||||||
|
|
||||||
HEADERS += \
|
HEADERS += \
|
||||||
|
SDL_compat.h \
|
||||||
|
backend/nvaddress.h \
|
||||||
|
backend/nvapp.h \
|
||||||
|
cli/pair.h \
|
||||||
|
settings/compatfetcher.h \
|
||||||
|
settings/mappingfetcher.h \
|
||||||
utils.h \
|
utils.h \
|
||||||
backend/computerseeker.h \
|
backend/computerseeker.h \
|
||||||
backend/identitymanager.h \
|
backend/identitymanager.h \
|
||||||
@@ -161,16 +223,18 @@ HEADERS += \
|
|||||||
backend/boxartmanager.h \
|
backend/boxartmanager.h \
|
||||||
backend/richpresencemanager.h \
|
backend/richpresencemanager.h \
|
||||||
cli/commandlineparser.h \
|
cli/commandlineparser.h \
|
||||||
|
cli/listapps.h \
|
||||||
cli/quitstream.h \
|
cli/quitstream.h \
|
||||||
cli/startstream.h \
|
cli/startstream.h \
|
||||||
settings/streamingpreferences.h \
|
settings/streamingpreferences.h \
|
||||||
streaming/input.h \
|
streaming/input/input.h \
|
||||||
streaming/session.h \
|
streaming/session.h \
|
||||||
streaming/audio/renderers/renderer.h \
|
streaming/audio/renderers/renderer.h \
|
||||||
streaming/audio/renderers/sdl.h \
|
streaming/audio/renderers/sdl.h \
|
||||||
gui/computermodel.h \
|
gui/computermodel.h \
|
||||||
gui/appmodel.h \
|
gui/appmodel.h \
|
||||||
streaming/video/decoder.h \
|
streaming/video/decoder.h \
|
||||||
|
streaming/bandwidth.h \
|
||||||
streaming/streamutils.h \
|
streaming/streamutils.h \
|
||||||
backend/autoupdatechecker.h \
|
backend/autoupdatechecker.h \
|
||||||
path.h \
|
path.h \
|
||||||
@@ -186,18 +250,18 @@ ffmpeg {
|
|||||||
DEFINES += HAVE_FFMPEG
|
DEFINES += HAVE_FFMPEG
|
||||||
SOURCES += \
|
SOURCES += \
|
||||||
streaming/video/ffmpeg.cpp \
|
streaming/video/ffmpeg.cpp \
|
||||||
|
streaming/video/ffmpeg-renderers/genhwaccel.cpp \
|
||||||
streaming/video/ffmpeg-renderers/sdlvid.cpp \
|
streaming/video/ffmpeg-renderers/sdlvid.cpp \
|
||||||
streaming/video/ffmpeg-renderers/cuda.cpp \
|
streaming/video/ffmpeg-renderers/swframemapper.cpp \
|
||||||
streaming/video/ffmpeg-renderers/pacer/pacer.cpp \
|
streaming/video/ffmpeg-renderers/pacer/pacer.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/genhwaccel.h \
|
||||||
streaming/video/ffmpeg-renderers/sdlvid.h \
|
streaming/video/ffmpeg-renderers/sdlvid.h \
|
||||||
streaming/video/ffmpeg-renderers/cuda.h \
|
streaming/video/ffmpeg-renderers/swframemapper.h \
|
||||||
streaming/video/ffmpeg-renderers/pacer/pacer.h \
|
streaming/video/ffmpeg-renderers/pacer/pacer.h
|
||||||
streaming/video/ffmpeg-renderers/pacer/nullthreadedvsyncsource.h
|
|
||||||
}
|
}
|
||||||
libva {
|
libva {
|
||||||
message(VAAPI renderer selected)
|
message(VAAPI renderer selected)
|
||||||
@@ -219,7 +283,7 @@ libva-wayland {
|
|||||||
PKGCONFIG += libva-wayland
|
PKGCONFIG += libva-wayland
|
||||||
DEFINES += HAVE_LIBVA_WAYLAND
|
DEFINES += HAVE_LIBVA_WAYLAND
|
||||||
}
|
}
|
||||||
libva-wayland {
|
libva-drm {
|
||||||
message(VAAPI DRM support enabled)
|
message(VAAPI DRM support enabled)
|
||||||
|
|
||||||
PKGCONFIG += libva-drm
|
PKGCONFIG += libva-drm
|
||||||
@@ -238,6 +302,18 @@ mmal {
|
|||||||
DEFINES += HAVE_MMAL
|
DEFINES += HAVE_MMAL
|
||||||
SOURCES += streaming/video/ffmpeg-renderers/mmal.cpp
|
SOURCES += streaming/video/ffmpeg-renderers/mmal.cpp
|
||||||
HEADERS += streaming/video/ffmpeg-renderers/mmal.h
|
HEADERS += streaming/video/ffmpeg-renderers/mmal.h
|
||||||
|
|
||||||
|
# We suppress EGL usage when MMAL is available because MMAL has
|
||||||
|
# significantly better performance than EGL on the Pi. Setting
|
||||||
|
# this option allows EGL usage even if built with MMAL support.
|
||||||
|
#
|
||||||
|
# It is highly recommended to also build with 'gpuslow' to avoid
|
||||||
|
# EGL being preferred if direct DRM rendering is available.
|
||||||
|
allow-egl-with-mmal {
|
||||||
|
message(Allowing EGL usage with MMAL enabled)
|
||||||
|
|
||||||
|
DEFINES += ALLOW_EGL_WITH_MMAL
|
||||||
|
}
|
||||||
}
|
}
|
||||||
libdrm {
|
libdrm {
|
||||||
message(DRM renderer selected)
|
message(DRM renderer selected)
|
||||||
@@ -245,11 +321,60 @@ libdrm {
|
|||||||
DEFINES += HAVE_DRM
|
DEFINES += HAVE_DRM
|
||||||
SOURCES += streaming/video/ffmpeg-renderers/drm.cpp
|
SOURCES += streaming/video/ffmpeg-renderers/drm.cpp
|
||||||
HEADERS += streaming/video/ffmpeg-renderers/drm.h
|
HEADERS += streaming/video/ffmpeg-renderers/drm.h
|
||||||
|
|
||||||
|
linux {
|
||||||
|
!disable-masterhooks {
|
||||||
|
message(Master hooks enabled)
|
||||||
|
DEFINES += HAVE_DRM_MASTER_HOOKS
|
||||||
|
SOURCES += masterhook.c masterhook_internal.c
|
||||||
|
LIBS += -ldl -pthread
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cuda {
|
||||||
|
message(CUDA support enabled)
|
||||||
|
|
||||||
|
DEFINES += HAVE_CUDA
|
||||||
|
SOURCES += streaming/video/ffmpeg-renderers/cuda.cpp
|
||||||
|
HEADERS += streaming/video/ffmpeg-renderers/cuda.h
|
||||||
|
|
||||||
|
# ffnvcodec uses libdl in cuda_load_functions()/cuda_free_functions()
|
||||||
|
LIBS += -ldl
|
||||||
|
}
|
||||||
|
libplacebo {
|
||||||
|
message(Vulkan support enabled via libplacebo)
|
||||||
|
|
||||||
|
DEFINES += HAVE_LIBPLACEBO_VULKAN
|
||||||
|
SOURCES += \
|
||||||
|
streaming/video/ffmpeg-renderers/plvk.cpp \
|
||||||
|
streaming/video/ffmpeg-renderers/plvk_c.c
|
||||||
|
HEADERS += \
|
||||||
|
streaming/video/ffmpeg-renderers/plvk.h
|
||||||
|
}
|
||||||
|
config_EGL {
|
||||||
|
message(EGL renderer selected)
|
||||||
|
|
||||||
|
CONFIG += egl
|
||||||
|
DEFINES += HAVE_EGL
|
||||||
|
SOURCES += \
|
||||||
|
streaming/video/ffmpeg-renderers/eglvid.cpp \
|
||||||
|
streaming/video/ffmpeg-renderers/egl_extensions.cpp \
|
||||||
|
streaming/video/ffmpeg-renderers/eglimagefactory.cpp
|
||||||
|
HEADERS += \
|
||||||
|
streaming/video/ffmpeg-renderers/eglvid.h \
|
||||||
|
streaming/video/ffmpeg-renderers/eglimagefactory.h
|
||||||
}
|
}
|
||||||
config_SL {
|
config_SL {
|
||||||
message(Steam Link build configuration selected)
|
message(Steam Link build configuration selected)
|
||||||
|
|
||||||
DEFINES += STEAM_LINK HAVE_SLVIDEO HAVE_SLAUDIO
|
!disable-prebuilts {
|
||||||
|
# Link against our NEON-optimized libopus build
|
||||||
|
LIBS += -L$$PWD/../libs/steamlink/lib
|
||||||
|
INCLUDEPATH += $$PWD/../libs/steamlink/include
|
||||||
|
LIBS += -lopus -larmasm -lNE10
|
||||||
|
}
|
||||||
|
|
||||||
|
DEFINES += EMBEDDED_BUILD STEAM_LINK HAVE_SLVIDEO HAVE_SLAUDIO
|
||||||
LIBS += -lSLVideo -lSLAudio
|
LIBS += -lSLVideo -lSLAudio
|
||||||
|
|
||||||
SOURCES += \
|
SOURCES += \
|
||||||
@@ -259,44 +384,103 @@ config_SL {
|
|||||||
streaming/video/slvid.h \
|
streaming/video/slvid.h \
|
||||||
streaming/audio/renderers/slaud.h
|
streaming/audio/renderers/slaud.h
|
||||||
}
|
}
|
||||||
|
win32 {
|
||||||
|
HEADERS += streaming/video/ffmpeg-renderers/dxutil.h
|
||||||
|
}
|
||||||
win32:!winrt {
|
win32:!winrt {
|
||||||
message(DXVA2 renderer selected)
|
message(DXVA2 and D3D11VA renderers selected)
|
||||||
|
|
||||||
SOURCES += \
|
SOURCES += \
|
||||||
streaming/video/ffmpeg-renderers/dxva2.cpp \
|
streaming/video/ffmpeg-renderers/dxva2.cpp \
|
||||||
|
streaming/video/ffmpeg-renderers/d3d11va.cpp \
|
||||||
streaming/video/ffmpeg-renderers/pacer/dxvsyncsource.cpp
|
streaming/video/ffmpeg-renderers/pacer/dxvsyncsource.cpp
|
||||||
|
|
||||||
HEADERS += \
|
HEADERS += \
|
||||||
streaming/video/ffmpeg-renderers/dxva2.h \
|
streaming/video/ffmpeg-renderers/dxva2.h \
|
||||||
|
streaming/video/ffmpeg-renderers/d3d11va.h \
|
||||||
streaming/video/ffmpeg-renderers/pacer/dxvsyncsource.h
|
streaming/video/ffmpeg-renderers/pacer/dxvsyncsource.h
|
||||||
}
|
}
|
||||||
macx {
|
macx {
|
||||||
message(VideoToolbox renderer selected)
|
message(VideoToolbox renderer selected)
|
||||||
|
|
||||||
SOURCES += \
|
SOURCES += \
|
||||||
streaming/video/ffmpeg-renderers/vt.mm
|
streaming/video/ffmpeg-renderers/vt_base.mm \
|
||||||
|
streaming/video/ffmpeg-renderers/vt_avsamplelayer.mm \
|
||||||
|
streaming/video/ffmpeg-renderers/vt_metal.mm
|
||||||
|
|
||||||
HEADERS += \
|
HEADERS += \
|
||||||
streaming/video/ffmpeg-renderers/vt.h
|
streaming/video/ffmpeg-renderers/vt.h
|
||||||
}
|
}
|
||||||
soundio {
|
|
||||||
message(libsoundio audio renderer selected)
|
|
||||||
|
|
||||||
DEFINES += HAVE_SOUNDIO SOUNDIO_STATIC_LIBRARY
|
|
||||||
SOURCES += streaming/audio/renderers/soundioaudiorenderer.cpp
|
|
||||||
HEADERS += streaming/audio/renderers/soundioaudiorenderer.h
|
|
||||||
}
|
|
||||||
discord-rpc {
|
discord-rpc {
|
||||||
message(Discord integration enabled)
|
message(Discord integration enabled)
|
||||||
|
|
||||||
LIBS += -ldiscord-rpc
|
LIBS += -ldiscord-rpc
|
||||||
DEFINES += HAVE_DISCORD
|
DEFINES += HAVE_DISCORD
|
||||||
}
|
}
|
||||||
|
embedded {
|
||||||
|
message(Embedded build)
|
||||||
|
|
||||||
|
DEFINES += EMBEDDED_BUILD
|
||||||
|
}
|
||||||
|
glslow {
|
||||||
|
message(GL slow build)
|
||||||
|
|
||||||
|
DEFINES += GL_IS_SLOW
|
||||||
|
}
|
||||||
|
vkslow {
|
||||||
|
message(Vulkan slow build)
|
||||||
|
|
||||||
|
DEFINES += VULKAN_IS_SLOW
|
||||||
|
}
|
||||||
|
gpuslow {
|
||||||
|
message(GPU slow build)
|
||||||
|
|
||||||
|
DEFINES += GL_IS_SLOW VULKAN_IS_SLOW
|
||||||
|
}
|
||||||
|
wayland {
|
||||||
|
message(Wayland extensions enabled)
|
||||||
|
|
||||||
|
DEFINES += HAS_WAYLAND
|
||||||
|
SOURCES += streaming/video/ffmpeg-renderers/pacer/waylandvsyncsource.cpp
|
||||||
|
HEADERS += streaming/video/ffmpeg-renderers/pacer/waylandvsyncsource.h
|
||||||
|
}
|
||||||
|
|
||||||
RESOURCES += \
|
RESOURCES += \
|
||||||
resources.qrc \
|
resources.qrc \
|
||||||
qml.qrc
|
qml.qrc
|
||||||
|
|
||||||
|
TRANSLATIONS += \
|
||||||
|
languages/qml_zh_CN.ts \
|
||||||
|
languages/qml_de.ts \
|
||||||
|
languages/qml_fr.ts \
|
||||||
|
languages/qml_nb_NO.ts \
|
||||||
|
languages/qml_ru.ts \
|
||||||
|
languages/qml_es.ts \
|
||||||
|
languages/qml_ja.ts \
|
||||||
|
languages/qml_vi.ts \
|
||||||
|
languages/qml_th.ts \
|
||||||
|
languages/qml_ko.ts \
|
||||||
|
languages/qml_hu.ts \
|
||||||
|
languages/qml_nl.ts \
|
||||||
|
languages/qml_sv.ts \
|
||||||
|
languages/qml_tr.ts \
|
||||||
|
languages/qml_uk.ts \
|
||||||
|
languages/qml_zh_TW.ts \
|
||||||
|
languages/qml_el.ts \
|
||||||
|
languages/qml_hi.ts \
|
||||||
|
languages/qml_it.ts \
|
||||||
|
languages/qml_pt.ts \
|
||||||
|
languages/qml_pt_BR.ts \
|
||||||
|
languages/qml_pl.ts \
|
||||||
|
languages/qml_cs.ts \
|
||||||
|
languages/qml_he.ts \
|
||||||
|
languages/qml_ckb.ts \
|
||||||
|
languages/qml_lt.ts \
|
||||||
|
languages/qml_et.ts \
|
||||||
|
languages/qml_bg.ts \
|
||||||
|
languages/qml_eo.ts \
|
||||||
|
languages/qml_ta.ts
|
||||||
|
|
||||||
# Additional import path used to resolve QML modules in Qt Creator's code model
|
# Additional import path used to resolve QML modules in Qt Creator's code model
|
||||||
QML_IMPORT_PATH =
|
QML_IMPORT_PATH =
|
||||||
|
|
||||||
@@ -317,15 +501,6 @@ 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
|
||||||
|
|
||||||
soundio {
|
|
||||||
win32:CONFIG(release, debug|release): LIBS += -L$$OUT_PWD/../soundio/release/ -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
|
|
||||||
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
|
||||||
else:unix: LIBS += -L$$OUT_PWD/../h264bitstream/ -lh264bitstream
|
else:unix: LIBS += -L$$OUT_PWD/../h264bitstream/ -lh264bitstream
|
||||||
@@ -384,12 +559,19 @@ macx {
|
|||||||
APP_BUNDLE_RESOURCES.files = moonlight.icns
|
APP_BUNDLE_RESOURCES.files = moonlight.icns
|
||||||
APP_BUNDLE_RESOURCES.path = Contents/Resources
|
APP_BUNDLE_RESOURCES.path = Contents/Resources
|
||||||
|
|
||||||
APP_BUNDLE_FRAMEWORKS.files = $$files(../libs/mac/Frameworks/*.framework, true) $$files(../libs/mac/lib/*.dylib, true)
|
APP_BUNDLE_PLIST.files = $$OUT_PWD/Info.plist
|
||||||
APP_BUNDLE_FRAMEWORKS.path = Contents/Frameworks
|
APP_BUNDLE_PLIST.path = Contents
|
||||||
|
|
||||||
QMAKE_BUNDLE_DATA += APP_BUNDLE_RESOURCES APP_BUNDLE_FRAMEWORKS
|
QMAKE_BUNDLE_DATA += APP_BUNDLE_RESOURCES APP_BUNDLE_PLIST
|
||||||
|
|
||||||
QMAKE_RPATHDIR += @executable_path/../Frameworks
|
!disable-prebuilts {
|
||||||
|
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_FRAMEWORKS
|
||||||
|
|
||||||
|
QMAKE_RPATHDIR += @executable_path/../Frameworks
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
VERSION = "$$cat(version.txt)"
|
VERSION = "$$cat(version.txt)"
|
||||||
|
|||||||
@@ -6,17 +6,18 @@
|
|||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
|
|
||||||
AutoUpdateChecker::AutoUpdateChecker(QObject *parent) :
|
AutoUpdateChecker::AutoUpdateChecker(QObject *parent) :
|
||||||
QObject(parent),
|
QObject(parent)
|
||||||
m_Nam(this)
|
|
||||||
{
|
{
|
||||||
|
m_Nam = new QNetworkAccessManager(this);
|
||||||
|
|
||||||
// Never communicate over HTTP
|
// Never communicate over HTTP
|
||||||
m_Nam.setStrictTransportSecurityEnabled(true);
|
m_Nam->setStrictTransportSecurityEnabled(true);
|
||||||
|
|
||||||
// Allow HTTP redirects
|
// Allow HTTP redirects
|
||||||
m_Nam.setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy);
|
m_Nam->setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy);
|
||||||
|
|
||||||
connect(&m_Nam, SIGNAL(finished(QNetworkReply*)),
|
connect(m_Nam, &QNetworkAccessManager::finished,
|
||||||
this, SLOT(handleUpdateCheckRequestFinished(QNetworkReply*)));
|
this, &AutoUpdateChecker::handleUpdateCheckRequestFinished);
|
||||||
|
|
||||||
QString currentVersion(VERSION_STR);
|
QString currentVersion(VERSION_STR);
|
||||||
qDebug() << "Current Moonlight version:" << currentVersion;
|
qDebug() << "Current Moonlight version:" << currentVersion;
|
||||||
@@ -28,40 +29,98 @@ AutoUpdateChecker::AutoUpdateChecker(QObject *parent) :
|
|||||||
|
|
||||||
void AutoUpdateChecker::start()
|
void AutoUpdateChecker::start()
|
||||||
{
|
{
|
||||||
#if defined(Q_OS_WIN32) || defined(Q_OS_DARWIN) || defined(STEAM_LINK) // Only run update checker on platforms without auto-update
|
if (!m_Nam) {
|
||||||
// HACK: Set network accessibility to work around QTBUG-80947
|
Q_ASSERT(m_Nam);
|
||||||
m_Nam.setNetworkAccessible(QNetworkAccessManager::Accessible);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
#if defined(Q_OS_WIN32) || defined(Q_OS_DARWIN) || defined(STEAM_LINK) || defined(APP_IMAGE) // Only run update checker on platforms without auto-update
|
||||||
|
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) && QT_VERSION < QT_VERSION_CHECK(5, 15, 1) && !defined(QT_NO_BEARERMANAGEMENT)
|
||||||
|
// HACK: Set network accessibility to work around QTBUG-80947 (introduced in Qt 5.14.0 and fixed in Qt 5.15.1)
|
||||||
|
QT_WARNING_PUSH
|
||||||
|
QT_WARNING_DISABLE_DEPRECATED
|
||||||
|
m_Nam->setNetworkAccessible(QNetworkAccessManager::Accessible);
|
||||||
|
QT_WARNING_POP
|
||||||
|
#endif
|
||||||
|
|
||||||
// We'll get a callback when this is finished
|
// We'll get a callback when this is finished
|
||||||
QUrl url("https://moonlight-stream.org/updates/qt.json");
|
QUrl url("https://moonlight-stream.org/updates/qt.json");
|
||||||
m_Nam.get(QNetworkRequest(url));
|
QNetworkRequest request(url);
|
||||||
|
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
|
||||||
|
request.setAttribute(QNetworkRequest::Http2AllowedAttribute, true);
|
||||||
|
#else
|
||||||
|
request.setAttribute(QNetworkRequest::HTTP2AllowedAttribute, true);
|
||||||
|
#endif
|
||||||
|
m_Nam->get(request);
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
void AutoUpdateChecker::parseStringToVersionQuad(QString& string, QVector<int>& version)
|
void AutoUpdateChecker::parseStringToVersionQuad(QString& string, QVector<int>& version)
|
||||||
{
|
{
|
||||||
QStringList list = string.split('.');
|
QStringList list = string.split('.');
|
||||||
for (const QString& component : list) {
|
for (const QString& component : std::as_const(list)) {
|
||||||
version.append(component.toInt());
|
version.append(component.toInt());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QString AutoUpdateChecker::getPlatform()
|
QString AutoUpdateChecker::getPlatform()
|
||||||
{
|
{
|
||||||
#ifdef STEAM_LINK
|
#if defined(STEAM_LINK)
|
||||||
return QStringLiteral("steamlink");
|
return QStringLiteral("steamlink");
|
||||||
|
#elif defined(APP_IMAGE)
|
||||||
|
return QStringLiteral("appimage");
|
||||||
|
#elif defined(Q_OS_DARWIN) && QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||||
|
// Qt 6 changed this from 'osx' to 'macos'. Use the old one
|
||||||
|
// to be consistent (and not require another entry in the manifest).
|
||||||
|
return QStringLiteral("osx");
|
||||||
#else
|
#else
|
||||||
return QSysInfo::productType();
|
return QSysInfo::productType();
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int AutoUpdateChecker::compareVersion(QVector<int>& version1, QVector<int>& version2) {
|
||||||
|
for (int i = 0;; i++) {
|
||||||
|
int v1Val = 0;
|
||||||
|
int v2Val = 0;
|
||||||
|
|
||||||
|
// Treat missing decimal places as 0
|
||||||
|
if (i < version1.count()) {
|
||||||
|
v1Val = version1[i];
|
||||||
|
}
|
||||||
|
if (i < version2.count()) {
|
||||||
|
v2Val = version2[i];
|
||||||
|
}
|
||||||
|
if (i >= version1.count() && i >= version2.count()) {
|
||||||
|
// Equal versions
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (v1Val < v2Val) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
else if (v1Val > v2Val) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void AutoUpdateChecker::handleUpdateCheckRequestFinished(QNetworkReply* reply)
|
void AutoUpdateChecker::handleUpdateCheckRequestFinished(QNetworkReply* reply)
|
||||||
{
|
{
|
||||||
Q_ASSERT(reply->isFinished());
|
Q_ASSERT(reply->isFinished());
|
||||||
|
|
||||||
|
// Delete the QNetworkAccessManager to free resources and
|
||||||
|
// prevent the bearer plugin from polling in the background.
|
||||||
|
m_Nam->deleteLater();
|
||||||
|
m_Nam = nullptr;
|
||||||
|
|
||||||
if (reply->error() == QNetworkReply::NoError) {
|
if (reply->error() == QNetworkReply::NoError) {
|
||||||
QTextStream stream(reply);
|
QTextStream stream(reply);
|
||||||
|
|
||||||
|
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||||
|
stream.setEncoding(QStringConverter::Utf8);
|
||||||
|
#else
|
||||||
stream.setCodec("UTF-8");
|
stream.setCodec("UTF-8");
|
||||||
|
#endif
|
||||||
|
|
||||||
// Read all data and queue the reply for deletion
|
// Read all data and queue the reply for deletion
|
||||||
QString jsonString = stream.readAll();
|
QString jsonString = stream.readAll();
|
||||||
@@ -80,7 +139,7 @@ void AutoUpdateChecker::handleUpdateCheckRequestFinished(QNetworkReply* reply)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (QJsonValueRef updateEntry : array) {
|
for (const auto& updateEntry : std::as_const(array)) {
|
||||||
if (updateEntry.isObject()) {
|
if (updateEntry.isObject()) {
|
||||||
QJsonObject updateObj = updateEntry.toObject();
|
QJsonObject updateObj = updateEntry.toObject();
|
||||||
if (!updateObj.contains("platform") ||
|
if (!updateObj.contains("platform") ||
|
||||||
@@ -101,46 +160,47 @@ void AutoUpdateChecker::handleUpdateCheckRequestFinished(QNetworkReply* reply)
|
|||||||
|
|
||||||
if (updateObj["arch"] == QSysInfo::buildCpuArchitecture() &&
|
if (updateObj["arch"] == QSysInfo::buildCpuArchitecture() &&
|
||||||
updateObj["platform"] == getPlatform()) {
|
updateObj["platform"] == getPlatform()) {
|
||||||
|
|
||||||
|
// Check the kernel version minimum if one exists
|
||||||
|
if (updateObj.contains("kernel_version_at_least") && updateObj["kernel_version_at_least"].isString()) {
|
||||||
|
QVector<int> requiredVersionQuad;
|
||||||
|
QVector<int> actualVersionQuad;
|
||||||
|
|
||||||
|
QString requiredVersion = updateObj["kernel_version_at_least"].toString();
|
||||||
|
QString actualVersion = QSysInfo::kernelVersion();
|
||||||
|
parseStringToVersionQuad(requiredVersion, requiredVersionQuad);
|
||||||
|
parseStringToVersionQuad(actualVersion, actualVersionQuad);
|
||||||
|
|
||||||
|
if (compareVersion(actualVersionQuad, requiredVersionQuad) < 0) {
|
||||||
|
qDebug() << "Skipping manifest entry due to kernel version (" << actualVersion << "<" << requiredVersion << ")";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
qDebug() << "Found update manifest match for current platform";
|
qDebug() << "Found update manifest match for current platform";
|
||||||
|
|
||||||
QString latestVersion = updateObj["version"].toString();
|
QString latestVersion = updateObj["version"].toString();
|
||||||
qDebug() << "Latest version of Moonlight for this platform is:" << latestVersion;
|
qDebug() << "Latest version of Moonlight for this platform is:" << latestVersion;
|
||||||
|
|
||||||
QVector<int> latestVersionQuad;
|
QVector<int> latestVersionQuad;
|
||||||
|
|
||||||
parseStringToVersionQuad(latestVersion, latestVersionQuad);
|
parseStringToVersionQuad(latestVersion, latestVersionQuad);
|
||||||
|
|
||||||
for (int i = 0;; i++) {
|
int res = compareVersion(m_CurrentVersionQuad, latestVersionQuad);
|
||||||
int latestVer = 0;
|
if (res < 0) {
|
||||||
int currentVer = 0;
|
// m_CurrentVersionQuad < latestVersionQuad
|
||||||
|
qDebug() << "Update available";
|
||||||
// Treat missing decimal places as 0
|
emit onUpdateAvailable(updateObj["version"].toString(),
|
||||||
if (i < latestVersionQuad.count()) {
|
updateObj["browser_url"].toString());
|
||||||
latestVer = latestVersionQuad[i];
|
return;
|
||||||
}
|
}
|
||||||
if (i < m_CurrentVersionQuad.count()) {
|
else if (res > 0) {
|
||||||
currentVer = m_CurrentVersionQuad[i];
|
qDebug() << "Update manifest version lower than current version";
|
||||||
}
|
return;
|
||||||
if (i >= latestVersionQuad.count() && i >= m_CurrentVersionQuad.count()) {
|
}
|
||||||
break;
|
else {
|
||||||
}
|
qDebug() << "Update manifest version equal to current version";
|
||||||
|
return;
|
||||||
if (currentVer < latestVer) {
|
|
||||||
qDebug() << "Update available";
|
|
||||||
|
|
||||||
emit onUpdateAvailable(updateObj["version"].toString(),
|
|
||||||
updateObj["browser_url"].toString());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
else if (currentVer > latestVer) {
|
|
||||||
qDebug() << "Update manifest version lower than current version";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
qDebug() << "Update manifest version equal to current version";
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -148,11 +208,11 @@ 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() << getPlatform();
|
<< QSysInfo::buildCpuArchitecture() << getPlatform() << QSysInfo::kernelVersion();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
qWarning() << "Update checking failed with error: " << reply->error();
|
qWarning() << "Update checking failed with error:" << reply->error();
|
||||||
reply->deleteLater();
|
reply->deleteLater();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,8 +20,10 @@ private slots:
|
|||||||
private:
|
private:
|
||||||
void parseStringToVersionQuad(QString& string, QVector<int>& version);
|
void parseStringToVersionQuad(QString& string, QVector<int>& version);
|
||||||
|
|
||||||
|
int compareVersion(QVector<int>& version1, QVector<int>& version2);
|
||||||
|
|
||||||
QString getPlatform();
|
QString getPlatform();
|
||||||
|
|
||||||
QVector<int> m_CurrentVersionQuad;
|
QVector<int> m_CurrentVersionQuad;
|
||||||
QNetworkAccessManager m_Nam;
|
QNetworkAccessManager* m_Nam;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -46,8 +46,8 @@ public:
|
|||||||
m_Computer(computer),
|
m_Computer(computer),
|
||||||
m_App(app)
|
m_App(app)
|
||||||
{
|
{
|
||||||
connect(this, SIGNAL(boxArtFetchCompleted(NvComputer*,NvApp,QUrl)),
|
connect(this, &NetworkBoxArtLoadTask::boxArtFetchCompleted,
|
||||||
boxArtManager, SLOT(handleBoxArtLoadComplete(NvComputer*,NvApp,QUrl)));
|
boxArtManager, &BoxArtManager::handleBoxArtLoadComplete);
|
||||||
}
|
}
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
@@ -71,10 +71,10 @@ private:
|
|||||||
|
|
||||||
QUrl BoxArtManager::loadBoxArt(NvComputer* computer, NvApp& app)
|
QUrl BoxArtManager::loadBoxArt(NvComputer* computer, NvApp& app)
|
||||||
{
|
{
|
||||||
// Try to open the cached file
|
// Try to open the cached file if it exists and contains data
|
||||||
QString cacheFilePath = getFilePathForBoxArt(computer, app.id);
|
QFile cacheFile(getFilePathForBoxArt(computer, app.id));
|
||||||
if (QFile::exists(cacheFilePath)) {
|
if (cacheFile.exists() && cacheFile.size() > 0) {
|
||||||
return QUrl::fromLocalFile(cacheFilePath);
|
return QUrl::fromLocalFile(cacheFile.fileName());
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we get here, we need to fetch asynchronously.
|
// If we get here, we need to fetch asynchronously.
|
||||||
@@ -87,6 +87,16 @@ QUrl BoxArtManager::loadBoxArt(NvComputer* computer, NvApp& app)
|
|||||||
return QUrl("qrc:/res/no_app_image.png");
|
return QUrl("qrc:/res/no_app_image.png");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void BoxArtManager::deleteBoxArt(NvComputer* computer)
|
||||||
|
{
|
||||||
|
QDir dir(Path::getBoxArtCacheDir());
|
||||||
|
|
||||||
|
// Delete everything in this computer's box art directory
|
||||||
|
if (dir.cd(computer->uuid)) {
|
||||||
|
dir.removeRecursively();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void BoxArtManager::handleBoxArtLoadComplete(NvComputer* computer, NvApp app, QUrl image)
|
void BoxArtManager::handleBoxArtLoadComplete(NvComputer* computer, NvApp app, QUrl image)
|
||||||
{
|
{
|
||||||
if (!image.isEmpty()) {
|
if (!image.isEmpty()) {
|
||||||
@@ -96,7 +106,7 @@ void BoxArtManager::handleBoxArtLoadComplete(NvComputer* computer, NvApp app, QU
|
|||||||
|
|
||||||
QUrl BoxArtManager::loadBoxArtFromNetwork(NvComputer* computer, int appId)
|
QUrl BoxArtManager::loadBoxArtFromNetwork(NvComputer* computer, int appId)
|
||||||
{
|
{
|
||||||
NvHTTP http(computer->activeAddress, computer->serverCert);
|
NvHTTP http(computer);
|
||||||
|
|
||||||
QString cachePath = getFilePathForBoxArt(computer, appId);
|
QString cachePath = getFilePathForBoxArt(computer, appId);
|
||||||
QImage image;
|
QImage image;
|
||||||
@@ -109,6 +119,10 @@ QUrl BoxArtManager::loadBoxArtFromNetwork(NvComputer* computer, int appId)
|
|||||||
if (image.save(cachePath)) {
|
if (image.save(cachePath)) {
|
||||||
return QUrl::fromLocalFile(cachePath);
|
return QUrl::fromLocalFile(cachePath);
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
// A failed save() may leave a zero byte file. Make sure that's removed.
|
||||||
|
QFile(cachePath).remove();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return QUrl();
|
return QUrl();
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ public:
|
|||||||
QUrl
|
QUrl
|
||||||
loadBoxArt(NvComputer* computer, NvApp& app);
|
loadBoxArt(NvComputer* computer, NvApp& app);
|
||||||
|
|
||||||
|
static
|
||||||
|
void
|
||||||
|
deleteBoxArt(NvComputer* computer);
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void
|
void
|
||||||
boxArtLoadComplete(NvComputer* computer, NvApp app, QUrl image);
|
boxArtLoadComplete(NvComputer* computer, NvApp app, QUrl image);
|
||||||
|
|||||||
+347
-92
@@ -1,6 +1,7 @@
|
|||||||
#include "computermanager.h"
|
#include "computermanager.h"
|
||||||
|
#include "boxartmanager.h"
|
||||||
#include "nvhttp.h"
|
#include "nvhttp.h"
|
||||||
#include "settings/streamingpreferences.h"
|
#include "nvpairingmanager.h"
|
||||||
|
|
||||||
#include <Limelight.h>
|
#include <Limelight.h>
|
||||||
#include <QtEndian>
|
#include <QtEndian>
|
||||||
@@ -8,8 +9,10 @@
|
|||||||
#include <QThread>
|
#include <QThread>
|
||||||
#include <QThreadPool>
|
#include <QThreadPool>
|
||||||
#include <QCoreApplication>
|
#include <QCoreApplication>
|
||||||
|
#include <QRandomGenerator>
|
||||||
|
|
||||||
#define SER_HOSTS "hosts"
|
#define SER_HOSTS "hosts"
|
||||||
|
#define SER_HOSTS_BACKUP "hostsbackup"
|
||||||
|
|
||||||
class PcMonitorThread : public QThread
|
class PcMonitorThread : public QThread
|
||||||
{
|
{
|
||||||
@@ -26,22 +29,22 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
bool tryPollComputer(QString address, bool& changed)
|
bool tryPollComputer(QNetworkAccessManager* nam, NvAddress address, bool& changed)
|
||||||
{
|
{
|
||||||
NvHTTP http(address, m_Computer->serverCert);
|
NvHTTP http(address, 0, m_Computer->serverCert, nam);
|
||||||
|
|
||||||
QString serverInfo;
|
QString serverInfo;
|
||||||
try {
|
try {
|
||||||
serverInfo = http.getServerInfo(NvHTTP::NvLogLevel::NVLL_NONE);
|
serverInfo = http.getServerInfo(NvHTTP::NvLogLevel::NVLL_NONE, true);
|
||||||
} catch (...) {
|
} catch (...) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
NvComputer newState(address, serverInfo, QSslCertificate());
|
NvComputer newState(http, serverInfo);
|
||||||
|
|
||||||
// Ensure the machine that responded is the one we intended to contact
|
// Ensure the machine that responded is the one we intended to contact
|
||||||
if (m_Computer->uuid != newState.uuid) {
|
if (m_Computer->uuid != newState.uuid) {
|
||||||
qInfo() << "Found unexpected PC " << newState.name << " looking for " << m_Computer->name;
|
qInfo() << "Found unexpected PC" << newState.name << "looking for" << m_Computer->name;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,11 +52,9 @@ private:
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool updateAppList(bool& changed)
|
bool updateAppList(QNetworkAccessManager* nam, bool& changed)
|
||||||
{
|
{
|
||||||
Q_ASSERT(m_Computer->activeAddress != nullptr);
|
NvHTTP http(m_Computer, nam);
|
||||||
|
|
||||||
NvHTTP http(m_Computer->activeAddress, m_Computer->serverCert);
|
|
||||||
|
|
||||||
QVector<NvApp> appList;
|
QVector<NvApp> appList;
|
||||||
|
|
||||||
@@ -67,32 +68,42 @@ private:
|
|||||||
}
|
}
|
||||||
|
|
||||||
QWriteLocker lock(&m_Computer->lock);
|
QWriteLocker lock(&m_Computer->lock);
|
||||||
if (m_Computer->appList != appList) {
|
changed = m_Computer->updateAppList(appList);
|
||||||
m_Computer->appList = appList;
|
|
||||||
m_Computer->sortAppList();
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void run() override
|
void run() override
|
||||||
{
|
{
|
||||||
|
// Reduce the power and performance impact of our
|
||||||
|
// computer status polling while it's running.
|
||||||
|
setPriority(QThread::LowPriority);
|
||||||
|
#if QT_VERSION >= QT_VERSION_CHECK(6, 9, 0)
|
||||||
|
setServiceLevel(QThread::QualityOfService::Eco);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Share the QNetworkAccessManager to conserve resources when polling.
|
||||||
|
// Each instance creates a worker thread, so sharing them ensures that
|
||||||
|
// we are not spamming a new thread for every single polling attempt.
|
||||||
|
//
|
||||||
|
// Since QThread inherit the priority of the current thread, this also
|
||||||
|
// ensures that the NAM's worker thread will inherit our lower priority.
|
||||||
|
QNetworkAccessManager nam;
|
||||||
|
|
||||||
// Always fetch the applist the first time
|
// Always fetch the applist the first time
|
||||||
int pollsSinceLastAppListFetch = POLLS_PER_APPLIST_FETCH;
|
int pollsSinceLastAppListFetch = POLLS_PER_APPLIST_FETCH;
|
||||||
while (!isInterruptionRequested()) {
|
while (!isInterruptionRequested()) {
|
||||||
bool stateChanged = false;
|
bool stateChanged = false;
|
||||||
bool online = false;
|
bool online = false;
|
||||||
bool wasOnline = m_Computer->state == NvComputer::CS_ONLINE;
|
bool wasOnline = m_Computer->state == NvComputer::CS_ONLINE;
|
||||||
for (int i = 0; i < TRIES_BEFORE_OFFLINING && !online; i++) {
|
for (int i = 0; i < (wasOnline ? TRIES_BEFORE_OFFLINING : 1) && !online; i++) {
|
||||||
for (auto& address : m_Computer->uniqueAddresses()) {
|
for (auto& address : m_Computer->uniqueAddresses()) {
|
||||||
if (isInterruptionRequested()) {
|
if (isInterruptionRequested()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tryPollComputer(address, stateChanged)) {
|
if (tryPollComputer(&nam, address, stateChanged)) {
|
||||||
if (!wasOnline) {
|
if (!wasOnline) {
|
||||||
qInfo() << m_Computer->name << "is now online at" << m_Computer->activeAddress;
|
qInfo() << m_Computer->name << "is now online at" << m_Computer->activeAddress.toString();
|
||||||
}
|
}
|
||||||
online = true;
|
online = true;
|
||||||
break;
|
break;
|
||||||
@@ -121,7 +132,7 @@ private:
|
|||||||
stateChanged = false;
|
stateChanged = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (updateAppList(stateChanged)) {
|
if (updateAppList(&nam, stateChanged)) {
|
||||||
pollsSinceLastAppListFetch = 0;
|
pollsSinceLastAppListFetch = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -147,32 +158,65 @@ private:
|
|||||||
NvComputer* m_Computer;
|
NvComputer* m_Computer;
|
||||||
};
|
};
|
||||||
|
|
||||||
ComputerManager::ComputerManager(QObject *parent)
|
ComputerManager::ComputerManager(StreamingPreferences* prefs)
|
||||||
: QObject(parent),
|
: m_Prefs(prefs),
|
||||||
m_PollingRef(0),
|
m_PollingRef(0),
|
||||||
m_MdnsBrowser(nullptr)
|
m_MdnsBrowser(nullptr),
|
||||||
|
m_CompatFetcher(nullptr),
|
||||||
|
m_NeedsDelayedFlush(false)
|
||||||
{
|
{
|
||||||
QSettings settings;
|
QSettings settings;
|
||||||
|
|
||||||
|
// If there's a hosts backup copy, we must have failed to commit
|
||||||
|
// a previous update before exiting. Restore the backup now.
|
||||||
|
int hosts = settings.beginReadArray(SER_HOSTS_BACKUP);
|
||||||
|
if (hosts == 0) {
|
||||||
|
// If there's no host backup, read from the primary location.
|
||||||
|
settings.endArray();
|
||||||
|
hosts = settings.beginReadArray(SER_HOSTS);
|
||||||
|
}
|
||||||
|
|
||||||
// Inflate our hosts from QSettings
|
// Inflate our hosts from QSettings
|
||||||
int hosts = settings.beginReadArray(SER_HOSTS);
|
|
||||||
for (int i = 0; i < hosts; i++) {
|
for (int i = 0; i < hosts; i++) {
|
||||||
settings.setArrayIndex(i);
|
settings.setArrayIndex(i);
|
||||||
NvComputer* computer = new NvComputer(settings);
|
NvComputer* computer = new NvComputer(settings);
|
||||||
m_KnownHosts[computer->uuid] = computer;
|
m_KnownHosts[computer->uuid] = computer;
|
||||||
|
m_LastSerializedHosts[computer->uuid] = *computer;
|
||||||
}
|
}
|
||||||
settings.endArray();
|
settings.endArray();
|
||||||
|
|
||||||
|
// Fetch latest compatibility data asynchronously
|
||||||
|
m_CompatFetcher.start();
|
||||||
|
|
||||||
|
// Start the delayed flush thread to handle saveHosts() calls
|
||||||
|
m_DelayedFlushThread = new DelayedFlushThread(this);
|
||||||
|
m_DelayedFlushThread->start();
|
||||||
|
|
||||||
// To quit in a timely manner, we must block additional requests
|
// To quit in a timely manner, we must block additional requests
|
||||||
// after we receive the aboutToQuit() signal. This is neccessary
|
// after we receive the aboutToQuit() signal. This is necessary
|
||||||
// because NvHTTP uses aboutToQuit() to abort requests in progres
|
// because NvHTTP uses aboutToQuit() to abort requests in progress
|
||||||
// while quitting, however this is a one time signal - additional
|
// while quitting, however this is a one time signal - additional
|
||||||
// requests would not be aborted and block termination.
|
// requests would not be aborted and block termination.
|
||||||
connect(QCoreApplication::instance(), SIGNAL(aboutToQuit()), this, SLOT(handleAboutToQuit()));
|
connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this, &ComputerManager::handleAboutToQuit);
|
||||||
}
|
}
|
||||||
|
|
||||||
ComputerManager::~ComputerManager()
|
ComputerManager::~ComputerManager()
|
||||||
{
|
{
|
||||||
|
// Stop the delayed flush thread before acquiring the lock in write mode
|
||||||
|
// to avoid deadlocking with a flush that needs the lock in read mode.
|
||||||
|
{
|
||||||
|
// Wake the delayed flush thread
|
||||||
|
m_DelayedFlushThread->requestInterruption();
|
||||||
|
m_DelayedFlushCondition.wakeOne();
|
||||||
|
|
||||||
|
// Wait for it to terminate (and finish any pending flush)
|
||||||
|
m_DelayedFlushThread->wait();
|
||||||
|
delete m_DelayedFlushThread;
|
||||||
|
|
||||||
|
// Delayed flushes should have completed by now
|
||||||
|
Q_ASSERT(!m_NeedsDelayedFlush);
|
||||||
|
}
|
||||||
|
|
||||||
QWriteLocker lock(&m_Lock);
|
QWriteLocker lock(&m_Lock);
|
||||||
|
|
||||||
// Delete machines that haven't been resolved yet
|
// Delete machines that haven't been resolved yet
|
||||||
@@ -187,33 +231,95 @@ ComputerManager::~ComputerManager()
|
|||||||
m_MdnsBrowser = nullptr;
|
m_MdnsBrowser = nullptr;
|
||||||
|
|
||||||
// Interrupt polling
|
// Interrupt polling
|
||||||
for (ComputerPollingEntry* entry : m_PollEntries) {
|
for (ComputerPollingEntry* entry : std::as_const(m_PollEntries)) {
|
||||||
entry->interrupt();
|
entry->interrupt();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete all polling entries (and associated threads)
|
// Delete all polling entries (and associated threads)
|
||||||
for (ComputerPollingEntry* entry : m_PollEntries) {
|
for (ComputerPollingEntry* entry : std::as_const(m_PollEntries)) {
|
||||||
delete entry;
|
delete entry;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Destroy all NvComputer objects now that polling is halted
|
// Destroy all NvComputer objects now that polling is halted
|
||||||
for (NvComputer* computer : m_KnownHosts) {
|
for (NvComputer* computer : std::as_const(m_KnownHosts)) {
|
||||||
delete computer;
|
delete computer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void DelayedFlushThread::run() {
|
||||||
|
for (;;) {
|
||||||
|
// Wait for a delayed flush request or an interruption
|
||||||
|
{
|
||||||
|
QMutexLocker locker(&m_ComputerManager->m_DelayedFlushMutex);
|
||||||
|
|
||||||
|
while (!QThread::currentThread()->isInterruptionRequested() && !m_ComputerManager->m_NeedsDelayedFlush) {
|
||||||
|
m_ComputerManager->m_DelayedFlushCondition.wait(&m_ComputerManager->m_DelayedFlushMutex);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bail without flushing if we woke up for an interruption alone.
|
||||||
|
// If we have both an interruption and a flush request, do the flush.
|
||||||
|
if (!m_ComputerManager->m_NeedsDelayedFlush) {
|
||||||
|
Q_ASSERT(QThread::currentThread()->isInterruptionRequested());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset the delayed flush flag to ensure any racing saveHosts() call will set it again
|
||||||
|
m_ComputerManager->m_NeedsDelayedFlush = false;
|
||||||
|
|
||||||
|
// Update the last serialized hosts map under the delayed flush mutex
|
||||||
|
m_ComputerManager->m_LastSerializedHosts.clear();
|
||||||
|
for (const NvComputer* computer : std::as_const(m_ComputerManager->m_KnownHosts)) {
|
||||||
|
// Copy the current state of the NvComputer to allow us to check later if we need
|
||||||
|
// to serialize it again when attribute updates occur.
|
||||||
|
QReadLocker computerLock(&computer->lock);
|
||||||
|
m_ComputerManager->m_LastSerializedHosts[computer->uuid] = *computer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform the flush
|
||||||
|
{
|
||||||
|
QSettings settings;
|
||||||
|
|
||||||
|
// First, write to the backup location
|
||||||
|
settings.beginWriteArray(SER_HOSTS_BACKUP);
|
||||||
|
{
|
||||||
|
QReadLocker lock(&m_ComputerManager->m_Lock);
|
||||||
|
int i = 0;
|
||||||
|
for (const NvComputer* computer : std::as_const(m_ComputerManager->m_KnownHosts)) {
|
||||||
|
settings.setArrayIndex(i++);
|
||||||
|
computer->serialize(settings, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
settings.endArray();
|
||||||
|
|
||||||
|
// Next, write to the primary location
|
||||||
|
settings.remove(SER_HOSTS);
|
||||||
|
settings.beginWriteArray(SER_HOSTS);
|
||||||
|
{
|
||||||
|
QReadLocker lock(&m_ComputerManager->m_Lock);
|
||||||
|
int i = 0;
|
||||||
|
for (const NvComputer* computer : std::as_const(m_ComputerManager->m_KnownHosts)) {
|
||||||
|
settings.setArrayIndex(i++);
|
||||||
|
computer->serialize(settings, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
settings.endArray();
|
||||||
|
|
||||||
|
// Finally, delete the backup copy
|
||||||
|
settings.remove(SER_HOSTS_BACKUP);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void ComputerManager::saveHosts()
|
void ComputerManager::saveHosts()
|
||||||
{
|
{
|
||||||
QSettings settings;
|
Q_ASSERT(m_DelayedFlushThread != nullptr && m_DelayedFlushThread->isRunning());
|
||||||
QReadLocker lock(&m_Lock);
|
|
||||||
|
|
||||||
settings.remove(SER_HOSTS);
|
// Punt to a worker thread because QSettings on macOS can take ages (> 500 ms)
|
||||||
settings.beginWriteArray(SER_HOSTS);
|
// to persist our host list to disk (especially when a host has a bunch of apps).
|
||||||
for (int i = 0; i < m_KnownHosts.count(); i++) {
|
QMutexLocker locker(&m_DelayedFlushMutex);
|
||||||
settings.setArrayIndex(i);
|
m_NeedsDelayedFlush = true;
|
||||||
m_KnownHosts.value(m_KnownHosts.keys()[i])->serialize(settings);
|
m_DelayedFlushCondition.wakeOne();
|
||||||
}
|
|
||||||
settings.endArray();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QHostAddress ComputerManager::getBestGlobalAddressV6(QVector<QHostAddress> &addresses)
|
QHostAddress ComputerManager::getBestGlobalAddressV6(QVector<QHostAddress> &addresses)
|
||||||
@@ -260,16 +366,15 @@ void ComputerManager::startPolling()
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
StreamingPreferences prefs;
|
if (m_Prefs->enableMdns) {
|
||||||
|
|
||||||
if (prefs.enableMdns) {
|
|
||||||
// Start an MDNS query for GameStream hosts
|
// Start an MDNS query for GameStream hosts
|
||||||
m_MdnsBrowser = new QMdnsEngine::Browser(&m_MdnsServer, "_nvstream._tcp.local.", &m_MdnsCache);
|
m_MdnsServer.reset(new QMdnsEngine::Server());
|
||||||
|
m_MdnsBrowser = new QMdnsEngine::Browser(m_MdnsServer.data(), "_nvstream._tcp.local.");
|
||||||
connect(m_MdnsBrowser, &QMdnsEngine::Browser::serviceAdded,
|
connect(m_MdnsBrowser, &QMdnsEngine::Browser::serviceAdded,
|
||||||
this, [this](const QMdnsEngine::Service& service) {
|
this, [this](const QMdnsEngine::Service& service) {
|
||||||
qInfo() << "Discovered mDNS host:" << service.hostname();
|
qInfo() << "Discovered mDNS host:" << service.hostname();
|
||||||
|
|
||||||
MdnsPendingComputer* pendingComputer = new MdnsPendingComputer(&m_MdnsServer, service);
|
MdnsPendingComputer* pendingComputer = new MdnsPendingComputer(m_MdnsServer, service);
|
||||||
connect(pendingComputer, &MdnsPendingComputer::resolvedHost,
|
connect(pendingComputer, &MdnsPendingComputer::resolvedHost,
|
||||||
this, &ComputerManager::handleMdnsServiceResolved);
|
this, &ComputerManager::handleMdnsServiceResolved);
|
||||||
m_PendingResolution.append(pendingComputer);
|
m_PendingResolution.append(pendingComputer);
|
||||||
@@ -305,8 +410,8 @@ void ComputerManager::startPollingComputer(NvComputer* computer)
|
|||||||
|
|
||||||
if (!pollingEntry->isActive()) {
|
if (!pollingEntry->isActive()) {
|
||||||
PcMonitorThread* thread = new PcMonitorThread(computer);
|
PcMonitorThread* thread = new PcMonitorThread(computer);
|
||||||
connect(thread, SIGNAL(computerStateChanged(NvComputer*)),
|
connect(thread, &PcMonitorThread::computerStateChanged,
|
||||||
this, SLOT(handleComputerStateChanged(NvComputer*)));
|
this, &ComputerManager::handleComputerStateChanged);
|
||||||
pollingEntry->setActiveThread(thread);
|
pollingEntry->setActiveThread(thread);
|
||||||
thread->start();
|
thread->start();
|
||||||
}
|
}
|
||||||
@@ -319,13 +424,15 @@ void ComputerManager::handleMdnsServiceResolved(MdnsPendingComputer* computer,
|
|||||||
bool added = false;
|
bool added = false;
|
||||||
|
|
||||||
// Add the host using the IPv4 address
|
// Add the host using the IPv4 address
|
||||||
for (const QHostAddress& address : addresses) {
|
for (const QHostAddress& address : std::as_const(addresses)) {
|
||||||
if (address.protocol() == QAbstractSocket::IPv4Protocol) {
|
if (address.protocol() == QAbstractSocket::IPv4Protocol) {
|
||||||
// NB: We don't just call addNewHost() here with v6Global because the IPv6
|
// 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
|
// 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
|
// or if this host lacks outbound IPv6 capability). We want to add IPv6 even if
|
||||||
// it's not currently reachable.
|
// it's not currently reachable.
|
||||||
addNewHost(address.toString(), true, v6Global);
|
addNewHost(NvAddress(address, computer->port()),
|
||||||
|
true, computer->hostname(),
|
||||||
|
NvAddress(v6Global, computer->port()));
|
||||||
added = true;
|
added = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -333,14 +440,15 @@ void ComputerManager::handleMdnsServiceResolved(MdnsPendingComputer* computer,
|
|||||||
|
|
||||||
if (!added) {
|
if (!added) {
|
||||||
// If we get here, there wasn't an IPv4 address so we'll do it v6-only
|
// If we get here, there wasn't an IPv4 address so we'll do it v6-only
|
||||||
for (const QHostAddress& address : addresses) {
|
for (const QHostAddress& address : std::as_const(addresses)) {
|
||||||
if (address.protocol() == QAbstractSocket::IPv6Protocol) {
|
if (address.protocol() == QAbstractSocket::IPv6Protocol) {
|
||||||
// Use a link-local or site-local address for the "local address"
|
// Use a link-local or site-local address for the "local address"
|
||||||
if (address.isInSubnet(QHostAddress("fe80::"), 10) ||
|
if (address.isInSubnet(QHostAddress("fe80::"), 10) ||
|
||||||
address.isInSubnet(QHostAddress("fec0::"), 10) ||
|
address.isInSubnet(QHostAddress("fec0::"), 10) ||
|
||||||
address.isInSubnet(QHostAddress("fc00::"), 7)) {
|
address.isInSubnet(QHostAddress("fc00::"), 7)) {
|
||||||
addNewHost(address.toString(), true, v6Global);
|
addNewHost(NvAddress(address, computer->port()),
|
||||||
added = true;
|
true, computer->hostname(),
|
||||||
|
NvAddress(v6Global, computer->port()));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -351,6 +459,19 @@ void ComputerManager::handleMdnsServiceResolved(MdnsPendingComputer* computer,
|
|||||||
computer->deleteLater();
|
computer->deleteLater();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ComputerManager::saveHost(NvComputer *computer)
|
||||||
|
{
|
||||||
|
// If no serializable properties changed, don't bother saving hosts
|
||||||
|
QMutexLocker lock(&m_DelayedFlushMutex);
|
||||||
|
QReadLocker computerLock(&computer->lock);
|
||||||
|
if (!m_LastSerializedHosts.value(computer->uuid).isEqualSerialized(*computer)) {
|
||||||
|
// Queue a request for a delayed flush to QSettings outside of the lock
|
||||||
|
computerLock.unlock();
|
||||||
|
lock.unlock();
|
||||||
|
saveHosts();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void ComputerManager::handleComputerStateChanged(NvComputer* computer)
|
void ComputerManager::handleComputerStateChanged(NvComputer* computer)
|
||||||
{
|
{
|
||||||
emit computerStateChanged(computer);
|
emit computerStateChanged(computer);
|
||||||
@@ -360,15 +481,20 @@ void ComputerManager::handleComputerStateChanged(NvComputer* computer)
|
|||||||
emit quitAppCompleted(QVariant());
|
emit quitAppCompleted(QVariant());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save updated hosts to QSettings
|
// Save updates to this host
|
||||||
saveHosts();
|
saveHost(computer);
|
||||||
}
|
}
|
||||||
|
|
||||||
QVector<NvComputer*> ComputerManager::getComputers()
|
QVector<NvComputer*> ComputerManager::getComputers()
|
||||||
{
|
{
|
||||||
QReadLocker lock(&m_Lock);
|
QReadLocker lock(&m_Lock);
|
||||||
|
|
||||||
return QVector<NvComputer*>::fromList(m_KnownHosts.values());
|
// Return a sorted host list
|
||||||
|
auto hosts = QVector<NvComputer*>::fromList(m_KnownHosts.values());
|
||||||
|
std::stable_sort(hosts.begin(), hosts.end(), [](const NvComputer* host1, const NvComputer* host2) {
|
||||||
|
return host1->name.toLower() < host2->name.toLower();
|
||||||
|
});
|
||||||
|
return hosts;
|
||||||
}
|
}
|
||||||
|
|
||||||
class DeferredHostDeletionTask : public QRunnable
|
class DeferredHostDeletionTask : public QRunnable
|
||||||
@@ -392,12 +518,15 @@ public:
|
|||||||
m_ComputerManager->m_KnownHosts.remove(m_Computer->uuid);
|
m_ComputerManager->m_KnownHosts.remove(m_Computer->uuid);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Persist the new host list
|
// Persist the new host list with this computer deleted
|
||||||
m_ComputerManager->saveHosts();
|
m_ComputerManager->saveHosts();
|
||||||
|
|
||||||
// Delete the polling entry first. This will stop all polling threads too.
|
// Delete the polling entry first. This will stop all polling threads too.
|
||||||
delete pollingEntry;
|
delete pollingEntry;
|
||||||
|
|
||||||
|
// Delete cached box art
|
||||||
|
BoxArtManager::deleteBoxArt(m_Computer);
|
||||||
|
|
||||||
// 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.
|
||||||
delete m_Computer;
|
delete m_Computer;
|
||||||
@@ -415,13 +544,32 @@ void ComputerManager::deleteHost(NvComputer* computer)
|
|||||||
QThreadPool::globalInstance()->start(new DeferredHostDeletionTask(this, computer));
|
QThreadPool::globalInstance()->start(new DeferredHostDeletionTask(this, computer));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ComputerManager::renameHost(NvComputer* computer, QString name)
|
||||||
|
{
|
||||||
|
{
|
||||||
|
QWriteLocker lock(&computer->lock);
|
||||||
|
|
||||||
|
computer->name = name;
|
||||||
|
computer->hasCustomName = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify the UI of the state change
|
||||||
|
handleComputerStateChanged(computer);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ComputerManager::clientSideAttributeUpdated(NvComputer* computer)
|
||||||
|
{
|
||||||
|
// Notify the UI of the state change
|
||||||
|
handleComputerStateChanged(computer);
|
||||||
|
}
|
||||||
|
|
||||||
void ComputerManager::handleAboutToQuit()
|
void ComputerManager::handleAboutToQuit()
|
||||||
{
|
{
|
||||||
QWriteLocker lock(&m_Lock);
|
QReadLocker lock(&m_Lock);
|
||||||
|
|
||||||
// Interrupt polling threads immediately, so they
|
// Interrupt polling threads immediately, so they
|
||||||
// avoid making additional requests while quitting
|
// avoid making additional requests while quitting
|
||||||
for (ComputerPollingEntry* entry : m_PollEntries) {
|
for (ComputerPollingEntry* entry : std::as_const(m_PollEntries)) {
|
||||||
entry->interrupt();
|
entry->interrupt();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -432,7 +580,8 @@ class PendingPairingTask : public QObject, public QRunnable
|
|||||||
|
|
||||||
public:
|
public:
|
||||||
PendingPairingTask(ComputerManager* computerManager, NvComputer* computer, QString pin)
|
PendingPairingTask(ComputerManager* computerManager, NvComputer* computer, QString pin)
|
||||||
: m_Computer(computer),
|
: m_ComputerManager(computerManager),
|
||||||
|
m_Computer(computer),
|
||||||
m_Pin(pin)
|
m_Pin(pin)
|
||||||
{
|
{
|
||||||
connect(this, &PendingPairingTask::pairingCompleted,
|
connect(this, &PendingPairingTask::pairingCompleted,
|
||||||
@@ -445,32 +594,41 @@ signals:
|
|||||||
private:
|
private:
|
||||||
void run()
|
void run()
|
||||||
{
|
{
|
||||||
NvPairingManager pairingManager(m_Computer->activeAddress);
|
NvPairingManager pairingManager(m_Computer);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
NvPairingManager::PairState result = pairingManager.pair(m_Computer->appVersion, m_Pin, m_Computer->serverCert);
|
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:
|
||||||
emit pairingCompleted(m_Computer, "The PIN from the PC didn't match. Please try again.");
|
emit pairingCompleted(m_Computer, tr("The PIN from the PC didn't match. Please try again."));
|
||||||
break;
|
break;
|
||||||
case NvPairingManager::PairState::FAILED:
|
case NvPairingManager::PairState::FAILED:
|
||||||
emit pairingCompleted(m_Computer, "Pairing failed. Please try again.");
|
if (m_Computer->currentGameId != 0) {
|
||||||
|
emit pairingCompleted(m_Computer, tr("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."));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
emit pairingCompleted(m_Computer, tr("Pairing failed. Please try again."));
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case NvPairingManager::PairState::ALREADY_IN_PROGRESS:
|
case NvPairingManager::PairState::ALREADY_IN_PROGRESS:
|
||||||
emit pairingCompleted(m_Computer, "Another pairing attempt is already in progress.");
|
emit pairingCompleted(m_Computer, tr("Another pairing attempt is already in progress."));
|
||||||
break;
|
break;
|
||||||
case NvPairingManager::PairState::PAIRED:
|
case NvPairingManager::PairState::PAIRED:
|
||||||
|
// Persist the newly pinned server certificate for this host
|
||||||
|
m_ComputerManager->saveHost(m_Computer);
|
||||||
|
|
||||||
emit pairingCompleted(m_Computer, nullptr);
|
emit pairingCompleted(m_Computer, nullptr);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} catch (const GfeHttpResponseException& e) {
|
} catch (const GfeHttpResponseException& e) {
|
||||||
emit pairingCompleted(m_Computer, "GeForce Experience returned error: " + e.toQString());
|
emit pairingCompleted(m_Computer, tr("GeForce Experience returned error: %1").arg(e.toQString()));
|
||||||
} catch (const QtNetworkReplyException& e) {
|
} catch (const QtNetworkReplyException& e) {
|
||||||
emit pairingCompleted(m_Computer, e.toQString());
|
emit pairingCompleted(m_Computer, e.toQString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ComputerManager* m_ComputerManager;
|
||||||
NvComputer* m_Computer;
|
NvComputer* m_Computer;
|
||||||
QString m_Pin;
|
QString m_Pin;
|
||||||
};
|
};
|
||||||
@@ -501,7 +659,7 @@ signals:
|
|||||||
private:
|
private:
|
||||||
void run()
|
void run()
|
||||||
{
|
{
|
||||||
NvHTTP http(m_Computer->activeAddress, m_Computer->serverCert);
|
NvHTTP http(m_Computer);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (m_Computer->currentGameId != 0) {
|
if (m_Computer->currentGameId != 0) {
|
||||||
@@ -514,8 +672,8 @@ private:
|
|||||||
}
|
}
|
||||||
if (e.getStatusCode() == 599) {
|
if (e.getStatusCode() == 599) {
|
||||||
// 599 is a special code we make a custom message for
|
// 599 is a special code we make a custom message for
|
||||||
emit quitAppFailed("The running game wasn't started by this PC. "
|
emit quitAppFailed(tr("The running game wasn't started by this PC. "
|
||||||
"You must quit the game on the host PC manually or use the device that originally started the game.");
|
"You must quit the game on the host PC manually or use the device that originally started the game."));
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
emit quitAppFailed(e.toQString());
|
emit quitAppFailed(e.toQString());
|
||||||
@@ -557,42 +715,74 @@ void ComputerManager::stopPollingAsync()
|
|||||||
m_PendingResolution.removeFirst();
|
m_PendingResolution.removeFirst();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete the browser to stop discovery
|
// Delete the browser and server to stop discovery and refresh polling
|
||||||
delete m_MdnsBrowser;
|
delete m_MdnsBrowser;
|
||||||
m_MdnsBrowser = nullptr;
|
m_MdnsBrowser = nullptr;
|
||||||
|
m_MdnsServer.reset();
|
||||||
|
|
||||||
// Interrupt all threads, but don't wait for them to terminate
|
// Interrupt all threads, but don't wait for them to terminate
|
||||||
for (ComputerPollingEntry* entry : m_PollEntries) {
|
for (ComputerPollingEntry* entry : std::as_const(m_PollEntries)) {
|
||||||
entry->interrupt();
|
entry->interrupt();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ComputerManager::addNewHostManually(QString address)
|
||||||
|
{
|
||||||
|
QUrl url = QUrl::fromUserInput("moonlight://" + address);
|
||||||
|
if (url.isValid() && !url.host().isEmpty() && url.scheme() == "moonlight") {
|
||||||
|
// If there wasn't a port specified, use the default
|
||||||
|
addNewHost(NvAddress(url.host(), url.port(DEFAULT_HTTP_PORT)), false);
|
||||||
|
}
|
||||||
|
else if (QHostAddress(address).protocol() == QAbstractSocket::IPv6Protocol) {
|
||||||
|
// The user specified an IPv6 literal without URL escaping, so use the default port
|
||||||
|
addNewHost(NvAddress(address, DEFAULT_HTTP_PORT), false);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
emit computerAddCompleted(false, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class PendingAddTask : public QObject, public QRunnable
|
class PendingAddTask : public QObject, public QRunnable
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
PendingAddTask(ComputerManager* computerManager, QString address, QHostAddress mdnsIpv6Address, bool mdns)
|
PendingAddTask(ComputerManager* computerManager, QString name, NvAddress address, NvAddress mdnsIpv6Address, bool mdns)
|
||||||
: m_ComputerManager(computerManager),
|
: m_ComputerManager(computerManager),
|
||||||
|
m_Name(name),
|
||||||
m_Address(address),
|
m_Address(address),
|
||||||
m_MdnsIpv6Address(mdnsIpv6Address),
|
m_MdnsIpv6Address(mdnsIpv6Address),
|
||||||
m_Mdns(mdns)
|
m_Mdns(mdns),
|
||||||
|
m_AboutToQuit(false)
|
||||||
{
|
{
|
||||||
connect(this, &PendingAddTask::computerAddCompleted,
|
connect(this, &PendingAddTask::computerAddCompleted,
|
||||||
computerManager, &ComputerManager::computerAddCompleted);
|
computerManager, &ComputerManager::computerAddCompleted);
|
||||||
connect(this, &PendingAddTask::computerStateChanged,
|
connect(this, &PendingAddTask::computerStateChanged,
|
||||||
computerManager, &ComputerManager::handleComputerStateChanged);
|
computerManager, &ComputerManager::handleComputerStateChanged);
|
||||||
|
connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit,
|
||||||
|
this, &PendingAddTask::handleAboutToQuit);
|
||||||
}
|
}
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void computerAddCompleted(QVariant success);
|
void computerAddCompleted(QVariant success, QVariant detectedPortBlocking);
|
||||||
|
|
||||||
void computerStateChanged(NvComputer* computer);
|
void computerStateChanged(NvComputer* computer);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
void handleAboutToQuit()
|
||||||
|
{
|
||||||
|
m_AboutToQuit = true;
|
||||||
|
}
|
||||||
|
|
||||||
QString fetchServerInfo(NvHTTP& http)
|
QString fetchServerInfo(NvHTTP& http)
|
||||||
{
|
{
|
||||||
QString serverInfo;
|
QString serverInfo;
|
||||||
|
|
||||||
|
// Do nothing if we're quitting
|
||||||
|
if (m_AboutToQuit) {
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// There's a race condition between GameStream servers reporting presence over
|
// There's a race condition between GameStream servers reporting presence over
|
||||||
// mDNS and the HTTPS server being ready to respond to our queries. To work
|
// mDNS and the HTTPS server being ready to respond to our queries. To work
|
||||||
@@ -615,7 +805,19 @@ private:
|
|||||||
return serverInfo;
|
return serverInfo;
|
||||||
} catch (...) {
|
} catch (...) {
|
||||||
if (!m_Mdns) {
|
if (!m_Mdns) {
|
||||||
emit computerAddCompleted(false);
|
unsigned int portTestResult;
|
||||||
|
|
||||||
|
if (m_ComputerManager->m_Prefs->detectNetworkBlocking) {
|
||||||
|
// We failed to connect to the specified PC. Let's test to make sure this network
|
||||||
|
// isn't blocking Moonlight, so we can tell the user about it.
|
||||||
|
portTestResult = LiTestClientConnectivity("qt.conntest.moonlight-stream.org", 443,
|
||||||
|
ML_PORT_FLAG_TCP_47984 | ML_PORT_FLAG_TCP_47989);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
portTestResult = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
emit computerAddCompleted(false, portTestResult != 0 && portTestResult != ML_TEST_RESULT_INCONCLUSIVE);
|
||||||
}
|
}
|
||||||
return QString();
|
return QString();
|
||||||
}
|
}
|
||||||
@@ -623,15 +825,26 @@ private:
|
|||||||
|
|
||||||
void run()
|
void run()
|
||||||
{
|
{
|
||||||
NvHTTP http(m_Address, QSslCertificate());
|
NvHTTP http(m_Address, 0, QSslCertificate());
|
||||||
|
|
||||||
qInfo() << "Processing new PC at" << m_Address << "from" << (m_Mdns ? "mDNS" : "user") << m_MdnsIpv6Address;
|
if (m_Mdns) {
|
||||||
|
if (m_MdnsIpv6Address.isNull()) {
|
||||||
|
qInfo() << "Processing new PC" << m_Name << "from mDNS with local address" << m_Address.toString();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
qInfo() << "Processing new PC" << m_Name << "from mDNS with local address" << m_Address.toString()
|
||||||
|
<< "and IPv6 address" << m_MdnsIpv6Address.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
qInfo() << "Processing new PC at" << m_Address.toString() << "from user";
|
||||||
|
}
|
||||||
|
|
||||||
// Perform initial serverinfo fetch over HTTP since we don't know which cert to use
|
// Perform initial serverinfo fetch over HTTP since we don't know which cert to use
|
||||||
QString serverInfo = fetchServerInfo(http);
|
QString serverInfo = fetchServerInfo(http);
|
||||||
if (serverInfo.isEmpty() && !m_MdnsIpv6Address.isNull()) {
|
if (serverInfo.isEmpty() && !m_MdnsIpv6Address.isNull()) {
|
||||||
// Retry using the global IPv6 address if the IPv4 or link-local IPv6 address fails
|
// Retry using the global IPv6 address if the IPv4 or link-local IPv6 address fails
|
||||||
http.setAddress(m_MdnsIpv6Address.toString());
|
http.setAddress(m_MdnsIpv6Address);
|
||||||
serverInfo = fetchServerInfo(http);
|
serverInfo = fetchServerInfo(http);
|
||||||
}
|
}
|
||||||
if (serverInfo.isEmpty()) {
|
if (serverInfo.isEmpty()) {
|
||||||
@@ -639,7 +852,7 @@ private:
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create initial newComputer using HTTP serverinfo with no pinned cert
|
// Create initial newComputer using HTTP serverinfo with no pinned cert
|
||||||
NvComputer* newComputer = new NvComputer(http.address(), serverInfo, QSslCertificate());
|
NvComputer* newComputer = new NvComputer(http, serverInfo);
|
||||||
|
|
||||||
// Check if we have a record of this host UUID to pull the pinned cert
|
// Check if we have a record of this host UUID to pull the pinned cert
|
||||||
NvComputer* existingComputer;
|
NvComputer* existingComputer;
|
||||||
@@ -653,13 +866,14 @@ private:
|
|||||||
|
|
||||||
// Fetch serverinfo again over HTTPS with the pinned cert
|
// Fetch serverinfo again over HTTPS with the pinned cert
|
||||||
if (existingComputer != nullptr) {
|
if (existingComputer != nullptr) {
|
||||||
|
Q_ASSERT(http.httpsPort() != 0);
|
||||||
serverInfo = fetchServerInfo(http);
|
serverInfo = fetchServerInfo(http);
|
||||||
if (serverInfo.isEmpty()) {
|
if (serverInfo.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the polled computer with the HTTPS information
|
// Update the polled computer with the HTTPS information
|
||||||
NvComputer httpsComputer(http.address(), serverInfo, QSslCertificate());
|
NvComputer httpsComputer(http, serverInfo);
|
||||||
newComputer->update(httpsComputer);
|
newComputer->update(httpsComputer);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -673,11 +887,11 @@ private:
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get the WAN IP address using STUN if we're on mDNS over IPv4
|
// Get the WAN IP address using STUN if we're on mDNS over IPv4
|
||||||
if (QHostAddress(newComputer->localAddress).protocol() == QAbstractSocket::IPv4Protocol) {
|
if (QHostAddress(newComputer->localAddress.address()).protocol() == QAbstractSocket::IPv4Protocol) {
|
||||||
quint32 addr;
|
quint32 addr;
|
||||||
int err = LiFindExternalAddressIP4("stun.moonlight-stream.org", 3478, &addr);
|
int err = LiFindExternalAddressIP4("stun.moonlight-stream.org", 3478, &addr);
|
||||||
if (err == 0) {
|
if (err == 0) {
|
||||||
newComputer->remoteAddress = QHostAddress(qFromBigEndian(addr)).toString();
|
newComputer->setRemoteAddress(QHostAddress(qFromBigEndian(addr)));
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
qWarning() << "STUN failed to get WAN address:" << err;
|
qWarning() << "STUN failed to get WAN address:" << err;
|
||||||
@@ -685,34 +899,55 @@ private:
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!m_MdnsIpv6Address.isNull()) {
|
if (!m_MdnsIpv6Address.isNull()) {
|
||||||
Q_ASSERT(m_MdnsIpv6Address.protocol() == QAbstractSocket::IPv6Protocol);
|
Q_ASSERT(QHostAddress(m_MdnsIpv6Address.address()).protocol() == QAbstractSocket::IPv6Protocol);
|
||||||
newComputer->ipv6Address = m_MdnsIpv6Address.toString();
|
newComputer->ipv6Address = m_MdnsIpv6Address;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
newComputer->manualAddress = m_Address;
|
newComputer->manualAddress = m_Address;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QHostAddress hostAddress(m_Address.address());
|
||||||
|
bool addressIsSiteLocalV4 =
|
||||||
|
hostAddress.isInSubnet(QHostAddress("10.0.0.0"), 8) ||
|
||||||
|
hostAddress.isInSubnet(QHostAddress("172.16.0.0"), 12) ||
|
||||||
|
hostAddress.isInSubnet(QHostAddress("192.168.0.0"), 16);
|
||||||
|
|
||||||
{
|
{
|
||||||
// Check if this PC already exists
|
// Check if this PC already exists using opportunistic read lock
|
||||||
QWriteLocker lock(&m_ComputerManager->m_Lock);
|
m_ComputerManager->m_Lock.lockForRead();
|
||||||
NvComputer* existingComputer = m_ComputerManager->m_KnownHosts.value(newComputer->uuid);
|
NvComputer* existingComputer = m_ComputerManager->m_KnownHosts.value(newComputer->uuid);
|
||||||
|
|
||||||
|
// If it doesn't already exist, convert to a write lock in preparation for updating.
|
||||||
|
//
|
||||||
|
// NB: ComputerManager's lock protects the host map itself, not the elements inside.
|
||||||
|
// Those are protected by their individual locks. Since we only mutate the map itself
|
||||||
|
// when the PC doesn't exist, we need the lock in write-mode for that case only.
|
||||||
|
if (existingComputer == nullptr) {
|
||||||
|
m_ComputerManager->m_Lock.unlock();
|
||||||
|
m_ComputerManager->m_Lock.lockForWrite();
|
||||||
|
|
||||||
|
// Since we had to unlock to lock for write, someone could have raced and added
|
||||||
|
// this PC before us. We have to check again whether it already exists.
|
||||||
|
existingComputer = m_ComputerManager->m_KnownHosts.value(newComputer->uuid);
|
||||||
|
}
|
||||||
|
|
||||||
if (existingComputer != nullptr) {
|
if (existingComputer != nullptr) {
|
||||||
// Fold it into the existing PC
|
// Fold it into the existing PC
|
||||||
bool changed = existingComputer->update(*newComputer);
|
bool changed = existingComputer->update(*newComputer);
|
||||||
delete newComputer;
|
delete newComputer;
|
||||||
|
|
||||||
// Drop the lock before notifying
|
// Drop the lock before notifying
|
||||||
lock.unlock();
|
m_ComputerManager->m_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, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tell our client if something changed
|
// Tell our client if something changed
|
||||||
if (changed) {
|
if (changed) {
|
||||||
qInfo() << existingComputer->name << "is now at" << existingComputer->activeAddress;
|
qInfo() << existingComputer->name << "is now at" << existingComputer->activeAddress.toString();
|
||||||
emit computerStateChanged(existingComputer);
|
emit computerStateChanged(existingComputer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -724,11 +959,24 @@ private:
|
|||||||
m_ComputerManager->startPollingComputer(newComputer);
|
m_ComputerManager->startPollingComputer(newComputer);
|
||||||
|
|
||||||
// Drop the lock before notifying
|
// Drop the lock before notifying
|
||||||
lock.unlock();
|
m_ComputerManager->m_Lock.unlock();
|
||||||
|
|
||||||
|
// If this wasn't added via mDNS but it is a RFC 1918 IPv4 address and not a VPN,
|
||||||
|
// go ahead and do the STUN request now to populate an external address.
|
||||||
|
if (!m_Mdns && addressIsSiteLocalV4 && newComputer->getActiveAddressReachability() != NvComputer::RI_VPN) {
|
||||||
|
quint32 addr;
|
||||||
|
int err = LiFindExternalAddressIP4("stun.moonlight-stream.org", 3478, &addr);
|
||||||
|
if (err == 0) {
|
||||||
|
newComputer->setRemoteAddress(QHostAddress(qFromBigEndian(addr)));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
qWarning() << "STUN failed to get WAN address:" << err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 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, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tell our client about this new PC
|
// Tell our client about this new PC
|
||||||
@@ -738,17 +986,24 @@ private:
|
|||||||
}
|
}
|
||||||
|
|
||||||
ComputerManager* m_ComputerManager;
|
ComputerManager* m_ComputerManager;
|
||||||
QString m_Address;
|
QString m_Name;
|
||||||
QHostAddress m_MdnsIpv6Address;
|
NvAddress m_Address;
|
||||||
|
NvAddress m_MdnsIpv6Address;
|
||||||
bool m_Mdns;
|
bool m_Mdns;
|
||||||
|
bool m_AboutToQuit;
|
||||||
};
|
};
|
||||||
|
|
||||||
void ComputerManager::addNewHost(QString address, bool mdns, QHostAddress mdnsIpv6Address)
|
void ComputerManager::addNewHost(NvAddress address, bool mdns, QString name, NvAddress 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, mdnsIpv6Address, mdns);
|
PendingAddTask* addTask = new PendingAddTask(this, name, address, mdnsIpv6Address, mdns);
|
||||||
QThreadPool::globalInstance()->start(addTask);
|
QThreadPool::globalInstance()->start(addTask);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QString ComputerManager::generatePinString()
|
||||||
|
{
|
||||||
|
return QString::asprintf("%04u", QRandomGenerator::system()->bounded(10000));
|
||||||
|
}
|
||||||
|
|
||||||
#include "computermanager.moc"
|
#include "computermanager.moc"
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "nvcomputer.h"
|
#include "nvcomputer.h"
|
||||||
#include "nvpairingmanager.h"
|
#include "settings/streamingpreferences.h"
|
||||||
|
#include "settings/compatfetcher.h"
|
||||||
|
|
||||||
#include <qmdnsengine/server.h>
|
#include <qmdnsengine/server.h>
|
||||||
#include <qmdnsengine/cache.h>
|
#include <qmdnsengine/cache.h>
|
||||||
@@ -14,16 +15,38 @@
|
|||||||
#include <QSettings>
|
#include <QSettings>
|
||||||
#include <QRunnable>
|
#include <QRunnable>
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
|
#include <QMutex>
|
||||||
|
#include <QWaitCondition>
|
||||||
|
|
||||||
|
class ComputerManager;
|
||||||
|
|
||||||
|
class DelayedFlushThread : public QThread
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
DelayedFlushThread(ComputerManager* cm)
|
||||||
|
: m_ComputerManager(cm)
|
||||||
|
{
|
||||||
|
setObjectName("CM Delayed Flush Thread");
|
||||||
|
}
|
||||||
|
|
||||||
|
void run();
|
||||||
|
|
||||||
|
private:
|
||||||
|
ComputerManager* m_ComputerManager;
|
||||||
|
};
|
||||||
|
|
||||||
class MdnsPendingComputer : public QObject
|
class MdnsPendingComputer : public QObject
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit MdnsPendingComputer(QMdnsEngine::Server* server,
|
explicit MdnsPendingComputer(const QSharedPointer<QMdnsEngine::Server> server,
|
||||||
const QMdnsEngine::Service& service)
|
const QMdnsEngine::Service& service)
|
||||||
: m_Hostname(service.hostname()),
|
: m_Hostname(service.hostname()),
|
||||||
m_Server(server),
|
m_Port(service.port()),
|
||||||
|
m_ServerWeak(server),
|
||||||
m_Resolver(nullptr)
|
m_Resolver(nullptr)
|
||||||
{
|
{
|
||||||
// Start resolving
|
// Start resolving
|
||||||
@@ -40,13 +63,24 @@ public:
|
|||||||
return m_Hostname;
|
return m_Hostname;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
uint16_t port()
|
||||||
|
{
|
||||||
|
return m_Port;
|
||||||
|
}
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void handleResolvedTimeout()
|
void handleResolvedTimeout()
|
||||||
{
|
{
|
||||||
if (m_Addresses.isEmpty()) {
|
if (m_Addresses.isEmpty()) {
|
||||||
// Try again
|
if (m_Retries-- > 0) {
|
||||||
qInfo() << "Resolving" << hostname() << "timed out. Retrying...";
|
// Try again
|
||||||
resolve();
|
qInfo() << "Resolving" << hostname() << "timed out. Retrying...";
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
qWarning() << "Giving up on resolving" << hostname() << "after repeated failures";
|
||||||
|
cleanup();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
Q_ASSERT(!m_Addresses.isEmpty());
|
Q_ASSERT(!m_Addresses.isEmpty());
|
||||||
@@ -56,7 +90,6 @@ private slots:
|
|||||||
|
|
||||||
void handleResolvedAddress(const QHostAddress& address)
|
void handleResolvedAddress(const QHostAddress& address)
|
||||||
{
|
{
|
||||||
qInfo() << "Resolved" << hostname() << "to" << address;
|
|
||||||
m_Addresses.push_back(address);
|
m_Addresses.push_back(address);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,19 +97,41 @@ signals:
|
|||||||
void resolvedHost(MdnsPendingComputer*,QVector<QHostAddress>&);
|
void resolvedHost(MdnsPendingComputer*,QVector<QHostAddress>&);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
void cleanup()
|
||||||
|
{
|
||||||
|
// Delete our resolver, so we're guaranteed that nothing is referencing m_Server.
|
||||||
|
delete m_Resolver;
|
||||||
|
m_Resolver = nullptr;
|
||||||
|
|
||||||
|
// Now delete our strong reference that we held on behalf of m_Resolver.
|
||||||
|
// The server may be destroyed after we make this call.
|
||||||
|
m_Server.reset();
|
||||||
|
}
|
||||||
|
|
||||||
void resolve()
|
void resolve()
|
||||||
{
|
{
|
||||||
delete m_Resolver;
|
// Clean up any existing resolver object and server references
|
||||||
m_Resolver = new QMdnsEngine::Resolver(m_Server, m_Hostname);
|
cleanup();
|
||||||
|
|
||||||
|
// Re-acquire a strong reference if the server still exists.
|
||||||
|
m_Server = m_ServerWeak.toStrongRef();
|
||||||
|
if (!m_Server) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_Resolver = new QMdnsEngine::Resolver(m_Server.data(), m_Hostname);
|
||||||
connect(m_Resolver, &QMdnsEngine::Resolver::resolved,
|
connect(m_Resolver, &QMdnsEngine::Resolver::resolved,
|
||||||
this, &MdnsPendingComputer::handleResolvedAddress);
|
this, &MdnsPendingComputer::handleResolvedAddress);
|
||||||
QTimer::singleShot(2000, this, SLOT(handleResolvedTimeout()));
|
QTimer::singleShot(2000, this, &MdnsPendingComputer::handleResolvedTimeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
QByteArray m_Hostname;
|
QByteArray m_Hostname;
|
||||||
QMdnsEngine::Server* m_Server;
|
uint16_t m_Port;
|
||||||
|
QWeakPointer<QMdnsEngine::Server> m_ServerWeak;
|
||||||
|
QSharedPointer<QMdnsEngine::Server> m_Server;
|
||||||
QMdnsEngine::Resolver* m_Resolver;
|
QMdnsEngine::Resolver* m_Resolver;
|
||||||
QVector<QHostAddress> m_Addresses;
|
QVector<QHostAddress> m_Addresses;
|
||||||
|
int m_Retries = 10;
|
||||||
};
|
};
|
||||||
|
|
||||||
class ComputerPollingEntry
|
class ComputerPollingEntry
|
||||||
@@ -95,7 +150,7 @@ public:
|
|||||||
// interrupt() should have taken care of this
|
// interrupt() should have taken care of this
|
||||||
Q_ASSERT(m_ActiveThread == nullptr);
|
Q_ASSERT(m_ActiveThread == nullptr);
|
||||||
|
|
||||||
for (QThread* thread : m_InactiveList) {
|
for (QThread* thread : std::as_const(m_InactiveList)) {
|
||||||
thread->wait();
|
thread->wait();
|
||||||
delete thread;
|
delete thread;
|
||||||
}
|
}
|
||||||
@@ -158,9 +213,11 @@ class ComputerManager : public QObject
|
|||||||
|
|
||||||
friend class DeferredHostDeletionTask;
|
friend class DeferredHostDeletionTask;
|
||||||
friend class PendingAddTask;
|
friend class PendingAddTask;
|
||||||
|
friend class PendingPairingTask;
|
||||||
|
friend class DelayedFlushThread;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit ComputerManager(QObject *parent = nullptr);
|
explicit ComputerManager(StreamingPreferences* prefs);
|
||||||
|
|
||||||
virtual ~ComputerManager();
|
virtual ~ComputerManager();
|
||||||
|
|
||||||
@@ -168,7 +225,11 @@ public:
|
|||||||
|
|
||||||
Q_INVOKABLE void stopPollingAsync();
|
Q_INVOKABLE void stopPollingAsync();
|
||||||
|
|
||||||
Q_INVOKABLE void addNewHost(QString address, bool mdns, QHostAddress mdnsIpv6Address = QHostAddress());
|
Q_INVOKABLE void addNewHostManually(QString address);
|
||||||
|
|
||||||
|
void addNewHost(NvAddress address, bool mdns, QString name = QString(), NvAddress mdnsIpv6Address = NvAddress());
|
||||||
|
|
||||||
|
QString generatePinString();
|
||||||
|
|
||||||
void pairHost(NvComputer* computer, QString pin);
|
void pairHost(NvComputer* computer, QString pin);
|
||||||
|
|
||||||
@@ -179,12 +240,16 @@ public:
|
|||||||
// computer is deleted inside this call
|
// computer is deleted inside this call
|
||||||
void deleteHost(NvComputer* computer);
|
void deleteHost(NvComputer* computer);
|
||||||
|
|
||||||
|
void renameHost(NvComputer* computer, QString name);
|
||||||
|
|
||||||
|
void clientSideAttributeUpdated(NvComputer* computer);
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void computerStateChanged(NvComputer* computer);
|
void computerStateChanged(NvComputer* computer);
|
||||||
|
|
||||||
void pairingCompleted(NvComputer* computer, QString error);
|
void pairingCompleted(NvComputer* computer, QString error);
|
||||||
|
|
||||||
void computerAddCompleted(QVariant success);
|
void computerAddCompleted(QVariant success, QVariant detectedPortBlocking);
|
||||||
|
|
||||||
void quitAppCompleted(QVariant error);
|
void quitAppCompleted(QVariant error);
|
||||||
|
|
||||||
@@ -198,16 +263,24 @@ private slots:
|
|||||||
private:
|
private:
|
||||||
void saveHosts();
|
void saveHosts();
|
||||||
|
|
||||||
|
void saveHost(NvComputer* computer);
|
||||||
|
|
||||||
QHostAddress getBestGlobalAddressV6(QVector<QHostAddress>& addresses);
|
QHostAddress getBestGlobalAddressV6(QVector<QHostAddress>& addresses);
|
||||||
|
|
||||||
void startPollingComputer(NvComputer* computer);
|
void startPollingComputer(NvComputer* computer);
|
||||||
|
|
||||||
|
StreamingPreferences* m_Prefs;
|
||||||
int m_PollingRef;
|
int m_PollingRef;
|
||||||
QReadWriteLock m_Lock;
|
QReadWriteLock m_Lock;
|
||||||
QMap<QString, NvComputer*> m_KnownHosts;
|
QMap<QString, NvComputer*> m_KnownHosts;
|
||||||
QMap<QString, ComputerPollingEntry*> m_PollEntries;
|
QMap<QString, ComputerPollingEntry*> m_PollEntries;
|
||||||
QMdnsEngine::Server m_MdnsServer;
|
QHash<QString, NvComputer> m_LastSerializedHosts; // Protected by m_DelayedFlushMutex
|
||||||
|
QSharedPointer<QMdnsEngine::Server> m_MdnsServer;
|
||||||
QMdnsEngine::Browser* m_MdnsBrowser;
|
QMdnsEngine::Browser* m_MdnsBrowser;
|
||||||
QMdnsEngine::Cache m_MdnsCache;
|
|
||||||
QVector<MdnsPendingComputer*> m_PendingResolution;
|
QVector<MdnsPendingComputer*> m_PendingResolution;
|
||||||
|
CompatFetcher m_CompatFetcher;
|
||||||
|
DelayedFlushThread* m_DelayedFlushThread;
|
||||||
|
QMutex m_DelayedFlushMutex; // Lock ordering: Must never be acquired while holding NvComputer lock
|
||||||
|
QWaitCondition m_DelayedFlushCondition;
|
||||||
|
bool m_NeedsDelayedFlush;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,6 +6,14 @@ ComputerSeeker::ComputerSeeker(ComputerManager *manager, QString computerName, Q
|
|||||||
: QObject(parent), m_ComputerManager(manager), m_ComputerName(computerName),
|
: QObject(parent), m_ComputerManager(manager), m_ComputerName(computerName),
|
||||||
m_TimeoutTimer(new QTimer(this))
|
m_TimeoutTimer(new QTimer(this))
|
||||||
{
|
{
|
||||||
|
// If we know this computer, send a WOL packet to wake it up in case it is asleep.
|
||||||
|
const auto computers = m_ComputerManager->getComputers();
|
||||||
|
for (NvComputer* computer : computers) {
|
||||||
|
if (this->matchComputer(computer)) {
|
||||||
|
computer->wake();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
m_TimeoutTimer->setSingleShot(true);
|
m_TimeoutTimer->setSingleShot(true);
|
||||||
connect(m_TimeoutTimer, &QTimer::timeout,
|
connect(m_TimeoutTimer, &QTimer::timeout,
|
||||||
this, &ComputerSeeker::onTimeout);
|
this, &ComputerSeeker::onTimeout);
|
||||||
@@ -20,7 +28,7 @@ void ComputerSeeker::start(int timeout)
|
|||||||
// if m_ComputerName is UUID, or the name that doesn't resolve to an IP
|
// 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
|
// address) and by polling it using mDNS, hopefully one of these methods
|
||||||
// would find the host
|
// would find the host
|
||||||
m_ComputerManager->addNewHost(m_ComputerName, false);
|
m_ComputerManager->addNewHostManually(m_ComputerName);
|
||||||
m_ComputerManager->startPolling();
|
m_ComputerManager->startPolling();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,8 +52,9 @@ bool ComputerSeeker::matchComputer(NvComputer *computer) const
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const QString& addr : computer->uniqueAddresses()) {
|
const auto uniqueAddresses = computer->uniqueAddresses();
|
||||||
if (addr.toLower() == value) {
|
for (const NvAddress& addr : uniqueAddresses) {
|
||||||
|
if (addr.address().toLower() == value || addr.toString().toLower() == value) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
|
|
||||||
#include <openssl/pem.h>
|
#include <openssl/pem.h>
|
||||||
#include <openssl/rsa.h>
|
#include <openssl/rsa.h>
|
||||||
#include <openssl/bn.h>
|
|
||||||
#include <openssl/x509.h>
|
#include <openssl/x509.h>
|
||||||
#include <openssl/rand.h>
|
#include <openssl/rand.h>
|
||||||
|
|
||||||
@@ -32,19 +31,23 @@ void IdentityManager::createCredentials(QSettings& settings)
|
|||||||
X509* cert = X509_new();
|
X509* cert = X509_new();
|
||||||
THROW_BAD_ALLOC_IF_NULL(cert);
|
THROW_BAD_ALLOC_IF_NULL(cert);
|
||||||
|
|
||||||
EVP_PKEY* pk = EVP_PKEY_new();
|
#if OPENSSL_VERSION_NUMBER >= 0x30000000L
|
||||||
|
EVP_PKEY* pk = EVP_RSA_gen(2048);
|
||||||
THROW_BAD_ALLOC_IF_NULL(pk);
|
THROW_BAD_ALLOC_IF_NULL(pk);
|
||||||
|
#else
|
||||||
|
EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, NULL);
|
||||||
|
THROW_BAD_ALLOC_IF_NULL(ctx);
|
||||||
|
|
||||||
BIGNUM* bne = BN_new();
|
EVP_PKEY_keygen_init(ctx);
|
||||||
THROW_BAD_ALLOC_IF_NULL(bne);
|
EVP_PKEY_CTX_set_rsa_keygen_bits(ctx, 2048);
|
||||||
|
|
||||||
RSA* rsa = RSA_new();
|
// pk must be initialized on input
|
||||||
THROW_BAD_ALLOC_IF_NULL(rsa);
|
EVP_PKEY* pk = NULL;
|
||||||
|
EVP_PKEY_keygen(ctx, &pk);
|
||||||
|
|
||||||
BN_set_word(bne, RSA_F4);
|
EVP_PKEY_CTX_free(ctx);
|
||||||
RSA_generate_key_ex(rsa, 2048, bne, nullptr);
|
THROW_BAD_ALLOC_IF_NULL(pk);
|
||||||
|
#endif
|
||||||
EVP_PKEY_assign_RSA(pk, rsa);
|
|
||||||
|
|
||||||
X509_set_version(cert, 2);
|
X509_set_version(cert, 2);
|
||||||
ASN1_INTEGER_set(X509_get_serialNumber(cert), 0);
|
ASN1_INTEGER_set(X509_get_serialNumber(cert), 0);
|
||||||
@@ -52,28 +55,20 @@ void IdentityManager::createCredentials(QSettings& settings)
|
|||||||
X509_gmtime_adj(X509_get_notBefore(cert), 0);
|
X509_gmtime_adj(X509_get_notBefore(cert), 0);
|
||||||
X509_gmtime_adj(X509_get_notAfter(cert), 60 * 60 * 24 * 365 * 20); // 20 yrs
|
X509_gmtime_adj(X509_get_notAfter(cert), 60 * 60 * 24 * 365 * 20); // 20 yrs
|
||||||
#else
|
#else
|
||||||
ASN1_TIME* before = ASN1_STRING_dup(X509_get0_notBefore(cert));
|
X509_gmtime_adj(X509_getm_notBefore(cert), 0);
|
||||||
THROW_BAD_ALLOC_IF_NULL(before);
|
X509_gmtime_adj(X509_getm_notAfter(cert), 60 * 60 * 24 * 365 * 20); // 20 yrs
|
||||||
ASN1_TIME* after = ASN1_STRING_dup(X509_get0_notAfter(cert));
|
|
||||||
THROW_BAD_ALLOC_IF_NULL(after);
|
|
||||||
|
|
||||||
X509_gmtime_adj(before, 0);
|
|
||||||
X509_gmtime_adj(after, 60 * 60 * 24 * 365 * 20); // 20 yrs
|
|
||||||
|
|
||||||
X509_set1_notBefore(cert, before);
|
|
||||||
X509_set1_notAfter(cert, after);
|
|
||||||
|
|
||||||
ASN1_STRING_free(before);
|
|
||||||
ASN1_STRING_free(after);
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
X509_set_pubkey(cert, pk);
|
X509_set_pubkey(cert, pk);
|
||||||
|
|
||||||
X509_NAME* name = X509_get_subject_name(cert);
|
X509_NAME* name = X509_NAME_new();
|
||||||
|
THROW_BAD_ALLOC_IF_NULL(name);
|
||||||
X509_NAME_add_entry_by_txt(name, "CN", MBSTRING_ASC,
|
X509_NAME_add_entry_by_txt(name, "CN", MBSTRING_ASC,
|
||||||
reinterpret_cast<unsigned char *>(const_cast<char*>("NVIDIA GameStream Client")),
|
reinterpret_cast<unsigned char *>(const_cast<char*>("NVIDIA GameStream Client")),
|
||||||
-1, -1, 0);
|
-1, -1, 0);
|
||||||
|
X509_set_subject_name(cert, name);
|
||||||
X509_set_issuer_name(cert, name);
|
X509_set_issuer_name(cert, name);
|
||||||
|
X509_NAME_free(name);
|
||||||
|
|
||||||
X509_sign(cert, pk, EVP_sha256());
|
X509_sign(cert, pk, EVP_sha256());
|
||||||
|
|
||||||
@@ -94,10 +89,17 @@ void IdentityManager::createCredentials(QSettings& settings)
|
|||||||
|
|
||||||
X509_free(cert);
|
X509_free(cert);
|
||||||
EVP_PKEY_free(pk);
|
EVP_PKEY_free(pk);
|
||||||
BN_free(bne);
|
|
||||||
BIO_free(biokey);
|
BIO_free(biokey);
|
||||||
BIO_free(biocert);
|
BIO_free(biocert);
|
||||||
|
|
||||||
|
// Check that the new keypair is valid before persisting it
|
||||||
|
if (getSslCertificate().isNull()) {
|
||||||
|
qFatal("Newly generated certificate is unreadable");
|
||||||
|
}
|
||||||
|
if (getSslKey().isNull()) {
|
||||||
|
qFatal("Newly generated private key is unreadable");
|
||||||
|
}
|
||||||
|
|
||||||
settings.setValue(SER_CERT, m_CachedPemCert);
|
settings.setValue(SER_CERT, m_CachedPemCert);
|
||||||
settings.setValue(SER_KEY, m_CachedPrivateKey);
|
settings.setValue(SER_KEY, m_CachedPrivateKey);
|
||||||
|
|
||||||
@@ -126,10 +128,10 @@ IdentityManager::IdentityManager()
|
|||||||
|
|
||||||
// We should have valid credentials now. If not, we're screwed
|
// We should have valid credentials now. If not, we're screwed
|
||||||
if (getSslCertificate().isNull()) {
|
if (getSslCertificate().isNull()) {
|
||||||
qFatal("Newly generated certificate is unreadable");
|
qFatal("Certificate is unreadable");
|
||||||
}
|
}
|
||||||
if (getSslKey().isNull()) {
|
if (getSslKey().isNull()) {
|
||||||
qFatal("Newly generated private key is unreadable");
|
qFatal("Private key is unreadable");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,7 +148,10 @@ QSslKey
|
|||||||
IdentityManager::getSslKey()
|
IdentityManager::getSslKey()
|
||||||
{
|
{
|
||||||
if (m_CachedSslKey.isNull()) {
|
if (m_CachedSslKey.isNull()) {
|
||||||
BIO* bio = BIO_new_mem_buf(m_CachedPrivateKey.data(), -1);
|
// This seemingly useless const_cast is required for old OpenSSL headers
|
||||||
|
// where BIO_new_mem_buf's parameter is not declared const like those on
|
||||||
|
// the Steam Link hardware.
|
||||||
|
BIO* bio = BIO_new_mem_buf(const_cast<char*>(m_CachedPrivateKey.constData()), -1);
|
||||||
THROW_BAD_ALLOC_IF_NULL(bio);
|
THROW_BAD_ALLOC_IF_NULL(bio);
|
||||||
|
|
||||||
EVP_PKEY* pk = PEM_read_bio_PrivateKey(bio, nullptr, nullptr, nullptr);
|
EVP_PKEY* pk = PEM_read_bio_PrivateKey(bio, nullptr, nullptr, nullptr);
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
#include "nvaddress.h"
|
||||||
|
|
||||||
|
#include <QHostAddress>
|
||||||
|
|
||||||
|
NvAddress::NvAddress()
|
||||||
|
{
|
||||||
|
setAddress(nullptr);
|
||||||
|
setPort(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
NvAddress::NvAddress(QString addr, uint16_t port)
|
||||||
|
{
|
||||||
|
setAddress(addr);
|
||||||
|
setPort(port);
|
||||||
|
}
|
||||||
|
|
||||||
|
NvAddress::NvAddress(QHostAddress addr, uint16_t port)
|
||||||
|
{
|
||||||
|
setAddress(addr);
|
||||||
|
setPort(port);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t NvAddress::port() const
|
||||||
|
{
|
||||||
|
return m_Port;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString NvAddress::address() const
|
||||||
|
{
|
||||||
|
return m_Address;
|
||||||
|
}
|
||||||
|
|
||||||
|
void NvAddress::setPort(uint16_t port)
|
||||||
|
{
|
||||||
|
m_Port = port;
|
||||||
|
}
|
||||||
|
|
||||||
|
void NvAddress::setAddress(QString addr)
|
||||||
|
{
|
||||||
|
m_Address = addr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void NvAddress::setAddress(QHostAddress addr)
|
||||||
|
{
|
||||||
|
m_Address = addr.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool NvAddress::isNull() const
|
||||||
|
{
|
||||||
|
return m_Address.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString NvAddress::toString() const
|
||||||
|
{
|
||||||
|
if (m_Address.isEmpty()) {
|
||||||
|
return "<NULL>";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (QHostAddress(m_Address).protocol() == QAbstractSocket::IPv6Protocol) {
|
||||||
|
return QString("[%1]:%2").arg(m_Address).arg(m_Port);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return QString("%1:%2").arg(m_Address).arg(m_Port);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QHostAddress>
|
||||||
|
|
||||||
|
#define DEFAULT_HTTP_PORT 47989
|
||||||
|
#define DEFAULT_HTTPS_PORT 47984
|
||||||
|
|
||||||
|
class NvAddress
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
NvAddress();
|
||||||
|
explicit NvAddress(QString addr, uint16_t port);
|
||||||
|
explicit NvAddress(QHostAddress addr, uint16_t port);
|
||||||
|
|
||||||
|
uint16_t port() const;
|
||||||
|
void setPort(uint16_t port);
|
||||||
|
|
||||||
|
QString address() const;
|
||||||
|
void setAddress(QString addr);
|
||||||
|
void setAddress(QHostAddress addr);
|
||||||
|
|
||||||
|
bool isNull() const;
|
||||||
|
QString toString() const;
|
||||||
|
|
||||||
|
bool operator==(const NvAddress& other) const
|
||||||
|
{
|
||||||
|
return m_Address == other.m_Address &&
|
||||||
|
m_Port == other.m_Port;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool operator!=(const NvAddress& other) const
|
||||||
|
{
|
||||||
|
return !operator==(other);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
QString m_Address;
|
||||||
|
uint16_t m_Port;
|
||||||
|
};
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
#include "nvapp.h"
|
||||||
|
|
||||||
|
#define SER_APPNAME "name"
|
||||||
|
#define SER_APPID "id"
|
||||||
|
#define SER_APPHDR "hdr"
|
||||||
|
#define SER_APPCOLLECTOR "appcollector"
|
||||||
|
#define SER_HIDDEN "hidden"
|
||||||
|
#define SER_DIRECTLAUNCH "directlaunch"
|
||||||
|
|
||||||
|
NvApp::NvApp(QSettings& settings)
|
||||||
|
{
|
||||||
|
name = settings.value(SER_APPNAME).toString();
|
||||||
|
id = settings.value(SER_APPID).toInt();
|
||||||
|
hdrSupported = settings.value(SER_APPHDR).toBool();
|
||||||
|
isAppCollectorGame = settings.value(SER_APPCOLLECTOR).toBool();
|
||||||
|
hidden = settings.value(SER_HIDDEN).toBool();
|
||||||
|
directLaunch = settings.value(SER_DIRECTLAUNCH).toBool();
|
||||||
|
}
|
||||||
|
|
||||||
|
void NvApp::serialize(QSettings& settings) const
|
||||||
|
{
|
||||||
|
settings.setValue(SER_APPNAME, name);
|
||||||
|
settings.setValue(SER_APPID, id);
|
||||||
|
settings.setValue(SER_APPHDR, hdrSupported);
|
||||||
|
settings.setValue(SER_APPCOLLECTOR, isAppCollectorGame);
|
||||||
|
settings.setValue(SER_HIDDEN, hidden);
|
||||||
|
settings.setValue(SER_DIRECTLAUNCH, directLaunch);
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QSettings>
|
||||||
|
|
||||||
|
class NvApp
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
NvApp() {}
|
||||||
|
explicit NvApp(QSettings& settings);
|
||||||
|
|
||||||
|
bool operator==(const NvApp& other) const
|
||||||
|
{
|
||||||
|
return id == other.id &&
|
||||||
|
name == other.name &&
|
||||||
|
hdrSupported == other.hdrSupported &&
|
||||||
|
isAppCollectorGame == other.isAppCollectorGame &&
|
||||||
|
hidden == other.hidden &&
|
||||||
|
directLaunch == other.directLaunch;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool operator!=(const NvApp& other) const
|
||||||
|
{
|
||||||
|
return !operator==(other);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isInitialized()
|
||||||
|
{
|
||||||
|
return id != 0 && !name.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
serialize(QSettings& settings) const;
|
||||||
|
|
||||||
|
int id = 0;
|
||||||
|
QString name;
|
||||||
|
bool hdrSupported = false;
|
||||||
|
bool isAppCollectorGame = false;
|
||||||
|
bool hidden = false;
|
||||||
|
bool directLaunch = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
Q_DECLARE_METATYPE(NvApp)
|
||||||
+279
-97
@@ -1,4 +1,6 @@
|
|||||||
#include "nvcomputer.h"
|
#include "nvcomputer.h"
|
||||||
|
#include "nvapp.h"
|
||||||
|
#include "settings/compatfetcher.h"
|
||||||
|
|
||||||
#include <QUdpSocket>
|
#include <QUdpSocket>
|
||||||
#include <QHostInfo>
|
#include <QHostInfo>
|
||||||
@@ -9,43 +11,46 @@
|
|||||||
#define SER_UUID "uuid"
|
#define SER_UUID "uuid"
|
||||||
#define SER_MAC "mac"
|
#define SER_MAC "mac"
|
||||||
#define SER_LOCALADDR "localaddress"
|
#define SER_LOCALADDR "localaddress"
|
||||||
|
#define SER_LOCALPORT "localport"
|
||||||
#define SER_REMOTEADDR "remoteaddress"
|
#define SER_REMOTEADDR "remoteaddress"
|
||||||
|
#define SER_REMOTEPORT "remoteport"
|
||||||
#define SER_MANUALADDR "manualaddress"
|
#define SER_MANUALADDR "manualaddress"
|
||||||
|
#define SER_MANUALPORT "manualport"
|
||||||
#define SER_IPV6ADDR "ipv6address"
|
#define SER_IPV6ADDR "ipv6address"
|
||||||
|
#define SER_IPV6PORT "ipv6port"
|
||||||
#define SER_APPLIST "apps"
|
#define SER_APPLIST "apps"
|
||||||
#define SER_SRVCERT "srvcert"
|
#define SER_SRVCERT "srvcert"
|
||||||
|
#define SER_CUSTOMNAME "customname"
|
||||||
#define SER_APPNAME "name"
|
#define SER_NVIDIASOFTWARE "nvidiasw"
|
||||||
#define SER_APPID "id"
|
|
||||||
#define SER_APPHDR "hdr"
|
|
||||||
|
|
||||||
NvComputer::NvComputer(QSettings& settings)
|
NvComputer::NvComputer(QSettings& settings)
|
||||||
{
|
{
|
||||||
this->name = settings.value(SER_NAME).toString();
|
this->name = settings.value(SER_NAME).toString();
|
||||||
this->uuid = settings.value(SER_UUID).toString();
|
this->uuid = settings.value(SER_UUID).toString();
|
||||||
|
this->hasCustomName = settings.value(SER_CUSTOMNAME).toBool();
|
||||||
this->macAddress = settings.value(SER_MAC).toByteArray();
|
this->macAddress = settings.value(SER_MAC).toByteArray();
|
||||||
this->localAddress = settings.value(SER_LOCALADDR).toString();
|
this->localAddress = NvAddress(settings.value(SER_LOCALADDR).toString(),
|
||||||
this->remoteAddress = settings.value(SER_REMOTEADDR).toString();
|
settings.value(SER_LOCALPORT, QVariant(DEFAULT_HTTP_PORT)).toUInt());
|
||||||
this->ipv6Address = settings.value(SER_IPV6ADDR).toString();
|
this->remoteAddress = NvAddress(settings.value(SER_REMOTEADDR).toString(),
|
||||||
this->manualAddress = settings.value(SER_MANUALADDR).toString();
|
settings.value(SER_REMOTEPORT, QVariant(DEFAULT_HTTP_PORT)).toUInt());
|
||||||
|
this->ipv6Address = NvAddress(settings.value(SER_IPV6ADDR).toString(),
|
||||||
|
settings.value(SER_IPV6PORT, QVariant(DEFAULT_HTTP_PORT)).toUInt());
|
||||||
|
this->manualAddress = NvAddress(settings.value(SER_MANUALADDR).toString(),
|
||||||
|
settings.value(SER_MANUALPORT, QVariant(DEFAULT_HTTP_PORT)).toUInt());
|
||||||
this->serverCert = QSslCertificate(settings.value(SER_SRVCERT).toByteArray());
|
this->serverCert = QSslCertificate(settings.value(SER_SRVCERT).toByteArray());
|
||||||
|
this->isNvidiaServerSoftware = settings.value(SER_NVIDIASOFTWARE).toBool();
|
||||||
|
|
||||||
int appCount = settings.beginReadArray(SER_APPLIST);
|
int appCount = settings.beginReadArray(SER_APPLIST);
|
||||||
|
this->appList.reserve(appCount);
|
||||||
for (int i = 0; i < appCount; i++) {
|
for (int i = 0; i < appCount; i++) {
|
||||||
NvApp app;
|
|
||||||
|
|
||||||
settings.setArrayIndex(i);
|
settings.setArrayIndex(i);
|
||||||
|
|
||||||
app.name = settings.value(SER_APPNAME).toString();
|
NvApp app(settings);
|
||||||
app.id = settings.value(SER_APPID).toInt();
|
|
||||||
app.hdrSupported = settings.value(SER_APPHDR).toBool();
|
|
||||||
|
|
||||||
this->appList.append(app);
|
this->appList.append(app);
|
||||||
}
|
}
|
||||||
settings.endArray();
|
settings.endArray();
|
||||||
sortAppList();
|
sortAppList();
|
||||||
|
|
||||||
this->activeAddress = nullptr;
|
|
||||||
this->currentGameId = 0;
|
this->currentGameId = 0;
|
||||||
this->pairState = PS_UNKNOWN;
|
this->pairState = PS_UNKNOWN;
|
||||||
this->state = CS_UNKNOWN;
|
this->state = CS_UNKNOWN;
|
||||||
@@ -55,36 +60,66 @@ NvComputer::NvComputer(QSettings& settings)
|
|||||||
this->serverCodecModeSupport = 0;
|
this->serverCodecModeSupport = 0;
|
||||||
this->pendingQuit = false;
|
this->pendingQuit = false;
|
||||||
this->gpuModel = nullptr;
|
this->gpuModel = nullptr;
|
||||||
|
this->isSupportedServerVersion = true;
|
||||||
|
this->externalPort = this->remoteAddress.port();
|
||||||
|
this->activeHttpsPort = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
void NvComputer::serialize(QSettings& settings)
|
void NvComputer::setRemoteAddress(QHostAddress address)
|
||||||
|
{
|
||||||
|
QWriteLocker lock(&this->lock);
|
||||||
|
|
||||||
|
Q_ASSERT(this->externalPort != 0);
|
||||||
|
|
||||||
|
this->remoteAddress = NvAddress(address, this->externalPort);
|
||||||
|
}
|
||||||
|
|
||||||
|
void NvComputer::serialize(QSettings& settings, bool serializeApps) const
|
||||||
{
|
{
|
||||||
QReadLocker lock(&this->lock);
|
QReadLocker lock(&this->lock);
|
||||||
|
|
||||||
settings.setValue(SER_NAME, name);
|
settings.setValue(SER_NAME, name);
|
||||||
|
settings.setValue(SER_CUSTOMNAME, hasCustomName);
|
||||||
settings.setValue(SER_UUID, uuid);
|
settings.setValue(SER_UUID, uuid);
|
||||||
settings.setValue(SER_MAC, macAddress);
|
settings.setValue(SER_MAC, macAddress);
|
||||||
settings.setValue(SER_LOCALADDR, localAddress);
|
settings.setValue(SER_LOCALADDR, localAddress.address());
|
||||||
settings.setValue(SER_REMOTEADDR, remoteAddress);
|
settings.setValue(SER_LOCALPORT, localAddress.port());
|
||||||
settings.setValue(SER_IPV6ADDR, ipv6Address);
|
settings.setValue(SER_REMOTEADDR, remoteAddress.address());
|
||||||
settings.setValue(SER_MANUALADDR, manualAddress);
|
settings.setValue(SER_REMOTEPORT, remoteAddress.port());
|
||||||
|
settings.setValue(SER_IPV6ADDR, ipv6Address.address());
|
||||||
|
settings.setValue(SER_IPV6PORT, ipv6Address.port());
|
||||||
|
settings.setValue(SER_MANUALADDR, manualAddress.address());
|
||||||
|
settings.setValue(SER_MANUALPORT, manualAddress.port());
|
||||||
settings.setValue(SER_SRVCERT, serverCert.toPem());
|
settings.setValue(SER_SRVCERT, serverCert.toPem());
|
||||||
|
settings.setValue(SER_NVIDIASOFTWARE, isNvidiaServerSoftware);
|
||||||
|
|
||||||
// Avoid deleting an existing applist if we couldn't get one
|
// Avoid deleting an existing applist if we couldn't get one
|
||||||
if (!appList.isEmpty()) {
|
if (!appList.isEmpty() && serializeApps) {
|
||||||
settings.remove(SER_APPLIST);
|
settings.remove(SER_APPLIST);
|
||||||
settings.beginWriteArray(SER_APPLIST);
|
settings.beginWriteArray(SER_APPLIST);
|
||||||
for (int i = 0; i < appList.count(); i++) {
|
for (int i = 0; i < appList.count(); i++) {
|
||||||
settings.setArrayIndex(i);
|
settings.setArrayIndex(i);
|
||||||
|
appList.at(i).serialize(settings);
|
||||||
settings.setValue(SER_APPNAME, appList[i].name);
|
|
||||||
settings.setValue(SER_APPID, appList[i].id);
|
|
||||||
settings.setValue(SER_APPHDR, appList[i].hdrSupported);
|
|
||||||
}
|
}
|
||||||
settings.endArray();
|
settings.endArray();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool NvComputer::isEqualSerialized(const NvComputer &that) const
|
||||||
|
{
|
||||||
|
return this->name == that.name &&
|
||||||
|
this->hasCustomName == that.hasCustomName &&
|
||||||
|
this->uuid == that.uuid &&
|
||||||
|
this->macAddress == that.macAddress &&
|
||||||
|
this->localAddress == that.localAddress &&
|
||||||
|
this->remoteAddress == that.remoteAddress &&
|
||||||
|
this->ipv6Address == that.ipv6Address &&
|
||||||
|
this->manualAddress == that.manualAddress &&
|
||||||
|
this->serverCert == that.serverCert &&
|
||||||
|
this->isNvidiaServerSoftware == that.isNvidiaServerSoftware &&
|
||||||
|
this->appList == that.appList;
|
||||||
|
}
|
||||||
|
|
||||||
void NvComputer::sortAppList()
|
void NvComputer::sortAppList()
|
||||||
{
|
{
|
||||||
std::stable_sort(appList.begin(), appList.end(), [](const NvApp& app1, const NvApp& app2) {
|
std::stable_sort(appList.begin(), appList.end(), [](const NvApp& app1, const NvApp& app2) {
|
||||||
@@ -92,10 +127,11 @@ void NvComputer::sortAppList()
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
NvComputer::NvComputer(QString address, QString serverInfo, QSslCertificate serverCert)
|
NvComputer::NvComputer(NvHTTP& http, QString serverInfo)
|
||||||
{
|
{
|
||||||
this->serverCert = serverCert;
|
this->serverCert = http.serverCert();
|
||||||
|
|
||||||
|
this->hasCustomName = false;
|
||||||
this->name = NvHTTP::getXmlString(serverInfo, "hostname");
|
this->name = NvHTTP::getXmlString(serverInfo, "hostname");
|
||||||
if (this->name.isEmpty()) {
|
if (this->name.isEmpty()) {
|
||||||
this->name = "UNKNOWN";
|
this->name = "UNKNOWN";
|
||||||
@@ -105,7 +141,7 @@ NvComputer::NvComputer(QString address, QString serverInfo, QSslCertificate serv
|
|||||||
QString newMacString = NvHTTP::getXmlString(serverInfo, "mac");
|
QString newMacString = NvHTTP::getXmlString(serverInfo, "mac");
|
||||||
if (newMacString != "00:00:00:00:00:00") {
|
if (newMacString != "00:00:00:00:00:00") {
|
||||||
QStringList macOctets = newMacString.split(':');
|
QStringList macOctets = newMacString.split(':');
|
||||||
for (QString macOctet : macOctets) {
|
for (const QString& macOctet : std::as_const(macOctets)) {
|
||||||
this->macAddress.append((char) macOctet.toInt(nullptr, 16));
|
this->macAddress.append((char) macOctet.toInt(nullptr, 16));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -115,7 +151,8 @@ NvComputer::NvComputer(QString address, QString serverInfo, QSslCertificate serv
|
|||||||
this->serverCodecModeSupport = codecSupport.toInt();
|
this->serverCodecModeSupport = codecSupport.toInt();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
this->serverCodecModeSupport = 0;
|
// Assume H.264 is always supported
|
||||||
|
this->serverCodecModeSupport = SCM_H264;
|
||||||
}
|
}
|
||||||
|
|
||||||
QString maxLumaPixelsHEVC = NvHTTP::getXmlString(serverInfo, "MaxLumaPixelsHEVC");
|
QString maxLumaPixelsHEVC = NvHTTP::getXmlString(serverInfo, "MaxLumaPixelsHEVC");
|
||||||
@@ -129,61 +166,104 @@ NvComputer::NvComputer(QString address, QString serverInfo, QSslCertificate serv
|
|||||||
this->displayModes = NvHTTP::getDisplayModeList(serverInfo);
|
this->displayModes = NvHTTP::getDisplayModeList(serverInfo);
|
||||||
std::stable_sort(this->displayModes.begin(), this->displayModes.end(),
|
std::stable_sort(this->displayModes.begin(), this->displayModes.end(),
|
||||||
[](const NvDisplayMode& mode1, const NvDisplayMode& mode2) {
|
[](const NvDisplayMode& mode1, const NvDisplayMode& mode2) {
|
||||||
return mode1.width * mode1.height * mode1.refreshRate <
|
return (uint64_t)mode1.width * mode1.height * mode1.refreshRate <
|
||||||
mode2.width * mode2.height * mode2.refreshRate;
|
(uint64_t)mode2.width * mode2.height * mode2.refreshRate;
|
||||||
});
|
});
|
||||||
|
|
||||||
// We can get an IPv4 loopback address if we're using the GS IPv6 Forwarder
|
// We can get an IPv4 loopback address if we're using the GS IPv6 Forwarder
|
||||||
this->localAddress = NvHTTP::getXmlString(serverInfo, "LocalIP");
|
this->localAddress = NvAddress(NvHTTP::getXmlString(serverInfo, "LocalIP"), http.httpPort());
|
||||||
if (this->localAddress.startsWith("127.")) {
|
if (this->localAddress.address().startsWith("127.")) {
|
||||||
this->localAddress = QString();
|
this->localAddress = NvAddress();
|
||||||
}
|
}
|
||||||
|
|
||||||
this->remoteAddress = NvHTTP::getXmlString(serverInfo, "ExternalIP");
|
QString httpsPort = NvHTTP::getXmlString(serverInfo, "HttpsPort");
|
||||||
|
if (httpsPort.isEmpty() || (this->activeHttpsPort = httpsPort.toUShort()) == 0) {
|
||||||
|
this->activeHttpsPort = DEFAULT_HTTPS_PORT;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is an extension which is not present in GFE. It is present for Sunshine to be able
|
||||||
|
// to support dynamic HTTP WAN ports without requiring the user to manually enter the port.
|
||||||
|
QString remotePortStr = NvHTTP::getXmlString(serverInfo, "ExternalPort");
|
||||||
|
if (remotePortStr.isEmpty() || (this->externalPort = remotePortStr.toUShort()) == 0) {
|
||||||
|
this->externalPort = http.httpPort();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString remoteAddress = NvHTTP::getXmlString(serverInfo, "ExternalIP");
|
||||||
|
if (!remoteAddress.isEmpty()) {
|
||||||
|
this->remoteAddress = NvAddress(remoteAddress, this->externalPort);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this->remoteAddress = NvAddress();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Real Nvidia host software (GeForce Experience and RTX Experience) both use the 'Mjolnir'
|
||||||
|
// codename in the state field and no version of Sunshine does. We can use this to bypass
|
||||||
|
// some assumptions about Nvidia hardware that don't apply to Sunshine hosts.
|
||||||
|
this->isNvidiaServerSoftware = NvHTTP::getXmlString(serverInfo, "state").contains("MJOLNIR");
|
||||||
|
|
||||||
this->pairState = NvHTTP::getXmlString(serverInfo, "PairStatus") == "1" ?
|
this->pairState = NvHTTP::getXmlString(serverInfo, "PairStatus") == "1" ?
|
||||||
PS_PAIRED : PS_NOT_PAIRED;
|
PS_PAIRED : PS_NOT_PAIRED;
|
||||||
this->currentGameId = NvHTTP::getCurrentGame(serverInfo);
|
this->currentGameId = NvHTTP::getCurrentGame(serverInfo);
|
||||||
this->appVersion = NvHTTP::getXmlString(serverInfo, "appversion");
|
this->appVersion = NvHTTP::getXmlString(serverInfo, "appversion");
|
||||||
this->gfeVersion = NvHTTP::getXmlString(serverInfo, "GfeVersion");
|
this->gfeVersion = NvHTTP::getXmlString(serverInfo, "GfeVersion");
|
||||||
this->gpuModel = NvHTTP::getXmlString(serverInfo, "gputype");
|
this->gpuModel = NvHTTP::getXmlString(serverInfo, "gputype");
|
||||||
this->activeAddress = address;
|
this->activeAddress = http.address();
|
||||||
this->state = NvComputer::CS_ONLINE;
|
this->state = NvComputer::CS_ONLINE;
|
||||||
this->pendingQuit = false;
|
this->pendingQuit = false;
|
||||||
|
this->isSupportedServerVersion = CompatFetcher::isGfeVersionSupported(this->gfeVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool NvComputer::wake()
|
bool NvComputer::wake() const
|
||||||
{
|
{
|
||||||
if (state == NvComputer::CS_ONLINE) {
|
QByteArray wolPayload;
|
||||||
qWarning() << name << "is already online";
|
|
||||||
return true;
|
{
|
||||||
|
QReadLocker readLocker(&lock);
|
||||||
|
|
||||||
|
if (state == NvComputer::CS_ONLINE) {
|
||||||
|
qWarning() << name << "is already online";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (macAddress.isEmpty()) {
|
||||||
|
qWarning() << name << "has no MAC address stored";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the WoL payload
|
||||||
|
wolPayload.append(QByteArray::fromHex("FFFFFFFFFFFF"));
|
||||||
|
for (int i = 0; i < 16; i++) {
|
||||||
|
wolPayload.append(macAddress);
|
||||||
|
}
|
||||||
|
Q_ASSERT(wolPayload.size() == 102);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (macAddress.isEmpty()) {
|
// Ports used as-is
|
||||||
qWarning() << name << "has no MAC address stored";
|
const quint16 STATIC_WOL_PORTS[] = {
|
||||||
return false;
|
9, // Standard WOL port (privileged port)
|
||||||
}
|
47009, // Port opened by Moonlight Internet Hosting Tool for WoL (non-privileged port)
|
||||||
|
|
||||||
const quint16 WOL_PORTS[] = {
|
|
||||||
7, 9, // Standard WOL ports
|
|
||||||
47998, 47999, 48000, 48002, 48010, // Ports opened by GFE
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create the WoL payload
|
// Ports offset by the HTTP base port for hosts using alternate ports
|
||||||
QByteArray wolPayload;
|
const quint16 DYNAMIC_WOL_PORTS[] = {
|
||||||
wolPayload.append(QByteArray::fromHex("FFFFFFFFFFFF"));
|
47998, 47999, 48000, 48002, 48010, // Ports opened by GFE
|
||||||
for (int i = 0; i < 16; i++) {
|
};
|
||||||
wolPayload.append(macAddress);
|
|
||||||
}
|
|
||||||
Q_ASSERT(wolPayload.count() == 102);
|
|
||||||
|
|
||||||
// Add the addresses that we know this host to be
|
// Add the addresses that we know this host to be
|
||||||
// and broadcast addresses for this link just in
|
// and broadcast addresses for this link just in
|
||||||
// case the host has timed out in ARP entries.
|
// case the host has timed out in ARP entries.
|
||||||
QVector<QString> addressList = uniqueAddresses();
|
QMap<QString, quint16> addressMap;
|
||||||
addressList.append("255.255.255.255");
|
QSet<quint16> basePortSet;
|
||||||
|
const auto uniqueHostAddresses = uniqueAddresses();
|
||||||
|
for (const NvAddress& addr : uniqueHostAddresses) {
|
||||||
|
addressMap.insert(addr.address(), addr.port());
|
||||||
|
basePortSet.insert(addr.port());
|
||||||
|
}
|
||||||
|
addressMap.insert("255.255.255.255", 0);
|
||||||
|
|
||||||
// Try to broadcast on all available NICs
|
// Try to broadcast on all available NICs
|
||||||
for (const QNetworkInterface& nic : QNetworkInterface::allInterfaces()) {
|
const auto allInterfaces = QNetworkInterface::allInterfaces();
|
||||||
|
for (const QNetworkInterface& nic : allInterfaces) {
|
||||||
// Ensure the interface is up and skip the loopback adapter
|
// Ensure the interface is up and skip the loopback adapter
|
||||||
if ((nic.flags() & QNetworkInterface::IsUp) == 0 ||
|
if ((nic.flags() & QNetworkInterface::IsUp) == 0 ||
|
||||||
(nic.flags() & QNetworkInterface::IsLoopBack) != 0) {
|
(nic.flags() & QNetworkInterface::IsLoopBack) != 0) {
|
||||||
@@ -191,7 +271,8 @@ bool NvComputer::wake()
|
|||||||
}
|
}
|
||||||
|
|
||||||
QHostAddress allNodesMulticast("FF02::1");
|
QHostAddress allNodesMulticast("FF02::1");
|
||||||
for (const QNetworkAddressEntry& addr : nic.addressEntries()) {
|
const auto allInterfaceAddresses = nic.addressEntries();
|
||||||
|
for (const QNetworkAddressEntry& addr : allInterfaceAddresses) {
|
||||||
// Store the scope ID for this NIC if IPv6 is enabled
|
// Store the scope ID for this NIC if IPv6 is enabled
|
||||||
if (!addr.ip().scopeId().isEmpty()) {
|
if (!addr.ip().scopeId().isEmpty()) {
|
||||||
allNodesMulticast.setScopeId(addr.ip().scopeId());
|
allNodesMulticast.setScopeId(addr.ip().scopeId());
|
||||||
@@ -199,31 +280,42 @@ bool NvComputer::wake()
|
|||||||
|
|
||||||
// Skip IPv6 which doesn't support broadcast
|
// Skip IPv6 which doesn't support broadcast
|
||||||
if (!addr.broadcast().isNull()) {
|
if (!addr.broadcast().isNull()) {
|
||||||
addressList.append(addr.broadcast().toString());
|
addressMap.insert(addr.broadcast().toString(), 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!allNodesMulticast.scopeId().isEmpty()) {
|
if (!allNodesMulticast.scopeId().isEmpty()) {
|
||||||
addressList.append(allNodesMulticast.toString());
|
addressMap.insert(allNodesMulticast.toString(), 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try all unique address strings or host names
|
// Try all unique address strings or host names
|
||||||
bool success = false;
|
bool success = false;
|
||||||
for (QString& addressString : addressList) {
|
for (auto i = addressMap.constBegin(); i != addressMap.constEnd(); i++) {
|
||||||
QHostInfo hostInfo = QHostInfo::fromName(addressString);
|
QHostAddress literalAddress;
|
||||||
|
QList<QHostAddress> addressList;
|
||||||
|
|
||||||
if (hostInfo.error() != QHostInfo::NoError) {
|
// If this is an IPv4/IPv6 literal, don't use QHostInfo::fromName() because that will
|
||||||
qWarning() << "Error resolving" << addressString << ":" << hostInfo.errorString();
|
// try to perform a reverse DNS lookup that leads to delays sending WoL packets.
|
||||||
continue;
|
if (literalAddress.setAddress(i.key())) {
|
||||||
|
addressList.append(literalAddress);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
QHostInfo hostInfo = QHostInfo::fromName(i.key());
|
||||||
|
if (hostInfo.error() != QHostInfo::NoError) {
|
||||||
|
qWarning() << "Error resolving" << i.key() << ":" << hostInfo.errorString();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
addressList.append(hostInfo.addresses());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try all IP addresses that this string resolves to
|
// Try all IP addresses that this string resolves to
|
||||||
for (QHostAddress& address : hostInfo.addresses()) {
|
for (QHostAddress& address : addressList) {
|
||||||
QUdpSocket sock;
|
QUdpSocket sock;
|
||||||
|
|
||||||
// Send to all ports
|
// Send to all static ports
|
||||||
for (quint16 port : WOL_PORTS) {
|
for (quint16 port : STATIC_WOL_PORTS) {
|
||||||
if (sock.writeDatagram(wolPayload, address, port)) {
|
if (sock.writeDatagram(wolPayload, address, port)) {
|
||||||
qInfo().nospace().noquote() << "Sent WoL packet to " << name << " via " << address.toString() << ":" << port;
|
qInfo().nospace().noquote() << "Sent WoL packet to " << name << " via " << address.toString() << ":" << port;
|
||||||
success = true;
|
success = true;
|
||||||
@@ -232,32 +324,69 @@ bool NvComputer::wake()
|
|||||||
qWarning() << "Send failed:" << sock.error();
|
qWarning() << "Send failed:" << sock.error();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QList<quint16> basePorts;
|
||||||
|
if (i.value() != 0) {
|
||||||
|
// If we have a known base port for this address, use only that port
|
||||||
|
basePorts.append(i.value());
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// If this is a broadcast address without a known HTTP port, try all of them
|
||||||
|
basePorts.append(basePortSet.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send to all dynamic ports using the HTTP port offset(s) for this address
|
||||||
|
for (quint16 basePort : basePorts) {
|
||||||
|
for (quint16 port : DYNAMIC_WOL_PORTS) {
|
||||||
|
port = (port - 47989) + basePort;
|
||||||
|
|
||||||
|
if (sock.writeDatagram(wolPayload, address, port)) {
|
||||||
|
qInfo().nospace().noquote() << "Sent WoL packet to " << name << " via " << address.toString() << ":" << port;
|
||||||
|
success = true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
qWarning() << "Send failed:" << sock.error();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool NvComputer::isReachableOverVpn()
|
NvComputer::ReachabilityType NvComputer::getActiveAddressReachability() const
|
||||||
{
|
{
|
||||||
if (activeAddress.isEmpty()) {
|
NvAddress copyOfActiveAddress;
|
||||||
return false;
|
|
||||||
|
{
|
||||||
|
QReadLocker readLocker(&lock);
|
||||||
|
|
||||||
|
if (activeAddress.isNull()) {
|
||||||
|
return ReachabilityType::RI_UNKNOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grab a copy of the active address to avoid having to hold
|
||||||
|
// the computer lock while doing socket operations
|
||||||
|
copyOfActiveAddress = activeAddress;
|
||||||
}
|
}
|
||||||
|
|
||||||
QTcpSocket s;
|
QTcpSocket s;
|
||||||
|
|
||||||
s.setProxy(QNetworkProxy::NoProxy);
|
s.setProxy(QNetworkProxy::NoProxy);
|
||||||
s.connectToHost(activeAddress, 47984);
|
s.connectToHost(copyOfActiveAddress.address(), copyOfActiveAddress.port());
|
||||||
if (s.waitForConnected(3000)) {
|
if (s.waitForConnected(3000)) {
|
||||||
Q_ASSERT(!s.localAddress().isNull());
|
Q_ASSERT(!s.localAddress().isNull());
|
||||||
|
Q_ASSERT(!s.peerAddress().isNull());
|
||||||
|
|
||||||
for (const QNetworkInterface& nic : QNetworkInterface::allInterfaces()) {
|
const auto allInterfaces = QNetworkInterface::allInterfaces();
|
||||||
|
for (const QNetworkInterface& nic : allInterfaces) {
|
||||||
// Ensure the interface is up
|
// Ensure the interface is up
|
||||||
if ((nic.flags() & QNetworkInterface::IsUp) == 0) {
|
if ((nic.flags() & QNetworkInterface::IsUp) == 0) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const QNetworkAddressEntry& addr : nic.addressEntries()) {
|
const auto allInterfaceAddresses = nic.addressEntries();
|
||||||
|
for (const QNetworkAddressEntry& addr : allInterfaceAddresses) {
|
||||||
if (addr.ip() == s.localAddress()) {
|
if (addr.ip() == s.localAddress()) {
|
||||||
qInfo() << "Found matching interface:" << nic.humanReadableName() << nic.hardwareAddress() << nic.flags();
|
qInfo() << "Found matching interface:" << nic.humanReadableName() << nic.hardwareAddress() << nic.flags();
|
||||||
|
|
||||||
@@ -268,54 +397,95 @@ bool NvComputer::isReachableOverVpn()
|
|||||||
if (nic.type() == QNetworkInterface::Virtual ||
|
if (nic.type() == QNetworkInterface::Virtual ||
|
||||||
nic.type() == QNetworkInterface::Ppp) {
|
nic.type() == QNetworkInterface::Ppp) {
|
||||||
// Treat PPP and virtual interfaces as likely VPNs
|
// Treat PPP and virtual interfaces as likely VPNs
|
||||||
return true;
|
return ReachabilityType::RI_VPN;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nic.maximumTransmissionUnit() != 0 && nic.maximumTransmissionUnit() < 1500) {
|
if (nic.maximumTransmissionUnit() != 0 && nic.maximumTransmissionUnit() < 1500) {
|
||||||
// Treat MTUs under 1500 as likely VPNs
|
// Treat MTUs under 1500 as likely VPNs
|
||||||
return true;
|
return ReachabilityType::RI_VPN;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
if (nic.flags() & QNetworkInterface::IsPointToPoint) {
|
if (nic.flags() & QNetworkInterface::IsPointToPoint) {
|
||||||
// Treat point-to-point links as likely VPNs.
|
// Treat point-to-point links as likely VPNs.
|
||||||
// This check detects OpenVPN on Unix-like OSes.
|
// This check detects OpenVPN on Unix-like OSes.
|
||||||
return true;
|
return ReachabilityType::RI_VPN;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef Q_OS_WINDOWS
|
||||||
|
if (nic.name().startsWith("iftype53_") || nic.name().startsWith("iftype131_")) {
|
||||||
|
// Match by NDIS interface type. These values are Microsoft's recommended values for VPN connections:
|
||||||
|
// https://learn.microsoft.com/en-US/troubleshoot/windows-client/networking/windows-connection-manager-disconnects-wlan#more-information
|
||||||
|
//
|
||||||
|
// The following VPNs use IF_TYPE_PROP_VIRTUAL under Windows:
|
||||||
|
// - WireguardNT VPNs
|
||||||
|
// - All WinTun-based VPNs (such as Slack Nebula)
|
||||||
|
// - OpenVPN with tap-windows6
|
||||||
|
return ReachabilityType::RI_VPN;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
if (nic.hardwareAddress().startsWith("00:FF", Qt::CaseInsensitive)) {
|
if (nic.hardwareAddress().startsWith("00:FF", Qt::CaseInsensitive)) {
|
||||||
// OpenVPN TAP interfaces have a MAC address starting with 00:FF on Windows
|
// OpenVPN TAP interfaces have a MAC address starting with 00:FF on Windows
|
||||||
return true;
|
return ReachabilityType::RI_VPN;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nic.humanReadableName().startsWith("ZeroTier")) {
|
if (nic.humanReadableName().startsWith("ZeroTier")) {
|
||||||
// ZeroTier interfaces always start with "ZeroTier"
|
// ZeroTier interfaces always start with "ZeroTier"
|
||||||
return true;
|
return ReachabilityType::RI_VPN;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nic.humanReadableName().contains("VPN")) {
|
if (nic.humanReadableName().contains("VPN")) {
|
||||||
// This one is just a final heuristic if all else fails
|
// This one is just a final VPN heuristic if all else fails
|
||||||
return true;
|
return ReachabilityType::RI_VPN;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Didn't meet any of our VPN heuristics
|
// Didn't meet any of our VPN heuristics. Let's see if the peer address is on-link.
|
||||||
return false;
|
Q_ASSERT(addr.prefixLength() >= 0);
|
||||||
|
if (addr.prefixLength() >= 0 && s.localAddress().isInSubnet(s.peerAddress(), addr.prefixLength())) {
|
||||||
|
return ReachabilityType::RI_LAN;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default to unknown if nothing else matched
|
||||||
|
return ReachabilityType::RI_UNKNOWN;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
qWarning() << "No match found for address:" << s.localAddress();
|
qWarning() << "No match found for address:" << s.localAddress();
|
||||||
return false;
|
return ReachabilityType::RI_UNKNOWN;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// If we fail to connect, just pretend that it's not a VPN
|
// If we fail to connect, just pretend that it's not a VPN
|
||||||
return false;
|
qWarning() << "Unable to check for reachability within 3 seconds";
|
||||||
|
return ReachabilityType::RI_UNKNOWN;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QVector<QString> NvComputer::uniqueAddresses()
|
bool NvComputer::updateAppList(QVector<NvApp> newAppList) {
|
||||||
|
if (appList == newAppList) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Propagate client-side attributes to the new app list
|
||||||
|
for (const NvApp& existingApp : std::as_const(appList)) {
|
||||||
|
for (NvApp& newApp : newAppList) {
|
||||||
|
if (existingApp.id == newApp.id) {
|
||||||
|
newApp.hidden = existingApp.hidden;
|
||||||
|
newApp.directLaunch = existingApp.directLaunch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
appList = newAppList;
|
||||||
|
sortAppList();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
QVector<NvAddress> NvComputer::uniqueAddresses() const
|
||||||
{
|
{
|
||||||
QVector<QString> uniqueAddressList;
|
QReadLocker readLocker(&lock);
|
||||||
|
QVector<NvAddress> uniqueAddressList;
|
||||||
|
|
||||||
// Start with addresses correctly ordered
|
// Start with addresses correctly ordered
|
||||||
uniqueAddressList.append(activeAddress);
|
uniqueAddressList.append(activeAddress);
|
||||||
@@ -326,7 +496,7 @@ QVector<QString> NvComputer::uniqueAddresses()
|
|||||||
|
|
||||||
// Prune duplicates (always giving precedence to the first)
|
// Prune duplicates (always giving precedence to the first)
|
||||||
for (int i = 0; i < uniqueAddressList.count(); i++) {
|
for (int i = 0; i < uniqueAddressList.count(); i++) {
|
||||||
if (uniqueAddressList[i].isEmpty()) {
|
if (uniqueAddressList[i].isNull()) {
|
||||||
uniqueAddressList.remove(i);
|
uniqueAddressList.remove(i);
|
||||||
i--;
|
i--;
|
||||||
continue;
|
continue;
|
||||||
@@ -346,7 +516,7 @@ QVector<QString> NvComputer::uniqueAddresses()
|
|||||||
return uniqueAddressList;
|
return uniqueAddressList;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool NvComputer::update(NvComputer& that)
|
bool NvComputer::update(const NvComputer& that)
|
||||||
{
|
{
|
||||||
bool changed = false;
|
bool changed = false;
|
||||||
|
|
||||||
@@ -377,12 +547,17 @@ bool NvComputer::update(NvComputer& that)
|
|||||||
changed = true; \
|
changed = true; \
|
||||||
}
|
}
|
||||||
|
|
||||||
ASSIGN_IF_CHANGED(name);
|
if (!hasCustomName) {
|
||||||
|
// Only overwrite the name if it's not custom
|
||||||
|
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_NONNULL(localAddress);
|
||||||
ASSIGN_IF_CHANGED_AND_NONEMPTY(remoteAddress);
|
ASSIGN_IF_CHANGED_AND_NONNULL(remoteAddress);
|
||||||
ASSIGN_IF_CHANGED_AND_NONEMPTY(ipv6Address);
|
ASSIGN_IF_CHANGED_AND_NONNULL(ipv6Address);
|
||||||
ASSIGN_IF_CHANGED_AND_NONEMPTY(manualAddress);
|
ASSIGN_IF_CHANGED_AND_NONNULL(manualAddress);
|
||||||
|
ASSIGN_IF_CHANGED(activeHttpsPort);
|
||||||
|
ASSIGN_IF_CHANGED(externalPort);
|
||||||
ASSIGN_IF_CHANGED(pairState);
|
ASSIGN_IF_CHANGED(pairState);
|
||||||
ASSIGN_IF_CHANGED(serverCodecModeSupport);
|
ASSIGN_IF_CHANGED(serverCodecModeSupport);
|
||||||
ASSIGN_IF_CHANGED(currentGameId);
|
ASSIGN_IF_CHANGED(currentGameId);
|
||||||
@@ -390,10 +565,17 @@ bool NvComputer::update(NvComputer& that)
|
|||||||
ASSIGN_IF_CHANGED(state);
|
ASSIGN_IF_CHANGED(state);
|
||||||
ASSIGN_IF_CHANGED(gfeVersion);
|
ASSIGN_IF_CHANGED(gfeVersion);
|
||||||
ASSIGN_IF_CHANGED(appVersion);
|
ASSIGN_IF_CHANGED(appVersion);
|
||||||
|
ASSIGN_IF_CHANGED(isSupportedServerVersion);
|
||||||
|
ASSIGN_IF_CHANGED(isNvidiaServerSoftware);
|
||||||
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_NONNULL(serverCert);
|
||||||
ASSIGN_IF_CHANGED_AND_NONEMPTY(appList);
|
|
||||||
ASSIGN_IF_CHANGED_AND_NONEMPTY(displayModes);
|
ASSIGN_IF_CHANGED_AND_NONEMPTY(displayModes);
|
||||||
|
|
||||||
|
if (!that.appList.isEmpty()) {
|
||||||
|
// updateAppList() handles merging client-side attributes
|
||||||
|
updateAppList(that.appList);
|
||||||
|
}
|
||||||
|
|
||||||
return changed;
|
return changed;
|
||||||
}
|
}
|
||||||
|
|||||||
+57
-14
@@ -1,12 +1,23 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "nvhttp.h"
|
#include "nvhttp.h"
|
||||||
|
#include "nvaddress.h"
|
||||||
|
|
||||||
#include <QThread>
|
#include <QThread>
|
||||||
#include <QReadWriteLock>
|
#include <QReadWriteLock>
|
||||||
#include <QSettings>
|
#include <QSettings>
|
||||||
#include <QRunnable>
|
#include <QRunnable>
|
||||||
|
|
||||||
|
class CopySafeReadWriteLock : public QReadWriteLock
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
CopySafeReadWriteLock() = default;
|
||||||
|
|
||||||
|
// Don't actually copy the QReadWriteLock
|
||||||
|
CopySafeReadWriteLock(const CopySafeReadWriteLock&) : QReadWriteLock() {}
|
||||||
|
CopySafeReadWriteLock& operator=(const CopySafeReadWriteLock &) { return *this; }
|
||||||
|
};
|
||||||
|
|
||||||
class NvComputer
|
class NvComputer
|
||||||
{
|
{
|
||||||
friend class PcMonitorThread;
|
friend class PcMonitorThread;
|
||||||
@@ -16,27 +27,51 @@ class NvComputer
|
|||||||
private:
|
private:
|
||||||
void sortAppList();
|
void sortAppList();
|
||||||
|
|
||||||
|
bool updateAppList(QVector<NvApp> newAppList);
|
||||||
|
|
||||||
bool pendingQuit;
|
bool pendingQuit;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit NvComputer(QString address, QString serverInfo, QSslCertificate serverCert);
|
NvComputer() = default;
|
||||||
|
|
||||||
|
// Caller is responsible for synchronizing read access to the other host
|
||||||
|
NvComputer(const NvComputer&) = default;
|
||||||
|
|
||||||
|
// Caller is responsible for synchronizing read access to the other host
|
||||||
|
NvComputer& operator=(const NvComputer &) = default;
|
||||||
|
|
||||||
|
explicit NvComputer(NvHTTP& http, QString serverInfo);
|
||||||
|
|
||||||
explicit NvComputer(QSettings& settings);
|
explicit NvComputer(QSettings& settings);
|
||||||
|
|
||||||
bool
|
void
|
||||||
update(NvComputer& that);
|
setRemoteAddress(QHostAddress);
|
||||||
|
|
||||||
bool
|
bool
|
||||||
wake();
|
update(const NvComputer& that);
|
||||||
|
|
||||||
bool
|
bool
|
||||||
isReachableOverVpn();
|
wake() const;
|
||||||
|
|
||||||
QVector<QString>
|
enum ReachabilityType
|
||||||
uniqueAddresses();
|
{
|
||||||
|
RI_UNKNOWN,
|
||||||
|
RI_LAN,
|
||||||
|
RI_VPN,
|
||||||
|
};
|
||||||
|
|
||||||
|
ReachabilityType
|
||||||
|
getActiveAddressReachability() const;
|
||||||
|
|
||||||
|
QVector<NvAddress>
|
||||||
|
uniqueAddresses() const;
|
||||||
|
|
||||||
void
|
void
|
||||||
serialize(QSettings& settings);
|
serialize(QSettings& settings, bool serializeApps) const;
|
||||||
|
|
||||||
|
// Caller is responsible for synchronizing read access to both hosts
|
||||||
|
bool
|
||||||
|
isEqualSerialized(const NvComputer& that) const;
|
||||||
|
|
||||||
enum PairState
|
enum PairState
|
||||||
{
|
{
|
||||||
@@ -55,7 +90,8 @@ public:
|
|||||||
// Ephemeral traits
|
// Ephemeral traits
|
||||||
ComputerState state;
|
ComputerState state;
|
||||||
PairState pairState;
|
PairState pairState;
|
||||||
QString activeAddress;
|
NvAddress activeAddress;
|
||||||
|
uint16_t activeHttpsPort;
|
||||||
int currentGameId;
|
int currentGameId;
|
||||||
QString gfeVersion;
|
QString gfeVersion;
|
||||||
QString appVersion;
|
QString appVersion;
|
||||||
@@ -63,18 +99,25 @@ public:
|
|||||||
int maxLumaPixelsHEVC;
|
int maxLumaPixelsHEVC;
|
||||||
int serverCodecModeSupport;
|
int serverCodecModeSupport;
|
||||||
QString gpuModel;
|
QString gpuModel;
|
||||||
|
bool isSupportedServerVersion;
|
||||||
|
|
||||||
// Persisted traits
|
// Persisted traits
|
||||||
QString localAddress;
|
NvAddress localAddress;
|
||||||
QString remoteAddress;
|
NvAddress remoteAddress;
|
||||||
QString ipv6Address;
|
NvAddress ipv6Address;
|
||||||
QString manualAddress;
|
NvAddress manualAddress;
|
||||||
QByteArray macAddress;
|
QByteArray macAddress;
|
||||||
QString name;
|
QString name;
|
||||||
|
bool hasCustomName;
|
||||||
QString uuid;
|
QString uuid;
|
||||||
QSslCertificate serverCert;
|
QSslCertificate serverCert;
|
||||||
QVector<NvApp> appList;
|
QVector<NvApp> appList;
|
||||||
|
bool isNvidiaServerSoftware;
|
||||||
|
// Remember to update isEqualSerialized() when adding fields here!
|
||||||
|
|
||||||
// Synchronization
|
// Synchronization
|
||||||
QReadWriteLock lock;
|
mutable CopySafeReadWriteLock lock;
|
||||||
|
|
||||||
|
private:
|
||||||
|
uint16_t externalPort;
|
||||||
};
|
};
|
||||||
|
|||||||
+145
-101
@@ -1,4 +1,4 @@
|
|||||||
#include "nvhttp.h"
|
#include "nvcomputer.h"
|
||||||
#include <Limelight.h>
|
#include <Limelight.h>
|
||||||
|
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
@@ -12,26 +12,31 @@
|
|||||||
#include <QtEndian>
|
#include <QtEndian>
|
||||||
#include <QNetworkProxy>
|
#include <QNetworkProxy>
|
||||||
|
|
||||||
|
#define FAST_FAIL_TIMEOUT_MS 2000
|
||||||
#define REQUEST_TIMEOUT_MS 5000
|
#define REQUEST_TIMEOUT_MS 5000
|
||||||
#define LAUNCH_TIMEOUT_MS 120000
|
#define LAUNCH_TIMEOUT_MS 120000
|
||||||
#define RESUME_TIMEOUT_MS 30000
|
#define RESUME_TIMEOUT_MS 30000
|
||||||
#define QUIT_TIMEOUT_MS 30000
|
#define QUIT_TIMEOUT_MS 30000
|
||||||
|
|
||||||
NvHTTP::NvHTTP(QString address, QSslCertificate serverCert) :
|
NvHTTP::NvHTTP(NvAddress address, uint16_t httpsPort, QSslCertificate serverCert, QNetworkAccessManager* nam) :
|
||||||
|
m_Nam(nam ? nam : new QNetworkAccessManager(this)),
|
||||||
m_ServerCert(serverCert)
|
m_ServerCert(serverCert)
|
||||||
{
|
{
|
||||||
m_BaseUrlHttp.setScheme("http");
|
m_BaseUrlHttp.setScheme("http");
|
||||||
m_BaseUrlHttps.setScheme("https");
|
m_BaseUrlHttps.setScheme("https");
|
||||||
m_BaseUrlHttp.setPort(47989);
|
|
||||||
m_BaseUrlHttps.setPort(47984);
|
|
||||||
|
|
||||||
setAddress(address);
|
setAddress(address);
|
||||||
|
setHttpsPort(httpsPort);
|
||||||
|
|
||||||
// Never use a proxy server
|
// Never use a proxy server
|
||||||
QNetworkProxy noProxy(QNetworkProxy::NoProxy);
|
QNetworkProxy noProxy(QNetworkProxy::NoProxy);
|
||||||
m_Nam.setProxy(noProxy);
|
m_Nam->setProxy(noProxy);
|
||||||
|
}
|
||||||
|
|
||||||
|
NvHTTP::NvHTTP(NvComputer* computer, QNetworkAccessManager* nam) :
|
||||||
|
NvHTTP(computer->activeAddress, computer->activeHttpsPort, computer->serverCert, nam)
|
||||||
|
{
|
||||||
|
|
||||||
connect(&m_Nam, &QNetworkAccessManager::sslErrors, this, &NvHTTP::handleSslErrors);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void NvHTTP::setServerCert(QSslCertificate serverCert)
|
void NvHTTP::setServerCert(QSslCertificate serverCert)
|
||||||
@@ -39,21 +44,43 @@ void NvHTTP::setServerCert(QSslCertificate serverCert)
|
|||||||
m_ServerCert = serverCert;
|
m_ServerCert = serverCert;
|
||||||
}
|
}
|
||||||
|
|
||||||
void NvHTTP::setAddress(QString address)
|
void NvHTTP::setAddress(NvAddress address)
|
||||||
{
|
{
|
||||||
Q_ASSERT(!address.isEmpty());
|
Q_ASSERT(!address.isNull());
|
||||||
|
|
||||||
m_Address = address;
|
m_Address = address;
|
||||||
|
|
||||||
m_BaseUrlHttp.setHost(address);
|
m_BaseUrlHttp.setHost(address.address());
|
||||||
m_BaseUrlHttps.setHost(address);
|
m_BaseUrlHttps.setHost(address.address());
|
||||||
|
|
||||||
|
m_BaseUrlHttp.setPort(address.port());
|
||||||
}
|
}
|
||||||
|
|
||||||
QString NvHTTP::address()
|
void NvHTTP::setHttpsPort(uint16_t port)
|
||||||
|
{
|
||||||
|
m_BaseUrlHttps.setPort(port);
|
||||||
|
}
|
||||||
|
|
||||||
|
NvAddress NvHTTP::address()
|
||||||
{
|
{
|
||||||
return m_Address;
|
return m_Address;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QSslCertificate NvHTTP::serverCert()
|
||||||
|
{
|
||||||
|
return m_ServerCert;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t NvHTTP::httpPort()
|
||||||
|
{
|
||||||
|
return m_BaseUrlHttp.port();
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t NvHTTP::httpsPort()
|
||||||
|
{
|
||||||
|
return m_BaseUrlHttps.port();
|
||||||
|
}
|
||||||
|
|
||||||
QVector<int>
|
QVector<int>
|
||||||
NvHTTP::parseQuad(QString quad)
|
NvHTTP::parseQuad(QString quad)
|
||||||
{
|
{
|
||||||
@@ -66,7 +93,8 @@ NvHTTP::parseQuad(QString quad)
|
|||||||
}
|
}
|
||||||
|
|
||||||
QStringList parts = quad.split(".");
|
QStringList parts = quad.split(".");
|
||||||
for (int i = 0; i < 4; i++)
|
ret.reserve(parts.length());
|
||||||
|
for (int i = 0; i < parts.length(); i++)
|
||||||
{
|
{
|
||||||
ret.append(parts.at(i).toInt());
|
ret.append(parts.at(i).toInt());
|
||||||
}
|
}
|
||||||
@@ -92,12 +120,12 @@ NvHTTP::getCurrentGame(QString serverInfo)
|
|||||||
}
|
}
|
||||||
|
|
||||||
QString
|
QString
|
||||||
NvHTTP::getServerInfo(NvLogLevel logLevel)
|
NvHTTP::getServerInfo(NvLogLevel logLevel, bool fastFail)
|
||||||
{
|
{
|
||||||
QString serverInfo;
|
QString serverInfo;
|
||||||
|
|
||||||
// Check if we have a pinned cert for this host yet
|
// Check if we have a pinned cert and HTTPS port for this host yet
|
||||||
if (!m_ServerCert.isNull())
|
if (!m_ServerCert.isNull() && httpsPort() != 0)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -106,7 +134,7 @@ NvHTTP::getServerInfo(NvLogLevel logLevel)
|
|||||||
serverInfo = openConnectionToString(m_BaseUrlHttps,
|
serverInfo = openConnectionToString(m_BaseUrlHttps,
|
||||||
"serverinfo",
|
"serverinfo",
|
||||||
nullptr,
|
nullptr,
|
||||||
REQUEST_TIMEOUT_MS,
|
fastFail ? FAST_FAIL_TIMEOUT_MS : REQUEST_TIMEOUT_MS,
|
||||||
logLevel);
|
logLevel);
|
||||||
// Throws if the request failed
|
// Throws if the request failed
|
||||||
verifyResponseStatus(serverInfo);
|
verifyResponseStatus(serverInfo);
|
||||||
@@ -119,7 +147,7 @@ NvHTTP::getServerInfo(NvLogLevel logLevel)
|
|||||||
serverInfo = openConnectionToString(m_BaseUrlHttp,
|
serverInfo = openConnectionToString(m_BaseUrlHttp,
|
||||||
"serverinfo",
|
"serverinfo",
|
||||||
nullptr,
|
nullptr,
|
||||||
REQUEST_TIMEOUT_MS,
|
fastFail ? FAST_FAIL_TIMEOUT_MS : REQUEST_TIMEOUT_MS,
|
||||||
logLevel);
|
logLevel);
|
||||||
verifyResponseStatus(serverInfo);
|
verifyResponseStatus(serverInfo);
|
||||||
}
|
}
|
||||||
@@ -132,48 +160,41 @@ NvHTTP::getServerInfo(NvLogLevel logLevel)
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Only use HTTP prior to pairing
|
// Only use HTTP prior to pairing or fetching HTTPS port
|
||||||
serverInfo = openConnectionToString(m_BaseUrlHttp,
|
serverInfo = openConnectionToString(m_BaseUrlHttp,
|
||||||
"serverinfo",
|
"serverinfo",
|
||||||
nullptr,
|
nullptr,
|
||||||
REQUEST_TIMEOUT_MS,
|
fastFail ? FAST_FAIL_TIMEOUT_MS : REQUEST_TIMEOUT_MS,
|
||||||
logLevel);
|
logLevel);
|
||||||
verifyResponseStatus(serverInfo);
|
verifyResponseStatus(serverInfo);
|
||||||
|
|
||||||
|
// Populate the HTTPS port
|
||||||
|
uint16_t httpsPort = getXmlString(serverInfo, "HttpsPort").toUShort();
|
||||||
|
if (httpsPort == 0) {
|
||||||
|
httpsPort = DEFAULT_HTTPS_PORT;
|
||||||
|
}
|
||||||
|
setHttpsPort(httpsPort);
|
||||||
|
|
||||||
|
// If we just needed to determine the HTTPS port, we'll try again over
|
||||||
|
// HTTPS now that we have the port number
|
||||||
|
if (!m_ServerCert.isNull()) {
|
||||||
|
return getServerInfo(logLevel, fastFail);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return serverInfo;
|
return serverInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
static QString
|
|
||||||
getSurroundAudioInfoString(int audioConfig)
|
|
||||||
{
|
|
||||||
int channelMask;
|
|
||||||
int channelCount;
|
|
||||||
|
|
||||||
switch (audioConfig)
|
|
||||||
{
|
|
||||||
case AUDIO_CONFIGURATION_STEREO:
|
|
||||||
channelCount = 2;
|
|
||||||
channelMask = 0x3;
|
|
||||||
break;
|
|
||||||
case AUDIO_CONFIGURATION_51_SURROUND:
|
|
||||||
channelCount = 6;
|
|
||||||
channelMask = 0xFC;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
Q_ASSERT(false);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return QString::number(channelMask << 16 | channelCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
void
|
||||||
NvHTTP::launchApp(int appId,
|
NvHTTP::startApp(QString verb,
|
||||||
PSTREAM_CONFIGURATION streamConfig,
|
bool isGfe,
|
||||||
bool sops,
|
int appId,
|
||||||
bool localAudio,
|
PSTREAM_CONFIGURATION streamConfig,
|
||||||
int gamepadMask)
|
bool sops,
|
||||||
|
bool localAudio,
|
||||||
|
int gamepadMask,
|
||||||
|
bool persistGameControllersOnDisconnect,
|
||||||
|
QString& rtspSessionUrl)
|
||||||
{
|
{
|
||||||
int riKeyId;
|
int riKeyId;
|
||||||
|
|
||||||
@@ -182,48 +203,35 @@ NvHTTP::launchApp(int appId,
|
|||||||
|
|
||||||
QString response =
|
QString response =
|
||||||
openConnectionToString(m_BaseUrlHttps,
|
openConnectionToString(m_BaseUrlHttps,
|
||||||
"launch",
|
verb,
|
||||||
"appid="+QString::number(appId)+
|
"appid="+QString::number(appId)+
|
||||||
"&mode="+QString::number(streamConfig->width)+"x"+
|
"&mode="+QString::number(streamConfig->width)+"x"+
|
||||||
QString::number(streamConfig->height)+"x"+
|
QString::number(streamConfig->height)+"x"+
|
||||||
// Using an FPS value over 60 causes SOPS to default to 720p60,
|
// Using an FPS value over 60 causes SOPS to default to 720p60,
|
||||||
// so force it to 60 when starting. This won't impact our ability
|
// so force it to 0 to ensure the correct resolution is set. We
|
||||||
// to get > 60 FPS while actually streaming though.
|
// used to use 60 here but that locked the frame rate to 60 FPS
|
||||||
QString::number(streamConfig->fps > 60 ? 60 : streamConfig->fps)+
|
// on GFE 3.20.3. We don't need this hack for Sunshine.
|
||||||
|
QString::number((streamConfig->fps > 60 && isGfe) ? 0 : streamConfig->fps)+
|
||||||
"&additionalStates=1&sops="+QString::number(sops ? 1 : 0)+
|
"&additionalStates=1&sops="+QString::number(sops ? 1 : 0)+
|
||||||
"&rikey="+QByteArray(streamConfig->remoteInputAesKey, sizeof(streamConfig->remoteInputAesKey)).toHex()+
|
"&rikey="+QByteArray(streamConfig->remoteInputAesKey, sizeof(streamConfig->remoteInputAesKey)).toHex()+
|
||||||
"&rikeyid="+QString::number(riKeyId)+
|
"&rikeyid="+QString::number(riKeyId)+
|
||||||
(streamConfig->enableHdr ?
|
((streamConfig->supportedVideoFormats & VIDEO_FORMAT_MASK_10BIT) ?
|
||||||
"&hdrMode=1&clientHdrCapVersion=0&clientHdrCapSupportedFlagsInUint32=0&clientHdrCapMetaDataId=NV_STATIC_METADATA_TYPE_1&clientHdrCapDisplayData=0x0x0x0x0x0x0x0x0x0x0" :
|
"&hdrMode=1&clientHdrCapVersion=0&clientHdrCapSupportedFlagsInUint32=0&clientHdrCapMetaDataId=NV_STATIC_METADATA_TYPE_1&clientHdrCapDisplayData=0x0x0x0x0x0x0x0x0x0x0" :
|
||||||
"")+
|
"")+
|
||||||
"&localAudioPlayMode="+QString::number(localAudio ? 1 : 0)+
|
"&localAudioPlayMode="+QString::number(localAudio ? 1 : 0)+
|
||||||
"&surroundAudioInfo="+getSurroundAudioInfoString(streamConfig->audioConfiguration)+
|
"&surroundAudioInfo="+QString::number(SURROUNDAUDIOINFO_FROM_AUDIO_CONFIGURATION(streamConfig->audioConfiguration))+
|
||||||
"&remoteControllersBitmap="+QString::number(gamepadMask)+
|
"&remoteControllersBitmap="+QString::number(gamepadMask)+
|
||||||
"&gcmap="+QString::number(gamepadMask),
|
"&gcmap="+QString::number(gamepadMask)+
|
||||||
|
"&gcpersist="+QString::number(persistGameControllersOnDisconnect ? 1 : 0)+
|
||||||
|
LiGetLaunchUrlQueryParameters(),
|
||||||
LAUNCH_TIMEOUT_MS);
|
LAUNCH_TIMEOUT_MS);
|
||||||
|
|
||||||
// Throws if the request failed
|
qInfo() << "Launch response:" << response;
|
||||||
verifyResponseStatus(response);
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
NvHTTP::resumeApp(PSTREAM_CONFIGURATION streamConfig)
|
|
||||||
{
|
|
||||||
int riKeyId;
|
|
||||||
|
|
||||||
memcpy(&riKeyId, streamConfig->remoteInputAesIv, sizeof(riKeyId));
|
|
||||||
riKeyId = qFromBigEndian(riKeyId);
|
|
||||||
|
|
||||||
QString response =
|
|
||||||
openConnectionToString(m_BaseUrlHttps,
|
|
||||||
"resume",
|
|
||||||
"rikey="+QString(QByteArray(streamConfig->remoteInputAesKey, sizeof(streamConfig->remoteInputAesKey)).toHex())+
|
|
||||||
"&rikeyid="+QString::number(riKeyId)+
|
|
||||||
"&surroundAudioInfo="+getSurroundAudioInfoString(streamConfig->audioConfiguration),
|
|
||||||
RESUME_TIMEOUT_MS);
|
|
||||||
|
|
||||||
// Throws if the request failed
|
// Throws if the request failed
|
||||||
verifyResponseStatus(response);
|
verifyResponseStatus(response);
|
||||||
|
|
||||||
|
rtspSessionUrl = getXmlString(response, "sessionUrl0");
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
@@ -235,11 +243,13 @@ NvHTTP::quitApp()
|
|||||||
nullptr,
|
nullptr,
|
||||||
QUIT_TIMEOUT_MS);
|
QUIT_TIMEOUT_MS);
|
||||||
|
|
||||||
|
qInfo() << "Quit response:" << response;
|
||||||
|
|
||||||
// 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 requester.
|
||||||
if (getCurrentGame(getServerInfo(NvHTTP::NVLL_ERROR)) != 0) {
|
if (getCurrentGame(getServerInfo(NvHTTP::NVLL_ERROR)) != 0) {
|
||||||
// Generate a synthetic GfeResponseException letting the caller know
|
// Generate a synthetic GfeResponseException letting the caller know
|
||||||
// that they can't kill someone else's stream.
|
// that they can't kill someone else's stream.
|
||||||
@@ -255,17 +265,17 @@ NvHTTP::getDisplayModeList(QString serverInfo)
|
|||||||
|
|
||||||
while (!xmlReader.atEnd()) {
|
while (!xmlReader.atEnd()) {
|
||||||
while (xmlReader.readNextStartElement()) {
|
while (xmlReader.readNextStartElement()) {
|
||||||
QStringRef name = xmlReader.name();
|
auto name = xmlReader.name();
|
||||||
if (xmlReader.name() == "DisplayMode") {
|
if (name == QString("DisplayMode")) {
|
||||||
modes.append(NvDisplayMode());
|
modes.append(NvDisplayMode());
|
||||||
}
|
}
|
||||||
else if (xmlReader.name() == "Width") {
|
else if (name == QString("Width")) {
|
||||||
modes.last().width = xmlReader.readElementText().toInt();
|
modes.last().width = xmlReader.readElementText().toInt();
|
||||||
}
|
}
|
||||||
else if (xmlReader.name() == "Height") {
|
else if (name == QString("Height")) {
|
||||||
modes.last().height = xmlReader.readElementText().toInt();
|
modes.last().height = xmlReader.readElementText().toInt();
|
||||||
}
|
}
|
||||||
else if (xmlReader.name() == "RefreshRate") {
|
else if (name == QString("RefreshRate")) {
|
||||||
modes.last().refreshRate = xmlReader.readElementText().toInt();
|
modes.last().refreshRate = xmlReader.readElementText().toInt();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -288,25 +298,27 @@ NvHTTP::getAppList()
|
|||||||
QVector<NvApp> apps;
|
QVector<NvApp> apps;
|
||||||
while (!xmlReader.atEnd()) {
|
while (!xmlReader.atEnd()) {
|
||||||
while (xmlReader.readNextStartElement()) {
|
while (xmlReader.readNextStartElement()) {
|
||||||
QStringRef name = xmlReader.name();
|
auto name = xmlReader.name();
|
||||||
if (xmlReader.name() == "App") {
|
if (name == QString("App")) {
|
||||||
// We must have a valid app before advancing to the next one
|
// We must have a valid app before advancing to the next one
|
||||||
if (!apps.isEmpty() && !apps.last().isInitialized()) {
|
if (!apps.isEmpty() && !apps.last().isInitialized()) {
|
||||||
qWarning() << "Invalid applist XML";
|
qWarning() << "Invalid applist XML";
|
||||||
Q_ASSERT(false);
|
throw std::runtime_error("Invalid applist XML");
|
||||||
return QVector<NvApp>();
|
|
||||||
}
|
}
|
||||||
apps.append(NvApp());
|
apps.append(NvApp());
|
||||||
}
|
}
|
||||||
else if (xmlReader.name() == "AppTitle") {
|
else if (name == QString("AppTitle")) {
|
||||||
apps.last().name = xmlReader.readElementText();
|
apps.last().name = xmlReader.readElementText();
|
||||||
}
|
}
|
||||||
else if (xmlReader.name() == "ID") {
|
else if (name == QString("ID")) {
|
||||||
apps.last().id = xmlReader.readElementText().toInt();
|
apps.last().id = xmlReader.readElementText().toInt();
|
||||||
}
|
}
|
||||||
else if (xmlReader.name() == "IsHdrSupported") {
|
else if (name == QString("IsHdrSupported")) {
|
||||||
apps.last().hdrSupported = xmlReader.readElementText() == "1";
|
apps.last().hdrSupported = xmlReader.readElementText() == "1";
|
||||||
}
|
}
|
||||||
|
else if (name == QString("IsAppCollectorGame")) {
|
||||||
|
apps.last().isAppCollectorGame = xmlReader.readElementText() == "1";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -320,9 +332,12 @@ NvHTTP::verifyResponseStatus(QString xml)
|
|||||||
|
|
||||||
while (xmlReader.readNextStartElement())
|
while (xmlReader.readNextStartElement())
|
||||||
{
|
{
|
||||||
if (xmlReader.name() == "root")
|
if (xmlReader.name() == QString("root"))
|
||||||
{
|
{
|
||||||
int statusCode = xmlReader.attributes().value("status_code").toInt();
|
// Status code can be 0xFFFFFFFF in some rare cases on GFE 3.20.3, and
|
||||||
|
// QString::toInt() will fail in that case, so use QString::toUInt()
|
||||||
|
// and cast the result to an int instead.
|
||||||
|
int statusCode = (int)xmlReader.attributes().value("status_code").toUInt();
|
||||||
if (statusCode == 200)
|
if (statusCode == 200)
|
||||||
{
|
{
|
||||||
// Successful
|
// Successful
|
||||||
@@ -335,10 +350,18 @@ NvHTTP::verifyResponseStatus(QString xml)
|
|||||||
// 401 is expected for unpaired PCs when we fetch serverinfo over HTTPS
|
// 401 is expected for unpaired PCs when we fetch serverinfo over HTTPS
|
||||||
qWarning() << "Request failed:" << statusCode << statusMessage;
|
qWarning() << "Request failed:" << statusCode << statusMessage;
|
||||||
}
|
}
|
||||||
|
if (statusCode == -1 && statusMessage == "Invalid") {
|
||||||
|
// Special case handling an audio capture error which GFE doesn't
|
||||||
|
// provide any useful status message for.
|
||||||
|
statusCode = 418;
|
||||||
|
statusMessage = tr("Missing audio capture device. Reinstalling GeForce Experience should resolve this error.");
|
||||||
|
}
|
||||||
throw GfeHttpResponseException(statusCode, statusMessage);
|
throw GfeHttpResponseException(statusCode, statusMessage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
throw GfeHttpResponseException(-1, "Malformed XML (missing root element)");
|
||||||
}
|
}
|
||||||
|
|
||||||
QImage
|
QImage
|
||||||
@@ -366,7 +389,7 @@ NvHTTP::getXmlStringFromHex(QString xml,
|
|||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
return QByteArray::fromHex(str.toLatin1());
|
return QByteArray::fromHex(str.toUtf8());
|
||||||
}
|
}
|
||||||
|
|
||||||
QString
|
QString
|
||||||
@@ -401,7 +424,7 @@ void NvHTTP::handleSslErrors(QNetworkReply* reply, const QList<QSslError>& error
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (auto error : errors) {
|
for (const QSslError& error : errors) {
|
||||||
if (m_ServerCert != error.certificate()) {
|
if (m_ServerCert != error.certificate()) {
|
||||||
ignoreErrors = false;
|
ignoreErrors = false;
|
||||||
break;
|
break;
|
||||||
@@ -424,7 +447,13 @@ NvHTTP::openConnectionToString(QUrl baseUrl,
|
|||||||
QString ret;
|
QString ret;
|
||||||
|
|
||||||
QTextStream stream(reply);
|
QTextStream stream(reply);
|
||||||
|
|
||||||
|
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||||
|
stream.setEncoding(QStringConverter::Utf8);
|
||||||
|
#else
|
||||||
stream.setCodec("UTF-8");
|
stream.setCodec("UTF-8");
|
||||||
|
#endif
|
||||||
|
|
||||||
ret = stream.readAll();
|
ret = stream.readAll();
|
||||||
delete reply;
|
delete reply;
|
||||||
|
|
||||||
@@ -438,6 +467,9 @@ NvHTTP::openConnection(QUrl baseUrl,
|
|||||||
int timeoutMs,
|
int timeoutMs,
|
||||||
NvLogLevel logLevel)
|
NvLogLevel logLevel)
|
||||||
{
|
{
|
||||||
|
// Port must be set
|
||||||
|
Q_ASSERT(baseUrl.port(0) != 0);
|
||||||
|
|
||||||
// Build a URL for the request
|
// Build a URL for the request
|
||||||
QUrl url(baseUrl);
|
QUrl url(baseUrl);
|
||||||
url.setPath("/" + command);
|
url.setPath("/" + command);
|
||||||
@@ -454,17 +486,27 @@ NvHTTP::openConnection(QUrl baseUrl,
|
|||||||
// Add our client certificate
|
// Add our client certificate
|
||||||
request.setSslConfiguration(IdentityManager::get()->getSslConfig());
|
request.setSslConfiguration(IdentityManager::get()->getSslConfig());
|
||||||
|
|
||||||
// HACK: Set reachability to work around QTBUG-80947
|
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||||
m_Nam.setNetworkAccessible(QNetworkAccessManager::Accessible);
|
// Disable HTTP/2 (GFE 3.22 doesn't like it) and Qt 6 enables it by default
|
||||||
|
request.setAttribute(QNetworkRequest::Http2AllowedAttribute, false);
|
||||||
|
#endif
|
||||||
|
|
||||||
QNetworkReply* reply = m_Nam.get(request);
|
#if QT_VERSION >= QT_VERSION_CHECK(6, 3, 0)
|
||||||
|
// Use fine-grained idle timeouts to avoid calling QNetworkAccessManager::clearAccessCache(),
|
||||||
|
// which tears down the NAM's global thread each time. We must not keep persistent connections
|
||||||
|
// or GFE will puke.
|
||||||
|
request.setAttribute(QNetworkRequest::ConnectionCacheExpiryTimeoutSecondsAttribute, 0);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
auto sslErrorsConnection = connect(m_Nam, &QNetworkAccessManager::sslErrors, this, &NvHTTP::handleSslErrors);
|
||||||
|
QNetworkReply* reply = m_Nam->get(request);
|
||||||
|
|
||||||
// Run the request with a timeout if requested
|
// Run the request with a timeout if requested
|
||||||
QEventLoop loop;
|
QEventLoop loop;
|
||||||
connect(reply, SIGNAL(finished()), &loop, SLOT(quit()));
|
connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
|
||||||
connect(QCoreApplication::instance(), SIGNAL(aboutToQuit()), &loop, SLOT(quit()));
|
connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, &loop, &QEventLoop::quit);
|
||||||
if (timeoutMs) {
|
if (timeoutMs) {
|
||||||
QTimer::singleShot(timeoutMs, &loop, SLOT(quit()));
|
QTimer::singleShot(timeoutMs, &loop, &QEventLoop::quit);
|
||||||
}
|
}
|
||||||
if (logLevel >= NvLogLevel::NVLL_VERBOSE) {
|
if (logLevel >= NvLogLevel::NVLL_VERBOSE) {
|
||||||
qInfo() << "Executing request:" << url.toString();
|
qInfo() << "Executing request:" << url.toString();
|
||||||
@@ -480,15 +522,17 @@ NvHTTP::openConnection(QUrl baseUrl,
|
|||||||
reply->abort();
|
reply->abort();
|
||||||
}
|
}
|
||||||
|
|
||||||
// We must clear out cached authentication and connections or
|
#if QT_VERSION < QT_VERSION_CHECK(6, 3, 0)
|
||||||
// GFE will puke next time
|
// If we couldn't use fine-grained connection idle timeouts, kill them all now
|
||||||
m_Nam.clearAccessCache();
|
m_Nam->clearAccessCache();
|
||||||
|
#endif
|
||||||
|
disconnect(sslErrorsConnection);
|
||||||
|
|
||||||
// Handle error
|
// Handle error
|
||||||
if (reply->error() != QNetworkReply::NoError)
|
if (reply->error() != QNetworkReply::NoError)
|
||||||
{
|
{
|
||||||
if (logLevel >= NvLogLevel::NVLL_ERROR) {
|
if (logLevel >= NvLogLevel::NVLL_ERROR) {
|
||||||
qWarning() << command << " request failed with error " << reply->error();
|
qWarning() << command << "request failed with error:" << reply->error();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (reply->error() == QNetworkReply::SslHandshakeFailedError) {
|
if (reply->error() == QNetworkReply::SslHandshakeFailedError) {
|
||||||
|
|||||||
+38
-43
@@ -1,6 +1,8 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "identitymanager.h"
|
#include "identitymanager.h"
|
||||||
|
#include "nvapp.h"
|
||||||
|
#include "nvaddress.h"
|
||||||
|
|
||||||
#include <Limelight.h>
|
#include <Limelight.h>
|
||||||
|
|
||||||
@@ -8,25 +10,7 @@
|
|||||||
#include <QNetworkAccessManager>
|
#include <QNetworkAccessManager>
|
||||||
#include <QNetworkReply>
|
#include <QNetworkReply>
|
||||||
|
|
||||||
class NvApp
|
class NvComputer;
|
||||||
{
|
|
||||||
public:
|
|
||||||
bool operator==(const NvApp& other) const
|
|
||||||
{
|
|
||||||
return id == other.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool isInitialized()
|
|
||||||
{
|
|
||||||
return id != 0 && !name.isEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
int id;
|
|
||||||
QString name;
|
|
||||||
bool hdrSupported;
|
|
||||||
};
|
|
||||||
|
|
||||||
Q_DECLARE_METATYPE(NvApp)
|
|
||||||
|
|
||||||
class NvDisplayMode
|
class NvDisplayMode
|
||||||
{
|
{
|
||||||
@@ -42,25 +26,26 @@ public:
|
|||||||
int height;
|
int height;
|
||||||
int refreshRate;
|
int refreshRate;
|
||||||
};
|
};
|
||||||
|
Q_DECLARE_TYPEINFO(NvDisplayMode, Q_PRIMITIVE_TYPE);
|
||||||
|
|
||||||
class GfeHttpResponseException : public std::exception
|
class GfeHttpResponseException : public std::exception
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
GfeHttpResponseException(int statusCode, QString message) :
|
GfeHttpResponseException(int statusCode, QString message) :
|
||||||
m_StatusCode(statusCode),
|
m_StatusCode(statusCode),
|
||||||
m_StatusMessage(message)
|
m_StatusMessage(message.toUtf8())
|
||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const char* what() const throw()
|
const char* what() const throw()
|
||||||
{
|
{
|
||||||
return m_StatusMessage.toLatin1();
|
return m_StatusMessage.constData();
|
||||||
}
|
}
|
||||||
|
|
||||||
const char* getStatusMessage() const
|
const char* getStatusMessage() const
|
||||||
{
|
{
|
||||||
return m_StatusMessage.toLatin1();
|
return m_StatusMessage.constData();
|
||||||
}
|
}
|
||||||
|
|
||||||
int getStatusCode() const
|
int getStatusCode() const
|
||||||
@@ -70,12 +55,12 @@ public:
|
|||||||
|
|
||||||
QString toQString() const
|
QString toQString() const
|
||||||
{
|
{
|
||||||
return m_StatusMessage + " (Error " + QString::number(m_StatusCode) + ")";
|
return QString::fromUtf8(m_StatusMessage) + " (Error " + QString::number(m_StatusCode) + ")";
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
int m_StatusCode;
|
int m_StatusCode;
|
||||||
QString m_StatusMessage;
|
QByteArray m_StatusMessage;
|
||||||
};
|
};
|
||||||
|
|
||||||
class QtNetworkReplyException : public std::exception
|
class QtNetworkReplyException : public std::exception
|
||||||
@@ -83,19 +68,19 @@ class QtNetworkReplyException : public std::exception
|
|||||||
public:
|
public:
|
||||||
QtNetworkReplyException(QNetworkReply::NetworkError error, QString errorText) :
|
QtNetworkReplyException(QNetworkReply::NetworkError error, QString errorText) :
|
||||||
m_Error(error),
|
m_Error(error),
|
||||||
m_ErrorText(errorText)
|
m_ErrorText(errorText.toUtf8())
|
||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const char* what() const throw()
|
const char* what() const throw()
|
||||||
{
|
{
|
||||||
return m_ErrorText.toLatin1();
|
return m_ErrorText.constData();
|
||||||
}
|
}
|
||||||
|
|
||||||
const char* getErrorText() const
|
const char* getErrorText() const
|
||||||
{
|
{
|
||||||
return m_ErrorText.toLatin1();
|
return m_ErrorText.constData();
|
||||||
}
|
}
|
||||||
|
|
||||||
QNetworkReply::NetworkError getError() const
|
QNetworkReply::NetworkError getError() const
|
||||||
@@ -105,12 +90,12 @@ public:
|
|||||||
|
|
||||||
QString toQString() const
|
QString toQString() const
|
||||||
{
|
{
|
||||||
return m_ErrorText + " (Error " + QString::number(m_Error) + ")";
|
return QString::fromUtf8(m_ErrorText) + " (Error " + QString::number(m_Error) + ")";
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QNetworkReply::NetworkError m_Error;
|
QNetworkReply::NetworkError m_Error;
|
||||||
QString m_ErrorText;
|
QByteArray m_ErrorText;
|
||||||
};
|
};
|
||||||
|
|
||||||
class NvHTTP : public QObject
|
class NvHTTP : public QObject
|
||||||
@@ -124,14 +109,16 @@ public:
|
|||||||
NVLL_VERBOSE
|
NVLL_VERBOSE
|
||||||
};
|
};
|
||||||
|
|
||||||
explicit NvHTTP(QString address, QSslCertificate serverCert);
|
explicit NvHTTP(NvAddress address, uint16_t httpsPort, QSslCertificate serverCert, QNetworkAccessManager* nam = nullptr);
|
||||||
|
|
||||||
|
explicit NvHTTP(NvComputer* computer, QNetworkAccessManager* nam = nullptr);
|
||||||
|
|
||||||
static
|
static
|
||||||
int
|
int
|
||||||
getCurrentGame(QString serverInfo);
|
getCurrentGame(QString serverInfo);
|
||||||
|
|
||||||
QString
|
QString
|
||||||
getServerInfo(NvLogLevel logLevel);
|
getServerInfo(NvLogLevel logLevel, bool fastFail = false);
|
||||||
|
|
||||||
static
|
static
|
||||||
void
|
void
|
||||||
@@ -156,9 +143,16 @@ public:
|
|||||||
|
|
||||||
void setServerCert(QSslCertificate serverCert);
|
void setServerCert(QSslCertificate serverCert);
|
||||||
|
|
||||||
void setAddress(QString address);
|
void setAddress(NvAddress address);
|
||||||
|
void setHttpsPort(uint16_t port);
|
||||||
|
|
||||||
QString address();
|
NvAddress address();
|
||||||
|
|
||||||
|
QSslCertificate serverCert();
|
||||||
|
|
||||||
|
uint16_t httpPort();
|
||||||
|
|
||||||
|
uint16_t httpsPort();
|
||||||
|
|
||||||
static
|
static
|
||||||
QVector<int>
|
QVector<int>
|
||||||
@@ -168,14 +162,15 @@ public:
|
|||||||
quitApp();
|
quitApp();
|
||||||
|
|
||||||
void
|
void
|
||||||
resumeApp(PSTREAM_CONFIGURATION streamConfig);
|
startApp(QString verb,
|
||||||
|
bool isGfe,
|
||||||
void
|
int appId,
|
||||||
launchApp(int appId,
|
PSTREAM_CONFIGURATION streamConfig,
|
||||||
PSTREAM_CONFIGURATION streamConfig,
|
bool sops,
|
||||||
bool sops,
|
bool localAudio,
|
||||||
bool localAudio,
|
int gamepadMask,
|
||||||
int gamepadMask);
|
bool persistGameControllersOnDisconnect,
|
||||||
|
QString& rtspSessionUrl);
|
||||||
|
|
||||||
QVector<NvApp>
|
QVector<NvApp>
|
||||||
getAppList();
|
getAppList();
|
||||||
@@ -200,7 +195,7 @@ private:
|
|||||||
int timeoutMs,
|
int timeoutMs,
|
||||||
NvLogLevel logLevel);
|
NvLogLevel logLevel);
|
||||||
|
|
||||||
QString m_Address;
|
NvAddress m_Address;
|
||||||
QNetworkAccessManager m_Nam;
|
QNetworkAccessManager* m_Nam;
|
||||||
QSslCertificate m_ServerCert;
|
QSslCertificate m_ServerCert;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
#include "nvpairingmanager.h"
|
#include "nvpairingmanager.h"
|
||||||
#include "utils.h"
|
#include "utils.h"
|
||||||
|
|
||||||
|
#include <stdexcept>
|
||||||
|
|
||||||
#include <openssl/bio.h>
|
#include <openssl/bio.h>
|
||||||
#include <openssl/aes.h>
|
|
||||||
#include <openssl/rand.h>
|
#include <openssl/rand.h>
|
||||||
#include <openssl/pem.h>
|
#include <openssl/pem.h>
|
||||||
#include <openssl/x509.h>
|
#include <openssl/x509.h>
|
||||||
@@ -10,8 +11,8 @@
|
|||||||
|
|
||||||
#define REQUEST_TIMEOUT_MS 5000
|
#define REQUEST_TIMEOUT_MS 5000
|
||||||
|
|
||||||
NvPairingManager::NvPairingManager(QString address) :
|
NvPairingManager::NvPairingManager(NvComputer* computer) :
|
||||||
m_Http(address, QSslCertificate())
|
m_Http(computer)
|
||||||
{
|
{
|
||||||
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);
|
||||||
@@ -51,44 +52,58 @@ NvPairingManager::generateRandomBytes(int length)
|
|||||||
}
|
}
|
||||||
|
|
||||||
QByteArray
|
QByteArray
|
||||||
NvPairingManager::encrypt(QByteArray plaintext, AES_KEY* key)
|
NvPairingManager::encrypt(const QByteArray& plaintext, const QByteArray& key)
|
||||||
{
|
{
|
||||||
QByteArray ciphertext(plaintext.size(), 0);
|
QByteArray ciphertext(plaintext.size(), 0);
|
||||||
|
EVP_CIPHER_CTX* cipher;
|
||||||
|
int ciphertextLen;
|
||||||
|
|
||||||
for (int i = 0; i < plaintext.size(); i += 16)
|
cipher = EVP_CIPHER_CTX_new();
|
||||||
{
|
THROW_BAD_ALLOC_IF_NULL(cipher);
|
||||||
AES_encrypt(reinterpret_cast<unsigned char*>(&plaintext.data()[i]),
|
|
||||||
reinterpret_cast<unsigned char*>(&ciphertext.data()[i]),
|
EVP_EncryptInit(cipher, EVP_aes_128_ecb(), reinterpret_cast<const unsigned char*>(key.data()), NULL);
|
||||||
key);
|
EVP_CIPHER_CTX_set_padding(cipher, 0);
|
||||||
}
|
|
||||||
|
EVP_EncryptUpdate(cipher,
|
||||||
|
reinterpret_cast<unsigned char*>(ciphertext.data()),
|
||||||
|
&ciphertextLen,
|
||||||
|
reinterpret_cast<const unsigned char*>(plaintext.data()),
|
||||||
|
plaintext.length());
|
||||||
|
Q_ASSERT(ciphertextLen == ciphertext.length());
|
||||||
|
|
||||||
|
EVP_CIPHER_CTX_free(cipher);
|
||||||
|
|
||||||
return ciphertext;
|
return ciphertext;
|
||||||
}
|
}
|
||||||
|
|
||||||
QByteArray
|
QByteArray
|
||||||
NvPairingManager::decrypt(QByteArray ciphertext, AES_KEY* key)
|
NvPairingManager::decrypt(const QByteArray& ciphertext, const QByteArray& key)
|
||||||
{
|
{
|
||||||
QByteArray plaintext(ciphertext.size(), 0);
|
QByteArray plaintext(ciphertext.size(), 0);
|
||||||
|
EVP_CIPHER_CTX* cipher;
|
||||||
|
int plaintextLen;
|
||||||
|
|
||||||
for (int i = 0; i < plaintext.size(); i += 16)
|
cipher = EVP_CIPHER_CTX_new();
|
||||||
{
|
THROW_BAD_ALLOC_IF_NULL(cipher);
|
||||||
AES_decrypt(reinterpret_cast<unsigned char*>(&ciphertext.data()[i]),
|
|
||||||
reinterpret_cast<unsigned char*>(&plaintext.data()[i]),
|
EVP_DecryptInit(cipher, EVP_aes_128_ecb(), reinterpret_cast<const unsigned char*>(key.data()), NULL);
|
||||||
key);
|
EVP_CIPHER_CTX_set_padding(cipher, 0);
|
||||||
}
|
|
||||||
|
EVP_DecryptUpdate(cipher,
|
||||||
|
reinterpret_cast<unsigned char*>(plaintext.data()),
|
||||||
|
&plaintextLen,
|
||||||
|
reinterpret_cast<const unsigned char*>(ciphertext.data()),
|
||||||
|
ciphertext.length());
|
||||||
|
Q_ASSERT(plaintextLen == plaintext.length());
|
||||||
|
|
||||||
|
EVP_CIPHER_CTX_free(cipher);
|
||||||
|
|
||||||
return plaintext;
|
return plaintext;
|
||||||
}
|
}
|
||||||
|
|
||||||
QByteArray
|
QByteArray
|
||||||
NvPairingManager::getSignatureFromPemCert(QByteArray certificate)
|
NvPairingManager::getSignatureFromCert(X509* cert)
|
||||||
{
|
{
|
||||||
BIO* bio = BIO_new_mem_buf(certificate.data(), -1);
|
|
||||||
THROW_BAD_ALLOC_IF_NULL(bio);
|
|
||||||
|
|
||||||
X509* cert = PEM_read_bio_X509(bio, nullptr, nullptr, nullptr);
|
|
||||||
BIO_free_all(bio);
|
|
||||||
|
|
||||||
#if (OPENSSL_VERSION_NUMBER < 0x10002000L)
|
#if (OPENSSL_VERSION_NUMBER < 0x10002000L)
|
||||||
ASN1_BIT_STRING *asnSignature = cert->signature;
|
ASN1_BIT_STRING *asnSignature = cert->signature;
|
||||||
#elif (OPENSSL_VERSION_NUMBER < 0x10100000L)
|
#elif (OPENSSL_VERSION_NUMBER < 0x10100000L)
|
||||||
@@ -99,17 +114,43 @@ NvPairingManager::getSignatureFromPemCert(QByteArray certificate)
|
|||||||
X509_get0_signature(&asnSignature, NULL, cert);
|
X509_get0_signature(&asnSignature, NULL, cert);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
QByteArray signature(reinterpret_cast<char*>(asnSignature->data), asnSignature->length);
|
return QByteArray(
|
||||||
|
#if (OPENSSL_VERSION_NUMBER < 0x10100000L)
|
||||||
|
reinterpret_cast<const char*>(ASN1_STRING_data(asnSignature)),
|
||||||
|
#else
|
||||||
|
reinterpret_cast<const char*>(ASN1_STRING_get0_data(asnSignature)),
|
||||||
|
#endif
|
||||||
|
ASN1_STRING_length(asnSignature)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
QByteArray
|
||||||
|
NvPairingManager::getSignatureFromPemCert(const QByteArray& certificate)
|
||||||
|
{
|
||||||
|
#if (OPENSSL_VERSION_NUMBER < 0x10100000L)
|
||||||
|
BIO* bio = BIO_new_mem_buf(const_cast<char*>(certificate.data()), -1);
|
||||||
|
#else
|
||||||
|
BIO* bio = BIO_new_mem_buf(certificate.data(), -1);
|
||||||
|
#endif
|
||||||
|
THROW_BAD_ALLOC_IF_NULL(bio);
|
||||||
|
|
||||||
|
X509* cert = PEM_read_bio_X509(bio, nullptr, nullptr, nullptr);
|
||||||
|
BIO_free_all(bio);
|
||||||
|
|
||||||
|
QByteArray signature = getSignatureFromCert(cert);
|
||||||
X509_free(cert);
|
X509_free(cert);
|
||||||
|
|
||||||
return signature;
|
return signature;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool
|
bool
|
||||||
NvPairingManager::verifySignature(QByteArray data, QByteArray signature, QByteArray serverCertificate)
|
NvPairingManager::verifySignature(const QByteArray& data, const QByteArray& signature, const QByteArray& serverCertificate)
|
||||||
{
|
{
|
||||||
|
#if (OPENSSL_VERSION_NUMBER < 0x10100000L)
|
||||||
|
BIO* bio = BIO_new_mem_buf(const_cast<char*>(serverCertificate.data()), -1);
|
||||||
|
#else
|
||||||
BIO* bio = BIO_new_mem_buf(serverCertificate.data(), -1);
|
BIO* bio = BIO_new_mem_buf(serverCertificate.data(), -1);
|
||||||
|
#endif
|
||||||
THROW_BAD_ALLOC_IF_NULL(bio);
|
THROW_BAD_ALLOC_IF_NULL(bio);
|
||||||
|
|
||||||
X509* cert = PEM_read_bio_X509(bio, nullptr, nullptr, nullptr);
|
X509* cert = PEM_read_bio_X509(bio, nullptr, nullptr, nullptr);
|
||||||
@@ -123,7 +164,7 @@ NvPairingManager::verifySignature(QByteArray data, QByteArray signature, QByteAr
|
|||||||
|
|
||||||
EVP_DigestVerifyInit(mdctx, nullptr, EVP_sha256(), nullptr, pubKey);
|
EVP_DigestVerifyInit(mdctx, nullptr, EVP_sha256(), nullptr, pubKey);
|
||||||
EVP_DigestVerifyUpdate(mdctx, data.data(), data.length());
|
EVP_DigestVerifyUpdate(mdctx, data.data(), data.length());
|
||||||
int result = EVP_DigestVerifyFinal(mdctx, reinterpret_cast<unsigned char*>(signature.data()), signature.length());
|
int result = EVP_DigestVerifyFinal(mdctx, reinterpret_cast<unsigned char*>(const_cast<char*>(signature.data())), signature.length());
|
||||||
|
|
||||||
EVP_PKEY_free(pubKey);
|
EVP_PKEY_free(pubKey);
|
||||||
EVP_MD_CTX_destroy(mdctx);
|
EVP_MD_CTX_destroy(mdctx);
|
||||||
@@ -133,17 +174,13 @@ NvPairingManager::verifySignature(QByteArray data, QByteArray signature, QByteAr
|
|||||||
}
|
}
|
||||||
|
|
||||||
QByteArray
|
QByteArray
|
||||||
NvPairingManager::signMessage(QByteArray message)
|
NvPairingManager::signMessage(const QByteArray& message)
|
||||||
{
|
{
|
||||||
EVP_MD_CTX *ctx = EVP_MD_CTX_create();
|
EVP_MD_CTX *ctx = EVP_MD_CTX_create();
|
||||||
THROW_BAD_ALLOC_IF_NULL(ctx);
|
THROW_BAD_ALLOC_IF_NULL(ctx);
|
||||||
|
|
||||||
const EVP_MD *md = EVP_get_digestbyname("SHA256");
|
EVP_DigestSignInit(ctx, NULL, EVP_sha256(), NULL, m_PrivateKey);
|
||||||
THROW_BAD_ALLOC_IF_NULL(md);
|
EVP_DigestSignUpdate(ctx, reinterpret_cast<unsigned char*>(const_cast<char*>(message.data())), message.length());
|
||||||
|
|
||||||
EVP_DigestInit_ex(ctx, md, NULL);
|
|
||||||
EVP_DigestSignInit(ctx, NULL, md, NULL, m_PrivateKey);
|
|
||||||
EVP_DigestSignUpdate(ctx, reinterpret_cast<unsigned char*>(message.data()), message.length());
|
|
||||||
|
|
||||||
size_t signatureLength = 0;
|
size_t signatureLength = 0;
|
||||||
EVP_DigestSignFinal(ctx, NULL, &signatureLength);
|
EVP_DigestSignFinal(ctx, NULL, &signatureLength);
|
||||||
@@ -157,9 +194,9 @@ NvPairingManager::signMessage(QByteArray message)
|
|||||||
}
|
}
|
||||||
|
|
||||||
QByteArray
|
QByteArray
|
||||||
NvPairingManager::saltPin(QByteArray salt, QString pin)
|
NvPairingManager::saltPin(const QByteArray& salt, QString pin)
|
||||||
{
|
{
|
||||||
return QByteArray().append(salt).append(pin.toLatin1());
|
return QByteArray().append(salt).append(pin.toUtf8());
|
||||||
}
|
}
|
||||||
|
|
||||||
NvPairingManager::PairState
|
NvPairingManager::PairState
|
||||||
@@ -186,9 +223,8 @@ NvPairingManager::pair(QString appVersion, QString pin, QSslCertificate& serverC
|
|||||||
QByteArray salt = generateRandomBytes(16);
|
QByteArray salt = generateRandomBytes(16);
|
||||||
QByteArray saltedPin = saltPin(salt, pin);
|
QByteArray saltedPin = saltPin(salt, pin);
|
||||||
|
|
||||||
AES_KEY encKey, decKey;
|
QByteArray aesKey = QCryptographicHash::hash(saltedPin, hashAlgo).constData();
|
||||||
AES_set_decrypt_key(reinterpret_cast<const unsigned char*>(QCryptographicHash::hash(saltedPin, hashAlgo).data()), 128, &decKey);
|
aesKey.truncate(16);
|
||||||
AES_set_encrypt_key(reinterpret_cast<const unsigned char*>(QCryptographicHash::hash(saltedPin, hashAlgo).data()), 128, &encKey);
|
|
||||||
|
|
||||||
QString getCert = m_Http.openConnectionToString(m_Http.m_BaseUrlHttp,
|
QString getCert = m_Http.openConnectionToString(m_Http.m_BaseUrlHttp,
|
||||||
"pair",
|
"pair",
|
||||||
@@ -210,20 +246,21 @@ NvPairingManager::pair(QString appVersion, QString pin, QSslCertificate& serverC
|
|||||||
return PairState::ALREADY_IN_PROGRESS;
|
return PairState::ALREADY_IN_PROGRESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
serverCert = QSslCertificate(serverCertStr);
|
QSslCertificate unverifiedServerCert = QSslCertificate(serverCertStr);
|
||||||
if (serverCert.isNull()) {
|
if (unverifiedServerCert.isNull()) {
|
||||||
Q_ASSERT(!serverCert.isNull());
|
Q_ASSERT(!unverifiedServerCert.isNull());
|
||||||
|
|
||||||
qCritical() << "Failed to parse plaincert";
|
qCritical() << "Failed to parse plaincert";
|
||||||
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, REQUEST_TIMEOUT_MS);
|
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, REQUEST_TIMEOUT_MS);
|
||||||
return PairState::FAILED;
|
return PairState::FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pin this cert for TLS
|
// Pin this cert for TLS until pairing is complete. If successful, we will propagate
|
||||||
m_Http.setServerCert(serverCert);
|
// the cert into the NvComputer object and persist it.
|
||||||
|
m_Http.setServerCert(unverifiedServerCert);
|
||||||
|
|
||||||
QByteArray randomChallenge = generateRandomBytes(16);
|
QByteArray randomChallenge = generateRandomBytes(16);
|
||||||
QByteArray encryptedChallenge = encrypt(randomChallenge, &encKey);
|
QByteArray encryptedChallenge = encrypt(randomChallenge, aesKey);
|
||||||
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=" +
|
||||||
@@ -237,28 +274,18 @@ NvPairingManager::pair(QString appVersion, QString pin, QSslCertificate& serverC
|
|||||||
return PairState::FAILED;
|
return PairState::FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
QByteArray challengeResponseData = decrypt(m_Http.getXmlStringFromHex(challengeXml, "challengeresponse"), &decKey);
|
QByteArray challengeResponseData = decrypt(m_Http.getXmlStringFromHex(challengeXml, "challengeresponse"), aesKey);
|
||||||
QByteArray clientSecretData = generateRandomBytes(16);
|
QByteArray clientSecretData = generateRandomBytes(16);
|
||||||
QByteArray challengeResponse;
|
QByteArray challengeResponse;
|
||||||
QByteArray serverResponse(challengeResponseData.data(), hashLength);
|
QByteArray serverResponse(challengeResponseData.data(), hashLength);
|
||||||
|
|
||||||
#if (OPENSSL_VERSION_NUMBER < 0x10002000L)
|
|
||||||
ASN1_BIT_STRING *asnSignature = m_Cert->signature;
|
|
||||||
#elif (OPENSSL_VERSION_NUMBER < 0x10100000L)
|
|
||||||
ASN1_BIT_STRING *asnSignature;
|
|
||||||
X509_get0_signature(&asnSignature, NULL, m_Cert);
|
|
||||||
#else
|
|
||||||
const ASN1_BIT_STRING *asnSignature;
|
|
||||||
X509_get0_signature(&asnSignature, NULL, m_Cert);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
challengeResponse.append(challengeResponseData.data() + hashLength, 16);
|
challengeResponse.append(challengeResponseData.data() + hashLength, 16);
|
||||||
challengeResponse.append(reinterpret_cast<char*>(asnSignature->data), asnSignature->length);
|
challengeResponse.append(getSignatureFromCert(m_Cert));
|
||||||
challengeResponse.append(clientSecretData);
|
challengeResponse.append(clientSecretData);
|
||||||
|
|
||||||
QByteArray paddedHash = QCryptographicHash::hash(challengeResponse, hashAlgo);
|
QByteArray paddedHash = QCryptographicHash::hash(challengeResponse, hashAlgo);
|
||||||
paddedHash.resize(32);
|
paddedHash.resize(32);
|
||||||
QByteArray encryptedChallengeResponseHash = encrypt(paddedHash, &encKey);
|
QByteArray encryptedChallengeResponseHash = encrypt(paddedHash, aesKey);
|
||||||
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=" +
|
||||||
@@ -273,8 +300,8 @@ NvPairingManager::pair(QString appVersion, QString pin, QSslCertificate& serverC
|
|||||||
}
|
}
|
||||||
|
|
||||||
QByteArray pairingSecret = NvHTTP::getXmlStringFromHex(respXml, "pairingsecret");
|
QByteArray pairingSecret = NvHTTP::getXmlStringFromHex(respXml, "pairingsecret");
|
||||||
QByteArray serverSecret = QByteArray(pairingSecret.data(), 16);
|
QByteArray serverSecret = pairingSecret.left(16);
|
||||||
QByteArray serverSignature = QByteArray(&pairingSecret.data()[16], 256);
|
QByteArray serverSignature = pairingSecret.mid(16);
|
||||||
|
|
||||||
if (!verifySignature(serverSecret,
|
if (!verifySignature(serverSecret,
|
||||||
serverSignature,
|
serverSignature,
|
||||||
@@ -325,5 +352,6 @@ NvPairingManager::pair(QString appVersion, QString pin, QSslCertificate& serverC
|
|||||||
return PairState::FAILED;
|
return PairState::FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
serverCert = std::move(unverifiedServerCert);
|
||||||
return PairState::PAIRED;
|
return PairState::PAIRED;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
#include "identitymanager.h"
|
#include "identitymanager.h"
|
||||||
#include "nvhttp.h"
|
#include "nvhttp.h"
|
||||||
|
|
||||||
#include <openssl/aes.h>
|
|
||||||
#include <openssl/x509.h>
|
#include <openssl/x509.h>
|
||||||
#include <openssl/evp.h>
|
#include <openssl/evp.h>
|
||||||
|
|
||||||
@@ -18,7 +17,7 @@ public:
|
|||||||
ALREADY_IN_PROGRESS
|
ALREADY_IN_PROGRESS
|
||||||
};
|
};
|
||||||
|
|
||||||
explicit NvPairingManager(QString address);
|
explicit NvPairingManager(NvComputer* computer);
|
||||||
|
|
||||||
~NvPairingManager();
|
~NvPairingManager();
|
||||||
|
|
||||||
@@ -30,22 +29,25 @@ private:
|
|||||||
generateRandomBytes(int length);
|
generateRandomBytes(int length);
|
||||||
|
|
||||||
QByteArray
|
QByteArray
|
||||||
saltPin(QByteArray salt, QString pin);
|
saltPin(const QByteArray& salt, QString pin);
|
||||||
|
|
||||||
QByteArray
|
QByteArray
|
||||||
encrypt(QByteArray plaintext, AES_KEY* key);
|
encrypt(const QByteArray& plaintext, const QByteArray& key);
|
||||||
|
|
||||||
QByteArray
|
QByteArray
|
||||||
decrypt(QByteArray ciphertext, AES_KEY* key);
|
decrypt(const QByteArray& ciphertext, const QByteArray& key);
|
||||||
|
|
||||||
QByteArray
|
QByteArray
|
||||||
getSignatureFromPemCert(QByteArray certificate);
|
getSignatureFromCert(X509* cert);
|
||||||
|
|
||||||
|
QByteArray
|
||||||
|
getSignatureFromPemCert(const QByteArray& certificate);
|
||||||
|
|
||||||
bool
|
bool
|
||||||
verifySignature(QByteArray data, QByteArray signature, QByteArray serverCertificate);
|
verifySignature(const QByteArray& data, const QByteArray& signature, const QByteArray& serverCertificate);
|
||||||
|
|
||||||
QByteArray
|
QByteArray
|
||||||
signMessage(QByteArray message);
|
signMessage(const QByteArray& message);
|
||||||
|
|
||||||
NvHTTP m_Http;
|
NvHTTP m_Http;
|
||||||
X509* m_Cert;
|
X509* m_Cert;
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ RichPresenceManager::RichPresenceManager(StreamingPreferences& prefs, QString ga
|
|||||||
discordPresence.largeImageKey = "icon";
|
discordPresence.largeImageKey = "icon";
|
||||||
Discord_UpdatePresence(&discordPresence);
|
Discord_UpdatePresence(&discordPresence);
|
||||||
}
|
}
|
||||||
|
#else
|
||||||
|
Q_UNUSED(prefs)
|
||||||
|
Q_UNUSED(gameName)
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,26 +1,95 @@
|
|||||||
#include "systemproperties.h"
|
#include "systemproperties.h"
|
||||||
|
#include "utils.h"
|
||||||
|
|
||||||
#include <QGuiApplication>
|
#include <QGuiApplication>
|
||||||
|
#include <QLibraryInfo>
|
||||||
|
|
||||||
#include "streaming/session.h"
|
#include "streaming/session.h"
|
||||||
#include "streaming/streamutils.h"
|
#include "streaming/streamutils.h"
|
||||||
|
|
||||||
|
#ifdef Q_OS_WIN32
|
||||||
|
#define WIN32_LEAN_AND_MEAN
|
||||||
|
#include <Windows.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
class SystemPropertyQueryThread : public QThread
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
SystemPropertyQueryThread(SystemProperties* properties)
|
||||||
|
: QThread(properties), m_Properties(properties)
|
||||||
|
{
|
||||||
|
setObjectName("System Properties Async Query Thread");
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
void run() override
|
||||||
|
{
|
||||||
|
bool hasHardwareAcceleration;
|
||||||
|
bool rendererAlwaysFullScreen;
|
||||||
|
bool supportsHdr;
|
||||||
|
QSize maximumResolution;
|
||||||
|
|
||||||
|
Session::getDecoderInfo(m_Properties->testWindow, hasHardwareAcceleration, rendererAlwaysFullScreen, supportsHdr, maximumResolution);
|
||||||
|
|
||||||
|
// Propagate the decoder properties to the SystemProperties singleton and emit any change signals on the main thread
|
||||||
|
QMetaObject::invokeMethod(m_Properties, "updateDecoderProperties",
|
||||||
|
Qt::QueuedConnection,
|
||||||
|
Q_ARG(bool, hasHardwareAcceleration),
|
||||||
|
Q_ARG(bool, rendererAlwaysFullScreen),
|
||||||
|
Q_ARG(QSize, maximumResolution),
|
||||||
|
Q_ARG(bool, supportsHdr));
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
SystemProperties* m_Properties;
|
||||||
|
};
|
||||||
|
|
||||||
SystemProperties::SystemProperties()
|
SystemProperties::SystemProperties()
|
||||||
{
|
{
|
||||||
isRunningWayland = qgetenv("XDG_SESSION_TYPE") == "wayland";
|
versionString = QString(VERSION_STR);
|
||||||
isRunningXWayland = qgetenv("XDG_SESSION_TYPE") == "wayland" && QGuiApplication::platformName() == "xcb";
|
hasDesktopEnvironment = WMUtils::isRunningDesktopEnvironment();
|
||||||
|
isRunningWayland = WMUtils::isRunningWayland();
|
||||||
|
isRunningXWayland = isRunningWayland && QGuiApplication::platformName() == "xcb";
|
||||||
|
usesMaterial3Theme = QLibraryInfo::version() >= QVersionNumber(6, 5, 0);
|
||||||
|
QString nativeArch = QSysInfo::currentCpuArchitecture();
|
||||||
|
|
||||||
#ifdef Q_OS_WIN32
|
#ifdef Q_OS_WIN32
|
||||||
isWow64 = QSysInfo::currentCpuArchitecture() != QSysInfo::buildCpuArchitecture();
|
{
|
||||||
|
USHORT processArch, machineArch;
|
||||||
|
|
||||||
|
// Use IsWow64Process2() because it doesn't lie on ARM64
|
||||||
|
if (IsWow64Process2(GetCurrentProcess(), &processArch, &machineArch)) {
|
||||||
|
switch (machineArch) {
|
||||||
|
case IMAGE_FILE_MACHINE_I386:
|
||||||
|
nativeArch = "i386";
|
||||||
|
break;
|
||||||
|
case IMAGE_FILE_MACHINE_AMD64:
|
||||||
|
nativeArch = "x86_64";
|
||||||
|
break;
|
||||||
|
case IMAGE_FILE_MACHINE_ARM64:
|
||||||
|
nativeArch = "arm64";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isWow64 = nativeArch != QSysInfo::buildCpuArchitecture();
|
||||||
|
}
|
||||||
#else
|
#else
|
||||||
isWow64 = false;
|
isWow64 = false;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifndef STEAM_LINK
|
if (nativeArch == "i386") {
|
||||||
hasBrowser = true;
|
friendlyNativeArchName = "x86";
|
||||||
#else
|
}
|
||||||
hasBrowser = false;
|
else if (nativeArch == "x86_64") {
|
||||||
#endif
|
friendlyNativeArchName = "x64";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
friendlyNativeArchName = nativeArch.toUpper();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assume we can probably launch a browser if we're in a GUI environment
|
||||||
|
hasBrowser = hasDesktopEnvironment;
|
||||||
|
|
||||||
#ifdef HAVE_DISCORD
|
#ifdef HAVE_DISCORD
|
||||||
hasDiscordIntegration = true;
|
hasDiscordIntegration = true;
|
||||||
@@ -28,38 +97,86 @@ SystemProperties::SystemProperties()
|
|||||||
hasDiscordIntegration = false;
|
hasDiscordIntegration = false;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
unmappedGamepads = SdlInputHandler::getUnmappedGamepads();
|
// These will be queried asynchronously to avoid blocking the UI
|
||||||
|
hasHardwareAcceleration = true;
|
||||||
// Populate data that requires talking to SDL. We do it all in one shot
|
rendererAlwaysFullScreen = false;
|
||||||
// and cache the results to speed up future queries on this data.
|
supportsHdr = true;
|
||||||
querySdlVideoInfo();
|
maximumResolution = QSize(0, 0);
|
||||||
|
|
||||||
Q_ASSERT(maximumStreamingFrameRate >= 60);
|
|
||||||
Q_ASSERT(!monitorDesktopResolutions.isEmpty());
|
|
||||||
Q_ASSERT(!monitorNativeResolutions.isEmpty());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QRect SystemProperties::getDesktopResolution(int displayIndex)
|
SystemProperties::~SystemProperties()
|
||||||
{
|
{
|
||||||
// Returns default constructed QRect if out of bounds
|
waitForAsyncLoad();
|
||||||
return monitorDesktopResolutions.value(displayIndex);
|
}
|
||||||
|
|
||||||
|
void SystemProperties::updateDecoderProperties(bool hasHardwareAcceleration, bool rendererAlwaysFullScreen, QSize maximumResolution, bool supportsHdr)
|
||||||
|
{
|
||||||
|
SDL_assert(testWindow);
|
||||||
|
|
||||||
|
if (hasHardwareAcceleration != this->hasHardwareAcceleration) {
|
||||||
|
this->hasHardwareAcceleration = hasHardwareAcceleration;
|
||||||
|
emit hasHardwareAccelerationChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rendererAlwaysFullScreen != this->rendererAlwaysFullScreen) {
|
||||||
|
this->rendererAlwaysFullScreen = rendererAlwaysFullScreen;
|
||||||
|
emit rendererAlwaysFullScreenChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (maximumResolution != this->maximumResolution) {
|
||||||
|
this->maximumResolution = maximumResolution;
|
||||||
|
emit maximumResolutionChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (supportsHdr != this->supportsHdr) {
|
||||||
|
this->supportsHdr = supportsHdr;
|
||||||
|
emit supportsHdrChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_DestroyWindow(testWindow);
|
||||||
|
testWindow = nullptr;
|
||||||
|
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||||
}
|
}
|
||||||
|
|
||||||
QRect SystemProperties::getNativeResolution(int displayIndex)
|
QRect SystemProperties::getNativeResolution(int displayIndex)
|
||||||
{
|
{
|
||||||
// Returns default constructed QRect if out of bounds
|
// Returns default constructed QRect if out of bounds
|
||||||
|
Q_ASSERT(!monitorNativeResolutions.isEmpty());
|
||||||
return monitorNativeResolutions.value(displayIndex);
|
return monitorNativeResolutions.value(displayIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
void SystemProperties::querySdlVideoInfo()
|
QRect SystemProperties::getSafeAreaResolution(int displayIndex)
|
||||||
{
|
{
|
||||||
monitorDesktopResolutions.clear();
|
// Returns default constructed QRect if out of bounds
|
||||||
monitorNativeResolutions.clear();
|
Q_ASSERT(!monitorSafeAreaResolutions.isEmpty());
|
||||||
hasHardwareAcceleration = false;
|
return monitorSafeAreaResolutions.value(displayIndex);
|
||||||
|
}
|
||||||
|
|
||||||
// Never let the maximum drop below 60 FPS
|
int SystemProperties::getRefreshRate(int displayIndex)
|
||||||
maximumStreamingFrameRate = 60;
|
{
|
||||||
|
// Returns 0 if out of bounds
|
||||||
|
Q_ASSERT(!monitorRefreshRates.isEmpty());
|
||||||
|
return monitorRefreshRates.value(displayIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SystemProperties::startAsyncLoad()
|
||||||
|
{
|
||||||
|
if (systemPropertyQueryThread) {
|
||||||
|
// Already started/completed
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This isn't actually asynchronous (due to the need to synchronize with
|
||||||
|
// SdlGamepadKeyNavigation), but we don't query it in the constructor
|
||||||
|
// because it's expensive.
|
||||||
|
unmappedGamepads = SdlInputHandler::getUnmappedGamepads();
|
||||||
|
if (!unmappedGamepads.isEmpty()) {
|
||||||
|
emit unmappedGamepadsChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
// We initialize the video subsystem and test window on the main thread
|
||||||
|
// because some platforms (macOS) do not support window creation on
|
||||||
|
// non-main threads.
|
||||||
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
|
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s",
|
"SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s",
|
||||||
@@ -67,27 +184,64 @@ void SystemProperties::querySdlVideoInfo()
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
testWindow = StreamUtils::createTestWindow();
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update display related attributes (max FPS, native resolution, etc).
|
||||||
|
//
|
||||||
|
// NB: SDL3 will forcefully refresh displays when a window is created,
|
||||||
|
// so we place this after the window creation to ensure we don't pay
|
||||||
|
// the penalty for mode enumeration twice.
|
||||||
|
refreshDisplays();
|
||||||
|
|
||||||
|
systemPropertyQueryThread = new SystemPropertyQueryThread(this);
|
||||||
|
systemPropertyQueryThread->start();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SystemProperties::waitForAsyncLoad()
|
||||||
|
{
|
||||||
|
if (systemPropertyQueryThread) {
|
||||||
|
systemPropertyQueryThread->wait();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SystemProperties::refreshDisplays()
|
||||||
|
{
|
||||||
|
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
monitorNativeResolutions.clear();
|
||||||
|
|
||||||
SDL_DisplayMode bestMode;
|
SDL_DisplayMode bestMode;
|
||||||
for (int displayIndex = 0; displayIndex < SDL_GetNumVideoDisplays(); displayIndex++) {
|
for (int displayIndex = 0; displayIndex < SDL_GetNumVideoDisplays(); displayIndex++) {
|
||||||
SDL_DisplayMode desktopMode;
|
SDL_DisplayMode desktopMode;
|
||||||
int err;
|
SDL_Rect safeArea;
|
||||||
|
|
||||||
err = SDL_GetDesktopDisplayMode(displayIndex, &desktopMode);
|
if (StreamUtils::getNativeDesktopMode(displayIndex, &desktopMode, &safeArea)) {
|
||||||
if (err == 0) {
|
if (desktopMode.w <= 8192 && desktopMode.h <= 8192) {
|
||||||
monitorDesktopResolutions.insert(displayIndex, QRect(0, 0, desktopMode.w, desktopMode.h));
|
monitorNativeResolutions.insert(displayIndex, QRect(0, 0, desktopMode.w, desktopMode.h));
|
||||||
}
|
monitorSafeAreaResolutions.insert(displayIndex, QRect(0, 0, safeArea.w, safeArea.h));
|
||||||
else {
|
}
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
else {
|
||||||
"SDL_GetDesktopDisplayMode() failed: %s",
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
SDL_GetError());
|
"Skipping resolution over 8K: %dx%d",
|
||||||
}
|
desktopMode.w, desktopMode.h);
|
||||||
|
}
|
||||||
if (StreamUtils::getRealDesktopMode(displayIndex, &desktopMode)) {
|
|
||||||
monitorNativeResolutions.insert(displayIndex, QRect(0, 0, desktopMode.w, desktopMode.h));
|
|
||||||
|
|
||||||
// Start at desktop mode and work our way up
|
// Start at desktop mode and work our way up
|
||||||
bestMode = desktopMode;
|
bestMode = desktopMode;
|
||||||
for (int i = 0; i < SDL_GetNumDisplayModes(displayIndex); i++) {
|
int numDisplayModes = SDL_GetNumDisplayModes(displayIndex);
|
||||||
|
for (int i = 0; i < numDisplayModes; i++) {
|
||||||
SDL_DisplayMode mode;
|
SDL_DisplayMode mode;
|
||||||
if (SDL_GetDisplayMode(displayIndex, i, &mode) == 0) {
|
if (SDL_GetDisplayMode(displayIndex, i, &mode) == 0) {
|
||||||
if (mode.w == desktopMode.w && mode.h == desktopMode.h) {
|
if (mode.w == desktopMode.w && mode.h == desktopMode.h) {
|
||||||
@@ -98,26 +252,19 @@ void SystemProperties::querySdlVideoInfo()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
maximumStreamingFrameRate = qMax(maximumStreamingFrameRate, bestMode.refresh_rate);
|
// Try to normalize values around our our standard refresh rates.
|
||||||
|
// Some displays/OSes report values that are slightly off.
|
||||||
|
if (bestMode.refresh_rate >= 58 && bestMode.refresh_rate <= 62) {
|
||||||
|
monitorRefreshRates.append(60);
|
||||||
|
}
|
||||||
|
else if (bestMode.refresh_rate >= 28 && bestMode.refresh_rate <= 32) {
|
||||||
|
monitorRefreshRates.append(30);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
monitorRefreshRates.append(bestMode.refresh_rate);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,40 +3,80 @@
|
|||||||
#include <QObject>
|
#include <QObject>
|
||||||
#include <QRect>
|
#include <QRect>
|
||||||
|
|
||||||
|
#include "SDL_compat.h"
|
||||||
|
|
||||||
class SystemProperties : public QObject
|
class SystemProperties : public QObject
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
|
friend class SystemPropertyQueryThread;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
SystemProperties();
|
SystemProperties();
|
||||||
|
~SystemProperties();
|
||||||
|
|
||||||
Q_PROPERTY(bool hasHardwareAcceleration MEMBER hasHardwareAcceleration CONSTANT)
|
// Static properties queried synchronously during the constructor
|
||||||
Q_PROPERTY(bool isRunningWayland MEMBER isRunningWayland CONSTANT)
|
Q_PROPERTY(bool isRunningWayland MEMBER isRunningWayland CONSTANT)
|
||||||
Q_PROPERTY(bool isRunningXWayland MEMBER isRunningXWayland CONSTANT)
|
Q_PROPERTY(bool isRunningXWayland MEMBER isRunningXWayland CONSTANT)
|
||||||
Q_PROPERTY(bool isWow64 MEMBER isWow64 CONSTANT)
|
Q_PROPERTY(bool isWow64 MEMBER isWow64 CONSTANT)
|
||||||
|
Q_PROPERTY(QString friendlyNativeArchName MEMBER friendlyNativeArchName CONSTANT)
|
||||||
|
Q_PROPERTY(bool hasDesktopEnvironment MEMBER hasDesktopEnvironment CONSTANT)
|
||||||
Q_PROPERTY(bool hasBrowser MEMBER hasBrowser CONSTANT)
|
Q_PROPERTY(bool hasBrowser MEMBER hasBrowser CONSTANT)
|
||||||
Q_PROPERTY(bool hasDiscordIntegration MEMBER hasDiscordIntegration CONSTANT)
|
Q_PROPERTY(bool hasDiscordIntegration MEMBER hasDiscordIntegration CONSTANT)
|
||||||
Q_PROPERTY(QString unmappedGamepads MEMBER unmappedGamepads NOTIFY unmappedGamepadsChanged)
|
Q_PROPERTY(bool usesMaterial3Theme MEMBER usesMaterial3Theme CONSTANT)
|
||||||
Q_PROPERTY(int maximumStreamingFrameRate MEMBER maximumStreamingFrameRate CONSTANT)
|
Q_PROPERTY(QString versionString MEMBER versionString CONSTANT)
|
||||||
|
|
||||||
Q_INVOKABLE QRect getDesktopResolution(int displayIndex);
|
// Properties queried asynchronously (startAsyncLoad() must be called!)
|
||||||
|
Q_PROPERTY(bool hasHardwareAcceleration MEMBER hasHardwareAcceleration NOTIFY hasHardwareAccelerationChanged)
|
||||||
|
Q_PROPERTY(bool rendererAlwaysFullScreen MEMBER rendererAlwaysFullScreen NOTIFY rendererAlwaysFullScreenChanged)
|
||||||
|
Q_PROPERTY(QString unmappedGamepads MEMBER unmappedGamepads NOTIFY unmappedGamepadsChanged)
|
||||||
|
Q_PROPERTY(QSize maximumResolution MEMBER maximumResolution NOTIFY maximumResolutionChanged)
|
||||||
|
Q_PROPERTY(bool supportsHdr MEMBER supportsHdr NOTIFY supportsHdrChanged)
|
||||||
|
|
||||||
|
// Either startAsyncLoad()+waitForAsyncLoad() or refreshDisplays() must be invoked first
|
||||||
Q_INVOKABLE QRect getNativeResolution(int displayIndex);
|
Q_INVOKABLE QRect getNativeResolution(int displayIndex);
|
||||||
|
Q_INVOKABLE QRect getSafeAreaResolution(int displayIndex);
|
||||||
|
Q_INVOKABLE int getRefreshRate(int displayIndex);
|
||||||
|
|
||||||
|
Q_INVOKABLE void startAsyncLoad();
|
||||||
|
Q_INVOKABLE void waitForAsyncLoad();
|
||||||
|
Q_INVOKABLE void refreshDisplays();
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void unmappedGamepadsChanged();
|
void unmappedGamepadsChanged();
|
||||||
|
void hasHardwareAccelerationChanged();
|
||||||
|
void rendererAlwaysFullScreenChanged();
|
||||||
|
void maximumResolutionChanged();
|
||||||
|
void supportsHdrChanged();
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void updateDecoderProperties(bool hasHardwareAcceleration, bool rendererAlwaysFullScreen, QSize maximumResolution, bool supportsHdr);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void querySdlVideoInfo();
|
QThread* systemPropertyQueryThread = nullptr;
|
||||||
|
SDL_Window* testWindow = nullptr;
|
||||||
|
|
||||||
bool hasHardwareAcceleration;
|
// Properties set by the constructor
|
||||||
bool isRunningWayland;
|
bool isRunningWayland;
|
||||||
bool isRunningXWayland;
|
bool isRunningXWayland;
|
||||||
bool isWow64;
|
bool isWow64;
|
||||||
|
QString friendlyNativeArchName;
|
||||||
|
bool hasDesktopEnvironment;
|
||||||
bool hasBrowser;
|
bool hasBrowser;
|
||||||
bool hasDiscordIntegration;
|
bool hasDiscordIntegration;
|
||||||
|
QString versionString;
|
||||||
|
bool usesMaterial3Theme;
|
||||||
|
|
||||||
|
// Properties only set if startAsyncLoad() is called
|
||||||
|
bool hasHardwareAcceleration;
|
||||||
|
bool rendererAlwaysFullScreen;
|
||||||
|
QSize maximumResolution;
|
||||||
|
bool supportsHdr;
|
||||||
QString unmappedGamepads;
|
QString unmappedGamepads;
|
||||||
int maximumStreamingFrameRate;
|
|
||||||
QList<QRect> monitorDesktopResolutions;
|
// Properties set by refreshDisplays()
|
||||||
QList<QRect> monitorNativeResolutions;
|
QList<QRect> monitorNativeResolutions;
|
||||||
|
QList<QRect> monitorSafeAreaResolutions;
|
||||||
|
QList<int> monitorRefreshRates;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+212
-33
@@ -52,20 +52,13 @@ public:
|
|||||||
|
|
||||||
void handleUnknownOptions()
|
void handleUnknownOptions()
|
||||||
{
|
{
|
||||||
if (unknownOptionNames().length()) {
|
if (!unknownOptionNames().isEmpty()) {
|
||||||
showError(QString("Unknown options: %1").arg(unknownOptionNames().join(", ")));
|
showError(QString("Unknown options: %1").arg(unknownOptionNames().join(", ")));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void showMessage(QString message, MessageType type) const
|
void showMessage(QString message, MessageType type) const
|
||||||
{
|
{
|
||||||
#if defined(Q_OS_WIN32)
|
|
||||||
UINT flags = MB_OK | MB_TOPMOST | MB_SETFOREGROUND;
|
|
||||||
flags |= (type == Info ? MB_ICONINFORMATION : MB_ICONERROR);
|
|
||||||
QString title = "Moonlight";
|
|
||||||
MessageBoxW(nullptr, reinterpret_cast<const wchar_t *>(message.utf16()),
|
|
||||||
reinterpret_cast<const wchar_t *>(title.utf16()), flags);
|
|
||||||
#endif
|
|
||||||
message = message.endsWith('\n') ? message : message + '\n';
|
message = message.endsWith('\n') ? message : message + '\n';
|
||||||
fputs(qPrintable(message), type == Info ? stdout : stderr);
|
fputs(qPrintable(message), type == Info ? stdout : stderr);
|
||||||
}
|
}
|
||||||
@@ -113,7 +106,7 @@ public:
|
|||||||
|
|
||||||
QPair<int,int> getResolutionOptionValue(QString name) const
|
QPair<int,int> getResolutionOptionValue(QString name) const
|
||||||
{
|
{
|
||||||
QRegularExpression re("^(\\d+)x(\\d+)$", QRegularExpression::CaseInsensitiveOption);
|
static QRegularExpression re("^(\\d+)x(\\d+)$", QRegularExpression::CaseInsensitiveOption);
|
||||||
auto match = re.match(value(name));
|
auto match = re.match(value(name));
|
||||||
if (!match.hasMatch()) {
|
if (!match.hasMatch()) {
|
||||||
showError(QString("Invalid %1 format: %2").arg(name, value(name)));
|
showError(QString("Invalid %1 format: %2").arg(name, value(name)));
|
||||||
@@ -164,28 +157,45 @@ 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"
|
||||||
|
" list List the available apps on a host\n"
|
||||||
" quit Quit the currently running app\n"
|
" quit Quit the currently running app\n"
|
||||||
" stream Start streaming an app\n"
|
" stream Start streaming an app\n"
|
||||||
|
" pair Pair a new host\n"
|
||||||
"\n"
|
"\n"
|
||||||
"See 'moonlight <action> --help' for help of specific action."
|
"See 'moonlight <action> --help' for help of specific action."
|
||||||
);
|
);
|
||||||
parser.addPositionalArgument("action", "Action to execute", "<action>");
|
parser.addPositionalArgument("action", "Action to execute", "<action>");
|
||||||
parser.parse(args);
|
parser.parse(args);
|
||||||
auto posArgs = parser.positionalArguments();
|
auto posArgs = parser.positionalArguments();
|
||||||
QString action = posArgs.isEmpty() ? QString() : posArgs.first().toLower();
|
|
||||||
|
|
||||||
if (action == "") {
|
if (posArgs.isEmpty()) {
|
||||||
// This method will not return and terminates the process if --version
|
// This method will not return and terminates the process if --version
|
||||||
// or --help is specified
|
// or --help is specified
|
||||||
parser.handleHelpAndVersionOptions();
|
parser.handleHelpAndVersionOptions();
|
||||||
parser.handleUnknownOptions();
|
parser.handleUnknownOptions();
|
||||||
return NormalStartRequested;
|
return NormalStartRequested;
|
||||||
} else if (action == "quit") {
|
}
|
||||||
return QuitRequested;
|
else {
|
||||||
} else if (action == "stream") {
|
// If users supply arguments that accept values prior to the "quit"
|
||||||
return StreamRequested;
|
// or "stream" positional arguments, we will not be able to correctly
|
||||||
} else {
|
// parse the value out of the input because this QCommandLineParser
|
||||||
parser.showError(QString("Invalid action: %1").arg(action));
|
// doesn't know about all of the options that "quit" and "stream"
|
||||||
|
// commands can accept. To work around this issue, we just look
|
||||||
|
// for "quit" or "stream" positional arguments anywhere.
|
||||||
|
for (int i = 0; i < posArgs.size(); i++) {
|
||||||
|
QString action = posArgs.at(i).toLower();
|
||||||
|
if (action == "quit") {
|
||||||
|
return QuitRequested;
|
||||||
|
} else if (action == "stream") {
|
||||||
|
return StreamRequested;
|
||||||
|
} else if (action == "pair") {
|
||||||
|
return PairRequested;
|
||||||
|
} else if (action == "list") {
|
||||||
|
return ListRequested;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parser.showError(QString("Invalid action"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,6 +241,58 @@ QString QuitCommandLineParser::getHost() const
|
|||||||
return m_Host;
|
return m_Host;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PairCommandLineParser::PairCommandLineParser()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
PairCommandLineParser::~PairCommandLineParser()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void PairCommandLineParser::parse(const QStringList &args)
|
||||||
|
{
|
||||||
|
CommandLineParser parser;
|
||||||
|
parser.setupCommonOptions();
|
||||||
|
parser.setApplicationDescription(
|
||||||
|
"\n"
|
||||||
|
"Pair with the specified host."
|
||||||
|
);
|
||||||
|
parser.addPositionalArgument("pair", "pair host");
|
||||||
|
parser.addPositionalArgument("host", "Host computer name, UUID, or IP address", "<host>");
|
||||||
|
parser.addValueOption("pin", "4 digit pairing PIN");
|
||||||
|
|
||||||
|
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);
|
||||||
|
m_PredefinedPin = parser.value("pin");
|
||||||
|
if (!m_PredefinedPin.isEmpty() && m_PredefinedPin.length() != 4) {
|
||||||
|
parser.showError("PIN must be 4 digits");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QString PairCommandLineParser::getHost() const
|
||||||
|
{
|
||||||
|
return m_Host;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString PairCommandLineParser::getPredefinedPin() const
|
||||||
|
{
|
||||||
|
return m_PredefinedPin;
|
||||||
|
}
|
||||||
|
|
||||||
StreamCommandLineParser::StreamCommandLineParser()
|
StreamCommandLineParser::StreamCommandLineParser()
|
||||||
{
|
{
|
||||||
m_WindowModeMap = {
|
m_WindowModeMap = {
|
||||||
@@ -241,16 +303,23 @@ StreamCommandLineParser::StreamCommandLineParser()
|
|||||||
m_AudioConfigMap = {
|
m_AudioConfigMap = {
|
||||||
{"stereo", StreamingPreferences::AC_STEREO},
|
{"stereo", StreamingPreferences::AC_STEREO},
|
||||||
{"5.1-surround", StreamingPreferences::AC_51_SURROUND},
|
{"5.1-surround", StreamingPreferences::AC_51_SURROUND},
|
||||||
|
{"7.1-surround", StreamingPreferences::AC_71_SURROUND},
|
||||||
};
|
};
|
||||||
m_VideoCodecMap = {
|
m_VideoCodecMap = {
|
||||||
{"auto", StreamingPreferences::VCC_AUTO},
|
{"auto", StreamingPreferences::VCC_AUTO},
|
||||||
{"H.264", StreamingPreferences::VCC_FORCE_H264},
|
{"H.264", StreamingPreferences::VCC_FORCE_H264},
|
||||||
{"HEVC", StreamingPreferences::VCC_FORCE_HEVC},
|
{"HEVC", StreamingPreferences::VCC_FORCE_HEVC},
|
||||||
|
{"AV1", StreamingPreferences::VCC_FORCE_AV1},
|
||||||
};
|
};
|
||||||
m_VideoDecoderMap = {
|
m_VideoDecoderMap = {
|
||||||
{"auto", StreamingPreferences::VDS_AUTO},
|
{"auto", StreamingPreferences::VDS_AUTO},
|
||||||
{"software", StreamingPreferences::VDS_FORCE_HARDWARE},
|
{"software", StreamingPreferences::VDS_FORCE_SOFTWARE},
|
||||||
{"hardware", StreamingPreferences::VDS_FORCE_SOFTWARE},
|
{"hardware", StreamingPreferences::VDS_FORCE_HARDWARE},
|
||||||
|
};
|
||||||
|
m_CaptureSysKeysModeMap = {
|
||||||
|
{"never", StreamingPreferences::CSK_OFF},
|
||||||
|
{"fullscreen", StreamingPreferences::CSK_FULLSCREEN},
|
||||||
|
{"always", StreamingPreferences::CSK_ALWAYS},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -280,14 +349,26 @@ void StreamCommandLineParser::parse(const QStringList &args, StreamingPreference
|
|||||||
parser.addToggleOption("vsync", "V-Sync");
|
parser.addToggleOption("vsync", "V-Sync");
|
||||||
parser.addValueOption("fps", "FPS");
|
parser.addValueOption("fps", "FPS");
|
||||||
parser.addValueOption("bitrate", "bitrate in Kbps");
|
parser.addValueOption("bitrate", "bitrate in Kbps");
|
||||||
|
parser.addValueOption("packet-size", "video packet size");
|
||||||
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("quit-after", "quit app after session");
|
||||||
parser.addToggleOption("mouse-acceleration", "mouse acceleration");
|
parser.addToggleOption("absolute-mouse", "remote desktop optimized mouse control");
|
||||||
|
parser.addToggleOption("mouse-buttons-swap", "left and right mouse buttons swap");
|
||||||
|
parser.addToggleOption("touchscreen-trackpad", "touchscreen in trackpad mode");
|
||||||
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.addToggleOption("frame-pacing", "frame pacing");
|
||||||
|
parser.addToggleOption("mute-on-focus-loss", "mute audio when Moonlight window loses focus");
|
||||||
|
parser.addToggleOption("background-gamepad", "background gamepad input");
|
||||||
|
parser.addToggleOption("reverse-scroll-direction", "inverted scroll direction");
|
||||||
|
parser.addToggleOption("swap-gamepad-buttons", "swap A/B and X/Y gamepad buttons (Nintendo-style)");
|
||||||
|
parser.addToggleOption("keep-awake", "prevent display sleep while streaming");
|
||||||
|
parser.addToggleOption("performance-overlay", "show performance overlay");
|
||||||
|
parser.addToggleOption("hdr", "HDR streaming");
|
||||||
|
parser.addToggleOption("yuv444", "YUV 4:4:4 sampling, if supported");
|
||||||
|
parser.addChoiceOption("capture-system-keys", "capture system key combos", m_CaptureSysKeysModeMap.keys());
|
||||||
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());
|
||||||
|
|
||||||
@@ -298,27 +379,23 @@ void StreamCommandLineParser::parse(const QStringList &args, StreamingPreference
|
|||||||
parser.handleUnknownOptions();
|
parser.handleUnknownOptions();
|
||||||
|
|
||||||
// Resolve display's width and height
|
// Resolve display's width and height
|
||||||
QRegularExpression resolutionRexExp("^(720|1080|1440|4K|resolution)$");
|
static QRegularExpression resolutionRexExp("^(720|1080|1440|4K|resolution)$");
|
||||||
QStringList resoOptions = parser.optionNames().filter(resolutionRexExp);
|
QStringList resoOptions = parser.optionNames().filter(resolutionRexExp);
|
||||||
bool displaySet = resoOptions.length();
|
bool displaySet = !resoOptions.isEmpty();
|
||||||
if (displaySet) {
|
if (displaySet) {
|
||||||
QString name = resoOptions.last();
|
QString name = resoOptions.last();
|
||||||
if (name == "720") {
|
if (name == "720") {
|
||||||
preferences->width = 1280;
|
preferences->width = 1280;
|
||||||
preferences->height = 720;
|
preferences->height = 720;
|
||||||
displaySet = true;
|
|
||||||
} else if (name == "1080") {
|
} else if (name == "1080") {
|
||||||
preferences->width = 1920;
|
preferences->width = 1920;
|
||||||
preferences->height = 1080;
|
preferences->height = 1080;
|
||||||
displaySet = true;
|
|
||||||
} else if (name == "1440") {
|
} else if (name == "1440") {
|
||||||
preferences->width = 2560;
|
preferences->width = 2560;
|
||||||
preferences->height = 1440;
|
preferences->height = 1440;
|
||||||
displaySet = true;
|
|
||||||
} else if (name == "4K") {
|
} else if (name == "4K") {
|
||||||
preferences->width = 3840;
|
preferences->width = 3840;
|
||||||
preferences->height = 2160;
|
preferences->height = 2160;
|
||||||
displaySet = true;
|
|
||||||
} else if (name == "resolution") {
|
} else if (name == "resolution") {
|
||||||
auto resolution = parser.getResolutionOptionValue(name);
|
auto resolution = parser.getResolutionOptionValue(name);
|
||||||
preferences->width = resolution.first;
|
preferences->width = resolution.first;
|
||||||
@@ -329,20 +406,28 @@ void StreamCommandLineParser::parse(const QStringList &args, StreamingPreference
|
|||||||
// Resolve --fps option
|
// Resolve --fps option
|
||||||
if (parser.isSet("fps")) {
|
if (parser.isSet("fps")) {
|
||||||
preferences->fps = parser.getIntOption("fps");
|
preferences->fps = parser.getIntOption("fps");
|
||||||
if (!inRange(preferences->fps, 30, 120)) {
|
if (!inRange(preferences->fps, 10, 480)) {
|
||||||
parser.showError("FPS must be in range: 30 - 120");
|
fprintf(stderr, "Warning: FPS is out of the supported range (10 - 480 FPS). Performance may suffer!\n");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve --bitrate option
|
// Resolve --bitrate option
|
||||||
if (parser.isSet("bitrate")) {
|
if (parser.isSet("bitrate")) {
|
||||||
preferences->bitrateKbps = parser.getIntOption("bitrate");
|
preferences->bitrateKbps = parser.getIntOption("bitrate");
|
||||||
if (!inRange(preferences->bitrateKbps, 500, 150000)) {
|
if (!inRange(preferences->bitrateKbps, 500, 500000)) {
|
||||||
parser.showError("Bitrate must be in range: 500 - 150000");
|
fprintf(stderr, "Warning: Bitrate is out of the supported range (500 - 500000 Kbps). Performance may suffer!\n");
|
||||||
}
|
}
|
||||||
} else if (displaySet || parser.isSet("fps")) {
|
} else if (displaySet || parser.isSet("fps")) {
|
||||||
preferences->bitrateKbps = preferences->getDefaultBitrate(
|
preferences->bitrateKbps = preferences->getDefaultBitrate(
|
||||||
preferences->width, preferences->height, preferences->fps);
|
preferences->width, preferences->height, preferences->fps, preferences->enableYUV444);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve --packet-size option
|
||||||
|
if (parser.isSet("packet-size")) {
|
||||||
|
preferences->packetSize = parser.getIntOption("packet-size");
|
||||||
|
if (preferences->packetSize < 1024) {
|
||||||
|
parser.showError("Packet size must be greater than 1024 bytes");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve --display option
|
// Resolve --display option
|
||||||
@@ -364,8 +449,14 @@ void StreamCommandLineParser::parse(const QStringList &args, StreamingPreference
|
|||||||
// Resolve --quit-after and --no-quit-after options
|
// Resolve --quit-after and --no-quit-after options
|
||||||
preferences->quitAppAfter = parser.getToggleOptionValue("quit-after", preferences->quitAppAfter);
|
preferences->quitAppAfter = parser.getToggleOptionValue("quit-after", preferences->quitAppAfter);
|
||||||
|
|
||||||
// Resolve --mouse-acceleration and --no-mouse-acceleration options
|
// Resolve --absolute-mouse and --no-absolute-mouse options
|
||||||
preferences->mouseAcceleration = parser.getToggleOptionValue("mouse-acceleration", preferences->mouseAcceleration);
|
preferences->absoluteMouseMode = parser.getToggleOptionValue("absolute-mouse", preferences->absoluteMouseMode);
|
||||||
|
|
||||||
|
// Resolve --mouse-buttons-swap and --no-mouse-buttons-swap options
|
||||||
|
preferences->swapMouseButtons = parser.getToggleOptionValue("mouse-buttons-swap", preferences->swapMouseButtons);
|
||||||
|
|
||||||
|
// Resolve --touchscreen-trackpad and --no-touchscreen-trackpad options
|
||||||
|
preferences->absoluteTouchMode = !parser.getToggleOptionValue("touchscreen-trackpad", !preferences->absoluteTouchMode);
|
||||||
|
|
||||||
// Resolve --game-optimization and --no-game-optimization options
|
// Resolve --game-optimization and --no-game-optimization options
|
||||||
preferences->gameOptimizations = parser.getToggleOptionValue("game-optimization", preferences->gameOptimizations);
|
preferences->gameOptimizations = parser.getToggleOptionValue("game-optimization", preferences->gameOptimizations);
|
||||||
@@ -376,6 +467,35 @@ void StreamCommandLineParser::parse(const QStringList &args, StreamingPreference
|
|||||||
// Resolve --frame-pacing and --no-frame-pacing options
|
// Resolve --frame-pacing and --no-frame-pacing options
|
||||||
preferences->framePacing = parser.getToggleOptionValue("frame-pacing", preferences->framePacing);
|
preferences->framePacing = parser.getToggleOptionValue("frame-pacing", preferences->framePacing);
|
||||||
|
|
||||||
|
// Resolve --mute-on-focus-loss and --no-mute-on-focus-loss options
|
||||||
|
preferences->muteOnFocusLoss = parser.getToggleOptionValue("mute-on-focus-loss", preferences->muteOnFocusLoss);
|
||||||
|
|
||||||
|
// Resolve --background-gamepad and --no-background-gamepad options
|
||||||
|
preferences->backgroundGamepad = parser.getToggleOptionValue("background-gamepad", preferences->backgroundGamepad);
|
||||||
|
|
||||||
|
// Resolve --reverse-scroll-direction and --no-reverse-scroll-direction options
|
||||||
|
preferences->reverseScrollDirection = parser.getToggleOptionValue("reverse-scroll-direction", preferences->reverseScrollDirection);
|
||||||
|
|
||||||
|
// Resolve --swap-gamepad-buttons and --no-swap-gamepad-buttons options
|
||||||
|
preferences->swapFaceButtons = parser.getToggleOptionValue("swap-gamepad-buttons", preferences->swapFaceButtons);
|
||||||
|
|
||||||
|
// Resolve --keep-awake and --no-keep-awake options
|
||||||
|
preferences->keepAwake = parser.getToggleOptionValue("keep-awake", preferences->keepAwake);
|
||||||
|
|
||||||
|
// Resolve --performance-overlay and --no-performance-overlay options
|
||||||
|
preferences->showPerformanceOverlay = parser.getToggleOptionValue("performance-overlay", preferences->showPerformanceOverlay);
|
||||||
|
|
||||||
|
// Resolve --hdr and --no-hdr options
|
||||||
|
preferences->enableHdr = parser.getToggleOptionValue("hdr", preferences->enableHdr);
|
||||||
|
|
||||||
|
// Resolve --yuv444 and --no-yuv444 options
|
||||||
|
preferences->enableYUV444 = parser.getToggleOptionValue("yuv444", preferences->enableYUV444);
|
||||||
|
|
||||||
|
// Resolve --capture-system-keys option
|
||||||
|
if (parser.isSet("capture-system-keys")) {
|
||||||
|
preferences->captureSysKeysMode = mapValue(m_CaptureSysKeysModeMap, parser.getChoiceOptionValue("capture-system-keys"));
|
||||||
|
}
|
||||||
|
|
||||||
// Resolve --video-codec option
|
// 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"));
|
||||||
@@ -412,3 +532,62 @@ QString StreamCommandLineParser::getAppName() const
|
|||||||
{
|
{
|
||||||
return m_AppName;
|
return m_AppName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ListCommandLineParser::ListCommandLineParser()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
ListCommandLineParser::~ListCommandLineParser()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void ListCommandLineParser::parse(const QStringList &args)
|
||||||
|
{
|
||||||
|
CommandLineParser parser;
|
||||||
|
parser.setupCommonOptions();
|
||||||
|
parser.setApplicationDescription(
|
||||||
|
"\n"
|
||||||
|
"List the available apps on the given host."
|
||||||
|
);
|
||||||
|
parser.addPositionalArgument("list", "list available apps");
|
||||||
|
parser.addPositionalArgument("host", "Host computer name, UUID, or IP address", "<host>");
|
||||||
|
|
||||||
|
parser.addFlagOption("csv", "Print as CSV with additional information");
|
||||||
|
parser.addFlagOption("verbose", "Displays additional information");
|
||||||
|
|
||||||
|
if (!parser.parse(args)) {
|
||||||
|
parser.showError(parser.errorText());
|
||||||
|
}
|
||||||
|
|
||||||
|
parser.handleUnknownOptions();
|
||||||
|
|
||||||
|
|
||||||
|
m_PrintCSV = parser.isSet("csv");
|
||||||
|
m_Verbose = parser.isSet("verbose");
|
||||||
|
|
||||||
|
// 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 ListCommandLineParser::getHost() const
|
||||||
|
{
|
||||||
|
return m_Host;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ListCommandLineParser::isPrintCSV() const
|
||||||
|
{
|
||||||
|
return m_PrintCSV;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ListCommandLineParser::isVerbose() const
|
||||||
|
{
|
||||||
|
return m_Verbose;
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ public:
|
|||||||
NormalStartRequested,
|
NormalStartRequested,
|
||||||
StreamRequested,
|
StreamRequested,
|
||||||
QuitRequested,
|
QuitRequested,
|
||||||
|
PairRequested,
|
||||||
|
ListRequested,
|
||||||
};
|
};
|
||||||
|
|
||||||
GlobalCommandLineParser();
|
GlobalCommandLineParser();
|
||||||
@@ -35,6 +37,22 @@ private:
|
|||||||
QString m_Host;
|
QString m_Host;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
class PairCommandLineParser
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
PairCommandLineParser();
|
||||||
|
virtual ~PairCommandLineParser();
|
||||||
|
|
||||||
|
void parse(const QStringList &args);
|
||||||
|
|
||||||
|
QString getHost() const;
|
||||||
|
QString getPredefinedPin() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
QString m_Host;
|
||||||
|
QString m_PredefinedPin;
|
||||||
|
};
|
||||||
|
|
||||||
class StreamCommandLineParser
|
class StreamCommandLineParser
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
@@ -53,4 +71,23 @@ private:
|
|||||||
QMap<QString, StreamingPreferences::AudioConfig> m_AudioConfigMap;
|
QMap<QString, StreamingPreferences::AudioConfig> m_AudioConfigMap;
|
||||||
QMap<QString, StreamingPreferences::VideoCodecConfig> m_VideoCodecMap;
|
QMap<QString, StreamingPreferences::VideoCodecConfig> m_VideoCodecMap;
|
||||||
QMap<QString, StreamingPreferences::VideoDecoderSelection> m_VideoDecoderMap;
|
QMap<QString, StreamingPreferences::VideoDecoderSelection> m_VideoDecoderMap;
|
||||||
|
QMap<QString, StreamingPreferences::CaptureSysKeysMode> m_CaptureSysKeysModeMap;
|
||||||
|
};
|
||||||
|
|
||||||
|
class ListCommandLineParser
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
ListCommandLineParser();
|
||||||
|
virtual ~ListCommandLineParser();
|
||||||
|
|
||||||
|
void parse(const QStringList &args);
|
||||||
|
|
||||||
|
QString getHost() const;
|
||||||
|
bool isPrintCSV() const;
|
||||||
|
bool isVerbose() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
QString m_Host;
|
||||||
|
bool m_PrintCSV;
|
||||||
|
bool m_Verbose;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,196 @@
|
|||||||
|
#include "listapps.h"
|
||||||
|
|
||||||
|
#include "backend/boxartmanager.h"
|
||||||
|
#include "backend/computermanager.h"
|
||||||
|
#include "backend/computerseeker.h"
|
||||||
|
|
||||||
|
#include <QCoreApplication>
|
||||||
|
|
||||||
|
#define COMPUTER_SEEK_TIMEOUT 30000
|
||||||
|
|
||||||
|
namespace CliListApps
|
||||||
|
{
|
||||||
|
|
||||||
|
enum State {
|
||||||
|
StateInit,
|
||||||
|
StateSeekComputer,
|
||||||
|
StateListApps,
|
||||||
|
StateFailure,
|
||||||
|
};
|
||||||
|
|
||||||
|
class Event
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
enum Type {
|
||||||
|
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 CLI main calls execute
|
||||||
|
case Event::Executed:
|
||||||
|
if (m_State == StateInit) {
|
||||||
|
m_State = StateSeekComputer;
|
||||||
|
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::onComputerSeekTimeout);
|
||||||
|
m_ComputerSeeker->start(COMPUTER_SEEK_TIMEOUT);
|
||||||
|
|
||||||
|
m_BoxArtManager = new BoxArtManager(q);
|
||||||
|
|
||||||
|
if (m_Arguments.isVerbose()) {
|
||||||
|
fprintf(stdout, "Establishing connection to PC...\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
// Occurs when computer search timed out
|
||||||
|
case Event::ComputerSeekTimedout:
|
||||||
|
if (m_State == StateSeekComputer) {
|
||||||
|
fprintf(stderr, "%s\n", qPrintable(QString("Failed to connect to %1").arg(m_ComputerName)));
|
||||||
|
|
||||||
|
QCoreApplication::exit(-1);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
// Occurs when searched computer is found
|
||||||
|
case Event::ComputerFound:
|
||||||
|
if (m_State == StateSeekComputer) {
|
||||||
|
if (event.computer->pairState == NvComputer::PS_PAIRED) {
|
||||||
|
m_State = StateListApps;
|
||||||
|
m_Computer = event.computer;
|
||||||
|
|
||||||
|
if (m_Arguments.isVerbose()) {
|
||||||
|
fprintf(stdout, "Loading app list...\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// To avoid race conditions where ComputerSeeker stops async polling, but
|
||||||
|
// ComputerManager is yet to update the app list, we will explicitly fetch the latest app list.
|
||||||
|
// Otherwise, it becomes complicated as we would have to guess whether ComputerManager
|
||||||
|
// would emit 1 signal (the list did not change) or 2 signals (indicating that the list has changed)
|
||||||
|
try {
|
||||||
|
NvHTTP http{m_Computer};
|
||||||
|
|
||||||
|
const auto appList = http.getAppList();
|
||||||
|
m_Arguments.isPrintCSV() ? printAppsCSV(appList) : printApps(appList);
|
||||||
|
|
||||||
|
QCoreApplication::exit(0);
|
||||||
|
} catch (std::exception& exception) {
|
||||||
|
fprintf(stderr, "%s\n", exception.what());
|
||||||
|
QCoreApplication::exit(1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
m_State = StateFailure;
|
||||||
|
fprintf(stderr, "%s\n", qPrintable(QObject::tr("Computer %1 has not been paired. "
|
||||||
|
"Please open Moonlight to pair before retrieving games list.")
|
||||||
|
.arg(event.computer->name)));
|
||||||
|
|
||||||
|
QCoreApplication::exit(-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void printApps(QVector<NvApp> apps) {
|
||||||
|
for (int i = 0; i < apps.length(); i++) {
|
||||||
|
fprintf(stdout, "%s\n", qPrintable(apps[i].name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void printAppsCSV(QVector<NvApp> apps) {
|
||||||
|
fprintf(stdout, "Name, ID, HDR Support, App Collection Game, Hidden, Direct Launch, Boxart URL\n");
|
||||||
|
for (int i = 0; i < apps.length(); i++) {
|
||||||
|
printAppCSV(apps[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void printAppCSV(NvApp app) const
|
||||||
|
{
|
||||||
|
fprintf(stdout, "\"%s\",%d,%s,%s,%s,%s,\"%s\"\n", qPrintable(app.name),
|
||||||
|
app.id,
|
||||||
|
app.hdrSupported ? "true" : "false",
|
||||||
|
app.isAppCollectorGame ? "true" : "false",
|
||||||
|
app.hidden ? "true" : "false",
|
||||||
|
app.directLaunch ? "true" : "false",
|
||||||
|
qPrintable(m_BoxArtManager->loadBoxArt(m_Computer, app).toDisplayString()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Launcher *q_ptr;
|
||||||
|
ComputerManager *m_ComputerManager;
|
||||||
|
QString m_ComputerName;
|
||||||
|
ComputerSeeker *m_ComputerSeeker;
|
||||||
|
BoxArtManager *m_BoxArtManager;
|
||||||
|
NvComputer *m_Computer;
|
||||||
|
State m_State;
|
||||||
|
ListCommandLineParser m_Arguments;
|
||||||
|
};
|
||||||
|
|
||||||
|
Launcher::Launcher(QString computer, ListCommandLineParser arguments, QObject *parent)
|
||||||
|
: QObject(parent),
|
||||||
|
m_DPtr(new LauncherPrivate(this))
|
||||||
|
{
|
||||||
|
Q_D(Launcher);
|
||||||
|
d->m_ComputerName = computer;
|
||||||
|
d->m_State = StateInit;
|
||||||
|
d->m_Arguments = arguments;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "commandlineparser.h"
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QVariant>
|
||||||
|
|
||||||
|
class ComputerManager;
|
||||||
|
class NvComputer;
|
||||||
|
|
||||||
|
namespace CliListApps
|
||||||
|
{
|
||||||
|
|
||||||
|
class LauncherPrivate;
|
||||||
|
|
||||||
|
class Launcher : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
Q_DECLARE_PRIVATE_D(m_DPtr, Launcher)
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit Launcher(QString computer, ListCommandLineParser arguments, QObject *parent = nullptr);
|
||||||
|
~Launcher();
|
||||||
|
|
||||||
|
Q_INVOKABLE void execute(ComputerManager *manager);
|
||||||
|
Q_INVOKABLE bool isExecuted() const;
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void onComputerFound(NvComputer *computer);
|
||||||
|
void onComputerSeekTimeout();
|
||||||
|
|
||||||
|
private:
|
||||||
|
QScopedPointer<LauncherPrivate> m_DPtr;
|
||||||
|
ListCommandLineParser m_Arguments;
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
#include "pair.h"
|
||||||
|
|
||||||
|
#include "backend/computermanager.h"
|
||||||
|
#include "backend/computerseeker.h"
|
||||||
|
#include <QCoreApplication>
|
||||||
|
#include <QTimer>
|
||||||
|
|
||||||
|
#define COMPUTER_SEEK_TIMEOUT 10000
|
||||||
|
|
||||||
|
namespace CliPair
|
||||||
|
{
|
||||||
|
|
||||||
|
enum State {
|
||||||
|
StateInit,
|
||||||
|
StateSeekComputer,
|
||||||
|
StatePairing,
|
||||||
|
StateFailure,
|
||||||
|
StateComplete,
|
||||||
|
};
|
||||||
|
|
||||||
|
class Event
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
enum Type {
|
||||||
|
ComputerFound,
|
||||||
|
Executed,
|
||||||
|
PairingCompleted,
|
||||||
|
Timedout,
|
||||||
|
};
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
switch (event.type) {
|
||||||
|
// Occurs when CliPair 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::pairingCompleted,
|
||||||
|
q, &Launcher::onPairingCompleted);
|
||||||
|
|
||||||
|
// If we weren't provided a predefined PIN, generate one now
|
||||||
|
if (m_PredefinedPin.isEmpty()) {
|
||||||
|
m_PredefinedPin = m_ComputerManager->generatePinString();
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
emit q->searchingComputer();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
// Occurs when searched computer is found
|
||||||
|
case Event::ComputerFound:
|
||||||
|
if (m_State == StateSeekComputer) {
|
||||||
|
if (event.computer->pairState == NvComputer::PS_PAIRED) {
|
||||||
|
m_State = StateFailure;
|
||||||
|
QString msg = QObject::tr("%1 is already paired").arg(event.computer->name);
|
||||||
|
emit q->failed(msg);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Q_ASSERT(!m_PredefinedPin.isEmpty());
|
||||||
|
|
||||||
|
m_State = StatePairing;
|
||||||
|
m_ComputerManager->pairHost(event.computer, m_PredefinedPin);
|
||||||
|
emit q->pairing(event.computer->name, m_PredefinedPin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
// Occurs when pairing operation completes
|
||||||
|
case Event::PairingCompleted:
|
||||||
|
if (m_State == StatePairing) {
|
||||||
|
if (event.errorMessage.isEmpty()) {
|
||||||
|
m_State = StateComplete;
|
||||||
|
emit q->success();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
m_State = StateFailure;
|
||||||
|
emit q->failed(event.errorMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
// Occurs when computer search timed out
|
||||||
|
case Event::Timedout:
|
||||||
|
if (m_State == StateSeekComputer) {
|
||||||
|
m_State = StateFailure;
|
||||||
|
emit q->failed(QObject::tr("Failed to connect to %1").arg(m_ComputerName));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Launcher *q_ptr;
|
||||||
|
QString m_ComputerName;
|
||||||
|
QString m_PredefinedPin;
|
||||||
|
ComputerManager *m_ComputerManager;
|
||||||
|
ComputerSeeker *m_ComputerSeeker;
|
||||||
|
NvComputer *m_Computer;
|
||||||
|
State m_State;
|
||||||
|
QTimer *m_TimeoutTimer;
|
||||||
|
};
|
||||||
|
|
||||||
|
Launcher::Launcher(QString computer, QString predefinedPin, QObject *parent)
|
||||||
|
: QObject(parent),
|
||||||
|
m_DPtr(new LauncherPrivate(this))
|
||||||
|
{
|
||||||
|
Q_D(Launcher);
|
||||||
|
d->m_ComputerName = computer;
|
||||||
|
d->m_PredefinedPin = predefinedPin;
|
||||||
|
d->m_State = StateInit;
|
||||||
|
d->m_TimeoutTimer = new QTimer(this);
|
||||||
|
d->m_TimeoutTimer->setSingleShot(true);
|
||||||
|
connect(d->m_TimeoutTimer, &QTimer::timeout,
|
||||||
|
this, &Launcher::onTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
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::onTimeout()
|
||||||
|
{
|
||||||
|
Q_D(Launcher);
|
||||||
|
Event event(Event::Timedout);
|
||||||
|
d->handleEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Launcher::onPairingCompleted(NvComputer* computer, QString error)
|
||||||
|
{
|
||||||
|
Q_D(Launcher);
|
||||||
|
Event event(Event::PairingCompleted);
|
||||||
|
event.computer = computer;
|
||||||
|
event.errorMessage = error;
|
||||||
|
d->handleEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QVariant>
|
||||||
|
|
||||||
|
class ComputerManager;
|
||||||
|
class NvComputer;
|
||||||
|
class Session;
|
||||||
|
class StreamingPreferences;
|
||||||
|
|
||||||
|
namespace CliPair
|
||||||
|
{
|
||||||
|
|
||||||
|
class Event;
|
||||||
|
class LauncherPrivate;
|
||||||
|
|
||||||
|
class Launcher : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
Q_DECLARE_PRIVATE_D(m_DPtr, Launcher)
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit Launcher(QString computer, QString predefinedPin,
|
||||||
|
QObject *parent = nullptr);
|
||||||
|
~Launcher();
|
||||||
|
Q_INVOKABLE void execute(ComputerManager *manager);
|
||||||
|
Q_INVOKABLE bool isExecuted() const;
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void searchingComputer();
|
||||||
|
void pairing(QString pcName, QString pin);
|
||||||
|
void failed(QString text);
|
||||||
|
void success();
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void onComputerFound(NvComputer *computer);
|
||||||
|
void onPairingCompleted(NvComputer *computer, QString error);
|
||||||
|
void onTimeout();
|
||||||
|
|
||||||
|
private:
|
||||||
|
QScopedPointer<LauncherPrivate> m_DPtr;
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
@@ -73,7 +73,7 @@ public:
|
|||||||
case Event::ComputerSeekTimedout:
|
case Event::ComputerSeekTimedout:
|
||||||
if (m_State == StateSeekComputer) {
|
if (m_State == StateSeekComputer) {
|
||||||
m_State = StateFailure;
|
m_State = StateFailure;
|
||||||
emit q->failed(QString("Failed to connect to %1").arg(m_ComputerName));
|
emit q->failed(QObject::tr("Failed to connect to %1").arg(m_ComputerName));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
// Occurs when searched computer is found
|
// Occurs when searched computer is found
|
||||||
@@ -85,8 +85,8 @@ public:
|
|||||||
m_ComputerManager->quitRunningApp(event.computer);
|
m_ComputerManager->quitRunningApp(event.computer);
|
||||||
} else {
|
} else {
|
||||||
m_State = StateFailure;
|
m_State = StateFailure;
|
||||||
QString msg = QString("Computer %1 has not been paired. "
|
QString msg = QObject::tr("Computer %1 has not been paired. "
|
||||||
"Please open Moonlight to pair before streaming.")
|
"Please open Moonlight to pair before streaming.")
|
||||||
.arg(event.computer->name);
|
.arg(event.computer->name);
|
||||||
emit q->failed(msg);
|
emit q->failed(msg);
|
||||||
}
|
}
|
||||||
@@ -99,7 +99,7 @@ public:
|
|||||||
QCoreApplication::exit(0);
|
QCoreApplication::exit(0);
|
||||||
} else {
|
} else {
|
||||||
m_State = StateFailure;
|
m_State = StateFailure;
|
||||||
emit q->failed(QString("Quitting app failed, reason: %1").arg(event.errorMessage));
|
emit q->failed(QObject::tr("Quitting app failed, reason: %1").arg(event.errorMessage));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
#include <QCoreApplication>
|
#include <QCoreApplication>
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
|
|
||||||
#define COMPUTER_SEEK_TIMEOUT 10000
|
#define COMPUTER_SEEK_TIMEOUT 30000
|
||||||
#define APP_SEEK_TIMEOUT 10000
|
#define APP_SEEK_TIMEOUT 10000
|
||||||
|
|
||||||
namespace CliStartStream
|
namespace CliStartStream
|
||||||
@@ -86,8 +86,8 @@ public:
|
|||||||
emit q->searchingApp();
|
emit q->searchingApp();
|
||||||
} else {
|
} else {
|
||||||
m_State = StateFailure;
|
m_State = StateFailure;
|
||||||
QString msg = QString("Computer %1 has not been paired. "
|
QString msg = QObject::tr("Computer %1 has not been paired. "
|
||||||
"Please open Moonlight to pair before streaming.")
|
"Please open Moonlight to pair before streaming.")
|
||||||
.arg(event.computer->name);
|
.arg(event.computer->name);
|
||||||
emit q->failed(msg);
|
emit q->failed(msg);
|
||||||
}
|
}
|
||||||
@@ -118,23 +118,23 @@ public:
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
// Occurs when the previous app quit has been completed, handles quitting errors if any
|
// Occurs when the previous app quit has been completed, handles quitting errors if any
|
||||||
// happended. ComputerUpdated event's handler handles session start when previous app has
|
// happened. ComputerUpdated event's handler handles session start when previous app has
|
||||||
// quit.
|
// quit.
|
||||||
case Event::AppQuitCompleted:
|
case Event::AppQuitCompleted:
|
||||||
if (m_State == StateSeekApp && !event.errorMessage.isEmpty()) {
|
if (m_State == StateSeekApp && !event.errorMessage.isEmpty()) {
|
||||||
m_State = StateFailure;
|
m_State = StateFailure;
|
||||||
emit q->failed(QString("Quitting app failed, reason: %1").arg(event.errorMessage));
|
emit q->failed(QObject::tr("Quitting app failed, reason: %1").arg(event.errorMessage));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
// Occurs when computer or app search timed out
|
// 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;
|
||||||
emit q->failed(QString("Failed to connect to %1").arg(m_ComputerName));
|
emit q->failed(QObject::tr("Failed to connect to %1").arg(m_ComputerName));
|
||||||
}
|
}
|
||||||
if (m_State == StateSeekApp) {
|
if (m_State == StateSeekApp) {
|
||||||
m_State = StateFailure;
|
m_State = StateFailure;
|
||||||
emit q->failed(QString("Failed to find application %1").arg(m_AppName));
|
emit q->failed(QObject::tr("Failed to find application %1").arg(m_AppName));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -162,7 +162,7 @@ public:
|
|||||||
|
|
||||||
QString getCurrentAppName() const
|
QString getCurrentAppName() const
|
||||||
{
|
{
|
||||||
for (NvApp app : m_Computer->appList) {
|
for (const NvApp& app : std::as_const(m_Computer->appList)) {
|
||||||
if (m_Computer->currentGameId == app.id) {
|
if (m_Computer->currentGameId == app.id) {
|
||||||
return app.name;
|
return app.name;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,38 +1,476 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<component type="desktop">
|
<component type="desktop-application">
|
||||||
<id>com.moonlight_stream.Moonlight</id>
|
<id>com.moonlight_stream.Moonlight</id>
|
||||||
<metadata_license>CC0-1.0</metadata_license>
|
<metadata_license>CC0-1.0</metadata_license>
|
||||||
<project_license>GPL-3.0+</project_license>
|
<project_license>GPL-3.0+</project_license>
|
||||||
<name>Moonlight</name>
|
<name>Moonlight</name>
|
||||||
<summary>Play games remotely from your NVIDIA GameStream-enabled PC</summary>
|
<summary>Stream games and other applications from another PC running Sunshine or GeForce Experience</summary>
|
||||||
|
|
||||||
<description>
|
<description>
|
||||||
<p>Moonlight can stream games and other applications from a PC with an NVIDIA GeForce GTX 600-series or higher GPU and GeForce Experience installed.</p>
|
<p>Moonlight can stream games and other applications from another PC with Sunshine or GeForce Experience software installed.</p>
|
||||||
<p>Features include:</p>
|
<p>Features include:</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>Streaming at up to 4K resolution</li>
|
<li>Hardware accelerated video decoding with VAAPI, VDPAU, and Vulkan Video support</li>
|
||||||
<li>Support for up to 120 FPS streaming (high refresh rate monitor recommended)</li>
|
<li>H.264, HEVC, and AV1 codec support</li>
|
||||||
<li>Hardware accelerated video decoding with VAAPI, VDPAU, and NVDEC support</li>
|
<li>7.1 surround sound audio support</li>
|
||||||
<li>5.1 surround sound audio support</li>
|
<li>10-point multitouch support</li>
|
||||||
<li>HEVC support for better video compression efficiency</li>
|
<li>Gamepad support with force feedback for up to 16 players</li>
|
||||||
<li>Gamepad support with force feedback for up to 4 players</li>
|
<li>Support for both pointer capture (for games) and direct mouse control (for remote desktop)</li>
|
||||||
|
<li>Support for passing system-wide keyboard shortcuts like Alt+Tab to the host</li>
|
||||||
</ul>
|
</ul>
|
||||||
</description>
|
</description>
|
||||||
|
|
||||||
<url type="homepage">https://moonlight-stream.org</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>
|
||||||
|
<url type="vcs-browser">https://github.com/moonlight-stream/moonlight-qt</url>
|
||||||
|
<developer id="org.moonlight-stream">
|
||||||
|
<name>Moonlight Game Streaming Team</name>
|
||||||
|
</developer>
|
||||||
|
<update_contact>cameron@moonlight-stream.org</update_contact>
|
||||||
|
<launchable type="desktop-id">com.moonlight_stream.Moonlight.desktop</launchable>
|
||||||
|
<provides>
|
||||||
|
<binary>moonlight</binary>
|
||||||
|
</provides>
|
||||||
|
<content_rating type="oars-1.1" />
|
||||||
|
|
||||||
|
<branding>
|
||||||
|
<color type="primary" scheme_preference="light">#710085</color>
|
||||||
|
<color type="primary" scheme_preference="dark">#55285d</color>
|
||||||
|
</branding>
|
||||||
|
|
||||||
<screenshots>
|
<screenshots>
|
||||||
<screenshot type="default">
|
<screenshot type="default">
|
||||||
<image>https://moonlight-stream.org/appstream_resources/moonlight_screenshot_1.png</image>
|
<image>https://moonlight-stream.org/appstream_resources/moonlight_screenshot_1.png</image>
|
||||||
|
<caption>Host selection page showing local PCs</caption>
|
||||||
</screenshot>
|
</screenshot>
|
||||||
<screenshot>
|
<screenshot>
|
||||||
<image>https://moonlight-stream.org/appstream_resources/moonlight_screenshot_2.png</image>
|
<image>https://moonlight-stream.org/appstream_resources/moonlight_screenshot_2.png</image>
|
||||||
|
<caption>Settings page displaying streaming options</caption>
|
||||||
</screenshot>
|
</screenshot>
|
||||||
</screenshots>
|
</screenshots>
|
||||||
|
|
||||||
<releases>
|
<releases>
|
||||||
|
<release version="6.1.0" date="2024-09-16">
|
||||||
|
<description>
|
||||||
|
<p>New features:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Experimental YUV 4:4:4 support for improved text clarity during remote desktop usage</li>
|
||||||
|
<li>HDR is now supported with software decoding (requires GPU with Vulkan support)</li>
|
||||||
|
<li>Bitrate limit can now be increased to 500 Mbps</li>
|
||||||
|
<li>Audio decoding now uses 32-bit floating point format</li>
|
||||||
|
</ul>
|
||||||
|
<p>Bugfixes:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Fixed incorrect color range when streaming with AV1 from a host with an AMD GPU</li>
|
||||||
|
<li>Updated community-contributed translations from Weblate</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release version="6.0.1" date="2024-06-30">
|
||||||
|
<description>
|
||||||
|
<p>New features:</p>
|
||||||
|
<ul>
|
||||||
|
<li>HDR to SDR tonemapping is now supported by the Vulkan renderer on Linux</li>
|
||||||
|
</ul>
|
||||||
|
<p>Behavior Changes:</p>
|
||||||
|
<ul>
|
||||||
|
<li>More time is now provided for PCs to wake up before displaying an error when starting a stream from the CLI</li>
|
||||||
|
<li>Moonlight will no longer try to guess VA driver names when running libva 2.20 or later</li>
|
||||||
|
</ul>
|
||||||
|
<p>Bugfixes:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Fixed crash when the Vulkan renderer executed on a system with no Vulkan devices</li>
|
||||||
|
<li>Updated community-contributed translations from Weblate</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release version="6.0.0" date="2024-06-06">
|
||||||
|
<description>
|
||||||
|
<p>New features:</p>
|
||||||
|
<ul>
|
||||||
|
<li>HDR is now supported on Steam Deck and other HDR-capable Linux systems using a new Vulkan-based renderer</li>
|
||||||
|
<li>Vulkan Video decoding of H.264, HEVC, and AV1 is now supported using the new Vulkan renderer</li>
|
||||||
|
<li>Full E2E stream encryption is now supported when streaming from Sunshine v0.22 or later</li>
|
||||||
|
<li>Initiating a stream via the command-line will now automatically wake the target machine if it is sleeping</li>
|
||||||
|
<li>Error codes are now displayed in the connection termination dialog</li>
|
||||||
|
<li>Detailed PC state information is now available via the new "View Details" context menu option (similar to the Android client)</li>
|
||||||
|
<li>The performance overlay can now be configured to be shown by default when starting a stream</li>
|
||||||
|
</ul>
|
||||||
|
<p>Behavior Changes:</p>
|
||||||
|
<ul>
|
||||||
|
<li>The selected bitrate is no longer silently adjusted by Moonlight based upon the video codec or HDR mode</li>
|
||||||
|
<li>The selected mouse mode when starting a stream no longer influences the acceleration curve when the mouse mode is toggled using the Ctrl+Alt+Shift+M key combo while streaming</li>
|
||||||
|
<li>Hardcoded fallback paths for VAAPI and VDPAU drivers are no longer used</li>
|
||||||
|
<li>Per-session keys (rikey and rikeyid values) are now automatically stripped from logs</li>
|
||||||
|
<li>Log messages will now be logged to stderr rather than stdout</li>
|
||||||
|
<li>EGL is now always used instead of GLX for loading OpenGL in X11 environments on non-x86 platforms</li>
|
||||||
|
</ul>
|
||||||
|
<p>Bugfixes:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Improved reliability of streaming during momentary drops of WiFi connections</li>
|
||||||
|
<li>Improved performance with high polling rate mice</li>
|
||||||
|
<li>Improved input latency when a gamepad is connected</li>
|
||||||
|
<li>Added missing command-line options to control HDR and AV1 codec usage</li>
|
||||||
|
<li>Fixed instant connection termination error when streaming over the Internet behind certain Carrier-Grade NAT devices</li>
|
||||||
|
<li>Fixed incorrect MTU detection when streaming over the Internet without a VPN</li>
|
||||||
|
<li>Fixed recovery of audio when no audio output device is present when the stream starts</li>
|
||||||
|
<li>Fixed merging of gamepad state when multi-controller mode is disabled</li>
|
||||||
|
<li>Fixed indefinitely resending mDNS queries when a host could not be resolved</li>
|
||||||
|
<li>Fixed inability to pair when the host is not using a RSA-2048 key</li>
|
||||||
|
<li>Updated included gamepad mappings</li>
|
||||||
|
<li>Updated community-contributed translations from Weblate</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release version="5.0.1" date="2023-11-02">
|
||||||
|
<description>
|
||||||
|
<p>Bugfixes:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Fixed connection issue streaming from some hosts running GeForce Experience or Sunshine v0.20 and earlier</li>
|
||||||
|
<li>Updated community-contributed translations from Weblate</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release version="5.0.0" date="2023-10-18">
|
||||||
|
<description>
|
||||||
|
<p>New features:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Custom FPS values</li>
|
||||||
|
<li>Streaming using the AV1 codec from Sunshine hosts</li>
|
||||||
|
<li>Horizontal scrolling with Sunshine hosts</li>
|
||||||
|
<li>Native multi-touch input with Sunshine hosts</li>
|
||||||
|
<li>Controller type information is passed to Sunshine for controller type autoselection</li>
|
||||||
|
<li>Extended features of PS4/PS5 controllers (touchpad, battery state, gyro/accelerometer motion, and RGB LED) are natively passed to Sunshine hosts</li>
|
||||||
|
<li>Support for up to 16 gamepads with Sunshine hosts</li>
|
||||||
|
<li>Frame pacing support on Wayland</li>
|
||||||
|
<li>Host-side processing latency is now displayed in the performance overlay with Sunshine hosts</li>
|
||||||
|
<li>Window title now includes the PC name when streaming</li>
|
||||||
|
<li>Alphabetical sorting of the PC list</li>
|
||||||
|
<li>Updated Play and Stop button design to improve contrast with app box art</li>
|
||||||
|
<li>The settings page automatically scrolls to the selected element when using keyboard or gamepad navigation</li>
|
||||||
|
<li>Pressing Share+PS buttons on PS4/PS5 controllers can be used to emulate a clickpad press</li>
|
||||||
|
<li>Improved audio quality when streaming with a high bitrate from remote hosts</li>
|
||||||
|
<li>Increased mouse polling rate</li>
|
||||||
|
</ul>
|
||||||
|
<p>Behavior Changes:</p>
|
||||||
|
<ul>
|
||||||
|
<li>IPv6 addresses must be provided in URL-escaped form. Example: [2001:db8::1234:5678] or [2001:db8::1234:5678]:47989</li>
|
||||||
|
<li>Default bitrate logic has been adjusted to behave better with custom resolutions and frame rates</li>
|
||||||
|
</ul>
|
||||||
|
<p>Bugfixes:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Reduced the frequency of audio underruns (clicks and pops) by increasing audio buffer size</li>
|
||||||
|
<li>Fixed "Quitting App" message never dismissing in some cases</li>
|
||||||
|
<li>Fixed parsing IPv6 literals with port numbers</li>
|
||||||
|
<li>Fixed unnecessary WiFi activity causing stream disruptions</li>
|
||||||
|
<li>Fixed paired hosts being forgotten if Moonlight terminates while writing hosts to disk</li>
|
||||||
|
<li>Fixed freeze when closing the stream on Windows with some software that uses DX11 hooks</li>
|
||||||
|
<li>Fixed delayed PC wake when activating Wake-On-LAN feature</li>
|
||||||
|
<li>Fixed Wake-on-LAN when Sunshine is running on alternate ports</li>
|
||||||
|
<li>Fixed leak of /dev/dri fds when trying to initialize VAAPI decoder</li>
|
||||||
|
<li>Fixed support for the Applications/Menu key</li>
|
||||||
|
<li>Fixed automatic language detection for Simplified Chinese and Traditional Chinese</li>
|
||||||
|
<li>Fixed streaming from Sunshine when the client PC has multiple network interfaces or IPv6 privacy extensions enabled</li>
|
||||||
|
<li>Fixed small receive buffer causing video packet loss on some systems</li>
|
||||||
|
<li>Fixed theme overrides causing the UI to be unusable on some systems</li>
|
||||||
|
<li>Fixed tearing with V-Sync enabled on macOS and Linux when using the SDL renderer (software decoding and some hardware decoders)</li>
|
||||||
|
<li>Fixed stateful V4L2 hardware decoding using h264_v4l2m2m and hevc_v4l2m2m decoders</li>
|
||||||
|
<li>Fixed CVE-2023-42801</li>
|
||||||
|
<li>Updated included gamepad mappings</li>
|
||||||
|
<li>Updated community-contributed translations from Weblate</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release version="4.3.1" date="2022-11-03">
|
||||||
|
<description>
|
||||||
|
<p>Changes:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Fixed crash during specific patterns of audio packet loss</li>
|
||||||
|
<li>Fixed detection of local streaming when using IPv6 addresses</li>
|
||||||
|
<li>Windowed mode streaming now starts maximized if the GUI window was maximized</li>
|
||||||
|
<li>Updated community-contributed translations from Weblate</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release version="4.3.0" date="2022-10-28">
|
||||||
|
<description>
|
||||||
|
<p>New features:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Improved support for GeForce Experience 3.26</li>
|
||||||
|
<li>Improved image quality and performance when recovering from video packet loss using HEVC</li>
|
||||||
|
<li>Improved error concealment after audio packet loss</li>
|
||||||
|
<li>Improved UI performance when paired with many hosts or hosts with large numbers of apps</li>
|
||||||
|
<li>Pairing with Sunshine hosts is now supported even if the host is in a streaming session</li>
|
||||||
|
</ul>
|
||||||
|
<p>Bugfixes:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Fixed streaming at resolutions below 720x480 with GeForce Experience 3.26</li>
|
||||||
|
<li>Fixed window position and size being lost when toggling between full-screen and windowed mode</li>
|
||||||
|
<li>Fixed initial window size not always respecting the stream aspect ratio</li>
|
||||||
|
<li>Fixed unexpected error when streaming at resolutions above 4096x4096 using Sunshine</li>
|
||||||
|
<li>Updated included gamepad mappings</li>
|
||||||
|
<li>Updated community-contributed translations from Weblate</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release version="4.2.1" date="2022-09-12">
|
||||||
|
<description>
|
||||||
|
<p>Bugfixes:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Fixed streaming from hosts running Sunshine</li>
|
||||||
|
<li>Updated community-contributed translations from Weblate</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release version="4.2.0" date="2022-09-10">
|
||||||
|
<description>
|
||||||
|
<p>New features:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Added support for pairing via the terminal</li>
|
||||||
|
<li>Added support for listing installed apps on a host via the terminal</li>
|
||||||
|
<li>Improved handling of V-Sync on Wayland systems to reduce latency</li>
|
||||||
|
<li>Improved performance using the legacy non-EGL VAAPI rendering code</li>
|
||||||
|
</ul>
|
||||||
|
<p>Bugfixes:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Fixed native resolution detection on high DPI Wayland systems</li>
|
||||||
|
<li>Fixed audio crackling with SDL 2.24.0 on systems using PulseAudio</li>
|
||||||
|
<li>Updated included gamepad mappings</li>
|
||||||
|
<li>Updated community-contributed translations from Weblate</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release version="4.1.0" date="2022-05-23">
|
||||||
|
<description>
|
||||||
|
<p>New features:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Overlays are now supported on the non-EGL X11 VAAPI renderer (requires RGBA subpicture support in the VAAPI driver)</li>
|
||||||
|
<li>Cursor lock can now be disabled when in full-screen mode</li>
|
||||||
|
</ul>
|
||||||
|
<p>Behavior Changes:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Moonlight can now reduce the client display resolution in full-screen mode if the current display mode doesn't have a high enough refresh rate for the selected stream frame rate</li>
|
||||||
|
<li>When choosing a new client display resolution in full-screen mode, Moonlight will pick a display mode with the closest aspect ratio to the selected stream resolution</li>
|
||||||
|
<li>Moonlight now defaults to borderless windowed mode on Wayland systems</li>
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
<p>Bugfixes:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Fixed missing toolbar buttons in certain languages</li>
|
||||||
|
<li>Fixed some text on the settings page not updating when the language is changed</li>
|
||||||
|
<li>Fixed a frame pacing bug that could cause excessive frame drops</li>
|
||||||
|
<li>Fixed the "keep display awake" setting not being saved when Moonlight restarts</li>
|
||||||
|
<li>Fixed black screen after unmaximizing the Moonlight window on some Linux systems</li>
|
||||||
|
<li>Updated included gamepad mappings</li>
|
||||||
|
<li>Updated community-contributed translations from Weblate</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release version="4.0.0" date="2022-04-02">
|
||||||
|
<description>
|
||||||
|
<p>New features:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Added a new key combo for locking the mouse cursor to the window (Ctrl+Alt+Shift+L)</li>
|
||||||
|
<li>Added an option to avoid keeping the client display awake while streaming</li>
|
||||||
|
</ul>
|
||||||
|
<p>Behavior Changes:</p>
|
||||||
|
<ul>
|
||||||
|
<li>HDR streaming is now a separate option rather than being part of the Video Codec override dropdown</li>
|
||||||
|
<li>HDR streaming is no longer restricted to apps that are certified by NVIDIA as compatible with GameStream HDR. Some newer games may require an HDR display or EDID emulator dongle connected to your host PC for HDR options to be available.</li>
|
||||||
|
</ul>
|
||||||
|
<p>Bugfixes:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Fixed loss of all audio if the active audio device is unplugged while streaming</li>
|
||||||
|
<li>Fixed the GUI being unresponsive after manually quitting when connection to the host is lost</li>
|
||||||
|
<li>Fixed a rare crash if a controller disconnects right as rumble is starting</li>
|
||||||
|
<li>Fixed text rendering on the overlay UI when using SDL_ttf 2.0.18 on Linux</li>
|
||||||
|
<li>Updated included gamepad mappings</li>
|
||||||
|
<li>Updated community-contributed translations from Weblate</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release version="3.2.0" date="2022-01-18">
|
||||||
|
<description>
|
||||||
|
<p>New features:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Reduced CPU usage while streaming</li>
|
||||||
|
<li>Non-ASCII characters are now supported when pasting clipboard data to the host</li>
|
||||||
|
<li>Added support for zero-copy NVDEC rendering for Nvidia Wayland systems</li>
|
||||||
|
<li>Significantly improved performance with asynchronous decoders like V4L2M2M on Linux</li>
|
||||||
|
<li>Added support for color compression planes on newer Intel systems when using VAAPI decoding</li>
|
||||||
|
<li>Added support for copy-back rendering in non-default pixel formats (like the Raspberry Pi SAND format)</li>
|
||||||
|
</ul>
|
||||||
|
<p>Bugfixes:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Fixed unexpected GUI input after using the gamepad quit combo</li>
|
||||||
|
<li>Updated included gamepad mappings</li>
|
||||||
|
<li>Updated community-contributed translations from Weblate</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release version="3.1.4" date="2021-06-25">
|
||||||
|
<description>
|
||||||
|
<p>Changes:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Added network latency statistics to the performance overlay</li>
|
||||||
|
<li>Improved audio quality on unreliable connections (requires GeForce Experience 3.19 or later)</li>
|
||||||
|
<li>Reduced video frame drops on unreliable connections (requires GeForce Experience 3.22 or later)</li>
|
||||||
|
<li>Updated community-contributed translations from Weblate</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release version="3.1.3" date="2021-05-07">
|
||||||
|
<description>
|
||||||
|
<p>Changes:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Fixed an issue that could cause excessive audio latency on some connections</li>
|
||||||
|
<li>Updated community-contributed translations from Weblate</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release version="3.1.2" date="2021-05-03">
|
||||||
|
<description>
|
||||||
|
<p>Changes:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Improved connection reliability and performance with GeForce Experience 3.22</li>
|
||||||
|
<li>Added support for audio encryption with GeForce Experience 3.22</li>
|
||||||
|
<li>Added compatibility update checks for future versions of GeForce Experience</li>
|
||||||
|
<li>Fixed keyboard capture on X11</li>
|
||||||
|
<li>Updated community-contributed translations from Weblate</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release version="3.1.1" date="2021-04-09">
|
||||||
|
<description>
|
||||||
|
<p>Changes:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Added support for streaming from GeForce Experience 3.22</li>
|
||||||
|
<li>Added support for AMF-encoded HEVC streams from servers like Sunshine and OpenStream</li>
|
||||||
|
<li>Added new community-contributed translations from Weblate</li>
|
||||||
|
<li>Fixed KMSDRM rendering with buffer modifiers on embedded devices</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release version="3.1.0" date="2021-03-14">
|
||||||
|
<description>
|
||||||
|
<p>New features:</p>
|
||||||
|
<ul>
|
||||||
|
<li>System keyboard shortcuts (Alt+Tab, Super key, Ctrl+Shift+Esc, etc.) can now be (optionally) captured in windowed mode</li>
|
||||||
|
<li>System keyboard shortcuts can now be captured under Wayland</li>
|
||||||
|
<li>Added support for typing the client's clipboard text on the host by pressing Ctrl+Alt+Shift+V</li>
|
||||||
|
<li>Added Ctrl+Alt+Shift+D key combo to minimize the streaming window</li>
|
||||||
|
<li>Added Ctrl+Alt+Shift+C key combo to show the local cursor while streaming in remote desktop mouse mode</li>
|
||||||
|
<li>Added Select+L1+R1+X gamepad button combo to toggle performance overlay</li>
|
||||||
|
<li>Added translations for Simplified Chinese and German</li>
|
||||||
|
<li>Added a language selector on the settings page</li>
|
||||||
|
</ul>
|
||||||
|
<p>Bugfixes:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Improved performance of loading the app grid</li>
|
||||||
|
<li>Fixed video scaling in High DPI mode on Wayland</li>
|
||||||
|
<li>Fixed window icons on Wayland</li>
|
||||||
|
<li>Fixed number pad enter key functionality in the UI</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release version="3.0.0" date="2021-01-17">
|
||||||
|
<description>
|
||||||
|
<p>New features:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Added the ability to define a custom streaming resolution using the UI</li>
|
||||||
|
<li>Added the option to capture system key combinations (Alt+Tab, Ctrl+Shift+Esc, Win+Tab, etc) in fullscreen mode</li>
|
||||||
|
<li>Updated gamepad mappings are automatically downloaded each time Moonlight launches</li>
|
||||||
|
<li>The performance overlay is now supported for Linux clients using VAAPI and VDPAU hardware acceleration</li>
|
||||||
|
<li>Added the option to enable "Direct Launch" which immediately launches the specified game when clicking on a PC</li>
|
||||||
|
<li>Added the option to mute audio when the Moonlight window loses input focus</li>
|
||||||
|
<li>Added the option to toggle background gamepad input</li>
|
||||||
|
<li>Added the option to invert scroll direction</li>
|
||||||
|
<li>Added the option to swap gamepad button layout to Nintendo-style</li>
|
||||||
|
<li>Added the option to start the Moonlight UI in fullscreen mode</li>
|
||||||
|
<li>The failing TCP or UDP ports are now displayed when the connection fails</li>
|
||||||
|
</ul>
|
||||||
|
<p>Bugfixes:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Fixed PS4 controllers not reading inputs during haptics in some games</li>
|
||||||
|
<li>Fixed some slow decoders causing the receive time value to increase rather than the decode time</li>
|
||||||
|
<li>Native resolution options now refresh each time the settings page is opened, rather than once per launch of Moonlight</li>
|
||||||
|
<li>Fixed no video or audio while streaming over Internet connections that drop packets with QoS tagging enabled</li>
|
||||||
|
<li>Improved connection testing accuracy for connections with low MTUs</li>
|
||||||
|
<li>Fixed inability to open an app's context menu with a gamepad</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release version="2.2.0" date="2020-09-10">
|
||||||
|
<description>
|
||||||
|
<p>New features:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Added the ability to hide unwanted apps and games</li>
|
||||||
|
<li>Added a network test to determine if your Internet connection is blocking Moonlight</li>
|
||||||
|
<li>Added the option to swap mouse buttons</li>
|
||||||
|
<li>Added codec and resolution to the on-screen performance stats</li>
|
||||||
|
<li>Added a warning if the connection is too unstable to receive any complete video frames</li>
|
||||||
|
<li>Added a Discord link to the toolbar in settings</li>
|
||||||
|
<li>Enabled the new EGL renderer by default on Linux</li>
|
||||||
|
</ul>
|
||||||
|
<p>Bugfixes:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Fixed streaming over networks that block QoS-tagged packets</li>
|
||||||
|
<li>Selecting a frame rate higher than 60 FPS no longer sets the host desktop to 720p</li>
|
||||||
|
<li>Right-side modifier keys no longer press the corresponding left-side keys too</li>
|
||||||
|
<li>Fixed mouse position never reaching the edge of the screen with certain desktop resolutions</li>
|
||||||
|
<li>Improved synchronization between host and client mouse cursor in remote desktop mouse mode</li>
|
||||||
|
<li>Cached box art is now deleted when the PC is removed from Moonlight</li>
|
||||||
|
<li>Fixed several CLI options not properly overriding GUI options</li>
|
||||||
|
<li>Updated included gamepad mappings</li>
|
||||||
|
<li>Updated to Qt 5.15.1</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release version="2.1.0" date="2020-05-17">
|
||||||
|
<description>
|
||||||
|
<p>New features:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Remote desktop mode now allows interaction with the host without having to click to focus the Moonlight window first</li>
|
||||||
|
<li>Added a new EGL renderer for improved performance on Wayland and XWayland</li>
|
||||||
|
<li>Added the option to toggle between direct touchscreen mode and the old trackpad mode</li>
|
||||||
|
<li>Added the option to rename host PCs</li>
|
||||||
|
<li>Added the ability to scroll the settings page with the right analog stick</li>
|
||||||
|
<li>Various UI improvements</li>
|
||||||
|
</ul>
|
||||||
|
<p>Bugfixes:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Fixed unreliable mouse capture when the stream begins</li>
|
||||||
|
<li>Fixed cursor being trapped in full-screen mode even when the mouse wasn't captured</li>
|
||||||
|
<li>Fixed key presses not being sent to the host if the Super/Meta/Windows key is pressed</li>
|
||||||
|
<li>Updated included gamepad mappings</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release version="2.0.0" date="2020-04-28">
|
||||||
|
<description>
|
||||||
|
<p>New features:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Remote desktop-optimized mouse mode which doesn't require capturing the mouse cursor</li>
|
||||||
|
<li>Touchscreen input now directly controls the mouse cursor</li>
|
||||||
|
<li>7.1 surround sound support</li>
|
||||||
|
</ul>
|
||||||
|
<p>Bugfixes:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Improved performance on some lossy networks</li>
|
||||||
|
<li>Fixed disconnect delay when streaming from GeForce Experience 3.20.3.63</li>
|
||||||
|
<li>Fixed frame rate not exceeding 60 when streaming from GeForce Experience 3.20.3.63</li>
|
||||||
|
<li>Fixed parsing of command-line options when passing flags prior to the action</li>
|
||||||
|
<li>Fixed an issue that may cause excessive mDNS network traffic while Moonlight is running</li>
|
||||||
|
<li>Fixed hardware decoding on Intel GPUs running Mesa 20.0 and/or the Iris driver</li>
|
||||||
|
<li>Fixed hardware decoding on Comet Lake and later Intel GPUs using the iHD VA-API driver</li>
|
||||||
|
<li>Fixed hardware decoding on AMD Radeon GPUs with XWayland</li>
|
||||||
|
<li>Fixed a crash when using the iHD VA-API driver on Wayland</li>
|
||||||
|
<li>Fixed delay after exiting the stream on Wayland</li>
|
||||||
|
<li>Updated included gamepad mappings</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
<release version="1.2.0" date="2019-12-17">
|
<release version="1.2.0" date="2019-12-17">
|
||||||
<description>
|
<description>
|
||||||
<p>Bugfixes:</p>
|
<p>Bugfixes:</p>
|
||||||
@@ -395,19 +833,4 @@
|
|||||||
</release>
|
</release>
|
||||||
</releases>
|
</releases>
|
||||||
|
|
||||||
<categories>
|
|
||||||
<category>Game</category>
|
|
||||||
<category>Qt</category>
|
|
||||||
</categories>
|
|
||||||
|
|
||||||
<developer_name>Moonlight Game Streaming Team</developer_name>
|
|
||||||
<update_contact>cameron@moonlight-stream.org</update_contact>
|
|
||||||
|
|
||||||
<launchable type="desktop-id">com.moonlight_stream.Moonlight.desktop</launchable>
|
|
||||||
|
|
||||||
<provides>
|
|
||||||
<binary>moonlight</binary>
|
|
||||||
</provides>
|
|
||||||
|
|
||||||
<content_rating type="oars-1.0" />
|
|
||||||
</component>
|
</component>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
[Desktop Entry]
|
[Desktop Entry]
|
||||||
Name=Moonlight
|
Name=Moonlight
|
||||||
Comment=Stream games from your NVIDIA GameStream-enabled PC
|
Comment=Stream games and other applications from another PC running Sunshine or GeForce Experience
|
||||||
Exec=moonlight
|
Exec=moonlight
|
||||||
Icon=moonlight
|
Icon=moonlight
|
||||||
Terminal=false
|
Terminal=false
|
||||||
Type=Application
|
Type=Application
|
||||||
Categories=Qt;Game;
|
Categories=Qt;Game;Network;RemoteAccess;
|
||||||
Keywords=nvidia;gamestream;stream;
|
Keywords=nvidia;gamestream;stream;sunshine;remote play;
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 13 KiB |
@@ -5,9 +5,13 @@
|
|||||||
# pairing data will be lost each reboot.
|
# pairing data will be lost each reboot.
|
||||||
HOME=/usr/local/moonlight
|
HOME=/usr/local/moonlight
|
||||||
|
|
||||||
|
# Enable Steam Controller detection in SDL
|
||||||
|
# https://github.com/moonlight-stream/moonlight-qt/issues/697
|
||||||
|
export SDL_ENABLE_STEAM_CONTROLLERS=1
|
||||||
|
|
||||||
# Renice PE_Single_CPU which seems to host A/V stuff
|
# Renice PE_Single_CPU which seems to host A/V stuff
|
||||||
renice -10 -p $(pidof PE_Single_CPU)
|
renice -10 -p $(pidof PE_Single_CPU)
|
||||||
|
|
||||||
# Renice Moonlight itself to avoid preemption by background tasks
|
# Renice Moonlight itself to avoid preemption by background tasks
|
||||||
# Write output to a logfile in /tmp
|
# Write output to a logfile in /tmp
|
||||||
exec nice -n -10 ./bin/moonlight > /tmp/moonlight.log
|
exec nice -n -10 ./bin/moonlight > /tmp/moonlight.log 2>&1
|
||||||
|
|||||||
+206
-108
@@ -1,5 +1,6 @@
|
|||||||
import QtQuick 2.9
|
import QtQuick 2.9
|
||||||
import QtQuick.Controls 2.2
|
import QtQuick.Controls 2.2
|
||||||
|
import QtQuick.Controls.Material 2.2
|
||||||
|
|
||||||
import AppModel 1.0
|
import AppModel 1.0
|
||||||
import ComputerManager 1.0
|
import ComputerManager 1.0
|
||||||
@@ -9,6 +10,8 @@ CenteredGridView {
|
|||||||
property int computerIndex
|
property int computerIndex
|
||||||
property AppModel appModel : createModel()
|
property AppModel appModel : createModel()
|
||||||
property bool activated
|
property bool activated
|
||||||
|
property bool showHiddenGames
|
||||||
|
property bool showGames
|
||||||
|
|
||||||
id: appGrid
|
id: appGrid
|
||||||
focus: true
|
focus: true
|
||||||
@@ -35,9 +38,22 @@ CenteredGridView {
|
|||||||
activated = true
|
activated = true
|
||||||
|
|
||||||
// Highlight the first item if a gamepad is connected
|
// Highlight the first item if a gamepad is connected
|
||||||
if (currentIndex == -1 && SdlGamepadKeyNavigation.getConnectedGamepads() > 0) {
|
if (currentIndex === -1 && SdlGamepadKeyNavigation.getConnectedGamepads() > 0) {
|
||||||
currentIndex = 0
|
currentIndex = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!showGames && !showHiddenGames) {
|
||||||
|
// Check if there's a direct launch app
|
||||||
|
var directLaunchAppIndex = model.getDirectLaunchAppIndex();
|
||||||
|
if (directLaunchAppIndex >= 0) {
|
||||||
|
// Start the direct launch app if nothing else is running
|
||||||
|
currentIndex = directLaunchAppIndex
|
||||||
|
currentItem.launchOrResumeSelectedApp(false)
|
||||||
|
|
||||||
|
// Set showGames so we will not loop when the stream ends
|
||||||
|
showGames = true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
StackView.onDeactivating: {
|
StackView.onDeactivating: {
|
||||||
@@ -48,7 +64,7 @@ CenteredGridView {
|
|||||||
function createModel()
|
function createModel()
|
||||||
{
|
{
|
||||||
var model = Qt.createQmlObject('import AppModel 1.0; AppModel {}', parent, '')
|
var model = Qt.createQmlObject('import AppModel 1.0; AppModel {}', parent, '')
|
||||||
model.initialize(ComputerManager, computerIndex)
|
model.initialize(ComputerManager, computerIndex, showHiddenGames)
|
||||||
return model
|
return model
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,6 +74,12 @@ CenteredGridView {
|
|||||||
width: 220; height: 287;
|
width: 220; height: 287;
|
||||||
grid: appGrid
|
grid: appGrid
|
||||||
|
|
||||||
|
property alias appContextMenu: appContextMenuLoader.item
|
||||||
|
property alias appNameText: appNameTextLoader.item
|
||||||
|
|
||||||
|
// Dim the app if it's hidden
|
||||||
|
opacity: model.hidden ? 0.4 : 1.0
|
||||||
|
|
||||||
Image {
|
Image {
|
||||||
property bool isPlaceholder: false
|
property bool isPlaceholder: false
|
||||||
|
|
||||||
@@ -67,9 +89,14 @@ CenteredGridView {
|
|||||||
source: model.boxart
|
source: model.boxart
|
||||||
|
|
||||||
onSourceSizeChanged: {
|
onSourceSizeChanged: {
|
||||||
if ((sourceSize.width == 130 && sourceSize.height == 180) || // GFE 2.0 placeholder image
|
// Nearly all of Nvidia's official box art does not match the dimensions of placeholder
|
||||||
(sourceSize.width == 628 && sourceSize.height == 888) || // GFE 3.0 placeholder image
|
// images, however the one known exception is Overcooked. Therefore, we only execute
|
||||||
(sourceSize.width == 200 && sourceSize.height == 266)) // Our no_app_image.png
|
// the image size checks if this is not an app collector game. We know the officially
|
||||||
|
// supported games all have box art, so this check is not required.
|
||||||
|
if (!model.isAppCollectorGame &&
|
||||||
|
((sourceSize.width === 130 && sourceSize.height === 180) || // GFE 2.0 placeholder image
|
||||||
|
(sourceSize.width === 628 && sourceSize.height === 888) || // GFE 3.0 placeholder image
|
||||||
|
(sourceSize.width === 200 && sourceSize.height === 266))) // Our no_app_image.png
|
||||||
{
|
{
|
||||||
isPlaceholder = true
|
isPlaceholder = true
|
||||||
}
|
}
|
||||||
@@ -86,97 +113,117 @@ CenteredGridView {
|
|||||||
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) && (!appNameText.visible || appNameText.truncated)
|
ToolTip.visible: (parent.hovered || parent.highlighted) && (!appNameText || appNameText.truncated)
|
||||||
}
|
}
|
||||||
|
|
||||||
ToolButton {
|
Loader {
|
||||||
id: resumeButton
|
active: model.running
|
||||||
anchors.horizontalCenterOffset: appIcon.isPlaceholder ? -47 : 0
|
asynchronous: true
|
||||||
anchors.verticalCenterOffset: appIcon.isPlaceholder ? -75 : -60
|
anchors.fill: appIcon
|
||||||
anchors.centerIn: appIcon
|
|
||||||
visible: model.running
|
|
||||||
implicitWidth: 125
|
|
||||||
implicitHeight: 125
|
|
||||||
|
|
||||||
Image {
|
sourceComponent: Item {
|
||||||
source: "qrc:/res/baseline-play_circle_filled_white-48px.svg"
|
RoundButton {
|
||||||
anchors.centerIn: parent
|
// Don't steal focus from the toolbar buttons
|
||||||
sourceSize {
|
focusPolicy: Qt.NoFocus
|
||||||
width: 75
|
|
||||||
height: 75
|
anchors.horizontalCenterOffset: appIcon.isPlaceholder ? -47 : 0
|
||||||
|
anchors.verticalCenterOffset: appIcon.isPlaceholder ? -75 : -60
|
||||||
|
anchors.centerIn: parent
|
||||||
|
implicitWidth: 85
|
||||||
|
implicitHeight: 85
|
||||||
|
|
||||||
|
icon.source: "qrc:/res/play_arrow_FILL1_wght700_GRAD200_opsz48.svg"
|
||||||
|
icon.width: 75
|
||||||
|
icon.height: 75
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
launchOrResumeSelectedApp(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
ToolTip.text: qsTr("Resume Game")
|
||||||
|
ToolTip.delay: 1000
|
||||||
|
ToolTip.timeout: 3000
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
|
||||||
|
Material.background: "#D0808080"
|
||||||
|
}
|
||||||
|
|
||||||
|
RoundButton {
|
||||||
|
// Don't steal focus from the toolbar buttons
|
||||||
|
focusPolicy: Qt.NoFocus
|
||||||
|
|
||||||
|
anchors.horizontalCenterOffset: appIcon.isPlaceholder ? 47 : 0
|
||||||
|
anchors.verticalCenterOffset: appIcon.isPlaceholder ? -75 : 60
|
||||||
|
anchors.centerIn: parent
|
||||||
|
implicitWidth: 85
|
||||||
|
implicitHeight: 85
|
||||||
|
|
||||||
|
icon.source: "qrc:/res/stop_FILL1_wght700_GRAD200_opsz48.svg"
|
||||||
|
icon.width: 75
|
||||||
|
icon.height: 75
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
doQuitGame()
|
||||||
|
}
|
||||||
|
|
||||||
|
ToolTip.text: qsTr("Quit Game")
|
||||||
|
ToolTip.delay: 1000
|
||||||
|
ToolTip.timeout: 3000
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
|
||||||
|
Material.background: "#D0808080"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onClicked: {
|
|
||||||
launchOrResumeSelectedApp()
|
|
||||||
}
|
|
||||||
|
|
||||||
ToolTip.text: "Resume Game"
|
|
||||||
ToolTip.delay: 1000
|
|
||||||
ToolTip.timeout: 3000
|
|
||||||
ToolTip.visible: hovered
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ToolButton {
|
Loader {
|
||||||
id: quitButton
|
id: appNameTextLoader
|
||||||
anchors.horizontalCenterOffset: appIcon.isPlaceholder ? 47 : 0
|
active: appIcon.isPlaceholder
|
||||||
anchors.verticalCenterOffset: appIcon.isPlaceholder ? -75 : 60
|
|
||||||
anchors.centerIn: appIcon
|
|
||||||
visible: model.running
|
|
||||||
implicitWidth: 125
|
|
||||||
implicitHeight: 125
|
|
||||||
|
|
||||||
Image {
|
// This loader is not asynchronous to avoid noticeable differences
|
||||||
source: "qrc:/res/baseline-cancel-24px.svg"
|
// in the time in which the text loads for each game.
|
||||||
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
|
width: appIcon.width
|
||||||
height: model.running ? 175 : appIcon.height
|
height: model.running ? 175 : appIcon.height
|
||||||
leftPadding: 20
|
|
||||||
rightPadding: 20
|
|
||||||
anchors.left: appIcon.left
|
anchors.left: appIcon.left
|
||||||
anchors.right: appIcon.right
|
anchors.right: appIcon.right
|
||||||
anchors.bottom: appIcon.bottom
|
anchors.bottom: appIcon.bottom
|
||||||
font.pointSize: 22
|
|
||||||
verticalAlignment: Text.AlignVCenter
|
sourceComponent: Label {
|
||||||
horizontalAlignment: Text.AlignHCenter
|
id: appNameText
|
||||||
wrapMode: Text.Wrap
|
text: model.name
|
||||||
elide: Text.ElideRight
|
font.pointSize: 22
|
||||||
|
leftPadding: 20
|
||||||
|
rightPadding: 20
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function launchOrResumeSelectedApp()
|
function launchOrResumeSelectedApp(quitExistingApp)
|
||||||
{
|
{
|
||||||
var runningIndex = appModel.getRunningAppIndex()
|
var runningId = appModel.getRunningAppId()
|
||||||
if (runningIndex >= 0 && runningIndex !== index) {
|
if (runningId !== 0 && runningId !== model.appid) {
|
||||||
quitAppDialog.appName = appModel.getRunningAppName()
|
if (quitExistingApp) {
|
||||||
quitAppDialog.segueToStream = true
|
quitAppDialog.appName = appModel.getRunningAppName()
|
||||||
quitAppDialog.nextAppName = model.name
|
quitAppDialog.segueToStream = true
|
||||||
quitAppDialog.nextAppIndex = index
|
quitAppDialog.nextAppName = model.name
|
||||||
quitAppDialog.open()
|
quitAppDialog.nextAppIndex = index
|
||||||
|
quitAppDialog.open()
|
||||||
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var component = Qt.createComponent("StreamSegue.qml")
|
var component = Qt.createComponent("StreamSegue.qml")
|
||||||
var segue = component.createObject(stackView, {"appName": model.name, "session": appModel.createSessionForApp(index)})
|
var segue = component.createObject(stackView, {
|
||||||
|
"appName": model.name,
|
||||||
|
"session": appModel.createSessionForApp(index),
|
||||||
|
"isResume": runningId === model.appid
|
||||||
|
})
|
||||||
stackView.push(segue)
|
stackView.push(segue)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,7 +233,26 @@ CenteredGridView {
|
|||||||
// will handle starting the game and clicks on the box art will
|
// will handle starting the game and clicks on the box art will
|
||||||
// be ignored.
|
// be ignored.
|
||||||
if (!model.running) {
|
if (!model.running) {
|
||||||
launchOrResumeSelectedApp()
|
launchOrResumeSelectedApp(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onPressAndHold: {
|
||||||
|
// popup() ensures the menu appears under the mouse cursor
|
||||||
|
if (appContextMenu.popup) {
|
||||||
|
appContextMenu.popup()
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Qt 5.9 doesn't have popup()
|
||||||
|
appContextMenu.open()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
acceptedButtons: Qt.RightButton;
|
||||||
|
onClicked: {
|
||||||
|
parent.pressAndHold()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,7 +267,10 @@ CenteredGridView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Keys.onMenuPressed: {
|
Keys.onEnterPressed: {
|
||||||
|
// Open the app context menu if activated via the gamepad or keyboard
|
||||||
|
// for running games. If the game isn't running, the above onClicked
|
||||||
|
// method will handle the launch.
|
||||||
if (model.running) {
|
if (model.running) {
|
||||||
// This will be keyboard/gamepad driven so use
|
// This will be keyboard/gamepad driven so use
|
||||||
// open() instead of popup()
|
// open() instead of popup()
|
||||||
@@ -209,40 +278,85 @@ CenteredGridView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Keys.onMenuPressed: {
|
||||||
|
// This will be keyboard/gamepad driven so use open() instead of popup()
|
||||||
|
appContextMenu.open()
|
||||||
|
}
|
||||||
|
|
||||||
function doQuitGame() {
|
function doQuitGame() {
|
||||||
quitAppDialog.appName = appModel.getRunningAppName()
|
quitAppDialog.appName = appModel.getRunningAppName()
|
||||||
quitAppDialog.segueToStream = false
|
quitAppDialog.segueToStream = false
|
||||||
quitAppDialog.open()
|
quitAppDialog.open()
|
||||||
}
|
}
|
||||||
|
|
||||||
NavigableMenu {
|
Loader {
|
||||||
id: appContextMenu
|
id: appContextMenuLoader
|
||||||
NavigableMenuItem {
|
asynchronous: true
|
||||||
parentMenu: appContextMenu
|
sourceComponent: NavigableMenu {
|
||||||
text: model.running ? "Resume Game" : "Launch Game"
|
id: appContextMenu
|
||||||
onTriggered: launchOrResumeSelectedApp()
|
initiator: appContextMenuLoader.parent
|
||||||
}
|
NavigableMenuItem {
|
||||||
NavigableMenuItem {
|
text: model.running ? qsTr("Resume Game") : qsTr("Launch Game")
|
||||||
parentMenu: appContextMenu
|
onTriggered: launchOrResumeSelectedApp(true)
|
||||||
text: "Quit Game"
|
}
|
||||||
onTriggered: doQuitGame()
|
NavigableMenuItem {
|
||||||
visible: model.running
|
text: qsTr("Quit Game")
|
||||||
|
onTriggered: doQuitGame()
|
||||||
|
visible: model.running
|
||||||
|
}
|
||||||
|
NavigableMenuItem {
|
||||||
|
checkable: true
|
||||||
|
checked: model.directLaunch
|
||||||
|
text: qsTr("Direct Launch")
|
||||||
|
onTriggered: appModel.setAppDirectLaunch(model.index, !model.directLaunch)
|
||||||
|
enabled: !model.hidden
|
||||||
|
|
||||||
|
ToolTip.text: qsTr("Launch this app immediately when the host is selected, bypassing the app selection grid.")
|
||||||
|
ToolTip.delay: 1000
|
||||||
|
ToolTip.timeout: 3000
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
}
|
||||||
|
NavigableMenuItem {
|
||||||
|
checkable: true
|
||||||
|
checked: model.hidden
|
||||||
|
text: qsTr("Hide Game")
|
||||||
|
onTriggered: appModel.setAppHidden(model.index, !model.hidden)
|
||||||
|
enabled: model.hidden || (!model.running && !model.directLaunch)
|
||||||
|
|
||||||
|
ToolTip.text: qsTr("Hide this game from the app grid. To access hidden games, right-click on the host and choose %1.").arg(qsTr("View All Apps"))
|
||||||
|
ToolTip.delay: 1000
|
||||||
|
ToolTip.timeout: 5000
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: 5
|
||||||
|
visible: appGrid.count === 0
|
||||||
|
|
||||||
|
Label {
|
||||||
|
text: qsTr("This computer doesn't seem to have any applications or some applications are hidden")
|
||||||
|
font.pointSize: 20
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
NavigableMessageDialog {
|
NavigableMessageDialog {
|
||||||
id: quitAppDialog
|
id: quitAppDialog
|
||||||
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:qsTr("Are you sure you want to quit %1? Any unsaved progress will be lost.").arg(appName)
|
||||||
standardButtons: Dialog.Yes | Dialog.No
|
standardButtons: Dialog.Yes | Dialog.No
|
||||||
|
|
||||||
function quitApp() {
|
function quitApp() {
|
||||||
var component = Qt.createComponent("QuitSegue.qml")
|
var component = Qt.createComponent("QuitSegue.qml")
|
||||||
var params = {"appName": appName}
|
var params = {"appName": appName, "quitRunningAppFn": function() { appModel.quitRunningApp() }}
|
||||||
if (segueToStream) {
|
if (segueToStream) {
|
||||||
// Store the session and app name if we're going to stream after
|
// Store the session and app name if we're going to stream after
|
||||||
// successfully quitting the old app.
|
// successfully quitting the old app.
|
||||||
@@ -255,26 +369,10 @@ CenteredGridView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
stackView.push(component.createObject(stackView, params))
|
stackView.push(component.createObject(stackView, params))
|
||||||
|
|
||||||
// Trigger the quit after pushing the quit segue on screen
|
|
||||||
appModel.quitRunningApp()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onAccepted: quitApp()
|
onAccepted: quitApp()
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
|
||||||
anchors {
|
|
||||||
top: parent.top
|
|
||||||
right: parent.right
|
|
||||||
bottom: parent.bottom
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,15 @@ import QtQuick 2.9
|
|||||||
import QtQuick.Controls 2.2
|
import QtQuick.Controls 2.2
|
||||||
|
|
||||||
import SdlGamepadKeyNavigation 1.0
|
import SdlGamepadKeyNavigation 1.0
|
||||||
|
import SystemProperties 1.0
|
||||||
|
|
||||||
// https://stackoverflow.com/questions/45029968/how-do-i-set-the-combobox-width-to-fit-the-largest-item
|
// https://stackoverflow.com/questions/45029968/how-do-i-set-the-combobox-width-to-fit-the-largest-item
|
||||||
ComboBox {
|
ComboBox {
|
||||||
property int textWidth
|
property int textWidth
|
||||||
|
property int desiredWidth : leftPadding + textWidth + indicator.width + rightPadding
|
||||||
|
property int maximumWidth : parent.width
|
||||||
|
|
||||||
implicitWidth: leftPadding + textWidth + indicator.width + rightPadding
|
implicitWidth: desiredWidth < maximumWidth ? desiredWidth : maximumWidth
|
||||||
|
|
||||||
TextMetrics {
|
TextMetrics {
|
||||||
id: popupMetrics
|
id: popupMetrics
|
||||||
@@ -17,9 +20,7 @@ ComboBox {
|
|||||||
id: textMetrics
|
id: textMetrics
|
||||||
}
|
}
|
||||||
|
|
||||||
// We call this every time the options change (and init)
|
function recalculateWidth() {
|
||||||
// so we can adjust the combo box width here too
|
|
||||||
onActivated: {
|
|
||||||
textMetrics.font = font
|
textMetrics.font = font
|
||||||
popupMetrics.font = popup.font
|
popupMetrics.font = popup.font
|
||||||
textWidth = 0
|
textWidth = 0
|
||||||
@@ -31,9 +32,19 @@ ComboBox {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We call this every time the options change (and init)
|
||||||
|
// so we can adjust the combo box width here too
|
||||||
|
onActivated: recalculateWidth()
|
||||||
|
|
||||||
popup.onAboutToShow: {
|
popup.onAboutToShow: {
|
||||||
// Switch to normal navigation for combo boxes
|
// Switch to normal navigation for combo boxes
|
||||||
SdlGamepadKeyNavigation.setUiNavMode(false)
|
SdlGamepadKeyNavigation.setUiNavMode(false)
|
||||||
|
|
||||||
|
// Override the popup color to improve contrast with the overridden
|
||||||
|
// Material 2 background color set in main.qml.
|
||||||
|
if (SystemProperties.usesMaterial3Theme) {
|
||||||
|
popup.background.color = "#424242"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
popup.onAboutToHide: {
|
popup.onAboutToHide: {
|
||||||
|
|||||||
@@ -2,10 +2,6 @@ import QtQuick 2.9
|
|||||||
import QtQuick.Controls 2.2
|
import QtQuick.Controls 2.2
|
||||||
|
|
||||||
GridView {
|
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 int minMargin: 10
|
||||||
property real availableWidth: (parent.width - 2 * minMargin)
|
property real availableWidth: (parent.width - 2 * minMargin)
|
||||||
property int itemsPerRow: availableWidth / cellWidth
|
property int itemsPerRow: availableWidth / cellWidth
|
||||||
@@ -15,11 +11,6 @@ GridView {
|
|||||||
function updateMargins() {
|
function updateMargins() {
|
||||||
leftMargin = horizontalMargin
|
leftMargin = horizontalMargin
|
||||||
rightMargin = horizontalMargin
|
rightMargin = horizontalMargin
|
||||||
|
|
||||||
if (hasBrokenMargins) {
|
|
||||||
anchors.leftMargin = leftMargin
|
|
||||||
anchors.rightMargin = rightMargin
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onHorizontalMarginChanged: {
|
onHorizontalMarginChanged: {
|
||||||
@@ -27,15 +18,8 @@ GridView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Component.onCompleted: {
|
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()
|
updateMargins()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
boundsBehavior: Flickable.OvershootBounds
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import QtQuick 2.0
|
||||||
|
import QtQuick.Controls 2.2
|
||||||
|
|
||||||
|
import ComputerManager 1.0
|
||||||
|
|
||||||
|
Item {
|
||||||
|
function onSearchingComputer() {
|
||||||
|
stageLabel.text = qsTr("Establishing connection to PC...")
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPairing(pcName, pin) {
|
||||||
|
stageLabel.text = qsTr("Pairing... Please enter '%1' on %2.").arg(pin).arg(pcName)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onFailed(message) {
|
||||||
|
stageIndicator.visible = false
|
||||||
|
errorDialog.text = message
|
||||||
|
errorDialog.open()
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSuccess(appName) {
|
||||||
|
stageIndicator.visible = false
|
||||||
|
pairCompleteDialog.open()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow user to back out of pairing
|
||||||
|
Keys.onEscapePressed: {
|
||||||
|
Qt.quit()
|
||||||
|
}
|
||||||
|
Keys.onBackPressed: {
|
||||||
|
Qt.quit()
|
||||||
|
}
|
||||||
|
Keys.onCancelPressed: {
|
||||||
|
Qt.quit()
|
||||||
|
}
|
||||||
|
|
||||||
|
StackView.onActivated: {
|
||||||
|
if (!launcher.isExecuted()) {
|
||||||
|
toolBar.visible = false
|
||||||
|
|
||||||
|
launcher.searchingComputer.connect(onSearchingComputer)
|
||||||
|
launcher.pairing.connect(onPairing)
|
||||||
|
launcher.failed.connect(onFailed)
|
||||||
|
launcher.success.connect(onSuccess)
|
||||||
|
launcher.execute(ComputerManager)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: 5
|
||||||
|
id: stageIndicator
|
||||||
|
|
||||||
|
BusyIndicator {
|
||||||
|
id: stageSpinner
|
||||||
|
running: visible
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
id: stageLabel
|
||||||
|
height: stageSpinner.height
|
||||||
|
font.pointSize: 20
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ErrorMessageDialog {
|
||||||
|
id: errorDialog
|
||||||
|
|
||||||
|
onClosed: {
|
||||||
|
Qt.quit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NavigableMessageDialog {
|
||||||
|
id: pairCompleteDialog
|
||||||
|
closePolicy: Popup.CloseOnEscape
|
||||||
|
|
||||||
|
text:qsTr("Pairing completed successfully")
|
||||||
|
standardButtons: Dialog.Ok
|
||||||
|
onClosed: {
|
||||||
|
Qt.quit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,11 +6,11 @@ import Session 1.0
|
|||||||
|
|
||||||
Item {
|
Item {
|
||||||
function onSearchingComputer() {
|
function onSearchingComputer() {
|
||||||
stageLabel.text = "Establishing connection to PC..."
|
stageLabel.text = qsTr("Establishing connection to PC...")
|
||||||
}
|
}
|
||||||
|
|
||||||
function onQuittingApp() {
|
function onQuittingApp() {
|
||||||
stageLabel.text = "Quitting app..."
|
stageLabel.text = qsTr("Quitting app...")
|
||||||
}
|
}
|
||||||
|
|
||||||
function onFailure(message) {
|
function onFailure(message) {
|
||||||
@@ -34,6 +34,7 @@ Item {
|
|||||||
|
|
||||||
BusyIndicator {
|
BusyIndicator {
|
||||||
id: stageSpinner
|
id: stageSpinner
|
||||||
|
running: visible
|
||||||
}
|
}
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ import ComputerManager 1.0
|
|||||||
|
|
||||||
Item {
|
Item {
|
||||||
function onSearchingComputer() {
|
function onSearchingComputer() {
|
||||||
stageLabel.text = "Establishing connection to PC..."
|
stageLabel.text = qsTr("Establishing connection to PC...")
|
||||||
}
|
}
|
||||||
|
|
||||||
function onSearchingApp() {
|
function onSearchingApp() {
|
||||||
stageLabel.text = "Loading app list..."
|
stageLabel.text = qsTr("Loading app list...")
|
||||||
}
|
}
|
||||||
|
|
||||||
function onSessionCreated(appName, session) {
|
function onSessionCreated(appName, session) {
|
||||||
@@ -25,6 +25,7 @@ Item {
|
|||||||
function onLaunchFailed(message) {
|
function onLaunchFailed(message) {
|
||||||
errorDialog.text = message
|
errorDialog.text = message
|
||||||
errorDialog.open()
|
errorDialog.open()
|
||||||
|
console.error(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
function onAppQuitRequired(appName) {
|
function onAppQuitRequired(appName) {
|
||||||
@@ -35,6 +36,7 @@ Item {
|
|||||||
StackView.onActivated: {
|
StackView.onActivated: {
|
||||||
if (!launcher.isExecuted()) {
|
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)
|
||||||
@@ -50,6 +52,7 @@ Item {
|
|||||||
|
|
||||||
BusyIndicator {
|
BusyIndicator {
|
||||||
id: stageSpinner
|
id: stageSpinner
|
||||||
|
running: visible
|
||||||
}
|
}
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
@@ -72,16 +75,14 @@ Item {
|
|||||||
|
|
||||||
NavigableMessageDialog {
|
NavigableMessageDialog {
|
||||||
id: quitAppDialog
|
id: quitAppDialog
|
||||||
text:"Are you sure you want to quit " + appName +"? Any unsaved progress will be lost."
|
text:qsTr("Are you sure you want to quit %1? Any unsaved progress will be lost.").arg(appName)
|
||||||
standardButtons: Dialog.Yes | Dialog.No
|
standardButtons: Dialog.Yes | Dialog.No
|
||||||
property string appName : ""
|
property string appName : ""
|
||||||
|
|
||||||
function quitApp() {
|
function quitApp() {
|
||||||
var component = Qt.createComponent("QuitSegue.qml")
|
var component = Qt.createComponent("QuitSegue.qml")
|
||||||
var params = {"appName": appName}
|
var params = {"appName": appName, "quitRunningAppFn": function() { launcher.quitRunningApp() }}
|
||||||
stackView.push(component.createObject(stackView, params))
|
stackView.push(component.createObject(stackView, params))
|
||||||
// Trigger the quit after pushing the quit segue on screen
|
|
||||||
launcher.quitRunningApp()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onAccepted: quitApp()
|
onAccepted: quitApp()
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import QtQuick 2.0
|
import QtQuick 2.0
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
objectName: "Gamepad Mapping"
|
objectName: qsTr("Gamepad Mapping")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,13 @@
|
|||||||
import QtQuick 2.0
|
import QtQuick 2.0
|
||||||
import QtQuick.Controls 2.2
|
import QtQuick.Controls 2.5
|
||||||
|
|
||||||
Dialog {
|
Dialog {
|
||||||
parent: overlay
|
modal: true
|
||||||
|
anchors.centerIn: Overlay.overlay
|
||||||
|
|
||||||
x: Math.round((parent.width - width) / 2)
|
onClosed: {
|
||||||
y: Math.round((parent.height - height) / 2)
|
// We must force focus back to the last item. If we don't,
|
||||||
|
// gamepad and keyboard navigation will break after a
|
||||||
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.
|
// dialog appears.
|
||||||
stackView.forceActiveFocus()
|
stackView.forceActiveFocus()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,8 +17,16 @@ ItemDelegate {
|
|||||||
}
|
}
|
||||||
Keys.onUpPressed: {
|
Keys.onUpPressed: {
|
||||||
grid.moveCurrentIndexUp()
|
grid.moveCurrentIndexUp()
|
||||||
|
|
||||||
|
// If we've reached the top of the grid, move focus to the toolbar
|
||||||
|
if (grid.currentItem === this) {
|
||||||
|
nextItemInFocusChain(false).forceActiveFocus(Qt.TabFocus)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Keys.onReturnPressed: {
|
Keys.onReturnPressed: {
|
||||||
clicked()
|
clicked()
|
||||||
}
|
}
|
||||||
|
Keys.onEnterPressed: {
|
||||||
|
clicked()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,19 @@ import QtQuick 2.0
|
|||||||
import QtQuick.Controls 2.2
|
import QtQuick.Controls 2.2
|
||||||
|
|
||||||
Menu {
|
Menu {
|
||||||
|
property var initiator
|
||||||
|
|
||||||
onOpened: {
|
onOpened: {
|
||||||
itemAt(0).forceActiveFocus(Qt.TabFocusReason)
|
// If the initiating object currently has keyboard focus,
|
||||||
|
// give focus to the first visible and enabled menu item
|
||||||
|
if (initiator.focus) {
|
||||||
|
for (var i = 0; i < count; i++) {
|
||||||
|
var item = itemAt(i)
|
||||||
|
if (item.visible && item.enabled) {
|
||||||
|
item.forceActiveFocus(Qt.TabFocusReason)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,6 @@ 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
|
||||||
@@ -15,14 +11,18 @@ MenuItem {
|
|||||||
// We must close the context menu first or
|
// We must close the context menu first or
|
||||||
// it can steal focus from any dialogs that
|
// it can steal focus from any dialogs that
|
||||||
// onTriggered may spawn.
|
// onTriggered may spawn.
|
||||||
parentMenu.close()
|
menu.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
Keys.onReturnPressed: {
|
Keys.onReturnPressed: {
|
||||||
triggered()
|
triggered()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Keys.onEnterPressed: {
|
||||||
|
triggered()
|
||||||
|
}
|
||||||
|
|
||||||
Keys.onEscapePressed: {
|
Keys.onEscapePressed: {
|
||||||
parentMenu.close()
|
menu.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ NavigableDialog {
|
|||||||
id: dialog
|
id: dialog
|
||||||
|
|
||||||
property alias text: dialogLabel.dialogText
|
property alias text: dialogLabel.dialogText
|
||||||
|
property alias showSpinner: dialogSpinner.visible
|
||||||
|
property alias imageSrc: dialogImage.source
|
||||||
|
|
||||||
property string helpText
|
property string helpText
|
||||||
property string helpUrl : "https://github.com/moonlight-stream/moonlight-docs/wiki/Troubleshooting"
|
property string helpUrl : "https://github.com/moonlight-stream/moonlight-docs/wiki/Troubleshooting"
|
||||||
@@ -13,13 +15,22 @@ NavigableDialog {
|
|||||||
|
|
||||||
onOpened: {
|
onOpened: {
|
||||||
// Force keyboard focus on the label so keyboard navigation works
|
// Force keyboard focus on the label so keyboard navigation works
|
||||||
dialogLabel.forceActiveFocus()
|
if (dialogButtonBox.count > 0) {
|
||||||
|
dialogButtonBox.itemAt(dialogButtonBox.count - 1).forceActiveFocus(Qt.TabFocus)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
spacing: 10
|
spacing: 10
|
||||||
|
|
||||||
|
BusyIndicator {
|
||||||
|
id: dialogSpinner
|
||||||
|
visible: false
|
||||||
|
running: visible
|
||||||
|
}
|
||||||
|
|
||||||
Image {
|
Image {
|
||||||
|
id: dialogImage
|
||||||
source: (standardButtons & Dialog.Yes) ?
|
source: (standardButtons & Dialog.Yes) ?
|
||||||
"qrc:/res/baseline-help_outline-24px.svg" :
|
"qrc:/res/baseline-help_outline-24px.svg" :
|
||||||
"qrc:/res/baseline-error_outline-24px.svg"
|
"qrc:/res/baseline-error_outline-24px.svg"
|
||||||
@@ -28,6 +39,7 @@ NavigableDialog {
|
|||||||
width: 50
|
width: 50
|
||||||
height: 50
|
height: 50
|
||||||
}
|
}
|
||||||
|
visible: !showSpinner
|
||||||
}
|
}
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
@@ -42,14 +54,6 @@ NavigableDialog {
|
|||||||
// will cause word wrap to kick in.
|
// will cause word wrap to kick in.
|
||||||
Layout.maximumWidth: 400
|
Layout.maximumWidth: 400
|
||||||
Layout.maximumHeight: 400
|
Layout.maximumHeight: 400
|
||||||
|
|
||||||
Keys.onReturnPressed: {
|
|
||||||
accept()
|
|
||||||
}
|
|
||||||
|
|
||||||
Keys.onEscapePressed: {
|
|
||||||
reject()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,6 +61,15 @@ NavigableDialog {
|
|||||||
id: dialogButtonBox
|
id: dialogButtonBox
|
||||||
standardButtons: dialog.standardButtons
|
standardButtons: dialog.standardButtons
|
||||||
|
|
||||||
|
delegate: Button {
|
||||||
|
flat: true
|
||||||
|
|
||||||
|
Keys.onReturnPressed: clicked()
|
||||||
|
Keys.onEnterPressed: clicked()
|
||||||
|
Keys.onRightPressed: nextItemInFocusChain(true).forceActiveFocus(Qt.TabFocus)
|
||||||
|
Keys.onLeftPressed: nextItemInFocusChain(false).forceActiveFocus(Qt.TabFocus)
|
||||||
|
}
|
||||||
|
|
||||||
onHelpRequested: {
|
onHelpRequested: {
|
||||||
Qt.openUrlExternally(helpUrl)
|
Qt.openUrlExternally(helpUrl)
|
||||||
close()
|
close()
|
||||||
|
|||||||
@@ -1,32 +1,28 @@
|
|||||||
import QtQuick 2.0
|
import QtQuick 2.0
|
||||||
import QtQuick.Controls 2.2
|
import QtQuick.Controls 2.2
|
||||||
|
import QtQuick.Layouts 1.3
|
||||||
|
|
||||||
ToolButton {
|
ToolButton {
|
||||||
property string iconSource
|
property string iconSource
|
||||||
|
|
||||||
activeFocusOnTab: true
|
activeFocusOnTab: true
|
||||||
|
|
||||||
// FIXME: We're using an Image here rather than icon.source because
|
icon.source: iconSource
|
||||||
// icons don't work on Qt 5.9 LTS.
|
icon.width: background.width
|
||||||
Image {
|
icon.height: background.height
|
||||||
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
|
// This determines the size of the Material highlight. We increase it
|
||||||
// from the default because we use larger than normal icons for TV readability.
|
// 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
|
Layout.preferredHeight: parent.height
|
||||||
background.height: (parent.height - parent.anchors.bottomMargin - parent.anchors.topMargin) * 0.60
|
|
||||||
|
|
||||||
Keys.onReturnPressed: {
|
Keys.onReturnPressed: {
|
||||||
clicked()
|
clicked()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Keys.onEnterPressed: {
|
||||||
|
clicked()
|
||||||
|
}
|
||||||
|
|
||||||
Keys.onRightPressed: {
|
Keys.onRightPressed: {
|
||||||
nextItemInFocusChain(true).forceActiveFocus(Qt.TabFocus)
|
nextItemInFocusChain(true).forceActiveFocus(Qt.TabFocus)
|
||||||
}
|
}
|
||||||
|
|||||||
+196
-68
@@ -1,10 +1,12 @@
|
|||||||
import QtQuick 2.9
|
import QtQuick 2.9
|
||||||
import QtQuick.Controls 2.2
|
import QtQuick.Controls 2.2
|
||||||
|
import QtQuick.Layouts 1.3
|
||||||
|
|
||||||
import ComputerModel 1.0
|
import ComputerModel 1.0
|
||||||
|
|
||||||
import ComputerManager 1.0
|
import ComputerManager 1.0
|
||||||
import StreamingPreferences 1.0
|
import StreamingPreferences 1.0
|
||||||
|
import SystemProperties 1.0
|
||||||
import SdlGamepadKeyNavigation 1.0
|
import SdlGamepadKeyNavigation 1.0
|
||||||
|
|
||||||
CenteredGridView {
|
CenteredGridView {
|
||||||
@@ -16,7 +18,7 @@ CenteredGridView {
|
|||||||
topMargin: 20
|
topMargin: 20
|
||||||
bottomMargin: 5
|
bottomMargin: 5
|
||||||
cellWidth: 310; cellHeight: 330;
|
cellWidth: 310; cellHeight: 330;
|
||||||
objectName: "Computers"
|
objectName: qsTr("Computers")
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
// Don't show any highlighted item until interacting with them.
|
// Don't show any highlighted item until interacting with them.
|
||||||
@@ -25,17 +27,15 @@ CenteredGridView {
|
|||||||
currentIndex = -1
|
currentIndex = -1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Note: Any initialization done here that is critical for streaming must
|
||||||
|
// also be done in CliStartStreamSegue.qml, since this code does not run
|
||||||
|
// for command-line initiated streams.
|
||||||
StackView.onActivated: {
|
StackView.onActivated: {
|
||||||
// Setup signals on CM
|
// Setup signals on CM
|
||||||
ComputerManager.computerAddCompleted.connect(addComplete)
|
ComputerManager.computerAddCompleted.connect(addComplete)
|
||||||
|
|
||||||
// This is a bit of a hack to do this here as opposed to main.qml, but
|
|
||||||
// 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
|
// Highlight the first item if a gamepad is connected
|
||||||
if (currentIndex == -1 && SdlGamepadKeyNavigation.getConnectedGamepads() > 0) {
|
if (currentIndex === -1 && SdlGamepadKeyNavigation.getConnectedGamepads() > 0) {
|
||||||
currentIndex = 0
|
currentIndex = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -57,11 +57,18 @@ CenteredGridView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function addComplete(success)
|
function addComplete(success, detectedPortBlocking)
|
||||||
{
|
{
|
||||||
if (!success) {
|
if (!success) {
|
||||||
errorDialog.text = "Unable to connect to the specified PC."
|
errorDialog.text = qsTr("Unable to connect to the specified PC.")
|
||||||
errorDialog.helpText = "Click the Help button for possible solutions."
|
|
||||||
|
if (detectedPortBlocking) {
|
||||||
|
errorDialog.text += "\n\n" + qsTr("This PC's Internet connection is blocking Moonlight. Streaming over the Internet may not work while connected to this network.")
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
errorDialog.helpText = qsTr("Click the Help button for possible solutions.")
|
||||||
|
}
|
||||||
|
|
||||||
errorDialog.open()
|
errorDialog.open()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -71,6 +78,7 @@ CenteredGridView {
|
|||||||
var model = Qt.createQmlObject('import ComputerModel 1.0; ComputerModel {}', parent, '')
|
var model = Qt.createQmlObject('import ComputerModel 1.0; ComputerModel {}', parent, '')
|
||||||
model.initialize(ComputerManager)
|
model.initialize(ComputerManager)
|
||||||
model.pairingCompleted.connect(pairingComplete)
|
model.pairingCompleted.connect(pairingComplete)
|
||||||
|
model.connectionTestCompleted.connect(testConnectionDialog.connectionTestComplete)
|
||||||
return model
|
return model
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,13 +90,14 @@ CenteredGridView {
|
|||||||
BusyIndicator {
|
BusyIndicator {
|
||||||
id: searchSpinner
|
id: searchSpinner
|
||||||
visible: StreamingPreferences.enableMdns
|
visible: StreamingPreferences.enableMdns
|
||||||
|
running: visible
|
||||||
}
|
}
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
height: searchSpinner.height
|
height: searchSpinner.height
|
||||||
elide: Label.ElideRight
|
elide: Label.ElideRight
|
||||||
text: StreamingPreferences.enableMdns ? "Searching for PCs with NVIDIA GameStream enabled..."
|
text: StreamingPreferences.enableMdns ? qsTr("Searching for compatible hosts on your local network...")
|
||||||
: "Automatic PC discovery is disabled. Add your PC manually."
|
: qsTr("Automatic PC discovery is disabled. Add your PC manually.")
|
||||||
font.pointSize: 20
|
font.pointSize: 20
|
||||||
verticalAlignment: Text.AlignVCenter
|
verticalAlignment: Text.AlignVCenter
|
||||||
wrapMode: Text.Wrap
|
wrapMode: Text.Wrap
|
||||||
@@ -101,6 +110,8 @@ CenteredGridView {
|
|||||||
width: 300; height: 320;
|
width: 300; height: 320;
|
||||||
grid: pcGrid
|
grid: pcGrid
|
||||||
|
|
||||||
|
property alias pcContextMenu : pcContextMenuLoader.item
|
||||||
|
|
||||||
Image {
|
Image {
|
||||||
id: pcIcon
|
id: pcIcon
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
@@ -116,12 +127,12 @@ CenteredGridView {
|
|||||||
id: stateIcon
|
id: stateIcon
|
||||||
anchors.horizontalCenter: pcIcon.horizontalCenter
|
anchors.horizontalCenter: pcIcon.horizontalCenter
|
||||||
anchors.verticalCenter: pcIcon.verticalCenter
|
anchors.verticalCenter: pcIcon.verticalCenter
|
||||||
anchors.verticalCenterOffset: -15
|
anchors.verticalCenterOffset: !model.online ? -18 : -16
|
||||||
visible: !model.statusUnknown && (!model.online || !model.paired)
|
visible: !model.statusUnknown && (!model.online || !model.paired)
|
||||||
source: !model.online ? "qrc:/res/baseline-warning-24px.svg" : "qrc:/res/baseline-lock-24px.svg"
|
source: !model.online ? "qrc:/res/warning_FILL1_wght300_GRAD200_opsz24.svg" : "qrc:/res/baseline-lock-24px.svg"
|
||||||
sourceSize {
|
sourceSize {
|
||||||
width: 75
|
width: !model.online ? 75 : 70
|
||||||
height: 75
|
height: !model.online ? 75 : 70
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,6 +144,7 @@ CenteredGridView {
|
|||||||
width: 75
|
width: 75
|
||||||
height: 75
|
height: 75
|
||||||
visible: model.statusUnknown
|
visible: model.statusUnknown
|
||||||
|
running: visible
|
||||||
}
|
}
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
@@ -148,50 +160,87 @@ CenteredGridView {
|
|||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
}
|
}
|
||||||
|
|
||||||
NavigableMenu {
|
Loader {
|
||||||
id: pcContextMenu
|
id: pcContextMenuLoader
|
||||||
NavigableMenuItem {
|
asynchronous: true
|
||||||
parentMenu: pcContextMenu
|
sourceComponent: NavigableMenu {
|
||||||
text: "Delete PC"
|
id: pcContextMenu
|
||||||
onTriggered: {
|
initiator: pcContextMenuLoader.parent
|
||||||
deletePcDialog.pcIndex = index
|
MenuItem {
|
||||||
// get confirmation first, actual closing is called from the dialog
|
text: qsTr("PC Status: %1").arg(model.online ? qsTr("Online") : qsTr("Offline"))
|
||||||
deletePcDialog.open()
|
font.bold: true
|
||||||
|
enabled: false
|
||||||
|
}
|
||||||
|
NavigableMenuItem {
|
||||||
|
text: qsTr("View All Apps")
|
||||||
|
onTriggered: {
|
||||||
|
var component = Qt.createComponent("AppView.qml")
|
||||||
|
var appView = component.createObject(stackView, {"computerIndex": index, "objectName": model.name, "showHiddenGames": true})
|
||||||
|
stackView.push(appView)
|
||||||
|
}
|
||||||
|
visible: model.online && model.paired
|
||||||
|
}
|
||||||
|
NavigableMenuItem {
|
||||||
|
text: qsTr("Wake PC")
|
||||||
|
onTriggered: computerModel.wakeComputer(index)
|
||||||
|
visible: !model.online && model.wakeable
|
||||||
|
}
|
||||||
|
NavigableMenuItem {
|
||||||
|
text: qsTr("Test Network")
|
||||||
|
onTriggered: {
|
||||||
|
computerModel.testConnectionForComputer(index)
|
||||||
|
testConnectionDialog.open()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NavigableMenuItem {
|
||||||
|
text: qsTr("Rename PC")
|
||||||
|
onTriggered: {
|
||||||
|
renamePcDialog.pcIndex = index
|
||||||
|
renamePcDialog.originalName = model.name
|
||||||
|
renamePcDialog.open()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NavigableMenuItem {
|
||||||
|
text: qsTr("Delete PC")
|
||||||
|
onTriggered: {
|
||||||
|
deletePcDialog.pcIndex = index
|
||||||
|
deletePcDialog.pcName = model.name
|
||||||
|
deletePcDialog.open()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NavigableMenuItem {
|
||||||
|
text: qsTr("View Details")
|
||||||
|
onTriggered: {
|
||||||
|
showPcDetailsDialog.pcDetails = model.details
|
||||||
|
showPcDetailsDialog.open()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
NavigableMenuItem {
|
|
||||||
parentMenu: pcContextMenu
|
|
||||||
text: "Wake PC"
|
|
||||||
onTriggered: computerModel.wakeComputer(index)
|
|
||||||
visible: !model.online && model.wakeable
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if (model.online) {
|
if (model.online) {
|
||||||
if (model.paired) {
|
if (!model.serverSupported) {
|
||||||
|
errorDialog.text = qsTr("The version of GeForce Experience on %1 is not supported by this build of Moonlight. You must update Moonlight to stream from %1.").arg(model.name)
|
||||||
|
errorDialog.helpText = ""
|
||||||
|
errorDialog.open()
|
||||||
|
}
|
||||||
|
else if (model.paired) {
|
||||||
// go to game view
|
// go to game view
|
||||||
var component = Qt.createComponent("AppView.qml")
|
var component = Qt.createComponent("AppView.qml")
|
||||||
var appView = component.createObject(stackView, {"computerIndex": index, "objectName": model.name})
|
var appView = component.createObject(stackView, {"computerIndex": index, "objectName": model.name})
|
||||||
stackView.push(appView)
|
stackView.push(appView)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
if (!model.busy) {
|
var pin = computerModel.generatePinString()
|
||||||
var pin = ("0000" + Math.floor(Math.random() * 10000)).slice(-4)
|
|
||||||
|
|
||||||
// Kick off pairing in the background
|
// Kick off pairing in the background
|
||||||
computerModel.pairComputer(index, pin)
|
computerModel.pairComputer(index, pin)
|
||||||
|
|
||||||
// Display the pairing dialog
|
// Display the pairing dialog
|
||||||
pairDialog.pin = pin
|
pairDialog.pin = pin
|
||||||
pairDialog.open()
|
pairDialog.open()
|
||||||
}
|
|
||||||
else {
|
|
||||||
// cannot pair while something is streaming or attempting to pair
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else if (!model.online) {
|
} else if (!model.online) {
|
||||||
// Using open() here because it may be activated by keyboard
|
// Using open() here because it may be activated by keyboard
|
||||||
@@ -214,7 +263,7 @@ CenteredGridView {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
acceptedButtons: Qt.RightButton;
|
acceptedButtons: Qt.RightButton;
|
||||||
onClicked: {
|
onClicked: {
|
||||||
parent.onPressAndHold()
|
parent.pressAndHold()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,6 +272,12 @@ CenteredGridView {
|
|||||||
// the ItemDelegate and not where the mouse cursor is
|
// the ItemDelegate and not where the mouse cursor is
|
||||||
pcContextMenu.open()
|
pcContextMenu.open()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Keys.onDeletePressed: {
|
||||||
|
deletePcDialog.pcIndex = index
|
||||||
|
deletePcDialog.pcName = model.name
|
||||||
|
deletePcDialog.open()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ErrorMessageDialog {
|
ErrorMessageDialog {
|
||||||
@@ -235,15 +290,12 @@ CenteredGridView {
|
|||||||
|
|
||||||
NavigableMessageDialog {
|
NavigableMessageDialog {
|
||||||
id: pairDialog
|
id: pairDialog
|
||||||
|
|
||||||
// Pairing dialog must be modal to prevent double-clicks from triggering
|
|
||||||
// pairing twice
|
|
||||||
modal: true
|
|
||||||
closePolicy: Popup.CloseOnEscape
|
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
|
||||||
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:qsTr("Please enter %1 on your host PC. This dialog will close when pairing is completed.").arg(pin)+"\n\n"+
|
||||||
|
qsTr("If your host PC is running Sunshine, navigate to the Sunshine web UI to enter the PIN.")
|
||||||
standardButtons: Dialog.Cancel
|
standardButtons: Dialog.Cancel
|
||||||
onRejected: {
|
onRejected: {
|
||||||
// FIXME: We should interrupt pairing here
|
// FIXME: We should interrupt pairing here
|
||||||
@@ -253,24 +305,100 @@ CenteredGridView {
|
|||||||
NavigableMessageDialog {
|
NavigableMessageDialog {
|
||||||
id: deletePcDialog
|
id: deletePcDialog
|
||||||
// don't allow edits to the rest of the window while open
|
// don't allow edits to the rest of the window while open
|
||||||
property int pcIndex : -1;
|
property int pcIndex : -1
|
||||||
text:"Are you sure you want to remove this PC?"
|
property string pcName : ""
|
||||||
|
text: qsTr("Are you sure you want to remove '%1'?").arg(pcName)
|
||||||
standardButtons: Dialog.Yes | Dialog.No
|
standardButtons: Dialog.Yes | Dialog.No
|
||||||
|
|
||||||
function deletePc() {
|
onAccepted: {
|
||||||
console.log("deleting PC pairing for PC at index: " + pcIndex)
|
computerModel.deleteComputer(pcIndex)
|
||||||
computerModel.deleteComputer(pcIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
onAccepted: deletePc()
|
|
||||||
}
|
|
||||||
|
|
||||||
ScrollBar.vertical: ScrollBar {
|
|
||||||
parent: pcGrid.parent
|
|
||||||
anchors {
|
|
||||||
top: parent.top
|
|
||||||
right: parent.right
|
|
||||||
bottom: parent.bottom
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
NavigableMessageDialog {
|
||||||
|
id: testConnectionDialog
|
||||||
|
closePolicy: Popup.CloseOnEscape
|
||||||
|
standardButtons: Dialog.Ok
|
||||||
|
|
||||||
|
onAboutToShow: {
|
||||||
|
testConnectionDialog.text = qsTr("Moonlight is testing your network connection to determine if any required ports are blocked.") + "\n\n" + qsTr("This may take a few seconds…")
|
||||||
|
showSpinner = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function connectionTestComplete(result, blockedPorts)
|
||||||
|
{
|
||||||
|
if (result === -1) {
|
||||||
|
text = qsTr("The network test could not be performed because none of Moonlight's connection testing servers were reachable from this PC. Check your Internet connection or try again later.")
|
||||||
|
imageSrc = "qrc:/res/baseline-warning-24px.svg"
|
||||||
|
}
|
||||||
|
else if (result === 0) {
|
||||||
|
text = qsTr("This network does not appear to be blocking Moonlight. If you still have trouble connecting, check your PC's firewall settings.") + "\n\n" + qsTr("If you are trying to stream over the Internet, install the Moonlight Internet Hosting Tool on your gaming PC and run the included Internet Streaming Tester to check your gaming PC's Internet connection.")
|
||||||
|
imageSrc = "qrc:/res/baseline-check_circle_outline-24px.svg"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
text = qsTr("Your PC's current network connection seems to be blocking Moonlight. Streaming over the Internet may not work while connected to this network.") + "\n\n" + qsTr("The following network ports were blocked:") + "\n"
|
||||||
|
text += blockedPorts
|
||||||
|
imageSrc = "qrc:/res/baseline-error_outline-24px.svg"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop showing the spinner and show the image instead
|
||||||
|
showSpinner = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NavigableDialog {
|
||||||
|
id: renamePcDialog
|
||||||
|
property string label: qsTr("Enter the new name for this PC:")
|
||||||
|
property string originalName
|
||||||
|
property int pcIndex : -1;
|
||||||
|
|
||||||
|
standardButtons: Dialog.Ok | Dialog.Cancel
|
||||||
|
|
||||||
|
onOpened: {
|
||||||
|
// Force keyboard focus on the textbox so keyboard navigation works
|
||||||
|
editText.forceActiveFocus()
|
||||||
|
}
|
||||||
|
|
||||||
|
onClosed: {
|
||||||
|
editText.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
onAccepted: {
|
||||||
|
if (editText.text) {
|
||||||
|
computerModel.renameComputer(pcIndex, editText.text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
Label {
|
||||||
|
text: renamePcDialog.label
|
||||||
|
font.bold: true
|
||||||
|
}
|
||||||
|
|
||||||
|
TextField {
|
||||||
|
id: editText
|
||||||
|
placeholderText: renamePcDialog.originalName
|
||||||
|
Layout.fillWidth: true
|
||||||
|
focus: true
|
||||||
|
|
||||||
|
Keys.onReturnPressed: {
|
||||||
|
renamePcDialog.accept()
|
||||||
|
}
|
||||||
|
|
||||||
|
Keys.onEnterPressed: {
|
||||||
|
renamePcDialog.accept()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NavigableMessageDialog {
|
||||||
|
id: showPcDetailsDialog
|
||||||
|
property string pcDetails : "";
|
||||||
|
text: showPcDetailsDialog.pcDetails
|
||||||
|
imageSrc: "qrc:/res/baseline-help_outline-24px.svg"
|
||||||
|
standardButtons: Dialog.Ok
|
||||||
|
}
|
||||||
|
|
||||||
|
ScrollBar.vertical: ScrollBar {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,10 +6,11 @@ import Session 1.0
|
|||||||
|
|
||||||
Item {
|
Item {
|
||||||
property string appName
|
property string appName
|
||||||
|
property var quitRunningAppFn
|
||||||
property Session nextSession : null
|
property Session nextSession : null
|
||||||
property string nextAppName : ""
|
property string nextAppName : ""
|
||||||
|
|
||||||
property string stageText : "Quitting " + appName + "..."
|
property string stageText : qsTr("Quitting %1...").arg(appName)
|
||||||
|
|
||||||
function quitAppCompleted(error)
|
function quitAppCompleted(error)
|
||||||
{
|
{
|
||||||
@@ -17,6 +18,7 @@ Item {
|
|||||||
if (error !== undefined) {
|
if (error !== undefined) {
|
||||||
errorDialog.text = error
|
errorDialog.text = error
|
||||||
errorDialog.open()
|
errorDialog.open()
|
||||||
|
console.error(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we're supposed to launch another game after this, do so now
|
// If we're supposed to launch another game after this, do so now
|
||||||
@@ -37,6 +39,11 @@ Item {
|
|||||||
|
|
||||||
// Connect the quit completion signal
|
// Connect the quit completion signal
|
||||||
ComputerManager.quitAppCompleted.connect(quitAppCompleted)
|
ComputerManager.quitAppCompleted.connect(quitAppCompleted)
|
||||||
|
|
||||||
|
// Start the quit operation if requested
|
||||||
|
if (quitRunningAppFn) {
|
||||||
|
quitRunningAppFn()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
StackView.onDeactivating: {
|
StackView.onDeactivating: {
|
||||||
@@ -53,6 +60,7 @@ Item {
|
|||||||
|
|
||||||
BusyIndicator {
|
BusyIndicator {
|
||||||
id: stageSpinner
|
id: stageSpinner
|
||||||
|
running: visible
|
||||||
}
|
}
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
|
|||||||
+1239
-268
File diff suppressed because it is too large
Load Diff
+102
-36
@@ -4,23 +4,30 @@ import QtQuick.Window 2.2
|
|||||||
|
|
||||||
import SdlGamepadKeyNavigation 1.0
|
import SdlGamepadKeyNavigation 1.0
|
||||||
import Session 1.0
|
import Session 1.0
|
||||||
|
import SystemProperties 1.0
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
property Session session
|
property Session session
|
||||||
property string appName
|
property string appName
|
||||||
property string stageText : "Starting " + appName + "..."
|
property string stageText : isResume ? qsTr("Resuming %1...").arg(appName) :
|
||||||
|
qsTr("Starting %1...").arg(appName)
|
||||||
|
property bool isResume : false
|
||||||
property bool quitAfter : false
|
property bool quitAfter : false
|
||||||
|
|
||||||
function stageStarting(stage)
|
function stageStarting(stage)
|
||||||
{
|
{
|
||||||
// Update the spinner text
|
// Update the spinner text
|
||||||
stageText = "Starting " + stage + "..."
|
stageText = qsTr("Starting %1...").arg(stage)
|
||||||
}
|
}
|
||||||
|
|
||||||
function stageFailed(stage, errorCode)
|
function stageFailed(stage, errorCode, failingPorts)
|
||||||
{
|
{
|
||||||
// Display the error dialog after Session::exec() returns
|
// Display the error dialog after Session::exec() returns
|
||||||
streamSegueErrorDialog.text = "Starting " + stage + " failed: Error " + errorCode
|
streamSegueErrorDialog.text = qsTr("Starting %1 failed: Error %2").arg(stage).arg(errorCode)
|
||||||
|
|
||||||
|
if (failingPorts) {
|
||||||
|
streamSegueErrorDialog.text += "\n\n" + qsTr("Check your firewall and port forwarding rules for port(s): %1").arg(failingPorts)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function connectionStarted()
|
function connectionStarted()
|
||||||
@@ -39,17 +46,7 @@ Item {
|
|||||||
{
|
{
|
||||||
// Display the error dialog after Session::exec() returns
|
// Display the error dialog after Session::exec() returns
|
||||||
streamSegueErrorDialog.text = text
|
streamSegueErrorDialog.text = text
|
||||||
}
|
console.error(text)
|
||||||
|
|
||||||
function displayLaunchWarning(text)
|
|
||||||
{
|
|
||||||
// This toast appears for 3 seconds, just shorter than how long
|
|
||||||
// Session will wait for it to be displayed. This gives it time
|
|
||||||
// to transition to invisible before continuing.
|
|
||||||
var toast = Qt.createQmlObject('import QtQuick.Controls 2.2; ToolTip {}', parent, '')
|
|
||||||
toast.text = text
|
|
||||||
toast.timeout = 3000
|
|
||||||
toast.visible = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function quitStarting()
|
function quitStarting()
|
||||||
@@ -62,25 +59,25 @@ Item {
|
|||||||
window.visible = true
|
window.visible = true
|
||||||
}
|
}
|
||||||
|
|
||||||
function sessionFinished()
|
function sessionFinished(portTestResult)
|
||||||
{
|
{
|
||||||
// Enable GUI gamepad usage now
|
if (portTestResult !== 0 && portTestResult !== -1 && streamSegueErrorDialog.text) {
|
||||||
|
streamSegueErrorDialog.text += "\n\n" + qsTr("This PC's Internet connection is blocking Moonlight. Streaming over the Internet may not work while connected to this network.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-enable GUI gamepad usage now
|
||||||
SdlGamepadKeyNavigation.enable()
|
SdlGamepadKeyNavigation.enable()
|
||||||
|
|
||||||
if (quitAfter) {
|
// Pop the StreamSegue off the stack if this is a GUI-based app launch
|
||||||
if (streamSegueErrorDialog.text) {
|
if (!quitAfter) {
|
||||||
// Quit when the error dialog is acknowledged
|
|
||||||
streamSegueErrorDialog.quitAfter = quitAfter
|
|
||||||
streamSegueErrorDialog.open()
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Quit immediately
|
|
||||||
Qt.quit()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Exit this view
|
|
||||||
stackView.pop()
|
stackView.pop()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (quitAfter && !streamSegueErrorDialog.text) {
|
||||||
|
// If this was a CLI launch without errors, exit now
|
||||||
|
Qt.quit()
|
||||||
|
}
|
||||||
|
else {
|
||||||
// Show the Qt window again after streaming
|
// Show the Qt window again after streaming
|
||||||
window.visible = true
|
window.visible = true
|
||||||
|
|
||||||
@@ -95,11 +92,19 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sessionReadyForDeletion()
|
||||||
|
{
|
||||||
|
// Garbage collect the Session object since it's pretty heavyweight
|
||||||
|
// and keeps other libraries (like SDL_TTF) around until it is deleted.
|
||||||
|
session = null
|
||||||
|
gc()
|
||||||
|
}
|
||||||
|
|
||||||
StackView.onDeactivating: {
|
StackView.onDeactivating: {
|
||||||
// Show the toolbar again when popped off the stack
|
// Show the toolbar again when popped off the stack
|
||||||
toolBar.visible = true
|
toolBar.visible = true
|
||||||
|
|
||||||
// Enable GUI gamepad usage now
|
// Re-enable GUI gamepad usage now
|
||||||
SdlGamepadKeyNavigation.enable()
|
SdlGamepadKeyNavigation.enable()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,14 +117,44 @@ Item {
|
|||||||
session.stageFailed.connect(stageFailed)
|
session.stageFailed.connect(stageFailed)
|
||||||
session.connectionStarted.connect(connectionStarted)
|
session.connectionStarted.connect(connectionStarted)
|
||||||
session.displayLaunchError.connect(displayLaunchError)
|
session.displayLaunchError.connect(displayLaunchError)
|
||||||
session.displayLaunchWarning.connect(displayLaunchWarning)
|
|
||||||
session.quitStarting.connect(quitStarting)
|
session.quitStarting.connect(quitStarting)
|
||||||
session.sessionFinished.connect(sessionFinished)
|
session.sessionFinished.connect(sessionFinished)
|
||||||
|
session.readyForDeletion.connect(sessionReadyForDeletion)
|
||||||
|
|
||||||
|
// Ensure the SystemProperties async thread is finished,
|
||||||
|
// since it may currently be using the SDL video subsystem
|
||||||
|
SystemProperties.waitForAsyncLoad()
|
||||||
|
|
||||||
// Kick off the stream
|
// Kick off the stream
|
||||||
|
spinnerTimer.start()
|
||||||
streamLoader.active = true
|
streamLoader.active = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: spinnerTimer
|
||||||
|
|
||||||
|
// Display the spinner appearance a bit to allow us to reach
|
||||||
|
// the code in Session.exec() that pumps the event loop.
|
||||||
|
// If we display it immediately, it will briefly hang in the
|
||||||
|
// middle of the animation on Windows, which looks very
|
||||||
|
// obviously broken.
|
||||||
|
interval: 100
|
||||||
|
onTriggered: stageSpinner.visible = true
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: startSessionTimer
|
||||||
|
onTriggered: {
|
||||||
|
// Garbage collect QML stuff before we start streaming,
|
||||||
|
// since we'll probably be streaming for a while and we
|
||||||
|
// won't be able to GC during the stream.
|
||||||
|
gc()
|
||||||
|
|
||||||
|
// Run the streaming session to completion
|
||||||
|
session.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Loader {
|
Loader {
|
||||||
id: streamLoader
|
id: streamLoader
|
||||||
active: false
|
active: false
|
||||||
@@ -130,15 +165,44 @@ Item {
|
|||||||
// 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 = SdlGamepadKeyNavigation.getConnectedGamepads() > 0 ?
|
hintText.text = qsTr("Tip:") + " " + qsTr("Press %1 to disconnect your session").arg(SdlGamepadKeyNavigation.getConnectedGamepads() > 0 ?
|
||||||
"Tip: Press Start+Select+L1+R1 to disconnect your session" :
|
qsTr("Start+Select+L1+R1") : qsTr("Ctrl+Alt+Shift+Q"))
|
||||||
"Tip: Press Ctrl+Alt+Shift+Q to disconnect your session"
|
|
||||||
|
|
||||||
// Stop GUI gamepad usage now
|
// Stop GUI gamepad usage now
|
||||||
SdlGamepadKeyNavigation.disable()
|
SdlGamepadKeyNavigation.disable()
|
||||||
|
|
||||||
// Run the streaming session to completion
|
// Initialize the session and probe for host/client capabilities
|
||||||
session.exec(Screen.virtualX, Screen.virtualY)
|
if (!session.initialize(window)) {
|
||||||
|
sessionFinished(0);
|
||||||
|
sessionReadyForDeletion();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't wait unless we have toasts to display
|
||||||
|
startSessionTimer.interval = 0
|
||||||
|
|
||||||
|
// Display the toasts together in a vertical centered arrangement
|
||||||
|
var yOffset = 0
|
||||||
|
for (var i = 0; i < session.launchWarnings.length; i++) {
|
||||||
|
var text = session.launchWarnings[i]
|
||||||
|
console.warn(text)
|
||||||
|
|
||||||
|
// Show the tooltip for 3 seconds
|
||||||
|
var toast = Qt.createQmlObject('import QtQuick.Controls 2.2; ToolTip {}', parent, '')
|
||||||
|
toast.timeout = 3000
|
||||||
|
toast.text = text
|
||||||
|
toast.y += yOffset
|
||||||
|
toast.visible = true
|
||||||
|
|
||||||
|
// Offset the next toast below the previous one
|
||||||
|
yOffset = toast.y + toast.padding + toast.height
|
||||||
|
|
||||||
|
// Allow an extra 500 ms for the tooltip's fade-out animation to finish
|
||||||
|
startSessionTimer.interval = toast.timeout + 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the timer to wait for toasts (or start the session immediately)
|
||||||
|
startSessionTimer.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceComponent: Item {}
|
sourceComponent: Item {}
|
||||||
@@ -150,6 +214,8 @@ Item {
|
|||||||
|
|
||||||
BusyIndicator {
|
BusyIndicator {
|
||||||
id: stageSpinner
|
id: stageSpinner
|
||||||
|
running: visible
|
||||||
|
visible: false
|
||||||
}
|
}
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
|
|||||||
+176
-32
@@ -7,7 +7,7 @@ AppModel::AppModel(QObject *parent)
|
|||||||
this, &AppModel::handleBoxArtLoaded);
|
this, &AppModel::handleBoxArtLoaded);
|
||||||
}
|
}
|
||||||
|
|
||||||
void AppModel::initialize(ComputerManager* computerManager, int computerIndex)
|
void AppModel::initialize(ComputerManager* computerManager, int computerIndex, bool showHiddenGames)
|
||||||
{
|
{
|
||||||
m_ComputerManager = computerManager;
|
m_ComputerManager = computerManager;
|
||||||
connect(m_ComputerManager, &ComputerManager::computerStateChanged,
|
connect(m_ComputerManager, &ComputerManager::computerStateChanged,
|
||||||
@@ -15,29 +15,23 @@ void AppModel::initialize(ComputerManager* computerManager, int computerIndex)
|
|||||||
|
|
||||||
Q_ASSERT(computerIndex < m_ComputerManager->getComputers().count());
|
Q_ASSERT(computerIndex < m_ComputerManager->getComputers().count());
|
||||||
m_Computer = m_ComputerManager->getComputers().at(computerIndex);
|
m_Computer = m_ComputerManager->getComputers().at(computerIndex);
|
||||||
m_Apps = m_Computer->appList;
|
|
||||||
m_CurrentGameId = m_Computer->currentGameId;
|
m_CurrentGameId = m_Computer->currentGameId;
|
||||||
|
m_ShowHiddenGames = showHiddenGames;
|
||||||
|
|
||||||
|
updateAppList(m_Computer->appList);
|
||||||
}
|
}
|
||||||
|
|
||||||
int AppModel::getRunningAppIndex()
|
int AppModel::getRunningAppId()
|
||||||
{
|
{
|
||||||
if (m_CurrentGameId != 0) {
|
return m_CurrentGameId;
|
||||||
for (int i = 0; i < m_Apps.count(); i++) {
|
|
||||||
if (m_Apps[i].id == m_CurrentGameId) {
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return -1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QString AppModel::getRunningAppName()
|
QString AppModel::getRunningAppName()
|
||||||
{
|
{
|
||||||
if (m_CurrentGameId != 0) {
|
if (m_CurrentGameId != 0) {
|
||||||
for (int i = 0; i < m_Apps.count(); i++) {
|
for (int i = 0; i < m_AllApps.count(); i++) {
|
||||||
if (m_Apps[i].id == m_CurrentGameId) {
|
if (m_AllApps[i].id == m_CurrentGameId) {
|
||||||
return m_Apps[i].name;
|
return m_AllApps[i].name;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -47,12 +41,23 @@ QString AppModel::getRunningAppName()
|
|||||||
|
|
||||||
Session* AppModel::createSessionForApp(int appIndex)
|
Session* AppModel::createSessionForApp(int appIndex)
|
||||||
{
|
{
|
||||||
Q_ASSERT(appIndex < m_Apps.count());
|
Q_ASSERT(appIndex < m_VisibleApps.count());
|
||||||
NvApp app = m_Apps.at(appIndex);
|
NvApp app = m_VisibleApps.at(appIndex);
|
||||||
|
|
||||||
return new Session(m_Computer, app);
|
return new Session(m_Computer, app);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int AppModel::getDirectLaunchAppIndex()
|
||||||
|
{
|
||||||
|
for (int i = 0; i < m_VisibleApps.count(); i++) {
|
||||||
|
if (m_VisibleApps[i].directLaunch) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
int AppModel::rowCount(const QModelIndex &parent) const
|
int AppModel::rowCount(const QModelIndex &parent) const
|
||||||
{
|
{
|
||||||
// For list models only the root node (an invalid parent) should return the list's size. For all
|
// For list models only the root node (an invalid parent) should return the list's size. For all
|
||||||
@@ -60,7 +65,7 @@ int AppModel::rowCount(const QModelIndex &parent) const
|
|||||||
if (parent.isValid())
|
if (parent.isValid())
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
return m_Apps.count();
|
return m_VisibleApps.count();
|
||||||
}
|
}
|
||||||
|
|
||||||
QVariant AppModel::data(const QModelIndex &index, int role) const
|
QVariant AppModel::data(const QModelIndex &index, int role) const
|
||||||
@@ -68,8 +73,8 @@ QVariant AppModel::data(const QModelIndex &index, int role) const
|
|||||||
if (!index.isValid())
|
if (!index.isValid())
|
||||||
return QVariant();
|
return QVariant();
|
||||||
|
|
||||||
Q_ASSERT(index.row() < m_Apps.count());
|
Q_ASSERT(index.row() < m_VisibleApps.count());
|
||||||
NvApp app = m_Apps.at(index.row());
|
NvApp app = m_VisibleApps.at(index.row());
|
||||||
|
|
||||||
switch (role)
|
switch (role)
|
||||||
{
|
{
|
||||||
@@ -80,6 +85,14 @@ QVariant AppModel::data(const QModelIndex &index, int role) const
|
|||||||
case BoxArtRole:
|
case BoxArtRole:
|
||||||
// FIXME: const-correctness
|
// FIXME: const-correctness
|
||||||
return const_cast<BoxArtManager&>(m_BoxArtManager).loadBoxArt(m_Computer, app);
|
return const_cast<BoxArtManager&>(m_BoxArtManager).loadBoxArt(m_Computer, app);
|
||||||
|
case HiddenRole:
|
||||||
|
return app.hidden;
|
||||||
|
case AppIdRole:
|
||||||
|
return app.id;
|
||||||
|
case DirectLaunchRole:
|
||||||
|
return app.directLaunch;
|
||||||
|
case AppCollectorGameRole:
|
||||||
|
return app.isAppCollectorGame;
|
||||||
default:
|
default:
|
||||||
return QVariant();
|
return QVariant();
|
||||||
}
|
}
|
||||||
@@ -92,6 +105,10 @@ QHash<int, QByteArray> AppModel::roleNames() const
|
|||||||
names[NameRole] = "name";
|
names[NameRole] = "name";
|
||||||
names[RunningRole] = "running";
|
names[RunningRole] = "running";
|
||||||
names[BoxArtRole] = "boxart";
|
names[BoxArtRole] = "boxart";
|
||||||
|
names[HiddenRole] = "hidden";
|
||||||
|
names[AppIdRole] = "appid";
|
||||||
|
names[DirectLaunchRole] = "directLaunch";
|
||||||
|
names[AppCollectorGameRole] = "appCollectorGame";
|
||||||
|
|
||||||
return names;
|
return names;
|
||||||
}
|
}
|
||||||
@@ -101,6 +118,138 @@ void AppModel::quitRunningApp()
|
|||||||
m_ComputerManager->quitRunningApp(m_Computer);
|
m_ComputerManager->quitRunningApp(m_Computer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool AppModel::isAppCurrentlyVisible(const NvApp& app)
|
||||||
|
{
|
||||||
|
for (const NvApp& visibleApp : std::as_const(m_VisibleApps)) {
|
||||||
|
if (app.id == visibleApp.id) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QVector<NvApp> AppModel::getVisibleApps(const QVector<NvApp>& appList)
|
||||||
|
{
|
||||||
|
QVector<NvApp> visibleApps;
|
||||||
|
|
||||||
|
for (const NvApp& app : appList) {
|
||||||
|
// Don't immediately hide games that were previously visible. This
|
||||||
|
// allows users to easily uncheck the "Hide App" checkbox if they
|
||||||
|
// check it by mistake.
|
||||||
|
if (m_ShowHiddenGames || !app.hidden || isAppCurrentlyVisible(app)) {
|
||||||
|
visibleApps.append(app);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return visibleApps;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AppModel::updateAppList(QVector<NvApp> newList)
|
||||||
|
{
|
||||||
|
m_AllApps = newList;
|
||||||
|
|
||||||
|
QVector<NvApp> newVisibleList = getVisibleApps(newList);
|
||||||
|
|
||||||
|
// Process removals and updates first
|
||||||
|
for (int i = 0; i < m_VisibleApps.count(); i++) {
|
||||||
|
const NvApp& existingApp = m_VisibleApps.at(i);
|
||||||
|
|
||||||
|
bool found = false;
|
||||||
|
for (const NvApp& newApp : std::as_const(newVisibleList)) {
|
||||||
|
if (existingApp.id == newApp.id) {
|
||||||
|
// If the data changed, update it in our list
|
||||||
|
if (existingApp != newApp) {
|
||||||
|
m_VisibleApps.replace(i, newApp);
|
||||||
|
emit dataChanged(createIndex(i, 0), createIndex(i, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!found) {
|
||||||
|
beginRemoveRows(QModelIndex(), i, i);
|
||||||
|
m_VisibleApps.removeAt(i);
|
||||||
|
endRemoveRows();
|
||||||
|
i--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process additions now
|
||||||
|
for (const NvApp& newApp : std::as_const(newVisibleList)) {
|
||||||
|
int insertionIndex = m_VisibleApps.size();
|
||||||
|
bool found = false;
|
||||||
|
|
||||||
|
for (int i = 0; i < m_VisibleApps.count(); i++) {
|
||||||
|
const NvApp& existingApp = m_VisibleApps.at(i);
|
||||||
|
|
||||||
|
if (existingApp.id == newApp.id) {
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else if (existingApp.name.toLower() > newApp.name.toLower()) {
|
||||||
|
insertionIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!found) {
|
||||||
|
beginInsertRows(QModelIndex(), insertionIndex, insertionIndex);
|
||||||
|
m_VisibleApps.insert(insertionIndex, newApp);
|
||||||
|
endInsertRows();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Q_ASSERT(newVisibleList == m_VisibleApps);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AppModel::setAppHidden(int appIndex, bool hidden)
|
||||||
|
{
|
||||||
|
Q_ASSERT(appIndex < m_VisibleApps.count());
|
||||||
|
int appId = m_VisibleApps.at(appIndex).id;
|
||||||
|
|
||||||
|
{
|
||||||
|
QWriteLocker lock(&m_Computer->lock);
|
||||||
|
|
||||||
|
for (NvApp& app : m_Computer->appList) {
|
||||||
|
if (app.id == appId) {
|
||||||
|
app.hidden = hidden;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m_ComputerManager->clientSideAttributeUpdated(m_Computer);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AppModel::setAppDirectLaunch(int appIndex, bool directLaunch)
|
||||||
|
{
|
||||||
|
Q_ASSERT(appIndex < m_VisibleApps.count());
|
||||||
|
int appId = m_VisibleApps.at(appIndex).id;
|
||||||
|
|
||||||
|
{
|
||||||
|
QWriteLocker lock(&m_Computer->lock);
|
||||||
|
|
||||||
|
for (NvApp& app : m_Computer->appList) {
|
||||||
|
if (directLaunch) {
|
||||||
|
// We must clear direct launch from all other apps
|
||||||
|
// to set it on the new app.
|
||||||
|
app.directLaunch = app.id == appId;
|
||||||
|
}
|
||||||
|
else if (app.id == appId) {
|
||||||
|
// If we're clearing direct launch, we're done once we
|
||||||
|
// find our matching app ID.
|
||||||
|
app.directLaunch = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m_ComputerManager->clientSideAttributeUpdated(m_Computer);
|
||||||
|
}
|
||||||
|
|
||||||
void AppModel::handleComputerStateChanged(NvComputer* computer)
|
void AppModel::handleComputerStateChanged(NvComputer* computer)
|
||||||
{
|
{
|
||||||
// Ignore updates for computers that aren't ours
|
// Ignore updates for computers that aren't ours
|
||||||
@@ -119,20 +268,15 @@ void AppModel::handleComputerStateChanged(NvComputer* computer)
|
|||||||
// First, process additions/removals from the app list. This
|
// First, process additions/removals from the app list. This
|
||||||
// is required because the new game may now be running, so
|
// is required because the new game may now be running, so
|
||||||
// we can't check that first.
|
// we can't check that first.
|
||||||
if (computer->appList != m_Apps) {
|
if (computer->appList != m_AllApps) {
|
||||||
// Just reset the whole thing if the list changes
|
updateAppList(computer->appList);
|
||||||
beginResetModel();
|
|
||||||
m_Apps = computer->appList;
|
|
||||||
m_CurrentGameId = computer->currentGameId;
|
|
||||||
endResetModel();
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Finally, process changes to the active app
|
// Finally, process changes to the active app
|
||||||
if (computer->currentGameId != m_CurrentGameId) {
|
if (computer->currentGameId != m_CurrentGameId) {
|
||||||
// First, invalidate the running state of newly running game
|
// First, invalidate the running state of newly running game
|
||||||
for (int i = 0; i < m_Apps.count(); i++) {
|
for (int i = 0; i < m_VisibleApps.count(); i++) {
|
||||||
if (m_Apps[i].id == computer->currentGameId) {
|
if (m_VisibleApps[i].id == computer->currentGameId) {
|
||||||
emit dataChanged(createIndex(i, 0),
|
emit dataChanged(createIndex(i, 0),
|
||||||
createIndex(i, 0),
|
createIndex(i, 0),
|
||||||
QVector<int>() << RunningRole);
|
QVector<int>() << RunningRole);
|
||||||
@@ -142,8 +286,8 @@ void AppModel::handleComputerStateChanged(NvComputer* computer)
|
|||||||
|
|
||||||
// Next, invalidate the running state of the old game (if it exists)
|
// Next, invalidate the running state of the old game (if it exists)
|
||||||
if (m_CurrentGameId != 0) {
|
if (m_CurrentGameId != 0) {
|
||||||
for (int i = 0; i < m_Apps.count(); i++) {
|
for (int i = 0; i < m_VisibleApps.count(); i++) {
|
||||||
if (m_Apps[i].id == m_CurrentGameId) {
|
if (m_VisibleApps[i].id == m_CurrentGameId) {
|
||||||
emit dataChanged(createIndex(i, 0),
|
emit dataChanged(createIndex(i, 0),
|
||||||
createIndex(i, 0),
|
createIndex(i, 0),
|
||||||
QVector<int>() << RunningRole);
|
QVector<int>() << RunningRole);
|
||||||
@@ -161,7 +305,7 @@ void AppModel::handleBoxArtLoaded(NvComputer* computer, NvApp app, QUrl /* image
|
|||||||
{
|
{
|
||||||
Q_ASSERT(computer == m_Computer);
|
Q_ASSERT(computer == m_Computer);
|
||||||
|
|
||||||
int index = m_Apps.indexOf(app);
|
int index = m_VisibleApps.indexOf(app);
|
||||||
|
|
||||||
// Make sure we're not delivering a callback to an app that's already been removed
|
// Make sure we're not delivering a callback to an app that's already been removed
|
||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
|
|||||||
+21
-4
@@ -14,23 +14,33 @@ class AppModel : public QAbstractListModel
|
|||||||
{
|
{
|
||||||
NameRole = Qt::UserRole,
|
NameRole = Qt::UserRole,
|
||||||
RunningRole,
|
RunningRole,
|
||||||
BoxArtRole
|
BoxArtRole,
|
||||||
|
HiddenRole,
|
||||||
|
AppIdRole,
|
||||||
|
DirectLaunchRole,
|
||||||
|
AppCollectorGameRole,
|
||||||
};
|
};
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit AppModel(QObject *parent = nullptr);
|
explicit AppModel(QObject *parent = nullptr);
|
||||||
|
|
||||||
// Must be called before any QAbstractListModel functions
|
// Must be called before any QAbstractListModel functions
|
||||||
Q_INVOKABLE void initialize(ComputerManager* computerManager, int computerIndex);
|
Q_INVOKABLE void initialize(ComputerManager* computerManager, int computerIndex, bool showHiddenGames);
|
||||||
|
|
||||||
Q_INVOKABLE Session* createSessionForApp(int appIndex);
|
Q_INVOKABLE Session* createSessionForApp(int appIndex);
|
||||||
|
|
||||||
Q_INVOKABLE int getRunningAppIndex();
|
Q_INVOKABLE int getDirectLaunchAppIndex();
|
||||||
|
|
||||||
|
Q_INVOKABLE int getRunningAppId();
|
||||||
|
|
||||||
Q_INVOKABLE QString getRunningAppName();
|
Q_INVOKABLE QString getRunningAppName();
|
||||||
|
|
||||||
Q_INVOKABLE void quitRunningApp();
|
Q_INVOKABLE void quitRunningApp();
|
||||||
|
|
||||||
|
Q_INVOKABLE void setAppHidden(int appIndex, bool hidden);
|
||||||
|
|
||||||
|
Q_INVOKABLE void setAppDirectLaunch(int appIndex, bool directLaunch);
|
||||||
|
|
||||||
QVariant data(const QModelIndex &index, int role) const override;
|
QVariant data(const QModelIndex &index, int role) const override;
|
||||||
|
|
||||||
int rowCount(const QModelIndex &parent) const override;
|
int rowCount(const QModelIndex &parent) const override;
|
||||||
@@ -46,9 +56,16 @@ signals:
|
|||||||
void computerLost();
|
void computerLost();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
void updateAppList(QVector<NvApp> newList);
|
||||||
|
|
||||||
|
QVector<NvApp> getVisibleApps(const QVector<NvApp>& appList);
|
||||||
|
|
||||||
|
bool isAppCurrentlyVisible(const NvApp& app);
|
||||||
|
|
||||||
NvComputer* m_Computer;
|
NvComputer* m_Computer;
|
||||||
BoxArtManager m_BoxArtManager;
|
BoxArtManager m_BoxArtManager;
|
||||||
ComputerManager* m_ComputerManager;
|
ComputerManager* m_ComputerManager;
|
||||||
QVector<NvApp> m_Apps;
|
QVector<NvApp> m_VisibleApps, m_AllApps;
|
||||||
int m_CurrentGameId;
|
int m_CurrentGameId;
|
||||||
|
bool m_ShowHiddenGames;
|
||||||
};
|
};
|
||||||
|
|||||||
+96
-11
@@ -40,6 +40,48 @@ QVariant ComputerModel::data(const QModelIndex& index, int role) const
|
|||||||
return !computer->macAddress.isEmpty();
|
return !computer->macAddress.isEmpty();
|
||||||
case StatusUnknownRole:
|
case StatusUnknownRole:
|
||||||
return computer->state == NvComputer::CS_UNKNOWN;
|
return computer->state == NvComputer::CS_UNKNOWN;
|
||||||
|
case ServerSupportedRole:
|
||||||
|
return computer->isSupportedServerVersion;
|
||||||
|
case DetailsRole: {
|
||||||
|
QString state, pairState;
|
||||||
|
|
||||||
|
switch (computer->state) {
|
||||||
|
case NvComputer::CS_ONLINE:
|
||||||
|
state = tr("Online");
|
||||||
|
break;
|
||||||
|
case NvComputer::CS_OFFLINE:
|
||||||
|
state = tr("Offline");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
state = tr("Unknown");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (computer->pairState) {
|
||||||
|
case NvComputer::PS_PAIRED:
|
||||||
|
pairState = tr("Paired");
|
||||||
|
break;
|
||||||
|
case NvComputer::PS_NOT_PAIRED:
|
||||||
|
pairState = tr("Unpaired");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
pairState = tr("Unknown");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return tr("Name: %1").arg(computer->name) + '\n' +
|
||||||
|
tr("Status: %1").arg(state) + '\n' +
|
||||||
|
tr("Active Address: %1").arg(computer->activeAddress.toString()) + '\n' +
|
||||||
|
tr("UUID: %1").arg(computer->uuid) + '\n' +
|
||||||
|
tr("Local Address: %1").arg(computer->localAddress.toString()) + '\n' +
|
||||||
|
tr("Remote Address: %1").arg(computer->remoteAddress.toString()) + '\n' +
|
||||||
|
tr("IPv6 Address: %1").arg(computer->ipv6Address.toString()) + '\n' +
|
||||||
|
tr("Manual Address: %1").arg(computer->manualAddress.toString()) + '\n' +
|
||||||
|
tr("MAC Address: %1").arg(computer->macAddress.isEmpty() ? tr("Unknown") : QString(computer->macAddress.toHex(':'))) + '\n' +
|
||||||
|
tr("Pair State: %1").arg(pairState) + '\n' +
|
||||||
|
tr("Running Game ID: %1").arg(computer->state == NvComputer::CS_ONLINE ? QString::number(computer->currentGameId) : tr("Unknown")) + '\n' +
|
||||||
|
tr("HTTPS Port: %1").arg(computer->state == NvComputer::CS_ONLINE ? QString::number(computer->activeHttpsPort) : tr("Unknown"));
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return QVariant();
|
return QVariant();
|
||||||
}
|
}
|
||||||
@@ -66,6 +108,8 @@ QHash<int, QByteArray> ComputerModel::roleNames() const
|
|||||||
names[BusyRole] = "busy";
|
names[BusyRole] = "busy";
|
||||||
names[WakeableRole] = "wakeable";
|
names[WakeableRole] = "wakeable";
|
||||||
names[StatusUnknownRole] = "statusUnknown";
|
names[StatusUnknownRole] = "statusUnknown";
|
||||||
|
names[ServerSupportedRole] = "serverSupported";
|
||||||
|
names[DetailsRole] = "details";
|
||||||
|
|
||||||
return names;
|
return names;
|
||||||
}
|
}
|
||||||
@@ -128,6 +172,47 @@ void ComputerModel::wakeComputer(int computerIndex)
|
|||||||
QThreadPool::globalInstance()->start(wakeTask);
|
QThreadPool::globalInstance()->start(wakeTask);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ComputerModel::renameComputer(int computerIndex, QString name)
|
||||||
|
{
|
||||||
|
Q_ASSERT(computerIndex < m_Computers.count());
|
||||||
|
|
||||||
|
m_ComputerManager->renameHost(m_Computers[computerIndex], name);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ComputerModel::generatePinString()
|
||||||
|
{
|
||||||
|
return m_ComputerManager->generatePinString();
|
||||||
|
}
|
||||||
|
|
||||||
|
class DeferredTestConnectionTask : public QObject, public QRunnable
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
void run()
|
||||||
|
{
|
||||||
|
unsigned int portTestResult = LiTestClientConnectivity("qt.conntest.moonlight-stream.org", 443, ML_PORT_FLAG_ALL);
|
||||||
|
if (portTestResult == ML_TEST_RESULT_INCONCLUSIVE) {
|
||||||
|
emit connectionTestCompleted(-1, QString());
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
char blockedPorts[512];
|
||||||
|
LiStringifyPortFlags(portTestResult, "\n", blockedPorts, sizeof(blockedPorts));
|
||||||
|
emit connectionTestCompleted(portTestResult, QString(blockedPorts));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void connectionTestCompleted(int result, QString blockedPorts);
|
||||||
|
};
|
||||||
|
|
||||||
|
void ComputerModel::testConnectionForComputer(int)
|
||||||
|
{
|
||||||
|
DeferredTestConnectionTask* testConnectionTask = new DeferredTestConnectionTask();
|
||||||
|
QObject::connect(testConnectionTask, &DeferredTestConnectionTask::connectionTestCompleted,
|
||||||
|
this, &ComputerModel::connectionTestCompleted);
|
||||||
|
QThreadPool::globalInstance()->start(testConnectionTask);
|
||||||
|
}
|
||||||
|
|
||||||
void ComputerModel::pairComputer(int computerIndex, QString pin)
|
void ComputerModel::pairComputer(int computerIndex, QString pin)
|
||||||
{
|
{
|
||||||
Q_ASSERT(computerIndex < m_Computers.count());
|
Q_ASSERT(computerIndex < m_Computers.count());
|
||||||
@@ -142,19 +227,19 @@ void ComputerModel::handlePairingCompleted(NvComputer*, QString error)
|
|||||||
|
|
||||||
void ComputerModel::handleComputerStateChanged(NvComputer* computer)
|
void ComputerModel::handleComputerStateChanged(NvComputer* computer)
|
||||||
{
|
{
|
||||||
// If this is an existing computer, we can report the data changed
|
QVector<NvComputer*> newComputerList = m_ComputerManager->getComputers();
|
||||||
int index = m_Computers.indexOf(computer);
|
|
||||||
if (index >= 0) {
|
// Reset the model if the structural layout of the list has changed
|
||||||
// Let the view know that this specific computer changed
|
if (m_Computers != newComputerList) {
|
||||||
emit dataChanged(createIndex(index, 0), createIndex(index, 0));
|
beginResetModel();
|
||||||
|
m_Computers = newComputerList;
|
||||||
|
endResetModel();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// This is a new PC which may be inserted at an arbitrary point
|
// Let the view know that this specific computer changed
|
||||||
// in our computer list (since it comes from CM's QMap). Reload
|
int index = m_Computers.indexOf(computer);
|
||||||
// the whole model state to ensure it stays consistent.
|
emit dataChanged(createIndex(index, 0), createIndex(index, 0));
|
||||||
beginResetModel();
|
|
||||||
m_Computers = m_ComputerManager->getComputers();
|
|
||||||
endResetModel();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#include "computermodel.moc"
|
||||||
|
|||||||
+10
-1
@@ -14,7 +14,9 @@ class ComputerModel : public QAbstractListModel
|
|||||||
PairedRole,
|
PairedRole,
|
||||||
BusyRole,
|
BusyRole,
|
||||||
WakeableRole,
|
WakeableRole,
|
||||||
StatusUnknownRole
|
StatusUnknownRole,
|
||||||
|
ServerSupportedRole,
|
||||||
|
DetailsRole
|
||||||
};
|
};
|
||||||
|
|
||||||
public:
|
public:
|
||||||
@@ -31,14 +33,21 @@ public:
|
|||||||
|
|
||||||
Q_INVOKABLE void deleteComputer(int computerIndex);
|
Q_INVOKABLE void deleteComputer(int computerIndex);
|
||||||
|
|
||||||
|
Q_INVOKABLE QString generatePinString();
|
||||||
|
|
||||||
Q_INVOKABLE void pairComputer(int computerIndex, QString pin);
|
Q_INVOKABLE void pairComputer(int computerIndex, QString pin);
|
||||||
|
|
||||||
|
Q_INVOKABLE void testConnectionForComputer(int computerIndex);
|
||||||
|
|
||||||
Q_INVOKABLE void wakeComputer(int computerIndex);
|
Q_INVOKABLE void wakeComputer(int computerIndex);
|
||||||
|
|
||||||
|
Q_INVOKABLE void renameComputer(int computerIndex, QString name);
|
||||||
|
|
||||||
Q_INVOKABLE Session* createSessionForCurrentGame(int computerIndex);
|
Q_INVOKABLE Session* createSessionForCurrentGame(int computerIndex);
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void pairingCompleted(QVariant error);
|
void pairingCompleted(QVariant error);
|
||||||
|
void connectionTestCompleted(int result, QString blockedPorts);
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void handleComputerStateChanged(NvComputer* computer);
|
void handleComputerStateChanged(NvComputer* computer);
|
||||||
|
|||||||
+185
-67
@@ -2,6 +2,7 @@ import QtQuick 2.9
|
|||||||
import QtQuick.Controls 2.2
|
import QtQuick.Controls 2.2
|
||||||
import QtQuick.Layouts 1.3
|
import QtQuick.Layouts 1.3
|
||||||
import QtQuick.Window 2.2
|
import QtQuick.Window 2.2
|
||||||
|
import QtQuick.Controls.Material 2.2
|
||||||
|
|
||||||
import ComputerManager 1.0
|
import ComputerManager 1.0
|
||||||
import AutoUpdateChecker 1.0
|
import AutoUpdateChecker 1.0
|
||||||
@@ -12,19 +13,111 @@ import SdlGamepadKeyNavigation 1.0
|
|||||||
ApplicationWindow {
|
ApplicationWindow {
|
||||||
property bool pollingActive: false
|
property bool pollingActive: false
|
||||||
|
|
||||||
|
// Set by SettingsView to force the back operation to pop all
|
||||||
|
// pages except the initial view. This is required when doing
|
||||||
|
// a retranslate() because AppView breaks for some reason.
|
||||||
|
property bool clearOnBack: false
|
||||||
|
|
||||||
id: window
|
id: window
|
||||||
visible: true
|
|
||||||
width: 1280
|
width: 1280
|
||||||
height: 600
|
height: 600
|
||||||
|
|
||||||
visibility: StreamingPreferences.startWindowed ? "Windowed" : "Maximized"
|
// This function runs prior to creation of the initial StackView item
|
||||||
|
function doEarlyInit() {
|
||||||
|
// Override the background color to Material 2 colors for Qt 6.5+
|
||||||
|
// in order to improve contrast between GFE's placeholder box art
|
||||||
|
// and the background of the app grid.
|
||||||
|
if (SystemProperties.usesMaterial3Theme) {
|
||||||
|
Material.background = "#303030"
|
||||||
|
}
|
||||||
|
|
||||||
|
SdlGamepadKeyNavigation.enable()
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
// Show the window according to the user's preferences
|
||||||
|
if (SystemProperties.hasDesktopEnvironment) {
|
||||||
|
if (StreamingPreferences.uiDisplayMode == StreamingPreferences.UI_MAXIMIZED) {
|
||||||
|
window.showMaximized()
|
||||||
|
}
|
||||||
|
else if (StreamingPreferences.uiDisplayMode == StreamingPreferences.UI_FULLSCREEN) {
|
||||||
|
window.showFullScreen()
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
window.show()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
window.showFullScreen()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display any modal dialogs for configuration warnings
|
||||||
|
if (runConfigChecks) {
|
||||||
|
if (SystemProperties.isWow64) {
|
||||||
|
wow64Dialog.open()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hardware acceleration and unmapped gamepads are checked asynchronously
|
||||||
|
SystemProperties.hasHardwareAccelerationChanged.connect(hasHardwareAccelerationChanged)
|
||||||
|
SystemProperties.unmappedGamepadsChanged.connect(hasUnmappedGamepadsChanged)
|
||||||
|
SystemProperties.startAsyncLoad()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasHardwareAccelerationChanged() {
|
||||||
|
if (!SystemProperties.hasHardwareAcceleration && StreamingPreferences.videoDecoderSelection !== StreamingPreferences.VDS_FORCE_SOFTWARE) {
|
||||||
|
if (SystemProperties.isRunningXWayland) {
|
||||||
|
xWaylandDialog.open()
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
noHwDecoderDialog.open()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasUnmappedGamepadsChanged() {
|
||||||
|
if (SystemProperties.unmappedGamepads) {
|
||||||
|
unmappedGamepadDialog.unmappedGamepads = SystemProperties.unmappedGamepads
|
||||||
|
unmappedGamepadDialog.open()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// It would be better to use TextMetrics here, but it always lays out
|
||||||
|
// the text slightly more compactly than real Text does in ToolTip,
|
||||||
|
// causing unexpected line breaks to be inserted
|
||||||
|
Text {
|
||||||
|
id: tooltipTextLayoutHelper
|
||||||
|
visible: false
|
||||||
|
font: ToolTip.toolTip.font
|
||||||
|
text: ToolTip.toolTip.text
|
||||||
|
}
|
||||||
|
|
||||||
|
// This configures the maximum width of the singleton attached QML ToolTip. If left unconstrained,
|
||||||
|
// it will never insert a line break and just extend on forever.
|
||||||
|
ToolTip.toolTip.contentWidth: Math.min(tooltipTextLayoutHelper.width, 400)
|
||||||
|
|
||||||
|
function goBack() {
|
||||||
|
if (clearOnBack) {
|
||||||
|
// Pop all items except the first one
|
||||||
|
stackView.pop(null)
|
||||||
|
clearOnBack = false
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
stackView.pop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
StackView {
|
StackView {
|
||||||
id: stackView
|
id: stackView
|
||||||
initialItem: initialView
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
focus: true
|
focus: true
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
// Perform our early initialization before constructing
|
||||||
|
// the initial view and pushing it to the StackView
|
||||||
|
doEarlyInit()
|
||||||
|
push(initialView)
|
||||||
|
}
|
||||||
|
|
||||||
onCurrentItemChanged: {
|
onCurrentItemChanged: {
|
||||||
// Ensure focus travels to the next view when going back
|
// Ensure focus travels to the next view when going back
|
||||||
if (currentItem) {
|
if (currentItem) {
|
||||||
@@ -34,7 +127,7 @@ ApplicationWindow {
|
|||||||
|
|
||||||
Keys.onEscapePressed: {
|
Keys.onEscapePressed: {
|
||||||
if (depth > 1) {
|
if (depth > 1) {
|
||||||
stackView.pop()
|
goBack()
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
quitConfirmationDialog.open()
|
quitConfirmationDialog.open()
|
||||||
@@ -43,7 +136,7 @@ ApplicationWindow {
|
|||||||
|
|
||||||
Keys.onBackPressed: {
|
Keys.onBackPressed: {
|
||||||
if (depth > 1) {
|
if (depth > 1) {
|
||||||
stackView.pop()
|
goBack()
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
quitConfirmationDialog.open()
|
quitConfirmationDialog.open()
|
||||||
@@ -98,6 +191,9 @@ ApplicationWindow {
|
|||||||
pollingActive = true
|
pollingActive = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Poll for gamepad input only when the window is in focus
|
||||||
|
SdlGamepadKeyNavigation.notifyWindowFocus(visible && active)
|
||||||
}
|
}
|
||||||
|
|
||||||
onActiveChanged: {
|
onActiveChanged: {
|
||||||
@@ -116,47 +212,15 @@ ApplicationWindow {
|
|||||||
// if focus does not return within a few minutes.
|
// if focus does not return within a few minutes.
|
||||||
inactivityTimer.restart()
|
inactivityTimer.restart()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Poll for gamepad input only when the window is in focus
|
||||||
|
SdlGamepadKeyNavigation.notifyWindowFocus(visible && active)
|
||||||
}
|
}
|
||||||
|
|
||||||
property bool initialized: false
|
function navigateTo(url, objectType)
|
||||||
|
|
||||||
// BUG: Using onAfterSynchronizing: here causes very strange
|
|
||||||
// failures on Linux. Many shaders fail to compile and we
|
|
||||||
// eventually segfault deep inside the Qt OpenGL code.
|
|
||||||
onAfterRendering: {
|
|
||||||
// We use this callback to trigger dialog display because
|
|
||||||
// it only happens once the window is fully constructed.
|
|
||||||
// Doing it earlier can lead to the dialog appearing behind
|
|
||||||
// the window or otherwise without input focus.
|
|
||||||
if (!initialized) {
|
|
||||||
// Set initialized before calling anything else, because
|
|
||||||
// pumping the event loop can cause us to get another
|
|
||||||
// onAfterRendering call and potentially reenter this code.
|
|
||||||
initialized = true;
|
|
||||||
|
|
||||||
if (SystemProperties.isWow64) {
|
|
||||||
wow64Dialog.open()
|
|
||||||
}
|
|
||||||
else if (!SystemProperties.hasHardwareAcceleration) {
|
|
||||||
if (SystemProperties.isRunningXWayland) {
|
|
||||||
xWaylandDialog.open()
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
noHwDecoderDialog.open()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (SystemProperties.unmappedGamepads) {
|
|
||||||
unmappedGamepadDialog.unmappedGamepads = SystemProperties.unmappedGamepads
|
|
||||||
unmappedGamepadDialog.open()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function navigateTo(url, objectName)
|
|
||||||
{
|
{
|
||||||
var existingItem = stackView.find(function(item, index) {
|
var existingItem = stackView.find(function(item, index) {
|
||||||
return item.objectName === objectName
|
return item instanceof objectType
|
||||||
})
|
})
|
||||||
|
|
||||||
if (existingItem !== null) {
|
if (existingItem !== null) {
|
||||||
@@ -175,8 +239,19 @@ ApplicationWindow {
|
|||||||
anchors.topMargin: 5
|
anchors.topMargin: 5
|
||||||
anchors.bottomMargin: 5
|
anchors.bottomMargin: 5
|
||||||
|
|
||||||
|
Label {
|
||||||
|
id: titleLabel
|
||||||
|
visible: toolBar.width > 700
|
||||||
|
anchors.fill: parent
|
||||||
|
text: stackView.currentItem.objectName
|
||||||
|
font.pointSize: 20
|
||||||
|
elide: Label.ElideRight
|
||||||
|
horizontalAlignment: Qt.AlignHCenter
|
||||||
|
verticalAlignment: Qt.AlignVCenter
|
||||||
|
}
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
spacing: 20
|
spacing: 10
|
||||||
anchors.leftMargin: 10
|
anchors.leftMargin: 10
|
||||||
anchors.rightMargin: 10
|
anchors.rightMargin: 10
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
@@ -187,33 +262,68 @@ ApplicationWindow {
|
|||||||
|
|
||||||
iconSource: "qrc:/res/arrow_left.svg"
|
iconSource: "qrc:/res/arrow_left.svg"
|
||||||
|
|
||||||
onClicked: stackView.pop()
|
onClicked: goBack()
|
||||||
|
|
||||||
Keys.onDownPressed: {
|
Keys.onDownPressed: {
|
||||||
stackView.currentItem.forceActiveFocus(Qt.TabFocus)
|
stackView.currentItem.forceActiveFocus(Qt.TabFocus)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This label will appear when the window gets too small and
|
||||||
|
// we need to ensure the toolbar controls don't collide
|
||||||
Label {
|
Label {
|
||||||
id: titleLabel
|
id: titleRowLabel
|
||||||
text: stackView.currentItem.objectName
|
font.pointSize: titleLabel.font.pointSize
|
||||||
font.pointSize: 15
|
|
||||||
elide: Label.ElideRight
|
elide: Label.ElideRight
|
||||||
horizontalAlignment: Qt.AlignHCenter
|
horizontalAlignment: Qt.AlignHCenter
|
||||||
verticalAlignment: Qt.AlignVCenter
|
verticalAlignment: Qt.AlignVCenter
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
// We need this label to always be visible so it can occupy
|
||||||
|
// the remaining space in the RowLayout. To "hide" it, we
|
||||||
|
// just set the text to empty string.
|
||||||
|
text: !titleLabel.visible ? stackView.currentItem.objectName : ""
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
id: versionLabel
|
||||||
|
visible: stackView.currentItem instanceof SettingsView
|
||||||
|
text: qsTr("Version %1").arg(SystemProperties.versionString)
|
||||||
|
font.pointSize: 12
|
||||||
|
horizontalAlignment: Qt.AlignRight
|
||||||
|
verticalAlignment: Qt.AlignVCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
NavigableToolButton {
|
||||||
|
id: discordButton
|
||||||
|
visible: SystemProperties.hasBrowser &&
|
||||||
|
stackView.currentItem instanceof SettingsView
|
||||||
|
|
||||||
|
iconSource: "qrc:/res/discord.svg"
|
||||||
|
|
||||||
|
ToolTip.delay: 1000
|
||||||
|
ToolTip.timeout: 3000
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
ToolTip.text: qsTr("Join our community on Discord")
|
||||||
|
|
||||||
|
// TODO need to make sure browser is brought to foreground.
|
||||||
|
onClicked: Qt.openUrlExternally("https://moonlight-stream.org/discord");
|
||||||
|
|
||||||
|
Keys.onDownPressed: {
|
||||||
|
stackView.currentItem.forceActiveFocus(Qt.TabFocus)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
NavigableToolButton {
|
NavigableToolButton {
|
||||||
id: addPcButton
|
id: addPcButton
|
||||||
visible: stackView.currentItem.objectName === "Computers"
|
visible: stackView.currentItem instanceof PcView
|
||||||
|
|
||||||
iconSource: "qrc:/res/ic_add_to_queue_white_48px.svg"
|
iconSource: "qrc:/res/ic_add_to_queue_white_48px.svg"
|
||||||
|
|
||||||
ToolTip.delay: 1000
|
ToolTip.delay: 1000
|
||||||
ToolTip.timeout: 3000
|
ToolTip.timeout: 3000
|
||||||
ToolTip.visible: hovered
|
ToolTip.visible: hovered
|
||||||
ToolTip.text: "Add PC manually" + (newPcShortcut.nativeText ? (" ("+newPcShortcut.nativeText+")") : "")
|
ToolTip.text: qsTr("Add PC manually") + (newPcShortcut.nativeText ? (" ("+newPcShortcut.nativeText+")") : "")
|
||||||
|
|
||||||
Shortcut {
|
Shortcut {
|
||||||
id: newPcShortcut
|
id: newPcShortcut
|
||||||
@@ -253,7 +363,7 @@ ApplicationWindow {
|
|||||||
|
|
||||||
function updateAvailable(version, url)
|
function updateAvailable(version, url)
|
||||||
{
|
{
|
||||||
ToolTip.text = "Update available for Moonlight: Version " + version
|
ToolTip.text = qsTr("Update available for Moonlight: Version %1").arg(version)
|
||||||
updateButton.browserUrl = url
|
updateButton.browserUrl = url
|
||||||
updateButton.visible = true
|
updateButton.visible = true
|
||||||
}
|
}
|
||||||
@@ -277,7 +387,7 @@ ApplicationWindow {
|
|||||||
ToolTip.delay: 1000
|
ToolTip.delay: 1000
|
||||||
ToolTip.timeout: 3000
|
ToolTip.timeout: 3000
|
||||||
ToolTip.visible: hovered
|
ToolTip.visible: hovered
|
||||||
ToolTip.text: "Help" + (helpShortcut.nativeText ? (" ("+helpShortcut.nativeText+")") : "")
|
ToolTip.text: qsTr("Help") + (helpShortcut.nativeText ? (" ("+helpShortcut.nativeText+")") : "")
|
||||||
|
|
||||||
Shortcut {
|
Shortcut {
|
||||||
id: helpShortcut
|
id: helpShortcut
|
||||||
@@ -300,11 +410,11 @@ ApplicationWindow {
|
|||||||
ToolTip.delay: 1000
|
ToolTip.delay: 1000
|
||||||
ToolTip.timeout: 3000
|
ToolTip.timeout: 3000
|
||||||
ToolTip.visible: hovered
|
ToolTip.visible: hovered
|
||||||
ToolTip.text: "Gamepad Mapper"
|
ToolTip.text: qsTr("Gamepad Mapper")
|
||||||
|
|
||||||
iconSource: "qrc:/res/ic_videogame_asset_white_48px.svg"
|
iconSource: "qrc:/res/ic_videogame_asset_white_48px.svg"
|
||||||
|
|
||||||
onClicked: navigateTo("qrc:/gui/GamepadMapper.qml", "Gamepad Mapping")
|
onClicked: navigateTo("qrc:/gui/GamepadMapper.qml", GamepadMapper)
|
||||||
|
|
||||||
Keys.onDownPressed: {
|
Keys.onDownPressed: {
|
||||||
stackView.currentItem.forceActiveFocus(Qt.TabFocus)
|
stackView.currentItem.forceActiveFocus(Qt.TabFocus)
|
||||||
@@ -316,7 +426,7 @@ ApplicationWindow {
|
|||||||
|
|
||||||
iconSource: "qrc:/res/settings.svg"
|
iconSource: "qrc:/res/settings.svg"
|
||||||
|
|
||||||
onClicked: navigateTo("qrc:/gui/SettingsView.qml", "Settings")
|
onClicked: navigateTo("qrc:/gui/SettingsView.qml", SettingsView)
|
||||||
|
|
||||||
Keys.onDownPressed: {
|
Keys.onDownPressed: {
|
||||||
stackView.currentItem.forceActiveFocus(Qt.TabFocus)
|
stackView.currentItem.forceActiveFocus(Qt.TabFocus)
|
||||||
@@ -331,31 +441,31 @@ ApplicationWindow {
|
|||||||
ToolTip.delay: 1000
|
ToolTip.delay: 1000
|
||||||
ToolTip.timeout: 3000
|
ToolTip.timeout: 3000
|
||||||
ToolTip.visible: hovered
|
ToolTip.visible: hovered
|
||||||
ToolTip.text: "Settings" + (settingsShortcut.nativeText ? (" ("+settingsShortcut.nativeText+")") : "")
|
ToolTip.text: qsTr("Settings") + (settingsShortcut.nativeText ? (" ("+settingsShortcut.nativeText+")") : "")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ErrorMessageDialog {
|
ErrorMessageDialog {
|
||||||
id: noHwDecoderDialog
|
id: noHwDecoderDialog
|
||||||
text: "No functioning hardware accelerated H.264 video decoder was detected by Moonlight. " +
|
text: qsTr("No functioning hardware accelerated video decoder was detected by Moonlight. " +
|
||||||
"Your streaming performance may be severely degraded in this configuration."
|
"Your streaming performance may be severely degraded in this configuration.")
|
||||||
helpText: "Click the Help button for more information on solving this problem."
|
helpText: qsTr("Click the Help button for more information on solving this problem.")
|
||||||
helpUrl: "https://github.com/moonlight-stream/moonlight-docs/wiki/Fixing-Hardware-Decoding-Problems"
|
helpUrl: "https://github.com/moonlight-stream/moonlight-docs/wiki/Fixing-Hardware-Decoding-Problems"
|
||||||
}
|
}
|
||||||
|
|
||||||
ErrorMessageDialog {
|
ErrorMessageDialog {
|
||||||
id: xWaylandDialog
|
id: xWaylandDialog
|
||||||
text: "Hardware acceleration doesn't work on XWayland. Continuing on XWayland may result in poor streaming performance. " +
|
text: qsTr("Hardware acceleration doesn't work on XWayland. Continuing on XWayland may result in poor streaming performance. " +
|
||||||
"Try running with QT_QPA_PLATFORM=wayland or switch to X11."
|
"Try running with QT_QPA_PLATFORM=wayland or switch to X11.")
|
||||||
helpText: "Click the Help button for more information."
|
helpText: qsTr("Click the Help button for more information.")
|
||||||
helpUrl: "https://github.com/moonlight-stream/moonlight-docs/wiki/Fixing-Hardware-Decoding-Problems"
|
helpUrl: "https://github.com/moonlight-stream/moonlight-docs/wiki/Fixing-Hardware-Decoding-Problems"
|
||||||
}
|
}
|
||||||
|
|
||||||
NavigableMessageDialog {
|
NavigableMessageDialog {
|
||||||
id: wow64Dialog
|
id: wow64Dialog
|
||||||
standardButtons: Dialog.Ok | Dialog.Cancel
|
standardButtons: Dialog.Ok | Dialog.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: qsTr("This version of Moonlight isn't optimized for your PC. Please download the '%1' version of Moonlight for the best streaming performance.").arg(SystemProperties.friendlyNativeArchName)
|
||||||
onAccepted: {
|
onAccepted: {
|
||||||
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-qt/releases");
|
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-qt/releases");
|
||||||
}
|
}
|
||||||
@@ -364,9 +474,9 @@ ApplicationWindow {
|
|||||||
ErrorMessageDialog {
|
ErrorMessageDialog {
|
||||||
id: unmappedGamepadDialog
|
id: unmappedGamepadDialog
|
||||||
property string unmappedGamepads : ""
|
property string unmappedGamepads : ""
|
||||||
text: "Moonlight detected gamepads without a mapping:\n" + unmappedGamepads
|
text: qsTr("Moonlight detected gamepads without a mapping:") + "\n" + unmappedGamepads
|
||||||
helpTextSeparator: "\n\n"
|
helpTextSeparator: "\n\n"
|
||||||
helpText: "Click the Help button for information on how to map your gamepads."
|
helpText: qsTr("Click the Help button for information on how to map your gamepads.")
|
||||||
helpUrl: "https://github.com/moonlight-stream/moonlight-docs/wiki/Gamepad-Mapping"
|
helpUrl: "https://github.com/moonlight-stream/moonlight-docs/wiki/Gamepad-Mapping"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -374,7 +484,7 @@ ApplicationWindow {
|
|||||||
NavigableMessageDialog {
|
NavigableMessageDialog {
|
||||||
id: quitConfirmationDialog
|
id: quitConfirmationDialog
|
||||||
standardButtons: Dialog.Yes | Dialog.No
|
standardButtons: Dialog.Yes | Dialog.No
|
||||||
text: "Are you sure you want to quit?"
|
text: qsTr("Are you sure you want to quit?")
|
||||||
// For keyboard/gamepad navigation
|
// For keyboard/gamepad navigation
|
||||||
onAccepted: Qt.quit()
|
onAccepted: Qt.quit()
|
||||||
}
|
}
|
||||||
@@ -404,7 +514,7 @@ ApplicationWindow {
|
|||||||
|
|
||||||
NavigableDialog {
|
NavigableDialog {
|
||||||
id: addPcDialog
|
id: addPcDialog
|
||||||
property string label: "Enter the IP address of your GameStream PC:"
|
property string label: qsTr("Enter the IP address of your host PC:")
|
||||||
|
|
||||||
standardButtons: Dialog.Ok | Dialog.Cancel
|
standardButtons: Dialog.Ok | Dialog.Cancel
|
||||||
|
|
||||||
@@ -413,9 +523,13 @@ ApplicationWindow {
|
|||||||
editText.forceActiveFocus()
|
editText.forceActiveFocus()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onClosed: {
|
||||||
|
editText.clear()
|
||||||
|
}
|
||||||
|
|
||||||
onAccepted: {
|
onAccepted: {
|
||||||
if (editText.text) {
|
if (editText.text) {
|
||||||
ComputerManager.addNewHost(editText.text, false)
|
ComputerManager.addNewHostManually(editText.text.trim())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -433,6 +547,10 @@ ApplicationWindow {
|
|||||||
Keys.onReturnPressed: {
|
Keys.onReturnPressed: {
|
||||||
addPcDialog.accept()
|
addPcDialog.accept()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Keys.onEnterPressed: {
|
||||||
|
addPcDialog.accept()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,13 +8,16 @@
|
|||||||
|
|
||||||
#define AXIS_NAVIGATION_REPEAT_DELAY 150
|
#define AXIS_NAVIGATION_REPEAT_DELAY 150
|
||||||
|
|
||||||
SdlGamepadKeyNavigation::SdlGamepadKeyNavigation()
|
SdlGamepadKeyNavigation::SdlGamepadKeyNavigation(StreamingPreferences* prefs)
|
||||||
: m_Enabled(false),
|
: m_Prefs(prefs),
|
||||||
|
m_Enabled(false),
|
||||||
m_UiNavMode(false),
|
m_UiNavMode(false),
|
||||||
|
m_FirstPoll(false),
|
||||||
|
m_HasFocus(false),
|
||||||
m_LastAxisNavigationEventTime(0)
|
m_LastAxisNavigationEventTime(0)
|
||||||
{
|
{
|
||||||
m_PollingTimer = new QTimer(this);
|
m_PollingTimer = new QTimer(this);
|
||||||
connect(m_PollingTimer, SIGNAL(timeout()), this, SLOT(onPollingTimerFired()));
|
connect(m_PollingTimer, &QTimer::timeout, this, &SdlGamepadKeyNavigation::onPollingTimerFired);
|
||||||
}
|
}
|
||||||
|
|
||||||
SdlGamepadKeyNavigation::~SdlGamepadKeyNavigation()
|
SdlGamepadKeyNavigation::~SdlGamepadKeyNavigation()
|
||||||
@@ -48,11 +51,16 @@ void SdlGamepadKeyNavigation::enable()
|
|||||||
// on first init of the GC subsystem. We can't depend on them due to
|
// on first init of the GC subsystem. We can't depend on them due to
|
||||||
// overlapping lifetimes of SdlGamepadKeyNavigation instances, so we
|
// overlapping lifetimes of SdlGamepadKeyNavigation instances, so we
|
||||||
// will attach ourselves.
|
// will attach ourselves.
|
||||||
SDL_PumpEvents();
|
//
|
||||||
|
// NB: We use SDL_JoystickUpdate() instead of SDL_PumpEvents() because
|
||||||
|
// the latter can do a bit more work that we want (like handling video
|
||||||
|
// events that we intentionally do not want to process yet).
|
||||||
|
SDL_JoystickUpdate();
|
||||||
SDL_FlushEvent(SDL_CONTROLLERDEVICEADDED);
|
SDL_FlushEvent(SDL_CONTROLLERDEVICEADDED);
|
||||||
|
|
||||||
// Open all currently attached game controllers
|
// Open all currently attached game controllers
|
||||||
for (int i = 0; i < SDL_NumJoysticks(); i++) {
|
int numJoysticks = SDL_NumJoysticks();
|
||||||
|
for (int i = 0; i < numJoysticks; i++) {
|
||||||
if (SDL_IsGameController(i)) {
|
if (SDL_IsGameController(i)) {
|
||||||
SDL_GameController* gc = SDL_GameControllerOpen(i);
|
SDL_GameController* gc = SDL_GameControllerOpen(i);
|
||||||
if (gc != nullptr) {
|
if (gc != nullptr) {
|
||||||
@@ -61,10 +69,10 @@ void SdlGamepadKeyNavigation::enable()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Poll every 50 ms for a new joystick event
|
|
||||||
m_PollingTimer->start(50);
|
|
||||||
|
|
||||||
m_Enabled = true;
|
m_Enabled = true;
|
||||||
|
|
||||||
|
// Start the polling timer if the window is focused
|
||||||
|
updateTimerState();
|
||||||
}
|
}
|
||||||
|
|
||||||
void SdlGamepadKeyNavigation::disable()
|
void SdlGamepadKeyNavigation::disable()
|
||||||
@@ -73,7 +81,9 @@ void SdlGamepadKeyNavigation::disable()
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
m_PollingTimer->stop();
|
m_Enabled = false;
|
||||||
|
updateTimerState();
|
||||||
|
Q_ASSERT(!m_PollingTimer->isActive());
|
||||||
|
|
||||||
while (!m_Gamepads.isEmpty()) {
|
while (!m_Gamepads.isEmpty()) {
|
||||||
SDL_GameControllerClose(m_Gamepads[0]);
|
SDL_GameControllerClose(m_Gamepads[0]);
|
||||||
@@ -81,15 +91,31 @@ void SdlGamepadKeyNavigation::disable()
|
|||||||
}
|
}
|
||||||
|
|
||||||
SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER);
|
SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER);
|
||||||
|
}
|
||||||
|
|
||||||
m_Enabled = false;
|
void SdlGamepadKeyNavigation::notifyWindowFocus(bool hasFocus)
|
||||||
|
{
|
||||||
|
m_HasFocus = hasFocus;
|
||||||
|
updateTimerState();
|
||||||
}
|
}
|
||||||
|
|
||||||
void SdlGamepadKeyNavigation::onPollingTimerFired()
|
void SdlGamepadKeyNavigation::onPollingTimerFired()
|
||||||
{
|
{
|
||||||
SDL_Event event;
|
SDL_Event event;
|
||||||
|
|
||||||
while (SDL_PollEvent(&event)) {
|
// Update joystick state without pumping other events (see enable() comment)
|
||||||
|
SDL_JoystickUpdate();
|
||||||
|
|
||||||
|
// Discard any pending button events on the first poll to avoid picking up
|
||||||
|
// stale input data from the stream session (like the quit combo).
|
||||||
|
if (m_FirstPoll) {
|
||||||
|
SDL_FlushEvent(SDL_CONTROLLERBUTTONDOWN);
|
||||||
|
SDL_FlushEvent(SDL_CONTROLLERBUTTONUP);
|
||||||
|
m_FirstPoll = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Peep events rather than polling to avoid calling SDL_PumpEvents()
|
||||||
|
while (SDL_PeepEvents(&event, 1, SDL_GETEVENT, SDL_FIRSTEVENT, SDL_LASTEVENT) == 1) {
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case SDL_QUIT:
|
case SDL_QUIT:
|
||||||
// SDL may send us a quit event since we initialize
|
// SDL may send us a quit event since we initialize
|
||||||
@@ -104,6 +130,24 @@ void SdlGamepadKeyNavigation::onPollingTimerFired()
|
|||||||
event.type == SDL_CONTROLLERBUTTONDOWN ?
|
event.type == SDL_CONTROLLERBUTTONDOWN ?
|
||||||
QEvent::Type::KeyPress : QEvent::Type::KeyRelease;
|
QEvent::Type::KeyPress : QEvent::Type::KeyRelease;
|
||||||
|
|
||||||
|
// Swap face buttons if needed
|
||||||
|
if (m_Prefs->swapFaceButtons) {
|
||||||
|
switch (event.cbutton.button) {
|
||||||
|
case SDL_CONTROLLER_BUTTON_A:
|
||||||
|
event.cbutton.button = SDL_CONTROLLER_BUTTON_B;
|
||||||
|
break;
|
||||||
|
case SDL_CONTROLLER_BUTTON_B:
|
||||||
|
event.cbutton.button = SDL_CONTROLLER_BUTTON_A;
|
||||||
|
break;
|
||||||
|
case SDL_CONTROLLER_BUTTON_X:
|
||||||
|
event.cbutton.button = SDL_CONTROLLER_BUTTON_Y;
|
||||||
|
break;
|
||||||
|
case SDL_CONTROLLER_BUTTON_Y:
|
||||||
|
event.cbutton.button = SDL_CONTROLLER_BUTTON_X;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
switch (event.cbutton.button) {
|
switch (event.cbutton.button) {
|
||||||
case SDL_CONTROLLER_BUTTON_DPAD_UP:
|
case SDL_CONTROLLER_BUTTON_DPAD_UP:
|
||||||
if (m_UiNavMode) {
|
if (m_UiNavMode) {
|
||||||
@@ -157,14 +201,24 @@ void SdlGamepadKeyNavigation::onPollingTimerFired()
|
|||||||
case SDL_CONTROLLERDEVICEADDED:
|
case SDL_CONTROLLERDEVICEADDED:
|
||||||
SDL_GameController* gc = SDL_GameControllerOpen(event.cdevice.which);
|
SDL_GameController* gc = SDL_GameControllerOpen(event.cdevice.which);
|
||||||
if (gc != nullptr) {
|
if (gc != nullptr) {
|
||||||
m_Gamepads.append(gc);
|
// SDL_CONTROLLERDEVICEADDED can be reported multiple times for the same
|
||||||
|
// gamepad in rare cases, because SDL doesn't fixup the device index in
|
||||||
|
// the SDL_CONTROLLERDEVICEADDED event if an unopened gamepad disappears
|
||||||
|
// before we've processed the add event.
|
||||||
|
if (!m_Gamepads.contains(gc)) {
|
||||||
|
m_Gamepads.append(gc);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// We already have this game controller open
|
||||||
|
SDL_GameControllerClose(gc);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle analog sticks by polling
|
// Handle analog sticks by polling
|
||||||
for (auto gc : m_Gamepads) {
|
for (auto gc : std::as_const(m_Gamepads)) {
|
||||||
short leftX = SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_LEFTX);
|
short leftX = SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_LEFTX);
|
||||||
short leftY = SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_LEFTY);
|
short leftY = SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_LEFTY);
|
||||||
if (SDL_GetTicks() - m_LastAxisNavigationEventTime < AXIS_NAVIGATION_REPEAT_DELAY) {
|
if (SDL_GetTicks() - m_LastAxisNavigationEventTime < AXIS_NAVIGATION_REPEAT_DELAY) {
|
||||||
@@ -218,6 +272,20 @@ void SdlGamepadKeyNavigation::sendKey(QEvent::Type type, Qt::Key key, Qt::Keyboa
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SdlGamepadKeyNavigation::updateTimerState()
|
||||||
|
{
|
||||||
|
if (m_PollingTimer->isActive() && (!m_HasFocus || !m_Enabled)) {
|
||||||
|
m_PollingTimer->stop();
|
||||||
|
}
|
||||||
|
else if (!m_PollingTimer->isActive() && m_HasFocus && m_Enabled) {
|
||||||
|
// Flush events on the first poll
|
||||||
|
m_FirstPoll = true;
|
||||||
|
|
||||||
|
// Poll every 50 ms for a new joystick event
|
||||||
|
m_PollingTimer->start(50);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void SdlGamepadKeyNavigation::setUiNavMode(bool uiNavMode)
|
void SdlGamepadKeyNavigation::setUiNavMode(bool uiNavMode)
|
||||||
{
|
{
|
||||||
m_UiNavMode = uiNavMode;
|
m_UiNavMode = uiNavMode;
|
||||||
@@ -228,7 +296,8 @@ int SdlGamepadKeyNavigation::getConnectedGamepads()
|
|||||||
Q_ASSERT(m_Enabled);
|
Q_ASSERT(m_Enabled);
|
||||||
|
|
||||||
int count = 0;
|
int count = 0;
|
||||||
for (int i = 0; i < SDL_NumJoysticks(); i++) {
|
int numJoysticks = SDL_NumJoysticks();
|
||||||
|
for (int i = 0; i < numJoysticks; i++) {
|
||||||
if (SDL_IsGameController(i)) {
|
if (SDL_IsGameController(i)) {
|
||||||
count++;
|
count++;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,14 +3,16 @@
|
|||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
#include <QEvent>
|
#include <QEvent>
|
||||||
|
|
||||||
#include <SDL.h>
|
#include "SDL_compat.h"
|
||||||
|
|
||||||
|
#include "settings/streamingpreferences.h"
|
||||||
|
|
||||||
class SdlGamepadKeyNavigation : public QObject
|
class SdlGamepadKeyNavigation : public QObject
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
SdlGamepadKeyNavigation();
|
SdlGamepadKeyNavigation(StreamingPreferences* prefs);
|
||||||
|
|
||||||
~SdlGamepadKeyNavigation();
|
~SdlGamepadKeyNavigation();
|
||||||
|
|
||||||
@@ -18,6 +20,8 @@ public:
|
|||||||
|
|
||||||
Q_INVOKABLE void disable();
|
Q_INVOKABLE void disable();
|
||||||
|
|
||||||
|
Q_INVOKABLE void notifyWindowFocus(bool hasFocus);
|
||||||
|
|
||||||
Q_INVOKABLE void setUiNavMode(bool settingsMode);
|
Q_INVOKABLE void setUiNavMode(bool settingsMode);
|
||||||
|
|
||||||
Q_INVOKABLE int getConnectedGamepads();
|
Q_INVOKABLE int getConnectedGamepads();
|
||||||
@@ -25,13 +29,18 @@ public:
|
|||||||
private:
|
private:
|
||||||
void sendKey(QEvent::Type type, Qt::Key key, Qt::KeyboardModifiers modifiers = Qt::NoModifier);
|
void sendKey(QEvent::Type type, Qt::Key key, Qt::KeyboardModifiers modifiers = Qt::NoModifier);
|
||||||
|
|
||||||
|
void updateTimerState();
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void onPollingTimerFired();
|
void onPollingTimerFired();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
StreamingPreferences* m_Prefs;
|
||||||
QTimer* m_PollingTimer;
|
QTimer* m_PollingTimer;
|
||||||
QList<SDL_GameController*> m_Gamepads;
|
QList<SDL_GameController*> m_Gamepads;
|
||||||
bool m_Enabled;
|
bool m_Enabled;
|
||||||
bool m_UiNavMode;
|
bool m_UiNavMode;
|
||||||
|
bool m_FirstPoll;
|
||||||
|
bool m_HasFocus;
|
||||||
Uint32 m_LastAxisNavigationEventTime;
|
Uint32 m_LastAxisNavigationEventTime;
|
||||||
};
|
};
|
||||||
|
|||||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user