mirror of
https://github.com/moonlight-stream/moonlight-qt.git
synced 2026-10-22 16:17:20 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
No files matched your search
@@ -37,12 +37,13 @@ List the games you've tried that exhibit the issue. To see if the issue is game-
|
|||||||
- 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]
|
- 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]
|
||||||
- Antivirus and firewall software: [e.g. Windows Defender and Windows Firewall]
|
- 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.
|
||||||
|
|||||||
@@ -158,17 +158,6 @@ private:
|
|||||||
L"EZFRD32.dll",
|
L"EZFRD32.dll",
|
||||||
L"EZFRD64.dll",
|
L"EZFRD64.dll",
|
||||||
|
|
||||||
// These are the newer dList DLLs for Optimus hybrid graphics DDI.
|
|
||||||
// https://docs.microsoft.com/en-us/windows-hardware/drivers/display/hybrid-system-ddi
|
|
||||||
//
|
|
||||||
// We forcefully block them from loading because Optimus has a bug that
|
|
||||||
// deadlocks DXVA2 when we present with D3DPRESENT_DONOTWAIT. This will prevent
|
|
||||||
// Optimus from ever using the dGPU even if the user has requested it.
|
|
||||||
// https://github.com/moonlight-stream/moonlight-qt/issues/240
|
|
||||||
// https://github.com/moonlight-stream/moonlight-qt/issues/235
|
|
||||||
L"nvdlist.dll",
|
|
||||||
L"nvdlistx.dll",
|
|
||||||
|
|
||||||
// These are the older dList/AppInit 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
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Moonlight PC
|
# Moonlight PC
|
||||||
|
|
||||||
[Moonlight PC](https://moonlight-stream.org) is an open source PC client for NVIDIA GameStream, as used by the NVIDIA Shield.
|
[Moonlight PC](https://moonlight-stream.org) is an open source PC client for NVIDIA GameStream and [Sunshine](https://github.com/LizardByte/Sunshine).
|
||||||
|
|
||||||
Moonlight also has mobile versions for [Android](https://github.com/moonlight-stream/moonlight-android) and [iOS](https://github.com/moonlight-stream/moonlight-ios).
|
Moonlight also has mobile versions for [Android](https://github.com/moonlight-stream/moonlight-android) and [iOS](https://github.com/moonlight-stream/moonlight-ios).
|
||||||
|
|
||||||
@@ -12,53 +12,60 @@ You can follow development on our [Discord server](https://moonlight-stream.org/
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
- Hardware accelerated video decoding on Windows, Mac, and Linux
|
- Hardware accelerated video decoding on Windows, Mac, and Linux
|
||||||
- Supports streaming at up to 120 FPS (high refresh rate monitor recommended)
|
- H.264, HEVC, and AV1 codec support (AV1 requires Sunshine and a supported host GPU)
|
||||||
- Supports streaming at 720p, 1080p, 1440p, 4K, and the client PC's native screen resolution
|
|
||||||
- HDR streaming support
|
- HDR streaming support
|
||||||
- 7.1 surround sound audio support
|
- 7.1 surround sound audio support
|
||||||
|
- 10-point multitouch support (Sunshine only)
|
||||||
|
- Gamepad support with force feedback and motion controls for up to 16 players
|
||||||
- Support for both pointer capture (for games) and direct mouse control (for remote desktop)
|
- Support for both pointer capture (for games) and direct mouse control (for remote desktop)
|
||||||
- Support for passing system-wide keyboard shortcuts like Alt+Tab to the host
|
- Support for passing system-wide keyboard shortcuts like Alt+Tab to the host
|
||||||
- Direct touchscreen mouse control
|
|
||||||
- HEVC support for better image quality at reduced bandwidth
|
|
||||||
- Gamepad support with force feedback for up to 4 players
|
|
||||||
|
|
||||||
## Downloads
|
## Downloads
|
||||||
- [Windows, macOS, and Steam Link](https://github.com/moonlight-stream/moonlight-qt/releases)
|
- [Windows, macOS, and Steam Link](https://github.com/moonlight-stream/moonlight-qt/releases)
|
||||||
- [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)
|
- [AppImage](https://github.com/moonlight-stream/moonlight-qt/releases)
|
||||||
- [Raspberry Pi 4](https://github.com/moonlight-stream/moonlight-docs/wiki/Installing-Moonlight-Qt-on-Raspberry-Pi-4)
|
- [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)
|
- [NVIDIA Jetson and Nintendo Switch (Ubuntu L4T)](https://github.com/moonlight-stream/moonlight-docs/wiki/Installing-Moonlight-Qt-on-Linux4Tegra-(L4T)-Ubuntu)
|
||||||
|
|
||||||
#### Special Thanks
|
#### Special Thanks
|
||||||
|
|
||||||
[](https://cloudsmith.com)
|
[](https://cloudsmith.com)
|
||||||
|
|
||||||
Hosting for Moonlight's Raspberry Pi and L4T package repositories is graciously provided for free by [Cloudsmith](https://cloudsmith.com).
|
Hosting for Moonlight's Debian and L4T package repositories is graciously provided for free by [Cloudsmith](https://cloudsmith.com).
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
|
|
||||||
### Windows Build Requirements
|
### Windows Build Requirements
|
||||||
* Qt 5.15 SDK or later
|
* Qt 5.15 SDK or later. Qt 6 is also supported for x64 and ARM64 builds.
|
||||||
* Windows 7 or later
|
* [Visual Studio 2022](https://visualstudio.microsoft.com/downloads/) (Community edition is fine)
|
||||||
* [Visual Studio 2019](https://visualstudio.microsoft.com/downloads/) (Community edition is fine)
|
* Select **MSVC** option during Qt installation. MinGW is not supported.
|
||||||
* Select **MSVC 2019** option during Qt installation. MinGW is not supported.
|
|
||||||
* [7-Zip](https://www.7-zip.org/) (only if building installers for non-development PCs)
|
* [7-Zip](https://www.7-zip.org/) (only if building installers for non-development PCs)
|
||||||
* [WiX Toolset](https://wixtoolset.org/releases/v3-14-0-5722/) v3.14.0.5722 or later (only if building installers for non-development PCs)
|
* 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.
|
||||||
|
|
||||||
### macOS Build Requirements
|
### macOS Build Requirements
|
||||||
* Qt 6.2 SDK or later
|
* Qt 6.4 SDK or later
|
||||||
* macOS Mojave (10.14) or later
|
* Xcode 13 or later
|
||||||
* Xcode 13
|
|
||||||
* [create-dmg](https://github.com/sindresorhus/create-dmg) (only if building DMGs for use on non-development Macs)
|
* [create-dmg](https://github.com/sindresorhus/create-dmg) (only if building DMGs for use on non-development Macs)
|
||||||
|
|
||||||
### Linux/Unix Build Requirements
|
### Linux/Unix Build Requirements
|
||||||
* Qt 5.9 SDK or later
|
* Qt 6 is recommended, but Qt 5.9 or later is also supported (replace `qmake6` with `qmake` when using Qt 5).
|
||||||
* GCC or Clang
|
* GCC or Clang
|
||||||
|
* FFmpeg 4.0 or later
|
||||||
* Install the required packages:
|
* Install the required packages:
|
||||||
* Debian/Ubuntu: `libegl1-mesa-dev libgl1-mesa-dev libopus-dev libqt5svg5-dev libsdl2-dev libsdl2-ttf-dev libssl-dev libavcodec-dev libva-dev libvdpau-dev libxkbcommon-dev qtwayland5 qt5-qmake qtbase5-dev qtdeclarative5-dev qtquickcontrols2-5-dev wayland-protocols qml-module-qtquick-controls2 qml-module-qtquick-layouts qml-module-qtquick-window2 qml-module-qtquick2`
|
* Debian/Ubuntu:
|
||||||
* RedHat/Fedora: `openssl-devel SDL2-devel SDL2_ttf-devel ffmpeg-devel qt5-qtsvg-devel qt5-qtquickcontrols2-devel libva-devel libvdpau-devel opus-devel pulseaudio-libs-devel alsa-lib-devel`
|
* 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`
|
||||||
* FFmpeg 4.0+ is required to build. If your distro doesn't package FFmpeg 4.0 or later, you can build and install it from source on https://ffmpeg.org/
|
* Qt 6 (Recommended): `qt6-base-dev qt6-declarative-dev libqt6svg6-dev 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 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
|
||||||
@@ -68,16 +75,17 @@ Hosting for Moonlight's Raspberry Pi and L4T package repositories is graciously
|
|||||||
1. Install the latest Qt SDK (and optionally, the Qt Creator IDE) from https://www.qt.io/download
|
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.9 or later.
|
||||||
* This step is not required for building on Steam Link, because the Steam Link SDK includes Qt 5.9.
|
* This step is not required for building on Steam Link, because the Steam Link SDK includes Qt 5.14.
|
||||||
2. Run `git submodule update --init --recursive` from within `moonlight-qt/`
|
2. Run `git submodule update --init --recursive` from within `moonlight-qt/`
|
||||||
3. Open the project in Qt Creator or build from qmake on the command line.
|
3. Open the project in Qt Creator or build from qmake on the command line.
|
||||||
* To build a binary for use on non-development machines, use the scripts in the `scripts` folder.
|
* To build a binary for use on non-development machines, use the scripts in the `scripts` folder.
|
||||||
* For Windows builds, use `scripts\build-arch.bat` and `scripts\generate-bundle.bat`. Execute these scripts from the root of the repository within a Qt command prompt. Ensure WiX and 7-Zip binary directories are in your `%PATH%`.
|
* For Windows builds, use `scripts\build-arch.bat` and `scripts\generate-bundle.bat`. Execute these scripts from the root of the repository within a Qt command prompt. Ensure 7-Zip binary directory is on your `%PATH%`.
|
||||||
* For macOS builds, use `scripts/generate-dmg.sh`. Execute this script from the root of the repository and ensure Qt's `bin` folder is in your `$PATH`.
|
* For macOS builds, use `scripts/generate-dmg.sh`. Execute this script from the root of the repository and ensure Qt's `bin` folder is in your `$PATH`.
|
||||||
* For Steam Link builds, run `scripts/build-steamlink-app.sh` from the root of the repository.
|
* For Steam Link builds, run `scripts/build-steamlink-app.sh` from the root of the repository.
|
||||||
* To build from the command line for development use, run `qmake moonlight-qt.pro` then `make debug` or `make release`
|
* To build from the command line for development use on macOS or Linux, run `qmake6 moonlight-qt.pro` then `make debug` or `make release`
|
||||||
* To create an embedded build for a single-purpose device, use `qmake "CONFIG+=embedded" moonlight-qt.pro` and build normally.
|
* To create an embedded build for a single-purpose device, use `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.
|
* 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
|
||||||
|
|||||||
+18
-2
@@ -5,7 +5,7 @@
|
|||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
<string>Moonlight</string>
|
<string>Moonlight</string>
|
||||||
<key>CFBundleGetInfoString</key>
|
<key>CFBundleGetInfoString</key>
|
||||||
<string>Stream games from your NVIDIA GameStream-enabled PC</string>
|
<string>Stream games and other applications from another PC</string>
|
||||||
<key>CFBundleIconFile</key>
|
<key>CFBundleIconFile</key>
|
||||||
<string>moonlight</string>
|
<string>moonlight</string>
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
@@ -17,11 +17,27 @@
|
|||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>LSMinimumSystemVersion</key>
|
<key>LSMinimumSystemVersion</key>
|
||||||
<string>10.14</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>
|
||||||
|
|||||||
+1
-1
Submodule app/SDL_GameControllerDB updated: ff26eb04d0...e5a5fa2ac6.
+164
-63
@@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,8 +51,11 @@ win32 {
|
|||||||
|
|
||||||
INCLUDEPATH += $$PWD/../libs/windows/include
|
INCLUDEPATH += $$PWD/../libs/windows/include
|
||||||
LIBS += ws2_32.lib winmm.lib dxva2.lib ole32.lib gdi32.lib user32.lib d3d9.lib dwmapi.lib dbghelp.lib
|
LIBS += ws2_32.lib winmm.lib dxva2.lib ole32.lib gdi32.lib user32.lib d3d9.lib dwmapi.lib dbghelp.lib
|
||||||
|
|
||||||
|
# Work around a conflict with math.h inclusion between SDL and Qt 6
|
||||||
|
DEFINES += _USE_MATH_DEFINES
|
||||||
}
|
}
|
||||||
macx {
|
macx:!disable-prebuilts {
|
||||||
INCLUDEPATH += $$PWD/../libs/mac/include
|
INCLUDEPATH += $$PWD/../libs/mac/include
|
||||||
INCLUDEPATH += $$PWD/../libs/mac/Frameworks/SDL2.framework/Versions/A/Headers
|
INCLUDEPATH += $$PWD/../libs/mac/Frameworks/SDL2.framework/Versions/A/Headers
|
||||||
INCLUDEPATH += $$PWD/../libs/mac/Frameworks/SDL2_ttf.framework/Versions/A/Headers
|
INCLUDEPATH += $$PWD/../libs/mac/Frameworks/SDL2_ttf.framework/Versions/A/Headers
|
||||||
@@ -61,72 +67,105 @@ macx {
|
|||||||
QMAKE_OBJECTIVE_CFLAGS += -F$$PWD/../libs/mac/Frameworks
|
QMAKE_OBJECTIVE_CFLAGS += -F$$PWD/../libs/mac/Frameworks
|
||||||
}
|
}
|
||||||
|
|
||||||
unix:!macx {
|
unix:if(!macx|disable-prebuilts) {
|
||||||
CONFIG += link_pkgconfig
|
CONFIG += link_pkgconfig
|
||||||
PKGCONFIG += openssl sdl2 SDL2_ttf opus
|
PKGCONFIG += openssl sdl2 SDL2_ttf opus
|
||||||
|
|
||||||
packagesExist(libavcodec) {
|
!disable-ffmpeg {
|
||||||
PKGCONFIG += libavcodec libavutil
|
packagesExist(libavcodec) {
|
||||||
CONFIG += ffmpeg
|
PKGCONFIG += libavcodec libavutil libswscale
|
||||||
|
CONFIG += ffmpeg
|
||||||
|
|
||||||
packagesExist(libva) {
|
!disable-libva {
|
||||||
packagesExist(libva-x11) {
|
packagesExist(libva) {
|
||||||
CONFIG += libva-x11
|
!disable-x11 {
|
||||||
|
packagesExist(libva-x11) {
|
||||||
|
CONFIG += libva-x11
|
||||||
|
}
|
||||||
|
}
|
||||||
|
!disable-wayland {
|
||||||
|
packagesExist(libva-wayland) {
|
||||||
|
CONFIG += libva-wayland
|
||||||
|
}
|
||||||
|
}
|
||||||
|
!disable-libdrm {
|
||||||
|
packagesExist(libva-drm) {
|
||||||
|
CONFIG += libva-drm
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CONFIG += libva
|
||||||
|
}
|
||||||
}
|
}
|
||||||
packagesExist(libva-wayland) {
|
|
||||||
CONFIG += libva-wayland
|
!disable-libvdpau {
|
||||||
|
packagesExist(vdpau) {
|
||||||
|
CONFIG += libvdpau
|
||||||
|
}
|
||||||
}
|
}
|
||||||
packagesExist(libva-drm) {
|
|
||||||
CONFIG += libva-drm
|
!disable-mmal {
|
||||||
|
packagesExist(mmal) {
|
||||||
|
PKGCONFIG += mmal
|
||||||
|
CONFIG += mmal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
!disable-libdrm {
|
||||||
|
packagesExist(libdrm) {
|
||||||
|
PKGCONFIG += libdrm
|
||||||
|
CONFIG += libdrm
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
!disable-cuda {
|
||||||
|
packagesExist(ffnvcodec) {
|
||||||
|
PKGCONFIG += ffnvcodec
|
||||||
|
CONFIG += cuda
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
!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
|
|
||||||
}
|
|
||||||
|
|
||||||
packagesExist(ffnvcodec) {
|
|
||||||
PKGCONFIG += ffnvcodec
|
|
||||||
CONFIG += cuda
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
packagesExist(wayland-client) {
|
|
||||||
DEFINES += HAS_WAYLAND
|
|
||||||
PKGCONFIG += wayland-client
|
|
||||||
}
|
|
||||||
|
|
||||||
packagesExist(x11) {
|
|
||||||
DEFINES += HAS_X11
|
|
||||||
PKGCONFIG += x11
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
win32 {
|
win32 {
|
||||||
LIBS += -llibssl -llibcrypto -lSDL2 -lSDL2_ttf -lavcodec -lavutil -lopus -ldxgi -ld3d11
|
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 += soundio discord-rpc
|
||||||
}
|
}
|
||||||
macx {
|
macx {
|
||||||
LIBS += -lssl -lcrypto -lavcodec.59 -lavutil.57 -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.61 -lavutil.59 -lswscale.8 -lopus -framework SDL2 -framework SDL2_ttf
|
||||||
|
CONFIG += discord-rpc
|
||||||
|
}
|
||||||
|
|
||||||
|
LIBS += -lobjc -framework VideoToolbox -framework AVFoundation -framework CoreVideo -framework CoreGraphics -framework CoreMedia -framework AppKit -framework Metal -framework QuartzCore
|
||||||
|
|
||||||
# For libsoundio
|
# For libsoundio
|
||||||
LIBS += -framework CoreAudio -framework AudioUnit
|
LIBS += -framework CoreAudio -framework AudioUnit
|
||||||
|
|
||||||
CONFIG += ffmpeg soundio discord-rpc
|
CONFIG += ffmpeg soundio
|
||||||
}
|
}
|
||||||
|
|
||||||
SOURCES += \
|
SOURCES += \
|
||||||
@@ -211,16 +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/pacer/pacer.cpp \
|
streaming/video/ffmpeg-renderers/swframemapper.cpp \
|
||||||
streaming/video/ffmpeg-renderers/pacer/nullthreadedvsyncsource.cpp
|
streaming/video/ffmpeg-renderers/pacer/pacer.cpp
|
||||||
|
|
||||||
HEADERS += \
|
HEADERS += \
|
||||||
streaming/video/ffmpeg.h \
|
streaming/video/ffmpeg.h \
|
||||||
streaming/video/ffmpeg-renderers/renderer.h \
|
streaming/video/ffmpeg-renderers/renderer.h \
|
||||||
|
streaming/video/ffmpeg-renderers/genhwaccel.h \
|
||||||
streaming/video/ffmpeg-renderers/sdlvid.h \
|
streaming/video/ffmpeg-renderers/sdlvid.h \
|
||||||
streaming/video/ffmpeg-renderers/pacer/pacer.h \
|
streaming/video/ffmpeg-renderers/swframemapper.h \
|
||||||
streaming/video/ffmpeg-renderers/pacer/nullthreadedvsyncsource.h
|
streaming/video/ffmpeg-renderers/pacer/pacer.h
|
||||||
}
|
}
|
||||||
libva {
|
libva {
|
||||||
message(VAAPI renderer selected)
|
message(VAAPI renderer selected)
|
||||||
@@ -242,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
|
||||||
@@ -261,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)
|
||||||
@@ -281,6 +334,19 @@ cuda {
|
|||||||
DEFINES += HAVE_CUDA
|
DEFINES += HAVE_CUDA
|
||||||
SOURCES += streaming/video/ffmpeg-renderers/cuda.cpp
|
SOURCES += streaming/video/ffmpeg-renderers/cuda.cpp
|
||||||
HEADERS += streaming/video/ffmpeg-renderers/cuda.h
|
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 {
|
config_EGL {
|
||||||
message(EGL renderer selected)
|
message(EGL renderer selected)
|
||||||
@@ -289,8 +355,11 @@ config_EGL {
|
|||||||
DEFINES += HAVE_EGL
|
DEFINES += HAVE_EGL
|
||||||
SOURCES += \
|
SOURCES += \
|
||||||
streaming/video/ffmpeg-renderers/eglvid.cpp \
|
streaming/video/ffmpeg-renderers/eglvid.cpp \
|
||||||
streaming/video/ffmpeg-renderers/egl_extensions.cpp
|
streaming/video/ffmpeg-renderers/egl_extensions.cpp \
|
||||||
HEADERS += streaming/video/ffmpeg-renderers/eglvid.h
|
streaming/video/ffmpeg-renderers/eglimagefactory.cpp
|
||||||
|
HEADERS += \
|
||||||
|
streaming/video/ffmpeg-renderers/eglvid.h \
|
||||||
|
streaming/video/ffmpeg-renderers/eglimagefactory.h
|
||||||
}
|
}
|
||||||
config_SL {
|
config_SL {
|
||||||
message(Steam Link build configuration selected)
|
message(Steam Link build configuration selected)
|
||||||
@@ -325,7 +394,9 @@ 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
|
||||||
@@ -348,13 +419,35 @@ embedded {
|
|||||||
|
|
||||||
DEFINES += 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 += \
|
TRANSLATIONS += \
|
||||||
languages/qml_zh_cn.ts \
|
languages/qml_zh_CN.ts \
|
||||||
languages/qml_de.ts \
|
languages/qml_de.ts \
|
||||||
languages/qml_fr.ts \
|
languages/qml_fr.ts \
|
||||||
languages/qml_nb_NO.ts \
|
languages/qml_nb_NO.ts \
|
||||||
@@ -369,14 +462,18 @@ TRANSLATIONS += \
|
|||||||
languages/qml_sv.ts \
|
languages/qml_sv.ts \
|
||||||
languages/qml_tr.ts \
|
languages/qml_tr.ts \
|
||||||
languages/qml_uk.ts \
|
languages/qml_uk.ts \
|
||||||
languages/qml_zh_Hant.ts \
|
languages/qml_zh_TW.ts \
|
||||||
languages/qml_el.ts \
|
languages/qml_el.ts \
|
||||||
languages/qml_hi.ts \
|
languages/qml_hi.ts \
|
||||||
languages/qml_it.ts \
|
languages/qml_it.ts \
|
||||||
languages/qml_pt.ts \
|
languages/qml_pt.ts \
|
||||||
languages/qml_pt_BR.ts \
|
languages/qml_pt_BR.ts \
|
||||||
languages/qml_pl.ts \
|
languages/qml_pl.ts \
|
||||||
languages/qml_cs.ts
|
languages/qml_cs.ts \
|
||||||
|
languages/qml_he.ts \
|
||||||
|
languages/qml_ckb.ts \
|
||||||
|
languages/qml_lt.ts \
|
||||||
|
languages/qml_et.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 =
|
||||||
@@ -465,15 +562,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_FRAMEWORKS.path = Contents/Frameworks
|
|
||||||
|
|
||||||
APP_BUNDLE_PLIST.files = $$OUT_PWD/Info.plist
|
APP_BUNDLE_PLIST.files = $$OUT_PWD/Info.plist
|
||||||
APP_BUNDLE_PLIST.path = Contents
|
APP_BUNDLE_PLIST.path = Contents
|
||||||
|
|
||||||
QMAKE_BUNDLE_DATA += APP_BUNDLE_RESOURCES APP_BUNDLE_FRAMEWORKS APP_BUNDLE_PLIST
|
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,16 +6,17 @@
|
|||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
|
|
||||||
AutoUpdateChecker::AutoUpdateChecker(QObject *parent) :
|
AutoUpdateChecker::AutoUpdateChecker(QObject *parent) :
|
||||||
QObject(parent),
|
QObject(parent)
|
||||||
m_Nam(this)
|
|
||||||
{
|
{
|
||||||
|
m_Nam = new QNetworkAccessManager(this);
|
||||||
|
|
||||||
// Never communicate over HTTP
|
// Never communicate over HTTP
|
||||||
m_Nam.setStrictTransportSecurityEnabled(true);
|
m_Nam->setStrictTransportSecurityEnabled(true);
|
||||||
|
|
||||||
// Allow HTTP redirects
|
// Allow HTTP redirects
|
||||||
m_Nam.setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy);
|
m_Nam->setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy);
|
||||||
|
|
||||||
connect(&m_Nam, &QNetworkAccessManager::finished,
|
connect(m_Nam, &QNetworkAccessManager::finished,
|
||||||
this, &AutoUpdateChecker::handleUpdateCheckRequestFinished);
|
this, &AutoUpdateChecker::handleUpdateCheckRequestFinished);
|
||||||
|
|
||||||
QString currentVersion(VERSION_STR);
|
QString currentVersion(VERSION_STR);
|
||||||
@@ -28,12 +29,17 @@ AutoUpdateChecker::AutoUpdateChecker(QObject *parent) :
|
|||||||
|
|
||||||
void AutoUpdateChecker::start()
|
void AutoUpdateChecker::start()
|
||||||
{
|
{
|
||||||
|
if (!m_Nam) {
|
||||||
|
Q_ASSERT(m_Nam);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
#if defined(Q_OS_WIN32) || defined(Q_OS_DARWIN) || defined(STEAM_LINK) || defined(APP_IMAGE) // Only run update checker on platforms without auto-update
|
#if defined(Q_OS_WIN32) || defined(Q_OS_DARWIN) || defined(STEAM_LINK) || defined(APP_IMAGE) // Only run update checker on platforms without auto-update
|
||||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) && QT_VERSION < QT_VERSION_CHECK(5, 15, 1) && !defined(QT_NO_BEARERMANAGEMENT)
|
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) && QT_VERSION < QT_VERSION_CHECK(5, 15, 1) && !defined(QT_NO_BEARERMANAGEMENT)
|
||||||
// HACK: Set network accessibility to work around QTBUG-80947 (introduced in Qt 5.14.0 and fixed in Qt 5.15.1)
|
// HACK: Set network accessibility to work around QTBUG-80947 (introduced in Qt 5.14.0 and fixed in Qt 5.15.1)
|
||||||
QT_WARNING_PUSH
|
QT_WARNING_PUSH
|
||||||
QT_WARNING_DISABLE_DEPRECATED
|
QT_WARNING_DISABLE_DEPRECATED
|
||||||
m_Nam.setNetworkAccessible(QNetworkAccessManager::Accessible);
|
m_Nam->setNetworkAccessible(QNetworkAccessManager::Accessible);
|
||||||
QT_WARNING_POP
|
QT_WARNING_POP
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -45,7 +51,7 @@ void AutoUpdateChecker::start()
|
|||||||
#else
|
#else
|
||||||
request.setAttribute(QNetworkRequest::HTTP2AllowedAttribute, true);
|
request.setAttribute(QNetworkRequest::HTTP2AllowedAttribute, true);
|
||||||
#endif
|
#endif
|
||||||
m_Nam.get(request);
|
m_Nam->get(request);
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,6 +108,11 @@ void AutoUpdateChecker::handleUpdateCheckRequestFinished(QNetworkReply* reply)
|
|||||||
{
|
{
|
||||||
Q_ASSERT(reply->isFinished());
|
Q_ASSERT(reply->isFinished());
|
||||||
|
|
||||||
|
// Delete the QNetworkAccessManager to free resources and
|
||||||
|
// prevent the bearer plugin from polling in the background.
|
||||||
|
m_Nam->deleteLater();
|
||||||
|
m_Nam = nullptr;
|
||||||
|
|
||||||
if (reply->error() == QNetworkReply::NoError) {
|
if (reply->error() == QNetworkReply::NoError) {
|
||||||
QTextStream stream(reply);
|
QTextStream stream(reply);
|
||||||
|
|
||||||
|
|||||||
@@ -25,5 +25,5 @@ private:
|
|||||||
QString getPlatform();
|
QString getPlatform();
|
||||||
|
|
||||||
QVector<int> m_CurrentVersionQuad;
|
QVector<int> m_CurrentVersionQuad;
|
||||||
QNetworkAccessManager m_Nam;
|
QNetworkAccessManager* m_Nam;
|
||||||
};
|
};
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
#include "computermanager.h"
|
#include "computermanager.h"
|
||||||
#include "boxartmanager.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>
|
||||||
@@ -13,6 +13,7 @@
|
|||||||
#include <random>
|
#include <random>
|
||||||
|
|
||||||
#define SER_HOSTS "hosts"
|
#define SER_HOSTS "hosts"
|
||||||
|
#define SER_HOSTS_BACKUP "hostsbackup"
|
||||||
|
|
||||||
class PcMonitorThread : public QThread
|
class PcMonitorThread : public QThread
|
||||||
{
|
{
|
||||||
@@ -143,8 +144,8 @@ 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_CompatFetcher(nullptr),
|
||||||
@@ -152,12 +153,21 @@ ComputerManager::ComputerManager(QObject *parent)
|
|||||||
{
|
{
|
||||||
QSettings settings;
|
QSettings settings;
|
||||||
|
|
||||||
|
// If there's a hosts backup copy, we must have failed to commit
|
||||||
|
// a previous update before exiting. Restore the backup now.
|
||||||
|
int hosts = settings.beginReadArray(SER_HOSTS_BACKUP);
|
||||||
|
if (hosts == 0) {
|
||||||
|
// If there's no host backup, read from the primary location.
|
||||||
|
settings.endArray();
|
||||||
|
hosts = settings.beginReadArray(SER_HOSTS);
|
||||||
|
}
|
||||||
|
|
||||||
// Inflate our hosts from QSettings
|
// Inflate our hosts from QSettings
|
||||||
int hosts = settings.beginReadArray(SER_HOSTS);
|
|
||||||
for (int i = 0; i < hosts; i++) {
|
for (int i = 0; i < hosts; i++) {
|
||||||
settings.setArrayIndex(i);
|
settings.setArrayIndex(i);
|
||||||
NvComputer* computer = new NvComputer(settings);
|
NvComputer* computer = new NvComputer(settings);
|
||||||
m_KnownHosts[computer->uuid] = computer;
|
m_KnownHosts[computer->uuid] = computer;
|
||||||
|
m_LastSerializedHosts[computer->uuid] = *computer;
|
||||||
}
|
}
|
||||||
settings.endArray();
|
settings.endArray();
|
||||||
|
|
||||||
@@ -169,8 +179,8 @@ ComputerManager::ComputerManager(QObject *parent)
|
|||||||
m_DelayedFlushThread->start();
|
m_DelayedFlushThread->start();
|
||||||
|
|
||||||
// To quit in a timely manner, we must block additional requests
|
// To quit in a timely manner, we must block additional requests
|
||||||
// after we receive the aboutToQuit() signal. This is neccessary
|
// after we receive the aboutToQuit() signal. This is necessary
|
||||||
// because NvHTTP uses aboutToQuit() to abort requests in progres
|
// because NvHTTP uses aboutToQuit() to abort requests in progress
|
||||||
// while quitting, however this is a one time signal - additional
|
// while quitting, however this is a one time signal - additional
|
||||||
// requests would not be aborted and block termination.
|
// requests would not be aborted and block termination.
|
||||||
connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this, &ComputerManager::handleAboutToQuit);
|
connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this, &ComputerManager::handleAboutToQuit);
|
||||||
@@ -241,21 +251,48 @@ void DelayedFlushThread::run() {
|
|||||||
|
|
||||||
// Reset the delayed flush flag to ensure any racing saveHosts() call will set it again
|
// Reset the delayed flush flag to ensure any racing saveHosts() call will set it again
|
||||||
m_ComputerManager->m_NeedsDelayedFlush = false;
|
m_ComputerManager->m_NeedsDelayedFlush = false;
|
||||||
|
|
||||||
|
// Update the last serialized hosts map under the delayed flush mutex
|
||||||
|
m_ComputerManager->m_LastSerializedHosts.clear();
|
||||||
|
for (const NvComputer* computer : m_ComputerManager->m_KnownHosts) {
|
||||||
|
// Copy the current state of the NvComputer to allow us to check later if we need
|
||||||
|
// to serialize it again when attribute updates occur.
|
||||||
|
QReadLocker computerLock(&computer->lock);
|
||||||
|
m_ComputerManager->m_LastSerializedHosts[computer->uuid] = *computer;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Perform the flush
|
// Perform the flush
|
||||||
{
|
{
|
||||||
QReadLocker lock(&m_ComputerManager->m_Lock);
|
|
||||||
|
|
||||||
QSettings settings;
|
QSettings settings;
|
||||||
settings.remove(SER_HOSTS);
|
|
||||||
settings.beginWriteArray(SER_HOSTS);
|
// First, write to the backup location
|
||||||
int i = 0;
|
settings.beginWriteArray(SER_HOSTS_BACKUP);
|
||||||
for (const NvComputer* computer : m_ComputerManager->m_KnownHosts) {
|
{
|
||||||
settings.setArrayIndex(i++);
|
QReadLocker lock(&m_ComputerManager->m_Lock);
|
||||||
computer->serialize(settings);
|
int i = 0;
|
||||||
|
for (const NvComputer* computer : m_ComputerManager->m_KnownHosts) {
|
||||||
|
settings.setArrayIndex(i++);
|
||||||
|
computer->serialize(settings, false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
settings.endArray();
|
settings.endArray();
|
||||||
|
|
||||||
|
// Next, write to the primary location
|
||||||
|
settings.remove(SER_HOSTS);
|
||||||
|
settings.beginWriteArray(SER_HOSTS);
|
||||||
|
{
|
||||||
|
QReadLocker lock(&m_ComputerManager->m_Lock);
|
||||||
|
int i = 0;
|
||||||
|
for (const NvComputer* computer : m_ComputerManager->m_KnownHosts) {
|
||||||
|
settings.setArrayIndex(i++);
|
||||||
|
computer->serialize(settings, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
settings.endArray();
|
||||||
|
|
||||||
|
// Finally, delete the backup copy
|
||||||
|
settings.remove(SER_HOSTS_BACKUP);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -315,16 +352,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);
|
||||||
@@ -405,6 +441,19 @@ void ComputerManager::handleMdnsServiceResolved(MdnsPendingComputer* computer,
|
|||||||
computer->deleteLater();
|
computer->deleteLater();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ComputerManager::saveHost(NvComputer *computer)
|
||||||
|
{
|
||||||
|
// If no serializable properties changed, don't bother saving hosts
|
||||||
|
QMutexLocker lock(&m_DelayedFlushMutex);
|
||||||
|
QReadLocker computerLock(&computer->lock);
|
||||||
|
if (!m_LastSerializedHosts.value(computer->uuid).isEqualSerialized(*computer)) {
|
||||||
|
// Queue a request for a delayed flush to QSettings outside of the lock
|
||||||
|
computerLock.unlock();
|
||||||
|
lock.unlock();
|
||||||
|
saveHosts();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void ComputerManager::handleComputerStateChanged(NvComputer* computer)
|
void ComputerManager::handleComputerStateChanged(NvComputer* computer)
|
||||||
{
|
{
|
||||||
emit computerStateChanged(computer);
|
emit computerStateChanged(computer);
|
||||||
@@ -414,15 +463,20 @@ void ComputerManager::handleComputerStateChanged(NvComputer* computer)
|
|||||||
emit quitAppCompleted(QVariant());
|
emit quitAppCompleted(QVariant());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save updated hosts to QSettings
|
// Save updates to this host
|
||||||
saveHosts();
|
saveHost(computer);
|
||||||
}
|
}
|
||||||
|
|
||||||
QVector<NvComputer*> ComputerManager::getComputers()
|
QVector<NvComputer*> ComputerManager::getComputers()
|
||||||
{
|
{
|
||||||
QReadLocker lock(&m_Lock);
|
QReadLocker lock(&m_Lock);
|
||||||
|
|
||||||
return QVector<NvComputer*>::fromList(m_KnownHosts.values());
|
// Return a sorted host list
|
||||||
|
auto hosts = QVector<NvComputer*>::fromList(m_KnownHosts.values());
|
||||||
|
std::stable_sort(hosts.begin(), hosts.end(), [](const NvComputer* host1, const NvComputer* host2) {
|
||||||
|
return host1->name.toLower() < host2->name.toLower();
|
||||||
|
});
|
||||||
|
return hosts;
|
||||||
}
|
}
|
||||||
|
|
||||||
class DeferredHostDeletionTask : public QRunnable
|
class DeferredHostDeletionTask : public QRunnable
|
||||||
@@ -446,7 +500,7 @@ public:
|
|||||||
m_ComputerManager->m_KnownHosts.remove(m_Computer->uuid);
|
m_ComputerManager->m_KnownHosts.remove(m_Computer->uuid);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Persist the new host list
|
// Persist the new host list with this computer deleted
|
||||||
m_ComputerManager->saveHosts();
|
m_ComputerManager->saveHosts();
|
||||||
|
|
||||||
// Delete the polling entry first. This will stop all polling threads too.
|
// Delete the polling entry first. This will stop all polling threads too.
|
||||||
@@ -487,9 +541,6 @@ void ComputerManager::renameHost(NvComputer* computer, QString name)
|
|||||||
|
|
||||||
void ComputerManager::clientSideAttributeUpdated(NvComputer* computer)
|
void ComputerManager::clientSideAttributeUpdated(NvComputer* computer)
|
||||||
{
|
{
|
||||||
// Persist the change
|
|
||||||
saveHosts();
|
|
||||||
|
|
||||||
// Notify the UI of the state change
|
// Notify the UI of the state change
|
||||||
handleComputerStateChanged(computer);
|
handleComputerStateChanged(computer);
|
||||||
}
|
}
|
||||||
@@ -547,7 +598,7 @@ private:
|
|||||||
break;
|
break;
|
||||||
case NvPairingManager::PairState::PAIRED:
|
case NvPairingManager::PairState::PAIRED:
|
||||||
// Persist the newly pinned server certificate for this host
|
// Persist the newly pinned server certificate for this host
|
||||||
m_ComputerManager->saveHosts();
|
m_ComputerManager->saveHost(m_Computer);
|
||||||
|
|
||||||
emit pairingCompleted(m_Computer, nullptr);
|
emit pairingCompleted(m_Computer, nullptr);
|
||||||
break;
|
break;
|
||||||
@@ -646,9 +697,10 @@ void ComputerManager::stopPollingAsync()
|
|||||||
m_PendingResolution.removeFirst();
|
m_PendingResolution.removeFirst();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete the browser to stop discovery
|
// Delete the browser and server to stop discovery and refresh polling
|
||||||
delete m_MdnsBrowser;
|
delete m_MdnsBrowser;
|
||||||
m_MdnsBrowser = nullptr;
|
m_MdnsBrowser = nullptr;
|
||||||
|
m_MdnsServer.reset();
|
||||||
|
|
||||||
// Interrupt all threads, but don't wait for them to terminate
|
// Interrupt all threads, but don't wait for them to terminate
|
||||||
for (ComputerPollingEntry* entry : m_PollEntries) {
|
for (ComputerPollingEntry* entry : m_PollEntries) {
|
||||||
@@ -658,8 +710,8 @@ void ComputerManager::stopPollingAsync()
|
|||||||
|
|
||||||
void ComputerManager::addNewHostManually(QString address)
|
void ComputerManager::addNewHostManually(QString address)
|
||||||
{
|
{
|
||||||
QUrl url = QUrl::fromUserInput(address);
|
QUrl url = QUrl::fromUserInput("moonlight://" + address);
|
||||||
if (url.isValid() && !url.host().isEmpty()) {
|
if (url.isValid() && !url.host().isEmpty() && url.scheme() == "moonlight") {
|
||||||
// If there wasn't a port specified, use the default
|
// If there wasn't a port specified, use the default
|
||||||
addNewHost(NvAddress(url.host(), url.port(DEFAULT_HTTP_PORT)), false);
|
addNewHost(NvAddress(url.host(), url.port(DEFAULT_HTTP_PORT)), false);
|
||||||
}
|
}
|
||||||
@@ -730,10 +782,9 @@ private:
|
|||||||
return serverInfo;
|
return serverInfo;
|
||||||
} catch (...) {
|
} catch (...) {
|
||||||
if (!m_Mdns) {
|
if (!m_Mdns) {
|
||||||
StreamingPreferences prefs;
|
unsigned int portTestResult;
|
||||||
int portTestResult;
|
|
||||||
|
|
||||||
if (prefs.detectNetworkBlocking) {
|
if (m_ComputerManager->m_Prefs->detectNetworkBlocking) {
|
||||||
// We failed to connect to the specified PC. Let's test to make sure this network
|
// 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.
|
// isn't blocking Moonlight, so we can tell the user about it.
|
||||||
portTestResult = LiTestClientConnectivity("qt.conntest.moonlight-stream.org", 443,
|
portTestResult = LiTestClientConnectivity("qt.conntest.moonlight-stream.org", 443,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "nvcomputer.h"
|
#include "nvcomputer.h"
|
||||||
#include "nvpairingmanager.h"
|
#include "settings/streamingpreferences.h"
|
||||||
#include "settings/compatfetcher.h"
|
#include "settings/compatfetcher.h"
|
||||||
|
|
||||||
#include <qmdnsengine/server.h>
|
#include <qmdnsengine/server.h>
|
||||||
@@ -42,11 +42,11 @@ class MdnsPendingComputer : public QObject
|
|||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit MdnsPendingComputer(QMdnsEngine::Server* server,
|
explicit MdnsPendingComputer(const QSharedPointer<QMdnsEngine::Server> server,
|
||||||
const QMdnsEngine::Service& service)
|
const QMdnsEngine::Service& service)
|
||||||
: m_Hostname(service.hostname()),
|
: m_Hostname(service.hostname()),
|
||||||
m_Port(service.port()),
|
m_Port(service.port()),
|
||||||
m_Server(server),
|
m_ServerWeak(server),
|
||||||
m_Resolver(nullptr)
|
m_Resolver(nullptr)
|
||||||
{
|
{
|
||||||
// Start resolving
|
// Start resolving
|
||||||
@@ -72,9 +72,15 @@ 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());
|
||||||
@@ -92,10 +98,29 @@ 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, &MdnsPendingComputer::handleResolvedTimeout);
|
QTimer::singleShot(2000, this, &MdnsPendingComputer::handleResolvedTimeout);
|
||||||
@@ -103,9 +128,11 @@ private:
|
|||||||
|
|
||||||
QByteArray m_Hostname;
|
QByteArray m_Hostname;
|
||||||
uint16_t m_Port;
|
uint16_t m_Port;
|
||||||
QMdnsEngine::Server* m_Server;
|
QWeakPointer<QMdnsEngine::Server> m_ServerWeak;
|
||||||
|
QSharedPointer<QMdnsEngine::Server> m_Server;
|
||||||
QMdnsEngine::Resolver* m_Resolver;
|
QMdnsEngine::Resolver* m_Resolver;
|
||||||
QVector<QHostAddress> m_Addresses;
|
QVector<QHostAddress> m_Addresses;
|
||||||
|
int m_Retries = 10;
|
||||||
};
|
};
|
||||||
|
|
||||||
class ComputerPollingEntry
|
class ComputerPollingEntry
|
||||||
@@ -191,7 +218,7 @@ class ComputerManager : public QObject
|
|||||||
friend class DelayedFlushThread;
|
friend class DelayedFlushThread;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit ComputerManager(QObject *parent = nullptr);
|
explicit ComputerManager(StreamingPreferences* prefs);
|
||||||
|
|
||||||
virtual ~ComputerManager();
|
virtual ~ComputerManager();
|
||||||
|
|
||||||
@@ -237,21 +264,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;
|
CompatFetcher m_CompatFetcher;
|
||||||
DelayedFlushThread* m_DelayedFlushThread;
|
DelayedFlushThread* m_DelayedFlushThread;
|
||||||
QMutex m_DelayedFlushMutex;
|
QMutex m_DelayedFlushMutex; // Lock ordering: Must never be acquired while holding NvComputer lock
|
||||||
QWaitCondition m_DelayedFlushCondition;
|
QWaitCondition m_DelayedFlushCondition;
|
||||||
bool m_NeedsDelayedFlush;
|
bool m_NeedsDelayedFlush;
|
||||||
};
|
};
|
||||||
@@ -6,6 +6,13 @@ 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.
|
||||||
|
for (NvComputer * computer: m_ComputerManager->getComputers()) {
|
||||||
|
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);
|
||||||
|
|||||||
@@ -95,6 +95,14 @@ void IdentityManager::createCredentials(QSettings& settings)
|
|||||||
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);
|
||||||
|
|
||||||
@@ -123,10 +131,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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+97
-24
@@ -74,7 +74,7 @@ void NvComputer::setRemoteAddress(QHostAddress address)
|
|||||||
this->remoteAddress = NvAddress(address, this->externalPort);
|
this->remoteAddress = NvAddress(address, this->externalPort);
|
||||||
}
|
}
|
||||||
|
|
||||||
void NvComputer::serialize(QSettings& settings) const
|
void NvComputer::serialize(QSettings& settings, bool serializeApps) const
|
||||||
{
|
{
|
||||||
QReadLocker lock(&this->lock);
|
QReadLocker lock(&this->lock);
|
||||||
|
|
||||||
@@ -94,7 +94,7 @@ void NvComputer::serialize(QSettings& settings) const
|
|||||||
settings.setValue(SER_NVIDIASOFTWARE, isNvidiaServerSoftware);
|
settings.setValue(SER_NVIDIASOFTWARE, isNvidiaServerSoftware);
|
||||||
|
|
||||||
// Avoid deleting an existing applist if we couldn't get one
|
// Avoid deleting an existing applist if we couldn't get one
|
||||||
if (!appList.isEmpty()) {
|
if (!appList.isEmpty() && serializeApps) {
|
||||||
settings.remove(SER_APPLIST);
|
settings.remove(SER_APPLIST);
|
||||||
settings.beginWriteArray(SER_APPLIST);
|
settings.beginWriteArray(SER_APPLIST);
|
||||||
for (int i = 0; i < appList.count(); i++) {
|
for (int i = 0; i < appList.count(); i++) {
|
||||||
@@ -105,6 +105,21 @@ void NvComputer::serialize(QSettings& settings) const
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool NvComputer::isEqualSerialized(const NvComputer &that) const
|
||||||
|
{
|
||||||
|
return this->name == that.name &&
|
||||||
|
this->hasCustomName == that.hasCustomName &&
|
||||||
|
this->uuid == that.uuid &&
|
||||||
|
this->macAddress == that.macAddress &&
|
||||||
|
this->localAddress == that.localAddress &&
|
||||||
|
this->remoteAddress == that.remoteAddress &&
|
||||||
|
this->ipv6Address == that.ipv6Address &&
|
||||||
|
this->manualAddress == that.manualAddress &&
|
||||||
|
this->serverCert == that.serverCert &&
|
||||||
|
this->isNvidiaServerSoftware == that.isNvidiaServerSoftware &&
|
||||||
|
this->appList == that.appList;
|
||||||
|
}
|
||||||
|
|
||||||
void NvComputer::sortAppList()
|
void NvComputer::sortAppList()
|
||||||
{
|
{
|
||||||
std::stable_sort(appList.begin(), appList.end(), [](const NvApp& app1, const NvApp& app2) {
|
std::stable_sort(appList.begin(), appList.end(), [](const NvApp& app1, const NvApp& app2) {
|
||||||
@@ -136,7 +151,8 @@ NvComputer::NvComputer(NvHTTP& http, QString serverInfo)
|
|||||||
this->serverCodecModeSupport = codecSupport.toInt();
|
this->serverCodecModeSupport = codecSupport.toInt();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
this->serverCodecModeSupport = 0;
|
// Assume H.264 is always supported
|
||||||
|
this->serverCodecModeSupport = SCM_H264;
|
||||||
}
|
}
|
||||||
|
|
||||||
QString maxLumaPixelsHEVC = NvHTTP::getXmlString(serverInfo, "MaxLumaPixelsHEVC");
|
QString maxLumaPixelsHEVC = NvHTTP::getXmlString(serverInfo, "MaxLumaPixelsHEVC");
|
||||||
@@ -150,8 +166,8 @@ NvComputer::NvComputer(NvHTTP& http, QString serverInfo)
|
|||||||
this->displayModes = NvHTTP::getDisplayModeList(serverInfo);
|
this->displayModes = NvHTTP::getDisplayModeList(serverInfo);
|
||||||
std::stable_sort(this->displayModes.begin(), this->displayModes.end(),
|
std::stable_sort(this->displayModes.begin(), this->displayModes.end(),
|
||||||
[](const NvDisplayMode& mode1, const NvDisplayMode& mode2) {
|
[](const NvDisplayMode& mode1, const NvDisplayMode& mode2) {
|
||||||
return mode1.width * mode1.height * mode1.refreshRate <
|
return (uint64_t)mode1.width * mode1.height * mode1.refreshRate <
|
||||||
mode2.width * mode2.height * mode2.refreshRate;
|
(uint64_t)mode2.width * mode2.height * mode2.refreshRate;
|
||||||
});
|
});
|
||||||
|
|
||||||
// We can get an IPv4 loopback address if we're using the GS IPv6 Forwarder
|
// We can get an IPv4 loopback address if we're using the GS IPv6 Forwarder
|
||||||
@@ -169,7 +185,7 @@ NvComputer::NvComputer(NvHTTP& http, QString serverInfo)
|
|||||||
// to support dynamic HTTP WAN ports without requiring the user to manually enter the port.
|
// to support dynamic HTTP WAN ports without requiring the user to manually enter the port.
|
||||||
QString remotePortStr = NvHTTP::getXmlString(serverInfo, "ExternalPort");
|
QString remotePortStr = NvHTTP::getXmlString(serverInfo, "ExternalPort");
|
||||||
if (remotePortStr.isEmpty() || (this->externalPort = remotePortStr.toUShort()) == 0) {
|
if (remotePortStr.isEmpty() || (this->externalPort = remotePortStr.toUShort()) == 0) {
|
||||||
this->externalPort = DEFAULT_HTTP_PORT;
|
this->externalPort = http.httpPort();
|
||||||
}
|
}
|
||||||
|
|
||||||
QString remoteAddress = NvHTTP::getXmlString(serverInfo, "ExternalIP");
|
QString remoteAddress = NvHTTP::getXmlString(serverInfo, "ExternalIP");
|
||||||
@@ -219,23 +235,30 @@ bool NvComputer::wake() const
|
|||||||
for (int i = 0; i < 16; i++) {
|
for (int i = 0; i < 16; i++) {
|
||||||
wolPayload.append(macAddress);
|
wolPayload.append(macAddress);
|
||||||
}
|
}
|
||||||
Q_ASSERT(wolPayload.count() == 102);
|
Q_ASSERT(wolPayload.size() == 102);
|
||||||
}
|
}
|
||||||
|
|
||||||
const quint16 WOL_PORTS[] = {
|
// Ports used as-is
|
||||||
|
const quint16 STATIC_WOL_PORTS[] = {
|
||||||
9, // Standard WOL port (privileged port)
|
9, // Standard WOL port (privileged port)
|
||||||
47998, 47999, 48000, 48002, 48010, // Ports opened by GFE
|
|
||||||
47009, // Port opened by Moonlight Internet Hosting Tool for WoL (non-privileged port)
|
47009, // Port opened by Moonlight Internet Hosting Tool for WoL (non-privileged port)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Ports offset by the HTTP base port for hosts using alternate ports
|
||||||
|
const quint16 DYNAMIC_WOL_PORTS[] = {
|
||||||
|
47998, 47999, 48000, 48002, 48010, // Ports opened by GFE
|
||||||
|
};
|
||||||
|
|
||||||
// Add the addresses that we know this host to be
|
// Add the addresses that we know this host to be
|
||||||
// and broadcast addresses for this link just in
|
// and broadcast addresses for this link just in
|
||||||
// case the host has timed out in ARP entries.
|
// case the host has timed out in ARP entries.
|
||||||
QVector<QString> addressList;
|
QMap<QString, quint16> addressMap;
|
||||||
|
QSet<quint16> basePortSet;
|
||||||
for (const NvAddress& addr : uniqueAddresses()) {
|
for (const NvAddress& addr : uniqueAddresses()) {
|
||||||
addressList.append(addr.address());
|
addressMap.insert(addr.address(), addr.port());
|
||||||
|
basePortSet.insert(addr.port());
|
||||||
}
|
}
|
||||||
addressList.append("255.255.255.255");
|
addressMap.insert("255.255.255.255", 0);
|
||||||
|
|
||||||
// Try to broadcast on all available NICs
|
// Try to broadcast on all available NICs
|
||||||
for (const QNetworkInterface& nic : QNetworkInterface::allInterfaces()) {
|
for (const QNetworkInterface& nic : QNetworkInterface::allInterfaces()) {
|
||||||
@@ -254,31 +277,42 @@ bool NvComputer::wake() const
|
|||||||
|
|
||||||
// Skip IPv6 which doesn't support broadcast
|
// Skip IPv6 which doesn't support broadcast
|
||||||
if (!addr.broadcast().isNull()) {
|
if (!addr.broadcast().isNull()) {
|
||||||
addressList.append(addr.broadcast().toString());
|
addressMap.insert(addr.broadcast().toString(), 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!allNodesMulticast.scopeId().isEmpty()) {
|
if (!allNodesMulticast.scopeId().isEmpty()) {
|
||||||
addressList.append(allNodesMulticast.toString());
|
addressMap.insert(allNodesMulticast.toString(), 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try all unique address strings or host names
|
// Try all unique address strings or host names
|
||||||
bool success = false;
|
bool success = false;
|
||||||
for (QString& addressString : addressList) {
|
for (auto i = addressMap.constBegin(); i != addressMap.constEnd(); i++) {
|
||||||
QHostInfo hostInfo = QHostInfo::fromName(addressString);
|
QHostAddress literalAddress;
|
||||||
|
QList<QHostAddress> addressList;
|
||||||
|
|
||||||
if (hostInfo.error() != QHostInfo::NoError) {
|
// If this is an IPv4/IPv6 literal, don't use QHostInfo::fromName() because that will
|
||||||
qWarning() << "Error resolving" << addressString << ":" << hostInfo.errorString();
|
// try to perform a reverse DNS lookup that leads to delays sending WoL packets.
|
||||||
continue;
|
if (literalAddress.setAddress(i.key())) {
|
||||||
|
addressList.append(literalAddress);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
QHostInfo hostInfo = QHostInfo::fromName(i.key());
|
||||||
|
if (hostInfo.error() != QHostInfo::NoError) {
|
||||||
|
qWarning() << "Error resolving" << i.key() << ":" << hostInfo.errorString();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
addressList.append(hostInfo.addresses());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try all IP addresses that this string resolves to
|
// Try all IP addresses that this string resolves to
|
||||||
for (QHostAddress& address : hostInfo.addresses()) {
|
for (QHostAddress& address : addressList) {
|
||||||
QUdpSocket sock;
|
QUdpSocket sock;
|
||||||
|
|
||||||
// Send to all ports
|
// Send to all static ports
|
||||||
for (quint16 port : WOL_PORTS) {
|
for (quint16 port : STATIC_WOL_PORTS) {
|
||||||
if (sock.writeDatagram(wolPayload, address, port)) {
|
if (sock.writeDatagram(wolPayload, address, port)) {
|
||||||
qInfo().nospace().noquote() << "Sent WoL packet to " << name << " via " << address.toString() << ":" << port;
|
qInfo().nospace().noquote() << "Sent WoL packet to " << name << " via " << address.toString() << ":" << port;
|
||||||
success = true;
|
success = true;
|
||||||
@@ -287,6 +321,31 @@ bool NvComputer::wake() const
|
|||||||
qWarning() << "Send failed:" << sock.error();
|
qWarning() << "Send failed:" << sock.error();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QList<quint16> basePorts;
|
||||||
|
if (i.value() != 0) {
|
||||||
|
// If we have a known base port for this address, use only that port
|
||||||
|
basePorts.append(i.value());
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// If this is a broadcast address without a known HTTP port, try all of them
|
||||||
|
basePorts.append(basePortSet.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send to all dynamic ports using the HTTP port offset(s) for this address
|
||||||
|
for (quint16 basePort : basePorts) {
|
||||||
|
for (quint16 port : DYNAMIC_WOL_PORTS) {
|
||||||
|
port = (port - 47989) + basePort;
|
||||||
|
|
||||||
|
if (sock.writeDatagram(wolPayload, address, port)) {
|
||||||
|
qInfo().nospace().noquote() << "Sent WoL packet to " << name << " via " << address.toString() << ":" << port;
|
||||||
|
success = true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
qWarning() << "Send failed:" << sock.error();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -314,6 +373,7 @@ NvComputer::ReachabilityType NvComputer::getActiveAddressReachability() const
|
|||||||
s.connectToHost(copyOfActiveAddress.address(), copyOfActiveAddress.port());
|
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()) {
|
for (const QNetworkInterface& nic : QNetworkInterface::allInterfaces()) {
|
||||||
// Ensure the interface is up
|
// Ensure the interface is up
|
||||||
@@ -347,6 +407,19 @@ NvComputer::ReachabilityType NvComputer::getActiveAddressReachability() const
|
|||||||
return ReachabilityType::RI_VPN;
|
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 ReachabilityType::RI_VPN;
|
return ReachabilityType::RI_VPN;
|
||||||
@@ -362,9 +435,9 @@ NvComputer::ReachabilityType NvComputer::getActiveAddressReachability() const
|
|||||||
return ReachabilityType::RI_VPN;
|
return ReachabilityType::RI_VPN;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Didn't meet any of our VPN heuristics. Let's see if it's on-link.
|
// Didn't meet any of our VPN heuristics. Let's see if the peer address is on-link.
|
||||||
Q_ASSERT(addr.prefixLength() >= 0);
|
Q_ASSERT(addr.prefixLength() >= 0);
|
||||||
if (addr.prefixLength() >= 0 && s.localAddress().isInSubnet(addr.ip(), addr.prefixLength())) {
|
if (addr.prefixLength() >= 0 && s.localAddress().isInSubnet(s.peerAddress(), addr.prefixLength())) {
|
||||||
return ReachabilityType::RI_LAN;
|
return ReachabilityType::RI_LAN;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,16 @@
|
|||||||
#include <QSettings>
|
#include <QSettings>
|
||||||
#include <QRunnable>
|
#include <QRunnable>
|
||||||
|
|
||||||
|
class CopySafeReadWriteLock : public QReadWriteLock
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
CopySafeReadWriteLock() = default;
|
||||||
|
|
||||||
|
// Don't actually copy the QReadWriteLock
|
||||||
|
CopySafeReadWriteLock(const CopySafeReadWriteLock&) : QReadWriteLock() {}
|
||||||
|
CopySafeReadWriteLock& operator=(const CopySafeReadWriteLock &) { return *this; }
|
||||||
|
};
|
||||||
|
|
||||||
class NvComputer
|
class NvComputer
|
||||||
{
|
{
|
||||||
friend class PcMonitorThread;
|
friend class PcMonitorThread;
|
||||||
@@ -22,6 +32,14 @@ private:
|
|||||||
bool pendingQuit;
|
bool pendingQuit;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
NvComputer() = default;
|
||||||
|
|
||||||
|
// Caller is responsible for synchronizing read access to the other host
|
||||||
|
NvComputer(const NvComputer&) = default;
|
||||||
|
|
||||||
|
// Caller is responsible for synchronizing read access to the other host
|
||||||
|
NvComputer& operator=(const NvComputer &) = default;
|
||||||
|
|
||||||
explicit NvComputer(NvHTTP& http, QString serverInfo);
|
explicit NvComputer(NvHTTP& http, QString serverInfo);
|
||||||
|
|
||||||
explicit NvComputer(QSettings& settings);
|
explicit NvComputer(QSettings& settings);
|
||||||
@@ -49,7 +67,11 @@ public:
|
|||||||
uniqueAddresses() const;
|
uniqueAddresses() const;
|
||||||
|
|
||||||
void
|
void
|
||||||
serialize(QSettings& settings) const;
|
serialize(QSettings& settings, bool serializeApps) const;
|
||||||
|
|
||||||
|
// Caller is responsible for synchronizing read access to both hosts
|
||||||
|
bool
|
||||||
|
isEqualSerialized(const NvComputer& that) const;
|
||||||
|
|
||||||
enum PairState
|
enum PairState
|
||||||
{
|
{
|
||||||
@@ -91,9 +113,10 @@ public:
|
|||||||
QSslCertificate serverCert;
|
QSslCertificate serverCert;
|
||||||
QVector<NvApp> appList;
|
QVector<NvApp> appList;
|
||||||
bool isNvidiaServerSoftware;
|
bool isNvidiaServerSoftware;
|
||||||
|
// Remember to update isEqualSerialized() when adding fields here!
|
||||||
|
|
||||||
// Synchronization
|
// Synchronization
|
||||||
mutable QReadWriteLock lock;
|
mutable CopySafeReadWriteLock lock;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
uint16_t externalPort;
|
uint16_t externalPort;
|
||||||
|
|||||||
+18
-37
@@ -187,12 +187,15 @@ NvHTTP::getServerInfo(NvLogLevel logLevel, bool fastFail)
|
|||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
NvHTTP::launchApp(int appId,
|
NvHTTP::startApp(QString verb,
|
||||||
PSTREAM_CONFIGURATION streamConfig,
|
bool isGfe,
|
||||||
bool sops,
|
int appId,
|
||||||
bool localAudio,
|
PSTREAM_CONFIGURATION streamConfig,
|
||||||
int gamepadMask,
|
bool sops,
|
||||||
QString& rtspSessionUrl)
|
bool localAudio,
|
||||||
|
int gamepadMask,
|
||||||
|
bool persistGameControllersOnDisconnect,
|
||||||
|
QString& rtspSessionUrl)
|
||||||
{
|
{
|
||||||
int riKeyId;
|
int riKeyId;
|
||||||
|
|
||||||
@@ -201,25 +204,27 @@ NvHTTP::launchApp(int appId,
|
|||||||
|
|
||||||
QString response =
|
QString response =
|
||||||
openConnectionToString(m_BaseUrlHttps,
|
openConnectionToString(m_BaseUrlHttps,
|
||||||
"launch",
|
verb,
|
||||||
"appid="+QString::number(appId)+
|
"appid="+QString::number(appId)+
|
||||||
"&mode="+QString::number(streamConfig->width)+"x"+
|
"&mode="+QString::number(streamConfig->width)+"x"+
|
||||||
QString::number(streamConfig->height)+"x"+
|
QString::number(streamConfig->height)+"x"+
|
||||||
// Using an FPS value over 60 causes SOPS to default to 720p60,
|
// Using an FPS value over 60 causes SOPS to default to 720p60,
|
||||||
// so force it to 0 to ensure the correct resolution is set. We
|
// so force it to 0 to ensure the correct resolution is set. We
|
||||||
// used to use 60 here but that locked the frame rate to 60 FPS
|
// used to use 60 here but that locked the frame rate to 60 FPS
|
||||||
// on GFE 3.20.3.
|
// on GFE 3.20.3. We don't need this hack for Sunshine.
|
||||||
QString::number(streamConfig->fps > 60 ? 0 : streamConfig->fps)+
|
QString::number((streamConfig->fps > 60 && isGfe) ? 0 : streamConfig->fps)+
|
||||||
"&additionalStates=1&sops="+QString::number(sops ? 1 : 0)+
|
"&additionalStates=1&sops="+QString::number(sops ? 1 : 0)+
|
||||||
"&rikey="+QByteArray(streamConfig->remoteInputAesKey, sizeof(streamConfig->remoteInputAesKey)).toHex()+
|
"&rikey="+QByteArray(streamConfig->remoteInputAesKey, sizeof(streamConfig->remoteInputAesKey)).toHex()+
|
||||||
"&rikeyid="+QString::number(riKeyId)+
|
"&rikeyid="+QString::number(riKeyId)+
|
||||||
(streamConfig->enableHdr ?
|
((streamConfig->supportedVideoFormats & VIDEO_FORMAT_MASK_10BIT) ?
|
||||||
"&hdrMode=1&clientHdrCapVersion=0&clientHdrCapSupportedFlagsInUint32=0&clientHdrCapMetaDataId=NV_STATIC_METADATA_TYPE_1&clientHdrCapDisplayData=0x0x0x0x0x0x0x0x0x0x0" :
|
"&hdrMode=1&clientHdrCapVersion=0&clientHdrCapSupportedFlagsInUint32=0&clientHdrCapMetaDataId=NV_STATIC_METADATA_TYPE_1&clientHdrCapDisplayData=0x0x0x0x0x0x0x0x0x0x0" :
|
||||||
"")+
|
"")+
|
||||||
"&localAudioPlayMode="+QString::number(localAudio ? 1 : 0)+
|
"&localAudioPlayMode="+QString::number(localAudio ? 1 : 0)+
|
||||||
"&surroundAudioInfo="+QString::number(SURROUNDAUDIOINFO_FROM_AUDIO_CONFIGURATION(streamConfig->audioConfiguration))+
|
"&surroundAudioInfo="+QString::number(SURROUNDAUDIOINFO_FROM_AUDIO_CONFIGURATION(streamConfig->audioConfiguration))+
|
||||||
"&remoteControllersBitmap="+QString::number(gamepadMask)+
|
"&remoteControllersBitmap="+QString::number(gamepadMask)+
|
||||||
"&gcmap="+QString::number(gamepadMask),
|
"&gcmap="+QString::number(gamepadMask)+
|
||||||
|
"&gcpersist="+QString::number(persistGameControllersOnDisconnect ? 1 : 0)+
|
||||||
|
LiGetLaunchUrlQueryParameters(),
|
||||||
LAUNCH_TIMEOUT_MS);
|
LAUNCH_TIMEOUT_MS);
|
||||||
|
|
||||||
qInfo() << "Launch response:" << response;
|
qInfo() << "Launch response:" << response;
|
||||||
@@ -230,30 +235,6 @@ NvHTTP::launchApp(int appId,
|
|||||||
rtspSessionUrl = getXmlString(response, "sessionUrl0");
|
rtspSessionUrl = getXmlString(response, "sessionUrl0");
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
|
||||||
NvHTTP::resumeApp(PSTREAM_CONFIGURATION streamConfig, QString& rtspSessionUrl)
|
|
||||||
{
|
|
||||||
int riKeyId;
|
|
||||||
|
|
||||||
memcpy(&riKeyId, streamConfig->remoteInputAesIv, sizeof(riKeyId));
|
|
||||||
riKeyId = qFromBigEndian(riKeyId);
|
|
||||||
|
|
||||||
QString response =
|
|
||||||
openConnectionToString(m_BaseUrlHttps,
|
|
||||||
"resume",
|
|
||||||
"rikey="+QString(QByteArray(streamConfig->remoteInputAesKey, sizeof(streamConfig->remoteInputAesKey)).toHex())+
|
|
||||||
"&rikeyid="+QString::number(riKeyId)+
|
|
||||||
"&surroundAudioInfo="+QString::number(SURROUNDAUDIOINFO_FROM_AUDIO_CONFIGURATION(streamConfig->audioConfiguration)),
|
|
||||||
RESUME_TIMEOUT_MS);
|
|
||||||
|
|
||||||
qInfo() << "Resume response:" << response;
|
|
||||||
|
|
||||||
// Throws if the request failed
|
|
||||||
verifyResponseStatus(response);
|
|
||||||
|
|
||||||
rtspSessionUrl = getXmlString(response, "sessionUrl0");
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
void
|
||||||
NvHTTP::quitApp()
|
NvHTTP::quitApp()
|
||||||
{
|
{
|
||||||
@@ -269,7 +250,7 @@ NvHTTP::quitApp()
|
|||||||
verifyResponseStatus(response);
|
verifyResponseStatus(response);
|
||||||
|
|
||||||
// Newer GFE versions will just return success even if quitting fails
|
// Newer GFE versions will just return success even if quitting fails
|
||||||
// if we're not the original requestor.
|
// if we're not the original requester.
|
||||||
if (getCurrentGame(getServerInfo(NvHTTP::NVLL_ERROR)) != 0) {
|
if (getCurrentGame(getServerInfo(NvHTTP::NVLL_ERROR)) != 0) {
|
||||||
// Generate a synthetic GfeResponseException letting the caller know
|
// Generate a synthetic GfeResponseException letting the caller know
|
||||||
// that they can't kill someone else's stream.
|
// that they can't kill someone else's stream.
|
||||||
@@ -382,7 +363,7 @@ NvHTTP::verifyResponseStatus(QString xml)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
throw GfeHttpResponseException(-1, "Malformed GFE XML (missing root element)");
|
throw GfeHttpResponseException(-1, "Malformed XML (missing root element)");
|
||||||
}
|
}
|
||||||
|
|
||||||
QImage
|
QImage
|
||||||
|
|||||||
+9
-10
@@ -162,16 +162,15 @@ public:
|
|||||||
quitApp();
|
quitApp();
|
||||||
|
|
||||||
void
|
void
|
||||||
resumeApp(PSTREAM_CONFIGURATION streamConfig,
|
startApp(QString verb,
|
||||||
QString& rtspSessionUrl);
|
bool isGfe,
|
||||||
|
int appId,
|
||||||
void
|
PSTREAM_CONFIGURATION streamConfig,
|
||||||
launchApp(int appId,
|
bool sops,
|
||||||
PSTREAM_CONFIGURATION streamConfig,
|
bool localAudio,
|
||||||
bool sops,
|
int gamepadMask,
|
||||||
bool localAudio,
|
bool persistGameControllersOnDisconnect,
|
||||||
int gamepadMask,
|
QString& rtspSessionUrl);
|
||||||
QString& rtspSessionUrl);
|
|
||||||
|
|
||||||
QVector<NvApp>
|
QVector<NvApp>
|
||||||
getAppList();
|
getAppList();
|
||||||
|
|||||||
@@ -298,8 +298,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,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
#include "utils.h"
|
#include "utils.h"
|
||||||
|
|
||||||
#include <QGuiApplication>
|
#include <QGuiApplication>
|
||||||
|
#include <QLibraryInfo>
|
||||||
|
|
||||||
#include "streaming/session.h"
|
#include "streaming/session.h"
|
||||||
#include "streaming/streamutils.h"
|
#include "streaming/streamutils.h"
|
||||||
@@ -17,6 +18,7 @@ SystemProperties::SystemProperties()
|
|||||||
hasDesktopEnvironment = WMUtils::isRunningDesktopEnvironment();
|
hasDesktopEnvironment = WMUtils::isRunningDesktopEnvironment();
|
||||||
isRunningWayland = WMUtils::isRunningWayland();
|
isRunningWayland = WMUtils::isRunningWayland();
|
||||||
isRunningXWayland = isRunningWayland && QGuiApplication::platformName() == "xcb";
|
isRunningXWayland = isRunningWayland && QGuiApplication::platformName() == "xcb";
|
||||||
|
usesMaterial3Theme = QLibraryInfo::version() >= QVersionNumber(6, 5, 0);
|
||||||
QString nativeArch = QSysInfo::currentCpuArchitecture();
|
QString nativeArch = QSysInfo::currentCpuArchitecture();
|
||||||
|
|
||||||
#ifdef Q_OS_WIN32
|
#ifdef Q_OS_WIN32
|
||||||
@@ -70,8 +72,9 @@ SystemProperties::SystemProperties()
|
|||||||
// and cache the results to speed up future queries on this data.
|
// and cache the results to speed up future queries on this data.
|
||||||
querySdlVideoInfo();
|
querySdlVideoInfo();
|
||||||
|
|
||||||
Q_ASSERT(maximumStreamingFrameRate >= 60);
|
Q_ASSERT(!monitorRefreshRates.isEmpty());
|
||||||
Q_ASSERT(!monitorNativeResolutions.isEmpty());
|
Q_ASSERT(!monitorNativeResolutions.isEmpty());
|
||||||
|
Q_ASSERT(!monitorSafeAreaResolutions.isEmpty());
|
||||||
}
|
}
|
||||||
|
|
||||||
QRect SystemProperties::getNativeResolution(int displayIndex)
|
QRect SystemProperties::getNativeResolution(int displayIndex)
|
||||||
@@ -80,6 +83,18 @@ QRect SystemProperties::getNativeResolution(int displayIndex)
|
|||||||
return monitorNativeResolutions.value(displayIndex);
|
return monitorNativeResolutions.value(displayIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QRect SystemProperties::getSafeAreaResolution(int displayIndex)
|
||||||
|
{
|
||||||
|
// Returns default constructed QRect if out of bounds
|
||||||
|
return monitorSafeAreaResolutions.value(displayIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
int SystemProperties::getRefreshRate(int displayIndex)
|
||||||
|
{
|
||||||
|
// Returns 0 if out of bounds
|
||||||
|
return monitorRefreshRates.value(displayIndex);
|
||||||
|
}
|
||||||
|
|
||||||
class QuerySdlVideoThread : public QThread
|
class QuerySdlVideoThread : public QThread
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
@@ -188,16 +203,15 @@ void SystemProperties::refreshDisplaysInternal()
|
|||||||
|
|
||||||
monitorNativeResolutions.clear();
|
monitorNativeResolutions.clear();
|
||||||
|
|
||||||
// Never let the maximum drop below 60 FPS
|
|
||||||
maximumStreamingFrameRate = 60;
|
|
||||||
|
|
||||||
SDL_DisplayMode bestMode;
|
SDL_DisplayMode bestMode;
|
||||||
for (int displayIndex = 0; displayIndex < SDL_GetNumVideoDisplays(); displayIndex++) {
|
for (int displayIndex = 0; displayIndex < SDL_GetNumVideoDisplays(); displayIndex++) {
|
||||||
SDL_DisplayMode desktopMode;
|
SDL_DisplayMode desktopMode;
|
||||||
|
SDL_Rect safeArea;
|
||||||
|
|
||||||
if (StreamUtils::getNativeDesktopMode(displayIndex, &desktopMode)) {
|
if (StreamUtils::getNativeDesktopMode(displayIndex, &desktopMode, &safeArea)) {
|
||||||
if (desktopMode.w <= 8192 && desktopMode.h <= 8192) {
|
if (desktopMode.w <= 8192 && desktopMode.h <= 8192) {
|
||||||
monitorNativeResolutions.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 {
|
else {
|
||||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
@@ -218,7 +232,17 @@ void SystemProperties::refreshDisplaysInternal()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
maximumStreamingFrameRate = qMax(maximumStreamingFrameRate, bestMode.refresh_rate);
|
// Try to normalize values around our our standard refresh rates.
|
||||||
|
// Some displays/OSes report values that are slightly off.
|
||||||
|
if (bestMode.refresh_rate >= 58 && bestMode.refresh_rate <= 62) {
|
||||||
|
monitorRefreshRates.append(60);
|
||||||
|
}
|
||||||
|
else if (bestMode.refresh_rate >= 28 && bestMode.refresh_rate <= 32) {
|
||||||
|
monitorRefreshRates.append(30);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
monitorRefreshRates.append(bestMode.refresh_rate);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,13 +23,15 @@ public:
|
|||||||
Q_PROPERTY(bool hasBrowser MEMBER hasBrowser CONSTANT)
|
Q_PROPERTY(bool hasBrowser MEMBER hasBrowser CONSTANT)
|
||||||
Q_PROPERTY(bool hasDiscordIntegration MEMBER hasDiscordIntegration CONSTANT)
|
Q_PROPERTY(bool hasDiscordIntegration MEMBER hasDiscordIntegration CONSTANT)
|
||||||
Q_PROPERTY(QString unmappedGamepads MEMBER unmappedGamepads NOTIFY unmappedGamepadsChanged)
|
Q_PROPERTY(QString unmappedGamepads MEMBER unmappedGamepads NOTIFY unmappedGamepadsChanged)
|
||||||
Q_PROPERTY(int maximumStreamingFrameRate MEMBER maximumStreamingFrameRate CONSTANT)
|
|
||||||
Q_PROPERTY(QSize maximumResolution MEMBER maximumResolution CONSTANT)
|
Q_PROPERTY(QSize maximumResolution MEMBER maximumResolution CONSTANT)
|
||||||
Q_PROPERTY(QString versionString MEMBER versionString CONSTANT)
|
Q_PROPERTY(QString versionString MEMBER versionString CONSTANT)
|
||||||
Q_PROPERTY(bool supportsHdr MEMBER supportsHdr CONSTANT)
|
Q_PROPERTY(bool supportsHdr MEMBER supportsHdr CONSTANT)
|
||||||
|
Q_PROPERTY(bool usesMaterial3Theme MEMBER usesMaterial3Theme CONSTANT)
|
||||||
|
|
||||||
Q_INVOKABLE void refreshDisplays();
|
Q_INVOKABLE void refreshDisplays();
|
||||||
Q_INVOKABLE QRect getNativeResolution(int displayIndex);
|
Q_INVOKABLE QRect getNativeResolution(int displayIndex);
|
||||||
|
Q_INVOKABLE QRect getSafeAreaResolution(int displayIndex);
|
||||||
|
Q_INVOKABLE int getRefreshRate(int displayIndex);
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void unmappedGamepadsChanged();
|
void unmappedGamepadsChanged();
|
||||||
@@ -49,10 +51,12 @@ private:
|
|||||||
bool hasBrowser;
|
bool hasBrowser;
|
||||||
bool hasDiscordIntegration;
|
bool hasDiscordIntegration;
|
||||||
QString unmappedGamepads;
|
QString unmappedGamepads;
|
||||||
int maximumStreamingFrameRate;
|
|
||||||
QSize maximumResolution;
|
QSize maximumResolution;
|
||||||
QList<QRect> monitorNativeResolutions;
|
QList<QRect> monitorNativeResolutions;
|
||||||
|
QList<QRect> monitorSafeAreaResolutions;
|
||||||
|
QList<int> monitorRefreshRates;
|
||||||
QString versionString;
|
QString versionString;
|
||||||
bool supportsHdr;
|
bool supportsHdr;
|
||||||
|
bool usesMaterial3Theme;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -316,6 +316,7 @@ StreamCommandLineParser::StreamCommandLineParser()
|
|||||||
{"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},
|
||||||
@@ -371,6 +372,9 @@ void StreamCommandLineParser::parse(const QStringList &args, StreamingPreference
|
|||||||
parser.addToggleOption("reverse-scroll-direction", "inverted scroll direction");
|
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("swap-gamepad-buttons", "swap A/B and X/Y gamepad buttons (Nintendo-style)");
|
||||||
parser.addToggleOption("keep-awake", "prevent display sleep while streaming");
|
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("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());
|
||||||
@@ -409,20 +413,20 @@ 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, 240)) {
|
if (!inRange(preferences->fps, 10, 480)) {
|
||||||
parser.showError("FPS must be in range: 30 - 240");
|
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
|
// Resolve --packet-size option
|
||||||
@@ -485,6 +489,15 @@ void StreamCommandLineParser::parse(const QStringList &args, StreamingPreference
|
|||||||
// Resolve --keep-awake and --no-keep-awake options
|
// Resolve --keep-awake and --no-keep-awake options
|
||||||
preferences->keepAwake = parser.getToggleOptionValue("keep-awake", preferences->keepAwake);
|
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
|
// Resolve --capture-system-keys option
|
||||||
if (parser.isSet("capture-system-keys")) {
|
if (parser.isSet("capture-system-keys")) {
|
||||||
preferences->captureSysKeysMode = mapValue(m_CaptureSysKeysModeMap, parser.getChoiceOptionValue("capture-system-keys"));
|
preferences->captureSysKeysMode = mapValue(m_CaptureSysKeysModeMap, parser.getChoiceOptionValue("capture-system-keys"));
|
||||||
|
|||||||
@@ -7,7 +7,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 CliListApps
|
namespace CliListApps
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -118,7 +118,7 @@ public:
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
// Occurs when the previous app quit has been completed, handles quitting errors if any
|
// Occurs when the previous app quit has been completed, handles quitting errors if any
|
||||||
// happended. ComputerUpdated event's handler handles session start when previous app has
|
// happened. ComputerUpdated event's handler handles session start when previous app has
|
||||||
// quit.
|
// quit.
|
||||||
case Event::AppQuitCompleted:
|
case Event::AppQuitCompleted:
|
||||||
if (m_State == StateSeekApp && !event.errorMessage.isEmpty()) {
|
if (m_State == StateSeekApp && !event.errorMessage.isEmpty()) {
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
From 78261eb6e2fe728fa5d3843b5b8962b1ca559b7e Mon Sep 17 00:00:00 2001
|
||||||
|
From: David Rosca <nowrep@gmail.com>
|
||||||
|
Date: Sat, 16 Dec 2023 14:52:01 +0100
|
||||||
|
Subject: [PATCH] Vulkan: Don't try to reuse old swapchain
|
||||||
|
|
||||||
|
---
|
||||||
|
src/vulkan/swapchain.c | 12 ++++++++++--
|
||||||
|
1 file changed, 10 insertions(+), 2 deletions(-)
|
||||||
|
|
||||||
|
diff --git a/src/vulkan/swapchain.c b/src/vulkan/swapchain.c
|
||||||
|
index 0646e57c..c0b07ce4 100644
|
||||||
|
--- a/src/vulkan/swapchain.c
|
||||||
|
+++ b/src/vulkan/swapchain.c
|
||||||
|
@@ -612,13 +612,21 @@ static bool vk_sw_recreate(pl_swapchain sw, int w, int h)
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
+ {
|
||||||
|
+ pl_gpu_flush(gpu);
|
||||||
|
+ vk_wait_idle(vk);
|
||||||
|
+ for (int i = 0; i < vk->pool_graphics->num_queues; i++)
|
||||||
|
+ vk->QueueWaitIdle(vk->pool_graphics->queues[i]);
|
||||||
|
+ vk->DestroySwapchainKHR(vk->dev, p->swapchain, PL_VK_ALLOC);
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
// Calling `vkCreateSwapchainKHR` puts sinfo.oldSwapchain into a retired
|
||||||
|
// state whether the call succeeds or not, so we always need to garbage
|
||||||
|
// collect it afterwards - asynchronously as it may still be in use
|
||||||
|
- sinfo.oldSwapchain = p->swapchain;
|
||||||
|
+ /* sinfo.oldSwapchain = p->swapchain; */
|
||||||
|
p->swapchain = VK_NULL_HANDLE;
|
||||||
|
VkResult res = vk->CreateSwapchainKHR(vk->dev, &sinfo, PL_VK_ALLOC, &p->swapchain);
|
||||||
|
- vk_dev_callback(vk, VK_CB_FUNC(destroy_swapchain), vk, vk_wrap_handle(sinfo.oldSwapchain));
|
||||||
|
+ /* vk_dev_callback(vk, VK_CB_FUNC(destroy_swapchain), vk, vk_wrap_handle(sinfo.oldSwapchain)); */
|
||||||
|
PL_VK_ASSERT(res, "vk->CreateSwapchainKHR(...)");
|
||||||
|
|
||||||
|
// Get the new swapchain images
|
||||||
|
--
|
||||||
|
2.43.0
|
||||||
|
|
||||||
@@ -4,21 +4,19 @@
|
|||||||
<metadata_license>CC0-1.0</metadata_license>
|
<metadata_license>CC0-1.0</metadata_license>
|
||||||
<project_license>GPL-3.0+</project_license>
|
<project_license>GPL-3.0+</project_license>
|
||||||
<name>Moonlight</name>
|
<name>Moonlight</name>
|
||||||
<summary>Play games remotely from your NVIDIA GameStream-enabled PC</summary>
|
<summary>Stream games and other applications from another PC running Sunshine or GeForce Experience</summary>
|
||||||
|
|
||||||
<description>
|
<description>
|
||||||
<p>Moonlight can stream games and other applications from a PC with an NVIDIA GeForce GTX/RTX or NVIDIA Quadro GPU and GeForce/Quadro Experience installed.</p>
|
<p>Moonlight can stream games and other applications from another PC with Sunshine or GeForce Experience software installed.</p>
|
||||||
<p>Features include:</p>
|
<p>Features include:</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>Streaming at up to 4K resolution</li>
|
|
||||||
<li>Support for up to 120 FPS streaming (high refresh rate monitor recommended)</li>
|
|
||||||
<li>Hardware accelerated video decoding with VAAPI, VDPAU, and NVDEC support</li>
|
<li>Hardware accelerated video decoding with VAAPI, VDPAU, and NVDEC support</li>
|
||||||
|
<li>H.264, HEVC, and AV1 codec support</li>
|
||||||
<li>7.1 surround sound audio support</li>
|
<li>7.1 surround sound audio support</li>
|
||||||
|
<li>10-point multitouch support</li>
|
||||||
|
<li>Gamepad support with force feedback for up to 16 players</li>
|
||||||
<li>Support for both pointer capture (for games) and direct mouse control (for remote desktop)</li>
|
<li>Support for both pointer capture (for games) and direct mouse control (for remote desktop)</li>
|
||||||
<li>Support for passing system-wide keyboard shortcuts like Alt+Tab to the host</li>
|
<li>Support for passing system-wide keyboard shortcuts like Alt+Tab to the host</li>
|
||||||
<li>Direct touchscreen mouse control</li>
|
|
||||||
<li>HEVC support for better video compression efficiency</li>
|
|
||||||
<li>Gamepad support with force feedback for up to 4 players</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</description>
|
</description>
|
||||||
|
|
||||||
@@ -36,6 +34,137 @@
|
|||||||
</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">
|
<release version="4.3.1" date="2022-11-03">
|
||||||
<description>
|
<description>
|
||||||
<p>Changes:</p>
|
<p>Changes:</p>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
[Desktop Entry]
|
[Desktop Entry]
|
||||||
Name=Moonlight
|
Name=Moonlight
|
||||||
Comment=Stream games from your NVIDIA GameStream-enabled PC
|
Comment=Stream games and other applications from another PC running Sunshine or GeForce Experience
|
||||||
Exec=moonlight
|
Exec=moonlight
|
||||||
Icon=moonlight
|
Icon=moonlight
|
||||||
Terminal=false
|
Terminal=false
|
||||||
Type=Application
|
Type=Application
|
||||||
Categories=Qt;Game;
|
Categories=Qt;Game;
|
||||||
Keywords=nvidia;gamestream;stream;
|
Keywords=nvidia;gamestream;stream;sunshine;remote play;
|
||||||
@@ -5,6 +5,10 @@
|
|||||||
# 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)
|
||||||
|
|
||||||
|
|||||||
+16
-14
@@ -1,5 +1,6 @@
|
|||||||
import QtQuick 2.9
|
import QtQuick 2.9
|
||||||
import QtQuick.Controls 2.2
|
import QtQuick.Controls 2.2
|
||||||
|
import QtQuick.Controls.Material 2.2
|
||||||
|
|
||||||
import AppModel 1.0
|
import AppModel 1.0
|
||||||
import ComputerManager 1.0
|
import ComputerManager 1.0
|
||||||
@@ -89,7 +90,7 @@ CenteredGridView {
|
|||||||
|
|
||||||
onSourceSizeChanged: {
|
onSourceSizeChanged: {
|
||||||
// Nearly all of Nvidia's official box art does not match the dimensions of placeholder
|
// Nearly all of Nvidia's official box art does not match the dimensions of placeholder
|
||||||
// images, however the one known exeception is Overcooked. Therefore, we only execute
|
// images, however the one known exception is Overcooked. Therefore, we only execute
|
||||||
// the image size checks if this is not an app collector game. We know the officially
|
// the image size checks if this is not an app collector game. We know the officially
|
||||||
// supported games all have box art, so this check is not required.
|
// supported games all have box art, so this check is not required.
|
||||||
if (!model.isAppCollectorGame &&
|
if (!model.isAppCollectorGame &&
|
||||||
@@ -121,15 +122,15 @@ CenteredGridView {
|
|||||||
anchors.fill: appIcon
|
anchors.fill: appIcon
|
||||||
|
|
||||||
sourceComponent: Item {
|
sourceComponent: Item {
|
||||||
ToolButton {
|
RoundButton {
|
||||||
anchors.horizontalCenterOffset: appIcon.isPlaceholder ? -47 : 0
|
anchors.horizontalCenterOffset: appIcon.isPlaceholder ? -47 : 0
|
||||||
anchors.verticalCenterOffset: appIcon.isPlaceholder ? -75 : -60
|
anchors.verticalCenterOffset: appIcon.isPlaceholder ? -75 : -60
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
implicitWidth: 125
|
implicitWidth: 85
|
||||||
implicitHeight: 125
|
implicitHeight: 85
|
||||||
|
|
||||||
Image {
|
Image {
|
||||||
source: "qrc:/res/baseline-play_circle_filled_white-48px.svg"
|
source: "qrc:/res/play_arrow_FILL1_wght700_GRAD200_opsz48.svg"
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
sourceSize {
|
sourceSize {
|
||||||
width: 75
|
width: 75
|
||||||
@@ -145,17 +146,19 @@ CenteredGridView {
|
|||||||
ToolTip.delay: 1000
|
ToolTip.delay: 1000
|
||||||
ToolTip.timeout: 3000
|
ToolTip.timeout: 3000
|
||||||
ToolTip.visible: hovered
|
ToolTip.visible: hovered
|
||||||
|
|
||||||
|
Material.background: "#D0808080"
|
||||||
}
|
}
|
||||||
|
|
||||||
ToolButton {
|
RoundButton {
|
||||||
anchors.horizontalCenterOffset: appIcon.isPlaceholder ? 47 : 0
|
anchors.horizontalCenterOffset: appIcon.isPlaceholder ? 47 : 0
|
||||||
anchors.verticalCenterOffset: appIcon.isPlaceholder ? -75 : 60
|
anchors.verticalCenterOffset: appIcon.isPlaceholder ? -75 : 60
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
implicitWidth: 125
|
implicitWidth: 85
|
||||||
implicitHeight: 125
|
implicitHeight: 85
|
||||||
|
|
||||||
Image {
|
Image {
|
||||||
source: "qrc:/res/baseline-cancel-24px.svg"
|
source: "qrc:/res/stop_FILL1_wght700_GRAD200_opsz48.svg"
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
sourceSize {
|
sourceSize {
|
||||||
width: 75
|
width: 75
|
||||||
@@ -171,6 +174,8 @@ CenteredGridView {
|
|||||||
ToolTip.delay: 1000
|
ToolTip.delay: 1000
|
||||||
ToolTip.timeout: 3000
|
ToolTip.timeout: 3000
|
||||||
ToolTip.visible: hovered
|
ToolTip.visible: hovered
|
||||||
|
|
||||||
|
Material.background: "#D0808080"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -251,7 +256,7 @@ CenteredGridView {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
acceptedButtons: Qt.RightButton;
|
acceptedButtons: Qt.RightButton;
|
||||||
onClicked: {
|
onClicked: {
|
||||||
parent.onPressAndHold()
|
parent.pressAndHold()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -345,7 +350,7 @@ CenteredGridView {
|
|||||||
|
|
||||||
function quitApp() {
|
function quitApp() {
|
||||||
var component = Qt.createComponent("QuitSegue.qml")
|
var component = Qt.createComponent("QuitSegue.qml")
|
||||||
var params = {"appName": appName}
|
var params = {"appName": appName, "quitRunningAppFn": 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.
|
||||||
@@ -358,9 +363,6 @@ CenteredGridView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
stackView.push(component.createObject(stackView, params))
|
stackView.push(component.createObject(stackView, params))
|
||||||
|
|
||||||
// Trigger the quit after pushing the quit segue on screen
|
|
||||||
appModel.quitRunningApp()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onAccepted: quitApp()
|
onAccepted: quitApp()
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import QtQuick 2.9
|
|||||||
import QtQuick.Controls 2.2
|
import QtQuick.Controls 2.2
|
||||||
|
|
||||||
import SdlGamepadKeyNavigation 1.0
|
import SdlGamepadKeyNavigation 1.0
|
||||||
|
import SystemProperties 1.0
|
||||||
|
|
||||||
// https://stackoverflow.com/questions/45029968/how-do-i-set-the-combobox-width-to-fit-the-largest-item
|
// https://stackoverflow.com/questions/45029968/how-do-i-set-the-combobox-width-to-fit-the-largest-item
|
||||||
ComboBox {
|
ComboBox {
|
||||||
@@ -38,6 +39,12 @@ ComboBox {
|
|||||||
popup.onAboutToShow: {
|
popup.onAboutToShow: {
|
||||||
// Switch to normal navigation for combo boxes
|
// Switch to normal navigation for combo boxes
|
||||||
SdlGamepadKeyNavigation.setUiNavMode(false)
|
SdlGamepadKeyNavigation.setUiNavMode(false)
|
||||||
|
|
||||||
|
// Override the popup color to improve contrast with the overridden
|
||||||
|
// Material 2 background color set in main.qml.
|
||||||
|
if (SystemProperties.usesMaterial3Theme) {
|
||||||
|
popup.background.color = "#424242"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
popup.onAboutToHide: {
|
popup.onAboutToHide: {
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ Item {
|
|||||||
function onLaunchFailed(message) {
|
function onLaunchFailed(message) {
|
||||||
errorDialog.text = message
|
errorDialog.text = message
|
||||||
errorDialog.open()
|
errorDialog.open()
|
||||||
|
console.error(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
function onAppQuitRequired(appName) {
|
function onAppQuitRequired(appName) {
|
||||||
@@ -84,10 +85,8 @@ Item {
|
|||||||
|
|
||||||
function quitApp() {
|
function quitApp() {
|
||||||
var component = Qt.createComponent("QuitSegue.qml")
|
var component = Qt.createComponent("QuitSegue.qml")
|
||||||
var params = {"appName": appName}
|
var params = {"appName": appName, "quitRunningAppFn": 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()
|
||||||
|
|||||||
+38
-16
@@ -6,6 +6,7 @@ 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 {
|
||||||
@@ -99,7 +100,7 @@ CenteredGridView {
|
|||||||
Label {
|
Label {
|
||||||
height: searchSpinner.height
|
height: searchSpinner.height
|
||||||
elide: Label.ElideRight
|
elide: Label.ElideRight
|
||||||
text: StreamingPreferences.enableMdns ? qsTr("Searching for PCs on your local network with NVIDIA GameStream enabled...")
|
text: StreamingPreferences.enableMdns ? qsTr("Searching for compatible hosts on your local network...")
|
||||||
: qsTr("Automatic PC discovery is disabled. Add your PC manually.")
|
: qsTr("Automatic PC discovery is disabled. Add your PC manually.")
|
||||||
font.pointSize: 20
|
font.pointSize: 20
|
||||||
verticalAlignment: Text.AlignVCenter
|
verticalAlignment: Text.AlignVCenter
|
||||||
@@ -130,12 +131,12 @@ CenteredGridView {
|
|||||||
id: stateIcon
|
id: stateIcon
|
||||||
anchors.horizontalCenter: pcIcon.horizontalCenter
|
anchors.horizontalCenter: pcIcon.horizontalCenter
|
||||||
anchors.verticalCenter: pcIcon.verticalCenter
|
anchors.verticalCenter: pcIcon.verticalCenter
|
||||||
anchors.verticalCenterOffset: -15
|
anchors.verticalCenterOffset: !model.online ? -18 : -16
|
||||||
visible: !model.statusUnknown && (!model.online || !model.paired)
|
visible: !model.statusUnknown && (!model.online || !model.paired)
|
||||||
source: !model.online ? "qrc:/res/baseline-warning-24px.svg" : "qrc:/res/baseline-lock-24px.svg"
|
source: !model.online ? "qrc:/res/warning_FILL1_wght300_GRAD200_opsz24.svg" : "qrc:/res/baseline-lock-24px.svg"
|
||||||
sourceSize {
|
sourceSize {
|
||||||
width: 75
|
width: !model.online ? 75 : 70
|
||||||
height: 75
|
height: !model.online ? 75 : 70
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,10 +212,18 @@ CenteredGridView {
|
|||||||
text: qsTr("Delete PC")
|
text: qsTr("Delete PC")
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
deletePcDialog.pcIndex = index
|
deletePcDialog.pcIndex = index
|
||||||
// get confirmation first, actual closing is called from the dialog
|
deletePcDialog.pcName = model.name
|
||||||
deletePcDialog.open()
|
deletePcDialog.open()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
NavigableMenuItem {
|
||||||
|
parentMenu: pcContextMenu
|
||||||
|
text: qsTr("View Details")
|
||||||
|
onTriggered: {
|
||||||
|
showPcDetailsDialog.pcDetails = model.details
|
||||||
|
showPcDetailsDialog.open()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -262,7 +271,7 @@ CenteredGridView {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
acceptedButtons: Qt.RightButton;
|
acceptedButtons: Qt.RightButton;
|
||||||
onClicked: {
|
onClicked: {
|
||||||
parent.onPressAndHold()
|
parent.pressAndHold()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -271,6 +280,12 @@ CenteredGridView {
|
|||||||
// the ItemDelegate and not where the mouse cursor is
|
// the ItemDelegate and not where the mouse cursor is
|
||||||
pcContextMenu.open()
|
pcContextMenu.open()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Keys.onDeletePressed: {
|
||||||
|
deletePcDialog.pcIndex = index
|
||||||
|
deletePcDialog.pcName = model.name
|
||||||
|
deletePcDialog.open()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ErrorMessageDialog {
|
ErrorMessageDialog {
|
||||||
@@ -291,7 +306,8 @@ CenteredGridView {
|
|||||||
|
|
||||||
// don't allow edits to the rest of the window while open
|
// don't allow edits to the rest of the window while open
|
||||||
property string pin : "0000"
|
property string pin : "0000"
|
||||||
text:qsTr("Please enter %1 on your GameStream PC. This dialog will close when pairing is completed.").arg(pin)
|
text:qsTr("Please enter %1 on your host PC. This dialog will close when pairing is completed.").arg(pin)+"\n\n"+
|
||||||
|
qsTr("If your host PC is running Sunshine, navigate to the Sunshine web UI to enter the PIN.")
|
||||||
standardButtons: Dialog.Cancel
|
standardButtons: Dialog.Cancel
|
||||||
onRejected: {
|
onRejected: {
|
||||||
// FIXME: We should interrupt pairing here
|
// FIXME: We should interrupt pairing here
|
||||||
@@ -301,16 +317,14 @@ CenteredGridView {
|
|||||||
NavigableMessageDialog {
|
NavigableMessageDialog {
|
||||||
id: deletePcDialog
|
id: deletePcDialog
|
||||||
// don't allow edits to the rest of the window while open
|
// don't allow edits to the rest of the window while open
|
||||||
property int pcIndex : -1;
|
property int pcIndex : -1
|
||||||
text:qsTr("Are you sure you want to remove this PC?")
|
property string pcName : ""
|
||||||
|
text: qsTr("Are you sure you want to remove '%1'?").arg(pcName)
|
||||||
standardButtons: Dialog.Yes | Dialog.No
|
standardButtons: Dialog.Yes | Dialog.No
|
||||||
|
|
||||||
function deletePc() {
|
onAccepted: {
|
||||||
console.log("deleting PC pairing for PC at index: " + pcIndex)
|
computerModel.deleteComputer(pcIndex)
|
||||||
computerModel.deleteComputer(pcIndex);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onAccepted: deletePc()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
NavigableMessageDialog {
|
NavigableMessageDialog {
|
||||||
@@ -319,7 +333,7 @@ CenteredGridView {
|
|||||||
standardButtons: Dialog.Ok
|
standardButtons: Dialog.Ok
|
||||||
|
|
||||||
onAboutToShow: {
|
onAboutToShow: {
|
||||||
testConnectionDialog.text = qsTr("Moonlight is testing your network connection to determine if NVIDIA GameStream is blocked.") + "\n\n" + qsTr("This may take a few seconds…")
|
testConnectionDialog.text = qsTr("Moonlight is testing your network connection to determine if any required ports are blocked.") + "\n\n" + qsTr("This may take a few seconds…")
|
||||||
showSpinner = true
|
showSpinner = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -390,5 +404,13 @@ CenteredGridView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
NavigableMessageDialog {
|
||||||
|
id: showPcDetailsDialog
|
||||||
|
property string pcDetails : "";
|
||||||
|
text: showPcDetailsDialog.pcDetails
|
||||||
|
imageSrc: "qrc:/res/baseline-help_outline-24px.svg"
|
||||||
|
standardButtons: Dialog.Ok
|
||||||
|
}
|
||||||
|
|
||||||
ScrollBar.vertical: ScrollBar {}
|
ScrollBar.vertical: ScrollBar {}
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@ import Session 1.0
|
|||||||
|
|
||||||
Item {
|
Item {
|
||||||
property string appName
|
property string appName
|
||||||
|
property var quitRunningAppFn
|
||||||
property Session nextSession : null
|
property Session nextSession : null
|
||||||
property string nextAppName : ""
|
property string nextAppName : ""
|
||||||
|
|
||||||
@@ -17,6 +18,7 @@ Item {
|
|||||||
if (error !== undefined) {
|
if (error !== undefined) {
|
||||||
errorDialog.text = error
|
errorDialog.text = error
|
||||||
errorDialog.open()
|
errorDialog.open()
|
||||||
|
console.error(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we're supposed to launch another game after this, do so now
|
// If we're supposed to launch another game after this, do so now
|
||||||
@@ -37,6 +39,11 @@ Item {
|
|||||||
|
|
||||||
// Connect the quit completion signal
|
// Connect the quit completion signal
|
||||||
ComputerManager.quitAppCompleted.connect(quitAppCompleted)
|
ComputerManager.quitAppCompleted.connect(quitAppCompleted)
|
||||||
|
|
||||||
|
// Start the quit operation if requested
|
||||||
|
if (quitRunningAppFn) {
|
||||||
|
quitRunningAppFn()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
StackView.onDeactivating: {
|
StackView.onDeactivating: {
|
||||||
|
|||||||
+396
-190
@@ -1,6 +1,7 @@
|
|||||||
import QtQuick 2.9
|
import QtQuick 2.9
|
||||||
import QtQuick.Controls 2.2
|
import QtQuick.Controls 2.2
|
||||||
import QtQuick.Layouts 1.2
|
import QtQuick.Layouts 1.2
|
||||||
|
import QtQuick.Window 2.2
|
||||||
|
|
||||||
import StreamingPreferences 1.0
|
import StreamingPreferences 1.0
|
||||||
import ComputerManager 1.0
|
import ComputerManager 1.0
|
||||||
@@ -25,6 +26,49 @@ Flickable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isChildOfFlickable(item) {
|
||||||
|
while (item) {
|
||||||
|
if (item.parent === contentItem) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
item = item.parent
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
NumberAnimation on contentY {
|
||||||
|
id: autoScrollAnimation
|
||||||
|
duration: 100
|
||||||
|
}
|
||||||
|
|
||||||
|
Window.onActiveFocusItemChanged: {
|
||||||
|
var item = Window.activeFocusItem
|
||||||
|
if (item) {
|
||||||
|
// Ignore non-child elements like the toolbar buttons
|
||||||
|
if (!isChildOfFlickable(item)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map the focus item's position into our content item's coordinate space
|
||||||
|
var pos = item.mapToItem(contentItem, 0, 0)
|
||||||
|
|
||||||
|
// Ensure some extra space is visible around the element we're scrolling to
|
||||||
|
var scrollMargin = height > 100 ? 50 : 0
|
||||||
|
|
||||||
|
if (pos.y - scrollMargin < contentY) {
|
||||||
|
autoScrollAnimation.from = contentY
|
||||||
|
autoScrollAnimation.to = Math.max(pos.y - scrollMargin, 0)
|
||||||
|
autoScrollAnimation.start()
|
||||||
|
}
|
||||||
|
else if (pos.y + item.height + scrollMargin > contentY + height) {
|
||||||
|
autoScrollAnimation.from = contentY
|
||||||
|
autoScrollAnimation.to = Math.min(pos.y + item.height + scrollMargin - height, contentHeight - height)
|
||||||
|
autoScrollAnimation.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
StackView.onActivated: {
|
StackView.onActivated: {
|
||||||
// This enables Tab and BackTab based navigation rather than arrow keys.
|
// This enables Tab and BackTab based navigation rather than arrow keys.
|
||||||
// It is required to shift focus between controls on the settings page.
|
// It is required to shift focus between controls on the settings page.
|
||||||
@@ -89,15 +133,45 @@ Flickable {
|
|||||||
AutoResizingComboBox {
|
AutoResizingComboBox {
|
||||||
property int lastIndexValue
|
property int lastIndexValue
|
||||||
|
|
||||||
|
function addDetectedResolution(friendlyNamePrefix, rect) {
|
||||||
|
var indexToAdd = 0
|
||||||
|
for (var j = 0; j < resolutionComboBox.count; j++) {
|
||||||
|
var existing_width = parseInt(resolutionListModel.get(j).video_width);
|
||||||
|
var existing_height = parseInt(resolutionListModel.get(j).video_height);
|
||||||
|
|
||||||
|
if (rect.width === existing_width && rect.height === existing_height) {
|
||||||
|
// Duplicate entry, skip
|
||||||
|
indexToAdd = -1
|
||||||
|
break
|
||||||
|
}
|
||||||
|
else if (rect.width * rect.height > existing_width * existing_height) {
|
||||||
|
// Candidate entrypoint after this entry
|
||||||
|
indexToAdd = j + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert this display's resolution if it's not a duplicate
|
||||||
|
if (indexToAdd >= 0) {
|
||||||
|
resolutionListModel.insert(indexToAdd,
|
||||||
|
{
|
||||||
|
"text": friendlyNamePrefix+" ("+rect.width+"x"+rect.height+")",
|
||||||
|
"video_width": ""+rect.width,
|
||||||
|
"video_height": ""+rect.height,
|
||||||
|
"is_custom": false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ignore setting the index at first, and actually set it when the component is loaded
|
// ignore setting the index at first, and actually set it when the component is loaded
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
// Refresh display data before using it to build the list
|
// Refresh display data before using it to build the list
|
||||||
SystemProperties.refreshDisplays()
|
SystemProperties.refreshDisplays()
|
||||||
|
|
||||||
// Add native resolutions for all attached displays
|
// Add native and safe area resolutions for all attached displays
|
||||||
var done = false
|
var done = false
|
||||||
for (var displayIndex = 0; !done; displayIndex++) {
|
for (var displayIndex = 0; !done; displayIndex++) {
|
||||||
var screenRect = SystemProperties.getNativeResolution(displayIndex);
|
var screenRect = SystemProperties.getNativeResolution(displayIndex);
|
||||||
|
var safeAreaRect = SystemProperties.getSafeAreaResolution(displayIndex);
|
||||||
|
|
||||||
if (screenRect.width === 0) {
|
if (screenRect.width === 0) {
|
||||||
// Exceeded max count of displays
|
// Exceeded max count of displays
|
||||||
@@ -105,32 +179,8 @@ Flickable {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
var indexToAdd = 0
|
addDetectedResolution(qsTr("Native"), screenRect)
|
||||||
for (var j = 0; j < resolutionComboBox.count; j++) {
|
addDetectedResolution(qsTr("Native (Excluding Notch)"), safeAreaRect)
|
||||||
var existing_width = parseInt(resolutionListModel.get(j).video_width);
|
|
||||||
var existing_height = parseInt(resolutionListModel.get(j).video_height);
|
|
||||||
|
|
||||||
if (screenRect.width === existing_width && screenRect.height === existing_height) {
|
|
||||||
// Duplicate entry, skip
|
|
||||||
indexToAdd = -1
|
|
||||||
break
|
|
||||||
}
|
|
||||||
else if (screenRect.width * screenRect.height > existing_width * existing_height) {
|
|
||||||
// Candidate entrypoint after this entry
|
|
||||||
indexToAdd = j + 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert this display's resolution if it's not a duplicate
|
|
||||||
if (indexToAdd >= 0) {
|
|
||||||
resolutionListModel.insert(indexToAdd,
|
|
||||||
{
|
|
||||||
"text": "Native ("+screenRect.width+"x"+screenRect.height+")",
|
|
||||||
"video_width": ""+screenRect.width,
|
|
||||||
"video_height": ""+screenRect.height,
|
|
||||||
"is_custom": false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prune resolutions that are over the decoder's maximum
|
// Prune resolutions that are over the decoder's maximum
|
||||||
@@ -166,7 +216,7 @@ Flickable {
|
|||||||
if (!index_set) {
|
if (!index_set) {
|
||||||
// We did not find a match. This must be a custom resolution.
|
// We did not find a match. This must be a custom resolution.
|
||||||
resolutionListModel.append({
|
resolutionListModel.append({
|
||||||
"text": "Custom ("+StreamingPreferences.width+"x"+StreamingPreferences.height+")",
|
"text": qsTr("Custom")+" ("+StreamingPreferences.width+"x"+StreamingPreferences.height+")",
|
||||||
"video_width": ""+StreamingPreferences.width,
|
"video_width": ""+StreamingPreferences.width,
|
||||||
"video_height": ""+StreamingPreferences.height,
|
"video_height": ""+StreamingPreferences.height,
|
||||||
"is_custom": true
|
"is_custom": true
|
||||||
@@ -175,7 +225,7 @@ Flickable {
|
|||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
resolutionListModel.append({
|
resolutionListModel.append({
|
||||||
"text": "Custom",
|
"text": qsTr("Custom"),
|
||||||
"video_width": "",
|
"video_width": "",
|
||||||
"video_height": "",
|
"video_height": "",
|
||||||
"is_custom": true
|
"is_custom": true
|
||||||
@@ -233,7 +283,8 @@ Flickable {
|
|||||||
|
|
||||||
StreamingPreferences.bitrateKbps = StreamingPreferences.getDefaultBitrate(StreamingPreferences.width,
|
StreamingPreferences.bitrateKbps = StreamingPreferences.getDefaultBitrate(StreamingPreferences.width,
|
||||||
StreamingPreferences.height,
|
StreamingPreferences.height,
|
||||||
StreamingPreferences.fps);
|
StreamingPreferences.fps,
|
||||||
|
StreamingPreferences.enableYUV444);
|
||||||
slider.value = StreamingPreferences.bitrateKbps
|
slider.value = StreamingPreferences.bitrateKbps
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -388,69 +439,191 @@ Flickable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
AutoResizingComboBox {
|
AutoResizingComboBox {
|
||||||
function createModel() {
|
property int lastIndexValue
|
||||||
var fpsListModel = Qt.createQmlObject('import QtQuick 2.0; ListModel {}', parent, '')
|
|
||||||
|
|
||||||
var max_fps = SystemProperties.maximumStreamingFrameRate
|
function updateBitrateForSelection() {
|
||||||
|
// Only modify the bitrate if the values actually changed
|
||||||
|
var selectedFps = parseInt(model.get(fpsComboBox.currentIndex).video_fps)
|
||||||
|
if (StreamingPreferences.fps !== selectedFps) {
|
||||||
|
StreamingPreferences.fps = selectedFps
|
||||||
|
|
||||||
// Default entries
|
StreamingPreferences.bitrateKbps = StreamingPreferences.getDefaultBitrate(StreamingPreferences.width,
|
||||||
fpsListModel.append({"text": qsTr("%1 FPS").arg("30"), "video_fps": "30"})
|
StreamingPreferences.height,
|
||||||
fpsListModel.append({"text": qsTr("%1 FPS").arg("60"), "video_fps": "60"})
|
StreamingPreferences.fps,
|
||||||
|
StreamingPreferences.enableYUV444);
|
||||||
|
slider.value = StreamingPreferences.bitrateKbps
|
||||||
|
}
|
||||||
|
|
||||||
// Add unsupported FPS values that come before the display max FPS
|
lastIndexValue = currentIndex
|
||||||
if (StreamingPreferences.unsupportedFps) {
|
}
|
||||||
if (max_fps > 90) {
|
|
||||||
fpsListModel.append({"text": qsTr("%1 FPS (Unsupported)").arg("90"), "video_fps": "90"})
|
NavigableDialog {
|
||||||
|
function isInputValid() {
|
||||||
|
// If we have text that isn't valid, reject the input.
|
||||||
|
if (!fpsField.acceptableInput && fpsField.text) {
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
if (max_fps > 120) {
|
|
||||||
fpsListModel.append({"text": qsTr("%1 FPS (Unsupported)").arg("120"), "video_fps": "120"})
|
// The textbox needs to have text or placeholder text
|
||||||
|
if (!fpsField.text && !fpsField.placeholderText) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
id: customFpsDialog
|
||||||
|
standardButtons: Dialog.Ok | Dialog.Cancel
|
||||||
|
onOpened: {
|
||||||
|
// Force keyboard focus on the textbox so keyboard navigation works
|
||||||
|
fpsField.forceActiveFocus()
|
||||||
|
|
||||||
|
// standardButton() was added in Qt 5.10, so we must check for it first
|
||||||
|
if (customFpsDialog.standardButton) {
|
||||||
|
customFpsDialog.standardButton(Dialog.Ok).enabled = customFpsDialog.isInputValid()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use 64 as the cutoff for adding a separate option to
|
onClosed: {
|
||||||
// handle wonky displays that report just over 60 Hz.
|
fpsField.clear()
|
||||||
if (max_fps > 64) {
|
}
|
||||||
// Mark any FPS value greater than 120 as unsupported
|
|
||||||
if (StreamingPreferences.unsupportedFps && max_fps > 120) {
|
onRejected: {
|
||||||
fpsListModel.append({"text": qsTr("%1 FPS (Unsupported)").arg(max_fps), "video_fps": ""+max_fps})
|
fpsComboBox.currentIndex = fpsComboBox.lastIndexValue
|
||||||
|
}
|
||||||
|
|
||||||
|
onAccepted: {
|
||||||
|
// Reject if there's invalid input
|
||||||
|
if (!isInputValid()) {
|
||||||
|
reject()
|
||||||
|
return
|
||||||
}
|
}
|
||||||
else if (max_fps > 120) {
|
|
||||||
fpsListModel.append({"text": qsTr("%1 FPS").arg("120"), "video_fps": "120"})
|
var fps = fpsField.text ? fpsField.text : fpsField.placeholderText
|
||||||
}
|
|
||||||
else {
|
// Find and update the custom entry
|
||||||
fpsListModel.append({"text": qsTr("%1 FPS").arg(max_fps), "video_fps": ""+max_fps})
|
for (var i = 0; i < fpsListModel.count; i++) {
|
||||||
|
if (fpsListModel.get(i).is_custom) {
|
||||||
|
fpsListModel.setProperty(i, "video_fps", fps)
|
||||||
|
fpsListModel.setProperty(i, "text", qsTr("Custom (%1 FPS)").arg(fps))
|
||||||
|
|
||||||
|
// Now update the bitrate using the custom resolution
|
||||||
|
fpsComboBox.currentIndex = i
|
||||||
|
fpsComboBox.updateBitrateForSelection()
|
||||||
|
|
||||||
|
// Update the combobox width too
|
||||||
|
fpsComboBox.recalculateWidth()
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add unsupported FPS values that come after the display max FPS
|
ColumnLayout {
|
||||||
if (StreamingPreferences.unsupportedFps) {
|
Label {
|
||||||
if (max_fps < 90) {
|
text: qsTr("Enter a custom frame rate:")
|
||||||
fpsListModel.append({"text":qsTr("%1 FPS (Unsupported)").arg("90"), "video_fps": "90"})
|
font.bold: true
|
||||||
}
|
}
|
||||||
if (max_fps < 120) {
|
|
||||||
fpsListModel.append({"text":qsTr("%1 FPS (Unsupported)").arg("120"), "video_fps": "120"})
|
RowLayout {
|
||||||
|
TextField {
|
||||||
|
id: fpsField
|
||||||
|
maximumLength: 4
|
||||||
|
inputMethodHints: Qt.ImhDigitsOnly
|
||||||
|
placeholderText: fpsListModel.get(fpsComboBox.currentIndex).video_fps
|
||||||
|
validator: IntValidator{bottom:10; top:9999}
|
||||||
|
focus: true
|
||||||
|
|
||||||
|
onTextChanged: {
|
||||||
|
// standardButton() was added in Qt 5.10, so we must check for it first
|
||||||
|
if (customFpsDialog.standardButton) {
|
||||||
|
customFpsDialog.standardButton(Dialog.Ok).enabled = customFpsDialog.isInputValid()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Keys.onReturnPressed: {
|
||||||
|
customFpsDialog.accept()
|
||||||
|
}
|
||||||
|
|
||||||
|
Keys.onEnterPressed: {
|
||||||
|
customFpsDialog.accept()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addRefreshRateOrdered(fpsListModel, refreshRate, description, custom) {
|
||||||
|
var indexToAdd = 0
|
||||||
|
for (var j = 0; j < fpsListModel.count; j++) {
|
||||||
|
var existing_fps = parseInt(fpsListModel.get(j).video_fps);
|
||||||
|
|
||||||
|
if (refreshRate === existing_fps || (custom && fpsListModel.get(j).is_custom)) {
|
||||||
|
// Duplicate entry, skip
|
||||||
|
indexToAdd = -1
|
||||||
|
break
|
||||||
|
}
|
||||||
|
else if (refreshRate > existing_fps) {
|
||||||
|
// Candidate entrypoint after this entry
|
||||||
|
indexToAdd = j + 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return fpsListModel
|
// Insert this frame rate if it's not a duplicate
|
||||||
|
if (indexToAdd >= 0) {
|
||||||
|
// Custom values always go at the end of the list
|
||||||
|
if (custom) {
|
||||||
|
indexToAdd = fpsListModel.count
|
||||||
|
}
|
||||||
|
|
||||||
|
fpsListModel.insert(indexToAdd,
|
||||||
|
{
|
||||||
|
"text": description,
|
||||||
|
"video_fps": ""+refreshRate,
|
||||||
|
"is_custom": custom
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return indexToAdd
|
||||||
}
|
}
|
||||||
|
|
||||||
function reinitialize() {
|
function reinitialize() {
|
||||||
model = createModel()
|
// Add native refresh rate for all attached displays
|
||||||
|
var done = false
|
||||||
|
for (var displayIndex = 0; !done; displayIndex++) {
|
||||||
|
var refreshRate = SystemProperties.getRefreshRate(displayIndex);
|
||||||
|
if (refreshRate === 0) {
|
||||||
|
// Exceeded max count of displays
|
||||||
|
done = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
addRefreshRateOrdered(fpsListModel, refreshRate, qsTr("%1 FPS").arg(refreshRate), false)
|
||||||
|
}
|
||||||
|
|
||||||
var saved_fps = StreamingPreferences.fps
|
var saved_fps = StreamingPreferences.fps
|
||||||
currentIndex = 0
|
var found = false
|
||||||
for (var i = 0; i < model.count; i++) {
|
for (var i = 0; i < model.count; i++) {
|
||||||
var el_fps = parseInt(model.get(i).video_fps);
|
var el_fps = parseInt(model.get(i).video_fps);
|
||||||
|
|
||||||
// Pick the highest value lesser or equal to the saved FPS
|
// Look for a matching frame rate
|
||||||
if (saved_fps >= el_fps) {
|
if (saved_fps === el_fps) {
|
||||||
currentIndex = i
|
currentIndex = i
|
||||||
|
found = true
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Persist the selected value
|
// If we didn't find one, add a custom frame rate for the current value
|
||||||
activated(currentIndex)
|
if (!found) {
|
||||||
|
currentIndex = addRefreshRateOrdered(model, saved_fps, qsTr("Custom (%1 FPS)").arg(saved_fps), true)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
addRefreshRateOrdered(model, "", qsTr("Custom"), true)
|
||||||
|
}
|
||||||
|
|
||||||
|
recalculateWidth()
|
||||||
|
|
||||||
|
lastIndexValue = currentIndex
|
||||||
}
|
}
|
||||||
|
|
||||||
// ignore setting the index at first, and actually set it when the component is loaded
|
// ignore setting the index at first, and actually set it when the component is loaded
|
||||||
@@ -459,21 +632,31 @@ Flickable {
|
|||||||
languageChanged.connect(reinitialize)
|
languageChanged.connect(reinitialize)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model: ListModel {
|
||||||
|
id: fpsListModel
|
||||||
|
// Other elements may be added at runtime
|
||||||
|
ListElement {
|
||||||
|
text: qsTr("30 FPS")
|
||||||
|
video_fps: "30"
|
||||||
|
is_custom: false
|
||||||
|
}
|
||||||
|
ListElement {
|
||||||
|
text: qsTr("60 FPS")
|
||||||
|
video_fps: "60"
|
||||||
|
is_custom: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
id: fpsComboBox
|
id: fpsComboBox
|
||||||
maximumWidth: parent.width / 2
|
maximumWidth: parent.width / 2
|
||||||
textRole: "text"
|
textRole: "text"
|
||||||
// ::onActivated must be used, as it only listens for when the index is changed by a human
|
// ::onActivated must be used, as it only listens for when the index is changed by a human
|
||||||
onActivated : {
|
onActivated : {
|
||||||
var selectedFps = parseInt(model.get(currentIndex).video_fps)
|
if (model.get(currentIndex).is_custom) {
|
||||||
|
customFpsDialog.open()
|
||||||
// Only modify the bitrate if the values actually changed
|
}
|
||||||
if (StreamingPreferences.fps !== selectedFps) {
|
else {
|
||||||
StreamingPreferences.fps = selectedFps
|
updateBitrateForSelection()
|
||||||
|
|
||||||
StreamingPreferences.bitrateKbps = StreamingPreferences.getDefaultBitrate(StreamingPreferences.width,
|
|
||||||
StreamingPreferences.height,
|
|
||||||
StreamingPreferences.fps);
|
|
||||||
slider.value = StreamingPreferences.bitrateKbps
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -502,7 +685,7 @@ Flickable {
|
|||||||
|
|
||||||
stepSize: 500
|
stepSize: 500
|
||||||
from : 500
|
from : 500
|
||||||
to: 150000
|
to: StreamingPreferences.unlockBitrate ? 500000 : 150000
|
||||||
|
|
||||||
snapMode: "SnapOnRelease"
|
snapMode: "SnapOnRelease"
|
||||||
width: Math.min(bitrateDesc.implicitWidth, parent.width)
|
width: Math.min(bitrateDesc.implicitWidth, parent.width)
|
||||||
@@ -511,6 +694,11 @@ Flickable {
|
|||||||
bitrateTitle.text = qsTr("Video bitrate: %1 Mbps").arg(value / 1000.0)
|
bitrateTitle.text = qsTr("Video bitrate: %1 Mbps").arg(value / 1000.0)
|
||||||
StreamingPreferences.bitrateKbps = value
|
StreamingPreferences.bitrateKbps = value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
// Refresh the text after translations change
|
||||||
|
languageChanged.connect(valueChanged)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
@@ -546,7 +734,7 @@ Flickable {
|
|||||||
for (var i = 0; i < model.count; i++) {
|
for (var i = 0; i < model.count; i++) {
|
||||||
var thisWm = model.get(i).val;
|
var thisWm = model.get(i).val;
|
||||||
if (thisWm === StreamingPreferences.recommendedFullScreenMode) {
|
if (thisWm === StreamingPreferences.recommendedFullScreenMode) {
|
||||||
model.get(i).text += qsTr(" (Recommended)")
|
model.get(i).text += " " + qsTr("(Recommended)")
|
||||||
model.move(i, 0, 1)
|
model.move(i, 0, 1)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -729,6 +917,46 @@ Flickable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
GroupBox {
|
||||||
|
id: hostSettingsGroupBox
|
||||||
|
width: (parent.width - (parent.leftPadding + parent.rightPadding))
|
||||||
|
padding: 12
|
||||||
|
title: "<font color=\"skyblue\">" + qsTr("Host Settings") + "</font>"
|
||||||
|
font.pointSize: 12
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.fill: parent
|
||||||
|
spacing: 5
|
||||||
|
|
||||||
|
CheckBox {
|
||||||
|
id: optimizeGameSettingsCheck
|
||||||
|
width: parent.width
|
||||||
|
text: qsTr("Optimize game settings for streaming")
|
||||||
|
font.pointSize: 12
|
||||||
|
checked: StreamingPreferences.gameOptimizations
|
||||||
|
onCheckedChanged: {
|
||||||
|
StreamingPreferences.gameOptimizations = checked
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CheckBox {
|
||||||
|
id: quitAppAfter
|
||||||
|
width: parent.width
|
||||||
|
text: qsTr("Quit app on host PC after ending stream")
|
||||||
|
font.pointSize: 12
|
||||||
|
checked: StreamingPreferences.quitAppAfter
|
||||||
|
onCheckedChanged: {
|
||||||
|
StreamingPreferences.quitAppAfter = checked
|
||||||
|
}
|
||||||
|
|
||||||
|
ToolTip.delay: 1000
|
||||||
|
ToolTip.timeout: 5000
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
ToolTip.text: qsTr("This will close the app or game you are streaming when you end your stream. You will lose any unsaved progress!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
GroupBox {
|
GroupBox {
|
||||||
id: uiSettingsGroupBox
|
id: uiSettingsGroupBox
|
||||||
width: (parent.width - (parent.leftPadding + parent.rightPadding))
|
width: (parent.width - (parent.leftPadding + parent.rightPadding))
|
||||||
@@ -816,10 +1044,10 @@ Flickable {
|
|||||||
text: "한국어" // Korean
|
text: "한국어" // Korean
|
||||||
val: StreamingPreferences.LANG_KO
|
val: StreamingPreferences.LANG_KO
|
||||||
}
|
}
|
||||||
/* ListElement {
|
ListElement {
|
||||||
text: "Magyar" // Hungarian
|
text: "Magyar" // Hungarian
|
||||||
val: StreamingPreferences.LANG_HU
|
val: StreamingPreferences.LANG_HU
|
||||||
} */
|
}
|
||||||
ListElement {
|
ListElement {
|
||||||
text: "Nederlands" // Dutch
|
text: "Nederlands" // Dutch
|
||||||
val: StreamingPreferences.LANG_NL
|
val: StreamingPreferences.LANG_NL
|
||||||
@@ -828,17 +1056,17 @@ Flickable {
|
|||||||
text: "Svenska" // Swedish
|
text: "Svenska" // Swedish
|
||||||
val: StreamingPreferences.LANG_SV
|
val: StreamingPreferences.LANG_SV
|
||||||
}
|
}
|
||||||
/* ListElement {
|
ListElement {
|
||||||
text: "Türkçe" // Turkish
|
text: "Türkçe" // Turkish
|
||||||
val: StreamingPreferences.LANG_TR
|
val: StreamingPreferences.LANG_TR
|
||||||
} */
|
}
|
||||||
/* ListElement {
|
/* ListElement {
|
||||||
text: "Українська" // Ukrainian
|
text: "Українська" // Ukrainian
|
||||||
val: StreamingPreferences.LANG_UK
|
val: StreamingPreferences.LANG_UK
|
||||||
} */
|
} */
|
||||||
ListElement {
|
ListElement {
|
||||||
text: "繁体字" // Traditional Chinese
|
text: "繁體中文" // Traditional Chinese
|
||||||
val: StreamingPreferences.LANG_ZH_HANT
|
val: StreamingPreferences.LANG_ZH_TW
|
||||||
}
|
}
|
||||||
ListElement {
|
ListElement {
|
||||||
text: "Português" // Portuguese
|
text: "Português" // Portuguese
|
||||||
@@ -868,6 +1096,22 @@ Flickable {
|
|||||||
text: "Čeština" // Czech
|
text: "Čeština" // Czech
|
||||||
val: StreamingPreferences.LANG_CS
|
val: StreamingPreferences.LANG_CS
|
||||||
}
|
}
|
||||||
|
/* ListElement {
|
||||||
|
text: "עִבְרִית" // Hebrew
|
||||||
|
val: StreamingPreferences.LANG_HE
|
||||||
|
} */
|
||||||
|
/* ListElement {
|
||||||
|
text: "کرمانجیی خواروو" // Central Kurdish
|
||||||
|
val: StreamingPreferences.LANG_CKB
|
||||||
|
} */
|
||||||
|
/* ListElement {
|
||||||
|
text: "Lietuvių kalba" // Lithuanian
|
||||||
|
val: StreamingPreferences.LANG_LT
|
||||||
|
} */
|
||||||
|
/* ListElement {
|
||||||
|
text: "Eesti" // Estonian
|
||||||
|
val: StreamingPreferences.LANG_ET
|
||||||
|
} */
|
||||||
}
|
}
|
||||||
// ::onActivated must be used, as it only listens for when the index is changed by a human
|
// ::onActivated must be used, as it only listens for when the index is changed by a human
|
||||||
onActivated : {
|
onActivated : {
|
||||||
@@ -1165,15 +1409,7 @@ Flickable {
|
|||||||
font.pointSize: 12
|
font.pointSize: 12
|
||||||
checked: StreamingPreferences.swapFaceButtons
|
checked: StreamingPreferences.swapFaceButtons
|
||||||
onCheckedChanged: {
|
onCheckedChanged: {
|
||||||
// Check if the value changed (this is called on init too)
|
StreamingPreferences.swapFaceButtons = checked
|
||||||
if (StreamingPreferences.swapFaceButtons !== checked) {
|
|
||||||
StreamingPreferences.swapFaceButtons = checked
|
|
||||||
|
|
||||||
// Save and restart SdlGamepadKeyNavigation so it can pull the new value
|
|
||||||
StreamingPreferences.save()
|
|
||||||
SdlGamepadKeyNavigation.disable()
|
|
||||||
SdlGamepadKeyNavigation.enable()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ToolTip.delay: 1000
|
ToolTip.delay: 1000
|
||||||
@@ -1230,46 +1466,6 @@ Flickable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
GroupBox {
|
|
||||||
id: hostSettingsGroupBox
|
|
||||||
width: (parent.width - (parent.leftPadding + parent.rightPadding))
|
|
||||||
padding: 12
|
|
||||||
title: "<font color=\"skyblue\">" + qsTr("Host Settings") + "</font>"
|
|
||||||
font.pointSize: 12
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.fill: parent
|
|
||||||
spacing: 5
|
|
||||||
|
|
||||||
CheckBox {
|
|
||||||
id: optimizeGameSettingsCheck
|
|
||||||
width: parent.width
|
|
||||||
text: qsTr("Optimize game settings for streaming")
|
|
||||||
font.pointSize: 12
|
|
||||||
checked: StreamingPreferences.gameOptimizations
|
|
||||||
onCheckedChanged: {
|
|
||||||
StreamingPreferences.gameOptimizations = checked
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CheckBox {
|
|
||||||
id: quitAppAfter
|
|
||||||
width: parent.width
|
|
||||||
text: qsTr("Quit app on host PC after ending stream")
|
|
||||||
font.pointSize: 12
|
|
||||||
checked: StreamingPreferences.quitAppAfter
|
|
||||||
onCheckedChanged: {
|
|
||||||
StreamingPreferences.quitAppAfter = checked
|
|
||||||
}
|
|
||||||
|
|
||||||
ToolTip.delay: 1000
|
|
||||||
ToolTip.timeout: 5000
|
|
||||||
ToolTip.visible: hovered
|
|
||||||
ToolTip.text: qsTr("This will close the app or game you are streaming when you end your stream. You will lose any unsaved progress!")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
GroupBox {
|
GroupBox {
|
||||||
id: advancedSettingsGroupBox
|
id: advancedSettingsGroupBox
|
||||||
width: (parent.width - (parent.leftPadding + parent.rightPadding))
|
width: (parent.width - (parent.leftPadding + parent.rightPadding))
|
||||||
@@ -1306,7 +1502,6 @@ Flickable {
|
|||||||
|
|
||||||
id: decoderComboBox
|
id: decoderComboBox
|
||||||
textRole: "text"
|
textRole: "text"
|
||||||
enabled: !enableHdr.checked
|
|
||||||
model: ListModel {
|
model: ListModel {
|
||||||
id: decoderListModel
|
id: decoderListModel
|
||||||
ListElement {
|
ListElement {
|
||||||
@@ -1328,21 +1523,6 @@ Flickable {
|
|||||||
StreamingPreferences.videoDecoderSelection = decoderListModel.get(currentIndex).val
|
StreamingPreferences.videoDecoderSelection = decoderListModel.get(currentIndex).val
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// This handles the state of the enableHdr checkbox changing
|
|
||||||
onEnabledChanged: {
|
|
||||||
if (enabled) {
|
|
||||||
StreamingPreferences.videoDecoderSelection = decoderListModel.get(currentIndex).val
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
StreamingPreferences.videoDecoderSelection = StreamingPreferences.VDS_AUTO
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ToolTip.delay: 1000
|
|
||||||
ToolTip.timeout: 5000
|
|
||||||
ToolTip.visible: hovered && !enabled
|
|
||||||
ToolTip.text: qsTr("Enabling HDR overrides manual decoder selections.")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
@@ -1375,7 +1555,6 @@ Flickable {
|
|||||||
|
|
||||||
id: codecComboBox
|
id: codecComboBox
|
||||||
textRole: "text"
|
textRole: "text"
|
||||||
enabled: !enableHdr.checked
|
|
||||||
model: ListModel {
|
model: ListModel {
|
||||||
id: codecListModel
|
id: codecListModel
|
||||||
ListElement {
|
ListElement {
|
||||||
@@ -1390,6 +1569,10 @@ Flickable {
|
|||||||
text: qsTr("HEVC (H.265)")
|
text: qsTr("HEVC (H.265)")
|
||||||
val: StreamingPreferences.VCC_FORCE_HEVC
|
val: StreamingPreferences.VCC_FORCE_HEVC
|
||||||
}
|
}
|
||||||
|
ListElement {
|
||||||
|
text: qsTr("AV1 (Experimental)")
|
||||||
|
val: StreamingPreferences.VCC_FORCE_AV1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// ::onActivated must be used, as it only listens for when the index is changed by a human
|
// ::onActivated must be used, as it only listens for when the index is changed by a human
|
||||||
onActivated : {
|
onActivated : {
|
||||||
@@ -1397,21 +1580,6 @@ Flickable {
|
|||||||
StreamingPreferences.videoCodecConfig = codecListModel.get(currentIndex).val
|
StreamingPreferences.videoCodecConfig = codecListModel.get(currentIndex).val
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// This handles the state of the enableHdr checkbox changing
|
|
||||||
onEnabledChanged: {
|
|
||||||
if (enabled) {
|
|
||||||
StreamingPreferences.videoCodecConfig = codecListModel.get(currentIndex).val
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
StreamingPreferences.videoCodecConfig = StreamingPreferences.VCC_FORCE_HEVC_HDR
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ToolTip.delay: 1000
|
|
||||||
ToolTip.timeout: 5000
|
|
||||||
ToolTip.visible: hovered && !enabled
|
|
||||||
ToolTip.text: qsTr("Enabling HDR overrides manual codec selections.")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
CheckBox {
|
CheckBox {
|
||||||
@@ -1419,8 +1587,12 @@ Flickable {
|
|||||||
width: parent.width
|
width: parent.width
|
||||||
text: qsTr("Enable HDR (Experimental)")
|
text: qsTr("Enable HDR (Experimental)")
|
||||||
font.pointSize: 12
|
font.pointSize: 12
|
||||||
|
|
||||||
enabled: SystemProperties.supportsHdr
|
enabled: SystemProperties.supportsHdr
|
||||||
checked: enabled && StreamingPreferences.videoCodecConfig == StreamingPreferences.VCC_FORCE_HEVC_HDR
|
checked: enabled && StreamingPreferences.enableHdr
|
||||||
|
onCheckedChanged: {
|
||||||
|
StreamingPreferences.enableHdr = checked
|
||||||
|
}
|
||||||
|
|
||||||
// Updating StreamingPreferences.videoCodecConfig is handled above
|
// Updating StreamingPreferences.videoCodecConfig is handled above
|
||||||
|
|
||||||
@@ -1434,22 +1606,50 @@ Flickable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
CheckBox {
|
CheckBox {
|
||||||
id: unlockUnsupportedFps
|
id: enableYUV444
|
||||||
width: parent.width
|
width: parent.width
|
||||||
text: qsTr("Unlock unsupported FPS options")
|
text: qsTr("Enable YUV 4:4:4 (Experimental)")
|
||||||
font.pointSize: 12
|
font.pointSize: 12
|
||||||
checked: StreamingPreferences.unsupportedFps
|
|
||||||
onCheckedChanged: {
|
|
||||||
// This is called on init, so only do the work if we've
|
|
||||||
// actually changed the value.
|
|
||||||
if (StreamingPreferences.unsupportedFps != checked) {
|
|
||||||
StreamingPreferences.unsupportedFps = checked
|
|
||||||
|
|
||||||
// The selectable FPS values depend on whether
|
checked: StreamingPreferences.enableYUV444
|
||||||
// this option is enabled or not
|
onCheckedChanged: {
|
||||||
fpsComboBox.reinitialize()
|
// This is called on init, so only reset to default bitrate when checked state changes.
|
||||||
|
if (StreamingPreferences.enableYUV444 != checked) {
|
||||||
|
StreamingPreferences.enableYUV444 = checked
|
||||||
|
StreamingPreferences.bitrateKbps = StreamingPreferences.getDefaultBitrate(StreamingPreferences.width,
|
||||||
|
StreamingPreferences.height,
|
||||||
|
StreamingPreferences.fps,
|
||||||
|
StreamingPreferences.enableYUV444);
|
||||||
|
slider.value = StreamingPreferences.bitrateKbps
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ToolTip.delay: 1000
|
||||||
|
ToolTip.timeout: 5000
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
ToolTip.text: enabled ?
|
||||||
|
qsTr("Good for streaming desktop and text-heavy games, but not recommended for fast-paced games.")
|
||||||
|
:
|
||||||
|
qsTr("YUV 4:4:4 is not supported on this PC.")
|
||||||
|
}
|
||||||
|
|
||||||
|
CheckBox {
|
||||||
|
id: unlockBitrate
|
||||||
|
width: parent.width
|
||||||
|
text: qsTr("Unlock bitrate limit (Experimental)")
|
||||||
|
font.pointSize: 12
|
||||||
|
|
||||||
|
checked: StreamingPreferences.unlockBitrate
|
||||||
|
onCheckedChanged: {
|
||||||
|
StreamingPreferences.unlockBitrate = checked
|
||||||
|
StreamingPreferences.bitrateKbps = Math.min(StreamingPreferences.bitrateKbps, slider.to)
|
||||||
|
slider.value = StreamingPreferences.bitrateKbps
|
||||||
|
}
|
||||||
|
|
||||||
|
ToolTip.delay: 1000
|
||||||
|
ToolTip.timeout: 5000
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
ToolTip.text: qsTr("This unlocks extremely high video bitrates for use with Sunshine hosts. It should only be used when streaming over an Ethernet LAN connection.")
|
||||||
}
|
}
|
||||||
|
|
||||||
CheckBox {
|
CheckBox {
|
||||||
@@ -1464,10 +1664,6 @@ Flickable {
|
|||||||
if (StreamingPreferences.enableMdns != checked) {
|
if (StreamingPreferences.enableMdns != checked) {
|
||||||
StreamingPreferences.enableMdns = checked
|
StreamingPreferences.enableMdns = checked
|
||||||
|
|
||||||
// We must save the updated preference to ensure
|
|
||||||
// ComputerManager can observe the change internally.
|
|
||||||
StreamingPreferences.save()
|
|
||||||
|
|
||||||
// Restart polling so the mDNS change takes effect
|
// Restart polling so the mDNS change takes effect
|
||||||
if (window.pollingActive) {
|
if (window.pollingActive) {
|
||||||
ComputerManager.stopPollingAsync()
|
ComputerManager.stopPollingAsync()
|
||||||
@@ -1484,17 +1680,27 @@ Flickable {
|
|||||||
font.pointSize: 12
|
font.pointSize: 12
|
||||||
checked: StreamingPreferences.detectNetworkBlocking
|
checked: StreamingPreferences.detectNetworkBlocking
|
||||||
onCheckedChanged: {
|
onCheckedChanged: {
|
||||||
// This is called on init, so only do the work if we've
|
StreamingPreferences.detectNetworkBlocking = checked
|
||||||
// actually changed the value.
|
|
||||||
if (StreamingPreferences.detectNetworkBlocking != checked) {
|
|
||||||
StreamingPreferences.detectNetworkBlocking = checked
|
|
||||||
|
|
||||||
// We must save the updated preference to ensure
|
|
||||||
// ComputerManager can observe the change internally.
|
|
||||||
StreamingPreferences.save()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CheckBox {
|
||||||
|
id: showPerformanceOverlay
|
||||||
|
width: parent.width
|
||||||
|
text: qsTr("Show performance stats while streaming")
|
||||||
|
font.pointSize: 12
|
||||||
|
checked: StreamingPreferences.showPerformanceOverlay
|
||||||
|
onCheckedChanged: {
|
||||||
|
StreamingPreferences.showPerformanceOverlay = checked
|
||||||
|
}
|
||||||
|
|
||||||
|
ToolTip.delay: 1000
|
||||||
|
ToolTip.timeout: 5000
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
ToolTip.text: qsTr("Display real-time stream performance information while streaming.") + "\n\n" +
|
||||||
|
qsTr("You can toggle it at any time while streaming using Ctrl+Alt+Shift+S or Select+L1+R1+X.") + "\n\n" +
|
||||||
|
qsTr("The performance overlay is not supported on Steam Link or Raspberry Pi.")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ Item {
|
|||||||
{
|
{
|
||||||
// Display the error dialog after Session::exec() returns
|
// Display the error dialog after Session::exec() returns
|
||||||
streamSegueErrorDialog.text = text
|
streamSegueErrorDialog.text = text
|
||||||
|
console.error(text)
|
||||||
}
|
}
|
||||||
|
|
||||||
function displayLaunchWarning(text)
|
function displayLaunchWarning(text)
|
||||||
@@ -56,6 +57,7 @@ Item {
|
|||||||
toast.text = text
|
toast.text = text
|
||||||
toast.timeout = 3000
|
toast.timeout = 3000
|
||||||
toast.visible = true
|
toast.visible = true
|
||||||
|
console.warn(text)
|
||||||
}
|
}
|
||||||
|
|
||||||
function quitStarting()
|
function quitStarting()
|
||||||
@@ -174,7 +176,7 @@ Item {
|
|||||||
gc()
|
gc()
|
||||||
|
|
||||||
// Run the streaming session to completion
|
// Run the streaming session to completion
|
||||||
session.exec(Screen.virtualX, Screen.virtualY)
|
session.exec(Window.window)
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceComponent: Item {}
|
sourceComponent: Item {}
|
||||||
|
|||||||
+51
-11
@@ -42,6 +42,46 @@ QVariant ComputerModel::data(const QModelIndex& index, int role) const
|
|||||||
return computer->state == NvComputer::CS_UNKNOWN;
|
return computer->state == NvComputer::CS_UNKNOWN;
|
||||||
case ServerSupportedRole:
|
case ServerSupportedRole:
|
||||||
return computer->isSupportedServerVersion;
|
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();
|
||||||
}
|
}
|
||||||
@@ -69,6 +109,7 @@ QHash<int, QByteArray> ComputerModel::roleNames() const
|
|||||||
names[WakeableRole] = "wakeable";
|
names[WakeableRole] = "wakeable";
|
||||||
names[StatusUnknownRole] = "statusUnknown";
|
names[StatusUnknownRole] = "statusUnknown";
|
||||||
names[ServerSupportedRole] = "serverSupported";
|
names[ServerSupportedRole] = "serverSupported";
|
||||||
|
names[DetailsRole] = "details";
|
||||||
|
|
||||||
return names;
|
return names;
|
||||||
}
|
}
|
||||||
@@ -186,19 +227,18 @@ void ComputerModel::handlePairingCompleted(NvComputer*, QString error)
|
|||||||
|
|
||||||
void ComputerModel::handleComputerStateChanged(NvComputer* computer)
|
void ComputerModel::handleComputerStateChanged(NvComputer* computer)
|
||||||
{
|
{
|
||||||
// If this is an existing computer, we can report the data changed
|
QVector<NvComputer*> newComputerList = m_ComputerManager->getComputers();
|
||||||
int index = m_Computers.indexOf(computer);
|
|
||||||
if (index >= 0) {
|
// Reset the model if the structural layout of the list has changed
|
||||||
// Let the view know that this specific computer changed
|
if (m_Computers != newComputerList) {
|
||||||
emit dataChanged(createIndex(index, 0), createIndex(index, 0));
|
beginResetModel();
|
||||||
|
m_Computers = newComputerList;
|
||||||
|
endResetModel();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// This is a new PC which may be inserted at an arbitrary point
|
// Let the view know that this specific computer changed
|
||||||
// in our computer list (since it comes from CM's QMap). Reload
|
int index = m_Computers.indexOf(computer);
|
||||||
// the whole model state to ensure it stays consistent.
|
emit dataChanged(createIndex(index, 0), createIndex(index, 0));
|
||||||
beginResetModel();
|
|
||||||
m_Computers = m_ComputerManager->getComputers();
|
|
||||||
endResetModel();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ class ComputerModel : public QAbstractListModel
|
|||||||
BusyRole,
|
BusyRole,
|
||||||
WakeableRole,
|
WakeableRole,
|
||||||
StatusUnknownRole,
|
StatusUnknownRole,
|
||||||
ServerSupportedRole
|
ServerSupportedRole,
|
||||||
|
DetailsRole
|
||||||
};
|
};
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
|||||||
+40
-43
@@ -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
|
||||||
@@ -18,17 +19,48 @@ ApplicationWindow {
|
|||||||
property bool clearOnBack: false
|
property bool clearOnBack: false
|
||||||
|
|
||||||
id: window
|
id: window
|
||||||
visible: true
|
|
||||||
width: 1280
|
width: 1280
|
||||||
height: 600
|
height: 600
|
||||||
|
|
||||||
visibility: {
|
Component.onCompleted: {
|
||||||
|
// 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"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show the window according to the user's preferences
|
||||||
if (SystemProperties.hasDesktopEnvironment) {
|
if (SystemProperties.hasDesktopEnvironment) {
|
||||||
if (StreamingPreferences.uiDisplayMode == StreamingPreferences.UI_WINDOWED) return "Windowed"
|
if (StreamingPreferences.uiDisplayMode == StreamingPreferences.UI_MAXIMIZED) {
|
||||||
else if (StreamingPreferences.uiDisplayMode == StreamingPreferences.UI_MAXIMIZED) return "Maximized"
|
window.showMaximized()
|
||||||
else if (StreamingPreferences.uiDisplayMode == StreamingPreferences.UI_FULLSCREEN) return "FullScreen"
|
}
|
||||||
|
else if (StreamingPreferences.uiDisplayMode == StreamingPreferences.UI_FULLSCREEN) {
|
||||||
|
window.showFullScreen()
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
window.show()
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return "FullScreen"
|
window.showFullScreen()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display any modal dialogs for configuration warnings
|
||||||
|
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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,41 +178,6 @@ ApplicationWindow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
property bool initialized: false
|
|
||||||
|
|
||||||
// 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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Workaround for lack of instanceof in Qt 5.9.
|
// Workaround for lack of instanceof in Qt 5.9.
|
||||||
//
|
//
|
||||||
// Based on https://stackoverflow.com/questions/13923794/how-to-do-a-is-a-typeof-or-instanceof-in-qml
|
// Based on https://stackoverflow.com/questions/13923794/how-to-do-a-is-a-typeof-or-instanceof-in-qml
|
||||||
@@ -273,7 +270,7 @@ ApplicationWindow {
|
|||||||
visible: SystemProperties.hasBrowser &&
|
visible: SystemProperties.hasBrowser &&
|
||||||
qmltypeof(stackView.currentItem, "SettingsView")
|
qmltypeof(stackView.currentItem, "SettingsView")
|
||||||
|
|
||||||
iconSource: "qrc:/res/Discord-Logo-White.svg"
|
iconSource: "qrc:/res/discord.svg"
|
||||||
|
|
||||||
ToolTip.delay: 1000
|
ToolTip.delay: 1000
|
||||||
ToolTip.timeout: 3000
|
ToolTip.timeout: 3000
|
||||||
@@ -488,7 +485,7 @@ ApplicationWindow {
|
|||||||
|
|
||||||
NavigableDialog {
|
NavigableDialog {
|
||||||
id: addPcDialog
|
id: addPcDialog
|
||||||
property string label: qsTr("Enter the IP address of your GameStream PC:")
|
property string label: qsTr("Enter the IP address of your host PC:")
|
||||||
|
|
||||||
standardButtons: Dialog.Ok | Dialog.Cancel
|
standardButtons: Dialog.Ok | Dialog.Cancel
|
||||||
|
|
||||||
|
|||||||
@@ -4,13 +4,13 @@
|
|||||||
#include <QGuiApplication>
|
#include <QGuiApplication>
|
||||||
#include <QWindow>
|
#include <QWindow>
|
||||||
|
|
||||||
#include "settings/streamingpreferences.h"
|
|
||||||
#include "settings/mappingmanager.h"
|
#include "settings/mappingmanager.h"
|
||||||
|
|
||||||
#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_FirstPoll(false),
|
||||||
m_LastAxisNavigationEventTime(0)
|
m_LastAxisNavigationEventTime(0)
|
||||||
@@ -93,7 +93,6 @@ void SdlGamepadKeyNavigation::disable()
|
|||||||
void SdlGamepadKeyNavigation::onPollingTimerFired()
|
void SdlGamepadKeyNavigation::onPollingTimerFired()
|
||||||
{
|
{
|
||||||
SDL_Event event;
|
SDL_Event event;
|
||||||
StreamingPreferences prefs;
|
|
||||||
|
|
||||||
// Discard any pending button events on the first poll to avoid picking up
|
// Discard any pending button events on the first poll to avoid picking up
|
||||||
// stale input data from the stream session (like the quit combo).
|
// stale input data from the stream session (like the quit combo).
|
||||||
@@ -121,7 +120,7 @@ void SdlGamepadKeyNavigation::onPollingTimerFired()
|
|||||||
QEvent::Type::KeyPress : QEvent::Type::KeyRelease;
|
QEvent::Type::KeyPress : QEvent::Type::KeyRelease;
|
||||||
|
|
||||||
// Swap face buttons if needed
|
// Swap face buttons if needed
|
||||||
if (prefs.swapFaceButtons) {
|
if (m_Prefs->swapFaceButtons) {
|
||||||
switch (event.cbutton.button) {
|
switch (event.cbutton.button) {
|
||||||
case SDL_CONTROLLER_BUTTON_A:
|
case SDL_CONTROLLER_BUTTON_A:
|
||||||
event.cbutton.button = SDL_CONTROLLER_BUTTON_B;
|
event.cbutton.button = SDL_CONTROLLER_BUTTON_B;
|
||||||
@@ -191,7 +190,17 @@ void SdlGamepadKeyNavigation::onPollingTimerFired()
|
|||||||
case SDL_CONTROLLERDEVICEADDED:
|
case SDL_CONTROLLERDEVICEADDED:
|
||||||
SDL_GameController* gc = SDL_GameControllerOpen(event.cdevice.which);
|
SDL_GameController* gc = SDL_GameControllerOpen(event.cdevice.which);
|
||||||
if (gc != nullptr) {
|
if (gc != nullptr) {
|
||||||
m_Gamepads.append(gc);
|
// SDL_CONTROLLERDEVICEADDED can be reported multiple times for the same
|
||||||
|
// gamepad in rare cases, because SDL doesn't fixup the device index in
|
||||||
|
// the SDL_CONTROLLERDEVICEADDED event if an unopened gamepad disappears
|
||||||
|
// before we've processed the add event.
|
||||||
|
if (!m_Gamepads.contains(gc)) {
|
||||||
|
m_Gamepads.append(gc);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// We already have this game controller open
|
||||||
|
SDL_GameControllerClose(gc);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -239,31 +248,6 @@ void SdlGamepadKeyNavigation::onPollingTimerFired()
|
|||||||
sendKey(QEvent::Type::KeyRelease, Qt::Key_Right);
|
sendKey(QEvent::Type::KeyRelease, Qt::Key_Right);
|
||||||
m_LastAxisNavigationEventTime = SDL_GetTicks();
|
m_LastAxisNavigationEventTime = SDL_GetTicks();
|
||||||
}
|
}
|
||||||
|
|
||||||
// In UI navigation mode (settings page), use the right stick to scroll
|
|
||||||
if (m_UiNavMode) {
|
|
||||||
short rightX = SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_RIGHTX);
|
|
||||||
short rightY = SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_RIGHTY);
|
|
||||||
|
|
||||||
QPoint wheelDelta;
|
|
||||||
if (rightX > 30000) {
|
|
||||||
wheelDelta.setX(30);
|
|
||||||
}
|
|
||||||
else if (rightX < -30000) {
|
|
||||||
wheelDelta.setX(-30);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rightY > 30000) {
|
|
||||||
wheelDelta.setY(-30);
|
|
||||||
}
|
|
||||||
else if (rightY < -30000) {
|
|
||||||
wheelDelta.setY(30);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!wheelDelta.isNull()) {
|
|
||||||
sendWheel(wheelDelta);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -277,22 +261,6 @@ void SdlGamepadKeyNavigation::sendKey(QEvent::Type type, Qt::Key key, Qt::Keyboa
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void SdlGamepadKeyNavigation::sendWheel(QPoint& angleDelta)
|
|
||||||
{
|
|
||||||
QGuiApplication* app = static_cast<QGuiApplication*>(QGuiApplication::instance());
|
|
||||||
QWindow* focusWindow = app->focusWindow();
|
|
||||||
if (focusWindow != nullptr) {
|
|
||||||
QPoint mousePos(focusWindow->width() / 2, focusWindow->height() / 2);
|
|
||||||
QPoint globalPos(focusWindow->mapToGlobal(mousePos));
|
|
||||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 12, 0)
|
|
||||||
QWheelEvent wheelEvent(mousePos, globalPos, QPoint(), angleDelta, Qt::NoButton, Qt::NoModifier, Qt::NoScrollPhase, false, Qt::MouseEventSynthesizedByApplication);
|
|
||||||
#else
|
|
||||||
QWheelEvent wheelEvent(mousePos, globalPos, QPoint(), angleDelta, angleDelta.y(), Qt::Vertical, Qt::NoButton, Qt::NoModifier, Qt::NoScrollPhase, Qt::MouseEventSynthesizedByApplication, false);
|
|
||||||
#endif
|
|
||||||
app->sendEvent(focusWindow, &wheelEvent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void SdlGamepadKeyNavigation::setUiNavMode(bool uiNavMode)
|
void SdlGamepadKeyNavigation::setUiNavMode(bool uiNavMode)
|
||||||
{
|
{
|
||||||
m_UiNavMode = uiNavMode;
|
m_UiNavMode = uiNavMode;
|
||||||
|
|||||||
@@ -5,12 +5,14 @@
|
|||||||
|
|
||||||
#include <SDL.h>
|
#include <SDL.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();
|
||||||
|
|
||||||
@@ -24,12 +26,12 @@ public:
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
void sendKey(QEvent::Type type, Qt::Key key, Qt::KeyboardModifiers modifiers = Qt::NoModifier);
|
void sendKey(QEvent::Type type, Qt::Key key, Qt::KeyboardModifiers modifiers = Qt::NoModifier);
|
||||||
void sendWheel(QPoint& angleDelta);
|
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void onPollingTimerFired();
|
void onPollingTimerFired();
|
||||||
|
|
||||||
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;
|
||||||
|
|||||||
Binary file not shown.
File diff suppressed because it is too large.
Load diff
Binary file not shown.
+483
-300
File diff suppressed because it is too large.
Load diff
Binary file not shown.
+557
-374
File diff suppressed because it is too large.
Load diff
Binary file not shown.
+478
-303
File diff suppressed because it is too large.
Load diff
Binary file not shown.
+494
-315
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.
+540
-396
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.
+477
-298
File diff suppressed because it is too large.
Load diff
Binary file not shown.
+779
-568
File diff suppressed because it is too large.
Load diff
Binary file not shown.
+495
-316
File diff suppressed because it is too large.
Load diff
Binary file not shown.
+506
-323
File diff suppressed because it is too large.
Load diff
Binary file not shown.
+482
-299
File diff suppressed because it is too large.
Load diff
Binary file not shown.
File diff suppressed because it is too large.
Load diff
Binary file not shown.
+475
-300
File diff suppressed because it is too large.
Load diff
Binary file not shown.
+477
-298
File diff suppressed because it is too large.
Load diff
Binary file not shown.
+498
-315
File diff suppressed because it is too large.
Load diff
Binary file not shown.
+484
-305
File diff suppressed because it is too large.
Load diff
Binary file not shown.
+487
-304
File diff suppressed because it is too large.
Load diff
Binary file not shown.
+509
-330
File diff suppressed because it is too large.
Load diff
Binary file not shown.
+618
-415
File diff suppressed because it is too large.
Load diff
Binary file not shown.
+525
-342
File diff suppressed because it is too large.
Load diff
Binary file not shown.
+779
-572
File diff suppressed because it is too large.
Load diff
Binary file not shown.
+502
-303
File diff suppressed because it is too large.
Load diff
Binary file not shown.
+481
-302
File diff suppressed because it is too large.
Load diff
Binary file not shown.
File diff suppressed because it is too large.
Load diff
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large.
Load diff
Binary file not shown.
+149
-60
@@ -10,7 +10,8 @@
|
|||||||
#include <QFont>
|
#include <QFont>
|
||||||
#include <QCursor>
|
#include <QCursor>
|
||||||
#include <QElapsedTimer>
|
#include <QElapsedTimer>
|
||||||
#include <QFile>
|
#include <QTemporaryFile>
|
||||||
|
#include <QRegularExpression>
|
||||||
|
|
||||||
// Don't let SDL hook our main function, since Qt is already
|
// Don't let SDL hook our main function, since Qt is already
|
||||||
// doing the same thing. This needs to be before any headers
|
// doing the same thing. This needs to be before any headers
|
||||||
@@ -24,6 +25,9 @@
|
|||||||
|
|
||||||
#if defined(Q_OS_WIN32)
|
#if defined(Q_OS_WIN32)
|
||||||
#include "antihookingprotection.h"
|
#include "antihookingprotection.h"
|
||||||
|
|
||||||
|
#define WIN32_LEAN_AND_MEAN
|
||||||
|
#include <Windows.h>
|
||||||
#elif defined(Q_OS_LINUX)
|
#elif defined(Q_OS_LINUX)
|
||||||
#include <openssl/ssl.h>
|
#include <openssl/ssl.h>
|
||||||
#endif
|
#endif
|
||||||
@@ -44,26 +48,24 @@
|
|||||||
#include "settings/streamingpreferences.h"
|
#include "settings/streamingpreferences.h"
|
||||||
#include "gui/sdlgamepadkeynavigation.h"
|
#include "gui/sdlgamepadkeynavigation.h"
|
||||||
|
|
||||||
#if !defined(QT_DEBUG) && defined(Q_OS_WIN32)
|
#if defined(Q_OS_WIN32)
|
||||||
// Log to file for release Windows builds
|
#define IS_UNSPECIFIED_HANDLE(x) ((x) == INVALID_HANDLE_VALUE || (x) == NULL)
|
||||||
#define USE_CUSTOM_LOGGER
|
|
||||||
|
// Log to file or console dynamically for Windows builds
|
||||||
#define LOG_TO_FILE
|
#define LOG_TO_FILE
|
||||||
#elif defined(Q_OS_UNIX) && !defined(Q_OS_DARWIN)
|
|
||||||
// Use stdout logger on all Linux/BSD builds
|
|
||||||
#define USE_CUSTOM_LOGGER
|
|
||||||
#elif !defined(QT_DEBUG) && defined(Q_OS_DARWIN)
|
#elif !defined(QT_DEBUG) && defined(Q_OS_DARWIN)
|
||||||
// Log to file for release Mac builds
|
// Log to file for release Mac builds
|
||||||
#define USE_CUSTOM_LOGGER
|
|
||||||
#define LOG_TO_FILE
|
#define LOG_TO_FILE
|
||||||
#else
|
#else
|
||||||
// For debug Windows and Mac builds, use default logger
|
// Log to console for debug Mac builds
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_CUSTOM_LOGGER
|
|
||||||
static QElapsedTimer s_LoggerTime;
|
static QElapsedTimer s_LoggerTime;
|
||||||
static QTextStream s_LoggerStream(stdout);
|
static QTextStream s_LoggerStream(stderr);
|
||||||
static QMutex s_LoggerLock;
|
static QMutex s_LoggerLock;
|
||||||
static bool s_SuppressVerboseOutput;
|
static bool s_SuppressVerboseOutput;
|
||||||
|
static QRegularExpression k_RikeyRegex("&rikey=\\w+");
|
||||||
|
static QRegularExpression k_RikeyIdRegex("&rikeyid=[\\d-]+");
|
||||||
#ifdef LOG_TO_FILE
|
#ifdef LOG_TO_FILE
|
||||||
// Max log file size of 10 MB
|
// Max log file size of 10 MB
|
||||||
#define MAX_LOG_SIZE_BYTES (10 * 1024 * 1024)
|
#define MAX_LOG_SIZE_BYTES (10 * 1024 * 1024)
|
||||||
@@ -76,6 +78,22 @@ void logToLoggerStream(QString& message)
|
|||||||
{
|
{
|
||||||
QMutexLocker lock(&s_LoggerLock);
|
QMutexLocker lock(&s_LoggerLock);
|
||||||
|
|
||||||
|
#if defined(QT_DEBUG) && defined(Q_OS_WIN32)
|
||||||
|
// Output log messages to a debugger if attached
|
||||||
|
if (IsDebuggerPresent()) {
|
||||||
|
static QString lineBuffer;
|
||||||
|
lineBuffer += message;
|
||||||
|
if (message.endsWith('\n')) {
|
||||||
|
OutputDebugStringW(lineBuffer.toStdWString().c_str());
|
||||||
|
lineBuffer.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Strip session encryption keys and IVs from the logs
|
||||||
|
message.replace(k_RikeyRegex, "&rikey=REDACTED");
|
||||||
|
message.replace(k_RikeyIdRegex, "&rikeyid=REDACTED");
|
||||||
|
|
||||||
#ifdef LOG_TO_FILE
|
#ifdef LOG_TO_FILE
|
||||||
if (s_LogLimitReached) {
|
if (s_LogLimitReached) {
|
||||||
return;
|
return;
|
||||||
@@ -216,8 +234,6 @@ void ffmpegLogToDiskHandler(void* ptr, int level, const char* fmt, va_list vl)
|
|||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef Q_OS_WIN32
|
#ifdef Q_OS_WIN32
|
||||||
#define WIN32_LEAN_AND_MEAN
|
#define WIN32_LEAN_AND_MEAN
|
||||||
#include <Windows.h>
|
#include <Windows.h>
|
||||||
@@ -301,13 +317,30 @@ int main(int argc, char *argv[])
|
|||||||
Path::initialize(false);
|
Path::initialize(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef USE_CUSTOM_LOGGER
|
// Override the default QML cache directory with the one we chose
|
||||||
|
if (qEnvironmentVariableIsEmpty("QML_DISK_CACHE_PATH")) {
|
||||||
|
qputenv("QML_DISK_CACHE_PATH", Path::getQmlCacheDir().toUtf8());
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef Q_OS_WIN32
|
||||||
|
// Grab the original std handles before we potentially redirect them later
|
||||||
|
HANDLE oldConOut = GetStdHandle(STD_OUTPUT_HANDLE);
|
||||||
|
HANDLE oldConErr = GetStdHandle(STD_ERROR_HANDLE);
|
||||||
|
#endif
|
||||||
|
|
||||||
#ifdef LOG_TO_FILE
|
#ifdef LOG_TO_FILE
|
||||||
QDir tempDir(Path::getLogDir());
|
QDir tempDir(Path::getLogDir());
|
||||||
s_LoggerFile = new QFile(tempDir.filePath(QString("Moonlight-%1.log").arg(QDateTime::currentSecsSinceEpoch())));
|
|
||||||
if (s_LoggerFile->open(QIODevice::WriteOnly | QIODevice::Text)) {
|
#ifdef Q_OS_WIN32
|
||||||
QTextStream(stderr) << "Redirecting log output to " << s_LoggerFile->fileName() << Qt::endl;
|
// Only log to a file if the user didn't redirect stderr somewhere else
|
||||||
s_LoggerStream.setDevice(s_LoggerFile);
|
if (IS_UNSPECIFIED_HANDLE(oldConErr))
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
s_LoggerFile = new QFile(tempDir.filePath(QString("Moonlight-%1.log").arg(QDateTime::currentSecsSinceEpoch())));
|
||||||
|
if (s_LoggerFile->open(QIODevice::WriteOnly | QIODevice::Text)) {
|
||||||
|
QTextStream(stderr) << "Redirecting log output to " << s_LoggerFile->fileName() << Qt::endl;
|
||||||
|
s_LoggerStream.setDevice(s_LoggerFile);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -319,13 +352,20 @@ int main(int argc, char *argv[])
|
|||||||
av_log_set_callback(ffmpegLogToDiskHandler);
|
av_log_set_callback(ffmpegLogToDiskHandler);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef Q_OS_WIN32
|
#ifdef Q_OS_WIN32
|
||||||
// Create a crash dump when we crash on Windows
|
// Create a crash dump when we crash on Windows
|
||||||
SetUnhandledExceptionFilter(UnhandledExceptionHandler);
|
SetUnhandledExceptionFilter(UnhandledExceptionHandler);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifdef LOG_TO_FILE
|
||||||
|
// Prune the oldest existing logs if there are more than 10
|
||||||
|
QStringList existingLogNames = tempDir.entryList(QStringList("Moonlight-*.log"), QDir::NoFilter, QDir::SortFlag::Time);
|
||||||
|
for (int i = 10; i < existingLogNames.size(); i++) {
|
||||||
|
qInfo() << "Removing old log file:" << existingLogNames.at(i);
|
||||||
|
QFile(tempDir.filePath(existingLogNames.at(i))).remove();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
#if defined(Q_OS_WIN32)
|
#if defined(Q_OS_WIN32)
|
||||||
// Force AntiHooking.dll to be statically imported and loaded
|
// Force AntiHooking.dll to be statically imported and loaded
|
||||||
// by ntdll on Win32 platforms by calling a dummy function.
|
// by ntdll on Win32 platforms by calling a dummy function.
|
||||||
@@ -337,6 +377,11 @@ int main(int argc, char *argv[])
|
|||||||
SSL_free(nullptr);
|
SSL_free(nullptr);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
// We keep this at function scope to ensure it stays around while we're running,
|
||||||
|
// becaue the Qt QPA will need to read it. Since the temporary file is only
|
||||||
|
// created when open() is called, this doesn't do any harm for other platforms.
|
||||||
|
QTemporaryFile eglfsConfigFile("eglfs_override_XXXXXX.conf");
|
||||||
|
|
||||||
// Avoid using High DPI on EGLFS. It breaks font rendering.
|
// Avoid using High DPI on EGLFS. It breaks font rendering.
|
||||||
// https://bugreports.qt.io/browse/QTBUG-64377
|
// https://bugreports.qt.io/browse/QTBUG-64377
|
||||||
//
|
//
|
||||||
@@ -370,13 +415,38 @@ int main(int argc, char *argv[])
|
|||||||
qWarning() << "Unable to find a KMSDRM display device!";
|
qWarning() << "Unable to find a KMSDRM display device!";
|
||||||
qWarning() << "On the Raspberry Pi, you must enable the 'fake KMS' driver in raspi-config to use Moonlight outside of the GUI environment.";
|
qWarning() << "On the Raspberry Pi, you must enable the 'fake KMS' driver in raspi-config to use Moonlight outside of the GUI environment.";
|
||||||
}
|
}
|
||||||
|
else if (!qEnvironmentVariableIsSet("QT_QPA_EGLFS_KMS_CONFIG")) {
|
||||||
|
// HACK: Remove this when Qt is fixed to properly check for display support before picking a card
|
||||||
|
QString cardOverride = WMUtils::getDrmCardOverride();
|
||||||
|
if (!cardOverride.isEmpty()) {
|
||||||
|
if (eglfsConfigFile.open()) {
|
||||||
|
qInfo() << "Overriding default Qt EGLFS card selection to" << cardOverride;
|
||||||
|
QTextStream(&eglfsConfigFile) << "{ \"device\": \"" << cardOverride << "\" }";
|
||||||
|
qputenv("QT_QPA_EGLFS_KMS_CONFIG", eglfsConfigFile.fileName().toUtf8());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EGLFS uses OpenGLES 2.0, so we will too. Some embedded platforms may not
|
||||||
|
// even have working OpenGL implementations, so GLES is the only option.
|
||||||
|
// See https://github.com/moonlight-stream/moonlight-qt/issues/868
|
||||||
|
SDL_SetHint(SDL_HINT_RENDER_DRIVER, "opengles2");
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if !defined(Q_PROCESSOR_X86) && defined(SDL_HINT_VIDEO_X11_FORCE_EGL)
|
||||||
|
// Some ARM and RISC-V embedded devices don't have working GLX which can cause
|
||||||
|
// SDL to fail to find a working OpenGL implementation at all. Let's force EGL
|
||||||
|
// on non-x86 platforms, since GLX is deprecated anyway.
|
||||||
|
SDL_SetHint(SDL_HINT_VIDEO_X11_FORCE_EGL, "1");
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef Q_OS_MACOS
|
||||||
// This avoids using the default keychain for SSL, which may cause
|
// This avoids using the default keychain for SSL, which may cause
|
||||||
// password prompts on macOS.
|
// password prompts on macOS.
|
||||||
qputenv("QT_SSL_USE_TEMPORARY_KEYCHAIN", "1");
|
qputenv("QT_SSL_USE_TEMPORARY_KEYCHAIN", "1");
|
||||||
|
#endif
|
||||||
|
|
||||||
#if defined(Q_OS_WIN32) && QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
|
#if defined(Q_OS_WIN32) && QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
|
||||||
if (!qEnvironmentVariableIsSet("QT_OPENGL")) {
|
if (!qEnvironmentVariableIsSet("QT_OPENGL")) {
|
||||||
@@ -399,6 +469,12 @@ int main(int argc, char *argv[])
|
|||||||
qputenv("QSG_RENDER_LOOP", "basic");
|
qputenv("QSG_RENDER_LOOP", "basic");
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#if defined(Q_OS_DARWIN) && defined(QT_DEBUG)
|
||||||
|
// Enable Metal valiation for debug builds
|
||||||
|
qputenv("MTL_DEBUG_LAYER", "1");
|
||||||
|
qputenv("MTL_SHADER_VALIDATION", "1");
|
||||||
|
#endif
|
||||||
|
|
||||||
// We don't want system proxies to apply to us
|
// We don't want system proxies to apply to us
|
||||||
QNetworkProxyFactory::setUseSystemConfiguration(false);
|
QNetworkProxyFactory::setUseSystemConfiguration(false);
|
||||||
|
|
||||||
@@ -414,12 +490,6 @@ int main(int argc, char *argv[])
|
|||||||
// initializing the SDL video subsystem to have any effect.
|
// initializing the SDL video subsystem to have any effect.
|
||||||
SDL_SetHint(SDL_HINT_VIDEO_ALLOW_SCREENSAVER, "1");
|
SDL_SetHint(SDL_HINT_VIDEO_ALLOW_SCREENSAVER, "1");
|
||||||
|
|
||||||
// For SDL backends that support it, use double buffering instead of triple buffering
|
|
||||||
// to save a frame of latency. This doesn't matter for MMAL or DRM renderers since they
|
|
||||||
// are drawing directly to the screen without involving SDL, but it may matter for other
|
|
||||||
// future KMSDRM platforms that use SDL for rendering.
|
|
||||||
SDL_SetHint(SDL_HINT_VIDEO_DOUBLE_BUFFER, "1");
|
|
||||||
|
|
||||||
// We use MMAL to render on Raspberry Pi, so we do not require DRM master.
|
// We use MMAL to render on Raspberry Pi, so we do not require DRM master.
|
||||||
SDL_SetHint("SDL_KMSDRM_REQUIRE_DRM_MASTER", "0");
|
SDL_SetHint("SDL_KMSDRM_REQUIRE_DRM_MASTER", "0");
|
||||||
|
|
||||||
@@ -477,6 +547,13 @@ int main(int argc, char *argv[])
|
|||||||
// SDL doing it for us behind our backs.
|
// SDL doing it for us behind our backs.
|
||||||
SDL_SetHint("SDL_MOUSE_AUTO_CAPTURE", "0");
|
SDL_SetHint("SDL_MOUSE_AUTO_CAPTURE", "0");
|
||||||
|
|
||||||
|
// SDL will try to lock the mouse cursor on Wayland if it's not visible in order to
|
||||||
|
// support applications that assume they can warp the cursor (which isn't possible
|
||||||
|
// on Wayland). We don't want this behavior because it interferes with seamless mouse
|
||||||
|
// mode when toggling between windowed and fullscreen modes by unexpectedly locking
|
||||||
|
// the mouse cursor.
|
||||||
|
SDL_SetHint("SDL_VIDEO_WAYLAND_EMULATE_MOUSE_WARP", "0");
|
||||||
|
|
||||||
#ifdef QT_DEBUG
|
#ifdef QT_DEBUG
|
||||||
// Allow thread naming using exceptions on debug builds. SDL doesn't use SEH
|
// Allow thread naming using exceptions on debug builds. SDL doesn't use SEH
|
||||||
// when throwing the exceptions, so we don't enable it for release builds out
|
// when throwing the exceptions, so we don't enable it for release builds out
|
||||||
@@ -486,25 +563,34 @@ int main(int argc, char *argv[])
|
|||||||
|
|
||||||
QGuiApplication app(argc, argv);
|
QGuiApplication app(argc, argv);
|
||||||
|
|
||||||
|
#ifndef STEAM_LINK
|
||||||
|
// Force use of the KMSDRM backend for SDL when using Qt platform plugins
|
||||||
|
// that directly draw to the display without a windowing system.
|
||||||
|
if (QGuiApplication::platformName() == "eglfs" || QGuiApplication::platformName() == "linuxfb") {
|
||||||
|
qputenv("SDL_VIDEODRIVER", "kmsdrm");
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
GlobalCommandLineParser parser;
|
GlobalCommandLineParser parser;
|
||||||
GlobalCommandLineParser::ParseResult commandLineParserResult = parser.parse(app.arguments());
|
GlobalCommandLineParser::ParseResult commandLineParserResult = parser.parse(app.arguments());
|
||||||
switch (commandLineParserResult) {
|
switch (commandLineParserResult) {
|
||||||
case GlobalCommandLineParser::ListRequested:
|
case GlobalCommandLineParser::ListRequested:
|
||||||
#ifdef USE_CUSTOM_LOGGER
|
|
||||||
// Don't log to the console since it will jumble the command output
|
// Don't log to the console since it will jumble the command output
|
||||||
s_SuppressVerboseOutput = true;
|
s_SuppressVerboseOutput = true;
|
||||||
#endif
|
|
||||||
#ifdef Q_OS_WIN32
|
#ifdef Q_OS_WIN32
|
||||||
// Attach to the console to be able to print output.
|
// If we don't have stdout or stderr handles (which will normally be the case
|
||||||
// Since we're a /SUBSYSTEM:WINDOWS app, we won't be attached by default.
|
// since we're a /SUBSYSTEM:WINDOWS app), attach to our parent console and use
|
||||||
|
// that for stdout and stderr.
|
||||||
|
//
|
||||||
|
// If we do have stdout or stderr handles, that means the user has used standard
|
||||||
|
// handle redirection. In that case, we don't want to override those handles.
|
||||||
if (AttachConsole(ATTACH_PARENT_PROCESS)) {
|
if (AttachConsole(ATTACH_PARENT_PROCESS)) {
|
||||||
HANDLE conOut = GetStdHandle(STD_OUTPUT_HANDLE);
|
// If we didn't have an old stdout/stderr handle, use the new CONOUT$ handle
|
||||||
if (conOut != INVALID_HANDLE_VALUE && conOut != NULL) {
|
if (IS_UNSPECIFIED_HANDLE(oldConOut)) {
|
||||||
freopen("CONOUT$", "w", stdout);
|
freopen("CONOUT$", "w", stdout);
|
||||||
setvbuf(stdout, NULL, _IONBF, 0);
|
setvbuf(stdout, NULL, _IONBF, 0);
|
||||||
}
|
}
|
||||||
HANDLE conErr = GetStdHandle(STD_ERROR_HANDLE);
|
if (IS_UNSPECIFIED_HANDLE(oldConErr)) {
|
||||||
if (conErr != INVALID_HANDLE_VALUE && conErr != NULL) {
|
|
||||||
freopen("CONOUT$", "w", stderr);
|
freopen("CONOUT$", "w", stderr);
|
||||||
setvbuf(stderr, NULL, _IONBF, 0);
|
setvbuf(stderr, NULL, _IONBF, 0);
|
||||||
}
|
}
|
||||||
@@ -528,8 +614,14 @@ int main(int argc, char *argv[])
|
|||||||
runtimeVersion.major, runtimeVersion.minor, runtimeVersion.patch);
|
runtimeVersion.major, runtimeVersion.minor, runtimeVersion.patch);
|
||||||
|
|
||||||
// Apply the initial translation based on user preference
|
// Apply the initial translation based on user preference
|
||||||
StreamingPreferences prefs;
|
StreamingPreferences::get()->retranslate();
|
||||||
prefs.retranslate();
|
|
||||||
|
// Trickily declare the translation for dialog buttons
|
||||||
|
QCoreApplication::translate("QPlatformTheme", "&Yes");
|
||||||
|
QCoreApplication::translate("QPlatformTheme", "&No");
|
||||||
|
QCoreApplication::translate("QPlatformTheme", "OK");
|
||||||
|
QCoreApplication::translate("QPlatformTheme", "Help");
|
||||||
|
QCoreApplication::translate("QPlatformTheme", "Cancel");
|
||||||
|
|
||||||
// After the QGuiApplication is created, the platform stuff will be initialized
|
// After the QGuiApplication is created, the platform stuff will be initialized
|
||||||
// and we can set the SDL video driver to match Qt.
|
// and we can set the SDL video driver to match Qt.
|
||||||
@@ -587,8 +679,8 @@ int main(int argc, char *argv[])
|
|||||||
qmlRegisterUncreatableType<Session>("Session", 1, 0, "Session", "Session cannot be created from QML");
|
qmlRegisterUncreatableType<Session>("Session", 1, 0, "Session", "Session cannot be created from QML");
|
||||||
qmlRegisterSingletonType<ComputerManager>("ComputerManager", 1, 0,
|
qmlRegisterSingletonType<ComputerManager>("ComputerManager", 1, 0,
|
||||||
"ComputerManager",
|
"ComputerManager",
|
||||||
[](QQmlEngine*, QJSEngine*) -> QObject* {
|
[](QQmlEngine* qmlEngine, QJSEngine*) -> QObject* {
|
||||||
return new ComputerManager();
|
return new ComputerManager(StreamingPreferences::get(qmlEngine));
|
||||||
});
|
});
|
||||||
qmlRegisterSingletonType<AutoUpdateChecker>("AutoUpdateChecker", 1, 0,
|
qmlRegisterSingletonType<AutoUpdateChecker>("AutoUpdateChecker", 1, 0,
|
||||||
"AutoUpdateChecker",
|
"AutoUpdateChecker",
|
||||||
@@ -602,41 +694,31 @@ int main(int argc, char *argv[])
|
|||||||
});
|
});
|
||||||
qmlRegisterSingletonType<SdlGamepadKeyNavigation>("SdlGamepadKeyNavigation", 1, 0,
|
qmlRegisterSingletonType<SdlGamepadKeyNavigation>("SdlGamepadKeyNavigation", 1, 0,
|
||||||
"SdlGamepadKeyNavigation",
|
"SdlGamepadKeyNavigation",
|
||||||
[](QQmlEngine*, QJSEngine*) -> QObject* {
|
[](QQmlEngine* qmlEngine, QJSEngine*) -> QObject* {
|
||||||
return new SdlGamepadKeyNavigation();
|
return new SdlGamepadKeyNavigation(StreamingPreferences::get(qmlEngine));
|
||||||
});
|
});
|
||||||
qmlRegisterSingletonType<StreamingPreferences>("StreamingPreferences", 1, 0,
|
qmlRegisterSingletonType<StreamingPreferences>("StreamingPreferences", 1, 0,
|
||||||
"StreamingPreferences",
|
"StreamingPreferences",
|
||||||
[](QQmlEngine* qmlEngine, QJSEngine*) -> QObject* {
|
[](QQmlEngine* qmlEngine, QJSEngine*) -> QObject* {
|
||||||
return new StreamingPreferences(qmlEngine);
|
return StreamingPreferences::get(qmlEngine);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create the identity manager on the main thread
|
// Create the identity manager on the main thread
|
||||||
IdentityManager::get();
|
IdentityManager::get();
|
||||||
|
|
||||||
#ifndef Q_OS_WINRT
|
// We require the Material theme
|
||||||
// Use the dense material dark theme by default
|
QQuickStyle::setStyle("Material");
|
||||||
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_STYLE")) {
|
|
||||||
qputenv("QT_QUICK_CONTROLS_STYLE", "Material");
|
// Our icons are styled for a dark theme, so we do not allow the user to override this
|
||||||
}
|
qputenv("QT_QUICK_CONTROLS_MATERIAL_THEME", "Dark");
|
||||||
#else
|
|
||||||
// Use universal dark on WinRT
|
// These are defaults that we allow the user to override
|
||||||
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_STYLE")) {
|
|
||||||
qputenv("QT_QUICK_CONTROLS_STYLE", "Universal");
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_MATERIAL_THEME")) {
|
|
||||||
qputenv("QT_QUICK_CONTROLS_MATERIAL_THEME", "Dark");
|
|
||||||
}
|
|
||||||
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_MATERIAL_ACCENT")) {
|
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_MATERIAL_ACCENT")) {
|
||||||
qputenv("QT_QUICK_CONTROLS_MATERIAL_ACCENT", "Purple");
|
qputenv("QT_QUICK_CONTROLS_MATERIAL_ACCENT", "Purple");
|
||||||
}
|
}
|
||||||
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_MATERIAL_VARIANT")) {
|
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_MATERIAL_VARIANT")) {
|
||||||
qputenv("QT_QUICK_CONTROLS_MATERIAL_VARIANT", "Dense");
|
qputenv("QT_QUICK_CONTROLS_MATERIAL_VARIANT", "Dense");
|
||||||
}
|
}
|
||||||
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_UNIVERSAL_THEME")) {
|
|
||||||
qputenv("QT_QUICK_CONTROLS_UNIVERSAL_THEME", "Dark");
|
|
||||||
}
|
|
||||||
|
|
||||||
QQmlApplicationEngine engine;
|
QQmlApplicationEngine engine;
|
||||||
QString initialView;
|
QString initialView;
|
||||||
@@ -649,7 +731,7 @@ int main(int argc, char *argv[])
|
|||||||
case GlobalCommandLineParser::StreamRequested:
|
case GlobalCommandLineParser::StreamRequested:
|
||||||
{
|
{
|
||||||
initialView = "qrc:/gui/CliStartStreamSegue.qml";
|
initialView = "qrc:/gui/CliStartStreamSegue.qml";
|
||||||
StreamingPreferences* preferences = new StreamingPreferences(&app);
|
StreamingPreferences* preferences = StreamingPreferences::get();
|
||||||
StreamCommandLineParser streamParser;
|
StreamCommandLineParser streamParser;
|
||||||
streamParser.parse(app.arguments(), preferences);
|
streamParser.parse(app.arguments(), preferences);
|
||||||
QString host = streamParser.getHost();
|
QString host = streamParser.getHost();
|
||||||
@@ -681,7 +763,7 @@ int main(int argc, char *argv[])
|
|||||||
ListCommandLineParser listParser;
|
ListCommandLineParser listParser;
|
||||||
listParser.parse(app.arguments());
|
listParser.parse(app.arguments());
|
||||||
auto launcher = new CliListApps::Launcher(listParser.getHost(), listParser, &app);
|
auto launcher = new CliListApps::Launcher(listParser.getHost(), listParser, &app);
|
||||||
launcher->execute(new ComputerManager(&app));
|
launcher->execute(new ComputerManager(StreamingPreferences::get()));
|
||||||
hasGUI = false;
|
hasGUI = false;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -702,5 +784,12 @@ int main(int argc, char *argv[])
|
|||||||
// sometimes freezing and blocking process exit.
|
// sometimes freezing and blocking process exit.
|
||||||
QThreadPool::globalInstance()->waitForDone(30000);
|
QThreadPool::globalInstance()->waitForDone(30000);
|
||||||
|
|
||||||
|
#ifdef Q_OS_WIN32
|
||||||
|
// Without an explicit flush, console redirection for the list command
|
||||||
|
// doesn't work reliably (sometimes the target file contains no text).
|
||||||
|
fflush(stderr);
|
||||||
|
fflush(stdout);
|
||||||
|
#endif
|
||||||
|
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
+3
-3
@@ -34,7 +34,7 @@
|
|||||||
|
|
||||||
// Qt's DRM master FD grabbed by our hook
|
// Qt's DRM master FD grabbed by our hook
|
||||||
int g_QtDrmMasterFd = -1;
|
int g_QtDrmMasterFd = -1;
|
||||||
struct stat64 g_DrmMasterStat;
|
struct stat g_DrmMasterStat;
|
||||||
|
|
||||||
// The DRM master FD created for SDL
|
// The DRM master FD created for SDL
|
||||||
int g_SdlDrmMasterFd = -1;
|
int g_SdlDrmMasterFd = -1;
|
||||||
@@ -55,7 +55,7 @@ int drmModeSetCrtc(int fd, uint32_t crtcId, uint32_t bufferId,
|
|||||||
// EGLFS backend's DRM FD, on which we will call drmDropMaster() later.
|
// EGLFS backend's DRM FD, on which we will call drmDropMaster() later.
|
||||||
if (g_QtDrmMasterFd == -1 && drmIsMaster(fd)) {
|
if (g_QtDrmMasterFd == -1 && drmIsMaster(fd)) {
|
||||||
g_QtDrmMasterFd = fd;
|
g_QtDrmMasterFd = fd;
|
||||||
fstat64(g_QtDrmMasterFd, &g_DrmMasterStat);
|
fstat(g_QtDrmMasterFd, &g_DrmMasterStat);
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"Captured Qt EGLFS DRM master fd (legacy): %d",
|
"Captured Qt EGLFS DRM master fd (legacy): %d",
|
||||||
g_QtDrmMasterFd);
|
g_QtDrmMasterFd);
|
||||||
@@ -73,7 +73,7 @@ int drmModeAtomicCommit(int fd, drmModeAtomicReqPtr req,
|
|||||||
// EGLFS backend's DRM FD, on which we will call drmDropMaster() later.
|
// EGLFS backend's DRM FD, on which we will call drmDropMaster() later.
|
||||||
if (g_QtDrmMasterFd == -1 && drmIsMaster(fd)) {
|
if (g_QtDrmMasterFd == -1 && drmIsMaster(fd)) {
|
||||||
g_QtDrmMasterFd = fd;
|
g_QtDrmMasterFd = fd;
|
||||||
fstat64(g_QtDrmMasterFd, &g_DrmMasterStat);
|
fstat(g_QtDrmMasterFd, &g_DrmMasterStat);
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"Captured Qt EGLFS DRM master fd (atomic): %d",
|
"Captured Qt EGLFS DRM master fd (atomic): %d",
|
||||||
g_QtDrmMasterFd);
|
g_QtDrmMasterFd);
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
#if SDL_VERSION_ATLEAST(2, 0, 15)
|
#if SDL_VERSION_ATLEAST(2, 0, 15)
|
||||||
|
|
||||||
extern int g_QtDrmMasterFd;
|
extern int g_QtDrmMasterFd;
|
||||||
extern struct stat64 g_DrmMasterStat;
|
extern struct stat g_DrmMasterStat;
|
||||||
extern int g_SdlDrmMasterFd;
|
extern int g_SdlDrmMasterFd;
|
||||||
|
|
||||||
int openHook(const char *funcname, const char *pathname, int flags, va_list va)
|
int openHook(const char *funcname, const char *pathname, int flags, va_list va)
|
||||||
@@ -50,9 +50,9 @@ int openHook(const char *funcname, const char *pathname, int flags, va_list va)
|
|||||||
if (fd >= 0 && g_QtDrmMasterFd != -1) {
|
if (fd >= 0 && g_QtDrmMasterFd != -1) {
|
||||||
if (strncmp(pathname, "/dev/dri/card", 13) == 0) {
|
if (strncmp(pathname, "/dev/dri/card", 13) == 0) {
|
||||||
// It's a DRM device, but is it _our_ DRM device?
|
// It's a DRM device, but is it _our_ DRM device?
|
||||||
struct stat64 fdstat;
|
struct stat fdstat;
|
||||||
|
|
||||||
fstat64(fd, &fdstat);
|
fstat(fd, &fdstat);
|
||||||
if (g_DrmMasterStat.st_dev == fdstat.st_dev &&
|
if (g_DrmMasterStat.st_dev == fdstat.st_dev &&
|
||||||
g_DrmMasterStat.st_ino == fdstat.st_ino) {
|
g_DrmMasterStat.st_ino == fdstat.st_ino) {
|
||||||
// It is our device. Time to do the magic!
|
// It is our device. Time to do the magic!
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
QString Path::s_CacheDir;
|
QString Path::s_CacheDir;
|
||||||
QString Path::s_LogDir;
|
QString Path::s_LogDir;
|
||||||
QString Path::s_BoxArtCacheDir;
|
QString Path::s_BoxArtCacheDir;
|
||||||
|
QString Path::s_QmlCacheDir;
|
||||||
|
|
||||||
QString Path::getLogDir()
|
QString Path::getLogDir()
|
||||||
{
|
{
|
||||||
@@ -22,6 +23,12 @@ QString Path::getBoxArtCacheDir()
|
|||||||
return s_BoxArtCacheDir;
|
return s_BoxArtCacheDir;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QString Path::getQmlCacheDir()
|
||||||
|
{
|
||||||
|
Q_ASSERT(!s_QmlCacheDir.isEmpty());
|
||||||
|
return s_QmlCacheDir;
|
||||||
|
}
|
||||||
|
|
||||||
QByteArray Path::readDataFile(QString fileName)
|
QByteArray Path::readDataFile(QString fileName)
|
||||||
{
|
{
|
||||||
QFile dataFile(getDataFilePath(fileName));
|
QFile dataFile(getDataFilePath(fileName));
|
||||||
@@ -97,6 +104,7 @@ void Path::initialize(bool portable)
|
|||||||
if (portable) {
|
if (portable) {
|
||||||
s_LogDir = QDir::currentPath();
|
s_LogDir = QDir::currentPath();
|
||||||
s_BoxArtCacheDir = QDir::currentPath() + "/boxart";
|
s_BoxArtCacheDir = QDir::currentPath() + "/boxart";
|
||||||
|
s_QmlCacheDir = QDir::currentPath() + "/qmlcache";
|
||||||
|
|
||||||
// In order for the If-Modified-Since logic to work in MappingFetcher,
|
// In order for the If-Modified-Since logic to work in MappingFetcher,
|
||||||
// the cache directory must be different than the current directory.
|
// the cache directory must be different than the current directory.
|
||||||
@@ -112,5 +120,6 @@ void Path::initialize(bool portable)
|
|||||||
#endif
|
#endif
|
||||||
s_CacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation);
|
s_CacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation);
|
||||||
s_BoxArtCacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/boxart";
|
s_BoxArtCacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/boxart";
|
||||||
|
s_QmlCacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/qmlcache";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+2
-1
@@ -7,8 +7,8 @@ class Path
|
|||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
static QString getLogDir();
|
static QString getLogDir();
|
||||||
|
|
||||||
static QString getBoxArtCacheDir();
|
static QString getBoxArtCacheDir();
|
||||||
|
static QString getQmlCacheDir();
|
||||||
|
|
||||||
static QByteArray readDataFile(QString fileName);
|
static QByteArray readDataFile(QString fileName);
|
||||||
static void writeCacheFile(QString fileName, QByteArray data);
|
static void writeCacheFile(QString fileName, QByteArray data);
|
||||||
@@ -24,4 +24,5 @@ private:
|
|||||||
static QString s_CacheDir;
|
static QString s_CacheDir;
|
||||||
static QString s_LogDir;
|
static QString s_LogDir;
|
||||||
static QString s_BoxArtCacheDir;
|
static QString s_BoxArtCacheDir;
|
||||||
|
static QString s_QmlCacheDir;
|
||||||
};
|
};
|
||||||
File renamed without changes.
Loaded 100 of 219 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user