Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
14a6a69dc0 | ||
|
|
3191182009 | ||
|
|
e210dc748f | ||
|
|
724dcb557f | ||
|
|
11d274ff07 | ||
|
|
ae246b1005 | ||
|
|
ca0250280a | ||
|
|
f49d6e15a4 | ||
|
|
7172cfd889 | ||
|
|
89e9652075 | ||
|
|
53098edc85 | ||
|
|
e9968fbab3 | ||
|
|
bc10fd0aba | ||
|
|
76b4922bbe | ||
|
|
8e790c4745 | ||
|
|
c331b180cb | ||
|
|
30e3b02867 | ||
|
|
bedea4a093 | ||
|
|
5372cfba20 | ||
|
|
19dac306d1 | ||
|
|
e9a98a3402 | ||
|
|
7020749987 | ||
|
|
2d62d090df | ||
|
|
e6f59062fa | ||
|
|
0d3092d62c | ||
|
|
af0921c5b2 | ||
|
|
137454beff | ||
|
|
a0cc93b813 | ||
|
|
ff516f63b4 | ||
|
|
68acb78ae6 | ||
|
|
949aaaff2a | ||
|
|
390544cd9a | ||
|
|
766a923f86 | ||
|
|
202ce14afa | ||
|
|
8ff9d8123d | ||
|
|
2715367411 | ||
|
|
44b61d9252 | ||
|
|
565bba6e55 | ||
|
|
2ecfb7af64 | ||
|
|
998d3b2cc1 | ||
|
|
c923d643b9 | ||
|
|
bc39c721e3 | ||
|
|
5096ff6496 | ||
|
|
a11f623b17 | ||
|
|
f7ffb30bc6 | ||
|
|
c7ddb54e67 | ||
|
|
c4108feb15 | ||
|
|
47cad71fcd | ||
|
|
0b56b1685a | ||
|
|
c414e1a168 | ||
|
|
22bebb6bbc | ||
|
|
c3f9742bb6 | ||
|
|
3defccb6e2 | ||
|
|
1197b17509 | ||
|
|
265e28e7c3 | ||
|
|
9c758f1628 | ||
|
|
5f4975ca5c | ||
|
|
e5bcc793e5 | ||
|
|
cd5b0e74e3 | ||
|
|
3162d2c17d | ||
|
|
6a54fb649c | ||
|
|
8031ed7fe1 | ||
|
|
dcbf5affda | ||
|
|
027c8dcd41 | ||
|
|
94b46a2173 | ||
|
|
b66f7f6c8b | ||
|
|
355e4a5c67 | ||
|
|
1e7cb7f13e | ||
|
|
d58837421f | ||
|
|
19d5358b97 | ||
|
|
a6551c320d | ||
|
|
59a73817c8 | ||
|
|
1231f3eeb1 | ||
|
|
cd40c1dacd | ||
|
|
33286a5bde | ||
|
|
b61c35aff9 | ||
|
|
2a3e1ba62e | ||
|
|
d95f3277b2 | ||
|
|
e0a2c8cb8a | ||
|
|
0aa46a57fe | ||
|
|
53e3cd143e | ||
|
|
8381039f97 | ||
|
|
a91c61e5e2 | ||
|
|
f90efd7e38 | ||
|
|
fab142dd38 | ||
|
|
125d546cea | ||
|
|
179f7ecc23 | ||
|
|
0fc30a70ca | ||
|
|
f81509a892 | ||
|
|
fc4488f17f | ||
|
|
2748f3d22e | ||
|
|
e0d42cbe9c | ||
|
|
1780d9417f | ||
|
|
5f182b1e62 | ||
|
|
80cee8367f | ||
|
|
7325d5657d | ||
|
|
72182c7caa | ||
|
|
d7ca3801be | ||
|
|
2ef714f87b | ||
|
|
46940eb15a | ||
|
|
3485983553 | ||
|
|
861ebc151a | ||
|
|
ae5df938b6 | ||
|
|
e224a7f0c7 | ||
|
|
aa4684077d | ||
|
|
193e196c46 | ||
|
|
6e7a42065c | ||
|
|
2c987d2912 | ||
|
|
19f5dc12fb | ||
|
|
38b59aa1a9 | ||
|
|
201b59cd4c | ||
|
|
de3afe27c1 | ||
|
|
7bee8e73f0 | ||
|
|
17e0fbbaf8 | ||
|
|
ec486e89e4 | ||
|
|
ec3b000264 | ||
|
|
90a513dc3d | ||
|
|
32a74e3416 | ||
|
|
1d90bf9230 | ||
|
|
81943b12c0 | ||
|
|
18fa7c2832 | ||
|
|
dd02bf809e | ||
|
|
2f3a1a5d79 | ||
|
|
18654cb011 | ||
|
|
5954f80645 | ||
|
|
3e36357315 | ||
|
|
a06a454ade | ||
|
|
c0fab4b41f | ||
|
|
80b79f3636 | ||
|
|
b97ae25f93 | ||
|
|
4722ea6096 | ||
|
|
3e9781033a | ||
|
|
62b296cf85 | ||
|
|
dcbaa52683 | ||
|
|
1e29f61656 | ||
|
|
89319dc7aa | ||
|
|
5755afc4ea | ||
|
|
d00c434ccc | ||
|
|
4ada59ba48 | ||
|
|
14ccc38f4c | ||
|
|
a8be299bbd | ||
|
|
2e87f5ff85 | ||
|
|
829ebba622 | ||
|
|
81019fa2a8 | ||
|
|
1bc3675548 | ||
|
|
678ec979c9 | ||
|
|
d1a48ae641 | ||
|
|
eded86bc10 | ||
|
|
5a7a49d1d5 | ||
|
|
d3de8049bf | ||
|
|
d09dd90a73 | ||
|
|
78203eb9be | ||
|
|
31c7d4e36f | ||
|
|
d3949806f5 | ||
|
|
28ff15c99e | ||
|
|
bd2c74fe90 | ||
|
|
fb6995db94 | ||
|
|
8c2c4d3282 | ||
|
|
b81392b818 | ||
|
|
ad08440023 | ||
|
|
ec17623400 | ||
|
|
288eca7849 | ||
|
|
b5f202150e | ||
|
|
539bf0cb30 | ||
|
|
9385d62c89 | ||
|
|
897f916552 | ||
|
|
914cf6753e | ||
|
|
392208057e | ||
|
|
8d2cbf806d | ||
|
|
ab5025efbf | ||
|
|
912e51d863 | ||
|
|
d1c4ca5eae | ||
|
|
16bb4a148e | ||
|
|
d7071cb67f | ||
|
|
a13fb3d7c5 | ||
|
|
85558904a4 | ||
|
|
88ed72fdc1 | ||
|
|
586a93d7aa | ||
|
|
a446699c6c | ||
|
|
f1c459fd6c | ||
|
|
752f0a988b | ||
|
|
719b266691 | ||
|
|
dcba5762c7 | ||
|
|
0757717bea | ||
|
|
a9f762aa96 | ||
|
|
db45f46d9e | ||
|
|
f00653964e | ||
|
|
e0154f9314 | ||
|
|
4408a3a354 | ||
|
|
5abf5a157d | ||
|
|
d55d5f60a5 | ||
|
|
feacf2c359 | ||
|
|
72ea1601e9 | ||
|
|
02cfd8d978 | ||
|
|
728377cba3 | ||
|
|
f2283b56b9 | ||
|
|
b515f2a599 | ||
|
|
4354b49496 | ||
|
|
994312c212 | ||
|
|
5f25e0d4e2 | ||
|
|
4bbe571f5f | ||
|
|
c58d21375c | ||
|
|
ed128c558f | ||
|
|
828932d95c | ||
|
|
42ced309d3 | ||
|
|
1fcd306879 | ||
|
|
26c9ad7f06 | ||
|
|
4b7b01ce0a | ||
|
|
6632154906 | ||
|
|
ca67c7ef43 | ||
|
|
c4039a929c | ||
|
|
a1b74e1220 | ||
|
|
01f6188824 | ||
|
|
283f31a734 | ||
|
|
b800ce9ebe | ||
|
|
eb9d54465c | ||
|
|
eb9e1f81d4 | ||
|
|
ddfbd70b55 | ||
|
|
51e9ab83ad | ||
|
|
5ed7e03b2e | ||
|
|
0cb8d5b12f | ||
|
|
b4548c1af2 | ||
|
|
0a396f3112 | ||
|
|
2aa18dc88f | ||
|
|
8d89983eeb | ||
|
|
573c470965 | ||
|
|
daa0c751e2 | ||
|
|
e1babefc80 | ||
|
|
ce99b0e162 | ||
|
|
4c3a23b1bd | ||
|
|
12016af326 | ||
|
|
fc9871b075 | ||
|
|
3cb3c1c6d8 | ||
|
|
8fe3474589 | ||
|
|
dc3c565ec0 | ||
|
|
350c7d7081 | ||
|
|
0428bc8fe3 | ||
|
|
514a8575a2 | ||
|
|
fc11377552 | ||
|
|
fbaa70a2ae | ||
|
|
f30c11ddf4 | ||
|
|
51c4ecc879 | ||
|
|
080421f2fb | ||
|
|
8cc61ca8b8 | ||
|
|
400754c5b3 | ||
|
|
05e82c24f8 | ||
|
|
97c15c7876 | ||
|
|
36e7cc8e89 | ||
|
|
ad2ce4de53 | ||
|
|
2a28f7c4d2 | ||
|
|
0cdbe080a6 | ||
|
|
96fe1a7da1 | ||
|
|
4f73af2920 | ||
|
|
7ca4e49bf5 | ||
|
|
dfa63297fc | ||
|
|
402e8c1897 | ||
|
|
33e814122e | ||
|
|
f28a4df5ee | ||
|
|
417aa2815c | ||
|
|
2fdabe37aa | ||
|
|
d8270dfb6a | ||
|
|
b75f662c41 | ||
|
|
0d9d0845f5 | ||
|
|
5d66a1f298 | ||
|
|
9bf52eb63a | ||
|
|
0c9d72a9a1 | ||
|
|
c2c2bd2df6 | ||
|
|
78fc594d84 | ||
|
|
53b1e7c573 | ||
|
|
c14404a7c1 | ||
|
|
96e6750f9c | ||
|
|
dbafd05a4e | ||
|
|
d7fd578cc3 | ||
|
|
0e8572e1db | ||
|
|
98a997bd8e | ||
|
|
6fbc95cc41 | ||
|
|
1852f74e8b | ||
|
|
55e3e16860 | ||
|
|
7cf2a55b43 | ||
|
|
c41cbbd2da | ||
|
|
7283bb046b | ||
|
|
dd2df6cbcf | ||
|
|
01f1cac811 | ||
|
|
aaa112c3f3 | ||
|
|
dc0fa9c64d | ||
|
|
f749b38aa2 | ||
|
|
43d79673e1 | ||
|
|
fc841f572c | ||
|
|
df9f117a08 | ||
|
|
bedc46a1be | ||
|
|
a7c6095be5 | ||
|
|
f2d5cf785e | ||
|
|
f926ad9b5e | ||
|
|
68acf969f2 | ||
|
|
65d53e800b | ||
|
|
e44d8ac7e9 | ||
|
|
d1823dd6e1 | ||
|
|
80bfd3b8e9 | ||
|
|
07128cc7fe | ||
|
|
0892f0b0bb | ||
|
|
8cf7f3ac08 | ||
|
|
ab2bb51879 | ||
|
|
8cb98b5365 | ||
|
|
782c5f8a34 | ||
|
|
d6b5c5f63e | ||
|
|
ef6dfa6d91 | ||
|
|
3840ee8ee7 | ||
|
|
f47e40fa2a | ||
|
|
49a4a61876 | ||
|
|
669c1a6f0d | ||
|
|
15342190fa | ||
|
|
c98ef6e8a9 | ||
|
|
d36d5936da | ||
|
|
70de75ae3d | ||
|
|
ea62e33e51 | ||
|
|
524e92c81a | ||
|
|
d5a26b1d70 | ||
|
|
cd7649a666 | ||
|
|
847cc0a38a | ||
|
|
3588eb9bf8 | ||
|
|
296e22d3ca | ||
|
|
923e590da1 | ||
|
|
e56329d05a | ||
|
|
2452e80e57 | ||
|
|
b9019831a9 | ||
|
|
b3b49b440e | ||
|
|
cbedf0f74f | ||
|
|
6c980eba68 | ||
|
|
e9f766904e | ||
|
|
62d76c006d | ||
|
|
615b78a612 | ||
|
|
aaf0340d6c | ||
|
|
93ea437760 | ||
|
|
fe6a1244ed | ||
|
|
953858b0e7 | ||
|
|
f3da46833f | ||
|
|
ba2488b6e2 | ||
|
|
4e7e0d67e5 | ||
|
|
faa16b207c | ||
|
|
2d9f62761c | ||
|
|
60689f531d | ||
|
|
3f97692311 | ||
|
|
81221336f4 | ||
|
|
ecfc758ce4 | ||
|
|
2cb3a132c6 | ||
|
|
519473e498 | ||
|
|
9b271d918a | ||
|
|
5b4d874851 | ||
|
|
9dc1dbc002 | ||
|
|
7ee323f0d5 | ||
|
|
4bb6f711b9 | ||
|
|
816cec8f5c | ||
|
|
2394dfe88a | ||
|
|
dfc0d2303b | ||
|
|
598a48ec50 | ||
|
|
9f41e98e3f | ||
|
|
489891ff3c | ||
|
|
a44ed0b047 | ||
|
|
1aaa97b91d | ||
|
|
13d68e789f | ||
|
|
10dae7482c | ||
|
|
979de190dc | ||
|
|
46bcbda972 | ||
|
|
43bdc353fb | ||
|
|
6314676b9c | ||
|
|
9f77f3d968 | ||
|
|
f4733dbbe8 | ||
|
|
61fc0e4893 | ||
|
|
652ad86cad | ||
|
|
60f308bd76 | ||
|
|
b2113ff95b | ||
|
|
b8188a46a9 | ||
|
|
e790b0761e | ||
|
|
fa3317f6cd | ||
|
|
b080f7a81c | ||
|
|
57a1c5eb76 | ||
|
|
5b7e2521cc | ||
|
|
fced653c1c | ||
|
|
9253e6ac5e | ||
|
|
aff2ed22b9 | ||
|
|
5520a52379 | ||
|
|
505050e936 | ||
|
|
0437835bde | ||
|
|
77386f8dfc | ||
|
|
529fdf0341 | ||
|
|
fa74700ad9 | ||
|
|
caca491943 | ||
|
|
4794f44a6d | ||
|
|
f0119b1231 | ||
|
|
4c579aa826 | ||
|
|
b9463b3c0e | ||
|
|
cede6ce8a9 | ||
|
|
956e6e3638 | ||
|
|
b282c7d815 | ||
|
|
5b6142e2d9 | ||
|
|
58313ad8fc | ||
|
|
7aff148e9f | ||
|
|
ee5c61fb74 | ||
|
|
91be6c8028 | ||
|
|
2e0bd2ec28 | ||
|
|
a0151fbc04 | ||
|
|
d853782fea | ||
|
|
c6638a7154 | ||
|
|
4abebe3aec | ||
|
|
5aed8f928a | ||
|
|
afe6cfc383 | ||
|
|
174983ce1e | ||
|
|
1313c7b3bf | ||
|
|
bee622bcad | ||
|
|
8cefdeae49 | ||
|
|
7cf70b9607 | ||
|
|
add4b582b5 | ||
|
|
b419d21940 | ||
|
|
227ff4e13a | ||
|
|
8e6e70683e | ||
|
|
626f0aec6a | ||
|
|
973d174a30 | ||
|
|
9f35690b95 | ||
|
|
d961f2bc13 | ||
|
|
10e8277044 | ||
|
|
b4c40d9b18 | ||
|
|
e1603f0808 | ||
|
|
d4c2350d63 | ||
|
|
5e5470ae12 | ||
|
|
6230a99360 | ||
|
|
801adb6af7 | ||
|
|
8b9b96f422 | ||
|
|
160684f23e | ||
|
|
88930a9de4 | ||
|
|
af6b8c9b88 | ||
|
|
bf52d3189d | ||
|
|
5d872e8b5b | ||
|
|
4832d8007b | ||
|
|
879e084383 | ||
|
|
1ae16bb657 | ||
|
|
2177bc68db | ||
|
|
c8b887fe32 | ||
|
|
7205242f38 | ||
|
|
be33b5b387 | ||
|
|
66a123f4b9 | ||
|
|
76a4ee22b3 | ||
|
|
3a6bf4d20b | ||
|
|
09cf105cdc | ||
|
|
ce02e086a3 | ||
|
|
e8d4313454 | ||
|
|
49046f88cf | ||
|
|
5e2f32e3eb | ||
|
|
b4a1393bf6 | ||
|
|
0ccee9d806 | ||
|
|
5700272dc4 | ||
|
|
7e5a3925b3 | ||
|
|
1febe32e73 | ||
|
|
6059c982cf | ||
|
|
de7b973239 | ||
|
|
741113a607 | ||
|
|
c20daa8867 | ||
|
|
5e57a7d5c0 | ||
|
|
7c8d27700e | ||
|
|
f8621be5ee | ||
|
|
8ed87e10b4 | ||
|
|
ca26fa2dd0 | ||
|
|
1669095fb1 | ||
|
|
13eed48877 | ||
|
|
07494e6eff | ||
|
|
41b1d72825 | ||
|
|
4d2965e74f | ||
|
|
12526bfca4 | ||
|
|
3e2c344140 | ||
|
|
33730dd584 | ||
|
|
1cb03b1882 | ||
|
|
2f0a6db414 | ||
|
|
a42328d971 | ||
|
|
96ff51d598 | ||
|
|
7c3d3af681 | ||
|
|
6942bca5a7 | ||
|
|
9eac9a190e | ||
|
|
40e4fbd9a6 | ||
|
|
7b6996ed80 | ||
|
|
ef2827a9d8 | ||
|
|
dbf98ce3a6 | ||
|
|
ee7cb5123d | ||
|
|
77e1d6ab8a | ||
|
|
1640bed7a5 | ||
|
|
c6fd5cbbce | ||
|
|
5288041da5 | ||
|
|
a848fb1dd1 | ||
|
|
172945434c | ||
|
|
a0967e0281 | ||
|
|
deab717c73 | ||
|
|
5db5a088ea | ||
|
|
5e106d2f19 | ||
|
|
940da6bc73 | ||
|
|
d110bff253 | ||
|
|
4caa329c36 | ||
|
|
7c7d279337 | ||
|
|
831e6ee76d | ||
|
|
b01842ad02 | ||
|
|
7acf91883a | ||
|
|
897f6fdd3e | ||
|
|
6f9c3fbc38 | ||
|
|
bcbd1a5b4b | ||
|
|
6468efd7e4 | ||
|
|
66863789c7 | ||
|
|
b30058fc29 | ||
|
|
a41cd0b070 | ||
|
|
ea8b905f02 | ||
|
|
16b301236b | ||
|
|
22162dda83 | ||
|
|
024af89be3 | ||
|
|
e1a697658e | ||
|
|
0a5051f959 | ||
|
|
82b6b60b31 | ||
|
|
06b3c3636a | ||
|
|
e5712a9474 | ||
|
|
6ff845c53b | ||
|
|
1c225ed04d | ||
|
|
612b738968 | ||
|
|
87356ef594 | ||
|
|
20a4dfa6d6 | ||
|
|
524cc97f51 | ||
|
|
fc849fb51a | ||
|
|
c88651939a | ||
|
|
01254f67f0 | ||
|
|
212ed33b21 | ||
|
|
bd3f8aef21 | ||
|
|
3785acea29 | ||
|
|
866e2f1762 | ||
|
|
0a705d6752 | ||
|
|
4f74fd5354 | ||
|
|
3df9a5d01c | ||
|
|
2682829bd3 | ||
|
|
30cfa2607d | ||
|
|
4467c2e6ad | ||
|
|
2c0e8a0ddf | ||
|
|
4c17f32a2e | ||
|
|
674220087f | ||
|
|
fc8d5d5799 | ||
|
|
4e4f04c174 | ||
|
|
65c21f3392 | ||
|
|
97fb30cdf1 | ||
|
|
c975279589 | ||
|
|
b3ee7a635f | ||
|
|
c2b12868bb | ||
|
|
bdbb03e16f | ||
|
|
53138d7c16 | ||
|
|
18d1d35104 | ||
|
|
99d9de35de | ||
|
|
e6a48481a5 | ||
|
|
21f2b1224a | ||
|
|
187f47a353 | ||
|
|
cb75b3d6d2 | ||
|
|
27708b9ecb | ||
|
|
5443165fce | ||
|
|
dfb006d264 | ||
|
|
4f00f62674 | ||
|
|
cec527ffd5 | ||
|
|
4f2fe5c988 | ||
|
|
2bf552f801 | ||
|
|
477fa8fedf | ||
|
|
f1c7c5f02c | ||
|
|
4e936193c7 | ||
|
|
f9895c22b6 | ||
|
|
6e46537112 | ||
|
|
92b3918b26 | ||
|
|
34c615a5fd | ||
|
|
fa29260e09 | ||
|
|
a05816b495 | ||
|
|
9c5d847dae | ||
|
|
082e330f9d | ||
|
|
8878c4a136 | ||
|
|
00926bac81 | ||
|
|
67b0907e52 | ||
|
|
dba479774b | ||
|
|
5c8a1e632b | ||
|
|
d65e29111f | ||
|
|
ed170b8092 | ||
|
|
ac36fef968 | ||
|
|
10f6b73cad | ||
|
|
13d933fc07 | ||
|
|
3ba98e86f2 | ||
|
|
1dfca525cf | ||
|
|
e51ad1a68a | ||
|
|
9c76700f74 | ||
|
|
773b1af6f1 | ||
|
|
b8b633a6cc | ||
|
|
ada2270bd1 | ||
|
|
25e5175c54 | ||
|
|
9dcd770df2 | ||
|
|
00a1948b73 | ||
|
|
859a5a5e0c | ||
|
|
6783cf57da | ||
|
|
a8dc5ac14a | ||
|
|
6182553d79 | ||
|
|
905a0cdac1 | ||
|
|
f20ddcf26d | ||
|
|
fe26098c2b | ||
|
|
2354a0a640 | ||
|
|
7c3f09c70c | ||
|
|
caf5350fe6 | ||
|
|
84084835ce | ||
|
|
7f38a67ede | ||
|
|
54383b84ec | ||
|
|
948910bfa2 | ||
|
|
ce5dac07d3 | ||
|
|
d4b0057423 | ||
|
|
de8b12f594 | ||
|
|
69e31a0a37 | ||
|
|
c383cd39f4 | ||
|
|
e88d2f0ab2 | ||
|
|
32535578a0 | ||
|
|
2e9af8fef0 | ||
|
|
0aa87aacc6 | ||
|
|
bc38c302ed | ||
|
|
c724902508 | ||
|
|
73604020d7 | ||
|
|
060a00b9c7 | ||
|
|
2cd32bf25a | ||
|
|
81ad1a18fa | ||
|
|
1d94903eef | ||
|
|
2d7dcb495c | ||
|
|
bc580c9aaa | ||
|
|
f8e2740532 | ||
|
|
a079ecd20b | ||
|
|
89c8725766 | ||
|
|
da7d532564 | ||
|
|
4752d4966d | ||
|
|
50f47f1ed5 | ||
|
|
26bdc3e1d5 | ||
|
|
764360c0f3 | ||
|
|
08482577eb | ||
|
|
cabbba2448 | ||
|
|
c48ed7bb0d | ||
|
|
fa4c0e82bd | ||
|
|
c0bf8b9c25 | ||
|
|
d2cff86c97 | ||
|
|
03c0be0720 | ||
|
|
a3361363e4 | ||
|
|
40e142c5c7 | ||
|
|
2703efedef | ||
|
|
c313f1a20b | ||
|
|
b7116657d9 | ||
|
|
2bf3f1b94b | ||
|
|
b78a7971d9 | ||
|
|
5079bc4976 | ||
|
|
555032c15c | ||
|
|
bcff327565 | ||
|
|
e95ff989b1 | ||
|
|
018e8999e2 | ||
|
|
f624a1817e | ||
|
|
202a643e5a | ||
|
|
c75b9c9221 | ||
|
|
9a6f5ba1a8 | ||
|
|
052194714a | ||
|
|
5ccb4a8b7f | ||
|
|
7c3cec0451 | ||
|
|
b79d3b31b3 | ||
|
|
bf5d43d0cf | ||
|
|
f2ac570949 | ||
|
|
37636ef1eb | ||
|
|
c540bec824 | ||
|
|
9288cfa684 | ||
|
|
b1de8b703b | ||
|
|
ccd0777ce9 | ||
|
|
49b67b3e18 | ||
|
|
bc97168dc7 | ||
|
|
0ec04df334 | ||
|
|
26d7946b4a | ||
|
|
292cc579cf | ||
|
|
8492482e55 | ||
|
|
ac8183feee | ||
|
|
8037fb76fc | ||
|
|
754a14941b | ||
|
|
e32bc1a0a3 | ||
|
|
e5a8b49c93 | ||
|
|
b6872456cd | ||
|
|
2a12b018da | ||
|
|
f64486c88f | ||
|
|
80a40ac5f9 | ||
|
|
a1956350b1 | ||
|
|
2c09875556 | ||
|
|
561eed4cbd | ||
|
|
a8f72df7f8 | ||
|
|
ab4127f940 | ||
|
|
f03d12b677 | ||
|
|
81c6202582 | ||
|
|
6cab2bec61 | ||
|
|
94ffda653c | ||
|
|
c2268b45f6 | ||
|
|
38b8a59fe5 | ||
|
|
640e3213ee | ||
|
|
b1799009b3 | ||
|
|
f8e693a060 | ||
|
|
aba806bc32 | ||
|
|
b1fb3f2391 | ||
|
|
041e901c61 | ||
|
|
bb72134992 | ||
|
|
f83853c2fb | ||
|
|
d5f7b20be3 | ||
|
|
b878d64460 | ||
|
|
5397e8ae1a | ||
|
|
46bd8ad83d | ||
|
|
4dac57cf8c | ||
|
|
95cb026446 | ||
|
|
76ea714257 | ||
|
|
5788fa7204 | ||
|
|
d077c6450f | ||
|
|
4499ed3878 | ||
|
|
feb5e3bc78 | ||
|
|
23f1060756 | ||
|
|
6ed512e762 | ||
|
|
947970e07b | ||
|
|
df567b405a | ||
|
|
7b0596d9df | ||
|
|
4b7200ae69 | ||
|
|
710648f3f4 | ||
|
|
d2002b6a62 | ||
|
|
8de42b3199 | ||
|
|
48e78d2cd3 | ||
|
|
1fd099db70 | ||
|
|
2986a40c5a | ||
|
|
8bb6d4d0d9 | ||
|
|
00f08578ee | ||
|
|
6a514c7f51 | ||
|
|
21aad643f3 | ||
|
|
8687448966 | ||
|
|
6fcc8b721b | ||
|
|
b4bbd58c77 | ||
|
|
1a45b8f07a | ||
|
|
4471a858db | ||
|
|
22e712af5a | ||
|
|
9799b82d53 | ||
|
|
78915e0b99 | ||
|
|
c9cc65aae7 | ||
|
|
a4edd9d202 | ||
|
|
7747624c80 | ||
|
|
aab7191a44 | ||
|
|
1f972b1828 | ||
|
|
58b77cf589 | ||
|
|
c064f87889 | ||
|
|
d6b4c8e7da | ||
|
|
49f0b45fa8 | ||
|
|
b19c4bfc5d | ||
|
|
e944c819d9 | ||
|
|
75f631599c | ||
|
|
e0bebeae25 | ||
|
|
32e7ddf272 | ||
|
|
7091e0324e | ||
|
|
8f8532921c | ||
|
|
e9936992ec | ||
|
|
6375055b3e | ||
|
|
88b1ea296c | ||
|
|
2a13a869f3 | ||
|
|
89c342bb6f | ||
|
|
067f5e33aa | ||
|
|
33695f1c6b | ||
|
|
52ef84873e | ||
|
|
c21ba5c808 | ||
|
|
6cf408f12b | ||
|
|
8bdc2d285d | ||
|
|
a66fbcd6be | ||
|
|
93402a62fc | ||
|
|
2a56fb6c70 | ||
|
|
8276c031e9 | ||
|
|
102e5f6de9 | ||
|
|
1f7bdbfbcb | ||
|
|
f2fdd048c7 | ||
|
|
5a2cfc15a6 | ||
|
|
11c7131875 | ||
|
|
34aacc90d7 | ||
|
|
e75dce2543 | ||
|
|
e2774309fb | ||
|
|
dfefdc93c5 | ||
|
|
d46ea28bba | ||
|
|
d35c5bcaba | ||
|
|
2b14856d7f | ||
|
|
3316fdb80e | ||
|
|
c5d70c48c9 | ||
|
|
82230ef2f5 | ||
|
|
dcfd3d3e48 | ||
|
|
cb1e2ac32e | ||
|
|
ec2c15946a | ||
|
|
8be73142f8 | ||
|
|
d1640e3bb8 | ||
|
|
8612e6726d | ||
|
|
3b4694e9f8 | ||
|
|
14cb2e1999 | ||
|
|
cfabaf334e | ||
|
|
c054536fab | ||
|
|
35aac18b4e | ||
|
|
de3928cb4d | ||
|
|
9bf612b800 | ||
|
|
d9a8d67e3d | ||
|
|
99b0a56a6a | ||
|
|
61d7aa0400 | ||
|
|
1e98594972 | ||
|
|
c6383f042c | ||
|
|
b4d8e0e551 | ||
|
|
d696536486 | ||
|
|
b46a3cf759 | ||
|
|
8a5e3cdb76 | ||
|
|
4fb39226ef | ||
|
|
c40174034c | ||
|
|
5029c855f0 | ||
|
|
2eee839eaa | ||
|
|
f42c7fec1e | ||
|
|
0ab07303c9 | ||
|
|
ad47990a87 | ||
|
|
1968461ce9 | ||
|
|
8aa3b5d3da | ||
|
|
23e77eefb0 | ||
|
|
094443cf26 | ||
|
|
b97758ef46 | ||
|
|
d1fe752028 | ||
|
|
b26df787cd | ||
|
|
4c5b254730 | ||
|
|
9a153b7170 | ||
|
|
065a6caee5 | ||
|
|
9c7df02941 | ||
|
|
c63c5100e2 | ||
|
|
bf05088d70 | ||
|
|
e7bb146f92 | ||
|
|
688c4a90d9 | ||
|
|
165ad96be1 | ||
|
|
115ced7ab4 | ||
|
|
ef15b5641a | ||
|
|
be50464619 | ||
|
|
d847b71c86 | ||
|
|
dfcc1ff899 | ||
|
|
65562937f3 | ||
|
|
7b5dc33a21 | ||
|
|
f64c13b9ec | ||
|
|
25f70e570e | ||
|
|
b27b64c25c | ||
|
|
bfc64c926a | ||
|
|
88ad983aa4 | ||
|
|
a95d2e76f5 | ||
|
|
037886ba5b | ||
|
|
011edfe2a0 | ||
|
|
1a5a6773ce | ||
|
|
dda22fd387 | ||
|
|
7254224347 | ||
|
|
789e52af57 | ||
|
|
b2fcd6c084 | ||
|
|
76e5794203 | ||
|
|
a81332dd23 | ||
|
|
e1fab22d98 | ||
|
|
cb5e58d465 |
@@ -1,8 +1,10 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us address broken functionality
|
||||
about: Ask for help on Discord before reporting a bug
|
||||
|
||||
---
|
||||
**READ ME FIRST!**
|
||||
This issue tracker is not the right place to ask for troubleshooting help if you're not able to connect or stream at all. For those requests, please use the #pc-help channel on our Discord server where there are many more people who can help (or direct you back here if it looks like a Moonlight bug after all). https://moonlight-stream.org/discord
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
@@ -13,19 +15,31 @@ Any special steps that are required for the bug to appear.
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem. If the issue is related to video glitching or poor quality, please include screenshots.
|
||||
|
||||
**Affected games**
|
||||
List the games you've tried that exhibit the issue. To see if the issue is game-specific, try streaming Steam Big Picture with Moonlight and see if the issue persists there.
|
||||
|
||||
**Other Moonlight clients**
|
||||
- Does the issue occur when using Moonlight on iOS or Android?
|
||||
|
||||
**Moonlight settings (please complete the following information)**
|
||||
- Have any settings been adjusted from defaults?
|
||||
- If so, which settings have been changed?
|
||||
- Does the problem still occur after reverting settings back to default?
|
||||
|
||||
**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]
|
||||
|
||||
**Logs (please attach)**
|
||||
**Server PC details (please complete the following information)**
|
||||
- OS: [e.g. Windows 10 1809]
|
||||
- GeForce Experience version: [e.g. 3.16.0.140]
|
||||
- Nvidia GPU driver: [e.g. 417.35]
|
||||
|
||||
**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**
|
||||
Add any other context about the problem here.
|
||||
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!
|
||||
@@ -2,4 +2,6 @@
|
||||
|
||||
|
||||
**/.vs/
|
||||
build/
|
||||
build/
|
||||
config.tests/*/.qmake.stash
|
||||
config.tests/*/Makefile
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
language: cpp
|
||||
|
||||
git:
|
||||
depth: 1
|
||||
|
||||
matrix:
|
||||
include:
|
||||
- os: osx
|
||||
compiler: clang
|
||||
- os: linux
|
||||
sudo: required
|
||||
dist: xenial
|
||||
compiler: gcc
|
||||
addons:
|
||||
apt:
|
||||
sources:
|
||||
- sourceline: 'ppa:beineri/opt-qt596-xenial'
|
||||
- sourceline: 'ppa:jonathonf/ffmpeg-4'
|
||||
packages:
|
||||
- qt59base
|
||||
- qt59quickcontrols2
|
||||
- qt59svg
|
||||
- qt59multimedia
|
||||
- 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.8 && wget https://www.libsdl.org/release/SDL2-$SDL2_VER.tar.gz && tar -xvf SDL2-$SDL2_VER.tar.gz && cd SDL2-$SDL2_VER && ./configure && make && sudo make install && cd ..; fi
|
||||
|
||||
before_script:
|
||||
- '[ "$TRAVIS_OS_NAME" != "linux" ] || source /opt/qt59/bin/qt59-env.sh'
|
||||
- qmake -v
|
||||
- '[[ "$TRAVIS_OS_NAME" != linux || "$CXX" != clang++ ]] || export QMAKESPEC=linux-clang'
|
||||
- '[[ "$TRAVIS_OS_NAME" != linux || "$CXX" != g++ ]] || export QMAKESPEC=linux-g++'
|
||||
- '[ "$TRAVIS_OS_NAME" != osx ] || export QMAKESPEC=macx-clang'
|
||||
|
||||
script:
|
||||
- qmake moonlight-qt.pro -spec $QMAKESPEC
|
||||
- make debug
|
||||
- make release
|
||||
@@ -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",
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -1,58 +1,77 @@
|
||||
# Moonlight PC
|
||||
|
||||
[Moonlight PC](http://moonlight-stream.com) is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield, but built to run on Windows, Mac, and Linux. This client is the successor to [Moonlight Chrome](https://github.com/moonlight-stream/moonlight-chrome) for streaming on PC.
|
||||
[Moonlight PC](https://moonlight-stream.org) is an open source 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).
|
||||
|
||||
See the [releases page](https://github.com/moonlight-stream/moonlight-qt/releases) for download links for Windows and Mac. For Linux, we're on [Flathub](https://flathub.org/apps/details/com.moonlight_stream.Moonlight).
|
||||
You can follow development on our [Discord server](https://moonlight-stream.org/discord).
|
||||
|
||||
You can follow development on our [Discord server](https://discord.gg/6ERtzFY).
|
||||
|
||||
[](https://ci.appveyor.com/project/cgutman/moonlight-qt/branch/master)
|
||||
[](https://travis-ci.org/moonlight-stream/moonlight-qt)
|
||||
[](https://ci.appveyor.com/project/cgutman/moonlight-qt/branch/master)
|
||||
[](https://github.com/moonlight-stream/moonlight-qt/releases)
|
||||
|
||||
## 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 in fullscreen mode
|
||||
- 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, 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 2015 or later
|
||||
* Select MSVC Desktop toolchain during Qt installation
|
||||
* 7-Zip (only if building installers for non-development PCs)
|
||||
* WiX Toolset v3.11 or later (only if building installers for non-development PCs)
|
||||
* [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](https://wixtoolset.org/releases/) v3.11 or later (only if building installers for non-development PCs)
|
||||
|
||||
### Mac-specific Requirements
|
||||
* macOS El Capitan (10.11) or later
|
||||
* Xcode with High Sierra (10.13) SDK (Mojave SDK not yet supported until Qt 5.12 later this year)
|
||||
### 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`
|
||||
* 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 may also use Homebrew on macOS or your Linux distro's package manager for the Qt SDK as long as the packages are Qt 5.9 or later.
|
||||
* 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`
|
||||
|
||||
## Contribute
|
||||
1. Fork us
|
||||
2. Write code
|
||||
3. Send Pull Requests
|
||||
|
||||
Check out our [website](http://moonlight-stream.com) for project links and information.
|
||||
Check out our [website](https://moonlight-stream.org) for project links and information.
|
||||
|
||||
@@ -17,15 +17,15 @@
|
||||
<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>
|
||||
<true/>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>0.6.2</string>
|
||||
<string>VERSION</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>0.6.2</string>
|
||||
<string>VERSION</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Moonlight</string>
|
||||
</dict>
|
||||
|
||||
@@ -8,17 +8,15 @@ 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
|
||||
# do this for Windows and Mac, since they ship with the Qt runtime.
|
||||
win32|macx {
|
||||
CONFIG += qtquickcompiler
|
||||
CONFIG(release, debug|release) {
|
||||
CONFIG += qtquickcompiler
|
||||
}
|
||||
}
|
||||
|
||||
TEMPLATE = app
|
||||
@@ -43,25 +41,22 @@ win32 {
|
||||
contains(QT_ARCH, x86_64) {
|
||||
LIBS += -L$$PWD/../libs/windows/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 $$PWD/../libs/mac/Frameworks/SDL2.framework/Versions/A/Headers
|
||||
INCLUDEPATH += $$PWD/../libs/mac/include
|
||||
INCLUDEPATH += $$PWD/../libs/mac/Frameworks/SDL2.framework/Versions/A/Headers
|
||||
INCLUDEPATH += $$PWD/../libs/mac/Frameworks/SDL2_ttf.framework/Versions/A/Headers
|
||||
LIBS += -L$$PWD/../libs/mac/lib -F$$PWD/../libs/mac/Frameworks
|
||||
}
|
||||
|
||||
unix:!macx {
|
||||
CONFIG += link_pkgconfig
|
||||
PKGCONFIG += openssl sdl2 opus
|
||||
|
||||
# For libsoundio
|
||||
packagesExist(libpulse) {
|
||||
PKGCONFIG += libpulse
|
||||
}
|
||||
packagesExist(alsa) {
|
||||
PKGCONFIG += alsa
|
||||
}
|
||||
PKGCONFIG += openssl sdl2 SDL2_ttf opus
|
||||
|
||||
packagesExist(libavcodec) {
|
||||
PKGCONFIG += libavcodec libavutil
|
||||
@@ -74,40 +69,76 @@ 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 -lavcodec -lavutil -lopus
|
||||
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
|
||||
LIBS += -lobjc -framework VideoToolbox -framework AVFoundation -framework CoreVideo -framework CoreGraphics -framework CoreMedia -framework AppKit
|
||||
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 -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 \
|
||||
backend/nvcomputer.cpp \
|
||||
backend/nvhttp.cpp \
|
||||
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/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 \
|
||||
@@ -117,20 +148,28 @@ SOURCES += \
|
||||
backend/autoupdatechecker.cpp \
|
||||
path.cpp \
|
||||
settings/mappingmanager.cpp \
|
||||
gui/sdlgamepadkeynavigation.cpp
|
||||
gui/sdlgamepadkeynavigation.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 \
|
||||
backend/nvcomputer.h \
|
||||
backend/nvhttp.h \
|
||||
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 \
|
||||
@@ -141,7 +180,9 @@ HEADERS += \
|
||||
backend/autoupdatechecker.h \
|
||||
path.h \
|
||||
settings/mappingmanager.h \
|
||||
gui/sdlgamepadkeynavigation.h
|
||||
gui/sdlgamepadkeynavigation.h \
|
||||
streaming/video/overlaymanager.h \
|
||||
backend/systemproperties.h
|
||||
|
||||
# Platform-specific renderers and decoders
|
||||
ffmpeg {
|
||||
@@ -151,13 +192,15 @@ 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
|
||||
|
||||
|
||||
HEADERS += \
|
||||
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
|
||||
}
|
||||
@@ -181,6 +224,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)
|
||||
|
||||
@@ -188,16 +237,44 @@ 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
|
||||
}
|
||||
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 += 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 += \
|
||||
@@ -212,12 +289,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)
|
||||
@@ -226,11 +301,20 @@ 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
|
||||
}
|
||||
|
||||
RESOURCES += \
|
||||
resources.qrc \
|
||||
qml.qrc
|
||||
|
||||
TRANSLATIONS += \
|
||||
languages/qml_fr.ts
|
||||
|
||||
# Additional import path used to resolve QML modules in Qt Creator's code model
|
||||
QML_IMPORT_PATH =
|
||||
|
||||
@@ -251,12 +335,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
|
||||
@@ -265,11 +351,15 @@ 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
|
||||
INCLUDEPATH += $$PWD/../AntiHooking
|
||||
DEPENDPATH += $$PWD/../AntiHooking
|
||||
}
|
||||
}
|
||||
|
||||
unix:!macx: {
|
||||
isEmpty(PREFIX) {
|
||||
@@ -293,10 +383,7 @@ unix:!macx: {
|
||||
appstream.files = deploy/linux/com.moonlight_stream.Moonlight.appdata.xml
|
||||
appstream.path = $$PREFIX/$$DATADIR/metainfo/
|
||||
|
||||
appdata.files = SDL_GameControllerDB/gamecontrollerdb.txt
|
||||
appdata.path = "$$PREFIX/$$DATADIR/Moonlight Game Streaming Project/Moonlight/"
|
||||
|
||||
INSTALLS += target appdata desktop icons appstream
|
||||
INSTALLS += target desktop icons appstream
|
||||
}
|
||||
win32 {
|
||||
RC_ICONS = moonlight.ico
|
||||
@@ -308,12 +395,25 @@ win32 {
|
||||
QMAKE_LFLAGS += /MANIFEST:embed /MANIFESTINPUT:$${PWD}/Moonlight.exe.manifest
|
||||
}
|
||||
macx {
|
||||
QMAKE_INFO_PLIST = $$PWD/Info.plist
|
||||
# Create Info.plist in object dir with the correct version string
|
||||
system(cp $$PWD/Info.plist $$OUT_PWD/Info.plist)
|
||||
system(sed -i -e 's/VERSION/$$cat(version.txt)/g' $$OUT_PWD/Info.plist)
|
||||
|
||||
APP_BUNDLE_RESOURCES.files = moonlight.icns SDL_GameControllerDB/gamecontrollerdb.txt
|
||||
QMAKE_INFO_PLIST = $$OUT_PWD/Info.plist
|
||||
|
||||
APP_BUNDLE_RESOURCES.files = moonlight.icns
|
||||
APP_BUNDLE_RESOURCES.path = Contents/Resources
|
||||
QMAKE_BUNDLE_DATA += APP_BUNDLE_RESOURCES
|
||||
|
||||
APP_BUNDLE_FRAMEWORKS.files = $$files(../libs/mac/Frameworks/*.framework, true) $$files(../libs/mac/lib/*.dylib, true)
|
||||
APP_BUNDLE_FRAMEWORKS.path = Contents/Frameworks
|
||||
|
||||
APP_BUNDLE_PLIST.files = $$OUT_PWD/Info.plist
|
||||
APP_BUNDLE_PLIST.path = Contents
|
||||
|
||||
QMAKE_BUNDLE_DATA += APP_BUNDLE_RESOURCES APP_BUNDLE_FRAMEWORKS APP_BUNDLE_PLIST
|
||||
|
||||
QMAKE_RPATHDIR += @executable_path/../Frameworks
|
||||
}
|
||||
|
||||
VERSION = 0.6.2
|
||||
DEFINES += VERSION_STR=\\\"0.6.2\\\"
|
||||
VERSION = "$$cat(version.txt)"
|
||||
DEFINES += VERSION_STR=\\\"$$cat(version.txt)\\\"
|
||||
|
||||
@@ -12,6 +12,9 @@ AutoUpdateChecker::AutoUpdateChecker(QObject *parent) :
|
||||
// Never communicate over HTTP
|
||||
m_Nam.setStrictTransportSecurityEnabled(true);
|
||||
|
||||
// Allow HTTP redirects
|
||||
m_Nam.setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy);
|
||||
|
||||
connect(&m_Nam, SIGNAL(finished(QNetworkReply*)),
|
||||
this, SLOT(handleUpdateCheckRequestFinished(QNetworkReply*)));
|
||||
|
||||
@@ -25,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.com/updates/qt.json");
|
||||
m_Nam.get(QNetworkRequest(url));
|
||||
QUrl url("https://moonlight-stream.org/updates/qt.json");
|
||||
QNetworkRequest request(url);
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
|
||||
request.setAttribute(QNetworkRequest::Http2AllowedAttribute, true);
|
||||
#else
|
||||
request.setAttribute(QNetworkRequest::HTTP2AllowedAttribute, true);
|
||||
#endif
|
||||
m_Nam.get(request);
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -40,13 +57,29 @@ void AutoUpdateChecker::parseStringToVersionQuad(QString& string, QVector<int>&
|
||||
}
|
||||
}
|
||||
|
||||
QString AutoUpdateChecker::getPlatform()
|
||||
{
|
||||
#if defined(STEAM_LINK)
|
||||
return QStringLiteral("steamlink");
|
||||
#elif defined(APP_IMAGE)
|
||||
return QStringLiteral("appimage");
|
||||
#else
|
||||
return QSysInfo::productType();
|
||||
#endif
|
||||
}
|
||||
|
||||
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();
|
||||
@@ -85,7 +118,7 @@ void AutoUpdateChecker::handleUpdateCheckRequestFinished(QNetworkReply* reply)
|
||||
}
|
||||
|
||||
if (updateObj["arch"] == QSysInfo::buildCpuArchitecture() &&
|
||||
updateObj["platform"] == QSysInfo::productType()) {
|
||||
updateObj["platform"] == getPlatform()) {
|
||||
qDebug() << "Found update manifest match for current platform";
|
||||
|
||||
QString latestVersion = updateObj["version"].toString();
|
||||
@@ -113,7 +146,8 @@ void AutoUpdateChecker::handleUpdateCheckRequestFinished(QNetworkReply* reply)
|
||||
if (currentVer < latestVer) {
|
||||
qDebug() << "Update available";
|
||||
|
||||
emit onUpdateAvailable(updateObj["browser_url"].toString());
|
||||
emit onUpdateAvailable(updateObj["version"].toString(),
|
||||
updateObj["browser_url"].toString());
|
||||
return;
|
||||
}
|
||||
else if (currentVer > latestVer) {
|
||||
@@ -133,7 +167,7 @@ void AutoUpdateChecker::handleUpdateCheckRequestFinished(QNetworkReply* reply)
|
||||
}
|
||||
|
||||
qWarning() << "No entry in update manifest found for current platform: "
|
||||
<< QSysInfo::buildCpuArchitecture() << QSysInfo::productType();
|
||||
<< QSysInfo::buildCpuArchitecture() << getPlatform();
|
||||
}
|
||||
else {
|
||||
qWarning() << "Update checking failed with error: " << reply->error();
|
||||
|
||||
@@ -12,7 +12,7 @@ public:
|
||||
Q_INVOKABLE void start();
|
||||
|
||||
signals:
|
||||
void onUpdateAvailable(QString url);
|
||||
void onUpdateAvailable(QString newVersion, QString url);
|
||||
|
||||
private slots:
|
||||
void handleUpdateCheckRequestFinished(QNetworkReply* reply);
|
||||
@@ -20,6 +20,8 @@ private slots:
|
||||
private:
|
||||
void parseStringToVersionQuad(QString& string, QVector<int>& version);
|
||||
|
||||
QString getPlatform();
|
||||
|
||||
QVector<int> m_CurrentVersionQuad;
|
||||
QNetworkAccessManager m_Nam;
|
||||
};
|
||||
|
||||
@@ -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,17 +87,26 @@ 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)
|
||||
{
|
||||
NvHTTP http(computer->activeAddress);
|
||||
NvHTTP http(computer->activeAddress, computer->serverCert);
|
||||
|
||||
QString cachePath = getFilePathForBoxArt(computer, appId);
|
||||
QImage image;
|
||||
@@ -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"
|
||||
|
||||
@@ -27,16 +29,16 @@ public:
|
||||
private:
|
||||
bool tryPollComputer(QString address, bool& changed)
|
||||
{
|
||||
NvHTTP http(address);
|
||||
NvHTTP http(address, m_Computer->serverCert);
|
||||
|
||||
QString serverInfo;
|
||||
try {
|
||||
serverInfo = http.getServerInfo(NvHTTP::NvLogLevel::NONE);
|
||||
serverInfo = http.getServerInfo(NvHTTP::NvLogLevel::NVLL_NONE, true);
|
||||
} catch (...) {
|
||||
return false;
|
||||
}
|
||||
|
||||
NvComputer newState(address, serverInfo);
|
||||
NvComputer newState(address, serverInfo, QSslCertificate());
|
||||
|
||||
// Ensure the machine that responded is the one we intended to contact
|
||||
if (m_Computer->uuid != newState.uuid) {
|
||||
@@ -52,7 +54,7 @@ private:
|
||||
{
|
||||
Q_ASSERT(m_Computer->activeAddress != nullptr);
|
||||
|
||||
NvHTTP http(m_Computer->activeAddress);
|
||||
NvHTTP http(m_Computer->activeAddress, m_Computer->serverCert);
|
||||
|
||||
QVector<NvApp> appList;
|
||||
|
||||
@@ -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(), SIGNAL(aboutToQuit()), this, SLOT(handleAboutToQuit()));
|
||||
}
|
||||
|
||||
ComputerManager::~ComputerManager()
|
||||
@@ -174,38 +182,19 @@ ComputerManager::~ComputerManager()
|
||||
delete m_MdnsBrowser;
|
||||
m_MdnsBrowser = nullptr;
|
||||
|
||||
{
|
||||
QMutableMapIterator<QString, QThread*> i(m_PollThreads);
|
||||
|
||||
// Interrupt all polling threads
|
||||
while (i.hasNext()) {
|
||||
i.next();
|
||||
i.value()->requestInterruption();
|
||||
}
|
||||
|
||||
// Wait for all threads to terminate before destroying
|
||||
// the NvComputer objects.
|
||||
i.toFront();
|
||||
while (i.hasNext()) {
|
||||
i.next();
|
||||
|
||||
QThread* thread = i.value();
|
||||
thread->wait();
|
||||
delete thread;
|
||||
|
||||
i.remove();
|
||||
}
|
||||
// Interrupt polling
|
||||
for (ComputerPollingEntry* entry : m_PollEntries) {
|
||||
entry->interrupt();
|
||||
}
|
||||
|
||||
{
|
||||
QMutableMapIterator<QString, NvComputer*> i(m_KnownHosts);
|
||||
// Delete all polling entries (and associated threads)
|
||||
for (ComputerPollingEntry* entry : m_PollEntries) {
|
||||
delete entry;
|
||||
}
|
||||
|
||||
// Destroy all NvComputer objects
|
||||
while (i.hasNext()) {
|
||||
i.next();
|
||||
delete i.value();
|
||||
i.remove();
|
||||
}
|
||||
// Destroy all NvComputer objects now that polling is halted
|
||||
for (NvComputer* computer : m_KnownHosts) {
|
||||
delete computer;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,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[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);
|
||||
@@ -240,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);
|
||||
});
|
||||
}
|
||||
@@ -265,24 +291,58 @@ void ComputerManager::startPollingComputer(NvComputer* computer)
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_PollThreads.contains(computer->uuid)) {
|
||||
Q_ASSERT(m_PollThreads[computer->uuid]->isRunning());
|
||||
return;
|
||||
ComputerPollingEntry* pollingEntry;
|
||||
|
||||
if (!m_PollEntries.contains(computer->uuid)) {
|
||||
pollingEntry = m_PollEntries[computer->uuid] = new ComputerPollingEntry();
|
||||
}
|
||||
else {
|
||||
pollingEntry = m_PollEntries[computer->uuid];
|
||||
}
|
||||
|
||||
PcMonitorThread* thread = new PcMonitorThread(computer);
|
||||
connect(thread, SIGNAL(computerStateChanged(NvComputer*)),
|
||||
this, SLOT(handleComputerStateChanged(NvComputer*)));
|
||||
m_PollThreads[computer->uuid] = thread;
|
||||
thread->start();
|
||||
if (!pollingEntry->isActive()) {
|
||||
PcMonitorThread* thread = new PcMonitorThread(computer);
|
||||
connect(thread, SIGNAL(computerStateChanged(NvComputer*)),
|
||||
this, SLOT(handleComputerStateChanged(NvComputer*)));
|
||||
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);
|
||||
added = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m_PendingResolution.removeOne(computer);
|
||||
computer->deleteLater();
|
||||
@@ -317,34 +377,26 @@ public:
|
||||
|
||||
void run()
|
||||
{
|
||||
QThread* pollingThread;
|
||||
ComputerPollingEntry* pollingEntry;
|
||||
|
||||
// Only do the minimum amount of work while holding the writer lock.
|
||||
// We must release it before calling saveHosts().
|
||||
{
|
||||
QWriteLocker lock(&m_ComputerManager->m_Lock);
|
||||
|
||||
pollingThread = m_ComputerManager->m_PollThreads[m_Computer->uuid];
|
||||
pollingEntry = m_ComputerManager->m_PollEntries.take(m_Computer->uuid);
|
||||
|
||||
m_ComputerManager->m_PollThreads.remove(m_Computer->uuid);
|
||||
m_ComputerManager->m_KnownHosts.remove(m_Computer->uuid);
|
||||
}
|
||||
|
||||
// Persist the new host list
|
||||
m_ComputerManager->saveHosts();
|
||||
|
||||
// Delete the polling thread
|
||||
if (pollingThread != nullptr) {
|
||||
pollingThread->requestInterruption();
|
||||
// Delete the polling entry first. This will stop all polling threads too.
|
||||
delete pollingEntry;
|
||||
|
||||
// We must wait here because we're going to delete computer
|
||||
// and we can't do that out from underneath the poller.
|
||||
pollingThread->wait();
|
||||
|
||||
// The thread is safe to delete now
|
||||
Q_ASSERT(pollingThread->isFinished());
|
||||
delete pollingThread;
|
||||
}
|
||||
// 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.
|
||||
@@ -363,6 +415,39 @@ void ComputerManager::deleteHost(NvComputer* computer)
|
||||
QThreadPool::globalInstance()->start(new DeferredHostDeletionTask(this, computer));
|
||||
}
|
||||
|
||||
void ComputerManager::renameHost(NvComputer* computer, QString name)
|
||||
{
|
||||
{
|
||||
QWriteLocker(&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
|
||||
@@ -385,7 +470,7 @@ private:
|
||||
NvPairingManager pairingManager(m_Computer->activeAddress);
|
||||
|
||||
try {
|
||||
NvPairingManager::PairState result = pairingManager.pair(m_Computer->appVersion, m_Pin);
|
||||
NvPairingManager::PairState result = pairingManager.pair(m_Computer->appVersion, m_Pin, m_Computer->serverCert);
|
||||
switch (result)
|
||||
{
|
||||
case NvPairingManager::PairState::PIN_WRONG:
|
||||
@@ -402,7 +487,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());
|
||||
}
|
||||
@@ -438,7 +523,7 @@ signals:
|
||||
private:
|
||||
void run()
|
||||
{
|
||||
NvHTTP http(m_Computer->activeAddress);
|
||||
NvHTTP http(m_Computer->activeAddress, m_Computer->serverCert);
|
||||
|
||||
try {
|
||||
if (m_Computer->currentGameId != 0) {
|
||||
@@ -451,8 +536,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());
|
||||
@@ -499,20 +584,8 @@ void ComputerManager::stopPollingAsync()
|
||||
m_MdnsBrowser = nullptr;
|
||||
|
||||
// Interrupt all threads, but don't wait for them to terminate
|
||||
QMutableMapIterator<QString, QThread*> i(m_PollThreads);
|
||||
while (i.hasNext()) {
|
||||
i.next();
|
||||
|
||||
QThread* thread = i.value();
|
||||
|
||||
// Let this thread delete itself when it's finished
|
||||
connect(thread, SIGNAL(finished()),
|
||||
thread, SLOT(deleteLater()));
|
||||
|
||||
// The threads will delete themselves when they terminate,
|
||||
// but we remove them from the polling threads map here.
|
||||
i.value()->requestInterruption();
|
||||
i.remove();
|
||||
for (ComputerPollingEntry* entry : m_PollEntries) {
|
||||
entry->interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -521,9 +594,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,
|
||||
@@ -533,17 +607,13 @@ public:
|
||||
}
|
||||
|
||||
signals:
|
||||
void computerAddCompleted(QVariant success);
|
||||
void computerAddCompleted(QVariant success, QVariant detectedPortBlocking);
|
||||
|
||||
void computerStateChanged(NvComputer* computer);
|
||||
|
||||
private:
|
||||
void run()
|
||||
QString fetchServerInfo(NvHTTP& http)
|
||||
{
|
||||
NvHTTP http(m_Address);
|
||||
|
||||
qInfo() << "Processing new PC at" << m_Address << "from" << (m_Mdns ? "mDNS" : "user");
|
||||
|
||||
QString serverInfo;
|
||||
try {
|
||||
// There's a race condition between GameStream servers reporting presence over
|
||||
@@ -551,12 +621,12 @@ private:
|
||||
// around this issue, we will issue the request again after a few seconds if
|
||||
// we see a ServiceUnavailableError error.
|
||||
try {
|
||||
serverInfo = http.getServerInfo(NvHTTP::NvLogLevel::VERBOSE);
|
||||
serverInfo = http.getServerInfo(NvHTTP::NVLL_VERBOSE);
|
||||
} catch (const QtNetworkReplyException& e) {
|
||||
if (e.getError() == QNetworkReply::ServiceUnavailableError) {
|
||||
qWarning() << "Retrying request in 5 seconds after ServiceUnavailableError";
|
||||
QThread::sleep(5);
|
||||
serverInfo = http.getServerInfo(NvHTTP::NvLogLevel::VERBOSE);
|
||||
serverInfo = http.getServerInfo(NvHTTP::NVLL_VERBOSE);
|
||||
qInfo() << "Retry successful";
|
||||
}
|
||||
else {
|
||||
@@ -564,85 +634,176 @@ private:
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
void run()
|
||||
{
|
||||
NvHTTP http(m_Address, QSslCertificate());
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
NvComputer* newComputer = new NvComputer(m_Address, serverInfo);
|
||||
// Create initial newComputer using HTTP serverinfo with no pinned cert
|
||||
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;
|
||||
{
|
||||
QReadLocker lock(&m_ComputerManager->m_Lock);
|
||||
existingComputer = m_ComputerManager->m_KnownHosts.value(newComputer->uuid);
|
||||
if (existingComputer != nullptr) {
|
||||
http.setServerCert(existingComputer->serverCert);
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch serverinfo again over HTTPS with the pinned cert
|
||||
if (existingComputer != nullptr) {
|
||||
serverInfo = fetchServerInfo(http);
|
||||
if (serverInfo.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the polled computer with the HTTPS information
|
||||
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.stunprotocol.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;
|
||||
}
|
||||
|
||||
// Check if this PC already exists
|
||||
QWriteLocker lock(&m_ComputerManager->m_Lock);
|
||||
NvComputer* existingComputer = m_ComputerManager->m_KnownHosts[newComputer->uuid];
|
||||
if (existingComputer != nullptr) {
|
||||
// Fold it into the existing PC
|
||||
bool changed = existingComputer->update(*newComputer);
|
||||
delete newComputer;
|
||||
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);
|
||||
|
||||
// Drop the lock before notifying
|
||||
lock.unlock();
|
||||
{
|
||||
// Check if this PC already exists
|
||||
QWriteLocker lock(&m_ComputerManager->m_Lock);
|
||||
NvComputer* existingComputer = m_ComputerManager->m_KnownHosts.value(newComputer->uuid);
|
||||
if (existingComputer != nullptr) {
|
||||
// Fold it into the existing PC
|
||||
bool changed = existingComputer->update(*newComputer);
|
||||
delete newComputer;
|
||||
|
||||
// For non-mDNS clients, let them know it succeeded
|
||||
if (!m_Mdns) {
|
||||
emit computerAddCompleted(true);
|
||||
// Drop the lock before notifying
|
||||
lock.unlock();
|
||||
|
||||
// For non-mDNS clients, let them know it succeeded
|
||||
if (!m_Mdns) {
|
||||
emit computerAddCompleted(true, false);
|
||||
}
|
||||
|
||||
// Tell our client if something changed
|
||||
if (changed) {
|
||||
qInfo() << existingComputer->name << "is now at" << existingComputer->activeAddress;
|
||||
emit computerStateChanged(existingComputer);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Store this in our active sets
|
||||
m_ComputerManager->m_KnownHosts[newComputer->uuid] = newComputer;
|
||||
|
||||
// Tell our client if something changed
|
||||
if (changed) {
|
||||
qInfo() << existingComputer->name << "is now at" << existingComputer->activeAddress;
|
||||
emit computerStateChanged(existingComputer);
|
||||
// Start polling if enabled (write lock required)
|
||||
m_ComputerManager->startPollingComputer(newComputer);
|
||||
|
||||
// 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, false);
|
||||
}
|
||||
|
||||
// Tell our client about this new PC
|
||||
emit computerStateChanged(newComputer);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Store this in our active sets
|
||||
m_ComputerManager->m_KnownHosts[newComputer->uuid] = newComputer;
|
||||
|
||||
// Start polling if enabled (write lock required)
|
||||
m_ComputerManager->startPollingComputer(newComputer);
|
||||
|
||||
// Drop the lock before notifying
|
||||
lock.unlock();
|
||||
|
||||
// For non-mDNS clients, let them know it succeeded
|
||||
if (!m_Mdns) {
|
||||
emit computerAddCompleted(true);
|
||||
}
|
||||
|
||||
// Tell our client about this new PC
|
||||
emit computerStateChanged(newComputer);
|
||||
}
|
||||
}
|
||||
|
||||
ComputerManager* m_ComputerManager;
|
||||
QString m_Address;
|
||||
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,115 @@ 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, SLOT(handleResolvedTimeout()));
|
||||
}
|
||||
|
||||
QByteArray m_Hostname;
|
||||
QMdnsEngine::Resolver m_Resolver;
|
||||
QMdnsEngine::Server* m_Server;
|
||||
QMdnsEngine::Resolver* m_Resolver;
|
||||
QVector<QHostAddress> m_Addresses;
|
||||
};
|
||||
|
||||
class ComputerPollingEntry
|
||||
{
|
||||
public:
|
||||
ComputerPollingEntry()
|
||||
: m_ActiveThread(nullptr)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
virtual ~ComputerPollingEntry()
|
||||
{
|
||||
interrupt();
|
||||
|
||||
// interrupt() should have taken care of this
|
||||
Q_ASSERT(m_ActiveThread == nullptr);
|
||||
|
||||
for (QThread* thread : m_InactiveList) {
|
||||
thread->wait();
|
||||
delete thread;
|
||||
}
|
||||
}
|
||||
|
||||
bool isActive()
|
||||
{
|
||||
cleanInactiveList();
|
||||
|
||||
return m_ActiveThread != nullptr;
|
||||
}
|
||||
|
||||
void setActiveThread(QThread* thread)
|
||||
{
|
||||
cleanInactiveList();
|
||||
|
||||
Q_ASSERT(!isActive());
|
||||
m_ActiveThread = thread;
|
||||
}
|
||||
|
||||
void interrupt()
|
||||
{
|
||||
cleanInactiveList();
|
||||
|
||||
if (m_ActiveThread != nullptr) {
|
||||
// Interrupt the active thread
|
||||
m_ActiveThread->requestInterruption();
|
||||
|
||||
// Place it on the inactive list awaiting death
|
||||
m_InactiveList.append(m_ActiveThread);
|
||||
|
||||
m_ActiveThread = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
void cleanInactiveList()
|
||||
{
|
||||
QMutableListIterator<QThread*> i(m_InactiveList);
|
||||
|
||||
// Reap any threads that have finished
|
||||
while (i.hasNext()) {
|
||||
i.next();
|
||||
|
||||
QThread* thread = i.value();
|
||||
if (thread->isFinished()) {
|
||||
delete thread;
|
||||
i.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QThread* m_ActiveThread;
|
||||
QList<QThread*> m_InactiveList;
|
||||
};
|
||||
|
||||
class ComputerManager : public QObject
|
||||
@@ -67,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);
|
||||
|
||||
@@ -78,29 +179,37 @@ 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;
|
||||
QReadWriteLock m_Lock;
|
||||
QMap<QString, NvComputer*> m_KnownHosts;
|
||||
QMap<QString, QThread*> m_PollThreads;
|
||||
QMap<QString, ComputerPollingEntry*> m_PollEntries;
|
||||
QMdnsEngine::Server m_MdnsServer;
|
||||
QMdnsEngine::Browser* m_MdnsBrowser;
|
||||
QMdnsEngine::Cache m_MdnsCache;
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
#include "computerseeker.h"
|
||||
#include "computermanager.h"
|
||||
#include <QTimer>
|
||||
|
||||
ComputerSeeker::ComputerSeeker(ComputerManager *manager, QString computerName, QObject *parent)
|
||||
: QObject(parent), m_ComputerManager(manager), m_ComputerName(computerName),
|
||||
m_TimeoutTimer(new QTimer(this))
|
||||
{
|
||||
m_TimeoutTimer->setSingleShot(true);
|
||||
connect(m_TimeoutTimer, &QTimer::timeout,
|
||||
this, &ComputerSeeker::onTimeout);
|
||||
connect(m_ComputerManager, &ComputerManager::computerStateChanged,
|
||||
this, &ComputerSeeker::onComputerUpdated);
|
||||
}
|
||||
|
||||
void ComputerSeeker::start(int timeout)
|
||||
{
|
||||
m_TimeoutTimer->start(timeout);
|
||||
// Seek desired computer by both connecting to it directly (this may fail
|
||||
// if m_ComputerName is UUID, or the name that doesn't resolve to an IP
|
||||
// address) and by polling it using mDNS, hopefully one of these methods
|
||||
// would find the host
|
||||
m_ComputerManager->addNewHost(m_ComputerName, false);
|
||||
m_ComputerManager->startPolling();
|
||||
}
|
||||
|
||||
void ComputerSeeker::onComputerUpdated(NvComputer *computer)
|
||||
{
|
||||
if (!m_TimeoutTimer->isActive()) {
|
||||
return;
|
||||
}
|
||||
if (matchComputer(computer) && isOnline(computer)) {
|
||||
m_ComputerManager->stopPollingAsync();
|
||||
m_TimeoutTimer->stop();
|
||||
emit computerFound(computer);
|
||||
}
|
||||
}
|
||||
|
||||
bool ComputerSeeker::matchComputer(NvComputer *computer) const
|
||||
{
|
||||
QString value = m_ComputerName.toLower();
|
||||
|
||||
if (computer->name.toLower() == value || computer->uuid.toLower() == value) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (const QString& addr : computer->uniqueAddresses()) {
|
||||
if (addr.toLower() == value) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ComputerSeeker::isOnline(NvComputer *computer) const
|
||||
{
|
||||
return computer->state == NvComputer::CS_ONLINE;
|
||||
}
|
||||
|
||||
void ComputerSeeker::onTimeout()
|
||||
{
|
||||
m_TimeoutTimer->stop();
|
||||
m_ComputerManager->stopPollingAsync();
|
||||
emit errorTimeout();
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
|
||||
class ComputerManager;
|
||||
class NvComputer;
|
||||
class QTimer;
|
||||
|
||||
class ComputerSeeker : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit ComputerSeeker(ComputerManager *manager, QString computerName, QObject *parent = nullptr);
|
||||
|
||||
void start(int timeout);
|
||||
|
||||
signals:
|
||||
void computerFound(NvComputer *computer);
|
||||
void errorTimeout();
|
||||
|
||||
private slots:
|
||||
void onComputerUpdated(NvComputer *computer);
|
||||
void onTimeout();
|
||||
|
||||
private:
|
||||
bool matchComputer(NvComputer *computer) const;
|
||||
bool isOnline(NvComputer *computer) const;
|
||||
|
||||
private:
|
||||
ComputerManager *m_ComputerManager;
|
||||
QString m_ComputerName;
|
||||
QTimer *m_TimeoutTimer;
|
||||
};
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
#include <openssl/pem.h>
|
||||
#include <openssl/rsa.h>
|
||||
#include <openssl/bn.h>
|
||||
#include <openssl/x509.h>
|
||||
#include <openssl/rand.h>
|
||||
|
||||
@@ -32,20 +31,19 @@ void IdentityManager::createCredentials(QSettings& settings)
|
||||
X509* cert = X509_new();
|
||||
THROW_BAD_ALLOC_IF_NULL(cert);
|
||||
|
||||
EVP_PKEY* pk = EVP_PKEY_new();
|
||||
EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, NULL);
|
||||
THROW_BAD_ALLOC_IF_NULL(ctx);
|
||||
|
||||
EVP_PKEY_keygen_init(ctx);
|
||||
EVP_PKEY_CTX_set_rsa_keygen_bits(ctx, 2048);
|
||||
|
||||
// pk must be initialized on input
|
||||
EVP_PKEY* pk = NULL;
|
||||
EVP_PKEY_keygen(ctx, &pk);
|
||||
|
||||
EVP_PKEY_CTX_free(ctx);
|
||||
THROW_BAD_ALLOC_IF_NULL(pk);
|
||||
|
||||
BIGNUM* bne = BN_new();
|
||||
THROW_BAD_ALLOC_IF_NULL(bne);
|
||||
|
||||
RSA* rsa = RSA_new();
|
||||
THROW_BAD_ALLOC_IF_NULL(rsa);
|
||||
|
||||
BN_set_word(bne, RSA_F4);
|
||||
RSA_generate_key_ex(rsa, 2048, bne, nullptr);
|
||||
|
||||
EVP_PKEY_assign_RSA(pk, rsa);
|
||||
|
||||
X509_set_version(cert, 2);
|
||||
ASN1_INTEGER_set(X509_get_serialNumber(cert), 0);
|
||||
#if OPENSSL_VERSION_NUMBER < 0x10100000L
|
||||
@@ -75,7 +73,7 @@ void IdentityManager::createCredentials(QSettings& settings)
|
||||
-1, -1, 0);
|
||||
X509_set_issuer_name(cert, name);
|
||||
|
||||
X509_sign(cert, pk, EVP_sha1());
|
||||
X509_sign(cert, pk, EVP_sha256());
|
||||
|
||||
BIO* biokey = BIO_new(BIO_s_mem());
|
||||
THROW_BAD_ALLOC_IF_NULL(biokey);
|
||||
@@ -94,7 +92,6 @@ void IdentityManager::createCredentials(QSettings& settings)
|
||||
|
||||
X509_free(cert);
|
||||
EVP_PKEY_free(pk);
|
||||
BN_free(bne);
|
||||
BIO_free(biokey);
|
||||
BIO_free(biocert);
|
||||
|
||||
|
||||
@@ -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,7 +1,10 @@
|
||||
#include "nvcomputer.h"
|
||||
#include "nvapp.h"
|
||||
|
||||
#include <QUdpSocket>
|
||||
#include <QHostInfo>
|
||||
#include <QNetworkInterface>
|
||||
#include <QNetworkProxy>
|
||||
|
||||
#define SER_NAME "hostname"
|
||||
#define SER_UUID "uuid"
|
||||
@@ -9,31 +12,28 @@
|
||||
#define SER_LOCALADDR "localaddress"
|
||||
#define SER_REMOTEADDR "remoteaddress"
|
||||
#define SER_MANUALADDR "manualaddress"
|
||||
#define SER_IPV6ADDR "ipv6address"
|
||||
#define SER_APPLIST "apps"
|
||||
|
||||
#define SER_APPNAME "name"
|
||||
#define SER_APPID "id"
|
||||
#define SER_APPHDR "hdr"
|
||||
#define SER_SRVCERT "srvcert"
|
||||
#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);
|
||||
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();
|
||||
@@ -51,16 +51,19 @@ 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());
|
||||
|
||||
// Avoid deleting an existing applist if we couldn't get one
|
||||
if (!appList.isEmpty()) {
|
||||
@@ -68,10 +71,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();
|
||||
}
|
||||
@@ -84,8 +84,11 @@ void NvComputer::sortAppList()
|
||||
});
|
||||
}
|
||||
|
||||
NvComputer::NvComputer(QString address, QString serverInfo)
|
||||
NvComputer::NvComputer(QString address, QString serverInfo, QSslCertificate serverCert)
|
||||
{
|
||||
this->serverCert = serverCert;
|
||||
|
||||
this->hasCustomName = false;
|
||||
this->name = NvHTTP::getXmlString(serverInfo, "hostname");
|
||||
if (this->name.isEmpty()) {
|
||||
this->name = "UNKNOWN";
|
||||
@@ -123,7 +126,12 @@ NvComputer::NvComputer(QString address, QString serverInfo)
|
||||
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;
|
||||
@@ -149,8 +157,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
|
||||
@@ -167,6 +176,32 @@ bool NvComputer::wake()
|
||||
QVector<QString> addressList = uniqueAddresses();
|
||||
addressList.append("255.255.255.255");
|
||||
|
||||
// Try to broadcast on all available NICs
|
||||
for (const QNetworkInterface& nic : QNetworkInterface::allInterfaces()) {
|
||||
// Ensure the interface is up and skip the loopback adapter
|
||||
if ((nic.flags() & QNetworkInterface::IsUp) == 0 ||
|
||||
(nic.flags() & QNetworkInterface::IsLoopBack) != 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
QHostAddress allNodesMulticast("FF02::1");
|
||||
for (const QNetworkAddressEntry& addr : nic.addressEntries()) {
|
||||
// Store the scope ID for this NIC if IPv6 is enabled
|
||||
if (!addr.ip().scopeId().isEmpty()) {
|
||||
allNodesMulticast.setScopeId(addr.ip().scopeId());
|
||||
}
|
||||
|
||||
// Skip IPv6 which doesn't support broadcast
|
||||
if (!addr.broadcast().isNull()) {
|
||||
addressList.append(addr.broadcast().toString());
|
||||
}
|
||||
}
|
||||
|
||||
if (!allNodesMulticast.scopeId().isEmpty()) {
|
||||
addressList.append(allNodesMulticast.toString());
|
||||
}
|
||||
}
|
||||
|
||||
// Try all unique address strings or host names
|
||||
bool success = false;
|
||||
for (QString& addressString : addressList) {
|
||||
@@ -181,16 +216,14 @@ bool NvComputer::wake()
|
||||
for (QHostAddress& address : hostInfo.addresses()) {
|
||||
QUdpSocket sock;
|
||||
|
||||
// Bind to any address on the correct protocol
|
||||
if (sock.bind(address.protocol() == QUdpSocket::IPv4Protocol ?
|
||||
QHostAddress::AnyIPv4 : QHostAddress::AnyIPv6)) {
|
||||
|
||||
// Send to all ports
|
||||
for (quint16 port : WOL_PORTS) {
|
||||
if (sock.writeDatagram(wolPayload, address, port)) {
|
||||
qInfo().nospace().noquote() << "Send WoL packet to " << name << " via " << address.toString() << ":" << port;
|
||||
success = true;
|
||||
}
|
||||
// Send to all ports
|
||||
for (quint16 port : WOL_PORTS) {
|
||||
if (sock.writeDatagram(wolPayload, address, port)) {
|
||||
qInfo().nospace().noquote() << "Sent WoL packet to " << name << " via " << address.toString() << ":" << port;
|
||||
success = true;
|
||||
}
|
||||
else {
|
||||
qWarning() << "Send failed:" << sock.error();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -199,7 +232,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;
|
||||
|
||||
@@ -207,6 +335,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)
|
||||
@@ -255,10 +384,21 @@ bool NvComputer::update(NvComputer& that)
|
||||
changed = true; \
|
||||
}
|
||||
|
||||
ASSIGN_IF_CHANGED(name);
|
||||
#define ASSIGN_IF_CHANGED_AND_NONNULL(field) \
|
||||
if (!that.field.isNull() && \
|
||||
this->field != that.field) { \
|
||||
this->field = that.field; \
|
||||
changed = true; \
|
||||
}
|
||||
|
||||
if (!hasCustomName) {
|
||||
// Only overwrite the name if it's not custom
|
||||
ASSIGN_IF_CHANGED(name);
|
||||
}
|
||||
ASSIGN_IF_CHANGED_AND_NONEMPTY(macAddress);
|
||||
ASSIGN_IF_CHANGED_AND_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);
|
||||
@@ -269,7 +409,13 @@ bool NvComputer::update(NvComputer& that)
|
||||
ASSIGN_IF_CHANGED(appVersion);
|
||||
ASSIGN_IF_CHANGED(maxLumaPixelsHEVC);
|
||||
ASSIGN_IF_CHANGED(gpuModel);
|
||||
ASSIGN_IF_CHANGED_AND_NONEMPTY(appList);
|
||||
ASSIGN_IF_CHANGED_AND_NONNULL(serverCert);
|
||||
ASSIGN_IF_CHANGED_AND_NONEMPTY(displayModes);
|
||||
|
||||
if (!that.appList.isEmpty()) {
|
||||
// updateAppList() handles merging client-side attributes
|
||||
updateAppList(that.appList);
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
@@ -16,10 +16,12 @@ class NvComputer
|
||||
private:
|
||||
void sortAppList();
|
||||
|
||||
bool updateAppList(QVector<NvApp> newAppList);
|
||||
|
||||
bool pendingQuit;
|
||||
|
||||
public:
|
||||
explicit NvComputer(QString address, QString serverInfo);
|
||||
explicit NvComputer(QString address, QString serverInfo, QSslCertificate serverCert);
|
||||
|
||||
explicit NvComputer(QSettings& settings);
|
||||
|
||||
@@ -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,12 +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;
|
||||
};
|
||||
|
||||
@@ -10,20 +10,49 @@
|
||||
#include <QSslKey>
|
||||
#include <QImageReader>
|
||||
#include <QtEndian>
|
||||
#include <QNetworkProxy>
|
||||
|
||||
#define FAST_FAIL_TIMEOUT_MS 2000
|
||||
#define REQUEST_TIMEOUT_MS 5000
|
||||
#define LAUNCH_TIMEOUT_MS 120000
|
||||
#define RESUME_TIMEOUT_MS 30000
|
||||
#define QUIT_TIMEOUT_MS 30000
|
||||
|
||||
NvHTTP::NvHTTP(QString address) :
|
||||
m_Address(address)
|
||||
NvHTTP::NvHTTP(QString address, QSslCertificate serverCert) :
|
||||
m_ServerCert(serverCert)
|
||||
{
|
||||
m_BaseUrlHttp.setScheme("http");
|
||||
m_BaseUrlHttps.setScheme("https");
|
||||
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)
|
||||
{
|
||||
m_ServerCert = serverCert;
|
||||
}
|
||||
|
||||
void NvHTTP::setAddress(QString address)
|
||||
{
|
||||
Q_ASSERT(!address.isEmpty());
|
||||
|
||||
m_BaseUrlHttp.setScheme("http");
|
||||
m_BaseUrlHttps.setScheme("https");
|
||||
m_Address = address;
|
||||
|
||||
m_BaseUrlHttp.setHost(address);
|
||||
m_BaseUrlHttps.setHost(address);
|
||||
m_BaseUrlHttp.setPort(47989);
|
||||
m_BaseUrlHttps.setPort(47984);
|
||||
}
|
||||
|
||||
QString NvHTTP::address()
|
||||
{
|
||||
return m_Address;
|
||||
}
|
||||
|
||||
QVector<int>
|
||||
@@ -64,68 +93,58 @@ NvHTTP::getCurrentGame(QString serverInfo)
|
||||
}
|
||||
|
||||
QString
|
||||
NvHTTP::getServerInfo(NvLogLevel logLevel)
|
||||
NvHTTP::getServerInfo(NvLogLevel logLevel, bool fastFail)
|
||||
{
|
||||
QString serverInfo;
|
||||
|
||||
try
|
||||
// Check if we have a pinned cert for this host yet
|
||||
if (!m_ServerCert.isNull())
|
||||
{
|
||||
// Always try HTTPS first, since it properly reports
|
||||
// pairing status (and a few other attributes).
|
||||
serverInfo = openConnectionToString(m_BaseUrlHttps,
|
||||
"serverinfo",
|
||||
nullptr,
|
||||
true,
|
||||
logLevel);
|
||||
// Throws if the request failed
|
||||
verifyResponseStatus(serverInfo);
|
||||
}
|
||||
catch (const GfeHttpResponseException& e)
|
||||
{
|
||||
if (e.getStatusCode() == 401)
|
||||
try
|
||||
{
|
||||
// Certificate validation error, fallback to HTTP
|
||||
serverInfo = openConnectionToString(m_BaseUrlHttp,
|
||||
// Always try HTTPS first, since it properly reports
|
||||
// pairing status (and a few other attributes).
|
||||
serverInfo = openConnectionToString(m_BaseUrlHttps,
|
||||
"serverinfo",
|
||||
nullptr,
|
||||
true,
|
||||
fastFail ? FAST_FAIL_TIMEOUT_MS : REQUEST_TIMEOUT_MS,
|
||||
logLevel);
|
||||
// Throws if the request failed
|
||||
verifyResponseStatus(serverInfo);
|
||||
}
|
||||
else
|
||||
catch (const GfeHttpResponseException& e)
|
||||
{
|
||||
// Rethrow real errors
|
||||
throw e;
|
||||
if (e.getStatusCode() == 401)
|
||||
{
|
||||
// Certificate validation error, fallback to HTTP
|
||||
serverInfo = openConnectionToString(m_BaseUrlHttp,
|
||||
"serverinfo",
|
||||
nullptr,
|
||||
fastFail ? FAST_FAIL_TIMEOUT_MS : REQUEST_TIMEOUT_MS,
|
||||
logLevel);
|
||||
verifyResponseStatus(serverInfo);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Rethrow real errors
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Only use HTTP prior to pairing
|
||||
serverInfo = openConnectionToString(m_BaseUrlHttp,
|
||||
"serverinfo",
|
||||
nullptr,
|
||||
fastFail ? FAST_FAIL_TIMEOUT_MS : REQUEST_TIMEOUT_MS,
|
||||
logLevel);
|
||||
verifyResponseStatus(serverInfo);
|
||||
}
|
||||
|
||||
return serverInfo;
|
||||
}
|
||||
|
||||
static QString
|
||||
getSurroundAudioInfoString(int audioConfig)
|
||||
{
|
||||
int channelMask;
|
||||
int channelCount;
|
||||
|
||||
switch (audioConfig)
|
||||
{
|
||||
case AUDIO_CONFIGURATION_STEREO:
|
||||
channelCount = 2;
|
||||
channelMask = 0x3;
|
||||
break;
|
||||
case AUDIO_CONFIGURATION_51_SURROUND:
|
||||
channelCount = 6;
|
||||
channelMask = 0xFC;
|
||||
break;
|
||||
default:
|
||||
Q_ASSERT(false);
|
||||
return 0;
|
||||
}
|
||||
|
||||
return QString::number(channelMask << 16 | channelCount);
|
||||
}
|
||||
|
||||
void
|
||||
NvHTTP::launchApp(int appId,
|
||||
PSTREAM_CONFIGURATION streamConfig,
|
||||
@@ -144,7 +163,11 @@ NvHTTP::launchApp(int appId,
|
||||
"appid="+QString::number(appId)+
|
||||
"&mode="+QString::number(streamConfig->width)+"x"+
|
||||
QString::number(streamConfig->height)+"x"+
|
||||
QString::number(streamConfig->fps)+
|
||||
// Using an FPS value over 60 causes SOPS to default to 720p60,
|
||||
// so force it to 0 to ensure the correct resolution is set. We
|
||||
// used to use 60 here but that locked the frame rate to 60 FPS
|
||||
// on GFE 3.20.3.
|
||||
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)+
|
||||
@@ -152,10 +175,10 @@ 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),
|
||||
false);
|
||||
LAUNCH_TIMEOUT_MS);
|
||||
|
||||
// Throws if the request failed
|
||||
verifyResponseStatus(response);
|
||||
@@ -174,8 +197,8 @@ NvHTTP::resumeApp(PSTREAM_CONFIGURATION streamConfig)
|
||||
"resume",
|
||||
"rikey="+QString(QByteArray(streamConfig->remoteInputAesKey, sizeof(streamConfig->remoteInputAesKey)).toHex())+
|
||||
"&rikeyid="+QString::number(riKeyId)+
|
||||
"&surroundAudioInfo="+getSurroundAudioInfoString(streamConfig->audioConfiguration),
|
||||
false);
|
||||
"&surroundAudioInfo="+QString::number(SURROUNDAUDIOINFO_FROM_AUDIO_CONFIGURATION(streamConfig->audioConfiguration)),
|
||||
RESUME_TIMEOUT_MS);
|
||||
|
||||
// Throws if the request failed
|
||||
verifyResponseStatus(response);
|
||||
@@ -188,14 +211,14 @@ NvHTTP::quitApp()
|
||||
openConnectionToString(m_BaseUrlHttps,
|
||||
"cancel",
|
||||
nullptr,
|
||||
false);
|
||||
QUIT_TIMEOUT_MS);
|
||||
|
||||
// Throws if the request failed
|
||||
verifyResponseStatus(response);
|
||||
|
||||
// Newer GFE versions will just return success even if quitting fails
|
||||
// if we're not the original requestor.
|
||||
if (getCurrentGame(getServerInfo(NvHTTP::ERROR)) != 0) {
|
||||
if (getCurrentGame(getServerInfo(NvHTTP::NVLL_ERROR)) != 0) {
|
||||
// Generate a synthetic GfeResponseException letting the caller know
|
||||
// that they can't kill someone else's stream.
|
||||
throw GfeHttpResponseException(599, "");
|
||||
@@ -210,17 +233,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();
|
||||
}
|
||||
}
|
||||
@@ -235,16 +258,16 @@ NvHTTP::getAppList()
|
||||
QString appxml = openConnectionToString(m_BaseUrlHttps,
|
||||
"applist",
|
||||
nullptr,
|
||||
true,
|
||||
NvLogLevel::ERROR);
|
||||
REQUEST_TIMEOUT_MS,
|
||||
NvLogLevel::NVLL_ERROR);
|
||||
verifyResponseStatus(appxml);
|
||||
|
||||
QXmlStreamReader xmlReader(appxml);
|
||||
QVector<NvApp> apps;
|
||||
while (!xmlReader.atEnd()) {
|
||||
while (xmlReader.readNextStartElement()) {
|
||||
QStringRef name = xmlReader.name();
|
||||
if (xmlReader.name() == "App") {
|
||||
auto name = xmlReader.name();
|
||||
if (name == QString("App")) {
|
||||
// We must have a valid app before advancing to the next one
|
||||
if (!apps.isEmpty() && !apps.last().isInitialized()) {
|
||||
qWarning() << "Invalid applist XML";
|
||||
@@ -253,15 +276,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";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -275,9 +301,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
|
||||
@@ -290,6 +319,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);
|
||||
}
|
||||
}
|
||||
@@ -303,8 +338,8 @@ NvHTTP::getBoxArt(int appId)
|
||||
"appasset",
|
||||
"appid="+QString::number(appId)+
|
||||
"&AssetType=2&AssetIdx=0",
|
||||
true,
|
||||
NvLogLevel::VERBOSE);
|
||||
REQUEST_TIMEOUT_MS,
|
||||
NvLogLevel::NVLL_VERBOSE);
|
||||
QImage image = QImageReader(reply).read();
|
||||
delete reply;
|
||||
|
||||
@@ -346,18 +381,46 @@ NvHTTP::getXmlString(QString xml,
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void NvHTTP::handleSslErrors(QNetworkReply* reply, const QList<QSslError>& errors)
|
||||
{
|
||||
bool ignoreErrors = true;
|
||||
|
||||
if (m_ServerCert.isNull()) {
|
||||
// We should never make an HTTPS request without a cert
|
||||
Q_ASSERT(!m_ServerCert.isNull());
|
||||
return;
|
||||
}
|
||||
|
||||
for (auto error : errors) {
|
||||
if (m_ServerCert != error.certificate()) {
|
||||
ignoreErrors = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (ignoreErrors) {
|
||||
reply->ignoreSslErrors(errors);
|
||||
}
|
||||
}
|
||||
|
||||
QString
|
||||
NvHTTP::openConnectionToString(QUrl baseUrl,
|
||||
QString command,
|
||||
QString arguments,
|
||||
bool enableTimeout,
|
||||
int timeoutMs,
|
||||
NvLogLevel logLevel)
|
||||
{
|
||||
QNetworkReply* reply = openConnection(baseUrl, command, arguments, enableTimeout, logLevel);
|
||||
QNetworkReply* reply = openConnection(baseUrl, command, arguments, timeoutMs, logLevel);
|
||||
QString ret;
|
||||
|
||||
QTextStream stream(reply);
|
||||
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
stream.setEncoding(QStringConverter::Utf8);
|
||||
#else
|
||||
stream.setCodec("UTF-8");
|
||||
#endif
|
||||
|
||||
ret = stream.readAll();
|
||||
delete reply;
|
||||
|
||||
@@ -368,34 +431,43 @@ QNetworkReply*
|
||||
NvHTTP::openConnection(QUrl baseUrl,
|
||||
QString command,
|
||||
QString arguments,
|
||||
bool enableTimeout,
|
||||
int timeoutMs,
|
||||
NvLogLevel logLevel)
|
||||
{
|
||||
// Build a URL for the request
|
||||
QUrl url(baseUrl);
|
||||
url.setPath("/" + command);
|
||||
url.setQuery("uniqueid=" + IdentityManager::get()->getUniqueId() +
|
||||
"&uuid=" + QUuid::createUuid().toRfc4122().toHex() +
|
||||
|
||||
// Use a common UID for Moonlight clients to allow them to quit
|
||||
// games for each other (otherwise GFE gets screwed up and it requires
|
||||
// manual intervention to solve).
|
||||
url.setQuery("uniqueid=0123456789ABCDEF&uuid=" +
|
||||
QUuid::createUuid().toRfc4122().toHex() +
|
||||
((arguments != nullptr) ? ("&" + arguments) : ""));
|
||||
|
||||
QNetworkRequest request = QNetworkRequest(url);
|
||||
QNetworkRequest request(url);
|
||||
|
||||
// Add our client certificate
|
||||
request.setSslConfiguration(IdentityManager::get()->getSslConfig());
|
||||
|
||||
QNetworkReply* reply = m_Nam.get(request);
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(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
|
||||
|
||||
// Ignore self-signed certificate errors (since GFE uses them)
|
||||
reply->ignoreSslErrors();
|
||||
QNetworkReply* reply = m_Nam.get(request);
|
||||
|
||||
// Run the request with a timeout if requested
|
||||
QEventLoop loop;
|
||||
QObject::connect(reply, SIGNAL(finished()), &loop, SLOT(quit()));
|
||||
if (enableTimeout)
|
||||
{
|
||||
QTimer::singleShot(REQUEST_TIMEOUT_MS, &loop, SLOT(quit()));
|
||||
connect(reply, SIGNAL(finished()), &loop, SLOT(quit()));
|
||||
connect(QCoreApplication::instance(), SIGNAL(aboutToQuit()), &loop, SLOT(quit()));
|
||||
if (timeoutMs) {
|
||||
QTimer::singleShot(timeoutMs, &loop, SLOT(quit()));
|
||||
}
|
||||
if (logLevel >= NvLogLevel::VERBOSE) {
|
||||
if (logLevel >= NvLogLevel::NVLL_VERBOSE) {
|
||||
qInfo() << "Executing request:" << url.toString();
|
||||
}
|
||||
loop.exec(QEventLoop::ExcludeUserInputEvents);
|
||||
@@ -403,7 +475,7 @@ NvHTTP::openConnection(QUrl baseUrl,
|
||||
// Abort the request if it timed out
|
||||
if (!reply->isFinished())
|
||||
{
|
||||
if (logLevel >= NvLogLevel::ERROR) {
|
||||
if (logLevel >= NvLogLevel::NVLL_ERROR) {
|
||||
qWarning() << "Aborting timed out request for" << url.toString();
|
||||
}
|
||||
reply->abort();
|
||||
@@ -416,12 +488,27 @@ NvHTTP::openConnection(QUrl baseUrl,
|
||||
// Handle error
|
||||
if (reply->error() != QNetworkReply::NoError)
|
||||
{
|
||||
if (logLevel >= NvLogLevel::ERROR) {
|
||||
if (logLevel >= NvLogLevel::NVLL_ERROR) {
|
||||
qWarning() << command << " request failed with error " << reply->error();
|
||||
}
|
||||
QtNetworkReplyException exception(reply->error(), reply->errorString());
|
||||
delete reply;
|
||||
throw exception;
|
||||
|
||||
if (reply->error() == QNetworkReply::SslHandshakeFailedError) {
|
||||
// This will trigger falling back to HTTP for the serverinfo query
|
||||
// then pairing again to get the updated certificate.
|
||||
GfeHttpResponseException exception(401, "Server certificate mismatch");
|
||||
delete reply;
|
||||
throw exception;
|
||||
}
|
||||
else if (reply->error() == QNetworkReply::OperationCanceledError) {
|
||||
QtNetworkReplyException exception(QNetworkReply::TimeoutError, "Request timed out");
|
||||
delete reply;
|
||||
throw exception;
|
||||
}
|
||||
else {
|
||||
QtNetworkReplyException exception(reply->error(), reply->errorString());
|
||||
delete reply;
|
||||
throw exception;
|
||||
}
|
||||
}
|
||||
|
||||
return reply;
|
||||
|
||||
@@ -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:
|
||||
@@ -113,23 +94,25 @@ private:
|
||||
QString m_ErrorText;
|
||||
};
|
||||
|
||||
class NvHTTP
|
||||
class NvHTTP : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
enum NvLogLevel {
|
||||
NONE,
|
||||
ERROR,
|
||||
VERBOSE
|
||||
NVLL_NONE,
|
||||
NVLL_ERROR,
|
||||
NVLL_VERBOSE
|
||||
};
|
||||
|
||||
explicit NvHTTP(QString address);
|
||||
explicit NvHTTP(QString address, QSslCertificate serverCert);
|
||||
|
||||
static
|
||||
int
|
||||
getCurrentGame(QString serverInfo);
|
||||
|
||||
QString
|
||||
getServerInfo(NvLogLevel logLevel);
|
||||
getServerInfo(NvLogLevel logLevel, bool fastFail = false);
|
||||
|
||||
static
|
||||
void
|
||||
@@ -149,8 +132,14 @@ public:
|
||||
openConnectionToString(QUrl baseUrl,
|
||||
QString command,
|
||||
QString arguments,
|
||||
bool enableTimeout,
|
||||
NvLogLevel logLevel = NvLogLevel::VERBOSE);
|
||||
int timeoutMs,
|
||||
NvLogLevel logLevel = NvLogLevel::NVLL_VERBOSE);
|
||||
|
||||
void setServerCert(QSslCertificate serverCert);
|
||||
|
||||
void setAddress(QString address);
|
||||
|
||||
QString address();
|
||||
|
||||
static
|
||||
QVector<int>
|
||||
@@ -182,13 +171,17 @@ public:
|
||||
QUrl m_BaseUrlHttp;
|
||||
QUrl m_BaseUrlHttps;
|
||||
private:
|
||||
void
|
||||
handleSslErrors(QNetworkReply* reply, const QList<QSslError>& errors);
|
||||
|
||||
QNetworkReply*
|
||||
openConnection(QUrl baseUrl,
|
||||
QString command,
|
||||
QString arguments,
|
||||
bool enableTimeout,
|
||||
int timeoutMs,
|
||||
NvLogLevel logLevel);
|
||||
|
||||
QString m_Address;
|
||||
QNetworkAccessManager m_Nam;
|
||||
QSslCertificate m_ServerCert;
|
||||
};
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
#include "nvpairingmanager.h"
|
||||
#include "utils.h"
|
||||
|
||||
#include <stdexcept>
|
||||
|
||||
#include <openssl/bio.h>
|
||||
#include <openssl/aes.h>
|
||||
#include <openssl/rand.h>
|
||||
#include <openssl/pem.h>
|
||||
#include <openssl/x509.h>
|
||||
#include <openssl/evp.h>
|
||||
|
||||
#define REQUEST_TIMEOUT_MS 5000
|
||||
|
||||
NvPairingManager::NvPairingManager(QString address) :
|
||||
m_Http(address)
|
||||
m_Http(address, QSslCertificate())
|
||||
{
|
||||
QByteArray cert = IdentityManager::get()->getCertificate();
|
||||
BIO *bio = BIO_new_mem_buf(cert.data(), -1);
|
||||
@@ -49,39 +52,63 @@ NvPairingManager::generateRandomBytes(int length)
|
||||
}
|
||||
|
||||
QByteArray
|
||||
NvPairingManager::encrypt(QByteArray plaintext, AES_KEY* key)
|
||||
NvPairingManager::encrypt(const QByteArray& plaintext, const QByteArray& key)
|
||||
{
|
||||
QByteArray ciphertext(plaintext.size(), 0);
|
||||
EVP_CIPHER_CTX* cipher;
|
||||
int ciphertextLen;
|
||||
|
||||
for (int i = 0; i < plaintext.size(); i += 16)
|
||||
{
|
||||
AES_encrypt(reinterpret_cast<unsigned char*>(&plaintext.data()[i]),
|
||||
reinterpret_cast<unsigned char*>(&ciphertext.data()[i]),
|
||||
key);
|
||||
}
|
||||
cipher = EVP_CIPHER_CTX_new();
|
||||
THROW_BAD_ALLOC_IF_NULL(cipher);
|
||||
|
||||
EVP_EncryptInit(cipher, EVP_aes_128_ecb(), reinterpret_cast<const unsigned char*>(key.data()), NULL);
|
||||
EVP_CIPHER_CTX_set_padding(cipher, 0);
|
||||
|
||||
EVP_EncryptUpdate(cipher,
|
||||
reinterpret_cast<unsigned char*>(ciphertext.data()),
|
||||
&ciphertextLen,
|
||||
reinterpret_cast<const unsigned char*>(plaintext.data()),
|
||||
plaintext.length());
|
||||
Q_ASSERT(ciphertextLen == ciphertext.length());
|
||||
|
||||
EVP_CIPHER_CTX_free(cipher);
|
||||
|
||||
return ciphertext;
|
||||
}
|
||||
|
||||
QByteArray
|
||||
NvPairingManager::decrypt(QByteArray ciphertext, AES_KEY* key)
|
||||
NvPairingManager::decrypt(const QByteArray& ciphertext, const QByteArray& key)
|
||||
{
|
||||
QByteArray plaintext(ciphertext.size(), 0);
|
||||
EVP_CIPHER_CTX* cipher;
|
||||
int plaintextLen;
|
||||
|
||||
for (int i = 0; i < plaintext.size(); i += 16)
|
||||
{
|
||||
AES_decrypt(reinterpret_cast<unsigned char*>(&ciphertext.data()[i]),
|
||||
reinterpret_cast<unsigned char*>(&plaintext.data()[i]),
|
||||
key);
|
||||
}
|
||||
cipher = EVP_CIPHER_CTX_new();
|
||||
THROW_BAD_ALLOC_IF_NULL(cipher);
|
||||
|
||||
EVP_DecryptInit(cipher, EVP_aes_128_ecb(), reinterpret_cast<const unsigned char*>(key.data()), NULL);
|
||||
EVP_CIPHER_CTX_set_padding(cipher, 0);
|
||||
|
||||
EVP_DecryptUpdate(cipher,
|
||||
reinterpret_cast<unsigned char*>(plaintext.data()),
|
||||
&plaintextLen,
|
||||
reinterpret_cast<const unsigned char*>(ciphertext.data()),
|
||||
ciphertext.length());
|
||||
Q_ASSERT(plaintextLen == plaintext.length());
|
||||
|
||||
EVP_CIPHER_CTX_free(cipher);
|
||||
|
||||
return plaintext;
|
||||
}
|
||||
|
||||
QByteArray
|
||||
NvPairingManager::getSignatureFromPemCert(QByteArray certificate)
|
||||
NvPairingManager::getSignatureFromPemCert(const QByteArray& certificate)
|
||||
{
|
||||
#if (OPENSSL_VERSION_NUMBER < 0x10100000L)
|
||||
BIO* bio = BIO_new_mem_buf(const_cast<char*>(certificate.data()), -1);
|
||||
#else
|
||||
BIO* bio = BIO_new_mem_buf(certificate.data(), -1);
|
||||
#endif
|
||||
THROW_BAD_ALLOC_IF_NULL(bio);
|
||||
|
||||
X509* cert = PEM_read_bio_X509(bio, nullptr, nullptr, nullptr);
|
||||
@@ -105,9 +132,13 @@ NvPairingManager::getSignatureFromPemCert(QByteArray certificate)
|
||||
}
|
||||
|
||||
bool
|
||||
NvPairingManager::verifySignature(QByteArray data, QByteArray signature, QByteArray serverCertificate)
|
||||
NvPairingManager::verifySignature(const QByteArray& data, const QByteArray& signature, const QByteArray& serverCertificate)
|
||||
{
|
||||
#if (OPENSSL_VERSION_NUMBER < 0x10100000L)
|
||||
BIO* bio = BIO_new_mem_buf(const_cast<char*>(serverCertificate.data()), -1);
|
||||
#else
|
||||
BIO* bio = BIO_new_mem_buf(serverCertificate.data(), -1);
|
||||
#endif
|
||||
THROW_BAD_ALLOC_IF_NULL(bio);
|
||||
|
||||
X509* cert = PEM_read_bio_X509(bio, nullptr, nullptr, nullptr);
|
||||
@@ -121,7 +152,7 @@ NvPairingManager::verifySignature(QByteArray data, QByteArray signature, QByteAr
|
||||
|
||||
EVP_DigestVerifyInit(mdctx, nullptr, EVP_sha256(), nullptr, pubKey);
|
||||
EVP_DigestVerifyUpdate(mdctx, data.data(), data.length());
|
||||
int result = EVP_DigestVerifyFinal(mdctx, reinterpret_cast<unsigned char*>(signature.data()), signature.length());
|
||||
int result = EVP_DigestVerifyFinal(mdctx, reinterpret_cast<unsigned char*>(const_cast<char*>(signature.data())), signature.length());
|
||||
|
||||
EVP_PKEY_free(pubKey);
|
||||
EVP_MD_CTX_destroy(mdctx);
|
||||
@@ -131,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);
|
||||
@@ -141,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);
|
||||
@@ -155,13 +186,13 @@ NvPairingManager::signMessage(QByteArray message)
|
||||
}
|
||||
|
||||
QByteArray
|
||||
NvPairingManager::saltPin(QByteArray salt, QString pin)
|
||||
NvPairingManager::saltPin(const QByteArray& salt, QString pin)
|
||||
{
|
||||
return QByteArray().append(salt).append(pin.toLatin1());
|
||||
}
|
||||
|
||||
NvPairingManager::PairState
|
||||
NvPairingManager::pair(QString appVersion, QString pin)
|
||||
NvPairingManager::pair(QString appVersion, QString pin, QSslCertificate& serverCert)
|
||||
{
|
||||
int serverMajorVersion = NvHTTP::parseQuad(appVersion).at(0);
|
||||
qInfo() << "Pairing with server generation:" << serverMajorVersion;
|
||||
@@ -184,15 +215,14 @@ NvPairingManager::pair(QString appVersion, QString pin)
|
||||
QByteArray salt = generateRandomBytes(16);
|
||||
QByteArray saltedPin = saltPin(salt, pin);
|
||||
|
||||
AES_KEY encKey, decKey;
|
||||
AES_set_decrypt_key(reinterpret_cast<const unsigned char*>(QCryptographicHash::hash(saltedPin, hashAlgo).data()), 128, &decKey);
|
||||
AES_set_encrypt_key(reinterpret_cast<const unsigned char*>(QCryptographicHash::hash(saltedPin, hashAlgo).data()), 128, &encKey);
|
||||
QByteArray aesKey = QCryptographicHash::hash(saltedPin, hashAlgo).data();
|
||||
aesKey.truncate(16);
|
||||
|
||||
QString getCert = m_Http.openConnectionToString(m_Http.m_BaseUrlHttp,
|
||||
"pair",
|
||||
"devicename=roth&updateState=1&phrase=getservercert&salt=" +
|
||||
salt.toHex() + "&clientcert=" + IdentityManager::get()->getCertificate().toHex(),
|
||||
false);
|
||||
0);
|
||||
NvHTTP::verifyResponseStatus(getCert);
|
||||
if (NvHTTP::getXmlString(getCert, "paired") != "1")
|
||||
{
|
||||
@@ -200,30 +230,43 @@ NvPairingManager::pair(QString appVersion, QString pin)
|
||||
return PairState::FAILED;
|
||||
}
|
||||
|
||||
QByteArray serverCert = NvHTTP::getXmlStringFromHex(getCert, "plaincert");
|
||||
if (serverCert == nullptr)
|
||||
QByteArray serverCertStr = NvHTTP::getXmlStringFromHex(getCert, "plaincert");
|
||||
if (serverCertStr == nullptr)
|
||||
{
|
||||
qCritical() << "Server likely already pairing";
|
||||
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
|
||||
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, REQUEST_TIMEOUT_MS);
|
||||
return PairState::ALREADY_IN_PROGRESS;
|
||||
}
|
||||
|
||||
QSslCertificate unverifiedServerCert = QSslCertificate(serverCertStr);
|
||||
if (unverifiedServerCert.isNull()) {
|
||||
Q_ASSERT(!unverifiedServerCert.isNull());
|
||||
|
||||
qCritical() << "Failed to parse plaincert";
|
||||
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, REQUEST_TIMEOUT_MS);
|
||||
return PairState::FAILED;
|
||||
}
|
||||
|
||||
// Pin this cert for TLS until pairing is complete. If successful, we will propagate
|
||||
// the cert into the NvComputer object and persist it.
|
||||
m_Http.setServerCert(unverifiedServerCert);
|
||||
|
||||
QByteArray randomChallenge = generateRandomBytes(16);
|
||||
QByteArray encryptedChallenge = encrypt(randomChallenge, &encKey);
|
||||
QByteArray encryptedChallenge = encrypt(randomChallenge, aesKey);
|
||||
QString challengeXml = m_Http.openConnectionToString(m_Http.m_BaseUrlHttp,
|
||||
"pair",
|
||||
"devicename=roth&updateState=1&clientchallenge=" +
|
||||
encryptedChallenge.toHex(),
|
||||
true);
|
||||
REQUEST_TIMEOUT_MS);
|
||||
NvHTTP::verifyResponseStatus(challengeXml);
|
||||
if (NvHTTP::getXmlString(challengeXml, "paired") != "1")
|
||||
{
|
||||
qCritical() << "Failed pairing at stage #2";
|
||||
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
|
||||
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, REQUEST_TIMEOUT_MS);
|
||||
return PairState::FAILED;
|
||||
}
|
||||
|
||||
QByteArray challengeResponseData = decrypt(m_Http.getXmlStringFromHex(challengeXml, "challengeresponse"), &decKey);
|
||||
QByteArray challengeResponseData = decrypt(m_Http.getXmlStringFromHex(challengeXml, "challengeresponse"), aesKey);
|
||||
QByteArray clientSecretData = generateRandomBytes(16);
|
||||
QByteArray challengeResponse;
|
||||
QByteArray serverResponse(challengeResponseData.data(), hashLength);
|
||||
@@ -242,17 +285,19 @@ NvPairingManager::pair(QString appVersion, QString pin)
|
||||
challengeResponse.append(reinterpret_cast<char*>(asnSignature->data), asnSignature->length);
|
||||
challengeResponse.append(clientSecretData);
|
||||
|
||||
QByteArray encryptedChallengeResponseHash = encrypt(QCryptographicHash::hash(challengeResponse, hashAlgo), &encKey);
|
||||
QByteArray paddedHash = QCryptographicHash::hash(challengeResponse, hashAlgo);
|
||||
paddedHash.resize(32);
|
||||
QByteArray encryptedChallengeResponseHash = encrypt(paddedHash, aesKey);
|
||||
QString respXml = m_Http.openConnectionToString(m_Http.m_BaseUrlHttp,
|
||||
"pair",
|
||||
"devicename=roth&updateState=1&serverchallengeresp=" +
|
||||
encryptedChallengeResponseHash.toHex(),
|
||||
true);
|
||||
REQUEST_TIMEOUT_MS);
|
||||
NvHTTP::verifyResponseStatus(respXml);
|
||||
if (NvHTTP::getXmlString(respXml, "paired") != "1")
|
||||
{
|
||||
qCritical() << "Failed pairing at stage #3";
|
||||
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
|
||||
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, REQUEST_TIMEOUT_MS);
|
||||
return PairState::FAILED;
|
||||
}
|
||||
|
||||
@@ -262,21 +307,21 @@ NvPairingManager::pair(QString appVersion, QString pin)
|
||||
|
||||
if (!verifySignature(serverSecret,
|
||||
serverSignature,
|
||||
serverCert))
|
||||
serverCertStr))
|
||||
{
|
||||
qCritical() << "MITM detected";
|
||||
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
|
||||
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, REQUEST_TIMEOUT_MS);
|
||||
return PairState::FAILED;
|
||||
}
|
||||
|
||||
QByteArray expectedResponseData;
|
||||
expectedResponseData.append(randomChallenge);
|
||||
expectedResponseData.append(getSignatureFromPemCert(serverCert));
|
||||
expectedResponseData.append(getSignatureFromPemCert(serverCertStr));
|
||||
expectedResponseData.append(serverSecret);
|
||||
if (QCryptographicHash::hash(expectedResponseData, hashAlgo) != serverResponse)
|
||||
{
|
||||
qCritical() << "Incorrect PIN";
|
||||
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
|
||||
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, REQUEST_TIMEOUT_MS);
|
||||
return PairState::PIN_WRONG;
|
||||
}
|
||||
|
||||
@@ -288,26 +333,27 @@ NvPairingManager::pair(QString appVersion, QString pin)
|
||||
"pair",
|
||||
"devicename=roth&updateState=1&clientpairingsecret=" +
|
||||
clientPairingSecret.toHex(),
|
||||
true);
|
||||
REQUEST_TIMEOUT_MS);
|
||||
NvHTTP::verifyResponseStatus(secretRespXml);
|
||||
if (NvHTTP::getXmlString(secretRespXml, "paired") != "1")
|
||||
{
|
||||
qCritical() << "Failed pairing at stage #4";
|
||||
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
|
||||
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, REQUEST_TIMEOUT_MS);
|
||||
return PairState::FAILED;
|
||||
}
|
||||
|
||||
QString pairChallengeXml = m_Http.openConnectionToString(m_Http.m_BaseUrlHttps,
|
||||
"pair",
|
||||
"devicename=roth&updateState=1&phrase=pairchallenge",
|
||||
true);
|
||||
REQUEST_TIMEOUT_MS);
|
||||
NvHTTP::verifyResponseStatus(pairChallengeXml);
|
||||
if (NvHTTP::getXmlString(pairChallengeXml, "paired") != "1")
|
||||
{
|
||||
qCritical() << "Failed pairing at stage #5";
|
||||
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
|
||||
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, REQUEST_TIMEOUT_MS);
|
||||
return PairState::FAILED;
|
||||
}
|
||||
|
||||
serverCert = std::move(unverifiedServerCert);
|
||||
return PairState::PAIRED;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
#include "identitymanager.h"
|
||||
#include "nvhttp.h"
|
||||
|
||||
#include <openssl/aes.h>
|
||||
#include <openssl/x509.h>
|
||||
#include <openssl/evp.h>
|
||||
|
||||
@@ -23,29 +22,29 @@ public:
|
||||
~NvPairingManager();
|
||||
|
||||
PairState
|
||||
pair(QString appVersion, QString pin);
|
||||
pair(QString appVersion, QString pin, QSslCertificate& serverCert);
|
||||
|
||||
private:
|
||||
QByteArray
|
||||
generateRandomBytes(int length);
|
||||
|
||||
QByteArray
|
||||
saltPin(QByteArray salt, QString pin);
|
||||
saltPin(const QByteArray& salt, QString pin);
|
||||
|
||||
QByteArray
|
||||
encrypt(QByteArray plaintext, AES_KEY* key);
|
||||
encrypt(const QByteArray& plaintext, const QByteArray& key);
|
||||
|
||||
QByteArray
|
||||
decrypt(QByteArray ciphertext, AES_KEY* key);
|
||||
decrypt(const QByteArray& ciphertext, const QByteArray& key);
|
||||
|
||||
QByteArray
|
||||
getSignatureFromPemCert(QByteArray certificate);
|
||||
getSignatureFromPemCert(const QByteArray& certificate);
|
||||
|
||||
bool
|
||||
verifySignature(QByteArray data, QByteArray signature, QByteArray serverCertificate);
|
||||
verifySignature(const QByteArray& data, const QByteArray& signature, const QByteArray& serverCertificate);
|
||||
|
||||
QByteArray
|
||||
signMessage(QByteArray message);
|
||||
signMessage(const QByteArray& message);
|
||||
|
||||
NvHTTP m_Http;
|
||||
X509* m_Cert;
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
#include "richpresencemanager.h"
|
||||
|
||||
#include <QDebug>
|
||||
|
||||
RichPresenceManager::RichPresenceManager(StreamingPreferences& prefs, QString gameName)
|
||||
: m_DiscordActive(false)
|
||||
{
|
||||
#ifdef HAVE_DISCORD
|
||||
if (prefs.richPresence) {
|
||||
DiscordEventHandlers handlers = {};
|
||||
handlers.ready = discordReady;
|
||||
handlers.disconnected = discordDisconnected;
|
||||
handlers.errored = discordErrored;
|
||||
Discord_Initialize("594668102021677159", &handlers, 0, nullptr);
|
||||
m_DiscordActive = true;
|
||||
}
|
||||
|
||||
if (m_DiscordActive) {
|
||||
QByteArray stateStr = (QString("Streaming ") + gameName).toUtf8();
|
||||
|
||||
DiscordRichPresence discordPresence = {};
|
||||
discordPresence.state = stateStr.data();
|
||||
discordPresence.startTimestamp = time(nullptr);
|
||||
discordPresence.largeImageKey = "icon";
|
||||
Discord_UpdatePresence(&discordPresence);
|
||||
}
|
||||
#else
|
||||
Q_UNUSED(prefs)
|
||||
Q_UNUSED(gameName)
|
||||
#endif
|
||||
}
|
||||
|
||||
RichPresenceManager::~RichPresenceManager()
|
||||
{
|
||||
#ifdef HAVE_DISCORD
|
||||
if (m_DiscordActive) {
|
||||
Discord_ClearPresence();
|
||||
Discord_Shutdown();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void RichPresenceManager::runCallbacks()
|
||||
{
|
||||
#ifdef HAVE_DISCORD
|
||||
if (m_DiscordActive) {
|
||||
Discord_RunCallbacks();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef HAVE_DISCORD
|
||||
void RichPresenceManager::discordReady(const DiscordUser* request)
|
||||
{
|
||||
qInfo() << "Discord integration ready for user:" << request->username;
|
||||
}
|
||||
|
||||
void RichPresenceManager::discordDisconnected(int errorCode, const char *message)
|
||||
{
|
||||
qInfo() << "Discord integration disconnected:" << errorCode << message;
|
||||
}
|
||||
|
||||
void RichPresenceManager::discordErrored(int errorCode, const char *message)
|
||||
{
|
||||
qWarning() << "Discord integration error:" << errorCode << message;
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,26 @@
|
||||
#pragma once
|
||||
|
||||
#include "settings/streamingpreferences.h"
|
||||
|
||||
#ifdef HAVE_DISCORD
|
||||
#include <discord_rpc.h>
|
||||
#endif
|
||||
|
||||
class RichPresenceManager
|
||||
{
|
||||
public:
|
||||
RichPresenceManager(StreamingPreferences& prefs, QString gameName);
|
||||
~RichPresenceManager();
|
||||
|
||||
void runCallbacks();
|
||||
|
||||
private:
|
||||
#ifdef HAVE_DISCORD
|
||||
static void discordReady(const DiscordUser* request);
|
||||
static void discordDisconnected(int errorCode, const char* message);
|
||||
static void discordErrored(int errorCode, const char* message);
|
||||
#endif
|
||||
|
||||
bool m_DiscordActive;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
#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);
|
||||
hasWindowManager = WMUtils::isRunningWindowManager();
|
||||
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();
|
||||
}
|
||||
|
||||
#ifndef STEAM_LINK
|
||||
// Assume we can probably launch a browser if we're in a GUI environment
|
||||
hasBrowser = hasWindowManager;
|
||||
#else
|
||||
hasBrowser = false;
|
||||
#endif
|
||||
|
||||
#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);
|
||||
}
|
||||
|
||||
void SystemProperties::querySdlVideoInfo()
|
||||
{
|
||||
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).
|
||||
refreshDisplays();
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
void SystemProperties::refreshDisplays()
|
||||
{
|
||||
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s",
|
||||
SDL_GetError());
|
||||
return;
|
||||
}
|
||||
|
||||
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,53 @@
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QRect>
|
||||
|
||||
class SystemProperties : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
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 hasWindowManager MEMBER hasWindowManager 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();
|
||||
|
||||
bool hasHardwareAcceleration;
|
||||
bool rendererAlwaysFullScreen;
|
||||
bool isRunningWayland;
|
||||
bool isRunningXWayland;
|
||||
bool isWow64;
|
||||
QString friendlyNativeArchName;
|
||||
bool hasWindowManager;
|
||||
bool hasBrowser;
|
||||
bool hasDiscordIntegration;
|
||||
QString unmappedGamepads;
|
||||
int maximumStreamingFrameRate;
|
||||
QSize maximumResolution;
|
||||
QList<QRect> monitorDesktopResolutions;
|
||||
QList<QRect> monitorNativeResolutions;
|
||||
QString versionString;
|
||||
};
|
||||
|
||||
@@ -59,7 +59,7 @@ public:
|
||||
|
||||
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";
|
||||
@@ -164,6 +164,7 @@ GlobalCommandLineParser::ParseResult GlobalCommandLineParser::parse(const QStrin
|
||||
"Starts Moonlight normally if no arguments are given.\n"
|
||||
"\n"
|
||||
"Available actions:\n"
|
||||
" quit Quit the currently running app\n"
|
||||
" stream Start streaming an app\n"
|
||||
"\n"
|
||||
"See 'moonlight <action> --help' for help of specific action."
|
||||
@@ -171,19 +172,74 @@ 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 == "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"));
|
||||
}
|
||||
}
|
||||
|
||||
QuitCommandLineParser::QuitCommandLineParser()
|
||||
{
|
||||
}
|
||||
|
||||
QuitCommandLineParser::~QuitCommandLineParser()
|
||||
{
|
||||
}
|
||||
|
||||
void QuitCommandLineParser::parse(const QStringList &args)
|
||||
{
|
||||
CommandLineParser parser;
|
||||
parser.setupCommonOptions();
|
||||
parser.setApplicationDescription(
|
||||
"\n"
|
||||
"Quit the currently running app on the given host."
|
||||
);
|
||||
parser.addPositionalArgument("quit", "quit running app");
|
||||
parser.addPositionalArgument("host", "Host computer name, UUID, or IP address", "<host>");
|
||||
|
||||
if (!parser.parse(args)) {
|
||||
parser.showError(parser.errorText());
|
||||
}
|
||||
|
||||
parser.handleUnknownOptions();
|
||||
|
||||
// This method will not return and terminates the process if --version or
|
||||
// --help is specified
|
||||
parser.handleHelpAndVersionOptions();
|
||||
|
||||
// Verify that host has been provided
|
||||
auto posArgs = parser.positionalArguments();
|
||||
if (posArgs.length() < 2) {
|
||||
parser.showError("Host not provided");
|
||||
}
|
||||
m_Host = parser.positionalArguments().at(1);
|
||||
}
|
||||
|
||||
QString QuitCommandLineParser::getHost() const
|
||||
{
|
||||
return m_Host;
|
||||
}
|
||||
|
||||
StreamCommandLineParser::StreamCommandLineParser()
|
||||
@@ -196,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},
|
||||
@@ -204,8 +261,8 @@ 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},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -235,12 +292,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("mouse-acceleration", "mouse acceleration");
|
||||
parser.addToggleOption("quit-after", "quit app after session");
|
||||
parser.addToggleOption("absolute-mouse", "remote desktop optimized mouse control");
|
||||
parser.addToggleOption("mouse-buttons-swap", "left and right mouse buttons swap");
|
||||
parser.addToggleOption("touchscreen-trackpad", "touchscreen in trackpad mode");
|
||||
parser.addToggleOption("game-optimization", "game optimizations");
|
||||
parser.addToggleOption("audio-on-host", "audio on host PC");
|
||||
parser.addToggleOption("frame-pacing", "frame pacing");
|
||||
parser.addToggleOption("mute-on-focus-loss", "mute audio when Moonlight window loses focus");
|
||||
parser.addToggleOption("background-gamepad", "background gamepad input");
|
||||
parser.addToggleOption("reverse-scroll-direction", "inverted scroll direction");
|
||||
parser.addToggleOption("swap-gamepad-buttons", "swap A/B and X/Y gamepad buttons (Nintendo-style)");
|
||||
parser.addToggleOption("capture-system-keys", "capture system key combos in fullscreen mode");
|
||||
parser.addChoiceOption("video-codec", "video codec", m_VideoCodecMap.keys());
|
||||
parser.addChoiceOption("video-decoder", "video decoder", m_VideoDecoderMap.keys());
|
||||
|
||||
@@ -298,6 +365,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"));
|
||||
@@ -314,8 +389,17 @@ void StreamCommandLineParser::parse(const QStringList &args, StreamingPreference
|
||||
// Resolve --multi-controller and --no-multi-controller options
|
||||
preferences->multiController = parser.getToggleOptionValue("multi-controller", preferences->multiController);
|
||||
|
||||
// Resolve --mouse-acceleration and --no-mouse-acceleration options
|
||||
preferences->mouseAcceleration = parser.getToggleOptionValue("mouse-acceleration", preferences->mouseAcceleration);
|
||||
// Resolve --quit-after and --no-quit-after options
|
||||
preferences->quitAppAfter = parser.getToggleOptionValue("quit-after", preferences->quitAppAfter);
|
||||
|
||||
// Resolve --absolute-mouse and --no-absolute-mouse options
|
||||
preferences->absoluteMouseMode = parser.getToggleOptionValue("absolute-mouse", preferences->absoluteMouseMode);
|
||||
|
||||
// Resolve --mouse-buttons-swap and --no-mouse-buttons-swap options
|
||||
preferences->swapMouseButtons = parser.getToggleOptionValue("mouse-buttons-swap", preferences->swapMouseButtons);
|
||||
|
||||
// Resolve --touchscreen-trackpad and --no-touchscreen-trackpad options
|
||||
preferences->absoluteTouchMode = !parser.getToggleOptionValue("touchscreen-trackpad", !preferences->absoluteTouchMode);
|
||||
|
||||
// Resolve --game-optimization and --no-game-optimization options
|
||||
preferences->gameOptimizations = parser.getToggleOptionValue("game-optimization", preferences->gameOptimizations);
|
||||
@@ -323,6 +407,24 @@ void StreamCommandLineParser::parse(const QStringList &args, StreamingPreference
|
||||
// Resolve --audio-on-host and --no-audio-on-host options
|
||||
preferences->playAudioOnHost = parser.getToggleOptionValue("audio-on-host", preferences->playAudioOnHost);
|
||||
|
||||
// Resolve --frame-pacing and --no-frame-pacing options
|
||||
preferences->framePacing = parser.getToggleOptionValue("frame-pacing", preferences->framePacing);
|
||||
|
||||
// Resolve --mute-on-focus-loss and --no-mute-on-focus-loss options
|
||||
preferences->muteOnFocusLoss = parser.getToggleOptionValue("mute-on-focus-loss", preferences->muteOnFocusLoss);
|
||||
|
||||
// Resolve --background-gamepad and --no-background-gamepad options
|
||||
preferences->backgroundGamepad = parser.getToggleOptionValue("background-gamepad", preferences->backgroundGamepad);
|
||||
|
||||
// Resolve --reverse-scroll-direction and --no-reverse-scroll-direction options
|
||||
preferences->reverseScrollDirection = parser.getToggleOptionValue("reverse-scroll-direction", preferences->reverseScrollDirection);
|
||||
|
||||
// Resolve --swap-gamepad-buttons and --no-swap-gamepad-buttons options
|
||||
preferences->swapFaceButtons = parser.getToggleOptionValue("swap-gamepad-buttons", preferences->swapFaceButtons);
|
||||
|
||||
// Resolve --capture-system-keys and --no-capture-system-keys options
|
||||
preferences->captureSysKeys = parser.getToggleOptionValue("capture-system-keys", preferences->captureSysKeys);
|
||||
|
||||
// Resolve --video-codec option
|
||||
if (parser.isSet("video-codec")) {
|
||||
preferences->videoCodecConfig = mapValue(m_VideoCodecMap, parser.getChoiceOptionValue("video-codec"));
|
||||
@@ -359,4 +461,3 @@ QString StreamCommandLineParser::getAppName() const
|
||||
{
|
||||
return m_AppName;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ public:
|
||||
enum ParseResult {
|
||||
NormalStartRequested,
|
||||
StreamRequested,
|
||||
QuitRequested,
|
||||
};
|
||||
|
||||
GlobalCommandLineParser();
|
||||
@@ -20,6 +21,20 @@ public:
|
||||
|
||||
};
|
||||
|
||||
class QuitCommandLineParser
|
||||
{
|
||||
public:
|
||||
QuitCommandLineParser();
|
||||
virtual ~QuitCommandLineParser();
|
||||
|
||||
void parse(const QStringList &args);
|
||||
|
||||
QString getHost() const;
|
||||
|
||||
private:
|
||||
QString m_Host;
|
||||
};
|
||||
|
||||
class StreamCommandLineParser
|
||||
{
|
||||
public:
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
#include "quitstream.h"
|
||||
|
||||
#include "backend/computermanager.h"
|
||||
#include "backend/computerseeker.h"
|
||||
#include "streaming/session.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QTimer>
|
||||
|
||||
#define COMPUTER_SEEK_TIMEOUT 10000
|
||||
|
||||
namespace CliQuitStream
|
||||
{
|
||||
|
||||
enum State {
|
||||
StateInit,
|
||||
StateSeekComputer,
|
||||
StateQuitApp,
|
||||
StateFailure,
|
||||
};
|
||||
|
||||
class Event
|
||||
{
|
||||
public:
|
||||
enum Type {
|
||||
AppQuitCompleted,
|
||||
ComputerFound,
|
||||
ComputerSeekTimedout,
|
||||
Executed,
|
||||
};
|
||||
|
||||
Event(Type type)
|
||||
: type(type), computerManager(nullptr), computer(nullptr) {}
|
||||
|
||||
Type type;
|
||||
ComputerManager *computerManager;
|
||||
NvComputer *computer;
|
||||
QString errorMessage;
|
||||
};
|
||||
|
||||
class LauncherPrivate
|
||||
{
|
||||
Q_DECLARE_PUBLIC(Launcher)
|
||||
|
||||
public:
|
||||
LauncherPrivate(Launcher *q) : q_ptr(q) {}
|
||||
|
||||
void handleEvent(Event event)
|
||||
{
|
||||
Q_Q(Launcher);
|
||||
NvApp app;
|
||||
|
||||
switch (event.type) {
|
||||
// Occurs when CliQuitStreamSegue becomes visible and the UI calls launcher's execute()
|
||||
case Event::Executed:
|
||||
if (m_State == StateInit) {
|
||||
m_State = StateSeekComputer;
|
||||
m_ComputerManager = event.computerManager;
|
||||
q->connect(m_ComputerManager, &ComputerManager::quitAppCompleted,
|
||||
q, &Launcher::onQuitAppCompleted);
|
||||
|
||||
m_ComputerSeeker = new ComputerSeeker(m_ComputerManager, m_ComputerName, q);
|
||||
q->connect(m_ComputerSeeker, &ComputerSeeker::computerFound,
|
||||
q, &Launcher::onComputerFound);
|
||||
q->connect(m_ComputerSeeker, &ComputerSeeker::errorTimeout,
|
||||
q, &Launcher::onComputerSeekTimeout);
|
||||
m_ComputerSeeker->start(COMPUTER_SEEK_TIMEOUT);
|
||||
|
||||
emit q->searchingComputer();
|
||||
}
|
||||
break;
|
||||
// Occurs when computer search timed out
|
||||
case Event::ComputerSeekTimedout:
|
||||
if (m_State == StateSeekComputer) {
|
||||
m_State = StateFailure;
|
||||
emit q->failed(QObject::tr("Failed to connect to %1").arg(m_ComputerName));
|
||||
}
|
||||
break;
|
||||
// Occurs when searched computer is found
|
||||
case Event::ComputerFound:
|
||||
if (m_State == StateSeekComputer) {
|
||||
if (event.computer->pairState == NvComputer::PS_PAIRED) {
|
||||
m_State = StateQuitApp;
|
||||
emit q->quittingApp();
|
||||
m_ComputerManager->quitRunningApp(event.computer);
|
||||
} else {
|
||||
m_State = StateFailure;
|
||||
QString msg = QObject::tr("Computer %1 has not been paired. "
|
||||
"Please open Moonlight to pair before streaming.")
|
||||
.arg(event.computer->name);
|
||||
emit q->failed(msg);
|
||||
}
|
||||
}
|
||||
break;
|
||||
// Occurs when app quit completed (error message is set if failed)
|
||||
case Event::AppQuitCompleted:
|
||||
if (m_State == StateQuitApp) {
|
||||
if (event.errorMessage.isEmpty()) {
|
||||
QCoreApplication::exit(0);
|
||||
} else {
|
||||
m_State = StateFailure;
|
||||
emit q->failed(QObject::tr("Quitting app failed, reason: %1").arg(event.errorMessage));
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Launcher *q_ptr;
|
||||
ComputerManager *m_ComputerManager;
|
||||
QString m_ComputerName;
|
||||
ComputerSeeker *m_ComputerSeeker;
|
||||
State m_State;
|
||||
QTimer *m_TimeoutTimer;
|
||||
};
|
||||
|
||||
Launcher::Launcher(QString computer, QObject *parent)
|
||||
: QObject(parent),
|
||||
m_DPtr(new LauncherPrivate(this))
|
||||
{
|
||||
Q_D(Launcher);
|
||||
d->m_ComputerName = computer;
|
||||
d->m_State = StateInit;
|
||||
d->m_TimeoutTimer = new QTimer(this);
|
||||
d->m_TimeoutTimer->setSingleShot(true);
|
||||
connect(d->m_TimeoutTimer, &QTimer::timeout,
|
||||
this, &Launcher::onComputerSeekTimeout);
|
||||
}
|
||||
|
||||
Launcher::~Launcher()
|
||||
{
|
||||
}
|
||||
|
||||
void Launcher::execute(ComputerManager *manager)
|
||||
{
|
||||
Q_D(Launcher);
|
||||
Event event(Event::Executed);
|
||||
event.computerManager = manager;
|
||||
d->handleEvent(event);
|
||||
}
|
||||
|
||||
bool Launcher::isExecuted() const
|
||||
{
|
||||
Q_D(const Launcher);
|
||||
return d->m_State != StateInit;
|
||||
}
|
||||
|
||||
void Launcher::onComputerFound(NvComputer *computer)
|
||||
{
|
||||
Q_D(Launcher);
|
||||
Event event(Event::ComputerFound);
|
||||
event.computer = computer;
|
||||
d->handleEvent(event);
|
||||
}
|
||||
|
||||
void Launcher::onComputerSeekTimeout()
|
||||
{
|
||||
Q_D(Launcher);
|
||||
Event event(Event::ComputerSeekTimedout);
|
||||
d->handleEvent(event);
|
||||
}
|
||||
|
||||
void Launcher::onQuitAppCompleted(QVariant error)
|
||||
{
|
||||
Q_D(Launcher);
|
||||
Event event(Event::AppQuitCompleted);
|
||||
event.errorMessage = error.toString();
|
||||
d->handleEvent(event);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QVariant>
|
||||
|
||||
class ComputerManager;
|
||||
class NvComputer;
|
||||
|
||||
namespace CliQuitStream
|
||||
{
|
||||
|
||||
class LauncherPrivate;
|
||||
|
||||
class Launcher : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_DECLARE_PRIVATE_D(m_DPtr, Launcher)
|
||||
|
||||
public:
|
||||
explicit Launcher(QString computer, QObject *parent = nullptr);
|
||||
~Launcher();
|
||||
|
||||
Q_INVOKABLE void execute(ComputerManager *manager);
|
||||
Q_INVOKABLE bool isExecuted() const;
|
||||
|
||||
signals:
|
||||
void searchingComputer();
|
||||
void quittingApp();
|
||||
void failed(QString text);
|
||||
|
||||
private slots:
|
||||
void onComputerFound(NvComputer *computer);
|
||||
void onComputerSeekTimeout();
|
||||
void onQuitAppCompleted(QVariant error);
|
||||
|
||||
private:
|
||||
QScopedPointer<LauncherPrivate> m_DPtr;
|
||||
};
|
||||
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
#include "startstream.h"
|
||||
#include "backend/computermanager.h"
|
||||
#include "backend/computerseeker.h"
|
||||
#include "streaming/session.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QTimer>
|
||||
|
||||
#define COMPUTER_SEEK_TIMEOUT 10000
|
||||
@@ -22,6 +24,9 @@ class Event
|
||||
{
|
||||
public:
|
||||
enum Type {
|
||||
AppQuitCompleted,
|
||||
AppQuitRequested,
|
||||
ComputerFound,
|
||||
ComputerUpdated,
|
||||
Executed,
|
||||
Timedout,
|
||||
@@ -33,6 +38,7 @@ public:
|
||||
Type type;
|
||||
ComputerManager *computerManager;
|
||||
NvComputer *computer;
|
||||
QString errorMessage;
|
||||
};
|
||||
|
||||
class LauncherPrivate
|
||||
@@ -49,91 +55,91 @@ public:
|
||||
NvApp app;
|
||||
|
||||
switch (event.type) {
|
||||
// Occurs when CliStartStreamSegue becomes visible and the UI calls launcher's execute()
|
||||
case Event::Executed:
|
||||
if (m_State == StateInit) {
|
||||
m_State = StateSeekComputer;
|
||||
m_TimeoutTimer->start(COMPUTER_SEEK_TIMEOUT);
|
||||
m_ComputerManager = event.computerManager;
|
||||
|
||||
m_ComputerSeeker = new ComputerSeeker(m_ComputerManager, m_ComputerName, q);
|
||||
q->connect(m_ComputerSeeker, &ComputerSeeker::computerFound,
|
||||
q, &Launcher::onComputerFound);
|
||||
q->connect(m_ComputerSeeker, &ComputerSeeker::errorTimeout,
|
||||
q, &Launcher::onTimeout);
|
||||
m_ComputerSeeker->start(COMPUTER_SEEK_TIMEOUT);
|
||||
|
||||
q->connect(m_ComputerManager, &ComputerManager::computerStateChanged,
|
||||
q, &Launcher::onComputerUpdated);
|
||||
// Seek desired computer by both connecting to it directly (this may fail
|
||||
// if m_ComputerName is UUID, or the name that doesn't resolve to an IP
|
||||
// address) and by polling it using mDNS, hopefully one of these methods
|
||||
// would find the host
|
||||
m_ComputerManager->addNewHost(m_ComputerName, false);
|
||||
m_ComputerManager->startPolling();
|
||||
q->connect(m_ComputerManager, &ComputerManager::quitAppCompleted,
|
||||
q, &Launcher::onQuitAppCompleted);
|
||||
|
||||
emit q->searchingComputer();
|
||||
}
|
||||
break;
|
||||
case Event::ComputerUpdated:
|
||||
// Occurs when searched computer is found
|
||||
case Event::ComputerFound:
|
||||
if (m_State == StateSeekComputer) {
|
||||
if (matchComputer(event.computer) && isOnline(event.computer)) {
|
||||
if (isPaired(event.computer)) {
|
||||
m_State = StateSeekApp;
|
||||
m_TimeoutTimer->start(APP_SEEK_TIMEOUT);
|
||||
m_Computer = event.computer;
|
||||
m_ComputerManager->stopPollingAsync();
|
||||
emit q->searchingApp();
|
||||
} else {
|
||||
m_State = StateFailure;
|
||||
QString msg = QString("Computer %1 has not been paired. "
|
||||
if (event.computer->pairState == NvComputer::PS_PAIRED) {
|
||||
m_State = StateSeekApp;
|
||||
m_Computer = event.computer;
|
||||
m_TimeoutTimer->start(APP_SEEK_TIMEOUT);
|
||||
emit q->searchingApp();
|
||||
} else {
|
||||
m_State = StateFailure;
|
||||
QString msg = QObject::tr("Computer %1 has not been paired. "
|
||||
"Please open Moonlight to pair before streaming.")
|
||||
.arg(event.computer->name);
|
||||
emit q->failed(msg);
|
||||
}
|
||||
.arg(event.computer->name);
|
||||
emit q->failed(msg);
|
||||
}
|
||||
}
|
||||
break;
|
||||
// Occurs when a computer is updated
|
||||
case Event::ComputerUpdated:
|
||||
if (m_State == StateSeekApp) {
|
||||
int index = getAppIndex();
|
||||
if (-1 != index) {
|
||||
app = m_Computer->appList[index];
|
||||
m_TimeoutTimer->stop();
|
||||
if (isNotStreaming() || isStreamingApp(app)) {
|
||||
m_State = StateStartSession;
|
||||
m_TimeoutTimer->stop();
|
||||
session = new Session(m_Computer, app, m_Preferences);
|
||||
emit q->sessionCreated(app.name, session);
|
||||
} else {
|
||||
m_State = StateFailure;
|
||||
QString msg = QString("%1 is already running. Please quit %1 to stream %2.")
|
||||
.arg(getCurrentAppName(), app.name);
|
||||
emit q->failed(msg);
|
||||
emit q->appQuitRequired(getCurrentAppName());
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
// Occurs when there was another app running on computer and user accepted quit
|
||||
// confirmation dialog
|
||||
case Event::AppQuitRequested:
|
||||
if (m_State == StateSeekApp) {
|
||||
m_ComputerManager->quitRunningApp(m_Computer);
|
||||
}
|
||||
break;
|
||||
// Occurs when the previous app quit has been completed, handles quitting errors if any
|
||||
// happended. ComputerUpdated event's handler handles session start when previous app has
|
||||
// quit.
|
||||
case Event::AppQuitCompleted:
|
||||
if (m_State == StateSeekApp && !event.errorMessage.isEmpty()) {
|
||||
m_State = StateFailure;
|
||||
emit q->failed(QObject::tr("Quitting app failed, reason: %1").arg(event.errorMessage));
|
||||
}
|
||||
break;
|
||||
// Occurs when computer or app search timed out
|
||||
case Event::Timedout:
|
||||
if (m_State == StateSeekComputer) {
|
||||
m_State = StateFailure;
|
||||
emit q->failed(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;
|
||||
}
|
||||
}
|
||||
|
||||
bool 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;
|
||||
}
|
||||
|
||||
bool isOnline(NvComputer *computer) const
|
||||
{
|
||||
return computer->state == NvComputer::CS_ONLINE;
|
||||
}
|
||||
|
||||
bool isPaired(NvComputer *computer) const
|
||||
{
|
||||
return computer->pairState == NvComputer::PS_PAIRED;
|
||||
}
|
||||
|
||||
int getAppIndex() const
|
||||
{
|
||||
for (int i = 0; i < m_Computer->appList.length(); i++) {
|
||||
@@ -169,6 +175,7 @@ public:
|
||||
QString m_AppName;
|
||||
StreamingPreferences *m_Preferences;
|
||||
ComputerManager *m_ComputerManager;
|
||||
ComputerSeeker *m_ComputerSeeker;
|
||||
NvComputer *m_Computer;
|
||||
State m_State;
|
||||
QTimer *m_TimeoutTimer;
|
||||
@@ -202,6 +209,27 @@ void Launcher::execute(ComputerManager *manager)
|
||||
d->handleEvent(event);
|
||||
}
|
||||
|
||||
void Launcher::quitRunningApp()
|
||||
{
|
||||
Q_D(Launcher);
|
||||
Event event(Event::AppQuitRequested);
|
||||
d->handleEvent(event);
|
||||
}
|
||||
|
||||
bool Launcher::isExecuted() const
|
||||
{
|
||||
Q_D(const Launcher);
|
||||
return d->m_State != StateInit;
|
||||
}
|
||||
|
||||
void Launcher::onComputerFound(NvComputer *computer)
|
||||
{
|
||||
Q_D(Launcher);
|
||||
Event event(Event::ComputerFound);
|
||||
event.computer = computer;
|
||||
d->handleEvent(event);
|
||||
}
|
||||
|
||||
void Launcher::onComputerUpdated(NvComputer *computer)
|
||||
{
|
||||
Q_D(Launcher);
|
||||
@@ -217,4 +245,12 @@ void Launcher::onTimeout()
|
||||
d->handleEvent(event);
|
||||
}
|
||||
|
||||
void Launcher::onQuitAppCompleted(QVariant error)
|
||||
{
|
||||
Q_D(Launcher);
|
||||
Event event(Event::AppQuitCompleted);
|
||||
event.errorMessage = error.toString();
|
||||
d->handleEvent(event);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QVariant>
|
||||
|
||||
class ComputerManager;
|
||||
class NvComputer;
|
||||
@@ -24,16 +25,21 @@ public:
|
||||
QObject *parent = nullptr);
|
||||
~Launcher();
|
||||
Q_INVOKABLE void execute(ComputerManager *manager);
|
||||
Q_INVOKABLE void quitRunningApp();
|
||||
Q_INVOKABLE bool isExecuted() const;
|
||||
|
||||
signals:
|
||||
void searchingComputer();
|
||||
void searchingApp();
|
||||
void sessionCreated(QString appName, Session *session);
|
||||
void failed(QString text);
|
||||
void appQuitRequired(QString appName);
|
||||
|
||||
private slots:
|
||||
void onComputerFound(NvComputer *computer);
|
||||
void onComputerUpdated(NvComputer *computer);
|
||||
void onTimeout();
|
||||
void onQuitAppCompleted(QVariant error);
|
||||
|
||||
private:
|
||||
QScopedPointer<LauncherPrivate> m_DPtr;
|
||||
|
||||
@@ -4,35 +4,351 @@
|
||||
<metadata_license>CC0-1.0</metadata_license>
|
||||
<project_license>GPL-3.0+</project_license>
|
||||
<name>Moonlight</name>
|
||||
<summary>Stream games from your NVIDIA GameStream-enabled PC</summary>
|
||||
<summary>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 in fullscreen mode on X11</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>
|
||||
|
||||
<url type="homepage">https://moonlight-stream.com</url>
|
||||
<url type="homepage">https://moonlight-stream.org</url>
|
||||
<url type="bugtracker">https://github.com/moonlight-stream/moonlight-qt/issues</url>
|
||||
<url type="help">https://github.com/moonlight-stream/moonlight-docs/wiki/Setup-Guide</url>
|
||||
|
||||
<screenshots>
|
||||
<screenshot type="default">
|
||||
<image>https://moonlight-stream.com/appstream_resources/moonlight_screenshot_1.png</image>
|
||||
<image>https://moonlight-stream.org/appstream_resources/moonlight_screenshot_1.png</image>
|
||||
</screenshot>
|
||||
<screenshot>
|
||||
<image>https://moonlight-stream.com/appstream_resources/moonlight_screenshot_2.png</image>
|
||||
<image>https://moonlight-stream.org/appstream_resources/moonlight_screenshot_2.png</image>
|
||||
</screenshot>
|
||||
</screenshots>
|
||||
|
||||
<releases>
|
||||
<release version="3.0.0" date="2021-01-17">
|
||||
<description>
|
||||
<p>New features:</p>
|
||||
<ul>
|
||||
<li>Added the ability to define a custom streaming resolution using the UI</li>
|
||||
<li>Added the option to capture system key combinations (Alt+Tab, Ctrl+Shift+Esc, Win+Tab, etc) in fullscreen mode</li>
|
||||
<li>Updated gamepad mappings are automatically downloaded each time Moonlight launches</li>
|
||||
<li>The performance overlay is now supported for Linux clients using VAAPI and VDPAU hardware acceleration</li>
|
||||
<li>Added the option to enable "Direct Launch" which immediately launches the specified game when clicking on a PC</li>
|
||||
<li>Added the option to mute audio when the Moonlight window loses input focus</li>
|
||||
<li>Added the option to toggle background gamepad input</li>
|
||||
<li>Added the option to invert scroll direction</li>
|
||||
<li>Added the option to swap gamepad button layout to Nintendo-style</li>
|
||||
<li>Added the option to start the Moonlight UI in fullscreen mode</li>
|
||||
<li>The failing TCP or UDP ports are now displayed when the connection fails</li>
|
||||
</ul>
|
||||
<p>Bugfixes:</p>
|
||||
<ul>
|
||||
<li>Fixed PS4 controllers not reading inputs during haptics in some games</li>
|
||||
<li>Fixed some slow decoders causing the receive time value to increase rather than the decode time</li>
|
||||
<li>Native resolution options now refresh each time the settings page is opened, rather than once per launch of Moonlight</li>
|
||||
<li>Fixed no video or audio while streaming over Internet connections that drop packets with QoS tagging enabled</li>
|
||||
<li>Improved connection testing accuracy for connections with low MTUs</li>
|
||||
<li>Fixed inability to open an app's context menu with a gamepad</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="2.2.0" date="2020-09-10">
|
||||
<description>
|
||||
<p>New features:</p>
|
||||
<ul>
|
||||
<li>Added the ability to hide unwanted apps and games</li>
|
||||
<li>Added a network test to determine if your Internet connection is blocking Moonlight</li>
|
||||
<li>Added the option to swap mouse buttons</li>
|
||||
<li>Added codec and resolution to the on-screen performance stats</li>
|
||||
<li>Added a warning if the connection is too unstable to receive any complete video frames</li>
|
||||
<li>Added a Discord link to the toolbar in settings</li>
|
||||
<li>Enabled the new EGL renderer by default on Linux</li>
|
||||
</ul>
|
||||
<p>Bugfixes:</p>
|
||||
<ul>
|
||||
<li>Fixed streaming over networks that block QoS-tagged packets</li>
|
||||
<li>Selecting a frame rate higher than 60 FPS no longer sets the host desktop to 720p</li>
|
||||
<li>Right-side modifier keys no longer press the corresponding left-side keys too</li>
|
||||
<li>Fixed mouse position never reaching the edge of the screen with certain desktop resolutions</li>
|
||||
<li>Improved synchronization between host and client mouse cursor in remote desktop mouse mode</li>
|
||||
<li>Cached box art is now deleted when the PC is removed from Moonlight</li>
|
||||
<li>Fixed several CLI options not properly overriding GUI options</li>
|
||||
<li>Updated included gamepad mappings</li>
|
||||
<li>Updated to Qt 5.15.1</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="2.1.0" date="2020-05-17">
|
||||
<description>
|
||||
<p>New features:</p>
|
||||
<ul>
|
||||
<li>Remote desktop mode now allows interaction with the host without having to click to focus the Moonlight window first</li>
|
||||
<li>Added a new EGL renderer for improved performance on Wayland and XWayland</li>
|
||||
<li>Added the option to toggle between direct touchscreen mode and the old trackpad mode</li>
|
||||
<li>Added the option to rename host PCs</li>
|
||||
<li>Added the ability to scroll the settings page with the right analog stick</li>
|
||||
<li>Various UI improvements</li>
|
||||
</ul>
|
||||
<p>Bugfixes:</p>
|
||||
<ul>
|
||||
<li>Fixed unreliable mouse capture when the stream begins</li>
|
||||
<li>Fixed cursor being trapped in full-screen mode even when the mouse wasn't captured</li>
|
||||
<li>Fixed key presses not being sent to the host if the Super/Meta/Windows key is pressed</li>
|
||||
<li>Updated included gamepad mappings</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="2.0.0" date="2020-04-28">
|
||||
<description>
|
||||
<p>New features:</p>
|
||||
<ul>
|
||||
<li>Remote desktop-optimized mouse mode which doesn't require capturing the mouse cursor</li>
|
||||
<li>Touchscreen input now directly controls the mouse cursor</li>
|
||||
<li>7.1 surround sound support</li>
|
||||
</ul>
|
||||
<p>Bugfixes:</p>
|
||||
<ul>
|
||||
<li>Improved performance on some lossy networks</li>
|
||||
<li>Fixed disconnect delay when streaming from GeForce Experience 3.20.3.63</li>
|
||||
<li>Fixed frame rate not exceeding 60 when streaming from GeForce Experience 3.20.3.63</li>
|
||||
<li>Fixed parsing of command-line options when passing flags prior to the action</li>
|
||||
<li>Fixed an issue that may cause excessive mDNS network traffic while Moonlight is running</li>
|
||||
<li>Fixed hardware decoding on Intel GPUs running Mesa 20.0 and/or the Iris driver</li>
|
||||
<li>Fixed hardware decoding on Comet Lake and later Intel GPUs using the iHD VA-API driver</li>
|
||||
<li>Fixed hardware decoding on AMD Radeon GPUs with XWayland</li>
|
||||
<li>Fixed a crash when using the iHD VA-API driver on Wayland</li>
|
||||
<li>Fixed delay after exiting the stream on Wayland</li>
|
||||
<li>Updated included gamepad mappings</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="1.2.0" date="2019-12-17">
|
||||
<description>
|
||||
<p>Bugfixes:</p>
|
||||
<ul>
|
||||
<li>Improved video encoding efficiency</li>
|
||||
<li>Improved performance when streaming over a VPN</li>
|
||||
<li>Fixed RTSP handshake error on certain networks</li>
|
||||
<li>Fixed rumble not working on certain gamepads</li>
|
||||
<li>Fixed pairing error when double-clicking on an unpaired PC</li>
|
||||
<li>Updated included SDL gamepad mappings</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="1.1.1" date="2019-09-29">
|
||||
<description>
|
||||
<p>Bugfixes:</p>
|
||||
<ul>
|
||||
<li>Improved reliability of public IP address detection</li>
|
||||
<li>Fixed connecting to a host PC with multiple network connections</li>
|
||||
<li>Fixed a hang after exiting a full-screen stream with GNOME on Wayland</li>
|
||||
<li>Fixed gamepad hotplugging</li>
|
||||
<li>Updated to SDL 2.0.10</li>
|
||||
<li>Updated to Qt 5.13.1</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="1.1.0" date="2019-08-05">
|
||||
<description>
|
||||
<p>New features:</p>
|
||||
<ul>
|
||||
<li>Added support for NVDEC for NVIDIA GPUs on Wayland</li>
|
||||
<li>Added an option to disable gamepad mouse mode for games that require holding the Start button</li>
|
||||
<li>Added automatic detection of IPv6 addresses for Internet streaming over IPv6 networks</li>
|
||||
</ul>
|
||||
<p>Bugfixes:</p>
|
||||
<ul>
|
||||
<li>Improved speed and reliability of automatic PC detection on the local network</li>
|
||||
<li>Fixed audio latency increasing under certain network conditions</li>
|
||||
<li>Fixed 5.1 surround sound sometimes causing no audio to play when streaming over the Internet</li>
|
||||
<li>Fixed graceful termination of games not being detected properly on GFE 3.19</li>
|
||||
<li>Fixed several audio issues on Linux by switching back to SDL for audio</li>
|
||||
<li>Changed client certificate signature algorithm from SHA-1 to SHA-256 for Debian Buster compatibility</li>
|
||||
<li>Updated included gamepad mappings</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="1.0.1" date="2019-05-27">
|
||||
<description>
|
||||
<p>Bugfixes:</p>
|
||||
<ul>
|
||||
<li>Fixed broken video on GeForce Experience 3.19</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="1.0.0" date="2019-05-21">
|
||||
<description>
|
||||
<p>New features:</p>
|
||||
<ul>
|
||||
<li>Mouse mode for gamepads (Press and hold Start to toggle)</li>
|
||||
<li>Improved gamepad UI navigation</li>
|
||||
</ul>
|
||||
<p>Bugfixes:</p>
|
||||
<ul>
|
||||
<li>Fixed duplicate non-working gamepads appearing on the host</li>
|
||||
<li>Fixed excessive frame queue delay on macOS</li>
|
||||
<li>Fixed excessive audio latency on Steam Link</li>
|
||||
<li>Fixed hiding mouse cursor after streaming on Steam Link</li>
|
||||
<li>Fixed incorrect receive time stats on 32-bit platforms</li>
|
||||
<li>Added UDP ports 48002 and 48010 for Wake-on-LAN</li>
|
||||
<li>Updated included gamepad mappings</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="0.10.1" date="2019-04-28">
|
||||
<description>
|
||||
<p>New features:</p>
|
||||
<ul>
|
||||
<li>Video performance optimizations</li>
|
||||
<li>VAAPI support on Wayland</li>
|
||||
</ul>
|
||||
<p>Bugfixes:</p>
|
||||
<ul>
|
||||
<li>Fixed a crash when attempting to dismiss the connection terminated dialog</li>
|
||||
<li>Fixed a crash when using the overlay on the software decoder</li>
|
||||
<li>Fixed software decoder not displaying output in some cases</li>
|
||||
<li>Fixed some minor UI issues on the PC grid</li>
|
||||
<li>Improved visibility of toolbar highlights</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="0.10.0" date="2019-04-07">
|
||||
<description>
|
||||
<p>New features:</p>
|
||||
<ul>
|
||||
<li>Redesigned App Grid with larger game artwork and less blank space</li>
|
||||
<li>Redesigned UI dialogs using Material Design</li>
|
||||
<li>Added a connection warning overlay (which can be disabled in settings)</li>
|
||||
<li>Streaming window no longer minimizes when it loses focus in full-screen mode</li>
|
||||
<li>The Steam Link device is now a supported platform</li>
|
||||
</ul>
|
||||
<p>Bugfixes:</p>
|
||||
<ul>
|
||||
<li>Fixed several bugs that could cause UI hangs or lag</li>
|
||||
<li>Fixed rumble support on gamepads with limited rumble motor control</li>
|
||||
<li>Fixed increased memory usage if the software renderer gets delayed</li>
|
||||
<li>Fixed crashes on certain desktop configurations when loading the UI</li>
|
||||
<li>Fixed handling of pairing certificates with a bogus date</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="0.9.1" date="2019-02-17">
|
||||
<description>
|
||||
<p>New features:</p>
|
||||
<ul>
|
||||
<li>Added gamepad rumble support</li>
|
||||
<li>Added a dedicated quit button to the running app tile</li>
|
||||
<li>Reduced video latency on Windows and Linux</li>
|
||||
<li>Added a performance overlay for software decoding</li>
|
||||
</ul>
|
||||
<p>Bugfixes:</p>
|
||||
<ul>
|
||||
<li>Fixed delayed first video frame with GFE 3.17</li>
|
||||
<li>Fixed erroneously setting host resolution to 720p in certain configurations</li>
|
||||
<li>The connection termination dialog is no longer displayed for intentional termination</li>
|
||||
<li>Fixed some gamepads only functioning for UI navigation and not in game</li>
|
||||
<li>Fixed audio latency increasing when switching audio devices</li>
|
||||
<li>Fixed VDPAU scaling behavior on AMD cards</li>
|
||||
<li>Fixed several bugs in gamepad navigation behavior</li>
|
||||
<li>Work around a bug in QGnomePlatform that breaks dialog rendering on Linux</li>
|
||||
<li>Updated included SDL gamepad mappings</li>
|
||||
<li>Fixed hotkey behavior on non-latin keyboards</li>
|
||||
<li>Fixed incorrect frame drop percentages in debug stats</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="0.8.1" date="2019-01-08">
|
||||
<description>
|
||||
<p>New features:</p>
|
||||
<ul>
|
||||
<li>Stricter adherence to the specified bitrate when streaming over the Internet</li>
|
||||
</ul>
|
||||
<p>Bugfixes:</p>
|
||||
<ul>
|
||||
<li>Fixed audio crackling that impacted certain systems</li>
|
||||
<li>Fixed Wake-on-LAN packets failing to send on some systems</li>
|
||||
<li>Reduced audio latency when resuming a session where audio is already playing</li>
|
||||
<li>Added option to enable the old SDL audio renderer by setting ML_AUDIO=SDL</li>
|
||||
<li>Updated to use the new Qt 5.12 Flatpak runtime</li>
|
||||
<li>Updated to FFmpeg 4.1</li>
|
||||
<li>Minor UI tweaks</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="0.8.0" date="2018-12-25">
|
||||
<description>
|
||||
<p>New features:</p>
|
||||
<ul>
|
||||
<li>Improved security of pairing - After this update, you must re-pair with your PC!</li>
|
||||
<li>Enabled quitting games started by other Moonlight clients also running this update</li>
|
||||
</ul>
|
||||
<p>Bugfixes:</p>
|
||||
<ul>
|
||||
<li>Fixed the video bitrate slider not working above certain values on GFE 3.16</li>
|
||||
<li>Fixed broken Bluetooth audio with PulseAudio</li>
|
||||
<li>Reduced audio latency</li>
|
||||
<li>Fixed some non-gamepad devices triggering the unmapped gamepad dialog box</li>
|
||||
<li>Minor UI tweaks</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="0.7.0" date="2018-12-05">
|
||||
<description>
|
||||
<p>New features:</p>
|
||||
<ul>
|
||||
<li>Added option to quit the app after stopping the stream</li>
|
||||
<li>Added CLI option to quit the running app</li>
|
||||
</ul>
|
||||
<p>Bugfixes:</p>
|
||||
<ul>
|
||||
<li>Switched to libsoundio backend to address audio glitches and reduce latency</li>
|
||||
<li>Fixed CLI version output</li>
|
||||
<li>Fixed streaming error messages not showing up when launching from CLI</li>
|
||||
<li>Updated included SDL gamepad mappings</li>
|
||||
<li>Various minor UI tweaks</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="0.6.4" date="2018-11-20">
|
||||
<description>
|
||||
<p>New features:</p>
|
||||
<ul>
|
||||
<li>Added an option to match client display refresh rate when FPS is unlocked</li>
|
||||
</ul>
|
||||
<p>Bugfixes:</p>
|
||||
<ul>
|
||||
<li>Fixed multiple gamepads not being detected on the host in some scenarios</li>
|
||||
<li>Fixed an incorrect warning when trying to stream at 4K</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="0.6.3" date="2018-11-16">
|
||||
<description>
|
||||
<p>New features:</p>
|
||||
<ul>
|
||||
<li>Added support for GeForce Experience 3.16</li>
|
||||
<li>Added an option to force Moonlight to start in windowed mode</li>
|
||||
</ul>
|
||||
<p>Bugfixes:</p>
|
||||
<ul>
|
||||
<li>Fixed scrolling not working on the settings page</li>
|
||||
<li>Skipped using proxy servers when attempting to stream</li>
|
||||
<li>Fixed a couple possible crashes</li>
|
||||
<li>Renamed the mouse acceleration option to be more clear</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="0.6.2" date="2018-10-28">
|
||||
<description>
|
||||
<p>New features:</p>
|
||||
@@ -185,7 +501,7 @@
|
||||
</categories>
|
||||
|
||||
<developer_name>Moonlight Game Streaming Team</developer_name>
|
||||
<update_contact>cameron@moonlight-stream.com</update_contact>
|
||||
<update_contact>cameron@moonlight-stream.org</update_contact>
|
||||
|
||||
<launchable type="desktop-id">com.moonlight_stream.Moonlight.desktop</launchable>
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 2.9 KiB |
@@ -0,0 +1,23 @@
|
||||
#!/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
|
||||
|
||||
# For a seamless transition across the Qt 5.9 -> Qt 5.14 boundary,
|
||||
# we bundle binaries built with both Qt 5.9 and Qt 5.14 and pick
|
||||
# the correct one at runtime.
|
||||
if [ -d "/usr/local/Qt-5.14.1" ]; then
|
||||
BIN="moonlight"
|
||||
else
|
||||
BIN="moonlight59"
|
||||
fi
|
||||
|
||||
# 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
|
||||
echo Launching $BIN
|
||||
exec nice -n -10 ./bin/$BIN > /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,149 +26,286 @@ 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
|
||||
|
||||
// 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
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
y: 10
|
||||
source: model.boxart
|
||||
sourceSize {
|
||||
width: 150
|
||||
height: 200
|
||||
}
|
||||
width: sourceSize.width
|
||||
height: sourceSize.height
|
||||
fillMode: Image.Pad
|
||||
}
|
||||
|
||||
Image {
|
||||
id: runningIcon
|
||||
anchors.centerIn: appIcon
|
||||
visible: model.running
|
||||
source: "qrc:/res/baseline-play_circle_filled_white-48px.svg"
|
||||
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
|
||||
}
|
||||
else
|
||||
{
|
||||
isPlaceholder = false
|
||||
}
|
||||
|
||||
Text {
|
||||
id: appNameText
|
||||
text: model.name
|
||||
color: "white"
|
||||
width: parent.width
|
||||
height: 125
|
||||
anchors.top: appIcon.bottom
|
||||
font.pointSize: 22
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
wrapMode: Text.Wrap
|
||||
elide: Text.ElideRight
|
||||
width = 200
|
||||
height = 267
|
||||
}
|
||||
|
||||
// Display a tooltip with the full name if it's truncated
|
||||
ToolTip.text: model.name
|
||||
ToolTip.delay: 1000
|
||||
ToolTip.timeout: 5000
|
||||
ToolTip.visible: (parent.hovered || parent.highlighted) && truncated
|
||||
ToolTip.visible: (parent.hovered || parent.highlighted) && (!appNameText.visible || appNameText.truncated)
|
||||
}
|
||||
|
||||
function launchOrResumeSelectedApp()
|
||||
ToolButton {
|
||||
id: resumeButton
|
||||
anchors.horizontalCenterOffset: appIcon.isPlaceholder ? -47 : 0
|
||||
anchors.verticalCenterOffset: appIcon.isPlaceholder ? -75 : -60
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
launchOrResumeSelectedApp(true)
|
||||
}
|
||||
|
||||
ToolTip.text: qsTr("Resume Game")
|
||||
ToolTip.delay: 1000
|
||||
ToolTip.timeout: 3000
|
||||
ToolTip.visible: hovered
|
||||
}
|
||||
|
||||
ToolButton {
|
||||
id: quitButton
|
||||
anchors.horizontalCenterOffset: appIcon.isPlaceholder ? 47 : 0
|
||||
anchors.verticalCenterOffset: appIcon.isPlaceholder ? -75 : 60
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
doQuitGame()
|
||||
}
|
||||
|
||||
ToolTip.text: qsTr("Quit Game")
|
||||
ToolTip.delay: 1000
|
||||
ToolTip.timeout: 3000
|
||||
ToolTip.visible: hovered
|
||||
}
|
||||
|
||||
Label {
|
||||
id: appNameText
|
||||
visible: appIcon.isPlaceholder
|
||||
text: model.name
|
||||
width: appIcon.width
|
||||
height: model.running ? 175 : appIcon.height
|
||||
leftPadding: 20
|
||||
rightPadding: 20
|
||||
anchors.left: appIcon.left
|
||||
anchors.right: appIcon.right
|
||||
anchors.bottom: appIcon.bottom
|
||||
font.pointSize: 22
|
||||
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: {
|
||||
// Nothing is running or this app is running
|
||||
launchOrResumeSelectedApp()
|
||||
// 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
|
||||
acceptedButtons: Qt.RightButton;
|
||||
onClicked: {
|
||||
// popup() ensures the menu appears under the mouse cursor
|
||||
appContextMenu.popup()
|
||||
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 be keyboard/gamepad driven so use
|
||||
// open() instead of popup()
|
||||
appContextMenu.open()
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onMenuPressed: {
|
||||
// We must use open() here so the menu is positioned on
|
||||
// the ItemDelegate and not where the mouse cursor is
|
||||
// This will be keyboard/gamepad driven so use open() instead of popup()
|
||||
appContextMenu.open()
|
||||
}
|
||||
|
||||
Menu {
|
||||
function doQuitGame() {
|
||||
quitAppDialog.appName = appModel.getRunningAppName()
|
||||
quitAppDialog.segueToStream = false
|
||||
quitAppDialog.open()
|
||||
}
|
||||
|
||||
NavigableMenu {
|
||||
id: appContextMenu
|
||||
NavigableMenuItem {
|
||||
text: model.running ? "Resume Game" : "Launch Game"
|
||||
onTriggered: {
|
||||
appContextMenu.close()
|
||||
launchOrResumeSelectedApp()
|
||||
}
|
||||
parentMenu: appContextMenu
|
||||
text: model.running ? qsTr("Resume Game") : qsTr("Launch Game")
|
||||
onTriggered: launchOrResumeSelectedApp(true)
|
||||
}
|
||||
NavigableMenuItem {
|
||||
text: "Quit Game"
|
||||
onTriggered: {
|
||||
quitAppDialog.appName = appModel.getRunningAppName()
|
||||
quitAppDialog.segueToStream = false
|
||||
quitAppDialog.open()
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
@@ -196,20 +327,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
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import QtQuick 2.0
|
||||
import QtQuick.Controls 2.2
|
||||
|
||||
import ComputerManager 1.0
|
||||
import Session 1.0
|
||||
|
||||
Item {
|
||||
function onSearchingComputer() {
|
||||
stageLabel.text = qsTr("Establishing connection to PC...")
|
||||
}
|
||||
|
||||
function onQuittingApp() {
|
||||
stageLabel.text = qsTr("Quitting app...")
|
||||
}
|
||||
|
||||
function onFailure(message) {
|
||||
errorDialog.text = message
|
||||
errorDialog.open()
|
||||
}
|
||||
|
||||
StackView.onActivated: {
|
||||
if (!launcher.isExecuted()) {
|
||||
toolBar.visible = false
|
||||
launcher.searchingComputer.connect(onSearchingComputer)
|
||||
launcher.quittingApp.connect(onQuittingApp)
|
||||
launcher.failed.connect(onFailure)
|
||||
launcher.execute(ComputerManager)
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: 5
|
||||
|
||||
BusyIndicator {
|
||||
id: stageSpinner
|
||||
}
|
||||
|
||||
Label {
|
||||
id: stageLabel
|
||||
height: stageSpinner.height
|
||||
text: stageText
|
||||
font.pointSize: 20
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
}
|
||||
|
||||
ErrorMessageDialog {
|
||||
id: errorDialog
|
||||
|
||||
onClosed: {
|
||||
Qt.quit()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,16 @@
|
||||
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) {
|
||||
@@ -30,13 +28,24 @@ Item {
|
||||
errorDialog.open()
|
||||
}
|
||||
|
||||
onVisibleChanged: {
|
||||
if (visible) {
|
||||
function onAppQuitRequired(appName) {
|
||||
quitAppDialog.appName = appName
|
||||
quitAppDialog.open()
|
||||
}
|
||||
|
||||
StackView.onActivated: {
|
||||
if (!launcher.isExecuted()) {
|
||||
toolBar.visible = false
|
||||
|
||||
// Normally this is enabled by PcView, but we will won't
|
||||
// load PcView when streaming from the command-line.
|
||||
SdlGamepadKeyNavigation.enable()
|
||||
|
||||
launcher.searchingComputer.connect(onSearchingComputer)
|
||||
launcher.searchingApp.connect(onSearchingApp)
|
||||
launcher.sessionCreated.connect(onSessionCreated)
|
||||
launcher.failed.connect(onLaunchFailed)
|
||||
launcher.appQuitRequired.connect(onAppQuitRequired)
|
||||
launcher.execute(ComputerManager)
|
||||
}
|
||||
}
|
||||
@@ -56,23 +65,32 @@ Item {
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
|
||||
wrapMode: Text.Wrap
|
||||
color: "white"
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
NavigableMessageDialog {
|
||||
id: quitAppDialog
|
||||
text:qsTr("Are you sure you want to quit %1? Any unsaved progress will be lost.").arg(appName)
|
||||
standardButtons: Dialog.Yes | Dialog.No
|
||||
property string appName : ""
|
||||
|
||||
function quitApp() {
|
||||
var component = Qt.createComponent("QuitSegue.qml")
|
||||
var params = {"appName": appName}
|
||||
stackView.push(component.createObject(stackView, params))
|
||||
// Trigger the quit after pushing the quit segue on screen
|
||||
launcher.quitRunningApp()
|
||||
}
|
||||
|
||||
onAccepted: quitApp()
|
||||
onRejected: Qt.quit()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import QtQuick 2.0
|
||||
import QtQuick.Controls 2.2
|
||||
|
||||
import SystemProperties 1.0
|
||||
|
||||
NavigableMessageDialog {
|
||||
standardButtons: Dialog.Ok | (SystemProperties.hasBrowser ? Dialog.Help : 0)
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import QtQuick 2.0
|
||||
|
||||
Item {
|
||||
objectName: "Gamepad Mapping"
|
||||
objectName: qsTr("Gamepad Mapping")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import QtQuick 2.0
|
||||
import QtQuick.Controls 2.2
|
||||
|
||||
Dialog {
|
||||
// We should use Overlay.overlay here but that's not available in Qt 5.9 :(
|
||||
parent: ApplicationWindow.contentItem
|
||||
|
||||
x: Math.round((parent.width - width) / 2)
|
||||
y: Math.round((parent.height - height) / 2)
|
||||
|
||||
onAboutToHide: {
|
||||
// We must force focus back to the last item for platforms without
|
||||
// support for more than one active window like Steam Link. If
|
||||
// we don't, gamepad and keyboard navigation will break after a
|
||||
// dialog appears.
|
||||
stackView.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import QtQuick 2.0
|
||||
import QtQuick.Controls 2.2
|
||||
|
||||
Menu {
|
||||
onOpened: {
|
||||
// Give focus to the first visible and enabled menu item
|
||||
for (var i = 0; i < count; i++) {
|
||||
var item = itemAt(i)
|
||||
if (item.visible && item.enabled) {
|
||||
item.forceActiveFocus(Qt.TabFocusReason)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,27 @@ import QtQuick 2.0
|
||||
import QtQuick.Controls 2.2
|
||||
|
||||
MenuItem {
|
||||
// Qt 5.10 has a menu property, but we need to support 5.9
|
||||
// so we must make our own.
|
||||
property Menu parentMenu
|
||||
|
||||
// Ensure focus can't be given to an invisible item
|
||||
enabled: visible
|
||||
height: visible ? implicitHeight : 0
|
||||
focusPolicy: visible ? Qt.TabFocus : Qt.NoFocus
|
||||
|
||||
onTriggered: {
|
||||
// We must close the context menu first or
|
||||
// it can steal focus from any dialogs that
|
||||
// onTriggered may spawn.
|
||||
parentMenu.close()
|
||||
}
|
||||
|
||||
Keys.onReturnPressed: {
|
||||
triggered()
|
||||
}
|
||||
|
||||
Keys.onEscapePressed: {
|
||||
parentMenu.close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
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.onEscapePressed: {
|
||||
reject()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
footer: DialogButtonBox {
|
||||
id: dialogButtonBox
|
||||
standardButtons: dialog.standardButtons
|
||||
|
||||
onHelpRequested: {
|
||||
Qt.openUrlExternally(helpUrl)
|
||||
close()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,27 @@ 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()
|
||||
}
|
||||
|
||||
@@ -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,41 @@ 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
|
||||
|
||||
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 +128,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,33 +141,69 @@ GridView {
|
||||
id: statusUnknownSpinner
|
||||
anchors.horizontalCenter: pcIcon.horizontalCenter
|
||||
anchors.verticalCenter: pcIcon.verticalCenter
|
||||
anchors.verticalCenterOffset: -10
|
||||
anchors.verticalCenterOffset: -15
|
||||
width: 75
|
||||
height: 75
|
||||
visible: !model.addPc && model.statusUnknown
|
||||
visible: model.statusUnknown
|
||||
}
|
||||
|
||||
Text {
|
||||
Label {
|
||||
id: pcNameText
|
||||
text: model.name
|
||||
color: "white"
|
||||
|
||||
width: parent.width
|
||||
anchors.top: pcIcon.bottom
|
||||
anchors.bottom: parent.bottom
|
||||
font.pointSize: 36
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
wrapMode: Text.Wrap
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Menu {
|
||||
NavigableMenu {
|
||||
id: pcContextMenu
|
||||
NavigableMenuItem {
|
||||
text: "Wake PC"
|
||||
onTriggered: computerModel.wakeComputer(index)
|
||||
visible: !model.addPc && !model.online && model.wakeable
|
||||
MenuItem {
|
||||
text: qsTr("PC Status: %1").arg(model.online ? qsTr("Online") : qsTr("Offline"))
|
||||
font.bold: true
|
||||
enabled: false
|
||||
}
|
||||
NavigableMenuItem {
|
||||
text: "Delete PC"
|
||||
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
|
||||
@@ -154,10 +213,7 @@ GridView {
|
||||
}
|
||||
|
||||
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")
|
||||
@@ -166,7 +222,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)
|
||||
@@ -177,7 +233,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()
|
||||
}
|
||||
}
|
||||
@@ -187,111 +244,144 @@ GridView {
|
||||
}
|
||||
}
|
||||
|
||||
onPressAndHold: {
|
||||
// popup() ensures the menu appears under the mouse cursor
|
||||
if (pcContextMenu.popup) {
|
||||
pcContextMenu.popup()
|
||||
}
|
||||
else {
|
||||
// Qt 5.9 doesn't have popup()
|
||||
pcContextMenu.open()
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.RightButton;
|
||||
onClicked: {
|
||||
if (!model.addPc) {
|
||||
// popup() ensures the menu appears under the mouse cursor
|
||||
pcContextMenu.popup()
|
||||
}
|
||||
parent.onPressAndHold()
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
@@ -27,26 +19,32 @@ Item {
|
||||
errorDialog.open()
|
||||
}
|
||||
|
||||
// Exit this view
|
||||
stackView.pop()
|
||||
|
||||
// If we're supposed to launch another game after this, do so now
|
||||
if (error === undefined && nextSession !== null) {
|
||||
var component = Qt.createComponent("StreamSegue.qml")
|
||||
var segue = component.createObject(stackView, {"appName": nextAppName, "session": nextSession})
|
||||
stackView.push(segue)
|
||||
stackView.replace(segue)
|
||||
}
|
||||
else {
|
||||
// Exit this view
|
||||
stackView.pop()
|
||||
}
|
||||
}
|
||||
|
||||
onVisibleChanged: {
|
||||
if (visible) {
|
||||
// Connect the quit completion signal
|
||||
ComputerManager.quitAppCompleted.connect(quitAppCompleted)
|
||||
}
|
||||
else {
|
||||
// Disconnect the signal
|
||||
ComputerManager.quitAppCompleted.disconnect(quitAppCompleted)
|
||||
}
|
||||
StackView.onActivated: {
|
||||
// Hide the toolbar before we start loading
|
||||
toolBar.visible = false
|
||||
|
||||
// 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 {
|
||||
@@ -65,14 +63,10 @@ Item {
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
|
||||
wrapMode: Text.Wrap
|
||||
color: "white"
|
||||
}
|
||||
}
|
||||
|
||||
MessageDialog {
|
||||
ErrorMessageDialog {
|
||||
id: errorDialog
|
||||
modality:Qt.WindowModal
|
||||
icon: StandardIcon.Critical
|
||||
standardButtons: StandardButton.Ok
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import QtQuick 2.0
|
||||
import QtQuick.Controls 2.2
|
||||
import QtQuick.Dialogs 1.2
|
||||
import QtQuick.Window 2.2
|
||||
|
||||
import SdlGamepadKeyNavigation 1.0
|
||||
@@ -9,26 +8,25 @@ 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()
|
||||
@@ -37,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
|
||||
@@ -45,7 +44,7 @@ Item {
|
||||
function displayLaunchError(text)
|
||||
{
|
||||
// Display the error dialog after Session::exec() returns
|
||||
errorDialog.text = text
|
||||
streamSegueErrorDialog.text = text
|
||||
}
|
||||
|
||||
function displayLaunchWarning(text)
|
||||
@@ -53,64 +52,128 @@ 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
|
||||
}
|
||||
|
||||
// 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
|
||||
function quitStarting()
|
||||
{
|
||||
// Avoid the push transition animation
|
||||
var component = Qt.createComponent("QuitSegue.qml")
|
||||
stackView.replace(stackView.currentItem, component.createObject(stackView, {"appName": appName}), StackView.Immediate)
|
||||
|
||||
// Show the Qt window again to show quit segue
|
||||
window.visible = true
|
||||
}
|
||||
|
||||
onVisibleChanged: {
|
||||
if (visible) {
|
||||
// Hide the toolbar before we start loading
|
||||
toolBar.visible = false
|
||||
function sessionFinished(portTestResult)
|
||||
{
|
||||
if (portTestResult !== 0 && portTestResult !== -1 && streamSegueErrorDialog.text) {
|
||||
streamSegueErrorDialog.text += "\n\n" + qsTr("This PC's Internet connection is blocking Moonlight. Streaming over the Internet may not work while connected to this network.")
|
||||
}
|
||||
|
||||
// Enable GUI gamepad usage now
|
||||
SdlGamepadKeyNavigation.enable()
|
||||
|
||||
if (quitAfter) {
|
||||
if (streamSegueErrorDialog.text) {
|
||||
// Quit when the error dialog is acknowledged
|
||||
streamSegueErrorDialog.quitAfter = quitAfter
|
||||
streamSegueErrorDialog.open()
|
||||
}
|
||||
else {
|
||||
// Quit immediately
|
||||
Qt.quit()
|
||||
}
|
||||
} else {
|
||||
// Exit this view
|
||||
stackView.pop()
|
||||
|
||||
// Show the Qt window again after streaming
|
||||
window.visible = true
|
||||
|
||||
// Display any launch errors. We do this after
|
||||
// the Qt UI is visible again to prevent losing
|
||||
// focus on the dialog which would impact gamepad
|
||||
// users.
|
||||
if (streamSegueErrorDialog.text) {
|
||||
streamSegueErrorDialog.quitAfter = quitAfter
|
||||
streamSegueErrorDialog.open()
|
||||
}
|
||||
}
|
||||
|
||||
// Garbage collect the Session object since it's pretty heavyweight
|
||||
// and keeps other libraries (like SDL_TTF) around until it is deleted.
|
||||
session = null
|
||||
gc()
|
||||
}
|
||||
|
||||
StackView.onDeactivating: {
|
||||
// Show the toolbar again when popped off the stack
|
||||
toolBar.visible = true
|
||||
|
||||
// Enable GUI gamepad usage now
|
||||
SdlGamepadKeyNavigation.enable()
|
||||
}
|
||||
|
||||
StackView.onActivated: {
|
||||
// Hide the toolbar before we start loading
|
||||
toolBar.visible = false
|
||||
|
||||
// Hook up our signals
|
||||
session.stageStarting.connect(stageStarting)
|
||||
session.stageFailed.connect(stageFailed)
|
||||
session.connectionStarted.connect(connectionStarted)
|
||||
session.displayLaunchError.connect(displayLaunchError)
|
||||
session.displayLaunchWarning.connect(displayLaunchWarning)
|
||||
session.quitStarting.connect(quitStarting)
|
||||
session.sessionFinished.connect(sessionFinished)
|
||||
|
||||
// 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)
|
||||
// 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)
|
||||
|
||||
if (quitAfter) {
|
||||
Qt.quit()
|
||||
} else {
|
||||
// Show the Qt window again after streaming
|
||||
window.visible = true
|
||||
|
||||
// Exit this view
|
||||
stackView.pop()
|
||||
|
||||
// Display any launch errors. We do this after
|
||||
// the Qt UI is visible again to prevent losing
|
||||
// focus on the dialog which would impact gamepad
|
||||
// users.
|
||||
if (errorDialog.text) {
|
||||
errorDialog.open()
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Show the toolbar again when we become hidden
|
||||
toolBar.visible = true
|
||||
}
|
||||
|
||||
sourceComponent: Item {}
|
||||
}
|
||||
|
||||
Row {
|
||||
@@ -119,6 +182,7 @@ Item {
|
||||
|
||||
BusyIndicator {
|
||||
id: stageSpinner
|
||||
running: false
|
||||
}
|
||||
|
||||
Label {
|
||||
@@ -129,7 +193,6 @@ Item {
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
|
||||
wrapMode: Text.Wrap
|
||||
color: "white"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,16 +205,5 @@ Item {
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
|
||||
wrapMode: Text.Wrap
|
||||
color: "white"
|
||||
}
|
||||
|
||||
MessageDialog {
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,14 +1,13 @@
|
||||
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 QtQuick.Controls.Material 2.1
|
||||
|
||||
import ComputerManager 1.0
|
||||
import AutoUpdateChecker 1.0
|
||||
import StreamingPreferences 1.0
|
||||
import SystemProperties 1.0
|
||||
import SdlGamepadKeyNavigation 1.0
|
||||
|
||||
ApplicationWindow {
|
||||
property bool pollingActive: false
|
||||
@@ -18,18 +17,19 @@ ApplicationWindow {
|
||||
width: 1280
|
||||
height: 600
|
||||
|
||||
// Maximize the window by default when the stream is configured
|
||||
// for full-screen or borderless windowed. This is ideal for TV
|
||||
// setups where the user doesn't want a tiny window in the middle
|
||||
// of their screen when starting Moonlight.
|
||||
visibility: prefs.windowMode != StreamingPreferences.WM_WINDOWED ? "Maximized" : "Windowed"
|
||||
|
||||
Material.theme: Material.Dark
|
||||
Material.accent: Material.Purple
|
||||
|
||||
StreamingPreferences {
|
||||
id: prefs
|
||||
visibility: {
|
||||
if (SystemProperties.hasWindowManager) {
|
||||
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
|
||||
|
||||
StackView {
|
||||
id: stackView
|
||||
@@ -141,23 +141,27 @@ ApplicationWindow {
|
||||
// Doing it earlier can lead to the dialog appearing behind
|
||||
// the window or otherwise without input focus.
|
||||
if (!initialized) {
|
||||
if (prefs.isRunningWayland()) {
|
||||
waylandDialog.open()
|
||||
}
|
||||
else if (prefs.isWow64()) {
|
||||
// Set initialized before calling anything else, because
|
||||
// pumping the event loop can cause us to get another
|
||||
// onAfterRendering call and potentially reenter this code.
|
||||
initialized = true;
|
||||
|
||||
if (SystemProperties.isWow64) {
|
||||
wow64Dialog.open()
|
||||
}
|
||||
else if (!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()
|
||||
}
|
||||
|
||||
initialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,28 +183,32 @@ 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()
|
||||
|
||||
@@ -209,14 +217,75 @@ ApplicationWindow {
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
@@ -224,28 +293,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.text: "Update available for Moonlight"
|
||||
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(url)
|
||||
function updateAvailable(version, url)
|
||||
{
|
||||
ToolTip.text = qsTr("Update available for Moonlight: Version %1").arg(version)
|
||||
updateButton.browserUrl = url
|
||||
updateButton.visible = true
|
||||
}
|
||||
@@ -262,20 +328,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
|
||||
@@ -298,18 +358,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)
|
||||
@@ -319,16 +372,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)
|
||||
@@ -343,72 +389,113 @@ 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,13 +4,14 @@
|
||||
#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);
|
||||
@@ -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,19 +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;
|
||||
}
|
||||
Q_ASSERT(m_Enabled);
|
||||
|
||||
int count = 0;
|
||||
for (int i = 0; i < SDL_NumJoysticks(); i++) {
|
||||
@@ -261,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,824 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!DOCTYPE TS>
|
||||
<TS version="2.1" language="fr_FR">
|
||||
<context>
|
||||
<name>AppView</name>
|
||||
<message>
|
||||
<location filename="../gui/AppView.qml" line="118"/>
|
||||
<location filename="../gui/AppView.qml" line="242"/>
|
||||
<source>Resume Game</source>
|
||||
<translation>Reprendre le jeu</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/AppView.qml" line="146"/>
|
||||
<location filename="../gui/AppView.qml" line="247"/>
|
||||
<source>Quit Game</source>
|
||||
<translation>Quitter le jeu</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/AppView.qml" line="242"/>
|
||||
<source>Launch Game</source>
|
||||
<translation>Lancer le jeu</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/AppView.qml" line="255"/>
|
||||
<source>Hide Game</source>
|
||||
<translation>Cacher le jeu</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/AppView.qml" line="268"/>
|
||||
<source>Are you sure you want to quit %1? Any unsaved progress will be lost.</source>
|
||||
<translation>Vous êtes sûr de vouloir arrêter %1? Tout progrès non enregistré sera perdu.</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>CliQuitStreamSegue</name>
|
||||
<message>
|
||||
<location filename="../gui/CliQuitStreamSegue.qml" line="9"/>
|
||||
<source>Establishing connection to PC...</source>
|
||||
<translation>connexion au PC...</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/CliQuitStreamSegue.qml" line="13"/>
|
||||
<source>Quitting app...</source>
|
||||
<translation>Fermeture de l'app...</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>CliStartStreamSegue</name>
|
||||
<message>
|
||||
<location filename="../gui/CliStartStreamSegue.qml" line="9"/>
|
||||
<source>Establishing connection to PC...</source>
|
||||
<translation>connexion au PC...</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/CliStartStreamSegue.qml" line="13"/>
|
||||
<source>Loading app list...</source>
|
||||
<translation>Chargement de la liste des applications...</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>Vous êtes sûr de vouloir arrêter %1? Tout progrès non enregistré sera perdu.</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>GamepadMapper</name>
|
||||
<message>
|
||||
<location filename="../gui/GamepadMapper.qml" line="4"/>
|
||||
<source>Gamepad Mapping</source>
|
||||
<translation>Configuration du controleur</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>NvHTTP</name>
|
||||
<message>
|
||||
<location filename="../backend/nvhttp.cpp" line="326"/>
|
||||
<source>Missing audio capture device. Reinstalling GeForce Experience should resolve this error.</source>
|
||||
<translation>Dispositif de capture audio manquant. La réinstallation de GeForce Experience devrait résoudre cette erreur.</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>PcView</name>
|
||||
<message>
|
||||
<location filename="../gui/PcView.qml" line="20"/>
|
||||
<source>Computers</source>
|
||||
<translation>Ordinateurs</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/PcView.qml" line="67"/>
|
||||
<source>Unable to connect to the specified PC.</source>
|
||||
<translation>Impossible de se connecter au PC spécifié.</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>La connexion Internet de ce PC bloque Moonlight. Le streaming sur Internet peut ne pas fonctionner lorsqu'il est connecté à ce réseau.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/PcView.qml" line="73"/>
|
||||
<source>Click the Help button for possible solutions.</source>
|
||||
<translation>Cliquez sur le bouton Aide pour connaître les solutions possibles.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Searching for PCs with NVIDIA GameStream enabled...</source>
|
||||
<translation type="vanished">La recherche de PC avec NVIDIA GameStream activé...</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/PcView.qml" line="103"/>
|
||||
<source>Automatic PC discovery is disabled. Add your PC manually.</source>
|
||||
<translation>La découverte automatique des PC est désactivée. Ajoutez votre PC manuellement.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/PcView.qml" line="167"/>
|
||||
<source>View Apps</source>
|
||||
<translation>Voir les applications</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/PcView.qml" line="177"/>
|
||||
<source>View Hidden Apps</source>
|
||||
<translation>Voir les applications cachées</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/PcView.qml" line="187"/>
|
||||
<source>Wake PC</source>
|
||||
<translation>Réveiller le PC</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/PcView.qml" line="193"/>
|
||||
<source>Test Network</source>
|
||||
<translation>Test Réseau</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/PcView.qml" line="202"/>
|
||||
<source>Rename PC</source>
|
||||
<translation>Renommer le PC</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/PcView.qml" line="211"/>
|
||||
<source>Delete PC</source>
|
||||
<translation>Supprimer le PC</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/PcView.qml" line="241"/>
|
||||
<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>Vous ne pouvez pas effectuer de couplage alors qu'une session précédente est encore en cours sur le PC hôte. Fermez les jeux en cours ou redémarrez le PC hôte, puis essayez à nouveau d'effectuer l'appariement.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/PcView.qml" line="296"/>
|
||||
<source>Please enter %1 on your GameStream PC. This dialog will close when pairing is completed.</source>
|
||||
<translation>Veuillez saisir %1 sur votre PC GameStream. Cette boîte de dialogue se fermera lorsque l'appariement sera terminé.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/PcView.qml" line="307"/>
|
||||
<source>Are you sure you want to remove this PC?</source>
|
||||
<translation>Êtes-vous sûr de vouloir supprimer ce PC ?</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/PcView.qml" line="324"/>
|
||||
<source>Moonlight is testing your network connection to determine if NVIDIA GameStream is blocked.</source>
|
||||
<translation>Moonlight teste votre connexion réseau pour déterminer si NVIDIA GameStream est bloqué.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/PcView.qml" line="324"/>
|
||||
<source>This may take a few seconds…</source>
|
||||
<translation>Cela peut prendre quelques secondes...</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/PcView.qml" line="335"/>
|
||||
<source>This network does not appear to be blocking Moonlight. If you still have trouble connecting, check your PC's firewall settings.</source>
|
||||
<translation>Ce réseau ne semble pas bloquer Moonlight. Si vous avez toujours des problèmes de connexion, vérifiez les paramètres du pare-feu de votre PC.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/PcView.qml" line="335"/>
|
||||
<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>Si vous essayez d'utiliser la diffusion en continu sur Internet, installez l'outil d'hébergement Internet Moonlight sur votre PC de jeu et exécutez le testeur de diffusion en continu sur Internet inclus pour vérifier la connexion Internet de votre PC de jeu.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/PcView.qml" line="339"/>
|
||||
<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>La connexion réseau actuelle de votre PC semble bloquer le Moonlight. Il se peut que la diffusion en continu sur Internet ne fonctionne pas lorsque vous êtes connecté à ce réseau.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/PcView.qml" line="339"/>
|
||||
<source>The following network ports were blocked:</source>
|
||||
<translation>Les ports de réseau suivants ont été bloqués :</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/PcView.qml" line="331"/>
|
||||
<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>Le test de réseau n'a pas pu être effectué car aucun des serveurs de test de connexion de Moonlight n'était accessible depuis ce PC. Vérifiez votre connexion Internet ou réessayez plus tard.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/PcView.qml" line="102"/>
|
||||
<source>Searching for PCs on your local network with NVIDIA GameStream enabled...</source>
|
||||
<translation>Recherche de PC sur votre réseau local avec NVIDIA GameStream activé...</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/PcView.qml" line="351"/>
|
||||
<source>Enter the new name for this PC:</source>
|
||||
<translation>Saisissez le nouveau nom de ce PC :</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>PendingQuitTask</name>
|
||||
<message>
|
||||
<location filename="../backend/computermanager.cpp" line="539"/>
|
||||
<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>Le jeu en cours d’exécution n'a pas été démarré par ce PC. Vous devez fermer le jeu sur le PC hôte manuellement ou utiliser l'appareil qui a démarré le jeu à l'origine.</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>N'a pas pu se connecter à %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>Le %1 de l'ordinateur n'a pas été jumelé. Veuillez ouvrir Moonlight pour jumeler avant le streaming.</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>La fermeture de l'application a échoué, raison : %1</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../cli/startstream.cpp" line="137"/>
|
||||
<source>Failed to find application %1</source>
|
||||
<translation>N'a pas trouvé l'application %1</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>QuitSegue</name>
|
||||
<message>
|
||||
<location filename="../gui/QuitSegue.qml" line="12"/>
|
||||
<source>Quitting %1...</source>
|
||||
<translation>Fermeture %1...</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>Session</name>
|
||||
<message>
|
||||
<location filename="../streaming/session.cpp" line="97"/>
|
||||
<source>No video received from host. Check the host PC's firewall and port forwarding rules.</source>
|
||||
<translation>Aucune vidéo reçue de l'hôte. Vérifiez le pare-feu et les règles de transfert de port du PC hôte.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../streaming/session.cpp" line="102"/>
|
||||
<source>Your network connection isn't performing well. Reduce your video bitrate setting or try a faster connection.</source>
|
||||
<translation>Votre connexion réseau ne fonctionne pas bien. Réduisez votre débit vidéo ou essayez une connexion plus rapide.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../streaming/session.cpp" line="107"/>
|
||||
<source>Connection terminated</source>
|
||||
<translation>Connexion terminée</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../streaming/session.cpp" line="567"/>
|
||||
<source>Your selection to enable remote desktop mouse mode may cause problems in games.</source>
|
||||
<translation>Votre sélection pour activer le mode de souris de bureau à distance peut causer des problèmes dans les jeux.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../streaming/session.cpp" line="572"/>
|
||||
<source>HDR is not supported with software decoding.</source>
|
||||
<translation>Le HDR n'est pas pris en charge par le décodage logiciel.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../streaming/session.cpp" line="576"/>
|
||||
<source>Your settings selection to force software decoding may cause poor streaming performance.</source>
|
||||
<translation>La sélection de vos paramètres pour forcer le décodage du logiciel peut entraîner de mauvaises performances de streaming.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../streaming/session.cpp" line="581"/>
|
||||
<source>Using unsupported FPS options may cause stuttering or lag.</source>
|
||||
<translation>L'utilisation d'options de FPS non prises en charge peut entraîner un stuttering ou des lag.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../streaming/session.cpp" line="584"/>
|
||||
<source>V-sync will be disabled when streaming at a higher frame rate than the display.</source>
|
||||
<translation>La synchronisation vertical sera désactivée lors de la diffusion en continu à une fréquence d'images supérieure à celle de l'affichage.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../streaming/session.cpp" line="601"/>
|
||||
<source>Using software decoding due to your selection to force HEVC without GPU support. This may cause poor streaming performance.</source>
|
||||
<translation>Utilisation du décodage logiciel dû à votre sélection pour forcer les HEVC sans support GPU. Cela peut entraîner de mauvaises performances de streaming.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../streaming/session.cpp" line="604"/>
|
||||
<source>This PC's GPU doesn't support HEVC decoding.</source>
|
||||
<translation>Le GPU de ce PC ne prend pas en charge le décodage HEVC.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../streaming/session.cpp" line="611"/>
|
||||
<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>Le GPU de votre PC hôte ne prend pas en charge les HEVC. Un GPU GeForce GTX série 900 (Maxwell) ou plus récent est nécessaire pour le streaming HEVC.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../streaming/session.cpp" line="628"/>
|
||||
<source>%1 doesn't support HDR10.</source>
|
||||
<translation>%1 ne prend pas en charge le HDR10.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../streaming/session.cpp" line="632"/>
|
||||
<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>Le GPU de votre PC hôte ne prend pas en charge le streaming HDR. Un GPU GeForce GTX série 1000 (Pascal) ou plus récent est nécessaire pour la diffusion HDR.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../streaming/session.cpp" line="641"/>
|
||||
<source>This PC's GPU doesn't support HEVC Main10 decoding for HDR streaming.</source>
|
||||
<translation>Le GPU de ce PC ne prend pas en charge le décodage HEVC pour le streaming HDR.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../streaming/session.cpp" line="654"/>
|
||||
<source>GeForce Experience 3.0 or higher is required for 4K streaming.</source>
|
||||
<translation>Une expérience GeForce 3.0 ou supérieure est requise pour le streaming 4K.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../streaming/session.cpp" line="669"/>
|
||||
<source>Your selected surround sound setting is not supported by the current audio device.</source>
|
||||
<translation>Le réglage du son surround que vous avez sélectionné n'est pas pris en charge par l'appareil audio actuel.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../streaming/session.cpp" line="676"/>
|
||||
<source>Failed to open audio device. Audio will be unavailable during this session.</source>
|
||||
<translation>Echec de l'ouverture de l'appareil audio. L'audio ne sera pas disponible pendant cette session.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../streaming/session.cpp" line="681"/>
|
||||
<source>An attached gamepad has no mapping and won't be usable. Visit the Moonlight help to resolve this.</source>
|
||||
<translation>Une manette de jeun n'a pas était configurée et ne sera pas utilisable. Visitez le Moonlight pour aider à résoudre ce problème.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../streaming/session.cpp" line="687"/>
|
||||
<source>Your host PC's GPU doesn't support streaming video resolutions over 4K.</source>
|
||||
<translation>Le GPU de votre PC hôte ne prend pas en charge les résolutions vidéo en continu supérieures à 4K.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../streaming/session.cpp" line="691"/>
|
||||
<source>Video resolutions over 4K are only supported by the HEVC codec.</source>
|
||||
<translation>Les résolutions vidéo supérieures à 4K sont uniquement prises en charge par le codec HEVC.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../streaming/session.cpp" line="705"/>
|
||||
<source>Your selection to force hardware decoding cannot be satisfied due to missing hardware decoding support on this PC's GPU.</source>
|
||||
<translation>Votre choix pour forcer le décodage matériel ne peut être satisfait en raison de l'absence de support de décodage matériel sur le GPU de ce PC.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../streaming/session.cpp" line="708"/>
|
||||
<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>Votre sélection de codecs et le réglage du décodage matériel ne sont pas compatibles. Le GPU de ce PC ne prend pas en charge le décodage du codec que vous avez choisi.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../streaming/session.cpp" line="1016"/>
|
||||
<source>GeForce Experience returned error: %1</source>
|
||||
<translation>GeForce Experience a renvoyé l'erreur : %1</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../streaming/session.cpp" line="1397"/>
|
||||
<source>Unable to initialize video decoder. Please check your streaming settings and try again.</source>
|
||||
<translation>Impossible d'initialiser le décodeur vidéo. Veuillez vérifier vos paramètres de diffusion en continu et réessayer.</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>SettingsView</name>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="11"/>
|
||||
<source>Settings</source>
|
||||
<translation>Paramètres</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="63"/>
|
||||
<source>Resolution and FPS</source>
|
||||
<translation>Résolution et FPS</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="71"/>
|
||||
<source>Setting values too high for your PC or network connection may cause lag, stuttering, or errors.</source>
|
||||
<translation>Le fait de fixer des valeurs trop élevées pour votre PC ou votre connexion réseau peut entraîner un décalage, des bégaiements ou des erreurs.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="53"/>
|
||||
<source>Basic Settings</source>
|
||||
<translation>Paramètres de base</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="173"/>
|
||||
<source>720p</source>
|
||||
<translation>720p</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="178"/>
|
||||
<source>1080p</source>
|
||||
<translation>1080p</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="183"/>
|
||||
<source>1440p</source>
|
||||
<translation>1440p</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="188"/>
|
||||
<source>4K</source>
|
||||
<translation>4K</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="218"/>
|
||||
<location filename="../gui/SettingsView.qml" line="219"/>
|
||||
<location filename="../gui/SettingsView.qml" line="239"/>
|
||||
<location filename="../gui/SettingsView.qml" line="242"/>
|
||||
<source>%1 FPS</source>
|
||||
<translation>%1 FPS</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="224"/>
|
||||
<location filename="../gui/SettingsView.qml" line="227"/>
|
||||
<location filename="../gui/SettingsView.qml" line="236"/>
|
||||
<location filename="../gui/SettingsView.qml" line="249"/>
|
||||
<location filename="../gui/SettingsView.qml" line="252"/>
|
||||
<source>%1 FPS (Unsupported)</source>
|
||||
<translation>%1 FPS (Non supporté)</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="305"/>
|
||||
<source>Video bitrate:</source>
|
||||
<translation>Bitrate vidéo :</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="313"/>
|
||||
<source>Lower the bitrate on slower connections. Raise the bitrate to increase image quality.</source>
|
||||
<translation>Baissez le débit binaire sur les connexions plus lentes. Augmentez le débit binaire pour améliorer la qualité de l'image.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="331"/>
|
||||
<source>Video bitrate: %1 Mbps</source>
|
||||
<translation>Bitrate vidéo : %1 Mbps</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="339"/>
|
||||
<source>Display mode</source>
|
||||
<translation>Mode d'affichage</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="351"/>
|
||||
<source> (Recommended)</source>
|
||||
<translation> (Recommandé)</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="380"/>
|
||||
<source>Full-screen</source>
|
||||
<translation>Plein écran</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="384"/>
|
||||
<source>Borderless windowed</source>
|
||||
<translation>fenêtré sans bordure</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="388"/>
|
||||
<source>Windowed</source>
|
||||
<translation>Fenêtré</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="399"/>
|
||||
<source>Full-screen 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>Le plein écran offre généralement les meilleures performances, mais les fenêtres sans bordure peuvent fonctionner mieux avec des fonctionnalités telles que MacOS Spaces, Alt+Tab, les outils de capture d'écran, les superpositions à l'écran, etc.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="406"/>
|
||||
<source>V-Sync</source>
|
||||
<translation>synchronisation verticale</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="416"/>
|
||||
<source>Disabling V-Sync allows sub-frame rendering latency, but it can display visible tearing</source>
|
||||
<translation>La désactivation de V-Sync permet une latence de rendu des sous-images plus faible, mais elle peut afficher des déchirures visibles</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="423"/>
|
||||
<source>Frame pacing</source>
|
||||
<translation>Frame pacing</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="433"/>
|
||||
<source>Frame pacing reduces micro-stutter by delaying frames that come in too early</source>
|
||||
<translation>Frame pacing réduit les micro-bégaiements en retardant les images qui arrivent trop tôt</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="443"/>
|
||||
<source>Audio Settings</source>
|
||||
<translation>Réglages audio</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="478"/>
|
||||
<source>Stereo</source>
|
||||
<translation>Stereo</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="482"/>
|
||||
<source>5.1 surround sound</source>
|
||||
<translation>5.1 son surround</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="486"/>
|
||||
<source>7.1 surround sound</source>
|
||||
<translation>7.1 son surround</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="503"/>
|
||||
<source>UI Settings</source>
|
||||
<translation>Réglages de l'interface utilisateur</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="565"/>
|
||||
<source>Input Settings</source>
|
||||
<translation>Paramètres d'entrée</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="585"/>
|
||||
<source>Forces a single gamepad to always stay connected to the host, even if no gamepads are actually connected to this PC.</source>
|
||||
<translation>Forces a single gamepad to always stay connected to the host, even if no gamepads are actually connected to this PC.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="604"/>
|
||||
<source>This enables mouse control without capturing the client's mouse cursor. It will not work in most games.</source>
|
||||
<translation>Cela permet de contrôler la souris sans avoir à capturer le curseur du client. Cela ne fonctionne pas dans la plupart des jeux.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="605"/>
|
||||
<source>You can toggle this while streaming using Ctrl+Alt+Shift+M.</source>
|
||||
<translation>Vous pouvez le basculer pendant la diffusion en utilisant Ctrl+Alt+Shift+M.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="665"/>
|
||||
<source>Host Settings</source>
|
||||
<translation>Paramètres de l'hôte</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="716"/>
|
||||
<source>Advanced Settings</source>
|
||||
<translation>Paramètres avancés</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="453"/>
|
||||
<source>Audio configuration</source>
|
||||
<translation>Configuration audio</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="513"/>
|
||||
<source>Maximize Moonlight window on startup</source>
|
||||
<translation>Maximiser la fenêtre Moonlight au démarrage</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="525"/>
|
||||
<source>Show connection quality warnings</source>
|
||||
<translation>Afficher les avertissements sur la qualité de la connexion</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="537"/>
|
||||
<source>Discord Rich Presence integration</source>
|
||||
<translation>Intégration de la présence Discord Rich</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="547"/>
|
||||
<source>Updates your Discord status to display the name of the game you're streaming.</source>
|
||||
<translation>Met à jour votre statut Discord pour afficher le nom du jeu que vous diffusez en continu.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="575"/>
|
||||
<source>Force gamepad #1 always present</source>
|
||||
<translation>Forcé la manette de jeu Force #1 à être toujours présente</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="586"/>
|
||||
<source>Only enable this option when streaming a game that doesn't support gamepads being connected after startup.</source>
|
||||
<translation>N'activez cette option que lorsque vous diffusez en continu un jeu qui ne prend pas en charge la connexion des manettes de jeu après le démarrage.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="593"/>
|
||||
<source>Optimize mouse for remote desktop instead of games</source>
|
||||
<translation>Optimiser la souris pour le bureau à distance plutôt que pour les jeux</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="612"/>
|
||||
<source>Use touchscreen as a trackpad</source>
|
||||
<translation>Utiliser l'écran tactile comme trackpad</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="622"/>
|
||||
<source>When checked, the touchscreen acts like a trackpad. When unchecked, the touchscreen will directly control the mouse pointer.</source>
|
||||
<translation>Lorsqu'on le vérifie, l'écran tactile agit comme un pavé tactile. Lorsqu'il n'est pas coché, l'écran tactile contrôle directement le pointeur de la souris.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="629"/>
|
||||
<source>Gamepad mouse mode support</source>
|
||||
<translation>Prise en charge du mode souris de la manette de jeu</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="639"/>
|
||||
<source>When enabled, holding the Start button will toggle mouse mode</source>
|
||||
<translation>Lorsqu'il est activé, le maintien du bouton "Start" permet de passer en mode souris</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="646"/>
|
||||
<source>Swap mouse buttons</source>
|
||||
<translation>Échanger les boutons de la souris</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="656"/>
|
||||
<source>When checked, swap the left and right mouse buttons</source>
|
||||
<translation>Lorsque la case est cochée, permuter les boutons gauche et droit de la souris</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="675"/>
|
||||
<source>Optimize game settings for streaming</source>
|
||||
<translation>Optimiser les paramètres du jeu pour la diffusion en continu</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="686"/>
|
||||
<source>Play audio on host PC</source>
|
||||
<translation>Lecture audio sur le PC hôte</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="697"/>
|
||||
<source>Quit app on host PC after ending stream</source>
|
||||
<translation>Quitter l'application sur le PC hôte après avoir terminé le flux</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="707"/>
|
||||
<source>This will close the app or game you are streaming when you end your stream. You will lose any unsaved progress!</source>
|
||||
<translation>Cela fermera l'application ou le jeu que vous êtes en train de diffuser lorsque vous aurez terminé votre flux. Vous perdrez tout progrès non sauvegardé !</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="726"/>
|
||||
<source>Video decoder</source>
|
||||
<translation>Décodeur vidéo</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="751"/>
|
||||
<location filename="../gui/SettingsView.qml" line="797"/>
|
||||
<source>Automatic (Recommended)</source>
|
||||
<translation>Automatique (recommandé)</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="755"/>
|
||||
<source>Force software decoding</source>
|
||||
<translation>Forcer le décodage des logiciels</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="759"/>
|
||||
<source>Force hardware decoding</source>
|
||||
<translation>Forcer le décodage du matériel</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="772"/>
|
||||
<source>Video codec</source>
|
||||
<translation>Codec vidéo</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="801"/>
|
||||
<source>H.264</source>
|
||||
<translation>H.264</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="805"/>
|
||||
<source>HEVC (H.265)</source>
|
||||
<translation>HEVC (H.265)</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="822"/>
|
||||
<source>Unlock unsupported FPS options</source>
|
||||
<translation>Débloquer les options non supportées du FPS</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="841"/>
|
||||
<source>Automatically find PCs on the local network (Recommended)</source>
|
||||
<translation>Trouver automatiquement les PC sur le réseau local (recommandé)</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/SettingsView.qml" line="866"/>
|
||||
<source>Automatically detect blocked connections (Recommended)</source>
|
||||
<translation>Détecter automatiquement les connexions bloquées (recommandé)</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>StreamSegue</name>
|
||||
<message>
|
||||
<location filename="../gui/StreamSegue.qml" line="11"/>
|
||||
<location filename="../gui/StreamSegue.qml" line="17"/>
|
||||
<source>Starting %1...</source>
|
||||
<translation>Démarrage de %1...</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/StreamSegue.qml" line="23"/>
|
||||
<source>Starting %1 failed: Error %2</source>
|
||||
<translation>Démarrage de %1... echec: erreur %2</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/StreamSegue.qml" line="68"/>
|
||||
<source>This PC's Internet connection is blocking Moonlight. Streaming over the Internet may not work while connected to this network.</source>
|
||||
<translation>La connexion Internet de ce PC bloque Moonlight. Le streaming sur Internet peut ne pas fonctionner lorsqu'il est connecté à ce réseau.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/StreamSegue.qml" line="150"/>
|
||||
<source>Tip:</source>
|
||||
<translation>Aide:</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/StreamSegue.qml" line="150"/>
|
||||
<source>Press %1 to disconnect your session</source>
|
||||
<translation>Appuyez sur %1 pour déconnecter votre session</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/StreamSegue.qml" line="151"/>
|
||||
<source>Start+Select+L1+R1</source>
|
||||
<translation>Start+Select+L1+R1</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/StreamSegue.qml" line="151"/>
|
||||
<source>Ctrl+Alt+Shift+Q</source>
|
||||
<translation>Ctrl + Alt + Shift + Q</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>main</name>
|
||||
<message>
|
||||
<location filename="../gui/main.qml" line="226"/>
|
||||
<location filename="../gui/main.qml" line="236"/>
|
||||
<location filename="../gui/main.qml" line="365"/>
|
||||
<location filename="../gui/main.qml" line="380"/>
|
||||
<source>Settings</source>
|
||||
<translation>Paramètres</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/main.qml" line="227"/>
|
||||
<source>Version %1</source>
|
||||
<translation>Version %1</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/main.qml" line="243"/>
|
||||
<source>Join our community on Discord</source>
|
||||
<translation>Rejoignez notre communauté sur Discord</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/main.qml" line="255"/>
|
||||
<source>Computers</source>
|
||||
<translation>Ordinateurs</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/main.qml" line="262"/>
|
||||
<source>Add PC manually</source>
|
||||
<translation>Ajouter un PC manuellement</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/main.qml" line="302"/>
|
||||
<source>Update available for Moonlight: Version %1</source>
|
||||
<translation>Mise à jour disponible pour Moonlight : Version %1</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/main.qml" line="349"/>
|
||||
<source>Gamepad Mapper</source>
|
||||
<translation>configuration du controleur</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/main.qml" line="353"/>
|
||||
<source>Gamepad Mapping</source>
|
||||
<translation>configuration du controleur</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/main.qml" line="387"/>
|
||||
<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>Aucun décodeur vidéo H.264 accéléré par le matériel n'a été détecté par Moonlight. Dans cette configuration, les performances de votre streaming peuvent être fortement dégradées.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/main.qml" line="395"/>
|
||||
<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>L'accélération matérielle ne fonctionne pas sur XWayland. Continuer sur XWayland peut entraîner de mauvaises performances de streaming. Essayez de fonctionner avec QT_QPA_PLATFORM=wayland ou passez à X11.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/main.qml" line="413"/>
|
||||
<source>Moonlight detected gamepads without a mapping:</source>
|
||||
<translation>Moonlight a détecté des manettes de jeu san configuration:</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/main.qml" line="326"/>
|
||||
<source>Help</source>
|
||||
<translation>Aide</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/main.qml" line="389"/>
|
||||
<source>Click the Help button for more information on solving this problem.</source>
|
||||
<translation>Cliquez sur le bouton Aide pour obtenir plus d'informations sur la manière de résoudre ce problème.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/main.qml" line="397"/>
|
||||
<source>Click the Help button for more information.</source>
|
||||
<translation>Cliquez sur le bouton Aide pour plus d'informations.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/main.qml" line="404"/>
|
||||
<source>This PC is running a 64-bit version of Windows. Please download the x64 version of Moonlight for the best streaming performance.</source>
|
||||
<translation>Ce PC fonctionne avec une version 64 bits de Windows. Veuillez télécharger la version x64 de Moonlight pour obtenir les meilleures performances de diffusion en continu.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/main.qml" line="415"/>
|
||||
<source>Click the Help button for information on how to map your gamepads.</source>
|
||||
<translation>Cliquez sur le bouton Aide pour obtenir des informations sur la configuration de vos manettes de jeu.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/main.qml" line="423"/>
|
||||
<source>Are you sure you want to quit?</source>
|
||||
<translation>Vous êtes sûr de vouloir quitter?</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../gui/main.qml" line="453"/>
|
||||
<source>Enter the IP address of your GameStream PC:</source>
|
||||
<translation>Saisissez l'adresse IP de votre PC GameStream :</translation>
|
||||
</message>
|
||||
</context>
|
||||
</TS>
|
||||
@@ -5,6 +5,13 @@
|
||||
#include <QQuickStyle>
|
||||
#include <QMutex>
|
||||
#include <QtDebug>
|
||||
#include <QNetworkProxyFactory>
|
||||
#include <QPalette>
|
||||
#include <QFont>
|
||||
#include <QCursor>
|
||||
#include <QElapsedTimer>
|
||||
#include <QFile>
|
||||
#include<QTranslator>
|
||||
|
||||
// Don't let SDL hook our main function, since Qt is already
|
||||
// doing the same thing. This needs to be before any headers
|
||||
@@ -16,16 +23,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"
|
||||
@@ -46,7 +58,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
|
||||
@@ -65,7 +77,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;
|
||||
}
|
||||
@@ -180,7 +197,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;
|
||||
@@ -221,6 +238,11 @@ 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);
|
||||
|
||||
// Set these here to allow us to use the default QSettings constructor.
|
||||
// These also ensure that our cache directory is named correctly. As such,
|
||||
// it is critical that these be called before Path::initialize().
|
||||
@@ -267,19 +289,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", QByteArray("1"));
|
||||
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)
|
||||
@@ -288,18 +351,157 @@ int main(int argc, char *argv[])
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(Q_OS_DARWIN) && QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
if (!qEnvironmentVariableIsSet("QSG_RHI_BACKEND")) {
|
||||
// The Metal backend in Qt 6.0.0 causes really strange issues transitioning to
|
||||
// full-screen in our SDL window (hangs in SDL's Cocoa_SetWindowFullscreenSpace())
|
||||
// and breaks drawing our status updates in the StreamSegue, so use OpenGL like
|
||||
// Qt 5 does until we figure out the cause of these issues.
|
||||
qputenv("QSG_RHI_BACKEND", "opengl");
|
||||
}
|
||||
#endif
|
||||
|
||||
// We don't want system proxies to apply to us
|
||||
QNetworkProxyFactory::setUseSystemConfiguration(false);
|
||||
|
||||
// Clear any default application proxy
|
||||
QNetworkProxy noProxy(QNetworkProxy::NoProxy);
|
||||
QNetworkProxy::setApplicationProxy(noProxy);
|
||||
|
||||
// Register custom metatypes for use in signals
|
||||
qRegisterMetaType<NvApp>("NvApp");
|
||||
|
||||
SDL_version compileVersion;
|
||||
SDL_VERSION(&compileVersion);
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Compiled with SDL %d.%d.%d",
|
||||
compileVersion.major, compileVersion.minor, compileVersion.patch);
|
||||
|
||||
SDL_version runtimeVersion;
|
||||
SDL_GetVersion(&runtimeVersion);
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Running with SDL %d.%d.%d",
|
||||
runtimeVersion.major, runtimeVersion.minor, runtimeVersion.patch);
|
||||
|
||||
// Allow the display to sleep by default. We will manually use SDL_DisableScreenSaver()
|
||||
// and SDL_EnableScreenSaver() when appropriate. This hint must be set before
|
||||
// initializing the SDL video subsystem to have any effect.
|
||||
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");
|
||||
|
||||
// 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);
|
||||
|
||||
QTranslator translator;
|
||||
qDebug() << "Translation loaded:" << translator.load(QString(":/languages/qml_") + QLocale::system().name());
|
||||
app.installTranslator(&translator);
|
||||
|
||||
// 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
|
||||
|
||||
// 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",
|
||||
@@ -311,8 +513,48 @@ 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*, QJSEngine*) -> QObject* {
|
||||
return new StreamingPreferences();
|
||||
});
|
||||
|
||||
QQuickStyle::setStyle("Material");
|
||||
// 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");
|
||||
}
|
||||
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_MATERIAL_ACCENT")) {
|
||||
qputenv("QT_QUICK_CONTROLS_MATERIAL_ACCENT", "Purple");
|
||||
}
|
||||
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_MATERIAL_VARIANT")) {
|
||||
qputenv("QT_QUICK_CONTROLS_MATERIAL_VARIANT", "Dense");
|
||||
}
|
||||
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_UNIVERSAL_THEME")) {
|
||||
qputenv("QT_QUICK_CONTROLS_UNIVERSAL_THEME", "Dark");
|
||||
}
|
||||
|
||||
QQmlApplicationEngine engine;
|
||||
QString initialView;
|
||||
@@ -320,11 +562,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);
|
||||
@@ -334,6 +576,15 @@ int main(int argc, char *argv[])
|
||||
engine.rootContext()->setContextProperty("launcher", launcher);
|
||||
break;
|
||||
}
|
||||
case GlobalCommandLineParser::QuitRequested:
|
||||
{
|
||||
initialView = "qrc:/gui/CliQuitStreamSegue.qml";
|
||||
QuitCommandLineParser quitParser;
|
||||
quitParser.parse(app.arguments());
|
||||
auto launcher = new CliQuitStream::Launcher(quitParser.getHost(), &app);
|
||||
engine.rootContext()->setContextProperty("launcher", launcher);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
engine.rootContext()->setContextProperty("initialView", initialView);
|
||||
@@ -342,24 +593,11 @@ 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(0) != 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
|
||||
// sometimes freezing and blocking process exit.
|
||||
QThreadPool::globalInstance()->waitForDone(30000);
|
||||
|
||||
return err;
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 345 KiB After Width: | Height: | Size: 328 KiB |
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 2.6 KiB |
@@ -6,9 +6,9 @@
|
||||
#include <QSettings>
|
||||
#include <QCoreApplication>
|
||||
|
||||
QString Path::s_CacheDir;
|
||||
QString Path::s_LogDir;
|
||||
QString Path::s_BoxArtCacheDir;
|
||||
const QString Path::k_GameControllerMappingFile("gamecontrollerdb.txt");
|
||||
|
||||
QString Path::getLogDir()
|
||||
{
|
||||
@@ -22,40 +22,74 @@ QString Path::getBoxArtCacheDir()
|
||||
return s_BoxArtCacheDir;
|
||||
}
|
||||
|
||||
QString Path::getGamepadMappingFile()
|
||||
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
|
||||
candidatePath = QDir(QDir::currentPath()).absoluteFilePath(k_GameControllerMappingFile);
|
||||
// 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, k_GameControllerMappingFile);
|
||||
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(k_GameControllerMappingFile);
|
||||
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(k_GameControllerMappingFile);
|
||||
candidatePath = dir.absoluteFilePath(k_GameControllerMappingFile);
|
||||
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)
|
||||
@@ -63,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
|
||||
@@ -72,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,12 +10,18 @@ public:
|
||||
|
||||
static QString getBoxArtCacheDir();
|
||||
|
||||
static QString getGamepadMappingFile();
|
||||
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;
|
||||
static const QString k_GameControllerMappingFile;
|
||||
};
|
||||
|
||||
@@ -10,7 +10,13 @@
|
||||
<file>gui/NavigableToolButton.qml</file>
|
||||
<file>gui/NavigableItemDelegate.qml</file>
|
||||
<file>gui/NavigableMenuItem.qml</file>
|
||||
<file>gui/CliQuitStreamSegue.qml</file>
|
||||
<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" width="24" height="24" viewBox="0 0 24 24" fill="#fff"><path d="M12 2C6.47 2 2 6.47 2 12s4.47 10 10 10 10-4.47 10-10S17.53 2 12 2zm5 13.59L15.59 17 12 13.41 8.41 17 7 15.59 10.59 12 7 8.41 8.41 7 12 10.59 15.59 7 17 8.41 13.41 12 17 15.59z"/><path d="M0 0h24v24H0z" fill="none"/></svg>
|
||||
|
After Width: | Height: | Size: 325 B |
@@ -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,69 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
id="svg3761"
|
||||
viewBox="0 0 680 680"
|
||||
height="680"
|
||||
width="680"
|
||||
version="1.0">
|
||||
<metadata
|
||||
id="metadata3767">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs3765" />
|
||||
<g
|
||||
id="g3759"
|
||||
transform="translate(-135,491) scale(0.100000,-0.100000)">
|
||||
<circle
|
||||
id="circle3741"
|
||||
fill="#565C64"
|
||||
r="3400"
|
||||
cy="1505"
|
||||
cx="4755" />
|
||||
<path
|
||||
id="path3743"
|
||||
style="fill:#ffffff"
|
||||
d="M4550 4230 c-582 -48 -1123 -271 -1568 -646 l-84 -70 869 -869 c477 -477 871 -868 876 -868 4 0 6 552 5 1228 l-3 1227 -25 1 c-14 0 -45 -1 -70 -3z" />
|
||||
<path
|
||||
id="path3745"
|
||||
style="fill:#ffffff"
|
||||
d="M4867 4230 c-10 -9 -9 -2453 1 -2453 4 0 398 391 875 868 l869 869 -84 70 c-344 290 -749 492 -1178 587 -166 36 -469 74 -483 59z" />
|
||||
<path
|
||||
id="path3747"
|
||||
style="fill:#ffffff"
|
||||
d="M2673 3275 c-241 -285 -412 -595 -526 -953 -63 -196 -111 -458 -119 -640 l-3 -70 1228 -3 c675 -1 1227 1 1227 5 0 5 -391 399 -868 876 l-869 869 -70 -84z" />
|
||||
<path
|
||||
id="path3749"
|
||||
style="fill:#ffffff"
|
||||
d="M5898 2490 c-478 -477 -868 -871 -868 -876 0 -4 552 -6 1228 -5 l1227 3 -3 70 c-14 326 -124 729 -287 1055 -91 182 -259 430 -387 573 l-43 48 -867 -868z" />
|
||||
<path
|
||||
id="path3751"
|
||||
style="fill:#ffffff"
|
||||
d="M2026 1388 c-7 -11 10 -198 30 -321 76 -486 291 -952 617 -1338 l70 -84 869 869 c477 477 868 871 868 875 0 11 -2448 10 -2454 -1z" />
|
||||
<path
|
||||
id="path3753"
|
||||
style="fill:#ffffff"
|
||||
d="M5030 1390 c0 -5 391 -399 868 -876 l869 -869 70 84 c241 285 412 595 526 953 63 196 111 458 119 640 l3 70 -1227 3 c-676 1 -1228 -1 -1228 -5z" />
|
||||
<path
|
||||
id="path3755"
|
||||
style="fill:#ffffff"
|
||||
d="M3767 359 l-869 -869 84 -70 c285 -241 595 -412 953 -526 196 -63 458 -111 640 -119 l70 -3 3 1228 c1 675 -1 1227 -5 1227 -5 0 -399 -391 -876 -868z" />
|
||||
<path
|
||||
id="path3757"
|
||||
style="fill:#ffffff"
|
||||
d="M4862 0 l3 -1228 70 3 c182 8 444 56 640 119 358 114 668 285 953 526 l84 70 -869 869 c-477 477 -871 868 -876 868 -4 0 -6 -552 -5 -1227z" />
|
||||
</g>
|
||||
</svg>
|
||||
<?xml version="1.0" standalone="no"?><!-- Generator: Gravit.io --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="isolation:isolate" viewBox="0 0 256 256" width="256" height="256"><g><circle vector-effect="non-scaling-stroke" cx="128" cy="128" r="128" fill="rgb(86,92,100)"/><circle vector-effect="non-scaling-stroke" cx="128.00000000000003" cy="128.00000000000003" r="96" fill="rgb(255,255,255)"/><path d=" M 137.66 132 L 224 132 L 224 124 L 137.66 124 L 198.71 62.95 L 193.05 57.29 L 132 118.34 L 132 32 L 124 32 L 124 118.34 L 62.95 57.29 L 57.29 62.95 L 118.34 124 L 32 124 L 32 132 L 118.34 132 L 57.29 193.05 L 62.95 198.71 L 124 137.66 L 124 224 L 132 224 L 132 137.66 L 193.05 198.71 L 198.71 193.05 L 137.66 132 Z " fill-rule="evenodd" fill="rgb(86,92,100)"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 820 B |
@@ -1,3 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="24" height="24" viewBox="0 0 24 24" fill="#0BF200">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="24" height="24" viewBox="0 0 24 24" fill="#FF0000">
|
||||
<path d="M21,10.12H14.22L16.96,7.3C14.23,4.6 9.81,4.5 7.08,7.2C4.35,9.91 4.35,14.28 7.08,17C9.81,19.7 14.23,19.7 16.96,17C18.32,15.65 19,14.08 19,12.1H21C21,14.08 20.12,16.65 18.36,18.39C14.85,21.87 9.15,21.87 5.64,18.39C2.14,14.92 2.11,9.28 5.62,5.81C9.13,2.34 14.76,2.34 18.27,5.81L21,3V10.12M12.5,8V12.25L16,14.33L15.28,15.54L11,13V8H12.5Z" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 499 B After Width: | Height: | Size: 499 B |
@@ -1,18 +1,31 @@
|
||||
<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>
|
||||
<file>res/baseline-warning-24px.svg</file>
|
||||
<file>res/baseline-cancel-24px.svg</file>
|
||||
<file>res/no_app_image.png</file>
|
||||
<file>res/settings.svg</file>
|
||||
<file>res/arrow_left.svg</file>
|
||||
<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_fr.ts</file>
|
||||
<file>languages/qml_fr.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, SIGNAL(finished(QNetworkReply*)),
|
||||
this, SLOT(handleMappingListFetched(QNetworkReply*)));
|
||||
}
|
||||
|
||||
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,7 +34,13 @@ MappingManager::MappingManager()
|
||||
settings.endArray();
|
||||
|
||||
// Finally load mappings from SDL_HINT_GAMECONTROLLERCONFIG
|
||||
QStringList sdlMappings = QString::fromLocal8Bit(SDL_GetHint(SDL_HINT_GAMECONTROLLERCONFIG)).split('\n', QString::SkipEmptyParts);
|
||||
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 (QString sdlMapping : sdlMappings) {
|
||||
SdlGamepadMapping mapping(sdlMapping);
|
||||
addMapping(mapping);
|
||||
@@ -54,29 +68,34 @@ void MappingManager::save()
|
||||
|
||||
void MappingManager::applyMappings()
|
||||
{
|
||||
QString mappingFile = Path::getGamepadMappingFile();
|
||||
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,6 +1,4 @@
|
||||
#include "streamingpreferences.h"
|
||||
#include "streaming/session.h"
|
||||
#include "streaming/streamutils.h"
|
||||
|
||||
#include <QSettings>
|
||||
|
||||
@@ -20,7 +18,26 @@
|
||||
#define SER_WINDOWMODE "windowmode"
|
||||
#define SER_UNSUPPORTEDFPS "unsupportedfps"
|
||||
#define SER_MDNS "mdns"
|
||||
#define SER_MOUSEACCELERATION "mouseacceleration"
|
||||
#define SER_QUITAPPAFTER "quitAppAfter"
|
||||
#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 CURRENT_DEFAULT_VER 1
|
||||
|
||||
StreamingPreferences::StreamingPreferences(QObject *parent)
|
||||
: QObject(parent)
|
||||
@@ -32,6 +49,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();
|
||||
@@ -42,7 +67,21 @@ void StreamingPreferences::reload()
|
||||
multiController = settings.value(SER_MULTICONT, true).toBool();
|
||||
unsupportedFps = settings.value(SER_UNSUPPORTEDFPS, false).toBool();
|
||||
enableMdns = settings.value(SER_MDNS, true).toBool();
|
||||
mouseAcceleration = settings.value(SER_MOUSEACCELERATION, false).toBool();
|
||||
quitAppAfter = settings.value(SER_QUITAPPAFTER, 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();
|
||||
captureSysKeys = settings.value(SER_CAPTURESYSKEYS, false).toBool();
|
||||
audioConfig = static_cast<AudioConfig>(settings.value(SER_AUDIOCFG,
|
||||
static_cast<int>(AudioConfig::AC_STEREO)).toInt());
|
||||
videoCodecConfig = static_cast<VideoCodecConfig>(settings.value(SER_VIDEOCFG,
|
||||
@@ -52,7 +91,21 @@ 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());
|
||||
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
void StreamingPreferences::save()
|
||||
@@ -69,134 +122,27 @@ void StreamingPreferences::save()
|
||||
settings.setValue(SER_MULTICONT, multiController);
|
||||
settings.setValue(SER_UNSUPPORTEDFPS, unsupportedFps);
|
||||
settings.setValue(SER_MDNS, enableMdns);
|
||||
settings.setValue(SER_MOUSEACCELERATION, mouseAcceleration);
|
||||
settings.setValue(SER_QUITAPPAFTER, quitAppAfter);
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cap the frame rate at 120 FPS. Past this, the encoders start
|
||||
// to max out and drop frames.
|
||||
maxFrameRate = qMax(maxFrameRate, qMin(120, 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_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, captureSysKeys);
|
||||
}
|
||||
|
||||
int StreamingPreferences::getDefaultBitrate(int width, int height, int fps)
|
||||
@@ -206,8 +152,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) {
|
||||
|
||||
@@ -15,26 +15,13 @@ public:
|
||||
|
||||
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 +50,14 @@ public:
|
||||
};
|
||||
Q_ENUM(WindowMode)
|
||||
|
||||
enum UIDisplayMode
|
||||
{
|
||||
UI_WINDOWED,
|
||||
UI_MAXIMIZED,
|
||||
UI_FULLSCREEN
|
||||
};
|
||||
Q_ENUM(UIDisplayMode)
|
||||
|
||||
Q_PROPERTY(int width MEMBER width NOTIFY displayModeChanged)
|
||||
Q_PROPERTY(int height MEMBER height NOTIFY displayModeChanged)
|
||||
Q_PROPERTY(int fps MEMBER fps NOTIFY displayModeChanged)
|
||||
@@ -73,11 +68,26 @@ public:
|
||||
Q_PROPERTY(bool multiController MEMBER multiController NOTIFY multiControllerChanged)
|
||||
Q_PROPERTY(bool unsupportedFps MEMBER unsupportedFps NOTIFY unsupportedFpsChanged)
|
||||
Q_PROPERTY(bool enableMdns MEMBER enableMdns NOTIFY enableMdnsChanged)
|
||||
Q_PROPERTY(bool mouseAcceleration MEMBER mouseAcceleration NOTIFY mouseAccelerationChanged)
|
||||
Q_PROPERTY(bool quitAppAfter MEMBER quitAppAfter NOTIFY quitAppAfterChanged)
|
||||
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(bool captureSysKeys MEMBER captureSysKeys NOTIFY captureSysKeysChanged)
|
||||
|
||||
// Directly accessible members for preferences
|
||||
int width;
|
||||
@@ -90,11 +100,27 @@ public:
|
||||
bool multiController;
|
||||
bool unsupportedFps;
|
||||
bool enableMdns;
|
||||
bool mouseAcceleration;
|
||||
bool quitAppAfter;
|
||||
bool absoluteMouseMode;
|
||||
bool absoluteTouchMode;
|
||||
bool framePacing;
|
||||
bool connectionWarnings;
|
||||
bool richPresence;
|
||||
bool gamepadMouse;
|
||||
bool detectNetworkBlocking;
|
||||
bool swapMouseButtons;
|
||||
bool muteOnFocusLoss;
|
||||
bool backgroundGamepad;
|
||||
bool reverseScrollDirection;
|
||||
bool swapFaceButtons;
|
||||
bool captureSysKeys;
|
||||
int packetSize;
|
||||
AudioConfig audioConfig;
|
||||
VideoCodecConfig videoCodecConfig;
|
||||
VideoDecoderSelection videoDecoderSelection;
|
||||
WindowMode windowMode;
|
||||
WindowMode recommendedFullScreenMode;
|
||||
UIDisplayMode uiDisplayMode;
|
||||
|
||||
signals:
|
||||
void displayModeChanged();
|
||||
@@ -105,10 +131,24 @@ signals:
|
||||
void multiControllerChanged();
|
||||
void unsupportedFpsChanged();
|
||||
void enableMdnsChanged();
|
||||
void mouseAccelerationChanged();
|
||||
void quitAppAfterChanged();
|
||||
void absoluteMouseModeChanged();
|
||||
void absoluteTouchModeChanged();
|
||||
void audioConfigChanged();
|
||||
void videoCodecConfigChanged();
|
||||
void videoDecoderSelectionChanged();
|
||||
void uiDisplayModeChanged();
|
||||
void windowModeChanged();
|
||||
void framePacingChanged();
|
||||
void connectionWarningsChanged();
|
||||
void richPresenceChanged();
|
||||
void gamepadMouseChanged();
|
||||
void detectNetworkBlockingChanged();
|
||||
void mouseButtonsChanged();
|
||||
void muteOnFocusLossChanged();
|
||||
void backgroundGamepadChanged();
|
||||
void reverseScrollDirectionChanged();
|
||||
void swapFaceButtonsChanged();
|
||||
void captureSysKeysChanged();
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -3,53 +3,114 @@
|
||||
|
||||
#ifdef HAVE_SOUNDIO
|
||||
#include "renderers/soundioaudiorenderer.h"
|
||||
#else
|
||||
#include "renderers/sdl.h"
|
||||
#endif
|
||||
|
||||
#ifdef HAVE_SLAUDIO
|
||||
#include "renderers/slaud.h"
|
||||
#endif
|
||||
|
||||
#include "renderers/sdl.h"
|
||||
|
||||
#include <Limelight.h>
|
||||
|
||||
IAudioRenderer* Session::createAudioRenderer()
|
||||
{
|
||||
#ifdef HAVE_SOUNDIO
|
||||
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 */,
|
||||
@@ -60,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;
|
||||
}
|
||||
@@ -101,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,
|
||||
@@ -110,29 +178,59 @@ void Session::arDecodeAndPlaySample(char* sampleData, int sampleLength)
|
||||
SDL_GetError());
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// See if we need to drop this sample
|
||||
if (s_ActiveSession->m_DropAudioEndTime != 0) {
|
||||
if (SDL_TICKS_PASSED(SDL_GetTicks(), s_ActiveSession->m_DropAudioEndTime)) {
|
||||
// Avoid calling SDL_GetTicks() now
|
||||
s_ActiveSession->m_DropAudioEndTime = 0;
|
||||
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Audio drop window has ended");
|
||||
}
|
||||
else {
|
||||
// We're still in the drop window
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,10 +239,18 @@ void Session::arDecodeAndPlaySample(char* sampleData, int sampleLength)
|
||||
// safe to reinitialize here because we can't be torn down while
|
||||
// the audio decoder/playback thread is still alive.
|
||||
if (s_ActiveSession->m_AudioRenderer == nullptr && (s_ActiveSession->m_AudioSampleCount % 200) == 0) {
|
||||
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;
|
||||
}
|
||||
// Since we're doing this inline and audio initialization takes time, we need
|
||||
// to drop samples to account for the time we've spent blocking audio rendering
|
||||
// so we return to real-time playback and don't accumulate latency.
|
||||
Uint32 audioReinitStartTime = SDL_GetTicks();
|
||||
|
||||
s_ActiveSession->m_AudioRenderer = s_ActiveSession->createAudioRenderer(&s_ActiveSession->m_AudioConfig);
|
||||
|
||||
Uint32 audioReinitStopTime = SDL_GetTicks();
|
||||
|
||||
s_ActiveSession->m_DropAudioEndTime = audioReinitStopTime + (audioReinitStopTime - audioReinitStartTime);
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Audio reinitialization took %d ms - starting drop window",
|
||||
audioReinitStopTime - audioReinitStartTime);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,13 +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;
|
||||
Uint32 m_ChannelCount;
|
||||
Uint32 m_PendingDrops;
|
||||
Uint32 m_PendingHardDrops;
|
||||
Uint32 m_SampleIndex;
|
||||
Uint32 m_BaselinePendingData;
|
||||
void* m_AudioBuffer;
|
||||
int m_FrameSize;
|
||||
};
|
||||
|
||||