mirror of
https://github.com/moonlight-stream/moonlight-qt.git
synced 2026-10-13 03:53:59 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3cb169ea26 | ||
|
|
58f87e39a9 | ||
|
|
b1c8005d1a | ||
|
|
f8a2e22a18 | ||
|
|
7ea118fbae | ||
|
|
2149024c35 | ||
|
|
f5d71c8c9a | ||
|
|
e108a05895 | ||
|
|
ea8bccd508 | ||
|
|
bca919c48c | ||
|
|
511ffe03b5 | ||
|
|
413993ab6f | ||
|
|
948b9c818a | ||
|
|
cf615ea88d | ||
|
|
2d1e16fe50 | ||
|
|
089c3d3ad3 | ||
|
|
63e3fa6609 | ||
|
|
8a7fa7e559 | ||
|
|
989a90a0f7 | ||
|
|
a707720577 | ||
|
|
eef270de26 | ||
|
|
0ac765bc32 | ||
|
|
e76551cb64 | ||
|
|
4fd902a0d3 | ||
|
|
9cba449092 | ||
|
|
905b2ecf9e | ||
|
|
53c2c612c9 | ||
|
|
8ffadde9cc | ||
|
|
f569bf633c | ||
|
|
6eb5e07007 | ||
|
|
f07201baa9 | ||
|
|
9120fd01a8 | ||
|
|
c14cc70a6a | ||
|
|
a66f91ae3c | ||
|
|
7e4fddbe62 | ||
|
|
0ff9f8c55a | ||
|
|
6f710faa8e | ||
|
|
2127e1d90b | ||
|
|
9d73aa6b4a | ||
|
|
a2f23b7053 | ||
|
|
5974980aff | ||
|
|
6fa53691aa | ||
|
|
49e0679864 | ||
|
|
0976bc39b3 | ||
|
|
facd6e4e56 | ||
|
|
d2dc0aa1b1 | ||
|
|
aa74e6930b | ||
|
|
b7811bf196 | ||
|
|
18130fd8de | ||
|
|
b6d4f97e96 | ||
|
|
0dd0112518 | ||
|
|
5711e6064e | ||
|
|
517b684f85 | ||
|
|
39465f6d8a | ||
|
|
738813cbac | ||
|
|
c2778e040a | ||
|
|
1efdeeb9dc | ||
|
|
b01dfea343 | ||
|
|
e143293e91 | ||
|
|
f420dc740a | ||
|
|
2e532224b7 | ||
|
|
cb8366a7e5 | ||
|
|
fee54a9d76 | ||
|
|
dc556d3c9b | ||
|
|
4fed3802aa | ||
|
|
f7bb4611e9 | ||
|
|
57791eec62 | ||
|
|
e1e3d8e710 | ||
|
|
654be492a0 | ||
|
|
1df0da4d4a | ||
|
|
1dd6cdb567 | ||
|
|
edfb3cfdc8 | ||
|
|
c6a8f44640 | ||
|
|
b7301d4415 | ||
|
|
4d668702b5 | ||
|
|
f4b0f4d78f | ||
|
|
7100262e09 | ||
|
|
e516943d53 | ||
|
|
fd41226cdd | ||
|
|
47b04eee3e | ||
|
|
24ee6f96e1 | ||
|
|
6e34c21175 | ||
|
|
64789ea8bc | ||
|
|
2ac7176787 | ||
|
|
b968886594 | ||
|
|
2f9c44103b | ||
|
|
e9ed4940cd | ||
|
|
85b5845ebb | ||
|
|
6f4db913aa | ||
|
|
2d20c711a6 | ||
|
|
2e829bc85f | ||
|
|
acddc3cabd | ||
|
|
a16647f16c | ||
|
|
3e3f384218 | ||
|
|
45a98a391a | ||
|
|
c9992201ce | ||
|
|
80062d262e | ||
|
|
652f7a7710 | ||
|
|
7e7e50ada2 | ||
|
|
87eba9bb89 | ||
|
|
da1fcd49fc | ||
|
|
c98289a9e7 | ||
|
|
1d0b65f798 | ||
|
|
3094185cd6 | ||
|
|
aaff04167b | ||
|
|
21bba02e6f | ||
|
|
f60e724167 | ||
|
|
7551e90899 | ||
|
|
715429c13c | ||
|
|
7976189d56 | ||
|
|
47d1eff45d | ||
|
|
073df32b01 | ||
|
|
e8ec7efa5c | ||
|
|
f08cdae2b4 | ||
|
|
1389d29d8c | ||
|
|
d4bfde7906 | ||
|
|
fe73c36958 | ||
|
|
a97f6d90f4 | ||
|
|
0735f164a8 | ||
|
|
9de0c215ba | ||
|
|
56474822f7 | ||
|
|
98b18d89eb | ||
|
|
78ea79b95a | ||
|
|
acbe503c12 | ||
|
|
0261c41574 | ||
|
|
c8182dfab3 | ||
|
|
2eee9c019f | ||
|
|
9ed40b5d9f | ||
|
|
ebc4c71c72 | ||
|
|
dec5a6370e | ||
|
|
519858b708 | ||
|
|
8fcc4cf7fe | ||
|
|
2f78350696 | ||
|
|
85d4037a89 | ||
|
|
d165bf7498 | ||
|
|
94d821a4a9 | ||
|
|
6a854f7a64 | ||
|
|
d76675f4b1 | ||
|
|
4a7e43ccb6 | ||
|
|
b53333d1ba | ||
|
|
f1f3c8bfb9 | ||
|
|
e580e7c672 | ||
|
|
a62fd96269 | ||
|
|
a1d592bb87 | ||
|
|
83f5f6cefc | ||
|
|
9166e70524 | ||
|
|
41f8fa95fe | ||
|
|
b140c21ed9 | ||
|
|
863712cb6e | ||
|
|
43f0aa7f06 | ||
|
|
12535d8142 | ||
|
|
8cf6a8bc98 | ||
|
|
7073dd26de | ||
|
|
66bfd2fad8 | ||
|
|
bb444f306a | ||
|
|
626fba4a8f | ||
|
|
9dc96bda58 | ||
|
|
1a2f1ed620 | ||
|
|
50255dd896 | ||
|
|
c8b9fda258 | ||
|
|
bb41732e59 | ||
|
|
7d9b984747 | ||
|
|
7404e5fce2 | ||
|
|
dc5414bd0d | ||
|
|
d147d36867 | ||
|
|
7763ec3c5c | ||
|
|
c61083ccd9 | ||
|
|
841aeccd6b | ||
|
|
bfcd373eec | ||
|
|
202ef41894 | ||
|
|
f112edc3c3 | ||
|
|
05fd7b18b9 | ||
|
|
9764174070 | ||
|
|
6b0370f0bc | ||
|
|
2e4313e7c3 | ||
|
|
2684d1874a | ||
|
|
882539a700 | ||
|
|
42960b7bf1 | ||
|
|
333f978186 | ||
|
|
e7e9df3261 | ||
|
|
24dbc96fe5 | ||
|
|
abc391f32a | ||
|
|
a589e8e3de | ||
|
|
39fa9a91b8 | ||
|
|
e10d98fb79 | ||
|
|
d17cdbda93 | ||
|
|
2252abefbf | ||
|
|
ce60a146dc | ||
|
|
cd4a6f3e17 | ||
|
|
fc5e1ed5d8 | ||
|
|
d70b98075e | ||
|
|
a4d9952061 | ||
|
|
45e4dfe09c | ||
|
|
34ba9063ad | ||
|
|
d5e2d59fe9 | ||
|
|
d39bdb28d6 | ||
|
|
8bf5622d21 | ||
|
|
9cafd5e099 | ||
|
|
d1b3bfd700 | ||
|
|
e60caf06f9 | ||
|
|
a243c6bb90 | ||
|
|
10523a7e27 | ||
|
|
ea06ec0133 | ||
|
|
e287ebcded | ||
|
|
2ee90289a1 | ||
|
|
193df861d1 | ||
|
|
9055496a19 | ||
|
|
5424576f73 | ||
|
|
a81c6a1c5e | ||
|
|
53a572a4a4 | ||
|
|
df283d80c6 | ||
|
|
b6d203b6dd | ||
|
|
ef988cc8af | ||
|
|
ba46905243 | ||
|
|
df0c4c8208 | ||
|
|
e5aaa1fc53 | ||
|
|
8b6df34d8b | ||
|
|
7a719aa886 | ||
|
|
9af58af5e4 | ||
|
|
b945c8c2dc | ||
|
|
2165f56aee | ||
|
|
e42de9d155 | ||
|
|
7fd95b9de2 | ||
|
|
a42c85802e | ||
|
|
cb3730100d | ||
|
|
7a04d2ad2c | ||
|
|
1f93976919 | ||
|
|
0b7505f109 | ||
|
|
392ca2d8b7 | ||
|
|
68daa90b7e | ||
|
|
fb39ad896e | ||
|
|
cb199054e3 | ||
|
|
7cf77211b0 | ||
|
|
a3d1e9a117 | ||
|
|
755c7c9895 | ||
|
|
45b5136233 | ||
|
|
f16b5cd337 | ||
|
|
21cda8141d | ||
|
|
90f26dfa45 | ||
|
|
ba507d8046 | ||
|
|
5639bcc0f5 | ||
|
|
4cd030582d | ||
|
|
7908076b39 | ||
|
|
ca91f9b702 | ||
|
|
0e86af1c9b | ||
|
|
c5ad84e2ee | ||
|
|
bfb5d6e8fe | ||
|
|
d63b0b5978 | ||
|
|
5e6b60bae0 | ||
|
|
65b6885c61 | ||
|
|
e8ceaa4085 | ||
|
|
db2b120b68 | ||
|
|
5f315d0093 | ||
|
|
016b212ff7 | ||
|
|
bccffe09e2 | ||
|
|
a0365c8e1c | ||
|
|
38156c9f7f | ||
|
|
640db09a64 | ||
|
|
5baa8c680a | ||
|
|
e1c4a488ed | ||
|
|
332d4433c4 | ||
|
|
f5d535b1ef | ||
|
|
35e226273c | ||
|
|
90e25e60d6 | ||
|
|
80659160d3 | ||
|
|
5de29928cd | ||
|
|
c65cea0f66 | ||
|
|
b3cfdbb835 | ||
|
|
9c15cceee7 | ||
|
|
8a0142bd0f | ||
|
|
3d6e05d8c0 | ||
|
|
ddf0e6a935 | ||
|
|
51852f044e | ||
|
|
17cad6b3ca | ||
|
|
8912869077 | ||
|
|
a2697634ba | ||
|
|
1bfb798107 | ||
|
|
cf60568f53 | ||
|
|
ddd18fdaf6 | ||
|
|
b04bb30930 | ||
|
|
e41aaf980b | ||
|
|
46df6601a1 | ||
|
|
5eb8b97469 | ||
|
|
8cd0536fb1 | ||
|
|
99a6789720 | ||
|
|
92670cc051 | ||
|
|
89cbbfc291 | ||
|
|
31869a7da2 | ||
|
|
68c6d6c932 | ||
|
|
e4e140fa53 | ||
|
|
b9709d0825 | ||
|
|
324e3dffa0 | ||
|
|
dd9de3fcb4 | ||
|
|
6710d0561a | ||
|
|
a30f97db0d | ||
|
|
cae666b834 | ||
|
|
5970ca49e9 | ||
|
|
6ae6218043 | ||
|
|
8e3e19a7f7 | ||
|
|
e3d51fd7f7 | ||
|
|
95f5c1d104 | ||
|
|
02a14069a3 | ||
|
|
394f28339e | ||
|
|
411998d4e3 | ||
|
|
371906445e | ||
|
|
34d91e58c0 | ||
|
|
f3f25bec7a | ||
|
|
08d74bea75 | ||
|
|
63f917b317 | ||
|
|
448633ec3c | ||
|
|
09ce3c9ce4 | ||
|
|
8fcc98102b | ||
|
|
e14075b464 | ||
|
|
ca89d14ec8 | ||
|
|
3f51998065 | ||
|
|
6e5b6a1a5c | ||
|
|
660f6c5bbf | ||
|
|
5034bd72ed | ||
|
|
5b7c144796 | ||
|
|
e45b6b9b85 | ||
|
|
245f242fb8 | ||
|
|
a6fccf93d1 | ||
|
|
3dcab90752 | ||
|
|
30a5f405f5 | ||
|
|
bad1c9628e | ||
|
|
1ab7bca43f | ||
|
|
7d0a53773c | ||
|
|
789236ee8f | ||
|
|
21cba0b080 | ||
|
|
31eb0891e5 | ||
|
|
a13f8e7937 | ||
|
|
5bfe56472f | ||
|
|
16a9ed682e | ||
|
|
59cae610d2 | ||
|
|
29ac7893d7 | ||
|
|
c8aeca8ace | ||
|
|
3da505c4c6 | ||
|
|
06bc05b708 | ||
|
|
a3dc9593c2 | ||
|
|
18a271a0ec | ||
|
|
cebd3bf751 | ||
|
|
cb04925c6e | ||
|
|
b7a340bd00 | ||
|
|
2a0f837120 | ||
|
|
d00bdff495 | ||
|
|
7e9c977b3d | ||
|
|
1629151528 | ||
|
|
24295197bd | ||
|
|
807dd84daa | ||
|
|
17848c097c | ||
|
|
563b760d31 | ||
|
|
8465a27a97 | ||
|
|
255cbd1e7e | ||
|
|
7717749b72 | ||
|
|
9e4337babf | ||
|
|
6771877a84 | ||
|
|
a860bd8dd5 | ||
|
|
704ce4ff0c | ||
|
|
21edcc8113 | ||
|
|
125738f13d | ||
|
|
02c781b8f4 | ||
|
|
453fe6aea0 | ||
|
|
e0fb7dfd14 | ||
|
|
c4012225c2 | ||
|
|
1750243537 | ||
|
|
84fc5f3008 | ||
|
|
8a224fbae4 | ||
|
|
e6c4332445 | ||
|
|
0928d1fab2 | ||
|
|
40dd588bef | ||
|
|
472e8ee92e | ||
|
|
c55af7fff3 | ||
|
|
7a39daf179 | ||
|
|
8d087ba5b1 | ||
|
|
c7a30d8728 | ||
|
|
1f678cafba | ||
|
|
579e0bc464 | ||
|
|
7f36dc5aad | ||
|
|
a7c2026128 | ||
|
|
7b0d67ebbe | ||
|
|
63f0910eac | ||
|
|
b16cf9fcc5 | ||
|
|
5501d7bc6b | ||
|
|
413ef42a34 | ||
|
|
a1694ab951 | ||
|
|
fbbbd5e0b8 | ||
|
|
f8c7e2ee41 | ||
|
|
2a4af0bc2c | ||
|
|
d24db13e9d | ||
|
|
bc68aad67b | ||
|
|
9e8a4cab7b | ||
|
|
3679964cf8 | ||
|
|
225ec1a87f | ||
|
|
071c76c06a | ||
|
|
d77a255721 | ||
|
|
26ee1aee25 | ||
|
|
b047395938 | ||
|
|
483a601867 | ||
|
|
c4bc2f6adf | ||
|
|
439e8fe33f | ||
|
|
99885f5b4b | ||
|
|
78b522ec1d | ||
|
|
dbfa5a9358 | ||
|
|
3910b3862a | ||
|
|
3b8e3b7d5c | ||
|
|
6129000dde | ||
|
|
65b274d695 | ||
|
|
a13dd0076b | ||
|
|
cc026c995c | ||
|
|
81cfacaa3d | ||
|
|
fabebc05d2 | ||
|
|
07a97bab03 | ||
|
|
69ac810c82 | ||
|
|
3f4609dc6e | ||
|
|
1ddb529d26 | ||
|
|
ab6b88073c | ||
|
|
ad3ed97472 | ||
|
|
07e902fe95 | ||
|
|
eb781ce689 | ||
|
|
555a65c8ea | ||
|
|
58429f4172 | ||
|
|
e2692a0abd | ||
|
|
d218c43edf | ||
|
|
fae9813127 | ||
|
|
adc56d4ee5 | ||
|
|
f982e33fab | ||
|
|
9ab4479fcf | ||
|
|
3e330b7929 | ||
|
|
ecddada150 | ||
|
|
0c828cbb37 | ||
|
|
84a32f8c16 | ||
|
|
4ee36fd405 | ||
|
|
0802609fd8 | ||
|
|
de88176995 | ||
|
|
aca82f400a | ||
|
|
54bb6fe527 | ||
|
|
c1f663cec1 | ||
|
|
aaf59c2d01 | ||
|
|
692942e5be | ||
|
|
f4acd629a1 | ||
|
|
0ff8a44ec3 | ||
|
|
c6d8f6e618 | ||
|
|
1baa23cc43 | ||
|
|
3be696e6bb | ||
|
|
3cfcb01603 | ||
|
|
d39a29759c | ||
|
|
bf4332b9e7 | ||
|
|
feaae7b684 | ||
|
|
be7852dfc0 | ||
|
|
b0804ce048 | ||
|
|
aa7d5fa924 | ||
|
|
b4cab12e9d | ||
|
|
133dda2de0 | ||
|
|
ef5605ae39 | ||
|
|
9b29b2702e | ||
|
|
abafa270e5 | ||
|
|
15852ccf81 | ||
|
|
a427ecd029 | ||
|
|
77228dcb2c | ||
|
|
3948cfe495 | ||
|
|
36218b54f8 | ||
|
|
be2f4433db | ||
|
|
dd5d36cf07 | ||
|
|
685136d98f | ||
|
|
852937421e | ||
|
|
a5389b7be5 | ||
|
|
1b96b18bc0 | ||
|
|
e9edad6f8c | ||
|
|
f8a6012c3a | ||
|
|
e103f6c9b6 | ||
|
|
9daf7b5a36 | ||
|
|
d6142e1af0 | ||
|
|
b0491210ff | ||
|
|
91d8550290 | ||
|
|
dfaaa7d72a | ||
|
|
5174a4f58a | ||
|
|
b9c73df488 | ||
|
|
f99f1f6441 | ||
|
|
1ec1f5d3be | ||
|
|
713db06a05 | ||
|
|
a0adcbd90b | ||
|
|
b94776421c | ||
|
|
79c1043063 | ||
|
|
cfabae17c0 | ||
|
|
71a1d25e90 | ||
|
|
c2d7b835fb | ||
|
|
f72888ce6f | ||
|
|
30f474d253 | ||
|
|
216f274a92 | ||
|
|
5ea79efe4b | ||
|
|
197c1ba21b | ||
|
|
95c4a24d99 | ||
|
|
f5f23a7c22 | ||
|
|
c469576460 | ||
|
|
0001bb0e4c | ||
|
|
56f184393f | ||
|
|
85777e85fa | ||
|
|
c750ec3cc7 | ||
|
|
c75b781d3f | ||
|
|
8aa42ff1d9 | ||
|
|
63a5bc5e12 | ||
|
|
e0b7b6dc3d | ||
|
|
3d7ecf98c2 | ||
|
|
ad5581c620 | ||
|
|
69af991772 | ||
|
|
2bb2745f91 | ||
|
|
5f682bb45f | ||
|
|
48bcb6b9a6 | ||
|
|
092c0a7392 | ||
|
|
4e8618c512 | ||
|
|
c51a69c0dc | ||
|
|
2480849f39 | ||
|
|
6bfe40607d | ||
|
|
a1c350a537 | ||
|
|
f732b1978e | ||
|
|
d3a542bac4 | ||
|
|
b12724db02 | ||
|
|
f8f5e8e869 | ||
|
|
0733f06103 | ||
|
|
6fdaa6ee8a | ||
|
|
f10b547a49 | ||
|
|
38396b2691 | ||
|
|
220f50ebe4 | ||
|
|
c989133d27 | ||
|
|
6c339551eb | ||
|
|
ef23a80b16 | ||
|
|
a521093731 | ||
|
|
747d3eedf1 | ||
|
|
fd563726f0 | ||
|
|
62136a1a96 | ||
|
|
cf16c31d38 | ||
|
|
0f5d4aa72f | ||
|
|
d8dbe77427 | ||
|
|
0e5670d50d | ||
|
|
22924a1e92 | ||
|
|
e3a7b54f90 | ||
|
|
81d5e7f014 | ||
|
|
9790c218dc | ||
|
|
6d3d51553b | ||
|
|
474591c6a5 | ||
|
|
5de0dc2713 | ||
|
|
57a34a7abb | ||
|
|
54517bda1b | ||
|
|
4291e15b60 | ||
|
|
940ae6406b | ||
|
|
6a0cf4bfd3 | ||
|
|
fee6f75223 | ||
|
|
8c93b2d54c | ||
|
|
dbbb1bd8a1 | ||
|
|
5c838b2fc5 | ||
|
|
3a094d4ba8 | ||
|
|
5d11c1ea6a | ||
|
|
a7e2e15eac | ||
|
|
585a0ca1f4 | ||
|
|
09ed82db65 | ||
|
|
a56cb0e88a | ||
|
|
9acf5ff0ef | ||
|
|
64a7790aed | ||
|
|
9a5134ffcf | ||
|
|
1d2c4d0843 | ||
|
|
efad34be62 | ||
|
|
52761ade87 | ||
|
|
e6b816e776 | ||
|
|
6d3bcba5d0 | ||
|
|
2e7ffa606b | ||
|
|
055cae90b8 | ||
|
|
23acf92803 | ||
|
|
91ee7d76eb | ||
|
|
570888e2b8 | ||
|
|
282f27f0c8 | ||
|
|
98376ade45 | ||
|
|
9682968b55 | ||
|
|
877cb5f1b0 | ||
|
|
6972573a6e | ||
|
|
742f1b1283 | ||
|
|
0b127a2f3b | ||
|
|
a6057835a8 | ||
|
|
c32794477e | ||
|
|
999ac60bda | ||
|
|
bca4b832a8 | ||
|
|
2fc1dda232 | ||
|
|
99495cff90 | ||
|
|
795962a4ba | ||
|
|
7bde9f447a | ||
|
|
b45c3e3c7f | ||
|
|
ce2a01fc59 | ||
|
|
edeea65c3e | ||
|
|
6e8ee94b91 | ||
|
|
97d8274911 | ||
|
|
73d84dc13f | ||
|
|
f32501d498 | ||
|
|
9c289c3fe9 | ||
|
|
2a610584ee | ||
|
|
8e06421567 | ||
|
|
57dc3725f5 | ||
|
|
d6ef8945f5 | ||
|
|
e078a8b7b6 | ||
|
|
a6395b9025 | ||
|
|
6c0983d77c | ||
|
|
4ae39f2bd5 | ||
|
|
71fc701809 | ||
|
|
d418921e4b | ||
|
|
4b83b022d3 | ||
|
|
8b13e5b47e | ||
|
|
0bfd565bda | ||
|
|
2224a558ee | ||
|
|
93c02fb43a | ||
|
|
93ee51cabb | ||
|
|
cd6ba554a4 | ||
|
|
89aa2be358 | ||
|
|
da261246b9 | ||
|
|
4d3e09f4a0 | ||
|
|
9add6b3696 | ||
|
|
ef037e18d0 | ||
|
|
36db791516 | ||
|
|
25d2ea5e94 | ||
|
|
34f2d18f27 | ||
|
|
4935c846aa | ||
|
|
b1f980c216 | ||
|
|
738f64c903 | ||
|
|
7235663447 | ||
|
|
9d78c416f7 | ||
|
|
9a64c026ea | ||
|
|
be2af1d17a | ||
|
|
b21131a466 | ||
|
|
41c5126472 | ||
|
|
b96cb1abaf | ||
|
|
20143606e2 | ||
|
|
1d79bcc657 | ||
|
|
e8b4494fc3 | ||
|
|
2518208c5c | ||
|
|
918fea7d4b | ||
|
|
4dc07bf63f | ||
|
|
04a0aca221 | ||
|
|
1942e72474 | ||
|
|
f256407789 | ||
|
|
67612f607e | ||
|
|
7274c42126 | ||
|
|
e288402e88 | ||
|
|
76bd4a1c7b | ||
|
|
13dbad6f35 | ||
|
|
cf089fdfe9 | ||
|
|
9755e31fa2 | ||
|
|
3fcf31d009 | ||
|
|
1cbdd0e675 | ||
|
|
e0fd84d6f5 | ||
|
|
443720c3de | ||
|
|
254526a213 | ||
|
|
ad0afb5fc9 | ||
|
|
e64b15d2a7 | ||
|
|
cdc3352318 | ||
|
|
2416618b34 | ||
|
|
c951d77a52 | ||
|
|
759ddc0b4b | ||
|
|
a0709bec51 | ||
|
|
f5bc4e7a24 | ||
|
|
b4665b6e5a | ||
|
|
b85d5b8822 | ||
|
|
12ad75a6b0 | ||
|
|
af92fe6ad1 | ||
|
|
fcf7ed4faa | ||
|
|
a58649fd10 | ||
|
|
ee5918cf4e | ||
|
|
d7de3301ed | ||
|
|
bd1988c775 | ||
|
|
474010b31d | ||
|
|
dd941154d6 | ||
|
|
cbb53745f6 | ||
|
|
3dff62e0cf |
@@ -12,8 +12,11 @@ contains(QT_ARCH, i386) {
|
||||
contains(QT_ARCH, x86_64) {
|
||||
LIBS += -L$$PWD/../libs/windows/lib/x64
|
||||
}
|
||||
contains(QT_ARCH, arm64) {
|
||||
LIBS += -L$$PWD/../libs/windows/lib/arm64
|
||||
}
|
||||
|
||||
LIBS += -lNktHookLib
|
||||
LIBS += -ldetours
|
||||
DEFINES += ANTIHOOKING_LIBRARY
|
||||
SOURCES += antihookingprotection.cpp
|
||||
HEADERS += antihookingprotection.h
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
#include "antihookingprotection.h"
|
||||
|
||||
#include <NktHookLib.h>
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <Windows.h>
|
||||
|
||||
#include <detours.h>
|
||||
|
||||
typedef HMODULE (WINAPI *LoadLibraryAFunc)(LPCSTR lpLibFileName);
|
||||
typedef HMODULE (WINAPI *LoadLibraryWFunc)(LPCWSTR lpLibFileName);
|
||||
@@ -12,25 +15,35 @@ class AntiHookingProtection
|
||||
public:
|
||||
static void enable()
|
||||
{
|
||||
#ifdef QT_DEBUG
|
||||
s_HookManager.SetEnableDebugOutput(true);
|
||||
#endif
|
||||
DetourTransactionBegin();
|
||||
DetourUpdateThread(GetCurrentThread());
|
||||
|
||||
HINSTANCE kernel32Handle = NktHookLibHelpers::GetModuleBaseAddress(L"kernel32.dll");
|
||||
SIZE_T hookId;
|
||||
s_RealLoadLibraryA = LoadLibraryA;
|
||||
DetourAttach(&(PVOID&)s_RealLoadLibraryA, LoadLibraryAHook);
|
||||
|
||||
s_HookManager.Hook(&hookId, (LPVOID*)&s_RealLoadLibraryA,
|
||||
NktHookLibHelpers::GetProcedureAddress(kernel32Handle, "LoadLibraryA"),
|
||||
(LPVOID)AntiHookingProtection::LoadLibraryAHook);
|
||||
s_HookManager.Hook(&hookId, (LPVOID*)&s_RealLoadLibraryW,
|
||||
NktHookLibHelpers::GetProcedureAddress(kernel32Handle, "LoadLibraryW"),
|
||||
(LPVOID)AntiHookingProtection::LoadLibraryWHook);
|
||||
s_HookManager.Hook(&hookId, (LPVOID*)&s_RealLoadLibraryExA,
|
||||
NktHookLibHelpers::GetProcedureAddress(kernel32Handle, "LoadLibraryExA"),
|
||||
(LPVOID)AntiHookingProtection::LoadLibraryExAHook);
|
||||
s_HookManager.Hook(&hookId, (LPVOID*)&s_RealLoadLibraryExW,
|
||||
NktHookLibHelpers::GetProcedureAddress(kernel32Handle, "LoadLibraryExW"),
|
||||
(LPVOID)AntiHookingProtection::LoadLibraryExWHook);
|
||||
s_RealLoadLibraryW = LoadLibraryW;
|
||||
DetourAttach(&(PVOID&)s_RealLoadLibraryW, LoadLibraryWHook);
|
||||
|
||||
s_RealLoadLibraryExA = LoadLibraryExA;
|
||||
DetourAttach(&(PVOID&)s_RealLoadLibraryExA, LoadLibraryExAHook);
|
||||
|
||||
s_RealLoadLibraryExW = LoadLibraryExW;
|
||||
DetourAttach(&(PVOID&)s_RealLoadLibraryExW, LoadLibraryExWHook);
|
||||
|
||||
DetourTransactionCommit();
|
||||
}
|
||||
|
||||
static void disable()
|
||||
{
|
||||
DetourTransactionBegin();
|
||||
DetourUpdateThread(GetCurrentThread());
|
||||
|
||||
DetourDetach(&(PVOID&)s_RealLoadLibraryA, LoadLibraryAHook);
|
||||
DetourDetach(&(PVOID&)s_RealLoadLibraryW, LoadLibraryWHook);
|
||||
DetourDetach(&(PVOID&)s_RealLoadLibraryExA, LoadLibraryExAHook);
|
||||
DetourDetach(&(PVOID&)s_RealLoadLibraryExW, LoadLibraryExWHook);
|
||||
|
||||
DetourTransactionCommit();
|
||||
}
|
||||
|
||||
private:
|
||||
@@ -124,7 +137,6 @@ private:
|
||||
return s_RealLoadLibraryExW(lpLibFileName, hFile, dwFlags);
|
||||
}
|
||||
|
||||
static CNktHookLib s_HookManager;
|
||||
static LoadLibraryAFunc s_RealLoadLibraryA;
|
||||
static LoadLibraryWFunc s_RealLoadLibraryW;
|
||||
static LoadLibraryExAFunc s_RealLoadLibraryExA;
|
||||
@@ -209,7 +221,6 @@ private:
|
||||
};
|
||||
};
|
||||
|
||||
CNktHookLib AntiHookingProtection::s_HookManager;
|
||||
LoadLibraryAFunc AntiHookingProtection::s_RealLoadLibraryA;
|
||||
LoadLibraryWFunc AntiHookingProtection::s_RealLoadLibraryW;
|
||||
LoadLibraryExAFunc AntiHookingProtection::s_RealLoadLibraryExA;
|
||||
@@ -218,14 +229,26 @@ LoadLibraryExWFunc AntiHookingProtection::s_RealLoadLibraryExW;
|
||||
AH_EXPORT void AntiHookingDummyImport() {}
|
||||
|
||||
extern "C"
|
||||
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID)
|
||||
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
|
||||
{
|
||||
if (DetourIsHelperProcess()) {
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
switch (fdwReason)
|
||||
{
|
||||
case DLL_PROCESS_ATTACH:
|
||||
DetourRestoreAfterWith();
|
||||
AntiHookingProtection::enable();
|
||||
DisableThreadLibraryCalls(hinstDLL);
|
||||
break;
|
||||
case DLL_PROCESS_DETACH:
|
||||
// Ignore DLL_PROCESS_DETACH on process exit. No need to waste time
|
||||
// unhooking everything if the whole process is being destroyed.
|
||||
if (lpvReserved == NULL) {
|
||||
AntiHookingProtection::disable();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Moonlight PC
|
||||
|
||||
[Moonlight PC](https://moonlight-stream.org) is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield, but built to run on Windows, Mac, and Linux. This client is the successor to [Moonlight Chrome](https://github.com/moonlight-stream/moonlight-chrome) for streaming on PC.
|
||||
[Moonlight PC](https://moonlight-stream.org) is an open source PC client for NVIDIA GameStream and [Sunshine](https://github.com/LizardByte/Sunshine).
|
||||
|
||||
Moonlight also has mobile versions for [Android](https://github.com/moonlight-stream/moonlight-android) and [iOS](https://github.com/moonlight-stream/moonlight-ios).
|
||||
|
||||
@@ -12,14 +12,13 @@ You can follow development on our [Discord server](https://moonlight-stream.org/
|
||||
|
||||
## Features
|
||||
- Hardware accelerated video decoding on Windows, Mac, and Linux
|
||||
- Supports streaming at up to 120 FPS (high refresh rate monitor recommended)
|
||||
- Supports streaming at 720p, 1080p, 1440p, 4K, and the client PC's native screen resolution
|
||||
- H.264, HEVC, and AV1 codec support (AV1 requires Sunshine and a supported host GPU)
|
||||
- HDR streaming support
|
||||
- 7.1 surround sound audio support
|
||||
- 10-point multitouch support (Sunshine only)
|
||||
- Gamepad support with force feedback and motion controls for up to 16 players
|
||||
- Support for both pointer capture (for games) and direct mouse control (for remote desktop)
|
||||
- Support for passing system-wide keyboard shortcuts like Alt+Tab to the host
|
||||
- Direct touchscreen mouse control
|
||||
- HEVC support for better image quality at reduced bandwidth
|
||||
- Gamepad support with force feedback for up to 4 players
|
||||
|
||||
## Downloads
|
||||
- [Windows, macOS, and Steam Link](https://github.com/moonlight-stream/moonlight-qt/releases)
|
||||
@@ -40,24 +39,23 @@ Hosting for Moonlight's Raspberry Pi and L4T package repositories is graciously
|
||||
### Windows Build Requirements
|
||||
* Qt 5.15 SDK or later
|
||||
* Windows 7 or later
|
||||
* [Visual Studio 2019](https://visualstudio.microsoft.com/downloads/) (Community edition is fine)
|
||||
* Select **MSVC 2019** option during Qt installation. MinGW is not supported.
|
||||
* [Visual Studio 2022](https://visualstudio.microsoft.com/downloads/) (Community edition is fine)
|
||||
* Select **MSVC** option during Qt installation. MinGW is not supported.
|
||||
* [7-Zip](https://www.7-zip.org/) (only if building installers for non-development PCs)
|
||||
* [WiX Toolset](https://wixtoolset.org/releases/v3-14-0-5722/) v3.14.0.5722 or later (only if building installers for non-development PCs)
|
||||
|
||||
### macOS Build Requirements
|
||||
* Qt 6.2 SDK or later
|
||||
* macOS Mojave (10.14) or later
|
||||
* Xcode 13
|
||||
* Xcode 13 or later
|
||||
* [create-dmg](https://github.com/sindresorhus/create-dmg) (only if building DMGs for use on non-development Macs)
|
||||
|
||||
### Linux/Unix Build Requirements
|
||||
* Qt 5.9 SDK or later
|
||||
* GCC or Clang
|
||||
* Install the required packages:
|
||||
* Debian/Ubuntu: `libegl1-mesa-dev libgl1-mesa-dev libopus-dev libqt5svg5-dev libsdl2-dev libsdl2-ttf-dev libssl-dev libavcodec-dev libva-dev libvdpau-dev libxkbcommon-dev qt5-default qt5-qmake qtbase5-dev qtdeclarative5-dev qtquickcontrols2-5-dev wayland-protocols qml-module-qtquick-controls2 qml-module-qtquick-layouts qml-module-qtquick-window2 qml-module-qtquick2`
|
||||
* Debian/Ubuntu: `libegl1-mesa-dev libgl1-mesa-dev libopus-dev libqt5svg5-dev libsdl2-dev libsdl2-ttf-dev libssl-dev libavcodec-dev libva-dev libvdpau-dev libxkbcommon-dev qtwayland5 qt5-qmake qtbase5-dev qtdeclarative5-dev qtquickcontrols2-5-dev wayland-protocols qml-module-qtquick-controls2 qml-module-qtquick-layouts qml-module-qtquick-window2 qml-module-qtquick2`
|
||||
* RedHat/Fedora: `openssl-devel SDL2-devel SDL2_ttf-devel ffmpeg-devel qt5-qtsvg-devel qt5-qtquickcontrols2-devel libva-devel libvdpau-devel opus-devel pulseaudio-libs-devel alsa-lib-devel`
|
||||
* FFmpeg 4.0+ is required to build. If your distro doesn't package FFmpeg 4.0 or later, you can build and install it from source on https://ffmpeg.org/
|
||||
* FFmpeg 4.0 or later is required to build. If your distro doesn't package FFmpeg 4.0 or later, you can build and install it from source on https://ffmpeg.org/
|
||||
|
||||
### Steam Link Build Requirements
|
||||
* [Steam Link SDK](https://github.com/ValveSoftware/steamlink-sdk) cloned on your build system
|
||||
@@ -71,12 +69,13 @@ Hosting for Moonlight's Raspberry Pi and L4T package repositories is graciously
|
||||
2. Run `git submodule update --init --recursive` from within `moonlight-qt/`
|
||||
3. Open the project in Qt Creator or build from qmake on the command line.
|
||||
* To build a binary for use on non-development machines, use the scripts in the `scripts` folder.
|
||||
* For Windows builds, use `scripts\build-arch.bat` and `scripts\generate-bundle.bat`. Execute these scripts from the root of the repository within a Qt command prompt. Ensure WiX and 7-Zip binary directories are in your `%PATH%`.
|
||||
* For Windows builds, use `scripts\build-arch.bat` and `scripts\generate-bundle.bat`. Execute these scripts from the root of the repository within a Qt command prompt. Ensure 7-Zip binary directory is on your `%PATH%`.
|
||||
* For macOS builds, use `scripts/generate-dmg.sh`. Execute this script from the root of the repository and ensure Qt's `bin` folder is in your `$PATH`.
|
||||
* For Steam Link builds, run `scripts/build-steamlink-app.sh` from the root of the repository.
|
||||
* To build from the command line for development use, run `qmake moonlight-qt.pro` then `make debug` or `make release`
|
||||
* To build from the command line for development use on macOS or Linux, run `qmake moonlight-qt.pro` then `make debug` or `make release`
|
||||
* To create an embedded build for a single-purpose device, use `qmake "CONFIG+=embedded" moonlight-qt.pro` and build normally.
|
||||
* This build will lack windowed mode, Discord/Help links, and other features that don't make sense on an embedded device.
|
||||
* For platforms with poor GL performance, add `"CONFIG+=glslow"` to prefer direct KMSDRM rendering over EGL/GLES renderers. Direct KMSDRM rendering can use dedicated YUV/RGB conversion and scaling hardware rather than slower GPU shaders for these operations.
|
||||
|
||||
## Contribute
|
||||
1. Fork us
|
||||
|
||||
+17
-1
@@ -5,7 +5,7 @@
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>Moonlight</string>
|
||||
<key>CFBundleGetInfoString</key>
|
||||
<string>Stream games from your NVIDIA GameStream-enabled PC</string>
|
||||
<string>Stream games and other applications from another PC</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>moonlight</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
@@ -22,6 +22,22 @@
|
||||
<string>NSApplication</string>
|
||||
<key>NSSupportsAutomaticGraphicsSwitching</key>
|
||||
<true/>
|
||||
<key>GCSupportedGameControllers</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>ProfileName</key>
|
||||
<string>ExtendedGamepad</string>
|
||||
</dict>
|
||||
</array>
|
||||
<key>GCSupportsControllerUserInteraction</key>
|
||||
<true/>
|
||||
<key>LSApplicationCategoryType</key>
|
||||
<string>public.app-category.games</string>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>VERSION</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
|
||||
+1
-1
Submodule app/SDL_GameControllerDB updated: 2924d8a562...dbcf31a670
+130
-55
@@ -48,74 +48,104 @@ win32 {
|
||||
|
||||
INCLUDEPATH += $$PWD/../libs/windows/include
|
||||
LIBS += ws2_32.lib winmm.lib dxva2.lib ole32.lib gdi32.lib user32.lib d3d9.lib dwmapi.lib dbghelp.lib
|
||||
|
||||
# Work around a conflict with math.h inclusion between SDL and Qt 6
|
||||
DEFINES += _USE_MATH_DEFINES
|
||||
}
|
||||
macx {
|
||||
INCLUDEPATH += $$PWD/../libs/mac/include
|
||||
INCLUDEPATH += $$PWD/../libs/mac/Frameworks/SDL2.framework/Versions/A/Headers
|
||||
INCLUDEPATH += $$PWD/../libs/mac/Frameworks/SDL2_ttf.framework/Versions/A/Headers
|
||||
LIBS += -L$$PWD/../libs/mac/lib -F$$PWD/../libs/mac/Frameworks
|
||||
|
||||
# QMake doesn't handle framework-style includes correctly on its own
|
||||
QMAKE_CFLAGS += -F$$PWD/../libs/mac/Frameworks
|
||||
QMAKE_CXXFLAGS += -F$$PWD/../libs/mac/Frameworks
|
||||
QMAKE_OBJECTIVE_CFLAGS += -F$$PWD/../libs/mac/Frameworks
|
||||
}
|
||||
|
||||
unix:!macx {
|
||||
CONFIG += link_pkgconfig
|
||||
PKGCONFIG += openssl sdl2 SDL2_ttf opus
|
||||
|
||||
packagesExist(libavcodec) {
|
||||
PKGCONFIG += libavcodec libavutil
|
||||
CONFIG += ffmpeg
|
||||
!disable-ffmpeg {
|
||||
packagesExist(libavcodec) {
|
||||
PKGCONFIG += libavcodec libavutil
|
||||
CONFIG += ffmpeg
|
||||
|
||||
packagesExist(libva) {
|
||||
packagesExist(libva-x11) {
|
||||
CONFIG += libva-x11
|
||||
!disable-libva {
|
||||
packagesExist(libva) {
|
||||
!disable-x11 {
|
||||
packagesExist(libva-x11) {
|
||||
CONFIG += libva-x11
|
||||
}
|
||||
}
|
||||
!disable-wayland {
|
||||
packagesExist(libva-wayland) {
|
||||
CONFIG += libva-wayland
|
||||
}
|
||||
}
|
||||
!disable-libdrm {
|
||||
packagesExist(libva-drm) {
|
||||
CONFIG += libva-drm
|
||||
}
|
||||
}
|
||||
CONFIG += libva
|
||||
}
|
||||
}
|
||||
packagesExist(libva-wayland) {
|
||||
CONFIG += libva-wayland
|
||||
|
||||
!disable-libvdpau {
|
||||
packagesExist(vdpau) {
|
||||
CONFIG += libvdpau
|
||||
}
|
||||
}
|
||||
packagesExist(libva-drm) {
|
||||
CONFIG += libva-drm
|
||||
|
||||
!disable-mmal {
|
||||
packagesExist(mmal) {
|
||||
PKGCONFIG += mmal
|
||||
CONFIG += mmal
|
||||
}
|
||||
}
|
||||
|
||||
!disable-libdrm {
|
||||
packagesExist(libdrm) {
|
||||
PKGCONFIG += libdrm
|
||||
CONFIG += libdrm
|
||||
}
|
||||
}
|
||||
|
||||
!disable-cuda {
|
||||
packagesExist(ffnvcodec) {
|
||||
PKGCONFIG += ffnvcodec
|
||||
CONFIG += cuda
|
||||
}
|
||||
}
|
||||
CONFIG += libva
|
||||
}
|
||||
|
||||
packagesExist(vdpau) {
|
||||
CONFIG += libvdpau
|
||||
!disable-wayland {
|
||||
packagesExist(wayland-client) {
|
||||
CONFIG += wayland
|
||||
PKGCONFIG += wayland-client
|
||||
}
|
||||
}
|
||||
|
||||
packagesExist(mmal) {
|
||||
PKGCONFIG += mmal
|
||||
CONFIG += mmal
|
||||
!disable-x11 {
|
||||
packagesExist(x11) {
|
||||
DEFINES += HAS_X11
|
||||
PKGCONFIG += x11
|
||||
}
|
||||
}
|
||||
|
||||
packagesExist(libdrm) {
|
||||
PKGCONFIG += libdrm
|
||||
CONFIG += libdrm
|
||||
}
|
||||
|
||||
packagesExist(ffnvcodec) {
|
||||
PKGCONFIG += ffnvcodec
|
||||
CONFIG += cuda
|
||||
}
|
||||
}
|
||||
|
||||
packagesExist(wayland-client) {
|
||||
DEFINES += HAS_WAYLAND
|
||||
PKGCONFIG += wayland-client
|
||||
}
|
||||
|
||||
packagesExist(x11) {
|
||||
DEFINES += HAS_X11
|
||||
PKGCONFIG += x11
|
||||
}
|
||||
}
|
||||
win32 {
|
||||
LIBS += -llibssl -llibcrypto -lSDL2 -lSDL2_ttf -lavcodec -lavutil -lopus
|
||||
LIBS += -llibssl -llibcrypto -lSDL2 -lSDL2_ttf -lavcodec -lavutil -lopus -ldxgi -ld3d11
|
||||
CONFIG += ffmpeg
|
||||
}
|
||||
win32:!winrt {
|
||||
CONFIG += soundio discord-rpc
|
||||
}
|
||||
macx {
|
||||
LIBS += -lssl -lcrypto -lavcodec.58 -lavutil.56 -lopus -framework SDL2 -framework SDL2_ttf
|
||||
LIBS += -lssl -lcrypto -lavcodec.60 -lavutil.58 -lopus -framework SDL2 -framework SDL2_ttf
|
||||
LIBS += -lobjc -framework VideoToolbox -framework AVFoundation -framework CoreVideo -framework CoreGraphics -framework CoreMedia -framework AppKit -framework Metal
|
||||
|
||||
# For libsoundio
|
||||
@@ -127,6 +157,7 @@ macx {
|
||||
SOURCES += \
|
||||
backend/nvaddress.cpp \
|
||||
backend/nvapp.cpp \
|
||||
cli/pair.cpp \
|
||||
main.cpp \
|
||||
backend/computerseeker.cpp \
|
||||
backend/identitymanager.cpp \
|
||||
@@ -137,6 +168,7 @@ SOURCES += \
|
||||
backend/boxartmanager.cpp \
|
||||
backend/richpresencemanager.cpp \
|
||||
cli/commandlineparser.cpp \
|
||||
cli/listapps.cpp \
|
||||
cli/quitstream.cpp \
|
||||
cli/startstream.cpp \
|
||||
settings/compatfetcher.cpp \
|
||||
@@ -165,6 +197,7 @@ SOURCES += \
|
||||
HEADERS += \
|
||||
backend/nvaddress.h \
|
||||
backend/nvapp.h \
|
||||
cli/pair.h \
|
||||
settings/compatfetcher.h \
|
||||
settings/mappingfetcher.h \
|
||||
utils.h \
|
||||
@@ -177,6 +210,7 @@ HEADERS += \
|
||||
backend/boxartmanager.h \
|
||||
backend/richpresencemanager.h \
|
||||
cli/commandlineparser.h \
|
||||
cli/listapps.h \
|
||||
cli/quitstream.h \
|
||||
cli/startstream.h \
|
||||
settings/streamingpreferences.h \
|
||||
@@ -203,15 +237,15 @@ ffmpeg {
|
||||
SOURCES += \
|
||||
streaming/video/ffmpeg.cpp \
|
||||
streaming/video/ffmpeg-renderers/sdlvid.cpp \
|
||||
streaming/video/ffmpeg-renderers/pacer/pacer.cpp \
|
||||
streaming/video/ffmpeg-renderers/pacer/nullthreadedvsyncsource.cpp
|
||||
streaming/video/ffmpeg-renderers/swframemapper.cpp \
|
||||
streaming/video/ffmpeg-renderers/pacer/pacer.cpp
|
||||
|
||||
HEADERS += \
|
||||
streaming/video/ffmpeg.h \
|
||||
streaming/video/ffmpeg-renderers/renderer.h \
|
||||
streaming/video/ffmpeg-renderers/sdlvid.h \
|
||||
streaming/video/ffmpeg-renderers/pacer/pacer.h \
|
||||
streaming/video/ffmpeg-renderers/pacer/nullthreadedvsyncsource.h
|
||||
streaming/video/ffmpeg-renderers/swframemapper.h \
|
||||
streaming/video/ffmpeg-renderers/pacer/pacer.h
|
||||
}
|
||||
libva {
|
||||
message(VAAPI renderer selected)
|
||||
@@ -233,7 +267,7 @@ libva-wayland {
|
||||
PKGCONFIG += libva-wayland
|
||||
DEFINES += HAVE_LIBVA_WAYLAND
|
||||
}
|
||||
libva-wayland {
|
||||
libva-drm {
|
||||
message(VAAPI DRM support enabled)
|
||||
|
||||
PKGCONFIG += libva-drm
|
||||
@@ -252,6 +286,18 @@ mmal {
|
||||
DEFINES += HAVE_MMAL
|
||||
SOURCES += streaming/video/ffmpeg-renderers/mmal.cpp
|
||||
HEADERS += streaming/video/ffmpeg-renderers/mmal.h
|
||||
|
||||
# We suppress EGL usage when MMAL is available because MMAL has
|
||||
# significantly better performance than EGL on the Pi. Setting
|
||||
# this option allows EGL usage even if built with MMAL support.
|
||||
#
|
||||
# It is highly recommended to also build with 'glslow' to avoid
|
||||
# EGL being preferred if direct DRM rendering is available.
|
||||
allow-egl-with-mmal {
|
||||
message(Allowing EGL usage with MMAL enabled)
|
||||
|
||||
DEFINES += ALLOW_EGL_WITH_MMAL
|
||||
}
|
||||
}
|
||||
libdrm {
|
||||
message(DRM renderer selected)
|
||||
@@ -262,7 +308,7 @@ libdrm {
|
||||
|
||||
linux {
|
||||
message(Master hooks enabled)
|
||||
SOURCES += masterhook.c
|
||||
SOURCES += masterhook.c masterhook_internal.c
|
||||
LIBS += -ldl
|
||||
}
|
||||
}
|
||||
@@ -272,6 +318,9 @@ cuda {
|
||||
DEFINES += HAVE_CUDA
|
||||
SOURCES += streaming/video/ffmpeg-renderers/cuda.cpp
|
||||
HEADERS += streaming/video/ffmpeg-renderers/cuda.h
|
||||
|
||||
# ffnvcodec uses libdl in cuda_load_functions()/cuda_free_functions()
|
||||
LIBS += -ldl
|
||||
}
|
||||
config_EGL {
|
||||
message(EGL renderer selected)
|
||||
@@ -280,8 +329,11 @@ config_EGL {
|
||||
DEFINES += HAVE_EGL
|
||||
SOURCES += \
|
||||
streaming/video/ffmpeg-renderers/eglvid.cpp \
|
||||
streaming/video/ffmpeg-renderers/egl_extensions.cpp
|
||||
HEADERS += streaming/video/ffmpeg-renderers/eglvid.h
|
||||
streaming/video/ffmpeg-renderers/egl_extensions.cpp \
|
||||
streaming/video/ffmpeg-renderers/eglimagefactory.cpp
|
||||
HEADERS += \
|
||||
streaming/video/ffmpeg-renderers/eglvid.h \
|
||||
streaming/video/ffmpeg-renderers/eglimagefactory.h
|
||||
}
|
||||
config_SL {
|
||||
message(Steam Link build configuration selected)
|
||||
@@ -296,15 +348,20 @@ config_SL {
|
||||
streaming/video/slvid.h \
|
||||
streaming/audio/renderers/slaud.h
|
||||
}
|
||||
win32 {
|
||||
HEADERS += streaming/video/ffmpeg-renderers/dxutil.h
|
||||
}
|
||||
win32:!winrt {
|
||||
message(DXVA2 renderer selected)
|
||||
message(DXVA2 and D3D11VA renderers selected)
|
||||
|
||||
SOURCES += \
|
||||
streaming/video/ffmpeg-renderers/dxva2.cpp \
|
||||
streaming/video/ffmpeg-renderers/d3d11va.cpp \
|
||||
streaming/video/ffmpeg-renderers/pacer/dxvsyncsource.cpp
|
||||
|
||||
HEADERS += \
|
||||
streaming/video/ffmpeg-renderers/dxva2.h \
|
||||
streaming/video/ffmpeg-renderers/d3d11va.h \
|
||||
streaming/video/ffmpeg-renderers/pacer/dxvsyncsource.h
|
||||
}
|
||||
macx {
|
||||
@@ -334,13 +391,25 @@ embedded {
|
||||
|
||||
DEFINES += EMBEDDED_BUILD
|
||||
}
|
||||
glslow {
|
||||
message(GL slow build)
|
||||
|
||||
DEFINES += GL_IS_SLOW
|
||||
}
|
||||
wayland {
|
||||
message(Wayland extensions enabled)
|
||||
|
||||
DEFINES += HAS_WAYLAND
|
||||
SOURCES += streaming/video/ffmpeg-renderers/pacer/waylandvsyncsource.cpp
|
||||
HEADERS += streaming/video/ffmpeg-renderers/pacer/waylandvsyncsource.h
|
||||
}
|
||||
|
||||
RESOURCES += \
|
||||
resources.qrc \
|
||||
qml.qrc
|
||||
|
||||
TRANSLATIONS += \
|
||||
languages/qml_zh_cn.ts \
|
||||
languages/qml_zh_CN.ts \
|
||||
languages/qml_de.ts \
|
||||
languages/qml_fr.ts \
|
||||
languages/qml_nb_NO.ts \
|
||||
@@ -355,7 +424,15 @@ TRANSLATIONS += \
|
||||
languages/qml_sv.ts \
|
||||
languages/qml_tr.ts \
|
||||
languages/qml_uk.ts \
|
||||
languages/qml_zh_Hant.ts
|
||||
languages/qml_zh_TW.ts \
|
||||
languages/qml_el.ts \
|
||||
languages/qml_hi.ts \
|
||||
languages/qml_it.ts \
|
||||
languages/qml_pt.ts \
|
||||
languages/qml_pt_BR.ts \
|
||||
languages/qml_pl.ts \
|
||||
languages/qml_cs.ts \
|
||||
languages/qml_he.ts
|
||||
|
||||
# Additional import path used to resolve QML modules in Qt Creator's code model
|
||||
QML_IMPORT_PATH =
|
||||
@@ -394,13 +471,11 @@ INCLUDEPATH += $$PWD/../h264bitstream/h264bitstream
|
||||
DEPENDPATH += $$PWD/../h264bitstream/h264bitstream
|
||||
|
||||
!winrt {
|
||||
contains(QT_ARCH, i386)|contains(QT_ARCH, x86_64) {
|
||||
win32:CONFIG(release, debug|release): LIBS += -L$$OUT_PWD/../AntiHooking/release/ -lAntiHooking
|
||||
else:win32:CONFIG(debug, debug|release): LIBS += -L$$OUT_PWD/../AntiHooking/debug/ -lAntiHooking
|
||||
win32:CONFIG(release, debug|release): LIBS += -L$$OUT_PWD/../AntiHooking/release/ -lAntiHooking
|
||||
else:win32:CONFIG(debug, debug|release): LIBS += -L$$OUT_PWD/../AntiHooking/debug/ -lAntiHooking
|
||||
|
||||
INCLUDEPATH += $$PWD/../AntiHooking
|
||||
DEPENDPATH += $$PWD/../AntiHooking
|
||||
}
|
||||
INCLUDEPATH += $$PWD/../AntiHooking
|
||||
DEPENDPATH += $$PWD/../AntiHooking
|
||||
}
|
||||
|
||||
unix:!macx: {
|
||||
|
||||
@@ -6,16 +6,17 @@
|
||||
#include <QJsonObject>
|
||||
|
||||
AutoUpdateChecker::AutoUpdateChecker(QObject *parent) :
|
||||
QObject(parent),
|
||||
m_Nam(this)
|
||||
QObject(parent)
|
||||
{
|
||||
m_Nam = new QNetworkAccessManager(this);
|
||||
|
||||
// Never communicate over HTTP
|
||||
m_Nam.setStrictTransportSecurityEnabled(true);
|
||||
m_Nam->setStrictTransportSecurityEnabled(true);
|
||||
|
||||
// Allow HTTP redirects
|
||||
m_Nam.setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy);
|
||||
m_Nam->setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy);
|
||||
|
||||
connect(&m_Nam, &QNetworkAccessManager::finished,
|
||||
connect(m_Nam, &QNetworkAccessManager::finished,
|
||||
this, &AutoUpdateChecker::handleUpdateCheckRequestFinished);
|
||||
|
||||
QString currentVersion(VERSION_STR);
|
||||
@@ -28,12 +29,17 @@ AutoUpdateChecker::AutoUpdateChecker(QObject *parent) :
|
||||
|
||||
void AutoUpdateChecker::start()
|
||||
{
|
||||
if (!m_Nam) {
|
||||
Q_ASSERT(m_Nam);
|
||||
return;
|
||||
}
|
||||
|
||||
#if defined(Q_OS_WIN32) || defined(Q_OS_DARWIN) || defined(STEAM_LINK) || defined(APP_IMAGE) // Only run update checker on platforms without auto-update
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) && QT_VERSION < QT_VERSION_CHECK(5, 15, 1) && !defined(QT_NO_BEARERMANAGEMENT)
|
||||
// HACK: Set network accessibility to work around QTBUG-80947 (introduced in Qt 5.14.0 and fixed in Qt 5.15.1)
|
||||
QT_WARNING_PUSH
|
||||
QT_WARNING_DISABLE_DEPRECATED
|
||||
m_Nam.setNetworkAccessible(QNetworkAccessManager::Accessible);
|
||||
m_Nam->setNetworkAccessible(QNetworkAccessManager::Accessible);
|
||||
QT_WARNING_POP
|
||||
#endif
|
||||
|
||||
@@ -45,7 +51,7 @@ void AutoUpdateChecker::start()
|
||||
#else
|
||||
request.setAttribute(QNetworkRequest::HTTP2AllowedAttribute, true);
|
||||
#endif
|
||||
m_Nam.get(request);
|
||||
m_Nam->get(request);
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -102,6 +108,11 @@ void AutoUpdateChecker::handleUpdateCheckRequestFinished(QNetworkReply* reply)
|
||||
{
|
||||
Q_ASSERT(reply->isFinished());
|
||||
|
||||
// Delete the QNetworkAccessManager to free resources and
|
||||
// prevent the bearer plugin from polling in the background.
|
||||
m_Nam->deleteLater();
|
||||
m_Nam = nullptr;
|
||||
|
||||
if (reply->error() == QNetworkReply::NoError) {
|
||||
QTextStream stream(reply);
|
||||
|
||||
@@ -201,7 +212,7 @@ void AutoUpdateChecker::handleUpdateCheckRequestFinished(QNetworkReply* reply)
|
||||
<< QSysInfo::buildCpuArchitecture() << getPlatform() << QSysInfo::kernelVersion();
|
||||
}
|
||||
else {
|
||||
qWarning() << "Update checking failed with error: " << reply->error();
|
||||
qWarning() << "Update checking failed with error:" << reply->error();
|
||||
reply->deleteLater();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,5 +25,5 @@ private:
|
||||
QString getPlatform();
|
||||
|
||||
QVector<int> m_CurrentVersionQuad;
|
||||
QNetworkAccessManager m_Nam;
|
||||
QNetworkAccessManager* m_Nam;
|
||||
};
|
||||
|
||||
+199
-41
@@ -10,7 +10,10 @@
|
||||
#include <QThreadPool>
|
||||
#include <QCoreApplication>
|
||||
|
||||
#include <random>
|
||||
|
||||
#define SER_HOSTS "hosts"
|
||||
#define SER_HOSTS_BACKUP "hostsbackup"
|
||||
|
||||
class PcMonitorThread : public QThread
|
||||
{
|
||||
@@ -42,7 +45,7 @@ private:
|
||||
|
||||
// Ensure the machine that responded is the one we intended to contact
|
||||
if (m_Computer->uuid != newState.uuid) {
|
||||
qInfo() << "Found unexpected PC " << newState.name << " looking for " << m_Computer->name;
|
||||
qInfo() << "Found unexpected PC" << newState.name << "looking for" << m_Computer->name;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -145,25 +148,39 @@ ComputerManager::ComputerManager(QObject *parent)
|
||||
: QObject(parent),
|
||||
m_PollingRef(0),
|
||||
m_MdnsBrowser(nullptr),
|
||||
m_CompatFetcher(nullptr)
|
||||
m_CompatFetcher(nullptr),
|
||||
m_NeedsDelayedFlush(false)
|
||||
{
|
||||
QSettings settings;
|
||||
|
||||
// If there's a hosts backup copy, we must have failed to commit
|
||||
// a previous update before exiting. Restore the backup now.
|
||||
int hosts = settings.beginReadArray(SER_HOSTS_BACKUP);
|
||||
if (hosts == 0) {
|
||||
// If there's no host backup, read from the primary location.
|
||||
settings.endArray();
|
||||
hosts = settings.beginReadArray(SER_HOSTS);
|
||||
}
|
||||
|
||||
// Inflate our hosts from QSettings
|
||||
int hosts = settings.beginReadArray(SER_HOSTS);
|
||||
for (int i = 0; i < hosts; i++) {
|
||||
settings.setArrayIndex(i);
|
||||
NvComputer* computer = new NvComputer(settings);
|
||||
m_KnownHosts[computer->uuid] = computer;
|
||||
m_LastSerializedHosts[computer->uuid] = *computer;
|
||||
}
|
||||
settings.endArray();
|
||||
|
||||
// Fetch latest compatibility data asynchronously
|
||||
m_CompatFetcher.start();
|
||||
|
||||
// Start the delayed flush thread to handle saveHosts() calls
|
||||
m_DelayedFlushThread = new DelayedFlushThread(this);
|
||||
m_DelayedFlushThread->start();
|
||||
|
||||
// To quit in a timely manner, we must block additional requests
|
||||
// after we receive the aboutToQuit() signal. This is neccessary
|
||||
// because NvHTTP uses aboutToQuit() to abort requests in progres
|
||||
// after we receive the aboutToQuit() signal. This is necessary
|
||||
// because NvHTTP uses aboutToQuit() to abort requests in progress
|
||||
// while quitting, however this is a one time signal - additional
|
||||
// requests would not be aborted and block termination.
|
||||
connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this, &ComputerManager::handleAboutToQuit);
|
||||
@@ -171,6 +188,21 @@ ComputerManager::ComputerManager(QObject *parent)
|
||||
|
||||
ComputerManager::~ComputerManager()
|
||||
{
|
||||
// Stop the delayed flush thread before acquiring the lock in write mode
|
||||
// to avoid deadlocking with a flush that needs the lock in read mode.
|
||||
{
|
||||
// Wake the delayed flush thread
|
||||
m_DelayedFlushThread->requestInterruption();
|
||||
m_DelayedFlushCondition.wakeOne();
|
||||
|
||||
// Wait for it to terminate (and finish any pending flush)
|
||||
m_DelayedFlushThread->wait();
|
||||
delete m_DelayedFlushThread;
|
||||
|
||||
// Delayed flushes should have completed by now
|
||||
Q_ASSERT(!m_NeedsDelayedFlush);
|
||||
}
|
||||
|
||||
QWriteLocker lock(&m_Lock);
|
||||
|
||||
// Delete machines that haven't been resolved yet
|
||||
@@ -200,19 +232,80 @@ ComputerManager::~ComputerManager()
|
||||
}
|
||||
}
|
||||
|
||||
void DelayedFlushThread::run() {
|
||||
for (;;) {
|
||||
// Wait for a delayed flush request or an interruption
|
||||
{
|
||||
QMutexLocker locker(&m_ComputerManager->m_DelayedFlushMutex);
|
||||
|
||||
while (!QThread::currentThread()->isInterruptionRequested() && !m_ComputerManager->m_NeedsDelayedFlush) {
|
||||
m_ComputerManager->m_DelayedFlushCondition.wait(&m_ComputerManager->m_DelayedFlushMutex);
|
||||
}
|
||||
|
||||
// Bail without flushing if we woke up for an interruption alone.
|
||||
// If we have both an interruption and a flush request, do the flush.
|
||||
if (!m_ComputerManager->m_NeedsDelayedFlush) {
|
||||
Q_ASSERT(QThread::currentThread()->isInterruptionRequested());
|
||||
break;
|
||||
}
|
||||
|
||||
// Reset the delayed flush flag to ensure any racing saveHosts() call will set it again
|
||||
m_ComputerManager->m_NeedsDelayedFlush = false;
|
||||
|
||||
// Update the last serialized hosts map under the delayed flush mutex
|
||||
m_ComputerManager->m_LastSerializedHosts.clear();
|
||||
for (const NvComputer* computer : m_ComputerManager->m_KnownHosts) {
|
||||
// Copy the current state of the NvComputer to allow us to check later if we need
|
||||
// to serialize it again when attribute updates occur.
|
||||
QReadLocker computerLock(&computer->lock);
|
||||
m_ComputerManager->m_LastSerializedHosts[computer->uuid] = *computer;
|
||||
}
|
||||
}
|
||||
|
||||
// Perform the flush
|
||||
{
|
||||
QSettings settings;
|
||||
|
||||
// First, write to the backup location
|
||||
settings.beginWriteArray(SER_HOSTS_BACKUP);
|
||||
{
|
||||
QReadLocker lock(&m_ComputerManager->m_Lock);
|
||||
int i = 0;
|
||||
for (const NvComputer* computer : m_ComputerManager->m_KnownHosts) {
|
||||
settings.setArrayIndex(i++);
|
||||
computer->serialize(settings, false);
|
||||
}
|
||||
}
|
||||
settings.endArray();
|
||||
|
||||
// Next, write to the primary location
|
||||
settings.remove(SER_HOSTS);
|
||||
settings.beginWriteArray(SER_HOSTS);
|
||||
{
|
||||
QReadLocker lock(&m_ComputerManager->m_Lock);
|
||||
int i = 0;
|
||||
for (const NvComputer* computer : m_ComputerManager->m_KnownHosts) {
|
||||
settings.setArrayIndex(i++);
|
||||
computer->serialize(settings, true);
|
||||
}
|
||||
}
|
||||
settings.endArray();
|
||||
|
||||
// Finally, delete the backup copy
|
||||
settings.remove(SER_HOSTS_BACKUP);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ComputerManager::saveHosts()
|
||||
{
|
||||
QSettings settings;
|
||||
QReadLocker lock(&m_Lock);
|
||||
Q_ASSERT(m_DelayedFlushThread != nullptr && m_DelayedFlushThread->isRunning());
|
||||
|
||||
settings.remove(SER_HOSTS);
|
||||
settings.beginWriteArray(SER_HOSTS);
|
||||
int i = 0;
|
||||
for (const NvComputer* computer : m_KnownHosts) {
|
||||
settings.setArrayIndex(i++);
|
||||
computer->serialize(settings);
|
||||
}
|
||||
settings.endArray();
|
||||
// Punt to a worker thread because QSettings on macOS can take ages (> 500 ms)
|
||||
// to persist our host list to disk (especially when a host has a bunch of apps).
|
||||
QMutexLocker locker(&m_DelayedFlushMutex);
|
||||
m_NeedsDelayedFlush = true;
|
||||
m_DelayedFlushCondition.wakeOne();
|
||||
}
|
||||
|
||||
QHostAddress ComputerManager::getBestGlobalAddressV6(QVector<QHostAddress> &addresses)
|
||||
@@ -263,12 +356,13 @@ void ComputerManager::startPolling()
|
||||
|
||||
if (prefs.enableMdns) {
|
||||
// Start an MDNS query for GameStream hosts
|
||||
m_MdnsBrowser = new QMdnsEngine::Browser(&m_MdnsServer, "_nvstream._tcp.local.", &m_MdnsCache);
|
||||
m_MdnsServer.reset(new QMdnsEngine::Server());
|
||||
m_MdnsBrowser = new QMdnsEngine::Browser(m_MdnsServer.data(), "_nvstream._tcp.local.");
|
||||
connect(m_MdnsBrowser, &QMdnsEngine::Browser::serviceAdded,
|
||||
this, [this](const QMdnsEngine::Service& service) {
|
||||
qInfo() << "Discovered mDNS host:" << service.hostname();
|
||||
|
||||
MdnsPendingComputer* pendingComputer = new MdnsPendingComputer(&m_MdnsServer, service);
|
||||
MdnsPendingComputer* pendingComputer = new MdnsPendingComputer(m_MdnsServer, service);
|
||||
connect(pendingComputer, &MdnsPendingComputer::resolvedHost,
|
||||
this, &ComputerManager::handleMdnsServiceResolved);
|
||||
m_PendingResolution.append(pendingComputer);
|
||||
@@ -349,6 +443,19 @@ void ComputerManager::handleMdnsServiceResolved(MdnsPendingComputer* computer,
|
||||
computer->deleteLater();
|
||||
}
|
||||
|
||||
void ComputerManager::saveHost(NvComputer *computer)
|
||||
{
|
||||
// If no serializable properties changed, don't bother saving hosts
|
||||
QMutexLocker lock(&m_DelayedFlushMutex);
|
||||
QReadLocker computerLock(&computer->lock);
|
||||
if (!m_LastSerializedHosts.value(computer->uuid).isEqualSerialized(*computer)) {
|
||||
// Queue a request for a delayed flush to QSettings outside of the lock
|
||||
computerLock.unlock();
|
||||
lock.unlock();
|
||||
saveHosts();
|
||||
}
|
||||
}
|
||||
|
||||
void ComputerManager::handleComputerStateChanged(NvComputer* computer)
|
||||
{
|
||||
emit computerStateChanged(computer);
|
||||
@@ -358,15 +465,20 @@ void ComputerManager::handleComputerStateChanged(NvComputer* computer)
|
||||
emit quitAppCompleted(QVariant());
|
||||
}
|
||||
|
||||
// Save updated hosts to QSettings
|
||||
saveHosts();
|
||||
// Save updates to this host
|
||||
saveHost(computer);
|
||||
}
|
||||
|
||||
QVector<NvComputer*> ComputerManager::getComputers()
|
||||
{
|
||||
QReadLocker lock(&m_Lock);
|
||||
|
||||
return QVector<NvComputer*>::fromList(m_KnownHosts.values());
|
||||
// Return a sorted host list
|
||||
auto hosts = QVector<NvComputer*>::fromList(m_KnownHosts.values());
|
||||
std::stable_sort(hosts.begin(), hosts.end(), [](const NvComputer* host1, const NvComputer* host2) {
|
||||
return host1->name.toLower() < host2->name.toLower();
|
||||
});
|
||||
return hosts;
|
||||
}
|
||||
|
||||
class DeferredHostDeletionTask : public QRunnable
|
||||
@@ -390,7 +502,7 @@ public:
|
||||
m_ComputerManager->m_KnownHosts.remove(m_Computer->uuid);
|
||||
}
|
||||
|
||||
// Persist the new host list
|
||||
// Persist the new host list with this computer deleted
|
||||
m_ComputerManager->saveHosts();
|
||||
|
||||
// Delete the polling entry first. This will stop all polling threads too.
|
||||
@@ -431,16 +543,13 @@ void ComputerManager::renameHost(NvComputer* computer, QString name)
|
||||
|
||||
void ComputerManager::clientSideAttributeUpdated(NvComputer* computer)
|
||||
{
|
||||
// Persist the change
|
||||
saveHosts();
|
||||
|
||||
// Notify the UI of the state change
|
||||
handleComputerStateChanged(computer);
|
||||
}
|
||||
|
||||
void ComputerManager::handleAboutToQuit()
|
||||
{
|
||||
QWriteLocker lock(&m_Lock);
|
||||
QReadLocker lock(&m_Lock);
|
||||
|
||||
// Interrupt polling threads immediately, so they
|
||||
// avoid making additional requests while quitting
|
||||
@@ -455,7 +564,8 @@ class PendingPairingTask : public QObject, public QRunnable
|
||||
|
||||
public:
|
||||
PendingPairingTask(ComputerManager* computerManager, NvComputer* computer, QString pin)
|
||||
: m_Computer(computer),
|
||||
: m_ComputerManager(computerManager),
|
||||
m_Computer(computer),
|
||||
m_Pin(pin)
|
||||
{
|
||||
connect(this, &PendingPairingTask::pairingCompleted,
|
||||
@@ -475,25 +585,34 @@ private:
|
||||
switch (result)
|
||||
{
|
||||
case NvPairingManager::PairState::PIN_WRONG:
|
||||
emit pairingCompleted(m_Computer, "The PIN from the PC didn't match. Please try again.");
|
||||
emit pairingCompleted(m_Computer, tr("The PIN from the PC didn't match. Please try again."));
|
||||
break;
|
||||
case NvPairingManager::PairState::FAILED:
|
||||
emit pairingCompleted(m_Computer, "Pairing failed. Please try again.");
|
||||
if (m_Computer->currentGameId != 0) {
|
||||
emit pairingCompleted(m_Computer, tr("You cannot pair while a previous session is still running on the host PC. Quit any running games or reboot the host PC, then try pairing again."));
|
||||
}
|
||||
else {
|
||||
emit pairingCompleted(m_Computer, tr("Pairing failed. Please try again."));
|
||||
}
|
||||
break;
|
||||
case NvPairingManager::PairState::ALREADY_IN_PROGRESS:
|
||||
emit pairingCompleted(m_Computer, "Another pairing attempt is already in progress.");
|
||||
emit pairingCompleted(m_Computer, tr("Another pairing attempt is already in progress."));
|
||||
break;
|
||||
case NvPairingManager::PairState::PAIRED:
|
||||
// Persist the newly pinned server certificate for this host
|
||||
m_ComputerManager->saveHost(m_Computer);
|
||||
|
||||
emit pairingCompleted(m_Computer, nullptr);
|
||||
break;
|
||||
}
|
||||
} catch (const GfeHttpResponseException& e) {
|
||||
emit pairingCompleted(m_Computer, "GeForce Experience returned error: " + e.toQString());
|
||||
emit pairingCompleted(m_Computer, tr("GeForce Experience returned error: %1").arg(e.toQString()));
|
||||
} catch (const QtNetworkReplyException& e) {
|
||||
emit pairingCompleted(m_Computer, e.toQString());
|
||||
}
|
||||
}
|
||||
|
||||
ComputerManager* m_ComputerManager;
|
||||
NvComputer* m_Computer;
|
||||
QString m_Pin;
|
||||
};
|
||||
@@ -580,9 +699,10 @@ void ComputerManager::stopPollingAsync()
|
||||
m_PendingResolution.removeFirst();
|
||||
}
|
||||
|
||||
// Delete the browser to stop discovery
|
||||
// Delete the browser and server to stop discovery and refresh polling
|
||||
delete m_MdnsBrowser;
|
||||
m_MdnsBrowser = nullptr;
|
||||
m_MdnsServer.reset();
|
||||
|
||||
// Interrupt all threads, but don't wait for them to terminate
|
||||
for (ComputerPollingEntry* entry : m_PollEntries) {
|
||||
@@ -592,8 +712,8 @@ void ComputerManager::stopPollingAsync()
|
||||
|
||||
void ComputerManager::addNewHostManually(QString address)
|
||||
{
|
||||
QUrl url = QUrl::fromUserInput(address);
|
||||
if (url.isValid() && !url.host().isEmpty()) {
|
||||
QUrl url = QUrl::fromUserInput("moonlight://" + address);
|
||||
if (url.isValid() && !url.host().isEmpty() && url.scheme() == "moonlight") {
|
||||
// If there wasn't a port specified, use the default
|
||||
addNewHost(NvAddress(url.host(), url.port(DEFAULT_HTTP_PORT)), false);
|
||||
}
|
||||
@@ -611,12 +731,15 @@ public:
|
||||
: m_ComputerManager(computerManager),
|
||||
m_Address(address),
|
||||
m_MdnsIpv6Address(mdnsIpv6Address),
|
||||
m_Mdns(mdns)
|
||||
m_Mdns(mdns),
|
||||
m_AboutToQuit(false)
|
||||
{
|
||||
connect(this, &PendingAddTask::computerAddCompleted,
|
||||
computerManager, &ComputerManager::computerAddCompleted);
|
||||
connect(this, &PendingAddTask::computerStateChanged,
|
||||
computerManager, &ComputerManager::handleComputerStateChanged);
|
||||
connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit,
|
||||
this, &PendingAddTask::handleAboutToQuit);
|
||||
}
|
||||
|
||||
signals:
|
||||
@@ -625,9 +748,20 @@ signals:
|
||||
void computerStateChanged(NvComputer* computer);
|
||||
|
||||
private:
|
||||
void handleAboutToQuit()
|
||||
{
|
||||
m_AboutToQuit = true;
|
||||
}
|
||||
|
||||
QString fetchServerInfo(NvHTTP& http)
|
||||
{
|
||||
QString serverInfo;
|
||||
|
||||
// Do nothing if we're quitting
|
||||
if (m_AboutToQuit) {
|
||||
return QString();
|
||||
}
|
||||
|
||||
try {
|
||||
// There's a race condition between GameStream servers reporting presence over
|
||||
// mDNS and the HTTPS server being ready to respond to our queries. To work
|
||||
@@ -749,16 +883,31 @@ private:
|
||||
hostAddress.isInSubnet(QHostAddress("192.168.0.0"), 16);
|
||||
|
||||
{
|
||||
// Check if this PC already exists
|
||||
QWriteLocker lock(&m_ComputerManager->m_Lock);
|
||||
// Check if this PC already exists using opportunistic read lock
|
||||
m_ComputerManager->m_Lock.lockForRead();
|
||||
NvComputer* existingComputer = m_ComputerManager->m_KnownHosts.value(newComputer->uuid);
|
||||
|
||||
// If it doesn't already exist, convert to a write lock in preparation for updating.
|
||||
//
|
||||
// NB: ComputerManager's lock protects the host map itself, not the elements inside.
|
||||
// Those are protected by their individual locks. Since we only mutate the map itself
|
||||
// when the PC doesn't exist, we need the lock in write-mode for that case only.
|
||||
if (existingComputer == nullptr) {
|
||||
m_ComputerManager->m_Lock.unlock();
|
||||
m_ComputerManager->m_Lock.lockForWrite();
|
||||
|
||||
// Since we had to unlock to lock for write, someone could have raced and added
|
||||
// this PC before us. We have to check again whether it already exists.
|
||||
existingComputer = m_ComputerManager->m_KnownHosts.value(newComputer->uuid);
|
||||
}
|
||||
|
||||
if (existingComputer != nullptr) {
|
||||
// Fold it into the existing PC
|
||||
bool changed = existingComputer->update(*newComputer);
|
||||
delete newComputer;
|
||||
|
||||
// Drop the lock before notifying
|
||||
lock.unlock();
|
||||
m_ComputerManager->m_Lock.unlock();
|
||||
|
||||
// For non-mDNS clients, let them know it succeeded
|
||||
if (!m_Mdns) {
|
||||
@@ -779,17 +928,15 @@ private:
|
||||
m_ComputerManager->startPollingComputer(newComputer);
|
||||
|
||||
// Drop the lock before notifying
|
||||
lock.unlock();
|
||||
m_ComputerManager->m_Lock.unlock();
|
||||
|
||||
// If this wasn't added via mDNS but it is a RFC 1918 IPv4 address and not a VPN,
|
||||
// go ahead and do the STUN request now to populate an external address.
|
||||
if (!m_Mdns && addressIsSiteLocalV4 && !newComputer->isReachableOverVpn()) {
|
||||
if (!m_Mdns && addressIsSiteLocalV4 && newComputer->getActiveAddressReachability() != NvComputer::RI_VPN) {
|
||||
quint32 addr;
|
||||
int err = LiFindExternalAddressIP4("stun.moonlight-stream.org", 3478, &addr);
|
||||
if (err == 0) {
|
||||
lock.relock();
|
||||
newComputer->setRemoteAddress(QHostAddress(qFromBigEndian(addr)));
|
||||
lock.unlock();
|
||||
}
|
||||
else {
|
||||
qWarning() << "STUN failed to get WAN address:" << err;
|
||||
@@ -811,6 +958,7 @@ private:
|
||||
NvAddress m_Address;
|
||||
NvAddress m_MdnsIpv6Address;
|
||||
bool m_Mdns;
|
||||
bool m_AboutToQuit;
|
||||
};
|
||||
|
||||
void ComputerManager::addNewHost(NvAddress address, bool mdns, NvAddress mdnsIpv6Address)
|
||||
@@ -821,4 +969,14 @@ void ComputerManager::addNewHost(NvAddress address, bool mdns, NvAddress mdnsIpv
|
||||
QThreadPool::globalInstance()->start(addTask);
|
||||
}
|
||||
|
||||
// TODO: Use QRandomGenerator when we drop Qt 5.9 support
|
||||
QString ComputerManager::generatePinString()
|
||||
{
|
||||
std::uniform_int_distribution<int> dist(0, 9999);
|
||||
std::random_device rd;
|
||||
std::mt19937 engine(rd());
|
||||
|
||||
return QString::asprintf("%04u", dist(engine));
|
||||
}
|
||||
|
||||
#include "computermanager.moc"
|
||||
|
||||
@@ -15,17 +15,38 @@
|
||||
#include <QSettings>
|
||||
#include <QRunnable>
|
||||
#include <QTimer>
|
||||
#include <QMutex>
|
||||
#include <QWaitCondition>
|
||||
|
||||
class ComputerManager;
|
||||
|
||||
class DelayedFlushThread : public QThread
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
DelayedFlushThread(ComputerManager* cm)
|
||||
: m_ComputerManager(cm)
|
||||
{
|
||||
setObjectName("CM Delayed Flush Thread");
|
||||
}
|
||||
|
||||
void run();
|
||||
|
||||
private:
|
||||
ComputerManager* m_ComputerManager;
|
||||
};
|
||||
|
||||
class MdnsPendingComputer : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit MdnsPendingComputer(QMdnsEngine::Server* server,
|
||||
explicit MdnsPendingComputer(const QSharedPointer<QMdnsEngine::Server> server,
|
||||
const QMdnsEngine::Service& service)
|
||||
: m_Hostname(service.hostname()),
|
||||
m_Port(service.port()),
|
||||
m_Server(server),
|
||||
m_ServerWeak(server),
|
||||
m_Resolver(nullptr)
|
||||
{
|
||||
// Start resolving
|
||||
@@ -73,8 +94,21 @@ signals:
|
||||
private:
|
||||
void resolve()
|
||||
{
|
||||
// Delete our resolver, so we're guaranteed that nothing is referencing m_Server.
|
||||
delete m_Resolver;
|
||||
m_Resolver = new QMdnsEngine::Resolver(m_Server, m_Hostname);
|
||||
m_Resolver = nullptr;
|
||||
|
||||
// Now delete our strong reference that we held on behalf of m_Resolver.
|
||||
// The server may be destroyed after we make this call.
|
||||
m_Server.reset();
|
||||
|
||||
// Re-acquire a strong reference if the server still exists.
|
||||
m_Server = m_ServerWeak.toStrongRef();
|
||||
if (!m_Server) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_Resolver = new QMdnsEngine::Resolver(m_Server.data(), m_Hostname);
|
||||
connect(m_Resolver, &QMdnsEngine::Resolver::resolved,
|
||||
this, &MdnsPendingComputer::handleResolvedAddress);
|
||||
QTimer::singleShot(2000, this, &MdnsPendingComputer::handleResolvedTimeout);
|
||||
@@ -82,7 +116,8 @@ private:
|
||||
|
||||
QByteArray m_Hostname;
|
||||
uint16_t m_Port;
|
||||
QMdnsEngine::Server* m_Server;
|
||||
QWeakPointer<QMdnsEngine::Server> m_ServerWeak;
|
||||
QSharedPointer<QMdnsEngine::Server> m_Server;
|
||||
QMdnsEngine::Resolver* m_Resolver;
|
||||
QVector<QHostAddress> m_Addresses;
|
||||
};
|
||||
@@ -166,6 +201,8 @@ class ComputerManager : public QObject
|
||||
|
||||
friend class DeferredHostDeletionTask;
|
||||
friend class PendingAddTask;
|
||||
friend class PendingPairingTask;
|
||||
friend class DelayedFlushThread;
|
||||
|
||||
public:
|
||||
explicit ComputerManager(QObject *parent = nullptr);
|
||||
@@ -180,6 +217,8 @@ public:
|
||||
|
||||
void addNewHost(NvAddress address, bool mdns, NvAddress mdnsIpv6Address = NvAddress());
|
||||
|
||||
QString generatePinString();
|
||||
|
||||
void pairHost(NvComputer* computer, QString pin);
|
||||
|
||||
void quitRunningApp(NvComputer* computer);
|
||||
@@ -212,6 +251,8 @@ private slots:
|
||||
private:
|
||||
void saveHosts();
|
||||
|
||||
void saveHost(NvComputer* computer);
|
||||
|
||||
QHostAddress getBestGlobalAddressV6(QVector<QHostAddress>& addresses);
|
||||
|
||||
void startPollingComputer(NvComputer* computer);
|
||||
@@ -220,9 +261,13 @@ private:
|
||||
QReadWriteLock m_Lock;
|
||||
QMap<QString, NvComputer*> m_KnownHosts;
|
||||
QMap<QString, ComputerPollingEntry*> m_PollEntries;
|
||||
QMdnsEngine::Server m_MdnsServer;
|
||||
QHash<QString, NvComputer> m_LastSerializedHosts; // Protected by m_DelayedFlushMutex
|
||||
QSharedPointer<QMdnsEngine::Server> m_MdnsServer;
|
||||
QMdnsEngine::Browser* m_MdnsBrowser;
|
||||
QMdnsEngine::Cache m_MdnsCache;
|
||||
QVector<MdnsPendingComputer*> m_PendingResolution;
|
||||
CompatFetcher m_CompatFetcher;
|
||||
DelayedFlushThread* m_DelayedFlushThread;
|
||||
QMutex m_DelayedFlushMutex; // Lock ordering: Must never be acquired while holding NvComputer lock
|
||||
QWaitCondition m_DelayedFlushCondition;
|
||||
bool m_NeedsDelayedFlush;
|
||||
};
|
||||
|
||||
+146
-54
@@ -21,6 +21,7 @@
|
||||
#define SER_APPLIST "apps"
|
||||
#define SER_SRVCERT "srvcert"
|
||||
#define SER_CUSTOMNAME "customname"
|
||||
#define SER_NVIDIASOFTWARE "nvidiasw"
|
||||
|
||||
NvComputer::NvComputer(QSettings& settings)
|
||||
{
|
||||
@@ -37,6 +38,7 @@ NvComputer::NvComputer(QSettings& settings)
|
||||
this->manualAddress = NvAddress(settings.value(SER_MANUALADDR).toString(),
|
||||
settings.value(SER_MANUALPORT, QVariant(DEFAULT_HTTP_PORT)).toUInt());
|
||||
this->serverCert = QSslCertificate(settings.value(SER_SRVCERT).toByteArray());
|
||||
this->isNvidiaServerSoftware = settings.value(SER_NVIDIASOFTWARE).toBool();
|
||||
|
||||
int appCount = settings.beginReadArray(SER_APPLIST);
|
||||
this->appList.reserve(appCount);
|
||||
@@ -65,12 +67,14 @@ NvComputer::NvComputer(QSettings& settings)
|
||||
|
||||
void NvComputer::setRemoteAddress(QHostAddress address)
|
||||
{
|
||||
QWriteLocker lock(&this->lock);
|
||||
|
||||
Q_ASSERT(this->externalPort != 0);
|
||||
|
||||
this->remoteAddress = NvAddress(address, this->externalPort);
|
||||
}
|
||||
|
||||
void NvComputer::serialize(QSettings& settings) const
|
||||
void NvComputer::serialize(QSettings& settings, bool serializeApps) const
|
||||
{
|
||||
QReadLocker lock(&this->lock);
|
||||
|
||||
@@ -87,9 +91,10 @@ void NvComputer::serialize(QSettings& settings) const
|
||||
settings.setValue(SER_MANUALADDR, manualAddress.address());
|
||||
settings.setValue(SER_MANUALPORT, manualAddress.port());
|
||||
settings.setValue(SER_SRVCERT, serverCert.toPem());
|
||||
settings.setValue(SER_NVIDIASOFTWARE, isNvidiaServerSoftware);
|
||||
|
||||
// Avoid deleting an existing applist if we couldn't get one
|
||||
if (!appList.isEmpty()) {
|
||||
if (!appList.isEmpty() && serializeApps) {
|
||||
settings.remove(SER_APPLIST);
|
||||
settings.beginWriteArray(SER_APPLIST);
|
||||
for (int i = 0; i < appList.count(); i++) {
|
||||
@@ -100,6 +105,21 @@ void NvComputer::serialize(QSettings& settings) const
|
||||
}
|
||||
}
|
||||
|
||||
bool NvComputer::isEqualSerialized(const NvComputer &that) const
|
||||
{
|
||||
return this->name == that.name &&
|
||||
this->hasCustomName == that.hasCustomName &&
|
||||
this->uuid == that.uuid &&
|
||||
this->macAddress == that.macAddress &&
|
||||
this->localAddress == that.localAddress &&
|
||||
this->remoteAddress == that.remoteAddress &&
|
||||
this->ipv6Address == that.ipv6Address &&
|
||||
this->manualAddress == that.manualAddress &&
|
||||
this->serverCert == that.serverCert &&
|
||||
this->isNvidiaServerSoftware == that.isNvidiaServerSoftware &&
|
||||
this->appList == that.appList;
|
||||
}
|
||||
|
||||
void NvComputer::sortAppList()
|
||||
{
|
||||
std::stable_sort(appList.begin(), appList.end(), [](const NvApp& app1, const NvApp& app2) {
|
||||
@@ -131,7 +151,8 @@ NvComputer::NvComputer(NvHTTP& http, QString serverInfo)
|
||||
this->serverCodecModeSupport = codecSupport.toInt();
|
||||
}
|
||||
else {
|
||||
this->serverCodecModeSupport = 0;
|
||||
// Assume H.264 is always supported
|
||||
this->serverCodecModeSupport = SCM_H264;
|
||||
}
|
||||
|
||||
QString maxLumaPixelsHEVC = NvHTTP::getXmlString(serverInfo, "MaxLumaPixelsHEVC");
|
||||
@@ -145,8 +166,8 @@ NvComputer::NvComputer(NvHTTP& http, QString serverInfo)
|
||||
this->displayModes = NvHTTP::getDisplayModeList(serverInfo);
|
||||
std::stable_sort(this->displayModes.begin(), this->displayModes.end(),
|
||||
[](const NvDisplayMode& mode1, const NvDisplayMode& mode2) {
|
||||
return mode1.width * mode1.height * mode1.refreshRate <
|
||||
mode2.width * mode2.height * mode2.refreshRate;
|
||||
return (uint64_t)mode1.width * mode1.height * mode1.refreshRate <
|
||||
(uint64_t)mode2.width * mode2.height * mode2.refreshRate;
|
||||
});
|
||||
|
||||
// We can get an IPv4 loopback address if we're using the GS IPv6 Forwarder
|
||||
@@ -164,7 +185,7 @@ NvComputer::NvComputer(NvHTTP& http, QString serverInfo)
|
||||
// to support dynamic HTTP WAN ports without requiring the user to manually enter the port.
|
||||
QString remotePortStr = NvHTTP::getXmlString(serverInfo, "ExternalPort");
|
||||
if (remotePortStr.isEmpty() || (this->externalPort = remotePortStr.toUShort()) == 0) {
|
||||
this->externalPort = DEFAULT_HTTP_PORT;
|
||||
this->externalPort = http.httpPort();
|
||||
}
|
||||
|
||||
QString remoteAddress = NvHTTP::getXmlString(serverInfo, "ExternalIP");
|
||||
@@ -175,6 +196,11 @@ NvComputer::NvComputer(NvHTTP& http, QString serverInfo)
|
||||
this->remoteAddress = NvAddress();
|
||||
}
|
||||
|
||||
// Real Nvidia host software (GeForce Experience and RTX Experience) both use the 'Mjolnir'
|
||||
// codename in the state field and no version of Sunshine does. We can use this to bypass
|
||||
// some assumptions about Nvidia hardware that don't apply to Sunshine hosts.
|
||||
this->isNvidiaServerSoftware = NvHTTP::getXmlString(serverInfo, "state").contains("MJOLNIR");
|
||||
|
||||
this->pairState = NvHTTP::getXmlString(serverInfo, "PairStatus") == "1" ?
|
||||
PS_PAIRED : PS_NOT_PAIRED;
|
||||
this->currentGameId = NvHTTP::getCurrentGame(serverInfo);
|
||||
@@ -187,40 +213,52 @@ NvComputer::NvComputer(NvHTTP& http, QString serverInfo)
|
||||
this->isSupportedServerVersion = CompatFetcher::isGfeVersionSupported(this->gfeVersion);
|
||||
}
|
||||
|
||||
bool NvComputer::wake()
|
||||
bool NvComputer::wake() const
|
||||
{
|
||||
if (state == NvComputer::CS_ONLINE) {
|
||||
qWarning() << name << "is already online";
|
||||
return true;
|
||||
QByteArray wolPayload;
|
||||
|
||||
{
|
||||
QReadLocker readLocker(&lock);
|
||||
|
||||
if (state == NvComputer::CS_ONLINE) {
|
||||
qWarning() << name << "is already online";
|
||||
return true;
|
||||
}
|
||||
|
||||
if (macAddress.isEmpty()) {
|
||||
qWarning() << name << "has no MAC address stored";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create the WoL payload
|
||||
wolPayload.append(QByteArray::fromHex("FFFFFFFFFFFF"));
|
||||
for (int i = 0; i < 16; i++) {
|
||||
wolPayload.append(macAddress);
|
||||
}
|
||||
Q_ASSERT(wolPayload.size() == 102);
|
||||
}
|
||||
|
||||
if (macAddress.isEmpty()) {
|
||||
qWarning() << name << "has no MAC address stored";
|
||||
return false;
|
||||
}
|
||||
|
||||
const quint16 WOL_PORTS[] = {
|
||||
// Ports used as-is
|
||||
const quint16 STATIC_WOL_PORTS[] = {
|
||||
9, // Standard WOL port (privileged port)
|
||||
47998, 47999, 48000, 48002, 48010, // Ports opened by GFE
|
||||
47009, // Port opened by Moonlight Internet Hosting Tool for WoL (non-privileged port)
|
||||
};
|
||||
|
||||
// Create the WoL payload
|
||||
QByteArray wolPayload;
|
||||
wolPayload.append(QByteArray::fromHex("FFFFFFFFFFFF"));
|
||||
for (int i = 0; i < 16; i++) {
|
||||
wolPayload.append(macAddress);
|
||||
}
|
||||
Q_ASSERT(wolPayload.count() == 102);
|
||||
// Ports offset by the HTTP base port for hosts using alternate ports
|
||||
const quint16 DYNAMIC_WOL_PORTS[] = {
|
||||
47998, 47999, 48000, 48002, 48010, // Ports opened by GFE
|
||||
};
|
||||
|
||||
// Add the addresses that we know this host to be
|
||||
// and broadcast addresses for this link just in
|
||||
// case the host has timed out in ARP entries.
|
||||
QVector<QString> addressList;
|
||||
QMap<QString, quint16> addressMap;
|
||||
QSet<quint16> basePortSet;
|
||||
for (const NvAddress& addr : uniqueAddresses()) {
|
||||
addressList.append(addr.address());
|
||||
addressMap.insert(addr.address(), addr.port());
|
||||
basePortSet.insert(addr.port());
|
||||
}
|
||||
addressList.append("255.255.255.255");
|
||||
addressMap.insert("255.255.255.255", 0);
|
||||
|
||||
// Try to broadcast on all available NICs
|
||||
for (const QNetworkInterface& nic : QNetworkInterface::allInterfaces()) {
|
||||
@@ -239,31 +277,42 @@ bool NvComputer::wake()
|
||||
|
||||
// Skip IPv6 which doesn't support broadcast
|
||||
if (!addr.broadcast().isNull()) {
|
||||
addressList.append(addr.broadcast().toString());
|
||||
addressMap.insert(addr.broadcast().toString(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
if (!allNodesMulticast.scopeId().isEmpty()) {
|
||||
addressList.append(allNodesMulticast.toString());
|
||||
addressMap.insert(allNodesMulticast.toString(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Try all unique address strings or host names
|
||||
bool success = false;
|
||||
for (QString& addressString : addressList) {
|
||||
QHostInfo hostInfo = QHostInfo::fromName(addressString);
|
||||
for (auto i = addressMap.constBegin(); i != addressMap.constEnd(); i++) {
|
||||
QHostAddress literalAddress;
|
||||
QList<QHostAddress> addressList;
|
||||
|
||||
if (hostInfo.error() != QHostInfo::NoError) {
|
||||
qWarning() << "Error resolving" << addressString << ":" << hostInfo.errorString();
|
||||
continue;
|
||||
// If this is an IPv4/IPv6 literal, don't use QHostInfo::fromName() because that will
|
||||
// try to perform a reverse DNS lookup that leads to delays sending WoL packets.
|
||||
if (literalAddress.setAddress(i.key())) {
|
||||
addressList.append(literalAddress);
|
||||
}
|
||||
else {
|
||||
QHostInfo hostInfo = QHostInfo::fromName(i.key());
|
||||
if (hostInfo.error() != QHostInfo::NoError) {
|
||||
qWarning() << "Error resolving" << i.key() << ":" << hostInfo.errorString();
|
||||
continue;
|
||||
}
|
||||
|
||||
addressList.append(hostInfo.addresses());
|
||||
}
|
||||
|
||||
// Try all IP addresses that this string resolves to
|
||||
for (QHostAddress& address : hostInfo.addresses()) {
|
||||
for (QHostAddress& address : addressList) {
|
||||
QUdpSocket sock;
|
||||
|
||||
// Send to all ports
|
||||
for (quint16 port : WOL_PORTS) {
|
||||
// Send to all static ports
|
||||
for (quint16 port : STATIC_WOL_PORTS) {
|
||||
if (sock.writeDatagram(wolPayload, address, port)) {
|
||||
qInfo().nospace().noquote() << "Sent WoL packet to " << name << " via " << address.toString() << ":" << port;
|
||||
success = true;
|
||||
@@ -272,22 +321,56 @@ bool NvComputer::wake()
|
||||
qWarning() << "Send failed:" << sock.error();
|
||||
}
|
||||
}
|
||||
|
||||
QList<quint16> basePorts;
|
||||
if (i.value() != 0) {
|
||||
// If we have a known base port for this address, use only that port
|
||||
basePorts.append(i.value());
|
||||
}
|
||||
else {
|
||||
// If this is a broadcast address without a known HTTP port, try all of them
|
||||
basePorts.append(basePortSet.values());
|
||||
}
|
||||
|
||||
// Send to all dynamic ports using the HTTP port offset(s) for this address
|
||||
for (quint16 basePort : basePorts) {
|
||||
for (quint16 port : DYNAMIC_WOL_PORTS) {
|
||||
port = (port - 47989) + basePort;
|
||||
|
||||
if (sock.writeDatagram(wolPayload, address, port)) {
|
||||
qInfo().nospace().noquote() << "Sent WoL packet to " << name << " via " << address.toString() << ":" << port;
|
||||
success = true;
|
||||
}
|
||||
else {
|
||||
qWarning() << "Send failed:" << sock.error();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
bool NvComputer::isReachableOverVpn()
|
||||
NvComputer::ReachabilityType NvComputer::getActiveAddressReachability() const
|
||||
{
|
||||
if (activeAddress.isNull()) {
|
||||
return false;
|
||||
NvAddress copyOfActiveAddress;
|
||||
|
||||
{
|
||||
QReadLocker readLocker(&lock);
|
||||
|
||||
if (activeAddress.isNull()) {
|
||||
return ReachabilityType::RI_UNKNOWN;
|
||||
}
|
||||
|
||||
// Grab a copy of the active address to avoid having to hold
|
||||
// the computer lock while doing socket operations
|
||||
copyOfActiveAddress = activeAddress;
|
||||
}
|
||||
|
||||
QTcpSocket s;
|
||||
|
||||
s.setProxy(QNetworkProxy::NoProxy);
|
||||
s.connectToHost(activeAddress.address(), activeAddress.port());
|
||||
s.connectToHost(copyOfActiveAddress.address(), copyOfActiveAddress.port());
|
||||
if (s.waitForConnected(3000)) {
|
||||
Q_ASSERT(!s.localAddress().isNull());
|
||||
|
||||
@@ -308,48 +391,55 @@ bool NvComputer::isReachableOverVpn()
|
||||
if (nic.type() == QNetworkInterface::Virtual ||
|
||||
nic.type() == QNetworkInterface::Ppp) {
|
||||
// Treat PPP and virtual interfaces as likely VPNs
|
||||
return true;
|
||||
return ReachabilityType::RI_VPN;
|
||||
}
|
||||
|
||||
if (nic.maximumTransmissionUnit() != 0 && nic.maximumTransmissionUnit() < 1500) {
|
||||
// Treat MTUs under 1500 as likely VPNs
|
||||
return true;
|
||||
return ReachabilityType::RI_VPN;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (nic.flags() & QNetworkInterface::IsPointToPoint) {
|
||||
// Treat point-to-point links as likely VPNs.
|
||||
// This check detects OpenVPN on Unix-like OSes.
|
||||
return true;
|
||||
return ReachabilityType::RI_VPN;
|
||||
}
|
||||
|
||||
if (nic.hardwareAddress().startsWith("00:FF", Qt::CaseInsensitive)) {
|
||||
// OpenVPN TAP interfaces have a MAC address starting with 00:FF on Windows
|
||||
return true;
|
||||
return ReachabilityType::RI_VPN;
|
||||
}
|
||||
|
||||
if (nic.humanReadableName().startsWith("ZeroTier")) {
|
||||
// ZeroTier interfaces always start with "ZeroTier"
|
||||
return true;
|
||||
return ReachabilityType::RI_VPN;
|
||||
}
|
||||
|
||||
if (nic.humanReadableName().contains("VPN")) {
|
||||
// This one is just a final heuristic if all else fails
|
||||
return true;
|
||||
// This one is just a final VPN heuristic if all else fails
|
||||
return ReachabilityType::RI_VPN;
|
||||
}
|
||||
|
||||
// Didn't meet any of our VPN heuristics
|
||||
return false;
|
||||
// Didn't meet any of our VPN heuristics. Let's see if it's on-link.
|
||||
Q_ASSERT(addr.prefixLength() >= 0);
|
||||
if (addr.prefixLength() >= 0 && s.localAddress().isInSubnet(addr.ip(), addr.prefixLength())) {
|
||||
return ReachabilityType::RI_LAN;
|
||||
}
|
||||
|
||||
// Default to unknown if nothing else matched
|
||||
return ReachabilityType::RI_UNKNOWN;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
qWarning() << "No match found for address:" << s.localAddress();
|
||||
return false;
|
||||
return ReachabilityType::RI_UNKNOWN;
|
||||
}
|
||||
else {
|
||||
// If we fail to connect, just pretend that it's not a VPN
|
||||
return false;
|
||||
qWarning() << "Unable to check for reachability within 3 seconds";
|
||||
return ReachabilityType::RI_UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -375,6 +465,7 @@ bool NvComputer::updateAppList(QVector<NvApp> newAppList) {
|
||||
|
||||
QVector<NvAddress> NvComputer::uniqueAddresses() const
|
||||
{
|
||||
QReadLocker readLocker(&lock);
|
||||
QVector<NvAddress> uniqueAddressList;
|
||||
|
||||
// Start with addresses correctly ordered
|
||||
@@ -406,7 +497,7 @@ QVector<NvAddress> NvComputer::uniqueAddresses() const
|
||||
return uniqueAddressList;
|
||||
}
|
||||
|
||||
bool NvComputer::update(NvComputer& that)
|
||||
bool NvComputer::update(const NvComputer& that)
|
||||
{
|
||||
bool changed = false;
|
||||
|
||||
@@ -456,6 +547,7 @@ bool NvComputer::update(NvComputer& that)
|
||||
ASSIGN_IF_CHANGED(gfeVersion);
|
||||
ASSIGN_IF_CHANGED(appVersion);
|
||||
ASSIGN_IF_CHANGED(isSupportedServerVersion);
|
||||
ASSIGN_IF_CHANGED(isNvidiaServerSoftware);
|
||||
ASSIGN_IF_CHANGED(maxLumaPixelsHEVC);
|
||||
ASSIGN_IF_CHANGED(gpuModel);
|
||||
ASSIGN_IF_CHANGED_AND_NONNULL(serverCert);
|
||||
|
||||
@@ -8,6 +8,16 @@
|
||||
#include <QSettings>
|
||||
#include <QRunnable>
|
||||
|
||||
class CopySafeReadWriteLock : public QReadWriteLock
|
||||
{
|
||||
public:
|
||||
CopySafeReadWriteLock() = default;
|
||||
|
||||
// Don't actually copy the QReadWriteLock
|
||||
CopySafeReadWriteLock(const CopySafeReadWriteLock&) : QReadWriteLock() {}
|
||||
CopySafeReadWriteLock& operator=(const CopySafeReadWriteLock &) { return *this; }
|
||||
};
|
||||
|
||||
class NvComputer
|
||||
{
|
||||
friend class PcMonitorThread;
|
||||
@@ -22,6 +32,14 @@ private:
|
||||
bool pendingQuit;
|
||||
|
||||
public:
|
||||
NvComputer() = default;
|
||||
|
||||
// Caller is responsible for synchronizing read access to the other host
|
||||
NvComputer(const NvComputer&) = default;
|
||||
|
||||
// Caller is responsible for synchronizing read access to the other host
|
||||
NvComputer& operator=(const NvComputer &) = default;
|
||||
|
||||
explicit NvComputer(NvHTTP& http, QString serverInfo);
|
||||
|
||||
explicit NvComputer(QSettings& settings);
|
||||
@@ -30,19 +48,30 @@ public:
|
||||
setRemoteAddress(QHostAddress);
|
||||
|
||||
bool
|
||||
update(NvComputer& that);
|
||||
update(const NvComputer& that);
|
||||
|
||||
bool
|
||||
wake();
|
||||
wake() const;
|
||||
|
||||
bool
|
||||
isReachableOverVpn();
|
||||
enum ReachabilityType
|
||||
{
|
||||
RI_UNKNOWN,
|
||||
RI_LAN,
|
||||
RI_VPN,
|
||||
};
|
||||
|
||||
ReachabilityType
|
||||
getActiveAddressReachability() const;
|
||||
|
||||
QVector<NvAddress>
|
||||
uniqueAddresses() const;
|
||||
|
||||
void
|
||||
serialize(QSettings& settings) const;
|
||||
serialize(QSettings& settings, bool serializeApps) const;
|
||||
|
||||
// Caller is responsible for synchronizing read access to both hosts
|
||||
bool
|
||||
isEqualSerialized(const NvComputer& that) const;
|
||||
|
||||
enum PairState
|
||||
{
|
||||
@@ -83,9 +112,11 @@ public:
|
||||
QString uuid;
|
||||
QSslCertificate serverCert;
|
||||
QVector<NvApp> appList;
|
||||
bool isNvidiaServerSoftware;
|
||||
// Remember to update isEqualSerialized() when adding fields here!
|
||||
|
||||
// Synchronization
|
||||
mutable QReadWriteLock lock;
|
||||
mutable CopySafeReadWriteLock lock;
|
||||
|
||||
private:
|
||||
uint16_t externalPort;
|
||||
|
||||
+18
-38
@@ -187,12 +187,15 @@ NvHTTP::getServerInfo(NvLogLevel logLevel, bool fastFail)
|
||||
}
|
||||
|
||||
void
|
||||
NvHTTP::launchApp(int appId,
|
||||
PSTREAM_CONFIGURATION streamConfig,
|
||||
bool sops,
|
||||
bool localAudio,
|
||||
int gamepadMask,
|
||||
QString& rtspSessionUrl)
|
||||
NvHTTP::startApp(QString verb,
|
||||
bool isGfe,
|
||||
int appId,
|
||||
PSTREAM_CONFIGURATION streamConfig,
|
||||
bool sops,
|
||||
bool localAudio,
|
||||
int gamepadMask,
|
||||
bool persistGameControllersOnDisconnect,
|
||||
QString& rtspSessionUrl)
|
||||
{
|
||||
int riKeyId;
|
||||
|
||||
@@ -201,25 +204,26 @@ NvHTTP::launchApp(int appId,
|
||||
|
||||
QString response =
|
||||
openConnectionToString(m_BaseUrlHttps,
|
||||
"launch",
|
||||
verb,
|
||||
"appid="+QString::number(appId)+
|
||||
"&mode="+QString::number(streamConfig->width)+"x"+
|
||||
QString::number(streamConfig->height)+"x"+
|
||||
// Using an FPS value over 60 causes SOPS to default to 720p60,
|
||||
// so force it to 0 to ensure the correct resolution is set. We
|
||||
// used to use 60 here but that locked the frame rate to 60 FPS
|
||||
// on GFE 3.20.3.
|
||||
QString::number(streamConfig->fps > 60 ? 0 : streamConfig->fps)+
|
||||
// on GFE 3.20.3. We don't need this hack for Sunshine.
|
||||
QString::number((streamConfig->fps > 60 && isGfe) ? 0 : streamConfig->fps)+
|
||||
"&additionalStates=1&sops="+QString::number(sops ? 1 : 0)+
|
||||
"&rikey="+QByteArray(streamConfig->remoteInputAesKey, sizeof(streamConfig->remoteInputAesKey)).toHex()+
|
||||
"&rikeyid="+QString::number(riKeyId)+
|
||||
(streamConfig->enableHdr ?
|
||||
((streamConfig->supportedVideoFormats & VIDEO_FORMAT_MASK_10BIT) ?
|
||||
"&hdrMode=1&clientHdrCapVersion=0&clientHdrCapSupportedFlagsInUint32=0&clientHdrCapMetaDataId=NV_STATIC_METADATA_TYPE_1&clientHdrCapDisplayData=0x0x0x0x0x0x0x0x0x0x0" :
|
||||
"")+
|
||||
"&localAudioPlayMode="+QString::number(localAudio ? 1 : 0)+
|
||||
"&surroundAudioInfo="+QString::number(SURROUNDAUDIOINFO_FROM_AUDIO_CONFIGURATION(streamConfig->audioConfiguration))+
|
||||
"&remoteControllersBitmap="+QString::number(gamepadMask)+
|
||||
"&gcmap="+QString::number(gamepadMask),
|
||||
"&gcmap="+QString::number(gamepadMask)+
|
||||
"&gcpersist="+QString::number(persistGameControllersOnDisconnect ? 1 : 0),
|
||||
LAUNCH_TIMEOUT_MS);
|
||||
|
||||
qInfo() << "Launch response:" << response;
|
||||
@@ -230,30 +234,6 @@ NvHTTP::launchApp(int appId,
|
||||
rtspSessionUrl = getXmlString(response, "sessionUrl0");
|
||||
}
|
||||
|
||||
void
|
||||
NvHTTP::resumeApp(PSTREAM_CONFIGURATION streamConfig, QString& rtspSessionUrl)
|
||||
{
|
||||
int riKeyId;
|
||||
|
||||
memcpy(&riKeyId, streamConfig->remoteInputAesIv, sizeof(riKeyId));
|
||||
riKeyId = qFromBigEndian(riKeyId);
|
||||
|
||||
QString response =
|
||||
openConnectionToString(m_BaseUrlHttps,
|
||||
"resume",
|
||||
"rikey="+QString(QByteArray(streamConfig->remoteInputAesKey, sizeof(streamConfig->remoteInputAesKey)).toHex())+
|
||||
"&rikeyid="+QString::number(riKeyId)+
|
||||
"&surroundAudioInfo="+QString::number(SURROUNDAUDIOINFO_FROM_AUDIO_CONFIGURATION(streamConfig->audioConfiguration)),
|
||||
RESUME_TIMEOUT_MS);
|
||||
|
||||
qInfo() << "Resume response:" << response;
|
||||
|
||||
// Throws if the request failed
|
||||
verifyResponseStatus(response);
|
||||
|
||||
rtspSessionUrl = getXmlString(response, "sessionUrl0");
|
||||
}
|
||||
|
||||
void
|
||||
NvHTTP::quitApp()
|
||||
{
|
||||
@@ -269,7 +249,7 @@ NvHTTP::quitApp()
|
||||
verifyResponseStatus(response);
|
||||
|
||||
// Newer GFE versions will just return success even if quitting fails
|
||||
// if we're not the original requestor.
|
||||
// if we're not the original requester.
|
||||
if (getCurrentGame(getServerInfo(NvHTTP::NVLL_ERROR)) != 0) {
|
||||
// Generate a synthetic GfeResponseException letting the caller know
|
||||
// that they can't kill someone else's stream.
|
||||
@@ -382,7 +362,7 @@ NvHTTP::verifyResponseStatus(QString xml)
|
||||
}
|
||||
}
|
||||
|
||||
throw GfeHttpResponseException(-1, "Malformed GFE XML (missing root element)");
|
||||
throw GfeHttpResponseException(-1, "Malformed XML (missing root element)");
|
||||
}
|
||||
|
||||
QImage
|
||||
@@ -551,7 +531,7 @@ NvHTTP::openConnection(QUrl baseUrl,
|
||||
if (reply->error() != QNetworkReply::NoError)
|
||||
{
|
||||
if (logLevel >= NvLogLevel::NVLL_ERROR) {
|
||||
qWarning() << command << " request failed with error " << reply->error();
|
||||
qWarning() << command << "request failed with error:" << reply->error();
|
||||
}
|
||||
|
||||
if (reply->error() == QNetworkReply::SslHandshakeFailedError) {
|
||||
|
||||
+9
-10
@@ -162,16 +162,15 @@ public:
|
||||
quitApp();
|
||||
|
||||
void
|
||||
resumeApp(PSTREAM_CONFIGURATION streamConfig,
|
||||
QString& rtspSessionUrl);
|
||||
|
||||
void
|
||||
launchApp(int appId,
|
||||
PSTREAM_CONFIGURATION streamConfig,
|
||||
bool sops,
|
||||
bool localAudio,
|
||||
int gamepadMask,
|
||||
QString& rtspSessionUrl);
|
||||
startApp(QString verb,
|
||||
bool isGfe,
|
||||
int appId,
|
||||
PSTREAM_CONFIGURATION streamConfig,
|
||||
bool sops,
|
||||
bool localAudio,
|
||||
int gamepadMask,
|
||||
bool persistGameControllersOnDisconnect,
|
||||
QString& rtspSessionUrl);
|
||||
|
||||
QVector<NvApp>
|
||||
getAppList();
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "utils.h"
|
||||
|
||||
#include <QGuiApplication>
|
||||
#include <QLibraryInfo>
|
||||
|
||||
#include "streaming/session.h"
|
||||
#include "streaming/streamutils.h"
|
||||
@@ -17,6 +18,7 @@ SystemProperties::SystemProperties()
|
||||
hasDesktopEnvironment = WMUtils::isRunningDesktopEnvironment();
|
||||
isRunningWayland = WMUtils::isRunningWayland();
|
||||
isRunningXWayland = isRunningWayland && QGuiApplication::platformName() == "xcb";
|
||||
usesMaterial3Theme = QLibraryInfo::version() >= QVersionNumber(6, 5, 0);
|
||||
QString nativeArch = QSysInfo::currentCpuArchitecture();
|
||||
|
||||
#ifdef Q_OS_WIN32
|
||||
@@ -64,39 +66,28 @@ SystemProperties::SystemProperties()
|
||||
hasDiscordIntegration = false;
|
||||
#endif
|
||||
|
||||
// TODO: Do something smarter than this. We should be able to query
|
||||
// this from the decoder (or just try VIDEO_FORMAT_H265_MAIN10 and
|
||||
// fail if we don't get a hardware accelerated decoder).
|
||||
#ifdef Q_OS_DARWIN
|
||||
// HDR is supported by the VideoToolbox renderer
|
||||
supportsHdr = true;
|
||||
#else
|
||||
supportsHdr = false;
|
||||
#endif
|
||||
|
||||
unmappedGamepads = SdlInputHandler::getUnmappedGamepads();
|
||||
|
||||
// Populate data that requires talking to SDL. We do it all in one shot
|
||||
// and cache the results to speed up future queries on this data.
|
||||
querySdlVideoInfo();
|
||||
|
||||
Q_ASSERT(maximumStreamingFrameRate >= 60);
|
||||
Q_ASSERT(!monitorDesktopResolutions.isEmpty());
|
||||
Q_ASSERT(!monitorRefreshRates.isEmpty());
|
||||
Q_ASSERT(!monitorNativeResolutions.isEmpty());
|
||||
}
|
||||
|
||||
QRect SystemProperties::getDesktopResolution(int displayIndex)
|
||||
{
|
||||
// Returns default constructed QRect if out of bounds
|
||||
return monitorDesktopResolutions.value(displayIndex);
|
||||
}
|
||||
|
||||
QRect SystemProperties::getNativeResolution(int displayIndex)
|
||||
{
|
||||
// Returns default constructed QRect if out of bounds
|
||||
return monitorNativeResolutions.value(displayIndex);
|
||||
}
|
||||
|
||||
int SystemProperties::getRefreshRate(int displayIndex)
|
||||
{
|
||||
// Returns 0 if out of bounds
|
||||
return monitorRefreshRates.value(displayIndex);
|
||||
}
|
||||
|
||||
class QuerySdlVideoThread : public QThread
|
||||
{
|
||||
public:
|
||||
@@ -158,7 +149,7 @@ void SystemProperties::querySdlVideoInfoInternal()
|
||||
}
|
||||
}
|
||||
|
||||
Session::getDecoderInfo(testWindow, hasHardwareAcceleration, rendererAlwaysFullScreen, maximumResolution);
|
||||
Session::getDecoderInfo(testWindow, hasHardwareAcceleration, rendererAlwaysFullScreen, supportsHdr, maximumResolution);
|
||||
|
||||
SDL_DestroyWindow(testWindow);
|
||||
|
||||
@@ -203,35 +194,13 @@ void SystemProperties::refreshDisplaysInternal()
|
||||
return;
|
||||
}
|
||||
|
||||
monitorDesktopResolutions.clear();
|
||||
monitorNativeResolutions.clear();
|
||||
|
||||
// Never let the maximum drop below 60 FPS
|
||||
maximumStreamingFrameRate = 60;
|
||||
|
||||
SDL_DisplayMode bestMode;
|
||||
for (int displayIndex = 0; displayIndex < SDL_GetNumVideoDisplays(); displayIndex++) {
|
||||
SDL_DisplayMode desktopMode;
|
||||
int err;
|
||||
|
||||
err = SDL_GetDesktopDisplayMode(displayIndex, &desktopMode);
|
||||
if (err == 0) {
|
||||
if (desktopMode.w <= 8192 && desktopMode.h <= 8192) {
|
||||
monitorDesktopResolutions.insert(displayIndex, QRect(0, 0, desktopMode.w, desktopMode.h));
|
||||
}
|
||||
else {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Skipping resolution over 8K: %dx%d",
|
||||
desktopMode.w, desktopMode.h);
|
||||
}
|
||||
}
|
||||
else {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"SDL_GetDesktopDisplayMode() failed: %s",
|
||||
SDL_GetError());
|
||||
}
|
||||
|
||||
if (StreamUtils::getRealDesktopMode(displayIndex, &desktopMode)) {
|
||||
if (StreamUtils::getNativeDesktopMode(displayIndex, &desktopMode)) {
|
||||
if (desktopMode.w <= 8192 && desktopMode.h <= 8192) {
|
||||
monitorNativeResolutions.insert(displayIndex, QRect(0, 0, desktopMode.w, desktopMode.h));
|
||||
}
|
||||
@@ -254,7 +223,17 @@ void SystemProperties::refreshDisplaysInternal()
|
||||
}
|
||||
}
|
||||
|
||||
maximumStreamingFrameRate = qMax(maximumStreamingFrameRate, bestMode.refresh_rate);
|
||||
// Try to normalize values around our our standard refresh rates.
|
||||
// Some displays/OSes report values that are slightly off.
|
||||
if (bestMode.refresh_rate >= 58 && bestMode.refresh_rate <= 62) {
|
||||
monitorRefreshRates.append(60);
|
||||
}
|
||||
else if (bestMode.refresh_rate >= 28 && bestMode.refresh_rate <= 32) {
|
||||
monitorRefreshRates.append(30);
|
||||
}
|
||||
else {
|
||||
monitorRefreshRates.append(bestMode.refresh_rate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,14 +23,14 @@ public:
|
||||
Q_PROPERTY(bool hasBrowser MEMBER hasBrowser CONSTANT)
|
||||
Q_PROPERTY(bool hasDiscordIntegration MEMBER hasDiscordIntegration CONSTANT)
|
||||
Q_PROPERTY(QString unmappedGamepads MEMBER unmappedGamepads NOTIFY unmappedGamepadsChanged)
|
||||
Q_PROPERTY(int maximumStreamingFrameRate MEMBER maximumStreamingFrameRate CONSTANT)
|
||||
Q_PROPERTY(QSize maximumResolution MEMBER maximumResolution CONSTANT)
|
||||
Q_PROPERTY(QString versionString MEMBER versionString CONSTANT)
|
||||
Q_PROPERTY(bool supportsHdr MEMBER supportsHdr CONSTANT)
|
||||
Q_PROPERTY(bool usesMaterial3Theme MEMBER usesMaterial3Theme CONSTANT)
|
||||
|
||||
Q_INVOKABLE void refreshDisplays();
|
||||
Q_INVOKABLE QRect getDesktopResolution(int displayIndex);
|
||||
Q_INVOKABLE QRect getNativeResolution(int displayIndex);
|
||||
Q_INVOKABLE int getRefreshRate(int displayIndex);
|
||||
|
||||
signals:
|
||||
void unmappedGamepadsChanged();
|
||||
@@ -50,11 +50,11 @@ private:
|
||||
bool hasBrowser;
|
||||
bool hasDiscordIntegration;
|
||||
QString unmappedGamepads;
|
||||
int maximumStreamingFrameRate;
|
||||
QSize maximumResolution;
|
||||
QList<QRect> monitorDesktopResolutions;
|
||||
QList<QRect> monitorNativeResolutions;
|
||||
QList<int> monitorRefreshRates;
|
||||
QString versionString;
|
||||
bool supportsHdr;
|
||||
bool usesMaterial3Theme;
|
||||
};
|
||||
|
||||
|
||||
@@ -164,8 +164,10 @@ GlobalCommandLineParser::ParseResult GlobalCommandLineParser::parse(const QStrin
|
||||
"Starts Moonlight normally if no arguments are given.\n"
|
||||
"\n"
|
||||
"Available actions:\n"
|
||||
" list List the available apps on a host\n"
|
||||
" quit Quit the currently running app\n"
|
||||
" stream Start streaming an app\n"
|
||||
" pair Pair a new host\n"
|
||||
"\n"
|
||||
"See 'moonlight <action> --help' for help of specific action."
|
||||
);
|
||||
@@ -193,6 +195,10 @@ GlobalCommandLineParser::ParseResult GlobalCommandLineParser::parse(const QStrin
|
||||
return QuitRequested;
|
||||
} else if (action == "stream") {
|
||||
return StreamRequested;
|
||||
} else if (action == "pair") {
|
||||
return PairRequested;
|
||||
} else if (action == "list") {
|
||||
return ListRequested;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -242,6 +248,58 @@ QString QuitCommandLineParser::getHost() const
|
||||
return m_Host;
|
||||
}
|
||||
|
||||
PairCommandLineParser::PairCommandLineParser()
|
||||
{
|
||||
}
|
||||
|
||||
PairCommandLineParser::~PairCommandLineParser()
|
||||
{
|
||||
}
|
||||
|
||||
void PairCommandLineParser::parse(const QStringList &args)
|
||||
{
|
||||
CommandLineParser parser;
|
||||
parser.setupCommonOptions();
|
||||
parser.setApplicationDescription(
|
||||
"\n"
|
||||
"Pair with the specified host."
|
||||
);
|
||||
parser.addPositionalArgument("pair", "pair host");
|
||||
parser.addPositionalArgument("host", "Host computer name, UUID, or IP address", "<host>");
|
||||
parser.addValueOption("pin", "4 digit pairing PIN");
|
||||
|
||||
if (!parser.parse(args)) {
|
||||
parser.showError(parser.errorText());
|
||||
}
|
||||
|
||||
parser.handleUnknownOptions();
|
||||
|
||||
// This method will not return and terminates the process if --version or
|
||||
// --help is specified
|
||||
parser.handleHelpAndVersionOptions();
|
||||
|
||||
// Verify that host has been provided
|
||||
auto posArgs = parser.positionalArguments();
|
||||
if (posArgs.length() < 2) {
|
||||
parser.showError("Host not provided");
|
||||
}
|
||||
m_Host = parser.positionalArguments().at(1);
|
||||
m_PredefinedPin = parser.value("pin");
|
||||
if (!m_PredefinedPin.isEmpty() && m_PredefinedPin.length() != 4) {
|
||||
parser.showError("PIN must be 4 digits");
|
||||
}
|
||||
}
|
||||
|
||||
QString PairCommandLineParser::getHost() const
|
||||
{
|
||||
return m_Host;
|
||||
}
|
||||
|
||||
QString PairCommandLineParser::getPredefinedPin() const
|
||||
{
|
||||
return m_PredefinedPin;
|
||||
}
|
||||
|
||||
StreamCommandLineParser::StreamCommandLineParser()
|
||||
{
|
||||
m_WindowModeMap = {
|
||||
@@ -312,6 +370,7 @@ void StreamCommandLineParser::parse(const QStringList &args, StreamingPreference
|
||||
parser.addToggleOption("background-gamepad", "background gamepad input");
|
||||
parser.addToggleOption("reverse-scroll-direction", "inverted scroll direction");
|
||||
parser.addToggleOption("swap-gamepad-buttons", "swap A/B and X/Y gamepad buttons (Nintendo-style)");
|
||||
parser.addToggleOption("keep-awake", "prevent display sleep while streaming");
|
||||
parser.addChoiceOption("capture-system-keys", "capture system key combos", m_CaptureSysKeysModeMap.keys());
|
||||
parser.addChoiceOption("video-codec", "video codec", m_VideoCodecMap.keys());
|
||||
parser.addChoiceOption("video-decoder", "video decoder", m_VideoDecoderMap.keys());
|
||||
@@ -423,6 +482,9 @@ void StreamCommandLineParser::parse(const QStringList &args, StreamingPreference
|
||||
// Resolve --swap-gamepad-buttons and --no-swap-gamepad-buttons options
|
||||
preferences->swapFaceButtons = parser.getToggleOptionValue("swap-gamepad-buttons", preferences->swapFaceButtons);
|
||||
|
||||
// Resolve --keep-awake and --no-keep-awake options
|
||||
preferences->keepAwake = parser.getToggleOptionValue("keep-awake", preferences->keepAwake);
|
||||
|
||||
// Resolve --capture-system-keys option
|
||||
if (parser.isSet("capture-system-keys")) {
|
||||
preferences->captureSysKeysMode = mapValue(m_CaptureSysKeysModeMap, parser.getChoiceOptionValue("capture-system-keys"));
|
||||
@@ -464,3 +526,62 @@ QString StreamCommandLineParser::getAppName() const
|
||||
{
|
||||
return m_AppName;
|
||||
}
|
||||
|
||||
ListCommandLineParser::ListCommandLineParser()
|
||||
{
|
||||
}
|
||||
|
||||
ListCommandLineParser::~ListCommandLineParser()
|
||||
{
|
||||
}
|
||||
|
||||
void ListCommandLineParser::parse(const QStringList &args)
|
||||
{
|
||||
CommandLineParser parser;
|
||||
parser.setupCommonOptions();
|
||||
parser.setApplicationDescription(
|
||||
"\n"
|
||||
"List the available apps on the given host."
|
||||
);
|
||||
parser.addPositionalArgument("list", "list available apps");
|
||||
parser.addPositionalArgument("host", "Host computer name, UUID, or IP address", "<host>");
|
||||
|
||||
parser.addFlagOption("csv", "Print as CSV with additional information");
|
||||
parser.addFlagOption("verbose", "Displays additional information");
|
||||
|
||||
if (!parser.parse(args)) {
|
||||
parser.showError(parser.errorText());
|
||||
}
|
||||
|
||||
parser.handleUnknownOptions();
|
||||
|
||||
|
||||
m_PrintCSV = parser.isSet("csv");
|
||||
m_Verbose = parser.isSet("verbose");
|
||||
|
||||
// This method will not return and terminates the process if --version or
|
||||
// --help is specified
|
||||
parser.handleHelpAndVersionOptions();
|
||||
|
||||
// Verify that host has been provided
|
||||
auto posArgs = parser.positionalArguments();
|
||||
if (posArgs.length() < 2) {
|
||||
parser.showError("Host not provided");
|
||||
}
|
||||
m_Host = parser.positionalArguments().at(1);
|
||||
}
|
||||
|
||||
QString ListCommandLineParser::getHost() const
|
||||
{
|
||||
return m_Host;
|
||||
}
|
||||
|
||||
bool ListCommandLineParser::isPrintCSV() const
|
||||
{
|
||||
return m_PrintCSV;
|
||||
}
|
||||
|
||||
bool ListCommandLineParser::isVerbose() const
|
||||
{
|
||||
return m_Verbose;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ public:
|
||||
NormalStartRequested,
|
||||
StreamRequested,
|
||||
QuitRequested,
|
||||
PairRequested,
|
||||
ListRequested,
|
||||
};
|
||||
|
||||
GlobalCommandLineParser();
|
||||
@@ -35,6 +37,22 @@ private:
|
||||
QString m_Host;
|
||||
};
|
||||
|
||||
class PairCommandLineParser
|
||||
{
|
||||
public:
|
||||
PairCommandLineParser();
|
||||
virtual ~PairCommandLineParser();
|
||||
|
||||
void parse(const QStringList &args);
|
||||
|
||||
QString getHost() const;
|
||||
QString getPredefinedPin() const;
|
||||
|
||||
private:
|
||||
QString m_Host;
|
||||
QString m_PredefinedPin;
|
||||
};
|
||||
|
||||
class StreamCommandLineParser
|
||||
{
|
||||
public:
|
||||
@@ -55,3 +73,21 @@ private:
|
||||
QMap<QString, StreamingPreferences::VideoDecoderSelection> m_VideoDecoderMap;
|
||||
QMap<QString, StreamingPreferences::CaptureSysKeysMode> m_CaptureSysKeysModeMap;
|
||||
};
|
||||
|
||||
class ListCommandLineParser
|
||||
{
|
||||
public:
|
||||
ListCommandLineParser();
|
||||
virtual ~ListCommandLineParser();
|
||||
|
||||
void parse(const QStringList &args);
|
||||
|
||||
QString getHost() const;
|
||||
bool isPrintCSV() const;
|
||||
bool isVerbose() const;
|
||||
|
||||
private:
|
||||
QString m_Host;
|
||||
bool m_PrintCSV;
|
||||
bool m_Verbose;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
#include "listapps.h"
|
||||
|
||||
#include "backend/boxartmanager.h"
|
||||
#include "backend/computermanager.h"
|
||||
#include "backend/computerseeker.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QTimer>
|
||||
|
||||
#define COMPUTER_SEEK_TIMEOUT 10000
|
||||
#define APP_SEEK_TIMEOUT 10000
|
||||
|
||||
namespace CliListApps
|
||||
{
|
||||
|
||||
enum State {
|
||||
StateInit,
|
||||
StateSeekComputer,
|
||||
StateListApp,
|
||||
StateSeekApp,
|
||||
StateFailure,
|
||||
};
|
||||
|
||||
class Event
|
||||
{
|
||||
public:
|
||||
enum Type {
|
||||
ComputerFound,
|
||||
ComputerUpdated,
|
||||
ComputerSeekTimedout,
|
||||
Executed,
|
||||
};
|
||||
|
||||
Event(Type type)
|
||||
: type(type), computerManager(nullptr), computer(nullptr) {}
|
||||
|
||||
Type type;
|
||||
ComputerManager *computerManager;
|
||||
NvComputer *computer;
|
||||
QString errorMessage;
|
||||
};
|
||||
|
||||
class LauncherPrivate
|
||||
{
|
||||
Q_DECLARE_PUBLIC(Launcher)
|
||||
|
||||
public:
|
||||
LauncherPrivate(Launcher *q) : q_ptr(q) {}
|
||||
|
||||
void handleEvent(Event event)
|
||||
{
|
||||
Q_Q(Launcher);
|
||||
NvApp app;
|
||||
|
||||
switch (event.type) {
|
||||
// Occurs when CLI main calls execute
|
||||
case Event::Executed:
|
||||
if (m_State == StateInit) {
|
||||
m_State = StateSeekComputer;
|
||||
m_ComputerManager = event.computerManager;
|
||||
|
||||
m_ComputerSeeker = new ComputerSeeker(m_ComputerManager, m_ComputerName, q);
|
||||
q->connect(m_ComputerSeeker, &ComputerSeeker::computerFound,
|
||||
q, &Launcher::onComputerFound);
|
||||
q->connect(m_ComputerSeeker, &ComputerSeeker::errorTimeout,
|
||||
q, &Launcher::onComputerSeekTimeout);
|
||||
m_ComputerSeeker->start(COMPUTER_SEEK_TIMEOUT);
|
||||
|
||||
q->connect(m_ComputerManager, &ComputerManager::computerStateChanged,
|
||||
q, &Launcher::onComputerUpdated);
|
||||
|
||||
m_BoxArtManager = new BoxArtManager(q);
|
||||
|
||||
if (m_Arguments.isVerbose()) {
|
||||
fprintf(stdout, "Establishing connection to PC...\n");
|
||||
}
|
||||
}
|
||||
break;
|
||||
// Occurs when computer search timed out
|
||||
case Event::ComputerSeekTimedout:
|
||||
if (m_State == StateSeekComputer) {
|
||||
fprintf(stderr, "%s\n", qPrintable(QString("Failed to connect to %1").arg(m_ComputerName)));
|
||||
|
||||
QCoreApplication::exit(-1);
|
||||
}
|
||||
break;
|
||||
// Occurs when searched computer is found
|
||||
case Event::ComputerFound:
|
||||
if (m_State == StateSeekComputer) {
|
||||
if (event.computer->pairState == NvComputer::PS_PAIRED) {
|
||||
m_State = StateSeekApp;
|
||||
m_Computer = event.computer;
|
||||
m_TimeoutTimer->start(APP_SEEK_TIMEOUT);
|
||||
if (m_Arguments.isVerbose()) {
|
||||
fprintf(stdout, "Loading app list...\n");
|
||||
}
|
||||
} else {
|
||||
m_State = StateFailure;
|
||||
fprintf(stderr, "%s\n", qPrintable(QObject::tr("Computer %1 has not been paired. "
|
||||
"Please open Moonlight to pair before retrieving games list.")
|
||||
.arg(event.computer->name)));
|
||||
|
||||
QCoreApplication::exit(-1);
|
||||
}
|
||||
}
|
||||
break;
|
||||
// Occurs when a computer is updated
|
||||
case Event::ComputerUpdated:
|
||||
if (m_State == StateSeekApp) {
|
||||
m_Arguments.isPrintCSV() ? printAppsCSV(m_Computer->appList) : printApps(m_Computer->appList);
|
||||
|
||||
QCoreApplication::exit(0);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void printApps(QVector<NvApp> apps) {
|
||||
for (int i = 0; i < apps.length(); i++) {
|
||||
fprintf(stdout, "%s\n", qPrintable(apps[i].name));
|
||||
}
|
||||
}
|
||||
|
||||
void printAppsCSV(QVector<NvApp> apps) {
|
||||
fprintf(stdout, "Name, ID, HDR Support, App Collection Game, Hidden, Direct Launch, Boxart URL\n");
|
||||
for (int i = 0; i < apps.length(); i++) {
|
||||
printAppCSV(apps[i]);
|
||||
}
|
||||
}
|
||||
|
||||
void printAppCSV(NvApp app) const
|
||||
{
|
||||
fprintf(stdout, "\"%s\",%d,%s,%s,%s,%s,\"%s\"\n", qPrintable(app.name),
|
||||
app.id,
|
||||
app.hdrSupported ? "true" : "false",
|
||||
app.isAppCollectorGame ? "true" : "false",
|
||||
app.hidden ? "true" : "false",
|
||||
app.directLaunch ? "true" : "false",
|
||||
qPrintable(m_BoxArtManager->loadBoxArt(m_Computer, app).toDisplayString()));
|
||||
}
|
||||
|
||||
Launcher *q_ptr;
|
||||
ComputerManager *m_ComputerManager;
|
||||
QString m_ComputerName;
|
||||
ComputerSeeker *m_ComputerSeeker;
|
||||
BoxArtManager *m_BoxArtManager;
|
||||
NvComputer *m_Computer;
|
||||
State m_State;
|
||||
QTimer *m_TimeoutTimer;
|
||||
ListCommandLineParser m_Arguments;
|
||||
};
|
||||
|
||||
Launcher::Launcher(QString computer, ListCommandLineParser arguments, QObject *parent)
|
||||
: QObject(parent),
|
||||
m_DPtr(new LauncherPrivate(this))
|
||||
{
|
||||
Q_D(Launcher);
|
||||
d->m_ComputerName = computer;
|
||||
d->m_State = StateInit;
|
||||
d->m_TimeoutTimer = new QTimer(this);
|
||||
d->m_TimeoutTimer->setSingleShot(true);
|
||||
d->m_Arguments = arguments;
|
||||
connect(d->m_TimeoutTimer, &QTimer::timeout,
|
||||
this, &Launcher::onComputerSeekTimeout);
|
||||
}
|
||||
|
||||
Launcher::~Launcher()
|
||||
{
|
||||
}
|
||||
|
||||
void Launcher::execute(ComputerManager *manager)
|
||||
{
|
||||
Q_D(Launcher);
|
||||
Event event(Event::Executed);
|
||||
event.computerManager = manager;
|
||||
d->handleEvent(event);
|
||||
}
|
||||
|
||||
bool Launcher::isExecuted() const
|
||||
{
|
||||
Q_D(const Launcher);
|
||||
return d->m_State != StateInit;
|
||||
}
|
||||
|
||||
void Launcher::onComputerFound(NvComputer *computer)
|
||||
{
|
||||
Q_D(Launcher);
|
||||
Event event(Event::ComputerFound);
|
||||
event.computer = computer;
|
||||
d->handleEvent(event);
|
||||
}
|
||||
|
||||
void Launcher::onComputerSeekTimeout()
|
||||
{
|
||||
Q_D(Launcher);
|
||||
Event event(Event::ComputerSeekTimedout);
|
||||
d->handleEvent(event);
|
||||
}
|
||||
|
||||
void Launcher::onComputerUpdated(NvComputer *computer)
|
||||
{
|
||||
Q_D(Launcher);
|
||||
Event event(Event::ComputerUpdated);
|
||||
event.computer = computer;
|
||||
d->handleEvent(event);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
#pragma once
|
||||
|
||||
#include "commandlineparser.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QVariant>
|
||||
|
||||
class ComputerManager;
|
||||
class NvComputer;
|
||||
|
||||
namespace CliListApps
|
||||
{
|
||||
|
||||
class LauncherPrivate;
|
||||
|
||||
class Launcher : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_DECLARE_PRIVATE_D(m_DPtr, Launcher)
|
||||
|
||||
public:
|
||||
explicit Launcher(QString computer, ListCommandLineParser arguments, QObject *parent = nullptr);
|
||||
~Launcher();
|
||||
|
||||
Q_INVOKABLE void execute(ComputerManager *manager);
|
||||
Q_INVOKABLE bool isExecuted() const;
|
||||
|
||||
private slots:
|
||||
void onComputerFound(NvComputer *computer);
|
||||
void onComputerUpdated(NvComputer *computer);
|
||||
void onComputerSeekTimeout();
|
||||
|
||||
private:
|
||||
QScopedPointer<LauncherPrivate> m_DPtr;
|
||||
ListCommandLineParser m_Arguments;
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
#include "pair.h"
|
||||
|
||||
#include "backend/computermanager.h"
|
||||
#include "backend/computerseeker.h"
|
||||
#include <QCoreApplication>
|
||||
#include <QTimer>
|
||||
|
||||
#define COMPUTER_SEEK_TIMEOUT 10000
|
||||
|
||||
namespace CliPair
|
||||
{
|
||||
|
||||
enum State {
|
||||
StateInit,
|
||||
StateSeekComputer,
|
||||
StatePairing,
|
||||
StateFailure,
|
||||
StateComplete,
|
||||
};
|
||||
|
||||
class Event
|
||||
{
|
||||
public:
|
||||
enum Type {
|
||||
ComputerFound,
|
||||
Executed,
|
||||
PairingCompleted,
|
||||
Timedout,
|
||||
};
|
||||
|
||||
Event(Type type)
|
||||
: type(type), computerManager(nullptr), computer(nullptr) {}
|
||||
|
||||
Type type;
|
||||
ComputerManager *computerManager;
|
||||
NvComputer *computer;
|
||||
QString errorMessage;
|
||||
};
|
||||
|
||||
class LauncherPrivate
|
||||
{
|
||||
Q_DECLARE_PUBLIC(Launcher)
|
||||
|
||||
public:
|
||||
LauncherPrivate(Launcher *q) : q_ptr(q) {}
|
||||
|
||||
void handleEvent(Event event)
|
||||
{
|
||||
Q_Q(Launcher);
|
||||
|
||||
switch (event.type) {
|
||||
// Occurs when CliPair becomes visible and the UI calls launcher's execute()
|
||||
case Event::Executed:
|
||||
if (m_State == StateInit) {
|
||||
m_State = StateSeekComputer;
|
||||
m_ComputerManager = event.computerManager;
|
||||
q->connect(m_ComputerManager, &ComputerManager::pairingCompleted,
|
||||
q, &Launcher::onPairingCompleted);
|
||||
|
||||
// If we weren't provided a predefined PIN, generate one now
|
||||
if (m_PredefinedPin.isEmpty()) {
|
||||
m_PredefinedPin = m_ComputerManager->generatePinString();
|
||||
}
|
||||
|
||||
m_ComputerSeeker = new ComputerSeeker(m_ComputerManager, m_ComputerName, q);
|
||||
q->connect(m_ComputerSeeker, &ComputerSeeker::computerFound,
|
||||
q, &Launcher::onComputerFound);
|
||||
q->connect(m_ComputerSeeker, &ComputerSeeker::errorTimeout,
|
||||
q, &Launcher::onTimeout);
|
||||
m_ComputerSeeker->start(COMPUTER_SEEK_TIMEOUT);
|
||||
|
||||
emit q->searchingComputer();
|
||||
}
|
||||
break;
|
||||
// Occurs when searched computer is found
|
||||
case Event::ComputerFound:
|
||||
if (m_State == StateSeekComputer) {
|
||||
if (event.computer->pairState == NvComputer::PS_PAIRED) {
|
||||
m_State = StateFailure;
|
||||
QString msg = QObject::tr("%1 is already paired").arg(event.computer->name);
|
||||
emit q->failed(msg);
|
||||
}
|
||||
else {
|
||||
Q_ASSERT(!m_PredefinedPin.isEmpty());
|
||||
|
||||
m_State = StatePairing;
|
||||
m_ComputerManager->pairHost(event.computer, m_PredefinedPin);
|
||||
emit q->pairing(event.computer->name, m_PredefinedPin);
|
||||
}
|
||||
}
|
||||
break;
|
||||
// Occurs when pairing operation completes
|
||||
case Event::PairingCompleted:
|
||||
if (m_State == StatePairing) {
|
||||
if (event.errorMessage.isEmpty()) {
|
||||
m_State = StateComplete;
|
||||
emit q->success();
|
||||
}
|
||||
else {
|
||||
m_State = StateFailure;
|
||||
emit q->failed(event.errorMessage);
|
||||
}
|
||||
}
|
||||
break;
|
||||
// Occurs when computer search timed out
|
||||
case Event::Timedout:
|
||||
if (m_State == StateSeekComputer) {
|
||||
m_State = StateFailure;
|
||||
emit q->failed(QObject::tr("Failed to connect to %1").arg(m_ComputerName));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Launcher *q_ptr;
|
||||
QString m_ComputerName;
|
||||
QString m_PredefinedPin;
|
||||
ComputerManager *m_ComputerManager;
|
||||
ComputerSeeker *m_ComputerSeeker;
|
||||
NvComputer *m_Computer;
|
||||
State m_State;
|
||||
QTimer *m_TimeoutTimer;
|
||||
};
|
||||
|
||||
Launcher::Launcher(QString computer, QString predefinedPin, QObject *parent)
|
||||
: QObject(parent),
|
||||
m_DPtr(new LauncherPrivate(this))
|
||||
{
|
||||
Q_D(Launcher);
|
||||
d->m_ComputerName = computer;
|
||||
d->m_PredefinedPin = predefinedPin;
|
||||
d->m_State = StateInit;
|
||||
d->m_TimeoutTimer = new QTimer(this);
|
||||
d->m_TimeoutTimer->setSingleShot(true);
|
||||
connect(d->m_TimeoutTimer, &QTimer::timeout,
|
||||
this, &Launcher::onTimeout);
|
||||
}
|
||||
|
||||
Launcher::~Launcher()
|
||||
{
|
||||
}
|
||||
|
||||
void Launcher::execute(ComputerManager *manager)
|
||||
{
|
||||
Q_D(Launcher);
|
||||
Event event(Event::Executed);
|
||||
event.computerManager = manager;
|
||||
d->handleEvent(event);
|
||||
}
|
||||
|
||||
bool Launcher::isExecuted() const
|
||||
{
|
||||
Q_D(const Launcher);
|
||||
return d->m_State != StateInit;
|
||||
}
|
||||
|
||||
void Launcher::onComputerFound(NvComputer *computer)
|
||||
{
|
||||
Q_D(Launcher);
|
||||
Event event(Event::ComputerFound);
|
||||
event.computer = computer;
|
||||
d->handleEvent(event);
|
||||
}
|
||||
|
||||
void Launcher::onTimeout()
|
||||
{
|
||||
Q_D(Launcher);
|
||||
Event event(Event::Timedout);
|
||||
d->handleEvent(event);
|
||||
}
|
||||
|
||||
void Launcher::onPairingCompleted(NvComputer* computer, QString error)
|
||||
{
|
||||
Q_D(Launcher);
|
||||
Event event(Event::PairingCompleted);
|
||||
event.computer = computer;
|
||||
event.errorMessage = error;
|
||||
d->handleEvent(event);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QVariant>
|
||||
|
||||
class ComputerManager;
|
||||
class NvComputer;
|
||||
class Session;
|
||||
class StreamingPreferences;
|
||||
|
||||
namespace CliPair
|
||||
{
|
||||
|
||||
class Event;
|
||||
class LauncherPrivate;
|
||||
|
||||
class Launcher : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_DECLARE_PRIVATE_D(m_DPtr, Launcher)
|
||||
|
||||
public:
|
||||
explicit Launcher(QString computer, QString predefinedPin,
|
||||
QObject *parent = nullptr);
|
||||
~Launcher();
|
||||
Q_INVOKABLE void execute(ComputerManager *manager);
|
||||
Q_INVOKABLE bool isExecuted() const;
|
||||
|
||||
signals:
|
||||
void searchingComputer();
|
||||
void pairing(QString pcName, QString pin);
|
||||
void failed(QString text);
|
||||
void success();
|
||||
|
||||
private slots:
|
||||
void onComputerFound(NvComputer *computer);
|
||||
void onPairingCompleted(NvComputer *computer, QString error);
|
||||
void onTimeout();
|
||||
|
||||
private:
|
||||
QScopedPointer<LauncherPrivate> m_DPtr;
|
||||
};
|
||||
|
||||
}
|
||||
@@ -118,7 +118,7 @@ public:
|
||||
}
|
||||
break;
|
||||
// Occurs when the previous app quit has been completed, handles quitting errors if any
|
||||
// happended. ComputerUpdated event's handler handles session start when previous app has
|
||||
// happened. ComputerUpdated event's handler handles session start when previous app has
|
||||
// quit.
|
||||
case Event::AppQuitCompleted:
|
||||
if (m_State == StateSeekApp && !event.errorMessage.isEmpty()) {
|
||||
@@ -162,7 +162,7 @@ public:
|
||||
|
||||
QString getCurrentAppName() const
|
||||
{
|
||||
for (NvApp app : m_Computer->appList) {
|
||||
for (const NvApp& app : m_Computer->appList) {
|
||||
if (m_Computer->currentGameId == app.id) {
|
||||
return app.name;
|
||||
}
|
||||
|
||||
@@ -4,21 +4,19 @@
|
||||
<metadata_license>CC0-1.0</metadata_license>
|
||||
<project_license>GPL-3.0+</project_license>
|
||||
<name>Moonlight</name>
|
||||
<summary>Play games remotely from your NVIDIA GameStream-enabled PC</summary>
|
||||
<summary>Stream games and other applications from another PC running Sunshine or GeForce Experience</summary>
|
||||
|
||||
<description>
|
||||
<p>Moonlight can stream games and other applications from a PC with an NVIDIA GeForce GTX/RTX or NVIDIA Quadro GPU and GeForce/Quadro Experience installed.</p>
|
||||
<p>Moonlight can stream games and other applications from another PC with Sunshine or GeForce Experience software installed.</p>
|
||||
<p>Features include:</p>
|
||||
<ul>
|
||||
<li>Streaming at up to 4K resolution</li>
|
||||
<li>Support for up to 120 FPS streaming (high refresh rate monitor recommended)</li>
|
||||
<li>Hardware accelerated video decoding with VAAPI, VDPAU, and NVDEC support</li>
|
||||
<li>H.264, HEVC, and AV1 codec support</li>
|
||||
<li>7.1 surround sound audio support</li>
|
||||
<li>10-point multitouch support</li>
|
||||
<li>Gamepad support with force feedback for up to 16 players</li>
|
||||
<li>Support for both pointer capture (for games) and direct mouse control (for remote desktop)</li>
|
||||
<li>Support for passing system-wide keyboard shortcuts like Alt+Tab to the host</li>
|
||||
<li>Direct touchscreen mouse control</li>
|
||||
<li>HEVC support for better video compression efficiency</li>
|
||||
<li>Gamepad support with force feedback for up to 4 players</li>
|
||||
</ul>
|
||||
</description>
|
||||
|
||||
@@ -36,6 +34,173 @@
|
||||
</screenshots>
|
||||
|
||||
<releases>
|
||||
<release version="5.0.1" date="2023-11-02">
|
||||
<description>
|
||||
<p>Bugfixes:</p>
|
||||
<ul>
|
||||
<li>Fixed connection issue streaming from some hosts running GeForce Experience or Sunshine v0.20 and earlier</li>
|
||||
<li>Updated community-contributed translations from Weblate</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="5.0.0" date="2023-10-18">
|
||||
<description>
|
||||
<p>New features:</p>
|
||||
<ul>
|
||||
<li>Custom FPS values</li>
|
||||
<li>Streaming using the AV1 codec from Sunshine hosts</li>
|
||||
<li>Horizontal scrolling with Sunshine hosts</li>
|
||||
<li>Native multi-touch input with Sunshine hosts</li>
|
||||
<li>Controller type information is passed to Sunshine for controller type autoselection</li>
|
||||
<li>Extended features of PS4/PS5 controllers (touchpad, battery state, gyro/accelerometer motion, and RGB LED) are natively passed to Sunshine hosts</li>
|
||||
<li>Support for up to 16 gamepads with Sunshine hosts</li>
|
||||
<li>Frame pacing support on Wayland</li>
|
||||
<li>Host-side processing latency is now displayed in the performance overlay with Sunshine hosts</li>
|
||||
<li>Window title now includes the PC name when streaming</li>
|
||||
<li>Alphabetical sorting of the PC list</li>
|
||||
<li>Updated Play and Stop button design to improve contrast with app box art</li>
|
||||
<li>The settings page automatically scrolls to the selected element when using keyboard or gamepad navigation</li>
|
||||
<li>Pressing Share+PS buttons on PS4/PS5 controllers can be used to emulate a clickpad press</li>
|
||||
<li>Improved audio quality when streaming with a high bitrate from remote hosts</li>
|
||||
<li>Increased mouse polling rate</li>
|
||||
</ul>
|
||||
<p>Behavior Changes:</p>
|
||||
<ul>
|
||||
<li>IPv6 addresses must be provided in URL-escaped form. Example: [2001:db8::1234:5678] or [2001:db8::1234:5678]:47989</li>
|
||||
<li>Default bitrate logic has been adjusted to behave better with custom resolutions and frame rates</li>
|
||||
</ul>
|
||||
<p>Bugfixes:</p>
|
||||
<ul>
|
||||
<li>Reduced the frequency of audio underruns (clicks and pops) by increasing audio buffer size</li>
|
||||
<li>Fixed "Quitting App" message never dismissing in some cases</li>
|
||||
<li>Fixed parsing IPv6 literals with port numbers</li>
|
||||
<li>Fixed unnecessary WiFi activity causing stream disruptions</li>
|
||||
<li>Fixed paired hosts being forgotten if Moonlight terminates while writing hosts to disk</li>
|
||||
<li>Fixed freeze when closing the stream on Windows with some software that uses DX11 hooks</li>
|
||||
<li>Fixed delayed PC wake when activating Wake-On-LAN feature</li>
|
||||
<li>Fixed Wake-on-LAN when Sunshine is running on alternate ports</li>
|
||||
<li>Fixed leak of /dev/dri fds when trying to initialize VAAPI decoder</li>
|
||||
<li>Fixed support for the Applications/Menu key</li>
|
||||
<li>Fixed automatic language detection for Simplified Chinese and Traditional Chinese</li>
|
||||
<li>Fixed streaming from Sunshine when the client PC has multiple network interfaces or IPv6 privacy extensions enabled</li>
|
||||
<li>Fixed small receive buffer causing video packet loss on some systems</li>
|
||||
<li>Fixed theme overrides causing the UI to be unusable on some systems</li>
|
||||
<li>Fixed tearing with V-Sync enabled on macOS and Linux when using the SDL renderer (software decoding and some hardware decoders)</li>
|
||||
<li>Fixed stateful V4L2 hardware decoding using h264_v4l2m2m and hevc_v4l2m2m decoders</li>
|
||||
<li>Fixed CVE-2023-42801</li>
|
||||
<li>Updated included gamepad mappings</li>
|
||||
<li>Updated community-contributed translations from Weblate</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="4.3.1" date="2022-11-03">
|
||||
<description>
|
||||
<p>Changes:</p>
|
||||
<ul>
|
||||
<li>Fixed crash during specific patterns of audio packet loss</li>
|
||||
<li>Fixed detection of local streaming when using IPv6 addresses</li>
|
||||
<li>Windowed mode streaming now starts maximized if the GUI window was maximized</li>
|
||||
<li>Updated community-contributed translations from Weblate</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="4.3.0" date="2022-10-28">
|
||||
<description>
|
||||
<p>New features:</p>
|
||||
<ul>
|
||||
<li>Improved support for GeForce Experience 3.26</li>
|
||||
<li>Improved image quality and performance when recovering from video packet loss using HEVC</li>
|
||||
<li>Improved error concealment after audio packet loss</li>
|
||||
<li>Improved UI performance when paired with many hosts or hosts with large numbers of apps</li>
|
||||
<li>Pairing with Sunshine hosts is now supported even if the host is in a streaming session</li>
|
||||
</ul>
|
||||
<p>Bugfixes:</p>
|
||||
<ul>
|
||||
<li>Fixed streaming at resolutions below 720x480 with GeForce Experience 3.26</li>
|
||||
<li>Fixed window position and size being lost when toggling between full-screen and windowed mode</li>
|
||||
<li>Fixed initial window size not always respecting the stream aspect ratio</li>
|
||||
<li>Fixed unexpected error when streaming at resolutions above 4096x4096 using Sunshine</li>
|
||||
<li>Updated included gamepad mappings</li>
|
||||
<li>Updated community-contributed translations from Weblate</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="4.2.1" date="2022-09-12">
|
||||
<description>
|
||||
<p>Bugfixes:</p>
|
||||
<ul>
|
||||
<li>Fixed streaming from hosts running Sunshine</li>
|
||||
<li>Updated community-contributed translations from Weblate</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="4.2.0" date="2022-09-10">
|
||||
<description>
|
||||
<p>New features:</p>
|
||||
<ul>
|
||||
<li>Added support for pairing via the terminal</li>
|
||||
<li>Added support for listing installed apps on a host via the terminal</li>
|
||||
<li>Improved handling of V-Sync on Wayland systems to reduce latency</li>
|
||||
<li>Improved performance using the legacy non-EGL VAAPI rendering code</li>
|
||||
</ul>
|
||||
<p>Bugfixes:</p>
|
||||
<ul>
|
||||
<li>Fixed native resolution detection on high DPI Wayland systems</li>
|
||||
<li>Fixed audio crackling with SDL 2.24.0 on systems using PulseAudio</li>
|
||||
<li>Updated included gamepad mappings</li>
|
||||
<li>Updated community-contributed translations from Weblate</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="4.1.0" date="2022-05-23">
|
||||
<description>
|
||||
<p>New features:</p>
|
||||
<ul>
|
||||
<li>Overlays are now supported on the non-EGL X11 VAAPI renderer (requires RGBA subpicture support in the VAAPI driver)</li>
|
||||
<li>Cursor lock can now be disabled when in full-screen mode</li>
|
||||
</ul>
|
||||
<p>Behavior Changes:</p>
|
||||
<ul>
|
||||
<li>Moonlight can now reduce the client display resolution in full-screen mode if the current display mode doesn't have a high enough refresh rate for the selected stream frame rate</li>
|
||||
<li>When choosing a new client display resolution in full-screen mode, Moonlight will pick a display mode with the closest aspect ratio to the selected stream resolution</li>
|
||||
<li>Moonlight now defaults to borderless windowed mode on Wayland systems</li>
|
||||
|
||||
</ul>
|
||||
<p>Bugfixes:</p>
|
||||
<ul>
|
||||
<li>Fixed missing toolbar buttons in certain languages</li>
|
||||
<li>Fixed some text on the settings page not updating when the language is changed</li>
|
||||
<li>Fixed a frame pacing bug that could cause excessive frame drops</li>
|
||||
<li>Fixed the "keep display awake" setting not being saved when Moonlight restarts</li>
|
||||
<li>Fixed black screen after unmaximizing the Moonlight window on some Linux systems</li>
|
||||
<li>Updated included gamepad mappings</li>
|
||||
<li>Updated community-contributed translations from Weblate</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="4.0.0" date="2022-04-02">
|
||||
<description>
|
||||
<p>New features:</p>
|
||||
<ul>
|
||||
<li>Added a new key combo for locking the mouse cursor to the window (Ctrl+Alt+Shift+L)</li>
|
||||
<li>Added an option to avoid keeping the client display awake while streaming</li>
|
||||
</ul>
|
||||
<p>Behavior Changes:</p>
|
||||
<ul>
|
||||
<li>HDR streaming is now a separate option rather than being part of the Video Codec override dropdown</li>
|
||||
<li>HDR streaming is no longer restricted to apps that are certified by NVIDIA as compatible with GameStream HDR. Some newer games may require an HDR display or EDID emulator dongle connected to your host PC for HDR options to be available.</li>
|
||||
</ul>
|
||||
<p>Bugfixes:</p>
|
||||
<ul>
|
||||
<li>Fixed loss of all audio if the active audio device is unplugged while streaming</li>
|
||||
<li>Fixed the GUI being unresponsive after manually quitting when connection to the host is lost</li>
|
||||
<li>Fixed a rare crash if a controller disconnects right as rumble is starting</li>
|
||||
<li>Fixed text rendering on the overlay UI when using SDL_ttf 2.0.18 on Linux</li>
|
||||
<li>Updated included gamepad mappings</li>
|
||||
<li>Updated community-contributed translations from Weblate</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="3.2.0" date="2022-01-18">
|
||||
<description>
|
||||
<p>New features:</p>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
[Desktop Entry]
|
||||
Name=Moonlight
|
||||
Comment=Stream games from your NVIDIA GameStream-enabled PC
|
||||
Comment=Stream games and other applications from another PC running Sunshine or GeForce Experience
|
||||
Exec=moonlight
|
||||
Icon=moonlight
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Categories=Qt;Game;
|
||||
Keywords=nvidia;gamestream;stream;
|
||||
Keywords=nvidia;gamestream;stream;sunshine;remote play;
|
||||
|
||||
+15
-13
@@ -1,5 +1,6 @@
|
||||
import QtQuick 2.9
|
||||
import QtQuick.Controls 2.2
|
||||
import QtQuick.Controls.Material 2.2
|
||||
|
||||
import AppModel 1.0
|
||||
import ComputerManager 1.0
|
||||
@@ -89,7 +90,7 @@ CenteredGridView {
|
||||
|
||||
onSourceSizeChanged: {
|
||||
// Nearly all of Nvidia's official box art does not match the dimensions of placeholder
|
||||
// images, however the one known exeception is Overcooked. Therefore, we only execute
|
||||
// images, however the one known exception is Overcooked. Therefore, we only execute
|
||||
// the image size checks if this is not an app collector game. We know the officially
|
||||
// supported games all have box art, so this check is not required.
|
||||
if (!model.isAppCollectorGame &&
|
||||
@@ -121,15 +122,15 @@ CenteredGridView {
|
||||
anchors.fill: appIcon
|
||||
|
||||
sourceComponent: Item {
|
||||
ToolButton {
|
||||
RoundButton {
|
||||
anchors.horizontalCenterOffset: appIcon.isPlaceholder ? -47 : 0
|
||||
anchors.verticalCenterOffset: appIcon.isPlaceholder ? -75 : -60
|
||||
anchors.centerIn: parent
|
||||
implicitWidth: 125
|
||||
implicitHeight: 125
|
||||
implicitWidth: 85
|
||||
implicitHeight: 85
|
||||
|
||||
Image {
|
||||
source: "qrc:/res/baseline-play_circle_filled_white-48px.svg"
|
||||
source: "qrc:/res/play_arrow_FILL1_wght700_GRAD200_opsz48.svg"
|
||||
anchors.centerIn: parent
|
||||
sourceSize {
|
||||
width: 75
|
||||
@@ -145,17 +146,19 @@ CenteredGridView {
|
||||
ToolTip.delay: 1000
|
||||
ToolTip.timeout: 3000
|
||||
ToolTip.visible: hovered
|
||||
|
||||
Material.background: "#D0808080"
|
||||
}
|
||||
|
||||
ToolButton {
|
||||
RoundButton {
|
||||
anchors.horizontalCenterOffset: appIcon.isPlaceholder ? 47 : 0
|
||||
anchors.verticalCenterOffset: appIcon.isPlaceholder ? -75 : 60
|
||||
anchors.centerIn: parent
|
||||
implicitWidth: 125
|
||||
implicitHeight: 125
|
||||
implicitWidth: 85
|
||||
implicitHeight: 85
|
||||
|
||||
Image {
|
||||
source: "qrc:/res/baseline-cancel-24px.svg"
|
||||
source: "qrc:/res/stop_FILL1_wght700_GRAD200_opsz48.svg"
|
||||
anchors.centerIn: parent
|
||||
sourceSize {
|
||||
width: 75
|
||||
@@ -171,6 +174,8 @@ CenteredGridView {
|
||||
ToolTip.delay: 1000
|
||||
ToolTip.timeout: 3000
|
||||
ToolTip.visible: hovered
|
||||
|
||||
Material.background: "#D0808080"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -345,7 +350,7 @@ CenteredGridView {
|
||||
|
||||
function quitApp() {
|
||||
var component = Qt.createComponent("QuitSegue.qml")
|
||||
var params = {"appName": appName}
|
||||
var params = {"appName": appName, "quitRunningAppFn": appModel.quitRunningApp}
|
||||
if (segueToStream) {
|
||||
// Store the session and app name if we're going to stream after
|
||||
// successfully quitting the old app.
|
||||
@@ -358,9 +363,6 @@ CenteredGridView {
|
||||
}
|
||||
|
||||
stackView.push(component.createObject(stackView, params))
|
||||
|
||||
// Trigger the quit after pushing the quit segue on screen
|
||||
appModel.quitRunningApp()
|
||||
}
|
||||
|
||||
onAccepted: quitApp()
|
||||
|
||||
@@ -2,6 +2,7 @@ import QtQuick 2.9
|
||||
import QtQuick.Controls 2.2
|
||||
|
||||
import SdlGamepadKeyNavigation 1.0
|
||||
import SystemProperties 1.0
|
||||
|
||||
// https://stackoverflow.com/questions/45029968/how-do-i-set-the-combobox-width-to-fit-the-largest-item
|
||||
ComboBox {
|
||||
@@ -38,6 +39,12 @@ ComboBox {
|
||||
popup.onAboutToShow: {
|
||||
// Switch to normal navigation for combo boxes
|
||||
SdlGamepadKeyNavigation.setUiNavMode(false)
|
||||
|
||||
// Override the popup color to improve contrast with the overridden
|
||||
// Material 2 background color set in main.qml.
|
||||
if (SystemProperties.usesMaterial3Theme) {
|
||||
popup.background.color = "#424242"
|
||||
}
|
||||
}
|
||||
|
||||
popup.onAboutToHide: {
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
import QtQuick 2.0
|
||||
import QtQuick.Controls 2.2
|
||||
|
||||
import ComputerManager 1.0
|
||||
import SdlGamepadKeyNavigation 1.0
|
||||
|
||||
Item {
|
||||
function onSearchingComputer() {
|
||||
stageLabel.text = qsTr("Establishing connection to PC...")
|
||||
}
|
||||
|
||||
function onPairing(pcName, pin) {
|
||||
stageLabel.text = qsTr("Pairing... Please enter '%1' on %2.").arg(pin).arg(pcName)
|
||||
}
|
||||
|
||||
function onFailed(message) {
|
||||
stageIndicator.visible = false
|
||||
errorDialog.text = message
|
||||
errorDialog.open()
|
||||
}
|
||||
|
||||
function onSuccess(appName) {
|
||||
stageIndicator.visible = false
|
||||
pairCompleteDialog.open()
|
||||
}
|
||||
|
||||
// Allow user to back out of pairing
|
||||
Keys.onEscapePressed: {
|
||||
Qt.quit()
|
||||
}
|
||||
Keys.onBackPressed: {
|
||||
Qt.quit()
|
||||
}
|
||||
Keys.onCancelPressed: {
|
||||
Qt.quit()
|
||||
}
|
||||
|
||||
StackView.onActivated: {
|
||||
if (!launcher.isExecuted()) {
|
||||
toolBar.visible = false
|
||||
|
||||
// Normally this is enabled by PcView, but we will won't
|
||||
// load PcView when streaming from the command-line.
|
||||
SdlGamepadKeyNavigation.enable()
|
||||
|
||||
launcher.searchingComputer.connect(onSearchingComputer)
|
||||
launcher.pairing.connect(onPairing)
|
||||
launcher.failed.connect(onFailed)
|
||||
launcher.success.connect(onSuccess)
|
||||
launcher.execute(ComputerManager)
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: 5
|
||||
id: stageIndicator
|
||||
|
||||
BusyIndicator {
|
||||
id: stageSpinner
|
||||
}
|
||||
|
||||
Label {
|
||||
id: stageLabel
|
||||
height: stageSpinner.height
|
||||
font.pointSize: 20
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
}
|
||||
|
||||
ErrorMessageDialog {
|
||||
id: errorDialog
|
||||
|
||||
onClosed: {
|
||||
Qt.quit();
|
||||
}
|
||||
}
|
||||
|
||||
NavigableMessageDialog {
|
||||
id: pairCompleteDialog
|
||||
closePolicy: Popup.CloseOnEscape
|
||||
|
||||
text:qsTr("Pairing completed successfully")
|
||||
standardButtons: Dialog.Ok
|
||||
onClosed: {
|
||||
Qt.quit()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@ Item {
|
||||
function onLaunchFailed(message) {
|
||||
errorDialog.text = message
|
||||
errorDialog.open()
|
||||
console.error(message)
|
||||
}
|
||||
|
||||
function onAppQuitRequired(appName) {
|
||||
@@ -84,10 +85,8 @@ Item {
|
||||
|
||||
function quitApp() {
|
||||
var component = Qt.createComponent("QuitSegue.qml")
|
||||
var params = {"appName": appName}
|
||||
var params = {"appName": appName, "quitRunningAppFn": launcher.quitRunningApp}
|
||||
stackView.push(component.createObject(stackView, params))
|
||||
// Trigger the quit after pushing the quit segue on screen
|
||||
launcher.quitRunningApp()
|
||||
}
|
||||
|
||||
onAccepted: quitApp()
|
||||
|
||||
+26
-29
@@ -99,7 +99,7 @@ CenteredGridView {
|
||||
Label {
|
||||
height: searchSpinner.height
|
||||
elide: Label.ElideRight
|
||||
text: StreamingPreferences.enableMdns ? qsTr("Searching for PCs on your local network with NVIDIA GameStream enabled...")
|
||||
text: StreamingPreferences.enableMdns ? qsTr("Searching for compatible hosts on your local network...")
|
||||
: qsTr("Automatic PC discovery is disabled. Add your PC manually.")
|
||||
font.pointSize: 20
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
@@ -130,12 +130,12 @@ CenteredGridView {
|
||||
id: stateIcon
|
||||
anchors.horizontalCenter: pcIcon.horizontalCenter
|
||||
anchors.verticalCenter: pcIcon.verticalCenter
|
||||
anchors.verticalCenterOffset: -15
|
||||
anchors.verticalCenterOffset: !model.online ? -18 : -16
|
||||
visible: !model.statusUnknown && (!model.online || !model.paired)
|
||||
source: !model.online ? "qrc:/res/baseline-warning-24px.svg" : "qrc:/res/baseline-lock-24px.svg"
|
||||
source: !model.online ? "qrc:/res/warning_FILL1_wght300_GRAD200_opsz24.svg" : "qrc:/res/baseline-lock-24px.svg"
|
||||
sourceSize {
|
||||
width: 75
|
||||
height: 75
|
||||
width: !model.online ? 75 : 70
|
||||
height: !model.online ? 75 : 70
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,7 +211,7 @@ CenteredGridView {
|
||||
text: qsTr("Delete PC")
|
||||
onTriggered: {
|
||||
deletePcDialog.pcIndex = index
|
||||
// get confirmation first, actual closing is called from the dialog
|
||||
deletePcDialog.pcName = model.name
|
||||
deletePcDialog.open()
|
||||
}
|
||||
}
|
||||
@@ -232,22 +232,14 @@ CenteredGridView {
|
||||
stackView.push(appView)
|
||||
}
|
||||
else {
|
||||
if (!model.busy) {
|
||||
var pin = computerModel.generatePinString()
|
||||
var pin = computerModel.generatePinString()
|
||||
|
||||
// Kick off pairing in the background
|
||||
computerModel.pairComputer(index, pin)
|
||||
// Kick off pairing in the background
|
||||
computerModel.pairComputer(index, pin)
|
||||
|
||||
// Display the pairing dialog
|
||||
pairDialog.pin = pin
|
||||
pairDialog.open()
|
||||
}
|
||||
else {
|
||||
// cannot pair while something is streaming or attempting to pair
|
||||
errorDialog.text = qsTr("You cannot pair while a previous session is still running on the host PC. Quit any running games or reboot the host PC, then try pairing again.")
|
||||
errorDialog.helpText = ""
|
||||
errorDialog.open()
|
||||
}
|
||||
// Display the pairing dialog
|
||||
pairDialog.pin = pin
|
||||
pairDialog.open()
|
||||
}
|
||||
} else if (!model.online) {
|
||||
// Using open() here because it may be activated by keyboard
|
||||
@@ -279,6 +271,12 @@ CenteredGridView {
|
||||
// the ItemDelegate and not where the mouse cursor is
|
||||
pcContextMenu.open()
|
||||
}
|
||||
|
||||
Keys.onDeletePressed: {
|
||||
deletePcDialog.pcIndex = index
|
||||
deletePcDialog.pcName = model.name
|
||||
deletePcDialog.open()
|
||||
}
|
||||
}
|
||||
|
||||
ErrorMessageDialog {
|
||||
@@ -299,7 +297,8 @@ CenteredGridView {
|
||||
|
||||
// don't allow edits to the rest of the window while open
|
||||
property string pin : "0000"
|
||||
text:qsTr("Please enter %1 on your GameStream PC. This dialog will close when pairing is completed.").arg(pin)
|
||||
text:qsTr("Please enter %1 on your host PC. This dialog will close when pairing is completed.").arg(pin)+"\n\n"+
|
||||
qsTr("If your host PC is running Sunshine, navigate to the Sunshine web UI to enter the PIN.")
|
||||
standardButtons: Dialog.Cancel
|
||||
onRejected: {
|
||||
// FIXME: We should interrupt pairing here
|
||||
@@ -309,16 +308,14 @@ CenteredGridView {
|
||||
NavigableMessageDialog {
|
||||
id: deletePcDialog
|
||||
// don't allow edits to the rest of the window while open
|
||||
property int pcIndex : -1;
|
||||
text:qsTr("Are you sure you want to remove this PC?")
|
||||
property int pcIndex : -1
|
||||
property string pcName : ""
|
||||
text: qsTr("Are you sure you want to remove '%1'?").arg(pcName)
|
||||
standardButtons: Dialog.Yes | Dialog.No
|
||||
|
||||
function deletePc() {
|
||||
console.log("deleting PC pairing for PC at index: " + pcIndex)
|
||||
computerModel.deleteComputer(pcIndex);
|
||||
onAccepted: {
|
||||
computerModel.deleteComputer(pcIndex)
|
||||
}
|
||||
|
||||
onAccepted: deletePc()
|
||||
}
|
||||
|
||||
NavigableMessageDialog {
|
||||
@@ -327,7 +324,7 @@ CenteredGridView {
|
||||
standardButtons: Dialog.Ok
|
||||
|
||||
onAboutToShow: {
|
||||
testConnectionDialog.text = qsTr("Moonlight is testing your network connection to determine if NVIDIA GameStream is blocked.") + "\n\n" + qsTr("This may take a few seconds…")
|
||||
testConnectionDialog.text = qsTr("Moonlight is testing your network connection to determine if any required ports are blocked.") + "\n\n" + qsTr("This may take a few seconds…")
|
||||
showSpinner = true
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import Session 1.0
|
||||
|
||||
Item {
|
||||
property string appName
|
||||
property var quitRunningAppFn
|
||||
property Session nextSession : null
|
||||
property string nextAppName : ""
|
||||
|
||||
@@ -17,6 +18,7 @@ Item {
|
||||
if (error !== undefined) {
|
||||
errorDialog.text = error
|
||||
errorDialog.open()
|
||||
console.error(error)
|
||||
}
|
||||
|
||||
// If we're supposed to launch another game after this, do so now
|
||||
@@ -37,6 +39,11 @@ Item {
|
||||
|
||||
// Connect the quit completion signal
|
||||
ComputerManager.quitAppCompleted.connect(quitAppCompleted)
|
||||
|
||||
// Start the quit operation if requested
|
||||
if (quitRunningAppFn) {
|
||||
quitRunningAppFn()
|
||||
}
|
||||
}
|
||||
|
||||
StackView.onDeactivating: {
|
||||
|
||||
+434
-171
@@ -1,6 +1,7 @@
|
||||
import QtQuick 2.9
|
||||
import QtQuick.Controls 2.2
|
||||
import QtQuick.Layouts 1.2
|
||||
import QtQuick.Window 2.2
|
||||
|
||||
import StreamingPreferences 1.0
|
||||
import ComputerManager 1.0
|
||||
@@ -11,6 +12,8 @@ Flickable {
|
||||
id: settingsPage
|
||||
objectName: qsTr("Settings")
|
||||
|
||||
signal languageChanged()
|
||||
|
||||
boundsBehavior: Flickable.OvershootBounds
|
||||
|
||||
contentWidth: settingsColumn1.width > settingsColumn2.width ? settingsColumn1.width : settingsColumn2.width
|
||||
@@ -23,6 +26,49 @@ Flickable {
|
||||
}
|
||||
}
|
||||
|
||||
function isChildOfFlickable(item) {
|
||||
while (item) {
|
||||
if (item.parent === contentItem) {
|
||||
return true
|
||||
}
|
||||
|
||||
item = item.parent
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
NumberAnimation on contentY {
|
||||
id: autoScrollAnimation
|
||||
duration: 100
|
||||
}
|
||||
|
||||
Window.onActiveFocusItemChanged: {
|
||||
var item = Window.activeFocusItem
|
||||
if (item) {
|
||||
// Ignore non-child elements like the toolbar buttons
|
||||
if (!isChildOfFlickable(item)) {
|
||||
return
|
||||
}
|
||||
|
||||
// Map the focus item's position into our content item's coordinate space
|
||||
var pos = item.mapToItem(contentItem, 0, 0)
|
||||
|
||||
// Ensure some extra space is visible around the element we're scrolling to
|
||||
var scrollMargin = height > 100 ? 50 : 0
|
||||
|
||||
if (pos.y - scrollMargin < contentY) {
|
||||
autoScrollAnimation.from = contentY
|
||||
autoScrollAnimation.to = Math.max(pos.y - scrollMargin, 0)
|
||||
autoScrollAnimation.start()
|
||||
}
|
||||
else if (pos.y + item.height + scrollMargin > contentY + height) {
|
||||
autoScrollAnimation.from = contentY
|
||||
autoScrollAnimation.to = Math.min(pos.y + item.height + scrollMargin - height, contentHeight - height)
|
||||
autoScrollAnimation.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StackView.onActivated: {
|
||||
// This enables Tab and BackTab based navigation rather than arrow keys.
|
||||
// It is required to shift focus between controls on the settings page.
|
||||
@@ -95,50 +141,39 @@ Flickable {
|
||||
// Add native resolutions for all attached displays
|
||||
var done = false
|
||||
for (var displayIndex = 0; !done; displayIndex++) {
|
||||
for (var displayResIndex = 0; displayResIndex < 2; displayResIndex++) {
|
||||
var screenRect;
|
||||
var screenRect = SystemProperties.getNativeResolution(displayIndex);
|
||||
|
||||
// Some platforms have different desktop resolutions
|
||||
// and native resolutions (like macOS with Retina displays)
|
||||
if (displayResIndex === 0) {
|
||||
screenRect = SystemProperties.getDesktopResolution(displayIndex)
|
||||
}
|
||||
else {
|
||||
screenRect = SystemProperties.getNativeResolution(displayIndex)
|
||||
}
|
||||
if (screenRect.width === 0) {
|
||||
// Exceeded max count of displays
|
||||
done = true
|
||||
break
|
||||
}
|
||||
|
||||
if (screenRect.width === 0) {
|
||||
// Exceeded max count of displays
|
||||
done = true
|
||||
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 (screenRect.width === existing_width && screenRect.height === existing_height) {
|
||||
// Duplicate entry, skip
|
||||
indexToAdd = -1
|
||||
break
|
||||
}
|
||||
|
||||
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 (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
|
||||
}
|
||||
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
|
||||
})
|
||||
}
|
||||
// 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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -345,7 +380,7 @@ Flickable {
|
||||
maximumLength: 5
|
||||
inputMethodHints: Qt.ImhDigitsOnly
|
||||
placeholderText: resolutionListModel.get(resolutionComboBox.currentIndex).video_width
|
||||
validator: IntValidator{bottom:128; top:8192}
|
||||
validator: IntValidator{bottom:256; top:8192}
|
||||
focus: true
|
||||
|
||||
onTextChanged: {
|
||||
@@ -374,7 +409,7 @@ Flickable {
|
||||
maximumLength: 5
|
||||
inputMethodHints: Qt.ImhDigitsOnly
|
||||
placeholderText: resolutionListModel.get(resolutionComboBox.currentIndex).video_height
|
||||
validator: IntValidator{bottom:128; top:8192}
|
||||
validator: IntValidator{bottom:256; top:8192}
|
||||
|
||||
onTextChanged: {
|
||||
// standardButton() was added in Qt 5.10, so we must check for it first
|
||||
@@ -397,84 +432,11 @@ Flickable {
|
||||
}
|
||||
|
||||
AutoResizingComboBox {
|
||||
function createModel() {
|
||||
var fpsListModel = Qt.createQmlObject('import QtQuick 2.0; ListModel {}', parent, '')
|
||||
|
||||
var max_fps = SystemProperties.maximumStreamingFrameRate
|
||||
|
||||
// Default entries
|
||||
fpsListModel.append({"text": qsTr("%1 FPS").arg("30"), "video_fps": "30"})
|
||||
fpsListModel.append({"text": qsTr("%1 FPS").arg("60"), "video_fps": "60"})
|
||||
|
||||
// Add unsupported FPS values that come before the display max FPS
|
||||
if (StreamingPreferences.unsupportedFps) {
|
||||
if (max_fps > 90) {
|
||||
fpsListModel.append({"text": qsTr("%1 FPS (Unsupported)").arg("90"), "video_fps": "90"})
|
||||
}
|
||||
if (max_fps > 120) {
|
||||
fpsListModel.append({"text": qsTr("%1 FPS (Unsupported)").arg("120"), "video_fps": "120"})
|
||||
}
|
||||
}
|
||||
|
||||
// Use 64 as the cutoff for adding a separate option to
|
||||
// handle wonky displays that report just over 60 Hz.
|
||||
if (max_fps > 64) {
|
||||
// Mark any FPS value greater than 120 as unsupported
|
||||
if (StreamingPreferences.unsupportedFps && max_fps > 120) {
|
||||
fpsListModel.append({"text": qsTr("%1 FPS (Unsupported)").arg(max_fps), "video_fps": ""+max_fps})
|
||||
}
|
||||
else if (max_fps > 120) {
|
||||
fpsListModel.append({"text": qsTr("%1 FPS").arg("120"), "video_fps": "120"})
|
||||
}
|
||||
else {
|
||||
fpsListModel.append({"text": qsTr("%1 FPS").arg(max_fps), "video_fps": ""+max_fps})
|
||||
}
|
||||
}
|
||||
|
||||
// Add unsupported FPS values that come after the display max FPS
|
||||
if (StreamingPreferences.unsupportedFps) {
|
||||
if (max_fps < 90) {
|
||||
fpsListModel.append({"text":qsTr("%1 FPS (Unsupported)").arg("90"), "video_fps": "90"})
|
||||
}
|
||||
if (max_fps < 120) {
|
||||
fpsListModel.append({"text":qsTr("%1 FPS (Unsupported)").arg("120"), "video_fps": "120"})
|
||||
}
|
||||
}
|
||||
|
||||
return fpsListModel
|
||||
}
|
||||
|
||||
function reinitialize() {
|
||||
model = createModel()
|
||||
|
||||
var saved_fps = StreamingPreferences.fps
|
||||
currentIndex = 0
|
||||
for (var i = 0; i < model.count; i++) {
|
||||
var el_fps = parseInt(model.get(i).video_fps);
|
||||
|
||||
// Pick the highest value lesser or equal to the saved FPS
|
||||
if (saved_fps >= el_fps) {
|
||||
currentIndex = i
|
||||
}
|
||||
}
|
||||
|
||||
// Persist the selected value
|
||||
activated(currentIndex)
|
||||
}
|
||||
|
||||
// ignore setting the index at first, and actually set it when the component is loaded
|
||||
Component.onCompleted: {
|
||||
reinitialize()
|
||||
}
|
||||
|
||||
id: fpsComboBox
|
||||
maximumWidth: parent.width / 2
|
||||
textRole: "text"
|
||||
// ::onActivated must be used, as it only listens for when the index is changed by a human
|
||||
onActivated : {
|
||||
var selectedFps = parseInt(model.get(currentIndex).video_fps)
|
||||
property int lastIndexValue
|
||||
|
||||
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
|
||||
|
||||
@@ -483,6 +445,211 @@ Flickable {
|
||||
StreamingPreferences.fps);
|
||||
slider.value = StreamingPreferences.bitrateKbps
|
||||
}
|
||||
|
||||
lastIndexValue = currentIndex
|
||||
}
|
||||
|
||||
NavigableDialog {
|
||||
function isInputValid() {
|
||||
// If we have text that isn't valid, reject the input.
|
||||
if (!fpsField.acceptableInput && fpsField.text) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
|
||||
onClosed: {
|
||||
fpsField.clear()
|
||||
}
|
||||
|
||||
onRejected: {
|
||||
fpsComboBox.currentIndex = fpsComboBox.lastIndexValue
|
||||
}
|
||||
|
||||
onAccepted: {
|
||||
// Reject if there's invalid input
|
||||
if (!isInputValid()) {
|
||||
reject()
|
||||
return
|
||||
}
|
||||
|
||||
var fps = fpsField.text ? fpsField.text : fpsField.placeholderText
|
||||
|
||||
// Find and update the custom entry
|
||||
for (var i = 0; i < fpsListModel.count; i++) {
|
||||
if (fpsListModel.get(i).is_custom) {
|
||||
fpsListModel.setProperty(i, "video_fps", fps)
|
||||
fpsListModel.setProperty(i, "text", qsTr("Custom (%1 FPS)").arg(fps))
|
||||
|
||||
// Now update the bitrate using the custom resolution
|
||||
fpsComboBox.currentIndex = i
|
||||
fpsComboBox.updateBitrateForSelection()
|
||||
|
||||
// Update the combobox width too
|
||||
fpsComboBox.recalculateWidth()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Label {
|
||||
text: qsTr("Enter a custom frame rate:")
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// 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() {
|
||||
// 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 found = false
|
||||
for (var i = 0; i < model.count; i++) {
|
||||
var el_fps = parseInt(model.get(i).video_fps);
|
||||
|
||||
// Look for a matching frame rate
|
||||
if (saved_fps === el_fps) {
|
||||
currentIndex = i
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// If we didn't find one, add a custom frame rate for the current value
|
||||
if (!found) {
|
||||
currentIndex = addRefreshRateOrdered(model, saved_fps, qsTr("Custom (%1 FPS)").arg(saved_fps), true)
|
||||
}
|
||||
else {
|
||||
addRefreshRateOrdered(model, "", qsTr("Custom"), true)
|
||||
}
|
||||
|
||||
recalculateWidth()
|
||||
|
||||
lastIndexValue = currentIndex
|
||||
}
|
||||
|
||||
// ignore setting the index at first, and actually set it when the component is loaded
|
||||
Component.onCompleted: {
|
||||
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
|
||||
maximumWidth: parent.width / 2
|
||||
textRole: "text"
|
||||
// ::onActivated must be used, as it only listens for when the index is changed by a human
|
||||
onActivated : {
|
||||
if (model.get(currentIndex).is_custom) {
|
||||
customFpsDialog.open()
|
||||
}
|
||||
else {
|
||||
updateBitrateForSelection()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -519,6 +686,11 @@ Flickable {
|
||||
bitrateTitle.text = qsTr("Video bitrate: %1 Mbps").arg(value / 1000.0)
|
||||
StreamingPreferences.bitrateKbps = value
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
// Refresh the text after translations change
|
||||
languageChanged.connect(onValueChanged)
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
@@ -531,28 +703,53 @@ Flickable {
|
||||
}
|
||||
|
||||
AutoResizingComboBox {
|
||||
// ignore setting the index at first, and actually set it when the component is loaded
|
||||
Component.onCompleted: {
|
||||
function createModel() {
|
||||
var model = Qt.createQmlObject('import QtQuick 2.0; ListModel {}', parent, '')
|
||||
|
||||
model.append({
|
||||
text: qsTr("Fullscreen"),
|
||||
val: StreamingPreferences.WM_FULLSCREEN
|
||||
})
|
||||
|
||||
model.append({
|
||||
text: qsTr("Borderless windowed"),
|
||||
val: StreamingPreferences.WM_FULLSCREEN_DESKTOP
|
||||
})
|
||||
|
||||
model.append({
|
||||
text: qsTr("Windowed"),
|
||||
val: StreamingPreferences.WM_WINDOWED
|
||||
})
|
||||
|
||||
|
||||
// Set the recommended option based on the OS
|
||||
for (var i = 0; i < model.count; i++) {
|
||||
var thisWm = model.get(i).val;
|
||||
if (thisWm === StreamingPreferences.recommendedFullScreenMode) {
|
||||
model.get(i).text += " " + qsTr("(Recommended)")
|
||||
model.move(i, 0, 1)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return model
|
||||
}
|
||||
|
||||
|
||||
// This is used on initialization and upon retranslation
|
||||
function reinitialize() {
|
||||
if (!visible) {
|
||||
// Do nothing if the control won't even be visible
|
||||
return
|
||||
}
|
||||
|
||||
// Set the recommended option based on the OS
|
||||
for (var i = 0; i < windowModeListModel.count; i++) {
|
||||
var thisWm = windowModeListModel.get(i).val;
|
||||
if (thisWm === StreamingPreferences.recommendedFullScreenMode) {
|
||||
windowModeListModel.get(i).text += qsTr(" (Recommended)")
|
||||
windowModeListModel.move(i, 0, 1);
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
model = createModel()
|
||||
currentIndex = 0
|
||||
|
||||
// Set the current value based on the saved preferences
|
||||
var savedWm = StreamingPreferences.windowMode
|
||||
for (var i = 0; i < windowModeListModel.count; i++) {
|
||||
var thisWm = windowModeListModel.get(i).val;
|
||||
for (var i = 0; i < model.count; i++) {
|
||||
var thisWm = model.get(i).val;
|
||||
if (savedWm === thisWm) {
|
||||
currentIndex = i
|
||||
break
|
||||
@@ -562,28 +759,18 @@ Flickable {
|
||||
activated(currentIndex)
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
reinitialize()
|
||||
languageChanged.connect(reinitialize)
|
||||
}
|
||||
|
||||
id: windowModeComboBox
|
||||
visible: SystemProperties.hasDesktopEnvironment
|
||||
enabled: !SystemProperties.rendererAlwaysFullScreen
|
||||
hoverEnabled: true
|
||||
textRole: "text"
|
||||
model: ListModel {
|
||||
id: windowModeListModel
|
||||
ListElement {
|
||||
text: qsTr("Fullscreen")
|
||||
val: StreamingPreferences.WM_FULLSCREEN
|
||||
}
|
||||
ListElement {
|
||||
text: qsTr("Borderless windowed")
|
||||
val: StreamingPreferences.WM_FULLSCREEN_DESKTOP
|
||||
}
|
||||
ListElement {
|
||||
text: qsTr("Windowed")
|
||||
val: StreamingPreferences.WM_WINDOWED
|
||||
}
|
||||
}
|
||||
onActivated: {
|
||||
StreamingPreferences.windowMode = windowModeListModel.get(currentIndex).val
|
||||
StreamingPreferences.windowMode = model.get(currentIndex).val
|
||||
}
|
||||
|
||||
ToolTip.delay: 1000
|
||||
@@ -830,9 +1017,41 @@ Flickable {
|
||||
val: StreamingPreferences.LANG_UK
|
||||
} */
|
||||
ListElement {
|
||||
text: "繁体字" // Traditional Chinese
|
||||
val: StreamingPreferences.LANG_ZH_HANT
|
||||
text: "繁體中文" // Traditional Chinese
|
||||
val: StreamingPreferences.LANG_ZH_TW
|
||||
}
|
||||
ListElement {
|
||||
text: "Português" // Portuguese
|
||||
val: StreamingPreferences.LANG_PT
|
||||
}
|
||||
/* ListElement {
|
||||
text: "Português do Brasil" // Brazilian Portuguese
|
||||
val: StreamingPreferences.LANG_PT_BR
|
||||
} */
|
||||
ListElement {
|
||||
text: "Ελληνικά" // Greek
|
||||
val: StreamingPreferences.LANG_EL
|
||||
}
|
||||
ListElement {
|
||||
text: "Italiano" // Italian
|
||||
val: StreamingPreferences.LANG_IT
|
||||
}
|
||||
/* ListElement {
|
||||
text: "हिन्दी, हिंदी" // Hindi
|
||||
val: StreamingPreferences.LANG_HI
|
||||
} */
|
||||
ListElement {
|
||||
text: "Język polski" // Polish
|
||||
val: StreamingPreferences.LANG_PL
|
||||
}
|
||||
ListElement {
|
||||
text: "Čeština" // Czech
|
||||
val: StreamingPreferences.LANG_CS
|
||||
}
|
||||
/* ListElement {
|
||||
text: "עִבְרִית" // Hebrew
|
||||
val: StreamingPreferences.LANG_HE
|
||||
} */
|
||||
}
|
||||
// ::onActivated must be used, as it only listens for when the index is changed by a human
|
||||
onActivated : {
|
||||
@@ -847,6 +1066,9 @@ Flickable {
|
||||
// Force the back operation to pop any AppView pages that exist.
|
||||
// The AppView stops working after retranslate() for some reason.
|
||||
window.clearOnBack = true
|
||||
|
||||
// Signal other controls to adjust their text
|
||||
languageChanged()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -933,6 +1155,22 @@ Flickable {
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.text: qsTr("Updates your Discord status to display the name of the game you're streaming.")
|
||||
}
|
||||
|
||||
CheckBox {
|
||||
id: keepAwakeCheck
|
||||
width: parent.width
|
||||
text: qsTr("Keep the display awake while streaming")
|
||||
font.pointSize: 12
|
||||
checked: StreamingPreferences.keepAwake
|
||||
onCheckedChanged: {
|
||||
StreamingPreferences.keepAwake = checked
|
||||
}
|
||||
|
||||
ToolTip.delay: 1000
|
||||
ToolTip.timeout: 5000
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.text: qsTr("Prevents the screensaver from starting or the display from going to sleep while streaming.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1252,6 +1490,7 @@ Flickable {
|
||||
|
||||
id: decoderComboBox
|
||||
textRole: "text"
|
||||
enabled: !enableHdr.checked
|
||||
model: ListModel {
|
||||
id: decoderListModel
|
||||
ListElement {
|
||||
@@ -1268,9 +1507,26 @@ Flickable {
|
||||
}
|
||||
}
|
||||
// ::onActivated must be used, as it only listens for when the index is changed by a human
|
||||
onActivated : {
|
||||
StreamingPreferences.videoDecoderSelection = decoderListModel.get(currentIndex).val
|
||||
onActivated: {
|
||||
if (enabled) {
|
||||
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 {
|
||||
@@ -1284,15 +1540,12 @@ Flickable {
|
||||
AutoResizingComboBox {
|
||||
// ignore setting the index at first, and actually set it when the component is loaded
|
||||
Component.onCompleted: {
|
||||
if (SystemProperties.supportsHdr) {
|
||||
codecListModel.append({
|
||||
"text": qsTr("HEVC HDR (Experimental)"),
|
||||
"val": StreamingPreferences.VCC_FORCE_HEVC_HDR
|
||||
});
|
||||
}
|
||||
|
||||
var saved_vcc = StreamingPreferences.videoCodecConfig
|
||||
|
||||
// Default to Automatic (relevant if HDR is enabled,
|
||||
// where we will match none of the codecs in the list)
|
||||
currentIndex = 0
|
||||
|
||||
for(var i = 0; i < codecListModel.count; i++) {
|
||||
var el_vcc = codecListModel.get(i).val;
|
||||
if (saved_vcc === el_vcc) {
|
||||
@@ -1300,6 +1553,7 @@ Flickable {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
activated(currentIndex)
|
||||
}
|
||||
|
||||
@@ -1319,31 +1573,40 @@ Flickable {
|
||||
text: qsTr("HEVC (H.265)")
|
||||
val: StreamingPreferences.VCC_FORCE_HEVC
|
||||
}
|
||||
/* HEVC HDR will be appended here */
|
||||
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 : {
|
||||
StreamingPreferences.videoCodecConfig = codecListModel.get(currentIndex).val
|
||||
if (enabled) {
|
||||
StreamingPreferences.videoCodecConfig = codecListModel.get(currentIndex).val
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CheckBox {
|
||||
id: unlockUnsupportedFps
|
||||
id: enableHdr
|
||||
width: parent.width
|
||||
text: qsTr("Unlock unsupported FPS options")
|
||||
text: qsTr("Enable HDR (Experimental)")
|
||||
font.pointSize: 12
|
||||
checked: StreamingPreferences.unsupportedFps
|
||||
onCheckedChanged: {
|
||||
// This is called on init, so only do the work if we've
|
||||
// actually changed the value.
|
||||
if (StreamingPreferences.unsupportedFps != checked) {
|
||||
StreamingPreferences.unsupportedFps = checked
|
||||
|
||||
// The selectable FPS values depend on whether
|
||||
// this option is enabled or not
|
||||
fpsComboBox.reinitialize()
|
||||
}
|
||||
enabled: SystemProperties.supportsHdr
|
||||
checked: enabled && StreamingPreferences.enableHdr
|
||||
onCheckedChanged: {
|
||||
StreamingPreferences.enableHdr = checked
|
||||
}
|
||||
|
||||
// Updating StreamingPreferences.videoCodecConfig is handled above
|
||||
|
||||
ToolTip.delay: 1000
|
||||
ToolTip.timeout: 5000
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.text: enabled ?
|
||||
qsTr("The stream will be HDR-capable, but some games may require an HDR monitor on your host PC to enable HDR mode.")
|
||||
:
|
||||
qsTr("HDR streaming is not supported on this PC.")
|
||||
}
|
||||
|
||||
CheckBox {
|
||||
|
||||
@@ -45,6 +45,7 @@ Item {
|
||||
{
|
||||
// Display the error dialog after Session::exec() returns
|
||||
streamSegueErrorDialog.text = text
|
||||
console.error(text)
|
||||
}
|
||||
|
||||
function displayLaunchWarning(text)
|
||||
@@ -56,6 +57,7 @@ Item {
|
||||
toast.text = text
|
||||
toast.timeout = 3000
|
||||
toast.visible = true
|
||||
console.warn(text)
|
||||
}
|
||||
|
||||
function quitStarting()
|
||||
@@ -103,7 +105,10 @@ Item {
|
||||
streamSegueErrorDialog.open()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function sessionReadyForDeletion()
|
||||
{
|
||||
// Garbage collect the Session object since it's pretty heavyweight
|
||||
// and keeps other libraries (like SDL_TTF) around until it is deleted.
|
||||
session = null
|
||||
@@ -130,6 +135,7 @@ Item {
|
||||
session.displayLaunchWarning.connect(displayLaunchWarning)
|
||||
session.quitStarting.connect(quitStarting)
|
||||
session.sessionFinished.connect(sessionFinished)
|
||||
session.readyForDeletion.connect(sessionReadyForDeletion)
|
||||
|
||||
// Kick off the stream
|
||||
spinnerTimer.start()
|
||||
|
||||
+11
-19
@@ -2,8 +2,6 @@
|
||||
|
||||
#include <QThreadPool>
|
||||
|
||||
#include <random>
|
||||
|
||||
ComputerModel::ComputerModel(QObject* object)
|
||||
: QAbstractListModel(object) {}
|
||||
|
||||
@@ -140,14 +138,9 @@ void ComputerModel::renameComputer(int computerIndex, QString name)
|
||||
m_ComputerManager->renameHost(m_Computers[computerIndex], name);
|
||||
}
|
||||
|
||||
// TODO: Use QRandomGenerator when we drop Qt 5.9 support
|
||||
QString ComputerModel::generatePinString()
|
||||
{
|
||||
std::uniform_int_distribution<int> dist(0, 9999);
|
||||
std::random_device rd;
|
||||
std::mt19937 engine(rd());
|
||||
|
||||
return QString::asprintf("%04u", dist(engine));
|
||||
return m_ComputerManager->generatePinString();
|
||||
}
|
||||
|
||||
class DeferredTestConnectionTask : public QObject, public QRunnable
|
||||
@@ -193,19 +186,18 @@ void ComputerModel::handlePairingCompleted(NvComputer*, QString error)
|
||||
|
||||
void ComputerModel::handleComputerStateChanged(NvComputer* computer)
|
||||
{
|
||||
// If this is an existing computer, we can report the data changed
|
||||
int index = m_Computers.indexOf(computer);
|
||||
if (index >= 0) {
|
||||
// Let the view know that this specific computer changed
|
||||
emit dataChanged(createIndex(index, 0), createIndex(index, 0));
|
||||
QVector<NvComputer*> newComputerList = m_ComputerManager->getComputers();
|
||||
|
||||
// Reset the model if the structural layout of the list has changed
|
||||
if (m_Computers != newComputerList) {
|
||||
beginResetModel();
|
||||
m_Computers = newComputerList;
|
||||
endResetModel();
|
||||
}
|
||||
else {
|
||||
// This is a new PC which may be inserted at an arbitrary point
|
||||
// in our computer list (since it comes from CM's QMap). Reload
|
||||
// the whole model state to ensure it stays consistent.
|
||||
beginResetModel();
|
||||
m_Computers = m_ComputerManager->getComputers();
|
||||
endResetModel();
|
||||
// Let the view know that this specific computer changed
|
||||
int index = m_Computers.indexOf(computer);
|
||||
emit dataChanged(createIndex(index, 0), createIndex(index, 0));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+28
-8
@@ -2,6 +2,7 @@ import QtQuick 2.9
|
||||
import QtQuick.Controls 2.2
|
||||
import QtQuick.Layouts 1.3
|
||||
import QtQuick.Window 2.2
|
||||
import QtQuick.Controls.Material 2.2
|
||||
|
||||
import ComputerManager 1.0
|
||||
import AutoUpdateChecker 1.0
|
||||
@@ -22,6 +23,15 @@ ApplicationWindow {
|
||||
width: 1280
|
||||
height: 600
|
||||
|
||||
// Override the background color to Material 2 colors for Qt 6.5+
|
||||
// in order to improve contrast between GFE's placeholder box art
|
||||
// and the background of the app grid.
|
||||
Component.onCompleted: {
|
||||
if (SystemProperties.usesMaterial3Theme) {
|
||||
Material.background = "#303030"
|
||||
}
|
||||
}
|
||||
|
||||
visibility: {
|
||||
if (SystemProperties.hasDesktopEnvironment) {
|
||||
if (StreamingPreferences.uiDisplayMode == StreamingPreferences.UI_WINDOWED) return "Windowed"
|
||||
@@ -181,10 +191,20 @@ ApplicationWindow {
|
||||
}
|
||||
}
|
||||
|
||||
function navigateTo(url, objectName)
|
||||
// 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
|
||||
function qmltypeof(obj, className) { // QtObject, string -> bool
|
||||
// className plus "(" is the class instance without modification
|
||||
// className plus "_QML" is the class instance with user-defined properties
|
||||
var str = obj.toString();
|
||||
return str.startsWith(className + "(") || str.startsWith(className + "_QML");
|
||||
}
|
||||
|
||||
function navigateTo(url, objectType)
|
||||
{
|
||||
var existingItem = stackView.find(function(item, index) {
|
||||
return item.objectName === objectName
|
||||
return qmltypeof(item, objectType)
|
||||
})
|
||||
|
||||
if (existingItem !== null) {
|
||||
@@ -251,7 +271,7 @@ ApplicationWindow {
|
||||
|
||||
Label {
|
||||
id: versionLabel
|
||||
visible: stackView.currentItem.objectName === qsTr("Settings")
|
||||
visible: qmltypeof(stackView.currentItem, "SettingsView")
|
||||
text: qsTr("Version %1").arg(SystemProperties.versionString)
|
||||
font.pointSize: 12
|
||||
horizontalAlignment: Qt.AlignRight
|
||||
@@ -261,7 +281,7 @@ ApplicationWindow {
|
||||
NavigableToolButton {
|
||||
id: discordButton
|
||||
visible: SystemProperties.hasBrowser &&
|
||||
stackView.currentItem.objectName === qsTr("Settings")
|
||||
qmltypeof(stackView.currentItem, "SettingsView")
|
||||
|
||||
iconSource: "qrc:/res/Discord-Logo-White.svg"
|
||||
|
||||
@@ -280,7 +300,7 @@ ApplicationWindow {
|
||||
|
||||
NavigableToolButton {
|
||||
id: addPcButton
|
||||
visible: stackView.currentItem.objectName === qsTr("Computers")
|
||||
visible: qmltypeof(stackView.currentItem, "PcView")
|
||||
|
||||
iconSource: "qrc:/res/ic_add_to_queue_white_48px.svg"
|
||||
|
||||
@@ -378,7 +398,7 @@ ApplicationWindow {
|
||||
|
||||
iconSource: "qrc:/res/ic_videogame_asset_white_48px.svg"
|
||||
|
||||
onClicked: navigateTo("qrc:/gui/GamepadMapper.qml", qsTr("Gamepad Mapping"))
|
||||
onClicked: navigateTo("qrc:/gui/GamepadMapper.qml", "GamepadMapper")
|
||||
|
||||
Keys.onDownPressed: {
|
||||
stackView.currentItem.forceActiveFocus(Qt.TabFocus)
|
||||
@@ -390,7 +410,7 @@ ApplicationWindow {
|
||||
|
||||
iconSource: "qrc:/res/settings.svg"
|
||||
|
||||
onClicked: navigateTo("qrc:/gui/SettingsView.qml", qsTr("Settings"))
|
||||
onClicked: navigateTo("qrc:/gui/SettingsView.qml", "SettingsView")
|
||||
|
||||
Keys.onDownPressed: {
|
||||
stackView.currentItem.forceActiveFocus(Qt.TabFocus)
|
||||
@@ -478,7 +498,7 @@ ApplicationWindow {
|
||||
|
||||
NavigableDialog {
|
||||
id: addPcDialog
|
||||
property string label: qsTr("Enter the IP address of your GameStream PC:")
|
||||
property string label: qsTr("Enter the IP address of your host PC:")
|
||||
|
||||
standardButtons: Dialog.Ok | Dialog.Cancel
|
||||
|
||||
|
||||
@@ -191,7 +191,17 @@ void SdlGamepadKeyNavigation::onPollingTimerFired()
|
||||
case SDL_CONTROLLERDEVICEADDED:
|
||||
SDL_GameController* gc = SDL_GameControllerOpen(event.cdevice.which);
|
||||
if (gc != nullptr) {
|
||||
m_Gamepads.append(gc);
|
||||
// SDL_CONTROLLERDEVICEADDED can be reported multiple times for the same
|
||||
// gamepad in rare cases, because SDL doesn't fixup the device index in
|
||||
// the SDL_CONTROLLERDEVICEADDED event if an unopened gamepad disappears
|
||||
// before we've processed the add event.
|
||||
if (!m_Gamepads.contains(gc)) {
|
||||
m_Gamepads.append(gc);
|
||||
}
|
||||
else {
|
||||
// We already have this game controller open
|
||||
SDL_GameControllerClose(gc);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -239,31 +249,6 @@ void SdlGamepadKeyNavigation::onPollingTimerFired()
|
||||
sendKey(QEvent::Type::KeyRelease, Qt::Key_Right);
|
||||
m_LastAxisNavigationEventTime = SDL_GetTicks();
|
||||
}
|
||||
|
||||
// In UI navigation mode (settings page), use the right stick to scroll
|
||||
if (m_UiNavMode) {
|
||||
short rightX = SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_RIGHTX);
|
||||
short rightY = SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_RIGHTY);
|
||||
|
||||
QPoint wheelDelta;
|
||||
if (rightX > 30000) {
|
||||
wheelDelta.setX(30);
|
||||
}
|
||||
else if (rightX < -30000) {
|
||||
wheelDelta.setX(-30);
|
||||
}
|
||||
|
||||
if (rightY > 30000) {
|
||||
wheelDelta.setY(-30);
|
||||
}
|
||||
else if (rightY < -30000) {
|
||||
wheelDelta.setY(30);
|
||||
}
|
||||
|
||||
if (!wheelDelta.isNull()) {
|
||||
sendWheel(wheelDelta);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -277,22 +262,6 @@ void SdlGamepadKeyNavigation::sendKey(QEvent::Type type, Qt::Key key, Qt::Keyboa
|
||||
}
|
||||
}
|
||||
|
||||
void SdlGamepadKeyNavigation::sendWheel(QPoint& angleDelta)
|
||||
{
|
||||
QGuiApplication* app = static_cast<QGuiApplication*>(QGuiApplication::instance());
|
||||
QWindow* focusWindow = app->focusWindow();
|
||||
if (focusWindow != nullptr) {
|
||||
QPoint mousePos(focusWindow->width() / 2, focusWindow->height() / 2);
|
||||
QPoint globalPos(focusWindow->mapToGlobal(mousePos));
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 12, 0)
|
||||
QWheelEvent wheelEvent(mousePos, globalPos, QPoint(), angleDelta, Qt::NoButton, Qt::NoModifier, Qt::NoScrollPhase, false, Qt::MouseEventSynthesizedByApplication);
|
||||
#else
|
||||
QWheelEvent wheelEvent(mousePos, globalPos, QPoint(), angleDelta, angleDelta.y(), Qt::Vertical, Qt::NoButton, Qt::NoModifier, Qt::NoScrollPhase, Qt::MouseEventSynthesizedByApplication, false);
|
||||
#endif
|
||||
app->sendEvent(focusWindow, &wheelEvent);
|
||||
}
|
||||
}
|
||||
|
||||
void SdlGamepadKeyNavigation::setUiNavMode(bool uiNavMode)
|
||||
{
|
||||
m_UiNavMode = uiNavMode;
|
||||
|
||||
@@ -24,7 +24,6 @@ public:
|
||||
|
||||
private:
|
||||
void sendKey(QEvent::Type type, Qt::Key key, Qt::KeyboardModifiers modifiers = Qt::NoModifier);
|
||||
void sendWheel(QPoint& angleDelta);
|
||||
|
||||
private slots:
|
||||
void onPollingTimerFired();
|
||||
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+442
-348
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.
+372
-278
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+392
-333
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+643
-541
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.
+367
-273
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+377
-283
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+393
-299
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+370
-276
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+377
-283
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+508
-406
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+368
-270
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+371
-269
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+427
-325
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+370
-276
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.
+159
-51
@@ -22,20 +22,23 @@
|
||||
#include "streaming/video/ffmpeg.h"
|
||||
#endif
|
||||
|
||||
#if defined(Q_OS_WIN32) && defined(Q_PROCESSOR_X86)
|
||||
#if defined(Q_OS_WIN32)
|
||||
#include "antihookingprotection.h"
|
||||
#elif defined(Q_OS_LINUX)
|
||||
#include <openssl/ssl.h>
|
||||
#endif
|
||||
|
||||
#include "cli/listapps.h"
|
||||
#include "cli/quitstream.h"
|
||||
#include "cli/startstream.h"
|
||||
#include "cli/pair.h"
|
||||
#include "cli/commandlineparser.h"
|
||||
#include "path.h"
|
||||
#include "utils.h"
|
||||
#include "gui/computermodel.h"
|
||||
#include "gui/appmodel.h"
|
||||
#include "backend/autoupdatechecker.h"
|
||||
#include "backend/computermanager.h"
|
||||
#include "backend/systemproperties.h"
|
||||
#include "streaming/session.h"
|
||||
#include "settings/streamingpreferences.h"
|
||||
@@ -60,9 +63,11 @@
|
||||
static QElapsedTimer s_LoggerTime;
|
||||
static QTextStream s_LoggerStream(stdout);
|
||||
static QMutex s_LoggerLock;
|
||||
static bool s_SuppressVerboseOutput;
|
||||
#ifdef LOG_TO_FILE
|
||||
#define MAX_LOG_LINES 10000
|
||||
static int s_LogLinesWritten = 0;
|
||||
// Max log file size of 10 MB
|
||||
#define MAX_LOG_SIZE_BYTES (10 * 1024 * 1024)
|
||||
static int s_LogBytesWritten = 0;
|
||||
static bool s_LogLimitReached = false;
|
||||
static QFile* s_LoggerFile;
|
||||
#endif
|
||||
@@ -75,7 +80,7 @@ void logToLoggerStream(QString& message)
|
||||
if (s_LogLimitReached) {
|
||||
return;
|
||||
}
|
||||
else if (s_LogLinesWritten == MAX_LOG_LINES) {
|
||||
else if (s_LogBytesWritten >= MAX_LOG_SIZE_BYTES) {
|
||||
s_LoggerStream << "Log size limit reached!";
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
|
||||
s_LoggerStream << Qt::endl;
|
||||
@@ -86,7 +91,7 @@ void logToLoggerStream(QString& message)
|
||||
return;
|
||||
}
|
||||
else {
|
||||
s_LogLinesWritten++;
|
||||
s_LogBytesWritten += message.size();
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -100,15 +105,27 @@ void sdlLogToDiskHandler(void*, int category, SDL_LogPriority priority, const ch
|
||||
|
||||
switch (priority) {
|
||||
case SDL_LOG_PRIORITY_VERBOSE:
|
||||
if (s_SuppressVerboseOutput) {
|
||||
return;
|
||||
}
|
||||
priorityTxt = "Verbose";
|
||||
break;
|
||||
case SDL_LOG_PRIORITY_DEBUG:
|
||||
if (s_SuppressVerboseOutput) {
|
||||
return;
|
||||
}
|
||||
priorityTxt = "Debug";
|
||||
break;
|
||||
case SDL_LOG_PRIORITY_INFO:
|
||||
if (s_SuppressVerboseOutput) {
|
||||
return;
|
||||
}
|
||||
priorityTxt = "Info";
|
||||
break;
|
||||
case SDL_LOG_PRIORITY_WARN:
|
||||
if (s_SuppressVerboseOutput) {
|
||||
return;
|
||||
}
|
||||
priorityTxt = "Warn";
|
||||
break;
|
||||
case SDL_LOG_PRIORITY_ERROR:
|
||||
@@ -134,12 +151,21 @@ void qtLogToDiskHandler(QtMsgType type, const QMessageLogContext&, const QString
|
||||
|
||||
switch (type) {
|
||||
case QtDebugMsg:
|
||||
if (s_SuppressVerboseOutput) {
|
||||
return;
|
||||
}
|
||||
typeTxt = "Debug";
|
||||
break;
|
||||
case QtInfoMsg:
|
||||
if (s_SuppressVerboseOutput) {
|
||||
return;
|
||||
}
|
||||
typeTxt = "Info";
|
||||
break;
|
||||
case QtWarningMsg:
|
||||
if (s_SuppressVerboseOutput) {
|
||||
return;
|
||||
}
|
||||
typeTxt = "Warning";
|
||||
break;
|
||||
case QtCriticalMsg:
|
||||
@@ -166,13 +192,26 @@ void ffmpegLogToDiskHandler(void* ptr, int level, const char* fmt, va_list vl)
|
||||
if ((level & 0xFF) > av_log_get_level()) {
|
||||
return;
|
||||
}
|
||||
else if ((level & 0xFF) > AV_LOG_WARNING && s_SuppressVerboseOutput) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We need to use the *previous* printPrefix value to determine whether to
|
||||
// print the prefix this time. av_log_format_line() will set the printPrefix
|
||||
// value to indicate whether the prefix should be printed *next time*.
|
||||
bool shouldPrefixThisMessage = printPrefix != 0;
|
||||
|
||||
av_log_format_line(ptr, level, fmt, vl, lineBuffer, sizeof(lineBuffer), &printPrefix);
|
||||
|
||||
QTime logTime = QTime::fromMSecsSinceStartOfDay(s_LoggerTime.elapsed());
|
||||
QString txt = QString("%1 - FFmpeg: %2").arg(logTime.toString()).arg(lineBuffer);
|
||||
|
||||
logToLoggerStream(txt);
|
||||
if (shouldPrefixThisMessage) {
|
||||
QTime logTime = QTime::fromMSecsSinceStartOfDay(s_LoggerTime.elapsed());
|
||||
QString txt = QString("%1 - FFmpeg: %2").arg(logTime.toString()).arg(lineBuffer);
|
||||
logToLoggerStream(txt);
|
||||
}
|
||||
else {
|
||||
QString txt = QString(lineBuffer);
|
||||
logToLoggerStream(txt);
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -250,7 +289,6 @@ int main(int argc, char *argv[])
|
||||
QCoreApplication::setApplicationName("Moonlight");
|
||||
|
||||
if (QFile(QDir::currentPath() + "/portable.dat").exists()) {
|
||||
qInfo() << "Running in portable mode from:" << QDir::currentPath();
|
||||
QSettings::setDefaultFormat(QSettings::IniFormat);
|
||||
QSettings::setPath(QSettings::IniFormat, QSettings::UserScope, QDir::currentPath());
|
||||
QSettings::setPath(QSettings::IniFormat, QSettings::SystemScope, QDir::currentPath());
|
||||
@@ -263,12 +301,17 @@ int main(int argc, char *argv[])
|
||||
Path::initialize(false);
|
||||
}
|
||||
|
||||
// Override the default QML cache directory with the one we chose
|
||||
if (qEnvironmentVariableIsEmpty("QML_DISK_CACHE_PATH")) {
|
||||
qputenv("QML_DISK_CACHE_PATH", Path::getQmlCacheDir().toUtf8());
|
||||
}
|
||||
|
||||
#ifdef USE_CUSTOM_LOGGER
|
||||
#ifdef LOG_TO_FILE
|
||||
QDir tempDir(Path::getLogDir());
|
||||
s_LoggerFile = new QFile(tempDir.filePath(QString("Moonlight-%1.log").arg(QDateTime::currentSecsSinceEpoch())));
|
||||
if (s_LoggerFile->open(QIODevice::WriteOnly)) {
|
||||
qInfo() << "Redirecting log output to " << s_LoggerFile->fileName();
|
||||
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
|
||||
@@ -288,10 +331,18 @@ int main(int argc, char *argv[])
|
||||
SetUnhandledExceptionFilter(UnhandledExceptionHandler);
|
||||
#endif
|
||||
|
||||
#if defined(Q_OS_WIN32) && defined(Q_PROCESSOR_X86)
|
||||
#ifdef LOG_TO_FILE
|
||||
// Prune the oldest existing logs if there are more than 10
|
||||
QStringList existingLogNames = tempDir.entryList(QStringList("Moonlight-*.log"), QDir::NoFilter, QDir::SortFlag::Time);
|
||||
for (int i = 10; i < existingLogNames.size(); i++) {
|
||||
qInfo() << "Removing old log file:" << existingLogNames.at(i);
|
||||
QFile(tempDir.filePath(existingLogNames.at(i))).remove();
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(Q_OS_WIN32)
|
||||
// Force AntiHooking.dll to be statically imported and loaded
|
||||
// by ntdll on x86/x64 platforms by calling a dummy function.
|
||||
// AntiHooking.dll is not currently built for ARM64.
|
||||
// by ntdll on Win32 platforms by calling a dummy function.
|
||||
AntiHookingDummyImport();
|
||||
#elif defined(Q_OS_LINUX)
|
||||
// Force libssl.so to be directly linked to our binary, so
|
||||
@@ -334,6 +385,11 @@ int main(int argc, char *argv[])
|
||||
qWarning() << "On the Raspberry Pi, you must enable the 'fake KMS' driver in raspi-config to use Moonlight outside of the GUI environment.";
|
||||
}
|
||||
}
|
||||
|
||||
// EGLFS uses OpenGLES 2.0, so we will too. Some embedded platforms may not
|
||||
// even have working OpenGL implementations, so GLES is the only option.
|
||||
// See https://github.com/moonlight-stream/moonlight-qt/issues/868
|
||||
SDL_SetHint(SDL_HINT_RENDER_DRIVER, "opengles2");
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -372,18 +428,6 @@ int main(int argc, char *argv[])
|
||||
// Register custom metatypes for use in signals
|
||||
qRegisterMetaType<NvApp>("NvApp");
|
||||
|
||||
SDL_version compileVersion;
|
||||
SDL_VERSION(&compileVersion);
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Compiled with SDL %d.%d.%d",
|
||||
compileVersion.major, compileVersion.minor, compileVersion.patch);
|
||||
|
||||
SDL_version runtimeVersion;
|
||||
SDL_GetVersion(&runtimeVersion);
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Running with SDL %d.%d.%d",
|
||||
runtimeVersion.major, runtimeVersion.minor, runtimeVersion.patch);
|
||||
|
||||
// Allow the display to sleep by default. We will manually use SDL_DisableScreenSaver()
|
||||
// and SDL_EnableScreenSaver() when appropriate. This hint must be set before
|
||||
// initializing the SDL video subsystem to have any effect.
|
||||
@@ -448,6 +492,10 @@ int main(int argc, char *argv[])
|
||||
SDL_SetHint("SDL_AUDIO_DEVICE_APP_NAME", "Moonlight");
|
||||
SDL_SetHint("SDL_APP_NAME", "Moonlight");
|
||||
|
||||
// We handle capturing the mouse ourselves when it leaves the window, so we don't need
|
||||
// SDL doing it for us behind our backs.
|
||||
SDL_SetHint("SDL_MOUSE_AUTO_CAPTURE", "0");
|
||||
|
||||
#ifdef QT_DEBUG
|
||||
// Allow thread naming using exceptions on debug builds. SDL doesn't use SEH
|
||||
// when throwing the exceptions, so we don't enable it for release builds out
|
||||
@@ -457,15 +505,64 @@ int main(int argc, char *argv[])
|
||||
|
||||
QGuiApplication app(argc, argv);
|
||||
|
||||
GlobalCommandLineParser parser;
|
||||
GlobalCommandLineParser::ParseResult commandLineParserResult = parser.parse(app.arguments());
|
||||
switch (commandLineParserResult) {
|
||||
case GlobalCommandLineParser::ListRequested:
|
||||
#ifdef USE_CUSTOM_LOGGER
|
||||
// Don't log to the console since it will jumble the command output
|
||||
s_SuppressVerboseOutput = true;
|
||||
#endif
|
||||
#ifdef Q_OS_WIN32
|
||||
// Attach to the console to be able to print output.
|
||||
// Since we're a /SUBSYSTEM:WINDOWS app, we won't be attached by default.
|
||||
if (AttachConsole(ATTACH_PARENT_PROCESS)) {
|
||||
HANDLE conOut = GetStdHandle(STD_OUTPUT_HANDLE);
|
||||
if (conOut != INVALID_HANDLE_VALUE && conOut != NULL) {
|
||||
freopen("CONOUT$", "w", stdout);
|
||||
setvbuf(stdout, NULL, _IONBF, 0);
|
||||
}
|
||||
HANDLE conErr = GetStdHandle(STD_ERROR_HANDLE);
|
||||
if (conErr != INVALID_HANDLE_VALUE && conErr != NULL) {
|
||||
freopen("CONOUT$", "w", stderr);
|
||||
setvbuf(stderr, NULL, _IONBF, 0);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
SDL_version compileVersion;
|
||||
SDL_VERSION(&compileVersion);
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Compiled with SDL %d.%d.%d",
|
||||
compileVersion.major, compileVersion.minor, compileVersion.patch);
|
||||
|
||||
SDL_version runtimeVersion;
|
||||
SDL_GetVersion(&runtimeVersion);
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Running with SDL %d.%d.%d",
|
||||
runtimeVersion.major, runtimeVersion.minor, runtimeVersion.patch);
|
||||
|
||||
// Apply the initial translation based on user preference
|
||||
StreamingPreferences prefs;
|
||||
prefs.retranslate();
|
||||
|
||||
// Trickily declare the translation for dialog buttons
|
||||
QCoreApplication::translate("QPlatformTheme", "&Yes");
|
||||
QCoreApplication::translate("QPlatformTheme", "&No");
|
||||
QCoreApplication::translate("QPlatformTheme", "OK");
|
||||
QCoreApplication::translate("QPlatformTheme", "Help");
|
||||
QCoreApplication::translate("QPlatformTheme", "Cancel");
|
||||
|
||||
// After the QGuiApplication is created, the platform stuff will be initialized
|
||||
// and we can set the SDL video driver to match Qt.
|
||||
if (WMUtils::isRunningWayland() && QGuiApplication::platformName() == "xcb") {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Detected XWayland. This will probably break hardware decoding! Try running with QT_QPA_PLATFORM=wayland or switch to X11.");
|
||||
qputenv("SDL_VIDEODRIVER", "x11");
|
||||
}
|
||||
else if (QGuiApplication::platformName().startsWith("wayland")) {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Detected Wayland");
|
||||
@@ -543,35 +640,25 @@ int main(int argc, char *argv[])
|
||||
// Create the identity manager on the main thread
|
||||
IdentityManager::get();
|
||||
|
||||
#ifndef Q_OS_WINRT
|
||||
// Use the dense material dark theme by default
|
||||
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_STYLE")) {
|
||||
qputenv("QT_QUICK_CONTROLS_STYLE", "Material");
|
||||
}
|
||||
#else
|
||||
// Use universal dark on WinRT
|
||||
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_STYLE")) {
|
||||
qputenv("QT_QUICK_CONTROLS_STYLE", "Universal");
|
||||
}
|
||||
#endif
|
||||
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_MATERIAL_THEME")) {
|
||||
qputenv("QT_QUICK_CONTROLS_MATERIAL_THEME", "Dark");
|
||||
}
|
||||
// We require the Material theme
|
||||
QQuickStyle::setStyle("Material");
|
||||
|
||||
// Our icons are styled for a dark theme, so we do not allow the user to override this
|
||||
qputenv("QT_QUICK_CONTROLS_MATERIAL_THEME", "Dark");
|
||||
|
||||
// These are defaults that we allow the user to override
|
||||
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_MATERIAL_ACCENT")) {
|
||||
qputenv("QT_QUICK_CONTROLS_MATERIAL_ACCENT", "Purple");
|
||||
}
|
||||
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_MATERIAL_VARIANT")) {
|
||||
qputenv("QT_QUICK_CONTROLS_MATERIAL_VARIANT", "Dense");
|
||||
}
|
||||
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_UNIVERSAL_THEME")) {
|
||||
qputenv("QT_QUICK_CONTROLS_UNIVERSAL_THEME", "Dark");
|
||||
}
|
||||
|
||||
QQmlApplicationEngine engine;
|
||||
QString initialView;
|
||||
bool hasGUI = true;
|
||||
|
||||
GlobalCommandLineParser parser;
|
||||
switch (parser.parse(app.arguments())) {
|
||||
switch (commandLineParserResult) {
|
||||
case GlobalCommandLineParser::NormalStartRequested:
|
||||
initialView = "qrc:/gui/PcView.qml";
|
||||
break;
|
||||
@@ -596,14 +683,35 @@ int main(int argc, char *argv[])
|
||||
engine.rootContext()->setContextProperty("launcher", launcher);
|
||||
break;
|
||||
}
|
||||
case GlobalCommandLineParser::PairRequested:
|
||||
{
|
||||
initialView = "qrc:/gui/CliPair.qml";
|
||||
PairCommandLineParser pairParser;
|
||||
pairParser.parse(app.arguments());
|
||||
auto launcher = new CliPair::Launcher(pairParser.getHost(), pairParser.getPredefinedPin(), &app);
|
||||
engine.rootContext()->setContextProperty("launcher", launcher);
|
||||
break;
|
||||
}
|
||||
case GlobalCommandLineParser::ListRequested:
|
||||
{
|
||||
ListCommandLineParser listParser;
|
||||
listParser.parse(app.arguments());
|
||||
auto launcher = new CliListApps::Launcher(listParser.getHost(), listParser, &app);
|
||||
launcher->execute(new ComputerManager(&app));
|
||||
hasGUI = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
engine.rootContext()->setContextProperty("initialView", initialView);
|
||||
if (hasGUI) {
|
||||
engine.rootContext()->setContextProperty("initialView", initialView);
|
||||
|
||||
// Load the main.qml file
|
||||
engine.load(QUrl(QStringLiteral("qrc:/gui/main.qml")));
|
||||
if (engine.rootObjects().isEmpty())
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Load the main.qml file
|
||||
engine.load(QUrl(QStringLiteral("qrc:/gui/main.qml")));
|
||||
if (engine.rootObjects().isEmpty())
|
||||
return -1;
|
||||
int err = app.exec();
|
||||
|
||||
// Give worker tasks time to properly exit. Fixes PendingQuitTask
|
||||
|
||||
+38
-50
@@ -11,6 +11,10 @@
|
||||
|
||||
#define _GNU_SOURCE
|
||||
|
||||
// NOTE: This file MUST NOT include fcntl.h due to open() -> open64()
|
||||
// redirection that happens when _FILE_OFFSET_BITS=64!
|
||||
// See masterhook_internal.c for details.
|
||||
|
||||
#include <SDL.h>
|
||||
#include <dlfcn.h>
|
||||
#include <unistd.h>
|
||||
@@ -30,92 +34,76 @@
|
||||
|
||||
// Qt's DRM master FD grabbed by our hook
|
||||
int g_QtDrmMasterFd = -1;
|
||||
struct stat64 g_DrmMasterStat;
|
||||
struct stat g_DrmMasterStat;
|
||||
|
||||
// The DRM master FD created for SDL
|
||||
int g_SdlDrmMasterFd = -1;
|
||||
|
||||
int drmIsMaster(int fd)
|
||||
{
|
||||
// Detect master by attempting something that requires master.
|
||||
// This method is available in Mesa DRM since Feb 2019.
|
||||
return drmAuthMagic(fd, 0) != -EACCES;
|
||||
}
|
||||
|
||||
// This hook will handle legacy DRM rendering
|
||||
int drmModeSetCrtc(int fd, uint32_t crtcId, uint32_t bufferId,
|
||||
uint32_t x, uint32_t y, uint32_t *connectors, int count,
|
||||
drmModeModeInfoPtr mode)
|
||||
{
|
||||
// Grab the first DRM FD that makes it in here. This will be the Qt
|
||||
// EGLFS backend's DRM FD, which we will need later.
|
||||
if (g_QtDrmMasterFd == -1) {
|
||||
// Grab the first DRM Master FD that makes it in here. This will be the Qt
|
||||
// EGLFS backend's DRM FD, on which we will call drmDropMaster() later.
|
||||
if (g_QtDrmMasterFd == -1 && drmIsMaster(fd)) {
|
||||
g_QtDrmMasterFd = fd;
|
||||
fstat64(g_QtDrmMasterFd, &g_DrmMasterStat);
|
||||
fstat(g_QtDrmMasterFd, &g_DrmMasterStat);
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Captured Qt EGLFS DRM master fd (legacy): %d",
|
||||
g_QtDrmMasterFd);
|
||||
}
|
||||
|
||||
// Call into the real thing
|
||||
return ((typeof(drmModeSetCrtc)*)dlsym(RTLD_NEXT, "drmModeSetCrtc"))(fd, crtcId, bufferId, x, y, connectors, count, mode);
|
||||
return ((typeof(drmModeSetCrtc)*)dlsym(RTLD_NEXT, __FUNCTION__))(fd, crtcId, bufferId, x, y, connectors, count, mode);
|
||||
}
|
||||
|
||||
// This hook will handle atomic DRM rendering
|
||||
int drmModeAtomicCommit(int fd, drmModeAtomicReqPtr req,
|
||||
uint32_t flags, void *user_data)
|
||||
{
|
||||
// Grab the first DRM FD that makes it in here. This will be the Qt
|
||||
// EGLFS backend's DRM FD, which we will need later.
|
||||
if (g_QtDrmMasterFd == -1) {
|
||||
// Grab the first DRM Master FD that makes it in here. This will be the Qt
|
||||
// EGLFS backend's DRM FD, on which we will call drmDropMaster() later.
|
||||
if (g_QtDrmMasterFd == -1 && drmIsMaster(fd)) {
|
||||
g_QtDrmMasterFd = fd;
|
||||
fstat64(g_QtDrmMasterFd, &g_DrmMasterStat);
|
||||
fstat(g_QtDrmMasterFd, &g_DrmMasterStat);
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Captured Qt EGLFS DRM master fd (atomic): %d",
|
||||
g_QtDrmMasterFd);
|
||||
}
|
||||
|
||||
// Call into the real thing
|
||||
return ((typeof(drmModeAtomicCommit)*)dlsym(RTLD_NEXT, "drmModeAtomicCommit"))(fd, req, flags, user_data);
|
||||
return ((typeof(drmModeAtomicCommit)*)dlsym(RTLD_NEXT, __FUNCTION__))(fd, req, flags, user_data);
|
||||
}
|
||||
|
||||
// This hook will handle SDL's open() on the DRM device. We just need to
|
||||
// hook this variant of open(), since that's what SDL uses. When we see
|
||||
// the open a FD for the same card as the Qt DRM master FD, we'll drop
|
||||
// master on the Qt FD to allow the new FD to have master.
|
||||
int open(const char *pathname, int flags)
|
||||
int openHook(const char *funcname, const char *pathname, int flags, va_list va);
|
||||
|
||||
int open(const char *pathname, int flags, ...)
|
||||
{
|
||||
// Call the real thing to do the open operation
|
||||
int fd = ((typeof(open)*)dlsym(RTLD_NEXT, "open"))(pathname, flags);
|
||||
|
||||
// If the file was successfully opened and we have a DRM master FD,
|
||||
// check if the FD we just opened is a DRM device.
|
||||
if (fd >= 0 && g_QtDrmMasterFd != -1) {
|
||||
if (strncmp(pathname, "/dev/dri/card", 13) == 0) {
|
||||
// It's a DRM device, but is it _our_ DRM device?
|
||||
struct stat64 fdstat;
|
||||
|
||||
fstat64(fd, &fdstat);
|
||||
if (g_DrmMasterStat.st_dev == fdstat.st_dev &&
|
||||
g_DrmMasterStat.st_ino == fdstat.st_ino) {
|
||||
// It is our device. Time to do the magic!
|
||||
|
||||
// This code assumes SDL only ever opens a single FD
|
||||
// for a given DRM device.
|
||||
SDL_assert(g_SdlDrmMasterFd == -1);
|
||||
|
||||
// Drop master on Qt's FD so we can pick it up for SDL.
|
||||
if (drmDropMaster(g_QtDrmMasterFd) < 0) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Failed to drop master on Qt DRM FD: %d",
|
||||
errno);
|
||||
// Hope for the best
|
||||
return fd;
|
||||
}
|
||||
|
||||
// We are not allowed to call drmSetMaster() without CAP_SYS_ADMIN,
|
||||
// but since we just dropped the master, we can become master by
|
||||
// simply creating a new FD. Let's do it.
|
||||
close(fd);
|
||||
fd = ((typeof(open)*)dlsym(RTLD_NEXT, "open"))(pathname, flags);
|
||||
g_SdlDrmMasterFd = fd;
|
||||
}
|
||||
}
|
||||
}
|
||||
va_list va;
|
||||
va_start(va, flags);
|
||||
int fd = openHook(__FUNCTION__, pathname, flags, va);
|
||||
va_end(va);
|
||||
return fd;
|
||||
}
|
||||
|
||||
int open64(const char *pathname, int flags, ...)
|
||||
{
|
||||
va_list va;
|
||||
va_start(va, flags);
|
||||
int fd = openHook(__FUNCTION__, pathname, flags, va);
|
||||
va_end(va);
|
||||
return fd;
|
||||
}
|
||||
|
||||
@@ -124,7 +112,7 @@ int open(const char *pathname, int flags)
|
||||
int close(int fd)
|
||||
{
|
||||
// Call the real thing
|
||||
int ret = ((typeof(close)*)dlsym(RTLD_NEXT, "close"))(fd);
|
||||
int ret = ((typeof(close)*)dlsym(RTLD_NEXT, __FUNCTION__))(fd);
|
||||
if (ret == 0) {
|
||||
// If we just closed the SDL DRM master FD, restore master
|
||||
// to the Qt DRM FD. This works because the Qt DRM master FD
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
// This file contains the private implementation of the open() hook
|
||||
// which must be in a separate compilation unit due to fcntl.h doing
|
||||
// unwanted redirection of open() to open64().
|
||||
|
||||
#define _GNU_SOURCE
|
||||
|
||||
#include <SDL.h>
|
||||
#include <dlfcn.h>
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <errno.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#include <xf86drm.h>
|
||||
#include <xf86drmMode.h>
|
||||
|
||||
// __OPEN_NEEDS_MODE is a glibc-ism, so define it ourselves for other libc
|
||||
#ifndef __OPEN_NEEDS_MODE
|
||||
#ifdef __O_TMPFILE
|
||||
# define __OPEN_NEEDS_MODE(oflag) \
|
||||
(((oflag) & O_CREAT) != 0 || ((oflag) & __O_TMPFILE) == __O_TMPFILE)
|
||||
#else
|
||||
# define __OPEN_NEEDS_MODE(oflag) (((oflag) & O_CREAT) != 0)
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if SDL_VERSION_ATLEAST(2, 0, 15)
|
||||
|
||||
extern int g_QtDrmMasterFd;
|
||||
extern struct stat g_DrmMasterStat;
|
||||
extern int g_SdlDrmMasterFd;
|
||||
|
||||
int openHook(const char *funcname, const char *pathname, int flags, va_list va)
|
||||
{
|
||||
int fd;
|
||||
mode_t mode;
|
||||
|
||||
// Call the real thing to do the open operation
|
||||
if (__OPEN_NEEDS_MODE(flags)) {
|
||||
mode = va_arg(va, mode_t);
|
||||
fd = ((typeof(open)*)dlsym(RTLD_NEXT, funcname))(pathname, flags, mode);
|
||||
}
|
||||
else {
|
||||
mode = 0;
|
||||
fd = ((typeof(open)*)dlsym(RTLD_NEXT, funcname))(pathname, flags);
|
||||
}
|
||||
|
||||
// If the file was successfully opened and we have a DRM master FD,
|
||||
// check if the FD we just opened is a DRM device.
|
||||
if (fd >= 0 && g_QtDrmMasterFd != -1) {
|
||||
if (strncmp(pathname, "/dev/dri/card", 13) == 0) {
|
||||
// It's a DRM device, but is it _our_ DRM device?
|
||||
struct stat fdstat;
|
||||
|
||||
fstat(fd, &fdstat);
|
||||
if (g_DrmMasterStat.st_dev == fdstat.st_dev &&
|
||||
g_DrmMasterStat.st_ino == fdstat.st_ino) {
|
||||
// It is our device. Time to do the magic!
|
||||
|
||||
// This code assumes SDL only ever opens a single FD
|
||||
// for a given DRM device.
|
||||
SDL_assert(g_SdlDrmMasterFd == -1);
|
||||
|
||||
// Drop master on Qt's FD so we can pick it up for SDL.
|
||||
if (drmDropMaster(g_QtDrmMasterFd) < 0) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Failed to drop master on Qt DRM FD: %d",
|
||||
errno);
|
||||
// Hope for the best
|
||||
return fd;
|
||||
}
|
||||
|
||||
// We are not allowed to call drmSetMaster() without CAP_SYS_ADMIN,
|
||||
// but since we just dropped the master, we can become master by
|
||||
// simply creating a new FD. Let's do it.
|
||||
close(fd);
|
||||
if (__OPEN_NEEDS_MODE(flags)) {
|
||||
fd = ((typeof(open)*)dlsym(RTLD_NEXT, funcname))(pathname, flags, mode);
|
||||
}
|
||||
else {
|
||||
fd = ((typeof(open)*)dlsym(RTLD_NEXT, funcname))(pathname, flags);
|
||||
}
|
||||
g_SdlDrmMasterFd = fd;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fd;
|
||||
}
|
||||
#endif
|
||||
@@ -9,6 +9,7 @@
|
||||
QString Path::s_CacheDir;
|
||||
QString Path::s_LogDir;
|
||||
QString Path::s_BoxArtCacheDir;
|
||||
QString Path::s_QmlCacheDir;
|
||||
|
||||
QString Path::getLogDir()
|
||||
{
|
||||
@@ -22,6 +23,12 @@ QString Path::getBoxArtCacheDir()
|
||||
return s_BoxArtCacheDir;
|
||||
}
|
||||
|
||||
QString Path::getQmlCacheDir()
|
||||
{
|
||||
Q_ASSERT(!s_QmlCacheDir.isEmpty());
|
||||
return s_QmlCacheDir;
|
||||
}
|
||||
|
||||
QByteArray Path::readDataFile(QString fileName)
|
||||
{
|
||||
QFile dataFile(getDataFilePath(fileName));
|
||||
@@ -97,6 +104,7 @@ void Path::initialize(bool portable)
|
||||
if (portable) {
|
||||
s_LogDir = QDir::currentPath();
|
||||
s_BoxArtCacheDir = QDir::currentPath() + "/boxart";
|
||||
s_QmlCacheDir = QDir::currentPath() + "/qmlcache";
|
||||
|
||||
// In order for the If-Modified-Since logic to work in MappingFetcher,
|
||||
// the cache directory must be different than the current directory.
|
||||
@@ -112,5 +120,6 @@ void Path::initialize(bool portable)
|
||||
#endif
|
||||
s_CacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation);
|
||||
s_BoxArtCacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/boxart";
|
||||
s_QmlCacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/qmlcache";
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -7,8 +7,8 @@ class Path
|
||||
{
|
||||
public:
|
||||
static QString getLogDir();
|
||||
|
||||
static QString getBoxArtCacheDir();
|
||||
static QString getQmlCacheDir();
|
||||
|
||||
static QByteArray readDataFile(QString fileName);
|
||||
static void writeCacheFile(QString fileName, QByteArray data);
|
||||
@@ -24,4 +24,5 @@ private:
|
||||
static QString s_CacheDir;
|
||||
static QString s_LogDir;
|
||||
static QString s_BoxArtCacheDir;
|
||||
static QString s_QmlCacheDir;
|
||||
};
|
||||
|
||||
@@ -18,5 +18,6 @@
|
||||
<file>gui/NavigableMessageDialog.qml</file>
|
||||
<file>gui/NavigableDialog.qml</file>
|
||||
<file>gui/CenteredGridView.qml</file>
|
||||
<file>gui/CliPair.qml</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
[Platforms]
|
||||
WindowsArguments = darkmode=1
|
||||
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="#fff"><path d="M12 2C6.47 2 2 6.47 2 12s4.47 10 10 10 10-4.47 10-10S17.53 2 12 2zm5 13.59L15.59 17 12 13.41 8.41 17 7 15.59 10.59 12 7 8.41 8.41 7 12 10.59 15.59 7 17 8.41 13.41 12 17 15.59z"/><path d="M0 0h24v24H0z" fill="none"/></svg>
|
||||
|
Before Width: | Height: | Size: 325 B |
@@ -1,4 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48" fill="#fff">
|
||||
<path d="M0 0h48v48H0z" fill="none"/>
|
||||
<path d="M24 4C12.95 4 4 12.95 4 24s8.95 20 20 20 20-8.95 20-20S35.05 4 24 4zm-4 29V15l12 9-12 9z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 250 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48" fill="#FFFFFF"><path d="M370-206q-26 14-49 1t-23-40v-477q0-26 23-39t49 1l372 239q23 14 23 38.5T742-445L370-206Z"/></svg>
|
||||
|
After Width: | Height: | Size: 208 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48" fill="#FFFFFF"><path d="M309-218q-38.425 0-64.712-26.288Q218-270.575 218-309v-342q0-38.838 26.288-65.419Q270.575-743 309-743h342q38.838 0 65.419 26.581Q743-689.838 743-651v342q0 38.425-26.581 64.712Q689.838-218 651-218H309Z"/></svg>
|
||||
|
After Width: | Height: | Size: 320 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24" fill="white"><path d="M108.018-98.001q-14.17 0-25.267-7.472-11.097-7.471-17.595-17.605-6.68-10.077-7.147-23-.468-12.923 7.085-26.165l372.43-640.512q7.553-13.242 18.399-18.665 10.846-5.424 24.077-5.424 13.231 0 24.077 5.424 10.846 5.423 18.399 18.665l372.43 640.512q7.553 13.242 7.085 26.165-.468 12.923-7.147 23-6.498 10.134-17.595 17.605-11.097 7.472-25.267 7.472H108.018ZM481-220.693q18.731 0 32.019-13.288 13.288-13.288 13.288-32.019 0-18.731-13.288-32.019-13.288-13.288-32.019-13.288-18.731 0-32.019 13.288-13.288 13.288-13.288 32.019 0 18.731 13.288 32.019 13.288 13.288 32.019 13.288Zm.013-127.614q17.756 0 30.371-12.625 12.615-12.625 12.615-30.374v-118.002q0-17.75-12.628-30.374-12.629-12.625-30.384-12.625-17.756 0-30.371 12.625-12.615 12.624-12.615 30.374v118.002q0 17.749 12.628 30.374 12.629 12.625 30.384 12.625Z"/></svg>
|
||||
|
After Width: | Height: | Size: 921 B |
+29
-6
@@ -4,9 +4,10 @@
|
||||
<file>res/desktop_windows-48px.svg</file>
|
||||
<file>res/ic_add_to_queue_white_48px.svg</file>
|
||||
<file>res/baseline-lock-24px.svg</file>
|
||||
<file>res/baseline-play_circle_filled_white-48px.svg</file>
|
||||
<file>res/play_arrow_FILL1_wght700_GRAD200_opsz48.svg</file>
|
||||
<file>res/baseline-warning-24px.svg</file>
|
||||
<file>res/baseline-cancel-24px.svg</file>
|
||||
<file>res/warning_FILL1_wght300_GRAD200_opsz24.svg</file>
|
||||
<file>res/stop_FILL1_wght700_GRAD200_opsz48.svg</file>
|
||||
<file>res/no_app_image.png</file>
|
||||
<file>res/settings.svg</file>
|
||||
<file>res/arrow_left.svg</file>
|
||||
@@ -21,8 +22,8 @@
|
||||
<file>languages/qml_de.qm</file>
|
||||
<file>languages/qml_fr.ts</file>
|
||||
<file>languages/qml_fr.qm</file>
|
||||
<file>languages/qml_zh_cn.ts</file>
|
||||
<file>languages/qml_zh_cn.qm</file>
|
||||
<file>languages/qml_zh_CN.ts</file>
|
||||
<file>languages/qml_zh_CN.qm</file>
|
||||
<file>languages/qml_nb_NO.ts</file>
|
||||
<file>languages/qml_nb_NO.qm</file>
|
||||
<file>languages/qml_ru.ts</file>
|
||||
@@ -47,8 +48,25 @@
|
||||
<file>languages/qml_tr.qm</file>
|
||||
<file>languages/qml_uk.ts</file>
|
||||
<file>languages/qml_uk.qm</file>
|
||||
<file>languages/qml_zh_Hant.ts</file>
|
||||
<file>languages/qml_zh_Hant.qm</file>
|
||||
<file>languages/qml_zh_TW.ts</file>
|
||||
<file>languages/qml_zh_TW.qm</file>
|
||||
<file>languages/qml_el.ts</file>
|
||||
<file>languages/qml_el.qm</file>
|
||||
<file>languages/qml_hi.ts</file>
|
||||
<file>languages/qml_hi.qm</file>
|
||||
<file>languages/qml_it.ts</file>
|
||||
<file>languages/qml_it.qm</file>
|
||||
<file>languages/qml_pt.ts</file>
|
||||
<file>languages/qml_pt.qm</file>
|
||||
<file>languages/qml_pl.ts</file>
|
||||
<file>languages/qml_pl.qm</file>
|
||||
<file>languages/qml_cs.ts</file>
|
||||
<file>languages/qml_cs.qm</file>
|
||||
<file>languages/qml_he.ts</file>
|
||||
<file>languages/qml_he.qm</file>
|
||||
<!-- Don't include pt_BR until it is more complete -->
|
||||
<!--file>languages/qml_pt_BR.qm</file-->
|
||||
<!--file>languages/qml_pt_BR.ts</file-->
|
||||
</qresource>
|
||||
<qresource prefix="/data">
|
||||
<file alias="gamecontrollerdb.txt">SDL_GameControllerDB/gamecontrollerdb.txt</file>
|
||||
@@ -59,5 +77,10 @@
|
||||
<file alias="egl_opaque.vert">shaders/egl_opaque.vert</file>
|
||||
<file alias="egl_overlay.frag">shaders/egl_overlay.frag</file>
|
||||
<file alias="egl_overlay.vert">shaders/egl_overlay.vert</file>
|
||||
<file alias="d3d11_vertex.fxc">shaders/d3d11_vertex.fxc</file>
|
||||
<file alias="d3d11_overlay_pixel.fxc">shaders/d3d11_overlay_pixel.fxc</file>
|
||||
<file alias="d3d11_genyuv_pixel.fxc">shaders/d3d11_genyuv_pixel.fxc</file>
|
||||
<file alias="d3d11_bt601lim_pixel.fxc">shaders/d3d11_bt601lim_pixel.fxc</file>
|
||||
<file alias="d3d11_bt2020lim_pixel.fxc">shaders/d3d11_bt2020lim_pixel.fxc</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user