Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
@@ -1,8 +1,12 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us address broken functionality
|
||||
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.
|
||||
@@ -16,24 +20,35 @@ If applicable, add screenshots to help explain your problem. If the issue is rel
|
||||
**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 1803]
|
||||
- Moonlight Version: [e.g. v0.2.0]
|
||||
- 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 1803]
|
||||
- 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, logs print to the terminal when running via `flatpak run com.moonlight_stream.Moonlight`
|
||||
- On Linux with the Flatpak, logs print to the terminal when running with the command: `flatpak run com.moonlight_stream.Moonlight`
|
||||
- On Linux with the Snap, logs print to the terminal when running with the command: `moonlight`
|
||||
|
||||
**Additional context**
|
||||
Anything else you think may be relevant to the issue
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
blank_issues_enabled: false
|
||||
@@ -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!
|
||||
@@ -9,7 +9,11 @@
|
||||
url = https://github.com/gabomdq/SDL_GameControllerDB.git
|
||||
[submodule "soundio/libsoundio"]
|
||||
path = soundio/libsoundio
|
||||
url = https://github.com/andrewrk/libsoundio.git
|
||||
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
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
language: cpp
|
||||
|
||||
git:
|
||||
depth: 1
|
||||
|
||||
env:
|
||||
- NJOBS=2
|
||||
|
||||
matrix:
|
||||
include:
|
||||
- os: osx
|
||||
compiler: clang
|
||||
- os: linux
|
||||
sudo: required
|
||||
dist: xenial
|
||||
compiler: gcc
|
||||
addons:
|
||||
apt:
|
||||
sources:
|
||||
- sourceline: 'ppa:beineri/opt-qt597-xenial'
|
||||
- sourceline: 'ppa:jonathonf/ffmpeg-4'
|
||||
packages:
|
||||
- qt59base
|
||||
- qt59quickcontrols2
|
||||
- qt59svg
|
||||
- libesd0-dev
|
||||
- libgl-dev
|
||||
- libegl1-mesa-dev
|
||||
- libxkbcommon-dev
|
||||
- wayland-protocols
|
||||
- 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.9 && wget https://www.libsdl.org/release/SDL2-$SDL2_VER.tar.gz && tar -xvf SDL2-$SDL2_VER.tar.gz && cd SDL2-$SDL2_VER && ./configure && make -j$NJOBS && sudo make install && cd ..; fi
|
||||
- if [[ "$TRAVIS_OS_NAME" = "linux" ]]; then export SDL2_TTF_VER=2.0.15 && wget https://www.libsdl.org/projects/SDL_ttf/release/SDL2_ttf-$SDL2_TTF_VER.tar.gz && tar -xvf SDL2_ttf-$SDL2_TTF_VER.tar.gz && cd SDL2_ttf-$SDL2_TTF_VER && ./configure && make -j$NJOBS && 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 -j$NJOBS debug
|
||||
- make -j$NJOBS release
|
||||
@@ -3,11 +3,7 @@ QT -= core gui
|
||||
TARGET = AntiHooking
|
||||
TEMPLATE = lib
|
||||
|
||||
# Support debug and release builds from command line for CI
|
||||
CONFIG += debug_and_release
|
||||
|
||||
# Ensure symbols are always generated
|
||||
CONFIG += force_debug_info
|
||||
include(../globaldefs.pri)
|
||||
|
||||
INCLUDEPATH += $$PWD/../libs/windows/include
|
||||
contains(QT_ARCH, i386) {
|
||||
|
||||
@@ -131,10 +131,81 @@ private:
|
||||
static LoadLibraryExWFunc s_RealLoadLibraryExW;
|
||||
|
||||
static constexpr LPCWSTR k_BlacklistedDlls[] = {
|
||||
// This DLL shipped with ASUS Sonic Radar 3 improperly handles
|
||||
// D3D9 exclusive fullscreen in a way that causes CreateDeviceEx()
|
||||
// to deadlock. https://github.com/moonlight-stream/moonlight-qt/issues/102
|
||||
L"NahimicOSD.dll"
|
||||
// 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",
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -2,59 +2,74 @@
|
||||
|
||||
[Moonlight PC](https://moonlight-stream.org) is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield, but built to run on Windows, Mac, and Linux. This client is the successor to [Moonlight Chrome](https://github.com/moonlight-stream/moonlight-chrome) for streaming on PC.
|
||||
|
||||
Moonlight also has mobile versions for [Android](https://github.com/moonlight-stream/moonlight-android) and [iOS](https://github.com/moonlight-stream/moonlight-ios).
|
||||
Moonlight also has mobile versions for [Android](https://github.com/moonlight-stream/moonlight-android) and [iOS](https://github.com/moonlight-stream/moonlight-ios).
|
||||
|
||||
You can follow development on our [Discord server](https://discord.gg/6ERtzFY).
|
||||
You can follow development on our [Discord server](https://moonlight-stream.org/discord).
|
||||
|
||||
[](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)
|
||||
|
||||
## Features
|
||||
- Hardware accelerated video decoding on Windows, Mac, and Linux
|
||||
- Supports streaming at up to 120 FPS (high refresh rate monitor recommended)
|
||||
- Supports streaming at 720p, 1080p, 1440p, 4K, and the client PC's native screen resolution
|
||||
- 5.1 surround sound audio
|
||||
- 7.1 surround sound audio support
|
||||
- Support for both pointer capture (for games) and direct mouse control (for remote desktop)
|
||||
- Support for passing system-wide keyboard shortcuts like Alt+Tab to the host
|
||||
- Direct touchscreen mouse control
|
||||
- HEVC support for better image quality at reduced bandwidth
|
||||
- Keyboard and mouse support
|
||||
- Gamepad support with SDL gamepad mappings
|
||||
- Gamepad support with force feedback for up to 4 players
|
||||
|
||||
## Downloads
|
||||
- [Windows and macOS](https://github.com/moonlight-stream/moonlight-qt/releases)
|
||||
- [Windows, macOS, and Steam Link](https://github.com/moonlight-stream/moonlight-qt/releases)
|
||||
- [Snap (for Ubuntu-based Linux distros)](https://snapcraft.io/moonlight)
|
||||
- [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)
|
||||
|
||||
## Building
|
||||
### General Build Requirements
|
||||
* Qt 5.9 SDK or later
|
||||
|
||||
### Windows-specific Requirements
|
||||
### Windows Build Requirements
|
||||
* Qt 5.15 SDK or later
|
||||
* Windows 7 or later
|
||||
* [Visual Studio](https://visualstudio.microsoft.com/downloads/) 2015 or later (Community edition is fine)
|
||||
* Select MSVC Desktop toolchain during Qt installation
|
||||
* [DirectX SDK](https://www.microsoft.com/en-us/download/details.aspx?id=6812)
|
||||
* [Visual Studio 2019](https://visualstudio.microsoft.com/downloads/) (Community edition is fine)
|
||||
* Select **MSVC 2019** option during Qt installation. MinGW is not supported.
|
||||
* [7-Zip](https://www.7-zip.org/) (only if building installers for non-development PCs)
|
||||
* [WiX Toolset](http://wixtoolset.org/releases/) v3.11 or later (only if building installers for non-development PCs)
|
||||
* [WiX Toolset](https://wixtoolset.org/releases/) v3.11 or later (only if building installers for non-development PCs)
|
||||
|
||||
### Mac-specific Requirements
|
||||
* macOS Sierra (10.12) or later
|
||||
* Xcode 10
|
||||
### macOS Build Requirements
|
||||
* Qt 5.15 SDK or later
|
||||
* macOS High Sierra (10.13) or later
|
||||
* Xcode 11
|
||||
* [create-dmg](https://github.com/sindresorhus/create-dmg) (only if building DMGs for use on non-development Macs)
|
||||
|
||||
### Linux-specific Requirements
|
||||
### Linux/Unix Build Requirements
|
||||
* Qt 5.9 SDK or later
|
||||
* GCC or Clang
|
||||
* Install your distro equivalents of: `openssl-devel qt5-devel SDL2-devel ffmpeg-devel qt5-qtquickcontrols2-devel libva-devel libvdpau-devel opus-devel pulseaudio-libs-devel alsa-lib-devel SDL2_ttf-devel`
|
||||
* FFmpeg 4.0 is required to build. If your distro doesn't package FFmpeg 4.0, you can build and install it from source on http://ffmpeg.org/
|
||||
* Install the required packages:
|
||||
* Debian/Ubuntu: `libegl1-mesa-dev libgl1-mesa-dev libopus-dev libqt5svg5-dev libsdl2-dev libsdl2-ttf-dev libssl-dev libavcodec-dev libva-dev libvdpau-dev libxkbcommon-dev qt5-default qt5-qmake qtbase5-dev qtdeclarative5-dev qtquickcontrols2-5-dev wayland-protocols qml-module-qtquick-controls2 qml-module-qtquick-layouts qml-module-qtquick-window2 qml-module-qtquick2`
|
||||
* RedHat/Fedora: `openssl-devel qt5-devel SDL2-devel ffmpeg-devel qt5-qtquickcontrols2-devel libva-devel libvdpau-devel opus-devel pulseaudio-libs-devel alsa-lib-devel SDL2_ttf-devel`
|
||||
* FFmpeg 4.0+ is required to build. If your distro doesn't package FFmpeg 4.0 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.
|
||||
* To build a binary for use on non-development machines, use the scripts in the `scripts` folder.
|
||||
* For Windows builds, use `scripts\generate-installers.bat`. Execute this script from the root of the repository within a Qt command prompt. Ensure WiX and 7-Zip binary directories are in your `%PATH%`.
|
||||
* For Windows builds, use `scripts\build-arch.bat` and `scripts\generate-bundle.bat`. Execute these scripts from the root of the repository within a Qt command prompt. Ensure WiX and 7-Zip binary directories are in your `%PATH%`.
|
||||
* For macOS builds, use `scripts/generate-dmg.sh`. Execute this script from the root of the repository and ensure Qt's `bin` folder is in your `$PATH`.
|
||||
* For Steam Link builds, run `scripts/build-steamlink-app.sh` from the root of the repository.
|
||||
* To build from the command line for development use, run `qmake moonlight-qt.pro` then `make debug` or `make release`
|
||||
* To 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.
|
||||
|
||||
## Contribute
|
||||
1. Fork us
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>10.11</string>
|
||||
<string>10.13</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string>NSApplication</string>
|
||||
<key>NSSupportsAutomaticGraphicsSwitching</key>
|
||||
|
||||
@@ -8,11 +8,7 @@ unix:!macx {
|
||||
TARGET = Moonlight
|
||||
}
|
||||
|
||||
# Support debug and release builds from command line for CI
|
||||
CONFIG += debug_and_release
|
||||
|
||||
# Ensure symbols are always generated
|
||||
CONFIG += force_debug_info
|
||||
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
|
||||
@@ -37,20 +33,19 @@ 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 \
|
||||
$$(DXSDK_DIR)/Include
|
||||
INCLUDEPATH += $$PWD/../libs/windows/include
|
||||
|
||||
contains(QT_ARCH, i386) {
|
||||
LIBS += -L$$PWD/../libs/windows/lib/x86
|
||||
LIBS += -L$$(DXSDK_DIR)/Lib/x86
|
||||
}
|
||||
contains(QT_ARCH, x86_64) {
|
||||
LIBS += -L$$PWD/../libs/windows/lib/x64
|
||||
LIBS += -L$$(DXSDK_DIR)/Lib/x64
|
||||
}
|
||||
contains(QT_ARCH, arm64) {
|
||||
LIBS += -L$$PWD/../libs/windows/lib/arm64
|
||||
}
|
||||
|
||||
LIBS += ws2_32.lib winmm.lib dxva2.lib ole32.lib gdi32.lib user32.lib d3d9.lib dwmapi.lib dbghelp.lib
|
||||
LIBS += ws2_32.lib winmm.lib dxva2.lib ole32.lib gdi32.lib user32.lib d3d9.lib dwmapi.lib dbghelp.lib qwave.lib
|
||||
}
|
||||
macx {
|
||||
INCLUDEPATH += $$PWD/../libs/mac/include
|
||||
@@ -60,17 +55,9 @@ macx {
|
||||
}
|
||||
|
||||
unix:!macx {
|
||||
CONFIG += link_pkgconfig soundio
|
||||
CONFIG += link_pkgconfig
|
||||
PKGCONFIG += openssl sdl2 SDL2_ttf opus
|
||||
|
||||
# For libsoundio
|
||||
packagesExist(libpulse) {
|
||||
PKGCONFIG += libpulse
|
||||
}
|
||||
packagesExist(alsa) {
|
||||
PKGCONFIG += alsa
|
||||
}
|
||||
|
||||
packagesExist(libavcodec) {
|
||||
PKGCONFIG += libavcodec libavutil
|
||||
CONFIG += ffmpeg
|
||||
@@ -82,29 +69,56 @@ unix:!macx {
|
||||
packagesExist(libva-wayland) {
|
||||
CONFIG += libva-wayland
|
||||
}
|
||||
packagesExist(libva-drm) {
|
||||
CONFIG += libva-drm
|
||||
}
|
||||
CONFIG += libva
|
||||
}
|
||||
|
||||
packagesExist(vdpau) {
|
||||
CONFIG += libvdpau
|
||||
}
|
||||
|
||||
packagesExist(mmal) {
|
||||
PKGCONFIG += mmal
|
||||
CONFIG += mmal
|
||||
}
|
||||
|
||||
packagesExist(libdrm) {
|
||||
PKGCONFIG += libdrm
|
||||
CONFIG += libdrm
|
||||
}
|
||||
}
|
||||
|
||||
packagesExist(wayland-client) {
|
||||
DEFINES += HAS_WAYLAND
|
||||
PKGCONFIG += wayland-client
|
||||
}
|
||||
|
||||
packagesExist(x11) {
|
||||
DEFINES += HAS_X11
|
||||
PKGCONFIG += x11
|
||||
}
|
||||
}
|
||||
win32 {
|
||||
LIBS += -llibssl -llibcrypto -lSDL2 -lSDL2_ttf -lavcodec -lavutil -lopus -ld3dx9
|
||||
CONFIG += ffmpeg soundio
|
||||
LIBS += -llibssl -llibcrypto -lSDL2 -lSDL2_ttf -lavcodec -lavutil -lopus
|
||||
CONFIG += ffmpeg
|
||||
}
|
||||
win32:!winrt {
|
||||
CONFIG += soundio discord-rpc
|
||||
}
|
||||
macx {
|
||||
LIBS += -lssl -lcrypto -lavcodec.58 -lavutil.56 -lopus -framework SDL2 -framework SDL2_ttf
|
||||
LIBS += -lobjc -framework VideoToolbox -framework AVFoundation -framework CoreVideo -framework CoreGraphics -framework CoreMedia -framework AppKit
|
||||
LIBS += -lobjc -framework VideoToolbox -framework AVFoundation -framework CoreVideo -framework CoreGraphics -framework CoreMedia -framework AppKit -framework Metal
|
||||
|
||||
# For libsoundio
|
||||
LIBS += -framework CoreAudio -framework AudioUnit
|
||||
|
||||
CONFIG += ffmpeg soundio
|
||||
CONFIG += ffmpeg soundio discord-rpc
|
||||
}
|
||||
|
||||
SOURCES += \
|
||||
backend/nvapp.cpp \
|
||||
main.cpp \
|
||||
backend/computerseeker.cpp \
|
||||
backend/identitymanager.cpp \
|
||||
@@ -113,11 +127,19 @@ SOURCES += \
|
||||
backend/nvpairingmanager.cpp \
|
||||
backend/computermanager.cpp \
|
||||
backend/boxartmanager.cpp \
|
||||
backend/richpresencemanager.cpp \
|
||||
cli/commandlineparser.cpp \
|
||||
cli/quitstream.cpp \
|
||||
cli/startstream.cpp \
|
||||
settings/mappingfetcher.cpp \
|
||||
settings/streamingpreferences.cpp \
|
||||
streaming/input.cpp \
|
||||
streaming/input/abstouch.cpp \
|
||||
streaming/input/clipboard.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/audio.cpp \
|
||||
streaming/audio/renderers/sdlaud.cpp \
|
||||
@@ -128,9 +150,13 @@ SOURCES += \
|
||||
path.cpp \
|
||||
settings/mappingmanager.cpp \
|
||||
gui/sdlgamepadkeynavigation.cpp \
|
||||
streaming/video/overlaymanager.cpp
|
||||
streaming/video/overlaymanager.cpp \
|
||||
backend/systemproperties.cpp \
|
||||
wm.cpp
|
||||
|
||||
HEADERS += \
|
||||
backend/nvapp.h \
|
||||
settings/mappingfetcher.h \
|
||||
utils.h \
|
||||
backend/computerseeker.h \
|
||||
backend/identitymanager.h \
|
||||
@@ -139,11 +165,12 @@ HEADERS += \
|
||||
backend/nvpairingmanager.h \
|
||||
backend/computermanager.h \
|
||||
backend/boxartmanager.h \
|
||||
backend/richpresencemanager.h \
|
||||
cli/commandlineparser.h \
|
||||
cli/quitstream.h \
|
||||
cli/startstream.h \
|
||||
settings/streamingpreferences.h \
|
||||
streaming/input.h \
|
||||
streaming/input/input.h \
|
||||
streaming/session.h \
|
||||
streaming/audio/renderers/renderer.h \
|
||||
streaming/audio/renderers/sdl.h \
|
||||
@@ -155,7 +182,8 @@ HEADERS += \
|
||||
path.h \
|
||||
settings/mappingmanager.h \
|
||||
gui/sdlgamepadkeynavigation.h \
|
||||
streaming/video/overlaymanager.h
|
||||
streaming/video/overlaymanager.h \
|
||||
backend/systemproperties.h
|
||||
|
||||
# Platform-specific renderers and decoders
|
||||
ffmpeg {
|
||||
@@ -165,6 +193,7 @@ ffmpeg {
|
||||
SOURCES += \
|
||||
streaming/video/ffmpeg.cpp \
|
||||
streaming/video/ffmpeg-renderers/sdlvid.cpp \
|
||||
streaming/video/ffmpeg-renderers/cuda.cpp \
|
||||
streaming/video/ffmpeg-renderers/pacer/pacer.cpp \
|
||||
streaming/video/ffmpeg-renderers/pacer/nullthreadedvsyncsource.cpp
|
||||
|
||||
@@ -172,6 +201,7 @@ ffmpeg {
|
||||
streaming/video/ffmpeg.h \
|
||||
streaming/video/ffmpeg-renderers/renderer.h \
|
||||
streaming/video/ffmpeg-renderers/sdlvid.h \
|
||||
streaming/video/ffmpeg-renderers/cuda.h \
|
||||
streaming/video/ffmpeg-renderers/pacer/pacer.h \
|
||||
streaming/video/ffmpeg-renderers/pacer/nullthreadedvsyncsource.h
|
||||
}
|
||||
@@ -195,6 +225,12 @@ libva-wayland {
|
||||
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)
|
||||
|
||||
@@ -202,16 +238,50 @@ 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
|
||||
}
|
||||
win32 {
|
||||
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
|
||||
LIBS += -ldl
|
||||
}
|
||||
}
|
||||
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
|
||||
HEADERS += streaming/video/ffmpeg-renderers/eglvid.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:!winrt {
|
||||
message(DXVA2 renderer selected)
|
||||
|
||||
SOURCES += \
|
||||
@@ -226,12 +296,10 @@ macx {
|
||||
message(VideoToolbox renderer selected)
|
||||
|
||||
SOURCES += \
|
||||
streaming/video/ffmpeg-renderers/vt.mm \
|
||||
streaming/video/ffmpeg-renderers/pacer/displaylinkvsyncsource.mm
|
||||
streaming/video/ffmpeg-renderers/vt.mm
|
||||
|
||||
HEADERS += \
|
||||
streaming/video/ffmpeg-renderers/vt.h \
|
||||
streaming/video/ffmpeg-renderers/pacer/displaylinkvsyncsource.h
|
||||
streaming/video/ffmpeg-renderers/vt.h
|
||||
}
|
||||
soundio {
|
||||
message(libsoundio audio renderer selected)
|
||||
@@ -240,11 +308,27 @@ soundio {
|
||||
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
|
||||
}
|
||||
|
||||
RESOURCES += \
|
||||
resources.qrc \
|
||||
qml.qrc
|
||||
|
||||
TRANSLATIONS += \
|
||||
languages/qml_zh_cn.ts \
|
||||
languages/qml_de.ts \
|
||||
languages/qml_fr.ts
|
||||
|
||||
# Additional import path used to resolve QML modules in Qt Creator's code model
|
||||
QML_IMPORT_PATH =
|
||||
|
||||
@@ -265,12 +349,14 @@ else:unix: LIBS += -L$$OUT_PWD/../qmdnsengine/ -lqmdnsengine
|
||||
INCLUDEPATH += $$PWD/../qmdnsengine/qmdnsengine/src/include $$PWD/../qmdnsengine
|
||||
DEPENDPATH += $$PWD/../qmdnsengine/qmdnsengine/src/include $$PWD/../qmdnsengine
|
||||
|
||||
win32:CONFIG(release, debug|release): LIBS += -L$$OUT_PWD/../soundio/release/ -lsoundio
|
||||
else:win32:CONFIG(debug, debug|release): LIBS += -L$$OUT_PWD/../soundio/debug/ -lsoundio
|
||||
else:unix: LIBS += -L$$OUT_PWD/../soundio/ -lsoundio
|
||||
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
|
||||
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
|
||||
@@ -279,22 +365,14 @@ else:unix: LIBS += -L$$OUT_PWD/../h264bitstream/ -lh264bitstream
|
||||
INCLUDEPATH += $$PWD/../h264bitstream/h264bitstream
|
||||
DEPENDPATH += $$PWD/../h264bitstream/h264bitstream
|
||||
|
||||
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
|
||||
!winrt {
|
||||
contains(QT_ARCH, i386)|contains(QT_ARCH, x86_64) {
|
||||
win32:CONFIG(release, debug|release): LIBS += -L$$OUT_PWD/../AntiHooking/release/ -lAntiHooking
|
||||
else:win32:CONFIG(debug, debug|release): LIBS += -L$$OUT_PWD/../AntiHooking/debug/ -lAntiHooking
|
||||
|
||||
INCLUDEPATH += $$PWD/../AntiHooking
|
||||
DEPENDPATH += $$PWD/../AntiHooking
|
||||
|
||||
defineTest(copyToDestDir) {
|
||||
files = $$1
|
||||
|
||||
for(FILE, files) {
|
||||
!equals(PWD, $$OUT_PWD) {
|
||||
QMAKE_POST_LINK += $$QMAKE_COPY $$shell_quote($$shell_path($$PWD/$$FILE)) $$shell_quote($$shell_path($$OUT_PWD)) $$escape_expand(\\n\\t)
|
||||
}
|
||||
INCLUDEPATH += $$PWD/../AntiHooking
|
||||
DEPENDPATH += $$PWD/../AntiHooking
|
||||
}
|
||||
|
||||
export(QMAKE_POST_LINK)
|
||||
}
|
||||
|
||||
unix:!macx: {
|
||||
@@ -319,16 +397,7 @@ unix:!macx: {
|
||||
appstream.files = deploy/linux/com.moonlight_stream.Moonlight.appdata.xml
|
||||
appstream.path = $$PREFIX/$$DATADIR/metainfo/
|
||||
|
||||
appdata.files = SDL_GameControllerDB/gamecontrollerdb.txt ModeSeven.ttf
|
||||
appdata.path = "$$PREFIX/$$DATADIR/Moonlight Game Streaming Project/Moonlight/"
|
||||
|
||||
CONFIG(debug, debug|release) {
|
||||
# Copy the required data files into the application directory only for debug builds.
|
||||
# Release builds will use the INSTALLS variable to place these in the correct folders.
|
||||
copyToDestDir(SDL_GameControllerDB/gamecontrollerdb.txt ModeSeven.ttf)
|
||||
}
|
||||
|
||||
INSTALLS += target appdata desktop icons appstream
|
||||
INSTALLS += target desktop icons appstream
|
||||
}
|
||||
win32 {
|
||||
RC_ICONS = moonlight.ico
|
||||
@@ -336,9 +405,6 @@ win32 {
|
||||
QMAKE_TARGET_DESCRIPTION = Moonlight Game Streaming Client
|
||||
QMAKE_TARGET_PRODUCT = Moonlight
|
||||
|
||||
# Copy the required data files into the application directory
|
||||
copyToDestDir(SDL_GameControllerDB/gamecontrollerdb.txt ModeSeven.ttf)
|
||||
|
||||
CONFIG -= embed_manifest_exe
|
||||
QMAKE_LFLAGS += /MANIFEST:embed /MANIFESTINPUT:$${PWD}/Moonlight.exe.manifest
|
||||
}
|
||||
@@ -349,13 +415,16 @@ macx {
|
||||
|
||||
QMAKE_INFO_PLIST = $$OUT_PWD/Info.plist
|
||||
|
||||
APP_BUNDLE_RESOURCES.files = moonlight.icns SDL_GameControllerDB/gamecontrollerdb.txt ModeSeven.ttf
|
||||
APP_BUNDLE_RESOURCES.files = moonlight.icns
|
||||
APP_BUNDLE_RESOURCES.path = Contents/Resources
|
||||
|
||||
APP_BUNDLE_FRAMEWORKS.files = $$files(../libs/mac/Frameworks/*.framework, true)
|
||||
APP_BUNDLE_FRAMEWORKS.files = $$files(../libs/mac/Frameworks/*.framework, true) $$files(../libs/mac/lib/*.dylib, true)
|
||||
APP_BUNDLE_FRAMEWORKS.path = Contents/Frameworks
|
||||
|
||||
QMAKE_BUNDLE_DATA += APP_BUNDLE_RESOURCES APP_BUNDLE_FRAMEWORKS
|
||||
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
|
||||
}
|
||||
|
||||
@@ -15,8 +15,8 @@ AutoUpdateChecker::AutoUpdateChecker(QObject *parent) :
|
||||
// Allow HTTP redirects
|
||||
m_Nam.setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy);
|
||||
|
||||
connect(&m_Nam, SIGNAL(finished(QNetworkReply*)),
|
||||
this, SLOT(handleUpdateCheckRequestFinished(QNetworkReply*)));
|
||||
connect(&m_Nam, &QNetworkAccessManager::finished,
|
||||
this, &AutoUpdateChecker::handleUpdateCheckRequestFinished);
|
||||
|
||||
QString currentVersion(VERSION_STR);
|
||||
qDebug() << "Current Moonlight version:" << currentVersion;
|
||||
@@ -28,10 +28,24 @@ AutoUpdateChecker::AutoUpdateChecker(QObject *parent) :
|
||||
|
||||
void AutoUpdateChecker::start()
|
||||
{
|
||||
#if defined(Q_OS_WIN32) || defined(Q_OS_DARWIN) // Only run update checker on platforms without auto-update
|
||||
#if defined(Q_OS_WIN32) || defined(Q_OS_DARWIN) || defined(STEAM_LINK) || 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");
|
||||
m_Nam.get(QNetworkRequest(url));
|
||||
QNetworkRequest request(url);
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
|
||||
request.setAttribute(QNetworkRequest::Http2AllowedAttribute, true);
|
||||
#else
|
||||
request.setAttribute(QNetworkRequest::HTTP2AllowedAttribute, true);
|
||||
#endif
|
||||
m_Nam.get(request);
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -43,13 +57,59 @@ void AutoUpdateChecker::parseStringToVersionQuad(QString& string, QVector<int>&
|
||||
}
|
||||
}
|
||||
|
||||
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());
|
||||
|
||||
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();
|
||||
@@ -88,47 +148,48 @@ void AutoUpdateChecker::handleUpdateCheckRequestFinished(QNetworkReply* reply)
|
||||
}
|
||||
|
||||
if (updateObj["arch"] == QSysInfo::buildCpuArchitecture() &&
|
||||
updateObj["platform"] == QSysInfo::productType()) {
|
||||
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);
|
||||
|
||||
for (int i = 0;; i++) {
|
||||
int latestVer = 0;
|
||||
int currentVer = 0;
|
||||
|
||||
// Treat missing decimal places as 0
|
||||
if (i < latestVersionQuad.count()) {
|
||||
latestVer = latestVersionQuad[i];
|
||||
}
|
||||
if (i < m_CurrentVersionQuad.count()) {
|
||||
currentVer = m_CurrentVersionQuad[i];
|
||||
}
|
||||
if (i >= latestVersionQuad.count() && i >= m_CurrentVersionQuad.count()) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (currentVer < latestVer) {
|
||||
qDebug() << "Update available";
|
||||
|
||||
emit onUpdateAvailable(updateObj["version"].toString(),
|
||||
updateObj["browser_url"].toString());
|
||||
return;
|
||||
}
|
||||
else if (currentVer > latestVer) {
|
||||
qDebug() << "Update manifest version lower than current version";
|
||||
return;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
qDebug() << "Update manifest version equal to current version";
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
else {
|
||||
@@ -136,8 +197,8 @@ void AutoUpdateChecker::handleUpdateCheckRequestFinished(QNetworkReply* reply)
|
||||
}
|
||||
}
|
||||
|
||||
qWarning() << "No entry in update manifest found for current platform: "
|
||||
<< QSysInfo::buildCpuArchitecture() << QSysInfo::productType();
|
||||
qWarning() << "No entry in update manifest found for current platform:"
|
||||
<< QSysInfo::buildCpuArchitecture() << getPlatform() << QSysInfo::kernelVersion();
|
||||
}
|
||||
else {
|
||||
qWarning() << "Update checking failed with error: " << reply->error();
|
||||
|
||||
@@ -20,6 +20,10 @@ private slots:
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -46,8 +46,8 @@ public:
|
||||
m_Computer(computer),
|
||||
m_App(app)
|
||||
{
|
||||
connect(this, SIGNAL(boxArtFetchCompleted(NvComputer*,NvApp,QUrl)),
|
||||
boxArtManager, SLOT(handleBoxArtLoadComplete(NvComputer*,NvApp,QUrl)));
|
||||
connect(this, &NetworkBoxArtLoadTask::boxArtFetchCompleted,
|
||||
boxArtManager, &BoxArtManager::handleBoxArtLoadComplete);
|
||||
}
|
||||
|
||||
signals:
|
||||
@@ -71,10 +71,10 @@ private:
|
||||
|
||||
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.
|
||||
@@ -87,12 +87,21 @@ QUrl BoxArtManager::loadBoxArt(NvComputer* computer, NvApp& app)
|
||||
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)
|
||||
@@ -110,6 +119,10 @@ 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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "computermanager.h"
|
||||
#include "boxartmanager.h"
|
||||
#include "nvhttp.h"
|
||||
#include "settings/streamingpreferences.h"
|
||||
|
||||
@@ -7,6 +8,7 @@
|
||||
|
||||
#include <QThread>
|
||||
#include <QThreadPool>
|
||||
#include <QCoreApplication>
|
||||
|
||||
#define SER_HOSTS "hosts"
|
||||
|
||||
@@ -31,7 +33,7 @@ private:
|
||||
|
||||
QString serverInfo;
|
||||
try {
|
||||
serverInfo = http.getServerInfo(NvHTTP::NvLogLevel::NVLL_NONE);
|
||||
serverInfo = http.getServerInfo(NvHTTP::NvLogLevel::NVLL_NONE, true);
|
||||
} catch (...) {
|
||||
return false;
|
||||
}
|
||||
@@ -66,12 +68,7 @@ private:
|
||||
}
|
||||
|
||||
QWriteLocker lock(&m_Computer->lock);
|
||||
if (m_Computer->appList != appList) {
|
||||
m_Computer->appList = appList;
|
||||
m_Computer->sortAppList();
|
||||
changed = true;
|
||||
}
|
||||
|
||||
changed = m_Computer->updateAppList(appList);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -83,7 +80,7 @@ private:
|
||||
bool stateChanged = false;
|
||||
bool online = false;
|
||||
bool wasOnline = m_Computer->state == NvComputer::CS_ONLINE;
|
||||
for (int i = 0; i < TRIES_BEFORE_OFFLINING && !online; i++) {
|
||||
for (int i = 0; i < (wasOnline ? TRIES_BEFORE_OFFLINING : 1) && !online; i++) {
|
||||
for (auto& address : m_Computer->uniqueAddresses()) {
|
||||
if (isInterruptionRequested()) {
|
||||
return;
|
||||
@@ -130,8 +127,12 @@ private:
|
||||
emit computerStateChanged(m_Computer);
|
||||
}
|
||||
|
||||
// Wait a bit to poll again
|
||||
QThread::sleep(3);
|
||||
// Wait a bit to poll again, but do it in 100 ms chunks
|
||||
// so we can be interrupted reasonably quickly.
|
||||
// FIXME: QWaitCondition would be better.
|
||||
for (int i = 0; i < 30 && !isInterruptionRequested(); i++) {
|
||||
QThread::msleep(100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,6 +158,13 @@ ComputerManager::ComputerManager(QObject *parent)
|
||||
m_KnownHosts[computer->uuid] = computer;
|
||||
}
|
||||
settings.endArray();
|
||||
|
||||
// To quit in a timely manner, we must block additional requests
|
||||
// after we receive the aboutToQuit() signal. This is neccessary
|
||||
// because NvHTTP uses aboutToQuit() to abort requests in progres
|
||||
// while quitting, however this is a one time signal - additional
|
||||
// requests would not be aborted and block termination.
|
||||
connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this, &ComputerManager::handleAboutToQuit);
|
||||
}
|
||||
|
||||
ComputerManager::~ComputerManager()
|
||||
@@ -197,13 +205,50 @@ void ComputerManager::saveHosts()
|
||||
|
||||
settings.remove(SER_HOSTS);
|
||||
settings.beginWriteArray(SER_HOSTS);
|
||||
for (int i = 0; i < m_KnownHosts.count(); i++) {
|
||||
settings.setArrayIndex(i);
|
||||
m_KnownHosts.value(m_KnownHosts.keys()[i])->serialize(settings);
|
||||
int i = 0;
|
||||
for (const NvComputer* computer : m_KnownHosts) {
|
||||
settings.setArrayIndex(i++);
|
||||
computer->serialize(settings);
|
||||
}
|
||||
settings.endArray();
|
||||
}
|
||||
|
||||
QHostAddress ComputerManager::getBestGlobalAddressV6(QVector<QHostAddress> &addresses)
|
||||
{
|
||||
for (const QHostAddress& address : addresses) {
|
||||
if (address.protocol() == QAbstractSocket::IPv6Protocol) {
|
||||
if (address.isInSubnet(QHostAddress("fe80::"), 10)) {
|
||||
// Link-local
|
||||
continue;
|
||||
}
|
||||
|
||||
if (address.isInSubnet(QHostAddress("fec0::"), 10)) {
|
||||
qInfo() << "Ignoring site-local address:" << address;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (address.isInSubnet(QHostAddress("fc00::"), 7)) {
|
||||
qInfo() << "Ignoring ULA:" << address;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (address.isInSubnet(QHostAddress("2002::"), 16)) {
|
||||
qInfo() << "Ignoring 6to4 address:" << address;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (address.isInSubnet(QHostAddress("2001::"), 32)) {
|
||||
qInfo() << "Ignoring Teredo address:" << address;
|
||||
continue;
|
||||
}
|
||||
|
||||
return address;
|
||||
}
|
||||
}
|
||||
|
||||
return QHostAddress();
|
||||
}
|
||||
|
||||
void ComputerManager::startPolling()
|
||||
{
|
||||
QWriteLocker lock(&m_Lock);
|
||||
@@ -221,9 +266,9 @@ void ComputerManager::startPolling()
|
||||
this, [this](const QMdnsEngine::Service& service) {
|
||||
qInfo() << "Discovered mDNS host:" << service.hostname();
|
||||
|
||||
MdnsPendingComputer* pendingComputer = new MdnsPendingComputer(&m_MdnsServer, &m_MdnsCache, service);
|
||||
connect(pendingComputer, SIGNAL(resolvedv4(MdnsPendingComputer*,QHostAddress)),
|
||||
this, SLOT(handleMdnsServiceResolved(MdnsPendingComputer*,QHostAddress)));
|
||||
MdnsPendingComputer* pendingComputer = new MdnsPendingComputer(&m_MdnsServer, service);
|
||||
connect(pendingComputer, &MdnsPendingComputer::resolvedHost,
|
||||
this, &ComputerManager::handleMdnsServiceResolved);
|
||||
m_PendingResolution.append(pendingComputer);
|
||||
});
|
||||
}
|
||||
@@ -257,19 +302,46 @@ void ComputerManager::startPollingComputer(NvComputer* computer)
|
||||
|
||||
if (!pollingEntry->isActive()) {
|
||||
PcMonitorThread* thread = new PcMonitorThread(computer);
|
||||
connect(thread, SIGNAL(computerStateChanged(NvComputer*)),
|
||||
this, SLOT(handleComputerStateChanged(NvComputer*)));
|
||||
connect(thread, &PcMonitorThread::computerStateChanged,
|
||||
this, &ComputerManager::handleComputerStateChanged);
|
||||
pollingEntry->setActiveThread(thread);
|
||||
thread->start();
|
||||
}
|
||||
}
|
||||
|
||||
void ComputerManager::handleMdnsServiceResolved(MdnsPendingComputer* computer,
|
||||
const QHostAddress& address)
|
||||
QVector<QHostAddress>& addresses)
|
||||
{
|
||||
qInfo() << "Resolved" << computer->hostname() << "to" << address.toString();
|
||||
QHostAddress v6Global = getBestGlobalAddressV6(addresses);
|
||||
bool added = false;
|
||||
|
||||
addNewHost(address.toString(), true);
|
||||
// Add the host using the IPv4 address
|
||||
for (const QHostAddress& address : addresses) {
|
||||
if (address.protocol() == QAbstractSocket::IPv4Protocol) {
|
||||
// NB: We don't just call addNewHost() here with v6Global because the IPv6
|
||||
// address may not be reachable (if the user hasn't installed the IPv6 helper yet
|
||||
// or if this host lacks outbound IPv6 capability). We want to add IPv6 even if
|
||||
// it's not currently reachable.
|
||||
addNewHost(address.toString(), true, v6Global);
|
||||
added = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!added) {
|
||||
// If we get here, there wasn't an IPv4 address so we'll do it v6-only
|
||||
for (const QHostAddress& address : addresses) {
|
||||
if (address.protocol() == QAbstractSocket::IPv6Protocol) {
|
||||
// Use a link-local or site-local address for the "local address"
|
||||
if (address.isInSubnet(QHostAddress("fe80::"), 10) ||
|
||||
address.isInSubnet(QHostAddress("fec0::"), 10) ||
|
||||
address.isInSubnet(QHostAddress("fc00::"), 7)) {
|
||||
addNewHost(address.toString(), true, v6Global);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m_PendingResolution.removeOne(computer);
|
||||
computer->deleteLater();
|
||||
@@ -322,6 +394,9 @@ public:
|
||||
// Delete the polling entry first. This will stop all polling threads too.
|
||||
delete pollingEntry;
|
||||
|
||||
// Delete cached box art
|
||||
BoxArtManager::deleteBoxArt(m_Computer);
|
||||
|
||||
// Finally, delete the computer itself. This must be done
|
||||
// last because the polling thread might be using it.
|
||||
delete m_Computer;
|
||||
@@ -339,6 +414,39 @@ void ComputerManager::deleteHost(NvComputer* computer)
|
||||
QThreadPool::globalInstance()->start(new DeferredHostDeletionTask(this, computer));
|
||||
}
|
||||
|
||||
void ComputerManager::renameHost(NvComputer* computer, QString name)
|
||||
{
|
||||
{
|
||||
QWriteLocker lock(&computer->lock);
|
||||
|
||||
computer->name = name;
|
||||
computer->hasCustomName = true;
|
||||
}
|
||||
|
||||
// Notify the UI of the state change
|
||||
handleComputerStateChanged(computer);
|
||||
}
|
||||
|
||||
void ComputerManager::clientSideAttributeUpdated(NvComputer* computer)
|
||||
{
|
||||
// Persist the change
|
||||
saveHosts();
|
||||
|
||||
// Notify the UI of the state change
|
||||
handleComputerStateChanged(computer);
|
||||
}
|
||||
|
||||
void ComputerManager::handleAboutToQuit()
|
||||
{
|
||||
QWriteLocker lock(&m_Lock);
|
||||
|
||||
// Interrupt polling threads immediately, so they
|
||||
// avoid making additional requests while quitting
|
||||
for (ComputerPollingEntry* entry : m_PollEntries) {
|
||||
entry->interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
class PendingPairingTask : public QObject, public QRunnable
|
||||
{
|
||||
Q_OBJECT
|
||||
@@ -378,7 +486,7 @@ private:
|
||||
break;
|
||||
}
|
||||
} catch (const GfeHttpResponseException& e) {
|
||||
emit pairingCompleted(m_Computer, e.toQString());
|
||||
emit pairingCompleted(m_Computer, "GeForce Experience returned error: " + e.toQString());
|
||||
} catch (const QtNetworkReplyException& e) {
|
||||
emit pairingCompleted(m_Computer, e.toQString());
|
||||
}
|
||||
@@ -427,8 +535,8 @@ private:
|
||||
}
|
||||
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.");
|
||||
emit quitAppFailed(tr("The running game wasn't started by this PC. "
|
||||
"You must quit the game on the host PC manually or use the device that originally started the game."));
|
||||
}
|
||||
else {
|
||||
emit quitAppFailed(e.toQString());
|
||||
@@ -485,9 +593,10 @@ class PendingAddTask : public QObject, public QRunnable
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
PendingAddTask(ComputerManager* computerManager, QString address, bool mdns)
|
||||
PendingAddTask(ComputerManager* computerManager, QString address, QHostAddress mdnsIpv6Address, bool mdns)
|
||||
: m_ComputerManager(computerManager),
|
||||
m_Address(address),
|
||||
m_MdnsIpv6Address(mdnsIpv6Address),
|
||||
m_Mdns(mdns)
|
||||
{
|
||||
connect(this, &PendingAddTask::computerAddCompleted,
|
||||
@@ -497,7 +606,7 @@ public:
|
||||
}
|
||||
|
||||
signals:
|
||||
void computerAddCompleted(QVariant success);
|
||||
void computerAddCompleted(QVariant success, QVariant detectedPortBlocking);
|
||||
|
||||
void computerStateChanged(NvComputer* computer);
|
||||
|
||||
@@ -527,7 +636,20 @@ private:
|
||||
return serverInfo;
|
||||
} catch (...) {
|
||||
if (!m_Mdns) {
|
||||
emit computerAddCompleted(false);
|
||||
StreamingPreferences prefs;
|
||||
int portTestResult;
|
||||
|
||||
if (prefs.detectNetworkBlocking) {
|
||||
// We failed to connect to the specified PC. Let's test to make sure this network
|
||||
// isn't blocking Moonlight, so we can tell the user about it.
|
||||
portTestResult = LiTestClientConnectivity("qt.conntest.moonlight-stream.org", 443,
|
||||
ML_PORT_FLAG_TCP_47984 | ML_PORT_FLAG_TCP_47989);
|
||||
}
|
||||
else {
|
||||
portTestResult = 0;
|
||||
}
|
||||
|
||||
emit computerAddCompleted(false, portTestResult != 0 && portTestResult != ML_TEST_RESULT_INCONCLUSIVE);
|
||||
}
|
||||
return QString();
|
||||
}
|
||||
@@ -537,16 +659,21 @@ private:
|
||||
{
|
||||
NvHTTP http(m_Address, QSslCertificate());
|
||||
|
||||
qInfo() << "Processing new PC at" << m_Address << "from" << (m_Mdns ? "mDNS" : "user");
|
||||
qInfo() << "Processing new PC at" << m_Address << "from" << (m_Mdns ? "mDNS" : "user") << m_MdnsIpv6Address;
|
||||
|
||||
// Perform initial serverinfo fetch over HTTP since we don't know which cert to use
|
||||
QString serverInfo = fetchServerInfo(http);
|
||||
if (serverInfo.isEmpty() && !m_MdnsIpv6Address.isNull()) {
|
||||
// Retry using the global IPv6 address if the IPv4 or link-local IPv6 address fails
|
||||
http.setAddress(m_MdnsIpv6Address.toString());
|
||||
serverInfo = fetchServerInfo(http);
|
||||
}
|
||||
if (serverInfo.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create initial newComputer using HTTP serverinfo with no pinned cert
|
||||
NvComputer* newComputer = new NvComputer(m_Address, serverInfo, QSslCertificate());
|
||||
NvComputer* newComputer = new NvComputer(http.address(), serverInfo, QSslCertificate());
|
||||
|
||||
// Check if we have a record of this host UUID to pull the pinned cert
|
||||
NvComputer* existingComputer;
|
||||
@@ -566,28 +693,46 @@ private:
|
||||
}
|
||||
|
||||
// Update the polled computer with the HTTPS information
|
||||
NvComputer httpsComputer(m_Address, serverInfo, QSslCertificate());
|
||||
NvComputer httpsComputer(http.address(), serverInfo, QSslCertificate());
|
||||
newComputer->update(httpsComputer);
|
||||
}
|
||||
|
||||
// Update addresses depending on the context
|
||||
if (m_Mdns) {
|
||||
newComputer->localAddress = m_Address;
|
||||
|
||||
// Get the WAN IP address using STUN if we're on mDNS
|
||||
quint32 addr;
|
||||
int err = LiFindExternalAddressIP4("stun.moonlight-stream.org", 3478, &addr);
|
||||
if (err == 0) {
|
||||
newComputer->remoteAddress = QHostAddress(qFromBigEndian(addr)).toString();
|
||||
// Only update local address if we actually reached the PC via this address.
|
||||
// If we reached it via the IPv6 address after the local address failed,
|
||||
// don't store the non-working local address.
|
||||
if (http.address() == m_Address) {
|
||||
newComputer->localAddress = m_Address;
|
||||
}
|
||||
else {
|
||||
qWarning() << "STUN failed to get WAN address:" << err;
|
||||
|
||||
// Get the WAN IP address using STUN if we're on mDNS over IPv4
|
||||
if (QHostAddress(newComputer->localAddress).protocol() == QAbstractSocket::IPv4Protocol) {
|
||||
quint32 addr;
|
||||
int err = LiFindExternalAddressIP4("stun.moonlight-stream.org", 3478, &addr);
|
||||
if (err == 0) {
|
||||
newComputer->remoteAddress = QHostAddress(qFromBigEndian(addr)).toString();
|
||||
}
|
||||
else {
|
||||
qWarning() << "STUN failed to get WAN address:" << err;
|
||||
}
|
||||
}
|
||||
|
||||
if (!m_MdnsIpv6Address.isNull()) {
|
||||
Q_ASSERT(m_MdnsIpv6Address.protocol() == QAbstractSocket::IPv6Protocol);
|
||||
newComputer->ipv6Address = m_MdnsIpv6Address.toString();
|
||||
}
|
||||
}
|
||||
else {
|
||||
newComputer->manualAddress = m_Address;
|
||||
}
|
||||
|
||||
QHostAddress hostAddress(m_Address);
|
||||
bool addressIsSiteLocalV4 =
|
||||
hostAddress.isInSubnet(QHostAddress("10.0.0.0"), 8) ||
|
||||
hostAddress.isInSubnet(QHostAddress("172.16.0.0"), 12) ||
|
||||
hostAddress.isInSubnet(QHostAddress("192.168.0.0"), 16);
|
||||
|
||||
{
|
||||
// Check if this PC already exists
|
||||
QWriteLocker lock(&m_ComputerManager->m_Lock);
|
||||
@@ -602,7 +747,7 @@ private:
|
||||
|
||||
// For non-mDNS clients, let them know it succeeded
|
||||
if (!m_Mdns) {
|
||||
emit computerAddCompleted(true);
|
||||
emit computerAddCompleted(true, false);
|
||||
}
|
||||
|
||||
// Tell our client if something changed
|
||||
@@ -621,9 +766,24 @@ private:
|
||||
// Drop the lock before notifying
|
||||
lock.unlock();
|
||||
|
||||
// If this wasn't added via mDNS but it is a RFC 1918 IPv4 address and not a VPN,
|
||||
// go ahead and do the STUN request now to populate an external address.
|
||||
if (!m_Mdns && addressIsSiteLocalV4 && !newComputer->isReachableOverVpn()) {
|
||||
quint32 addr;
|
||||
int err = LiFindExternalAddressIP4("stun.moonlight-stream.org", 3478, &addr);
|
||||
if (err == 0) {
|
||||
lock.relock();
|
||||
newComputer->remoteAddress = QHostAddress(qFromBigEndian(addr)).toString();
|
||||
lock.unlock();
|
||||
}
|
||||
else {
|
||||
qWarning() << "STUN failed to get WAN address:" << err;
|
||||
}
|
||||
}
|
||||
|
||||
// For non-mDNS clients, let them know it succeeded
|
||||
if (!m_Mdns) {
|
||||
emit computerAddCompleted(true);
|
||||
emit computerAddCompleted(true, false);
|
||||
}
|
||||
|
||||
// Tell our client about this new PC
|
||||
@@ -634,14 +794,15 @@ private:
|
||||
|
||||
ComputerManager* m_ComputerManager;
|
||||
QString m_Address;
|
||||
QHostAddress m_MdnsIpv6Address;
|
||||
bool m_Mdns;
|
||||
};
|
||||
|
||||
void ComputerManager::addNewHost(QString address, bool mdns)
|
||||
void ComputerManager::addNewHost(QString address, bool mdns, QHostAddress mdnsIpv6Address)
|
||||
{
|
||||
// Punt to a worker thread to avoid stalling the
|
||||
// UI while waiting for serverinfo query to complete
|
||||
PendingAddTask* addTask = new PendingAddTask(this, address, mdns);
|
||||
PendingAddTask* addTask = new PendingAddTask(this, address, mdnsIpv6Address, mdns);
|
||||
QThreadPool::globalInstance()->start(addTask);
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include <QReadWriteLock>
|
||||
#include <QSettings>
|
||||
#include <QRunnable>
|
||||
#include <QTimer>
|
||||
|
||||
class MdnsPendingComputer : public QObject
|
||||
{
|
||||
@@ -20,13 +21,18 @@ class MdnsPendingComputer : public QObject
|
||||
|
||||
public:
|
||||
explicit MdnsPendingComputer(QMdnsEngine::Server* server,
|
||||
QMdnsEngine::Cache* cache,
|
||||
const QMdnsEngine::Service& service)
|
||||
: m_Hostname(service.hostname()),
|
||||
m_Resolver(server, m_Hostname, cache)
|
||||
m_Server(server),
|
||||
m_Resolver(nullptr)
|
||||
{
|
||||
connect(&m_Resolver, SIGNAL(resolved(QHostAddress)),
|
||||
this, SLOT(handleResolved(QHostAddress)));
|
||||
// Start resolving
|
||||
resolve();
|
||||
}
|
||||
|
||||
virtual ~MdnsPendingComputer()
|
||||
{
|
||||
delete m_Resolver;
|
||||
}
|
||||
|
||||
QString hostname()
|
||||
@@ -35,20 +41,42 @@ public:
|
||||
}
|
||||
|
||||
private slots:
|
||||
void handleResolved(const QHostAddress& address)
|
||||
void handleResolvedTimeout()
|
||||
{
|
||||
if (address.protocol() == QAbstractSocket::IPv4Protocol) {
|
||||
m_Resolver.disconnect();
|
||||
emit resolvedv4(this, address);
|
||||
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 m_Resolver;
|
||||
m_Resolver = new QMdnsEngine::Resolver(m_Server, m_Hostname);
|
||||
connect(m_Resolver, &QMdnsEngine::Resolver::resolved,
|
||||
this, &MdnsPendingComputer::handleResolvedAddress);
|
||||
QTimer::singleShot(2000, this, &MdnsPendingComputer::handleResolvedTimeout);
|
||||
}
|
||||
|
||||
QByteArray m_Hostname;
|
||||
QMdnsEngine::Resolver m_Resolver;
|
||||
QMdnsEngine::Server* m_Server;
|
||||
QMdnsEngine::Resolver* m_Resolver;
|
||||
QVector<QHostAddress> m_Addresses;
|
||||
};
|
||||
|
||||
class ComputerPollingEntry
|
||||
@@ -140,7 +168,7 @@ public:
|
||||
|
||||
Q_INVOKABLE void stopPollingAsync();
|
||||
|
||||
Q_INVOKABLE void addNewHost(QString address, bool mdns);
|
||||
Q_INVOKABLE void addNewHost(QString address, bool mdns, QHostAddress mdnsIpv6Address = QHostAddress());
|
||||
|
||||
void pairHost(NvComputer* computer, QString pin);
|
||||
|
||||
@@ -151,23 +179,31 @@ 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();
|
||||
|
||||
QHostAddress getBestGlobalAddressV6(QVector<QHostAddress>& addresses);
|
||||
|
||||
void startPollingComputer(NvComputer* computer);
|
||||
|
||||
int m_PollingRef;
|
||||
|
||||
@@ -39,11 +39,18 @@ void ComputerSeeker::onComputerUpdated(NvComputer *computer)
|
||||
bool ComputerSeeker::matchComputer(NvComputer *computer) const
|
||||
{
|
||||
QString value = m_ComputerName.toLower();
|
||||
return computer->name.toLower() == value ||
|
||||
computer->localAddress.toLower() == value ||
|
||||
computer->remoteAddress.toLower() == value ||
|
||||
computer->manualAddress.toLower() == value ||
|
||||
computer->uuid.toLower() == value;
|
||||
|
||||
if (computer->name.toLower() == value || computer->uuid.toLower() == value) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (const QString& addr : computer->uniqueAddresses()) {
|
||||
if (addr.toLower() == value) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ComputerSeeker::isOnline(NvComputer *computer) const
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
@@ -1,8 +1,10 @@
|
||||
#include "nvcomputer.h"
|
||||
#include "nvapp.h"
|
||||
|
||||
#include <QUdpSocket>
|
||||
#include <QHostInfo>
|
||||
#include <QNetworkInterface>
|
||||
#include <QNetworkProxy>
|
||||
|
||||
#define SER_NAME "hostname"
|
||||
#define SER_UUID "uuid"
|
||||
@@ -10,33 +12,29 @@
|
||||
#define SER_LOCALADDR "localaddress"
|
||||
#define SER_REMOTEADDR "remoteaddress"
|
||||
#define SER_MANUALADDR "manualaddress"
|
||||
#define SER_IPV6ADDR "ipv6address"
|
||||
#define SER_APPLIST "apps"
|
||||
#define SER_SRVCERT "srvcert"
|
||||
|
||||
#define SER_APPNAME "name"
|
||||
#define SER_APPID "id"
|
||||
#define SER_APPHDR "hdr"
|
||||
#define SER_CUSTOMNAME "customname"
|
||||
|
||||
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 = settings.value(SER_LOCALADDR).toString();
|
||||
this->remoteAddress = settings.value(SER_REMOTEADDR).toString();
|
||||
this->ipv6Address = settings.value(SER_IPV6ADDR).toString();
|
||||
this->manualAddress = settings.value(SER_MANUALADDR).toString();
|
||||
this->serverCert = QSslCertificate(settings.value(SER_SRVCERT).toByteArray());
|
||||
|
||||
int appCount = settings.beginReadArray(SER_APPLIST);
|
||||
this->appList.reserve(appCount);
|
||||
for (int i = 0; i < appCount; i++) {
|
||||
NvApp app;
|
||||
|
||||
settings.setArrayIndex(i);
|
||||
|
||||
app.name = settings.value(SER_APPNAME).toString();
|
||||
app.id = settings.value(SER_APPID).toInt();
|
||||
app.hdrSupported = settings.value(SER_APPHDR).toBool();
|
||||
|
||||
NvApp app(settings);
|
||||
this->appList.append(app);
|
||||
}
|
||||
settings.endArray();
|
||||
@@ -54,15 +52,17 @@ NvComputer::NvComputer(QSettings& settings)
|
||||
this->gpuModel = nullptr;
|
||||
}
|
||||
|
||||
void NvComputer::serialize(QSettings& settings)
|
||||
void NvComputer::serialize(QSettings& settings) 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);
|
||||
settings.setValue(SER_REMOTEADDR, remoteAddress);
|
||||
settings.setValue(SER_IPV6ADDR, ipv6Address);
|
||||
settings.setValue(SER_MANUALADDR, manualAddress);
|
||||
settings.setValue(SER_SRVCERT, serverCert.toPem());
|
||||
|
||||
@@ -72,10 +72,7 @@ void NvComputer::serialize(QSettings& settings)
|
||||
settings.beginWriteArray(SER_APPLIST);
|
||||
for (int i = 0; i < appList.count(); i++) {
|
||||
settings.setArrayIndex(i);
|
||||
|
||||
settings.setValue(SER_APPNAME, appList[i].name);
|
||||
settings.setValue(SER_APPID, appList[i].id);
|
||||
settings.setValue(SER_APPHDR, appList[i].hdrSupported);
|
||||
appList[i].serialize(settings);
|
||||
}
|
||||
settings.endArray();
|
||||
}
|
||||
@@ -92,6 +89,7 @@ NvComputer::NvComputer(QString address, QString serverInfo, QSslCertificate serv
|
||||
{
|
||||
this->serverCert = serverCert;
|
||||
|
||||
this->hasCustomName = false;
|
||||
this->name = NvHTTP::getXmlString(serverInfo, "hostname");
|
||||
if (this->name.isEmpty()) {
|
||||
this->name = "UNKNOWN";
|
||||
@@ -101,7 +99,7 @@ NvComputer::NvComputer(QString address, QString serverInfo, QSslCertificate serv
|
||||
QString newMacString = NvHTTP::getXmlString(serverInfo, "mac");
|
||||
if (newMacString != "00:00:00:00:00:00") {
|
||||
QStringList macOctets = newMacString.split(':');
|
||||
for (QString macOctet : macOctets) {
|
||||
for (const QString& macOctet : macOctets) {
|
||||
this->macAddress.append((char) macOctet.toInt(nullptr, 16));
|
||||
}
|
||||
}
|
||||
@@ -129,7 +127,12 @@ NvComputer::NvComputer(QString address, QString serverInfo, QSslCertificate serv
|
||||
mode2.width * mode2.height * mode2.refreshRate;
|
||||
});
|
||||
|
||||
// We can get an IPv4 loopback address if we're using the GS IPv6 Forwarder
|
||||
this->localAddress = NvHTTP::getXmlString(serverInfo, "LocalIP");
|
||||
if (this->localAddress.startsWith("127.")) {
|
||||
this->localAddress = QString();
|
||||
}
|
||||
|
||||
this->remoteAddress = NvHTTP::getXmlString(serverInfo, "ExternalIP");
|
||||
this->pairState = NvHTTP::getXmlString(serverInfo, "PairStatus") == "1" ?
|
||||
PS_PAIRED : PS_NOT_PAIRED;
|
||||
@@ -155,8 +158,9 @@ bool NvComputer::wake()
|
||||
}
|
||||
|
||||
const quint16 WOL_PORTS[] = {
|
||||
7, 9, // Standard WOL ports
|
||||
47998, 47999, 48000, // Ports opened by GFE
|
||||
9, // Standard WOL port (privileged port)
|
||||
47998, 47999, 48000, 48002, 48010, // Ports opened by GFE
|
||||
47009, // Port opened by Moonlight Internet Hosting Tool for WoL (non-privileged port)
|
||||
};
|
||||
|
||||
// Create the WoL payload
|
||||
@@ -229,7 +233,102 @@ bool NvComputer::wake()
|
||||
return success;
|
||||
}
|
||||
|
||||
QVector<QString> NvComputer::uniqueAddresses()
|
||||
bool NvComputer::isReachableOverVpn()
|
||||
{
|
||||
if (activeAddress.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QTcpSocket s;
|
||||
|
||||
s.setProxy(QNetworkProxy::NoProxy);
|
||||
s.connectToHost(activeAddress, 47984);
|
||||
if (s.waitForConnected(3000)) {
|
||||
Q_ASSERT(!s.localAddress().isNull());
|
||||
|
||||
for (const QNetworkInterface& nic : QNetworkInterface::allInterfaces()) {
|
||||
// Ensure the interface is up
|
||||
if ((nic.flags() & QNetworkInterface::IsUp) == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const QNetworkAddressEntry& addr : nic.addressEntries()) {
|
||||
if (addr.ip() == s.localAddress()) {
|
||||
qInfo() << "Found matching interface:" << nic.humanReadableName() << nic.hardwareAddress() << nic.flags();
|
||||
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 11, 0)
|
||||
qInfo() << "Interface Type:" << nic.type();
|
||||
qInfo() << "Interface MTU:" << nic.maximumTransmissionUnit();
|
||||
|
||||
if (nic.type() == QNetworkInterface::Virtual ||
|
||||
nic.type() == QNetworkInterface::Ppp) {
|
||||
// Treat PPP and virtual interfaces as likely VPNs
|
||||
return true;
|
||||
}
|
||||
|
||||
if (nic.maximumTransmissionUnit() != 0 && nic.maximumTransmissionUnit() < 1500) {
|
||||
// Treat MTUs under 1500 as likely VPNs
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (nic.flags() & QNetworkInterface::IsPointToPoint) {
|
||||
// Treat point-to-point links as likely VPNs.
|
||||
// This check detects OpenVPN on Unix-like OSes.
|
||||
return true;
|
||||
}
|
||||
|
||||
if (nic.hardwareAddress().startsWith("00:FF", Qt::CaseInsensitive)) {
|
||||
// OpenVPN TAP interfaces have a MAC address starting with 00:FF on Windows
|
||||
return true;
|
||||
}
|
||||
|
||||
if (nic.humanReadableName().startsWith("ZeroTier")) {
|
||||
// ZeroTier interfaces always start with "ZeroTier"
|
||||
return true;
|
||||
}
|
||||
|
||||
if (nic.humanReadableName().contains("VPN")) {
|
||||
// This one is just a final heuristic if all else fails
|
||||
return true;
|
||||
}
|
||||
|
||||
// Didn't meet any of our VPN heuristics
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
qWarning() << "No match found for address:" << s.localAddress();
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
// If we fail to connect, just pretend that it's not a VPN
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
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<QString> NvComputer::uniqueAddresses() const
|
||||
{
|
||||
QVector<QString> uniqueAddressList;
|
||||
|
||||
@@ -237,6 +336,7 @@ QVector<QString> NvComputer::uniqueAddresses()
|
||||
uniqueAddressList.append(activeAddress);
|
||||
uniqueAddressList.append(localAddress);
|
||||
uniqueAddressList.append(remoteAddress);
|
||||
uniqueAddressList.append(ipv6Address);
|
||||
uniqueAddressList.append(manualAddress);
|
||||
|
||||
// Prune duplicates (always giving precedence to the first)
|
||||
@@ -292,10 +392,14 @@ bool NvComputer::update(NvComputer& that)
|
||||
changed = true; \
|
||||
}
|
||||
|
||||
ASSIGN_IF_CHANGED(name);
|
||||
if (!hasCustomName) {
|
||||
// Only overwrite the name if it's not custom
|
||||
ASSIGN_IF_CHANGED(name);
|
||||
}
|
||||
ASSIGN_IF_CHANGED_AND_NONEMPTY(macAddress);
|
||||
ASSIGN_IF_CHANGED_AND_NONEMPTY(localAddress);
|
||||
ASSIGN_IF_CHANGED_AND_NONEMPTY(remoteAddress);
|
||||
ASSIGN_IF_CHANGED_AND_NONEMPTY(ipv6Address);
|
||||
ASSIGN_IF_CHANGED_AND_NONEMPTY(manualAddress);
|
||||
ASSIGN_IF_CHANGED(pairState);
|
||||
ASSIGN_IF_CHANGED(serverCodecModeSupport);
|
||||
@@ -307,7 +411,12 @@ bool NvComputer::update(NvComputer& that)
|
||||
ASSIGN_IF_CHANGED(maxLumaPixelsHEVC);
|
||||
ASSIGN_IF_CHANGED(gpuModel);
|
||||
ASSIGN_IF_CHANGED_AND_NONNULL(serverCert);
|
||||
ASSIGN_IF_CHANGED_AND_NONEMPTY(appList);
|
||||
ASSIGN_IF_CHANGED_AND_NONEMPTY(displayModes);
|
||||
|
||||
if (!that.appList.isEmpty()) {
|
||||
// updateAppList() handles merging client-side attributes
|
||||
updateAppList(that.appList);
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@ class NvComputer
|
||||
private:
|
||||
void sortAppList();
|
||||
|
||||
bool updateAppList(QVector<NvApp> newAppList);
|
||||
|
||||
bool pendingQuit;
|
||||
|
||||
public:
|
||||
@@ -29,11 +31,14 @@ public:
|
||||
bool
|
||||
wake();
|
||||
|
||||
bool
|
||||
isReachableOverVpn();
|
||||
|
||||
QVector<QString>
|
||||
uniqueAddresses();
|
||||
uniqueAddresses() const;
|
||||
|
||||
void
|
||||
serialize(QSettings& settings);
|
||||
serialize(QSettings& settings) const;
|
||||
|
||||
enum PairState
|
||||
{
|
||||
@@ -64,13 +69,15 @@ public:
|
||||
// Persisted traits
|
||||
QString localAddress;
|
||||
QString remoteAddress;
|
||||
QString ipv6Address;
|
||||
QString manualAddress;
|
||||
QByteArray macAddress;
|
||||
QString name;
|
||||
bool hasCustomName;
|
||||
QString uuid;
|
||||
QSslCertificate serverCert;
|
||||
QVector<NvApp> appList;
|
||||
|
||||
// Synchronization
|
||||
QReadWriteLock lock;
|
||||
mutable QReadWriteLock lock;
|
||||
};
|
||||
|
||||
@@ -12,27 +12,27 @@
|
||||
#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, QSslCertificate serverCert) :
|
||||
m_Address(address),
|
||||
m_ServerCert(serverCert)
|
||||
{
|
||||
Q_ASSERT(!address.isEmpty());
|
||||
|
||||
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);
|
||||
|
||||
// Never use a proxy server
|
||||
QNetworkProxy noProxy(QNetworkProxy::NoProxy);
|
||||
m_Nam.setProxy(noProxy);
|
||||
|
||||
connect(&m_Nam, &QNetworkAccessManager::sslErrors, this, &NvHTTP::handleSslErrors);
|
||||
}
|
||||
|
||||
void NvHTTP::setServerCert(QSslCertificate serverCert)
|
||||
@@ -40,6 +40,21 @@ void NvHTTP::setServerCert(QSslCertificate serverCert)
|
||||
m_ServerCert = serverCert;
|
||||
}
|
||||
|
||||
void NvHTTP::setAddress(QString address)
|
||||
{
|
||||
Q_ASSERT(!address.isEmpty());
|
||||
|
||||
m_Address = address;
|
||||
|
||||
m_BaseUrlHttp.setHost(address);
|
||||
m_BaseUrlHttps.setHost(address);
|
||||
}
|
||||
|
||||
QString NvHTTP::address()
|
||||
{
|
||||
return m_Address;
|
||||
}
|
||||
|
||||
QVector<int>
|
||||
NvHTTP::parseQuad(QString quad)
|
||||
{
|
||||
@@ -52,7 +67,8 @@ NvHTTP::parseQuad(QString quad)
|
||||
}
|
||||
|
||||
QStringList parts = quad.split(".");
|
||||
for (int i = 0; i < 4; i++)
|
||||
ret.reserve(parts.length());
|
||||
for (int i = 0; i < parts.length(); i++)
|
||||
{
|
||||
ret.append(parts.at(i).toInt());
|
||||
}
|
||||
@@ -78,7 +94,7 @@ NvHTTP::getCurrentGame(QString serverInfo)
|
||||
}
|
||||
|
||||
QString
|
||||
NvHTTP::getServerInfo(NvLogLevel logLevel)
|
||||
NvHTTP::getServerInfo(NvLogLevel logLevel, bool fastFail)
|
||||
{
|
||||
QString serverInfo;
|
||||
|
||||
@@ -92,7 +108,7 @@ NvHTTP::getServerInfo(NvLogLevel logLevel)
|
||||
serverInfo = openConnectionToString(m_BaseUrlHttps,
|
||||
"serverinfo",
|
||||
nullptr,
|
||||
REQUEST_TIMEOUT_MS,
|
||||
fastFail ? FAST_FAIL_TIMEOUT_MS : REQUEST_TIMEOUT_MS,
|
||||
logLevel);
|
||||
// Throws if the request failed
|
||||
verifyResponseStatus(serverInfo);
|
||||
@@ -105,7 +121,7 @@ NvHTTP::getServerInfo(NvLogLevel logLevel)
|
||||
serverInfo = openConnectionToString(m_BaseUrlHttp,
|
||||
"serverinfo",
|
||||
nullptr,
|
||||
REQUEST_TIMEOUT_MS,
|
||||
fastFail ? FAST_FAIL_TIMEOUT_MS : REQUEST_TIMEOUT_MS,
|
||||
logLevel);
|
||||
verifyResponseStatus(serverInfo);
|
||||
}
|
||||
@@ -122,7 +138,7 @@ NvHTTP::getServerInfo(NvLogLevel logLevel)
|
||||
serverInfo = openConnectionToString(m_BaseUrlHttp,
|
||||
"serverinfo",
|
||||
nullptr,
|
||||
REQUEST_TIMEOUT_MS,
|
||||
fastFail ? FAST_FAIL_TIMEOUT_MS : REQUEST_TIMEOUT_MS,
|
||||
logLevel);
|
||||
verifyResponseStatus(serverInfo);
|
||||
}
|
||||
@@ -130,30 +146,6 @@ NvHTTP::getServerInfo(NvLogLevel logLevel)
|
||||
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,
|
||||
@@ -173,9 +165,10 @@ NvHTTP::launchApp(int appId,
|
||||
"&mode="+QString::number(streamConfig->width)+"x"+
|
||||
QString::number(streamConfig->height)+"x"+
|
||||
// Using an FPS value over 60 causes SOPS to default to 720p60,
|
||||
// so force it to 60 when starting. This won't impact our ability
|
||||
// to get > 60 FPS while actually streaming though.
|
||||
QString::number(streamConfig->fps > 60 ? 60 : streamConfig->fps)+
|
||||
// so force it to 0 to ensure the correct resolution is set. We
|
||||
// used to use 60 here but that locked the frame rate to 60 FPS
|
||||
// on GFE 3.20.3.
|
||||
QString::number(streamConfig->fps > 60 ? 0 : streamConfig->fps)+
|
||||
"&additionalStates=1&sops="+QString::number(sops ? 1 : 0)+
|
||||
"&rikey="+QByteArray(streamConfig->remoteInputAesKey, sizeof(streamConfig->remoteInputAesKey)).toHex()+
|
||||
"&rikeyid="+QString::number(riKeyId)+
|
||||
@@ -183,7 +176,7 @@ NvHTTP::launchApp(int appId,
|
||||
"&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),
|
||||
LAUNCH_TIMEOUT_MS);
|
||||
@@ -205,7 +198,7 @@ NvHTTP::resumeApp(PSTREAM_CONFIGURATION streamConfig)
|
||||
"resume",
|
||||
"rikey="+QString(QByteArray(streamConfig->remoteInputAesKey, sizeof(streamConfig->remoteInputAesKey)).toHex())+
|
||||
"&rikeyid="+QString::number(riKeyId)+
|
||||
"&surroundAudioInfo="+getSurroundAudioInfoString(streamConfig->audioConfiguration),
|
||||
"&surroundAudioInfo="+QString::number(SURROUNDAUDIOINFO_FROM_AUDIO_CONFIGURATION(streamConfig->audioConfiguration)),
|
||||
RESUME_TIMEOUT_MS);
|
||||
|
||||
// Throws if the request failed
|
||||
@@ -241,17 +234,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();
|
||||
}
|
||||
}
|
||||
@@ -274,8 +267,8 @@ NvHTTP::getAppList()
|
||||
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";
|
||||
@@ -284,15 +277,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";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -306,9 +302,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
|
||||
@@ -321,6 +320,12 @@ 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);
|
||||
}
|
||||
}
|
||||
@@ -377,6 +382,28 @@ 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,
|
||||
@@ -388,7 +415,13 @@ NvHTTP::openConnectionToString(QUrl baseUrl,
|
||||
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;
|
||||
|
||||
@@ -413,35 +446,27 @@ NvHTTP::openConnection(QUrl baseUrl,
|
||||
QUuid::createUuid().toRfc4122().toHex() +
|
||||
((arguments != nullptr) ? ("&" + arguments) : ""));
|
||||
|
||||
QNetworkRequest request = QNetworkRequest(url);
|
||||
QNetworkRequest request(url);
|
||||
|
||||
// Add our client certificate
|
||||
request.setSslConfiguration(IdentityManager::get()->getSslConfig());
|
||||
|
||||
#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);
|
||||
|
||||
// Assert that we always have a server cert for HTTPS, since
|
||||
// the request will fail anyway if we do not.
|
||||
Q_ASSERT(!m_ServerCert.isNull() || baseUrl == m_BaseUrlHttp);
|
||||
|
||||
if (!m_ServerCert.isNull()) {
|
||||
// Pin the server certificate received during pairing
|
||||
QList<QSslError> expectedSslErrors;
|
||||
expectedSslErrors.append(QSslError(QSslError::HostNameMismatch, m_ServerCert));
|
||||
expectedSslErrors.append(QSslError(QSslError::SelfSignedCertificate, m_ServerCert));
|
||||
|
||||
// The SecureTransport backend for Qt TLS on macOS throws CertificateUntrusted
|
||||
// instead of SelfSignedCertificate, so we will need to allow that error too.
|
||||
expectedSslErrors.append(QSslError(QSslError::CertificateUntrusted, m_ServerCert));
|
||||
|
||||
reply->ignoreSslErrors(expectedSslErrors);
|
||||
}
|
||||
|
||||
// Run the request with a timeout if requested
|
||||
QEventLoop loop;
|
||||
QObject::connect(reply, SIGNAL(finished()), &loop, SLOT(quit()));
|
||||
connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
|
||||
connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, &loop, &QEventLoop::quit);
|
||||
if (timeoutMs) {
|
||||
QTimer::singleShot(timeoutMs, &loop, SLOT(quit()));
|
||||
QTimer::singleShot(timeoutMs, &loop, &QEventLoop::quit);
|
||||
}
|
||||
if (logLevel >= NvLogLevel::NVLL_VERBOSE) {
|
||||
qInfo() << "Executing request:" << url.toString();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "identitymanager.h"
|
||||
#include "nvapp.h"
|
||||
|
||||
#include <Limelight.h>
|
||||
|
||||
@@ -8,26 +9,6 @@
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkReply>
|
||||
|
||||
class NvApp
|
||||
{
|
||||
public:
|
||||
bool operator==(const NvApp& other) const
|
||||
{
|
||||
return id == other.id;
|
||||
}
|
||||
|
||||
bool isInitialized()
|
||||
{
|
||||
return id != 0 && !name.isEmpty();
|
||||
}
|
||||
|
||||
int id;
|
||||
QString name;
|
||||
bool hdrSupported;
|
||||
};
|
||||
|
||||
Q_DECLARE_METATYPE(NvApp)
|
||||
|
||||
class NvDisplayMode
|
||||
{
|
||||
public:
|
||||
@@ -42,6 +23,7 @@ public:
|
||||
int height;
|
||||
int refreshRate;
|
||||
};
|
||||
Q_DECLARE_TYPEINFO(NvDisplayMode, Q_PRIMITIVE_TYPE);
|
||||
|
||||
class GfeHttpResponseException : public std::exception
|
||||
{
|
||||
@@ -113,8 +95,10 @@ private:
|
||||
QString m_ErrorText;
|
||||
};
|
||||
|
||||
class NvHTTP
|
||||
class NvHTTP : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
enum NvLogLevel {
|
||||
NVLL_NONE,
|
||||
@@ -129,7 +113,7 @@ public:
|
||||
getCurrentGame(QString serverInfo);
|
||||
|
||||
QString
|
||||
getServerInfo(NvLogLevel logLevel);
|
||||
getServerInfo(NvLogLevel logLevel, bool fastFail = false);
|
||||
|
||||
static
|
||||
void
|
||||
@@ -154,6 +138,10 @@ public:
|
||||
|
||||
void setServerCert(QSslCertificate serverCert);
|
||||
|
||||
void setAddress(QString address);
|
||||
|
||||
QString address();
|
||||
|
||||
static
|
||||
QVector<int>
|
||||
parseQuad(QString quad);
|
||||
@@ -184,6 +172,9 @@ public:
|
||||
QUrl m_BaseUrlHttp;
|
||||
QUrl m_BaseUrlHttps;
|
||||
private:
|
||||
void
|
||||
handleSslErrors(QNetworkReply* reply, const QList<QSslError>& errors);
|
||||
|
||||
QNetworkReply*
|
||||
openConnection(QUrl baseUrl,
|
||||
QString command,
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
#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>
|
||||
@@ -51,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);
|
||||
@@ -107,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);
|
||||
@@ -123,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);
|
||||
@@ -133,7 +162,7 @@ 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);
|
||||
@@ -143,7 +172,7 @@ NvPairingManager::signMessage(QByteArray message)
|
||||
|
||||
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_DigestSignUpdate(ctx, reinterpret_cast<unsigned char*>(const_cast<char*>(message.data())), message.length());
|
||||
|
||||
size_t signatureLength = 0;
|
||||
EVP_DigestSignFinal(ctx, NULL, &signatureLength);
|
||||
@@ -157,7 +186,7 @@ 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());
|
||||
}
|
||||
@@ -186,9 +215,8 @@ NvPairingManager::pair(QString appVersion, QString pin, QSslCertificate& serverC
|
||||
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",
|
||||
@@ -210,20 +238,21 @@ NvPairingManager::pair(QString appVersion, QString pin, QSslCertificate& serverC
|
||||
return PairState::ALREADY_IN_PROGRESS;
|
||||
}
|
||||
|
||||
serverCert = QSslCertificate(serverCertStr);
|
||||
if (serverCert.isNull()) {
|
||||
Q_ASSERT(!serverCert.isNull());
|
||||
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
|
||||
m_Http.setServerCert(serverCert);
|
||||
// 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=" +
|
||||
@@ -237,7 +266,7 @@ NvPairingManager::pair(QString appVersion, QString pin, QSslCertificate& serverC
|
||||
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);
|
||||
@@ -256,7 +285,9 @@ NvPairingManager::pair(QString appVersion, QString pin, QSslCertificate& serverC
|
||||
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=" +
|
||||
@@ -323,5 +354,6 @@ NvPairingManager::pair(QString appVersion, QString pin, QSslCertificate& serverC
|
||||
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>
|
||||
|
||||
@@ -30,22 +29,22 @@ private:
|
||||
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,252 @@
|
||||
#include "systemproperties.h"
|
||||
#include "utils.h"
|
||||
|
||||
#include <QGuiApplication>
|
||||
|
||||
#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";
|
||||
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(maximumStreamingFrameRate >= 60);
|
||||
Q_ASSERT(!monitorDesktopResolutions.isEmpty());
|
||||
Q_ASSERT(!monitorNativeResolutions.isEmpty());
|
||||
}
|
||||
|
||||
QRect SystemProperties::getDesktopResolution(int displayIndex)
|
||||
{
|
||||
// Returns default constructed QRect if out of bounds
|
||||
return monitorDesktopResolutions.value(displayIndex);
|
||||
}
|
||||
|
||||
QRect SystemProperties::getNativeResolution(int displayIndex)
|
||||
{
|
||||
// Returns default constructed QRect if out of bounds
|
||||
return monitorNativeResolutions.value(displayIndex);
|
||||
}
|
||||
|
||||
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, 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;
|
||||
}
|
||||
|
||||
monitorDesktopResolutions.clear();
|
||||
monitorNativeResolutions.clear();
|
||||
|
||||
// Never let the maximum drop below 60 FPS
|
||||
maximumStreamingFrameRate = 60;
|
||||
|
||||
SDL_DisplayMode bestMode;
|
||||
for (int displayIndex = 0; displayIndex < SDL_GetNumVideoDisplays(); displayIndex++) {
|
||||
SDL_DisplayMode desktopMode;
|
||||
int err;
|
||||
|
||||
err = SDL_GetDesktopDisplayMode(displayIndex, &desktopMode);
|
||||
if (err == 0) {
|
||||
if (desktopMode.w <= 8192 && desktopMode.h <= 8192) {
|
||||
monitorDesktopResolutions.insert(displayIndex, QRect(0, 0, desktopMode.w, desktopMode.h));
|
||||
}
|
||||
else {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Skipping resolution over 8K: %dx%d",
|
||||
desktopMode.w, desktopMode.h);
|
||||
}
|
||||
}
|
||||
else {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"SDL_GetDesktopDisplayMode() failed: %s",
|
||||
SDL_GetError());
|
||||
}
|
||||
|
||||
if (StreamUtils::getRealDesktopMode(displayIndex, &desktopMode)) {
|
||||
if (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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
maximumStreamingFrameRate = qMax(maximumStreamingFrameRate, bestMode.refresh_rate);
|
||||
}
|
||||
}
|
||||
|
||||
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
#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(int maximumStreamingFrameRate MEMBER maximumStreamingFrameRate CONSTANT)
|
||||
Q_PROPERTY(QSize maximumResolution MEMBER maximumResolution CONSTANT)
|
||||
Q_PROPERTY(QString versionString MEMBER versionString CONSTANT)
|
||||
|
||||
Q_INVOKABLE void refreshDisplays();
|
||||
Q_INVOKABLE QRect getDesktopResolution(int displayIndex);
|
||||
Q_INVOKABLE QRect getNativeResolution(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;
|
||||
int maximumStreamingFrameRate;
|
||||
QSize maximumResolution;
|
||||
QList<QRect> monitorDesktopResolutions;
|
||||
QList<QRect> monitorNativeResolutions;
|
||||
QString versionString;
|
||||
};
|
||||
|
||||
@@ -52,14 +52,14 @@ public:
|
||||
|
||||
void handleUnknownOptions()
|
||||
{
|
||||
if (unknownOptionNames().length()) {
|
||||
if (!unknownOptionNames().isEmpty()) {
|
||||
showError(QString("Unknown options: %1").arg(unknownOptionNames().join(", ")));
|
||||
}
|
||||
}
|
||||
|
||||
void showMessage(QString message, MessageType type) const
|
||||
{
|
||||
#if defined(Q_OS_WIN)
|
||||
#if defined(Q_OS_WIN32)
|
||||
UINT flags = MB_OK | MB_TOPMOST | MB_SETFOREGROUND;
|
||||
flags |= (type == Info ? MB_ICONINFORMATION : MB_ICONERROR);
|
||||
QString title = "Moonlight";
|
||||
@@ -172,20 +172,31 @@ GlobalCommandLineParser::ParseResult GlobalCommandLineParser::parse(const QStrin
|
||||
parser.addPositionalArgument("action", "Action to execute", "<action>");
|
||||
parser.parse(args);
|
||||
auto posArgs = parser.positionalArguments();
|
||||
QString action = posArgs.isEmpty() ? QString() : posArgs.first().toLower();
|
||||
|
||||
if (action == "") {
|
||||
if (posArgs.isEmpty()) {
|
||||
// This method will not return and terminates the process if --version
|
||||
// or --help is specified
|
||||
parser.handleHelpAndVersionOptions();
|
||||
parser.handleUnknownOptions();
|
||||
return NormalStartRequested;
|
||||
} else if (action == "quit") {
|
||||
return QuitRequested;
|
||||
} else if (action == "stream") {
|
||||
return StreamRequested;
|
||||
} else {
|
||||
parser.showError(QString("Invalid action: %1").arg(action));
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
parser.showError(QString("Invalid action"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,6 +252,7 @@ StreamCommandLineParser::StreamCommandLineParser()
|
||||
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},
|
||||
@@ -249,8 +261,13 @@ StreamCommandLineParser::StreamCommandLineParser()
|
||||
};
|
||||
m_VideoDecoderMap = {
|
||||
{"auto", StreamingPreferences::VDS_AUTO},
|
||||
{"software", StreamingPreferences::VDS_FORCE_HARDWARE},
|
||||
{"hardware", StreamingPreferences::VDS_FORCE_SOFTWARE},
|
||||
{"software", StreamingPreferences::VDS_FORCE_SOFTWARE},
|
||||
{"hardware", StreamingPreferences::VDS_FORCE_HARDWARE},
|
||||
};
|
||||
m_CaptureSysKeysModeMap = {
|
||||
{"never", StreamingPreferences::CSK_OFF},
|
||||
{"fullscreen", StreamingPreferences::CSK_FULLSCREEN},
|
||||
{"always", StreamingPreferences::CSK_ALWAYS},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -280,14 +297,22 @@ void StreamCommandLineParser::parse(const QStringList &args, StreamingPreference
|
||||
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("mouse-acceleration", "mouse acceleration");
|
||||
parser.addToggleOption("absolute-mouse", "remote desktop optimized mouse control");
|
||||
parser.addToggleOption("mouse-buttons-swap", "left and right mouse buttons swap");
|
||||
parser.addToggleOption("touchscreen-trackpad", "touchscreen in trackpad mode");
|
||||
parser.addToggleOption("game-optimization", "game optimizations");
|
||||
parser.addToggleOption("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.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());
|
||||
|
||||
@@ -300,25 +325,21 @@ void StreamCommandLineParser::parse(const QStringList &args, StreamingPreference
|
||||
// Resolve display's width and height
|
||||
QRegularExpression resolutionRexExp("^(720|1080|1440|4K|resolution)$");
|
||||
QStringList resoOptions = parser.optionNames().filter(resolutionRexExp);
|
||||
bool displaySet = resoOptions.length();
|
||||
bool displaySet = !resoOptions.isEmpty();
|
||||
if (displaySet) {
|
||||
QString name = resoOptions.last();
|
||||
if (name == "720") {
|
||||
preferences->width = 1280;
|
||||
preferences->height = 720;
|
||||
displaySet = true;
|
||||
} else if (name == "1080") {
|
||||
preferences->width = 1920;
|
||||
preferences->height = 1080;
|
||||
displaySet = true;
|
||||
} else if (name == "1440") {
|
||||
preferences->width = 2560;
|
||||
preferences->height = 1440;
|
||||
displaySet = true;
|
||||
} else if (name == "4K") {
|
||||
preferences->width = 3840;
|
||||
preferences->height = 2160;
|
||||
displaySet = true;
|
||||
} else if (name == "resolution") {
|
||||
auto resolution = parser.getResolutionOptionValue(name);
|
||||
preferences->width = resolution.first;
|
||||
@@ -345,6 +366,14 @@ void StreamCommandLineParser::parse(const QStringList &args, StreamingPreference
|
||||
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"));
|
||||
@@ -364,8 +393,14 @@ void StreamCommandLineParser::parse(const QStringList &args, StreamingPreference
|
||||
// Resolve --quit-after and --no-quit-after options
|
||||
preferences->quitAppAfter = parser.getToggleOptionValue("quit-after", preferences->quitAppAfter);
|
||||
|
||||
// Resolve --mouse-acceleration and --no-mouse-acceleration options
|
||||
preferences->mouseAcceleration = parser.getToggleOptionValue("mouse-acceleration", preferences->mouseAcceleration);
|
||||
// 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);
|
||||
@@ -376,6 +411,23 @@ void StreamCommandLineParser::parse(const QStringList &args, StreamingPreference
|
||||
// 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 --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"));
|
||||
|
||||
@@ -53,4 +53,5 @@ private:
|
||||
QMap<QString, StreamingPreferences::AudioConfig> m_AudioConfigMap;
|
||||
QMap<QString, StreamingPreferences::VideoCodecConfig> m_VideoCodecMap;
|
||||
QMap<QString, StreamingPreferences::VideoDecoderSelection> m_VideoDecoderMap;
|
||||
QMap<QString, StreamingPreferences::CaptureSysKeysMode> m_CaptureSysKeysModeMap;
|
||||
};
|
||||
|
||||
@@ -73,7 +73,7 @@ public:
|
||||
case Event::ComputerSeekTimedout:
|
||||
if (m_State == StateSeekComputer) {
|
||||
m_State = StateFailure;
|
||||
emit q->failed(QString("Failed to connect to %1").arg(m_ComputerName));
|
||||
emit q->failed(QObject::tr("Failed to connect to %1").arg(m_ComputerName));
|
||||
}
|
||||
break;
|
||||
// Occurs when searched computer is found
|
||||
@@ -85,8 +85,8 @@ public:
|
||||
m_ComputerManager->quitRunningApp(event.computer);
|
||||
} else {
|
||||
m_State = StateFailure;
|
||||
QString msg = QString("Computer %1 has not been paired. "
|
||||
"Please open Moonlight to pair before streaming.")
|
||||
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);
|
||||
}
|
||||
@@ -99,7 +99,7 @@ public:
|
||||
QCoreApplication::exit(0);
|
||||
} else {
|
||||
m_State = StateFailure;
|
||||
emit q->failed(QString("Quitting app failed, reason: %1").arg(event.errorMessage));
|
||||
emit q->failed(QObject::tr("Quitting app failed, reason: %1").arg(event.errorMessage));
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -86,8 +86,8 @@ public:
|
||||
emit q->searchingApp();
|
||||
} else {
|
||||
m_State = StateFailure;
|
||||
QString msg = QString("Computer %1 has not been paired. "
|
||||
"Please open Moonlight to pair before streaming.")
|
||||
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);
|
||||
}
|
||||
@@ -123,18 +123,18 @@ public:
|
||||
case Event::AppQuitCompleted:
|
||||
if (m_State == StateSeekApp && !event.errorMessage.isEmpty()) {
|
||||
m_State = StateFailure;
|
||||
emit q->failed(QString("Quitting app failed, reason: %1").arg(event.errorMessage));
|
||||
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(QString("Failed to connect to %1").arg(m_ComputerName));
|
||||
emit q->failed(QObject::tr("Failed to connect to %1").arg(m_ComputerName));
|
||||
}
|
||||
if (m_State == StateSeekApp) {
|
||||
m_State = StateFailure;
|
||||
emit q->failed(QString("Failed to find application %1").arg(m_AppName));
|
||||
emit q->failed(QObject::tr("Failed to find application %1").arg(m_AppName));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -7,15 +7,18 @@
|
||||
<summary>Play games remotely from your NVIDIA GameStream-enabled PC</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 a PC with an NVIDIA GeForce GTX/RTX or NVIDIA Quadro GPU and GeForce/Quadro Experience installed.</p>
|
||||
<p>Features include:</p>
|
||||
<ul>
|
||||
<li>Streaming at up to 4K resolution</li>
|
||||
<li>Support for up to 120 FPS streaming (high refresh rate monitor recommended)</li>
|
||||
<li>Hardware accelerated video decoding with VAAPI and VDPAU support</li>
|
||||
<li>5.1 surround sound audio support</li>
|
||||
<li>Hardware accelerated video decoding with VAAPI, VDPAU, and NVDEC support</li>
|
||||
<li>7.1 surround sound audio support</li>
|
||||
<li>Support for both pointer capture (for games) and direct mouse control (for remote desktop)</li>
|
||||
<li>Support for passing system-wide keyboard shortcuts like Alt+Tab to the host</li>
|
||||
<li>Direct touchscreen mouse control</li>
|
||||
<li>HEVC support for better video compression efficiency</li>
|
||||
<li>Gamepad support with SDL mapping compatibility</li>
|
||||
<li>Gamepad support with force feedback for up to 4 players</li>
|
||||
</ul>
|
||||
</description>
|
||||
|
||||
@@ -33,7 +36,236 @@
|
||||
</screenshots>
|
||||
|
||||
<releases>
|
||||
<release version="0.9.0" date="2019-02-17">
|
||||
<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>
|
||||
|
||||
|
After Width: | Height: | Size: 2.9 KiB |
@@ -0,0 +1,13 @@
|
||||
#!/bin/sh
|
||||
|
||||
# The default HOME is not persistent, so override
|
||||
# it to a path on the onboard flash. Otherwise our
|
||||
# pairing data will be lost each reboot.
|
||||
HOME=/usr/local/moonlight
|
||||
|
||||
# Renice PE_Single_CPU which seems to host A/V stuff
|
||||
renice -10 -p $(pidof PE_Single_CPU)
|
||||
|
||||
# Renice Moonlight itself to avoid preemption by background tasks
|
||||
# Write output to a logfile in /tmp
|
||||
exec nice -n -10 ./bin/moonlight > /tmp/moonlight.log
|
||||
@@ -0,0 +1,3 @@
|
||||
name=Moonlight
|
||||
icon=moonlight.png
|
||||
run=moonlight.sh
|
||||
@@ -1,29 +1,23 @@
|
||||
import QtQuick 2.9
|
||||
import QtQuick.Dialogs 1.2
|
||||
import QtQuick.Controls 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
|
||||
focus: true
|
||||
activeFocusOnTab: true
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: (parent.width % (cellWidth + anchors.rightMargin)) / 2
|
||||
anchors.topMargin: 20
|
||||
anchors.rightMargin: 5
|
||||
anchors.bottomMargin: 5
|
||||
cellWidth: 225; cellHeight: 385;
|
||||
|
||||
// The StackView will trigger a visibility change when
|
||||
// we're pushed onto it, causing our onVisibleChanged
|
||||
// routine to run, but only if we start as invisible
|
||||
visible: false
|
||||
topMargin: 20
|
||||
bottomMargin: 5
|
||||
cellWidth: 230; cellHeight: 297;
|
||||
|
||||
function computerLost()
|
||||
{
|
||||
@@ -32,158 +26,255 @@ GridView {
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
// Don't show any highlighted item until interacting with them
|
||||
// Don't show any highlighted item until interacting with them.
|
||||
// We do this here instead of onActivated to avoid losing the user's
|
||||
// selection when backing out of a different page of the app.
|
||||
currentIndex = -1
|
||||
}
|
||||
|
||||
SdlGamepadKeyNavigation {
|
||||
id: gamepadKeyNav
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onVisibleChanged: {
|
||||
if (visible) {
|
||||
appModel.computerLost.connect(computerLost)
|
||||
gamepadKeyNav.enable()
|
||||
}
|
||||
else {
|
||||
appModel.computerLost.disconnect(computerLost)
|
||||
gamepadKeyNav.disable()
|
||||
}
|
||||
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: NavigableItemDelegate {
|
||||
width: 200; height: 335;
|
||||
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
|
||||
y: 20
|
||||
y: 10
|
||||
source: model.boxart
|
||||
sourceSize {
|
||||
width: 150
|
||||
height: 200
|
||||
}
|
||||
width: sourceSize.width
|
||||
height: sourceSize.height
|
||||
fillMode: Image.Pad
|
||||
}
|
||||
|
||||
ToolButton {
|
||||
id: resumeButton
|
||||
anchors.verticalCenterOffset: -50
|
||||
anchors.centerIn: appIcon
|
||||
visible: model.running
|
||||
implicitWidth: 125
|
||||
implicitHeight: 125
|
||||
|
||||
Image {
|
||||
source: "qrc:/res/baseline-play_circle_filled_white-48px.svg"
|
||||
anchors.centerIn: parent
|
||||
sourceSize {
|
||||
width: 75
|
||||
height: 75
|
||||
onSourceSizeChanged: {
|
||||
if ((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
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
launchOrResumeSelectedApp()
|
||||
}
|
||||
|
||||
ToolTip.text: "Resume Game"
|
||||
ToolTip.delay: 1000
|
||||
ToolTip.timeout: 3000
|
||||
ToolTip.visible: hovered
|
||||
}
|
||||
|
||||
ToolButton {
|
||||
id: quitButton
|
||||
anchors.verticalCenterOffset: 50
|
||||
anchors.centerIn: appIcon
|
||||
visible: model.running
|
||||
implicitWidth: 125
|
||||
implicitHeight: 125
|
||||
|
||||
Image {
|
||||
source: "qrc:/res/baseline-cancel-24px.svg"
|
||||
anchors.centerIn: parent
|
||||
sourceSize {
|
||||
width: 75
|
||||
height: 75
|
||||
else
|
||||
{
|
||||
isPlaceholder = false
|
||||
}
|
||||
|
||||
width = 200
|
||||
height = 267
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
doQuitGame()
|
||||
}
|
||||
|
||||
ToolTip.text: "Quit Game"
|
||||
ToolTip.delay: 1000
|
||||
ToolTip.timeout: 3000
|
||||
ToolTip.visible: hovered
|
||||
}
|
||||
|
||||
Label {
|
||||
id: appNameText
|
||||
text: model.name
|
||||
width: parent.width
|
||||
height: 125
|
||||
anchors.top: appIcon.bottom
|
||||
font.pointSize: 22
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
wrapMode: Text.Wrap
|
||||
elide: Text.ElideRight
|
||||
|
||||
// Display a tooltip with the full name if it's truncated
|
||||
ToolTip.text: model.name
|
||||
ToolTip.delay: 1000
|
||||
ToolTip.timeout: 5000
|
||||
ToolTip.visible: (parent.hovered || parent.highlighted) && truncated
|
||||
ToolTip.visible: (parent.hovered || parent.highlighted) && (!appNameText || appNameText.truncated)
|
||||
}
|
||||
|
||||
function launchOrResumeSelectedApp()
|
||||
Loader {
|
||||
active: model.running
|
||||
asynchronous: true
|
||||
anchors.fill: appIcon
|
||||
|
||||
sourceComponent: Item {
|
||||
ToolButton {
|
||||
anchors.horizontalCenterOffset: appIcon.isPlaceholder ? -47 : 0
|
||||
anchors.verticalCenterOffset: appIcon.isPlaceholder ? -75 : -60
|
||||
anchors.centerIn: parent
|
||||
implicitWidth: 125
|
||||
implicitHeight: 125
|
||||
|
||||
Image {
|
||||
source: "qrc:/res/baseline-play_circle_filled_white-48px.svg"
|
||||
anchors.centerIn: parent
|
||||
sourceSize {
|
||||
width: 75
|
||||
height: 75
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
launchOrResumeSelectedApp(true)
|
||||
}
|
||||
|
||||
ToolTip.text: qsTr("Resume Game")
|
||||
ToolTip.delay: 1000
|
||||
ToolTip.timeout: 3000
|
||||
ToolTip.visible: hovered
|
||||
}
|
||||
|
||||
ToolButton {
|
||||
anchors.horizontalCenterOffset: appIcon.isPlaceholder ? 47 : 0
|
||||
anchors.verticalCenterOffset: appIcon.isPlaceholder ? -75 : 60
|
||||
anchors.centerIn: parent
|
||||
implicitWidth: 125
|
||||
implicitHeight: 125
|
||||
|
||||
Image {
|
||||
source: "qrc:/res/baseline-cancel-24px.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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
function launchOrResumeSelectedApp(quitExistingApp)
|
||||
{
|
||||
var runningIndex = appModel.getRunningAppIndex()
|
||||
if (runningIndex >= 0 && runningIndex !== index) {
|
||||
quitAppDialog.appName = appModel.getRunningAppName()
|
||||
quitAppDialog.segueToStream = true
|
||||
quitAppDialog.nextAppName = model.name
|
||||
quitAppDialog.nextAppIndex = index
|
||||
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, {"appName": model.name, "session": appModel.createSessionForApp(index)})
|
||||
var segue = component.createObject(stackView, {
|
||||
"appName": model.name,
|
||||
"session": appModel.createSessionForApp(index),
|
||||
"isResume": runningId === model.appid
|
||||
})
|
||||
stackView.push(segue)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
onPressAndHold: {
|
||||
// popup() ensures the menu appears under the mouse cursor
|
||||
if (appContextMenu.popup) {
|
||||
appContextMenu.popup()
|
||||
}
|
||||
else {
|
||||
// Qt 5.9 doesn't have popup()
|
||||
appContextMenu.open()
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.RightButton;
|
||||
onClicked: {
|
||||
parent.onPressAndHold()
|
||||
}
|
||||
}
|
||||
|
||||
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 primarily be keyboard/gamepad driven so use
|
||||
// This will be keyboard/gamepad driven so use
|
||||
// open() instead of popup()
|
||||
appContextMenu.open()
|
||||
}
|
||||
else {
|
||||
launchOrResumeSelectedApp()
|
||||
}
|
||||
|
||||
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: {
|
||||
if (model.running) {
|
||||
// This will primarily be keyboard/gamepad driven so use
|
||||
// open() instead of popup()
|
||||
appContextMenu.open()
|
||||
}
|
||||
// This will be keyboard/gamepad driven so use open() instead of popup()
|
||||
appContextMenu.open()
|
||||
}
|
||||
|
||||
function doQuitGame() {
|
||||
@@ -192,34 +283,60 @@ GridView {
|
||||
quitAppDialog.open()
|
||||
}
|
||||
|
||||
NavigableMenu {
|
||||
id: appContextMenu
|
||||
NavigableMenuItem {
|
||||
parentMenu: appContextMenu
|
||||
text: model.running ? "Resume Game" : "Launch Game"
|
||||
onTriggered: {
|
||||
appContextMenu.close()
|
||||
launchOrResumeSelectedApp()
|
||||
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
|
||||
}
|
||||
}
|
||||
NavigableMenuItem {
|
||||
parentMenu: appContextMenu
|
||||
text: "Quit Game"
|
||||
onTriggered: doQuitGame()
|
||||
visible: model.running
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MessageDialog {
|
||||
NavigableMessageDialog {
|
||||
id: quitAppDialog
|
||||
modality:Qt.WindowModal
|
||||
property string appName : ""
|
||||
property bool segueToStream : false
|
||||
property string nextAppName: ""
|
||||
property int nextAppIndex: 0
|
||||
text:"Are you sure you want to quit " + appName +"? Any unsaved progress will be lost."
|
||||
standardButtons: StandardButton.Yes | StandardButton.No
|
||||
text:qsTr("Are you sure you want to quit %1? Any unsaved progress will be lost.").arg(appName)
|
||||
standardButtons: Dialog.Yes | Dialog.No
|
||||
|
||||
function quitApp() {
|
||||
var component = Qt.createComponent("QuitSegue.qml")
|
||||
@@ -241,20 +358,8 @@ GridView {
|
||||
appModel.quitRunningApp()
|
||||
}
|
||||
|
||||
onYes: quitApp()
|
||||
|
||||
// For keyboard/gamepad navigation
|
||||
onAccepted: quitApp()
|
||||
}
|
||||
|
||||
ScrollBar.vertical: ScrollBar {
|
||||
parent: appGrid.parent
|
||||
anchors {
|
||||
top: appGrid.top
|
||||
left: appGrid.right
|
||||
bottom: appGrid.bottom
|
||||
|
||||
leftMargin: -10
|
||||
}
|
||||
}
|
||||
ScrollBar.vertical: ScrollBar {}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import QtQuick 2.9
|
||||
import QtQuick.Controls 2.2
|
||||
|
||||
import SdlGamepadKeyNavigation 1.0
|
||||
|
||||
// https://stackoverflow.com/questions/45029968/how-do-i-set-the-combobox-width-to-fit-the-largest-item
|
||||
ComboBox {
|
||||
property int textWidth
|
||||
property int desiredWidth : leftPadding + textWidth + indicator.width + rightPadding
|
||||
property int maximumWidth : parent.width
|
||||
|
||||
implicitWidth: leftPadding + textWidth + indicator.width + rightPadding
|
||||
implicitWidth: desiredWidth < maximumWidth ? desiredWidth : maximumWidth
|
||||
|
||||
TextMetrics {
|
||||
id: popupMetrics
|
||||
@@ -15,9 +19,7 @@ ComboBox {
|
||||
id: textMetrics
|
||||
}
|
||||
|
||||
// We call this every time the options change (and init)
|
||||
// so we can adjust the combo box width here too
|
||||
onActivated: {
|
||||
function recalculateWidth() {
|
||||
textMetrics.font = font
|
||||
popupMetrics.font = popup.font
|
||||
textWidth = 0
|
||||
@@ -28,4 +30,17 @@ ComboBox {
|
||||
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)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -1,19 +1,16 @@
|
||||
import QtQuick 2.0
|
||||
import QtQuick.Controls 2.2
|
||||
import QtQuick.Dialogs 1.2
|
||||
|
||||
import ComputerManager 1.0
|
||||
import Session 1.0
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
|
||||
function onSearchingComputer() {
|
||||
stageLabel.text = "Establishing connection to PC..."
|
||||
stageLabel.text = qsTr("Establishing connection to PC...")
|
||||
}
|
||||
|
||||
function onQuittingApp() {
|
||||
stageLabel.text = "Quitting app..."
|
||||
stageLabel.text = qsTr("Quitting app...")
|
||||
}
|
||||
|
||||
function onFailure(message) {
|
||||
@@ -21,13 +18,8 @@ Item {
|
||||
errorDialog.open()
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
onVisibleChanged: {
|
||||
if (visible && !launcher.isExecuted()) {
|
||||
StackView.onActivated: {
|
||||
if (!launcher.isExecuted()) {
|
||||
toolBar.visible = false
|
||||
launcher.searchingComputer.connect(onSearchingComputer)
|
||||
launcher.quittingApp.connect(onQuittingApp)
|
||||
@@ -55,16 +47,11 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
MessageDialog {
|
||||
ErrorMessageDialog {
|
||||
id: errorDialog
|
||||
modality:Qt.WindowModal
|
||||
icon: StandardIcon.Critical
|
||||
standardButtons: StandardButton.Ok
|
||||
|
||||
onVisibleChanged: {
|
||||
if (!visible) {
|
||||
Qt.quit()
|
||||
}
|
||||
onClosed: {
|
||||
Qt.quit()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
import QtQml 2.2
|
||||
import QtQuick 2.0
|
||||
import QtQuick.Controls 2.2
|
||||
import QtQuick.Dialogs 1.2
|
||||
|
||||
import ComputerManager 1.0
|
||||
import SdlGamepadKeyNavigation 1.0
|
||||
|
||||
Item {
|
||||
visible: false
|
||||
|
||||
function onSearchingComputer() {
|
||||
stageLabel.text = "Establishing connection to PC..."
|
||||
stageLabel.text = qsTr("Establishing connection to PC...")
|
||||
}
|
||||
|
||||
function onSearchingApp() {
|
||||
stageLabel.text = "Loading app list..."
|
||||
stageLabel.text = qsTr("Loading app list...")
|
||||
}
|
||||
|
||||
function onSessionCreated(appName, session) {
|
||||
@@ -36,9 +33,14 @@ Item {
|
||||
quitAppDialog.open()
|
||||
}
|
||||
|
||||
onVisibleChanged: {
|
||||
if (visible && !launcher.isExecuted()) {
|
||||
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)
|
||||
@@ -66,26 +68,18 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
MessageDialog {
|
||||
ErrorMessageDialog {
|
||||
id: errorDialog
|
||||
modality:Qt.WindowModal
|
||||
icon: StandardIcon.Critical
|
||||
standardButtons: StandardButton.Ok | StandardButton.Help
|
||||
onHelp: {
|
||||
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Troubleshooting");
|
||||
}
|
||||
onVisibleChanged: {
|
||||
if (!visible) {
|
||||
Qt.quit();
|
||||
}
|
||||
|
||||
onClosed: {
|
||||
Qt.quit();
|
||||
}
|
||||
}
|
||||
|
||||
MessageDialog {
|
||||
NavigableMessageDialog {
|
||||
id: quitAppDialog
|
||||
modality:Qt.WindowModal
|
||||
text:"Are you sure you want to quit " + appName +"? Any unsaved progress will be lost."
|
||||
standardButtons: StandardButton.Yes | StandardButton.No
|
||||
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() {
|
||||
@@ -96,29 +90,7 @@ Item {
|
||||
launcher.quitRunningApp()
|
||||
}
|
||||
|
||||
onYes: quitApp()
|
||||
|
||||
// For keyboard/gamepad navigation
|
||||
onAccepted: quitApp()
|
||||
|
||||
// Exit process if app quit is rejected (reacts also to closing of the
|
||||
// dialog from title bar's close button).
|
||||
// Note: this depends on undocumented behavior of visibleChanged()
|
||||
// signal being emitted before yes() or accepted() has been emitted.
|
||||
onVisibleChanged: {
|
||||
if (!visible) {
|
||||
quitTimer.start()
|
||||
}
|
||||
}
|
||||
Component.onCompleted: {
|
||||
yes.connect(quitTimer.stop)
|
||||
accepted.connect(quitTimer.stop)
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: quitTimer
|
||||
interval: 100
|
||||
onTriggered: Qt.quit()
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -21,4 +21,7 @@ ItemDelegate {
|
||||
Keys.onReturnPressed: {
|
||||
clicked()
|
||||
}
|
||||
Keys.onEnterPressed: {
|
||||
clicked()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,13 @@ import QtQuick.Controls 2.2
|
||||
|
||||
Menu {
|
||||
onOpened: {
|
||||
itemAt(0).forceActiveFocus(Qt.TabFocusReason)
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,10 +11,21 @@ MenuItem {
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,35 @@ import QtQuick 2.0
|
||||
import QtQuick.Controls 2.2
|
||||
|
||||
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.
|
||||
background.width: (parent.height - parent.anchors.bottomMargin - parent.anchors.topMargin) * 0.60
|
||||
background.height: (parent.height - parent.anchors.bottomMargin - parent.anchors.topMargin) * 0.60
|
||||
|
||||
Keys.onReturnPressed: {
|
||||
clicked()
|
||||
}
|
||||
|
||||
Keys.onEnterPressed: {
|
||||
clicked()
|
||||
}
|
||||
|
||||
Keys.onRightPressed: {
|
||||
nextItemInFocusChain(true).forceActiveFocus(Qt.TabFocus)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import QtQuick 2.9
|
||||
import QtQuick.Controls 2.2
|
||||
import QtQuick.Dialogs 1.2
|
||||
import QtQuick.Layouts 1.3
|
||||
import QtQuick.Window 2.2
|
||||
|
||||
import ComputerModel 1.0
|
||||
|
||||
@@ -10,48 +8,44 @@ import ComputerManager 1.0
|
||||
import StreamingPreferences 1.0
|
||||
import SdlGamepadKeyNavigation 1.0
|
||||
|
||||
GridView {
|
||||
CenteredGridView {
|
||||
property ComputerModel computerModel : createModel()
|
||||
|
||||
id: pcGrid
|
||||
focus: true
|
||||
activeFocusOnTab: true
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: (parent.width % (cellWidth + anchors.rightMargin)) / 2
|
||||
anchors.topMargin: 20
|
||||
anchors.rightMargin: 5
|
||||
anchors.bottomMargin: 5
|
||||
cellWidth: 350; cellHeight: 350;
|
||||
objectName: "Computers"
|
||||
|
||||
// The StackView will trigger a visibility change when
|
||||
// we're pushed onto it, causing our onVisibleChanged
|
||||
// routine to run, but only if we start as invisible
|
||||
visible: false
|
||||
|
||||
StreamingPreferences {
|
||||
id: prefs
|
||||
}
|
||||
|
||||
SdlGamepadKeyNavigation {
|
||||
id: gamepadKeyNav
|
||||
}
|
||||
|
||||
onVisibleChanged: {
|
||||
if (visible) {
|
||||
gamepadKeyNav.enable()
|
||||
}
|
||||
else {
|
||||
gamepadKeyNav.disable()
|
||||
}
|
||||
}
|
||||
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)
|
||||
|
||||
// Don't show any highlighted item until interacting with them
|
||||
currentIndex = -1
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
StackView.onDeactivating: {
|
||||
ComputerManager.computerAddCompleted.disconnect(addComplete)
|
||||
}
|
||||
|
||||
function pairingComplete(error)
|
||||
@@ -62,14 +56,23 @@ GridView {
|
||||
// Display a failed dialog if we got an error
|
||||
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. Click the Help button for possible solutions."
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -79,21 +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 PCs on your local network with NVIDIA GameStream enabled...")
|
||||
: qsTr("Automatic PC discovery is disabled. Add your PC manually.")
|
||||
font.pointSize: 20
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
}
|
||||
|
||||
model: computerModel
|
||||
|
||||
delegate: NavigableItemDelegate {
|
||||
width: 300; height: 300;
|
||||
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
|
||||
@@ -105,8 +130,8 @@ GridView {
|
||||
id: stateIcon
|
||||
anchors.horizontalCenter: pcIcon.horizontalCenter
|
||||
anchors.verticalCenter: pcIcon.verticalCenter
|
||||
anchors.verticalCenterOffset: -10
|
||||
visible: !model.addPc && !model.statusUnknown && (!model.online || !model.paired)
|
||||
anchors.verticalCenterOffset: -15
|
||||
visible: !model.statusUnknown && (!model.online || !model.paired)
|
||||
source: !model.online ? "qrc:/res/baseline-warning-24px.svg" : "qrc:/res/baseline-lock-24px.svg"
|
||||
sourceSize {
|
||||
width: 75
|
||||
@@ -118,10 +143,10 @@ 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
|
||||
}
|
||||
|
||||
Label {
|
||||
@@ -130,35 +155,71 @@ GridView {
|
||||
|
||||
width: parent.width
|
||||
anchors.top: pcIcon.bottom
|
||||
anchors.bottom: parent.bottom
|
||||
font.pointSize: 36
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
wrapMode: Text.Wrap
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
NavigableMenu {
|
||||
id: pcContextMenu
|
||||
NavigableMenuItem {
|
||||
parentMenu: pcContextMenu
|
||||
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
|
||||
// get confirmation first, actual closing is called from the dialog
|
||||
deletePcDialog.open()
|
||||
}
|
||||
}
|
||||
}
|
||||
NavigableMenuItem {
|
||||
parentMenu: pcContextMenu
|
||||
text: "Wake PC"
|
||||
onTriggered: computerModel.wakeComputer(index)
|
||||
visible: !model.addPc && !model.online && model.wakeable
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
if (model.addPc) {
|
||||
addPcDialog.open()
|
||||
}
|
||||
else if (model.online) {
|
||||
if (model.online) {
|
||||
if (model.paired) {
|
||||
// go to game view
|
||||
var component = Qt.createComponent("AppView.qml")
|
||||
@@ -167,7 +228,7 @@ GridView {
|
||||
}
|
||||
else {
|
||||
if (!model.busy) {
|
||||
var pin = ("0000" + Math.floor(Math.random() * 10000)).slice(-4)
|
||||
var pin = computerModel.generatePinString()
|
||||
|
||||
// Kick off pairing in the background
|
||||
computerModel.pairComputer(index, pin)
|
||||
@@ -178,7 +239,8 @@ GridView {
|
||||
}
|
||||
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.text = qsTr("You cannot pair while a previous session is still running on the host PC. Quit any running games or reboot the host PC, then try pairing again.")
|
||||
errorDialog.helpText = ""
|
||||
errorDialog.open()
|
||||
}
|
||||
}
|
||||
@@ -189,15 +251,13 @@ GridView {
|
||||
}
|
||||
|
||||
onPressAndHold: {
|
||||
if (!model.addPc) {
|
||||
// popup() ensures the menu appears under the mouse cursor
|
||||
if (pcContextMenu.popup) {
|
||||
pcContextMenu.popup()
|
||||
}
|
||||
else {
|
||||
// Qt 5.9 doesn't have popup()
|
||||
pcContextMenu.open()
|
||||
}
|
||||
// popup() ensures the menu appears under the mouse cursor
|
||||
if (pcContextMenu.popup) {
|
||||
pcContextMenu.popup()
|
||||
}
|
||||
else {
|
||||
// Qt 5.9 doesn't have popup()
|
||||
pcContextMenu.open()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,99 +270,128 @@ GridView {
|
||||
}
|
||||
|
||||
Keys.onMenuPressed: {
|
||||
if (!model.addPc) {
|
||||
// We must use open() here so the menu is positioned on
|
||||
// the ItemDelegate and not where the mouse cursor is
|
||||
pcContextMenu.open()
|
||||
}
|
||||
// We must use open() here so the menu is positioned on
|
||||
// the ItemDelegate and not where the mouse cursor is
|
||||
pcContextMenu.open()
|
||||
}
|
||||
}
|
||||
|
||||
MessageDialog {
|
||||
ErrorMessageDialog {
|
||||
id: errorDialog
|
||||
// don't allow edits to the rest of the window while open
|
||||
modality:Qt.WindowModal
|
||||
icon: StandardIcon.Critical
|
||||
standardButtons: StandardButton.Ok | StandardButton.Help
|
||||
onHelp: {
|
||||
// Using Setup-Guide here instead of Troubleshooting because it's likely that users
|
||||
// will arrive here by forgetting to enable GameStream or not forwarding ports.
|
||||
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Setup-Guide");
|
||||
}
|
||||
|
||||
// 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"
|
||||
}
|
||||
|
||||
MessageDialog {
|
||||
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 GameStream PC. This dialog will close when pairing is completed.").arg(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
|
||||
text:qsTr("Are you sure you want to remove this PC?")
|
||||
standardButtons: Dialog.Yes | Dialog.No
|
||||
|
||||
function deletePc() {
|
||||
console.log("deleting PC pairing for PC at index: " + pcIndex)
|
||||
computerModel.deleteComputer(pcIndex);
|
||||
}
|
||||
|
||||
onYes: deletePc()
|
||||
|
||||
// For keyboard/gamepad activation
|
||||
onAccepted: deletePc()
|
||||
}
|
||||
|
||||
Dialog {
|
||||
id: addPcDialog
|
||||
property string label: "Enter the IP address of your GameStream PC"
|
||||
property string hint: "192.168.1.100"
|
||||
property alias editText : editTextItem
|
||||
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 NVIDIA GameStream is 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 {}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import QtQuick 2.0
|
||||
import QtQuick.Controls 2.2
|
||||
import QtQuick.Dialogs 1.2
|
||||
|
||||
import ComputerManager 1.0
|
||||
import Session 1.0
|
||||
@@ -10,14 +9,7 @@ Item {
|
||||
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)
|
||||
{
|
||||
@@ -34,26 +26,25 @@ Item {
|
||||
stackView.replace(segue)
|
||||
}
|
||||
else {
|
||||
// Show the toolbar again
|
||||
toolBar.visible = true
|
||||
|
||||
// Exit this view
|
||||
stackView.pop()
|
||||
}
|
||||
}
|
||||
|
||||
onVisibleChanged: {
|
||||
if (visible) {
|
||||
// Hide the toolbar before we start loading
|
||||
toolBar.visible = false
|
||||
StackView.onActivated: {
|
||||
// Hide the toolbar before we start loading
|
||||
toolBar.visible = false
|
||||
|
||||
// Connect the quit completion signal
|
||||
ComputerManager.quitAppCompleted.connect(quitAppCompleted)
|
||||
}
|
||||
else {
|
||||
// Disconnect the signal
|
||||
ComputerManager.quitAppCompleted.disconnect(quitAppCompleted)
|
||||
}
|
||||
// Connect the quit completion signal
|
||||
ComputerManager.quitAppCompleted.connect(quitAppCompleted)
|
||||
}
|
||||
|
||||
StackView.onDeactivating: {
|
||||
// Show the toolbar again
|
||||
toolBar.visible = true
|
||||
|
||||
// Disconnect the signal
|
||||
ComputerManager.quitAppCompleted.disconnect(quitAppCompleted)
|
||||
}
|
||||
|
||||
Row {
|
||||
@@ -75,10 +66,7 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
MessageDialog {
|
||||
ErrorMessageDialog {
|
||||
id: errorDialog
|
||||
modality:Qt.WindowModal
|
||||
icon: StandardIcon.Critical
|
||||
standardButtons: StandardButton.Ok
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,35 +1,32 @@
|
||||
import QtQuick 2.0
|
||||
import QtQuick.Controls 2.2
|
||||
import QtQuick.Dialogs 1.2
|
||||
import QtQuick.Window 2.2
|
||||
|
||||
import ComputerManager 1.0
|
||||
import SdlGamepadKeyNavigation 1.0
|
||||
import Session 1.0
|
||||
|
||||
Item {
|
||||
property Session session
|
||||
property string appName
|
||||
property string stageText : "Starting " + appName + "..."
|
||||
property string stageText : isResume ? qsTr("Resuming %1...").arg(appName) :
|
||||
qsTr("Starting %1...").arg(appName)
|
||||
property bool isResume : false
|
||||
property bool quitAfter : false
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
// The StackView will trigger a visibility change when
|
||||
// we're pushed onto it, causing our onVisibleChanged
|
||||
// routine to run, but only if we start as invisible
|
||||
visible: false
|
||||
|
||||
function stageStarting(stage)
|
||||
{
|
||||
// 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()
|
||||
@@ -38,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
|
||||
@@ -46,7 +44,7 @@ Item {
|
||||
function displayLaunchError(text)
|
||||
{
|
||||
// Display the error dialog after Session::exec() returns
|
||||
errorDialog.text = text
|
||||
streamSegueErrorDialog.text = text
|
||||
}
|
||||
|
||||
function displayLaunchWarning(text)
|
||||
@@ -54,7 +52,7 @@ 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
|
||||
@@ -70,12 +68,20 @@ Item {
|
||||
window.visible = true
|
||||
}
|
||||
|
||||
function sessionFinished()
|
||||
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 (errorDialog.text) {
|
||||
if (streamSegueErrorDialog.text) {
|
||||
// Quit when the error dialog is acknowledged
|
||||
errorDialog.open()
|
||||
streamSegueErrorDialog.quitAfter = quitAfter
|
||||
streamSegueErrorDialog.open()
|
||||
}
|
||||
else {
|
||||
// Quit immediately
|
||||
@@ -92,48 +98,82 @@ Item {
|
||||
// the Qt UI is visible again to prevent losing
|
||||
// focus on the dialog which would impact gamepad
|
||||
// users.
|
||||
if (errorDialog.text) {
|
||||
errorDialog.open()
|
||||
if (streamSegueErrorDialog.text) {
|
||||
streamSegueErrorDialog.quitAfter = quitAfter
|
||||
streamSegueErrorDialog.open()
|
||||
}
|
||||
}
|
||||
|
||||
// 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()
|
||||
}
|
||||
|
||||
// It's important that we don't call enable() here
|
||||
// or it may interfere with the Session instance
|
||||
// getting notified of initial connected gamepads.
|
||||
SdlGamepadKeyNavigation {
|
||||
id: gamepadKeyNav
|
||||
StackView.onDeactivating: {
|
||||
// Show the toolbar again when popped off the stack
|
||||
toolBar.visible = true
|
||||
|
||||
// Enable GUI gamepad usage now
|
||||
SdlGamepadKeyNavigation.enable()
|
||||
}
|
||||
|
||||
onVisibleChanged: {
|
||||
if (visible) {
|
||||
// Hide the toolbar before we start loading
|
||||
toolBar.visible = false
|
||||
StackView.onActivated: {
|
||||
// Hide the toolbar before we start loading
|
||||
toolBar.visible = false
|
||||
|
||||
// Hook up our signals
|
||||
session.stageStarting.connect(stageStarting)
|
||||
session.stageFailed.connect(stageFailed)
|
||||
session.connectionStarted.connect(connectionStarted)
|
||||
session.displayLaunchError.connect(displayLaunchError)
|
||||
session.displayLaunchWarning.connect(displayLaunchWarning)
|
||||
session.quitStarting.connect(quitStarting)
|
||||
session.sessionFinished.connect(sessionFinished)
|
||||
|
||||
// Kick off the stream
|
||||
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 = gamepadKeyNav.getConnectedGamepads() > 0 ?
|
||||
"Tip: Press Start+Select+L1+R1 to disconnect your session" :
|
||||
"Tip: Press Ctrl+Alt+Shift+Q to disconnect your session"
|
||||
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"))
|
||||
|
||||
// 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)
|
||||
// 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)
|
||||
}
|
||||
else if (!quitAfter) {
|
||||
// Show the toolbar again when we become hidden
|
||||
toolBar.visible = true
|
||||
}
|
||||
|
||||
sourceComponent: Item {}
|
||||
}
|
||||
|
||||
Row {
|
||||
@@ -142,6 +182,7 @@ Item {
|
||||
|
||||
BusyIndicator {
|
||||
id: stageSpinner
|
||||
running: false
|
||||
}
|
||||
|
||||
Label {
|
||||
@@ -165,25 +206,4 @@ Item {
|
||||
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
|
||||
MessageDialog {
|
||||
id: errorDialog
|
||||
modality:Qt.WindowModal
|
||||
icon: StandardIcon.Critical
|
||||
standardButtons: StandardButton.Ok | StandardButton.Help
|
||||
|
||||
onAccepted: {
|
||||
if (quitAfter) {
|
||||
Qt.quit()
|
||||
}
|
||||
}
|
||||
|
||||
onHelp: {
|
||||
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Troubleshooting");
|
||||
|
||||
if (quitAfter) {
|
||||
Qt.quit()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,12 @@ 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;
|
||||
default:
|
||||
return QVariant();
|
||||
}
|
||||
@@ -92,6 +103,9 @@ QHash<int, QByteArray> AppModel::roleNames() const
|
||||
names[NameRole] = "name";
|
||||
names[RunningRole] = "running";
|
||||
names[BoxArtRole] = "boxart";
|
||||
names[HiddenRole] = "hidden";
|
||||
names[AppIdRole] = "appid";
|
||||
names[DirectLaunchRole] = "directLaunch";
|
||||
|
||||
return names;
|
||||
}
|
||||
@@ -101,6 +115,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
|
||||
@@ -119,20 +265,15 @@ void AppModel::handleComputerStateChanged(NvComputer* computer)
|
||||
// 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);
|
||||
@@ -142,8 +283,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);
|
||||
@@ -161,7 +302,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) {
|
||||
|
||||
@@ -14,23 +14,32 @@ class AppModel : public QAbstractListModel
|
||||
{
|
||||
NameRole = Qt::UserRole,
|
||||
RunningRole,
|
||||
BoxArtRole
|
||||
BoxArtRole,
|
||||
HiddenRole,
|
||||
AppIdRole,
|
||||
DirectLaunchRole
|
||||
};
|
||||
|
||||
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;
|
||||
@@ -46,9 +55,16 @@ 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;
|
||||
};
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
#include <QThreadPool>
|
||||
|
||||
#include <random>
|
||||
|
||||
ComputerModel::ComputerModel(QObject* object)
|
||||
: QAbstractListModel(object) {}
|
||||
|
||||
@@ -22,18 +24,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()];
|
||||
@@ -50,8 +40,6 @@ 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;
|
||||
default:
|
||||
@@ -67,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
|
||||
@@ -79,7 +66,6 @@ QHash<int, QByteArray> ComputerModel::roleNames() const
|
||||
names[OnlineRole] = "online";
|
||||
names[PairedRole] = "paired";
|
||||
names[BusyRole] = "busy";
|
||||
names[AddPcRole] = "addPc";
|
||||
names[WakeableRole] = "wakeable";
|
||||
names[StatusUnknownRole] = "statusUnknown";
|
||||
|
||||
@@ -144,6 +130,52 @@ void ComputerModel::wakeComputer(int 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);
|
||||
}
|
||||
|
||||
// TODO: Use QRandomGenerator when we drop Qt 5.9 support
|
||||
QString ComputerModel::generatePinString()
|
||||
{
|
||||
std::uniform_int_distribution<int> dist(0, 9999);
|
||||
std::random_device rd;
|
||||
std::mt19937 engine(rd());
|
||||
|
||||
return QString::asprintf("%04u", dist(engine));
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
Q_ASSERT(computerIndex < m_Computers.count());
|
||||
@@ -174,3 +206,4 @@ void ComputerModel::handleComputerStateChanged(NvComputer* computer)
|
||||
}
|
||||
}
|
||||
|
||||
#include "computermodel.moc"
|
||||
|
||||
@@ -14,8 +14,7 @@ class ComputerModel : public QAbstractListModel
|
||||
PairedRole,
|
||||
BusyRole,
|
||||
WakeableRole,
|
||||
StatusUnknownRole,
|
||||
AddPcRole
|
||||
StatusUnknownRole
|
||||
};
|
||||
|
||||
public:
|
||||
@@ -32,14 +31,21 @@ public:
|
||||
|
||||
Q_INVOKABLE void deleteComputer(int computerIndex);
|
||||
|
||||
Q_INVOKABLE QString generatePinString();
|
||||
|
||||
Q_INVOKABLE void pairComputer(int computerIndex, QString pin);
|
||||
|
||||
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);
|
||||
|
||||
@@ -1,25 +1,50 @@
|
||||
import QtQuick 2.9
|
||||
import QtQuick.Controls 2.2
|
||||
import QtQuick.Dialogs 1.2
|
||||
import QtQuick.Layouts 1.3
|
||||
import QtQuick.Window 2.2
|
||||
|
||||
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
|
||||
|
||||
visibility: prefs.startWindowed ? "Windowed" : "Maximized"
|
||||
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
|
||||
|
||||
StreamingPreferences {
|
||||
id: prefs
|
||||
function goBack() {
|
||||
if (clearOnBack) {
|
||||
// Pop all items except the first one
|
||||
stackView.pop(null)
|
||||
clearOnBack = false
|
||||
}
|
||||
else {
|
||||
stackView.pop()
|
||||
}
|
||||
}
|
||||
|
||||
StackView {
|
||||
@@ -37,7 +62,7 @@ ApplicationWindow {
|
||||
|
||||
Keys.onEscapePressed: {
|
||||
if (depth > 1) {
|
||||
stackView.pop()
|
||||
goBack()
|
||||
}
|
||||
else {
|
||||
quitConfirmationDialog.open()
|
||||
@@ -46,7 +71,7 @@ ApplicationWindow {
|
||||
|
||||
Keys.onBackPressed: {
|
||||
if (depth > 1) {
|
||||
stackView.pop()
|
||||
goBack()
|
||||
}
|
||||
else {
|
||||
quitConfirmationDialog.open()
|
||||
@@ -137,19 +162,20 @@ ApplicationWindow {
|
||||
// onAfterRendering call and potentially reenter this code.
|
||||
initialized = true;
|
||||
|
||||
if (prefs.isRunningWayland()) {
|
||||
waylandDialog.open()
|
||||
}
|
||||
else if (prefs.isWow64()) {
|
||||
if (SystemProperties.isWow64) {
|
||||
wow64Dialog.open()
|
||||
}
|
||||
else if (!prefs.hasAnyHardwareAcceleration()) {
|
||||
noHwDecoderDialog.open()
|
||||
else if (!SystemProperties.hasHardwareAcceleration) {
|
||||
if (SystemProperties.isRunningXWayland) {
|
||||
xWaylandDialog.open()
|
||||
}
|
||||
else {
|
||||
noHwDecoderDialog.open()
|
||||
}
|
||||
}
|
||||
|
||||
var unmappedGamepads = prefs.getUnmappedGamepads()
|
||||
if (unmappedGamepads) {
|
||||
unmappedGamepadDialog.unmappedGamepads = unmappedGamepads
|
||||
if (SystemProperties.unmappedGamepads) {
|
||||
unmappedGamepadDialog.unmappedGamepads = SystemProperties.unmappedGamepads
|
||||
unmappedGamepadDialog.open()
|
||||
}
|
||||
}
|
||||
@@ -173,44 +199,109 @@ 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
|
||||
anchors.leftMargin: 10
|
||||
anchors.rightMargin: 10
|
||||
anchors.fill: parent
|
||||
|
||||
NavigableToolButton {
|
||||
// Only make the button visible if the user has navigated somewhere.
|
||||
visible: stackView.depth > 1
|
||||
|
||||
// FIXME: We're using an Image here rather than icon.source because
|
||||
// icons don't work on Qt 5.9 LTS.
|
||||
Image {
|
||||
source: "qrc:/res/arrow_left.svg"
|
||||
anchors.centerIn: parent
|
||||
sourceSize {
|
||||
// The icon should be square so use the height as the width too
|
||||
width: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
|
||||
height: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
|
||||
}
|
||||
}
|
||||
iconSource: "qrc:/res/arrow_left.svg"
|
||||
|
||||
onClicked: stackView.pop()
|
||||
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 : ""
|
||||
}
|
||||
|
||||
Label {
|
||||
id: versionLabel
|
||||
visible: stackView.currentItem.objectName === qsTr("Settings")
|
||||
text: qsTr("Version %1").arg(SystemProperties.versionString)
|
||||
font.pointSize: 12
|
||||
horizontalAlignment: Qt.AlignRight
|
||||
verticalAlignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
NavigableToolButton {
|
||||
id: discordButton
|
||||
visible: SystemProperties.hasBrowser &&
|
||||
stackView.currentItem.objectName === qsTr("Settings")
|
||||
|
||||
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: stackView.currentItem.objectName === qsTr("Computers")
|
||||
|
||||
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 {
|
||||
@@ -218,28 +309,25 @@ ApplicationWindow {
|
||||
|
||||
id: updateButton
|
||||
|
||||
Image {
|
||||
source: "qrc:/res/update.svg"
|
||||
anchors.centerIn: parent
|
||||
sourceSize {
|
||||
width: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
|
||||
height: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
|
||||
}
|
||||
}
|
||||
iconSource: "qrc:/res/update.svg"
|
||||
|
||||
ToolTip.delay: 1000
|
||||
ToolTip.timeout: 3000
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.visible: hovered || visible
|
||||
|
||||
// Invisible until we get a callback notifying us that
|
||||
// an update is available
|
||||
visible: false
|
||||
|
||||
onClicked: Qt.openUrlExternally(browserUrl);
|
||||
onClicked: {
|
||||
if (SystemProperties.hasBrowser) {
|
||||
Qt.openUrlExternally(browserUrl);
|
||||
}
|
||||
}
|
||||
|
||||
function updateAvailable(version, url)
|
||||
{
|
||||
ToolTip.text = "Update available for Moonlight: Version " + version
|
||||
ToolTip.text = qsTr("Update available for Moonlight: Version %1").arg(version)
|
||||
updateButton.browserUrl = url
|
||||
updateButton.visible = true
|
||||
}
|
||||
@@ -256,20 +344,14 @@ ApplicationWindow {
|
||||
|
||||
NavigableToolButton {
|
||||
id: helpButton
|
||||
visible: SystemProperties.hasBrowser
|
||||
|
||||
Image {
|
||||
source: "qrc:/res/question_mark.svg"
|
||||
anchors.centerIn: parent
|
||||
sourceSize {
|
||||
width: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
|
||||
height: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
|
||||
}
|
||||
}
|
||||
iconSource: "qrc:/res/question_mark.svg"
|
||||
|
||||
ToolTip.delay: 1000
|
||||
ToolTip.timeout: 3000
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.text: "Help" + (helpShortcut.nativeText ? (" ("+helpShortcut.nativeText+")") : "")
|
||||
ToolTip.text: qsTr("Help") + (helpShortcut.nativeText ? (" ("+helpShortcut.nativeText+")") : "")
|
||||
|
||||
Shortcut {
|
||||
id: helpShortcut
|
||||
@@ -292,18 +374,11 @@ ApplicationWindow {
|
||||
ToolTip.delay: 1000
|
||||
ToolTip.timeout: 3000
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.text: "Gamepad Mapper"
|
||||
ToolTip.text: qsTr("Gamepad Mapper")
|
||||
|
||||
Image {
|
||||
source: "qrc:/res/ic_videogame_asset_white_48px.svg"
|
||||
anchors.centerIn: parent
|
||||
sourceSize {
|
||||
width: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
|
||||
height: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
|
||||
}
|
||||
}
|
||||
iconSource: "qrc:/res/ic_videogame_asset_white_48px.svg"
|
||||
|
||||
onClicked: navigateTo("qrc:/gui/GamepadMapper.qml", "Gamepad Mapping")
|
||||
onClicked: navigateTo("qrc:/gui/GamepadMapper.qml", qsTr("Gamepad Mapping"))
|
||||
|
||||
Keys.onDownPressed: {
|
||||
stackView.currentItem.forceActiveFocus(Qt.TabFocus)
|
||||
@@ -313,16 +388,9 @@ ApplicationWindow {
|
||||
NavigableToolButton {
|
||||
id: settingsButton
|
||||
|
||||
Image {
|
||||
source: "qrc:/res/settings.svg"
|
||||
anchors.centerIn: parent
|
||||
sourceSize {
|
||||
width: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
|
||||
height: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
|
||||
}
|
||||
}
|
||||
iconSource: "qrc:/res/settings.svg"
|
||||
|
||||
onClicked: navigateTo("qrc:/gui/SettingsView.qml", "Settings")
|
||||
onClicked: navigateTo("qrc:/gui/SettingsView.qml", qsTr("Settings"))
|
||||
|
||||
Keys.onDownPressed: {
|
||||
stackView.currentItem.forceActiveFocus(Qt.TabFocus)
|
||||
@@ -337,72 +405,117 @@ ApplicationWindow {
|
||||
ToolTip.delay: 1000
|
||||
ToolTip.timeout: 3000
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.text: "Settings" + (settingsShortcut.nativeText ? (" ("+settingsShortcut.nativeText+")") : "")
|
||||
ToolTip.text: qsTr("Settings") + (settingsShortcut.nativeText ? (" ("+settingsShortcut.nativeText+")") : "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MessageDialog {
|
||||
ErrorMessageDialog {
|
||||
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");
|
||||
}
|
||||
text: qsTr("No functioning hardware accelerated H.264 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"
|
||||
}
|
||||
|
||||
MessageDialog {
|
||||
id: waylandDialog
|
||||
modality:Qt.WindowModal
|
||||
icon: StandardIcon.Warning
|
||||
standardButtons: StandardButton.Ok | StandardButton.Help
|
||||
text: "Moonlight does not support hardware acceleration on Wayland. Continuing on Wayland may result in poor streaming performance. " +
|
||||
"Please switch to an X session for optimal performance."
|
||||
onHelp: {
|
||||
Qt.openUrlExternally("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"
|
||||
}
|
||||
|
||||
MessageDialog {
|
||||
NavigableMessageDialog {
|
||||
id: wow64Dialog
|
||||
modality:Qt.WindowModal
|
||||
icon: StandardIcon.Warning
|
||||
standardButtons: StandardButton.Ok | StandardButton.Cancel
|
||||
text: "This PC is running a 64-bit version of Windows. Please download the x64 version of Moonlight for the best streaming performance."
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
MessageDialog {
|
||||
ErrorMessageDialog {
|
||||
id: unmappedGamepadDialog
|
||||
property string unmappedGamepads : ""
|
||||
modality:Qt.WindowModal
|
||||
icon: StandardIcon.Warning
|
||||
standardButtons: StandardButton.Ok | StandardButton.Help
|
||||
text: "Moonlight detected gamepads without a proper mapping. " +
|
||||
"The following gamepads will not function until this is resolved: " + unmappedGamepads + "\n\n" +
|
||||
"Click the Help button for information on how to map your gamepads."
|
||||
onHelp: {
|
||||
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Gamepad-Mapping");
|
||||
}
|
||||
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
|
||||
MessageDialog {
|
||||
NavigableMessageDialog {
|
||||
id: quitConfirmationDialog
|
||||
modality:Qt.WindowModal
|
||||
icon: StandardIcon.Warning
|
||||
standardButtons: StandardButton.Yes | StandardButton.No
|
||||
text: "Are you sure you want to quit?"
|
||||
|
||||
onYes: Qt.quit()
|
||||
|
||||
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 GameStream 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.addNewHost(editText.text.trim(), false)
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Label {
|
||||
text: addPcDialog.label
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
TextField {
|
||||
id: editText
|
||||
Layout.fillWidth: true
|
||||
focus: true
|
||||
|
||||
Keys.onReturnPressed: {
|
||||
addPcDialog.accept()
|
||||
}
|
||||
|
||||
Keys.onEnterPressed: {
|
||||
addPcDialog.accept()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,17 +4,18 @@
|
||||
#include <QGuiApplication>
|
||||
#include <QWindow>
|
||||
|
||||
#include "settings/streamingpreferences.h"
|
||||
#include "settings/mappingmanager.h"
|
||||
|
||||
#define AXIS_NAVIGATION_REPEAT_DELAY 150
|
||||
|
||||
SdlGamepadKeyNavigation::SdlGamepadKeyNavigation()
|
||||
: m_Enabled(false),
|
||||
m_SettingsMode(false),
|
||||
m_UiNavMode(false),
|
||||
m_LastAxisNavigationEventTime(0)
|
||||
{
|
||||
m_PollingTimer = new QTimer(this);
|
||||
connect(m_PollingTimer, SIGNAL(timeout()), this, SLOT(onPollingTimerFired()));
|
||||
connect(m_PollingTimer, &QTimer::timeout, this, &SdlGamepadKeyNavigation::onPollingTimerFired);
|
||||
}
|
||||
|
||||
SdlGamepadKeyNavigation::~SdlGamepadKeyNavigation()
|
||||
@@ -88,9 +89,16 @@ void SdlGamepadKeyNavigation::disable()
|
||||
void SdlGamepadKeyNavigation::onPollingTimerFired()
|
||||
{
|
||||
SDL_Event event;
|
||||
StreamingPreferences prefs;
|
||||
|
||||
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:
|
||||
{
|
||||
@@ -98,9 +106,27 @@ void SdlGamepadKeyNavigation::onPollingTimerFired()
|
||||
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_SettingsMode) {
|
||||
if (m_UiNavMode) {
|
||||
// Back-tab
|
||||
sendKey(type, Qt::Key_Tab, Qt::ShiftModifier);
|
||||
}
|
||||
@@ -109,7 +135,7 @@ void SdlGamepadKeyNavigation::onPollingTimerFired()
|
||||
}
|
||||
break;
|
||||
case SDL_CONTROLLER_BUTTON_DPAD_DOWN:
|
||||
if (m_SettingsMode) {
|
||||
if (m_UiNavMode) {
|
||||
sendKey(type, Qt::Key_Tab);
|
||||
}
|
||||
else {
|
||||
@@ -118,24 +144,12 @@ void SdlGamepadKeyNavigation::onPollingTimerFired()
|
||||
break;
|
||||
case SDL_CONTROLLER_BUTTON_DPAD_LEFT:
|
||||
sendKey(type, Qt::Key_Left);
|
||||
if (m_SettingsMode) {
|
||||
// Some settings controls respond to left/right (like the slider)
|
||||
// and others respond to up/down (like combo boxes). They seem to
|
||||
// be mutually exclusive though so let's just send both.
|
||||
sendKey(type, Qt::Key_Up);
|
||||
}
|
||||
break;
|
||||
case SDL_CONTROLLER_BUTTON_DPAD_RIGHT:
|
||||
sendKey(type, Qt::Key_Right);
|
||||
if (m_SettingsMode) {
|
||||
// Some settings controls respond to left/right (like the slider)
|
||||
// and others respond to up/down (like combo boxes). They seem to
|
||||
// be mutually exclusive though so let's just send both.
|
||||
sendKey(type, Qt::Key_Down);
|
||||
}
|
||||
break;
|
||||
case SDL_CONTROLLER_BUTTON_A:
|
||||
if (m_SettingsMode) {
|
||||
if (m_UiNavMode) {
|
||||
sendKey(type, Qt::Key_Space);
|
||||
}
|
||||
else {
|
||||
@@ -177,7 +191,7 @@ void SdlGamepadKeyNavigation::onPollingTimerFired()
|
||||
// Do nothing
|
||||
}
|
||||
else if (leftY < -30000) {
|
||||
if (m_SettingsMode) {
|
||||
if (m_UiNavMode) {
|
||||
// Back-tab
|
||||
sendKey(QEvent::Type::KeyPress, Qt::Key_Tab, Qt::ShiftModifier);
|
||||
sendKey(QEvent::Type::KeyRelease, Qt::Key_Tab, Qt::ShiftModifier);
|
||||
@@ -190,7 +204,7 @@ void SdlGamepadKeyNavigation::onPollingTimerFired()
|
||||
m_LastAxisNavigationEventTime = SDL_GetTicks();
|
||||
}
|
||||
else if (leftY > 30000) {
|
||||
if (m_SettingsMode) {
|
||||
if (m_UiNavMode) {
|
||||
sendKey(QEvent::Type::KeyPress, Qt::Key_Tab);
|
||||
sendKey(QEvent::Type::KeyRelease, Qt::Key_Tab);
|
||||
}
|
||||
@@ -204,28 +218,37 @@ void SdlGamepadKeyNavigation::onPollingTimerFired()
|
||||
else if (leftX < -30000) {
|
||||
sendKey(QEvent::Type::KeyPress, Qt::Key_Left);
|
||||
sendKey(QEvent::Type::KeyRelease, Qt::Key_Left);
|
||||
if (m_SettingsMode) {
|
||||
// Some settings controls respond to left/right (like the slider)
|
||||
// and others respond to up/down (like combo boxes). They seem to
|
||||
// be mutually exclusive though so let's just send both.
|
||||
sendKey(QEvent::Type::KeyPress, Qt::Key_Up);
|
||||
sendKey(QEvent::Type::KeyRelease, Qt::Key_Up);
|
||||
}
|
||||
|
||||
m_LastAxisNavigationEventTime = SDL_GetTicks();
|
||||
}
|
||||
else if (leftX > 30000) {
|
||||
sendKey(QEvent::Type::KeyPress, Qt::Key_Right);
|
||||
sendKey(QEvent::Type::KeyRelease, Qt::Key_Right);
|
||||
if (m_SettingsMode) {
|
||||
// Some settings controls respond to left/right (like the slider)
|
||||
// and others respond to up/down (like combo boxes). They seem to
|
||||
// be mutually exclusive though so let's just send both.
|
||||
sendKey(QEvent::Type::KeyPress, Qt::Key_Down);
|
||||
sendKey(QEvent::Type::KeyRelease, Qt::Key_Down);
|
||||
m_LastAxisNavigationEventTime = SDL_GetTicks();
|
||||
}
|
||||
|
||||
// In UI navigation mode (settings page), use the right stick to scroll
|
||||
if (m_UiNavMode) {
|
||||
short rightX = SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_RIGHTX);
|
||||
short rightY = SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_RIGHTY);
|
||||
|
||||
QPoint wheelDelta;
|
||||
if (rightX > 30000) {
|
||||
wheelDelta.setX(30);
|
||||
}
|
||||
else if (rightX < -30000) {
|
||||
wheelDelta.setX(-30);
|
||||
}
|
||||
|
||||
m_LastAxisNavigationEventTime = SDL_GetTicks();
|
||||
if (rightY > 30000) {
|
||||
wheelDelta.setY(-30);
|
||||
}
|
||||
else if (rightY < -30000) {
|
||||
wheelDelta.setY(30);
|
||||
}
|
||||
|
||||
if (!wheelDelta.isNull()) {
|
||||
sendWheel(wheelDelta);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -240,24 +263,30 @@ void SdlGamepadKeyNavigation::sendKey(QEvent::Type type, Qt::Key key, Qt::Keyboa
|
||||
}
|
||||
}
|
||||
|
||||
void SdlGamepadKeyNavigation::setSettingsMode(bool settingsMode)
|
||||
void SdlGamepadKeyNavigation::sendWheel(QPoint& angleDelta)
|
||||
{
|
||||
m_SettingsMode = settingsMode;
|
||||
QGuiApplication* app = static_cast<QGuiApplication*>(QGuiApplication::instance());
|
||||
QWindow* focusWindow = app->focusWindow();
|
||||
if (focusWindow != nullptr) {
|
||||
QPoint mousePos(focusWindow->width() / 2, focusWindow->height() / 2);
|
||||
QPoint globalPos(focusWindow->mapToGlobal(mousePos));
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 12, 0)
|
||||
QWheelEvent wheelEvent(mousePos, globalPos, QPoint(), angleDelta, Qt::NoButton, Qt::NoModifier, Qt::NoScrollPhase, false, Qt::MouseEventSynthesizedByApplication);
|
||||
#else
|
||||
QWheelEvent wheelEvent(mousePos, globalPos, QPoint(), angleDelta, angleDelta.y(), Qt::Vertical, Qt::NoButton, Qt::NoModifier, Qt::NoScrollPhase, Qt::MouseEventSynthesizedByApplication, false);
|
||||
#endif
|
||||
app->sendEvent(focusWindow, &wheelEvent);
|
||||
}
|
||||
}
|
||||
|
||||
void SdlGamepadKeyNavigation::setUiNavMode(bool uiNavMode)
|
||||
{
|
||||
m_UiNavMode = uiNavMode;
|
||||
}
|
||||
|
||||
int SdlGamepadKeyNavigation::getConnectedGamepads()
|
||||
{
|
||||
if (SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) != 0) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) failed: %s",
|
||||
SDL_GetError());
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Applying mappings is necessary to ensure gamepad without
|
||||
// a built-in mapping are properly counted.
|
||||
MappingManager mappingManager;
|
||||
mappingManager.applyMappings();
|
||||
Q_ASSERT(m_Enabled);
|
||||
|
||||
int count = 0;
|
||||
for (int i = 0; i < SDL_NumJoysticks(); i++) {
|
||||
@@ -266,6 +295,5 @@ int SdlGamepadKeyNavigation::getConnectedGamepads()
|
||||
}
|
||||
}
|
||||
|
||||
SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER);
|
||||
return count;
|
||||
}
|
||||
|
||||
@@ -18,12 +18,13 @@ public:
|
||||
|
||||
Q_INVOKABLE void disable();
|
||||
|
||||
Q_INVOKABLE void setSettingsMode(bool settingsMode);
|
||||
Q_INVOKABLE void setUiNavMode(bool settingsMode);
|
||||
|
||||
Q_INVOKABLE int getConnectedGamepads();
|
||||
|
||||
private:
|
||||
void sendKey(QEvent::Type type, Qt::Key key, Qt::KeyboardModifiers modifiers = Qt::NoModifier);
|
||||
void sendWheel(QPoint& angleDelta);
|
||||
|
||||
private slots:
|
||||
void onPollingTimerFired();
|
||||
@@ -32,6 +33,6 @@ private:
|
||||
QTimer* m_PollingTimer;
|
||||
QList<SDL_GameController*> m_Gamepads;
|
||||
bool m_Enabled;
|
||||
bool m_SettingsMode;
|
||||
bool m_UiNavMode;
|
||||
Uint32 m_LastAxisNavigationEventTime;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,990 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!DOCTYPE TS>
|
||||
<TS version="2.1" language="zh_CN">
|
||||
<context>
|
||||
<name>AppView</name>
|
||||
<message>
|
||||
<location filename="../gui/AppView.qml" line="139"/>
|
||||
<location filename="../gui/AppView.qml" line="293"/>
|
||||
<source>Resume Game</source>
|
||||
<translation>继续游戏</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/AppView.qml" line="165"/>
|
||||
<location filename="../gui/AppView.qml" line="298"/>
|
||||
<source>Quit Game</source>
|
||||
<translation>退出游戏</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/AppView.qml" line="293"/>
|
||||
<source>Launch Game</source>
|
||||
<translation>启动游戏</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/AppView.qml" line="306"/>
|
||||
<source>Direct Launch</source>
|
||||
<translation>直接启动</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/AppView.qml" line="310"/>
|
||||
<source>Launch this app immediately when the host is selected, bypassing the app selection grid.</source>
|
||||
<translation>选择目标计算机后立即启动此应用程序,绕过应用程序选择网格。</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/AppView.qml" line="319"/>
|
||||
<source>Hide Game</source>
|
||||
<translation>隐藏游戏</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/AppView.qml" line="323"/>
|
||||
<source>Hide this game from the app grid. To access hidden games, right-click on the host and choose %1.</source>
|
||||
<translation>从应用程序网格中隐藏此游戏。要访问隐藏的游戏,请右键单击目标计算机并选择 %1。</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/AppView.qml" line="323"/>
|
||||
<source>View All Apps</source>
|
||||
<translation>查看所有应用程序</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/AppView.qml" line="338"/>
|
||||
<source>Are you sure you want to quit %1? Any unsaved progress will be lost.</source>
|
||||
<translation>你确定要退出 %1 吗?所有未保存的游戏进度都将丢失。</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>CliQuitStreamSegue</name>
|
||||
<message>
|
||||
<location filename="../gui/CliQuitStreamSegue.qml" line="9"/>
|
||||
<source>Establishing connection to PC...</source>
|
||||
<translation>正在建立与目标计算机的连接...</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/CliQuitStreamSegue.qml" line="13"/>
|
||||
<source>Quitting app...</source>
|
||||
<translation>应用程序退出中...</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>CliStartStreamSegue</name>
|
||||
<message>
|
||||
<location filename="../gui/CliStartStreamSegue.qml" line="9"/>
|
||||
<source>Establishing connection to PC...</source>
|
||||
<translation>正在建立与目标计算机的连接...</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/CliStartStreamSegue.qml" line="13"/>
|
||||
<source>Loading app list...</source>
|
||||
<translation>应用程序列表加载中...</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/CliStartStreamSegue.qml" line="81"/>
|
||||
<source>Are you sure you want to quit %1? Any unsaved progress will be lost.</source>
|
||||
<translation>你确定要退出 %1 吗?任何未保存的进度都将丢失。</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>GamepadMapper</name>
|
||||
<message>
|
||||
<location filename="../gui/GamepadMapper.qml" line="4"/>
|
||||
<source>Gamepad Mapping</source>
|
||||
<translation>手柄按键映射</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>NvHTTP</name>
|
||||
<message>
|
||||
<location filename="../backend/nvhttp.cpp" line="327"/>
|
||||
<source>Missing audio capture device. Reinstalling GeForce Experience should resolve this error.</source>
|
||||
<translation>缺少音频捕获设备。重新安装 GeForce Experience 可以解决此错误。</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>PcView</name>
|
||||
<message>
|
||||
<location filename="../gui/PcView.qml" line="20"/>
|
||||
<source>Computers</source>
|
||||
<translation>计算机列表</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/PcView.qml" line="67"/>
|
||||
<source>Unable to connect to the specified PC.</source>
|
||||
<translation>无法连接到目标计算机。</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/PcView.qml" line="70"/>
|
||||
<source>This PC's Internet connection is blocking Moonlight. Streaming over the Internet may not work while connected to this network.</source>
|
||||
<translation>此计算机的互联网连接被阻塞。当连接到此网络时,互联网上的流式传输可能无法工作。</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/PcView.qml" line="73"/>
|
||||
<source>Click the Help button for possible solutions.</source>
|
||||
<translation>单击帮助按钮以获取可能的解决方案。</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/PcView.qml" line="102"/>
|
||||
<source>Searching for PCs on your local network with NVIDIA GameStream enabled...</source>
|
||||
<translation>正在本地网络上搜索启用 NVIDIA GameStrea 的计算机...</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/PcView.qml" line="103"/>
|
||||
<source>Automatic PC discovery is disabled. Add your PC manually.</source>
|
||||
<translation>已禁用计算机自动发现。需要手动添加计算机。</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/PcView.qml" line="171"/>
|
||||
<source>PC Status: %1</source>
|
||||
<translation>计算机状态: %1</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/PcView.qml" line="171"/>
|
||||
<source>Online</source>
|
||||
<translation>在线</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/PcView.qml" line="171"/>
|
||||
<source>Offline</source>
|
||||
<translation>离线</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/PcView.qml" line="177"/>
|
||||
<source>View All Apps</source>
|
||||
<translation>查看所有应用程序</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/PcView.qml" line="187"/>
|
||||
<source>Wake PC</source>
|
||||
<translation>唤醒目标计算机</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/PcView.qml" line="193"/>
|
||||
<source>Test Network</source>
|
||||
<translation>网络测试</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/PcView.qml" line="202"/>
|
||||
<source>Rename PC</source>
|
||||
<translation>重命名计算机</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/PcView.qml" line="211"/>
|
||||
<source>Delete PC</source>
|
||||
<translation>删除计算机</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/PcView.qml" line="242"/>
|
||||
<source>You cannot pair while a previous session is still running on the host PC. Quit any running games or reboot the host PC, then try pairing again.</source>
|
||||
<translation>当上一个会话仍在运行时,无法进行配对。请退出所有正在运行的游戏或重新启动目标计算机,然后重试配对。</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/PcView.qml" line="297"/>
|
||||
<source>Please enter %1 on your GameStream PC. This dialog will close when pairing is completed.</source>
|
||||
<translation>请在启用 GameStream 的计算机上输入 %1。配对完成后,此对话框将关闭。</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/PcView.qml" line="308"/>
|
||||
<source>Are you sure you want to remove this PC?</source>
|
||||
<translation>确定要移除这个计算机吗?</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/PcView.qml" line="325"/>
|
||||
<source>Moonlight is testing your network connection to determine if NVIDIA GameStream is blocked.</source>
|
||||
<translation>Moonlight 正在测试你的网络连接,以检测 NVIDIA GameStream 是否被阻止。</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/PcView.qml" line="325"/>
|
||||
<source>This may take a few seconds…</source>
|
||||
<translation>这可能需要几秒钟...</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/PcView.qml" line="332"/>
|
||||
<source>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.</source>
|
||||
<translation>无法执行网络测试,因为此计算机无法访问 Moonlight 的连接测试服务器。请检查你的互联网连接或稍后再试。</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/PcView.qml" line="336"/>
|
||||
<source>This network does not appear to be blocking Moonlight. If you still have trouble connecting, check your PC's firewall settings.</source>
|
||||
<translation>此网络似乎没有阻止 Moonlight。如果仍然无法连接,请检查计算机的防火墙设置。</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/PcView.qml" line="336"/>
|
||||
<source>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.</source>
|
||||
<translation>如果你想通过互联网进行流式传输,请在你的目标计算机上安装 Moonlight Internet Hosting 工具,并运行包含的互联网流式传输测试程序来检查你目标计算机的互联网连接。</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/PcView.qml" line="340"/>
|
||||
<source>Your PC's current network connection seems to be blocking Moonlight. Streaming over the Internet may not work while connected to this network.</source>
|
||||
<translation>此计算机当前的网络连接似乎被阻塞了。当连接到此网络时,互联网上的流式传输可能无法工作。</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/PcView.qml" line="340"/>
|
||||
<source>The following network ports were blocked:</source>
|
||||
<translation>下列网络端口被阻止:</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/PcView.qml" line="352"/>
|
||||
<source>Enter the new name for this PC:</source>
|
||||
<translation>输入这台计算机的新名称:</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>PendingQuitTask</name>
|
||||
<message>
|
||||
<location filename="../backend/computermanager.cpp" line="538"/>
|
||||
<source>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.</source>
|
||||
<translation>正在运行的游戏不是由这台计算机启动的。你必须在目标计算机上手动退出游戏或使用最初启动游戏的设备。</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>QObject</name>
|
||||
<message>
|
||||
<location filename="../cli/quitstream.cpp" line="76"/>
|
||||
<location filename="../cli/startstream.cpp" line="133"/>
|
||||
<source>Failed to connect to %1</source>
|
||||
<translation>无法连接到 %1</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../cli/quitstream.cpp" line="88"/>
|
||||
<location filename="../cli/startstream.cpp" line="89"/>
|
||||
<source>Computer %1 has not been paired. Please open Moonlight to pair before streaming.</source>
|
||||
<translation>计算机 %1 尚未配对。</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../cli/quitstream.cpp" line="102"/>
|
||||
<location filename="../cli/startstream.cpp" line="126"/>
|
||||
<source>Quitting app failed, reason: %1</source>
|
||||
<translation>退出应用程序失败,原因: %1</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../cli/startstream.cpp" line="137"/>
|
||||
<source>Failed to find application %1</source>
|
||||
<translation>找不到应用程序 %1</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>QuitSegue</name>
|
||||
<message>
|
||||
<location filename="../gui/QuitSegue.qml" line="12"/>
|
||||
<source>Quitting %1...</source>
|
||||
<translation>正在退出 %1...</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>Session</name>
|
||||
<message>
|
||||
<location filename="../streaming/session.cpp" line="89"/>
|
||||
<source>No video received from host.</source>
|
||||
<translation>没有从目标计算机接收到视频画面。</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../streaming/session.cpp" line="90"/>
|
||||
<source>Check your firewall and port forwarding rules for port(s): %1</source>
|
||||
<translation>检查防火墙和端口转发规则: %1</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../streaming/session.cpp" line="95"/>
|
||||
<source>Your network connection isn't performing well. Reduce your video bitrate setting or try a faster connection.</source>
|
||||
<translation>网络连接性能不佳。请降低视频比特率或尝试更快的网络连接。</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../streaming/session.cpp" line="100"/>
|
||||
<source>Something went wrong on your host PC when starting the stream.</source>
|
||||
<translation>目标计算机启动流式传输时出现问题。</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../streaming/session.cpp" line="101"/>
|
||||
<source>Make sure you don't have any DRM-protected content open on your host PC. You can also try restarting your host PC.</source>
|
||||
<translation>确保目标计算机上没有打开任何受 DRM 保护的内容。你也可以尝试重新启动目标计算机。</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../streaming/session.cpp" line="102"/>
|
||||
<source>If the issue persists, try reinstalling your GPU drivers and GeForce Experience.</source>
|
||||
<translation>如果问题仍然存在,请尝试重新安装显卡驱动程序和 GeForce Experience。</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../streaming/session.cpp" line="107"/>
|
||||
<source>Connection terminated</source>
|
||||
<translation>会话已终止</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../streaming/session.cpp" line="578"/>
|
||||
<source>Your selection to enable remote desktop mouse mode may cause problems in games.</source>
|
||||
<translation>启用远程桌面鼠标模式可能会导致游戏出现问题。</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../streaming/session.cpp" line="583"/>
|
||||
<source>HDR is not supported with software decoding.</source>
|
||||
<translation>软件解码不支持 HDR。</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../streaming/session.cpp" line="587"/>
|
||||
<source>Your settings selection to force software decoding may cause poor streaming performance.</source>
|
||||
<translation>你选择的用于强制软件解码的设置可能会导致流式传输性能不佳。</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../streaming/session.cpp" line="592"/>
|
||||
<source>Using unsupported FPS options may cause stuttering or lag.</source>
|
||||
<translation>使用不受支持的 FPS 选项可能会导致卡顿或延迟。</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../streaming/session.cpp" line="595"/>
|
||||
<source>V-sync will be disabled when streaming at a higher frame rate than the display.</source>
|
||||
<translation>当流式传输的帧率高于显示时,垂直同步将被禁用。</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../streaming/session.cpp" line="612"/>
|
||||
<source>Using software decoding due to your selection to force HEVC without GPU support. This may cause poor streaming performance.</source>
|
||||
<translation>根据你的选择使用软件解码,以在没有显卡支持的情况下强制 HEVC。这可能会导致流式传输性能不佳。</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../streaming/session.cpp" line="615"/>
|
||||
<source>This PC's GPU doesn't support HEVC decoding.</source>
|
||||
<translation>此计算机的显卡不支持 HEVC 解码。</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../streaming/session.cpp" line="622"/>
|
||||
<source>Your host PC GPU doesn't support HEVC. A GeForce GTX 900-series (Maxwell) or later GPU is required for HEVC streaming.</source>
|
||||
<translation>目标计算机的显卡不支持 HEVC。</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../streaming/session.cpp" line="639"/>
|
||||
<source>%1 doesn't support HDR10.</source>
|
||||
<translation>%1 不支持 HDR10。</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../streaming/session.cpp" line="643"/>
|
||||
<source>Your host PC GPU doesn't support HDR streaming. A GeForce GTX 1000-series (Pascal) or later GPU is required for HDR streaming.</source>
|
||||
<translation>目标计算机的显卡不支持 HDR 流。</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../streaming/session.cpp" line="652"/>
|
||||
<source>This PC's GPU doesn't support HEVC Main10 decoding for HDR streaming.</source>
|
||||
<translation>此计算机的显卡不支持 HEVC Main10 解码 HDR 流。</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../streaming/session.cpp" line="665"/>
|
||||
<source>GeForce Experience 3.0 or higher is required for 4K streaming.</source>
|
||||
<translation>4K 流式传输需要 GeForce Experience 3.0 或更高版本。</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../streaming/session.cpp" line="680"/>
|
||||
<source>Your selected surround sound setting is not supported by the current audio device.</source>
|
||||
<translation>当前音频设备不支持你选择的环绕声设置。</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../streaming/session.cpp" line="687"/>
|
||||
<source>Failed to open audio device. Audio will be unavailable during this session.</source>
|
||||
<translation>无法打开音频设备。音频在此会话期间将不可用。</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../streaming/session.cpp" line="692"/>
|
||||
<source>An attached gamepad has no mapping and won't be usable. Visit the Moonlight help to resolve this.</source>
|
||||
<translation>附加的手柄没有映射,因此无法使用。请访问 Moonlight 帮助解决此问题。</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../streaming/session.cpp" line="700"/>
|
||||
<source>Your host PC's GPU doesn't support streaming video resolutions over 4K.</source>
|
||||
<translation>目标计算机的显卡不支持 4K 以上的流式传输视频分辨率。</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../streaming/session.cpp" line="704"/>
|
||||
<source>Video resolutions over 4K are only supported by the HEVC codec.</source>
|
||||
<translation>只有 HEVC 编解码器才支持 4K 以上的视频分辨率。</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../streaming/session.cpp" line="718"/>
|
||||
<source>Your selection to force hardware decoding cannot be satisfied due to missing hardware decoding support on this PC's GPU.</source>
|
||||
<translation>由于此计算机的显卡上缺少硬件解码支持,无法满足强制硬件解码的选择。</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../streaming/session.cpp" line="721"/>
|
||||
<source>Your codec selection and force hardware decoding setting are not compatible. This PC's GPU lacks support for decoding your chosen codec.</source>
|
||||
<translation>你选择的编解码器和强制硬件解码设置不兼容。此计算机的显卡不支持解码你选择的编解码器。</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../streaming/session.cpp" line="1022"/>
|
||||
<source>GeForce Experience returned error: %1</source>
|
||||
<translation>GeForce Experience 返回错误: %1</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../streaming/session.cpp" line="1445"/>
|
||||
<source>Unable to initialize video decoder. Please check your streaming settings and try again.</source>
|
||||
<translation>无法初始化视频解码器。请检查你的流式传输设置,然后重试。</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>SettingsView</name>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="12"/>
|
||||
<source>Settings</source>
|
||||
<translation>设置</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="60"/>
|
||||
<source>Basic Settings</source>
|
||||
<translation>基本设置</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="70"/>
|
||||
<source>Resolution and FPS</source>
|
||||
<translation>分辨率和 FPS</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="78"/>
|
||||
<source>Setting values too high for your PC or network connection may cause lag, stuttering, or errors.</source>
|
||||
<translation>如果你的计算机或网络连接设置的值过高,可能会导致延迟,卡顿或错误。</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="209"/>
|
||||
<source>720p</source>
|
||||
<translation></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="215"/>
|
||||
<source>1080p</source>
|
||||
<translation></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="221"/>
|
||||
<source>1440p</source>
|
||||
<translation></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="227"/>
|
||||
<source>4K</source>
|
||||
<translation></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="331"/>
|
||||
<source>Custom resolutions are not officially supported by GeForce Experience, so it will not set your host display resolution. You will need to set it manually while in game.</source>
|
||||
<translation>自定义分辨率不受 GeForce Experience 的正式支持,因此不会设置目标计算机的分辨率。你需要在游戏中手动设置。</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="332"/>
|
||||
<source>Resolutions that are not supported by your client or host PC may cause streaming errors.</source>
|
||||
<translation>使用当前计算机或目标计算机不支持的分辨率可能会导致流式传输错误。</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="338"/>
|
||||
<source>Enter a custom resolution:</source>
|
||||
<translation>输入自定义分辨率:</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="406"/>
|
||||
<location filename="../gui/SettingsView.qml" line="407"/>
|
||||
<location filename="../gui/SettingsView.qml" line="427"/>
|
||||
<location filename="../gui/SettingsView.qml" line="430"/>
|
||||
<source>%1 FPS</source>
|
||||
<translation></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="412"/>
|
||||
<location filename="../gui/SettingsView.qml" line="415"/>
|
||||
<location filename="../gui/SettingsView.qml" line="424"/>
|
||||
<location filename="../gui/SettingsView.qml" line="437"/>
|
||||
<location filename="../gui/SettingsView.qml" line="440"/>
|
||||
<source>%1 FPS (Unsupported)</source>
|
||||
<translation>%1 FPS (不支持)</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="493"/>
|
||||
<source>Video bitrate:</source>
|
||||
<translation>视频比特率:</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="501"/>
|
||||
<source>Lower the bitrate on slower connections. Raise the bitrate to increase image quality.</source>
|
||||
<translation>在较慢的连接上降低比特率。提高比特率以提高图像质量。</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="519"/>
|
||||
<source>Video bitrate: %1 Mbps</source>
|
||||
<translation>视频比特率: %1 Mbps</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="527"/>
|
||||
<source>Display mode</source>
|
||||
<translation>显示模式</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="545"/>
|
||||
<source> (Recommended)</source>
|
||||
<translation> (推荐)</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="573"/>
|
||||
<location filename="../gui/SettingsView.qml" line="847"/>
|
||||
<source>Fullscreen</source>
|
||||
<translation>全屏</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="577"/>
|
||||
<source>Borderless windowed</source>
|
||||
<translation>无边框窗口</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="581"/>
|
||||
<location filename="../gui/SettingsView.qml" line="839"/>
|
||||
<source>Windowed</source>
|
||||
<translation>窗口化</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="592"/>
|
||||
<source>Fullscreen generally provides the best performance, but borderless windowed may work better with features like macOS Spaces, Alt+Tab, screenshot tools, on-screen overlays, etc.</source>
|
||||
<translation>全屏通常提供最好的性能,但是无边框窗口可以更好地与 macOS Spaces、Alt+Tab、屏幕截图工具、屏幕覆盖等功能配合使用。</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="599"/>
|
||||
<source>V-Sync</source>
|
||||
<translation>垂直同步</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="609"/>
|
||||
<source>Disabling V-Sync allows sub-frame rendering latency, but it can display visible tearing</source>
|
||||
<translation>禁用垂直同步可允许子帧渲染延迟,但会显示可见的撕裂</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="616"/>
|
||||
<source>Frame pacing</source>
|
||||
<translation>帧速调节</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="626"/>
|
||||
<source>Frame pacing reduces micro-stutter by delaying frames that come in too early</source>
|
||||
<translation>通过延迟过早进入的帧来减少卡顿</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="636"/>
|
||||
<source>Audio Settings</source>
|
||||
<translation>音频设置</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="646"/>
|
||||
<source>Audio configuration</source>
|
||||
<translation>音频配置</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="671"/>
|
||||
<source>Stereo</source>
|
||||
<translation>立体声</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="675"/>
|
||||
<source>5.1 surround sound</source>
|
||||
<translation>5.1 环绕声</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="679"/>
|
||||
<source>7.1 surround sound</source>
|
||||
<translation>7.1 环绕声</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="693"/>
|
||||
<source>Mute host PC speakers while streaming</source>
|
||||
<translation>流式传输启动时将目标计算机的扬声器静音</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="703"/>
|
||||
<source>You must restart any game currently in progress for this setting to take effect</source>
|
||||
<translation>要使此设置生效,必须重新启动当前正在进行的所有游戏</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="709"/>
|
||||
<source>Mute audio stream when Moonlight is not the active window</source>
|
||||
<translation>当 Moonlight 失去焦点时静音</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="720"/>
|
||||
<source>Mutes Moonlight's audio when you Alt+Tab out of the stream or click on a different window.</source>
|
||||
<translation>当你用 Alt+Tab 键离开流式传输或者点击其他窗口时,Moonlight 将静音。</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="729"/>
|
||||
<source>UI Settings</source>
|
||||
<translation>界面设置</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="739"/>
|
||||
<source>Language</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="765"/>
|
||||
<source>Automatic</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="792"/>
|
||||
<source>You must restart Moonlight for this change to take effect</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="806"/>
|
||||
<source>GUI display mode</source>
|
||||
<translation>窗口显示模式</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="843"/>
|
||||
<source>Maximized</source>
|
||||
<translation>最大化</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="860"/>
|
||||
<source>Show connection quality warnings</source>
|
||||
<translation>显示连接质量警告</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="872"/>
|
||||
<source>Discord Rich Presence integration</source>
|
||||
<translation>Discord Rich Presence 集成</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="882"/>
|
||||
<source>Updates your Discord status to display the name of the game you're streaming.</source>
|
||||
<translation>更新你的 Discord 状态,以显示正在运行的游戏名称。</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="900"/>
|
||||
<source>Input Settings</source>
|
||||
<translation>输入设置</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="911"/>
|
||||
<source>Optimize mouse for remote desktop instead of games</source>
|
||||
<translation>为远程桌面而不是游戏优化鼠标</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="922"/>
|
||||
<source>This enables seamless mouse control without capturing the client's mouse cursor. It is ideal for remote desktop usage but will not work in most games.</source>
|
||||
<translation>这可以实现无缝鼠标控制,而无需捕获客户端的鼠标光标。这是理想的远程桌面使用方案,但不会在大多数游戏中工作。</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="923"/>
|
||||
<source>You can toggle this while streaming using Ctrl+Alt+Shift+M.</source>
|
||||
<translation>你可以使用 Ctrl+Alt+Shift+M 切换鼠标控制模式。</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="924"/>
|
||||
<source>NOTE: Due to a bug in GeForce Experience, this option may not work properly if your host PC has multiple monitors.</source>
|
||||
<translation>注意:由于 GeForce Experience 的 Bug,如果你的目标计算机有多个显示器,则此选项可能无法正常工作。</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="934"/>
|
||||
<source>Capture system keyboard shortcuts</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="942"/>
|
||||
<source>This enables the capture of system-wide keyboard shortcuts like Alt+Tab that would normally be handled by the client OS while streaming.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Capture system keyboard shortcuts while streaming in fullscreen</source>
|
||||
<translation type="vanished">在全屏流式传输时捕获系统键盘快捷键</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>This enables the capture of system-wide keyboard shortcuts like Alt+Tab that would normally be handled by the client OS while streaming in fullscreen.</source>
|
||||
<translation type="vanished">这允许捕获系统范围内的键盘快捷键(如Alt+Tab),这些快捷键通常在全屏流式传输时由客户端操作系统处理。</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="943"/>
|
||||
<source>NOTE: Certain keyboard shortcuts like Ctrl+Alt+Del on Windows cannot be intercepted by any application, including Moonlight.</source>
|
||||
<translation>注意:Windows 上的某些键盘快捷键(如 Ctrl+Alt+Del)不能被任何应用程序截获,包括 Moonlight。</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="972"/>
|
||||
<source>in fullscreen</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="976"/>
|
||||
<source>always</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="1006"/>
|
||||
<source>Use touchscreen as a virtual trackpad</source>
|
||||
<translation>将触摸屏用作虚拟触控板</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="1016"/>
|
||||
<source>When checked, the touchscreen acts like a trackpad. When unchecked, the touchscreen will directly control the mouse pointer.</source>
|
||||
<translation>选中时,触摸屏就像一个触控板。取消选中时,触摸屏将直接控制鼠标指针。</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="1023"/>
|
||||
<source>Swap left and right mouse buttons</source>
|
||||
<translation>交换鼠标左键和右键</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="1035"/>
|
||||
<source>Reverse mouse scrolling direction</source>
|
||||
<translation>反转鼠标滚动方向</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="1049"/>
|
||||
<source>Gamepad Settings</source>
|
||||
<translation>手柄设置</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="1059"/>
|
||||
<source>Swap A/B and X/Y gamepad buttons</source>
|
||||
<translation>交换手柄的 A/B 和 X/Y 按钮</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="1077"/>
|
||||
<source>This switches gamepads into a Nintendo-style button layout</source>
|
||||
<translation>这会将手柄切换为任天堂风格的按钮布局</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="1083"/>
|
||||
<source>Force gamepad #1 always connected</source>
|
||||
<translation>强制手柄 #1 始终连接</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="1093"/>
|
||||
<source>Forces a single gamepad to always stay connected to the host, even if no gamepads are actually connected to this PC.</source>
|
||||
<translation>强制单个手柄始终与目标计算机保持连接,即使没有任何手柄实际连接到此计算机。</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="1094"/>
|
||||
<source>Only enable this option when streaming a game that doesn't support gamepads being connected after startup.</source>
|
||||
<translation>仅在游戏启动后不支持手柄连接时才启用此选项。</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="1101"/>
|
||||
<source>Enable mouse control with gamepads by holding the 'Start' button</source>
|
||||
<translation>按住手柄的“Start”按钮启用鼠标控制</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="1112"/>
|
||||
<source>Process gamepad input when Moonlight is in the background</source>
|
||||
<translation>当 Moonlight 在后台时处理手柄输入</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="1123"/>
|
||||
<source>Allows Moonlight to capture gamepad inputs even if it's not the current window in focus</source>
|
||||
<translation>即使不是焦点窗口,也允许 Moonlight 捕获手柄的输入</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="1132"/>
|
||||
<source>Host Settings</source>
|
||||
<translation>目标计算机设置</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="1142"/>
|
||||
<source>Optimize game settings for streaming</source>
|
||||
<translation>优化游戏设置以进行流式传输</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="1153"/>
|
||||
<source>Quit app on host PC after ending stream</source>
|
||||
<translation>流式传输结束后退出目标计算机上的应用程序</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="1163"/>
|
||||
<source>This will close the app or game you are streaming when you end your stream. You will lose any unsaved progress!</source>
|
||||
<translation>当你结束流式传输时,这将关闭正在运行的应用程序或游戏。这会导致你失去所有未保存的游戏进度!</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="1172"/>
|
||||
<source>Advanced Settings</source>
|
||||
<translation>高级设置</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="1182"/>
|
||||
<source>Video decoder</source>
|
||||
<translation>视频解码器</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="1207"/>
|
||||
<location filename="../gui/SettingsView.qml" line="1253"/>
|
||||
<source>Automatic (Recommended)</source>
|
||||
<translation>自动 (推荐)</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="1211"/>
|
||||
<source>Force software decoding</source>
|
||||
<translation>强制软件解码</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="1215"/>
|
||||
<source>Force hardware decoding</source>
|
||||
<translation>强制硬件解码</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="1228"/>
|
||||
<source>Video codec</source>
|
||||
<translation>视频编解码器</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="1257"/>
|
||||
<source>H.264</source>
|
||||
<translation></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="1261"/>
|
||||
<source>HEVC (H.265)</source>
|
||||
<translation></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="1278"/>
|
||||
<source>Unlock unsupported FPS options</source>
|
||||
<translation>解锁不受支持的 FPS 选项</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="1297"/>
|
||||
<source>Automatically find PCs on the local network (Recommended)</source>
|
||||
<translation>自动在本地网络上查找计算机 (推荐)</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="1322"/>
|
||||
<source>Automatically detect blocked connections (Recommended)</source>
|
||||
<translation>自动检测被阻止的连接 (推荐)</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>StreamSegue</name>
|
||||
<message>
|
||||
<location filename="../gui/StreamSegue.qml" line="11"/>
|
||||
<source>Resuming %1...</source>
|
||||
<translation>正在恢复 %1...</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/StreamSegue.qml" line="12"/>
|
||||
<location filename="../gui/StreamSegue.qml" line="19"/>
|
||||
<source>Starting %1...</source>
|
||||
<translation>正在启动 %1...</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/StreamSegue.qml" line="25"/>
|
||||
<source>Starting %1 failed: Error %2</source>
|
||||
<translation>启动 %1 错误: 错误 %2</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/StreamSegue.qml" line="28"/>
|
||||
<source>Check your firewall and port forwarding rules for port(s): %1</source>
|
||||
<translation>检查防火墙和端口转发规则: %1</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/StreamSegue.qml" line="74"/>
|
||||
<source>This PC's Internet connection is blocking Moonlight. Streaming over the Internet may not work while connected to this network.</source>
|
||||
<translation>此计算机的互联网连接被阻塞。当连接到此网络时,互联网上的流式传输可能无法工作。</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/StreamSegue.qml" line="161"/>
|
||||
<source>Tip:</source>
|
||||
<translation>提示:</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/StreamSegue.qml" line="161"/>
|
||||
<source>Press %1 to disconnect your session</source>
|
||||
<translation>按 %1 来断开会话</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/StreamSegue.qml" line="162"/>
|
||||
<source>Start+Select+L1+R1</source>
|
||||
<translation></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/StreamSegue.qml" line="162"/>
|
||||
<source>Ctrl+Alt+Shift+Q</source>
|
||||
<translation></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>main</name>
|
||||
<message>
|
||||
<location filename="../gui/main.qml" line="254"/>
|
||||
<location filename="../gui/main.qml" line="264"/>
|
||||
<location filename="../gui/main.qml" line="393"/>
|
||||
<location filename="../gui/main.qml" line="408"/>
|
||||
<source>Settings</source>
|
||||
<translation>设置</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/main.qml" line="255"/>
|
||||
<source>Version %1</source>
|
||||
<translation>版本 %1</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/main.qml" line="271"/>
|
||||
<source>Join our community on Discord</source>
|
||||
<translation>加入我们的社区</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/main.qml" line="283"/>
|
||||
<source>Computers</source>
|
||||
<translation>计算机列表</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/main.qml" line="290"/>
|
||||
<source>Add PC manually</source>
|
||||
<translation>手动添加计算机</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/main.qml" line="330"/>
|
||||
<source>Update available for Moonlight: Version %1</source>
|
||||
<translation>Moonlight 可用更新:版本 %1</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/main.qml" line="354"/>
|
||||
<source>Help</source>
|
||||
<translation>帮助</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/main.qml" line="377"/>
|
||||
<source>Gamepad Mapper</source>
|
||||
<translation>手柄按键映射</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/main.qml" line="381"/>
|
||||
<source>Gamepad Mapping</source>
|
||||
<translation>手柄按键映射</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/main.qml" line="415"/>
|
||||
<source>No functioning hardware accelerated H.264 video decoder was detected by Moonlight. Your streaming performance may be severely degraded in this configuration.</source>
|
||||
<translation>Moonlight 未检测到功能正常的硬件加速 H.264 视频解码器。</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/main.qml" line="417"/>
|
||||
<source>Click the Help button for more information on solving this problem.</source>
|
||||
<translation>单击帮助按钮以获取有关解决此问题的更多信息。</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/main.qml" line="423"/>
|
||||
<source>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.</source>
|
||||
<translation>硬件加速在 XWayland 上不起作用。在 XWayland 上继续操作可能会导致流式传输性能下降。</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/main.qml" line="425"/>
|
||||
<source>Click the Help button for more information.</source>
|
||||
<translation>单击帮助按钮以获取更多信息。</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/main.qml" line="432"/>
|
||||
<source>This version of Moonlight isn't optimized for your PC. Please download the '%1' version of Moonlight for the best streaming performance.</source>
|
||||
<translation>此版本的 Moonlight 尚未针对你的计算机优化。请下载“%1”版本的 Moonlight,以获得最佳的流式传输性能。</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/main.qml" line="441"/>
|
||||
<source>Moonlight detected gamepads without a mapping:</source>
|
||||
<translation>Moonlight 检测到没有映射的手柄:</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/main.qml" line="443"/>
|
||||
<source>Click the Help button for information on how to map your gamepads.</source>
|
||||
<translation>单击帮助按钮以获取有关如何映射手柄的信息。</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/main.qml" line="451"/>
|
||||
<source>Are you sure you want to quit?</source>
|
||||
<translation>确定要退出吗?</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/main.qml" line="481"/>
|
||||
<source>Enter the IP address of your GameStream PC:</source>
|
||||
<translation>输入目标计算机的 IP 地址:</translation>
|
||||
</message>
|
||||
</context>
|
||||
</TS>
|
||||
@@ -6,6 +6,11 @@
|
||||
#include <QMutex>
|
||||
#include <QtDebug>
|
||||
#include <QNetworkProxyFactory>
|
||||
#include <QPalette>
|
||||
#include <QFont>
|
||||
#include <QCursor>
|
||||
#include <QElapsedTimer>
|
||||
#include <QFile>
|
||||
|
||||
// Don't let SDL hook our main function, since Qt is already
|
||||
// doing the same thing. This needs to be before any headers
|
||||
@@ -17,17 +22,21 @@
|
||||
#include "streaming/video/ffmpeg.h"
|
||||
#endif
|
||||
|
||||
#ifdef Q_OS_WIN32
|
||||
#if defined(Q_OS_WIN32) && defined(Q_PROCESSOR_X86)
|
||||
#include "antihookingprotection.h"
|
||||
#elif defined(Q_OS_LINUX)
|
||||
#include <openssl/ssl.h>
|
||||
#endif
|
||||
|
||||
#include "cli/quitstream.h"
|
||||
#include "cli/startstream.h"
|
||||
#include "cli/commandlineparser.h"
|
||||
#include "path.h"
|
||||
#include "utils.h"
|
||||
#include "gui/computermodel.h"
|
||||
#include "gui/appmodel.h"
|
||||
#include "backend/autoupdatechecker.h"
|
||||
#include "backend/systemproperties.h"
|
||||
#include "streaming/session.h"
|
||||
#include "settings/streamingpreferences.h"
|
||||
#include "gui/sdlgamepadkeynavigation.h"
|
||||
@@ -48,7 +57,7 @@
|
||||
#endif
|
||||
|
||||
#ifdef USE_CUSTOM_LOGGER
|
||||
static QTime s_LoggerTime;
|
||||
static QElapsedTimer s_LoggerTime;
|
||||
static QTextStream s_LoggerStream(stdout);
|
||||
static QMutex s_LoggerLock;
|
||||
#ifdef LOG_TO_FILE
|
||||
@@ -67,7 +76,12 @@ void logToLoggerStream(QString& message)
|
||||
return;
|
||||
}
|
||||
else if (s_LogLinesWritten == MAX_LOG_LINES) {
|
||||
s_LoggerStream << "Log size limit reached!" << endl;
|
||||
s_LoggerStream << "Log size limit reached!";
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
|
||||
s_LoggerStream << Qt::endl;
|
||||
#else
|
||||
s_LoggerStream << endl;
|
||||
#endif
|
||||
s_LogLimitReached = true;
|
||||
return;
|
||||
}
|
||||
@@ -136,14 +150,6 @@ void qtLogToDiskHandler(QtMsgType type, const QMessageLogContext&, const QString
|
||||
break;
|
||||
}
|
||||
|
||||
// HACK: Avoid printing this internal Qt warning which seems to print every time we
|
||||
// start our event loop for the QNetworkReply. I have found this warning in many
|
||||
// unrelated bug reports, but none actually tackle this warning itself. It seems to be
|
||||
// new in Qt 5.12.
|
||||
if (msg.startsWith("QNetworkReplyHttpImplPrivate::_q_startOperation was called more than once")) {
|
||||
return;
|
||||
}
|
||||
|
||||
QTime logTime = QTime::fromMSecsSinceStartOfDay(s_LoggerTime.elapsed());
|
||||
QString txt = QString("%1 - Qt %2: %3\n").arg(logTime.toString()).arg(typeTxt).arg(msg);
|
||||
|
||||
@@ -190,7 +196,7 @@ LONG WINAPI UnhandledExceptionHandler(struct _EXCEPTION_POINTERS *ExceptionInfo)
|
||||
WCHAR dmpFileName[MAX_PATH];
|
||||
swprintf_s(dmpFileName, L"%ls\\Moonlight-%I64u.dmp",
|
||||
(PWCHAR)QDir::toNativeSeparators(Path::getLogDir()).utf16(), QDateTime::currentSecsSinceEpoch());
|
||||
QString qDmpFileName = QString::fromUtf16((unsigned short*)dmpFileName);
|
||||
QString qDmpFileName = QString::fromUtf16((const char16_t*)dmpFileName);
|
||||
HANDLE dumpHandle = CreateFileW(dmpFileName, GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
|
||||
if (dumpHandle != INVALID_HANDLE_VALUE) {
|
||||
MINIDUMP_EXCEPTION_INFORMATION info;
|
||||
@@ -231,6 +237,8 @@ LONG WINAPI UnhandledExceptionHandler(struct _EXCEPTION_POINTERS *ExceptionInfo)
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
SDL_SetMainReady();
|
||||
|
||||
// Set the app version for the QCommandLineParser's showVersion() command
|
||||
QCoreApplication::setApplicationVersion(VERSION_STR);
|
||||
|
||||
@@ -280,19 +288,60 @@ int main(int argc, char *argv[])
|
||||
SetUnhandledExceptionFilter(UnhandledExceptionHandler);
|
||||
#endif
|
||||
|
||||
#ifdef Q_OS_WIN32
|
||||
#if defined(Q_OS_WIN32) && defined(Q_PROCESSOR_X86)
|
||||
// Force AntiHooking.dll to be statically imported and loaded
|
||||
// by ntdll by calling a dummy function.
|
||||
// by ntdll on x86/x64 platforms by calling a dummy function.
|
||||
// AntiHooking.dll is not currently built for ARM64.
|
||||
AntiHookingDummyImport();
|
||||
#elif defined(Q_OS_LINUX)
|
||||
// Force libssl.so to be directly linked to our binary, so
|
||||
// linuxdeployqt can find it and include it in our AppImage.
|
||||
// QtNetwork will pull it in via dlopen().
|
||||
SSL_free(nullptr);
|
||||
#endif
|
||||
|
||||
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
|
||||
// Avoid using High DPI on EGLFS. It breaks font rendering.
|
||||
// https://bugreports.qt.io/browse/QTBUG-64377
|
||||
//
|
||||
// NB: We can't use QGuiApplication::platformName() here because it is only
|
||||
// set once the QGuiApplication is created, which is too late to enable High DPI :(
|
||||
if (WMUtils::isRunningWindowManager()) {
|
||||
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
|
||||
// Enable High DPI support on Qt 5.x. It is always enabled on Qt 6.0
|
||||
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
|
||||
#endif
|
||||
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
|
||||
// Enable fractional High DPI scaling on Qt 5.14 and later
|
||||
QGuiApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::PassThrough);
|
||||
#endif
|
||||
}
|
||||
else {
|
||||
#ifndef STEAM_LINK
|
||||
if (!qEnvironmentVariableIsSet("QT_QPA_PLATFORM")) {
|
||||
qInfo() << "Unable to detect Wayland or X11, so EGLFS will be used by default. Set QT_QPA_PLATFORM to override this.";
|
||||
qputenv("QT_QPA_PLATFORM", "eglfs");
|
||||
|
||||
if (!qEnvironmentVariableIsSet("QT_QPA_EGLFS_ALWAYS_SET_MODE")) {
|
||||
qInfo() << "Setting display mode by default. Set QT_QPA_EGLFS_ALWAYS_SET_MODE=0 to override this.";
|
||||
|
||||
// The UI doesn't appear on RetroPie without this option.
|
||||
qputenv("QT_QPA_EGLFS_ALWAYS_SET_MODE", "1");
|
||||
}
|
||||
|
||||
if (!QFile("/dev/dri").exists()) {
|
||||
qWarning() << "Unable to find a KMSDRM display device!";
|
||||
qWarning() << "On the Raspberry Pi, you must enable the 'fake KMS' driver in raspi-config to use Moonlight outside of the GUI environment.";
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// This avoids using the default keychain for SSL, which may cause
|
||||
// password prompts on macOS.
|
||||
qputenv("QT_SSL_USE_TEMPORARY_KEYCHAIN", "1");
|
||||
|
||||
#ifdef Q_OS_WIN32
|
||||
#if defined(Q_OS_WIN32) && QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
|
||||
if (!qEnvironmentVariableIsSet("QT_OPENGL")) {
|
||||
// On Windows, use ANGLE so we don't have to load OpenGL
|
||||
// user-mode drivers into our app. OGL drivers (especially Intel)
|
||||
@@ -301,6 +350,18 @@ int main(int argc, char *argv[])
|
||||
}
|
||||
#endif
|
||||
|
||||
#if !defined(Q_OS_WIN32) || QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
// Moonlight requires the non-threaded renderer because we depend
|
||||
// on being able to control the render thread by blocking in the
|
||||
// main thread (and pumping events from the main thread when needed).
|
||||
// That doesn't work with the threaded renderer which causes all
|
||||
// sorts of odd behavior depending on the platform.
|
||||
//
|
||||
// NB: Windows defaults to the "windows" non-threaded render loop on
|
||||
// Qt 5 and the threaded render loop on Qt 6.
|
||||
qputenv("QSG_RENDER_LOOP", "basic");
|
||||
#endif
|
||||
|
||||
// We don't want system proxies to apply to us
|
||||
QNetworkProxyFactory::setUseSystemConfiguration(false);
|
||||
|
||||
@@ -311,19 +372,145 @@ int main(int argc, char *argv[])
|
||||
// Register custom metatypes for use in signals
|
||||
qRegisterMetaType<NvApp>("NvApp");
|
||||
|
||||
// Disable desktop settings awareness due to issues with QGnomePlatform
|
||||
// breaking our dialog rendering on Linux.
|
||||
QGuiApplication::setDesktopSettingsAware(false);
|
||||
SDL_version compileVersion;
|
||||
SDL_VERSION(&compileVersion);
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Compiled with SDL %d.%d.%d",
|
||||
compileVersion.major, compileVersion.minor, compileVersion.patch);
|
||||
|
||||
SDL_version runtimeVersion;
|
||||
SDL_GetVersion(&runtimeVersion);
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Running with SDL %d.%d.%d",
|
||||
runtimeVersion.major, runtimeVersion.minor, runtimeVersion.patch);
|
||||
|
||||
// Allow the display to sleep by default. We will manually use SDL_DisableScreenSaver()
|
||||
// and SDL_EnableScreenSaver() when appropriate. This hint must be set before
|
||||
// initializing the SDL video subsystem to have any effect.
|
||||
SDL_SetHint(SDL_HINT_VIDEO_ALLOW_SCREENSAVER, "1");
|
||||
|
||||
// For SDL backends that support it, use double buffering instead of triple buffering
|
||||
// to save a frame of latency. This doesn't matter for MMAL or DRM renderers since they
|
||||
// are drawing directly to the screen without involving SDL, but it may matter for other
|
||||
// future KMSDRM platforms that use SDL for rendering.
|
||||
SDL_SetHint(SDL_HINT_VIDEO_DOUBLE_BUFFER, "1");
|
||||
|
||||
// We use MMAL to render on Raspberry Pi, so we do not require DRM master.
|
||||
SDL_SetHint("SDL_KMSDRM_REQUIRE_DRM_MASTER", "0");
|
||||
|
||||
// Use Direct3D 9Ex to avoid a deadlock caused by the D3D device being reset when
|
||||
// the user triggers a UAC prompt. This option controls the software/SDL renderer.
|
||||
// The DXVA2 renderer uses Direct3D 9Ex itself directly.
|
||||
SDL_SetHint("SDL_WINDOWS_USE_D3D9EX", "1");
|
||||
|
||||
if (SDL_InitSubSystem(SDL_INIT_TIMER) != 0) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"SDL_InitSubSystem(SDL_INIT_TIMER) failed: %s",
|
||||
SDL_GetError());
|
||||
return -1;
|
||||
}
|
||||
|
||||
#ifdef STEAM_LINK
|
||||
// Steam Link requires that we initialize video before creating our
|
||||
// QGuiApplication in order to configure the framebuffer correctly.
|
||||
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s",
|
||||
SDL_GetError());
|
||||
return -1;
|
||||
}
|
||||
#endif
|
||||
|
||||
// Use atexit() to ensure SDL_Quit() is called. This avoids
|
||||
// racing with object destruction where SDL may be used.
|
||||
atexit(SDL_Quit);
|
||||
|
||||
// Avoid the default behavior of changing the timer resolution to 1 ms.
|
||||
// We don't want this all the time that Moonlight is open. We will set
|
||||
// it manually when we start streaming.
|
||||
SDL_SetHint(SDL_HINT_TIMER_RESOLUTION, "0");
|
||||
|
||||
// Disable minimize on focus loss by default. Users seem to want this off by default.
|
||||
SDL_SetHint(SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS, "0");
|
||||
|
||||
// SDL 2.0.12 changes the default behavior to use the button label rather than the button
|
||||
// position as most other software does. Set this back to 0 to stay consistent with prior
|
||||
// releases of Moonlight.
|
||||
SDL_SetHint("SDL_GAMECONTROLLER_USE_BUTTON_LABELS", "0");
|
||||
|
||||
// Disable relative mouse scaling to renderer size or logical DPI. We want to send
|
||||
// the mouse motion exactly how it was given to us.
|
||||
SDL_SetHint("SDL_MOUSE_RELATIVE_SCALING", "0");
|
||||
|
||||
// Set our app name for SDL to use with PulseAudio. This matches what we provide
|
||||
// as our app name to libsoundio too.
|
||||
SDL_SetHint("SDL_AUDIO_DEVICE_APP_NAME", "Moonlight");
|
||||
|
||||
#ifdef QT_DEBUG
|
||||
// Allow thread naming using exceptions on debug builds. SDL doesn't use SEH
|
||||
// when throwing the exceptions, so we don't enable it for release builds out
|
||||
// of caution.
|
||||
SDL_SetHint(SDL_HINT_WINDOWS_DISABLE_THREAD_NAMING, "0");
|
||||
#endif
|
||||
|
||||
QGuiApplication app(argc, argv);
|
||||
|
||||
// Apply the initial translation based on user preference
|
||||
StreamingPreferences prefs;
|
||||
prefs.retranslate();
|
||||
|
||||
// After the QGuiApplication is created, the platform stuff will be initialized
|
||||
// and we can set the SDL video driver to match Qt.
|
||||
if (WMUtils::isRunningWayland() && QGuiApplication::platformName() == "xcb") {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Detected XWayland. This will probably break hardware decoding! Try running with QT_QPA_PLATFORM=wayland or switch to X11.");
|
||||
}
|
||||
else if (QGuiApplication::platformName().startsWith("wayland")) {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Detected Wayland");
|
||||
qputenv("SDL_VIDEODRIVER", "wayland");
|
||||
}
|
||||
|
||||
#ifdef STEAM_LINK
|
||||
// Qt 5.9 from the Steam Link SDK is not able to load any fonts
|
||||
// since the Steam Link doesn't include any of the ones it looks
|
||||
// for. We know it has NotoSans so we will explicitly ask for that.
|
||||
if (app.font().family().isEmpty()) {
|
||||
qWarning() << "SL HACK: No default font - using NotoSans";
|
||||
|
||||
QFont fon("NotoSans");
|
||||
app.setFont(fon);
|
||||
}
|
||||
|
||||
// Move the mouse to the bottom right so it's invisible when using
|
||||
// gamepad-only navigation.
|
||||
QCursor().setPos(0xFFFF, 0xFFFF);
|
||||
#elif !SDL_VERSION_ATLEAST(2, 0, 11) && defined(Q_OS_LINUX) && (defined(__arm__) || defined(__aarch64__))
|
||||
if (qgetenv("SDL_VIDEO_GL_DRIVER").isEmpty() && QGuiApplication::platformName() == "eglfs") {
|
||||
// Look for Raspberry Pi GLES libraries. SDL 2.0.10 and earlier needs some help finding
|
||||
// the correct libraries for the KMSDRM backend if not compiled with the RPI backend enabled.
|
||||
if (SDL_LoadObject("libbrcmGLESv2.so") != nullptr) {
|
||||
qputenv("SDL_VIDEO_GL_DRIVER", "libbrcmGLESv2.so");
|
||||
}
|
||||
else if (SDL_LoadObject("/opt/vc/lib/libbrcmGLESv2.so") != nullptr) {
|
||||
qputenv("SDL_VIDEO_GL_DRIVER", "/opt/vc/lib/libbrcmGLESv2.so");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifndef Q_OS_DARWIN
|
||||
// Set the window icon except on macOS where we want to keep the
|
||||
// modified macOS 11 style rounded corner icon.
|
||||
app.setWindowIcon(QIcon(":/res/moonlight.svg"));
|
||||
#endif
|
||||
|
||||
// This is necessary to show our icon correctly on Wayland
|
||||
app.setDesktopFileName("com.moonlight_stream.Moonlight.desktop");
|
||||
qputenv("SDL_VIDEO_WAYLAND_WMCLASS", "com.moonlight_stream.Moonlight");
|
||||
qputenv("SDL_VIDEO_X11_WMCLASS", "com.moonlight_stream.Moonlight");
|
||||
|
||||
// Register our C++ types for QML
|
||||
qmlRegisterType<ComputerModel>("ComputerModel", 1, 0, "ComputerModel");
|
||||
qmlRegisterType<AppModel>("AppModel", 1, 0, "AppModel");
|
||||
qmlRegisterType<StreamingPreferences>("StreamingPreferences", 1, 0, "StreamingPreferences");
|
||||
qmlRegisterType<SdlGamepadKeyNavigation>("SdlGamepadKeyNavigation", 1, 0, "SdlGamepadKeyNavigation");
|
||||
qmlRegisterUncreatableType<Session>("Session", 1, 0, "Session", "Session cannot be created from QML");
|
||||
qmlRegisterSingletonType<ComputerManager>("ComputerManager", 1, 0,
|
||||
"ComputerManager",
|
||||
@@ -335,11 +522,36 @@ int main(int argc, char *argv[])
|
||||
[](QQmlEngine*, QJSEngine*) -> QObject* {
|
||||
return new AutoUpdateChecker();
|
||||
});
|
||||
qmlRegisterSingletonType<SystemProperties>("SystemProperties", 1, 0,
|
||||
"SystemProperties",
|
||||
[](QQmlEngine*, QJSEngine*) -> QObject* {
|
||||
return new SystemProperties();
|
||||
});
|
||||
qmlRegisterSingletonType<SdlGamepadKeyNavigation>("SdlGamepadKeyNavigation", 1, 0,
|
||||
"SdlGamepadKeyNavigation",
|
||||
[](QQmlEngine*, QJSEngine*) -> QObject* {
|
||||
return new SdlGamepadKeyNavigation();
|
||||
});
|
||||
qmlRegisterSingletonType<StreamingPreferences>("StreamingPreferences", 1, 0,
|
||||
"StreamingPreferences",
|
||||
[](QQmlEngine* qmlEngine, QJSEngine*) -> QObject* {
|
||||
return new StreamingPreferences(qmlEngine);
|
||||
});
|
||||
|
||||
// Create the identity manager on the main thread
|
||||
IdentityManager::get();
|
||||
|
||||
#ifndef Q_OS_WINRT
|
||||
// Use the dense material dark theme by default
|
||||
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_STYLE")) {
|
||||
qputenv("QT_QUICK_CONTROLS_STYLE", "Material");
|
||||
}
|
||||
#else
|
||||
// Use universal dark on WinRT
|
||||
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_STYLE")) {
|
||||
qputenv("QT_QUICK_CONTROLS_STYLE", "Universal");
|
||||
}
|
||||
#endif
|
||||
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_MATERIAL_THEME")) {
|
||||
qputenv("QT_QUICK_CONTROLS_MATERIAL_THEME", "Dark");
|
||||
}
|
||||
@@ -352,9 +564,6 @@ int main(int argc, char *argv[])
|
||||
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_UNIVERSAL_THEME")) {
|
||||
qputenv("QT_QUICK_CONTROLS_UNIVERSAL_THEME", "Dark");
|
||||
}
|
||||
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_1_STYLE")) {
|
||||
qputenv("QT_QUICK_CONTROLS_1_STYLE", "Base");
|
||||
}
|
||||
|
||||
QQmlApplicationEngine engine;
|
||||
QString initialView;
|
||||
@@ -362,11 +571,11 @@ int main(int argc, char *argv[])
|
||||
GlobalCommandLineParser parser;
|
||||
switch (parser.parse(app.arguments())) {
|
||||
case GlobalCommandLineParser::NormalStartRequested:
|
||||
initialView = "PcView.qml";
|
||||
initialView = "qrc:/gui/PcView.qml";
|
||||
break;
|
||||
case GlobalCommandLineParser::StreamRequested:
|
||||
{
|
||||
initialView = "CliStartStreamSegue.qml";
|
||||
initialView = "qrc:/gui/CliStartStreamSegue.qml";
|
||||
StreamingPreferences* preferences = new StreamingPreferences(&app);
|
||||
StreamCommandLineParser streamParser;
|
||||
streamParser.parse(app.arguments(), preferences);
|
||||
@@ -378,7 +587,7 @@ int main(int argc, char *argv[])
|
||||
}
|
||||
case GlobalCommandLineParser::QuitRequested:
|
||||
{
|
||||
initialView = "CliQuitStreamSegue.qml";
|
||||
initialView = "qrc:/gui/CliQuitStreamSegue.qml";
|
||||
QuitCommandLineParser quitParser;
|
||||
quitParser.parse(app.arguments());
|
||||
auto launcher = new CliQuitStream::Launcher(quitParser.getHost(), &app);
|
||||
@@ -393,23 +602,6 @@ int main(int argc, char *argv[])
|
||||
engine.load(QUrl(QStringLiteral("qrc:/gui/main.qml")));
|
||||
if (engine.rootObjects().isEmpty())
|
||||
return -1;
|
||||
|
||||
SDL_SetMainReady();
|
||||
if (SDL_Init(SDL_INIT_TIMER) != 0) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"SDL_Init() failed: %s",
|
||||
SDL_GetError());
|
||||
}
|
||||
|
||||
// Use atexit() to ensure SDL_Quit() is called. This avoids
|
||||
// racing with object destruction where SDL may be used.
|
||||
atexit(SDL_Quit);
|
||||
|
||||
// Avoid the default behavior of changing the timer resolution to 1 ms.
|
||||
// We don't want this all the time that Moonlight is open. We will set
|
||||
// it manually when we start streaming.
|
||||
SDL_SetHint(SDL_HINT_TIMER_RESOLUTION, "0");
|
||||
|
||||
int err = app.exec();
|
||||
|
||||
// Give worker tasks time to properly exit. Fixes PendingQuitTask
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
// This file contains hooks for several functions that allow
|
||||
// Qt and SDL to more or less share DRM master ownership.
|
||||
//
|
||||
// This technique requires Linux v5.8 or later, or for Moonlight
|
||||
// to run as root (with CAP_SYS_ADMIN). Prior to Linux v5.8,
|
||||
// DRM_IOCTL_DROP_MASTER required CAP_SYS_ADMIN, which prevents
|
||||
// our trick from working (without root, that is).
|
||||
//
|
||||
// The specific kernel change required to run without root is:
|
||||
// https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=45bc3d26c95a8fc63a7d8668ca9e57ef0883351c
|
||||
|
||||
#define _GNU_SOURCE
|
||||
|
||||
#include <SDL.h>
|
||||
#include <dlfcn.h>
|
||||
#include <unistd.h>
|
||||
#include <errno.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#include <xf86drm.h>
|
||||
#include <xf86drmMode.h>
|
||||
|
||||
// We require SDL 2.0.15+ to hook because it supports sharing
|
||||
// the DRM FD with our code. This avoids having multiple DRM FDs
|
||||
// in flight at the same time which would significantly complicate
|
||||
// the logic here because we'd need to figure out exactly which FD
|
||||
// should be the master at any given time. With the position of our
|
||||
// hooks, that is definitely not trivial.
|
||||
#if SDL_VERSION_ATLEAST(2, 0, 15)
|
||||
|
||||
// Qt's DRM master FD grabbed by our hook
|
||||
int g_QtDrmMasterFd = -1;
|
||||
struct stat64 g_DrmMasterStat;
|
||||
|
||||
// The DRM master FD created for SDL
|
||||
int g_SdlDrmMasterFd = -1;
|
||||
|
||||
// This hook will handle legacy DRM rendering
|
||||
int drmModeSetCrtc(int fd, uint32_t crtcId, uint32_t bufferId,
|
||||
uint32_t x, uint32_t y, uint32_t *connectors, int count,
|
||||
drmModeModeInfoPtr mode)
|
||||
{
|
||||
// Grab the first DRM FD that makes it in here. This will be the Qt
|
||||
// EGLFS backend's DRM FD, which we will need later.
|
||||
if (g_QtDrmMasterFd == -1) {
|
||||
g_QtDrmMasterFd = fd;
|
||||
fstat64(g_QtDrmMasterFd, &g_DrmMasterStat);
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Captured Qt EGLFS DRM master fd (legacy): %d",
|
||||
g_QtDrmMasterFd);
|
||||
}
|
||||
|
||||
// Call into the real thing
|
||||
return ((typeof(drmModeSetCrtc)*)dlsym(RTLD_NEXT, "drmModeSetCrtc"))(fd, crtcId, bufferId, x, y, connectors, count, mode);
|
||||
}
|
||||
|
||||
// This hook will handle atomic DRM rendering
|
||||
int drmModeAtomicCommit(int fd, drmModeAtomicReqPtr req,
|
||||
uint32_t flags, void *user_data)
|
||||
{
|
||||
// Grab the first DRM FD that makes it in here. This will be the Qt
|
||||
// EGLFS backend's DRM FD, which we will need later.
|
||||
if (g_QtDrmMasterFd == -1) {
|
||||
g_QtDrmMasterFd = fd;
|
||||
fstat64(g_QtDrmMasterFd, &g_DrmMasterStat);
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Captured Qt EGLFS DRM master fd (atomic): %d",
|
||||
g_QtDrmMasterFd);
|
||||
}
|
||||
|
||||
// Call into the real thing
|
||||
return ((typeof(drmModeAtomicCommit)*)dlsym(RTLD_NEXT, "drmModeAtomicCommit"))(fd, req, flags, user_data);
|
||||
}
|
||||
|
||||
// This hook will handle SDL's open() on the DRM device. We just need to
|
||||
// hook this variant of open(), since that's what SDL uses. When we see
|
||||
// the open a FD for the same card as the Qt DRM master FD, we'll drop
|
||||
// master on the Qt FD to allow the new FD to have master.
|
||||
int open(const char *pathname, int flags)
|
||||
{
|
||||
// Call the real thing to do the open operation
|
||||
int fd = ((typeof(open)*)dlsym(RTLD_NEXT, "open"))(pathname, flags);
|
||||
|
||||
// If the file was successfully opened and we have a DRM master FD,
|
||||
// check if the FD we just opened is a DRM device.
|
||||
if (fd >= 0 && g_QtDrmMasterFd != -1) {
|
||||
if (strncmp(pathname, "/dev/dri/card", 13) == 0) {
|
||||
// It's a DRM device, but is it _our_ DRM device?
|
||||
struct stat64 fdstat;
|
||||
|
||||
fstat64(fd, &fdstat);
|
||||
if (g_DrmMasterStat.st_dev == fdstat.st_dev &&
|
||||
g_DrmMasterStat.st_ino == fdstat.st_ino) {
|
||||
// It is our device. Time to do the magic!
|
||||
|
||||
// This code assumes SDL only ever opens a single FD
|
||||
// for a given DRM device.
|
||||
SDL_assert(g_SdlDrmMasterFd == -1);
|
||||
|
||||
// Drop master on Qt's FD so we can pick it up for SDL.
|
||||
if (drmDropMaster(g_QtDrmMasterFd) < 0) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Failed to drop master on Qt DRM FD: %d",
|
||||
errno);
|
||||
// Hope for the best
|
||||
return fd;
|
||||
}
|
||||
|
||||
// We are not allowed to call drmSetMaster() without CAP_SYS_ADMIN,
|
||||
// but since we just dropped the master, we can become master by
|
||||
// simply creating a new FD. Let's do it.
|
||||
close(fd);
|
||||
fd = ((typeof(open)*)dlsym(RTLD_NEXT, "open"))(pathname, flags);
|
||||
g_SdlDrmMasterFd = fd;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fd;
|
||||
}
|
||||
|
||||
// Our close() hook handles restoring DRM master to the Qt FD
|
||||
// after SDL closes its DRM FD.
|
||||
int close(int fd)
|
||||
{
|
||||
// Call the real thing
|
||||
int ret = ((typeof(close)*)dlsym(RTLD_NEXT, "close"))(fd);
|
||||
if (ret == 0) {
|
||||
// If we just closed the SDL DRM master FD, restore master
|
||||
// to the Qt DRM FD. This works because the Qt DRM master FD
|
||||
// was master once before, so we can set it as master again
|
||||
// using drmSetMaster() without CAP_SYS_ADMIN.
|
||||
if (g_SdlDrmMasterFd != -1 && fd == g_SdlDrmMasterFd) {
|
||||
if (drmSetMaster(g_QtDrmMasterFd) < 0) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Failed to restore master to Qt DRM FD: %d",
|
||||
errno);
|
||||
}
|
||||
|
||||
g_SdlDrmMasterFd = -1;
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <QSettings>
|
||||
#include <QCoreApplication>
|
||||
|
||||
QString Path::s_CacheDir;
|
||||
QString Path::s_LogDir;
|
||||
QString Path::s_BoxArtCacheDir;
|
||||
|
||||
@@ -21,40 +22,74 @@ QString Path::getBoxArtCacheDir()
|
||||
return s_BoxArtCacheDir;
|
||||
}
|
||||
|
||||
QByteArray Path::readDataFile(QString fileName)
|
||||
{
|
||||
QFile dataFile(getDataFilePath(fileName));
|
||||
dataFile.open(QIODevice::ReadOnly);
|
||||
return dataFile.readAll();
|
||||
}
|
||||
|
||||
void Path::writeCacheFile(QString fileName, QByteArray data)
|
||||
{
|
||||
QDir cacheDir(s_CacheDir);
|
||||
|
||||
// Create the cache path if it does not exist
|
||||
if (!cacheDir.exists()) {
|
||||
cacheDir.mkpath(".");
|
||||
}
|
||||
|
||||
QFile dataFile(cacheDir.absoluteFilePath(fileName));
|
||||
dataFile.open(QIODevice::WriteOnly);
|
||||
dataFile.write(data);
|
||||
}
|
||||
|
||||
void Path::deleteCacheFile(QString fileName)
|
||||
{
|
||||
QFile dataFile(QDir(s_CacheDir).absoluteFilePath(fileName));
|
||||
dataFile.remove();
|
||||
}
|
||||
|
||||
QFileInfo Path::getCacheFileInfo(QString fileName)
|
||||
{
|
||||
return QFileInfo(QDir(s_CacheDir), fileName);
|
||||
}
|
||||
|
||||
QString Path::getDataFilePath(QString fileName)
|
||||
{
|
||||
QString candidatePath;
|
||||
|
||||
// Check the current directory first
|
||||
// Check the cache location first (used by Path::writeDataFile())
|
||||
candidatePath = QDir(s_CacheDir).absoluteFilePath(fileName);
|
||||
if (QFile::exists(candidatePath)) {
|
||||
qInfo() << "Found" << fileName << "at" << candidatePath;
|
||||
return candidatePath;
|
||||
}
|
||||
|
||||
// Check the current directory
|
||||
candidatePath = QDir(QDir::currentPath()).absoluteFilePath(fileName);
|
||||
if (QFile::exists(candidatePath)) {
|
||||
qInfo() << "Found" << fileName << "at" << candidatePath;
|
||||
return candidatePath;
|
||||
}
|
||||
|
||||
// Now check the data directories (for Linux, in particular)
|
||||
candidatePath = QStandardPaths::locate(QStandardPaths::DataLocation, fileName);
|
||||
candidatePath = QStandardPaths::locate(QStandardPaths::AppDataLocation, fileName);
|
||||
if (!candidatePath.isEmpty() && QFile::exists(candidatePath)) {
|
||||
qInfo() << "Found" << fileName << "at" << candidatePath;
|
||||
return candidatePath;
|
||||
}
|
||||
|
||||
// Now try the directory of our app installation (for Windows, if current dir doesn't find it)
|
||||
candidatePath = QDir(QCoreApplication::applicationDirPath()).absoluteFilePath(fileName);
|
||||
if (QFile::exists(candidatePath)) {
|
||||
qInfo() << "Found" << fileName << "at" << candidatePath;
|
||||
return candidatePath;
|
||||
}
|
||||
|
||||
// Now try the Resources folder in our app bundle for macOS
|
||||
QDir dir = QDir(QCoreApplication::applicationDirPath());
|
||||
dir.cdUp();
|
||||
dir.cd("Resources");
|
||||
dir.filePath(fileName);
|
||||
candidatePath = dir.absoluteFilePath(fileName);
|
||||
if (QFile::exists(candidatePath)) {
|
||||
return candidatePath;
|
||||
}
|
||||
|
||||
// Couldn't find it
|
||||
return QString();
|
||||
// Return the QRC embedded copy
|
||||
candidatePath = ":/data/" + fileName;
|
||||
qInfo() << "Found" << fileName << "at" << candidatePath;
|
||||
return QString(candidatePath);
|
||||
}
|
||||
|
||||
void Path::initialize(bool portable)
|
||||
@@ -62,6 +97,10 @@ void Path::initialize(bool portable)
|
||||
if (portable) {
|
||||
s_LogDir = QDir::currentPath();
|
||||
s_BoxArtCacheDir = QDir::currentPath() + "/boxart";
|
||||
|
||||
// In order for the If-Modified-Since logic to work in MappingFetcher,
|
||||
// the cache directory must be different than the current directory.
|
||||
s_CacheDir = QDir::currentPath() + "/cache";
|
||||
}
|
||||
else {
|
||||
#ifdef Q_OS_DARWIN
|
||||
@@ -71,6 +110,7 @@ void Path::initialize(bool portable)
|
||||
#else
|
||||
s_LogDir = QDir::tempPath();
|
||||
#endif
|
||||
s_CacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation);
|
||||
s_BoxArtCacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/boxart";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <QString>
|
||||
#include <QFileInfo>
|
||||
|
||||
class Path
|
||||
{
|
||||
@@ -9,11 +10,18 @@ public:
|
||||
|
||||
static QString getBoxArtCacheDir();
|
||||
|
||||
static QByteArray readDataFile(QString fileName);
|
||||
static void writeCacheFile(QString fileName, QByteArray data);
|
||||
static void deleteCacheFile(QString fileName);
|
||||
static QFileInfo getCacheFileInfo(QString fileName);
|
||||
|
||||
// Only safe to use directly for Qt classes
|
||||
static QString getDataFilePath(QString fileName);
|
||||
|
||||
static void initialize(bool portable);
|
||||
|
||||
private:
|
||||
static QString s_CacheDir;
|
||||
static QString s_LogDir;
|
||||
static QString s_BoxArtCacheDir;
|
||||
};
|
||||
|
||||
@@ -14,5 +14,9 @@
|
||||
<file>gui/CliStartStreamSegue.qml</file>
|
||||
<file>gui/AutoResizingComboBox.qml</file>
|
||||
<file>gui/NavigableMenu.qml</file>
|
||||
<file>gui/ErrorMessageDialog.qml</file>
|
||||
<file>gui/NavigableMessageDialog.qml</file>
|
||||
<file>gui/NavigableDialog.qml</file>
|
||||
<file>gui/CenteredGridView.qml</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 245 240" fill="white"><path class="st0" d="M104.4 103.9c-5.7 0-10.2 5-10.2 11.1s4.6 11.1 10.2 11.1c5.7 0 10.2-5 10.2-11.1.1-6.1-4.5-11.1-10.2-11.1zM140.9 103.9c-5.7 0-10.2 5-10.2 11.1s4.6 11.1 10.2 11.1c5.7 0 10.2-5 10.2-11.1s-4.5-11.1-10.2-11.1z"/><path class="st0" d="M189.5 20h-134C44.2 20 35 29.2 35 40.6v135.2c0 11.4 9.2 20.6 20.5 20.6h113.4l-5.3-18.5 12.8 11.9 12.1 11.2 21.5 19V40.6c0-11.4-9.2-20.6-20.5-20.6zm-38.6 130.6s-3.6-4.3-6.6-8.1c13.1-3.7 18.1-11.9 18.1-11.9-4.1 2.7-8 4.6-11.5 5.9-5 2.1-9.8 3.5-14.5 4.3-9.6 1.8-18.4 1.3-25.9-.1-5.7-1.1-10.6-2.7-14.7-4.3-2.3-.9-4.8-2-7.3-3.4-.3-.2-.6-.3-.9-.5-.2-.1-.3-.2-.4-.3-1.8-1-2.8-1.7-2.8-1.7s4.8 8 17.5 11.8c-3 3.8-6.7 8.3-6.7 8.3-22.1-.7-30.5-15.2-30.5-15.2 0-32.2 14.4-58.3 14.4-58.3 14.4-10.8 28.1-10.5 28.1-10.5l1 1.2c-18 5.2-26.3 13.1-26.3 13.1s2.2-1.2 5.9-2.9c10.7-4.7 19.2-6 22.7-6.3.6-.1 1.1-.2 1.7-.2 6.1-.8 13-1 20.2-.2 9.5 1.1 19.7 3.9 30.1 9.6 0 0-7.9-7.5-24.9-12.7l1.4-1.6s13.7-.3 28.1 10.5c0 0 14.4 26.1 14.4 58.3 0 0-8.5 14.5-30.6 15.2z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24" fill="#FFFFFF"><path d="M0 0h24v24H0V0zm0 0h24v24H0V0z" fill="none"/><path d="M16.59 7.58L10 14.17l-3.59-3.58L5 12l5 5 8-8zM12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"/></svg>
|
||||
|
After Width: | Height: | Size: 339 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="#FFFFFF"><path fill="none" d="M0 0h24v24H0V0z"/><path d="M11 15h2v2h-2zm0-8h2v6h-2zm.99-5C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"/></svg>
|
||||
|
After Width: | Height: | Size: 323 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="#FFFFFF"><path fill="none" d="M0 0h24v24H0z"/><path d="M11 18h2v-2h-2v2zm1-16C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm0-14c-2.21 0-4 1.79-4 4h2c0-1.1.9-2 2-2s2 .9 2 2c0 2-3 1.75-3 5h2c0-2.25 3-2.5 3-5 0-2.21-1.79-4-4-4z"/></svg>
|
||||
|
After Width: | Height: | Size: 397 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="#FFFFFF" width="48" height="48" viewBox="0 0 24 24">
|
||||
<path fill="none" d="M0 0h24v24H0V0z"/>
|
||||
<path d="M21 2H3c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h7v2H8v2h8v-2h-2v-2h7c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 14H3V4h18v12z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 276 B |
@@ -1,4 +0,0 @@
|
||||
<svg fill="#FFFFFF" height="48" viewBox="0 0 24 24" width="48" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M7 11v2h10v-2H7zm5-9C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 303 B |
@@ -1,9 +0,0 @@
|
||||
<svg fill="#FFFFFF" height="48" viewBox="0 0 24 24" width="48" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<defs>
|
||||
<path d="M0 0h24v24H0V0z" id="a"/>
|
||||
</defs>
|
||||
<clipPath id="b">
|
||||
<use overflow="visible" xlink:href="#a"/>
|
||||
</clipPath>
|
||||
<path clip-path="url(#b)" d="M21 3H3c-1.11 0-2 .89-2 2v12c0 1.1.89 2 2 2h5v2h8v-2h5c1.1 0 1.99-.9 1.99-2L23 5c0-1.11-.9-2-2-2zm0 14H3V5h18v12zm-5-7v2H8v-2h8z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 467 B |
@@ -1,4 +0,0 @@
|
||||
<svg fill="#FFFFFF" height="48" viewBox="0 0 24 24" width="48" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M21 3H3c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h5v2h8v-2h5c1.1 0 1.99-.9 1.99-2L23 5c0-1.1-.9-2-2-2zm0 14H3V5h18v12z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 273 B |
@@ -1,9 +1,7 @@
|
||||
<RCC>
|
||||
<qresource prefix="/">
|
||||
<file>res/ic_remove_circle_outline_white_48px.svg</file>
|
||||
<file>res/ic_remove_from_queue_white_48px.svg</file>
|
||||
<file>res/ic_videogame_asset_white_48px.svg</file>
|
||||
<file>res/ic_tv_white_48px.svg</file>
|
||||
<file>res/desktop_windows-48px.svg</file>
|
||||
<file>res/ic_add_to_queue_white_48px.svg</file>
|
||||
<file>res/baseline-lock-24px.svg</file>
|
||||
<file>res/baseline-play_circle_filled_white-48px.svg</file>
|
||||
@@ -15,5 +13,23 @@
|
||||
<file>res/question_mark.svg</file>
|
||||
<file>res/moonlight.svg</file>
|
||||
<file>res/update.svg</file>
|
||||
<file>res/baseline-help_outline-24px.svg</file>
|
||||
<file>res/baseline-error_outline-24px.svg</file>
|
||||
<file>res/baseline-check_circle_outline-24px.svg</file>
|
||||
<file>res/Discord-Logo-White.svg</file>
|
||||
<file>languages/qml_de.ts</file>
|
||||
<file>languages/qml_de.qm</file>
|
||||
<file>languages/qml_fr.ts</file>
|
||||
<file>languages/qml_fr.qm</file>
|
||||
<file>languages/qml_zh_cn.ts</file>
|
||||
<file>languages/qml_zh_cn.qm</file>
|
||||
</qresource>
|
||||
<qresource prefix="/data">
|
||||
<file alias="gamecontrollerdb.txt">SDL_GameControllerDB/gamecontrollerdb.txt</file>
|
||||
<file alias="ModeSeven.ttf">ModeSeven.ttf</file>
|
||||
<file alias="egl.frag">shaders/egl.frag</file>
|
||||
<file alias="egl.vert">shaders/egl.vert</file>
|
||||
<file alias="egl_overlay.frag">shaders/egl_overlay.frag</file>
|
||||
<file alias="egl_overlay.vert">shaders/egl_overlay.vert</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
#include "mappingfetcher.h"
|
||||
#include "path.h"
|
||||
|
||||
#include <QNetworkReply>
|
||||
|
||||
MappingFetcher::MappingFetcher(QObject *parent) :
|
||||
QObject(parent),
|
||||
m_Nam(this)
|
||||
{
|
||||
// Never communicate over HTTP
|
||||
m_Nam.setStrictTransportSecurityEnabled(true);
|
||||
|
||||
// Allow HTTP redirects
|
||||
m_Nam.setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy);
|
||||
|
||||
connect(&m_Nam, &QNetworkAccessManager::finished,
|
||||
this, &MappingFetcher::handleMappingListFetched);
|
||||
}
|
||||
|
||||
void MappingFetcher::start()
|
||||
{
|
||||
#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
|
||||
|
||||
QUrl url("https://moonlight-stream.org/SDL_GameControllerDB/gamecontrollerdb.txt");
|
||||
QNetworkRequest request(url);
|
||||
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
|
||||
request.setAttribute(QNetworkRequest::Http2AllowedAttribute, true);
|
||||
#else
|
||||
request.setAttribute(QNetworkRequest::HTTP2AllowedAttribute, true);
|
||||
#endif
|
||||
|
||||
// Qt 5.12 introduced QNetworkRequest::IfModifiedSinceHeader. We _could_ implement it
|
||||
// by hand (including QDateTime conversion to the correct string form) for earlier Qt
|
||||
// versions, but that's a pain and this is just an optimization anyway. We'll do a bit
|
||||
// of extra work on those legacy Qt versions by fetching the GCDB each time we launch.
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 12, 0)
|
||||
// Only download the file if it's newer than what we have
|
||||
QFileInfo existingFileInfo = Path::getCacheFileInfo("gamecontrollerdb.txt");
|
||||
if (existingFileInfo.exists()) {
|
||||
// Make sure the cached file looks reasonable. It should have some data and
|
||||
// the last modified time should not be in the future.
|
||||
QDateTime lastModifiedTime = existingFileInfo.lastModified().toUTC();
|
||||
if (existingFileInfo.size() > 0 && lastModifiedTime <= QDateTime::currentDateTimeUtc()) {
|
||||
request.setHeader(QNetworkRequest::IfModifiedSinceHeader, existingFileInfo.lastModified().toUTC());
|
||||
}
|
||||
else {
|
||||
Path::deleteCacheFile("gamecontrollerdb.txt");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// We'll get a callback when this is finished
|
||||
m_Nam.get(request);
|
||||
}
|
||||
|
||||
void MappingFetcher::handleMappingListFetched(QNetworkReply* reply)
|
||||
{
|
||||
Q_ASSERT(reply->isFinished());
|
||||
|
||||
if (reply->error() == QNetworkReply::NoError) {
|
||||
// Queue the reply for deletion
|
||||
reply->deleteLater();
|
||||
|
||||
// If we get a 304 back, Qt will happily just tell us our request was
|
||||
// successful and let us try to write our empty response to disk. Check
|
||||
// the status code directly to prevent this.
|
||||
if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute) == 304) {
|
||||
qInfo() << "Gamepad mappings are up to date";
|
||||
}
|
||||
else {
|
||||
// Update the cached data on disk for next call to applyMappings()
|
||||
QByteArray data = reply->readAll();
|
||||
if (!data.isEmpty()) {
|
||||
Path::writeCacheFile("gamecontrollerdb.txt", data);
|
||||
}
|
||||
|
||||
qInfo() << "Downloaded updated gamepad mappings";
|
||||
}
|
||||
}
|
||||
else {
|
||||
qWarning() << "Failed to download updated gamepad mappings:" << reply->error();
|
||||
reply->deleteLater();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QNetworkAccessManager>
|
||||
|
||||
class MappingFetcher : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit MappingFetcher(QObject *parent = nullptr);
|
||||
|
||||
void start();
|
||||
|
||||
private slots:
|
||||
void handleMappingListFetched(QNetworkReply* reply);
|
||||
|
||||
private:
|
||||
QNetworkAccessManager m_Nam;
|
||||
};
|
||||
@@ -10,10 +10,18 @@
|
||||
#define SER_GUID "guid"
|
||||
#define SER_MAPPING "mapping"
|
||||
|
||||
MappingFetcher* MappingManager::s_MappingFetcher;
|
||||
|
||||
MappingManager::MappingManager()
|
||||
{
|
||||
QSettings settings;
|
||||
|
||||
// Load updated mappings from the Internet once per Moonlight launch
|
||||
if (s_MappingFetcher == nullptr) {
|
||||
s_MappingFetcher = new MappingFetcher();
|
||||
s_MappingFetcher->start();
|
||||
}
|
||||
|
||||
// First load existing saved mappings. This ensures the user's
|
||||
// hints can always override the old data.
|
||||
int mappingCount = settings.beginReadArray(SER_GAMEPADMAPPING);
|
||||
@@ -26,8 +34,14 @@ MappingManager::MappingManager()
|
||||
settings.endArray();
|
||||
|
||||
// Finally load mappings from SDL_HINT_GAMECONTROLLERCONFIG
|
||||
QStringList sdlMappings = QString::fromLocal8Bit(SDL_GetHint(SDL_HINT_GAMECONTROLLERCONFIG)).split('\n', QString::SkipEmptyParts);
|
||||
for (QString sdlMapping : sdlMappings) {
|
||||
QStringList sdlMappings =
|
||||
QString::fromLocal8Bit(SDL_GetHint(SDL_HINT_GAMECONTROLLERCONFIG))
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
|
||||
.split('\n', Qt::SkipEmptyParts);
|
||||
#else
|
||||
.split('\n', QString::SkipEmptyParts);
|
||||
#endif
|
||||
for (const QString& sdlMapping : sdlMappings) {
|
||||
SdlGamepadMapping mapping(sdlMapping);
|
||||
addMapping(mapping);
|
||||
}
|
||||
@@ -54,29 +68,34 @@ void MappingManager::save()
|
||||
|
||||
void MappingManager::applyMappings()
|
||||
{
|
||||
QString mappingFile = Path::getDataFilePath("gamecontrollerdb.txt");
|
||||
if (!mappingFile.isEmpty()) {
|
||||
std::string mappingFileNative = QDir::toNativeSeparators(mappingFile).toStdString();
|
||||
QByteArray mappingData = Path::readDataFile("gamecontrollerdb.txt");
|
||||
if (!mappingData.isEmpty()) {
|
||||
int newMappings = SDL_GameControllerAddMappingsFromRW(
|
||||
SDL_RWFromConstMem(mappingData.constData(), mappingData.size()), 1);
|
||||
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Loading gamepad mappings from: %s",
|
||||
mappingFileNative.c_str());
|
||||
|
||||
int newMappings = SDL_GameControllerAddMappingsFromFile(mappingFileNative.c_str());
|
||||
if (newMappings < 0) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Error loading gamepad mappings: %s",
|
||||
SDL_GetError());
|
||||
}
|
||||
else {
|
||||
if (newMappings > 0) {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Loaded %d new gamepad mappings",
|
||||
newMappings);
|
||||
}
|
||||
else {
|
||||
if (newMappings < 0) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Error loading gamepad mappings: %s",
|
||||
SDL_GetError());
|
||||
}
|
||||
else if (newMappings == 0) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"0 new mappings found in gamecontrollerdb.txt. Is it corrupt?");
|
||||
}
|
||||
|
||||
// Try deleting the cached mapping list just in case it's corrupt
|
||||
Path::deleteCacheFile("gamecontrollerdb.txt");
|
||||
}
|
||||
}
|
||||
else {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"No gamepad mapping file found");
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Unable to load gamepad mapping file");
|
||||
}
|
||||
|
||||
QList<SdlGamepadMapping> mappings = m_Mappings.values();
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "mappingfetcher.h"
|
||||
|
||||
#include <QSettings>
|
||||
|
||||
class SdlGamepadMapping
|
||||
@@ -70,5 +72,7 @@ public:
|
||||
|
||||
private:
|
||||
QMap<QString, SdlGamepadMapping> m_Mappings;
|
||||
|
||||
static MappingFetcher* s_MappingFetcher;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
#include "streamingpreferences.h"
|
||||
#include "streaming/session.h"
|
||||
#include "streaming/streamutils.h"
|
||||
|
||||
#include <QSettings>
|
||||
#include <QTranslator>
|
||||
#include <QCoreApplication>
|
||||
#include <QLocale>
|
||||
|
||||
#include <QtDebug>
|
||||
|
||||
#define SER_STREAMSETTINGS "streamsettings"
|
||||
#define SER_WIDTH "width"
|
||||
@@ -21,12 +24,37 @@
|
||||
#define SER_UNSUPPORTEDFPS "unsupportedfps"
|
||||
#define SER_MDNS "mdns"
|
||||
#define SER_QUITAPPAFTER "quitAppAfter"
|
||||
#define SER_MOUSEACCELERATION "mouseacceleration"
|
||||
#define SER_ABSMOUSEMODE "mouseacceleration"
|
||||
#define SER_ABSTOUCHMODE "abstouchmode"
|
||||
#define SER_STARTWINDOWED "startwindowed"
|
||||
#define SER_FRAMEPACING "framepacing"
|
||||
#define SER_CONNWARNINGS "connwarnings"
|
||||
#define SER_UIDISPLAYMODE "uidisplaymode"
|
||||
#define SER_RICHPRESENCE "richpresence"
|
||||
#define SER_GAMEPADMOUSE "gamepadmouse"
|
||||
#define SER_DEFAULTVER "defaultver"
|
||||
#define SER_PACKETSIZE "packetsize"
|
||||
#define SER_DETECTNETBLOCKING "detectnetblocking"
|
||||
#define SER_SWAPMOUSEBUTTONS "swapmousebuttons"
|
||||
#define SER_MUTEONFOCUSLOSS "muteonfocusloss"
|
||||
#define SER_BACKGROUNDGAMEPAD "backgroundgamepad"
|
||||
#define SER_REVERSESCROLL "reversescroll"
|
||||
#define SER_SWAPFACEBUTTONS "swapfacebuttons"
|
||||
#define SER_CAPTURESYSKEYS "capturesyskeys"
|
||||
#define SER_LANGUAGE "language"
|
||||
|
||||
#define CURRENT_DEFAULT_VER 1
|
||||
|
||||
StreamingPreferences::StreamingPreferences(QObject *parent)
|
||||
: QObject(parent)
|
||||
: QObject(parent),
|
||||
m_QmlEngine(nullptr)
|
||||
{
|
||||
reload();
|
||||
}
|
||||
|
||||
StreamingPreferences::StreamingPreferences(QQmlEngine *qmlEngine, QObject *parent)
|
||||
: QObject(parent),
|
||||
m_QmlEngine(qmlEngine)
|
||||
{
|
||||
reload();
|
||||
}
|
||||
@@ -35,6 +63,14 @@ void StreamingPreferences::reload()
|
||||
{
|
||||
QSettings settings;
|
||||
|
||||
int defaultVer = settings.value(SER_DEFAULTVER, 0).toInt();
|
||||
|
||||
#ifdef Q_OS_DARWIN
|
||||
recommendedFullScreenMode = WindowMode::WM_FULLSCREEN_DESKTOP;
|
||||
#else
|
||||
recommendedFullScreenMode = WindowMode::WM_FULLSCREEN;
|
||||
#endif
|
||||
|
||||
width = settings.value(SER_WIDTH, 1280).toInt();
|
||||
height = settings.value(SER_HEIGHT, 720).toInt();
|
||||
fps = settings.value(SER_FPS, 60).toInt();
|
||||
@@ -46,9 +82,21 @@ void StreamingPreferences::reload()
|
||||
unsupportedFps = settings.value(SER_UNSUPPORTEDFPS, false).toBool();
|
||||
enableMdns = settings.value(SER_MDNS, true).toBool();
|
||||
quitAppAfter = settings.value(SER_QUITAPPAFTER, false).toBool();
|
||||
mouseAcceleration = settings.value(SER_MOUSEACCELERATION, false).toBool();
|
||||
startWindowed = settings.value(SER_STARTWINDOWED, false).toBool();
|
||||
absoluteMouseMode = settings.value(SER_ABSMOUSEMODE, false).toBool();
|
||||
absoluteTouchMode = settings.value(SER_ABSTOUCHMODE, true).toBool();
|
||||
framePacing = settings.value(SER_FRAMEPACING, false).toBool();
|
||||
connectionWarnings = settings.value(SER_CONNWARNINGS, true).toBool();
|
||||
richPresence = settings.value(SER_RICHPRESENCE, true).toBool();
|
||||
gamepadMouse = settings.value(SER_GAMEPADMOUSE, true).toBool();
|
||||
detectNetworkBlocking = settings.value(SER_DETECTNETBLOCKING, true).toBool();
|
||||
packetSize = settings.value(SER_PACKETSIZE, 0).toInt();
|
||||
swapMouseButtons = settings.value(SER_SWAPMOUSEBUTTONS, false).toBool();
|
||||
muteOnFocusLoss = settings.value(SER_MUTEONFOCUSLOSS, false).toBool();
|
||||
backgroundGamepad = settings.value(SER_BACKGROUNDGAMEPAD, false).toBool();
|
||||
reverseScrollDirection = settings.value(SER_REVERSESCROLL, false).toBool();
|
||||
swapFaceButtons = settings.value(SER_SWAPFACEBUTTONS, false).toBool();
|
||||
captureSysKeysMode = static_cast<CaptureSysKeysMode>(settings.value(SER_CAPTURESYSKEYS,
|
||||
static_cast<int>(CaptureSysKeysMode::CSK_OFF)).toInt());
|
||||
audioConfig = static_cast<AudioConfig>(settings.value(SER_AUDIOCFG,
|
||||
static_cast<int>(AudioConfig::AC_STEREO)).toInt());
|
||||
videoCodecConfig = static_cast<VideoCodecConfig>(settings.value(SER_VIDEOCFG,
|
||||
@@ -58,7 +106,93 @@ void StreamingPreferences::reload()
|
||||
windowMode = static_cast<WindowMode>(settings.value(SER_WINDOWMODE,
|
||||
// Try to load from the old preference value too
|
||||
static_cast<int>(settings.value(SER_FULLSCREEN, true).toBool() ?
|
||||
WindowMode::WM_FULLSCREEN : WindowMode::WM_WINDOWED)).toInt());
|
||||
recommendedFullScreenMode : WindowMode::WM_WINDOWED)).toInt());
|
||||
uiDisplayMode = static_cast<UIDisplayMode>(settings.value(SER_UIDISPLAYMODE,
|
||||
static_cast<int>(settings.value(SER_STARTWINDOWED, true).toBool() ? UIDisplayMode::UI_WINDOWED
|
||||
: UIDisplayMode::UI_MAXIMIZED)).toInt());
|
||||
language = static_cast<Language>(settings.value(SER_LANGUAGE,
|
||||
static_cast<int>(Language::LANG_AUTO)).toInt());
|
||||
|
||||
|
||||
// Perform default settings updates as required based on last default version
|
||||
if (defaultVer == 0) {
|
||||
#ifdef Q_OS_DARWIN
|
||||
// Update window mode setting on macOS from full-screen (old default) to borderless windowed (new default)
|
||||
if (windowMode == WindowMode::WM_FULLSCREEN) {
|
||||
windowMode = WindowMode::WM_FULLSCREEN_DESKTOP;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
bool StreamingPreferences::retranslate()
|
||||
{
|
||||
static QTranslator* translator = nullptr;
|
||||
|
||||
#if QT_VERSION < QT_VERSION_CHECK(5, 10, 0)
|
||||
if (m_QmlEngine != nullptr) {
|
||||
// Dynamic retranslation is not supported until Qt 5.10
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
QTranslator* newTranslator = new QTranslator();
|
||||
QString languageSuffix = getSuffixFromLanguage(language);
|
||||
|
||||
// Remove the old translator, even if we can't load a new one.
|
||||
// Otherwise we'll be stuck with the old translated values instead
|
||||
// of defaulting to English.
|
||||
if (translator != nullptr) {
|
||||
QCoreApplication::removeTranslator(translator);
|
||||
delete translator;
|
||||
translator = nullptr;
|
||||
}
|
||||
|
||||
if (newTranslator->load(QString(":/languages/qml_") + languageSuffix)) {
|
||||
qInfo() << "Successfully loaded translation for " << languageSuffix;
|
||||
|
||||
translator = newTranslator;
|
||||
QCoreApplication::installTranslator(translator);
|
||||
}
|
||||
else {
|
||||
qInfo() << "No translation available for " << languageSuffix;
|
||||
delete newTranslator;
|
||||
}
|
||||
|
||||
if (m_QmlEngine != nullptr) {
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
|
||||
// This is a dynamic retranslation from the settings page.
|
||||
// We have to kick the QML engine into reloading our text.
|
||||
m_QmlEngine->retranslate();
|
||||
#else
|
||||
// Unreachable below Qt 5.10 due to the check above
|
||||
Q_ASSERT(false);
|
||||
#endif
|
||||
}
|
||||
else {
|
||||
// This is a translation from a non-QML context, which means
|
||||
// it is probably app startup. There's nothing to refresh.
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
QString StreamingPreferences::getSuffixFromLanguage(StreamingPreferences::Language lang)
|
||||
{
|
||||
switch (lang)
|
||||
{
|
||||
case LANG_DE:
|
||||
return "de";
|
||||
case LANG_EN:
|
||||
return "en";
|
||||
case LANG_FR:
|
||||
return "fr";
|
||||
case LANG_ZH_CN:
|
||||
return "zh_cn";
|
||||
case LANG_AUTO:
|
||||
default:
|
||||
return QLocale::system().name();
|
||||
}
|
||||
}
|
||||
|
||||
void StreamingPreferences::save()
|
||||
@@ -76,134 +210,27 @@ void StreamingPreferences::save()
|
||||
settings.setValue(SER_UNSUPPORTEDFPS, unsupportedFps);
|
||||
settings.setValue(SER_MDNS, enableMdns);
|
||||
settings.setValue(SER_QUITAPPAFTER, quitAppAfter);
|
||||
settings.setValue(SER_MOUSEACCELERATION, mouseAcceleration);
|
||||
settings.setValue(SER_STARTWINDOWED, startWindowed);
|
||||
settings.setValue(SER_ABSMOUSEMODE, absoluteMouseMode);
|
||||
settings.setValue(SER_ABSTOUCHMODE, absoluteTouchMode);
|
||||
settings.setValue(SER_FRAMEPACING, framePacing);
|
||||
settings.setValue(SER_CONNWARNINGS, connectionWarnings);
|
||||
settings.setValue(SER_RICHPRESENCE, richPresence);
|
||||
settings.setValue(SER_GAMEPADMOUSE, gamepadMouse);
|
||||
settings.setValue(SER_PACKETSIZE, packetSize);
|
||||
settings.setValue(SER_DETECTNETBLOCKING, detectNetworkBlocking);
|
||||
settings.setValue(SER_AUDIOCFG, static_cast<int>(audioConfig));
|
||||
settings.setValue(SER_VIDEOCFG, static_cast<int>(videoCodecConfig));
|
||||
settings.setValue(SER_VIDEODEC, static_cast<int>(videoDecoderSelection));
|
||||
settings.setValue(SER_WINDOWMODE, static_cast<int>(windowMode));
|
||||
}
|
||||
|
||||
bool StreamingPreferences::hasAnyHardwareAcceleration()
|
||||
{
|
||||
// Always use VDS_AUTO to avoid spamming the user with warnings
|
||||
// if they've forced software decoding.
|
||||
return Session::isHardwareDecodeAvailable(VDS_AUTO,
|
||||
VIDEO_FORMAT_H264,
|
||||
1920, 1080, 60);
|
||||
}
|
||||
|
||||
bool StreamingPreferences::isRunningWayland()
|
||||
{
|
||||
return qgetenv("XDG_SESSION_TYPE") == QByteArray("wayland");
|
||||
}
|
||||
|
||||
bool StreamingPreferences::isWow64()
|
||||
{
|
||||
#ifdef Q_OS_WIN32
|
||||
return QSysInfo::currentCpuArchitecture() != QSysInfo::buildCpuArchitecture();
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
QString StreamingPreferences::getUnmappedGamepads()
|
||||
{
|
||||
return SdlInputHandler::getUnmappedGamepads();
|
||||
}
|
||||
|
||||
int StreamingPreferences::getMaximumStreamingFrameRate()
|
||||
{
|
||||
// Never let the maximum drop below 60 FPS
|
||||
int maxFrameRate = 60;
|
||||
|
||||
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s",
|
||||
SDL_GetError());
|
||||
return maxFrameRate;
|
||||
}
|
||||
|
||||
SDL_DisplayMode mode, bestMode, desktopMode;
|
||||
for (int displayIndex = 0; displayIndex < SDL_GetNumVideoDisplays(); displayIndex++) {
|
||||
// Get the native desktop resolution
|
||||
if (StreamUtils::getRealDesktopMode(displayIndex, &desktopMode)) {
|
||||
|
||||
// Start at desktop mode and work our way up
|
||||
bestMode = desktopMode;
|
||||
for (int i = 0; i < SDL_GetNumDisplayModes(displayIndex); i++) {
|
||||
if (SDL_GetDisplayMode(displayIndex, i, &mode) == 0) {
|
||||
if (mode.w == desktopMode.w && mode.h == desktopMode.h) {
|
||||
if (mode.refresh_rate > bestMode.refresh_rate) {
|
||||
bestMode = mode;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
maxFrameRate = qMax(maxFrameRate, bestMode.refresh_rate);
|
||||
}
|
||||
}
|
||||
|
||||
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||
|
||||
return maxFrameRate;
|
||||
}
|
||||
|
||||
QRect StreamingPreferences::getDesktopResolution(int displayIndex)
|
||||
{
|
||||
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s",
|
||||
SDL_GetError());
|
||||
return QRect();
|
||||
}
|
||||
|
||||
if (displayIndex >= SDL_GetNumVideoDisplays()) {
|
||||
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||
return QRect();
|
||||
}
|
||||
|
||||
SDL_DisplayMode mode;
|
||||
int err = SDL_GetDesktopDisplayMode(displayIndex, &mode);
|
||||
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||
|
||||
if (err == 0) {
|
||||
return QRect(0, 0, mode.w, mode.h);
|
||||
}
|
||||
else {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"SDL_GetDesktopDisplayMode() failed: %s",
|
||||
SDL_GetError());
|
||||
return QRect();
|
||||
}
|
||||
}
|
||||
|
||||
QRect StreamingPreferences::getNativeResolution(int displayIndex)
|
||||
{
|
||||
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s",
|
||||
SDL_GetError());
|
||||
return QRect();
|
||||
}
|
||||
|
||||
if (displayIndex >= SDL_GetNumVideoDisplays()) {
|
||||
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||
return QRect();
|
||||
}
|
||||
|
||||
SDL_DisplayMode mode;
|
||||
bool success = StreamUtils::getRealDesktopMode(displayIndex, &mode);
|
||||
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||
|
||||
if (success) {
|
||||
return QRect(0, 0, mode.w, mode.h);
|
||||
}
|
||||
else {
|
||||
return QRect();
|
||||
}
|
||||
settings.setValue(SER_UIDISPLAYMODE, static_cast<int>(uiDisplayMode));
|
||||
settings.setValue(SER_LANGUAGE, static_cast<int>(language));
|
||||
settings.setValue(SER_DEFAULTVER, CURRENT_DEFAULT_VER);
|
||||
settings.setValue(SER_SWAPMOUSEBUTTONS, swapMouseButtons);
|
||||
settings.setValue(SER_MUTEONFOCUSLOSS, muteOnFocusLoss);
|
||||
settings.setValue(SER_BACKGROUNDGAMEPAD, backgroundGamepad);
|
||||
settings.setValue(SER_REVERSESCROLL, reverseScrollDirection);
|
||||
settings.setValue(SER_SWAPFACEBUTTONS, swapFaceButtons);
|
||||
settings.setValue(SER_CAPTURESYSKEYS, captureSysKeysMode);
|
||||
}
|
||||
|
||||
int StreamingPreferences::getDefaultBitrate(int width, int height, int fps)
|
||||
@@ -213,8 +240,14 @@ int StreamingPreferences::getDefaultBitrate(int width, int height, int fps)
|
||||
// we don't want to bump those 16:10 resolutions up to the
|
||||
// next 16:9 slot.
|
||||
|
||||
if (width * height <= 640 * 360) {
|
||||
return static_cast<int>(1000 * (fps / 30.0));
|
||||
}
|
||||
else if (width * height <= 854 * 480) {
|
||||
return static_cast<int>(1500 * (fps / 30.0));
|
||||
}
|
||||
// This covers 1280x720 and 1280x800 too
|
||||
if (width * height <= 1366 * 768) {
|
||||
else if (width * height <= 1366 * 768) {
|
||||
return static_cast<int>(5000 * (fps / 30.0));
|
||||
}
|
||||
else if (width * height <= 1920 * 1200) {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <QObject>
|
||||
#include <QRect>
|
||||
#include <QQmlEngine>
|
||||
|
||||
class StreamingPreferences : public QObject
|
||||
{
|
||||
@@ -9,32 +10,20 @@ class StreamingPreferences : public QObject
|
||||
|
||||
public:
|
||||
StreamingPreferences(QObject *parent = nullptr);
|
||||
StreamingPreferences(QQmlEngine *qmlEngine, QObject *parent = nullptr);
|
||||
|
||||
Q_INVOKABLE static int
|
||||
getDefaultBitrate(int width, int height, int fps);
|
||||
|
||||
Q_INVOKABLE void save();
|
||||
|
||||
Q_INVOKABLE static bool hasAnyHardwareAcceleration();
|
||||
|
||||
Q_INVOKABLE static bool isRunningWayland();
|
||||
|
||||
Q_INVOKABLE static bool isWow64();
|
||||
|
||||
Q_INVOKABLE static int getMaximumStreamingFrameRate();
|
||||
|
||||
Q_INVOKABLE QRect getDesktopResolution(int displayIndex);
|
||||
|
||||
Q_INVOKABLE QRect getNativeResolution(int displayIndex);
|
||||
|
||||
Q_INVOKABLE QString getUnmappedGamepads();
|
||||
|
||||
void reload();
|
||||
|
||||
enum AudioConfig
|
||||
{
|
||||
AC_STEREO,
|
||||
AC_51_SURROUND
|
||||
AC_51_SURROUND,
|
||||
AC_71_SURROUND
|
||||
};
|
||||
Q_ENUM(AudioConfig)
|
||||
|
||||
@@ -63,6 +52,35 @@ public:
|
||||
};
|
||||
Q_ENUM(WindowMode)
|
||||
|
||||
enum UIDisplayMode
|
||||
{
|
||||
UI_WINDOWED,
|
||||
UI_MAXIMIZED,
|
||||
UI_FULLSCREEN
|
||||
};
|
||||
Q_ENUM(UIDisplayMode)
|
||||
|
||||
// New entries must go at the end of the enum
|
||||
// to avoid renumbering existing entries (which
|
||||
// would affect existing user preferences).
|
||||
enum Language
|
||||
{
|
||||
LANG_AUTO,
|
||||
LANG_EN,
|
||||
LANG_FR,
|
||||
LANG_ZH_CN,
|
||||
LANG_DE
|
||||
};
|
||||
Q_ENUM(Language);
|
||||
|
||||
enum CaptureSysKeysMode
|
||||
{
|
||||
CSK_OFF,
|
||||
CSK_FULLSCREEN,
|
||||
CSK_ALWAYS,
|
||||
};
|
||||
Q_ENUM(CaptureSysKeysMode);
|
||||
|
||||
Q_PROPERTY(int width MEMBER width NOTIFY displayModeChanged)
|
||||
Q_PROPERTY(int height MEMBER height NOTIFY displayModeChanged)
|
||||
Q_PROPERTY(int fps MEMBER fps NOTIFY displayModeChanged)
|
||||
@@ -74,13 +92,28 @@ public:
|
||||
Q_PROPERTY(bool unsupportedFps MEMBER unsupportedFps NOTIFY unsupportedFpsChanged)
|
||||
Q_PROPERTY(bool enableMdns MEMBER enableMdns NOTIFY enableMdnsChanged)
|
||||
Q_PROPERTY(bool quitAppAfter MEMBER quitAppAfter NOTIFY quitAppAfterChanged)
|
||||
Q_PROPERTY(bool mouseAcceleration MEMBER mouseAcceleration NOTIFY mouseAccelerationChanged)
|
||||
Q_PROPERTY(bool startWindowed MEMBER startWindowed NOTIFY startWindowedChanged)
|
||||
Q_PROPERTY(bool absoluteMouseMode MEMBER absoluteMouseMode NOTIFY absoluteMouseModeChanged)
|
||||
Q_PROPERTY(bool absoluteTouchMode MEMBER absoluteTouchMode NOTIFY absoluteTouchModeChanged)
|
||||
Q_PROPERTY(bool framePacing MEMBER framePacing NOTIFY framePacingChanged)
|
||||
Q_PROPERTY(bool connectionWarnings MEMBER connectionWarnings NOTIFY connectionWarningsChanged)
|
||||
Q_PROPERTY(bool richPresence MEMBER richPresence NOTIFY richPresenceChanged)
|
||||
Q_PROPERTY(bool gamepadMouse MEMBER gamepadMouse NOTIFY gamepadMouseChanged)
|
||||
Q_PROPERTY(bool detectNetworkBlocking MEMBER detectNetworkBlocking NOTIFY detectNetworkBlockingChanged);
|
||||
Q_PROPERTY(AudioConfig audioConfig MEMBER audioConfig NOTIFY audioConfigChanged)
|
||||
Q_PROPERTY(VideoCodecConfig videoCodecConfig MEMBER videoCodecConfig NOTIFY videoCodecConfigChanged)
|
||||
Q_PROPERTY(VideoDecoderSelection videoDecoderSelection MEMBER videoDecoderSelection NOTIFY videoDecoderSelectionChanged)
|
||||
Q_PROPERTY(WindowMode windowMode MEMBER windowMode NOTIFY windowModeChanged)
|
||||
Q_PROPERTY(WindowMode recommendedFullScreenMode MEMBER recommendedFullScreenMode CONSTANT)
|
||||
Q_PROPERTY(UIDisplayMode uiDisplayMode MEMBER uiDisplayMode NOTIFY uiDisplayModeChanged)
|
||||
Q_PROPERTY(bool swapMouseButtons MEMBER swapMouseButtons NOTIFY mouseButtonsChanged)
|
||||
Q_PROPERTY(bool muteOnFocusLoss MEMBER muteOnFocusLoss NOTIFY muteOnFocusLossChanged)
|
||||
Q_PROPERTY(bool backgroundGamepad MEMBER backgroundGamepad NOTIFY backgroundGamepadChanged)
|
||||
Q_PROPERTY(bool reverseScrollDirection MEMBER reverseScrollDirection NOTIFY reverseScrollDirectionChanged)
|
||||
Q_PROPERTY(bool swapFaceButtons MEMBER swapFaceButtons NOTIFY swapFaceButtonsChanged)
|
||||
Q_PROPERTY(CaptureSysKeysMode captureSysKeysMode MEMBER captureSysKeysMode NOTIFY captureSysKeysModeChanged)
|
||||
Q_PROPERTY(Language language MEMBER language NOTIFY languageChanged);
|
||||
|
||||
Q_INVOKABLE bool retranslate();
|
||||
|
||||
// Directly accessible members for preferences
|
||||
int width;
|
||||
@@ -94,13 +127,27 @@ public:
|
||||
bool unsupportedFps;
|
||||
bool enableMdns;
|
||||
bool quitAppAfter;
|
||||
bool mouseAcceleration;
|
||||
bool startWindowed;
|
||||
bool absoluteMouseMode;
|
||||
bool absoluteTouchMode;
|
||||
bool framePacing;
|
||||
bool connectionWarnings;
|
||||
bool richPresence;
|
||||
bool gamepadMouse;
|
||||
bool detectNetworkBlocking;
|
||||
bool swapMouseButtons;
|
||||
bool muteOnFocusLoss;
|
||||
bool backgroundGamepad;
|
||||
bool reverseScrollDirection;
|
||||
bool swapFaceButtons;
|
||||
int packetSize;
|
||||
AudioConfig audioConfig;
|
||||
VideoCodecConfig videoCodecConfig;
|
||||
VideoDecoderSelection videoDecoderSelection;
|
||||
WindowMode windowMode;
|
||||
WindowMode recommendedFullScreenMode;
|
||||
UIDisplayMode uiDisplayMode;
|
||||
Language language;
|
||||
CaptureSysKeysMode captureSysKeysMode;
|
||||
|
||||
signals:
|
||||
void displayModeChanged();
|
||||
@@ -112,12 +159,29 @@ signals:
|
||||
void unsupportedFpsChanged();
|
||||
void enableMdnsChanged();
|
||||
void quitAppAfterChanged();
|
||||
void mouseAccelerationChanged();
|
||||
void absoluteMouseModeChanged();
|
||||
void absoluteTouchModeChanged();
|
||||
void audioConfigChanged();
|
||||
void videoCodecConfigChanged();
|
||||
void videoDecoderSelectionChanged();
|
||||
void uiDisplayModeChanged();
|
||||
void windowModeChanged();
|
||||
void startWindowedChanged();
|
||||
void framePacingChanged();
|
||||
void connectionWarningsChanged();
|
||||
void richPresenceChanged();
|
||||
void gamepadMouseChanged();
|
||||
void detectNetworkBlockingChanged();
|
||||
void mouseButtonsChanged();
|
||||
void muteOnFocusLossChanged();
|
||||
void backgroundGamepadChanged();
|
||||
void reverseScrollDirectionChanged();
|
||||
void swapFaceButtonsChanged();
|
||||
void captureSysKeysModeChanged();
|
||||
void languageChanged();
|
||||
|
||||
private:
|
||||
QString getSuffixFromLanguage(Language lang);
|
||||
|
||||
QQmlEngine* m_QmlEngine;
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
#version 300 es
|
||||
#extension GL_OES_EGL_image_external : require
|
||||
precision mediump float;
|
||||
out vec4 FragColor;
|
||||
|
||||
in vec2 vTextCoord;
|
||||
|
||||
uniform mat3 yuvmat;
|
||||
uniform vec3 offset;
|
||||
uniform samplerExternalOES plane1;
|
||||
uniform samplerExternalOES plane2;
|
||||
|
||||
void main() {
|
||||
vec3 YCbCr = vec3(
|
||||
texture2D(plane1, vTextCoord)[0],
|
||||
texture2D(plane2, vTextCoord).xy
|
||||
);
|
||||
|
||||
YCbCr -= offset;
|
||||
FragColor = vec4(clamp(yuvmat * YCbCr, 0.0, 1.0), 1.0f);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
#version 300 es
|
||||
|
||||
layout (location = 0) in vec2 aPosition; // 2D: X,Y
|
||||
layout (location = 1) in vec2 aTexCoord;
|
||||
out vec2 vTextCoord;
|
||||
|
||||
void main() {
|
||||
vTextCoord = aTexCoord;
|
||||
gl_Position = vec4(aPosition, 0, 1);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
precision mediump float;
|
||||
uniform sampler2D uTexture;
|
||||
varying vec2 vTexCoord;
|
||||
|
||||
void main() {
|
||||
vec4 abgr = texture2D(uTexture, vTexCoord);
|
||||
|
||||
gl_FragColor = abgr;
|
||||
gl_FragColor.r = abgr.b;
|
||||
gl_FragColor.b = abgr.r;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
attribute vec2 aPosition; // 2D: X,Y
|
||||
attribute vec2 aTexCoord;
|
||||
varying vec2 vTexCoord;
|
||||
|
||||
void main() {
|
||||
vTexCoord = aTexCoord;
|
||||
gl_Position = vec4(aPosition, 0, 1);
|
||||
}
|
||||
@@ -5,55 +5,112 @@
|
||||
#include "renderers/soundioaudiorenderer.h"
|
||||
#endif
|
||||
|
||||
#ifdef HAVE_SLAUDIO
|
||||
#include "renderers/slaud.h"
|
||||
#endif
|
||||
|
||||
#include "renderers/sdl.h"
|
||||
|
||||
#include <Limelight.h>
|
||||
|
||||
IAudioRenderer* Session::createAudioRenderer()
|
||||
{
|
||||
#ifdef HAVE_SOUNDIO
|
||||
if (qgetenv("ML_AUDIO") == "SDL") {
|
||||
return new SdlAudioRenderer();
|
||||
}
|
||||
|
||||
return new SoundIoAudioRenderer();
|
||||
#else
|
||||
return new SdlAudioRenderer();
|
||||
#endif
|
||||
#define TRY_INIT_RENDERER(renderer, opusConfig) \
|
||||
{ \
|
||||
IAudioRenderer* __renderer = new renderer(); \
|
||||
if (__renderer->prepareForPlayback(opusConfig)) \
|
||||
return __renderer; \
|
||||
delete __renderer; \
|
||||
}
|
||||
|
||||
bool Session::testAudio(int audioConfiguration)
|
||||
IAudioRenderer* Session::createAudioRenderer(const POPUS_MULTISTREAM_CONFIGURATION opusConfig)
|
||||
{
|
||||
IAudioRenderer* audioRenderer;
|
||||
|
||||
audioRenderer = createAudioRenderer();
|
||||
if (audioRenderer == nullptr) {
|
||||
return false;
|
||||
// Handle explicit ML_AUDIO setting and fail if the requested backend fails
|
||||
QString mlAudio = qgetenv("ML_AUDIO").toLower();
|
||||
if (mlAudio == "sdl") {
|
||||
TRY_INIT_RENDERER(SdlAudioRenderer, opusConfig)
|
||||
return nullptr;
|
||||
}
|
||||
#ifdef HAVE_SOUNDIO
|
||||
else if (mlAudio == "libsoundio") {
|
||||
TRY_INIT_RENDERER(SoundIoAudioRenderer, opusConfig)
|
||||
return nullptr;
|
||||
}
|
||||
#endif
|
||||
#if defined(HAVE_SLAUDIO)
|
||||
else if (mlAudio == "slaudio") {
|
||||
TRY_INIT_RENDERER(SLAudioRenderer, opusConfig)
|
||||
return nullptr;
|
||||
}
|
||||
#endif
|
||||
else if (!mlAudio.isEmpty()) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Unknown audio backend: %s",
|
||||
SDL_getenv("ML_AUDIO"));
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// -------------- Automatic backend selection below this line ---------------
|
||||
|
||||
#if defined(HAVE_SLAUDIO)
|
||||
// Steam Link should always have SLAudio
|
||||
TRY_INIT_RENDERER(SLAudioRenderer, opusConfig)
|
||||
#endif
|
||||
|
||||
#if !defined(Q_OS_WIN32) && !defined(Q_OS_DARWIN)
|
||||
// Linux defaults to SDL due to persistent glitching issues under libsoundio.
|
||||
// Platforms that libsoundio doesn't support also default to SDL.
|
||||
TRY_INIT_RENDERER(SdlAudioRenderer, opusConfig)
|
||||
#ifdef HAVE_SOUNDIO
|
||||
TRY_INIT_RENDERER(SoundIoAudioRenderer, opusConfig)
|
||||
#endif
|
||||
#else
|
||||
// Windows and macOS default to libsoundio and fall back to SDL
|
||||
#ifdef HAVE_SOUNDIO
|
||||
TRY_INIT_RENDERER(SoundIoAudioRenderer, opusConfig)
|
||||
#endif
|
||||
TRY_INIT_RENDERER(SdlAudioRenderer, opusConfig)
|
||||
#endif
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
int Session::getAudioRendererCapabilities(int audioConfiguration)
|
||||
{
|
||||
// Build a fake OPUS_MULTISTREAM_CONFIGURATION to give
|
||||
// the renderer the channel count and sample rate.
|
||||
OPUS_MULTISTREAM_CONFIGURATION opusConfig = {};
|
||||
opusConfig.sampleRate = 48000;
|
||||
opusConfig.samplesPerFrame = 240;
|
||||
opusConfig.channelCount = CHANNEL_COUNT_FROM_AUDIO_CONFIGURATION(audioConfiguration);
|
||||
|
||||
switch (audioConfiguration)
|
||||
{
|
||||
case AUDIO_CONFIGURATION_STEREO:
|
||||
opusConfig.channelCount = 2;
|
||||
break;
|
||||
case AUDIO_CONFIGURATION_51_SURROUND:
|
||||
opusConfig.channelCount = 6;
|
||||
break;
|
||||
default:
|
||||
SDL_assert(false);
|
||||
return false;
|
||||
IAudioRenderer* audioRenderer = createAudioRenderer(&opusConfig);
|
||||
if (audioRenderer == nullptr) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool ret = audioRenderer->prepareForPlayback(&opusConfig);
|
||||
int caps = audioRenderer->getCapabilities();
|
||||
|
||||
delete audioRenderer;
|
||||
|
||||
return ret;
|
||||
return caps;
|
||||
}
|
||||
|
||||
bool Session::testAudio(int audioConfiguration)
|
||||
{
|
||||
// Build a fake OPUS_MULTISTREAM_CONFIGURATION to give
|
||||
// the renderer the channel count and sample rate.
|
||||
OPUS_MULTISTREAM_CONFIGURATION opusConfig = {};
|
||||
opusConfig.sampleRate = 48000;
|
||||
opusConfig.samplesPerFrame = 240;
|
||||
opusConfig.channelCount = CHANNEL_COUNT_FROM_AUDIO_CONFIGURATION(audioConfiguration);
|
||||
|
||||
IAudioRenderer* audioRenderer = createAudioRenderer(&opusConfig);
|
||||
if (audioRenderer == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
delete audioRenderer;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
int Session::arInit(int /* audioConfiguration */,
|
||||
@@ -64,30 +121,34 @@ int Session::arInit(int /* audioConfiguration */,
|
||||
|
||||
SDL_memcpy(&s_ActiveSession->m_AudioConfig, opusConfig, sizeof(*opusConfig));
|
||||
|
||||
s_ActiveSession->m_AudioRenderer = s_ActiveSession->createAudioRenderer(&s_ActiveSession->m_AudioConfig);
|
||||
if (s_ActiveSession->m_AudioRenderer == nullptr) {
|
||||
return -2;
|
||||
}
|
||||
|
||||
// Allow the chosen renderer to remap Opus channels as needed to ensure proper output
|
||||
s_ActiveSession->m_AudioRenderer->remapChannels(&s_ActiveSession->m_AudioConfig);
|
||||
|
||||
// Create the Opus decoder with the renderer's preferred channel mapping
|
||||
s_ActiveSession->m_OpusDecoder =
|
||||
opus_multistream_decoder_create(opusConfig->sampleRate,
|
||||
opusConfig->channelCount,
|
||||
opusConfig->streams,
|
||||
opusConfig->coupledStreams,
|
||||
opusConfig->mapping,
|
||||
opus_multistream_decoder_create(s_ActiveSession->m_AudioConfig.sampleRate,
|
||||
s_ActiveSession->m_AudioConfig.channelCount,
|
||||
s_ActiveSession->m_AudioConfig.streams,
|
||||
s_ActiveSession->m_AudioConfig.coupledStreams,
|
||||
s_ActiveSession->m_AudioConfig.mapping,
|
||||
&error);
|
||||
if (s_ActiveSession->m_OpusDecoder == NULL) {
|
||||
delete s_ActiveSession->m_AudioRenderer;
|
||||
s_ActiveSession->m_AudioRenderer = nullptr;
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Failed to create decoder: %d",
|
||||
error);
|
||||
return -1;
|
||||
}
|
||||
|
||||
s_ActiveSession->m_AudioRenderer = s_ActiveSession->createAudioRenderer();
|
||||
if (!s_ActiveSession->m_AudioRenderer->prepareForPlayback(opusConfig)) {
|
||||
delete s_ActiveSession->m_AudioRenderer;
|
||||
opus_multistream_decoder_destroy(s_ActiveSession->m_OpusDecoder);
|
||||
return -2;
|
||||
}
|
||||
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Audio stream has %d channels",
|
||||
opusConfig->channelCount);
|
||||
s_ActiveSession->m_AudioConfig.channelCount);
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -105,8 +166,11 @@ void Session::arDecodeAndPlaySample(char* sampleData, int sampleLength)
|
||||
{
|
||||
int samplesDecoded;
|
||||
|
||||
// Set this thread to high priority to reduce
|
||||
// the chance of missing our sample delivery time
|
||||
#ifndef STEAM_LINK
|
||||
// Set this thread to high priority to reduce the chance of missing
|
||||
// our sample delivery time. On Steam Link, this causes starvation
|
||||
// of other threads due to severely restricted CPU time available,
|
||||
// so we will skip it on that platform.
|
||||
if (s_ActiveSession->m_AudioSampleCount == 0) {
|
||||
if (SDL_SetThreadPriority(SDL_THREAD_PRIORITY_HIGH) < 0) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
@@ -114,6 +178,7 @@ void Session::arDecodeAndPlaySample(char* sampleData, int sampleLength)
|
||||
SDL_GetError());
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// See if we need to drop this sample
|
||||
if (s_ActiveSession->m_DropAudioEndTime != 0) {
|
||||
@@ -132,26 +197,40 @@ void Session::arDecodeAndPlaySample(char* sampleData, int sampleLength)
|
||||
|
||||
s_ActiveSession->m_AudioSampleCount++;
|
||||
|
||||
// If audio is muted, don't decode or play the audio
|
||||
if (s_ActiveSession->m_AudioMuted) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (s_ActiveSession->m_AudioRenderer != nullptr) {
|
||||
int desiredSize = sizeof(short) * s_ActiveSession->m_AudioConfig.samplesPerFrame * s_ActiveSession->m_AudioConfig.channelCount;
|
||||
void* buffer = s_ActiveSession->m_AudioRenderer->getAudioBuffer(&desiredSize);
|
||||
if (buffer == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
samplesDecoded = opus_multistream_decode(s_ActiveSession->m_OpusDecoder,
|
||||
(unsigned char*)sampleData,
|
||||
sampleLength,
|
||||
s_ActiveSession->m_OpusDecodeBuffer,
|
||||
SAMPLES_PER_FRAME,
|
||||
(short*)buffer,
|
||||
desiredSize / sizeof(short) / s_ActiveSession->m_AudioConfig.channelCount,
|
||||
0);
|
||||
|
||||
// Update desiredSize with the number of bytes actually populated by the decoding operation
|
||||
if (samplesDecoded > 0) {
|
||||
if (!s_ActiveSession->m_AudioRenderer->submitAudio(s_ActiveSession->m_OpusDecodeBuffer,
|
||||
static_cast<int>(
|
||||
sizeof(short) *
|
||||
samplesDecoded *
|
||||
s_ActiveSession->m_AudioConfig.channelCount))) {
|
||||
SDL_assert(desiredSize >= (int)(sizeof(short) * samplesDecoded * s_ActiveSession->m_AudioConfig.channelCount));
|
||||
desiredSize = sizeof(short) * samplesDecoded * s_ActiveSession->m_AudioConfig.channelCount;
|
||||
}
|
||||
else {
|
||||
desiredSize = 0;
|
||||
}
|
||||
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Reinitializing audio renderer after failure");
|
||||
if (!s_ActiveSession->m_AudioRenderer->submitAudio(desiredSize)) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Reinitializing audio renderer after failure");
|
||||
|
||||
delete s_ActiveSession->m_AudioRenderer;
|
||||
s_ActiveSession->m_AudioRenderer = nullptr;
|
||||
}
|
||||
delete s_ActiveSession->m_AudioRenderer;
|
||||
s_ActiveSession->m_AudioRenderer = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,11 +244,7 @@ void Session::arDecodeAndPlaySample(char* sampleData, int sampleLength)
|
||||
// so we return to real-time playback and don't accumulate latency.
|
||||
Uint32 audioReinitStartTime = SDL_GetTicks();
|
||||
|
||||
s_ActiveSession->m_AudioRenderer = s_ActiveSession->createAudioRenderer();
|
||||
if (!s_ActiveSession->m_AudioRenderer->prepareForPlayback(&s_ActiveSession->m_AudioConfig)) {
|
||||
delete s_ActiveSession->m_AudioRenderer;
|
||||
s_ActiveSession->m_AudioRenderer = nullptr;
|
||||
}
|
||||
s_ActiveSession->m_AudioRenderer = s_ActiveSession->createAudioRenderer(&s_ActiveSession->m_AudioConfig);
|
||||
|
||||
Uint32 audioReinitStopTime = SDL_GetTicks();
|
||||
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
#include "qtaud.h"
|
||||
|
||||
#include <QDebug>
|
||||
|
||||
QtAudioRenderer::QtAudioRenderer()
|
||||
: m_AudioOutput(nullptr),
|
||||
m_OutputDevice(nullptr)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
QtAudioRenderer::~QtAudioRenderer()
|
||||
{
|
||||
delete m_AudioOutput;
|
||||
}
|
||||
|
||||
bool QtAudioRenderer::prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig)
|
||||
{
|
||||
QAudioFormat format;
|
||||
|
||||
// testAudio() assumes this sample rate
|
||||
Q_ASSERT(opusConfig->sampleRate == 48000);
|
||||
|
||||
format.setSampleRate(opusConfig->sampleRate);
|
||||
format.setChannelCount(opusConfig->channelCount);
|
||||
format.setSampleSize(16);
|
||||
format.setSampleType(QAudioFormat::SignedInt);
|
||||
format.setCodec("audio/pcm");
|
||||
|
||||
m_AudioOutput = new QAudioOutput(format);
|
||||
|
||||
// Buffer 40 ms of audio data
|
||||
m_AudioOutput->setBufferSize(format.bytesForDuration(40000));
|
||||
|
||||
m_OutputDevice = m_AudioOutput->start();
|
||||
if (m_OutputDevice == nullptr) {
|
||||
qWarning() << "Audio start failed:" << m_AudioOutput->error();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check what buffer size was really used after starting
|
||||
qInfo() << "Audio stream buffer:" << m_AudioOutput->bufferSize() << "bytes";
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void QtAudioRenderer::submitAudio(short* audioBuffer, int audioSize)
|
||||
{
|
||||
m_OutputDevice->write((const char *)audioBuffer, audioSize);
|
||||
}
|
||||
|
||||
bool QtAudioRenderer::testAudio(int audioConfiguration)
|
||||
{
|
||||
QAudioFormat format;
|
||||
|
||||
format.setSampleRate(48000);
|
||||
format.setSampleSize(16);
|
||||
format.setSampleType(QAudioFormat::SignedInt);
|
||||
format.setCodec("audio/pcm");
|
||||
|
||||
switch (audioConfiguration) {
|
||||
case AUDIO_CONFIGURATION_STEREO:
|
||||
format.setChannelCount(2);
|
||||
break;
|
||||
case AUDIO_CONFIGURATION_51_SURROUND:
|
||||
format.setChannelCount(6);
|
||||
break;
|
||||
default:
|
||||
Q_ASSERT(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (QAudioDeviceInfo::defaultOutputDevice().isFormatSupported(format)) {
|
||||
qInfo() << "Audio test successful with" << format.channelCount() << "channels";
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
qWarning() << "Audio test failed with" << format.channelCount() << "channels";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
int QtAudioRenderer::detectAudioConfiguration()
|
||||
{
|
||||
int preferredChannelCount = QAudioDeviceInfo::defaultOutputDevice().preferredFormat().channelCount();
|
||||
|
||||
qInfo() << "Audio output device prefers" << preferredChannelCount << "channel configuration";
|
||||
|
||||
// We can better downmix 5.1 to quad than we can upmix stereo
|
||||
if (preferredChannelCount > 2) {
|
||||
return AUDIO_CONFIGURATION_51_SURROUND;
|
||||
}
|
||||
else {
|
||||
return AUDIO_CONFIGURATION_STEREO;
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "renderer.h"
|
||||
#include <QAudioOutput>
|
||||
|
||||
class QtAudioRenderer : public IAudioRenderer
|
||||
{
|
||||
public:
|
||||
QtAudioRenderer();
|
||||
|
||||
virtual ~QtAudioRenderer();
|
||||
|
||||
virtual bool prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig);
|
||||
|
||||
virtual void submitAudio(short* audioBuffer, int audioSize);
|
||||
|
||||
virtual bool testAudio(int audioConfiguration);
|
||||
|
||||
virtual int detectAudioConfiguration();
|
||||
|
||||
private:
|
||||
QAudioOutput* m_AudioOutput;
|
||||
QIODevice* m_OutputDevice;
|
||||
};
|
||||
@@ -2,9 +2,6 @@
|
||||
|
||||
#include <Limelight.h>
|
||||
|
||||
#define MAX_CHANNELS 6
|
||||
#define SAMPLES_PER_FRAME 240
|
||||
|
||||
class IAudioRenderer
|
||||
{
|
||||
public:
|
||||
@@ -12,6 +9,20 @@ public:
|
||||
|
||||
virtual bool prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig) = 0;
|
||||
|
||||
virtual void* getAudioBuffer(int* size) = 0;
|
||||
|
||||
// Return false if an unrecoverable error has occurred and the renderer must be reinitialized
|
||||
virtual bool submitAudio(short* audioBuffer, int audioSize) = 0;
|
||||
virtual bool submitAudio(int bytesWritten) = 0;
|
||||
|
||||
virtual int getCapabilities() = 0;
|
||||
|
||||
virtual void remapChannels(POPUS_MULTISTREAM_CONFIGURATION) {
|
||||
// Use default channel mapping:
|
||||
// 0 - Front Left
|
||||
// 1 - Front Right
|
||||
// 2 - Center
|
||||
// 3 - LFE
|
||||
// 4 - Surround Left
|
||||
// 5 - Surround Right
|
||||
}
|
||||
};
|
||||
|
||||
@@ -12,8 +12,14 @@ public:
|
||||
|
||||
virtual bool prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig);
|
||||
|
||||
virtual bool submitAudio(short* audioBuffer, int audioSize);
|
||||
virtual void* getAudioBuffer(int* size);
|
||||
|
||||
virtual bool submitAudio(int bytesWritten);
|
||||
|
||||
virtual int getCapabilities();
|
||||
|
||||
private:
|
||||
SDL_AudioDeviceID m_AudioDevice;
|
||||
void* m_AudioBuffer;
|
||||
int m_FrameSize;
|
||||
};
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
#include <Limelight.h>
|
||||
#include <SDL.h>
|
||||
|
||||
#include <QtGlobal>
|
||||
|
||||
SdlAudioRenderer::SdlAudioRenderer()
|
||||
: m_AudioDevice(0)
|
||||
: m_AudioDevice(0),
|
||||
m_AudioBuffer(nullptr)
|
||||
{
|
||||
SDL_assert(!SDL_WasInit(SDL_INIT_AUDIO));
|
||||
|
||||
if (SDL_InitSubSystem(SDL_INIT_AUDIO) != 0) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"SDL_InitSubSystem(SDL_INIT_AUDIO) failed: %s",
|
||||
@@ -30,21 +30,29 @@ bool SdlAudioRenderer::prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION*
|
||||
// frames contain a non-power of 2 number of samples,
|
||||
// so the slop would require buffering another full frame.
|
||||
// Specifying non-Po2 seems to work for our supported platforms.
|
||||
want.samples = SAMPLES_PER_FRAME;
|
||||
want.samples = opusConfig->samplesPerFrame;
|
||||
|
||||
m_FrameSize = opusConfig->samplesPerFrame * sizeof(short) * opusConfig->channelCount;
|
||||
|
||||
m_AudioDevice = SDL_OpenAudioDevice(NULL, 0, &want, &have, 0);
|
||||
if (m_AudioDevice == 0) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Failed to open audio device: %s",
|
||||
SDL_GetError());
|
||||
SDL_QuitSubSystem(SDL_INIT_AUDIO);
|
||||
return false;
|
||||
}
|
||||
|
||||
m_AudioBuffer = SDL_malloc(m_FrameSize);
|
||||
if (m_AudioBuffer == nullptr) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Failed to allocate audio buffer");
|
||||
return false;
|
||||
}
|
||||
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Desired audio buffer: %u samples (%lu bytes)",
|
||||
"Desired audio buffer: %u samples (%u bytes)",
|
||||
want.samples,
|
||||
want.samples * sizeof(short) * want.channels);
|
||||
want.samples * (Uint32)sizeof(short) * want.channels);
|
||||
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Obtained audio buffer: %u samples (%u bytes)",
|
||||
@@ -65,13 +73,39 @@ SdlAudioRenderer::~SdlAudioRenderer()
|
||||
SDL_CloseAudioDevice(m_AudioDevice);
|
||||
}
|
||||
|
||||
if (m_AudioBuffer != nullptr) {
|
||||
SDL_free(m_AudioBuffer);
|
||||
}
|
||||
|
||||
SDL_QuitSubSystem(SDL_INIT_AUDIO);
|
||||
SDL_assert(!SDL_WasInit(SDL_INIT_AUDIO));
|
||||
}
|
||||
|
||||
bool SdlAudioRenderer::submitAudio(short* audioBuffer, int audioSize)
|
||||
void* SdlAudioRenderer::getAudioBuffer(int*)
|
||||
{
|
||||
if (SDL_QueueAudio(m_AudioDevice, audioBuffer, audioSize) < 0) {
|
||||
return m_AudioBuffer;
|
||||
}
|
||||
|
||||
bool SdlAudioRenderer::submitAudio(int bytesWritten)
|
||||
{
|
||||
if (bytesWritten == 0) {
|
||||
// Nothing to do
|
||||
return true;
|
||||
}
|
||||
|
||||
// Don't queue if there's already more than 30 ms of audio data waiting
|
||||
// in Moonlight's audio queue.
|
||||
if (LiGetPendingAudioDuration() > 30) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Provide backpressure on the queue to ensure too many frames don't build up
|
||||
// in SDL's audio queue.
|
||||
while (SDL_GetQueuedAudioSize(m_AudioDevice) / m_FrameSize > 10) {
|
||||
SDL_Delay(1);
|
||||
}
|
||||
|
||||
if (SDL_QueueAudio(m_AudioDevice, m_AudioBuffer, bytesWritten) < 0) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Failed to queue audio sample: %s",
|
||||
SDL_GetError());
|
||||
@@ -79,3 +113,9 @@ bool SdlAudioRenderer::submitAudio(short* audioBuffer, int audioSize)
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
int SdlAudioRenderer::getCapabilities()
|
||||
{
|
||||
// Direct submit can't be used because we use LiGetPendingAudioDuration()
|
||||
return CAPABILITY_SUPPORTS_ARBITRARY_AUDIO_DURATION;
|
||||
}
|
||||
|
||||