mirror of
https://github.com/moonlight-stream/moonlight-qt.git
synced 2026-10-12 03:24:00 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
413993ab6f | ||
|
|
948b9c818a | ||
|
|
cf615ea88d | ||
|
|
2d1e16fe50 | ||
|
|
089c3d3ad3 | ||
|
|
63e3fa6609 | ||
|
|
8a7fa7e559 | ||
|
|
989a90a0f7 | ||
|
|
a707720577 | ||
|
|
eef270de26 | ||
|
|
0ac765bc32 | ||
|
|
e76551cb64 | ||
|
|
4fd902a0d3 | ||
|
|
9cba449092 | ||
|
|
905b2ecf9e | ||
|
|
53c2c612c9 | ||
|
|
8ffadde9cc | ||
|
|
f569bf633c | ||
|
|
6eb5e07007 | ||
|
|
f07201baa9 | ||
|
|
9120fd01a8 | ||
|
|
c14cc70a6a | ||
|
|
a66f91ae3c | ||
|
|
7e4fddbe62 | ||
|
|
0ff9f8c55a | ||
|
|
6f710faa8e | ||
|
|
2127e1d90b | ||
|
|
9d73aa6b4a | ||
|
|
a2f23b7053 | ||
|
|
5974980aff | ||
|
|
6fa53691aa | ||
|
|
49e0679864 | ||
|
|
0976bc39b3 | ||
|
|
facd6e4e56 | ||
|
|
d2dc0aa1b1 | ||
|
|
aa74e6930b | ||
|
|
b7811bf196 | ||
|
|
18130fd8de | ||
|
|
b6d4f97e96 | ||
|
|
0dd0112518 | ||
|
|
5711e6064e | ||
|
|
517b684f85 | ||
|
|
39465f6d8a | ||
|
|
738813cbac | ||
|
|
c2778e040a | ||
|
|
1efdeeb9dc | ||
|
|
b01dfea343 | ||
|
|
e143293e91 | ||
|
|
f420dc740a | ||
|
|
2e532224b7 | ||
|
|
cb8366a7e5 | ||
|
|
fee54a9d76 | ||
|
|
dc556d3c9b | ||
|
|
4fed3802aa | ||
|
|
f7bb4611e9 | ||
|
|
57791eec62 | ||
|
|
e1e3d8e710 | ||
|
|
654be492a0 | ||
|
|
1df0da4d4a | ||
|
|
1dd6cdb567 | ||
|
|
edfb3cfdc8 | ||
|
|
c6a8f44640 | ||
|
|
b7301d4415 | ||
|
|
4d668702b5 | ||
|
|
f4b0f4d78f | ||
|
|
7100262e09 | ||
|
|
e516943d53 | ||
|
|
fd41226cdd | ||
|
|
47b04eee3e | ||
|
|
24ee6f96e1 | ||
|
|
6e34c21175 | ||
|
|
64789ea8bc | ||
|
|
2ac7176787 | ||
|
|
b968886594 | ||
|
|
2f9c44103b | ||
|
|
e9ed4940cd | ||
|
|
85b5845ebb | ||
|
|
6f4db913aa | ||
|
|
2d20c711a6 | ||
|
|
2e829bc85f | ||
|
|
acddc3cabd | ||
|
|
a16647f16c | ||
|
|
3e3f384218 | ||
|
|
45a98a391a | ||
|
|
c9992201ce | ||
|
|
80062d262e | ||
|
|
652f7a7710 | ||
|
|
7e7e50ada2 | ||
|
|
87eba9bb89 | ||
|
|
da1fcd49fc | ||
|
|
c98289a9e7 | ||
|
|
1d0b65f798 | ||
|
|
3094185cd6 | ||
|
|
aaff04167b | ||
|
|
21bba02e6f | ||
|
|
f60e724167 | ||
|
|
7551e90899 | ||
|
|
715429c13c | ||
|
|
7976189d56 | ||
|
|
47d1eff45d | ||
|
|
073df32b01 | ||
|
|
e8ec7efa5c | ||
|
|
f08cdae2b4 | ||
|
|
1389d29d8c | ||
|
|
d4bfde7906 | ||
|
|
fe73c36958 | ||
|
|
a97f6d90f4 | ||
|
|
0735f164a8 | ||
|
|
9de0c215ba | ||
|
|
56474822f7 | ||
|
|
98b18d89eb | ||
|
|
78ea79b95a | ||
|
|
acbe503c12 | ||
|
|
0261c41574 | ||
|
|
c8182dfab3 | ||
|
|
2eee9c019f | ||
|
|
9ed40b5d9f | ||
|
|
ebc4c71c72 | ||
|
|
dec5a6370e | ||
|
|
519858b708 | ||
|
|
8fcc4cf7fe | ||
|
|
2f78350696 | ||
|
|
85d4037a89 | ||
|
|
d165bf7498 | ||
|
|
94d821a4a9 | ||
|
|
6a854f7a64 | ||
|
|
d76675f4b1 | ||
|
|
4a7e43ccb6 | ||
|
|
b53333d1ba | ||
|
|
f1f3c8bfb9 | ||
|
|
e580e7c672 | ||
|
|
a62fd96269 | ||
|
|
a1d592bb87 | ||
|
|
83f5f6cefc | ||
|
|
9166e70524 | ||
|
|
41f8fa95fe | ||
|
|
b140c21ed9 | ||
|
|
863712cb6e | ||
|
|
43f0aa7f06 | ||
|
|
12535d8142 | ||
|
|
8cf6a8bc98 | ||
|
|
7073dd26de | ||
|
|
66bfd2fad8 | ||
|
|
bb444f306a | ||
|
|
626fba4a8f | ||
|
|
9dc96bda58 | ||
|
|
1a2f1ed620 | ||
|
|
50255dd896 | ||
|
|
c8b9fda258 | ||
|
|
bb41732e59 | ||
|
|
7d9b984747 | ||
|
|
7404e5fce2 | ||
|
|
dc5414bd0d | ||
|
|
d147d36867 | ||
|
|
7763ec3c5c | ||
|
|
c61083ccd9 | ||
|
|
841aeccd6b | ||
|
|
bfcd373eec | ||
|
|
202ef41894 | ||
|
|
f112edc3c3 | ||
|
|
05fd7b18b9 | ||
|
|
9764174070 | ||
|
|
6b0370f0bc | ||
|
|
2e4313e7c3 | ||
|
|
2684d1874a | ||
|
|
882539a700 | ||
|
|
42960b7bf1 | ||
|
|
333f978186 | ||
|
|
e7e9df3261 | ||
|
|
24dbc96fe5 | ||
|
|
abc391f32a | ||
|
|
a589e8e3de | ||
|
|
39fa9a91b8 | ||
|
|
e10d98fb79 | ||
|
|
d17cdbda93 | ||
|
|
2252abefbf | ||
|
|
ce60a146dc | ||
|
|
cd4a6f3e17 | ||
|
|
fc5e1ed5d8 | ||
|
|
d70b98075e | ||
|
|
a4d9952061 | ||
|
|
45e4dfe09c | ||
|
|
34ba9063ad | ||
|
|
d5e2d59fe9 | ||
|
|
d39bdb28d6 | ||
|
|
8bf5622d21 | ||
|
|
9cafd5e099 | ||
|
|
d1b3bfd700 | ||
|
|
e60caf06f9 | ||
|
|
a243c6bb90 | ||
|
|
10523a7e27 | ||
|
|
ea06ec0133 | ||
|
|
e287ebcded | ||
|
|
2ee90289a1 | ||
|
|
193df861d1 | ||
|
|
9055496a19 | ||
|
|
5424576f73 | ||
|
|
a81c6a1c5e | ||
|
|
53a572a4a4 | ||
|
|
df283d80c6 | ||
|
|
b6d203b6dd | ||
|
|
ef988cc8af | ||
|
|
ba46905243 | ||
|
|
df0c4c8208 | ||
|
|
e5aaa1fc53 | ||
|
|
8b6df34d8b | ||
|
|
7a719aa886 | ||
|
|
9af58af5e4 | ||
|
|
b945c8c2dc | ||
|
|
2165f56aee | ||
|
|
e42de9d155 | ||
|
|
7fd95b9de2 | ||
|
|
a42c85802e | ||
|
|
cb3730100d | ||
|
|
7a04d2ad2c | ||
|
|
1f93976919 | ||
|
|
0b7505f109 | ||
|
|
392ca2d8b7 | ||
|
|
68daa90b7e | ||
|
|
fb39ad896e | ||
|
|
cb199054e3 | ||
|
|
7cf77211b0 | ||
|
|
a3d1e9a117 | ||
|
|
755c7c9895 | ||
|
|
45b5136233 | ||
|
|
f16b5cd337 | ||
|
|
21cda8141d | ||
|
|
90f26dfa45 | ||
|
|
ba507d8046 | ||
|
|
5639bcc0f5 | ||
|
|
4cd030582d | ||
|
|
7908076b39 | ||
|
|
ca91f9b702 | ||
|
|
0e86af1c9b | ||
|
|
c5ad84e2ee | ||
|
|
bfb5d6e8fe | ||
|
|
d63b0b5978 | ||
|
|
5e6b60bae0 | ||
|
|
65b6885c61 | ||
|
|
e8ceaa4085 | ||
|
|
db2b120b68 | ||
|
|
5f315d0093 | ||
|
|
016b212ff7 | ||
|
|
bccffe09e2 | ||
|
|
a0365c8e1c | ||
|
|
38156c9f7f | ||
|
|
640db09a64 | ||
|
|
5baa8c680a | ||
|
|
e1c4a488ed | ||
|
|
332d4433c4 | ||
|
|
f5d535b1ef | ||
|
|
35e226273c | ||
|
|
90e25e60d6 | ||
|
|
80659160d3 | ||
|
|
5de29928cd | ||
|
|
c65cea0f66 | ||
|
|
b3cfdbb835 | ||
|
|
9c15cceee7 | ||
|
|
8a0142bd0f | ||
|
|
3d6e05d8c0 | ||
|
|
ddf0e6a935 | ||
|
|
51852f044e | ||
|
|
17cad6b3ca | ||
|
|
8912869077 | ||
|
|
a2697634ba | ||
|
|
1bfb798107 | ||
|
|
cf60568f53 | ||
|
|
ddd18fdaf6 | ||
|
|
b04bb30930 | ||
|
|
e41aaf980b | ||
|
|
46df6601a1 | ||
|
|
5eb8b97469 | ||
|
|
8cd0536fb1 | ||
|
|
99a6789720 | ||
|
|
92670cc051 | ||
|
|
89cbbfc291 | ||
|
|
31869a7da2 | ||
|
|
68c6d6c932 | ||
|
|
e4e140fa53 | ||
|
|
b9709d0825 | ||
|
|
324e3dffa0 | ||
|
|
dd9de3fcb4 | ||
|
|
6710d0561a | ||
|
|
a30f97db0d | ||
|
|
cae666b834 | ||
|
|
5970ca49e9 | ||
|
|
6ae6218043 | ||
|
|
8e3e19a7f7 | ||
|
|
e3d51fd7f7 | ||
|
|
95f5c1d104 | ||
|
|
02a14069a3 | ||
|
|
394f28339e | ||
|
|
411998d4e3 | ||
|
|
371906445e | ||
|
|
34d91e58c0 | ||
|
|
f3f25bec7a | ||
|
|
08d74bea75 | ||
|
|
63f917b317 | ||
|
|
448633ec3c | ||
|
|
09ce3c9ce4 | ||
|
|
8fcc98102b | ||
|
|
e14075b464 | ||
|
|
ca89d14ec8 | ||
|
|
3f51998065 | ||
|
|
6e5b6a1a5c | ||
|
|
660f6c5bbf | ||
|
|
5034bd72ed | ||
|
|
5b7c144796 | ||
|
|
e45b6b9b85 | ||
|
|
245f242fb8 | ||
|
|
a6fccf93d1 | ||
|
|
3dcab90752 | ||
|
|
30a5f405f5 | ||
|
|
bad1c9628e | ||
|
|
1ab7bca43f | ||
|
|
7d0a53773c | ||
|
|
789236ee8f | ||
|
|
21cba0b080 | ||
|
|
31eb0891e5 | ||
|
|
a13f8e7937 | ||
|
|
5bfe56472f | ||
|
|
16a9ed682e | ||
|
|
59cae610d2 | ||
|
|
29ac7893d7 | ||
|
|
c8aeca8ace | ||
|
|
3da505c4c6 | ||
|
|
06bc05b708 | ||
|
|
a3dc9593c2 | ||
|
|
18a271a0ec | ||
|
|
cebd3bf751 | ||
|
|
cb04925c6e | ||
|
|
b7a340bd00 | ||
|
|
2a0f837120 | ||
|
|
d00bdff495 | ||
|
|
7e9c977b3d | ||
|
|
1629151528 | ||
|
|
24295197bd | ||
|
|
807dd84daa | ||
|
|
17848c097c | ||
|
|
563b760d31 | ||
|
|
8465a27a97 | ||
|
|
255cbd1e7e | ||
|
|
7717749b72 | ||
|
|
9e4337babf | ||
|
|
6771877a84 | ||
|
|
a860bd8dd5 | ||
|
|
704ce4ff0c | ||
|
|
21edcc8113 | ||
|
|
125738f13d | ||
|
|
02c781b8f4 | ||
|
|
453fe6aea0 | ||
|
|
e0fb7dfd14 | ||
|
|
c4012225c2 | ||
|
|
1750243537 | ||
|
|
84fc5f3008 | ||
|
|
8a224fbae4 | ||
|
|
e6c4332445 | ||
|
|
0928d1fab2 | ||
|
|
40dd588bef | ||
|
|
472e8ee92e | ||
|
|
c55af7fff3 | ||
|
|
7a39daf179 | ||
|
|
8d087ba5b1 | ||
|
|
c7a30d8728 | ||
|
|
1f678cafba | ||
|
|
579e0bc464 | ||
|
|
7f36dc5aad | ||
|
|
a7c2026128 | ||
|
|
7b0d67ebbe | ||
|
|
63f0910eac | ||
|
|
b16cf9fcc5 | ||
|
|
5501d7bc6b | ||
|
|
413ef42a34 | ||
|
|
a1694ab951 | ||
|
|
fbbbd5e0b8 | ||
|
|
f8c7e2ee41 | ||
|
|
2a4af0bc2c | ||
|
|
d24db13e9d | ||
|
|
bc68aad67b | ||
|
|
9e8a4cab7b | ||
|
|
3679964cf8 | ||
|
|
225ec1a87f | ||
|
|
071c76c06a | ||
|
|
d77a255721 | ||
|
|
26ee1aee25 | ||
|
|
b047395938 | ||
|
|
483a601867 | ||
|
|
c4bc2f6adf | ||
|
|
439e8fe33f | ||
|
|
99885f5b4b | ||
|
|
78b522ec1d | ||
|
|
dbfa5a9358 | ||
|
|
3910b3862a | ||
|
|
3b8e3b7d5c | ||
|
|
6129000dde | ||
|
|
65b274d695 | ||
|
|
a13dd0076b | ||
|
|
cc026c995c | ||
|
|
81cfacaa3d | ||
|
|
fabebc05d2 | ||
|
|
07a97bab03 | ||
|
|
69ac810c82 | ||
|
|
3f4609dc6e | ||
|
|
1ddb529d26 | ||
|
|
ab6b88073c | ||
|
|
ad3ed97472 | ||
|
|
07e902fe95 | ||
|
|
eb781ce689 | ||
|
|
555a65c8ea | ||
|
|
58429f4172 | ||
|
|
e2692a0abd | ||
|
|
d218c43edf | ||
|
|
fae9813127 | ||
|
|
adc56d4ee5 | ||
|
|
f982e33fab | ||
|
|
9ab4479fcf | ||
|
|
3e330b7929 | ||
|
|
ecddada150 | ||
|
|
0c828cbb37 | ||
|
|
84a32f8c16 | ||
|
|
4ee36fd405 | ||
|
|
0802609fd8 | ||
|
|
de88176995 | ||
|
|
aca82f400a | ||
|
|
54bb6fe527 | ||
|
|
c1f663cec1 | ||
|
|
aaf59c2d01 | ||
|
|
692942e5be | ||
|
|
f4acd629a1 | ||
|
|
0ff8a44ec3 | ||
|
|
c6d8f6e618 | ||
|
|
1baa23cc43 | ||
|
|
3be696e6bb | ||
|
|
3cfcb01603 | ||
|
|
d39a29759c | ||
|
|
bf4332b9e7 | ||
|
|
feaae7b684 | ||
|
|
be7852dfc0 | ||
|
|
b0804ce048 | ||
|
|
aa7d5fa924 | ||
|
|
b4cab12e9d | ||
|
|
133dda2de0 | ||
|
|
ef5605ae39 | ||
|
|
9b29b2702e | ||
|
|
abafa270e5 | ||
|
|
15852ccf81 | ||
|
|
a427ecd029 | ||
|
|
77228dcb2c | ||
|
|
3948cfe495 | ||
|
|
36218b54f8 | ||
|
|
be2f4433db | ||
|
|
dd5d36cf07 | ||
|
|
685136d98f | ||
|
|
852937421e | ||
|
|
a5389b7be5 | ||
|
|
1b96b18bc0 | ||
|
|
e9edad6f8c | ||
|
|
f8a6012c3a | ||
|
|
e103f6c9b6 | ||
|
|
9daf7b5a36 | ||
|
|
d6142e1af0 | ||
|
|
b0491210ff | ||
|
|
91d8550290 | ||
|
|
dfaaa7d72a | ||
|
|
5174a4f58a | ||
|
|
b9c73df488 | ||
|
|
f99f1f6441 | ||
|
|
1ec1f5d3be | ||
|
|
713db06a05 | ||
|
|
a0adcbd90b | ||
|
|
b94776421c | ||
|
|
79c1043063 | ||
|
|
cfabae17c0 | ||
|
|
71a1d25e90 | ||
|
|
c2d7b835fb | ||
|
|
f72888ce6f | ||
|
|
30f474d253 | ||
|
|
216f274a92 | ||
|
|
5ea79efe4b | ||
|
|
197c1ba21b | ||
|
|
95c4a24d99 | ||
|
|
f5f23a7c22 | ||
|
|
c469576460 | ||
|
|
0001bb0e4c | ||
|
|
56f184393f | ||
|
|
85777e85fa | ||
|
|
c750ec3cc7 | ||
|
|
c75b781d3f | ||
|
|
8aa42ff1d9 | ||
|
|
63a5bc5e12 | ||
|
|
e0b7b6dc3d | ||
|
|
3d7ecf98c2 | ||
|
|
ad5581c620 | ||
|
|
69af991772 | ||
|
|
2bb2745f91 | ||
|
|
5f682bb45f | ||
|
|
48bcb6b9a6 | ||
|
|
092c0a7392 | ||
|
|
4e8618c512 | ||
|
|
c51a69c0dc | ||
|
|
2480849f39 | ||
|
|
6bfe40607d | ||
|
|
a1c350a537 | ||
|
|
f732b1978e | ||
|
|
d3a542bac4 | ||
|
|
b12724db02 | ||
|
|
f8f5e8e869 | ||
|
|
0733f06103 | ||
|
|
6fdaa6ee8a | ||
|
|
f10b547a49 | ||
|
|
38396b2691 | ||
|
|
220f50ebe4 | ||
|
|
c989133d27 | ||
|
|
6c339551eb | ||
|
|
ef23a80b16 | ||
|
|
a521093731 | ||
|
|
747d3eedf1 | ||
|
|
fd563726f0 | ||
|
|
62136a1a96 | ||
|
|
cf16c31d38 | ||
|
|
0f5d4aa72f | ||
|
|
d8dbe77427 | ||
|
|
0e5670d50d | ||
|
|
22924a1e92 | ||
|
|
e3a7b54f90 | ||
|
|
81d5e7f014 | ||
|
|
9790c218dc | ||
|
|
6d3d51553b | ||
|
|
474591c6a5 | ||
|
|
5de0dc2713 | ||
|
|
57a34a7abb | ||
|
|
54517bda1b | ||
|
|
4291e15b60 | ||
|
|
940ae6406b | ||
|
|
6a0cf4bfd3 | ||
|
|
fee6f75223 | ||
|
|
8c93b2d54c | ||
|
|
dbbb1bd8a1 | ||
|
|
5c838b2fc5 | ||
|
|
3a094d4ba8 | ||
|
|
5d11c1ea6a | ||
|
|
a7e2e15eac | ||
|
|
585a0ca1f4 | ||
|
|
09ed82db65 | ||
|
|
a56cb0e88a | ||
|
|
9acf5ff0ef | ||
|
|
64a7790aed | ||
|
|
9a5134ffcf | ||
|
|
1d2c4d0843 | ||
|
|
efad34be62 | ||
|
|
52761ade87 | ||
|
|
e6b816e776 | ||
|
|
6d3bcba5d0 | ||
|
|
2e7ffa606b | ||
|
|
055cae90b8 | ||
|
|
23acf92803 | ||
|
|
91ee7d76eb | ||
|
|
570888e2b8 | ||
|
|
282f27f0c8 | ||
|
|
98376ade45 | ||
|
|
9682968b55 | ||
|
|
877cb5f1b0 | ||
|
|
6972573a6e | ||
|
|
742f1b1283 | ||
|
|
0b127a2f3b | ||
|
|
a6057835a8 | ||
|
|
c32794477e | ||
|
|
999ac60bda | ||
|
|
bca4b832a8 | ||
|
|
2fc1dda232 | ||
|
|
99495cff90 | ||
|
|
795962a4ba | ||
|
|
7bde9f447a | ||
|
|
b45c3e3c7f | ||
|
|
ce2a01fc59 | ||
|
|
edeea65c3e | ||
|
|
6e8ee94b91 | ||
|
|
97d8274911 | ||
|
|
73d84dc13f | ||
|
|
f32501d498 | ||
|
|
9c289c3fe9 | ||
|
|
2a610584ee | ||
|
|
8e06421567 | ||
|
|
57dc3725f5 | ||
|
|
d6ef8945f5 | ||
|
|
e078a8b7b6 | ||
|
|
a6395b9025 | ||
|
|
6c0983d77c | ||
|
|
4ae39f2bd5 | ||
|
|
71fc701809 | ||
|
|
d418921e4b | ||
|
|
4b83b022d3 | ||
|
|
8b13e5b47e | ||
|
|
0bfd565bda | ||
|
|
2224a558ee | ||
|
|
93c02fb43a | ||
|
|
93ee51cabb | ||
|
|
cd6ba554a4 | ||
|
|
89aa2be358 | ||
|
|
da261246b9 | ||
|
|
4d3e09f4a0 | ||
|
|
9add6b3696 | ||
|
|
ef037e18d0 | ||
|
|
36db791516 | ||
|
|
25d2ea5e94 | ||
|
|
34f2d18f27 | ||
|
|
4935c846aa | ||
|
|
b1f980c216 | ||
|
|
738f64c903 | ||
|
|
7235663447 | ||
|
|
9d78c416f7 | ||
|
|
9a64c026ea | ||
|
|
be2af1d17a | ||
|
|
b21131a466 | ||
|
|
41c5126472 | ||
|
|
b96cb1abaf | ||
|
|
20143606e2 | ||
|
|
1d79bcc657 | ||
|
|
e8b4494fc3 | ||
|
|
2518208c5c | ||
|
|
918fea7d4b | ||
|
|
4dc07bf63f | ||
|
|
04a0aca221 | ||
|
|
1942e72474 | ||
|
|
f256407789 | ||
|
|
67612f607e | ||
|
|
7274c42126 | ||
|
|
e288402e88 | ||
|
|
76bd4a1c7b | ||
|
|
13dbad6f35 | ||
|
|
cf089fdfe9 | ||
|
|
9755e31fa2 | ||
|
|
3fcf31d009 | ||
|
|
1cbdd0e675 | ||
|
|
e0fd84d6f5 | ||
|
|
443720c3de | ||
|
|
254526a213 | ||
|
|
ad0afb5fc9 | ||
|
|
e64b15d2a7 | ||
|
|
cdc3352318 | ||
|
|
2416618b34 | ||
|
|
c951d77a52 | ||
|
|
759ddc0b4b | ||
|
|
a0709bec51 | ||
|
|
f5bc4e7a24 | ||
|
|
b4665b6e5a | ||
|
|
b85d5b8822 | ||
|
|
12ad75a6b0 | ||
|
|
af92fe6ad1 | ||
|
|
fcf7ed4faa | ||
|
|
a58649fd10 | ||
|
|
ee5918cf4e | ||
|
|
d7de3301ed | ||
|
|
bd1988c775 | ||
|
|
474010b31d | ||
|
|
dd941154d6 | ||
|
|
cbb53745f6 | ||
|
|
3dff62e0cf | ||
|
|
719024ec01 | ||
|
|
f0659b4f3c | ||
|
|
a649f2736f | ||
|
|
046ccb1dee | ||
|
|
d340556c28 | ||
|
|
32250db126 | ||
|
|
cb8c30db2e | ||
|
|
dacd5a9e3d | ||
|
|
859d8b96a7 | ||
|
|
02981e3cee | ||
|
|
49892b3ce8 | ||
|
|
b223e7ec36 | ||
|
|
0926337c1d | ||
|
|
346e72a678 | ||
|
|
d612e47b62 | ||
|
|
c138cbf994 | ||
|
|
dfe275ab67 | ||
|
|
d6cfbdb273 | ||
|
|
8a27fa7bb5 | ||
|
|
b9a6fb1fe5 | ||
|
|
4b6194e15e | ||
|
|
e1b3f90f8b | ||
|
|
c5cc0b8c59 | ||
|
|
5c849ec666 | ||
|
|
c2a30cbe19 | ||
|
|
31eddd8da0 | ||
|
|
abf2a14ee6 | ||
|
|
e555874485 | ||
|
|
a7aef8f147 | ||
|
|
5b8c11f1a9 | ||
|
|
645040c943 | ||
|
|
fcc5ea348c | ||
|
|
3fbb45448f | ||
|
|
8a83289709 | ||
|
|
08175de069 | ||
|
|
808933cd70 | ||
|
|
384dd2adab | ||
|
|
4a37952b41 | ||
|
|
1046d264d8 | ||
|
|
bb235ab1ae | ||
|
|
ef88b83727 | ||
|
|
5612e9864e | ||
|
|
8302187dee | ||
|
|
77d07c7c4d | ||
|
|
b2265080fd | ||
|
|
0d70bd9ac0 | ||
|
|
8f63d59b81 | ||
|
|
0395c0cc95 | ||
|
|
72065412a3 | ||
|
|
7af2cdd0ef | ||
|
|
67c801c4e7 | ||
|
|
f7c8ad4c5a | ||
|
|
61092b34de | ||
|
|
1e9e96fb8c | ||
|
|
17d1ced07c | ||
|
|
a26ced04ff | ||
|
|
154b5b6ded | ||
|
|
8219dbef6f | ||
|
|
3da724e76a | ||
|
|
1616d237ae | ||
|
|
3d21facc9f | ||
|
|
fead2a08a3 | ||
|
|
d8acf24af0 | ||
|
|
97412b3c79 | ||
|
|
6145913068 | ||
|
|
e56bb6785f | ||
|
|
6d63d6c54a | ||
|
|
6e8a103e70 | ||
|
|
00d3530f64 | ||
|
|
1e6ca18193 | ||
|
|
5a32992497 | ||
|
|
79ba00a2f4 | ||
|
|
fdfaf8ec70 | ||
|
|
162c58b697 | ||
|
|
75959ca692 | ||
|
|
06fbcd19c8 | ||
|
|
36e0468a99 | ||
|
|
c4d85cf928 | ||
|
|
daf8495acb | ||
|
|
9999ded933 | ||
|
|
80128e8293 | ||
|
|
9579b2c85e | ||
|
|
eb7f184848 | ||
|
|
71b21920e2 | ||
|
|
91b787a5be | ||
|
|
11f659aadb | ||
|
|
0870dd15a8 | ||
|
|
7cd52041d8 | ||
|
|
f2bcb49478 | ||
|
|
60a4bda94f | ||
|
|
54cfc46500 | ||
|
|
c09559ade5 | ||
|
|
3e9aea1f7a | ||
|
|
f0c292f508 | ||
|
|
1444ba2c35 | ||
|
|
fac4c85133 | ||
|
|
97a09e0571 | ||
|
|
c7121516c1 | ||
|
|
56a5fbf63b | ||
|
|
80604a752b | ||
|
|
3fe6e18609 | ||
|
|
47f3f30d83 | ||
|
|
085a904970 | ||
|
|
0fa7723f51 | ||
|
|
84ea483b4c | ||
|
|
4de3aa96b6 | ||
|
|
1193943228 | ||
|
|
2959faca0c | ||
|
|
d0df4fa3a0 | ||
|
|
0a301dd2f9 | ||
|
|
43e3679297 | ||
|
|
0455063854 | ||
|
|
12d90d77c7 | ||
|
|
61d7749aaf | ||
|
|
69fb4a0c37 | ||
|
|
d2c9f058f8 | ||
|
|
acaa3afa0f | ||
|
|
3c58e21cbf | ||
|
|
9d3c5b9bc6 | ||
|
|
ce34358d46 | ||
|
|
278bcec238 | ||
|
|
128f458848 | ||
|
|
7ae2bcd6db | ||
|
|
130f252f97 | ||
|
|
d339e05a3b | ||
|
|
933f4813aa | ||
|
|
f14e71c1d6 | ||
|
|
d78dbd7b0f | ||
|
|
71a6ef6eba | ||
|
|
4873862d16 | ||
|
|
425efe3d72 | ||
|
|
666dc13c01 | ||
|
|
267f9f5759 | ||
|
|
c8d8c00f84 | ||
|
|
dcc567ebda | ||
|
|
a199a3ccc3 | ||
|
|
8159e07427 | ||
|
|
32d70758c5 | ||
|
|
3a9e284f29 | ||
|
|
c658daeac6 | ||
|
|
f605169b15 | ||
|
|
f82b495bcc | ||
|
|
33b44a87c5 | ||
|
|
a88d994fcb | ||
|
|
dd9224d9a6 | ||
|
|
48728efee0 | ||
|
|
043c55ae66 | ||
|
|
fa3f822cf1 | ||
|
|
5afab126b3 | ||
|
|
bee8378795 | ||
|
|
cbd42f4a58 | ||
|
|
3333fd7fc8 | ||
|
|
142258b4b5 | ||
|
|
01213b58ba | ||
|
|
51df7032bb | ||
|
|
c033ce5ecf | ||
|
|
bfaf57cfd0 | ||
|
|
6c523570e5 | ||
|
|
047e5bdd81 | ||
|
|
6b1559d397 | ||
|
|
a3167b8152 | ||
|
|
cbeb23f16a | ||
|
|
b4a6512784 | ||
|
|
1af2742013 | ||
|
|
8048938158 | ||
|
|
3689c7f982 | ||
|
|
96c7057fee | ||
|
|
df7c4c82a9 | ||
|
|
18d8db338b | ||
|
|
9637639e31 | ||
|
|
17abe8f164 | ||
|
|
badaaece04 | ||
|
|
56119eebae | ||
|
|
c17c8e2a2c | ||
|
|
e45a60f2ed | ||
|
|
ebe9356937 | ||
|
|
0094305299 | ||
|
|
b27050d3bf | ||
|
|
d941e596ad | ||
|
|
ddaec92da5 | ||
|
|
1c0a4c92a8 | ||
|
|
383c00afbd | ||
|
|
d3b2bff8ad | ||
|
|
77aaa6141d | ||
|
|
2bb1af61d0 | ||
|
|
40b29472b3 | ||
|
|
5d5029de59 | ||
|
|
cc96e6a222 | ||
|
|
581c9438ca | ||
|
|
54bd427f26 | ||
|
|
c55d184195 | ||
|
|
b279f94789 | ||
|
|
f3a897e99a | ||
|
|
392b1b5904 | ||
|
|
4daa8066a6 | ||
|
|
8a89951286 | ||
|
|
c41ed55291 | ||
|
|
caba0499d7 | ||
|
|
13deacfc3b | ||
|
|
7231085456 | ||
|
|
663bb4aa10 | ||
|
|
18721b4542 | ||
|
|
19232722e2 | ||
|
|
d2eca68484 | ||
|
|
091167478e | ||
|
|
547b8abc56 | ||
|
|
dbfac45d59 | ||
|
|
086f6de867 | ||
|
|
0f93091848 | ||
|
|
ac19f62ed9 | ||
|
|
8fcef63890 | ||
|
|
34cd2c1afa | ||
|
|
5d1a19d49c | ||
|
|
2fbb320539 | ||
|
|
b50e5ed7e6 | ||
|
|
26c82ca72c | ||
|
|
f8a8a431a8 | ||
|
|
fd27867195 | ||
|
|
5e2a627795 | ||
|
|
e081401c2d | ||
|
|
e62a268aac | ||
|
|
140a9e5191 | ||
|
|
51b5f8046f | ||
|
|
91b965d1aa | ||
|
|
f8faccee59 | ||
|
|
6ac46a36a7 | ||
|
|
27572ba4bc | ||
|
|
21be832161 | ||
|
|
5769632793 | ||
|
|
9efd959ed8 | ||
|
|
5c72afe17e | ||
|
|
dc24319333 | ||
|
|
1acbdf94aa | ||
|
|
44229dfe51 | ||
|
|
13206227d9 | ||
|
|
e3e8a57df6 | ||
|
|
7afff80984 | ||
|
|
500522418e | ||
|
|
5ccac5a5c3 | ||
|
|
69822f4401 | ||
|
|
f290cfbd35 | ||
|
|
fae30f59ce | ||
|
|
a15f303c0a | ||
|
|
5e85038d78 | ||
|
|
4e7b35c54e | ||
|
|
48dde310ae | ||
|
|
6b041e7657 | ||
|
|
69576ee1e1 | ||
|
|
bd3105f62e | ||
|
|
02e3f0e71b | ||
|
|
74bf981f3d | ||
|
|
32f0678692 | ||
|
|
e74753bec1 | ||
|
|
4a8c9ad17f | ||
|
|
0c6c882f50 | ||
|
|
579f25c1ba | ||
|
|
d5eb5c8816 | ||
|
|
26d04b5f9c | ||
|
|
a0b9684504 | ||
|
|
402b6d1f0c | ||
|
|
e87a5fa354 | ||
|
|
63574f643e | ||
|
|
1d6b1020c3 | ||
|
|
96337a3230 | ||
|
|
950c59d8b4 | ||
|
|
9cfd095281 | ||
|
|
996e049863 | ||
|
|
6e42817c3e | ||
|
|
7758344a57 | ||
|
|
b5df566b6b | ||
|
|
e21fd19a70 | ||
|
|
98672c8e44 | ||
|
|
2e92f0640b | ||
|
|
d6b314f573 | ||
|
|
d7a150ffa8 | ||
|
|
955da6be30 | ||
|
|
b5f8e6738a | ||
|
|
d7fa8c9795 | ||
|
|
acb7a5b3c8 | ||
|
|
783a57ef89 | ||
|
|
d33ccce683 | ||
|
|
d722bf10b9 | ||
|
|
41337475a2 | ||
|
|
f504a069d7 | ||
|
|
96f79688dd | ||
|
|
c330898aeb | ||
|
|
94952cf0a4 | ||
|
|
a3c249abe9 | ||
|
|
c26562fab8 | ||
|
|
b7cbd99212 | ||
|
|
b53ef35a85 | ||
|
|
1ebb5fefb1 | ||
|
|
164b3edd41 | ||
|
|
819ea36cca | ||
|
|
088c9b9e4e | ||
|
|
ab25a9b8ee | ||
|
|
b2aea9f8f0 | ||
|
|
5399b95936 | ||
|
|
1d5c120748 | ||
|
|
6be7301e69 | ||
|
|
fff9f2b17d | ||
|
|
87a7d2e45c | ||
|
|
58803ef40d | ||
|
|
b27ca993aa | ||
|
|
28ecc6bcbf | ||
|
|
30c5e3f237 | ||
|
|
6a25a3a3d0 | ||
|
|
9a84c0ae30 | ||
|
|
133cc5457d | ||
|
|
05e4c5b507 | ||
|
|
8372717a09 | ||
|
|
5f3f2a96fd | ||
|
|
b83c6f0c28 | ||
|
|
36dc0f3e3c | ||
|
|
b4edde6f90 | ||
|
|
395eb5581c | ||
|
|
6dab251055 | ||
|
|
3bd6a6a614 | ||
|
|
93d21a0d1d | ||
|
|
2c2a73e320 | ||
|
|
a88a3c9ea3 | ||
|
|
e4df70cd56 | ||
|
|
90f7d35150 | ||
|
|
75b19a60c1 | ||
|
|
9fc921f49f | ||
|
|
4b9e90ac72 | ||
|
|
36dd6c83e0 | ||
|
|
d790313221 | ||
|
|
c3895f06c0 | ||
|
|
76e81fa651 | ||
|
|
d63a1b0eb0 | ||
|
|
4dffac3c78 | ||
|
|
88391b0274 | ||
|
|
c3cea0238e | ||
|
|
5bf4266973 | ||
|
|
ed57c54d06 | ||
|
|
01936e000e | ||
|
|
80137848dc | ||
|
|
149575e0c4 | ||
|
|
6a66b462ce | ||
|
|
1e31f6fe77 | ||
|
|
f173b3ff09 | ||
|
|
baffab62ce | ||
|
|
d141cec2a8 | ||
|
|
df87ac3810 | ||
|
|
46960ce0a4 | ||
|
|
522206b0bc | ||
|
|
24d60dd3ab | ||
|
|
dbfdc2fd14 | ||
|
|
a97912e297 | ||
|
|
cedba501ee | ||
|
|
ac947d3bda | ||
|
|
243ba371b3 | ||
|
|
3af24eafbb | ||
|
|
69b6022afe | ||
|
|
1cd66419d4 | ||
|
|
5309a02f6d | ||
|
|
541784382a | ||
|
|
2a7fae2c82 | ||
|
|
ed3a5448b1 | ||
|
|
2eb4769bbc | ||
|
|
02019000df | ||
|
|
6262f51c96 | ||
|
|
d38ff5759a | ||
|
|
f6de2e4dd3 | ||
|
|
3959f447d4 | ||
|
|
14a6a69dc0 | ||
|
|
3191182009 | ||
|
|
e210dc748f | ||
|
|
724dcb557f | ||
|
|
11d274ff07 | ||
|
|
ae246b1005 | ||
|
|
ca0250280a | ||
|
|
f49d6e15a4 | ||
|
|
7172cfd889 | ||
|
|
89e9652075 | ||
|
|
53098edc85 | ||
|
|
e9968fbab3 | ||
|
|
bc10fd0aba | ||
|
|
76b4922bbe | ||
|
|
8e790c4745 | ||
|
|
c331b180cb | ||
|
|
30e3b02867 | ||
|
|
bedea4a093 | ||
|
|
5372cfba20 | ||
|
|
19dac306d1 | ||
|
|
e9a98a3402 | ||
|
|
7020749987 | ||
|
|
2d62d090df | ||
|
|
e6f59062fa | ||
|
|
0d3092d62c | ||
|
|
af0921c5b2 | ||
|
|
137454beff | ||
|
|
a0cc93b813 | ||
|
|
ff516f63b4 | ||
|
|
68acb78ae6 | ||
|
|
949aaaff2a | ||
|
|
390544cd9a | ||
|
|
766a923f86 | ||
|
|
202ce14afa | ||
|
|
8ff9d8123d | ||
|
|
2715367411 | ||
|
|
44b61d9252 | ||
|
|
565bba6e55 | ||
|
|
2ecfb7af64 | ||
|
|
998d3b2cc1 | ||
|
|
c923d643b9 | ||
|
|
bc39c721e3 | ||
|
|
5096ff6496 | ||
|
|
a11f623b17 | ||
|
|
f7ffb30bc6 | ||
|
|
c7ddb54e67 | ||
|
|
c4108feb15 | ||
|
|
47cad71fcd | ||
|
|
0b56b1685a | ||
|
|
c414e1a168 | ||
|
|
22bebb6bbc | ||
|
|
c3f9742bb6 | ||
|
|
3defccb6e2 | ||
|
|
1197b17509 | ||
|
|
265e28e7c3 | ||
|
|
9c758f1628 | ||
|
|
5f4975ca5c | ||
|
|
e5bcc793e5 | ||
|
|
cd5b0e74e3 | ||
|
|
3162d2c17d | ||
|
|
6a54fb649c | ||
|
|
8031ed7fe1 | ||
|
|
dcbf5affda | ||
|
|
027c8dcd41 | ||
|
|
94b46a2173 | ||
|
|
b66f7f6c8b | ||
|
|
355e4a5c67 | ||
|
|
1e7cb7f13e | ||
|
|
d58837421f | ||
|
|
19d5358b97 | ||
|
|
a6551c320d | ||
|
|
59a73817c8 | ||
|
|
1231f3eeb1 | ||
|
|
cd40c1dacd | ||
|
|
33286a5bde | ||
|
|
b61c35aff9 | ||
|
|
2a3e1ba62e | ||
|
|
d95f3277b2 | ||
|
|
e0a2c8cb8a | ||
|
|
0aa46a57fe | ||
|
|
53e3cd143e | ||
|
|
8381039f97 | ||
|
|
a91c61e5e2 | ||
|
|
f90efd7e38 | ||
|
|
fab142dd38 | ||
|
|
125d546cea | ||
|
|
179f7ecc23 | ||
|
|
0fc30a70ca | ||
|
|
f81509a892 | ||
|
|
fc4488f17f | ||
|
|
2748f3d22e | ||
|
|
e0d42cbe9c | ||
|
|
1780d9417f | ||
|
|
5f182b1e62 | ||
|
|
80cee8367f | ||
|
|
7325d5657d | ||
|
|
72182c7caa | ||
|
|
d7ca3801be | ||
|
|
2ef714f87b | ||
|
|
46940eb15a | ||
|
|
3485983553 | ||
|
|
861ebc151a | ||
|
|
ae5df938b6 | ||
|
|
e224a7f0c7 | ||
|
|
aa4684077d | ||
|
|
193e196c46 | ||
|
|
6e7a42065c | ||
|
|
2c987d2912 | ||
|
|
19f5dc12fb | ||
|
|
38b59aa1a9 | ||
|
|
201b59cd4c | ||
|
|
de3afe27c1 | ||
|
|
7bee8e73f0 | ||
|
|
17e0fbbaf8 | ||
|
|
ec486e89e4 | ||
|
|
ec3b000264 | ||
|
|
90a513dc3d | ||
|
|
32a74e3416 | ||
|
|
1d90bf9230 | ||
|
|
81943b12c0 | ||
|
|
18fa7c2832 | ||
|
|
dd02bf809e | ||
|
|
2f3a1a5d79 | ||
|
|
18654cb011 | ||
|
|
5954f80645 | ||
|
|
3e36357315 | ||
|
|
a06a454ade | ||
|
|
c0fab4b41f | ||
|
|
80b79f3636 | ||
|
|
b97ae25f93 | ||
|
|
4722ea6096 | ||
|
|
3e9781033a | ||
|
|
62b296cf85 | ||
|
|
dcbaa52683 | ||
|
|
1e29f61656 | ||
|
|
89319dc7aa | ||
|
|
5755afc4ea | ||
|
|
d00c434ccc | ||
|
|
4ada59ba48 | ||
|
|
14ccc38f4c | ||
|
|
a8be299bbd | ||
|
|
2e87f5ff85 | ||
|
|
829ebba622 | ||
|
|
81019fa2a8 | ||
|
|
1bc3675548 | ||
|
|
678ec979c9 | ||
|
|
d1a48ae641 | ||
|
|
eded86bc10 | ||
|
|
5a7a49d1d5 | ||
|
|
d3de8049bf | ||
|
|
d09dd90a73 | ||
|
|
78203eb9be | ||
|
|
31c7d4e36f | ||
|
|
d3949806f5 | ||
|
|
28ff15c99e | ||
|
|
bd2c74fe90 | ||
|
|
fb6995db94 | ||
|
|
8c2c4d3282 | ||
|
|
b81392b818 | ||
|
|
ad08440023 | ||
|
|
ec17623400 | ||
|
|
288eca7849 | ||
|
|
b5f202150e | ||
|
|
539bf0cb30 | ||
|
|
9385d62c89 | ||
|
|
897f916552 | ||
|
|
914cf6753e | ||
|
|
392208057e | ||
|
|
8d2cbf806d | ||
|
|
ab5025efbf | ||
|
|
912e51d863 | ||
|
|
d1c4ca5eae | ||
|
|
16bb4a148e | ||
|
|
d7071cb67f | ||
|
|
a13fb3d7c5 | ||
|
|
85558904a4 | ||
|
|
88ed72fdc1 | ||
|
|
586a93d7aa | ||
|
|
a446699c6c | ||
|
|
f1c459fd6c | ||
|
|
752f0a988b | ||
|
|
719b266691 | ||
|
|
dcba5762c7 | ||
|
|
0757717bea | ||
|
|
a9f762aa96 | ||
|
|
db45f46d9e | ||
|
|
f00653964e | ||
|
|
e0154f9314 | ||
|
|
4408a3a354 | ||
|
|
5abf5a157d | ||
|
|
d55d5f60a5 | ||
|
|
feacf2c359 | ||
|
|
72ea1601e9 | ||
|
|
02cfd8d978 | ||
|
|
728377cba3 | ||
|
|
f2283b56b9 | ||
|
|
b515f2a599 | ||
|
|
4354b49496 | ||
|
|
994312c212 | ||
|
|
5f25e0d4e2 | ||
|
|
4bbe571f5f | ||
|
|
c58d21375c | ||
|
|
ed128c558f | ||
|
|
828932d95c | ||
|
|
42ced309d3 | ||
|
|
1fcd306879 | ||
|
|
26c9ad7f06 | ||
|
|
4b7b01ce0a | ||
|
|
6632154906 | ||
|
|
ca67c7ef43 | ||
|
|
c4039a929c | ||
|
|
a1b74e1220 | ||
|
|
01f6188824 | ||
|
|
283f31a734 | ||
|
|
b800ce9ebe | ||
|
|
eb9d54465c | ||
|
|
eb9e1f81d4 | ||
|
|
ddfbd70b55 | ||
|
|
51e9ab83ad | ||
|
|
5ed7e03b2e | ||
|
|
0cb8d5b12f | ||
|
|
b4548c1af2 | ||
|
|
0a396f3112 | ||
|
|
2aa18dc88f | ||
|
|
8d89983eeb | ||
|
|
573c470965 | ||
|
|
daa0c751e2 | ||
|
|
e1babefc80 | ||
|
|
ce99b0e162 | ||
|
|
4c3a23b1bd | ||
|
|
12016af326 | ||
|
|
fc9871b075 | ||
|
|
3cb3c1c6d8 | ||
|
|
8fe3474589 | ||
|
|
dc3c565ec0 | ||
|
|
350c7d7081 | ||
|
|
0428bc8fe3 | ||
|
|
514a8575a2 | ||
|
|
fc11377552 | ||
|
|
fbaa70a2ae | ||
|
|
f30c11ddf4 | ||
|
|
51c4ecc879 | ||
|
|
080421f2fb | ||
|
|
8cc61ca8b8 | ||
|
|
400754c5b3 | ||
|
|
05e82c24f8 | ||
|
|
97c15c7876 | ||
|
|
36e7cc8e89 | ||
|
|
ad2ce4de53 | ||
|
|
2a28f7c4d2 | ||
|
|
0cdbe080a6 | ||
|
|
96fe1a7da1 | ||
|
|
4f73af2920 | ||
|
|
7ca4e49bf5 | ||
|
|
dfa63297fc | ||
|
|
402e8c1897 | ||
|
|
33e814122e | ||
|
|
f28a4df5ee | ||
|
|
417aa2815c | ||
|
|
2fdabe37aa | ||
|
|
d8270dfb6a | ||
|
|
b75f662c41 | ||
|
|
0d9d0845f5 | ||
|
|
5d66a1f298 | ||
|
|
9bf52eb63a | ||
|
|
0c9d72a9a1 | ||
|
|
c2c2bd2df6 | ||
|
|
78fc594d84 | ||
|
|
53b1e7c573 | ||
|
|
c14404a7c1 | ||
|
|
96e6750f9c | ||
|
|
dbafd05a4e | ||
|
|
d7fd578cc3 | ||
|
|
0e8572e1db | ||
|
|
98a997bd8e | ||
|
|
6fbc95cc41 | ||
|
|
1852f74e8b | ||
|
|
55e3e16860 | ||
|
|
7cf2a55b43 | ||
|
|
c41cbbd2da | ||
|
|
7283bb046b | ||
|
|
dd2df6cbcf | ||
|
|
01f1cac811 | ||
|
|
aaa112c3f3 | ||
|
|
dc0fa9c64d | ||
|
|
f749b38aa2 | ||
|
|
43d79673e1 | ||
|
|
fc841f572c | ||
|
|
df9f117a08 | ||
|
|
bedc46a1be | ||
|
|
a7c6095be5 | ||
|
|
f2d5cf785e | ||
|
|
f926ad9b5e | ||
|
|
68acf969f2 | ||
|
|
65d53e800b | ||
|
|
e44d8ac7e9 | ||
|
|
d1823dd6e1 | ||
|
|
80bfd3b8e9 | ||
|
|
07128cc7fe | ||
|
|
0892f0b0bb | ||
|
|
8cf7f3ac08 | ||
|
|
ab2bb51879 | ||
|
|
8cb98b5365 | ||
|
|
782c5f8a34 | ||
|
|
d6b5c5f63e | ||
|
|
ef6dfa6d91 | ||
|
|
3840ee8ee7 | ||
|
|
f47e40fa2a | ||
|
|
49a4a61876 | ||
|
|
669c1a6f0d | ||
|
|
15342190fa | ||
|
|
c98ef6e8a9 | ||
|
|
d36d5936da | ||
|
|
70de75ae3d | ||
|
|
ea62e33e51 | ||
|
|
524e92c81a | ||
|
|
d5a26b1d70 | ||
|
|
cd7649a666 | ||
|
|
847cc0a38a | ||
|
|
3588eb9bf8 | ||
|
|
296e22d3ca | ||
|
|
923e590da1 | ||
|
|
e56329d05a | ||
|
|
2452e80e57 | ||
|
|
b9019831a9 | ||
|
|
b3b49b440e | ||
|
|
cbedf0f74f | ||
|
|
6c980eba68 | ||
|
|
e9f766904e | ||
|
|
62d76c006d | ||
|
|
615b78a612 | ||
|
|
aaf0340d6c | ||
|
|
93ea437760 | ||
|
|
fe6a1244ed | ||
|
|
953858b0e7 | ||
|
|
f3da46833f | ||
|
|
ba2488b6e2 | ||
|
|
4e7e0d67e5 | ||
|
|
faa16b207c | ||
|
|
2d9f62761c | ||
|
|
60689f531d | ||
|
|
3f97692311 | ||
|
|
81221336f4 | ||
|
|
ecfc758ce4 | ||
|
|
2cb3a132c6 | ||
|
|
519473e498 | ||
|
|
9b271d918a | ||
|
|
5b4d874851 | ||
|
|
9dc1dbc002 | ||
|
|
7ee323f0d5 | ||
|
|
4bb6f711b9 | ||
|
|
816cec8f5c | ||
|
|
2394dfe88a | ||
|
|
dfc0d2303b | ||
|
|
598a48ec50 | ||
|
|
9f41e98e3f | ||
|
|
489891ff3c | ||
|
|
a44ed0b047 | ||
|
|
1aaa97b91d | ||
|
|
13d68e789f | ||
|
|
10dae7482c | ||
|
|
979de190dc | ||
|
|
46bcbda972 | ||
|
|
43bdc353fb | ||
|
|
6314676b9c | ||
|
|
9f77f3d968 | ||
|
|
f4733dbbe8 | ||
|
|
61fc0e4893 | ||
|
|
652ad86cad | ||
|
|
60f308bd76 | ||
|
|
b2113ff95b | ||
|
|
b8188a46a9 | ||
|
|
e790b0761e | ||
|
|
fa3317f6cd | ||
|
|
b080f7a81c | ||
|
|
57a1c5eb76 | ||
|
|
5b7e2521cc | ||
|
|
fced653c1c | ||
|
|
9253e6ac5e | ||
|
|
aff2ed22b9 | ||
|
|
5520a52379 | ||
|
|
505050e936 | ||
|
|
0437835bde | ||
|
|
77386f8dfc | ||
|
|
529fdf0341 | ||
|
|
fa74700ad9 | ||
|
|
caca491943 | ||
|
|
4794f44a6d | ||
|
|
f0119b1231 | ||
|
|
4c579aa826 | ||
|
|
b9463b3c0e | ||
|
|
cede6ce8a9 | ||
|
|
956e6e3638 | ||
|
|
b282c7d815 | ||
|
|
5b6142e2d9 | ||
|
|
58313ad8fc | ||
|
|
7aff148e9f | ||
|
|
ee5c61fb74 | ||
|
|
91be6c8028 | ||
|
|
2e0bd2ec28 | ||
|
|
a0151fbc04 | ||
|
|
d853782fea | ||
|
|
c6638a7154 | ||
|
|
4abebe3aec | ||
|
|
5aed8f928a | ||
|
|
afe6cfc383 | ||
|
|
174983ce1e | ||
|
|
1313c7b3bf | ||
|
|
bee622bcad | ||
|
|
8cefdeae49 | ||
|
|
7cf70b9607 | ||
|
|
add4b582b5 | ||
|
|
b419d21940 | ||
|
|
227ff4e13a | ||
|
|
8e6e70683e | ||
|
|
626f0aec6a | ||
|
|
973d174a30 | ||
|
|
9f35690b95 | ||
|
|
d961f2bc13 | ||
|
|
10e8277044 | ||
|
|
b4c40d9b18 | ||
|
|
e1603f0808 | ||
|
|
d4c2350d63 | ||
|
|
5e5470ae12 | ||
|
|
6230a99360 | ||
|
|
801adb6af7 | ||
|
|
8b9b96f422 | ||
|
|
160684f23e | ||
|
|
88930a9de4 | ||
|
|
af6b8c9b88 | ||
|
|
bf52d3189d | ||
|
|
5d872e8b5b | ||
|
|
4832d8007b | ||
|
|
879e084383 | ||
|
|
1ae16bb657 | ||
|
|
2177bc68db | ||
|
|
c8b887fe32 | ||
|
|
7205242f38 | ||
|
|
be33b5b387 | ||
|
|
66a123f4b9 | ||
|
|
76a4ee22b3 | ||
|
|
3a6bf4d20b | ||
|
|
09cf105cdc | ||
|
|
ce02e086a3 | ||
|
|
e8d4313454 | ||
|
|
49046f88cf | ||
|
|
5e2f32e3eb | ||
|
|
b4a1393bf6 | ||
|
|
0ccee9d806 | ||
|
|
5700272dc4 | ||
|
|
7e5a3925b3 | ||
|
|
1febe32e73 | ||
|
|
6059c982cf | ||
|
|
de7b973239 | ||
|
|
741113a607 | ||
|
|
c20daa8867 | ||
|
|
5e57a7d5c0 | ||
|
|
7c8d27700e | ||
|
|
f8621be5ee | ||
|
|
8ed87e10b4 | ||
|
|
ca26fa2dd0 | ||
|
|
1669095fb1 | ||
|
|
13eed48877 | ||
|
|
07494e6eff | ||
|
|
41b1d72825 | ||
|
|
4d2965e74f | ||
|
|
12526bfca4 | ||
|
|
3e2c344140 | ||
|
|
33730dd584 | ||
|
|
1cb03b1882 | ||
|
|
2f0a6db414 | ||
|
|
a42328d971 | ||
|
|
96ff51d598 | ||
|
|
7c3d3af681 | ||
|
|
6942bca5a7 | ||
|
|
9eac9a190e | ||
|
|
40e4fbd9a6 | ||
|
|
7b6996ed80 | ||
|
|
ef2827a9d8 | ||
|
|
dbf98ce3a6 | ||
|
|
ee7cb5123d | ||
|
|
77e1d6ab8a | ||
|
|
1640bed7a5 | ||
|
|
c6fd5cbbce | ||
|
|
5288041da5 | ||
|
|
a848fb1dd1 | ||
|
|
172945434c | ||
|
|
a0967e0281 | ||
|
|
deab717c73 | ||
|
|
5db5a088ea | ||
|
|
5e106d2f19 | ||
|
|
940da6bc73 | ||
|
|
d110bff253 | ||
|
|
4caa329c36 | ||
|
|
7c7d279337 | ||
|
|
831e6ee76d | ||
|
|
b01842ad02 | ||
|
|
7acf91883a | ||
|
|
897f6fdd3e | ||
|
|
6f9c3fbc38 | ||
|
|
bcbd1a5b4b | ||
|
|
6468efd7e4 | ||
|
|
66863789c7 | ||
|
|
b30058fc29 | ||
|
|
a41cd0b070 | ||
|
|
ea8b905f02 | ||
|
|
16b301236b | ||
|
|
22162dda83 | ||
|
|
024af89be3 | ||
|
|
e1a697658e | ||
|
|
0a5051f959 | ||
|
|
82b6b60b31 | ||
|
|
06b3c3636a | ||
|
|
e5712a9474 | ||
|
|
6ff845c53b | ||
|
|
1c225ed04d | ||
|
|
612b738968 | ||
|
|
87356ef594 | ||
|
|
20a4dfa6d6 | ||
|
|
524cc97f51 | ||
|
|
fc849fb51a | ||
|
|
c88651939a | ||
|
|
01254f67f0 | ||
|
|
212ed33b21 | ||
|
|
bd3f8aef21 | ||
|
|
3785acea29 | ||
|
|
866e2f1762 | ||
|
|
0a705d6752 | ||
|
|
4f74fd5354 | ||
|
|
3df9a5d01c | ||
|
|
2682829bd3 | ||
|
|
30cfa2607d | ||
|
|
4467c2e6ad | ||
|
|
2c0e8a0ddf | ||
|
|
4c17f32a2e | ||
|
|
674220087f | ||
|
|
fc8d5d5799 | ||
|
|
4e4f04c174 | ||
|
|
65c21f3392 | ||
|
|
97fb30cdf1 | ||
|
|
c975279589 | ||
|
|
b3ee7a635f | ||
|
|
c2b12868bb | ||
|
|
bdbb03e16f | ||
|
|
53138d7c16 | ||
|
|
18d1d35104 | ||
|
|
99d9de35de | ||
|
|
e6a48481a5 | ||
|
|
21f2b1224a | ||
|
|
187f47a353 | ||
|
|
cb75b3d6d2 | ||
|
|
27708b9ecb | ||
|
|
5443165fce | ||
|
|
dfb006d264 | ||
|
|
4f00f62674 | ||
|
|
cec527ffd5 | ||
|
|
4f2fe5c988 | ||
|
|
2bf552f801 | ||
|
|
477fa8fedf | ||
|
|
f1c7c5f02c | ||
|
|
4e936193c7 | ||
|
|
f9895c22b6 | ||
|
|
6e46537112 | ||
|
|
92b3918b26 | ||
|
|
34c615a5fd | ||
|
|
fa29260e09 | ||
|
|
a05816b495 | ||
|
|
9c5d847dae | ||
|
|
082e330f9d | ||
|
|
8878c4a136 | ||
|
|
00926bac81 | ||
|
|
67b0907e52 | ||
|
|
dba479774b | ||
|
|
5c8a1e632b | ||
|
|
d65e29111f | ||
|
|
ed170b8092 | ||
|
|
ac36fef968 | ||
|
|
10f6b73cad | ||
|
|
13d933fc07 | ||
|
|
3ba98e86f2 | ||
|
|
1dfca525cf | ||
|
|
e51ad1a68a | ||
|
|
9c76700f74 | ||
|
|
773b1af6f1 | ||
|
|
b8b633a6cc | ||
|
|
ada2270bd1 | ||
|
|
25e5175c54 | ||
|
|
9dcd770df2 | ||
|
|
00a1948b73 | ||
|
|
859a5a5e0c | ||
|
|
6783cf57da | ||
|
|
a8dc5ac14a | ||
|
|
6182553d79 | ||
|
|
905a0cdac1 | ||
|
|
f20ddcf26d | ||
|
|
fe26098c2b | ||
|
|
2354a0a640 | ||
|
|
7c3f09c70c | ||
|
|
caf5350fe6 | ||
|
|
84084835ce | ||
|
|
7f38a67ede | ||
|
|
54383b84ec | ||
|
|
948910bfa2 | ||
|
|
ce5dac07d3 | ||
|
|
d4b0057423 | ||
|
|
de8b12f594 | ||
|
|
69e31a0a37 | ||
|
|
c383cd39f4 | ||
|
|
e88d2f0ab2 | ||
|
|
32535578a0 | ||
|
|
2e9af8fef0 | ||
|
|
0aa87aacc6 | ||
|
|
bc38c302ed | ||
|
|
c724902508 | ||
|
|
73604020d7 | ||
|
|
060a00b9c7 | ||
|
|
2cd32bf25a | ||
|
|
81ad1a18fa | ||
|
|
1d94903eef | ||
|
|
2d7dcb495c | ||
|
|
bc580c9aaa | ||
|
|
f8e2740532 | ||
|
|
a079ecd20b | ||
|
|
89c8725766 | ||
|
|
da7d532564 | ||
|
|
4752d4966d | ||
|
|
50f47f1ed5 | ||
|
|
26bdc3e1d5 | ||
|
|
764360c0f3 | ||
|
|
08482577eb | ||
|
|
cabbba2448 | ||
|
|
c48ed7bb0d | ||
|
|
fa4c0e82bd | ||
|
|
c0bf8b9c25 | ||
|
|
d2cff86c97 | ||
|
|
03c0be0720 | ||
|
|
a3361363e4 | ||
|
|
40e142c5c7 | ||
|
|
2703efedef | ||
|
|
c313f1a20b | ||
|
|
b7116657d9 | ||
|
|
2bf3f1b94b | ||
|
|
b78a7971d9 | ||
|
|
5079bc4976 | ||
|
|
555032c15c | ||
|
|
bcff327565 | ||
|
|
e95ff989b1 | ||
|
|
018e8999e2 | ||
|
|
f624a1817e | ||
|
|
202a643e5a | ||
|
|
c75b9c9221 | ||
|
|
9a6f5ba1a8 | ||
|
|
052194714a | ||
|
|
5ccb4a8b7f | ||
|
|
7c3cec0451 | ||
|
|
b79d3b31b3 | ||
|
|
bf5d43d0cf | ||
|
|
f2ac570949 | ||
|
|
37636ef1eb | ||
|
|
c540bec824 | ||
|
|
9288cfa684 | ||
|
|
b1de8b703b | ||
|
|
ccd0777ce9 | ||
|
|
49b67b3e18 | ||
|
|
bc97168dc7 | ||
|
|
0ec04df334 | ||
|
|
26d7946b4a | ||
|
|
292cc579cf | ||
|
|
8492482e55 | ||
|
|
ac8183feee | ||
|
|
8037fb76fc | ||
|
|
754a14941b | ||
|
|
e32bc1a0a3 | ||
|
|
e5a8b49c93 | ||
|
|
b6872456cd | ||
|
|
2a12b018da | ||
|
|
f64486c88f | ||
|
|
80a40ac5f9 | ||
|
|
a1956350b1 | ||
|
|
2c09875556 | ||
|
|
561eed4cbd | ||
|
|
a8f72df7f8 | ||
|
|
ab4127f940 | ||
|
|
f03d12b677 | ||
|
|
81c6202582 | ||
|
|
6cab2bec61 | ||
|
|
94ffda653c | ||
|
|
c2268b45f6 | ||
|
|
38b8a59fe5 | ||
|
|
640e3213ee | ||
|
|
b1799009b3 | ||
|
|
f8e693a060 | ||
|
|
aba806bc32 | ||
|
|
b1fb3f2391 | ||
|
|
041e901c61 | ||
|
|
bb72134992 | ||
|
|
f83853c2fb | ||
|
|
d5f7b20be3 | ||
|
|
b878d64460 | ||
|
|
5397e8ae1a | ||
|
|
46bd8ad83d | ||
|
|
4dac57cf8c | ||
|
|
95cb026446 | ||
|
|
76ea714257 | ||
|
|
5788fa7204 | ||
|
|
d077c6450f | ||
|
|
4499ed3878 | ||
|
|
feb5e3bc78 | ||
|
|
23f1060756 | ||
|
|
6ed512e762 | ||
|
|
947970e07b | ||
|
|
df567b405a | ||
|
|
7b0596d9df | ||
|
|
4b7200ae69 | ||
|
|
710648f3f4 | ||
|
|
d2002b6a62 | ||
|
|
8de42b3199 | ||
|
|
48e78d2cd3 | ||
|
|
1fd099db70 | ||
|
|
2986a40c5a | ||
|
|
8bb6d4d0d9 | ||
|
|
00f08578ee | ||
|
|
6a514c7f51 | ||
|
|
21aad643f3 | ||
|
|
8687448966 | ||
|
|
6fcc8b721b | ||
|
|
b4bbd58c77 | ||
|
|
1a45b8f07a | ||
|
|
4471a858db | ||
|
|
22e712af5a | ||
|
|
9799b82d53 | ||
|
|
78915e0b99 | ||
|
|
c9cc65aae7 | ||
|
|
a4edd9d202 | ||
|
|
7747624c80 | ||
|
|
aab7191a44 | ||
|
|
1f972b1828 | ||
|
|
58b77cf589 | ||
|
|
c064f87889 | ||
|
|
d6b4c8e7da | ||
|
|
49f0b45fa8 | ||
|
|
b19c4bfc5d | ||
|
|
e944c819d9 | ||
|
|
75f631599c | ||
|
|
e0bebeae25 | ||
|
|
32e7ddf272 | ||
|
|
7091e0324e | ||
|
|
8f8532921c | ||
|
|
e9936992ec | ||
|
|
6375055b3e | ||
|
|
88b1ea296c | ||
|
|
2a13a869f3 | ||
|
|
89c342bb6f | ||
|
|
067f5e33aa | ||
|
|
33695f1c6b | ||
|
|
52ef84873e | ||
|
|
c21ba5c808 | ||
|
|
6cf408f12b | ||
|
|
8bdc2d285d | ||
|
|
a66fbcd6be | ||
|
|
93402a62fc | ||
|
|
2a56fb6c70 | ||
|
|
8276c031e9 | ||
|
|
102e5f6de9 | ||
|
|
1f7bdbfbcb | ||
|
|
f2fdd048c7 | ||
|
|
5a2cfc15a6 | ||
|
|
11c7131875 | ||
|
|
34aacc90d7 | ||
|
|
e75dce2543 | ||
|
|
e2774309fb | ||
|
|
dfefdc93c5 | ||
|
|
d46ea28bba | ||
|
|
d35c5bcaba | ||
|
|
2b14856d7f | ||
|
|
3316fdb80e | ||
|
|
c5d70c48c9 | ||
|
|
82230ef2f5 | ||
|
|
dcfd3d3e48 | ||
|
|
cb1e2ac32e | ||
|
|
ec2c15946a | ||
|
|
8be73142f8 | ||
|
|
d1640e3bb8 | ||
|
|
8612e6726d | ||
|
|
3b4694e9f8 | ||
|
|
14cb2e1999 | ||
|
|
cfabaf334e | ||
|
|
c054536fab | ||
|
|
35aac18b4e | ||
|
|
de3928cb4d | ||
|
|
9bf612b800 | ||
|
|
d9a8d67e3d | ||
|
|
99b0a56a6a | ||
|
|
61d7aa0400 | ||
|
|
1e98594972 | ||
|
|
c6383f042c | ||
|
|
b4d8e0e551 | ||
|
|
d696536486 | ||
|
|
b46a3cf759 | ||
|
|
8a5e3cdb76 | ||
|
|
4fb39226ef | ||
|
|
c40174034c | ||
|
|
5029c855f0 | ||
|
|
2eee839eaa | ||
|
|
f42c7fec1e | ||
|
|
0ab07303c9 | ||
|
|
ad47990a87 | ||
|
|
1968461ce9 | ||
|
|
8aa3b5d3da | ||
|
|
23e77eefb0 | ||
|
|
094443cf26 | ||
|
|
b97758ef46 | ||
|
|
d1fe752028 | ||
|
|
b26df787cd | ||
|
|
4c5b254730 | ||
|
|
9a153b7170 | ||
|
|
065a6caee5 | ||
|
|
9c7df02941 | ||
|
|
c63c5100e2 | ||
|
|
bf05088d70 | ||
|
|
e7bb146f92 | ||
|
|
688c4a90d9 | ||
|
|
165ad96be1 | ||
|
|
115ced7ab4 | ||
|
|
ef15b5641a | ||
|
|
be50464619 | ||
|
|
d847b71c86 | ||
|
|
dfcc1ff899 | ||
|
|
65562937f3 | ||
|
|
7b5dc33a21 | ||
|
|
f64c13b9ec | ||
|
|
25f70e570e | ||
|
|
b27b64c25c | ||
|
|
bfc64c926a | ||
|
|
88ad983aa4 | ||
|
|
a95d2e76f5 | ||
|
|
037886ba5b | ||
|
|
011edfe2a0 | ||
|
|
1a5a6773ce | ||
|
|
dda22fd387 | ||
|
|
7254224347 | ||
|
|
789e52af57 | ||
|
|
b2fcd6c084 | ||
|
|
76e5794203 | ||
|
|
a81332dd23 | ||
|
|
e1fab22d98 | ||
|
|
cb5e58d465 | ||
|
|
eaeb9ec6f7 | ||
|
|
88bb17fca8 | ||
|
|
f672b8534f | ||
|
|
839c0a45a0 | ||
|
|
974e44ba4c | ||
|
|
9461ebec2d | ||
|
|
48d8a53cd6 | ||
|
|
2a8dfd63da | ||
|
|
49ac431792 | ||
|
|
8a7c463c52 | ||
|
|
eab77fba7a | ||
|
|
61d90a5a88 | ||
|
|
ef99f02bc7 | ||
|
|
9166a604d6 | ||
|
|
d5a1189053 | ||
|
|
cb52a53922 | ||
|
|
1aff2ff0c3 | ||
|
|
fd63aad3b8 | ||
|
|
9a807a0685 | ||
|
|
40fc9fa26f | ||
|
|
cf0d29d452 | ||
|
|
e411c207ae | ||
|
|
5df2704ccc | ||
|
|
36f468a599 | ||
|
|
dfae69834a | ||
|
|
e53b32fa57 | ||
|
|
969afac696 | ||
|
|
50d4f267ba | ||
|
|
33b9caaca9 | ||
|
|
4f84843b00 | ||
|
|
44f415f94d | ||
|
|
6e4dd161ac | ||
|
|
c724f01c98 | ||
|
|
1fc63a6529 | ||
|
|
5d84ab4a47 | ||
|
|
fc315feac4 | ||
|
|
82a87e96b3 | ||
|
|
7d1ecc8d82 | ||
|
|
f9daf2a45c | ||
|
|
113751a4bc | ||
|
|
e182445593 | ||
|
|
6661ca17c2 | ||
|
|
1b4e75f49e | ||
|
|
22ed7f4e49 | ||
|
|
1d691cf763 | ||
|
|
9075f309ad | ||
|
|
18d97e9f73 | ||
|
|
bccd118abb | ||
|
|
3dc049ee5f | ||
|
|
a614a693e5 | ||
|
|
8dddcd04d5 | ||
|
|
209b4a1b02 | ||
|
|
b14a0fa2ce | ||
|
|
5f05363996 | ||
|
|
d5770ad8cc | ||
|
|
c239d0814b | ||
|
|
f7789a1e4a | ||
|
|
6d382078d3 | ||
|
|
162a16ed3a | ||
|
|
d706e81cd4 | ||
|
|
9fb0bffd61 | ||
|
|
4a452a2467 | ||
|
|
396d65b003 | ||
|
|
fa52e7c1b7 | ||
|
|
6758d6c43e | ||
|
|
4ad27670ec | ||
|
|
1ad072236d | ||
|
|
76387aacad | ||
|
|
200b1c5095 | ||
|
|
d14cfb577b | ||
|
|
c99b4f1559 | ||
|
|
45fdbd16b4 | ||
|
|
c04113c9fa | ||
|
|
4620fa5001 | ||
|
|
ba7dd31035 | ||
|
|
3928155b9c | ||
|
|
a0249b4337 | ||
|
|
d89982237d | ||
|
|
7e414f0af3 | ||
|
|
8fb37ae229 | ||
|
|
2e3de32810 | ||
|
|
f2e40889b2 | ||
|
|
896b247bc0 | ||
|
|
605d52f160 | ||
|
|
afb2b465a8 | ||
|
|
b1de66d878 | ||
|
|
71b625081a | ||
|
|
2cd4851601 | ||
|
|
987d104a6c | ||
|
|
60e6398dfa | ||
|
|
14ec0259df | ||
|
|
9f0617f6ee | ||
|
|
98297e0aed | ||
|
|
926b192722 | ||
|
|
97e5acecd7 | ||
|
|
2c960059ee | ||
|
|
e20bf3fbd9 | ||
|
|
af9fb0e091 | ||
|
|
f96911e1ba | ||
|
|
fdbb1c8d72 | ||
|
|
8a0089eb81 | ||
|
|
821d4ac9dd | ||
|
|
f4408473e6 | ||
|
|
1f759577ce | ||
|
|
04376ca047 | ||
|
|
7ac108f495 | ||
|
|
a2eece0f5c | ||
|
|
a03279df3b | ||
|
|
bef6c2b665 | ||
|
|
8002c65693 | ||
|
|
5c7247b8b5 | ||
|
|
1a18e68441 | ||
|
|
72afa55897 | ||
|
|
eed9fca10c | ||
|
|
25d97e187a | ||
|
|
7ca2ca8045 | ||
|
|
54885c3e5f | ||
|
|
b8ffb6e53d | ||
|
|
7bc43c226d | ||
|
|
da4697794d | ||
|
|
879c524658 | ||
|
|
e3a8f010c3 | ||
|
|
f2f8f92172 | ||
|
|
265ec96c6f | ||
|
|
6e06174db6 | ||
|
|
9677569293 | ||
|
|
c8e00195ed | ||
|
|
64bc189010 | ||
|
|
12496e4432 | ||
|
|
9e2fd67487 | ||
|
|
af17d56cbd | ||
|
|
6e0a657b76 | ||
|
|
b0a9fc7085 | ||
|
|
5e47b8a835 | ||
|
|
0b94ad1fef | ||
|
|
9dcd856099 | ||
|
|
cfbcc4093c | ||
|
|
b8169ae08b | ||
|
|
103625fbdd | ||
|
|
4712080a1b | ||
|
|
93c683be3a | ||
|
|
c9a7c15f98 | ||
|
|
64a08f0533 | ||
|
|
52c6455ca5 | ||
|
|
238681d72f | ||
|
|
dbcc6feb3f | ||
|
|
300b53f28c | ||
|
|
3dabb1c891 | ||
|
|
4381f7a973 | ||
|
|
77b767ae5b | ||
|
|
2c4f78133f | ||
|
|
ec2c181f4e | ||
|
|
fae98eb13a | ||
|
|
f1d117d5d7 | ||
|
|
529ac8041a | ||
|
|
391e84e0f4 | ||
|
|
6a2b717ff1 | ||
|
|
475a39cb73 | ||
|
|
4dba74e2f9 | ||
|
|
587d783955 | ||
|
|
3bfe87d516 | ||
|
|
d3e43012fb | ||
|
|
5da156b433 | ||
|
|
b2eeb63b6f | ||
|
|
a7517dace3 | ||
|
|
bd9f2d2009 | ||
|
|
78730f7235 | ||
|
|
8a2491d955 | ||
|
|
0d68faf40c | ||
|
|
957a2b2169 | ||
|
|
856318f947 | ||
|
|
4df9d6b20a | ||
|
|
8519d09141 | ||
|
|
1b4fe83739 | ||
|
|
8fc825b2eb | ||
|
|
6d0fc22ab5 | ||
|
|
9a71b4c237 | ||
|
|
9aa487418f | ||
|
|
203b2fcaca | ||
|
|
dae21f2638 | ||
|
|
af6e99cbac | ||
|
|
6d6acf4308 | ||
|
|
7dd4815edf | ||
|
|
2c068a99a3 | ||
|
|
fa93364ddd | ||
|
|
9b1eef3f63 | ||
|
|
ed6c07fd4b | ||
|
|
5ad96f5287 | ||
|
|
fa8845c7e2 | ||
|
|
b96ace7f04 | ||
|
|
234a8dc3d5 | ||
|
|
0467c2f893 | ||
|
|
762fe92933 | ||
|
|
16d105d462 | ||
|
|
8a8b624f47 | ||
|
|
9470615c63 | ||
|
|
8f0ea700a5 | ||
|
|
b7cbec0b1c | ||
|
|
9be9934b8c | ||
|
|
55f0e1e1d5 | ||
|
|
bbd784fc85 | ||
|
|
408f93732b | ||
|
|
fb53c95207 | ||
|
|
960e083152 | ||
|
|
7e560aa09e | ||
|
|
7d9478b774 | ||
|
|
0fa0ab43c9 | ||
|
|
f9cdd49d2e | ||
|
|
7661f5a3d7 | ||
|
|
395f337bbb | ||
|
|
9e9fe2a2cf | ||
|
|
5bf88cfe96 | ||
|
|
f7d3c10c9d | ||
|
|
6b395c816f | ||
|
|
d6e7173af0 | ||
|
|
1a60484abc | ||
|
|
62f765b2b4 | ||
|
|
57f2fb07f0 | ||
|
|
1b8c3aae73 | ||
|
|
845e84adb7 | ||
|
|
91c0429e2c | ||
|
|
30f673efe1 | ||
|
|
c1b1719914 | ||
|
|
9fbdd794b9 | ||
|
|
38ff2bf5cb | ||
|
|
345e800abd | ||
|
|
404eaa44e4 | ||
|
|
8e257ab241 | ||
|
|
fabbad5370 | ||
|
|
7d61acb2a8 | ||
|
|
f6a451d6e7 | ||
|
|
e68a15c825 | ||
|
|
f929cffce7 | ||
|
|
f171588616 | ||
|
|
e467ce7ce8 | ||
|
|
1fda7550da | ||
|
|
f714a5d0cb | ||
|
|
2a3d5a27a8 | ||
|
|
c9bf5ae79d | ||
|
|
cf2d8f71d6 | ||
|
|
611d9ac1ba | ||
|
|
f3309087ab | ||
|
|
860d317330 | ||
|
|
bab11a731e | ||
|
|
df9e7bead0 | ||
|
|
962dc847e7 | ||
|
|
566181e8d7 | ||
|
|
5f5f835b2e | ||
|
|
c0e697eb58 | ||
|
|
d3c2f50b20 | ||
|
|
f1ee1eeb63 | ||
|
|
bb18519e44 | ||
|
|
8e2ad133ea | ||
|
|
5e82e8413f | ||
|
|
917f6e5ee9 | ||
|
|
e1ca6e644e | ||
|
|
f1e89242cb | ||
|
|
91145cde0f | ||
|
|
8ae01fae3f | ||
|
|
4e60ccb2ff | ||
|
|
186052f21d | ||
|
|
0fb3bb0727 | ||
|
|
8f61a11452 | ||
|
|
4b1d3b0125 | ||
|
|
3f81d055af | ||
|
|
c61182bcb2 | ||
|
|
2fdfb3e998 |
@@ -0,0 +1,54 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Follow the troubleshooting guide before reporting a bug
|
||||
|
||||
---
|
||||
**READ ME FIRST!**
|
||||
If you're here because something basic is not working (like gamepad input, video, or similar), it's probably something specific to your setup, so make sure you've gone through the Troubleshooting Guide first: https://github.com/moonlight-stream/moonlight-docs/wiki/Troubleshooting
|
||||
|
||||
If you still have trouble with basic functionality after following the guide, join our Discord server where there are many other volunteers who can help (or direct you back here if it looks like a Moonlight bug after all). https://moonlight-stream.org/discord
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**Steps to reproduce**
|
||||
Any special steps that are required for the bug to appear.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem. If the issue is related to video glitching or poor quality, please include screenshots.
|
||||
|
||||
**Affected games**
|
||||
List the games you've tried that exhibit the issue. To see if the issue is game-specific, try streaming Steam Big Picture with Moonlight and see if the issue persists there.
|
||||
|
||||
**Other Moonlight clients**
|
||||
- Does the issue occur when using Moonlight on iOS or Android?
|
||||
|
||||
**Moonlight settings (please complete the following information)**
|
||||
- Have any settings been adjusted from defaults?
|
||||
- If so, which settings have been changed?
|
||||
- Does the problem still occur after reverting settings back to default?
|
||||
|
||||
**Gamepad-related issues (please complete if problem is gamepad-related)**
|
||||
- Do you have any gamepads connected to your host PC directly?
|
||||
- Does the problem still remain if you stream the desktop and use https://html5gamepad.com to test your gamepad?
|
||||
- Instructions for streaming the desktop can be found here: https://github.com/moonlight-stream/moonlight-docs/wiki/Setup-Guide
|
||||
|
||||
**Client PC details (please complete the following information)**
|
||||
- OS: [e.g. Windows 10 1809]
|
||||
- Moonlight Version: [e.g. v0.9.0]
|
||||
- GPU: [e.g. Intel HD Graphics 520]
|
||||
|
||||
**Server PC details (please complete the following information)**
|
||||
- OS: [e.g. Windows 10 1809]
|
||||
- GeForce Experience version: [e.g. 3.16.0.140]
|
||||
- Nvidia GPU driver: [e.g. 417.35]
|
||||
- Antivirus and firewall software: [e.g. Windows Defender and Windows Firewall]
|
||||
|
||||
**Moonlight Logs (please attach)**
|
||||
- On Windows, `Moonlight-###.log` files can be found in `%TEMP%`. Simply type that into the File Explorer path field to navigate there.
|
||||
- On macOS, `Moonlight-###.log` files can be found in `/tmp`. In Finder, press Cmd+Shift+G, then type `/tmp` to navigate there.
|
||||
- On Linux with the Flatpak, logs print to the terminal when running with the command: `flatpak run com.moonlight_stream.Moonlight`
|
||||
- On Linux with the Snap, logs print to the terminal when running with the command: `moonlight`
|
||||
|
||||
**Additional context**
|
||||
Anything else you think may be relevant to the issue
|
||||
@@ -0,0 +1 @@
|
||||
blank_issues_enabled: false
|
||||
@@ -0,0 +1,17 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
@@ -0,0 +1,4 @@
|
||||
issuesOpened: >
|
||||
If this is a question about Moonlight or you need help troubleshooting a streaming problem, please use the help channels on our [Discord server](https://moonlight-stream.org/discord) instead of GitHub issues. There are many more people available on Discord to help you and answer your questions.<br /><br />
|
||||
This issue tracker should only be used for specific bugs or feature requests.<br /><br />
|
||||
Thank you, and happy streaming!
|
||||
@@ -0,0 +1,8 @@
|
||||
# ProBot No Response (https://probot.github.io/apps/no-response/)
|
||||
|
||||
daysUntilClose: 7
|
||||
responseRequiredLabel: 'need more info'
|
||||
closeComment: >
|
||||
This issue has been automatically closed because there was no response to a
|
||||
request for more information from the issue opener. Please leave a comment or
|
||||
open a new issue if you have additional information related to this issue.
|
||||
@@ -0,0 +1,14 @@
|
||||
# ProBot Stale (https://probot.github.io/apps/stale/)
|
||||
|
||||
daysUntilStale: 90
|
||||
daysUntilClose: 7
|
||||
exemptLabels:
|
||||
- accepted
|
||||
- bug
|
||||
- enhancement
|
||||
- meta
|
||||
staleLabel: stale
|
||||
markComment: >
|
||||
This issue has been automatically marked as stale because it has not had
|
||||
recent activity. It will be closed if no further activity occurs.
|
||||
closeComment: false
|
||||
+3
-1
@@ -2,4 +2,6 @@
|
||||
|
||||
|
||||
**/.vs/
|
||||
build/
|
||||
build/
|
||||
config.tests/*/.qmake.stash
|
||||
config.tests/*/Makefile
|
||||
|
||||
+13
@@ -4,3 +4,16 @@
|
||||
[submodule "qmdnsengine/qmdnsengine"]
|
||||
path = qmdnsengine/qmdnsengine
|
||||
url = https://github.com/cgutman/qmdnsengine.git
|
||||
[submodule "app/SDL_GameControllerDB"]
|
||||
path = app/SDL_GameControllerDB
|
||||
url = https://github.com/gabomdq/SDL_GameControllerDB.git
|
||||
[submodule "soundio/libsoundio"]
|
||||
path = soundio/libsoundio
|
||||
url = https://github.com/cgutman/libsoundio.git
|
||||
[submodule "h264bitstream/h264bitstream"]
|
||||
path = h264bitstream/h264bitstream
|
||||
url = https://github.com/aizvorski/h264bitstream.git
|
||||
[submodule "libs"]
|
||||
path = libs
|
||||
url = https://github.com/cgutman/moonlight-qt-prebuilts.git
|
||||
shallow = true
|
||||
|
||||
-46
@@ -1,46 +0,0 @@
|
||||
language: cpp
|
||||
|
||||
git:
|
||||
depth: 1
|
||||
|
||||
matrix:
|
||||
include:
|
||||
- os: osx
|
||||
compiler: clang
|
||||
- os: linux
|
||||
sudo: required
|
||||
dist: xenial
|
||||
compiler: gcc
|
||||
addons:
|
||||
apt:
|
||||
sources:
|
||||
- sourceline: 'ppa:beineri/opt-qt596-xenial'
|
||||
- sourceline: 'ppa:jonathonf/ffmpeg-4'
|
||||
packages:
|
||||
- qt59base
|
||||
- qt59quickcontrols2
|
||||
- qt59svg
|
||||
- libesd0-dev
|
||||
- libgl-dev
|
||||
- libopus-dev
|
||||
- libavcodec-dev
|
||||
- libavutil-dev
|
||||
- libva-dev
|
||||
- libvdpau-dev
|
||||
|
||||
install:
|
||||
- '[ "$TRAVIS_OS_NAME" != osx ] || brew install qt5'
|
||||
- '[ "$TRAVIS_OS_NAME" != osx ] || brew link --force qt5'
|
||||
- if [[ "$TRAVIS_OS_NAME" = "linux" ]]; then export SDL2_VER=2.0.8 && wget https://www.libsdl.org/release/SDL2-$SDL2_VER.tar.gz && tar -xvf SDL2-$SDL2_VER.tar.gz && cd SDL2-$SDL2_VER && ./configure && make && sudo make install && cd ..; fi
|
||||
|
||||
before_script:
|
||||
- '[ "$TRAVIS_OS_NAME" != "linux" ] || source /opt/qt59/bin/qt59-env.sh'
|
||||
- qmake -v
|
||||
- '[[ "$TRAVIS_OS_NAME" != linux || "$CXX" != clang++ ]] || export QMAKESPEC=linux-clang'
|
||||
- '[[ "$TRAVIS_OS_NAME" != linux || "$CXX" != g++ ]] || export QMAKESPEC=linux-g++'
|
||||
- '[ "$TRAVIS_OS_NAME" != osx ] || export QMAKESPEC=macx-clang'
|
||||
|
||||
script:
|
||||
- qmake moonlight-qt.pro -spec $QMAKESPEC
|
||||
- make debug
|
||||
- make release
|
||||
@@ -0,0 +1,22 @@
|
||||
QT -= core gui
|
||||
|
||||
TARGET = AntiHooking
|
||||
TEMPLATE = lib
|
||||
|
||||
include(../globaldefs.pri)
|
||||
|
||||
INCLUDEPATH += $$PWD/../libs/windows/include
|
||||
contains(QT_ARCH, i386) {
|
||||
LIBS += -L$$PWD/../libs/windows/lib/x86
|
||||
}
|
||||
contains(QT_ARCH, x86_64) {
|
||||
LIBS += -L$$PWD/../libs/windows/lib/x64
|
||||
}
|
||||
contains(QT_ARCH, arm64) {
|
||||
LIBS += -L$$PWD/../libs/windows/lib/arm64
|
||||
}
|
||||
|
||||
LIBS += -ldetours
|
||||
DEFINES += ANTIHOOKING_LIBRARY
|
||||
SOURCES += antihookingprotection.cpp
|
||||
HEADERS += antihookingprotection.h
|
||||
@@ -0,0 +1,255 @@
|
||||
#include "antihookingprotection.h"
|
||||
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <Windows.h>
|
||||
|
||||
#include <detours.h>
|
||||
|
||||
typedef HMODULE (WINAPI *LoadLibraryAFunc)(LPCSTR lpLibFileName);
|
||||
typedef HMODULE (WINAPI *LoadLibraryWFunc)(LPCWSTR lpLibFileName);
|
||||
typedef HMODULE (WINAPI *LoadLibraryExAFunc)(LPCSTR lpLibFileName, HANDLE hFile, DWORD dwFlags);
|
||||
typedef HMODULE (WINAPI *LoadLibraryExWFunc)(LPCWSTR lpLibFileName, HANDLE hFile, DWORD dwFlags);
|
||||
|
||||
class AntiHookingProtection
|
||||
{
|
||||
public:
|
||||
static void enable()
|
||||
{
|
||||
DetourTransactionBegin();
|
||||
DetourUpdateThread(GetCurrentThread());
|
||||
|
||||
s_RealLoadLibraryA = LoadLibraryA;
|
||||
DetourAttach(&(PVOID&)s_RealLoadLibraryA, LoadLibraryAHook);
|
||||
|
||||
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:
|
||||
static bool isImageBlacklistedW(LPCWSTR lpLibFileName)
|
||||
{
|
||||
LPCWSTR dllName;
|
||||
|
||||
// If the library has a path prefixed, remove it
|
||||
dllName = wcsrchr(lpLibFileName, '\\');
|
||||
if (!dllName) {
|
||||
// No prefix, so use the full name
|
||||
dllName = lpLibFileName;
|
||||
}
|
||||
else {
|
||||
// Advance past the backslash
|
||||
dllName++;
|
||||
}
|
||||
|
||||
// FIXME: We don't currently handle LoadLibrary calls where the
|
||||
// library name does not include a file extension and the loader
|
||||
// automatically assumes .dll.
|
||||
|
||||
for (int i = 0; i < ARRAYSIZE(k_BlacklistedDlls); i++) {
|
||||
if (_wcsicmp(dllName, k_BlacklistedDlls[i]) == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool isImageBlacklistedA(LPCSTR lpLibFileName)
|
||||
{
|
||||
int uniChars = MultiByteToWideChar(CP_THREAD_ACP, 0, lpLibFileName, -1, nullptr, 0);
|
||||
if (uniChars > 0) {
|
||||
PWCHAR wideBuffer = new WCHAR[uniChars];
|
||||
uniChars = MultiByteToWideChar(CP_THREAD_ACP, 0,
|
||||
lpLibFileName, -1,
|
||||
wideBuffer, uniChars * sizeof(WCHAR));
|
||||
if (uniChars > 0) {
|
||||
bool ret = isImageBlacklistedW(wideBuffer);
|
||||
delete[] wideBuffer;
|
||||
return ret;
|
||||
}
|
||||
else {
|
||||
delete[] wideBuffer;
|
||||
}
|
||||
}
|
||||
|
||||
// Error path
|
||||
return false;
|
||||
}
|
||||
|
||||
static HMODULE WINAPI LoadLibraryAHook(LPCSTR lpLibFileName)
|
||||
{
|
||||
if (lpLibFileName && isImageBlacklistedA(lpLibFileName)) {
|
||||
SetLastError(ERROR_ACCESS_DISABLED_BY_POLICY);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return s_RealLoadLibraryA(lpLibFileName);
|
||||
}
|
||||
|
||||
static HMODULE WINAPI LoadLibraryWHook(LPCWSTR lpLibFileName)
|
||||
{
|
||||
if (lpLibFileName && isImageBlacklistedW(lpLibFileName)) {
|
||||
SetLastError(ERROR_ACCESS_DISABLED_BY_POLICY);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return s_RealLoadLibraryW(lpLibFileName);
|
||||
}
|
||||
|
||||
static HMODULE WINAPI LoadLibraryExAHook(LPCSTR lpLibFileName, HANDLE hFile, DWORD dwFlags)
|
||||
{
|
||||
if (lpLibFileName && isImageBlacklistedA(lpLibFileName)) {
|
||||
SetLastError(ERROR_ACCESS_DISABLED_BY_POLICY);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return s_RealLoadLibraryExA(lpLibFileName, hFile, dwFlags);
|
||||
}
|
||||
|
||||
static HMODULE WINAPI LoadLibraryExWHook(LPCWSTR lpLibFileName, HANDLE hFile, DWORD dwFlags)
|
||||
{
|
||||
if (lpLibFileName && isImageBlacklistedW(lpLibFileName)) {
|
||||
SetLastError(ERROR_ACCESS_DISABLED_BY_POLICY);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return s_RealLoadLibraryExW(lpLibFileName, hFile, dwFlags);
|
||||
}
|
||||
|
||||
static LoadLibraryAFunc s_RealLoadLibraryA;
|
||||
static LoadLibraryWFunc s_RealLoadLibraryW;
|
||||
static LoadLibraryExAFunc s_RealLoadLibraryExA;
|
||||
static LoadLibraryExWFunc s_RealLoadLibraryExW;
|
||||
|
||||
static constexpr LPCWSTR k_BlacklistedDlls[] = {
|
||||
// These A-Volute DLLs shipped with various audio driver packages improperly handle
|
||||
// D3D9 exclusive fullscreen in a way that causes CreateDeviceEx() to deadlock.
|
||||
// https://github.com/moonlight-stream/moonlight-qt/issues/102
|
||||
L"NahimicOSD.dll", // ASUS Sonic Radar 3
|
||||
L"SSAudioOSD.dll", // SteelSeries headsets
|
||||
L"SS2OSD.dll", // ASUS Sonic Studio 2
|
||||
L"Nahimic2OSD.dll",
|
||||
L"NahimicMSIOSD.dll",
|
||||
L"nhAsusPhoebusOSD.dll", // ASUS Phoebus
|
||||
|
||||
// This DLL has been seen in several crash reports. Some Googling
|
||||
// suggests it's highly unstable and causes issues in many games.
|
||||
L"EZFRD32.dll",
|
||||
L"EZFRD64.dll",
|
||||
|
||||
// These are the newer dList DLLs for Optimus hybrid graphics DDI.
|
||||
// https://docs.microsoft.com/en-us/windows-hardware/drivers/display/hybrid-system-ddi
|
||||
//
|
||||
// We forcefully block them from loading because Optimus has a bug that
|
||||
// deadlocks DXVA2 when we present with D3DPRESENT_DONOTWAIT. This will prevent
|
||||
// Optimus from ever using the dGPU even if the user has requested it.
|
||||
// https://github.com/moonlight-stream/moonlight-qt/issues/240
|
||||
// https://github.com/moonlight-stream/moonlight-qt/issues/235
|
||||
L"nvdlist.dll",
|
||||
L"nvdlistx.dll",
|
||||
|
||||
// These are the older dList/AppInit DLLs for Optimus hybrid graphics DDI.
|
||||
// https://docs.microsoft.com/en-us/windows-hardware/drivers/display/hybrid-system-ddi
|
||||
//
|
||||
// These seem to cause a crash in PresentEx() in full-screen exclusive mode.
|
||||
// This block will prevent Optimus from ever using the dGPU even if the user has requested it.
|
||||
// https://github.com/moonlight-stream/moonlight-qt/issues/386
|
||||
//
|
||||
// d3d9!CSwapChain::BltToHybridPrimary+0x200:
|
||||
// 00007ffa`23f37e58 488b01 mov rax,qword ptr [rcx] ds:00000000`00000038=????????????????
|
||||
// 00 0000004e`496ff4e0 00007ffa`23f39e2c d3d9!CSwapChain::BltToHybridPrimary+0x200
|
||||
// 01 0000004e`496ff880 00007ffa`23ee39ce d3d9!CSwapChain::FlipToSurface+0x15c
|
||||
// 02 0000004e`496ff900 00007ffa`23f4dd75 d3d9!CSwapChain::PresentMain+0x3e13e
|
||||
// 03 0000004e`496ffab0 00007ffa`23f4dccd d3d9!CBaseDevice::PresentMain+0x9d
|
||||
// 04 0000004e`496ffb00 00007ff7`8e31016f d3d9!CBaseDevice::PresentEx+0xbd
|
||||
// 05 0000004e`496ffb50 00007ff7`8e30df1e Moonlight!DXVA2Renderer::renderFrame+0x61f [C:\moonlight-qt\app\streaming\video\ffmpeg-renderers\dxva2.cpp @ 1035]
|
||||
// 06 0000004e`496ffd50 00007ff7`8e30e46a Moonlight!Pacer::renderFrame+0x3e [C:\moonlight-qt\app\streaming\video\ffmpeg-renderers\pacer\pacer.cpp @ 265]
|
||||
// 07 (Inline Function) --------`-------- Moonlight!Pacer::renderLastFrameAndUnlock+0x197 [C:\moonlight-qt\app\streaming\video\ffmpeg-renderers\pacer\pacer.cpp @ 156]
|
||||
// 08 0000004e`496ffda0 00007ffa`14476978 Moonlight!Pacer::renderThread+0x1ca [C:\moonlight-qt\app\streaming\video\ffmpeg-renderers\pacer\pacer.cpp @ 88]
|
||||
// 09 0000004e`496ffde0 00007ffa`14476ee2 SDL2!SDL_RunThread+0x38 [C:\Users\camer\SDL\src\thread\SDL_thread.c @ 276]
|
||||
// 0a 0000004e`496ffe10 00007ffa`2aae0e82 SDL2!RunThread+0x12 [C:\Users\camer\SDL\src\thread\windows\SDL_systhread.c @ 83]
|
||||
// 0b 0000004e`496ffe40 00007ffa`2d627bd4 ucrtbase!thread_start<unsigned int (__cdecl*)(void *),1>+0x42
|
||||
// 0c 0000004e`496ffe70 00007ffa`2da4ce51 kernel32!BaseThreadInitThunk+0x14
|
||||
// 0d 0000004e`496ffea0 00000000`00000000 ntdll!RtlUserThreadStart+0x21
|
||||
//
|
||||
L"nvinit.dll",
|
||||
L"nvinitx.dll",
|
||||
|
||||
// In some unknown circumstances, RTSS tries to hook in the middle of an instruction, leaving garbage
|
||||
// code inside d3d9.dll that causes a crash when executed:
|
||||
//
|
||||
// 0:000> u
|
||||
// d3d9!D3D9GetCurrentOwnershipMode+0x5d:
|
||||
// 00007ff8`95b95861 9b wait
|
||||
// 00007ff8`95b95862 a7 cmps dword ptr [rsi],dword ptr [rdi] <--- crash happens here
|
||||
// 00007ff8`95b95863 ff ???
|
||||
// 00007ff8`95b95864 bfe8ca8a00 mov edi,8ACAE8h
|
||||
// 00007ff8`95b95869 00eb add bl,ch
|
||||
// 00007ff8`95b9586b f1 ???
|
||||
// 00007ff8`95b9586c b808000000 mov eax,8
|
||||
// 00007ff8`95b95871 ebe6 jmp d3d9!D3D9GetCurrentOwnershipMode+0x55 (00007ff8`95b95859)
|
||||
//
|
||||
// Disassembling starting at the exact address of the attempted hook yields the intended jmp instruction
|
||||
//
|
||||
// 0:000> u d3d9!D3D9GetCurrentOwnershipMode+0x5c:
|
||||
// 00007ff8`95b95860 e99ba7ffbf jmp 00007ff8`55b90000
|
||||
//
|
||||
// Since the RTSS OSD doesn't even work with DXVA2, we'll just block the hooks entirely.
|
||||
L"RTSSHooks.dll",
|
||||
L"RTSSHooks64.dll",
|
||||
};
|
||||
};
|
||||
|
||||
LoadLibraryAFunc AntiHookingProtection::s_RealLoadLibraryA;
|
||||
LoadLibraryWFunc AntiHookingProtection::s_RealLoadLibraryW;
|
||||
LoadLibraryExAFunc AntiHookingProtection::s_RealLoadLibraryExA;
|
||||
LoadLibraryExWFunc AntiHookingProtection::s_RealLoadLibraryExW;
|
||||
|
||||
AH_EXPORT void AntiHookingDummyImport() {}
|
||||
|
||||
extern "C"
|
||||
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;
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef ANTIHOOKING_LIBRARY
|
||||
#define AH_EXPORT extern "C" __declspec(dllexport)
|
||||
#else
|
||||
#define AH_EXPORT extern "C" __declspec(dllimport)
|
||||
#endif
|
||||
|
||||
AH_EXPORT void AntiHookingDummyImport();
|
||||
|
||||
@@ -1,33 +1,85 @@
|
||||
# Moonlight PC
|
||||
|
||||
[Moonlight PC](http://moonlight-stream.com) is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield, but built to run on Windows, Mac, and Linux. This client is the successor to [Moonlight Chrome](https://github.com/moonlight-stream/moonlight-chrome) for streaming on PC.
|
||||
[Moonlight PC](https://moonlight-stream.org) is an open source PC client for NVIDIA GameStream and [Sunshine](https://github.com/LizardByte/Sunshine).
|
||||
|
||||
Moonlight also has mobile versions for [Android](https://github.com/moonlight-stream/moonlight-android) and [iOS](https://github.com/moonlight-stream/moonlight-ios).
|
||||
Moonlight also has mobile versions for [Android](https://github.com/moonlight-stream/moonlight-android) and [iOS](https://github.com/moonlight-stream/moonlight-ios).
|
||||
|
||||
See the [releases page](https://github.com/moonlight-stream/moonlight-qt/releases) for download links for Windows and Mac. For Linux, we're on [Flathub](https://flathub.org/apps/details/com.moonlight_stream.Moonlight).
|
||||
You can follow development on our [Discord server](https://moonlight-stream.org/discord) and help translate Moonlight into your language on [Weblate](https://hosted.weblate.org/projects/moonlight/moonlight-qt/).
|
||||
|
||||
You can follow development on our [Discord server](https://discord.gg/6ERtzFY).
|
||||
|
||||
[](https://ci.appveyor.com/project/cgutman/moonlight-qt/branch/master)
|
||||
[](https://travis-ci.org/moonlight-stream/moonlight-qt)
|
||||
[](https://ci.appveyor.com/project/cgutman/moonlight-qt/branch/master)
|
||||
[](https://github.com/moonlight-stream/moonlight-qt/releases)
|
||||
[](https://hosted.weblate.org/projects/moonlight/moonlight-qt/)
|
||||
|
||||
## Features
|
||||
- Hardware accelerated video decoding on Windows, Mac, and Linux
|
||||
- Supports streaming at up to 120 FPS on high refresh rate monitors
|
||||
- Supports streaming at 720p, 1080p, 1440p, or 4K
|
||||
- 5.1 surround sound audio
|
||||
- HEVC support for better image quality at reduced bandwidth
|
||||
- Keyboard and mouse support
|
||||
- Gamepad support with SDL gamepad mappings
|
||||
- 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
|
||||
|
||||
## Downloads
|
||||
- [Windows, macOS, and Steam Link](https://github.com/moonlight-stream/moonlight-qt/releases)
|
||||
- [Snap (for Ubuntu-based Linux distros)](https://snapcraft.io/moonlight)
|
||||
- [Flatpak (for other Linux distros)](https://flathub.org/apps/details/com.moonlight_stream.Moonlight)
|
||||
- [AppImage](https://github.com/moonlight-stream/moonlight-qt/releases)
|
||||
- [Raspberry Pi 4](https://github.com/moonlight-stream/moonlight-docs/wiki/Installing-Moonlight-Qt-on-Raspberry-Pi-4)
|
||||
- [NVIDIA Jetson and Nintendo Switch (Ubuntu L4T)](https://github.com/moonlight-stream/moonlight-docs/wiki/Installing-Moonlight-Qt-on-Linux4Tegra-(L4T)-Ubuntu)
|
||||
|
||||
#### Special Thanks
|
||||
|
||||
[](https://cloudsmith.com)
|
||||
|
||||
Hosting for Moonlight's Raspberry Pi and L4T package repositories is graciously provided for free by [Cloudsmith](https://cloudsmith.com).
|
||||
|
||||
## Building
|
||||
1. Install the latest Qt SDK (and optionally, the Qt Creator IDE) from https://www.qt.io/download (select MSVC toolchain on Windows)
|
||||
|
||||
### Windows Build Requirements
|
||||
* Qt 5.15 SDK or later
|
||||
* Windows 7 or later
|
||||
* [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)
|
||||
|
||||
### macOS Build Requirements
|
||||
* Qt 6.2 SDK or later
|
||||
* macOS Mojave (10.14) or later
|
||||
* 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 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 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
|
||||
* STEAMLINK_SDK_PATH environment variable set to the Steam Link SDK path
|
||||
|
||||
### Build Setup Steps
|
||||
1. Install the latest Qt SDK (and optionally, the Qt Creator IDE) from https://www.qt.io/download
|
||||
* You can install Qt via Homebrew on macOS, but you will need to use `brew install qt --with-debug` to be able to create debug builds of Moonlight.
|
||||
* You may also use your Linux distro's package manager for the Qt SDK as long as the packages are Qt 5.9 or later.
|
||||
* This step is not required for building on Steam Link, because the Steam Link SDK includes Qt 5.9.
|
||||
2. Run `git submodule update --init --recursive` from within `moonlight-qt/`
|
||||
3. Open the project in Qt Creator or build from qmake on the command line
|
||||
3. Open the project in Qt Creator or build from qmake on the command line.
|
||||
* To build a binary for use on non-development machines, use the scripts in the `scripts` folder.
|
||||
* 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 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
|
||||
2. Write code
|
||||
3. Send Pull Requests
|
||||
|
||||
Check out our [website](http://moonlight-stream.com) for project links and information.
|
||||
Check out our [website](https://moonlight-stream.org) for project links and information.
|
||||
|
||||
+21
-3
@@ -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>
|
||||
@@ -17,13 +17,31 @@
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>10.10</string>
|
||||
<string>10.14</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<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>0.1.0</string>
|
||||
<string>VERSION</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>VERSION</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Moonlight</string>
|
||||
</dict>
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3" manifestVersion="1.0">
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<security>
|
||||
<requestedPrivileges>
|
||||
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
|
||||
</requestedPrivileges>
|
||||
</security>
|
||||
</trustInfo>
|
||||
<description>Moonlight Game Streaming</description>
|
||||
<asmv3:application>
|
||||
<asmv3:windowsSettings>
|
||||
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">permonitor</dpiAwareness>
|
||||
</asmv3:windowsSettings>
|
||||
</asmv3:application>
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<!-- Windows 10 -->
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
|
||||
<!-- Windows 8.1 -->
|
||||
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
|
||||
<!-- Windows 8 -->
|
||||
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>
|
||||
<!-- Windows 7 -->
|
||||
<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/>
|
||||
<!-- Windows Vista -->
|
||||
<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}"/>
|
||||
</application>
|
||||
</compatibility>
|
||||
</assembly>
|
||||
Submodule
+1
Submodule app/SDL_GameControllerDB added at dbcf31a670
+374
-46
@@ -8,8 +8,16 @@ unix:!macx {
|
||||
TARGET = Moonlight
|
||||
}
|
||||
|
||||
# Support debug and release builds from command line for CI
|
||||
CONFIG += debug_and_release
|
||||
include(../globaldefs.pri)
|
||||
|
||||
# Precompile QML files to avoid writing qmlcache on portable versions.
|
||||
# Since this binds the app against the Qt runtime version, we will only
|
||||
# do this for Windows and Mac, since they ship with the Qt runtime.
|
||||
win32|macx {
|
||||
CONFIG(release, debug|release) {
|
||||
CONFIG += qtquickcompiler
|
||||
}
|
||||
}
|
||||
|
||||
TEMPLATE = app
|
||||
|
||||
@@ -25,79 +33,201 @@ DEFINES += QT_DEPRECATED_WARNINGS
|
||||
DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
|
||||
|
||||
win32 {
|
||||
INCLUDEPATH += $$PWD/../libs/windows/include
|
||||
|
||||
contains(QT_ARCH, i386) {
|
||||
LIBS += -L$$PWD/../libs/windows/lib/x86
|
||||
INCLUDEPATH += $$PWD/../libs/windows/include/x86
|
||||
}
|
||||
contains(QT_ARCH, x86_64) {
|
||||
LIBS += -L$$PWD/../libs/windows/lib/x64
|
||||
INCLUDEPATH += $$PWD/../libs/windows/include/x64
|
||||
}
|
||||
contains(QT_ARCH, arm64) {
|
||||
LIBS += -L$$PWD/../libs/windows/lib/arm64
|
||||
INCLUDEPATH += $$PWD/../libs/windows/include/arm64
|
||||
}
|
||||
|
||||
LIBS += ws2_32.lib winmm.lib dxva2.lib ole32.lib
|
||||
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 $$PWD/../libs/mac/Frameworks/SDL2.framework/Versions/A/Headers
|
||||
INCLUDEPATH += $$PWD/../libs/mac/include
|
||||
INCLUDEPATH += $$PWD/../libs/mac/Frameworks/SDL2.framework/Versions/A/Headers
|
||||
INCLUDEPATH += $$PWD/../libs/mac/Frameworks/SDL2_ttf.framework/Versions/A/Headers
|
||||
LIBS += -L$$PWD/../libs/mac/lib -F$$PWD/../libs/mac/Frameworks
|
||||
|
||||
# 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 opus
|
||||
LIBS += -ldl
|
||||
PKGCONFIG += openssl sdl2 SDL2_ttf opus
|
||||
|
||||
packagesExist(libavcodec) {
|
||||
PKGCONFIG += libavcodec libavutil
|
||||
CONFIG += ffmpeg
|
||||
!disable-ffmpeg {
|
||||
packagesExist(libavcodec) {
|
||||
PKGCONFIG += libavcodec libavutil
|
||||
CONFIG += ffmpeg
|
||||
|
||||
packagesExist(libva) {
|
||||
CONFIG += libva
|
||||
!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
|
||||
}
|
||||
}
|
||||
|
||||
!disable-libvdpau {
|
||||
packagesExist(vdpau) {
|
||||
CONFIG += libvdpau
|
||||
}
|
||||
}
|
||||
|
||||
!disable-mmal {
|
||||
packagesExist(mmal) {
|
||||
PKGCONFIG += mmal
|
||||
CONFIG += mmal
|
||||
}
|
||||
}
|
||||
|
||||
!disable-libdrm {
|
||||
packagesExist(libdrm) {
|
||||
PKGCONFIG += libdrm
|
||||
CONFIG += libdrm
|
||||
}
|
||||
}
|
||||
|
||||
!disable-cuda {
|
||||
packagesExist(ffnvcodec) {
|
||||
PKGCONFIG += ffnvcodec
|
||||
CONFIG += cuda
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
packagesExist(vdpau) {
|
||||
CONFIG += libvdpau
|
||||
!disable-wayland {
|
||||
packagesExist(wayland-client) {
|
||||
CONFIG += wayland
|
||||
PKGCONFIG += wayland-client
|
||||
}
|
||||
}
|
||||
|
||||
!disable-x11 {
|
||||
packagesExist(x11) {
|
||||
DEFINES += HAS_X11
|
||||
PKGCONFIG += x11
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
win32 {
|
||||
LIBS += -llibssl -llibcrypto -lSDL2 -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
|
||||
LIBS += -lobjc -framework VideoToolbox -framework AVFoundation -framework CoreVideo -framework CoreGraphics -framework CoreMedia -framework AppKit
|
||||
CONFIG += ffmpeg
|
||||
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
|
||||
LIBS += -framework CoreAudio -framework AudioUnit
|
||||
|
||||
CONFIG += ffmpeg soundio discord-rpc
|
||||
}
|
||||
|
||||
SOURCES += \
|
||||
backend/nvaddress.cpp \
|
||||
backend/nvapp.cpp \
|
||||
cli/pair.cpp \
|
||||
main.cpp \
|
||||
backend/computerseeker.cpp \
|
||||
backend/identitymanager.cpp \
|
||||
backend/nvcomputer.cpp \
|
||||
backend/nvhttp.cpp \
|
||||
backend/nvpairingmanager.cpp \
|
||||
backend/computermanager.cpp \
|
||||
backend/boxartmanager.cpp \
|
||||
backend/richpresencemanager.cpp \
|
||||
cli/commandlineparser.cpp \
|
||||
cli/listapps.cpp \
|
||||
cli/quitstream.cpp \
|
||||
cli/startstream.cpp \
|
||||
settings/compatfetcher.cpp \
|
||||
settings/mappingfetcher.cpp \
|
||||
settings/streamingpreferences.cpp \
|
||||
streaming/input.cpp \
|
||||
streaming/input/abstouch.cpp \
|
||||
streaming/input/gamepad.cpp \
|
||||
streaming/input/input.cpp \
|
||||
streaming/input/keyboard.cpp \
|
||||
streaming/input/mouse.cpp \
|
||||
streaming/input/reltouch.cpp \
|
||||
streaming/session.cpp \
|
||||
streaming/audio.cpp \
|
||||
streaming/audio/audio.cpp \
|
||||
streaming/audio/renderers/sdlaud.cpp \
|
||||
gui/computermodel.cpp \
|
||||
gui/appmodel.cpp \
|
||||
streaming/streamutils.cpp
|
||||
streaming/streamutils.cpp \
|
||||
backend/autoupdatechecker.cpp \
|
||||
path.cpp \
|
||||
settings/mappingmanager.cpp \
|
||||
gui/sdlgamepadkeynavigation.cpp \
|
||||
streaming/video/overlaymanager.cpp \
|
||||
backend/systemproperties.cpp \
|
||||
wm.cpp
|
||||
|
||||
HEADERS += \
|
||||
backend/nvaddress.h \
|
||||
backend/nvapp.h \
|
||||
cli/pair.h \
|
||||
settings/compatfetcher.h \
|
||||
settings/mappingfetcher.h \
|
||||
utils.h \
|
||||
backend/computerseeker.h \
|
||||
backend/identitymanager.h \
|
||||
backend/nvcomputer.h \
|
||||
backend/nvhttp.h \
|
||||
backend/nvpairingmanager.h \
|
||||
backend/computermanager.h \
|
||||
backend/boxartmanager.h \
|
||||
backend/richpresencemanager.h \
|
||||
cli/commandlineparser.h \
|
||||
cli/listapps.h \
|
||||
cli/quitstream.h \
|
||||
cli/startstream.h \
|
||||
settings/streamingpreferences.h \
|
||||
streaming/input.hpp \
|
||||
streaming/session.hpp \
|
||||
streaming/input/input.h \
|
||||
streaming/session.h \
|
||||
streaming/audio/renderers/renderer.h \
|
||||
streaming/audio/renderers/sdl.h \
|
||||
gui/computermodel.h \
|
||||
gui/appmodel.h \
|
||||
streaming/video/decoder.h \
|
||||
streaming/streamutils.h
|
||||
streaming/streamutils.h \
|
||||
backend/autoupdatechecker.h \
|
||||
path.h \
|
||||
settings/mappingmanager.h \
|
||||
gui/sdlgamepadkeynavigation.h \
|
||||
streaming/video/overlaymanager.h \
|
||||
backend/systemproperties.h
|
||||
|
||||
# Platform-specific renderers and decoders
|
||||
ffmpeg {
|
||||
@@ -106,18 +236,43 @@ ffmpeg {
|
||||
DEFINES += HAVE_FFMPEG
|
||||
SOURCES += \
|
||||
streaming/video/ffmpeg.cpp \
|
||||
streaming/video/ffmpeg-renderers/sdl.cpp
|
||||
streaming/video/ffmpeg-renderers/sdlvid.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/renderer.h \
|
||||
streaming/video/ffmpeg-renderers/sdlvid.h \
|
||||
streaming/video/ffmpeg-renderers/swframemapper.h \
|
||||
streaming/video/ffmpeg-renderers/pacer/pacer.h
|
||||
}
|
||||
libva {
|
||||
message(VAAPI renderer selected)
|
||||
|
||||
PKGCONFIG += libva
|
||||
DEFINES += HAVE_LIBVA
|
||||
SOURCES += streaming/video/ffmpeg-renderers/vaapi.cpp
|
||||
HEADERS += streaming/video/ffmpeg-renderers/vaapi.h
|
||||
}
|
||||
libva-x11 {
|
||||
message(VAAPI X11 support enabled)
|
||||
|
||||
PKGCONFIG += libva-x11
|
||||
DEFINES += HAVE_LIBVA_X11
|
||||
}
|
||||
libva-wayland {
|
||||
message(VAAPI Wayland support enabled)
|
||||
|
||||
PKGCONFIG += libva-wayland
|
||||
DEFINES += HAVE_LIBVA_WAYLAND
|
||||
}
|
||||
libva-wayland {
|
||||
message(VAAPI DRM support enabled)
|
||||
|
||||
PKGCONFIG += libva-drm
|
||||
DEFINES += HAVE_LIBVA_DRM
|
||||
}
|
||||
libvdpau {
|
||||
message(VDPAU renderer selected)
|
||||
|
||||
@@ -125,31 +280,157 @@ libvdpau {
|
||||
SOURCES += streaming/video/ffmpeg-renderers/vdpau.cpp
|
||||
HEADERS += streaming/video/ffmpeg-renderers/vdpau.h
|
||||
}
|
||||
config_SLVideo {
|
||||
message(SLVideo decoder/renderer selected)
|
||||
mmal {
|
||||
message(MMAL renderer selected)
|
||||
|
||||
DEFINES += HAVE_SLVIDEO
|
||||
LIBS += -lSLVideo
|
||||
SOURCES += streaming/video/sl.cpp
|
||||
HEADERS += streaming/video/sl.h
|
||||
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)
|
||||
|
||||
DEFINES += HAVE_DRM
|
||||
SOURCES += streaming/video/ffmpeg-renderers/drm.cpp
|
||||
HEADERS += streaming/video/ffmpeg-renderers/drm.h
|
||||
|
||||
linux {
|
||||
message(Master hooks enabled)
|
||||
SOURCES += masterhook.c masterhook_internal.c
|
||||
LIBS += -ldl
|
||||
}
|
||||
}
|
||||
cuda {
|
||||
message(CUDA support enabled)
|
||||
|
||||
DEFINES += HAVE_CUDA
|
||||
SOURCES += streaming/video/ffmpeg-renderers/cuda.cpp
|
||||
HEADERS += streaming/video/ffmpeg-renderers/cuda.h
|
||||
}
|
||||
config_EGL {
|
||||
message(EGL renderer selected)
|
||||
|
||||
CONFIG += egl
|
||||
DEFINES += HAVE_EGL
|
||||
SOURCES += \
|
||||
streaming/video/ffmpeg-renderers/eglvid.cpp \
|
||||
streaming/video/ffmpeg-renderers/egl_extensions.cpp \
|
||||
streaming/video/ffmpeg-renderers/eglimagefactory.cpp
|
||||
HEADERS += \
|
||||
streaming/video/ffmpeg-renderers/eglvid.h \
|
||||
streaming/video/ffmpeg-renderers/eglimagefactory.h
|
||||
}
|
||||
config_SL {
|
||||
message(Steam Link build configuration selected)
|
||||
|
||||
DEFINES += EMBEDDED_BUILD STEAM_LINK HAVE_SLVIDEO HAVE_SLAUDIO
|
||||
LIBS += -lSLVideo -lSLAudio
|
||||
|
||||
SOURCES += \
|
||||
streaming/video/slvid.cpp \
|
||||
streaming/audio/renderers/slaud.cpp
|
||||
HEADERS += \
|
||||
streaming/video/slvid.h \
|
||||
streaming/audio/renderers/slaud.h
|
||||
}
|
||||
win32 {
|
||||
message(DXVA2 renderer selected)
|
||||
HEADERS += streaming/video/ffmpeg-renderers/dxutil.h
|
||||
}
|
||||
win32:!winrt {
|
||||
message(DXVA2 and D3D11VA renderers selected)
|
||||
|
||||
SOURCES += streaming/video/ffmpeg-renderers/dxva2.cpp
|
||||
HEADERS += streaming/video/ffmpeg-renderers/dxva2.h
|
||||
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 {
|
||||
message(VideoToolbox renderer selected)
|
||||
|
||||
SOURCES += streaming/video/ffmpeg-renderers/vt.mm
|
||||
HEADERS += streaming/video/ffmpeg-renderers/vt.h
|
||||
SOURCES += \
|
||||
streaming/video/ffmpeg-renderers/vt.mm
|
||||
|
||||
HEADERS += \
|
||||
streaming/video/ffmpeg-renderers/vt.h
|
||||
}
|
||||
soundio {
|
||||
message(libsoundio audio renderer selected)
|
||||
|
||||
DEFINES += HAVE_SOUNDIO SOUNDIO_STATIC_LIBRARY
|
||||
SOURCES += streaming/audio/renderers/soundioaudiorenderer.cpp
|
||||
HEADERS += streaming/audio/renderers/soundioaudiorenderer.h
|
||||
}
|
||||
discord-rpc {
|
||||
message(Discord integration enabled)
|
||||
|
||||
LIBS += -ldiscord-rpc
|
||||
DEFINES += HAVE_DISCORD
|
||||
}
|
||||
embedded {
|
||||
message(Embedded build)
|
||||
|
||||
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_de.ts \
|
||||
languages/qml_fr.ts \
|
||||
languages/qml_nb_NO.ts \
|
||||
languages/qml_ru.ts \
|
||||
languages/qml_es.ts \
|
||||
languages/qml_ja.ts \
|
||||
languages/qml_vi.ts \
|
||||
languages/qml_th.ts \
|
||||
languages/qml_ko.ts \
|
||||
languages/qml_hu.ts \
|
||||
languages/qml_nl.ts \
|
||||
languages/qml_sv.ts \
|
||||
languages/qml_tr.ts \
|
||||
languages/qml_uk.ts \
|
||||
languages/qml_zh_TW.ts \
|
||||
languages/qml_el.ts \
|
||||
languages/qml_hi.ts \
|
||||
languages/qml_it.ts \
|
||||
languages/qml_pt.ts \
|
||||
languages/qml_pt_BR.ts \
|
||||
languages/qml_pl.ts \
|
||||
languages/qml_cs.ts \
|
||||
languages/qml_he.ts
|
||||
|
||||
# Additional import path used to resolve QML modules in Qt Creator's code model
|
||||
QML_IMPORT_PATH =
|
||||
|
||||
@@ -170,6 +451,30 @@ else:unix: LIBS += -L$$OUT_PWD/../qmdnsengine/ -lqmdnsengine
|
||||
INCLUDEPATH += $$PWD/../qmdnsengine/qmdnsengine/src/include $$PWD/../qmdnsengine
|
||||
DEPENDPATH += $$PWD/../qmdnsengine/qmdnsengine/src/include $$PWD/../qmdnsengine
|
||||
|
||||
soundio {
|
||||
win32:CONFIG(release, debug|release): LIBS += -L$$OUT_PWD/../soundio/release/ -lsoundio
|
||||
else:win32:CONFIG(debug, debug|release): LIBS += -L$$OUT_PWD/../soundio/debug/ -lsoundio
|
||||
else:unix: LIBS += -L$$OUT_PWD/../soundio/ -lsoundio
|
||||
|
||||
INCLUDEPATH += $$PWD/../soundio/libsoundio
|
||||
DEPENDPATH += $$PWD/../soundio/libsoundio
|
||||
}
|
||||
|
||||
win32:CONFIG(release, debug|release): LIBS += -L$$OUT_PWD/../h264bitstream/release/ -lh264bitstream
|
||||
else:win32:CONFIG(debug, debug|release): LIBS += -L$$OUT_PWD/../h264bitstream/debug/ -lh264bitstream
|
||||
else:unix: LIBS += -L$$OUT_PWD/../h264bitstream/ -lh264bitstream
|
||||
|
||||
INCLUDEPATH += $$PWD/../h264bitstream/h264bitstream
|
||||
DEPENDPATH += $$PWD/../h264bitstream/h264bitstream
|
||||
|
||||
!winrt {
|
||||
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
|
||||
}
|
||||
|
||||
unix:!macx: {
|
||||
isEmpty(PREFIX) {
|
||||
PREFIX = /usr/local
|
||||
@@ -177,29 +482,52 @@ unix:!macx: {
|
||||
isEmpty(BINDIR) {
|
||||
BINDIR = bin
|
||||
}
|
||||
isEmpty(DATADIR) {
|
||||
DATADIR = share
|
||||
}
|
||||
|
||||
target.path = $$PREFIX/$$BINDIR/
|
||||
|
||||
desktop.files = deploy/linux/com.moonlight_stream.Moonlight.desktop
|
||||
desktop.path = $$PREFIX/share/applications/
|
||||
desktop.path = $$PREFIX/$$DATADIR/applications/
|
||||
|
||||
icons.files = res/moonlight.svg
|
||||
icons.path = $$PREFIX/share/icons/hicolor/scalable/apps/
|
||||
icons.path = $$PREFIX/$$DATADIR/icons/hicolor/scalable/apps/
|
||||
|
||||
appdata.files = deploy/linux/com.moonlight_stream.Moonlight.appdata.xml
|
||||
appdata.path = $$PREFIX/share/metainfo/
|
||||
appstream.files = deploy/linux/com.moonlight_stream.Moonlight.appdata.xml
|
||||
appstream.path = $$PREFIX/$$DATADIR/metainfo/
|
||||
|
||||
INSTALLS += target desktop icons appdata
|
||||
INSTALLS += target desktop icons appstream
|
||||
}
|
||||
win32 {
|
||||
RC_ICONS = moonlight.ico
|
||||
QMAKE_TARGET_COMPANY = Moonlight Game Streaming Project
|
||||
QMAKE_TARGET_DESCRIPTION = Moonlight Game Streaming Client
|
||||
QMAKE_TARGET_PRODUCT = Moonlight
|
||||
|
||||
CONFIG -= embed_manifest_exe
|
||||
QMAKE_LFLAGS += /MANIFEST:embed /MANIFESTINPUT:$${PWD}/Moonlight.exe.manifest
|
||||
}
|
||||
macx {
|
||||
QMAKE_INFO_PLIST = $$PWD/Info.plist
|
||||
# Create Info.plist in object dir with the correct version string
|
||||
system(cp $$PWD/Info.plist $$OUT_PWD/Info.plist)
|
||||
system(sed -i -e 's/VERSION/$$cat(version.txt)/g' $$OUT_PWD/Info.plist)
|
||||
|
||||
QMAKE_INFO_PLIST = $$OUT_PWD/Info.plist
|
||||
|
||||
APP_BUNDLE_RESOURCES.files = moonlight.icns
|
||||
APP_BUNDLE_RESOURCES.path = Contents/Resources
|
||||
QMAKE_BUNDLE_DATA += APP_BUNDLE_RESOURCES
|
||||
|
||||
APP_BUNDLE_FRAMEWORKS.files = $$files(../libs/mac/Frameworks/*.framework, true) $$files(../libs/mac/lib/*.dylib, true)
|
||||
APP_BUNDLE_FRAMEWORKS.path = Contents/Frameworks
|
||||
|
||||
APP_BUNDLE_PLIST.files = $$OUT_PWD/Info.plist
|
||||
APP_BUNDLE_PLIST.path = Contents
|
||||
|
||||
QMAKE_BUNDLE_DATA += APP_BUNDLE_RESOURCES APP_BUNDLE_FRAMEWORKS APP_BUNDLE_PLIST
|
||||
|
||||
QMAKE_RPATHDIR += @executable_path/../Frameworks
|
||||
}
|
||||
|
||||
VERSION = 0.1.0
|
||||
VERSION = "$$cat(version.txt)"
|
||||
DEFINES += VERSION_STR=\\\"$$cat(version.txt)\\\"
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
#include "autoupdatechecker.h"
|
||||
|
||||
#include <QNetworkReply>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
|
||||
AutoUpdateChecker::AutoUpdateChecker(QObject *parent) :
|
||||
QObject(parent)
|
||||
{
|
||||
m_Nam = new QNetworkAccessManager(this);
|
||||
|
||||
// Never communicate over HTTP
|
||||
m_Nam->setStrictTransportSecurityEnabled(true);
|
||||
|
||||
// Allow HTTP redirects
|
||||
m_Nam->setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy);
|
||||
|
||||
connect(m_Nam, &QNetworkAccessManager::finished,
|
||||
this, &AutoUpdateChecker::handleUpdateCheckRequestFinished);
|
||||
|
||||
QString currentVersion(VERSION_STR);
|
||||
qDebug() << "Current Moonlight version:" << currentVersion;
|
||||
parseStringToVersionQuad(currentVersion, m_CurrentVersionQuad);
|
||||
|
||||
// Should at least have a 1.0-style version number
|
||||
Q_ASSERT(m_CurrentVersionQuad.count() > 1);
|
||||
}
|
||||
|
||||
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);
|
||||
QT_WARNING_POP
|
||||
#endif
|
||||
|
||||
// We'll get a callback when this is finished
|
||||
QUrl url("https://moonlight-stream.org/updates/qt.json");
|
||||
QNetworkRequest request(url);
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
|
||||
request.setAttribute(QNetworkRequest::Http2AllowedAttribute, true);
|
||||
#else
|
||||
request.setAttribute(QNetworkRequest::HTTP2AllowedAttribute, true);
|
||||
#endif
|
||||
m_Nam->get(request);
|
||||
#endif
|
||||
}
|
||||
|
||||
void AutoUpdateChecker::parseStringToVersionQuad(QString& string, QVector<int>& version)
|
||||
{
|
||||
QStringList list = string.split('.');
|
||||
for (const QString& component : list) {
|
||||
version.append(component.toInt());
|
||||
}
|
||||
}
|
||||
|
||||
QString AutoUpdateChecker::getPlatform()
|
||||
{
|
||||
#if defined(STEAM_LINK)
|
||||
return QStringLiteral("steamlink");
|
||||
#elif defined(APP_IMAGE)
|
||||
return QStringLiteral("appimage");
|
||||
#elif defined(Q_OS_DARWIN) && QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
// Qt 6 changed this from 'osx' to 'macos'. Use the old one
|
||||
// to be consistent (and not require another entry in the manifest).
|
||||
return QStringLiteral("osx");
|
||||
#else
|
||||
return QSysInfo::productType();
|
||||
#endif
|
||||
}
|
||||
|
||||
int AutoUpdateChecker::compareVersion(QVector<int>& version1, QVector<int>& version2) {
|
||||
for (int i = 0;; i++) {
|
||||
int v1Val = 0;
|
||||
int v2Val = 0;
|
||||
|
||||
// Treat missing decimal places as 0
|
||||
if (i < version1.count()) {
|
||||
v1Val = version1[i];
|
||||
}
|
||||
if (i < version2.count()) {
|
||||
v2Val = version2[i];
|
||||
}
|
||||
if (i >= version1.count() && i >= version2.count()) {
|
||||
// Equal versions
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (v1Val < v2Val) {
|
||||
return -1;
|
||||
}
|
||||
else if (v1Val > v2Val) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AutoUpdateChecker::handleUpdateCheckRequestFinished(QNetworkReply* reply)
|
||||
{
|
||||
Q_ASSERT(reply->isFinished());
|
||||
|
||||
// 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);
|
||||
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
stream.setEncoding(QStringConverter::Utf8);
|
||||
#else
|
||||
stream.setCodec("UTF-8");
|
||||
#endif
|
||||
|
||||
// Read all data and queue the reply for deletion
|
||||
QString jsonString = stream.readAll();
|
||||
reply->deleteLater();
|
||||
|
||||
QJsonParseError error;
|
||||
QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonString.toUtf8(), &error);
|
||||
if (jsonDoc.isNull()) {
|
||||
qWarning() << "Update manifest malformed:" << error.errorString();
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonArray array = jsonDoc.array();
|
||||
if (array.isEmpty()) {
|
||||
qWarning() << "Update manifest doesn't contain an array";
|
||||
return;
|
||||
}
|
||||
|
||||
for (QJsonValueRef updateEntry : array) {
|
||||
if (updateEntry.isObject()) {
|
||||
QJsonObject updateObj = updateEntry.toObject();
|
||||
if (!updateObj.contains("platform") ||
|
||||
!updateObj.contains("arch") ||
|
||||
!updateObj.contains("version") ||
|
||||
!updateObj.contains("browser_url")) {
|
||||
qWarning() << "Update manifest entry missing vital field";
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!updateObj["platform"].isString() ||
|
||||
!updateObj["arch"].isString() ||
|
||||
!updateObj["version"].isString() ||
|
||||
!updateObj["browser_url"].isString()) {
|
||||
qWarning() << "Update manifest entry has unexpected vital field type";
|
||||
continue;
|
||||
}
|
||||
|
||||
if (updateObj["arch"] == QSysInfo::buildCpuArchitecture() &&
|
||||
updateObj["platform"] == getPlatform()) {
|
||||
|
||||
// Check the kernel version minimum if one exists
|
||||
if (updateObj.contains("kernel_version_at_least") && updateObj["kernel_version_at_least"].isString()) {
|
||||
QVector<int> requiredVersionQuad;
|
||||
QVector<int> actualVersionQuad;
|
||||
|
||||
QString requiredVersion = updateObj["kernel_version_at_least"].toString();
|
||||
QString actualVersion = QSysInfo::kernelVersion();
|
||||
parseStringToVersionQuad(requiredVersion, requiredVersionQuad);
|
||||
parseStringToVersionQuad(actualVersion, actualVersionQuad);
|
||||
|
||||
if (compareVersion(actualVersionQuad, requiredVersionQuad) < 0) {
|
||||
qDebug() << "Skipping manifest entry due to kernel version (" << actualVersion << "<" << requiredVersion << ")";
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
qDebug() << "Found update manifest match for current platform";
|
||||
|
||||
QString latestVersion = updateObj["version"].toString();
|
||||
qDebug() << "Latest version of Moonlight for this platform is:" << latestVersion;
|
||||
|
||||
QVector<int> latestVersionQuad;
|
||||
parseStringToVersionQuad(latestVersion, latestVersionQuad);
|
||||
|
||||
int res = compareVersion(m_CurrentVersionQuad, latestVersionQuad);
|
||||
if (res < 0) {
|
||||
// m_CurrentVersionQuad < latestVersionQuad
|
||||
qDebug() << "Update available";
|
||||
emit onUpdateAvailable(updateObj["version"].toString(),
|
||||
updateObj["browser_url"].toString());
|
||||
return;
|
||||
}
|
||||
else if (res > 0) {
|
||||
qDebug() << "Update manifest version lower than current version";
|
||||
return;
|
||||
}
|
||||
else {
|
||||
qDebug() << "Update manifest version equal to current version";
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
qWarning() << "Update manifest contained unrecognized entry:" << updateEntry.toString();
|
||||
}
|
||||
}
|
||||
|
||||
qWarning() << "No entry in update manifest found for current platform:"
|
||||
<< QSysInfo::buildCpuArchitecture() << getPlatform() << QSysInfo::kernelVersion();
|
||||
}
|
||||
else {
|
||||
qWarning() << "Update checking failed with error:" << reply->error();
|
||||
reply->deleteLater();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QNetworkAccessManager>
|
||||
|
||||
class AutoUpdateChecker : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit AutoUpdateChecker(QObject *parent = nullptr);
|
||||
|
||||
Q_INVOKABLE void start();
|
||||
|
||||
signals:
|
||||
void onUpdateAvailable(QString newVersion, QString url);
|
||||
|
||||
private slots:
|
||||
void handleUpdateCheckRequestFinished(QNetworkReply* reply);
|
||||
|
||||
private:
|
||||
void parseStringToVersionQuad(QString& string, QVector<int>& version);
|
||||
|
||||
int compareVersion(QVector<int>& version1, QVector<int>& version2);
|
||||
|
||||
QString getPlatform();
|
||||
|
||||
QVector<int> m_CurrentVersionQuad;
|
||||
QNetworkAccessManager* m_Nam;
|
||||
};
|
||||
@@ -1,14 +1,19 @@
|
||||
#include "boxartmanager.h"
|
||||
#include "../path.h"
|
||||
|
||||
#include <QStandardPaths>
|
||||
#include <QImageReader>
|
||||
#include <QImageWriter>
|
||||
|
||||
BoxArtManager::BoxArtManager(QObject *parent) :
|
||||
QObject(parent),
|
||||
m_BoxArtDir(
|
||||
QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/boxart")
|
||||
m_BoxArtDir(Path::getBoxArtCacheDir()),
|
||||
m_ThreadPool(this)
|
||||
{
|
||||
// 4 is a good balance between fast loading for large
|
||||
// app grids and not crushing GFE with tons of requests
|
||||
// and causing UI jank from constantly stalling to decode
|
||||
// new images.
|
||||
m_ThreadPool.setMaxThreadCount(4);
|
||||
if (!m_BoxArtDir.exists()) {
|
||||
m_BoxArtDir.mkpath(".");
|
||||
}
|
||||
@@ -31,35 +36,77 @@ BoxArtManager::getFilePathForBoxArt(NvComputer* computer, int appId)
|
||||
return dir.filePath(QString::number(appId) + ".png");
|
||||
}
|
||||
|
||||
class NetworkBoxArtLoadTask : public QObject, public QRunnable
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
NetworkBoxArtLoadTask(BoxArtManager* boxArtManager, NvComputer* computer, NvApp& app)
|
||||
: m_Bam(boxArtManager),
|
||||
m_Computer(computer),
|
||||
m_App(app)
|
||||
{
|
||||
connect(this, &NetworkBoxArtLoadTask::boxArtFetchCompleted,
|
||||
boxArtManager, &BoxArtManager::handleBoxArtLoadComplete);
|
||||
}
|
||||
|
||||
signals:
|
||||
void boxArtFetchCompleted(NvComputer* computer, NvApp app, QUrl image);
|
||||
|
||||
private:
|
||||
void run()
|
||||
{
|
||||
QUrl image = m_Bam->loadBoxArtFromNetwork(m_Computer, m_App.id);
|
||||
if (image.isEmpty()) {
|
||||
// Give it another shot if it fails once
|
||||
image = m_Bam->loadBoxArtFromNetwork(m_Computer, m_App.id);
|
||||
}
|
||||
emit boxArtFetchCompleted(m_Computer, m_App, image);
|
||||
}
|
||||
|
||||
BoxArtManager* m_Bam;
|
||||
NvComputer* m_Computer;
|
||||
NvApp m_App;
|
||||
};
|
||||
|
||||
QUrl BoxArtManager::loadBoxArt(NvComputer* computer, NvApp& app)
|
||||
{
|
||||
// Try to open the cached file
|
||||
QString cacheFilePath = getFilePathForBoxArt(computer, app.id);
|
||||
if (QFile::exists(cacheFilePath)) {
|
||||
return QUrl::fromLocalFile(cacheFilePath);
|
||||
// Try to open the cached file if it exists and contains data
|
||||
QFile cacheFile(getFilePathForBoxArt(computer, app.id));
|
||||
if (cacheFile.exists() && cacheFile.size() > 0) {
|
||||
return QUrl::fromLocalFile(cacheFile.fileName());
|
||||
}
|
||||
|
||||
// If we get here, we need to fetch asynchronously.
|
||||
// Kick off a worker on our thread pool to do just that.
|
||||
NetworkBoxArtLoadTask* netLoadTask = new NetworkBoxArtLoadTask(this, computer, app);
|
||||
QThreadPool::globalInstance()->start(netLoadTask);
|
||||
m_ThreadPool.start(netLoadTask);
|
||||
|
||||
// Return the placeholder then we can notify the caller
|
||||
// later when the real image is ready.
|
||||
return QUrl("qrc:/res/no_app_image.png");
|
||||
}
|
||||
|
||||
void BoxArtManager::deleteBoxArt(NvComputer* computer)
|
||||
{
|
||||
QDir dir(Path::getBoxArtCacheDir());
|
||||
|
||||
// Delete everything in this computer's box art directory
|
||||
if (dir.cd(computer->uuid)) {
|
||||
dir.removeRecursively();
|
||||
}
|
||||
}
|
||||
|
||||
void BoxArtManager::handleBoxArtLoadComplete(NvComputer* computer, NvApp app, QUrl image)
|
||||
{
|
||||
if (image.isEmpty()) {
|
||||
image = QUrl("qrc:/res/no_app_image.png");
|
||||
if (!image.isEmpty()) {
|
||||
emit boxArtLoadComplete(computer, app, image);
|
||||
}
|
||||
emit boxArtLoadComplete(computer, app, image);
|
||||
}
|
||||
|
||||
QUrl BoxArtManager::loadBoxArtFromNetwork(NvComputer* computer, int appId)
|
||||
{
|
||||
NvHTTP http(computer->activeAddress);
|
||||
NvHTTP http(computer);
|
||||
|
||||
QString cachePath = getFilePathForBoxArt(computer, appId);
|
||||
QImage image;
|
||||
@@ -72,7 +119,13 @@ QUrl BoxArtManager::loadBoxArtFromNetwork(NvComputer* computer, int appId)
|
||||
if (image.save(cachePath)) {
|
||||
return QUrl::fromLocalFile(cachePath);
|
||||
}
|
||||
else {
|
||||
// A failed save() may leave a zero byte file. Make sure that's removed.
|
||||
QFile(cachePath).remove();
|
||||
}
|
||||
}
|
||||
|
||||
return QUrl();
|
||||
}
|
||||
|
||||
#include "boxartmanager.moc"
|
||||
|
||||
@@ -18,6 +18,10 @@ public:
|
||||
QUrl
|
||||
loadBoxArt(NvComputer* computer, NvApp& app);
|
||||
|
||||
static
|
||||
void
|
||||
deleteBoxArt(NvComputer* computer);
|
||||
|
||||
signals:
|
||||
void
|
||||
boxArtLoadComplete(NvComputer* computer, NvApp app, QUrl image);
|
||||
@@ -36,37 +40,5 @@ private:
|
||||
getFilePathForBoxArt(NvComputer* computer, int appId);
|
||||
|
||||
QDir m_BoxArtDir;
|
||||
};
|
||||
|
||||
class NetworkBoxArtLoadTask : public QObject, public QRunnable
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
NetworkBoxArtLoadTask(BoxArtManager* boxArtManager, NvComputer* computer, NvApp& app)
|
||||
: m_Bam(boxArtManager),
|
||||
m_Computer(computer),
|
||||
m_App(app)
|
||||
{
|
||||
connect(this, SIGNAL(boxArtFetchCompleted(NvComputer*,NvApp,QUrl)),
|
||||
boxArtManager, SLOT(handleBoxArtLoadComplete(NvComputer*,NvApp,QUrl)));
|
||||
}
|
||||
|
||||
signals:
|
||||
void boxArtFetchCompleted(NvComputer* computer, NvApp app, QUrl image);
|
||||
|
||||
private:
|
||||
void run()
|
||||
{
|
||||
QUrl image = m_Bam->loadBoxArtFromNetwork(m_Computer, m_App.id);
|
||||
if (image.isEmpty()) {
|
||||
// Give it another shot if it fails once
|
||||
image = m_Bam->loadBoxArtFromNetwork(m_Computer, m_App.id);
|
||||
}
|
||||
emit boxArtFetchCompleted(m_Computer, m_App, image);
|
||||
}
|
||||
|
||||
BoxArtManager* m_Bam;
|
||||
NvComputer* m_Computer;
|
||||
NvApp m_App;
|
||||
QThreadPool m_ThreadPool;
|
||||
};
|
||||
|
||||
+790
-350
File diff suppressed because it is too large
Load Diff
+174
-379
@@ -1,6 +1,8 @@
|
||||
#pragma once
|
||||
#include "nvhttp.h"
|
||||
|
||||
#include "nvcomputer.h"
|
||||
#include "nvpairingmanager.h"
|
||||
#include "settings/compatfetcher.h"
|
||||
|
||||
#include <qmdnsengine/server.h>
|
||||
#include <qmdnsengine/cache.h>
|
||||
@@ -12,194 +14,27 @@
|
||||
#include <QReadWriteLock>
|
||||
#include <QSettings>
|
||||
#include <QRunnable>
|
||||
#include <QTimer>
|
||||
#include <QMutex>
|
||||
#include <QWaitCondition>
|
||||
|
||||
class NvComputer
|
||||
{
|
||||
friend class PcMonitorThread;
|
||||
friend class ComputerManager;
|
||||
friend class PendingQuitTask;
|
||||
class ComputerManager;
|
||||
|
||||
private:
|
||||
void sortAppList();
|
||||
|
||||
bool pendingQuit;
|
||||
|
||||
public:
|
||||
explicit NvComputer(QString address, QString serverInfo);
|
||||
|
||||
explicit NvComputer(QSettings& settings);
|
||||
|
||||
bool
|
||||
update(NvComputer& that);
|
||||
|
||||
bool
|
||||
wake();
|
||||
|
||||
QVector<QString>
|
||||
uniqueAddresses();
|
||||
|
||||
void
|
||||
serialize(QSettings& settings);
|
||||
|
||||
enum PairState
|
||||
{
|
||||
PS_UNKNOWN,
|
||||
PS_PAIRED,
|
||||
PS_NOT_PAIRED
|
||||
};
|
||||
|
||||
enum ComputerState
|
||||
{
|
||||
CS_UNKNOWN,
|
||||
CS_ONLINE,
|
||||
CS_OFFLINE
|
||||
};
|
||||
|
||||
// Ephemeral traits
|
||||
ComputerState state;
|
||||
PairState pairState;
|
||||
QString activeAddress;
|
||||
int currentGameId;
|
||||
QString gfeVersion;
|
||||
QString appVersion;
|
||||
QVector<NvDisplayMode> displayModes;
|
||||
int maxLumaPixelsHEVC;
|
||||
int serverCodecModeSupport;
|
||||
QString gpuModel;
|
||||
|
||||
// Persisted traits
|
||||
QString localAddress;
|
||||
QString remoteAddress;
|
||||
QString manualAddress;
|
||||
QByteArray macAddress;
|
||||
QString name;
|
||||
QString uuid;
|
||||
QVector<NvApp> appList;
|
||||
|
||||
// Synchronization
|
||||
QReadWriteLock lock;
|
||||
};
|
||||
|
||||
// FIXME: MOC isn't finding Q_OBJECT properly when this is confined
|
||||
// to computermanager.cpp as it should be.
|
||||
class PcMonitorThread : public QThread
|
||||
class DelayedFlushThread : public QThread
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
#define TRIES_BEFORE_OFFLINING 2
|
||||
#define POLLS_PER_APPLIST_FETCH 10
|
||||
|
||||
public:
|
||||
PcMonitorThread(NvComputer* computer)
|
||||
: m_Computer(computer)
|
||||
DelayedFlushThread(ComputerManager* cm)
|
||||
: m_ComputerManager(cm)
|
||||
{
|
||||
setObjectName("Polling thread for " + computer->name);
|
||||
setObjectName("CM Delayed Flush Thread");
|
||||
}
|
||||
|
||||
void run();
|
||||
|
||||
private:
|
||||
bool tryPollComputer(QString address, bool& changed)
|
||||
{
|
||||
NvHTTP http(address);
|
||||
|
||||
QString serverInfo;
|
||||
try {
|
||||
serverInfo = http.getServerInfo();
|
||||
} catch (...) {
|
||||
return false;
|
||||
}
|
||||
|
||||
NvComputer newState(address, serverInfo);
|
||||
|
||||
// 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;
|
||||
return false;
|
||||
}
|
||||
|
||||
changed = m_Computer->update(newState);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool updateAppList(bool& changed)
|
||||
{
|
||||
Q_ASSERT(m_Computer->activeAddress != nullptr);
|
||||
|
||||
NvHTTP http(m_Computer->activeAddress);
|
||||
|
||||
QVector<NvApp> appList;
|
||||
|
||||
try {
|
||||
appList = http.getAppList();
|
||||
if (appList.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
} catch (...) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QWriteLocker lock(&m_Computer->lock);
|
||||
if (m_Computer->appList != appList) {
|
||||
m_Computer->appList = appList;
|
||||
m_Computer->sortAppList();
|
||||
changed = true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void run() override
|
||||
{
|
||||
// Always fetch the applist the first time
|
||||
int pollsSinceLastAppListFetch = POLLS_PER_APPLIST_FETCH;
|
||||
while (!isInterruptionRequested()) {
|
||||
bool stateChanged = false;
|
||||
bool online = false;
|
||||
for (int i = 0; i < TRIES_BEFORE_OFFLINING && !online; i++) {
|
||||
for (auto& address : m_Computer->uniqueAddresses()) {
|
||||
if (isInterruptionRequested()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (tryPollComputer(address, stateChanged)) {
|
||||
online = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we failed after all retry attempts
|
||||
// Note: we don't need to acquire the read lock here,
|
||||
// because we're on the writing thread.
|
||||
if (!online && m_Computer->state != NvComputer::CS_OFFLINE) {
|
||||
m_Computer->state = NvComputer::CS_OFFLINE;
|
||||
stateChanged = true;
|
||||
}
|
||||
|
||||
// Grab the applist if it's empty or it's been long enough that we need to refresh
|
||||
pollsSinceLastAppListFetch++;
|
||||
if (m_Computer->state == NvComputer::CS_ONLINE &&
|
||||
m_Computer->pairState == NvComputer::PS_PAIRED &&
|
||||
(m_Computer->appList.isEmpty() || pollsSinceLastAppListFetch >= POLLS_PER_APPLIST_FETCH)) {
|
||||
if (updateAppList(stateChanged)) {
|
||||
pollsSinceLastAppListFetch = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (stateChanged) {
|
||||
// Tell anyone listening that we've changed state
|
||||
emit computerStateChanged(m_Computer);
|
||||
}
|
||||
|
||||
// Wait a bit to poll again
|
||||
QThread::sleep(3);
|
||||
}
|
||||
}
|
||||
|
||||
signals:
|
||||
void computerStateChanged(NvComputer* computer);
|
||||
|
||||
private:
|
||||
NvComputer* m_Computer;
|
||||
ComputerManager* m_ComputerManager;
|
||||
};
|
||||
|
||||
class MdnsPendingComputer : public QObject
|
||||
@@ -207,14 +42,20 @@ class MdnsPendingComputer : public QObject
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit MdnsPendingComputer(QMdnsEngine::Server* server,
|
||||
QMdnsEngine::Cache* cache,
|
||||
explicit MdnsPendingComputer(const QSharedPointer<QMdnsEngine::Server> server,
|
||||
const QMdnsEngine::Service& service)
|
||||
: m_Hostname(service.hostname()),
|
||||
m_Resolver(server, m_Hostname, cache)
|
||||
m_Port(service.port()),
|
||||
m_ServerWeak(server),
|
||||
m_Resolver(nullptr)
|
||||
{
|
||||
connect(&m_Resolver, SIGNAL(resolved(QHostAddress)),
|
||||
this, SLOT(handleResolved(QHostAddress)));
|
||||
// Start resolving
|
||||
resolve();
|
||||
}
|
||||
|
||||
virtual ~MdnsPendingComputer()
|
||||
{
|
||||
delete m_Resolver;
|
||||
}
|
||||
|
||||
QString hostname()
|
||||
@@ -222,21 +63,136 @@ public:
|
||||
return m_Hostname;
|
||||
}
|
||||
|
||||
private slots:
|
||||
void handleResolved(const QHostAddress& address)
|
||||
uint16_t port()
|
||||
{
|
||||
if (address.protocol() == QAbstractSocket::IPv4Protocol) {
|
||||
m_Resolver.disconnect();
|
||||
emit resolvedv4(this, address);
|
||||
return m_Port;
|
||||
}
|
||||
|
||||
private slots:
|
||||
void handleResolvedTimeout()
|
||||
{
|
||||
if (m_Addresses.isEmpty()) {
|
||||
// Try again
|
||||
qInfo() << "Resolving" << hostname() << "timed out. Retrying...";
|
||||
resolve();
|
||||
}
|
||||
else {
|
||||
Q_ASSERT(!m_Addresses.isEmpty());
|
||||
emit resolvedHost(this, m_Addresses);
|
||||
}
|
||||
}
|
||||
|
||||
void handleResolvedAddress(const QHostAddress& address)
|
||||
{
|
||||
qInfo() << "Resolved" << hostname() << "to" << address;
|
||||
m_Addresses.push_back(address);
|
||||
}
|
||||
|
||||
signals:
|
||||
void resolvedv4(MdnsPendingComputer*, const QHostAddress&);
|
||||
void resolvedHost(MdnsPendingComputer*,QVector<QHostAddress>&);
|
||||
|
||||
private:
|
||||
void resolve()
|
||||
{
|
||||
// Delete our resolver, so we're guaranteed that nothing is referencing m_Server.
|
||||
delete m_Resolver;
|
||||
m_Resolver = nullptr;
|
||||
|
||||
// Now delete our strong reference that we held on behalf of m_Resolver.
|
||||
// The server may be destroyed after we make this call.
|
||||
m_Server.reset();
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
QByteArray m_Hostname;
|
||||
QMdnsEngine::Resolver m_Resolver;
|
||||
uint16_t m_Port;
|
||||
QWeakPointer<QMdnsEngine::Server> m_ServerWeak;
|
||||
QSharedPointer<QMdnsEngine::Server> m_Server;
|
||||
QMdnsEngine::Resolver* m_Resolver;
|
||||
QVector<QHostAddress> m_Addresses;
|
||||
};
|
||||
|
||||
class ComputerPollingEntry
|
||||
{
|
||||
public:
|
||||
ComputerPollingEntry()
|
||||
: m_ActiveThread(nullptr)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
virtual ~ComputerPollingEntry()
|
||||
{
|
||||
interrupt();
|
||||
|
||||
// interrupt() should have taken care of this
|
||||
Q_ASSERT(m_ActiveThread == nullptr);
|
||||
|
||||
for (QThread* thread : m_InactiveList) {
|
||||
thread->wait();
|
||||
delete thread;
|
||||
}
|
||||
}
|
||||
|
||||
bool isActive()
|
||||
{
|
||||
cleanInactiveList();
|
||||
|
||||
return m_ActiveThread != nullptr;
|
||||
}
|
||||
|
||||
void setActiveThread(QThread* thread)
|
||||
{
|
||||
cleanInactiveList();
|
||||
|
||||
Q_ASSERT(!isActive());
|
||||
m_ActiveThread = thread;
|
||||
}
|
||||
|
||||
void interrupt()
|
||||
{
|
||||
cleanInactiveList();
|
||||
|
||||
if (m_ActiveThread != nullptr) {
|
||||
// Interrupt the active thread
|
||||
m_ActiveThread->requestInterruption();
|
||||
|
||||
// Place it on the inactive list awaiting death
|
||||
m_InactiveList.append(m_ActiveThread);
|
||||
|
||||
m_ActiveThread = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
void cleanInactiveList()
|
||||
{
|
||||
QMutableListIterator<QThread*> i(m_InactiveList);
|
||||
|
||||
// Reap any threads that have finished
|
||||
while (i.hasNext()) {
|
||||
i.next();
|
||||
|
||||
QThread* thread = i.value();
|
||||
if (thread->isFinished()) {
|
||||
delete thread;
|
||||
i.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QThread* m_ActiveThread;
|
||||
QList<QThread*> m_InactiveList;
|
||||
};
|
||||
|
||||
class ComputerManager : public QObject
|
||||
@@ -245,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);
|
||||
@@ -255,7 +213,11 @@ public:
|
||||
|
||||
Q_INVOKABLE void stopPollingAsync();
|
||||
|
||||
Q_INVOKABLE void addNewHost(QString address, bool mdns);
|
||||
Q_INVOKABLE void addNewHostManually(QString address);
|
||||
|
||||
void addNewHost(NvAddress address, bool mdns, NvAddress mdnsIpv6Address = NvAddress());
|
||||
|
||||
QString generatePinString();
|
||||
|
||||
void pairHost(NvComputer* computer, QString pin);
|
||||
|
||||
@@ -266,213 +228,46 @@ public:
|
||||
// computer is deleted inside this call
|
||||
void deleteHost(NvComputer* computer);
|
||||
|
||||
void renameHost(NvComputer* computer, QString name);
|
||||
|
||||
void clientSideAttributeUpdated(NvComputer* computer);
|
||||
|
||||
signals:
|
||||
void computerStateChanged(NvComputer* computer);
|
||||
|
||||
void pairingCompleted(NvComputer* computer, QString error);
|
||||
|
||||
void computerAddCompleted(QVariant success);
|
||||
void computerAddCompleted(QVariant success, QVariant detectedPortBlocking);
|
||||
|
||||
void quitAppCompleted(QVariant error);
|
||||
|
||||
private slots:
|
||||
void handleAboutToQuit();
|
||||
|
||||
void handleComputerStateChanged(NvComputer* computer);
|
||||
|
||||
void handleMdnsServiceResolved(MdnsPendingComputer* computer, const QHostAddress& address);
|
||||
void handleMdnsServiceResolved(MdnsPendingComputer* computer, QVector<QHostAddress>& addresses);
|
||||
|
||||
private:
|
||||
void saveHosts();
|
||||
|
||||
void saveHost(NvComputer* computer);
|
||||
|
||||
QHostAddress getBestGlobalAddressV6(QVector<QHostAddress>& addresses);
|
||||
|
||||
void startPollingComputer(NvComputer* computer);
|
||||
|
||||
int m_PollingRef;
|
||||
QReadWriteLock m_Lock;
|
||||
QMap<QString, NvComputer*> m_KnownHosts;
|
||||
QMap<QString, QThread*> m_PollThreads;
|
||||
QMdnsEngine::Server m_MdnsServer;
|
||||
QMap<QString, ComputerPollingEntry*> m_PollEntries;
|
||||
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;
|
||||
};
|
||||
|
||||
class PendingPairingTask : public QObject, public QRunnable
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
PendingPairingTask(ComputerManager* computerManager, NvComputer* computer, QString pin)
|
||||
: m_Computer(computer),
|
||||
m_Pin(pin)
|
||||
{
|
||||
connect(this, &PendingPairingTask::pairingCompleted,
|
||||
computerManager, &ComputerManager::pairingCompleted);
|
||||
}
|
||||
|
||||
signals:
|
||||
void pairingCompleted(NvComputer* computer, QString error);
|
||||
|
||||
private:
|
||||
void run()
|
||||
{
|
||||
NvPairingManager pairingManager(m_Computer->activeAddress);
|
||||
|
||||
try {
|
||||
NvPairingManager::PairState result = pairingManager.pair(m_Computer->appVersion, m_Pin);
|
||||
switch (result)
|
||||
{
|
||||
case NvPairingManager::PairState::PIN_WRONG:
|
||||
emit pairingCompleted(m_Computer, "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.");
|
||||
break;
|
||||
case NvPairingManager::PairState::ALREADY_IN_PROGRESS:
|
||||
emit pairingCompleted(m_Computer, "Another pairing attempt is already in progress.");
|
||||
break;
|
||||
case NvPairingManager::PairState::PAIRED:
|
||||
emit pairingCompleted(m_Computer, nullptr);
|
||||
break;
|
||||
}
|
||||
} catch (const GfeHttpResponseException& e) {
|
||||
emit pairingCompleted(m_Computer, e.toQString());
|
||||
}
|
||||
}
|
||||
|
||||
NvComputer* m_Computer;
|
||||
QString m_Pin;
|
||||
};
|
||||
|
||||
class PendingQuitTask : public QObject, public QRunnable
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
PendingQuitTask(ComputerManager* computerManager, NvComputer* computer)
|
||||
: m_Computer(computer)
|
||||
{
|
||||
connect(this, &PendingQuitTask::quitAppFailed,
|
||||
computerManager, &ComputerManager::quitAppCompleted);
|
||||
}
|
||||
|
||||
signals:
|
||||
void quitAppFailed(QString error);
|
||||
|
||||
private:
|
||||
void run()
|
||||
{
|
||||
NvHTTP http(m_Computer->activeAddress);
|
||||
|
||||
try {
|
||||
if (m_Computer->currentGameId != 0) {
|
||||
http.quitApp();
|
||||
}
|
||||
} catch (const GfeHttpResponseException& e) {
|
||||
{
|
||||
QWriteLocker lock(&m_Computer->lock);
|
||||
m_Computer->pendingQuit = false;
|
||||
}
|
||||
if (e.getStatusCode() == 599) {
|
||||
// 599 is a special code we make a custom message for
|
||||
emit quitAppFailed("The running game wasn't started by this PC. "
|
||||
"You must quit the game on the host PC manually or use the device that originally started the game.");
|
||||
}
|
||||
else {
|
||||
emit quitAppFailed(e.toQString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NvComputer* m_Computer;
|
||||
};
|
||||
|
||||
class PendingAddTask : public QObject, public QRunnable
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
PendingAddTask(ComputerManager* computerManager, QString address, bool mdns)
|
||||
: m_ComputerManager(computerManager),
|
||||
m_Address(address),
|
||||
m_Mdns(mdns)
|
||||
{
|
||||
connect(this, &PendingAddTask::computerAddCompleted,
|
||||
computerManager, &ComputerManager::computerAddCompleted);
|
||||
connect(this, &PendingAddTask::computerStateChanged,
|
||||
computerManager, &ComputerManager::handleComputerStateChanged);
|
||||
}
|
||||
|
||||
signals:
|
||||
void computerAddCompleted(QVariant success);
|
||||
|
||||
void computerStateChanged(NvComputer* computer);
|
||||
|
||||
private:
|
||||
void run()
|
||||
{
|
||||
NvHTTP http(m_Address);
|
||||
|
||||
QString serverInfo;
|
||||
try {
|
||||
serverInfo = http.getServerInfo();
|
||||
} catch (...) {
|
||||
if (!m_Mdns) {
|
||||
emit computerAddCompleted(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
NvComputer* newComputer = new NvComputer(m_Address, serverInfo);
|
||||
|
||||
// Update addresses depending on the context
|
||||
if (m_Mdns) {
|
||||
newComputer->localAddress = m_Address;
|
||||
}
|
||||
else {
|
||||
newComputer->manualAddress = m_Address;
|
||||
}
|
||||
|
||||
// Check if this PC already exists
|
||||
QWriteLocker lock(&m_ComputerManager->m_Lock);
|
||||
NvComputer* existingComputer = m_ComputerManager->m_KnownHosts[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();
|
||||
|
||||
// For non-mDNS clients, let them know it succeeded
|
||||
if (!m_Mdns) {
|
||||
emit computerAddCompleted(true);
|
||||
}
|
||||
|
||||
// Tell our client if something changed
|
||||
if (changed) {
|
||||
emit computerStateChanged(existingComputer);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Store this in our active sets
|
||||
m_ComputerManager->m_KnownHosts[newComputer->uuid] = newComputer;
|
||||
|
||||
// Start polling if enabled (write lock required)
|
||||
m_ComputerManager->startPollingComputer(newComputer);
|
||||
|
||||
// Drop the lock before notifying
|
||||
lock.unlock();
|
||||
|
||||
// For non-mDNS clients, let them know it succeeded
|
||||
if (!m_Mdns) {
|
||||
emit computerAddCompleted(true);
|
||||
}
|
||||
|
||||
// Tell our client about this new PC
|
||||
emit computerStateChanged(newComputer);
|
||||
}
|
||||
}
|
||||
|
||||
ComputerManager* m_ComputerManager;
|
||||
QString m_Address;
|
||||
bool m_Mdns;
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
#include "computerseeker.h"
|
||||
#include "computermanager.h"
|
||||
#include <QTimer>
|
||||
|
||||
ComputerSeeker::ComputerSeeker(ComputerManager *manager, QString computerName, QObject *parent)
|
||||
: QObject(parent), m_ComputerManager(manager), m_ComputerName(computerName),
|
||||
m_TimeoutTimer(new QTimer(this))
|
||||
{
|
||||
m_TimeoutTimer->setSingleShot(true);
|
||||
connect(m_TimeoutTimer, &QTimer::timeout,
|
||||
this, &ComputerSeeker::onTimeout);
|
||||
connect(m_ComputerManager, &ComputerManager::computerStateChanged,
|
||||
this, &ComputerSeeker::onComputerUpdated);
|
||||
}
|
||||
|
||||
void ComputerSeeker::start(int timeout)
|
||||
{
|
||||
m_TimeoutTimer->start(timeout);
|
||||
// Seek desired computer by both connecting to it directly (this may fail
|
||||
// if m_ComputerName is UUID, or the name that doesn't resolve to an IP
|
||||
// address) and by polling it using mDNS, hopefully one of these methods
|
||||
// would find the host
|
||||
m_ComputerManager->addNewHostManually(m_ComputerName);
|
||||
m_ComputerManager->startPolling();
|
||||
}
|
||||
|
||||
void ComputerSeeker::onComputerUpdated(NvComputer *computer)
|
||||
{
|
||||
if (!m_TimeoutTimer->isActive()) {
|
||||
return;
|
||||
}
|
||||
if (matchComputer(computer) && isOnline(computer)) {
|
||||
m_ComputerManager->stopPollingAsync();
|
||||
m_TimeoutTimer->stop();
|
||||
emit computerFound(computer);
|
||||
}
|
||||
}
|
||||
|
||||
bool ComputerSeeker::matchComputer(NvComputer *computer) const
|
||||
{
|
||||
QString value = m_ComputerName.toLower();
|
||||
|
||||
if (computer->name.toLower() == value || computer->uuid.toLower() == value) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (const NvAddress& addr : computer->uniqueAddresses()) {
|
||||
if (addr.address().toLower() == value || addr.toString().toLower() == value) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ComputerSeeker::isOnline(NvComputer *computer) const
|
||||
{
|
||||
return computer->state == NvComputer::CS_ONLINE;
|
||||
}
|
||||
|
||||
void ComputerSeeker::onTimeout()
|
||||
{
|
||||
m_TimeoutTimer->stop();
|
||||
m_ComputerManager->stopPollingAsync();
|
||||
emit errorTimeout();
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
|
||||
class ComputerManager;
|
||||
class NvComputer;
|
||||
class QTimer;
|
||||
|
||||
class ComputerSeeker : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit ComputerSeeker(ComputerManager *manager, QString computerName, QObject *parent = nullptr);
|
||||
|
||||
void start(int timeout);
|
||||
|
||||
signals:
|
||||
void computerFound(NvComputer *computer);
|
||||
void errorTimeout();
|
||||
|
||||
private slots:
|
||||
void onComputerUpdated(NvComputer *computer);
|
||||
void onTimeout();
|
||||
|
||||
private:
|
||||
bool matchComputer(NvComputer *computer) const;
|
||||
bool isOnline(NvComputer *computer) const;
|
||||
|
||||
private:
|
||||
ComputerManager *m_ComputerManager;
|
||||
QString m_ComputerName;
|
||||
QTimer *m_TimeoutTimer;
|
||||
};
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
#include <openssl/pem.h>
|
||||
#include <openssl/rsa.h>
|
||||
#include <openssl/bn.h>
|
||||
#include <openssl/x509.h>
|
||||
#include <openssl/rand.h>
|
||||
|
||||
@@ -32,20 +31,19 @@ void IdentityManager::createCredentials(QSettings& settings)
|
||||
X509* cert = X509_new();
|
||||
THROW_BAD_ALLOC_IF_NULL(cert);
|
||||
|
||||
EVP_PKEY* pk = EVP_PKEY_new();
|
||||
EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, NULL);
|
||||
THROW_BAD_ALLOC_IF_NULL(ctx);
|
||||
|
||||
EVP_PKEY_keygen_init(ctx);
|
||||
EVP_PKEY_CTX_set_rsa_keygen_bits(ctx, 2048);
|
||||
|
||||
// pk must be initialized on input
|
||||
EVP_PKEY* pk = NULL;
|
||||
EVP_PKEY_keygen(ctx, &pk);
|
||||
|
||||
EVP_PKEY_CTX_free(ctx);
|
||||
THROW_BAD_ALLOC_IF_NULL(pk);
|
||||
|
||||
BIGNUM* bne = BN_new();
|
||||
THROW_BAD_ALLOC_IF_NULL(bne);
|
||||
|
||||
RSA* rsa = RSA_new();
|
||||
THROW_BAD_ALLOC_IF_NULL(rsa);
|
||||
|
||||
BN_set_word(bne, RSA_F4);
|
||||
RSA_generate_key_ex(rsa, 2048, bne, nullptr);
|
||||
|
||||
EVP_PKEY_assign_RSA(pk, rsa);
|
||||
|
||||
X509_set_version(cert, 2);
|
||||
ASN1_INTEGER_set(X509_get_serialNumber(cert), 0);
|
||||
#if OPENSSL_VERSION_NUMBER < 0x10100000L
|
||||
@@ -75,7 +73,7 @@ void IdentityManager::createCredentials(QSettings& settings)
|
||||
-1, -1, 0);
|
||||
X509_set_issuer_name(cert, name);
|
||||
|
||||
X509_sign(cert, pk, EVP_sha1());
|
||||
X509_sign(cert, pk, EVP_sha256());
|
||||
|
||||
BIO* biokey = BIO_new(BIO_s_mem());
|
||||
THROW_BAD_ALLOC_IF_NULL(biokey);
|
||||
@@ -94,7 +92,6 @@ void IdentityManager::createCredentials(QSettings& settings)
|
||||
|
||||
X509_free(cert);
|
||||
EVP_PKEY_free(pk);
|
||||
BN_free(bne);
|
||||
BIO_free(biokey);
|
||||
BIO_free(biocert);
|
||||
|
||||
@@ -146,7 +143,10 @@ QSslKey
|
||||
IdentityManager::getSslKey()
|
||||
{
|
||||
if (m_CachedSslKey.isNull()) {
|
||||
BIO* bio = BIO_new_mem_buf(m_CachedPrivateKey.data(), -1);
|
||||
// This seemingly useless const_cast is required for old OpenSSL headers
|
||||
// where BIO_new_mem_buf's parameter is not declared const like those on
|
||||
// the Steam Link hardware.
|
||||
BIO* bio = BIO_new_mem_buf(const_cast<char*>(m_CachedPrivateKey.constData()), -1);
|
||||
THROW_BAD_ALLOC_IF_NULL(bio);
|
||||
|
||||
EVP_PKEY* pk = PEM_read_bio_PrivateKey(bio, nullptr, nullptr, nullptr);
|
||||
@@ -185,12 +185,12 @@ IdentityManager::getSslConfig()
|
||||
QString
|
||||
IdentityManager::getUniqueId()
|
||||
{
|
||||
if (m_CachedUniqueId.isNull()) {
|
||||
if (m_CachedUniqueId.isEmpty()) {
|
||||
QSettings settings;
|
||||
|
||||
// Load the unique ID from settings
|
||||
m_CachedUniqueId = settings.value(SER_UNIQUEID).toString();
|
||||
if (!m_CachedUniqueId.isEmpty() && !m_CachedUniqueId.isNull()) {
|
||||
if (!m_CachedUniqueId.isEmpty()) {
|
||||
qInfo() << "Loaded unique ID from settings:" << m_CachedUniqueId;
|
||||
}
|
||||
else {
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
#include "nvaddress.h"
|
||||
|
||||
#include <QHostAddress>
|
||||
|
||||
NvAddress::NvAddress()
|
||||
{
|
||||
setAddress(nullptr);
|
||||
setPort(0);
|
||||
}
|
||||
|
||||
NvAddress::NvAddress(QString addr, uint16_t port)
|
||||
{
|
||||
setAddress(addr);
|
||||
setPort(port);
|
||||
}
|
||||
|
||||
NvAddress::NvAddress(QHostAddress addr, uint16_t port)
|
||||
{
|
||||
setAddress(addr);
|
||||
setPort(port);
|
||||
}
|
||||
|
||||
uint16_t NvAddress::port() const
|
||||
{
|
||||
return m_Port;
|
||||
}
|
||||
|
||||
QString NvAddress::address() const
|
||||
{
|
||||
return m_Address;
|
||||
}
|
||||
|
||||
void NvAddress::setPort(uint16_t port)
|
||||
{
|
||||
m_Port = port;
|
||||
}
|
||||
|
||||
void NvAddress::setAddress(QString addr)
|
||||
{
|
||||
m_Address = addr;
|
||||
}
|
||||
|
||||
void NvAddress::setAddress(QHostAddress addr)
|
||||
{
|
||||
m_Address = addr.toString();
|
||||
}
|
||||
|
||||
bool NvAddress::isNull() const
|
||||
{
|
||||
return m_Address.isEmpty();
|
||||
}
|
||||
|
||||
QString NvAddress::toString() const
|
||||
{
|
||||
if (m_Address.isEmpty()) {
|
||||
return "<NULL>";
|
||||
}
|
||||
|
||||
if (QHostAddress(m_Address).protocol() == QAbstractSocket::IPv6Protocol) {
|
||||
return QString("[%1]:%2").arg(m_Address).arg(m_Port);
|
||||
}
|
||||
else {
|
||||
return QString("%1:%2").arg(m_Address).arg(m_Port);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
#pragma once
|
||||
|
||||
#include <QHostAddress>
|
||||
|
||||
#define DEFAULT_HTTP_PORT 47989
|
||||
#define DEFAULT_HTTPS_PORT 47984
|
||||
|
||||
class NvAddress
|
||||
{
|
||||
public:
|
||||
NvAddress();
|
||||
explicit NvAddress(QString addr, uint16_t port);
|
||||
explicit NvAddress(QHostAddress addr, uint16_t port);
|
||||
|
||||
uint16_t port() const;
|
||||
void setPort(uint16_t port);
|
||||
|
||||
QString address() const;
|
||||
void setAddress(QString addr);
|
||||
void setAddress(QHostAddress addr);
|
||||
|
||||
bool isNull() const;
|
||||
QString toString() const;
|
||||
|
||||
bool operator==(const NvAddress& other) const
|
||||
{
|
||||
return m_Address == other.m_Address &&
|
||||
m_Port == other.m_Port;
|
||||
}
|
||||
|
||||
bool operator!=(const NvAddress& other) const
|
||||
{
|
||||
return !operator==(other);
|
||||
}
|
||||
|
||||
private:
|
||||
QString m_Address;
|
||||
uint16_t m_Port;
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
#include "nvapp.h"
|
||||
|
||||
#define SER_APPNAME "name"
|
||||
#define SER_APPID "id"
|
||||
#define SER_APPHDR "hdr"
|
||||
#define SER_APPCOLLECTOR "appcollector"
|
||||
#define SER_HIDDEN "hidden"
|
||||
#define SER_DIRECTLAUNCH "directlaunch"
|
||||
|
||||
NvApp::NvApp(QSettings& settings)
|
||||
{
|
||||
name = settings.value(SER_APPNAME).toString();
|
||||
id = settings.value(SER_APPID).toInt();
|
||||
hdrSupported = settings.value(SER_APPHDR).toBool();
|
||||
isAppCollectorGame = settings.value(SER_APPCOLLECTOR).toBool();
|
||||
hidden = settings.value(SER_HIDDEN).toBool();
|
||||
directLaunch = settings.value(SER_DIRECTLAUNCH).toBool();
|
||||
}
|
||||
|
||||
void NvApp::serialize(QSettings& settings) const
|
||||
{
|
||||
settings.setValue(SER_APPNAME, name);
|
||||
settings.setValue(SER_APPID, id);
|
||||
settings.setValue(SER_APPHDR, hdrSupported);
|
||||
settings.setValue(SER_APPCOLLECTOR, isAppCollectorGame);
|
||||
settings.setValue(SER_HIDDEN, hidden);
|
||||
settings.setValue(SER_DIRECTLAUNCH, directLaunch);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
#pragma once
|
||||
|
||||
#include <QSettings>
|
||||
|
||||
class NvApp
|
||||
{
|
||||
public:
|
||||
NvApp() {}
|
||||
explicit NvApp(QSettings& settings);
|
||||
|
||||
bool operator==(const NvApp& other) const
|
||||
{
|
||||
return id == other.id &&
|
||||
name == other.name &&
|
||||
hdrSupported == other.hdrSupported &&
|
||||
isAppCollectorGame == other.isAppCollectorGame &&
|
||||
hidden == other.hidden &&
|
||||
directLaunch == other.directLaunch;
|
||||
}
|
||||
|
||||
bool operator!=(const NvApp& other) const
|
||||
{
|
||||
return !operator==(other);
|
||||
}
|
||||
|
||||
bool isInitialized()
|
||||
{
|
||||
return id != 0 && !name.isEmpty();
|
||||
}
|
||||
|
||||
void
|
||||
serialize(QSettings& settings) const;
|
||||
|
||||
int id = 0;
|
||||
QString name;
|
||||
bool hdrSupported = false;
|
||||
bool isAppCollectorGame = false;
|
||||
bool hidden = false;
|
||||
bool directLaunch = false;
|
||||
};
|
||||
|
||||
Q_DECLARE_METATYPE(NvApp)
|
||||
@@ -0,0 +1,562 @@
|
||||
#include "nvcomputer.h"
|
||||
#include "nvapp.h"
|
||||
#include "settings/compatfetcher.h"
|
||||
|
||||
#include <QUdpSocket>
|
||||
#include <QHostInfo>
|
||||
#include <QNetworkInterface>
|
||||
#include <QNetworkProxy>
|
||||
|
||||
#define SER_NAME "hostname"
|
||||
#define SER_UUID "uuid"
|
||||
#define SER_MAC "mac"
|
||||
#define SER_LOCALADDR "localaddress"
|
||||
#define SER_LOCALPORT "localport"
|
||||
#define SER_REMOTEADDR "remoteaddress"
|
||||
#define SER_REMOTEPORT "remoteport"
|
||||
#define SER_MANUALADDR "manualaddress"
|
||||
#define SER_MANUALPORT "manualport"
|
||||
#define SER_IPV6ADDR "ipv6address"
|
||||
#define SER_IPV6PORT "ipv6port"
|
||||
#define SER_APPLIST "apps"
|
||||
#define SER_SRVCERT "srvcert"
|
||||
#define SER_CUSTOMNAME "customname"
|
||||
#define SER_NVIDIASOFTWARE "nvidiasw"
|
||||
|
||||
NvComputer::NvComputer(QSettings& settings)
|
||||
{
|
||||
this->name = settings.value(SER_NAME).toString();
|
||||
this->uuid = settings.value(SER_UUID).toString();
|
||||
this->hasCustomName = settings.value(SER_CUSTOMNAME).toBool();
|
||||
this->macAddress = settings.value(SER_MAC).toByteArray();
|
||||
this->localAddress = NvAddress(settings.value(SER_LOCALADDR).toString(),
|
||||
settings.value(SER_LOCALPORT, QVariant(DEFAULT_HTTP_PORT)).toUInt());
|
||||
this->remoteAddress = NvAddress(settings.value(SER_REMOTEADDR).toString(),
|
||||
settings.value(SER_REMOTEPORT, QVariant(DEFAULT_HTTP_PORT)).toUInt());
|
||||
this->ipv6Address = NvAddress(settings.value(SER_IPV6ADDR).toString(),
|
||||
settings.value(SER_IPV6PORT, QVariant(DEFAULT_HTTP_PORT)).toUInt());
|
||||
this->manualAddress = NvAddress(settings.value(SER_MANUALADDR).toString(),
|
||||
settings.value(SER_MANUALPORT, QVariant(DEFAULT_HTTP_PORT)).toUInt());
|
||||
this->serverCert = QSslCertificate(settings.value(SER_SRVCERT).toByteArray());
|
||||
this->isNvidiaServerSoftware = settings.value(SER_NVIDIASOFTWARE).toBool();
|
||||
|
||||
int appCount = settings.beginReadArray(SER_APPLIST);
|
||||
this->appList.reserve(appCount);
|
||||
for (int i = 0; i < appCount; i++) {
|
||||
settings.setArrayIndex(i);
|
||||
|
||||
NvApp app(settings);
|
||||
this->appList.append(app);
|
||||
}
|
||||
settings.endArray();
|
||||
sortAppList();
|
||||
|
||||
this->currentGameId = 0;
|
||||
this->pairState = PS_UNKNOWN;
|
||||
this->state = CS_UNKNOWN;
|
||||
this->gfeVersion = nullptr;
|
||||
this->appVersion = nullptr;
|
||||
this->maxLumaPixelsHEVC = 0;
|
||||
this->serverCodecModeSupport = 0;
|
||||
this->pendingQuit = false;
|
||||
this->gpuModel = nullptr;
|
||||
this->isSupportedServerVersion = true;
|
||||
this->externalPort = this->remoteAddress.port();
|
||||
this->activeHttpsPort = 0;
|
||||
}
|
||||
|
||||
void NvComputer::setRemoteAddress(QHostAddress address)
|
||||
{
|
||||
QWriteLocker lock(&this->lock);
|
||||
|
||||
Q_ASSERT(this->externalPort != 0);
|
||||
|
||||
this->remoteAddress = NvAddress(address, this->externalPort);
|
||||
}
|
||||
|
||||
void NvComputer::serialize(QSettings& settings, bool serializeApps) const
|
||||
{
|
||||
QReadLocker lock(&this->lock);
|
||||
|
||||
settings.setValue(SER_NAME, name);
|
||||
settings.setValue(SER_CUSTOMNAME, hasCustomName);
|
||||
settings.setValue(SER_UUID, uuid);
|
||||
settings.setValue(SER_MAC, macAddress);
|
||||
settings.setValue(SER_LOCALADDR, localAddress.address());
|
||||
settings.setValue(SER_LOCALPORT, localAddress.port());
|
||||
settings.setValue(SER_REMOTEADDR, remoteAddress.address());
|
||||
settings.setValue(SER_REMOTEPORT, remoteAddress.port());
|
||||
settings.setValue(SER_IPV6ADDR, ipv6Address.address());
|
||||
settings.setValue(SER_IPV6PORT, ipv6Address.port());
|
||||
settings.setValue(SER_MANUALADDR, manualAddress.address());
|
||||
settings.setValue(SER_MANUALPORT, manualAddress.port());
|
||||
settings.setValue(SER_SRVCERT, serverCert.toPem());
|
||||
settings.setValue(SER_NVIDIASOFTWARE, isNvidiaServerSoftware);
|
||||
|
||||
// Avoid deleting an existing applist if we couldn't get one
|
||||
if (!appList.isEmpty() && serializeApps) {
|
||||
settings.remove(SER_APPLIST);
|
||||
settings.beginWriteArray(SER_APPLIST);
|
||||
for (int i = 0; i < appList.count(); i++) {
|
||||
settings.setArrayIndex(i);
|
||||
appList[i].serialize(settings);
|
||||
}
|
||||
settings.endArray();
|
||||
}
|
||||
}
|
||||
|
||||
bool NvComputer::isEqualSerialized(const NvComputer &that) const
|
||||
{
|
||||
return this->name == that.name &&
|
||||
this->hasCustomName == that.hasCustomName &&
|
||||
this->uuid == that.uuid &&
|
||||
this->macAddress == that.macAddress &&
|
||||
this->localAddress == that.localAddress &&
|
||||
this->remoteAddress == that.remoteAddress &&
|
||||
this->ipv6Address == that.ipv6Address &&
|
||||
this->manualAddress == that.manualAddress &&
|
||||
this->serverCert == that.serverCert &&
|
||||
this->isNvidiaServerSoftware == that.isNvidiaServerSoftware &&
|
||||
this->appList == that.appList;
|
||||
}
|
||||
|
||||
void NvComputer::sortAppList()
|
||||
{
|
||||
std::stable_sort(appList.begin(), appList.end(), [](const NvApp& app1, const NvApp& app2) {
|
||||
return app1.name.toLower() < app2.name.toLower();
|
||||
});
|
||||
}
|
||||
|
||||
NvComputer::NvComputer(NvHTTP& http, QString serverInfo)
|
||||
{
|
||||
this->serverCert = http.serverCert();
|
||||
|
||||
this->hasCustomName = false;
|
||||
this->name = NvHTTP::getXmlString(serverInfo, "hostname");
|
||||
if (this->name.isEmpty()) {
|
||||
this->name = "UNKNOWN";
|
||||
}
|
||||
|
||||
this->uuid = NvHTTP::getXmlString(serverInfo, "uniqueid");
|
||||
QString newMacString = NvHTTP::getXmlString(serverInfo, "mac");
|
||||
if (newMacString != "00:00:00:00:00:00") {
|
||||
QStringList macOctets = newMacString.split(':');
|
||||
for (const QString& macOctet : macOctets) {
|
||||
this->macAddress.append((char) macOctet.toInt(nullptr, 16));
|
||||
}
|
||||
}
|
||||
|
||||
QString codecSupport = NvHTTP::getXmlString(serverInfo, "ServerCodecModeSupport");
|
||||
if (!codecSupport.isEmpty()) {
|
||||
this->serverCodecModeSupport = codecSupport.toInt();
|
||||
}
|
||||
else {
|
||||
// Assume H.264 is always supported
|
||||
this->serverCodecModeSupport = SCM_H264;
|
||||
}
|
||||
|
||||
QString maxLumaPixelsHEVC = NvHTTP::getXmlString(serverInfo, "MaxLumaPixelsHEVC");
|
||||
if (!maxLumaPixelsHEVC.isEmpty()) {
|
||||
this->maxLumaPixelsHEVC = maxLumaPixelsHEVC.toInt();
|
||||
}
|
||||
else {
|
||||
this->maxLumaPixelsHEVC = 0;
|
||||
}
|
||||
|
||||
this->displayModes = NvHTTP::getDisplayModeList(serverInfo);
|
||||
std::stable_sort(this->displayModes.begin(), this->displayModes.end(),
|
||||
[](const NvDisplayMode& mode1, const NvDisplayMode& mode2) {
|
||||
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
|
||||
this->localAddress = NvAddress(NvHTTP::getXmlString(serverInfo, "LocalIP"), http.httpPort());
|
||||
if (this->localAddress.address().startsWith("127.")) {
|
||||
this->localAddress = NvAddress();
|
||||
}
|
||||
|
||||
QString httpsPort = NvHTTP::getXmlString(serverInfo, "HttpsPort");
|
||||
if (httpsPort.isEmpty() || (this->activeHttpsPort = httpsPort.toUShort()) == 0) {
|
||||
this->activeHttpsPort = DEFAULT_HTTPS_PORT;
|
||||
}
|
||||
|
||||
// This is an extension which is not present in GFE. It is present for Sunshine to be able
|
||||
// to support dynamic HTTP WAN ports without requiring the user to manually enter the port.
|
||||
QString remotePortStr = NvHTTP::getXmlString(serverInfo, "ExternalPort");
|
||||
if (remotePortStr.isEmpty() || (this->externalPort = remotePortStr.toUShort()) == 0) {
|
||||
this->externalPort = http.httpPort();
|
||||
}
|
||||
|
||||
QString remoteAddress = NvHTTP::getXmlString(serverInfo, "ExternalIP");
|
||||
if (!remoteAddress.isEmpty()) {
|
||||
this->remoteAddress = NvAddress(remoteAddress, this->externalPort);
|
||||
}
|
||||
else {
|
||||
this->remoteAddress = NvAddress();
|
||||
}
|
||||
|
||||
// Real Nvidia host software (GeForce Experience and RTX Experience) both use the 'Mjolnir'
|
||||
// codename in the state field and no version of Sunshine does. We can use this to bypass
|
||||
// some assumptions about Nvidia hardware that don't apply to Sunshine hosts.
|
||||
this->isNvidiaServerSoftware = NvHTTP::getXmlString(serverInfo, "state").contains("MJOLNIR");
|
||||
|
||||
this->pairState = NvHTTP::getXmlString(serverInfo, "PairStatus") == "1" ?
|
||||
PS_PAIRED : PS_NOT_PAIRED;
|
||||
this->currentGameId = NvHTTP::getCurrentGame(serverInfo);
|
||||
this->appVersion = NvHTTP::getXmlString(serverInfo, "appversion");
|
||||
this->gfeVersion = NvHTTP::getXmlString(serverInfo, "GfeVersion");
|
||||
this->gpuModel = NvHTTP::getXmlString(serverInfo, "gputype");
|
||||
this->activeAddress = http.address();
|
||||
this->state = NvComputer::CS_ONLINE;
|
||||
this->pendingQuit = false;
|
||||
this->isSupportedServerVersion = CompatFetcher::isGfeVersionSupported(this->gfeVersion);
|
||||
}
|
||||
|
||||
bool NvComputer::wake() const
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
// Ports used as-is
|
||||
const quint16 STATIC_WOL_PORTS[] = {
|
||||
9, // Standard WOL port (privileged port)
|
||||
47009, // Port opened by Moonlight Internet Hosting Tool for WoL (non-privileged port)
|
||||
};
|
||||
|
||||
// Ports offset by the HTTP base port for hosts using alternate ports
|
||||
const quint16 DYNAMIC_WOL_PORTS[] = {
|
||||
47998, 47999, 48000, 48002, 48010, // Ports opened by GFE
|
||||
};
|
||||
|
||||
// Add the addresses that we know this host to be
|
||||
// and broadcast addresses for this link just in
|
||||
// case the host has timed out in ARP entries.
|
||||
QMap<QString, quint16> addressMap;
|
||||
QSet<quint16> basePortSet;
|
||||
for (const NvAddress& addr : uniqueAddresses()) {
|
||||
addressMap.insert(addr.address(), addr.port());
|
||||
basePortSet.insert(addr.port());
|
||||
}
|
||||
addressMap.insert("255.255.255.255", 0);
|
||||
|
||||
// Try to broadcast on all available NICs
|
||||
for (const QNetworkInterface& nic : QNetworkInterface::allInterfaces()) {
|
||||
// Ensure the interface is up and skip the loopback adapter
|
||||
if ((nic.flags() & QNetworkInterface::IsUp) == 0 ||
|
||||
(nic.flags() & QNetworkInterface::IsLoopBack) != 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
QHostAddress allNodesMulticast("FF02::1");
|
||||
for (const QNetworkAddressEntry& addr : nic.addressEntries()) {
|
||||
// Store the scope ID for this NIC if IPv6 is enabled
|
||||
if (!addr.ip().scopeId().isEmpty()) {
|
||||
allNodesMulticast.setScopeId(addr.ip().scopeId());
|
||||
}
|
||||
|
||||
// Skip IPv6 which doesn't support broadcast
|
||||
if (!addr.broadcast().isNull()) {
|
||||
addressMap.insert(addr.broadcast().toString(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
if (!allNodesMulticast.scopeId().isEmpty()) {
|
||||
addressMap.insert(allNodesMulticast.toString(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Try all unique address strings or host names
|
||||
bool success = false;
|
||||
for (auto i = addressMap.constBegin(); i != addressMap.constEnd(); i++) {
|
||||
QHostAddress literalAddress;
|
||||
QList<QHostAddress> addressList;
|
||||
|
||||
// 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 : addressList) {
|
||||
QUdpSocket sock;
|
||||
|
||||
// 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;
|
||||
}
|
||||
else {
|
||||
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;
|
||||
}
|
||||
|
||||
NvComputer::ReachabilityType NvComputer::getActiveAddressReachability() const
|
||||
{
|
||||
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(copyOfActiveAddress.address(), copyOfActiveAddress.port());
|
||||
if (s.waitForConnected(3000)) {
|
||||
Q_ASSERT(!s.localAddress().isNull());
|
||||
|
||||
for (const QNetworkInterface& nic : QNetworkInterface::allInterfaces()) {
|
||||
// Ensure the interface is up
|
||||
if ((nic.flags() & QNetworkInterface::IsUp) == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const QNetworkAddressEntry& addr : nic.addressEntries()) {
|
||||
if (addr.ip() == s.localAddress()) {
|
||||
qInfo() << "Found matching interface:" << nic.humanReadableName() << nic.hardwareAddress() << nic.flags();
|
||||
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 11, 0)
|
||||
qInfo() << "Interface Type:" << nic.type();
|
||||
qInfo() << "Interface MTU:" << nic.maximumTransmissionUnit();
|
||||
|
||||
if (nic.type() == QNetworkInterface::Virtual ||
|
||||
nic.type() == QNetworkInterface::Ppp) {
|
||||
// Treat PPP and virtual interfaces as likely VPNs
|
||||
return ReachabilityType::RI_VPN;
|
||||
}
|
||||
|
||||
if (nic.maximumTransmissionUnit() != 0 && nic.maximumTransmissionUnit() < 1500) {
|
||||
// Treat MTUs under 1500 as likely VPNs
|
||||
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 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 ReachabilityType::RI_VPN;
|
||||
}
|
||||
|
||||
if (nic.humanReadableName().startsWith("ZeroTier")) {
|
||||
// ZeroTier interfaces always start with "ZeroTier"
|
||||
return ReachabilityType::RI_VPN;
|
||||
}
|
||||
|
||||
if (nic.humanReadableName().contains("VPN")) {
|
||||
// This one is just a final VPN heuristic if all else fails
|
||||
return ReachabilityType::RI_VPN;
|
||||
}
|
||||
|
||||
// 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 ReachabilityType::RI_UNKNOWN;
|
||||
}
|
||||
else {
|
||||
// If we fail to connect, just pretend that it's not a VPN
|
||||
qWarning() << "Unable to check for reachability within 3 seconds";
|
||||
return ReachabilityType::RI_UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
bool NvComputer::updateAppList(QVector<NvApp> newAppList) {
|
||||
if (appList == newAppList) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Propagate client-side attributes to the new app list
|
||||
for (const NvApp& existingApp : appList) {
|
||||
for (NvApp& newApp : newAppList) {
|
||||
if (existingApp.id == newApp.id) {
|
||||
newApp.hidden = existingApp.hidden;
|
||||
newApp.directLaunch = existingApp.directLaunch;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
appList = newAppList;
|
||||
sortAppList();
|
||||
return true;
|
||||
}
|
||||
|
||||
QVector<NvAddress> NvComputer::uniqueAddresses() const
|
||||
{
|
||||
QReadLocker readLocker(&lock);
|
||||
QVector<NvAddress> uniqueAddressList;
|
||||
|
||||
// Start with addresses correctly ordered
|
||||
uniqueAddressList.append(activeAddress);
|
||||
uniqueAddressList.append(localAddress);
|
||||
uniqueAddressList.append(remoteAddress);
|
||||
uniqueAddressList.append(ipv6Address);
|
||||
uniqueAddressList.append(manualAddress);
|
||||
|
||||
// Prune duplicates (always giving precedence to the first)
|
||||
for (int i = 0; i < uniqueAddressList.count(); i++) {
|
||||
if (uniqueAddressList[i].isNull()) {
|
||||
uniqueAddressList.remove(i);
|
||||
i--;
|
||||
continue;
|
||||
}
|
||||
for (int j = i + 1; j < uniqueAddressList.count(); j++) {
|
||||
if (uniqueAddressList[i] == uniqueAddressList[j]) {
|
||||
// Always remove the later occurrence
|
||||
uniqueAddressList.remove(j);
|
||||
j--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We must have at least 1 address
|
||||
Q_ASSERT(!uniqueAddressList.isEmpty());
|
||||
|
||||
return uniqueAddressList;
|
||||
}
|
||||
|
||||
bool NvComputer::update(const NvComputer& that)
|
||||
{
|
||||
bool changed = false;
|
||||
|
||||
// Lock us for write and them for read
|
||||
QWriteLocker thisLock(&this->lock);
|
||||
QReadLocker thatLock(&that.lock);
|
||||
|
||||
// UUID may not change or we're talking to a new PC
|
||||
Q_ASSERT(this->uuid == that.uuid);
|
||||
|
||||
#define ASSIGN_IF_CHANGED(field) \
|
||||
if (this->field != that.field) { \
|
||||
this->field = that.field; \
|
||||
changed = true; \
|
||||
}
|
||||
|
||||
#define ASSIGN_IF_CHANGED_AND_NONEMPTY(field) \
|
||||
if (!that.field.isEmpty() && \
|
||||
this->field != that.field) { \
|
||||
this->field = that.field; \
|
||||
changed = true; \
|
||||
}
|
||||
|
||||
#define ASSIGN_IF_CHANGED_AND_NONNULL(field) \
|
||||
if (!that.field.isNull() && \
|
||||
this->field != that.field) { \
|
||||
this->field = that.field; \
|
||||
changed = true; \
|
||||
}
|
||||
|
||||
if (!hasCustomName) {
|
||||
// Only overwrite the name if it's not custom
|
||||
ASSIGN_IF_CHANGED(name);
|
||||
}
|
||||
ASSIGN_IF_CHANGED_AND_NONEMPTY(macAddress);
|
||||
ASSIGN_IF_CHANGED_AND_NONNULL(localAddress);
|
||||
ASSIGN_IF_CHANGED_AND_NONNULL(remoteAddress);
|
||||
ASSIGN_IF_CHANGED_AND_NONNULL(ipv6Address);
|
||||
ASSIGN_IF_CHANGED_AND_NONNULL(manualAddress);
|
||||
ASSIGN_IF_CHANGED(activeHttpsPort);
|
||||
ASSIGN_IF_CHANGED(externalPort);
|
||||
ASSIGN_IF_CHANGED(pairState);
|
||||
ASSIGN_IF_CHANGED(serverCodecModeSupport);
|
||||
ASSIGN_IF_CHANGED(currentGameId);
|
||||
ASSIGN_IF_CHANGED(activeAddress);
|
||||
ASSIGN_IF_CHANGED(state);
|
||||
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);
|
||||
ASSIGN_IF_CHANGED_AND_NONEMPTY(displayModes);
|
||||
|
||||
if (!that.appList.isEmpty()) {
|
||||
// updateAppList() handles merging client-side attributes
|
||||
updateAppList(that.appList);
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
#pragma once
|
||||
|
||||
#include "nvhttp.h"
|
||||
#include "nvaddress.h"
|
||||
|
||||
#include <QThread>
|
||||
#include <QReadWriteLock>
|
||||
#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;
|
||||
friend class ComputerManager;
|
||||
friend class PendingQuitTask;
|
||||
|
||||
private:
|
||||
void sortAppList();
|
||||
|
||||
bool updateAppList(QVector<NvApp> newAppList);
|
||||
|
||||
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);
|
||||
|
||||
void
|
||||
setRemoteAddress(QHostAddress);
|
||||
|
||||
bool
|
||||
update(const NvComputer& that);
|
||||
|
||||
bool
|
||||
wake() const;
|
||||
|
||||
enum ReachabilityType
|
||||
{
|
||||
RI_UNKNOWN,
|
||||
RI_LAN,
|
||||
RI_VPN,
|
||||
};
|
||||
|
||||
ReachabilityType
|
||||
getActiveAddressReachability() const;
|
||||
|
||||
QVector<NvAddress>
|
||||
uniqueAddresses() const;
|
||||
|
||||
void
|
||||
serialize(QSettings& settings, bool serializeApps) const;
|
||||
|
||||
// Caller is responsible for synchronizing read access to both hosts
|
||||
bool
|
||||
isEqualSerialized(const NvComputer& that) const;
|
||||
|
||||
enum PairState
|
||||
{
|
||||
PS_UNKNOWN,
|
||||
PS_PAIRED,
|
||||
PS_NOT_PAIRED
|
||||
};
|
||||
|
||||
enum ComputerState
|
||||
{
|
||||
CS_UNKNOWN,
|
||||
CS_ONLINE,
|
||||
CS_OFFLINE
|
||||
};
|
||||
|
||||
// Ephemeral traits
|
||||
ComputerState state;
|
||||
PairState pairState;
|
||||
NvAddress activeAddress;
|
||||
uint16_t activeHttpsPort;
|
||||
int currentGameId;
|
||||
QString gfeVersion;
|
||||
QString appVersion;
|
||||
QVector<NvDisplayMode> displayModes;
|
||||
int maxLumaPixelsHEVC;
|
||||
int serverCodecModeSupport;
|
||||
QString gpuModel;
|
||||
bool isSupportedServerVersion;
|
||||
|
||||
// Persisted traits
|
||||
NvAddress localAddress;
|
||||
NvAddress remoteAddress;
|
||||
NvAddress ipv6Address;
|
||||
NvAddress manualAddress;
|
||||
QByteArray macAddress;
|
||||
QString name;
|
||||
bool hasCustomName;
|
||||
QString uuid;
|
||||
QSslCertificate serverCert;
|
||||
QVector<NvApp> appList;
|
||||
bool isNvidiaServerSoftware;
|
||||
// Remember to update isEqualSerialized() when adding fields here!
|
||||
|
||||
// Synchronization
|
||||
mutable CopySafeReadWriteLock lock;
|
||||
|
||||
private:
|
||||
uint16_t externalPort;
|
||||
};
|
||||
+263
-125
@@ -1,4 +1,4 @@
|
||||
#include "nvhttp.h"
|
||||
#include "nvcomputer.h"
|
||||
#include <Limelight.h>
|
||||
|
||||
#include <QDebug>
|
||||
@@ -10,27 +10,92 @@
|
||||
#include <QSslKey>
|
||||
#include <QImageReader>
|
||||
#include <QtEndian>
|
||||
#include <QNetworkProxy>
|
||||
|
||||
#define FAST_FAIL_TIMEOUT_MS 2000
|
||||
#define REQUEST_TIMEOUT_MS 5000
|
||||
#define LAUNCH_TIMEOUT_MS 120000
|
||||
#define RESUME_TIMEOUT_MS 30000
|
||||
#define QUIT_TIMEOUT_MS 30000
|
||||
|
||||
NvHTTP::NvHTTP(QString address) :
|
||||
m_Address(address)
|
||||
NvHTTP::NvHTTP(NvAddress address, uint16_t httpsPort, QSslCertificate serverCert) :
|
||||
m_ServerCert(serverCert)
|
||||
{
|
||||
m_BaseUrlHttp.setScheme("http");
|
||||
m_BaseUrlHttps.setScheme("https");
|
||||
m_BaseUrlHttp.setHost(address);
|
||||
m_BaseUrlHttps.setHost(address);
|
||||
m_BaseUrlHttp.setPort(47989);
|
||||
m_BaseUrlHttps.setPort(47984);
|
||||
|
||||
setAddress(address);
|
||||
setHttpsPort(httpsPort);
|
||||
|
||||
// Never use a proxy server
|
||||
QNetworkProxy noProxy(QNetworkProxy::NoProxy);
|
||||
m_Nam.setProxy(noProxy);
|
||||
|
||||
connect(&m_Nam, &QNetworkAccessManager::sslErrors, this, &NvHTTP::handleSslErrors);
|
||||
}
|
||||
|
||||
NvHTTP::NvHTTP(NvComputer* computer) :
|
||||
NvHTTP(computer->activeAddress, computer->activeHttpsPort, computer->serverCert)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
void NvHTTP::setServerCert(QSslCertificate serverCert)
|
||||
{
|
||||
m_ServerCert = serverCert;
|
||||
}
|
||||
|
||||
void NvHTTP::setAddress(NvAddress address)
|
||||
{
|
||||
Q_ASSERT(!address.isNull());
|
||||
|
||||
m_Address = address;
|
||||
|
||||
m_BaseUrlHttp.setHost(address.address());
|
||||
m_BaseUrlHttps.setHost(address.address());
|
||||
|
||||
m_BaseUrlHttp.setPort(address.port());
|
||||
}
|
||||
|
||||
void NvHTTP::setHttpsPort(uint16_t port)
|
||||
{
|
||||
m_BaseUrlHttps.setPort(port);
|
||||
}
|
||||
|
||||
NvAddress NvHTTP::address()
|
||||
{
|
||||
return m_Address;
|
||||
}
|
||||
|
||||
QSslCertificate NvHTTP::serverCert()
|
||||
{
|
||||
return m_ServerCert;
|
||||
}
|
||||
|
||||
uint16_t NvHTTP::httpPort()
|
||||
{
|
||||
return m_BaseUrlHttp.port();
|
||||
}
|
||||
|
||||
uint16_t NvHTTP::httpsPort()
|
||||
{
|
||||
return m_BaseUrlHttps.port();
|
||||
}
|
||||
|
||||
QVector<int>
|
||||
NvHTTP::parseQuad(QString quad)
|
||||
{
|
||||
QStringList parts = quad.split(".");
|
||||
QVector<int> ret;
|
||||
|
||||
for (int i = 0; i < 4; i++)
|
||||
// Return an empty vector for old GFE versions
|
||||
// that were missing GfeVersion.
|
||||
if (quad.isEmpty()) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
QStringList parts = quad.split(".");
|
||||
ret.reserve(parts.length());
|
||||
for (int i = 0; i < parts.length(); i++)
|
||||
{
|
||||
ret.append(parts.at(i).toInt());
|
||||
}
|
||||
@@ -56,74 +121,81 @@ NvHTTP::getCurrentGame(QString serverInfo)
|
||||
}
|
||||
|
||||
QString
|
||||
NvHTTP::getServerInfo()
|
||||
NvHTTP::getServerInfo(NvLogLevel logLevel, bool fastFail)
|
||||
{
|
||||
QString serverInfo;
|
||||
|
||||
try
|
||||
// Check if we have a pinned cert and HTTPS port for this host yet
|
||||
if (!m_ServerCert.isNull() && httpsPort() != 0)
|
||||
{
|
||||
// Always try HTTPS first, since it properly reports
|
||||
// pairing status (and a few other attributes).
|
||||
serverInfo = openConnectionToString(m_BaseUrlHttps,
|
||||
"serverinfo",
|
||||
nullptr,
|
||||
true,
|
||||
true);
|
||||
// Throws if the request failed
|
||||
verifyResponseStatus(serverInfo);
|
||||
}
|
||||
catch (const GfeHttpResponseException& e)
|
||||
{
|
||||
if (e.getStatusCode() == 401)
|
||||
try
|
||||
{
|
||||
// Certificate validation error, fallback to HTTP
|
||||
serverInfo = openConnectionToString(m_BaseUrlHttp,
|
||||
// Always try HTTPS first, since it properly reports
|
||||
// pairing status (and a few other attributes).
|
||||
serverInfo = openConnectionToString(m_BaseUrlHttps,
|
||||
"serverinfo",
|
||||
nullptr,
|
||||
true,
|
||||
true);
|
||||
fastFail ? FAST_FAIL_TIMEOUT_MS : REQUEST_TIMEOUT_MS,
|
||||
logLevel);
|
||||
// Throws if the request failed
|
||||
verifyResponseStatus(serverInfo);
|
||||
}
|
||||
else
|
||||
catch (const GfeHttpResponseException& e)
|
||||
{
|
||||
// Rethrow real errors
|
||||
throw e;
|
||||
if (e.getStatusCode() == 401)
|
||||
{
|
||||
// Certificate validation error, fallback to HTTP
|
||||
serverInfo = openConnectionToString(m_BaseUrlHttp,
|
||||
"serverinfo",
|
||||
nullptr,
|
||||
fastFail ? FAST_FAIL_TIMEOUT_MS : REQUEST_TIMEOUT_MS,
|
||||
logLevel);
|
||||
verifyResponseStatus(serverInfo);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Rethrow real errors
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Only use HTTP prior to pairing or fetching HTTPS port
|
||||
serverInfo = openConnectionToString(m_BaseUrlHttp,
|
||||
"serverinfo",
|
||||
nullptr,
|
||||
fastFail ? FAST_FAIL_TIMEOUT_MS : REQUEST_TIMEOUT_MS,
|
||||
logLevel);
|
||||
verifyResponseStatus(serverInfo);
|
||||
|
||||
// Populate the HTTPS port
|
||||
uint16_t httpsPort = getXmlString(serverInfo, "HttpsPort").toUShort();
|
||||
if (httpsPort == 0) {
|
||||
httpsPort = DEFAULT_HTTPS_PORT;
|
||||
}
|
||||
setHttpsPort(httpsPort);
|
||||
|
||||
// If we just needed to determine the HTTPS port, we'll try again over
|
||||
// HTTPS now that we have the port number
|
||||
if (!m_ServerCert.isNull()) {
|
||||
return getServerInfo(logLevel, fastFail);
|
||||
}
|
||||
}
|
||||
|
||||
return serverInfo;
|
||||
}
|
||||
|
||||
static QString
|
||||
getSurroundAudioInfoString(int audioConfig)
|
||||
{
|
||||
int channelMask;
|
||||
int channelCount;
|
||||
|
||||
switch (audioConfig)
|
||||
{
|
||||
case AUDIO_CONFIGURATION_STEREO:
|
||||
channelCount = 2;
|
||||
channelMask = 0x3;
|
||||
break;
|
||||
case AUDIO_CONFIGURATION_51_SURROUND:
|
||||
channelCount = 6;
|
||||
channelMask = 0xFC;
|
||||
break;
|
||||
default:
|
||||
Q_ASSERT(false);
|
||||
return 0;
|
||||
}
|
||||
|
||||
return QString::number(channelMask << 16 | channelCount);
|
||||
}
|
||||
|
||||
void
|
||||
NvHTTP::launchApp(int appId,
|
||||
PSTREAM_CONFIGURATION streamConfig,
|
||||
bool sops,
|
||||
bool localAudio,
|
||||
int gamepadMask)
|
||||
NvHTTP::startApp(QString verb,
|
||||
bool isGfe,
|
||||
int appId,
|
||||
PSTREAM_CONFIGURATION streamConfig,
|
||||
bool sops,
|
||||
bool localAudio,
|
||||
int gamepadMask,
|
||||
bool persistGameControllersOnDisconnect,
|
||||
QString& rtspSessionUrl)
|
||||
{
|
||||
int riKeyId;
|
||||
|
||||
@@ -132,45 +204,34 @@ 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"+
|
||||
QString::number(streamConfig->fps)+
|
||||
// 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. 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="+getSurroundAudioInfoString(streamConfig->audioConfiguration)+
|
||||
"&surroundAudioInfo="+QString::number(SURROUNDAUDIOINFO_FROM_AUDIO_CONFIGURATION(streamConfig->audioConfiguration))+
|
||||
"&remoteControllersBitmap="+QString::number(gamepadMask)+
|
||||
"&gcmap="+QString::number(gamepadMask),
|
||||
false);
|
||||
"&gcmap="+QString::number(gamepadMask)+
|
||||
"&gcpersist="+QString::number(persistGameControllersOnDisconnect ? 1 : 0),
|
||||
LAUNCH_TIMEOUT_MS);
|
||||
|
||||
qInfo() << "Launch response:" << response;
|
||||
|
||||
// Throws if the request failed
|
||||
verifyResponseStatus(response);
|
||||
}
|
||||
|
||||
void
|
||||
NvHTTP::resumeApp(PSTREAM_CONFIGURATION streamConfig)
|
||||
{
|
||||
int riKeyId;
|
||||
|
||||
memcpy(&riKeyId, streamConfig->remoteInputAesIv, sizeof(riKeyId));
|
||||
riKeyId = qFromBigEndian(riKeyId);
|
||||
|
||||
QString response =
|
||||
openConnectionToString(m_BaseUrlHttps,
|
||||
"resume",
|
||||
"rikey="+QString(QByteArray(streamConfig->remoteInputAesKey, sizeof(streamConfig->remoteInputAesKey)).toHex())+
|
||||
"&rikeyid="+QString::number(riKeyId)+
|
||||
"&surroundAudioInfo="+getSurroundAudioInfoString(streamConfig->audioConfiguration),
|
||||
false);
|
||||
|
||||
// Throws if the request failed
|
||||
verifyResponseStatus(response);
|
||||
rtspSessionUrl = getXmlString(response, "sessionUrl0");
|
||||
}
|
||||
|
||||
void
|
||||
@@ -180,14 +241,16 @@ NvHTTP::quitApp()
|
||||
openConnectionToString(m_BaseUrlHttps,
|
||||
"cancel",
|
||||
nullptr,
|
||||
false);
|
||||
QUIT_TIMEOUT_MS);
|
||||
|
||||
qInfo() << "Quit response:" << response;
|
||||
|
||||
// Throws if the request failed
|
||||
verifyResponseStatus(response);
|
||||
|
||||
// Newer GFE versions will just return success even if quitting fails
|
||||
// if we're not the original requestor.
|
||||
if (getCurrentGame(getServerInfo()) != 0) {
|
||||
// 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.
|
||||
throw GfeHttpResponseException(599, "");
|
||||
@@ -202,17 +265,17 @@ NvHTTP::getDisplayModeList(QString serverInfo)
|
||||
|
||||
while (!xmlReader.atEnd()) {
|
||||
while (xmlReader.readNextStartElement()) {
|
||||
QStringRef name = xmlReader.name();
|
||||
if (xmlReader.name() == "DisplayMode") {
|
||||
auto name = xmlReader.name();
|
||||
if (name == QString("DisplayMode")) {
|
||||
modes.append(NvDisplayMode());
|
||||
}
|
||||
else if (xmlReader.name() == "Width") {
|
||||
else if (name == QString("Width")) {
|
||||
modes.last().width = xmlReader.readElementText().toInt();
|
||||
}
|
||||
else if (xmlReader.name() == "Height") {
|
||||
else if (name == QString("Height")) {
|
||||
modes.last().height = xmlReader.readElementText().toInt();
|
||||
}
|
||||
else if (xmlReader.name() == "RefreshRate") {
|
||||
else if (name == QString("RefreshRate")) {
|
||||
modes.last().refreshRate = xmlReader.readElementText().toInt();
|
||||
}
|
||||
}
|
||||
@@ -227,16 +290,16 @@ NvHTTP::getAppList()
|
||||
QString appxml = openConnectionToString(m_BaseUrlHttps,
|
||||
"applist",
|
||||
nullptr,
|
||||
true,
|
||||
true);
|
||||
REQUEST_TIMEOUT_MS,
|
||||
NvLogLevel::NVLL_ERROR);
|
||||
verifyResponseStatus(appxml);
|
||||
|
||||
QXmlStreamReader xmlReader(appxml);
|
||||
QVector<NvApp> apps;
|
||||
while (!xmlReader.atEnd()) {
|
||||
while (xmlReader.readNextStartElement()) {
|
||||
QStringRef name = xmlReader.name();
|
||||
if (xmlReader.name() == "App") {
|
||||
auto name = xmlReader.name();
|
||||
if (name == QString("App")) {
|
||||
// We must have a valid app before advancing to the next one
|
||||
if (!apps.isEmpty() && !apps.last().isInitialized()) {
|
||||
qWarning() << "Invalid applist XML";
|
||||
@@ -245,15 +308,18 @@ NvHTTP::getAppList()
|
||||
}
|
||||
apps.append(NvApp());
|
||||
}
|
||||
else if (xmlReader.name() == "AppTitle") {
|
||||
else if (name == QString("AppTitle")) {
|
||||
apps.last().name = xmlReader.readElementText();
|
||||
}
|
||||
else if (xmlReader.name() == "ID") {
|
||||
else if (name == QString("ID")) {
|
||||
apps.last().id = xmlReader.readElementText().toInt();
|
||||
}
|
||||
else if (xmlReader.name() == "IsHdrSupported") {
|
||||
else if (name == QString("IsHdrSupported")) {
|
||||
apps.last().hdrSupported = xmlReader.readElementText() == "1";
|
||||
}
|
||||
else if (name == QString("IsAppCollectorGame")) {
|
||||
apps.last().isAppCollectorGame = xmlReader.readElementText() == "1";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -267,9 +333,12 @@ NvHTTP::verifyResponseStatus(QString xml)
|
||||
|
||||
while (xmlReader.readNextStartElement())
|
||||
{
|
||||
if (xmlReader.name() == "root")
|
||||
if (xmlReader.name() == QString("root"))
|
||||
{
|
||||
int statusCode = xmlReader.attributes().value("status_code").toInt();
|
||||
// Status code can be 0xFFFFFFFF in some rare cases on GFE 3.20.3, and
|
||||
// QString::toInt() will fail in that case, so use QString::toUInt()
|
||||
// and cast the result to an int instead.
|
||||
int statusCode = (int)xmlReader.attributes().value("status_code").toUInt();
|
||||
if (statusCode == 200)
|
||||
{
|
||||
// Successful
|
||||
@@ -282,10 +351,18 @@ NvHTTP::verifyResponseStatus(QString xml)
|
||||
// 401 is expected for unpaired PCs when we fetch serverinfo over HTTPS
|
||||
qWarning() << "Request failed:" << statusCode << statusMessage;
|
||||
}
|
||||
if (statusCode == -1 && statusMessage == "Invalid") {
|
||||
// Special case handling an audio capture error which GFE doesn't
|
||||
// provide any useful status message for.
|
||||
statusCode = 418;
|
||||
statusMessage = tr("Missing audio capture device. Reinstalling GeForce Experience should resolve this error.");
|
||||
}
|
||||
throw GfeHttpResponseException(statusCode, statusMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw GfeHttpResponseException(-1, "Malformed XML (missing root element)");
|
||||
}
|
||||
|
||||
QImage
|
||||
@@ -295,7 +372,8 @@ NvHTTP::getBoxArt(int appId)
|
||||
"appasset",
|
||||
"appid="+QString::number(appId)+
|
||||
"&AssetType=2&AssetIdx=0",
|
||||
true);
|
||||
REQUEST_TIMEOUT_MS,
|
||||
NvLogLevel::NVLL_VERBOSE);
|
||||
QImage image = QImageReader(reply).read();
|
||||
delete reply;
|
||||
|
||||
@@ -337,18 +415,46 @@ NvHTTP::getXmlString(QString xml,
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void NvHTTP::handleSslErrors(QNetworkReply* reply, const QList<QSslError>& errors)
|
||||
{
|
||||
bool ignoreErrors = true;
|
||||
|
||||
if (m_ServerCert.isNull()) {
|
||||
// We should never make an HTTPS request without a cert
|
||||
Q_ASSERT(!m_ServerCert.isNull());
|
||||
return;
|
||||
}
|
||||
|
||||
for (const QSslError& error : errors) {
|
||||
if (m_ServerCert != error.certificate()) {
|
||||
ignoreErrors = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (ignoreErrors) {
|
||||
reply->ignoreSslErrors(errors);
|
||||
}
|
||||
}
|
||||
|
||||
QString
|
||||
NvHTTP::openConnectionToString(QUrl baseUrl,
|
||||
QString command,
|
||||
QString arguments,
|
||||
bool enableTimeout,
|
||||
bool suppressLogging)
|
||||
int timeoutMs,
|
||||
NvLogLevel logLevel)
|
||||
{
|
||||
QNetworkReply* reply = openConnection(baseUrl, command, arguments, enableTimeout, suppressLogging);
|
||||
QNetworkReply* reply = openConnection(baseUrl, command, arguments, timeoutMs, logLevel);
|
||||
QString ret;
|
||||
|
||||
QTextStream stream(reply);
|
||||
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
stream.setEncoding(QStringConverter::Utf8);
|
||||
#else
|
||||
stream.setCodec("UTF-8");
|
||||
#endif
|
||||
|
||||
ret = stream.readAll();
|
||||
delete reply;
|
||||
|
||||
@@ -359,34 +465,51 @@ QNetworkReply*
|
||||
NvHTTP::openConnection(QUrl baseUrl,
|
||||
QString command,
|
||||
QString arguments,
|
||||
bool enableTimeout,
|
||||
bool suppressLogging)
|
||||
int timeoutMs,
|
||||
NvLogLevel logLevel)
|
||||
{
|
||||
// Port must be set
|
||||
Q_ASSERT(baseUrl.port(0) != 0);
|
||||
|
||||
// Build a URL for the request
|
||||
QUrl url(baseUrl);
|
||||
url.setPath("/" + command);
|
||||
url.setQuery("uniqueid=" + IdentityManager::get()->getUniqueId() +
|
||||
"&uuid=" + QUuid::createUuid().toRfc4122().toHex() +
|
||||
|
||||
// Use a common UID for Moonlight clients to allow them to quit
|
||||
// games for each other (otherwise GFE gets screwed up and it requires
|
||||
// manual intervention to solve).
|
||||
url.setQuery("uniqueid=0123456789ABCDEF&uuid=" +
|
||||
QUuid::createUuid().toRfc4122().toHex() +
|
||||
((arguments != nullptr) ? ("&" + arguments) : ""));
|
||||
|
||||
QNetworkRequest request = QNetworkRequest(url);
|
||||
QNetworkRequest request(url);
|
||||
|
||||
// Add our client certificate
|
||||
request.setSslConfiguration(IdentityManager::get()->getSslConfig());
|
||||
|
||||
QNetworkReply* reply = m_Nam.get(request);
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
// Disable HTTP/2 (GFE 3.22 doesn't like it) and Qt 6 enables it by default
|
||||
request.setAttribute(QNetworkRequest::Http2AllowedAttribute, false);
|
||||
#endif
|
||||
|
||||
// Ignore self-signed certificate errors (since GFE uses them)
|
||||
reply->ignoreSslErrors();
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) && QT_VERSION < QT_VERSION_CHECK(5, 15, 1) && !defined(QT_NO_BEARERMANAGEMENT)
|
||||
// HACK: Set network accessibility to work around QTBUG-80947 (introduced in Qt 5.14.0 and fixed in Qt 5.15.1)
|
||||
QT_WARNING_PUSH
|
||||
QT_WARNING_DISABLE_DEPRECATED
|
||||
m_Nam.setNetworkAccessible(QNetworkAccessManager::Accessible);
|
||||
QT_WARNING_POP
|
||||
#endif
|
||||
|
||||
QNetworkReply* reply = m_Nam.get(request);
|
||||
|
||||
// Run the request with a timeout if requested
|
||||
QEventLoop loop;
|
||||
QObject::connect(reply, SIGNAL(finished()), &loop, SLOT(quit()));
|
||||
if (enableTimeout)
|
||||
{
|
||||
QTimer::singleShot(REQUEST_TIMEOUT_MS, &loop, SLOT(quit()));
|
||||
connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
|
||||
connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, &loop, &QEventLoop::quit);
|
||||
if (timeoutMs) {
|
||||
QTimer::singleShot(timeoutMs, &loop, &QEventLoop::quit);
|
||||
}
|
||||
if (!suppressLogging) {
|
||||
if (logLevel >= NvLogLevel::NVLL_VERBOSE) {
|
||||
qInfo() << "Executing request:" << url.toString();
|
||||
}
|
||||
loop.exec(QEventLoop::ExcludeUserInputEvents);
|
||||
@@ -394,7 +517,7 @@ NvHTTP::openConnection(QUrl baseUrl,
|
||||
// Abort the request if it timed out
|
||||
if (!reply->isFinished())
|
||||
{
|
||||
if (!suppressLogging) {
|
||||
if (logLevel >= NvLogLevel::NVLL_ERROR) {
|
||||
qWarning() << "Aborting timed out request for" << url.toString();
|
||||
}
|
||||
reply->abort();
|
||||
@@ -407,12 +530,27 @@ NvHTTP::openConnection(QUrl baseUrl,
|
||||
// Handle error
|
||||
if (reply->error() != QNetworkReply::NoError)
|
||||
{
|
||||
if (!suppressLogging) {
|
||||
qWarning() << command << " request failed with error " << reply->error();
|
||||
if (logLevel >= NvLogLevel::NVLL_ERROR) {
|
||||
qWarning() << command << "request failed with error:" << reply->error();
|
||||
}
|
||||
|
||||
if (reply->error() == QNetworkReply::SslHandshakeFailedError) {
|
||||
// This will trigger falling back to HTTP for the serverinfo query
|
||||
// then pairing again to get the updated certificate.
|
||||
GfeHttpResponseException exception(401, "Server certificate mismatch");
|
||||
delete reply;
|
||||
throw exception;
|
||||
}
|
||||
else if (reply->error() == QNetworkReply::OperationCanceledError) {
|
||||
QtNetworkReplyException exception(QNetworkReply::TimeoutError, "Request timed out");
|
||||
delete reply;
|
||||
throw exception;
|
||||
}
|
||||
else {
|
||||
QtNetworkReplyException exception(reply->error(), reply->errorString());
|
||||
delete reply;
|
||||
throw exception;
|
||||
}
|
||||
GfeHttpResponseException exception(reply->error(), reply->errorString());
|
||||
delete reply;
|
||||
throw exception;
|
||||
}
|
||||
|
||||
return reply;
|
||||
|
||||
+85
-36
@@ -1,31 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
#include "identitymanager.h"
|
||||
#include "nvapp.h"
|
||||
#include "nvaddress.h"
|
||||
|
||||
#include <Limelight.h>
|
||||
|
||||
#include <QUrl>
|
||||
#include <QtNetwork/QNetworkAccessManager>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkReply>
|
||||
|
||||
class NvApp
|
||||
{
|
||||
public:
|
||||
bool operator==(const NvApp& other) const
|
||||
{
|
||||
return id == other.id;
|
||||
}
|
||||
|
||||
bool isInitialized()
|
||||
{
|
||||
return id != 0 && !name.isNull();
|
||||
}
|
||||
|
||||
int id;
|
||||
QString name;
|
||||
bool hdrSupported;
|
||||
};
|
||||
|
||||
Q_DECLARE_METATYPE(NvApp)
|
||||
class NvComputer;
|
||||
|
||||
class NvDisplayMode
|
||||
{
|
||||
@@ -41,6 +26,7 @@ public:
|
||||
int height;
|
||||
int refreshRate;
|
||||
};
|
||||
Q_DECLARE_TYPEINFO(NvDisplayMode, Q_PRIMITIVE_TYPE);
|
||||
|
||||
class GfeHttpResponseException : public std::exception
|
||||
{
|
||||
@@ -77,17 +63,62 @@ private:
|
||||
QString m_StatusMessage;
|
||||
};
|
||||
|
||||
class NvHTTP
|
||||
class QtNetworkReplyException : public std::exception
|
||||
{
|
||||
public:
|
||||
explicit NvHTTP(QString address);
|
||||
QtNetworkReplyException(QNetworkReply::NetworkError error, QString errorText) :
|
||||
m_Error(error),
|
||||
m_ErrorText(errorText)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
const char* what() const throw()
|
||||
{
|
||||
return m_ErrorText.toLatin1();
|
||||
}
|
||||
|
||||
const char* getErrorText() const
|
||||
{
|
||||
return m_ErrorText.toLatin1();
|
||||
}
|
||||
|
||||
QNetworkReply::NetworkError getError() const
|
||||
{
|
||||
return m_Error;
|
||||
}
|
||||
|
||||
QString toQString() const
|
||||
{
|
||||
return m_ErrorText + " (Error " + QString::number(m_Error) + ")";
|
||||
}
|
||||
|
||||
private:
|
||||
QNetworkReply::NetworkError m_Error;
|
||||
QString m_ErrorText;
|
||||
};
|
||||
|
||||
class NvHTTP : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
enum NvLogLevel {
|
||||
NVLL_NONE,
|
||||
NVLL_ERROR,
|
||||
NVLL_VERBOSE
|
||||
};
|
||||
|
||||
explicit NvHTTP(NvAddress address, uint16_t httpsPort, QSslCertificate serverCert);
|
||||
|
||||
explicit NvHTTP(NvComputer* computer);
|
||||
|
||||
static
|
||||
int
|
||||
getCurrentGame(QString serverInfo);
|
||||
|
||||
QString
|
||||
getServerInfo();
|
||||
getServerInfo(NvLogLevel logLevel, bool fastFail = false);
|
||||
|
||||
static
|
||||
void
|
||||
@@ -107,8 +138,21 @@ public:
|
||||
openConnectionToString(QUrl baseUrl,
|
||||
QString command,
|
||||
QString arguments,
|
||||
bool enableTimeout,
|
||||
bool suppressLogging = false);
|
||||
int timeoutMs,
|
||||
NvLogLevel logLevel = NvLogLevel::NVLL_VERBOSE);
|
||||
|
||||
void setServerCert(QSslCertificate serverCert);
|
||||
|
||||
void setAddress(NvAddress address);
|
||||
void setHttpsPort(uint16_t port);
|
||||
|
||||
NvAddress address();
|
||||
|
||||
QSslCertificate serverCert();
|
||||
|
||||
uint16_t httpPort();
|
||||
|
||||
uint16_t httpsPort();
|
||||
|
||||
static
|
||||
QVector<int>
|
||||
@@ -118,14 +162,15 @@ public:
|
||||
quitApp();
|
||||
|
||||
void
|
||||
resumeApp(PSTREAM_CONFIGURATION streamConfig);
|
||||
|
||||
void
|
||||
launchApp(int appId,
|
||||
PSTREAM_CONFIGURATION streamConfig,
|
||||
bool sops,
|
||||
bool localAudio,
|
||||
int gamepadMask);
|
||||
startApp(QString verb,
|
||||
bool isGfe,
|
||||
int appId,
|
||||
PSTREAM_CONFIGURATION streamConfig,
|
||||
bool sops,
|
||||
bool localAudio,
|
||||
int gamepadMask,
|
||||
bool persistGameControllersOnDisconnect,
|
||||
QString& rtspSessionUrl);
|
||||
|
||||
QVector<NvApp>
|
||||
getAppList();
|
||||
@@ -140,13 +185,17 @@ public:
|
||||
QUrl m_BaseUrlHttp;
|
||||
QUrl m_BaseUrlHttps;
|
||||
private:
|
||||
void
|
||||
handleSslErrors(QNetworkReply* reply, const QList<QSslError>& errors);
|
||||
|
||||
QNetworkReply*
|
||||
openConnection(QUrl baseUrl,
|
||||
QString command,
|
||||
QString arguments,
|
||||
bool enableTimeout,
|
||||
bool suppressLogging = false);
|
||||
int timeoutMs,
|
||||
NvLogLevel logLevel);
|
||||
|
||||
QString m_Address;
|
||||
NvAddress m_Address;
|
||||
QNetworkAccessManager m_Nam;
|
||||
QSslCertificate m_ServerCert;
|
||||
};
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
#include "nvpairingmanager.h"
|
||||
#include "utils.h"
|
||||
|
||||
#include <stdexcept>
|
||||
|
||||
#include <openssl/bio.h>
|
||||
#include <openssl/aes.h>
|
||||
#include <openssl/rand.h>
|
||||
#include <openssl/pem.h>
|
||||
#include <openssl/x509.h>
|
||||
#include <openssl/evp.h>
|
||||
|
||||
NvPairingManager::NvPairingManager(QString address) :
|
||||
m_Http(address)
|
||||
#define REQUEST_TIMEOUT_MS 5000
|
||||
|
||||
NvPairingManager::NvPairingManager(NvComputer* computer) :
|
||||
m_Http(computer)
|
||||
{
|
||||
QByteArray cert = IdentityManager::get()->getCertificate();
|
||||
BIO *bio = BIO_new_mem_buf(cert.data(), -1);
|
||||
@@ -49,39 +52,63 @@ NvPairingManager::generateRandomBytes(int length)
|
||||
}
|
||||
|
||||
QByteArray
|
||||
NvPairingManager::encrypt(QByteArray plaintext, AES_KEY* key)
|
||||
NvPairingManager::encrypt(const QByteArray& plaintext, const QByteArray& key)
|
||||
{
|
||||
QByteArray ciphertext(plaintext.size(), 0);
|
||||
EVP_CIPHER_CTX* cipher;
|
||||
int ciphertextLen;
|
||||
|
||||
for (int i = 0; i < plaintext.size(); i += 16)
|
||||
{
|
||||
AES_encrypt(reinterpret_cast<unsigned char*>(&plaintext.data()[i]),
|
||||
reinterpret_cast<unsigned char*>(&ciphertext.data()[i]),
|
||||
key);
|
||||
}
|
||||
cipher = EVP_CIPHER_CTX_new();
|
||||
THROW_BAD_ALLOC_IF_NULL(cipher);
|
||||
|
||||
EVP_EncryptInit(cipher, EVP_aes_128_ecb(), reinterpret_cast<const unsigned char*>(key.data()), NULL);
|
||||
EVP_CIPHER_CTX_set_padding(cipher, 0);
|
||||
|
||||
EVP_EncryptUpdate(cipher,
|
||||
reinterpret_cast<unsigned char*>(ciphertext.data()),
|
||||
&ciphertextLen,
|
||||
reinterpret_cast<const unsigned char*>(plaintext.data()),
|
||||
plaintext.length());
|
||||
Q_ASSERT(ciphertextLen == ciphertext.length());
|
||||
|
||||
EVP_CIPHER_CTX_free(cipher);
|
||||
|
||||
return ciphertext;
|
||||
}
|
||||
|
||||
QByteArray
|
||||
NvPairingManager::decrypt(QByteArray ciphertext, AES_KEY* key)
|
||||
NvPairingManager::decrypt(const QByteArray& ciphertext, const QByteArray& key)
|
||||
{
|
||||
QByteArray plaintext(ciphertext.size(), 0);
|
||||
EVP_CIPHER_CTX* cipher;
|
||||
int plaintextLen;
|
||||
|
||||
for (int i = 0; i < plaintext.size(); i += 16)
|
||||
{
|
||||
AES_decrypt(reinterpret_cast<unsigned char*>(&ciphertext.data()[i]),
|
||||
reinterpret_cast<unsigned char*>(&plaintext.data()[i]),
|
||||
key);
|
||||
}
|
||||
cipher = EVP_CIPHER_CTX_new();
|
||||
THROW_BAD_ALLOC_IF_NULL(cipher);
|
||||
|
||||
EVP_DecryptInit(cipher, EVP_aes_128_ecb(), reinterpret_cast<const unsigned char*>(key.data()), NULL);
|
||||
EVP_CIPHER_CTX_set_padding(cipher, 0);
|
||||
|
||||
EVP_DecryptUpdate(cipher,
|
||||
reinterpret_cast<unsigned char*>(plaintext.data()),
|
||||
&plaintextLen,
|
||||
reinterpret_cast<const unsigned char*>(ciphertext.data()),
|
||||
ciphertext.length());
|
||||
Q_ASSERT(plaintextLen == plaintext.length());
|
||||
|
||||
EVP_CIPHER_CTX_free(cipher);
|
||||
|
||||
return plaintext;
|
||||
}
|
||||
|
||||
QByteArray
|
||||
NvPairingManager::getSignatureFromPemCert(QByteArray certificate)
|
||||
NvPairingManager::getSignatureFromPemCert(const QByteArray& certificate)
|
||||
{
|
||||
#if (OPENSSL_VERSION_NUMBER < 0x10100000L)
|
||||
BIO* bio = BIO_new_mem_buf(const_cast<char*>(certificate.data()), -1);
|
||||
#else
|
||||
BIO* bio = BIO_new_mem_buf(certificate.data(), -1);
|
||||
#endif
|
||||
THROW_BAD_ALLOC_IF_NULL(bio);
|
||||
|
||||
X509* cert = PEM_read_bio_X509(bio, nullptr, nullptr, nullptr);
|
||||
@@ -105,9 +132,13 @@ NvPairingManager::getSignatureFromPemCert(QByteArray certificate)
|
||||
}
|
||||
|
||||
bool
|
||||
NvPairingManager::verifySignature(QByteArray data, QByteArray signature, QByteArray serverCertificate)
|
||||
NvPairingManager::verifySignature(const QByteArray& data, const QByteArray& signature, const QByteArray& serverCertificate)
|
||||
{
|
||||
#if (OPENSSL_VERSION_NUMBER < 0x10100000L)
|
||||
BIO* bio = BIO_new_mem_buf(const_cast<char*>(serverCertificate.data()), -1);
|
||||
#else
|
||||
BIO* bio = BIO_new_mem_buf(serverCertificate.data(), -1);
|
||||
#endif
|
||||
THROW_BAD_ALLOC_IF_NULL(bio);
|
||||
|
||||
X509* cert = PEM_read_bio_X509(bio, nullptr, nullptr, nullptr);
|
||||
@@ -121,7 +152,7 @@ NvPairingManager::verifySignature(QByteArray data, QByteArray signature, QByteAr
|
||||
|
||||
EVP_DigestVerifyInit(mdctx, nullptr, EVP_sha256(), nullptr, pubKey);
|
||||
EVP_DigestVerifyUpdate(mdctx, data.data(), data.length());
|
||||
int result = EVP_DigestVerifyFinal(mdctx, reinterpret_cast<unsigned char*>(signature.data()), signature.length());
|
||||
int result = EVP_DigestVerifyFinal(mdctx, reinterpret_cast<unsigned char*>(const_cast<char*>(signature.data())), signature.length());
|
||||
|
||||
EVP_PKEY_free(pubKey);
|
||||
EVP_MD_CTX_destroy(mdctx);
|
||||
@@ -131,17 +162,13 @@ NvPairingManager::verifySignature(QByteArray data, QByteArray signature, QByteAr
|
||||
}
|
||||
|
||||
QByteArray
|
||||
NvPairingManager::signMessage(QByteArray message)
|
||||
NvPairingManager::signMessage(const QByteArray& message)
|
||||
{
|
||||
EVP_MD_CTX *ctx = EVP_MD_CTX_create();
|
||||
THROW_BAD_ALLOC_IF_NULL(ctx);
|
||||
|
||||
const EVP_MD *md = EVP_get_digestbyname("SHA256");
|
||||
THROW_BAD_ALLOC_IF_NULL(md);
|
||||
|
||||
EVP_DigestInit_ex(ctx, md, NULL);
|
||||
EVP_DigestSignInit(ctx, NULL, md, NULL, m_PrivateKey);
|
||||
EVP_DigestSignUpdate(ctx, reinterpret_cast<unsigned char*>(message.data()), message.length());
|
||||
EVP_DigestSignInit(ctx, NULL, EVP_sha256(), NULL, m_PrivateKey);
|
||||
EVP_DigestSignUpdate(ctx, reinterpret_cast<unsigned char*>(const_cast<char*>(message.data())), message.length());
|
||||
|
||||
size_t signatureLength = 0;
|
||||
EVP_DigestSignFinal(ctx, NULL, &signatureLength);
|
||||
@@ -155,13 +182,13 @@ NvPairingManager::signMessage(QByteArray message)
|
||||
}
|
||||
|
||||
QByteArray
|
||||
NvPairingManager::saltPin(QByteArray salt, QString pin)
|
||||
NvPairingManager::saltPin(const QByteArray& salt, QString pin)
|
||||
{
|
||||
return QByteArray().append(salt).append(pin.toLatin1());
|
||||
}
|
||||
|
||||
NvPairingManager::PairState
|
||||
NvPairingManager::pair(QString appVersion, QString pin)
|
||||
NvPairingManager::pair(QString appVersion, QString pin, QSslCertificate& serverCert)
|
||||
{
|
||||
int serverMajorVersion = NvHTTP::parseQuad(appVersion).at(0);
|
||||
qInfo() << "Pairing with server generation:" << serverMajorVersion;
|
||||
@@ -184,15 +211,14 @@ NvPairingManager::pair(QString appVersion, QString pin)
|
||||
QByteArray salt = generateRandomBytes(16);
|
||||
QByteArray saltedPin = saltPin(salt, pin);
|
||||
|
||||
AES_KEY encKey, decKey;
|
||||
AES_set_decrypt_key(reinterpret_cast<const unsigned char*>(QCryptographicHash::hash(saltedPin, hashAlgo).data()), 128, &decKey);
|
||||
AES_set_encrypt_key(reinterpret_cast<const unsigned char*>(QCryptographicHash::hash(saltedPin, hashAlgo).data()), 128, &encKey);
|
||||
QByteArray aesKey = QCryptographicHash::hash(saltedPin, hashAlgo).constData();
|
||||
aesKey.truncate(16);
|
||||
|
||||
QString getCert = m_Http.openConnectionToString(m_Http.m_BaseUrlHttp,
|
||||
"pair",
|
||||
"devicename=roth&updateState=1&phrase=getservercert&salt=" +
|
||||
salt.toHex() + "&clientcert=" + IdentityManager::get()->getCertificate().toHex(),
|
||||
false);
|
||||
0);
|
||||
NvHTTP::verifyResponseStatus(getCert);
|
||||
if (NvHTTP::getXmlString(getCert, "paired") != "1")
|
||||
{
|
||||
@@ -200,30 +226,43 @@ NvPairingManager::pair(QString appVersion, QString pin)
|
||||
return PairState::FAILED;
|
||||
}
|
||||
|
||||
QByteArray serverCert = NvHTTP::getXmlStringFromHex(getCert, "plaincert");
|
||||
if (serverCert == nullptr)
|
||||
QByteArray serverCertStr = NvHTTP::getXmlStringFromHex(getCert, "plaincert");
|
||||
if (serverCertStr == nullptr)
|
||||
{
|
||||
qCritical() << "Server likely already pairing";
|
||||
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
|
||||
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, REQUEST_TIMEOUT_MS);
|
||||
return PairState::ALREADY_IN_PROGRESS;
|
||||
}
|
||||
|
||||
QSslCertificate unverifiedServerCert = QSslCertificate(serverCertStr);
|
||||
if (unverifiedServerCert.isNull()) {
|
||||
Q_ASSERT(!unverifiedServerCert.isNull());
|
||||
|
||||
qCritical() << "Failed to parse plaincert";
|
||||
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, REQUEST_TIMEOUT_MS);
|
||||
return PairState::FAILED;
|
||||
}
|
||||
|
||||
// Pin this cert for TLS until pairing is complete. If successful, we will propagate
|
||||
// the cert into the NvComputer object and persist it.
|
||||
m_Http.setServerCert(unverifiedServerCert);
|
||||
|
||||
QByteArray randomChallenge = generateRandomBytes(16);
|
||||
QByteArray encryptedChallenge = encrypt(randomChallenge, &encKey);
|
||||
QByteArray encryptedChallenge = encrypt(randomChallenge, aesKey);
|
||||
QString challengeXml = m_Http.openConnectionToString(m_Http.m_BaseUrlHttp,
|
||||
"pair",
|
||||
"devicename=roth&updateState=1&clientchallenge=" +
|
||||
encryptedChallenge.toHex(),
|
||||
true);
|
||||
REQUEST_TIMEOUT_MS);
|
||||
NvHTTP::verifyResponseStatus(challengeXml);
|
||||
if (NvHTTP::getXmlString(challengeXml, "paired") != "1")
|
||||
{
|
||||
qCritical() << "Failed pairing at stage #2";
|
||||
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
|
||||
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, REQUEST_TIMEOUT_MS);
|
||||
return PairState::FAILED;
|
||||
}
|
||||
|
||||
QByteArray challengeResponseData = decrypt(m_Http.getXmlStringFromHex(challengeXml, "challengeresponse"), &decKey);
|
||||
QByteArray challengeResponseData = decrypt(m_Http.getXmlStringFromHex(challengeXml, "challengeresponse"), aesKey);
|
||||
QByteArray clientSecretData = generateRandomBytes(16);
|
||||
QByteArray challengeResponse;
|
||||
QByteArray serverResponse(challengeResponseData.data(), hashLength);
|
||||
@@ -242,17 +281,19 @@ NvPairingManager::pair(QString appVersion, QString pin)
|
||||
challengeResponse.append(reinterpret_cast<char*>(asnSignature->data), asnSignature->length);
|
||||
challengeResponse.append(clientSecretData);
|
||||
|
||||
QByteArray encryptedChallengeResponseHash = encrypt(QCryptographicHash::hash(challengeResponse, hashAlgo), &encKey);
|
||||
QByteArray paddedHash = QCryptographicHash::hash(challengeResponse, hashAlgo);
|
||||
paddedHash.resize(32);
|
||||
QByteArray encryptedChallengeResponseHash = encrypt(paddedHash, aesKey);
|
||||
QString respXml = m_Http.openConnectionToString(m_Http.m_BaseUrlHttp,
|
||||
"pair",
|
||||
"devicename=roth&updateState=1&serverchallengeresp=" +
|
||||
encryptedChallengeResponseHash.toHex(),
|
||||
true);
|
||||
REQUEST_TIMEOUT_MS);
|
||||
NvHTTP::verifyResponseStatus(respXml);
|
||||
if (NvHTTP::getXmlString(respXml, "paired") != "1")
|
||||
{
|
||||
qCritical() << "Failed pairing at stage #3";
|
||||
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
|
||||
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, REQUEST_TIMEOUT_MS);
|
||||
return PairState::FAILED;
|
||||
}
|
||||
|
||||
@@ -262,21 +303,21 @@ NvPairingManager::pair(QString appVersion, QString pin)
|
||||
|
||||
if (!verifySignature(serverSecret,
|
||||
serverSignature,
|
||||
serverCert))
|
||||
serverCertStr))
|
||||
{
|
||||
qCritical() << "MITM detected";
|
||||
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
|
||||
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, REQUEST_TIMEOUT_MS);
|
||||
return PairState::FAILED;
|
||||
}
|
||||
|
||||
QByteArray expectedResponseData;
|
||||
expectedResponseData.append(randomChallenge);
|
||||
expectedResponseData.append(getSignatureFromPemCert(serverCert));
|
||||
expectedResponseData.append(getSignatureFromPemCert(serverCertStr));
|
||||
expectedResponseData.append(serverSecret);
|
||||
if (QCryptographicHash::hash(expectedResponseData, hashAlgo) != serverResponse)
|
||||
{
|
||||
qCritical() << "Incorrect PIN";
|
||||
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
|
||||
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, REQUEST_TIMEOUT_MS);
|
||||
return PairState::PIN_WRONG;
|
||||
}
|
||||
|
||||
@@ -288,26 +329,27 @@ NvPairingManager::pair(QString appVersion, QString pin)
|
||||
"pair",
|
||||
"devicename=roth&updateState=1&clientpairingsecret=" +
|
||||
clientPairingSecret.toHex(),
|
||||
true);
|
||||
REQUEST_TIMEOUT_MS);
|
||||
NvHTTP::verifyResponseStatus(secretRespXml);
|
||||
if (NvHTTP::getXmlString(secretRespXml, "paired") != "1")
|
||||
{
|
||||
qCritical() << "Failed pairing at stage #4";
|
||||
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
|
||||
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, REQUEST_TIMEOUT_MS);
|
||||
return PairState::FAILED;
|
||||
}
|
||||
|
||||
QString pairChallengeXml = m_Http.openConnectionToString(m_Http.m_BaseUrlHttps,
|
||||
"pair",
|
||||
"devicename=roth&updateState=1&phrase=pairchallenge",
|
||||
true);
|
||||
REQUEST_TIMEOUT_MS);
|
||||
NvHTTP::verifyResponseStatus(pairChallengeXml);
|
||||
if (NvHTTP::getXmlString(pairChallengeXml, "paired") != "1")
|
||||
{
|
||||
qCritical() << "Failed pairing at stage #5";
|
||||
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
|
||||
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, REQUEST_TIMEOUT_MS);
|
||||
return PairState::FAILED;
|
||||
}
|
||||
|
||||
serverCert = std::move(unverifiedServerCert);
|
||||
return PairState::PAIRED;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
#include "identitymanager.h"
|
||||
#include "nvhttp.h"
|
||||
|
||||
#include <openssl/aes.h>
|
||||
#include <openssl/x509.h>
|
||||
#include <openssl/evp.h>
|
||||
|
||||
@@ -18,34 +17,34 @@ public:
|
||||
ALREADY_IN_PROGRESS
|
||||
};
|
||||
|
||||
explicit NvPairingManager(QString address);
|
||||
explicit NvPairingManager(NvComputer* computer);
|
||||
|
||||
~NvPairingManager();
|
||||
|
||||
PairState
|
||||
pair(QString appVersion, QString pin);
|
||||
pair(QString appVersion, QString pin, QSslCertificate& serverCert);
|
||||
|
||||
private:
|
||||
QByteArray
|
||||
generateRandomBytes(int length);
|
||||
|
||||
QByteArray
|
||||
saltPin(QByteArray salt, QString pin);
|
||||
saltPin(const QByteArray& salt, QString pin);
|
||||
|
||||
QByteArray
|
||||
encrypt(QByteArray plaintext, AES_KEY* key);
|
||||
encrypt(const QByteArray& plaintext, const QByteArray& key);
|
||||
|
||||
QByteArray
|
||||
decrypt(QByteArray ciphertext, AES_KEY* key);
|
||||
decrypt(const QByteArray& ciphertext, const QByteArray& key);
|
||||
|
||||
QByteArray
|
||||
getSignatureFromPemCert(QByteArray certificate);
|
||||
getSignatureFromPemCert(const QByteArray& certificate);
|
||||
|
||||
bool
|
||||
verifySignature(QByteArray data, QByteArray signature, QByteArray serverCertificate);
|
||||
verifySignature(const QByteArray& data, const QByteArray& signature, const QByteArray& serverCertificate);
|
||||
|
||||
QByteArray
|
||||
signMessage(QByteArray message);
|
||||
signMessage(const QByteArray& message);
|
||||
|
||||
NvHTTP m_Http;
|
||||
X509* m_Cert;
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
#include "richpresencemanager.h"
|
||||
|
||||
#include <QDebug>
|
||||
|
||||
RichPresenceManager::RichPresenceManager(StreamingPreferences& prefs, QString gameName)
|
||||
: m_DiscordActive(false)
|
||||
{
|
||||
#ifdef HAVE_DISCORD
|
||||
if (prefs.richPresence) {
|
||||
DiscordEventHandlers handlers = {};
|
||||
handlers.ready = discordReady;
|
||||
handlers.disconnected = discordDisconnected;
|
||||
handlers.errored = discordErrored;
|
||||
Discord_Initialize("594668102021677159", &handlers, 0, nullptr);
|
||||
m_DiscordActive = true;
|
||||
}
|
||||
|
||||
if (m_DiscordActive) {
|
||||
QByteArray stateStr = (QString("Streaming ") + gameName).toUtf8();
|
||||
|
||||
DiscordRichPresence discordPresence = {};
|
||||
discordPresence.state = stateStr.data();
|
||||
discordPresence.startTimestamp = time(nullptr);
|
||||
discordPresence.largeImageKey = "icon";
|
||||
Discord_UpdatePresence(&discordPresence);
|
||||
}
|
||||
#else
|
||||
Q_UNUSED(prefs)
|
||||
Q_UNUSED(gameName)
|
||||
#endif
|
||||
}
|
||||
|
||||
RichPresenceManager::~RichPresenceManager()
|
||||
{
|
||||
#ifdef HAVE_DISCORD
|
||||
if (m_DiscordActive) {
|
||||
Discord_ClearPresence();
|
||||
Discord_Shutdown();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void RichPresenceManager::runCallbacks()
|
||||
{
|
||||
#ifdef HAVE_DISCORD
|
||||
if (m_DiscordActive) {
|
||||
Discord_RunCallbacks();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef HAVE_DISCORD
|
||||
void RichPresenceManager::discordReady(const DiscordUser* request)
|
||||
{
|
||||
qInfo() << "Discord integration ready for user:" << request->username;
|
||||
}
|
||||
|
||||
void RichPresenceManager::discordDisconnected(int errorCode, const char *message)
|
||||
{
|
||||
qInfo() << "Discord integration disconnected:" << errorCode << message;
|
||||
}
|
||||
|
||||
void RichPresenceManager::discordErrored(int errorCode, const char *message)
|
||||
{
|
||||
qWarning() << "Discord integration error:" << errorCode << message;
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,26 @@
|
||||
#pragma once
|
||||
|
||||
#include "settings/streamingpreferences.h"
|
||||
|
||||
#ifdef HAVE_DISCORD
|
||||
#include <discord_rpc.h>
|
||||
#endif
|
||||
|
||||
class RichPresenceManager
|
||||
{
|
||||
public:
|
||||
RichPresenceManager(StreamingPreferences& prefs, QString gameName);
|
||||
~RichPresenceManager();
|
||||
|
||||
void runCallbacks();
|
||||
|
||||
private:
|
||||
#ifdef HAVE_DISCORD
|
||||
static void discordReady(const DiscordUser* request);
|
||||
static void discordDisconnected(int errorCode, const char* message);
|
||||
static void discordErrored(int errorCode, const char* message);
|
||||
#endif
|
||||
|
||||
bool m_DiscordActive;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
#include "systemproperties.h"
|
||||
#include "utils.h"
|
||||
|
||||
#include <QGuiApplication>
|
||||
#include <QLibraryInfo>
|
||||
|
||||
#include "streaming/session.h"
|
||||
#include "streaming/streamutils.h"
|
||||
|
||||
#ifdef Q_OS_WIN32
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <Windows.h>
|
||||
#endif
|
||||
|
||||
SystemProperties::SystemProperties()
|
||||
{
|
||||
versionString = QString(VERSION_STR);
|
||||
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
|
||||
{
|
||||
USHORT processArch, machineArch;
|
||||
|
||||
// Use IsWow64Process2 on TH2 and later, because it supports ARM64
|
||||
auto fnIsWow64Process2 = (decltype(IsWow64Process2)*)GetProcAddress(GetModuleHandleA("kernel32.dll"), "IsWow64Process2");
|
||||
if (fnIsWow64Process2 != nullptr && fnIsWow64Process2(GetCurrentProcess(), &processArch, &machineArch)) {
|
||||
switch (machineArch) {
|
||||
case IMAGE_FILE_MACHINE_I386:
|
||||
nativeArch = "i386";
|
||||
break;
|
||||
case IMAGE_FILE_MACHINE_AMD64:
|
||||
nativeArch = "x86_64";
|
||||
break;
|
||||
case IMAGE_FILE_MACHINE_ARM64:
|
||||
nativeArch = "arm64";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
isWow64 = nativeArch != QSysInfo::buildCpuArchitecture();
|
||||
}
|
||||
#else
|
||||
isWow64 = false;
|
||||
#endif
|
||||
|
||||
if (nativeArch == "i386") {
|
||||
friendlyNativeArchName = "x86";
|
||||
}
|
||||
else if (nativeArch == "x86_64") {
|
||||
friendlyNativeArchName = "x64";
|
||||
}
|
||||
else {
|
||||
friendlyNativeArchName = nativeArch.toUpper();
|
||||
}
|
||||
|
||||
// Assume we can probably launch a browser if we're in a GUI environment
|
||||
hasBrowser = hasDesktopEnvironment;
|
||||
|
||||
#ifdef HAVE_DISCORD
|
||||
hasDiscordIntegration = true;
|
||||
#else
|
||||
hasDiscordIntegration = false;
|
||||
#endif
|
||||
|
||||
unmappedGamepads = SdlInputHandler::getUnmappedGamepads();
|
||||
|
||||
// Populate data that requires talking to SDL. We do it all in one shot
|
||||
// and cache the results to speed up future queries on this data.
|
||||
querySdlVideoInfo();
|
||||
|
||||
Q_ASSERT(!monitorRefreshRates.isEmpty());
|
||||
Q_ASSERT(!monitorNativeResolutions.isEmpty());
|
||||
}
|
||||
|
||||
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:
|
||||
QuerySdlVideoThread(SystemProperties* me) :
|
||||
QThread(nullptr),
|
||||
m_Me(me) {}
|
||||
|
||||
void run() override
|
||||
{
|
||||
m_Me->querySdlVideoInfoInternal();
|
||||
}
|
||||
|
||||
SystemProperties* m_Me;
|
||||
};
|
||||
|
||||
void SystemProperties::querySdlVideoInfo()
|
||||
{
|
||||
if (WMUtils::isRunningX11() || WMUtils::isRunningWayland()) {
|
||||
// Use a separate thread to temporarily initialize SDL
|
||||
// video to avoid stomping on Qt's X11 and OGL state.
|
||||
QuerySdlVideoThread thread(this);
|
||||
thread.start();
|
||||
thread.wait();
|
||||
}
|
||||
else {
|
||||
querySdlVideoInfoInternal();
|
||||
}
|
||||
}
|
||||
|
||||
void SystemProperties::querySdlVideoInfoInternal()
|
||||
{
|
||||
hasHardwareAcceleration = false;
|
||||
|
||||
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s",
|
||||
SDL_GetError());
|
||||
return;
|
||||
}
|
||||
|
||||
// Update display related attributes (max FPS, native resolution, etc).
|
||||
// We call the internal variant because we're already in a safe thread context.
|
||||
refreshDisplaysInternal();
|
||||
|
||||
SDL_Window* testWindow = SDL_CreateWindow("", 0, 0, 1280, 720,
|
||||
SDL_WINDOW_HIDDEN | StreamUtils::getPlatformWindowFlags());
|
||||
if (!testWindow) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Failed to create test window with platform flags: %s",
|
||||
SDL_GetError());
|
||||
|
||||
testWindow = SDL_CreateWindow("", 0, 0, 1280, 720, SDL_WINDOW_HIDDEN);
|
||||
if (!testWindow) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Failed to create window for hardware decode test: %s",
|
||||
SDL_GetError());
|
||||
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Session::getDecoderInfo(testWindow, hasHardwareAcceleration, rendererAlwaysFullScreen, supportsHdr, maximumResolution);
|
||||
|
||||
SDL_DestroyWindow(testWindow);
|
||||
|
||||
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||
}
|
||||
|
||||
class RefreshDisplaysThread : public QThread
|
||||
{
|
||||
public:
|
||||
RefreshDisplaysThread(SystemProperties* me) :
|
||||
QThread(nullptr),
|
||||
m_Me(me) {}
|
||||
|
||||
void run() override
|
||||
{
|
||||
m_Me->refreshDisplaysInternal();
|
||||
}
|
||||
|
||||
SystemProperties* m_Me;
|
||||
};
|
||||
|
||||
void SystemProperties::refreshDisplays()
|
||||
{
|
||||
if (WMUtils::isRunningX11() || WMUtils::isRunningWayland()) {
|
||||
// Use a separate thread to temporarily initialize SDL
|
||||
// video to avoid stomping on Qt's X11 and OGL state.
|
||||
RefreshDisplaysThread thread(this);
|
||||
thread.start();
|
||||
thread.wait();
|
||||
}
|
||||
else {
|
||||
refreshDisplaysInternal();
|
||||
}
|
||||
}
|
||||
|
||||
void SystemProperties::refreshDisplaysInternal()
|
||||
{
|
||||
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s",
|
||||
SDL_GetError());
|
||||
return;
|
||||
}
|
||||
|
||||
monitorNativeResolutions.clear();
|
||||
|
||||
SDL_DisplayMode bestMode;
|
||||
for (int displayIndex = 0; displayIndex < SDL_GetNumVideoDisplays(); displayIndex++) {
|
||||
SDL_DisplayMode desktopMode;
|
||||
|
||||
if (StreamUtils::getNativeDesktopMode(displayIndex, &desktopMode)) {
|
||||
if (desktopMode.w <= 8192 && desktopMode.h <= 8192) {
|
||||
monitorNativeResolutions.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);
|
||||
}
|
||||
|
||||
// Start at desktop mode and work our way up
|
||||
bestMode = desktopMode;
|
||||
for (int i = 0; i < SDL_GetNumDisplayModes(displayIndex); i++) {
|
||||
SDL_DisplayMode mode;
|
||||
if (SDL_GetDisplayMode(displayIndex, i, &mode) == 0) {
|
||||
if (mode.w == desktopMode.w && mode.h == desktopMode.h) {
|
||||
if (mode.refresh_rate > bestMode.refresh_rate) {
|
||||
bestMode = mode;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try to normalize values around our our standard refresh rates.
|
||||
// Some displays/OSes report values that are slightly off.
|
||||
if (bestMode.refresh_rate >= 58 && bestMode.refresh_rate <= 62) {
|
||||
monitorRefreshRates.append(60);
|
||||
}
|
||||
else if (bestMode.refresh_rate >= 28 && bestMode.refresh_rate <= 32) {
|
||||
monitorRefreshRates.append(30);
|
||||
}
|
||||
else {
|
||||
monitorRefreshRates.append(bestMode.refresh_rate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QRect>
|
||||
|
||||
class SystemProperties : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
friend class QuerySdlVideoThread;
|
||||
friend class RefreshDisplaysThread;
|
||||
|
||||
public:
|
||||
SystemProperties();
|
||||
|
||||
Q_PROPERTY(bool hasHardwareAcceleration MEMBER hasHardwareAcceleration CONSTANT)
|
||||
Q_PROPERTY(bool rendererAlwaysFullScreen MEMBER rendererAlwaysFullScreen CONSTANT)
|
||||
Q_PROPERTY(bool isRunningWayland MEMBER isRunningWayland CONSTANT)
|
||||
Q_PROPERTY(bool isRunningXWayland MEMBER isRunningXWayland CONSTANT)
|
||||
Q_PROPERTY(bool isWow64 MEMBER isWow64 CONSTANT)
|
||||
Q_PROPERTY(QString friendlyNativeArchName MEMBER friendlyNativeArchName CONSTANT)
|
||||
Q_PROPERTY(bool hasDesktopEnvironment MEMBER hasDesktopEnvironment CONSTANT)
|
||||
Q_PROPERTY(bool hasBrowser MEMBER hasBrowser CONSTANT)
|
||||
Q_PROPERTY(bool hasDiscordIntegration MEMBER hasDiscordIntegration CONSTANT)
|
||||
Q_PROPERTY(QString unmappedGamepads MEMBER unmappedGamepads NOTIFY unmappedGamepadsChanged)
|
||||
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 getNativeResolution(int displayIndex);
|
||||
Q_INVOKABLE int getRefreshRate(int displayIndex);
|
||||
|
||||
signals:
|
||||
void unmappedGamepadsChanged();
|
||||
|
||||
private:
|
||||
void querySdlVideoInfo();
|
||||
void querySdlVideoInfoInternal();
|
||||
void refreshDisplaysInternal();
|
||||
|
||||
bool hasHardwareAcceleration;
|
||||
bool rendererAlwaysFullScreen;
|
||||
bool isRunningWayland;
|
||||
bool isRunningXWayland;
|
||||
bool isWow64;
|
||||
QString friendlyNativeArchName;
|
||||
bool hasDesktopEnvironment;
|
||||
bool hasBrowser;
|
||||
bool hasDiscordIntegration;
|
||||
QString unmappedGamepads;
|
||||
QSize maximumResolution;
|
||||
QList<QRect> monitorNativeResolutions;
|
||||
QList<int> monitorRefreshRates;
|
||||
QString versionString;
|
||||
bool supportsHdr;
|
||||
bool usesMaterial3Theme;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,587 @@
|
||||
#include "commandlineparser.h"
|
||||
|
||||
#include <QCommandLineParser>
|
||||
#include <QRegularExpression>
|
||||
|
||||
#if defined(Q_OS_WIN)
|
||||
#include <qt_windows.h>
|
||||
#endif
|
||||
|
||||
static bool inRange(int value, int min, int max)
|
||||
{
|
||||
return value >= min && value <= max;
|
||||
}
|
||||
|
||||
// This method returns key's value from QMap where the key is a QString.
|
||||
// Key matching is case insensitive.
|
||||
template <typename T>
|
||||
static T mapValue(QMap<QString, T> map, QString key)
|
||||
{
|
||||
for(auto& item : map.toStdMap()) {
|
||||
if (QString::compare(item.first, key, Qt::CaseInsensitive) == 0) {
|
||||
return item.second;
|
||||
}
|
||||
}
|
||||
return T();
|
||||
}
|
||||
|
||||
class CommandLineParser : public QCommandLineParser
|
||||
{
|
||||
public:
|
||||
enum MessageType {
|
||||
Info,
|
||||
Error
|
||||
};
|
||||
|
||||
void setupCommonOptions()
|
||||
{
|
||||
setSingleDashWordOptionMode(QCommandLineParser::ParseAsLongOptions);
|
||||
addHelpOption();
|
||||
addVersionOption();
|
||||
}
|
||||
|
||||
void handleHelpAndVersionOptions()
|
||||
{
|
||||
if (isSet("help")) {
|
||||
showInfo(helpText());
|
||||
}
|
||||
if (isSet("version")) {
|
||||
showVersion();
|
||||
}
|
||||
}
|
||||
|
||||
void handleUnknownOptions()
|
||||
{
|
||||
if (!unknownOptionNames().isEmpty()) {
|
||||
showError(QString("Unknown options: %1").arg(unknownOptionNames().join(", ")));
|
||||
}
|
||||
}
|
||||
|
||||
void showMessage(QString message, MessageType type) const
|
||||
{
|
||||
#if defined(Q_OS_WIN32)
|
||||
UINT flags = MB_OK | MB_TOPMOST | MB_SETFOREGROUND;
|
||||
flags |= (type == Info ? MB_ICONINFORMATION : MB_ICONERROR);
|
||||
QString title = "Moonlight";
|
||||
MessageBoxW(nullptr, reinterpret_cast<const wchar_t *>(message.utf16()),
|
||||
reinterpret_cast<const wchar_t *>(title.utf16()), flags);
|
||||
#endif
|
||||
message = message.endsWith('\n') ? message : message + '\n';
|
||||
fputs(qPrintable(message), type == Info ? stdout : stderr);
|
||||
}
|
||||
|
||||
[[ noreturn ]] void showInfo(QString message) const
|
||||
{
|
||||
showMessage(message, Info);
|
||||
exit(0);
|
||||
}
|
||||
|
||||
[[ noreturn ]] void showError(QString message) const
|
||||
{
|
||||
showMessage(message + "\n\n" + helpText(), Error);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
int getIntOption(QString name) const
|
||||
{
|
||||
bool ok;
|
||||
int intValue = value(name).toInt(&ok);
|
||||
if (!ok) {
|
||||
showError(QString("Invalid %1 value: %2").arg(name, value(name)));
|
||||
}
|
||||
return intValue;
|
||||
}
|
||||
|
||||
bool getToggleOptionValue(QString name, bool defaultValue) const
|
||||
{
|
||||
QRegularExpression re(QString("^(%1|no-%1)$").arg(name));
|
||||
QStringList options = optionNames().filter(re);
|
||||
if (options.isEmpty()) {
|
||||
return defaultValue;
|
||||
} else {
|
||||
return options.last() == name;
|
||||
}
|
||||
}
|
||||
|
||||
QString getChoiceOptionValue(QString name) const
|
||||
{
|
||||
if (!m_Choices[name].contains(value(name), Qt::CaseInsensitive)) {
|
||||
showError(QString("Invalid %1 choice: %2").arg(name, value(name)));
|
||||
}
|
||||
return value(name);
|
||||
}
|
||||
|
||||
QPair<int,int> getResolutionOptionValue(QString name) const
|
||||
{
|
||||
QRegularExpression re("^(\\d+)x(\\d+)$", QRegularExpression::CaseInsensitiveOption);
|
||||
auto match = re.match(value(name));
|
||||
if (!match.hasMatch()) {
|
||||
showError(QString("Invalid %1 format: %2").arg(name, value(name)));
|
||||
}
|
||||
return qMakePair(match.captured(1).toInt(), match.captured(2).toInt());
|
||||
}
|
||||
|
||||
void addFlagOption(QString name, QString descriptiveName)
|
||||
{
|
||||
addOption(QCommandLineOption(name, QString("Use %1.").arg(descriptiveName)));
|
||||
}
|
||||
|
||||
void addToggleOption(QString name, QString descriptiveName)
|
||||
{
|
||||
addOption(QCommandLineOption(name, QString("Use %1.").arg(descriptiveName)));
|
||||
addOption(QCommandLineOption("no-" + name, QString("Do not use %1.").arg(descriptiveName)));
|
||||
}
|
||||
|
||||
void addValueOption(QString name, QString descriptiveName)
|
||||
{
|
||||
addOption(QCommandLineOption(name, QString("Specify %1 to use.").arg(descriptiveName), name));
|
||||
}
|
||||
|
||||
void addChoiceOption(QString name, QString descriptiveName, QStringList choices)
|
||||
{
|
||||
addOption(QCommandLineOption(name, QString("Select %1: %2.").arg(descriptiveName, choices.join('/')), name));
|
||||
m_Choices[name] = choices;
|
||||
}
|
||||
|
||||
private:
|
||||
QMap<QString, QStringList> m_Choices;
|
||||
};
|
||||
|
||||
GlobalCommandLineParser::GlobalCommandLineParser()
|
||||
{
|
||||
}
|
||||
|
||||
GlobalCommandLineParser::~GlobalCommandLineParser()
|
||||
{
|
||||
}
|
||||
|
||||
GlobalCommandLineParser::ParseResult GlobalCommandLineParser::parse(const QStringList &args)
|
||||
{
|
||||
CommandLineParser parser;
|
||||
parser.setupCommonOptions();
|
||||
parser.setApplicationDescription(
|
||||
"\n"
|
||||
"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."
|
||||
);
|
||||
parser.addPositionalArgument("action", "Action to execute", "<action>");
|
||||
parser.parse(args);
|
||||
auto posArgs = parser.positionalArguments();
|
||||
|
||||
if (posArgs.isEmpty()) {
|
||||
// This method will not return and terminates the process if --version
|
||||
// or --help is specified
|
||||
parser.handleHelpAndVersionOptions();
|
||||
parser.handleUnknownOptions();
|
||||
return NormalStartRequested;
|
||||
}
|
||||
else {
|
||||
// If users supply arguments that accept values prior to the "quit"
|
||||
// or "stream" positional arguments, we will not be able to correctly
|
||||
// parse the value out of the input because this QCommandLineParser
|
||||
// doesn't know about all of the options that "quit" and "stream"
|
||||
// commands can accept. To work around this issue, we just look
|
||||
// for "quit" or "stream" positional arguments anywhere.
|
||||
for (int i = 0; i < posArgs.size(); i++) {
|
||||
QString action = posArgs.at(i).toLower();
|
||||
if (action == "quit") {
|
||||
return QuitRequested;
|
||||
} else if (action == "stream") {
|
||||
return StreamRequested;
|
||||
} else if (action == "pair") {
|
||||
return PairRequested;
|
||||
} else if (action == "list") {
|
||||
return ListRequested;
|
||||
}
|
||||
}
|
||||
|
||||
parser.showError(QString("Invalid action"));
|
||||
}
|
||||
}
|
||||
|
||||
QuitCommandLineParser::QuitCommandLineParser()
|
||||
{
|
||||
}
|
||||
|
||||
QuitCommandLineParser::~QuitCommandLineParser()
|
||||
{
|
||||
}
|
||||
|
||||
void QuitCommandLineParser::parse(const QStringList &args)
|
||||
{
|
||||
CommandLineParser parser;
|
||||
parser.setupCommonOptions();
|
||||
parser.setApplicationDescription(
|
||||
"\n"
|
||||
"Quit the currently running app on the given host."
|
||||
);
|
||||
parser.addPositionalArgument("quit", "quit running app");
|
||||
parser.addPositionalArgument("host", "Host computer name, UUID, or IP address", "<host>");
|
||||
|
||||
if (!parser.parse(args)) {
|
||||
parser.showError(parser.errorText());
|
||||
}
|
||||
|
||||
parser.handleUnknownOptions();
|
||||
|
||||
// This method will not return and terminates the process if --version or
|
||||
// --help is specified
|
||||
parser.handleHelpAndVersionOptions();
|
||||
|
||||
// Verify that host has been provided
|
||||
auto posArgs = parser.positionalArguments();
|
||||
if (posArgs.length() < 2) {
|
||||
parser.showError("Host not provided");
|
||||
}
|
||||
m_Host = parser.positionalArguments().at(1);
|
||||
}
|
||||
|
||||
QString QuitCommandLineParser::getHost() const
|
||||
{
|
||||
return m_Host;
|
||||
}
|
||||
|
||||
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 = {
|
||||
{"fullscreen", StreamingPreferences::WM_FULLSCREEN},
|
||||
{"windowed", StreamingPreferences::WM_WINDOWED},
|
||||
{"borderless", StreamingPreferences::WM_FULLSCREEN_DESKTOP},
|
||||
};
|
||||
m_AudioConfigMap = {
|
||||
{"stereo", StreamingPreferences::AC_STEREO},
|
||||
{"5.1-surround", StreamingPreferences::AC_51_SURROUND},
|
||||
{"7.1-surround", StreamingPreferences::AC_71_SURROUND},
|
||||
};
|
||||
m_VideoCodecMap = {
|
||||
{"auto", StreamingPreferences::VCC_AUTO},
|
||||
{"H.264", StreamingPreferences::VCC_FORCE_H264},
|
||||
{"HEVC", StreamingPreferences::VCC_FORCE_HEVC},
|
||||
};
|
||||
m_VideoDecoderMap = {
|
||||
{"auto", StreamingPreferences::VDS_AUTO},
|
||||
{"software", StreamingPreferences::VDS_FORCE_SOFTWARE},
|
||||
{"hardware", StreamingPreferences::VDS_FORCE_HARDWARE},
|
||||
};
|
||||
m_CaptureSysKeysModeMap = {
|
||||
{"never", StreamingPreferences::CSK_OFF},
|
||||
{"fullscreen", StreamingPreferences::CSK_FULLSCREEN},
|
||||
{"always", StreamingPreferences::CSK_ALWAYS},
|
||||
};
|
||||
}
|
||||
|
||||
StreamCommandLineParser::~StreamCommandLineParser()
|
||||
{
|
||||
}
|
||||
|
||||
void StreamCommandLineParser::parse(const QStringList &args, StreamingPreferences *preferences)
|
||||
{
|
||||
CommandLineParser parser;
|
||||
parser.setupCommonOptions();
|
||||
parser.setApplicationDescription(
|
||||
"\n"
|
||||
"Starts directly streaming a given app."
|
||||
);
|
||||
parser.addPositionalArgument("stream", "Start stream");
|
||||
|
||||
// Add other arguments and options
|
||||
parser.addPositionalArgument("host", "Host computer name, UUID, or IP address", "<host>");
|
||||
parser.addPositionalArgument("app", "App to stream", "\"<app>\"");
|
||||
|
||||
parser.addFlagOption("720", "1280x720 resolution");
|
||||
parser.addFlagOption("1080", "1920x1080 resolution");
|
||||
parser.addFlagOption("1440", "2560x1440 resolution");
|
||||
parser.addFlagOption("4K", "3840x2160 resolution");
|
||||
parser.addValueOption("resolution", "custom <width>x<height> resolution");
|
||||
parser.addToggleOption("vsync", "V-Sync");
|
||||
parser.addValueOption("fps", "FPS");
|
||||
parser.addValueOption("bitrate", "bitrate in Kbps");
|
||||
parser.addValueOption("packet-size", "video packet size");
|
||||
parser.addChoiceOption("display-mode", "display mode", m_WindowModeMap.keys());
|
||||
parser.addChoiceOption("audio-config", "audio config", m_AudioConfigMap.keys());
|
||||
parser.addToggleOption("multi-controller", "multiple controller support");
|
||||
parser.addToggleOption("quit-after", "quit app after session");
|
||||
parser.addToggleOption("absolute-mouse", "remote desktop optimized mouse control");
|
||||
parser.addToggleOption("mouse-buttons-swap", "left and right mouse buttons swap");
|
||||
parser.addToggleOption("touchscreen-trackpad", "touchscreen in trackpad mode");
|
||||
parser.addToggleOption("game-optimization", "game optimizations");
|
||||
parser.addToggleOption("audio-on-host", "audio on host PC");
|
||||
parser.addToggleOption("frame-pacing", "frame pacing");
|
||||
parser.addToggleOption("mute-on-focus-loss", "mute audio when Moonlight window loses focus");
|
||||
parser.addToggleOption("background-gamepad", "background gamepad input");
|
||||
parser.addToggleOption("reverse-scroll-direction", "inverted scroll direction");
|
||||
parser.addToggleOption("swap-gamepad-buttons", "swap A/B and X/Y gamepad buttons (Nintendo-style)");
|
||||
parser.addToggleOption("keep-awake", "prevent display sleep while streaming");
|
||||
parser.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());
|
||||
|
||||
if (!parser.parse(args)) {
|
||||
parser.showError(parser.errorText());
|
||||
}
|
||||
|
||||
parser.handleUnknownOptions();
|
||||
|
||||
// Resolve display's width and height
|
||||
QRegularExpression resolutionRexExp("^(720|1080|1440|4K|resolution)$");
|
||||
QStringList resoOptions = parser.optionNames().filter(resolutionRexExp);
|
||||
bool displaySet = !resoOptions.isEmpty();
|
||||
if (displaySet) {
|
||||
QString name = resoOptions.last();
|
||||
if (name == "720") {
|
||||
preferences->width = 1280;
|
||||
preferences->height = 720;
|
||||
} else if (name == "1080") {
|
||||
preferences->width = 1920;
|
||||
preferences->height = 1080;
|
||||
} else if (name == "1440") {
|
||||
preferences->width = 2560;
|
||||
preferences->height = 1440;
|
||||
} else if (name == "4K") {
|
||||
preferences->width = 3840;
|
||||
preferences->height = 2160;
|
||||
} else if (name == "resolution") {
|
||||
auto resolution = parser.getResolutionOptionValue(name);
|
||||
preferences->width = resolution.first;
|
||||
preferences->height = resolution.second;
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve --fps option
|
||||
if (parser.isSet("fps")) {
|
||||
preferences->fps = parser.getIntOption("fps");
|
||||
if (!inRange(preferences->fps, 30, 240)) {
|
||||
parser.showError("FPS must be in range: 30 - 240");
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve --bitrate option
|
||||
if (parser.isSet("bitrate")) {
|
||||
preferences->bitrateKbps = parser.getIntOption("bitrate");
|
||||
if (!inRange(preferences->bitrateKbps, 500, 150000)) {
|
||||
parser.showError("Bitrate must be in range: 500 - 150000");
|
||||
}
|
||||
} else if (displaySet || parser.isSet("fps")) {
|
||||
preferences->bitrateKbps = preferences->getDefaultBitrate(
|
||||
preferences->width, preferences->height, preferences->fps);
|
||||
}
|
||||
|
||||
// Resolve --packet-size option
|
||||
if (parser.isSet("packet-size")) {
|
||||
preferences->packetSize = parser.getIntOption("packet-size");
|
||||
if (preferences->packetSize < 1024) {
|
||||
parser.showError("Packet size must be greater than 1024 bytes");
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve --display option
|
||||
if (parser.isSet("display-mode")) {
|
||||
preferences->windowMode = mapValue(m_WindowModeMap, parser.getChoiceOptionValue("display-mode"));
|
||||
}
|
||||
|
||||
// Resolve --vsync and --no-vsync options
|
||||
preferences->enableVsync = parser.getToggleOptionValue("vsync", preferences->enableVsync);
|
||||
|
||||
// Resolve --audio-config option
|
||||
if (parser.isSet("audio-config")) {
|
||||
preferences->audioConfig = mapValue(m_AudioConfigMap, parser.getChoiceOptionValue("audio-config"));
|
||||
}
|
||||
|
||||
// Resolve --multi-controller and --no-multi-controller options
|
||||
preferences->multiController = parser.getToggleOptionValue("multi-controller", preferences->multiController);
|
||||
|
||||
// Resolve --quit-after and --no-quit-after options
|
||||
preferences->quitAppAfter = parser.getToggleOptionValue("quit-after", preferences->quitAppAfter);
|
||||
|
||||
// Resolve --absolute-mouse and --no-absolute-mouse options
|
||||
preferences->absoluteMouseMode = parser.getToggleOptionValue("absolute-mouse", preferences->absoluteMouseMode);
|
||||
|
||||
// Resolve --mouse-buttons-swap and --no-mouse-buttons-swap options
|
||||
preferences->swapMouseButtons = parser.getToggleOptionValue("mouse-buttons-swap", preferences->swapMouseButtons);
|
||||
|
||||
// Resolve --touchscreen-trackpad and --no-touchscreen-trackpad options
|
||||
preferences->absoluteTouchMode = !parser.getToggleOptionValue("touchscreen-trackpad", !preferences->absoluteTouchMode);
|
||||
|
||||
// Resolve --game-optimization and --no-game-optimization options
|
||||
preferences->gameOptimizations = parser.getToggleOptionValue("game-optimization", preferences->gameOptimizations);
|
||||
|
||||
// Resolve --audio-on-host and --no-audio-on-host options
|
||||
preferences->playAudioOnHost = parser.getToggleOptionValue("audio-on-host", preferences->playAudioOnHost);
|
||||
|
||||
// Resolve --frame-pacing and --no-frame-pacing options
|
||||
preferences->framePacing = parser.getToggleOptionValue("frame-pacing", preferences->framePacing);
|
||||
|
||||
// Resolve --mute-on-focus-loss and --no-mute-on-focus-loss options
|
||||
preferences->muteOnFocusLoss = parser.getToggleOptionValue("mute-on-focus-loss", preferences->muteOnFocusLoss);
|
||||
|
||||
// Resolve --background-gamepad and --no-background-gamepad options
|
||||
preferences->backgroundGamepad = parser.getToggleOptionValue("background-gamepad", preferences->backgroundGamepad);
|
||||
|
||||
// Resolve --reverse-scroll-direction and --no-reverse-scroll-direction options
|
||||
preferences->reverseScrollDirection = parser.getToggleOptionValue("reverse-scroll-direction", preferences->reverseScrollDirection);
|
||||
|
||||
// Resolve --swap-gamepad-buttons and --no-swap-gamepad-buttons options
|
||||
preferences->swapFaceButtons = parser.getToggleOptionValue("swap-gamepad-buttons", preferences->swapFaceButtons);
|
||||
|
||||
// Resolve --keep-awake and --no-keep-awake options
|
||||
preferences->keepAwake = parser.getToggleOptionValue("keep-awake", preferences->keepAwake);
|
||||
|
||||
// Resolve --capture-system-keys option
|
||||
if (parser.isSet("capture-system-keys")) {
|
||||
preferences->captureSysKeysMode = mapValue(m_CaptureSysKeysModeMap, parser.getChoiceOptionValue("capture-system-keys"));
|
||||
}
|
||||
|
||||
// Resolve --video-codec option
|
||||
if (parser.isSet("video-codec")) {
|
||||
preferences->videoCodecConfig = mapValue(m_VideoCodecMap, parser.getChoiceOptionValue("video-codec"));
|
||||
}
|
||||
|
||||
// Resolve --video-decoder option
|
||||
if (parser.isSet("video-decoder")) {
|
||||
preferences->videoDecoderSelection = mapValue(m_VideoDecoderMap, parser.getChoiceOptionValue("video-decoder"));
|
||||
}
|
||||
|
||||
// This method will not return and terminates the process if --version or
|
||||
// --help is specified
|
||||
parser.handleHelpAndVersionOptions();
|
||||
|
||||
// Verify that both host and app has been provided
|
||||
auto posArgs = parser.positionalArguments();
|
||||
if (posArgs.length() < 2) {
|
||||
parser.showError("Host not provided");
|
||||
}
|
||||
m_Host = parser.positionalArguments().at(1);
|
||||
|
||||
if (posArgs.length() < 3) {
|
||||
parser.showError("App not provided");
|
||||
}
|
||||
m_AppName = parser.positionalArguments().at(2);
|
||||
}
|
||||
|
||||
QString StreamCommandLineParser::getHost() const
|
||||
{
|
||||
return m_Host;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
#pragma once
|
||||
|
||||
#include "settings/streamingpreferences.h"
|
||||
|
||||
#include <QMap>
|
||||
#include <QString>
|
||||
|
||||
class GlobalCommandLineParser
|
||||
{
|
||||
public:
|
||||
enum ParseResult {
|
||||
NormalStartRequested,
|
||||
StreamRequested,
|
||||
QuitRequested,
|
||||
PairRequested,
|
||||
ListRequested,
|
||||
};
|
||||
|
||||
GlobalCommandLineParser();
|
||||
virtual ~GlobalCommandLineParser();
|
||||
|
||||
ParseResult parse(const QStringList &args);
|
||||
|
||||
};
|
||||
|
||||
class QuitCommandLineParser
|
||||
{
|
||||
public:
|
||||
QuitCommandLineParser();
|
||||
virtual ~QuitCommandLineParser();
|
||||
|
||||
void parse(const QStringList &args);
|
||||
|
||||
QString getHost() const;
|
||||
|
||||
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:
|
||||
StreamCommandLineParser();
|
||||
virtual ~StreamCommandLineParser();
|
||||
|
||||
void parse(const QStringList &args, StreamingPreferences *preferences);
|
||||
|
||||
QString getHost() const;
|
||||
QString getAppName() const;
|
||||
|
||||
private:
|
||||
QString m_Host;
|
||||
QString m_AppName;
|
||||
QMap<QString, StreamingPreferences::WindowMode> m_WindowModeMap;
|
||||
QMap<QString, StreamingPreferences::AudioConfig> m_AudioConfigMap;
|
||||
QMap<QString, StreamingPreferences::VideoCodecConfig> m_VideoCodecMap;
|
||||
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;
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
#include "quitstream.h"
|
||||
|
||||
#include "backend/computermanager.h"
|
||||
#include "backend/computerseeker.h"
|
||||
#include "streaming/session.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QTimer>
|
||||
|
||||
#define COMPUTER_SEEK_TIMEOUT 10000
|
||||
|
||||
namespace CliQuitStream
|
||||
{
|
||||
|
||||
enum State {
|
||||
StateInit,
|
||||
StateSeekComputer,
|
||||
StateQuitApp,
|
||||
StateFailure,
|
||||
};
|
||||
|
||||
class Event
|
||||
{
|
||||
public:
|
||||
enum Type {
|
||||
AppQuitCompleted,
|
||||
ComputerFound,
|
||||
ComputerSeekTimedout,
|
||||
Executed,
|
||||
};
|
||||
|
||||
Event(Type type)
|
||||
: type(type), computerManager(nullptr), computer(nullptr) {}
|
||||
|
||||
Type type;
|
||||
ComputerManager *computerManager;
|
||||
NvComputer *computer;
|
||||
QString errorMessage;
|
||||
};
|
||||
|
||||
class LauncherPrivate
|
||||
{
|
||||
Q_DECLARE_PUBLIC(Launcher)
|
||||
|
||||
public:
|
||||
LauncherPrivate(Launcher *q) : q_ptr(q) {}
|
||||
|
||||
void handleEvent(Event event)
|
||||
{
|
||||
Q_Q(Launcher);
|
||||
NvApp app;
|
||||
|
||||
switch (event.type) {
|
||||
// Occurs when CliQuitStreamSegue becomes visible and the UI calls launcher's execute()
|
||||
case Event::Executed:
|
||||
if (m_State == StateInit) {
|
||||
m_State = StateSeekComputer;
|
||||
m_ComputerManager = event.computerManager;
|
||||
q->connect(m_ComputerManager, &ComputerManager::quitAppCompleted,
|
||||
q, &Launcher::onQuitAppCompleted);
|
||||
|
||||
m_ComputerSeeker = new ComputerSeeker(m_ComputerManager, m_ComputerName, q);
|
||||
q->connect(m_ComputerSeeker, &ComputerSeeker::computerFound,
|
||||
q, &Launcher::onComputerFound);
|
||||
q->connect(m_ComputerSeeker, &ComputerSeeker::errorTimeout,
|
||||
q, &Launcher::onComputerSeekTimeout);
|
||||
m_ComputerSeeker->start(COMPUTER_SEEK_TIMEOUT);
|
||||
|
||||
emit q->searchingComputer();
|
||||
}
|
||||
break;
|
||||
// Occurs when computer search timed out
|
||||
case Event::ComputerSeekTimedout:
|
||||
if (m_State == StateSeekComputer) {
|
||||
m_State = StateFailure;
|
||||
emit q->failed(QObject::tr("Failed to connect to %1").arg(m_ComputerName));
|
||||
}
|
||||
break;
|
||||
// Occurs when searched computer is found
|
||||
case Event::ComputerFound:
|
||||
if (m_State == StateSeekComputer) {
|
||||
if (event.computer->pairState == NvComputer::PS_PAIRED) {
|
||||
m_State = StateQuitApp;
|
||||
emit q->quittingApp();
|
||||
m_ComputerManager->quitRunningApp(event.computer);
|
||||
} else {
|
||||
m_State = StateFailure;
|
||||
QString msg = QObject::tr("Computer %1 has not been paired. "
|
||||
"Please open Moonlight to pair before streaming.")
|
||||
.arg(event.computer->name);
|
||||
emit q->failed(msg);
|
||||
}
|
||||
}
|
||||
break;
|
||||
// Occurs when app quit completed (error message is set if failed)
|
||||
case Event::AppQuitCompleted:
|
||||
if (m_State == StateQuitApp) {
|
||||
if (event.errorMessage.isEmpty()) {
|
||||
QCoreApplication::exit(0);
|
||||
} else {
|
||||
m_State = StateFailure;
|
||||
emit q->failed(QObject::tr("Quitting app failed, reason: %1").arg(event.errorMessage));
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Launcher *q_ptr;
|
||||
ComputerManager *m_ComputerManager;
|
||||
QString m_ComputerName;
|
||||
ComputerSeeker *m_ComputerSeeker;
|
||||
State m_State;
|
||||
QTimer *m_TimeoutTimer;
|
||||
};
|
||||
|
||||
Launcher::Launcher(QString computer, QObject *parent)
|
||||
: QObject(parent),
|
||||
m_DPtr(new LauncherPrivate(this))
|
||||
{
|
||||
Q_D(Launcher);
|
||||
d->m_ComputerName = computer;
|
||||
d->m_State = StateInit;
|
||||
d->m_TimeoutTimer = new QTimer(this);
|
||||
d->m_TimeoutTimer->setSingleShot(true);
|
||||
connect(d->m_TimeoutTimer, &QTimer::timeout,
|
||||
this, &Launcher::onComputerSeekTimeout);
|
||||
}
|
||||
|
||||
Launcher::~Launcher()
|
||||
{
|
||||
}
|
||||
|
||||
void Launcher::execute(ComputerManager *manager)
|
||||
{
|
||||
Q_D(Launcher);
|
||||
Event event(Event::Executed);
|
||||
event.computerManager = manager;
|
||||
d->handleEvent(event);
|
||||
}
|
||||
|
||||
bool Launcher::isExecuted() const
|
||||
{
|
||||
Q_D(const Launcher);
|
||||
return d->m_State != StateInit;
|
||||
}
|
||||
|
||||
void Launcher::onComputerFound(NvComputer *computer)
|
||||
{
|
||||
Q_D(Launcher);
|
||||
Event event(Event::ComputerFound);
|
||||
event.computer = computer;
|
||||
d->handleEvent(event);
|
||||
}
|
||||
|
||||
void Launcher::onComputerSeekTimeout()
|
||||
{
|
||||
Q_D(Launcher);
|
||||
Event event(Event::ComputerSeekTimedout);
|
||||
d->handleEvent(event);
|
||||
}
|
||||
|
||||
void Launcher::onQuitAppCompleted(QVariant error)
|
||||
{
|
||||
Q_D(Launcher);
|
||||
Event event(Event::AppQuitCompleted);
|
||||
event.errorMessage = error.toString();
|
||||
d->handleEvent(event);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QVariant>
|
||||
|
||||
class ComputerManager;
|
||||
class NvComputer;
|
||||
|
||||
namespace CliQuitStream
|
||||
{
|
||||
|
||||
class LauncherPrivate;
|
||||
|
||||
class Launcher : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_DECLARE_PRIVATE_D(m_DPtr, Launcher)
|
||||
|
||||
public:
|
||||
explicit Launcher(QString computer, QObject *parent = nullptr);
|
||||
~Launcher();
|
||||
|
||||
Q_INVOKABLE void execute(ComputerManager *manager);
|
||||
Q_INVOKABLE bool isExecuted() const;
|
||||
|
||||
signals:
|
||||
void searchingComputer();
|
||||
void quittingApp();
|
||||
void failed(QString text);
|
||||
|
||||
private slots:
|
||||
void onComputerFound(NvComputer *computer);
|
||||
void onComputerSeekTimeout();
|
||||
void onQuitAppCompleted(QVariant error);
|
||||
|
||||
private:
|
||||
QScopedPointer<LauncherPrivate> m_DPtr;
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
#include "startstream.h"
|
||||
#include "backend/computermanager.h"
|
||||
#include "backend/computerseeker.h"
|
||||
#include "streaming/session.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QTimer>
|
||||
|
||||
#define COMPUTER_SEEK_TIMEOUT 10000
|
||||
#define APP_SEEK_TIMEOUT 10000
|
||||
|
||||
namespace CliStartStream
|
||||
{
|
||||
|
||||
enum State {
|
||||
StateInit,
|
||||
StateSeekComputer,
|
||||
StateSeekApp,
|
||||
StateStartSession,
|
||||
StateFailure,
|
||||
};
|
||||
|
||||
class Event
|
||||
{
|
||||
public:
|
||||
enum Type {
|
||||
AppQuitCompleted,
|
||||
AppQuitRequested,
|
||||
ComputerFound,
|
||||
ComputerUpdated,
|
||||
Executed,
|
||||
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);
|
||||
Session* session;
|
||||
NvApp app;
|
||||
|
||||
switch (event.type) {
|
||||
// Occurs when CliStartStreamSegue becomes visible and the UI calls launcher's 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::onTimeout);
|
||||
m_ComputerSeeker->start(COMPUTER_SEEK_TIMEOUT);
|
||||
|
||||
q->connect(m_ComputerManager, &ComputerManager::computerStateChanged,
|
||||
q, &Launcher::onComputerUpdated);
|
||||
q->connect(m_ComputerManager, &ComputerManager::quitAppCompleted,
|
||||
q, &Launcher::onQuitAppCompleted);
|
||||
|
||||
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 = StateSeekApp;
|
||||
m_Computer = event.computer;
|
||||
m_TimeoutTimer->start(APP_SEEK_TIMEOUT);
|
||||
emit q->searchingApp();
|
||||
} else {
|
||||
m_State = StateFailure;
|
||||
QString msg = QObject::tr("Computer %1 has not been paired. "
|
||||
"Please open Moonlight to pair before streaming.")
|
||||
.arg(event.computer->name);
|
||||
emit q->failed(msg);
|
||||
}
|
||||
}
|
||||
break;
|
||||
// Occurs when a computer is updated
|
||||
case Event::ComputerUpdated:
|
||||
if (m_State == StateSeekApp) {
|
||||
int index = getAppIndex();
|
||||
if (-1 != index) {
|
||||
app = m_Computer->appList[index];
|
||||
m_TimeoutTimer->stop();
|
||||
if (isNotStreaming() || isStreamingApp(app)) {
|
||||
m_State = StateStartSession;
|
||||
session = new Session(m_Computer, app, m_Preferences);
|
||||
emit q->sessionCreated(app.name, session);
|
||||
} else {
|
||||
emit q->appQuitRequired(getCurrentAppName());
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
// Occurs when there was another app running on computer and user accepted quit
|
||||
// confirmation dialog
|
||||
case Event::AppQuitRequested:
|
||||
if (m_State == StateSeekApp) {
|
||||
m_ComputerManager->quitRunningApp(m_Computer);
|
||||
}
|
||||
break;
|
||||
// Occurs when the previous app quit has been completed, handles quitting errors if any
|
||||
// happened. ComputerUpdated event's handler handles session start when previous app has
|
||||
// quit.
|
||||
case Event::AppQuitCompleted:
|
||||
if (m_State == StateSeekApp && !event.errorMessage.isEmpty()) {
|
||||
m_State = StateFailure;
|
||||
emit q->failed(QObject::tr("Quitting app failed, reason: %1").arg(event.errorMessage));
|
||||
}
|
||||
break;
|
||||
// Occurs when computer or app 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));
|
||||
}
|
||||
if (m_State == StateSeekApp) {
|
||||
m_State = StateFailure;
|
||||
emit q->failed(QObject::tr("Failed to find application %1").arg(m_AppName));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
int getAppIndex() const
|
||||
{
|
||||
for (int i = 0; i < m_Computer->appList.length(); i++) {
|
||||
if (m_Computer->appList[i].name.toLower() == m_AppName.toLower()) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool isNotStreaming() const
|
||||
{
|
||||
return m_Computer->currentGameId == 0;
|
||||
}
|
||||
|
||||
bool isStreamingApp(NvApp app) const
|
||||
{
|
||||
return m_Computer->currentGameId == app.id;
|
||||
}
|
||||
|
||||
QString getCurrentAppName() const
|
||||
{
|
||||
for (const NvApp& app : m_Computer->appList) {
|
||||
if (m_Computer->currentGameId == app.id) {
|
||||
return app.name;
|
||||
}
|
||||
}
|
||||
return "<UNKNOWN>";
|
||||
}
|
||||
|
||||
Launcher *q_ptr;
|
||||
QString m_ComputerName;
|
||||
QString m_AppName;
|
||||
StreamingPreferences *m_Preferences;
|
||||
ComputerManager *m_ComputerManager;
|
||||
ComputerSeeker *m_ComputerSeeker;
|
||||
NvComputer *m_Computer;
|
||||
State m_State;
|
||||
QTimer *m_TimeoutTimer;
|
||||
};
|
||||
|
||||
Launcher::Launcher(QString computer, QString app,
|
||||
StreamingPreferences* preferences, QObject *parent)
|
||||
: QObject(parent),
|
||||
m_DPtr(new LauncherPrivate(this))
|
||||
{
|
||||
Q_D(Launcher);
|
||||
d->m_ComputerName = computer;
|
||||
d->m_AppName = app;
|
||||
d->m_Preferences = preferences;
|
||||
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);
|
||||
}
|
||||
|
||||
void Launcher::quitRunningApp()
|
||||
{
|
||||
Q_D(Launcher);
|
||||
Event event(Event::AppQuitRequested);
|
||||
d->handleEvent(event);
|
||||
}
|
||||
|
||||
bool Launcher::isExecuted() const
|
||||
{
|
||||
Q_D(const Launcher);
|
||||
return d->m_State != StateInit;
|
||||
}
|
||||
|
||||
void Launcher::onComputerFound(NvComputer *computer)
|
||||
{
|
||||
Q_D(Launcher);
|
||||
Event event(Event::ComputerFound);
|
||||
event.computer = computer;
|
||||
d->handleEvent(event);
|
||||
}
|
||||
|
||||
void Launcher::onComputerUpdated(NvComputer *computer)
|
||||
{
|
||||
Q_D(Launcher);
|
||||
Event event(Event::ComputerUpdated);
|
||||
event.computer = computer;
|
||||
d->handleEvent(event);
|
||||
}
|
||||
|
||||
void Launcher::onTimeout()
|
||||
{
|
||||
Q_D(Launcher);
|
||||
Event event(Event::Timedout);
|
||||
d->handleEvent(event);
|
||||
}
|
||||
|
||||
void Launcher::onQuitAppCompleted(QVariant error)
|
||||
{
|
||||
Q_D(Launcher);
|
||||
Event event(Event::AppQuitCompleted);
|
||||
event.errorMessage = error.toString();
|
||||
d->handleEvent(event);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QVariant>
|
||||
|
||||
class ComputerManager;
|
||||
class NvComputer;
|
||||
class Session;
|
||||
class StreamingPreferences;
|
||||
|
||||
namespace CliStartStream
|
||||
{
|
||||
|
||||
class Event;
|
||||
class LauncherPrivate;
|
||||
|
||||
class Launcher : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_DECLARE_PRIVATE_D(m_DPtr, Launcher)
|
||||
|
||||
public:
|
||||
explicit Launcher(QString computer, QString app,
|
||||
StreamingPreferences* preferences,
|
||||
QObject *parent = nullptr);
|
||||
~Launcher();
|
||||
Q_INVOKABLE void execute(ComputerManager *manager);
|
||||
Q_INVOKABLE void quitRunningApp();
|
||||
Q_INVOKABLE bool isExecuted() const;
|
||||
|
||||
signals:
|
||||
void searchingComputer();
|
||||
void searchingApp();
|
||||
void sessionCreated(QString appName, Session *session);
|
||||
void failed(QString text);
|
||||
void appQuitRequired(QString appName);
|
||||
|
||||
private slots:
|
||||
void onComputerFound(NvComputer *computer);
|
||||
void onComputerUpdated(NvComputer *computer);
|
||||
void onTimeout();
|
||||
void onQuitAppCompleted(QVariant error);
|
||||
|
||||
private:
|
||||
QScopedPointer<LauncherPrivate> m_DPtr;
|
||||
};
|
||||
|
||||
}
|
||||
@@ -4,35 +4,700 @@
|
||||
<metadata_license>CC0-1.0</metadata_license>
|
||||
<project_license>GPL-3.0+</project_license>
|
||||
<name>Moonlight</name>
|
||||
<summary>Stream games 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 600-series or higher GPU and GeForce Experience installed.</p>
|
||||
<p>Moonlight can stream games and other applications from another PC with Sunshine or GeForce Experience software installed.</p>
|
||||
<p>Features include:</p>
|
||||
<ul>
|
||||
<li>Streaming at up to 4K resolution</li>
|
||||
<li>Support for up to 120 FPS streaming on high refresh rate monitors</li>
|
||||
<li>Hardware accelerated video decoding with VAAPI and VDPAU support</li>
|
||||
<li>5.1 surround sound audio support</li>
|
||||
<li>HEVC support for better video compression efficiency</li>
|
||||
<li>Gamepad support with SDL mapping compatibility</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>
|
||||
</ul>
|
||||
</description>
|
||||
|
||||
<url type="homepage">https://moonlight-stream.com</url>
|
||||
<url type="homepage">https://moonlight-stream.org</url>
|
||||
<url type="bugtracker">https://github.com/moonlight-stream/moonlight-qt/issues</url>
|
||||
<url type="help">https://github.com/moonlight-stream/moonlight-docs/wiki/Setup-Guide</url>
|
||||
|
||||
<screenshots>
|
||||
<screenshot type="default">
|
||||
<image>https://moonlight-stream.com/appstream_resources/moonlight_screenshot_1.png</image>
|
||||
<image>https://moonlight-stream.org/appstream_resources/moonlight_screenshot_1.png</image>
|
||||
</screenshot>
|
||||
<screenshot>
|
||||
<image>https://moonlight-stream.com/appstream_resources/moonlight_screenshot_2.png</image>
|
||||
<image>https://moonlight-stream.org/appstream_resources/moonlight_screenshot_2.png</image>
|
||||
</screenshot>
|
||||
</screenshots>
|
||||
|
||||
<releases>
|
||||
<release version="5.0.0" date="2023-10-18">
|
||||
<description>
|
||||
<p>New features:</p>
|
||||
<ul>
|
||||
<li>Custom FPS values</li>
|
||||
<li>Streaming using the AV1 codec from Sunshine hosts</li>
|
||||
<li>Horizontal scrolling with Sunshine hosts</li>
|
||||
<li>Native multi-touch input with Sunshine hosts</li>
|
||||
<li>Controller type information is passed to Sunshine for controller type autoselection</li>
|
||||
<li>Extended features of PS4/PS5 controllers (touchpad, battery state, gyro/accelerometer motion, and RGB LED) are natively passed to Sunshine hosts</li>
|
||||
<li>Support for up to 16 gamepads with Sunshine hosts</li>
|
||||
<li>Frame pacing support on Wayland</li>
|
||||
<li>Host-side processing latency is now displayed in the performance overlay with Sunshine hosts</li>
|
||||
<li>Window title now includes the PC name when streaming</li>
|
||||
<li>Alphabetical sorting of the PC list</li>
|
||||
<li>Updated Play and Stop button design to improve contrast with app box art</li>
|
||||
<li>The settings page automatically scrolls to the selected element when using keyboard or gamepad navigation</li>
|
||||
<li>Pressing Share+PS buttons on PS4/PS5 controllers can be used to emulate a clickpad press</li>
|
||||
<li>Improved audio quality when streaming with a high bitrate from remote hosts</li>
|
||||
<li>Increased mouse polling rate</li>
|
||||
</ul>
|
||||
<p>Behavior Changes:</p>
|
||||
<ul>
|
||||
<li>IPv6 addresses must be provided in URL-escaped form. Example: [2001:db8::1234:5678] or [2001:db8::1234:5678]:47989</li>
|
||||
<li>Default bitrate logic has been adjusted to behave better with custom resolutions and frame rates</li>
|
||||
</ul>
|
||||
<p>Bugfixes:</p>
|
||||
<ul>
|
||||
<li>Reduced the frequency of audio underruns (clicks and pops) by increasing audio buffer size</li>
|
||||
<li>Fixed "Quitting App" message never dismissing in some cases</li>
|
||||
<li>Fixed parsing IPv6 literals with port numbers</li>
|
||||
<li>Fixed unnecessary WiFi activity causing stream disruptions</li>
|
||||
<li>Fixed paired hosts being forgotten if Moonlight terminates while writing hosts to disk</li>
|
||||
<li>Fixed freeze when closing the stream on Windows with some software that uses DX11 hooks</li>
|
||||
<li>Fixed delayed PC wake when activating Wake-On-LAN feature</li>
|
||||
<li>Fixed Wake-on-LAN when Sunshine is running on alternate ports</li>
|
||||
<li>Fixed leak of /dev/dri fds when trying to initialize VAAPI decoder</li>
|
||||
<li>Fixed support for the Applications/Menu key</li>
|
||||
<li>Fixed automatic language detection for Simplified Chinese and Traditional Chinese</li>
|
||||
<li>Fixed streaming from Sunshine when the client PC has multiple network interfaces or IPv6 privacy extensions enabled</li>
|
||||
<li>Fixed small receive buffer causing video packet loss on some systems</li>
|
||||
<li>Fixed theme overrides causing the UI to be unusable on some systems</li>
|
||||
<li>Fixed tearing with V-Sync enabled on macOS and Linux when using the SDL renderer (software decoding and some hardware decoders)</li>
|
||||
<li>Fixed stateful V4L2 hardware decoding using h264_v4l2m2m and hevc_v4l2m2m decoders</li>
|
||||
<li>Fixed CVE-2023-42801</li>
|
||||
<li>Updated included gamepad mappings</li>
|
||||
<li>Updated community-contributed translations from Weblate</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="4.3.1" date="2022-11-03">
|
||||
<description>
|
||||
<p>Changes:</p>
|
||||
<ul>
|
||||
<li>Fixed crash during specific patterns of audio packet loss</li>
|
||||
<li>Fixed detection of local streaming when using IPv6 addresses</li>
|
||||
<li>Windowed mode streaming now starts maximized if the GUI window was maximized</li>
|
||||
<li>Updated community-contributed translations from Weblate</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="4.3.0" date="2022-10-28">
|
||||
<description>
|
||||
<p>New features:</p>
|
||||
<ul>
|
||||
<li>Improved support for GeForce Experience 3.26</li>
|
||||
<li>Improved image quality and performance when recovering from video packet loss using HEVC</li>
|
||||
<li>Improved error concealment after audio packet loss</li>
|
||||
<li>Improved UI performance when paired with many hosts or hosts with large numbers of apps</li>
|
||||
<li>Pairing with Sunshine hosts is now supported even if the host is in a streaming session</li>
|
||||
</ul>
|
||||
<p>Bugfixes:</p>
|
||||
<ul>
|
||||
<li>Fixed streaming at resolutions below 720x480 with GeForce Experience 3.26</li>
|
||||
<li>Fixed window position and size being lost when toggling between full-screen and windowed mode</li>
|
||||
<li>Fixed initial window size not always respecting the stream aspect ratio</li>
|
||||
<li>Fixed unexpected error when streaming at resolutions above 4096x4096 using Sunshine</li>
|
||||
<li>Updated included gamepad mappings</li>
|
||||
<li>Updated community-contributed translations from Weblate</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="4.2.1" date="2022-09-12">
|
||||
<description>
|
||||
<p>Bugfixes:</p>
|
||||
<ul>
|
||||
<li>Fixed streaming from hosts running Sunshine</li>
|
||||
<li>Updated community-contributed translations from Weblate</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="4.2.0" date="2022-09-10">
|
||||
<description>
|
||||
<p>New features:</p>
|
||||
<ul>
|
||||
<li>Added support for pairing via the terminal</li>
|
||||
<li>Added support for listing installed apps on a host via the terminal</li>
|
||||
<li>Improved handling of V-Sync on Wayland systems to reduce latency</li>
|
||||
<li>Improved performance using the legacy non-EGL VAAPI rendering code</li>
|
||||
</ul>
|
||||
<p>Bugfixes:</p>
|
||||
<ul>
|
||||
<li>Fixed native resolution detection on high DPI Wayland systems</li>
|
||||
<li>Fixed audio crackling with SDL 2.24.0 on systems using PulseAudio</li>
|
||||
<li>Updated included gamepad mappings</li>
|
||||
<li>Updated community-contributed translations from Weblate</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="4.1.0" date="2022-05-23">
|
||||
<description>
|
||||
<p>New features:</p>
|
||||
<ul>
|
||||
<li>Overlays are now supported on the non-EGL X11 VAAPI renderer (requires RGBA subpicture support in the VAAPI driver)</li>
|
||||
<li>Cursor lock can now be disabled when in full-screen mode</li>
|
||||
</ul>
|
||||
<p>Behavior Changes:</p>
|
||||
<ul>
|
||||
<li>Moonlight can now reduce the client display resolution in full-screen mode if the current display mode doesn't have a high enough refresh rate for the selected stream frame rate</li>
|
||||
<li>When choosing a new client display resolution in full-screen mode, Moonlight will pick a display mode with the closest aspect ratio to the selected stream resolution</li>
|
||||
<li>Moonlight now defaults to borderless windowed mode on Wayland systems</li>
|
||||
|
||||
</ul>
|
||||
<p>Bugfixes:</p>
|
||||
<ul>
|
||||
<li>Fixed missing toolbar buttons in certain languages</li>
|
||||
<li>Fixed some text on the settings page not updating when the language is changed</li>
|
||||
<li>Fixed a frame pacing bug that could cause excessive frame drops</li>
|
||||
<li>Fixed the "keep display awake" setting not being saved when Moonlight restarts</li>
|
||||
<li>Fixed black screen after unmaximizing the Moonlight window on some Linux systems</li>
|
||||
<li>Updated included gamepad mappings</li>
|
||||
<li>Updated community-contributed translations from Weblate</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="4.0.0" date="2022-04-02">
|
||||
<description>
|
||||
<p>New features:</p>
|
||||
<ul>
|
||||
<li>Added a new key combo for locking the mouse cursor to the window (Ctrl+Alt+Shift+L)</li>
|
||||
<li>Added an option to avoid keeping the client display awake while streaming</li>
|
||||
</ul>
|
||||
<p>Behavior Changes:</p>
|
||||
<ul>
|
||||
<li>HDR streaming is now a separate option rather than being part of the Video Codec override dropdown</li>
|
||||
<li>HDR streaming is no longer restricted to apps that are certified by NVIDIA as compatible with GameStream HDR. Some newer games may require an HDR display or EDID emulator dongle connected to your host PC for HDR options to be available.</li>
|
||||
</ul>
|
||||
<p>Bugfixes:</p>
|
||||
<ul>
|
||||
<li>Fixed loss of all audio if the active audio device is unplugged while streaming</li>
|
||||
<li>Fixed the GUI being unresponsive after manually quitting when connection to the host is lost</li>
|
||||
<li>Fixed a rare crash if a controller disconnects right as rumble is starting</li>
|
||||
<li>Fixed text rendering on the overlay UI when using SDL_ttf 2.0.18 on Linux</li>
|
||||
<li>Updated included gamepad mappings</li>
|
||||
<li>Updated community-contributed translations from Weblate</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="3.2.0" date="2022-01-18">
|
||||
<description>
|
||||
<p>New features:</p>
|
||||
<ul>
|
||||
<li>Reduced CPU usage while streaming</li>
|
||||
<li>Non-ASCII characters are now supported when pasting clipboard data to the host</li>
|
||||
<li>Added support for zero-copy NVDEC rendering for Nvidia Wayland systems</li>
|
||||
<li>Significantly improved performance with asynchronous decoders like V4L2M2M on Linux</li>
|
||||
<li>Added support for color compression planes on newer Intel systems when using VAAPI decoding</li>
|
||||
<li>Added support for copy-back rendering in non-default pixel formats (like the Raspberry Pi SAND format)</li>
|
||||
</ul>
|
||||
<p>Bugfixes:</p>
|
||||
<ul>
|
||||
<li>Fixed unexpected GUI input after using the gamepad quit combo</li>
|
||||
<li>Updated included gamepad mappings</li>
|
||||
<li>Updated community-contributed translations from Weblate</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="3.1.4" date="2021-06-25">
|
||||
<description>
|
||||
<p>Changes:</p>
|
||||
<ul>
|
||||
<li>Added network latency statistics to the performance overlay</li>
|
||||
<li>Improved audio quality on unreliable connections (requires GeForce Experience 3.19 or later)</li>
|
||||
<li>Reduced video frame drops on unreliable connections (requires GeForce Experience 3.22 or later)</li>
|
||||
<li>Updated community-contributed translations from Weblate</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="3.1.3" date="2021-05-07">
|
||||
<description>
|
||||
<p>Changes:</p>
|
||||
<ul>
|
||||
<li>Fixed an issue that could cause excessive audio latency on some connections</li>
|
||||
<li>Updated community-contributed translations from Weblate</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="3.1.2" date="2021-05-03">
|
||||
<description>
|
||||
<p>Changes:</p>
|
||||
<ul>
|
||||
<li>Improved connection reliability and performance with GeForce Experience 3.22</li>
|
||||
<li>Added support for audio encryption with GeForce Experience 3.22</li>
|
||||
<li>Added compatibility update checks for future versions of GeForce Experience</li>
|
||||
<li>Fixed keyboard capture on X11</li>
|
||||
<li>Updated community-contributed translations from Weblate</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="3.1.1" date="2021-04-09">
|
||||
<description>
|
||||
<p>Changes:</p>
|
||||
<ul>
|
||||
<li>Added support for streaming from GeForce Experience 3.22</li>
|
||||
<li>Added support for AMF-encoded HEVC streams from servers like Sunshine and OpenStream</li>
|
||||
<li>Added new community-contributed translations from Weblate</li>
|
||||
<li>Fixed KMSDRM rendering with buffer modifiers on embedded devices</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="3.1.0" date="2021-03-14">
|
||||
<description>
|
||||
<p>New features:</p>
|
||||
<ul>
|
||||
<li>System keyboard shortcuts (Alt+Tab, Super key, Ctrl+Shift+Esc, etc.) can now be (optionally) captured in windowed mode</li>
|
||||
<li>System keyboard shortcuts can now be captured under Wayland</li>
|
||||
<li>Added support for typing the client's clipboard text on the host by pressing Ctrl+Alt+Shift+V</li>
|
||||
<li>Added Ctrl+Alt+Shift+D key combo to minimize the streaming window</li>
|
||||
<li>Added Ctrl+Alt+Shift+C key combo to show the local cursor while streaming in remote desktop mouse mode</li>
|
||||
<li>Added Select+L1+R1+X gamepad button combo to toggle performance overlay</li>
|
||||
<li>Added translations for Simplified Chinese and German</li>
|
||||
<li>Added a language selector on the settings page</li>
|
||||
</ul>
|
||||
<p>Bugfixes:</p>
|
||||
<ul>
|
||||
<li>Improved performance of loading the app grid</li>
|
||||
<li>Fixed video scaling in High DPI mode on Wayland</li>
|
||||
<li>Fixed window icons on Wayland</li>
|
||||
<li>Fixed number pad enter key functionality in the UI</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="3.0.0" date="2021-01-17">
|
||||
<description>
|
||||
<p>New features:</p>
|
||||
<ul>
|
||||
<li>Added the ability to define a custom streaming resolution using the UI</li>
|
||||
<li>Added the option to capture system key combinations (Alt+Tab, Ctrl+Shift+Esc, Win+Tab, etc) in fullscreen mode</li>
|
||||
<li>Updated gamepad mappings are automatically downloaded each time Moonlight launches</li>
|
||||
<li>The performance overlay is now supported for Linux clients using VAAPI and VDPAU hardware acceleration</li>
|
||||
<li>Added the option to enable "Direct Launch" which immediately launches the specified game when clicking on a PC</li>
|
||||
<li>Added the option to mute audio when the Moonlight window loses input focus</li>
|
||||
<li>Added the option to toggle background gamepad input</li>
|
||||
<li>Added the option to invert scroll direction</li>
|
||||
<li>Added the option to swap gamepad button layout to Nintendo-style</li>
|
||||
<li>Added the option to start the Moonlight UI in fullscreen mode</li>
|
||||
<li>The failing TCP or UDP ports are now displayed when the connection fails</li>
|
||||
</ul>
|
||||
<p>Bugfixes:</p>
|
||||
<ul>
|
||||
<li>Fixed PS4 controllers not reading inputs during haptics in some games</li>
|
||||
<li>Fixed some slow decoders causing the receive time value to increase rather than the decode time</li>
|
||||
<li>Native resolution options now refresh each time the settings page is opened, rather than once per launch of Moonlight</li>
|
||||
<li>Fixed no video or audio while streaming over Internet connections that drop packets with QoS tagging enabled</li>
|
||||
<li>Improved connection testing accuracy for connections with low MTUs</li>
|
||||
<li>Fixed inability to open an app's context menu with a gamepad</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="2.2.0" date="2020-09-10">
|
||||
<description>
|
||||
<p>New features:</p>
|
||||
<ul>
|
||||
<li>Added the ability to hide unwanted apps and games</li>
|
||||
<li>Added a network test to determine if your Internet connection is blocking Moonlight</li>
|
||||
<li>Added the option to swap mouse buttons</li>
|
||||
<li>Added codec and resolution to the on-screen performance stats</li>
|
||||
<li>Added a warning if the connection is too unstable to receive any complete video frames</li>
|
||||
<li>Added a Discord link to the toolbar in settings</li>
|
||||
<li>Enabled the new EGL renderer by default on Linux</li>
|
||||
</ul>
|
||||
<p>Bugfixes:</p>
|
||||
<ul>
|
||||
<li>Fixed streaming over networks that block QoS-tagged packets</li>
|
||||
<li>Selecting a frame rate higher than 60 FPS no longer sets the host desktop to 720p</li>
|
||||
<li>Right-side modifier keys no longer press the corresponding left-side keys too</li>
|
||||
<li>Fixed mouse position never reaching the edge of the screen with certain desktop resolutions</li>
|
||||
<li>Improved synchronization between host and client mouse cursor in remote desktop mouse mode</li>
|
||||
<li>Cached box art is now deleted when the PC is removed from Moonlight</li>
|
||||
<li>Fixed several CLI options not properly overriding GUI options</li>
|
||||
<li>Updated included gamepad mappings</li>
|
||||
<li>Updated to Qt 5.15.1</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="2.1.0" date="2020-05-17">
|
||||
<description>
|
||||
<p>New features:</p>
|
||||
<ul>
|
||||
<li>Remote desktop mode now allows interaction with the host without having to click to focus the Moonlight window first</li>
|
||||
<li>Added a new EGL renderer for improved performance on Wayland and XWayland</li>
|
||||
<li>Added the option to toggle between direct touchscreen mode and the old trackpad mode</li>
|
||||
<li>Added the option to rename host PCs</li>
|
||||
<li>Added the ability to scroll the settings page with the right analog stick</li>
|
||||
<li>Various UI improvements</li>
|
||||
</ul>
|
||||
<p>Bugfixes:</p>
|
||||
<ul>
|
||||
<li>Fixed unreliable mouse capture when the stream begins</li>
|
||||
<li>Fixed cursor being trapped in full-screen mode even when the mouse wasn't captured</li>
|
||||
<li>Fixed key presses not being sent to the host if the Super/Meta/Windows key is pressed</li>
|
||||
<li>Updated included gamepad mappings</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="2.0.0" date="2020-04-28">
|
||||
<description>
|
||||
<p>New features:</p>
|
||||
<ul>
|
||||
<li>Remote desktop-optimized mouse mode which doesn't require capturing the mouse cursor</li>
|
||||
<li>Touchscreen input now directly controls the mouse cursor</li>
|
||||
<li>7.1 surround sound support</li>
|
||||
</ul>
|
||||
<p>Bugfixes:</p>
|
||||
<ul>
|
||||
<li>Improved performance on some lossy networks</li>
|
||||
<li>Fixed disconnect delay when streaming from GeForce Experience 3.20.3.63</li>
|
||||
<li>Fixed frame rate not exceeding 60 when streaming from GeForce Experience 3.20.3.63</li>
|
||||
<li>Fixed parsing of command-line options when passing flags prior to the action</li>
|
||||
<li>Fixed an issue that may cause excessive mDNS network traffic while Moonlight is running</li>
|
||||
<li>Fixed hardware decoding on Intel GPUs running Mesa 20.0 and/or the Iris driver</li>
|
||||
<li>Fixed hardware decoding on Comet Lake and later Intel GPUs using the iHD VA-API driver</li>
|
||||
<li>Fixed hardware decoding on AMD Radeon GPUs with XWayland</li>
|
||||
<li>Fixed a crash when using the iHD VA-API driver on Wayland</li>
|
||||
<li>Fixed delay after exiting the stream on Wayland</li>
|
||||
<li>Updated included gamepad mappings</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="1.2.0" date="2019-12-17">
|
||||
<description>
|
||||
<p>Bugfixes:</p>
|
||||
<ul>
|
||||
<li>Improved video encoding efficiency</li>
|
||||
<li>Improved performance when streaming over a VPN</li>
|
||||
<li>Fixed RTSP handshake error on certain networks</li>
|
||||
<li>Fixed rumble not working on certain gamepads</li>
|
||||
<li>Fixed pairing error when double-clicking on an unpaired PC</li>
|
||||
<li>Updated included SDL gamepad mappings</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="1.1.1" date="2019-09-29">
|
||||
<description>
|
||||
<p>Bugfixes:</p>
|
||||
<ul>
|
||||
<li>Improved reliability of public IP address detection</li>
|
||||
<li>Fixed connecting to a host PC with multiple network connections</li>
|
||||
<li>Fixed a hang after exiting a full-screen stream with GNOME on Wayland</li>
|
||||
<li>Fixed gamepad hotplugging</li>
|
||||
<li>Updated to SDL 2.0.10</li>
|
||||
<li>Updated to Qt 5.13.1</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="1.1.0" date="2019-08-05">
|
||||
<description>
|
||||
<p>New features:</p>
|
||||
<ul>
|
||||
<li>Added support for NVDEC for NVIDIA GPUs on Wayland</li>
|
||||
<li>Added an option to disable gamepad mouse mode for games that require holding the Start button</li>
|
||||
<li>Added automatic detection of IPv6 addresses for Internet streaming over IPv6 networks</li>
|
||||
</ul>
|
||||
<p>Bugfixes:</p>
|
||||
<ul>
|
||||
<li>Improved speed and reliability of automatic PC detection on the local network</li>
|
||||
<li>Fixed audio latency increasing under certain network conditions</li>
|
||||
<li>Fixed 5.1 surround sound sometimes causing no audio to play when streaming over the Internet</li>
|
||||
<li>Fixed graceful termination of games not being detected properly on GFE 3.19</li>
|
||||
<li>Fixed several audio issues on Linux by switching back to SDL for audio</li>
|
||||
<li>Changed client certificate signature algorithm from SHA-1 to SHA-256 for Debian Buster compatibility</li>
|
||||
<li>Updated included gamepad mappings</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="1.0.1" date="2019-05-27">
|
||||
<description>
|
||||
<p>Bugfixes:</p>
|
||||
<ul>
|
||||
<li>Fixed broken video on GeForce Experience 3.19</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="1.0.0" date="2019-05-21">
|
||||
<description>
|
||||
<p>New features:</p>
|
||||
<ul>
|
||||
<li>Mouse mode for gamepads (Press and hold Start to toggle)</li>
|
||||
<li>Improved gamepad UI navigation</li>
|
||||
</ul>
|
||||
<p>Bugfixes:</p>
|
||||
<ul>
|
||||
<li>Fixed duplicate non-working gamepads appearing on the host</li>
|
||||
<li>Fixed excessive frame queue delay on macOS</li>
|
||||
<li>Fixed excessive audio latency on Steam Link</li>
|
||||
<li>Fixed hiding mouse cursor after streaming on Steam Link</li>
|
||||
<li>Fixed incorrect receive time stats on 32-bit platforms</li>
|
||||
<li>Added UDP ports 48002 and 48010 for Wake-on-LAN</li>
|
||||
<li>Updated included gamepad mappings</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="0.10.1" date="2019-04-28">
|
||||
<description>
|
||||
<p>New features:</p>
|
||||
<ul>
|
||||
<li>Video performance optimizations</li>
|
||||
<li>VAAPI support on Wayland</li>
|
||||
</ul>
|
||||
<p>Bugfixes:</p>
|
||||
<ul>
|
||||
<li>Fixed a crash when attempting to dismiss the connection terminated dialog</li>
|
||||
<li>Fixed a crash when using the overlay on the software decoder</li>
|
||||
<li>Fixed software decoder not displaying output in some cases</li>
|
||||
<li>Fixed some minor UI issues on the PC grid</li>
|
||||
<li>Improved visibility of toolbar highlights</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="0.10.0" date="2019-04-07">
|
||||
<description>
|
||||
<p>New features:</p>
|
||||
<ul>
|
||||
<li>Redesigned App Grid with larger game artwork and less blank space</li>
|
||||
<li>Redesigned UI dialogs using Material Design</li>
|
||||
<li>Added a connection warning overlay (which can be disabled in settings)</li>
|
||||
<li>Streaming window no longer minimizes when it loses focus in full-screen mode</li>
|
||||
<li>The Steam Link device is now a supported platform</li>
|
||||
</ul>
|
||||
<p>Bugfixes:</p>
|
||||
<ul>
|
||||
<li>Fixed several bugs that could cause UI hangs or lag</li>
|
||||
<li>Fixed rumble support on gamepads with limited rumble motor control</li>
|
||||
<li>Fixed increased memory usage if the software renderer gets delayed</li>
|
||||
<li>Fixed crashes on certain desktop configurations when loading the UI</li>
|
||||
<li>Fixed handling of pairing certificates with a bogus date</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="0.9.1" date="2019-02-17">
|
||||
<description>
|
||||
<p>New features:</p>
|
||||
<ul>
|
||||
<li>Added gamepad rumble support</li>
|
||||
<li>Added a dedicated quit button to the running app tile</li>
|
||||
<li>Reduced video latency on Windows and Linux</li>
|
||||
<li>Added a performance overlay for software decoding</li>
|
||||
</ul>
|
||||
<p>Bugfixes:</p>
|
||||
<ul>
|
||||
<li>Fixed delayed first video frame with GFE 3.17</li>
|
||||
<li>Fixed erroneously setting host resolution to 720p in certain configurations</li>
|
||||
<li>The connection termination dialog is no longer displayed for intentional termination</li>
|
||||
<li>Fixed some gamepads only functioning for UI navigation and not in game</li>
|
||||
<li>Fixed audio latency increasing when switching audio devices</li>
|
||||
<li>Fixed VDPAU scaling behavior on AMD cards</li>
|
||||
<li>Fixed several bugs in gamepad navigation behavior</li>
|
||||
<li>Work around a bug in QGnomePlatform that breaks dialog rendering on Linux</li>
|
||||
<li>Updated included SDL gamepad mappings</li>
|
||||
<li>Fixed hotkey behavior on non-latin keyboards</li>
|
||||
<li>Fixed incorrect frame drop percentages in debug stats</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="0.8.1" date="2019-01-08">
|
||||
<description>
|
||||
<p>New features:</p>
|
||||
<ul>
|
||||
<li>Stricter adherence to the specified bitrate when streaming over the Internet</li>
|
||||
</ul>
|
||||
<p>Bugfixes:</p>
|
||||
<ul>
|
||||
<li>Fixed audio crackling that impacted certain systems</li>
|
||||
<li>Fixed Wake-on-LAN packets failing to send on some systems</li>
|
||||
<li>Reduced audio latency when resuming a session where audio is already playing</li>
|
||||
<li>Added option to enable the old SDL audio renderer by setting ML_AUDIO=SDL</li>
|
||||
<li>Updated to use the new Qt 5.12 Flatpak runtime</li>
|
||||
<li>Updated to FFmpeg 4.1</li>
|
||||
<li>Minor UI tweaks</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="0.8.0" date="2018-12-25">
|
||||
<description>
|
||||
<p>New features:</p>
|
||||
<ul>
|
||||
<li>Improved security of pairing - After this update, you must re-pair with your PC!</li>
|
||||
<li>Enabled quitting games started by other Moonlight clients also running this update</li>
|
||||
</ul>
|
||||
<p>Bugfixes:</p>
|
||||
<ul>
|
||||
<li>Fixed the video bitrate slider not working above certain values on GFE 3.16</li>
|
||||
<li>Fixed broken Bluetooth audio with PulseAudio</li>
|
||||
<li>Reduced audio latency</li>
|
||||
<li>Fixed some non-gamepad devices triggering the unmapped gamepad dialog box</li>
|
||||
<li>Minor UI tweaks</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="0.7.0" date="2018-12-05">
|
||||
<description>
|
||||
<p>New features:</p>
|
||||
<ul>
|
||||
<li>Added option to quit the app after stopping the stream</li>
|
||||
<li>Added CLI option to quit the running app</li>
|
||||
</ul>
|
||||
<p>Bugfixes:</p>
|
||||
<ul>
|
||||
<li>Switched to libsoundio backend to address audio glitches and reduce latency</li>
|
||||
<li>Fixed CLI version output</li>
|
||||
<li>Fixed streaming error messages not showing up when launching from CLI</li>
|
||||
<li>Updated included SDL gamepad mappings</li>
|
||||
<li>Various minor UI tweaks</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="0.6.4" date="2018-11-20">
|
||||
<description>
|
||||
<p>New features:</p>
|
||||
<ul>
|
||||
<li>Added an option to match client display refresh rate when FPS is unlocked</li>
|
||||
</ul>
|
||||
<p>Bugfixes:</p>
|
||||
<ul>
|
||||
<li>Fixed multiple gamepads not being detected on the host in some scenarios</li>
|
||||
<li>Fixed an incorrect warning when trying to stream at 4K</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="0.6.3" date="2018-11-16">
|
||||
<description>
|
||||
<p>New features:</p>
|
||||
<ul>
|
||||
<li>Added support for GeForce Experience 3.16</li>
|
||||
<li>Added an option to force Moonlight to start in windowed mode</li>
|
||||
</ul>
|
||||
<p>Bugfixes:</p>
|
||||
<ul>
|
||||
<li>Fixed scrolling not working on the settings page</li>
|
||||
<li>Skipped using proxy servers when attempting to stream</li>
|
||||
<li>Fixed a couple possible crashes</li>
|
||||
<li>Renamed the mouse acceleration option to be more clear</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="0.6.2" date="2018-10-28">
|
||||
<description>
|
||||
<p>New features:</p>
|
||||
<ul>
|
||||
<li>Added automatic IP address detection for Internet streaming</li>
|
||||
<li>Added a quit shortcut tip for gamepad users</li>
|
||||
</ul>
|
||||
<p>Bugfixes:</p>
|
||||
<ul>
|
||||
<li>Fixed server state polling not being stopped while streaming</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="0.6.1" date="2018-10-14">
|
||||
<description>
|
||||
<p>New features:</p>
|
||||
<ul>
|
||||
<li>Added support for quitting Moonlight via gamepad</li>
|
||||
<li>Added tooltips for games with very long names</li>
|
||||
</ul>
|
||||
<p>Bugfixes:</p>
|
||||
<ul>
|
||||
<li>Added a workaround for a memory leak in the VAAPI driver for AMD GPUs</li>
|
||||
<li>Fixed combo boxes on the settings page being too small for certain DPI scaling</li>
|
||||
<li>Reduced power usage when Moonlight is idling in the background for a while</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="0.6.0" date="2018-10-06">
|
||||
<description>
|
||||
<p>New features:</p>
|
||||
<ul>
|
||||
<li>Added keyboard and gamepad UI navigation</li>
|
||||
<li>Added support for mouse X1 and X2 buttons (back and forward)</li>
|
||||
<li>Added support for mouse movement with touchscreens</li>
|
||||
<li>Improved HEVC performance at high bitrates</li>
|
||||
<li>Added support for launching streaming sessions directly from a terminal or script</li>
|
||||
<li>Added quit shortcut for gamepads: Select+Start+L1+R1</li>
|
||||
</ul>
|
||||
<p>Bugfixes:</p>
|
||||
<ul>
|
||||
<li>Fixed not saving deletion of PCs</li>
|
||||
<li>Fixed keys being stuck down on the host PC after Moonlight loses focus or quits</li>
|
||||
<li>Fixed some warning dialogs being stuck behind the UI window</li>
|
||||
<li>Updated included gamepad mapping list</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="0.5.0" date="2018-09-15">
|
||||
<description>
|
||||
<p>New features:</p>
|
||||
<ul>
|
||||
<li>Added option to enable mouse acceleration for remote desktop usage</li>
|
||||
<li>Added option to disable automatic PC discovery</li>
|
||||
<li>Added gamepad mappings from gabomdq's SDL_GameControllerDB</li>
|
||||
<li>Added help links to error dialogs</li>
|
||||
<li>Added quit shortcut tip to stream loading page</li>
|
||||
<li>Restored the autodetect audio configuration option</li>
|
||||
</ul>
|
||||
<p>Bugfixes:</p>
|
||||
<ul>
|
||||
<li>Fixed quit app dialog starting the wrong app</li>
|
||||
<li>Fixed very high input lag on GFE 3.14.0 and earlier</li>
|
||||
<li>Fixed sending errant mouse click when capturing the mouse</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="0.4.0" date="2018-09-08">
|
||||
<description>
|
||||
<p>New features:</p>
|
||||
<ul>
|
||||
<li>Added option to enable (unsupported) 90/120 FPS streaming on 60 Hz monitors</li>
|
||||
<li>Added refresh rate matching for better frame pacing on high refresh rate monitors</li>
|
||||
<li>The streaming window now appears on the same monitor the UI was displayed on</li>
|
||||
<li>The streaming window is now centered on the target monitor</li>
|
||||
<li>Display a dialog when the connection is terminated by the remote PC</li>
|
||||
<li>Added option to select between full-screen exclusive and borderless windowed mode</li>
|
||||
</ul>
|
||||
<p>Bugfixes:</p>
|
||||
<ul>
|
||||
<li>Fixed streaming with no audio device</li>
|
||||
<li>Fixed mouse lag with some high polling rate mice</li>
|
||||
<li>Improved UI performance on the app grid page</li>
|
||||
<li>1440p and 4K 120 FPS performance is improved to avoid encoder bottlenecks</li>
|
||||
<li>Autodetect audio configuration option removed because it was broken</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="0.3.0" date="2018-08-21">
|
||||
<description>
|
||||
<p>Bugfixes:</p>
|
||||
<ul>
|
||||
<li>Reduced power consumption when the app is minimized</li>
|
||||
<li>Fixed malformed asset requests being sent when the first host in the grid is offline</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="0.2.0" date="2018-08-12">
|
||||
<description>
|
||||
<p>Bugfixes:</p>
|
||||
<ul>
|
||||
<li>Fixed functionality of '=', 'Numpad Dot', and 'Numpad Enter' keys</li>
|
||||
<li>Fixed uninitialized variable causing strange gamepad behavior</li>
|
||||
<li>Added a warning for incompatible configs and decoder failures</li>
|
||||
<li>Added a warning for Wayland performance issues</li>
|
||||
<li>Stopped capturing the mouse by default for windowed streams</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="0.1.0" date="2018-08-05">
|
||||
<description>
|
||||
<p>New features:</p>
|
||||
@@ -76,7 +741,7 @@
|
||||
</categories>
|
||||
|
||||
<developer_name>Moonlight Game Streaming Team</developer_name>
|
||||
<update_contact>cameron@moonlight-stream.com</update_contact>
|
||||
<update_contact>cameron@moonlight-stream.org</update_contact>
|
||||
|
||||
<launchable type="desktop-id">com.moonlight_stream.Moonlight.desktop</launchable>
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
@@ -0,0 +1,13 @@
|
||||
#!/bin/sh
|
||||
|
||||
# The default HOME is not persistent, so override
|
||||
# it to a path on the onboard flash. Otherwise our
|
||||
# pairing data will be lost each reboot.
|
||||
HOME=/usr/local/moonlight
|
||||
|
||||
# Renice PE_Single_CPU which seems to host A/V stuff
|
||||
renice -10 -p $(pidof PE_Single_CPU)
|
||||
|
||||
# Renice Moonlight itself to avoid preemption by background tasks
|
||||
# Write output to a logfile in /tmp
|
||||
exec nice -n -10 ./bin/moonlight > /tmp/moonlight.log
|
||||
@@ -0,0 +1,3 @@
|
||||
name=Moonlight
|
||||
icon=moonlight.png
|
||||
run=moonlight.sh
|
||||
+308
-118
@@ -1,182 +1,372 @@
|
||||
import QtQuick 2.9
|
||||
import QtQuick.Dialogs 1.3
|
||||
import QtQuick.Controls 2.2
|
||||
import QtQuick.Controls.Material 2.2
|
||||
|
||||
import AppModel 1.0
|
||||
|
||||
import ComputerManager 1.0
|
||||
import SdlGamepadKeyNavigation 1.0
|
||||
|
||||
GridView {
|
||||
CenteredGridView {
|
||||
property int computerIndex
|
||||
property AppModel appModel : createModel()
|
||||
property bool activated
|
||||
property bool showHiddenGames
|
||||
property bool showGames
|
||||
|
||||
id: appGrid
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: (parent.width % (cellWidth + anchors.rightMargin)) / 2
|
||||
anchors.topMargin: 20
|
||||
anchors.rightMargin: 5
|
||||
anchors.bottomMargin: 5
|
||||
cellWidth: 225; cellHeight: 350;
|
||||
focus: true
|
||||
activeFocusOnTab: true
|
||||
topMargin: 20
|
||||
bottomMargin: 5
|
||||
cellWidth: 230; cellHeight: 297;
|
||||
|
||||
// Cache delegates for 1000px in both directions to improve
|
||||
// scrolling and resizing performance
|
||||
cacheBuffer: 1000
|
||||
function computerLost()
|
||||
{
|
||||
// Go back to the PC view on PC loss
|
||||
stackView.pop()
|
||||
}
|
||||
|
||||
// The StackView will trigger a visibility change when
|
||||
// we're pushed onto it, causing our onVisibleChanged
|
||||
// routine to run, but only if we start as invisible
|
||||
visible: false
|
||||
Component.onCompleted: {
|
||||
// Don't show any highlighted item until interacting with them.
|
||||
// We do this here instead of onActivated to avoid losing the user's
|
||||
// selection when backing out of a different page of the app.
|
||||
currentIndex = -1
|
||||
}
|
||||
|
||||
onVisibleChanged: {
|
||||
if (visible) {
|
||||
// Start polling when this view is shown
|
||||
ComputerManager.startPolling()
|
||||
StackView.onActivated: {
|
||||
appModel.computerLost.connect(computerLost)
|
||||
activated = true
|
||||
|
||||
// Highlight the first item if a gamepad is connected
|
||||
if (currentIndex == -1 && SdlGamepadKeyNavigation.getConnectedGamepads() > 0) {
|
||||
currentIndex = 0
|
||||
}
|
||||
else {
|
||||
// Stop polling when this view is not on top
|
||||
ComputerManager.stopPollingAsync()
|
||||
|
||||
if (!showGames && !showHiddenGames) {
|
||||
// Check if there's a direct launch app
|
||||
var directLaunchAppIndex = model.getDirectLaunchAppIndex();
|
||||
if (directLaunchAppIndex >= 0) {
|
||||
// Start the direct launch app if nothing else is running
|
||||
currentIndex = directLaunchAppIndex
|
||||
currentItem.launchOrResumeSelectedApp(false)
|
||||
|
||||
// Set showGames so we will not loop when the stream ends
|
||||
showGames = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StackView.onDeactivating: {
|
||||
appModel.computerLost.disconnect(computerLost)
|
||||
activated = false
|
||||
}
|
||||
|
||||
function createModel()
|
||||
{
|
||||
var model = Qt.createQmlObject('import AppModel 1.0; AppModel {}', parent, '')
|
||||
model.initialize(ComputerManager, computerIndex)
|
||||
model.initialize(ComputerManager, computerIndex, showHiddenGames)
|
||||
return model
|
||||
}
|
||||
|
||||
model: appModel
|
||||
|
||||
delegate: Item {
|
||||
width: 200; height: 300;
|
||||
delegate: NavigableItemDelegate {
|
||||
width: 220; height: 287;
|
||||
grid: appGrid
|
||||
|
||||
property alias appContextMenu: appContextMenuLoader.item
|
||||
property alias appNameText: appNameTextLoader.item
|
||||
|
||||
// Dim the app if it's hidden
|
||||
opacity: model.hidden ? 0.4 : 1.0
|
||||
|
||||
Image {
|
||||
property bool isPlaceholder: false
|
||||
|
||||
id: appIcon
|
||||
anchors.horizontalCenter: parent.horizontalCenter;
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
y: 10
|
||||
source: model.boxart
|
||||
sourceSize {
|
||||
width: 150
|
||||
height: 200
|
||||
|
||||
onSourceSizeChanged: {
|
||||
// Nearly all of Nvidia's official box art does not match the dimensions of placeholder
|
||||
// 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 &&
|
||||
((sourceSize.width == 130 && sourceSize.height == 180) || // GFE 2.0 placeholder image
|
||||
(sourceSize.width == 628 && sourceSize.height == 888) || // GFE 3.0 placeholder image
|
||||
(sourceSize.width == 200 && sourceSize.height == 266))) // Our no_app_image.png
|
||||
{
|
||||
isPlaceholder = true
|
||||
}
|
||||
else
|
||||
{
|
||||
isPlaceholder = false
|
||||
}
|
||||
|
||||
width = 200
|
||||
height = 267
|
||||
}
|
||||
|
||||
// Display a tooltip with the full name if it's truncated
|
||||
ToolTip.text: model.name
|
||||
ToolTip.delay: 1000
|
||||
ToolTip.timeout: 5000
|
||||
ToolTip.visible: (parent.hovered || parent.highlighted) && (!appNameText || appNameText.truncated)
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: model.running
|
||||
asynchronous: true
|
||||
anchors.fill: appIcon
|
||||
|
||||
sourceComponent: Item {
|
||||
RoundButton {
|
||||
anchors.horizontalCenterOffset: appIcon.isPlaceholder ? -47 : 0
|
||||
anchors.verticalCenterOffset: appIcon.isPlaceholder ? -75 : -60
|
||||
anchors.centerIn: parent
|
||||
implicitWidth: 85
|
||||
implicitHeight: 85
|
||||
|
||||
Image {
|
||||
source: "qrc:/res/play_arrow_FILL1_wght700_GRAD200_opsz48.svg"
|
||||
anchors.centerIn: parent
|
||||
sourceSize {
|
||||
width: 75
|
||||
height: 75
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
launchOrResumeSelectedApp(true)
|
||||
}
|
||||
|
||||
ToolTip.text: qsTr("Resume Game")
|
||||
ToolTip.delay: 1000
|
||||
ToolTip.timeout: 3000
|
||||
ToolTip.visible: hovered
|
||||
|
||||
Material.background: "#D0808080"
|
||||
}
|
||||
|
||||
RoundButton {
|
||||
anchors.horizontalCenterOffset: appIcon.isPlaceholder ? 47 : 0
|
||||
anchors.verticalCenterOffset: appIcon.isPlaceholder ? -75 : 60
|
||||
anchors.centerIn: parent
|
||||
implicitWidth: 85
|
||||
implicitHeight: 85
|
||||
|
||||
Image {
|
||||
source: "qrc:/res/stop_FILL1_wght700_GRAD200_opsz48.svg"
|
||||
anchors.centerIn: parent
|
||||
sourceSize {
|
||||
width: 75
|
||||
height: 75
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
doQuitGame()
|
||||
}
|
||||
|
||||
ToolTip.text: qsTr("Quit Game")
|
||||
ToolTip.delay: 1000
|
||||
ToolTip.timeout: 3000
|
||||
ToolTip.visible: hovered
|
||||
|
||||
Material.background: "#D0808080"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Image {
|
||||
id: runningIcon
|
||||
anchors.centerIn: appIcon
|
||||
visible: model.running
|
||||
source: "qrc:/res/baseline-play_circle_filled_white-48px.svg"
|
||||
sourceSize {
|
||||
width: 75
|
||||
height: 75
|
||||
Loader {
|
||||
id: appNameTextLoader
|
||||
active: appIcon.isPlaceholder
|
||||
|
||||
// This loader is not asynchronous to avoid noticeable differences
|
||||
// in the time in which the text loads for each game.
|
||||
|
||||
width: appIcon.width
|
||||
height: model.running ? 175 : appIcon.height
|
||||
|
||||
anchors.left: appIcon.left
|
||||
anchors.right: appIcon.right
|
||||
anchors.bottom: appIcon.bottom
|
||||
|
||||
sourceComponent: Label {
|
||||
id: appNameText
|
||||
text: model.name
|
||||
font.pointSize: 22
|
||||
leftPadding: 20
|
||||
rightPadding: 20
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
wrapMode: Text.Wrap
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
id: appNameText
|
||||
text: model.name
|
||||
color: "white"
|
||||
width: parent.width
|
||||
height: 125
|
||||
anchors.top: appIcon.bottom
|
||||
font.pointSize: 22
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
wrapMode: Text.Wrap
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
function launchOrResumeSelectedApp()
|
||||
function launchOrResumeSelectedApp(quitExistingApp)
|
||||
{
|
||||
var runningIndex = appModel.getRunningAppIndex()
|
||||
if (runningIndex >= 0 && runningIndex !== index) {
|
||||
quitAppDialog.appName = appModel.getRunningAppName()
|
||||
quitAppDialog.segueToStream = true
|
||||
quitAppDialog.open()
|
||||
var runningId = appModel.getRunningAppId()
|
||||
if (runningId !== 0 && runningId !== model.appid) {
|
||||
if (quitExistingApp) {
|
||||
quitAppDialog.appName = appModel.getRunningAppName()
|
||||
quitAppDialog.segueToStream = true
|
||||
quitAppDialog.nextAppName = model.name
|
||||
quitAppDialog.nextAppIndex = index
|
||||
quitAppDialog.open()
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
var component = Qt.createComponent("StreamSegue.qml")
|
||||
var segue = component.createObject(stackView)
|
||||
segue.appName = model.name
|
||||
segue.session = appModel.createSessionForApp(index)
|
||||
var segue = component.createObject(stackView, {
|
||||
"appName": model.name,
|
||||
"session": appModel.createSessionForApp(index),
|
||||
"isResume": runningId === model.appid
|
||||
})
|
||||
stackView.push(segue)
|
||||
}
|
||||
|
||||
MessageDialog {
|
||||
id: quitAppDialog
|
||||
modality:Qt.WindowModal
|
||||
property string appName : "";
|
||||
property bool segueToStream : false
|
||||
text:"Are you sure you want to quit " + appName +"? Any unsaved progress will be lost."
|
||||
standardButtons: StandardButton.Yes | StandardButton.No
|
||||
onYes: {
|
||||
var component = Qt.createComponent("QuitSegue.qml")
|
||||
var segue = component.createObject(stackView)
|
||||
segue.appName = appName
|
||||
if (segueToStream) {
|
||||
// Store the session and app name if we're going to stream after
|
||||
// successfully quitting the old app.
|
||||
segue.nextAppName = model.name
|
||||
segue.nextSession = appModel.createSessionForApp(index)
|
||||
}
|
||||
else {
|
||||
segue.nextAppName = null
|
||||
segue.nextSession = null
|
||||
}
|
||||
onClicked: {
|
||||
// Only allow clicking on the box art for non-running games.
|
||||
// For running games, buttons will appear to resume or quit which
|
||||
// will handle starting the game and clicks on the box art will
|
||||
// be ignored.
|
||||
if (!model.running) {
|
||||
launchOrResumeSelectedApp(true)
|
||||
}
|
||||
}
|
||||
|
||||
stackView.push(segue)
|
||||
|
||||
// Trigger the quit after pushing the quit segue on screen
|
||||
appModel.quitRunningApp()
|
||||
onPressAndHold: {
|
||||
// popup() ensures the menu appears under the mouse cursor
|
||||
if (appContextMenu.popup) {
|
||||
appContextMenu.popup()
|
||||
}
|
||||
else {
|
||||
// Qt 5.9 doesn't have popup()
|
||||
appContextMenu.open()
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
acceptedButtons: Qt.RightButton;
|
||||
onClicked: {
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
// Nothing is running or this app is running
|
||||
launchOrResumeSelectedApp()
|
||||
}
|
||||
else {
|
||||
// Right click
|
||||
appContextMenu.open()
|
||||
}
|
||||
parent.onPressAndHold()
|
||||
}
|
||||
}
|
||||
|
||||
Menu {
|
||||
id: appContextMenu
|
||||
MenuItem {
|
||||
text: model.running ? "Resume Game" : "Launch Game"
|
||||
onTriggered: {
|
||||
appContextMenu.close()
|
||||
launchOrResumeSelectedApp()
|
||||
}
|
||||
height: visible ? implicitHeight : 0
|
||||
Keys.onReturnPressed: {
|
||||
// Open the app context menu if activated via the gamepad or keyboard
|
||||
// for running games. If the game isn't running, the above onClicked
|
||||
// method will handle the launch.
|
||||
if (model.running) {
|
||||
// This will be keyboard/gamepad driven so use
|
||||
// open() instead of popup()
|
||||
appContextMenu.open()
|
||||
}
|
||||
MenuItem {
|
||||
text: "Quit Game"
|
||||
onTriggered: {
|
||||
quitAppDialog.appName = appModel.getRunningAppName()
|
||||
quitAppDialog.segueToStream = false
|
||||
quitAppDialog.open()
|
||||
}
|
||||
|
||||
Keys.onEnterPressed: {
|
||||
// Open the app context menu if activated via the gamepad or keyboard
|
||||
// for running games. If the game isn't running, the above onClicked
|
||||
// method will handle the launch.
|
||||
if (model.running) {
|
||||
// This will be keyboard/gamepad driven so use
|
||||
// open() instead of popup()
|
||||
appContextMenu.open()
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onMenuPressed: {
|
||||
// This will be keyboard/gamepad driven so use open() instead of popup()
|
||||
appContextMenu.open()
|
||||
}
|
||||
|
||||
function doQuitGame() {
|
||||
quitAppDialog.appName = appModel.getRunningAppName()
|
||||
quitAppDialog.segueToStream = false
|
||||
quitAppDialog.open()
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: appContextMenuLoader
|
||||
asynchronous: true
|
||||
sourceComponent: NavigableMenu {
|
||||
id: appContextMenu
|
||||
NavigableMenuItem {
|
||||
parentMenu: appContextMenu
|
||||
text: model.running ? qsTr("Resume Game") : qsTr("Launch Game")
|
||||
onTriggered: launchOrResumeSelectedApp(true)
|
||||
}
|
||||
NavigableMenuItem {
|
||||
parentMenu: appContextMenu
|
||||
text: qsTr("Quit Game")
|
||||
onTriggered: doQuitGame()
|
||||
visible: model.running
|
||||
}
|
||||
NavigableMenuItem {
|
||||
parentMenu: appContextMenu
|
||||
checkable: true
|
||||
checked: model.directLaunch
|
||||
text: qsTr("Direct Launch")
|
||||
onTriggered: appModel.setAppDirectLaunch(model.index, !model.directLaunch)
|
||||
enabled: !model.hidden
|
||||
|
||||
ToolTip.text: qsTr("Launch this app immediately when the host is selected, bypassing the app selection grid.")
|
||||
ToolTip.delay: 1000
|
||||
ToolTip.timeout: 3000
|
||||
ToolTip.visible: hovered
|
||||
}
|
||||
NavigableMenuItem {
|
||||
parentMenu: appContextMenu
|
||||
checkable: true
|
||||
checked: model.hidden
|
||||
text: qsTr("Hide Game")
|
||||
onTriggered: appModel.setAppHidden(model.index, !model.hidden)
|
||||
enabled: model.hidden || (!model.running && !model.directLaunch)
|
||||
|
||||
ToolTip.text: qsTr("Hide this game from the app grid. To access hidden games, right-click on the host and choose %1.").arg(qsTr("View All Apps"))
|
||||
ToolTip.delay: 1000
|
||||
ToolTip.timeout: 5000
|
||||
ToolTip.visible: hovered
|
||||
}
|
||||
visible: model.running
|
||||
height: visible ? implicitHeight : 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ScrollBar.vertical: ScrollBar {
|
||||
parent: appGrid.parent
|
||||
anchors {
|
||||
top: appGrid.top
|
||||
left: appGrid.right
|
||||
bottom: appGrid.bottom
|
||||
NavigableMessageDialog {
|
||||
id: quitAppDialog
|
||||
property string appName : ""
|
||||
property bool segueToStream : false
|
||||
property string nextAppName: ""
|
||||
property int nextAppIndex: 0
|
||||
text:qsTr("Are you sure you want to quit %1? Any unsaved progress will be lost.").arg(appName)
|
||||
standardButtons: Dialog.Yes | Dialog.No
|
||||
|
||||
leftMargin: -10
|
||||
function quitApp() {
|
||||
var component = Qt.createComponent("QuitSegue.qml")
|
||||
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.
|
||||
params.nextAppName = nextAppName
|
||||
params.nextSession = appModel.createSessionForApp(nextAppIndex)
|
||||
}
|
||||
else {
|
||||
params.nextAppName = null
|
||||
params.nextSession = null
|
||||
}
|
||||
|
||||
stackView.push(component.createObject(stackView, params))
|
||||
}
|
||||
|
||||
onAccepted: quitApp()
|
||||
}
|
||||
|
||||
ScrollBar.vertical: ScrollBar {}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
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 {
|
||||
property int textWidth
|
||||
property int desiredWidth : leftPadding + textWidth + indicator.width + rightPadding
|
||||
property int maximumWidth : parent.width
|
||||
|
||||
implicitWidth: desiredWidth < maximumWidth ? desiredWidth : maximumWidth
|
||||
|
||||
TextMetrics {
|
||||
id: popupMetrics
|
||||
}
|
||||
|
||||
TextMetrics {
|
||||
id: textMetrics
|
||||
}
|
||||
|
||||
function recalculateWidth() {
|
||||
textMetrics.font = font
|
||||
popupMetrics.font = popup.font
|
||||
textWidth = 0
|
||||
for (var i = 0; i < count; i++){
|
||||
textMetrics.text = textAt(i)
|
||||
popupMetrics.text = textAt(i)
|
||||
textWidth = Math.max(textMetrics.width, textWidth)
|
||||
textWidth = Math.max(popupMetrics.width, textWidth)
|
||||
}
|
||||
}
|
||||
|
||||
// We call this every time the options change (and init)
|
||||
// so we can adjust the combo box width here too
|
||||
onActivated: recalculateWidth()
|
||||
|
||||
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: {
|
||||
SdlGamepadKeyNavigation.setUiNavMode(true)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import QtQuick 2.9
|
||||
import QtQuick.Controls 2.2
|
||||
|
||||
GridView {
|
||||
// Detect Qt 5.11 or earlier using presence of synchronousDrag.
|
||||
// Prior to 5.12, the leftMargin and rightMargin values did not work.
|
||||
property bool hasBrokenMargins: this.synchronousDrag === undefined
|
||||
|
||||
property int minMargin: 10
|
||||
property real availableWidth: (parent.width - 2 * minMargin)
|
||||
property int itemsPerRow: availableWidth / cellWidth
|
||||
property real horizontalMargin: itemsPerRow < count && availableWidth >= cellWidth ?
|
||||
(availableWidth % cellWidth) / 2 : minMargin
|
||||
|
||||
function updateMargins() {
|
||||
leftMargin = horizontalMargin
|
||||
rightMargin = horizontalMargin
|
||||
|
||||
if (hasBrokenMargins) {
|
||||
anchors.leftMargin = leftMargin
|
||||
anchors.rightMargin = rightMargin
|
||||
}
|
||||
}
|
||||
|
||||
onHorizontalMarginChanged: {
|
||||
updateMargins()
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
if (hasBrokenMargins) {
|
||||
// This will cause an anchor conflict with the parent StackView
|
||||
// which breaks animation, but otherwise the grid will not be
|
||||
// centered in the window.
|
||||
anchors.fill = parent
|
||||
anchors.topMargin = topMargin
|
||||
anchors.bottomMargin = bottomMargin
|
||||
}
|
||||
|
||||
updateMargins()
|
||||
}
|
||||
|
||||
boundsBehavior: Flickable.OvershootBounds
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import QtQuick 2.0
|
||||
import QtQuick.Controls 2.2
|
||||
|
||||
import ComputerManager 1.0
|
||||
import Session 1.0
|
||||
|
||||
Item {
|
||||
function onSearchingComputer() {
|
||||
stageLabel.text = qsTr("Establishing connection to PC...")
|
||||
}
|
||||
|
||||
function onQuittingApp() {
|
||||
stageLabel.text = qsTr("Quitting app...")
|
||||
}
|
||||
|
||||
function onFailure(message) {
|
||||
errorDialog.text = message
|
||||
errorDialog.open()
|
||||
}
|
||||
|
||||
StackView.onActivated: {
|
||||
if (!launcher.isExecuted()) {
|
||||
toolBar.visible = false
|
||||
launcher.searchingComputer.connect(onSearchingComputer)
|
||||
launcher.quittingApp.connect(onQuittingApp)
|
||||
launcher.failed.connect(onFailure)
|
||||
launcher.execute(ComputerManager)
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: 5
|
||||
|
||||
BusyIndicator {
|
||||
id: stageSpinner
|
||||
}
|
||||
|
||||
Label {
|
||||
id: stageLabel
|
||||
height: stageSpinner.height
|
||||
text: stageText
|
||||
font.pointSize: 20
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
}
|
||||
|
||||
ErrorMessageDialog {
|
||||
id: errorDialog
|
||||
|
||||
onClosed: {
|
||||
Qt.quit()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
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 onSearchingApp() {
|
||||
stageLabel.text = qsTr("Loading app list...")
|
||||
}
|
||||
|
||||
function onSessionCreated(appName, session) {
|
||||
var component = Qt.createComponent("StreamSegue.qml")
|
||||
var segue = component.createObject(stackView, {
|
||||
"appName": appName,
|
||||
"session": session,
|
||||
"quitAfter": true
|
||||
})
|
||||
stackView.push(segue)
|
||||
}
|
||||
|
||||
function onLaunchFailed(message) {
|
||||
errorDialog.text = message
|
||||
errorDialog.open()
|
||||
console.error(message)
|
||||
}
|
||||
|
||||
function onAppQuitRequired(appName) {
|
||||
quitAppDialog.appName = appName
|
||||
quitAppDialog.open()
|
||||
}
|
||||
|
||||
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.searchingApp.connect(onSearchingApp)
|
||||
launcher.sessionCreated.connect(onSessionCreated)
|
||||
launcher.failed.connect(onLaunchFailed)
|
||||
launcher.appQuitRequired.connect(onAppQuitRequired)
|
||||
launcher.execute(ComputerManager)
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: 5
|
||||
|
||||
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: quitAppDialog
|
||||
text:qsTr("Are you sure you want to quit %1? Any unsaved progress will be lost.").arg(appName)
|
||||
standardButtons: Dialog.Yes | Dialog.No
|
||||
property string appName : ""
|
||||
|
||||
function quitApp() {
|
||||
var component = Qt.createComponent("QuitSegue.qml")
|
||||
var params = {"appName": appName, "quitRunningAppFn": launcher.quitRunningApp}
|
||||
stackView.push(component.createObject(stackView, params))
|
||||
}
|
||||
|
||||
onAccepted: quitApp()
|
||||
onRejected: Qt.quit()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import QtQuick 2.0
|
||||
import QtQuick.Controls 2.2
|
||||
|
||||
import SystemProperties 1.0
|
||||
|
||||
NavigableMessageDialog {
|
||||
standardButtons: Dialog.Ok | (SystemProperties.hasBrowser ? Dialog.Help : 0)
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import QtQuick 2.0
|
||||
|
||||
Item {
|
||||
objectName: "Gamepad Mapping"
|
||||
objectName: qsTr("Gamepad Mapping")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import QtQuick 2.0
|
||||
import QtQuick.Controls 2.2
|
||||
|
||||
Dialog {
|
||||
// We should use Overlay.overlay here but that's not available in Qt 5.9 :(
|
||||
parent: ApplicationWindow.contentItem
|
||||
|
||||
x: Math.round((parent.width - width) / 2)
|
||||
y: Math.round((parent.height - height) / 2)
|
||||
|
||||
onAboutToHide: {
|
||||
// We must force focus back to the last item for platforms without
|
||||
// support for more than one active window like Steam Link. If
|
||||
// we don't, gamepad and keyboard navigation will break after a
|
||||
// dialog appears.
|
||||
stackView.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import QtQuick 2.0
|
||||
import QtQuick.Controls 2.2
|
||||
|
||||
ItemDelegate {
|
||||
property GridView grid
|
||||
|
||||
highlighted: grid.activeFocus && grid.currentItem === this
|
||||
|
||||
Keys.onLeftPressed: {
|
||||
grid.moveCurrentIndexLeft()
|
||||
}
|
||||
Keys.onRightPressed: {
|
||||
grid.moveCurrentIndexRight()
|
||||
}
|
||||
Keys.onDownPressed: {
|
||||
grid.moveCurrentIndexDown()
|
||||
}
|
||||
Keys.onUpPressed: {
|
||||
grid.moveCurrentIndexUp()
|
||||
}
|
||||
Keys.onReturnPressed: {
|
||||
clicked()
|
||||
}
|
||||
Keys.onEnterPressed: {
|
||||
clicked()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import QtQuick 2.0
|
||||
import QtQuick.Controls 2.2
|
||||
|
||||
Menu {
|
||||
onOpened: {
|
||||
// Give focus to the first visible and enabled menu item
|
||||
for (var i = 0; i < count; i++) {
|
||||
var item = itemAt(i)
|
||||
if (item.visible && item.enabled) {
|
||||
item.forceActiveFocus(Qt.TabFocusReason)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import QtQuick 2.0
|
||||
import QtQuick.Controls 2.2
|
||||
|
||||
MenuItem {
|
||||
// Qt 5.10 has a menu property, but we need to support 5.9
|
||||
// so we must make our own.
|
||||
property Menu parentMenu
|
||||
|
||||
// Ensure focus can't be given to an invisible item
|
||||
enabled: visible
|
||||
height: visible ? implicitHeight : 0
|
||||
focusPolicy: visible ? Qt.TabFocus : Qt.NoFocus
|
||||
|
||||
onTriggered: {
|
||||
// We must close the context menu first or
|
||||
// it can steal focus from any dialogs that
|
||||
// onTriggered may spawn.
|
||||
parentMenu.close()
|
||||
}
|
||||
|
||||
Keys.onReturnPressed: {
|
||||
triggered()
|
||||
}
|
||||
|
||||
Keys.onEnterPressed: {
|
||||
triggered()
|
||||
}
|
||||
|
||||
Keys.onEscapePressed: {
|
||||
parentMenu.close()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import QtQuick 2.0
|
||||
import QtQuick.Controls 2.2
|
||||
import QtQuick.Layouts 1.2
|
||||
|
||||
NavigableDialog {
|
||||
id: dialog
|
||||
|
||||
property alias text: dialogLabel.dialogText
|
||||
property alias showSpinner: dialogSpinner.visible
|
||||
property alias imageSrc: dialogImage.source
|
||||
|
||||
property string helpText
|
||||
property string helpUrl : "https://github.com/moonlight-stream/moonlight-docs/wiki/Troubleshooting"
|
||||
property string helpTextSeparator : " "
|
||||
|
||||
onOpened: {
|
||||
// Force keyboard focus on the label so keyboard navigation works
|
||||
dialogLabel.forceActiveFocus()
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: 10
|
||||
|
||||
BusyIndicator {
|
||||
id: dialogSpinner
|
||||
visible: false
|
||||
}
|
||||
|
||||
Image {
|
||||
id: dialogImage
|
||||
source: (standardButtons & Dialog.Yes) ?
|
||||
"qrc:/res/baseline-help_outline-24px.svg" :
|
||||
"qrc:/res/baseline-error_outline-24px.svg"
|
||||
sourceSize {
|
||||
// The icon should be square so use the height as the width too
|
||||
width: 50
|
||||
height: 50
|
||||
}
|
||||
visible: !showSpinner
|
||||
}
|
||||
|
||||
Label {
|
||||
property string dialogText
|
||||
|
||||
id: dialogLabel
|
||||
text: dialogText + ((helpText && (standardButtons & Dialog.Help)) ? (helpTextSeparator + helpText) : "")
|
||||
wrapMode: Text.Wrap
|
||||
elide: Label.ElideRight
|
||||
|
||||
// Cap the width so the dialog doesn't grow horizontally forever. This
|
||||
// will cause word wrap to kick in.
|
||||
Layout.maximumWidth: 400
|
||||
Layout.maximumHeight: 400
|
||||
|
||||
Keys.onReturnPressed: {
|
||||
accept()
|
||||
}
|
||||
|
||||
Keys.onEnterPressed: {
|
||||
accept()
|
||||
}
|
||||
|
||||
Keys.onEscapePressed: {
|
||||
reject()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
footer: DialogButtonBox {
|
||||
id: dialogButtonBox
|
||||
standardButtons: dialog.standardButtons
|
||||
|
||||
onHelpRequested: {
|
||||
Qt.openUrlExternally(helpUrl)
|
||||
close()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import QtQuick 2.0
|
||||
import QtQuick.Controls 2.2
|
||||
import QtQuick.Layouts 1.3
|
||||
|
||||
ToolButton {
|
||||
property string iconSource
|
||||
|
||||
activeFocusOnTab: true
|
||||
|
||||
// FIXME: We're using an Image here rather than icon.source because
|
||||
// icons don't work on Qt 5.9 LTS.
|
||||
Image {
|
||||
id: image
|
||||
source: iconSource
|
||||
anchors.centerIn: parent.background
|
||||
sourceSize {
|
||||
width: parent.background.width * 1.10
|
||||
height: parent.background.height * 1.10
|
||||
}
|
||||
}
|
||||
|
||||
// This determines the size of the Material highlight. We increase it
|
||||
// from the default because we use larger than normal icons for TV readability.
|
||||
Layout.preferredHeight: parent.height
|
||||
|
||||
Keys.onReturnPressed: {
|
||||
clicked()
|
||||
}
|
||||
|
||||
Keys.onEnterPressed: {
|
||||
clicked()
|
||||
}
|
||||
|
||||
Keys.onRightPressed: {
|
||||
nextItemInFocusChain(true).forceActiveFocus(Qt.TabFocus)
|
||||
}
|
||||
|
||||
Keys.onLeftPressed: {
|
||||
nextItemInFocusChain(false).forceActiveFocus(Qt.TabFocus)
|
||||
}
|
||||
}
|
||||
+271
-167
@@ -1,53 +1,51 @@
|
||||
import QtQuick 2.9
|
||||
import QtQuick.Controls 2.2
|
||||
import QtQuick.Dialogs 1.3
|
||||
import QtQuick.Layouts 1.3
|
||||
|
||||
import ComputerModel 1.0
|
||||
|
||||
import ComputerManager 1.0
|
||||
import StreamingPreferences 1.0
|
||||
import SdlGamepadKeyNavigation 1.0
|
||||
|
||||
GridView {
|
||||
CenteredGridView {
|
||||
property ComputerModel computerModel : createModel()
|
||||
|
||||
id: pcGrid
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: (parent.width % (cellWidth + anchors.rightMargin)) / 2
|
||||
anchors.topMargin: 20
|
||||
anchors.rightMargin: 5
|
||||
anchors.bottomMargin: 5
|
||||
cellWidth: 350; cellHeight: 350;
|
||||
focus: true
|
||||
objectName: "Computers"
|
||||
|
||||
// The StackView will trigger a visibility change when
|
||||
// we're pushed onto it, causing our onVisibleChanged
|
||||
// routine to run, but only if we start as invisible
|
||||
visible: false
|
||||
|
||||
StreamingPreferences {
|
||||
id: prefs
|
||||
}
|
||||
activeFocusOnTab: true
|
||||
topMargin: 20
|
||||
bottomMargin: 5
|
||||
cellWidth: 310; cellHeight: 330;
|
||||
objectName: qsTr("Computers")
|
||||
|
||||
Component.onCompleted: {
|
||||
// Don't show any highlighted item until interacting with them.
|
||||
// We do this here instead of onActivated to avoid losing the user's
|
||||
// selection when backing out of a different page of the app.
|
||||
currentIndex = -1
|
||||
}
|
||||
|
||||
// Note: Any initialization done here that is critical for streaming must
|
||||
// also be done in CliStartStreamSegue.qml, since this code does not run
|
||||
// for command-line initiated streams.
|
||||
StackView.onActivated: {
|
||||
// Setup signals on CM
|
||||
ComputerManager.computerAddCompleted.connect(addComplete)
|
||||
|
||||
if (!prefs.hasAnyHardwareAcceleration()) {
|
||||
noHwDecoderDialog.open()
|
||||
// This is a bit of a hack to do this here as opposed to main.qml, but
|
||||
// we need it enabled before calling getConnectedGamepads() and PcView
|
||||
// is never destroyed, so it should be okay.
|
||||
SdlGamepadKeyNavigation.enable()
|
||||
|
||||
// Highlight the first item if a gamepad is connected
|
||||
if (currentIndex == -1 && SdlGamepadKeyNavigation.getConnectedGamepads() > 0) {
|
||||
currentIndex = 0
|
||||
}
|
||||
}
|
||||
|
||||
onVisibleChanged: {
|
||||
if (visible) {
|
||||
// Start polling when this view is shown
|
||||
ComputerManager.startPolling()
|
||||
}
|
||||
else {
|
||||
// Stop polling when this view is not top-most
|
||||
ComputerManager.stopPollingAsync()
|
||||
}
|
||||
StackView.onDeactivating: {
|
||||
ComputerManager.computerAddCompleted.disconnect(addComplete)
|
||||
}
|
||||
|
||||
function pairingComplete(error)
|
||||
@@ -55,20 +53,26 @@ GridView {
|
||||
// Close the PIN dialog
|
||||
pairDialog.close()
|
||||
|
||||
// Start polling again
|
||||
ComputerManager.startPolling()
|
||||
|
||||
// Display a failed dialog if we got an error
|
||||
if (error !== null) {
|
||||
if (error !== undefined) {
|
||||
errorDialog.text = error
|
||||
errorDialog.helpText = ""
|
||||
errorDialog.open()
|
||||
}
|
||||
}
|
||||
|
||||
function addComplete(success)
|
||||
function addComplete(success, detectedPortBlocking)
|
||||
{
|
||||
if (!success) {
|
||||
errorDialog.text = "Unable to connect to the specified PC. Ensure GameStream is enabled in GeForce Experience."
|
||||
errorDialog.text = qsTr("Unable to connect to the specified PC.")
|
||||
|
||||
if (detectedPortBlocking) {
|
||||
errorDialog.text += "\n\n" + qsTr("This PC's Internet connection is blocking Moonlight. Streaming over the Internet may not work while connected to this network.")
|
||||
}
|
||||
else {
|
||||
errorDialog.helpText = qsTr("Click the Help button for possible solutions.")
|
||||
}
|
||||
|
||||
errorDialog.open()
|
||||
}
|
||||
}
|
||||
@@ -78,20 +82,43 @@ GridView {
|
||||
var model = Qt.createQmlObject('import ComputerModel 1.0; ComputerModel {}', parent, '')
|
||||
model.initialize(ComputerManager)
|
||||
model.pairingCompleted.connect(pairingComplete)
|
||||
model.connectionTestCompleted.connect(testConnectionDialog.connectionTestComplete)
|
||||
return model
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: 5
|
||||
visible: pcGrid.count === 0
|
||||
|
||||
BusyIndicator {
|
||||
id: searchSpinner
|
||||
visible: StreamingPreferences.enableMdns
|
||||
}
|
||||
|
||||
Label {
|
||||
height: searchSpinner.height
|
||||
elide: Label.ElideRight
|
||||
text: StreamingPreferences.enableMdns ? 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
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
}
|
||||
|
||||
model: computerModel
|
||||
|
||||
delegate: Item {
|
||||
width: 300; height: 300;
|
||||
delegate: NavigableItemDelegate {
|
||||
width: 300; height: 320;
|
||||
grid: pcGrid
|
||||
|
||||
property alias pcContextMenu : pcContextMenuLoader.item
|
||||
|
||||
Image {
|
||||
id: pcIcon
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
source: {
|
||||
model.addPc ? "qrc:/res/ic_add_to_queue_white_48px.svg" : "qrc:/res/ic_tv_white_48px.svg"
|
||||
}
|
||||
source: "qrc:/res/desktop_windows-48px.svg"
|
||||
sourceSize {
|
||||
width: 200
|
||||
height: 200
|
||||
@@ -103,12 +130,12 @@ GridView {
|
||||
id: stateIcon
|
||||
anchors.horizontalCenter: pcIcon.horizontalCenter
|
||||
anchors.verticalCenter: pcIcon.verticalCenter
|
||||
anchors.verticalCenterOffset: -10
|
||||
visible: !model.addPc && !model.statusUnknown && (!model.online || !model.paired)
|
||||
source: !model.online ? "qrc:/res/baseline-warning-24px.svg" : "qrc:/res/baseline-lock-24px.svg"
|
||||
anchors.verticalCenterOffset: !model.online ? -18 : -16
|
||||
visible: !model.statusUnknown && (!model.online || !model.paired)
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,180 +143,257 @@ GridView {
|
||||
id: statusUnknownSpinner
|
||||
anchors.horizontalCenter: pcIcon.horizontalCenter
|
||||
anchors.verticalCenter: pcIcon.verticalCenter
|
||||
anchors.verticalCenterOffset: -10
|
||||
anchors.verticalCenterOffset: -15
|
||||
width: 75
|
||||
height: 75
|
||||
visible: !model.addPc && model.statusUnknown
|
||||
visible: model.statusUnknown
|
||||
}
|
||||
|
||||
Text {
|
||||
Label {
|
||||
id: pcNameText
|
||||
text: model.name
|
||||
color: "white"
|
||||
|
||||
width: parent.width
|
||||
anchors.top: pcIcon.bottom
|
||||
anchors.bottom: parent.bottom
|
||||
font.pointSize: 36
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
wrapMode: Text.Wrap
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Menu {
|
||||
id: pcContextMenu
|
||||
MenuItem {
|
||||
text: "Wake PC"
|
||||
onTriggered: computerModel.wakeComputer(index)
|
||||
visible: !model.addPc && !model.online && model.wakeable
|
||||
height: visible ? implicitHeight : 0
|
||||
}
|
||||
MenuItem {
|
||||
text: "Delete PC"
|
||||
onTriggered: {
|
||||
deletePcDialog.pcIndex = index
|
||||
// get confirmation first, actual closing is called from the dialog
|
||||
deletePcDialog.open()
|
||||
Loader {
|
||||
id: pcContextMenuLoader
|
||||
asynchronous: true
|
||||
sourceComponent: NavigableMenu {
|
||||
id: pcContextMenu
|
||||
MenuItem {
|
||||
text: qsTr("PC Status: %1").arg(model.online ? qsTr("Online") : qsTr("Offline"))
|
||||
font.bold: true
|
||||
enabled: false
|
||||
}
|
||||
NavigableMenuItem {
|
||||
parentMenu: pcContextMenu
|
||||
text: qsTr("View All Apps")
|
||||
onTriggered: {
|
||||
var component = Qt.createComponent("AppView.qml")
|
||||
var appView = component.createObject(stackView, {"computerIndex": index, "objectName": model.name, "showHiddenGames": true})
|
||||
stackView.push(appView)
|
||||
}
|
||||
visible: model.online && model.paired
|
||||
}
|
||||
NavigableMenuItem {
|
||||
parentMenu: pcContextMenu
|
||||
text: qsTr("Wake PC")
|
||||
onTriggered: computerModel.wakeComputer(index)
|
||||
visible: !model.online && model.wakeable
|
||||
}
|
||||
NavigableMenuItem {
|
||||
parentMenu: pcContextMenu
|
||||
text: qsTr("Test Network")
|
||||
onTriggered: {
|
||||
computerModel.testConnectionForComputer(index)
|
||||
testConnectionDialog.open()
|
||||
}
|
||||
}
|
||||
|
||||
NavigableMenuItem {
|
||||
parentMenu: pcContextMenu
|
||||
text: qsTr("Rename PC")
|
||||
onTriggered: {
|
||||
renamePcDialog.pcIndex = index
|
||||
renamePcDialog.originalName = model.name
|
||||
renamePcDialog.open()
|
||||
}
|
||||
}
|
||||
NavigableMenuItem {
|
||||
parentMenu: pcContextMenu
|
||||
text: qsTr("Delete PC")
|
||||
onTriggered: {
|
||||
deletePcDialog.pcIndex = index
|
||||
deletePcDialog.pcName = model.name
|
||||
deletePcDialog.open()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
if (model.online) {
|
||||
if (!model.serverSupported) {
|
||||
errorDialog.text = qsTr("The version of GeForce Experience on %1 is not supported by this build of Moonlight. You must update Moonlight to stream from %1.").arg(model.name)
|
||||
errorDialog.helpText = ""
|
||||
errorDialog.open()
|
||||
}
|
||||
else if (model.paired) {
|
||||
// go to game view
|
||||
var component = Qt.createComponent("AppView.qml")
|
||||
var appView = component.createObject(stackView, {"computerIndex": index, "objectName": model.name})
|
||||
stackView.push(appView)
|
||||
}
|
||||
else {
|
||||
var pin = computerModel.generatePinString()
|
||||
|
||||
// Kick off pairing in the background
|
||||
computerModel.pairComputer(index, pin)
|
||||
|
||||
// Display the pairing dialog
|
||||
pairDialog.pin = pin
|
||||
pairDialog.open()
|
||||
}
|
||||
} else if (!model.online) {
|
||||
// Using open() here because it may be activated by keyboard
|
||||
pcContextMenu.open()
|
||||
}
|
||||
}
|
||||
|
||||
onPressAndHold: {
|
||||
// popup() ensures the menu appears under the mouse cursor
|
||||
if (pcContextMenu.popup) {
|
||||
pcContextMenu.popup()
|
||||
}
|
||||
else {
|
||||
// Qt 5.9 doesn't have popup()
|
||||
pcContextMenu.open()
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton;
|
||||
acceptedButtons: Qt.RightButton;
|
||||
onClicked: {
|
||||
if(mouse.button === Qt.LeftButton) {
|
||||
if (model.addPc) {
|
||||
addPcDialog.open()
|
||||
}
|
||||
else if (model.online) {
|
||||
if (model.paired) {
|
||||
// go to game view
|
||||
var component = Qt.createComponent("AppView.qml")
|
||||
var appView = component.createObject(stackView)
|
||||
appView.computerIndex = index
|
||||
appView.objectName = model.name
|
||||
stackView.push(appView)
|
||||
}
|
||||
else {
|
||||
if (!model.busy) {
|
||||
var pin = ("0000" + Math.floor(Math.random() * 10000)).slice(-4)
|
||||
|
||||
// Stop polling, since pairing may make GFE unresponsive
|
||||
ComputerManager.stopPollingAsync()
|
||||
|
||||
// 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 = "This PC is currently busy. Make sure to quit any running games and try again."
|
||||
errorDialog.open()
|
||||
}
|
||||
}
|
||||
} else if(!model.online) {
|
||||
pcContextMenu.open()
|
||||
}
|
||||
|
||||
}
|
||||
else { // right click
|
||||
if(!model.addPc) { // but only for actual PCs, not the add-pc option
|
||||
pcContextMenu.open()
|
||||
}
|
||||
}
|
||||
parent.onPressAndHold()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MessageDialog {
|
||||
id: errorDialog
|
||||
// don't allow edits to the rest of the window while open
|
||||
modality:Qt.WindowModal
|
||||
icon: StandardIcon.Critical
|
||||
standardButtons: StandardButton.Ok
|
||||
}
|
||||
Keys.onMenuPressed: {
|
||||
// We must use open() here so the menu is positioned on
|
||||
// the ItemDelegate and not where the mouse cursor is
|
||||
pcContextMenu.open()
|
||||
}
|
||||
|
||||
MessageDialog {
|
||||
id: noHwDecoderDialog
|
||||
modality:Qt.WindowModal
|
||||
icon: StandardIcon.Warning
|
||||
standardButtons: StandardButton.Ok | StandardButton.Help
|
||||
text: "No functioning hardware accelerated H.264 video decoder was detected by Moonlight. " +
|
||||
"Your streaming performance may be severely degraded in this configuration. " +
|
||||
"Click the Help button for more information on solving this problem."
|
||||
onHelp: {
|
||||
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Fixing-Hardware-Decoding-Problems");
|
||||
Keys.onDeletePressed: {
|
||||
deletePcDialog.pcIndex = index
|
||||
deletePcDialog.pcName = model.name
|
||||
deletePcDialog.open()
|
||||
}
|
||||
}
|
||||
|
||||
MessageDialog {
|
||||
ErrorMessageDialog {
|
||||
id: errorDialog
|
||||
|
||||
// Using Setup-Guide here instead of Troubleshooting because it's likely that users
|
||||
// will arrive here by forgetting to enable GameStream or not forwarding ports.
|
||||
helpUrl: "https://github.com/moonlight-stream/moonlight-docs/wiki/Setup-Guide"
|
||||
}
|
||||
|
||||
NavigableMessageDialog {
|
||||
id: pairDialog
|
||||
|
||||
// Pairing dialog must be modal to prevent double-clicks from triggering
|
||||
// pairing twice
|
||||
modal: true
|
||||
closePolicy: Popup.CloseOnEscape
|
||||
|
||||
// don't allow edits to the rest of the window while open
|
||||
modality:Qt.WindowModal
|
||||
property string pin : "0000"
|
||||
text:"Please enter " + pin + " on your GameStream PC. This dialog will close when pairing is completed."
|
||||
standardButtons: StandardButton.Cancel
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
MessageDialog {
|
||||
NavigableMessageDialog {
|
||||
id: deletePcDialog
|
||||
// don't allow edits to the rest of the window while open
|
||||
modality:Qt.WindowModal
|
||||
property int pcIndex : -1;
|
||||
text:"Are you sure you want to remove this PC?"
|
||||
standardButtons: StandardButton.Yes | StandardButton.No
|
||||
onYes: {
|
||||
console.log("deleting PC pairing for PC at index: " + pcIndex)
|
||||
computerModel.deleteComputer(pcIndex);
|
||||
property int pcIndex : -1
|
||||
property string pcName : ""
|
||||
text: qsTr("Are you sure you want to remove '%1'?").arg(pcName)
|
||||
standardButtons: Dialog.Yes | Dialog.No
|
||||
|
||||
onAccepted: {
|
||||
computerModel.deleteComputer(pcIndex)
|
||||
}
|
||||
}
|
||||
|
||||
Dialog {
|
||||
id: addPcDialog
|
||||
property string label: "Enter the IP address of your GameStream PC"
|
||||
property string hint: "192.168.1.100"
|
||||
property alias editText : editTextItem
|
||||
NavigableMessageDialog {
|
||||
id: testConnectionDialog
|
||||
closePolicy: Popup.CloseOnEscape
|
||||
standardButtons: Dialog.Ok
|
||||
|
||||
standardButtons: StandardButton.Ok | StandardButton.Cancel
|
||||
onVisibleChanged: {
|
||||
editTextItem.focus = true
|
||||
editTextItem.selectAll()
|
||||
onAboutToShow: {
|
||||
testConnectionDialog.text = qsTr("Moonlight is testing your network connection to determine if any required ports are blocked.") + "\n\n" + qsTr("This may take a few seconds…")
|
||||
showSpinner = true
|
||||
}
|
||||
|
||||
function connectionTestComplete(result, blockedPorts)
|
||||
{
|
||||
if (result === -1) {
|
||||
text = qsTr("The network test could not be performed because none of Moonlight's connection testing servers were reachable from this PC. Check your Internet connection or try again later.")
|
||||
imageSrc = "qrc:/res/baseline-warning-24px.svg"
|
||||
}
|
||||
else if (result === 0) {
|
||||
text = qsTr("This network does not appear to be blocking Moonlight. If you still have trouble connecting, check your PC's firewall settings.") + "\n\n" + qsTr("If you are trying to stream over the Internet, install the Moonlight Internet Hosting Tool on your gaming PC and run the included Internet Streaming Tester to check your gaming PC's Internet connection.")
|
||||
imageSrc = "qrc:/res/baseline-check_circle_outline-24px.svg"
|
||||
}
|
||||
else {
|
||||
text = qsTr("Your PC's current network connection seems to be blocking Moonlight. Streaming over the Internet may not work while connected to this network.") + "\n\n" + qsTr("The following network ports were blocked:") + "\n"
|
||||
text += blockedPorts
|
||||
imageSrc = "qrc:/res/baseline-error_outline-24px.svg"
|
||||
}
|
||||
|
||||
// Stop showing the spinner and show the image instead
|
||||
showSpinner = false
|
||||
}
|
||||
}
|
||||
|
||||
NavigableDialog {
|
||||
id: renamePcDialog
|
||||
property string label: qsTr("Enter the new name for this PC:")
|
||||
property string originalName
|
||||
property int pcIndex : -1;
|
||||
|
||||
standardButtons: Dialog.Ok | Dialog.Cancel
|
||||
|
||||
onOpened: {
|
||||
// Force keyboard focus on the textbox so keyboard navigation works
|
||||
editText.forceActiveFocus()
|
||||
}
|
||||
|
||||
onClosed: {
|
||||
editText.clear()
|
||||
}
|
||||
|
||||
onAccepted: {
|
||||
ComputerManager.addNewHost(editText.text, false)
|
||||
if (editText.text) {
|
||||
computerModel.renameComputer(pcIndex, editText.text)
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Text {
|
||||
id: addPcDialogLabel
|
||||
text: addPcDialog.label
|
||||
Label {
|
||||
text: renamePcDialog.label
|
||||
font.bold: true
|
||||
}
|
||||
Rectangle {
|
||||
implicitWidth: parent.parent.width
|
||||
height: editTextItem.height
|
||||
|
||||
TextInput {
|
||||
id: editTextItem
|
||||
inputMethodHints: Qt.ImhPreferUppercase
|
||||
text: addPcDialog.hint
|
||||
TextField {
|
||||
id: editText
|
||||
placeholderText: renamePcDialog.originalName
|
||||
Layout.fillWidth: true
|
||||
focus: true
|
||||
|
||||
Keys.onReturnPressed: {
|
||||
renamePcDialog.accept()
|
||||
}
|
||||
|
||||
Keys.onEnterPressed: {
|
||||
renamePcDialog.accept()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ScrollBar.vertical: ScrollBar {
|
||||
parent: pcGrid.parent
|
||||
anchors {
|
||||
top: pcGrid.top
|
||||
left: pcGrid.right
|
||||
bottom: pcGrid.bottom
|
||||
|
||||
leftMargin: -10
|
||||
}
|
||||
}
|
||||
ScrollBar.vertical: ScrollBar {}
|
||||
}
|
||||
|
||||
+26
-33
@@ -1,23 +1,16 @@
|
||||
import QtQuick 2.0
|
||||
import QtQuick.Controls 2.2
|
||||
import QtQuick.Dialogs 1.3
|
||||
|
||||
import ComputerManager 1.0
|
||||
import Session 1.0
|
||||
|
||||
Item {
|
||||
property string appName
|
||||
property var quitRunningAppFn
|
||||
property Session nextSession : null
|
||||
property string nextAppName : ""
|
||||
|
||||
property string stageText : "Quitting " + appName + "..."
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
// The StackView will trigger a visibility change when
|
||||
// we're pushed onto it, causing our onVisibleChanged
|
||||
// routine to run, but only if we start as invisible
|
||||
visible: false
|
||||
property string stageText : qsTr("Quitting %1...").arg(appName)
|
||||
|
||||
function quitAppCompleted(error)
|
||||
{
|
||||
@@ -25,38 +18,42 @@ Item {
|
||||
if (error !== undefined) {
|
||||
errorDialog.text = error
|
||||
errorDialog.open()
|
||||
console.error(error)
|
||||
}
|
||||
|
||||
// Exit this view
|
||||
stackView.pop()
|
||||
|
||||
// If we're supposed to launch another game after this, do so now
|
||||
if (error === undefined && nextSession !== null) {
|
||||
var component = Qt.createComponent("StreamSegue.qml")
|
||||
var segue = component.createObject(stackView)
|
||||
segue.appName = nextAppName
|
||||
segue.session = nextSession
|
||||
stackView.push(segue)
|
||||
var segue = component.createObject(stackView, {"appName": nextAppName, "session": nextSession})
|
||||
stackView.replace(segue)
|
||||
}
|
||||
else {
|
||||
// Exit this view
|
||||
stackView.pop()
|
||||
}
|
||||
}
|
||||
|
||||
onVisibleChanged: {
|
||||
if (visible) {
|
||||
// Connect the quit completion signal
|
||||
ComputerManager.quitAppCompleted.connect(quitAppCompleted)
|
||||
StackView.onActivated: {
|
||||
// Hide the toolbar before we start loading
|
||||
toolBar.visible = false
|
||||
|
||||
// We must be polling or we won't get the quitAppCompleted() callback
|
||||
ComputerManager.startPolling()
|
||||
}
|
||||
else {
|
||||
// Disconnect the signal
|
||||
ComputerManager.quitAppCompleted.disconnect(quitAppCompleted)
|
||||
// Connect the quit completion signal
|
||||
ComputerManager.quitAppCompleted.connect(quitAppCompleted)
|
||||
|
||||
// Stop polling when this view is not on top
|
||||
ComputerManager.stopPollingAsync()
|
||||
// Start the quit operation if requested
|
||||
if (quitRunningAppFn) {
|
||||
quitRunningAppFn()
|
||||
}
|
||||
}
|
||||
|
||||
StackView.onDeactivating: {
|
||||
// Show the toolbar again
|
||||
toolBar.visible = true
|
||||
|
||||
// Disconnect the signal
|
||||
ComputerManager.quitAppCompleted.disconnect(quitAppCompleted)
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: 5
|
||||
@@ -73,14 +70,10 @@ Item {
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
|
||||
wrapMode: Text.Wrap
|
||||
color: "white"
|
||||
}
|
||||
}
|
||||
|
||||
MessageDialog {
|
||||
ErrorMessageDialog {
|
||||
id: errorDialog
|
||||
modality:Qt.WindowModal
|
||||
icon: StandardIcon.Critical
|
||||
standardButtons: StandardButton.Ok
|
||||
}
|
||||
}
|
||||
|
||||
+1318
-142
File diff suppressed because it is too large
Load Diff
+142
-39
@@ -1,31 +1,32 @@
|
||||
import QtQuick 2.0
|
||||
import QtQuick.Controls 2.2
|
||||
import QtQuick.Dialogs 1.3
|
||||
import QtQuick.Window 2.2
|
||||
|
||||
import SdlGamepadKeyNavigation 1.0
|
||||
import Session 1.0
|
||||
|
||||
Item {
|
||||
property Session session
|
||||
property string appName
|
||||
property string stageText : "Starting " + appName + "..."
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
// The StackView will trigger a visibility change when
|
||||
// we're pushed onto it, causing our onVisibleChanged
|
||||
// routine to run, but only if we start as invisible
|
||||
visible: false
|
||||
property string stageText : isResume ? qsTr("Resuming %1...").arg(appName) :
|
||||
qsTr("Starting %1...").arg(appName)
|
||||
property bool isResume : false
|
||||
property bool quitAfter : false
|
||||
|
||||
function stageStarting(stage)
|
||||
{
|
||||
// Update the spinner text
|
||||
stageText = "Starting " + stage + "..."
|
||||
stageText = qsTr("Starting %1...").arg(stage)
|
||||
}
|
||||
|
||||
function stageFailed(stage, errorCode)
|
||||
function stageFailed(stage, errorCode, failingPorts)
|
||||
{
|
||||
errorDialog.text = "Starting " + stage + " failed: Error " + errorCode
|
||||
errorDialog.open()
|
||||
// Display the error dialog after Session::exec() returns
|
||||
streamSegueErrorDialog.text = qsTr("Starting %1 failed: Error %2").arg(stage).arg(errorCode)
|
||||
|
||||
if (failingPorts) {
|
||||
streamSegueErrorDialog.text += "\n\n" + qsTr("Check your firewall and port forwarding rules for port(s): %1").arg(failingPorts)
|
||||
}
|
||||
}
|
||||
|
||||
function connectionStarted()
|
||||
@@ -34,6 +35,7 @@ Item {
|
||||
// see them briefly when we pop off the StackView
|
||||
stageSpinner.visible = false
|
||||
stageLabel.visible = false
|
||||
hintText.visible = false
|
||||
|
||||
// Hide the window now that streaming has begun
|
||||
window.visible = false
|
||||
@@ -41,8 +43,9 @@ Item {
|
||||
|
||||
function displayLaunchError(text)
|
||||
{
|
||||
errorDialog.text = text
|
||||
errorDialog.open()
|
||||
// Display the error dialog after Session::exec() returns
|
||||
streamSegueErrorDialog.text = text
|
||||
console.error(text)
|
||||
}
|
||||
|
||||
function displayLaunchWarning(text)
|
||||
@@ -50,37 +53,133 @@ Item {
|
||||
// This toast appears for 3 seconds, just shorter than how long
|
||||
// Session will wait for it to be displayed. This gives it time
|
||||
// to transition to invisible before continuing.
|
||||
var toast = Qt.createQmlObject('import QtQuick.Controls 2.3; ToolTip {}', parent, '')
|
||||
var toast = Qt.createQmlObject('import QtQuick.Controls 2.2; ToolTip {}', parent, '')
|
||||
toast.text = text
|
||||
toast.timeout = 3000
|
||||
toast.visible = true
|
||||
console.warn(text)
|
||||
}
|
||||
|
||||
onVisibleChanged: {
|
||||
if (visible) {
|
||||
// Hide the toolbar before we start loading
|
||||
toolBar.visible = false
|
||||
function quitStarting()
|
||||
{
|
||||
// Avoid the push transition animation
|
||||
var component = Qt.createComponent("QuitSegue.qml")
|
||||
stackView.replace(stackView.currentItem, component.createObject(stackView, {"appName": appName}), StackView.Immediate)
|
||||
|
||||
// Hook up our signals
|
||||
session.stageStarting.connect(stageStarting)
|
||||
session.stageFailed.connect(stageFailed)
|
||||
session.connectionStarted.connect(connectionStarted)
|
||||
session.displayLaunchError.connect(displayLaunchError)
|
||||
session.displayLaunchWarning.connect(displayLaunchWarning)
|
||||
// Show the Qt window again to show quit segue
|
||||
window.visible = true
|
||||
}
|
||||
|
||||
// Run the streaming session to completion
|
||||
session.exec()
|
||||
function sessionFinished(portTestResult)
|
||||
{
|
||||
if (portTestResult !== 0 && portTestResult !== -1 && streamSegueErrorDialog.text) {
|
||||
streamSegueErrorDialog.text += "\n\n" + qsTr("This PC's Internet connection is blocking Moonlight. Streaming over the Internet may not work while connected to this network.")
|
||||
}
|
||||
|
||||
// Enable GUI gamepad usage now
|
||||
SdlGamepadKeyNavigation.enable()
|
||||
|
||||
if (quitAfter) {
|
||||
if (streamSegueErrorDialog.text) {
|
||||
// Quit when the error dialog is acknowledged
|
||||
streamSegueErrorDialog.quitAfter = quitAfter
|
||||
streamSegueErrorDialog.open()
|
||||
}
|
||||
else {
|
||||
// Quit immediately
|
||||
Qt.quit()
|
||||
}
|
||||
} else {
|
||||
// Exit this view
|
||||
stackView.pop()
|
||||
|
||||
// Show the Qt window again after streaming
|
||||
window.visible = true
|
||||
|
||||
// Exit this view
|
||||
stackView.pop()
|
||||
// Display any launch errors. We do this after
|
||||
// the Qt UI is visible again to prevent losing
|
||||
// focus on the dialog which would impact gamepad
|
||||
// users.
|
||||
if (streamSegueErrorDialog.text) {
|
||||
streamSegueErrorDialog.quitAfter = quitAfter
|
||||
streamSegueErrorDialog.open()
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Show the toolbar again when we become hidden
|
||||
toolBar.visible = true
|
||||
}
|
||||
|
||||
function sessionReadyForDeletion()
|
||||
{
|
||||
// Garbage collect the Session object since it's pretty heavyweight
|
||||
// and keeps other libraries (like SDL_TTF) around until it is deleted.
|
||||
session = null
|
||||
gc()
|
||||
}
|
||||
|
||||
StackView.onDeactivating: {
|
||||
// Show the toolbar again when popped off the stack
|
||||
toolBar.visible = true
|
||||
|
||||
// Enable GUI gamepad usage now
|
||||
SdlGamepadKeyNavigation.enable()
|
||||
}
|
||||
|
||||
StackView.onActivated: {
|
||||
// Hide the toolbar before we start loading
|
||||
toolBar.visible = false
|
||||
|
||||
// Hook up our signals
|
||||
session.stageStarting.connect(stageStarting)
|
||||
session.stageFailed.connect(stageFailed)
|
||||
session.connectionStarted.connect(connectionStarted)
|
||||
session.displayLaunchError.connect(displayLaunchError)
|
||||
session.displayLaunchWarning.connect(displayLaunchWarning)
|
||||
session.quitStarting.connect(quitStarting)
|
||||
session.sessionFinished.connect(sessionFinished)
|
||||
session.readyForDeletion.connect(sessionReadyForDeletion)
|
||||
|
||||
// Kick off the stream
|
||||
spinnerTimer.start()
|
||||
streamLoader.active = true
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: spinnerTimer
|
||||
|
||||
// Display the spinner appearance a bit to allow us to reach
|
||||
// the code in Session.exec() that pumps the event loop.
|
||||
// If we display it immediately, it will briefly hang in the
|
||||
// middle of the animation on Windows, which looks very
|
||||
// obviously broken.
|
||||
interval: 100
|
||||
onTriggered: stageSpinner.running = true
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: streamLoader
|
||||
active: false
|
||||
asynchronous: true
|
||||
|
||||
onLoaded: {
|
||||
// Set the hint text. We do this here rather than
|
||||
// in the hintText control itself to synchronize
|
||||
// with Session.exec() which requires no concurrent
|
||||
// gamepad usage.
|
||||
hintText.text = qsTr("Tip:") + " " + qsTr("Press %1 to disconnect your session").arg(SdlGamepadKeyNavigation.getConnectedGamepads() > 0 ?
|
||||
qsTr("Start+Select+L1+R1") : qsTr("Ctrl+Alt+Shift+Q"))
|
||||
|
||||
// Stop GUI gamepad usage now
|
||||
SdlGamepadKeyNavigation.disable()
|
||||
|
||||
// Garbage collect QML stuff before we start streaming,
|
||||
// since we'll probably be streaming for a while and we
|
||||
// won't be able to GC during the stream.
|
||||
gc()
|
||||
|
||||
// Run the streaming session to completion
|
||||
session.exec(Screen.virtualX, Screen.virtualY)
|
||||
}
|
||||
|
||||
sourceComponent: Item {}
|
||||
}
|
||||
|
||||
Row {
|
||||
@@ -89,6 +188,7 @@ Item {
|
||||
|
||||
BusyIndicator {
|
||||
id: stageSpinner
|
||||
running: false
|
||||
}
|
||||
|
||||
Label {
|
||||
@@ -99,14 +199,17 @@ Item {
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
|
||||
wrapMode: Text.Wrap
|
||||
color: "white"
|
||||
}
|
||||
}
|
||||
|
||||
MessageDialog {
|
||||
id: errorDialog
|
||||
modality:Qt.WindowModal
|
||||
icon: StandardIcon.Critical
|
||||
standardButtons: StandardButton.Ok
|
||||
Label {
|
||||
id: hintText
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: 50
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
font.pointSize: 18
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
}
|
||||
|
||||
+184
-32
@@ -7,7 +7,7 @@ AppModel::AppModel(QObject *parent)
|
||||
this, &AppModel::handleBoxArtLoaded);
|
||||
}
|
||||
|
||||
void AppModel::initialize(ComputerManager* computerManager, int computerIndex)
|
||||
void AppModel::initialize(ComputerManager* computerManager, int computerIndex, bool showHiddenGames)
|
||||
{
|
||||
m_ComputerManager = computerManager;
|
||||
connect(m_ComputerManager, &ComputerManager::computerStateChanged,
|
||||
@@ -15,29 +15,23 @@ void AppModel::initialize(ComputerManager* computerManager, int computerIndex)
|
||||
|
||||
Q_ASSERT(computerIndex < m_ComputerManager->getComputers().count());
|
||||
m_Computer = m_ComputerManager->getComputers().at(computerIndex);
|
||||
m_Apps = m_Computer->appList;
|
||||
m_CurrentGameId = m_Computer->currentGameId;
|
||||
m_ShowHiddenGames = showHiddenGames;
|
||||
|
||||
updateAppList(m_Computer->appList);
|
||||
}
|
||||
|
||||
int AppModel::getRunningAppIndex()
|
||||
int AppModel::getRunningAppId()
|
||||
{
|
||||
if (m_CurrentGameId != 0) {
|
||||
for (int i = 0; i < m_Apps.count(); i++) {
|
||||
if (m_Apps[i].id == m_CurrentGameId) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
return m_CurrentGameId;
|
||||
}
|
||||
|
||||
QString AppModel::getRunningAppName()
|
||||
{
|
||||
if (m_CurrentGameId != 0) {
|
||||
for (int i = 0; i < m_Apps.count(); i++) {
|
||||
if (m_Apps[i].id == m_CurrentGameId) {
|
||||
return m_Apps[i].name;
|
||||
for (int i = 0; i < m_AllApps.count(); i++) {
|
||||
if (m_AllApps[i].id == m_CurrentGameId) {
|
||||
return m_AllApps[i].name;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -47,12 +41,23 @@ QString AppModel::getRunningAppName()
|
||||
|
||||
Session* AppModel::createSessionForApp(int appIndex)
|
||||
{
|
||||
Q_ASSERT(appIndex < m_Apps.count());
|
||||
NvApp app = m_Apps.at(appIndex);
|
||||
Q_ASSERT(appIndex < m_VisibleApps.count());
|
||||
NvApp app = m_VisibleApps.at(appIndex);
|
||||
|
||||
return new Session(m_Computer, app);
|
||||
}
|
||||
|
||||
int AppModel::getDirectLaunchAppIndex()
|
||||
{
|
||||
for (int i = 0; i < m_VisibleApps.count(); i++) {
|
||||
if (m_VisibleApps[i].directLaunch) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
int AppModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
// For list models only the root node (an invalid parent) should return the list's size. For all
|
||||
@@ -60,7 +65,7 @@ int AppModel::rowCount(const QModelIndex &parent) const
|
||||
if (parent.isValid())
|
||||
return 0;
|
||||
|
||||
return m_Apps.count();
|
||||
return m_VisibleApps.count();
|
||||
}
|
||||
|
||||
QVariant AppModel::data(const QModelIndex &index, int role) const
|
||||
@@ -68,8 +73,8 @@ QVariant AppModel::data(const QModelIndex &index, int role) const
|
||||
if (!index.isValid())
|
||||
return QVariant();
|
||||
|
||||
Q_ASSERT(index.row() < m_Apps.count());
|
||||
NvApp app = m_Apps.at(index.row());
|
||||
Q_ASSERT(index.row() < m_VisibleApps.count());
|
||||
NvApp app = m_VisibleApps.at(index.row());
|
||||
|
||||
switch (role)
|
||||
{
|
||||
@@ -80,6 +85,14 @@ QVariant AppModel::data(const QModelIndex &index, int role) const
|
||||
case BoxArtRole:
|
||||
// FIXME: const-correctness
|
||||
return const_cast<BoxArtManager&>(m_BoxArtManager).loadBoxArt(m_Computer, app);
|
||||
case HiddenRole:
|
||||
return app.hidden;
|
||||
case AppIdRole:
|
||||
return app.id;
|
||||
case DirectLaunchRole:
|
||||
return app.directLaunch;
|
||||
case AppCollectorGameRole:
|
||||
return app.isAppCollectorGame;
|
||||
default:
|
||||
return QVariant();
|
||||
}
|
||||
@@ -92,6 +105,10 @@ QHash<int, QByteArray> AppModel::roleNames() const
|
||||
names[NameRole] = "name";
|
||||
names[RunningRole] = "running";
|
||||
names[BoxArtRole] = "boxart";
|
||||
names[HiddenRole] = "hidden";
|
||||
names[AppIdRole] = "appid";
|
||||
names[DirectLaunchRole] = "directLaunch";
|
||||
names[AppCollectorGameRole] = "appCollectorGame";
|
||||
|
||||
return names;
|
||||
}
|
||||
@@ -101,6 +118,138 @@ void AppModel::quitRunningApp()
|
||||
m_ComputerManager->quitRunningApp(m_Computer);
|
||||
}
|
||||
|
||||
bool AppModel::isAppCurrentlyVisible(const NvApp& app)
|
||||
{
|
||||
for (const NvApp& visibleApp : m_VisibleApps) {
|
||||
if (app.id == visibleApp.id) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
QVector<NvApp> AppModel::getVisibleApps(const QVector<NvApp>& appList)
|
||||
{
|
||||
QVector<NvApp> visibleApps;
|
||||
|
||||
for (const NvApp& app : appList) {
|
||||
// Don't immediately hide games that were previously visible. This
|
||||
// allows users to easily uncheck the "Hide App" checkbox if they
|
||||
// check it by mistake.
|
||||
if (m_ShowHiddenGames || !app.hidden || isAppCurrentlyVisible(app)) {
|
||||
visibleApps.append(app);
|
||||
}
|
||||
}
|
||||
|
||||
return visibleApps;
|
||||
}
|
||||
|
||||
void AppModel::updateAppList(QVector<NvApp> newList)
|
||||
{
|
||||
m_AllApps = newList;
|
||||
|
||||
QVector<NvApp> newVisibleList = getVisibleApps(newList);
|
||||
|
||||
// Process removals and updates first
|
||||
for (int i = 0; i < m_VisibleApps.count(); i++) {
|
||||
const NvApp& existingApp = m_VisibleApps.at(i);
|
||||
|
||||
bool found = false;
|
||||
for (const NvApp& newApp : newVisibleList) {
|
||||
if (existingApp.id == newApp.id) {
|
||||
// If the data changed, update it in our list
|
||||
if (existingApp != newApp) {
|
||||
m_VisibleApps.replace(i, newApp);
|
||||
emit dataChanged(createIndex(i, 0), createIndex(i, 0));
|
||||
}
|
||||
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
beginRemoveRows(QModelIndex(), i, i);
|
||||
m_VisibleApps.removeAt(i);
|
||||
endRemoveRows();
|
||||
i--;
|
||||
}
|
||||
}
|
||||
|
||||
// Process additions now
|
||||
for (const NvApp& newApp : newVisibleList) {
|
||||
int insertionIndex = m_VisibleApps.size();
|
||||
bool found = false;
|
||||
|
||||
for (int i = 0; i < m_VisibleApps.count(); i++) {
|
||||
const NvApp& existingApp = m_VisibleApps.at(i);
|
||||
|
||||
if (existingApp.id == newApp.id) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
else if (existingApp.name.toLower() > newApp.name.toLower()) {
|
||||
insertionIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
beginInsertRows(QModelIndex(), insertionIndex, insertionIndex);
|
||||
m_VisibleApps.insert(insertionIndex, newApp);
|
||||
endInsertRows();
|
||||
}
|
||||
}
|
||||
|
||||
Q_ASSERT(newVisibleList == m_VisibleApps);
|
||||
}
|
||||
|
||||
void AppModel::setAppHidden(int appIndex, bool hidden)
|
||||
{
|
||||
Q_ASSERT(appIndex < m_VisibleApps.count());
|
||||
int appId = m_VisibleApps.at(appIndex).id;
|
||||
|
||||
{
|
||||
QWriteLocker lock(&m_Computer->lock);
|
||||
|
||||
for (NvApp& app : m_Computer->appList) {
|
||||
if (app.id == appId) {
|
||||
app.hidden = hidden;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m_ComputerManager->clientSideAttributeUpdated(m_Computer);
|
||||
}
|
||||
|
||||
void AppModel::setAppDirectLaunch(int appIndex, bool directLaunch)
|
||||
{
|
||||
Q_ASSERT(appIndex < m_VisibleApps.count());
|
||||
int appId = m_VisibleApps.at(appIndex).id;
|
||||
|
||||
{
|
||||
QWriteLocker lock(&m_Computer->lock);
|
||||
|
||||
for (NvApp& app : m_Computer->appList) {
|
||||
if (directLaunch) {
|
||||
// We must clear direct launch from all other apps
|
||||
// to set it on the new app.
|
||||
app.directLaunch = app.id == appId;
|
||||
}
|
||||
else if (app.id == appId) {
|
||||
// If we're clearing direct launch, we're done once we
|
||||
// find our matching app ID.
|
||||
app.directLaunch = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m_ComputerManager->clientSideAttributeUpdated(m_Computer);
|
||||
}
|
||||
|
||||
void AppModel::handleComputerStateChanged(NvComputer* computer)
|
||||
{
|
||||
// Ignore updates for computers that aren't ours
|
||||
@@ -108,23 +257,26 @@ void AppModel::handleComputerStateChanged(NvComputer* computer)
|
||||
return;
|
||||
}
|
||||
|
||||
// If the computer has gone offline or we've been unpaired,
|
||||
// signal the UI so we can go back to the PC view.
|
||||
if (m_Computer->state == NvComputer::CS_OFFLINE ||
|
||||
m_Computer->pairState == NvComputer::PS_NOT_PAIRED) {
|
||||
emit computerLost();
|
||||
return;
|
||||
}
|
||||
|
||||
// First, process additions/removals from the app list. This
|
||||
// is required because the new game may now be running, so
|
||||
// we can't check that first.
|
||||
if (computer->appList != m_Apps) {
|
||||
// Just reset the whole thing if the list changes
|
||||
beginResetModel();
|
||||
m_Apps = computer->appList;
|
||||
m_CurrentGameId = computer->currentGameId;
|
||||
endResetModel();
|
||||
return;
|
||||
if (computer->appList != m_AllApps) {
|
||||
updateAppList(computer->appList);
|
||||
}
|
||||
|
||||
// Finally, process changes to the active app
|
||||
if (computer->currentGameId != m_CurrentGameId) {
|
||||
// First, invalidate the running state of newly running game
|
||||
for (int i = 0; i < m_Apps.count(); i++) {
|
||||
if (m_Apps[i].id == computer->currentGameId) {
|
||||
for (int i = 0; i < m_VisibleApps.count(); i++) {
|
||||
if (m_VisibleApps[i].id == computer->currentGameId) {
|
||||
emit dataChanged(createIndex(i, 0),
|
||||
createIndex(i, 0),
|
||||
QVector<int>() << RunningRole);
|
||||
@@ -134,8 +286,8 @@ void AppModel::handleComputerStateChanged(NvComputer* computer)
|
||||
|
||||
// Next, invalidate the running state of the old game (if it exists)
|
||||
if (m_CurrentGameId != 0) {
|
||||
for (int i = 0; i < m_Apps.count(); i++) {
|
||||
if (m_Apps[i].id == m_CurrentGameId) {
|
||||
for (int i = 0; i < m_VisibleApps.count(); i++) {
|
||||
if (m_VisibleApps[i].id == m_CurrentGameId) {
|
||||
emit dataChanged(createIndex(i, 0),
|
||||
createIndex(i, 0),
|
||||
QVector<int>() << RunningRole);
|
||||
@@ -153,7 +305,7 @@ void AppModel::handleBoxArtLoaded(NvComputer* computer, NvApp app, QUrl /* image
|
||||
{
|
||||
Q_ASSERT(computer == m_Computer);
|
||||
|
||||
int index = m_Apps.indexOf(app);
|
||||
int index = m_VisibleApps.indexOf(app);
|
||||
|
||||
// Make sure we're not delivering a callback to an app that's already been removed
|
||||
if (index >= 0) {
|
||||
|
||||
+25
-5
@@ -2,7 +2,7 @@
|
||||
|
||||
#include "backend/boxartmanager.h"
|
||||
#include "backend/computermanager.h"
|
||||
#include "streaming/session.hpp"
|
||||
#include "streaming/session.h"
|
||||
|
||||
#include <QAbstractListModel>
|
||||
|
||||
@@ -14,23 +14,33 @@ class AppModel : public QAbstractListModel
|
||||
{
|
||||
NameRole = Qt::UserRole,
|
||||
RunningRole,
|
||||
BoxArtRole
|
||||
BoxArtRole,
|
||||
HiddenRole,
|
||||
AppIdRole,
|
||||
DirectLaunchRole,
|
||||
AppCollectorGameRole,
|
||||
};
|
||||
|
||||
public:
|
||||
explicit AppModel(QObject *parent = nullptr);
|
||||
|
||||
// Must be called before any QAbstractListModel functions
|
||||
Q_INVOKABLE void initialize(ComputerManager* computerManager, int computerIndex);
|
||||
Q_INVOKABLE void initialize(ComputerManager* computerManager, int computerIndex, bool showHiddenGames);
|
||||
|
||||
Q_INVOKABLE Session* createSessionForApp(int appIndex);
|
||||
|
||||
Q_INVOKABLE int getRunningAppIndex();
|
||||
Q_INVOKABLE int getDirectLaunchAppIndex();
|
||||
|
||||
Q_INVOKABLE int getRunningAppId();
|
||||
|
||||
Q_INVOKABLE QString getRunningAppName();
|
||||
|
||||
Q_INVOKABLE void quitRunningApp();
|
||||
|
||||
Q_INVOKABLE void setAppHidden(int appIndex, bool hidden);
|
||||
|
||||
Q_INVOKABLE void setAppDirectLaunch(int appIndex, bool directLaunch);
|
||||
|
||||
QVariant data(const QModelIndex &index, int role) const override;
|
||||
|
||||
int rowCount(const QModelIndex &parent) const override;
|
||||
@@ -42,10 +52,20 @@ private slots:
|
||||
|
||||
void handleBoxArtLoaded(NvComputer* computer, NvApp app, QUrl image);
|
||||
|
||||
signals:
|
||||
void computerLost();
|
||||
|
||||
private:
|
||||
void updateAppList(QVector<NvApp> newList);
|
||||
|
||||
QVector<NvApp> getVisibleApps(const QVector<NvApp>& appList);
|
||||
|
||||
bool isAppCurrentlyVisible(const NvApp& app);
|
||||
|
||||
NvComputer* m_Computer;
|
||||
BoxArtManager m_BoxArtManager;
|
||||
ComputerManager* m_ComputerManager;
|
||||
QVector<NvApp> m_Apps;
|
||||
QVector<NvApp> m_VisibleApps, m_AllApps;
|
||||
int m_CurrentGameId;
|
||||
bool m_ShowHiddenGames;
|
||||
};
|
||||
|
||||
+77
-31
@@ -1,5 +1,7 @@
|
||||
#include "computermodel.h"
|
||||
|
||||
#include <QThreadPool>
|
||||
|
||||
ComputerModel::ComputerModel(QObject* object)
|
||||
: QAbstractListModel(object) {}
|
||||
|
||||
@@ -20,18 +22,6 @@ QVariant ComputerModel::data(const QModelIndex& index, int role) const
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
if (index.row() == m_Computers.count()) {
|
||||
// We insert a synthetic item at the end for the Add PC option
|
||||
switch (role) {
|
||||
case NameRole:
|
||||
return "Add PC";
|
||||
case AddPcRole:
|
||||
return true;
|
||||
default:
|
||||
return QVariant();
|
||||
}
|
||||
}
|
||||
|
||||
Q_ASSERT(index.row() < m_Computers.count());
|
||||
|
||||
NvComputer* computer = m_Computers[index.row()];
|
||||
@@ -48,10 +38,10 @@ QVariant ComputerModel::data(const QModelIndex& index, int role) const
|
||||
return computer->currentGameId != 0;
|
||||
case WakeableRole:
|
||||
return !computer->macAddress.isEmpty();
|
||||
case AddPcRole:
|
||||
return false;
|
||||
case StatusUnknownRole:
|
||||
return computer->state == NvComputer::CS_UNKNOWN;
|
||||
case ServerSupportedRole:
|
||||
return computer->isSupportedServerVersion;
|
||||
default:
|
||||
return QVariant();
|
||||
}
|
||||
@@ -65,8 +55,7 @@ int ComputerModel::rowCount(const QModelIndex& parent) const
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Add PC placeholder counts as 1
|
||||
return m_Computers.count() + 1;
|
||||
return m_Computers.count();
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> ComputerModel::roleNames() const
|
||||
@@ -77,9 +66,9 @@ QHash<int, QByteArray> ComputerModel::roleNames() const
|
||||
names[OnlineRole] = "online";
|
||||
names[PairedRole] = "paired";
|
||||
names[BusyRole] = "busy";
|
||||
names[AddPcRole] = "addPc";
|
||||
names[WakeableRole] = "wakeable";
|
||||
names[StatusUnknownRole] = "statusUnknown";
|
||||
names[ServerSupportedRole] = "serverSupported";
|
||||
|
||||
return names;
|
||||
}
|
||||
@@ -119,11 +108,68 @@ void ComputerModel::deleteComputer(int computerIndex)
|
||||
endRemoveRows();
|
||||
}
|
||||
|
||||
bool ComputerModel::wakeComputer(int computerIndex)
|
||||
class DeferredWakeHostTask : public QRunnable
|
||||
{
|
||||
public:
|
||||
DeferredWakeHostTask(NvComputer* computer)
|
||||
: m_Computer(computer) {}
|
||||
|
||||
void run()
|
||||
{
|
||||
m_Computer->wake();
|
||||
}
|
||||
|
||||
private:
|
||||
NvComputer* m_Computer;
|
||||
};
|
||||
|
||||
void ComputerModel::wakeComputer(int computerIndex)
|
||||
{
|
||||
Q_ASSERT(computerIndex < m_Computers.count());
|
||||
|
||||
return m_Computers[computerIndex]->wake();
|
||||
DeferredWakeHostTask* wakeTask = new DeferredWakeHostTask(m_Computers[computerIndex]);
|
||||
QThreadPool::globalInstance()->start(wakeTask);
|
||||
}
|
||||
|
||||
void ComputerModel::renameComputer(int computerIndex, QString name)
|
||||
{
|
||||
Q_ASSERT(computerIndex < m_Computers.count());
|
||||
|
||||
m_ComputerManager->renameHost(m_Computers[computerIndex], name);
|
||||
}
|
||||
|
||||
QString ComputerModel::generatePinString()
|
||||
{
|
||||
return m_ComputerManager->generatePinString();
|
||||
}
|
||||
|
||||
class DeferredTestConnectionTask : public QObject, public QRunnable
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
void run()
|
||||
{
|
||||
unsigned int portTestResult = LiTestClientConnectivity("qt.conntest.moonlight-stream.org", 443, ML_PORT_FLAG_ALL);
|
||||
if (portTestResult == ML_TEST_RESULT_INCONCLUSIVE) {
|
||||
emit connectionTestCompleted(-1, QString());
|
||||
}
|
||||
else {
|
||||
char blockedPorts[512];
|
||||
LiStringifyPortFlags(portTestResult, "\n", blockedPorts, sizeof(blockedPorts));
|
||||
emit connectionTestCompleted(portTestResult, QString(blockedPorts));
|
||||
}
|
||||
}
|
||||
|
||||
signals:
|
||||
void connectionTestCompleted(int result, QString blockedPorts);
|
||||
};
|
||||
|
||||
void ComputerModel::testConnectionForComputer(int)
|
||||
{
|
||||
DeferredTestConnectionTask* testConnectionTask = new DeferredTestConnectionTask();
|
||||
QObject::connect(testConnectionTask, &DeferredTestConnectionTask::connectionTestCompleted,
|
||||
this, &ComputerModel::connectionTestCompleted);
|
||||
QThreadPool::globalInstance()->start(testConnectionTask);
|
||||
}
|
||||
|
||||
void ComputerModel::pairComputer(int computerIndex, QString pin)
|
||||
@@ -135,24 +181,24 @@ void ComputerModel::pairComputer(int computerIndex, QString pin)
|
||||
|
||||
void ComputerModel::handlePairingCompleted(NvComputer*, QString error)
|
||||
{
|
||||
emit pairingCompleted(error.isNull() ? QVariant() : error);
|
||||
emit pairingCompleted(error.isEmpty() ? QVariant() : 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));
|
||||
}
|
||||
}
|
||||
|
||||
#include "computermodel.moc"
|
||||
|
||||
+10
-3
@@ -1,5 +1,5 @@
|
||||
#include "backend/computermanager.h"
|
||||
#include "streaming/session.hpp"
|
||||
#include "streaming/session.h"
|
||||
|
||||
#include <QAbstractListModel>
|
||||
|
||||
@@ -15,7 +15,7 @@ class ComputerModel : public QAbstractListModel
|
||||
BusyRole,
|
||||
WakeableRole,
|
||||
StatusUnknownRole,
|
||||
AddPcRole
|
||||
ServerSupportedRole
|
||||
};
|
||||
|
||||
public:
|
||||
@@ -32,14 +32,21 @@ public:
|
||||
|
||||
Q_INVOKABLE void deleteComputer(int computerIndex);
|
||||
|
||||
Q_INVOKABLE QString generatePinString();
|
||||
|
||||
Q_INVOKABLE void pairComputer(int computerIndex, QString pin);
|
||||
|
||||
Q_INVOKABLE bool wakeComputer(int computerIndex);
|
||||
Q_INVOKABLE void testConnectionForComputer(int computerIndex);
|
||||
|
||||
Q_INVOKABLE void wakeComputer(int computerIndex);
|
||||
|
||||
Q_INVOKABLE void renameComputer(int computerIndex, QString name);
|
||||
|
||||
Q_INVOKABLE Session* createSessionForCurrentGame(int computerIndex);
|
||||
|
||||
signals:
|
||||
void pairingCompleted(QVariant error);
|
||||
void connectionTestCompleted(int result, QString blockedPorts);
|
||||
|
||||
private slots:
|
||||
void handleComputerStateChanged(NvComputer* computer);
|
||||
|
||||
+470
-37
@@ -1,28 +1,210 @@
|
||||
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 QtQuick.Controls.Material 2.1
|
||||
import ComputerManager 1.0
|
||||
import AutoUpdateChecker 1.0
|
||||
import StreamingPreferences 1.0
|
||||
import SystemProperties 1.0
|
||||
import SdlGamepadKeyNavigation 1.0
|
||||
|
||||
ApplicationWindow {
|
||||
property bool pollingActive: false
|
||||
|
||||
// Set by SettingsView to force the back operation to pop all
|
||||
// pages except the initial view. This is required when doing
|
||||
// a retranslate() because AppView breaks for some reason.
|
||||
property bool clearOnBack: false
|
||||
|
||||
id: window
|
||||
visible: true
|
||||
width: 1280
|
||||
height: 600
|
||||
|
||||
Material.theme: Material.Dark
|
||||
Material.accent: Material.Purple
|
||||
// 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"
|
||||
else if (StreamingPreferences.uiDisplayMode == StreamingPreferences.UI_MAXIMIZED) return "Maximized"
|
||||
else if (StreamingPreferences.uiDisplayMode == StreamingPreferences.UI_FULLSCREEN) return "FullScreen"
|
||||
} else {
|
||||
return "FullScreen"
|
||||
}
|
||||
}
|
||||
|
||||
// This configures the maximum width of the singleton attached QML ToolTip. If left unconstrained,
|
||||
// it will never insert a line break and just extend on forever.
|
||||
ToolTip.toolTip.contentWidth: ToolTip.toolTip.implicitContentWidth < 400 ? ToolTip.toolTip.implicitContentWidth : 400
|
||||
|
||||
function goBack() {
|
||||
if (clearOnBack) {
|
||||
// Pop all items except the first one
|
||||
stackView.pop(null)
|
||||
clearOnBack = false
|
||||
}
|
||||
else {
|
||||
stackView.pop()
|
||||
}
|
||||
}
|
||||
|
||||
StackView {
|
||||
id: stackView
|
||||
initialItem: "PcView.qml"
|
||||
initialItem: initialView
|
||||
anchors.fill: parent
|
||||
focus: true
|
||||
|
||||
onCurrentItemChanged: {
|
||||
// Ensure focus travels to the next view when going back
|
||||
if (currentItem) {
|
||||
currentItem.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onEscapePressed: {
|
||||
if (depth > 1) {
|
||||
goBack()
|
||||
}
|
||||
else {
|
||||
quitConfirmationDialog.open()
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onBackPressed: {
|
||||
if (depth > 1) {
|
||||
goBack()
|
||||
}
|
||||
else {
|
||||
quitConfirmationDialog.open()
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onMenuPressed: {
|
||||
settingsButton.clicked()
|
||||
}
|
||||
|
||||
// This is a keypress we've reserved for letting the
|
||||
// SdlGamepadKeyNavigation object tell us to show settings
|
||||
// when Menu is consumed by a focused control.
|
||||
Keys.onHangupPressed: {
|
||||
settingsButton.clicked()
|
||||
}
|
||||
}
|
||||
|
||||
function navigateTo(url, objectName)
|
||||
// This timer keeps us polling for 5 minutes of inactivity
|
||||
// to allow the user to work with Moonlight on a second display
|
||||
// while dealing with configuration issues. This will ensure
|
||||
// machines come online even if the input focus isn't on Moonlight.
|
||||
Timer {
|
||||
id: inactivityTimer
|
||||
interval: 5 * 60000
|
||||
onTriggered: {
|
||||
if (!active && pollingActive) {
|
||||
ComputerManager.stopPollingAsync()
|
||||
pollingActive = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onVisibleChanged: {
|
||||
// When we become invisible while streaming is going on,
|
||||
// stop polling immediately.
|
||||
if (!visible) {
|
||||
inactivityTimer.stop()
|
||||
|
||||
if (pollingActive) {
|
||||
ComputerManager.stopPollingAsync()
|
||||
pollingActive = false
|
||||
}
|
||||
}
|
||||
else if (active) {
|
||||
// When we become visible and active again, start polling
|
||||
inactivityTimer.stop()
|
||||
|
||||
// Restart polling if it was stopped
|
||||
if (!pollingActive) {
|
||||
ComputerManager.startPolling()
|
||||
pollingActive = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onActiveChanged: {
|
||||
if (active) {
|
||||
// Stop the inactivity timer
|
||||
inactivityTimer.stop()
|
||||
|
||||
// Restart polling if it was stopped
|
||||
if (!pollingActive) {
|
||||
ComputerManager.startPolling()
|
||||
pollingActive = true
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Start the inactivity timer to stop polling
|
||||
// if focus does not return within a few minutes.
|
||||
inactivityTimer.restart()
|
||||
}
|
||||
}
|
||||
|
||||
property bool initialized: false
|
||||
|
||||
// BUG: Using onAfterSynchronizing: here causes very strange
|
||||
// failures on Linux. Many shaders fail to compile and we
|
||||
// eventually segfault deep inside the Qt OpenGL code.
|
||||
onAfterRendering: {
|
||||
// We use this callback to trigger dialog display because
|
||||
// it only happens once the window is fully constructed.
|
||||
// Doing it earlier can lead to the dialog appearing behind
|
||||
// the window or otherwise without input focus.
|
||||
if (!initialized) {
|
||||
// Set initialized before calling anything else, because
|
||||
// pumping the event loop can cause us to get another
|
||||
// onAfterRendering call and potentially reenter this code.
|
||||
initialized = true;
|
||||
|
||||
if (SystemProperties.isWow64) {
|
||||
wow64Dialog.open()
|
||||
}
|
||||
else if (!SystemProperties.hasHardwareAcceleration) {
|
||||
if (SystemProperties.isRunningXWayland) {
|
||||
xWaylandDialog.open()
|
||||
}
|
||||
else {
|
||||
noHwDecoderDialog.open()
|
||||
}
|
||||
}
|
||||
|
||||
if (SystemProperties.unmappedGamepads) {
|
||||
unmappedGamepadDialog.unmappedGamepads = SystemProperties.unmappedGamepads
|
||||
unmappedGamepadDialog.open()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Workaround for lack of instanceof in Qt 5.9.
|
||||
//
|
||||
// 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) {
|
||||
@@ -37,70 +219,321 @@ ApplicationWindow {
|
||||
|
||||
header: ToolBar {
|
||||
id: toolBar
|
||||
height: 60
|
||||
anchors.topMargin: 5
|
||||
anchors.bottomMargin: 5
|
||||
|
||||
Label {
|
||||
id: titleLabel
|
||||
visible: toolBar.width > 700
|
||||
anchors.fill: parent
|
||||
text: stackView.currentItem.objectName
|
||||
font.pointSize: 20
|
||||
elide: Label.ElideRight
|
||||
horizontalAlignment: Qt.AlignHCenter
|
||||
verticalAlignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: 20
|
||||
spacing: 10
|
||||
anchors.leftMargin: 10
|
||||
anchors.rightMargin: 10
|
||||
anchors.fill: parent
|
||||
|
||||
ToolButton {
|
||||
NavigableToolButton {
|
||||
// Only make the button visible if the user has navigated somewhere.
|
||||
visible: stackView.depth > 1
|
||||
|
||||
icon.source: "qrc:/res/arrow_left.svg"
|
||||
onClicked: stackView.pop()
|
||||
iconSource: "qrc:/res/arrow_left.svg"
|
||||
|
||||
Menu {
|
||||
id: backButton
|
||||
x: parent.width - width
|
||||
transformOrigin: Menu.TopRight
|
||||
onClicked: goBack()
|
||||
|
||||
Keys.onDownPressed: {
|
||||
stackView.currentItem.forceActiveFocus(Qt.TabFocus)
|
||||
}
|
||||
}
|
||||
|
||||
// This label will appear when the window gets too small and
|
||||
// we need to ensure the toolbar controls don't collide
|
||||
Label {
|
||||
id: titleLabel
|
||||
text: stackView.currentItem.objectName
|
||||
font.pointSize: 15
|
||||
id: titleRowLabel
|
||||
font.pointSize: titleLabel.font.pointSize
|
||||
elide: Label.ElideRight
|
||||
horizontalAlignment: Qt.AlignHCenter
|
||||
verticalAlignment: Qt.AlignVCenter
|
||||
Layout.fillWidth: true
|
||||
|
||||
// We need this label to always be visible so it can occupy
|
||||
// the remaining space in the RowLayout. To "hide" it, we
|
||||
// just set the text to empty string.
|
||||
text: !titleLabel.visible ? stackView.currentItem.objectName : ""
|
||||
}
|
||||
|
||||
ToolButton {
|
||||
icon.source: "qrc:/res/question_mark.svg"
|
||||
Label {
|
||||
id: versionLabel
|
||||
visible: qmltypeof(stackView.currentItem, "SettingsView")
|
||||
text: qsTr("Version %1").arg(SystemProperties.versionString)
|
||||
font.pointSize: 12
|
||||
horizontalAlignment: Qt.AlignRight
|
||||
verticalAlignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
NavigableToolButton {
|
||||
id: discordButton
|
||||
visible: SystemProperties.hasBrowser &&
|
||||
qmltypeof(stackView.currentItem, "SettingsView")
|
||||
|
||||
iconSource: "qrc:/res/Discord-Logo-White.svg"
|
||||
|
||||
ToolTip.delay: 1000
|
||||
ToolTip.timeout: 3000
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.text: qsTr("Join our community on Discord")
|
||||
|
||||
// TODO need to make sure browser is brought to foreground.
|
||||
onClicked: Qt.openUrlExternally("https://moonlight-stream.org/discord");
|
||||
|
||||
Keys.onDownPressed: {
|
||||
stackView.currentItem.forceActiveFocus(Qt.TabFocus)
|
||||
}
|
||||
}
|
||||
|
||||
NavigableToolButton {
|
||||
id: addPcButton
|
||||
visible: qmltypeof(stackView.currentItem, "PcView")
|
||||
|
||||
iconSource: "qrc:/res/ic_add_to_queue_white_48px.svg"
|
||||
|
||||
ToolTip.delay: 1000
|
||||
ToolTip.timeout: 3000
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.text: qsTr("Add PC manually") + (newPcShortcut.nativeText ? (" ("+newPcShortcut.nativeText+")") : "")
|
||||
|
||||
Shortcut {
|
||||
id: newPcShortcut
|
||||
sequence: StandardKey.New
|
||||
onActivated: addPcButton.clicked()
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
addPcDialog.open()
|
||||
}
|
||||
|
||||
Keys.onDownPressed: {
|
||||
stackView.currentItem.forceActiveFocus(Qt.TabFocus)
|
||||
}
|
||||
}
|
||||
|
||||
NavigableToolButton {
|
||||
property string browserUrl: ""
|
||||
|
||||
id: updateButton
|
||||
|
||||
iconSource: "qrc:/res/update.svg"
|
||||
|
||||
ToolTip.delay: 1000
|
||||
ToolTip.timeout: 3000
|
||||
ToolTip.visible: hovered || visible
|
||||
|
||||
// Invisible until we get a callback notifying us that
|
||||
// an update is available
|
||||
visible: false
|
||||
|
||||
onClicked: {
|
||||
if (SystemProperties.hasBrowser) {
|
||||
Qt.openUrlExternally(browserUrl);
|
||||
}
|
||||
}
|
||||
|
||||
function updateAvailable(version, url)
|
||||
{
|
||||
ToolTip.text = qsTr("Update available for Moonlight: Version %1").arg(version)
|
||||
updateButton.browserUrl = url
|
||||
updateButton.visible = true
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
AutoUpdateChecker.onUpdateAvailable.connect(updateAvailable)
|
||||
AutoUpdateChecker.start()
|
||||
}
|
||||
|
||||
Keys.onDownPressed: {
|
||||
stackView.currentItem.forceActiveFocus(Qt.TabFocus)
|
||||
}
|
||||
}
|
||||
|
||||
NavigableToolButton {
|
||||
id: helpButton
|
||||
visible: SystemProperties.hasBrowser
|
||||
|
||||
iconSource: "qrc:/res/question_mark.svg"
|
||||
|
||||
ToolTip.delay: 1000
|
||||
ToolTip.timeout: 3000
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.text: qsTr("Help") + (helpShortcut.nativeText ? (" ("+helpShortcut.nativeText+")") : "")
|
||||
|
||||
Shortcut {
|
||||
id: helpShortcut
|
||||
sequence: StandardKey.HelpContents
|
||||
onActivated: helpButton.clicked()
|
||||
}
|
||||
|
||||
// TODO need to make sure browser is brought to foreground.
|
||||
onClicked: Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Setup-Guide");
|
||||
|
||||
Menu {
|
||||
id: helpButton
|
||||
x: parent.width
|
||||
transformOrigin: Menu.TopRight
|
||||
Keys.onDownPressed: {
|
||||
stackView.currentItem.forceActiveFocus(Qt.TabFocus)
|
||||
}
|
||||
}
|
||||
|
||||
ToolButton {
|
||||
NavigableToolButton {
|
||||
// TODO: Implement gamepad mapping then unhide this button
|
||||
visible: false
|
||||
|
||||
icon.source: "qrc:/res/ic_videogame_asset_white_48px.svg"
|
||||
onClicked: navigateTo("qrc:/gui/GamepadMapper.qml", "Gamepad Mapping")
|
||||
ToolTip.delay: 1000
|
||||
ToolTip.timeout: 3000
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.text: qsTr("Gamepad Mapper")
|
||||
|
||||
Menu {
|
||||
id: gamepadMappingMenu
|
||||
x: parent.width
|
||||
transformOrigin: Menu.TopRight
|
||||
iconSource: "qrc:/res/ic_videogame_asset_white_48px.svg"
|
||||
|
||||
onClicked: navigateTo("qrc:/gui/GamepadMapper.qml", "GamepadMapper")
|
||||
|
||||
Keys.onDownPressed: {
|
||||
stackView.currentItem.forceActiveFocus(Qt.TabFocus)
|
||||
}
|
||||
}
|
||||
|
||||
ToolButton {
|
||||
icon.source: "qrc:/res/settings.svg"
|
||||
onClicked: navigateTo("qrc:/gui/SettingsView.qml", "Settings")
|
||||
NavigableToolButton {
|
||||
id: settingsButton
|
||||
|
||||
Menu {
|
||||
id: optionsMenu
|
||||
x: parent.width
|
||||
transformOrigin: Menu.TopRight
|
||||
iconSource: "qrc:/res/settings.svg"
|
||||
|
||||
onClicked: navigateTo("qrc:/gui/SettingsView.qml", "SettingsView")
|
||||
|
||||
Keys.onDownPressed: {
|
||||
stackView.currentItem.forceActiveFocus(Qt.TabFocus)
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
id: settingsShortcut
|
||||
sequence: StandardKey.Preferences
|
||||
onActivated: settingsButton.clicked()
|
||||
}
|
||||
|
||||
ToolTip.delay: 1000
|
||||
ToolTip.timeout: 3000
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.text: qsTr("Settings") + (settingsShortcut.nativeText ? (" ("+settingsShortcut.nativeText+")") : "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ErrorMessageDialog {
|
||||
id: noHwDecoderDialog
|
||||
text: qsTr("No functioning hardware accelerated video decoder was detected by Moonlight. " +
|
||||
"Your streaming performance may be severely degraded in this configuration.")
|
||||
helpText: qsTr("Click the Help button for more information on solving this problem.")
|
||||
helpUrl: "https://github.com/moonlight-stream/moonlight-docs/wiki/Fixing-Hardware-Decoding-Problems"
|
||||
}
|
||||
|
||||
ErrorMessageDialog {
|
||||
id: xWaylandDialog
|
||||
text: qsTr("Hardware acceleration doesn't work on XWayland. Continuing on XWayland may result in poor streaming performance. " +
|
||||
"Try running with QT_QPA_PLATFORM=wayland or switch to X11.")
|
||||
helpText: qsTr("Click the Help button for more information.")
|
||||
helpUrl: "https://github.com/moonlight-stream/moonlight-docs/wiki/Fixing-Hardware-Decoding-Problems"
|
||||
}
|
||||
|
||||
NavigableMessageDialog {
|
||||
id: wow64Dialog
|
||||
standardButtons: Dialog.Ok | Dialog.Cancel
|
||||
text: qsTr("This version of Moonlight isn't optimized for your PC. Please download the '%1' version of Moonlight for the best streaming performance.").arg(SystemProperties.friendlyNativeArchName)
|
||||
onAccepted: {
|
||||
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-qt/releases");
|
||||
}
|
||||
}
|
||||
|
||||
ErrorMessageDialog {
|
||||
id: unmappedGamepadDialog
|
||||
property string unmappedGamepads : ""
|
||||
text: qsTr("Moonlight detected gamepads without a mapping:") + "\n" + unmappedGamepads
|
||||
helpTextSeparator: "\n\n"
|
||||
helpText: qsTr("Click the Help button for information on how to map your gamepads.")
|
||||
helpUrl: "https://github.com/moonlight-stream/moonlight-docs/wiki/Gamepad-Mapping"
|
||||
}
|
||||
|
||||
// This dialog appears when quitting via keyboard or gamepad button
|
||||
NavigableMessageDialog {
|
||||
id: quitConfirmationDialog
|
||||
standardButtons: Dialog.Yes | Dialog.No
|
||||
text: qsTr("Are you sure you want to quit?")
|
||||
// For keyboard/gamepad navigation
|
||||
onAccepted: Qt.quit()
|
||||
}
|
||||
|
||||
// HACK: This belongs in StreamSegue but keeping a dialog around after the parent
|
||||
// dies can trigger bugs in Qt 5.12 that cause the app to crash. For now, we will
|
||||
// host this dialog in a QML component that is never destroyed.
|
||||
//
|
||||
// To repro: Start a stream, cut the network connection to trigger the "Connection
|
||||
// terminated" dialog, wait until the app grid times out back to the PC grid, then
|
||||
// try to dismiss the dialog.
|
||||
ErrorMessageDialog {
|
||||
id: streamSegueErrorDialog
|
||||
|
||||
property bool quitAfter: false
|
||||
|
||||
onClosed: {
|
||||
if (quitAfter) {
|
||||
Qt.quit()
|
||||
}
|
||||
|
||||
// StreamSegue assumes its dialog will be re-created each time we
|
||||
// start streaming, so fake it by wiping out the text each time.
|
||||
text = ""
|
||||
}
|
||||
}
|
||||
|
||||
NavigableDialog {
|
||||
id: addPcDialog
|
||||
property string label: qsTr("Enter the IP address of your host PC:")
|
||||
|
||||
standardButtons: Dialog.Ok | Dialog.Cancel
|
||||
|
||||
onOpened: {
|
||||
// Force keyboard focus on the textbox so keyboard navigation works
|
||||
editText.forceActiveFocus()
|
||||
}
|
||||
|
||||
onClosed: {
|
||||
editText.clear()
|
||||
}
|
||||
|
||||
onAccepted: {
|
||||
if (editText.text) {
|
||||
ComputerManager.addNewHostManually(editText.text.trim())
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Label {
|
||||
text: addPcDialog.label
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
TextField {
|
||||
id: editText
|
||||
Layout.fillWidth: true
|
||||
focus: true
|
||||
|
||||
Keys.onReturnPressed: {
|
||||
addPcDialog.accept()
|
||||
}
|
||||
|
||||
Keys.onEnterPressed: {
|
||||
addPcDialog.accept()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,282 @@
|
||||
#include "sdlgamepadkeynavigation.h"
|
||||
|
||||
#include <QKeyEvent>
|
||||
#include <QGuiApplication>
|
||||
#include <QWindow>
|
||||
|
||||
#include "settings/streamingpreferences.h"
|
||||
#include "settings/mappingmanager.h"
|
||||
|
||||
#define AXIS_NAVIGATION_REPEAT_DELAY 150
|
||||
|
||||
SdlGamepadKeyNavigation::SdlGamepadKeyNavigation()
|
||||
: m_Enabled(false),
|
||||
m_UiNavMode(false),
|
||||
m_FirstPoll(false),
|
||||
m_LastAxisNavigationEventTime(0)
|
||||
{
|
||||
m_PollingTimer = new QTimer(this);
|
||||
connect(m_PollingTimer, &QTimer::timeout, this, &SdlGamepadKeyNavigation::onPollingTimerFired);
|
||||
}
|
||||
|
||||
SdlGamepadKeyNavigation::~SdlGamepadKeyNavigation()
|
||||
{
|
||||
disable();
|
||||
}
|
||||
|
||||
void SdlGamepadKeyNavigation::enable()
|
||||
{
|
||||
if (m_Enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We have to initialize and uninitialize this in enable()/disable()
|
||||
// because we need to get out of the way of the Session class. If it
|
||||
// doesn't get to reinitialize the GC subsystem, it won't get initial
|
||||
// arrival events. Additionally, there's a race condition between
|
||||
// our QML objects being destroyed and SDL being deinitialized that
|
||||
// this solves too.
|
||||
if (SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) != 0) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) failed: %s",
|
||||
SDL_GetError());
|
||||
return;
|
||||
}
|
||||
|
||||
MappingManager mappingManager;
|
||||
mappingManager.applyMappings();
|
||||
|
||||
// Drop all pending gamepad add events. SDL will generate these for us
|
||||
// on first init of the GC subsystem. We can't depend on them due to
|
||||
// overlapping lifetimes of SdlGamepadKeyNavigation instances, so we
|
||||
// will attach ourselves.
|
||||
SDL_PumpEvents();
|
||||
SDL_FlushEvent(SDL_CONTROLLERDEVICEADDED);
|
||||
|
||||
// Open all currently attached game controllers
|
||||
for (int i = 0; i < SDL_NumJoysticks(); i++) {
|
||||
if (SDL_IsGameController(i)) {
|
||||
SDL_GameController* gc = SDL_GameControllerOpen(i);
|
||||
if (gc != nullptr) {
|
||||
m_Gamepads.append(gc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Flush events on the first poll
|
||||
m_FirstPoll = true;
|
||||
|
||||
// Poll every 50 ms for a new joystick event
|
||||
m_PollingTimer->start(50);
|
||||
|
||||
m_Enabled = true;
|
||||
}
|
||||
|
||||
void SdlGamepadKeyNavigation::disable()
|
||||
{
|
||||
if (!m_Enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_PollingTimer->stop();
|
||||
|
||||
while (!m_Gamepads.isEmpty()) {
|
||||
SDL_GameControllerClose(m_Gamepads[0]);
|
||||
m_Gamepads.removeAt(0);
|
||||
}
|
||||
|
||||
SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER);
|
||||
|
||||
m_Enabled = false;
|
||||
}
|
||||
|
||||
void SdlGamepadKeyNavigation::onPollingTimerFired()
|
||||
{
|
||||
SDL_Event event;
|
||||
StreamingPreferences prefs;
|
||||
|
||||
// Discard any pending button events on the first poll to avoid picking up
|
||||
// stale input data from the stream session (like the quit combo).
|
||||
if (m_FirstPoll) {
|
||||
SDL_PumpEvents();
|
||||
SDL_FlushEvent(SDL_CONTROLLERBUTTONDOWN);
|
||||
SDL_FlushEvent(SDL_CONTROLLERBUTTONUP);
|
||||
|
||||
m_FirstPoll = false;
|
||||
}
|
||||
|
||||
while (SDL_PollEvent(&event)) {
|
||||
switch (event.type) {
|
||||
case SDL_QUIT:
|
||||
// SDL may send us a quit event since we initialize
|
||||
// the video subsystem on startup. If we get one,
|
||||
// forward it on for Qt to take care of.
|
||||
QCoreApplication::instance()->quit();
|
||||
break;
|
||||
case SDL_CONTROLLERBUTTONDOWN:
|
||||
case SDL_CONTROLLERBUTTONUP:
|
||||
{
|
||||
QEvent::Type type =
|
||||
event.type == SDL_CONTROLLERBUTTONDOWN ?
|
||||
QEvent::Type::KeyPress : QEvent::Type::KeyRelease;
|
||||
|
||||
// Swap face buttons if needed
|
||||
if (prefs.swapFaceButtons) {
|
||||
switch (event.cbutton.button) {
|
||||
case SDL_CONTROLLER_BUTTON_A:
|
||||
event.cbutton.button = SDL_CONTROLLER_BUTTON_B;
|
||||
break;
|
||||
case SDL_CONTROLLER_BUTTON_B:
|
||||
event.cbutton.button = SDL_CONTROLLER_BUTTON_A;
|
||||
break;
|
||||
case SDL_CONTROLLER_BUTTON_X:
|
||||
event.cbutton.button = SDL_CONTROLLER_BUTTON_Y;
|
||||
break;
|
||||
case SDL_CONTROLLER_BUTTON_Y:
|
||||
event.cbutton.button = SDL_CONTROLLER_BUTTON_X;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
switch (event.cbutton.button) {
|
||||
case SDL_CONTROLLER_BUTTON_DPAD_UP:
|
||||
if (m_UiNavMode) {
|
||||
// Back-tab
|
||||
sendKey(type, Qt::Key_Tab, Qt::ShiftModifier);
|
||||
}
|
||||
else {
|
||||
sendKey(type, Qt::Key_Up);
|
||||
}
|
||||
break;
|
||||
case SDL_CONTROLLER_BUTTON_DPAD_DOWN:
|
||||
if (m_UiNavMode) {
|
||||
sendKey(type, Qt::Key_Tab);
|
||||
}
|
||||
else {
|
||||
sendKey(type, Qt::Key_Down);
|
||||
}
|
||||
break;
|
||||
case SDL_CONTROLLER_BUTTON_DPAD_LEFT:
|
||||
sendKey(type, Qt::Key_Left);
|
||||
break;
|
||||
case SDL_CONTROLLER_BUTTON_DPAD_RIGHT:
|
||||
sendKey(type, Qt::Key_Right);
|
||||
break;
|
||||
case SDL_CONTROLLER_BUTTON_A:
|
||||
if (m_UiNavMode) {
|
||||
sendKey(type, Qt::Key_Space);
|
||||
}
|
||||
else {
|
||||
sendKey(type, Qt::Key_Return);
|
||||
}
|
||||
break;
|
||||
case SDL_CONTROLLER_BUTTON_B:
|
||||
sendKey(type, Qt::Key_Escape);
|
||||
break;
|
||||
case SDL_CONTROLLER_BUTTON_X:
|
||||
sendKey(type, Qt::Key_Menu);
|
||||
break;
|
||||
case SDL_CONTROLLER_BUTTON_Y:
|
||||
case SDL_CONTROLLER_BUTTON_START:
|
||||
// HACK: We use this keycode to inform main.qml
|
||||
// to show the settings when Key_Menu is handled
|
||||
// by the control in focus.
|
||||
sendKey(type, Qt::Key_Hangup);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case SDL_CONTROLLERDEVICEADDED:
|
||||
SDL_GameController* gc = SDL_GameControllerOpen(event.cdevice.which);
|
||||
if (gc != nullptr) {
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle analog sticks by polling
|
||||
for (auto gc : m_Gamepads) {
|
||||
short leftX = SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_LEFTX);
|
||||
short leftY = SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_LEFTY);
|
||||
if (SDL_GetTicks() - m_LastAxisNavigationEventTime < AXIS_NAVIGATION_REPEAT_DELAY) {
|
||||
// Do nothing
|
||||
}
|
||||
else if (leftY < -30000) {
|
||||
if (m_UiNavMode) {
|
||||
// Back-tab
|
||||
sendKey(QEvent::Type::KeyPress, Qt::Key_Tab, Qt::ShiftModifier);
|
||||
sendKey(QEvent::Type::KeyRelease, Qt::Key_Tab, Qt::ShiftModifier);
|
||||
}
|
||||
else {
|
||||
sendKey(QEvent::Type::KeyPress, Qt::Key_Up);
|
||||
sendKey(QEvent::Type::KeyRelease, Qt::Key_Up);
|
||||
}
|
||||
|
||||
m_LastAxisNavigationEventTime = SDL_GetTicks();
|
||||
}
|
||||
else if (leftY > 30000) {
|
||||
if (m_UiNavMode) {
|
||||
sendKey(QEvent::Type::KeyPress, Qt::Key_Tab);
|
||||
sendKey(QEvent::Type::KeyRelease, Qt::Key_Tab);
|
||||
}
|
||||
else {
|
||||
sendKey(QEvent::Type::KeyPress, Qt::Key_Down);
|
||||
sendKey(QEvent::Type::KeyRelease, Qt::Key_Down);
|
||||
}
|
||||
|
||||
m_LastAxisNavigationEventTime = SDL_GetTicks();
|
||||
}
|
||||
else if (leftX < -30000) {
|
||||
sendKey(QEvent::Type::KeyPress, Qt::Key_Left);
|
||||
sendKey(QEvent::Type::KeyRelease, Qt::Key_Left);
|
||||
m_LastAxisNavigationEventTime = SDL_GetTicks();
|
||||
}
|
||||
else if (leftX > 30000) {
|
||||
sendKey(QEvent::Type::KeyPress, Qt::Key_Right);
|
||||
sendKey(QEvent::Type::KeyRelease, Qt::Key_Right);
|
||||
m_LastAxisNavigationEventTime = SDL_GetTicks();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SdlGamepadKeyNavigation::sendKey(QEvent::Type type, Qt::Key key, Qt::KeyboardModifiers modifiers)
|
||||
{
|
||||
QGuiApplication* app = static_cast<QGuiApplication*>(QGuiApplication::instance());
|
||||
QWindow* focusWindow = app->focusWindow();
|
||||
if (focusWindow != nullptr) {
|
||||
QKeyEvent keyPressEvent(type, key, modifiers);
|
||||
app->sendEvent(focusWindow, &keyPressEvent);
|
||||
}
|
||||
}
|
||||
|
||||
void SdlGamepadKeyNavigation::setUiNavMode(bool uiNavMode)
|
||||
{
|
||||
m_UiNavMode = uiNavMode;
|
||||
}
|
||||
|
||||
int SdlGamepadKeyNavigation::getConnectedGamepads()
|
||||
{
|
||||
Q_ASSERT(m_Enabled);
|
||||
|
||||
int count = 0;
|
||||
for (int i = 0; i < SDL_NumJoysticks(); i++) {
|
||||
if (SDL_IsGameController(i)) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
#pragma once
|
||||
|
||||
#include <QTimer>
|
||||
#include <QEvent>
|
||||
|
||||
#include <SDL.h>
|
||||
|
||||
class SdlGamepadKeyNavigation : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
SdlGamepadKeyNavigation();
|
||||
|
||||
~SdlGamepadKeyNavigation();
|
||||
|
||||
Q_INVOKABLE void enable();
|
||||
|
||||
Q_INVOKABLE void disable();
|
||||
|
||||
Q_INVOKABLE void setUiNavMode(bool settingsMode);
|
||||
|
||||
Q_INVOKABLE int getConnectedGamepads();
|
||||
|
||||
private:
|
||||
void sendKey(QEvent::Type type, Qt::Key key, Qt::KeyboardModifiers modifiers = Qt::NoModifier);
|
||||
|
||||
private slots:
|
||||
void onPollingTimerFired();
|
||||
|
||||
private:
|
||||
QTimer* m_PollingTimer;
|
||||
QList<SDL_GameController*> m_Gamepads;
|
||||
bool m_Enabled;
|
||||
bool m_UiNavMode;
|
||||
bool m_FirstPoll;
|
||||
Uint32 m_LastAxisNavigationEventTime;
|
||||
};
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user