mirror of
https://github.com/moonlight-stream/moonlight-qt.git
synced 2026-10-13 03:53:59 +00:00
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 |
@@ -1,10 +1,10 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us address issues in Moonlight
|
||||
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://discord.gg/MySTSdq
|
||||
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.
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
blank_issues_enabled: false
|
||||
@@ -1,4 +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://discord.gg/MySTSdq) instead of GitHub issues. There are many more people available on Discord to help you and answer your questions.<br /><br />
|
||||
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!
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@
|
||||
url = https://github.com/gabomdq/SDL_GameControllerDB.git
|
||||
[submodule "soundio/libsoundio"]
|
||||
path = soundio/libsoundio
|
||||
url = https://github.com/andrewrk/libsoundio.git
|
||||
url = https://github.com/cgutman/libsoundio.git
|
||||
[submodule "h264bitstream/h264bitstream"]
|
||||
path = h264bitstream/h264bitstream
|
||||
url = https://github.com/aizvorski/h264bitstream.git
|
||||
|
||||
-72
@@ -1,72 +0,0 @@
|
||||
language: cpp
|
||||
|
||||
git:
|
||||
depth: 1
|
||||
|
||||
matrix:
|
||||
include:
|
||||
- os: osx
|
||||
osx_image: xcode10.2
|
||||
compiler: clang
|
||||
env: TRAVIS_TARGET=osx
|
||||
addons:
|
||||
homebrew:
|
||||
packages:
|
||||
- qt
|
||||
- os: linux
|
||||
sudo: required
|
||||
dist: xenial
|
||||
compiler: gcc
|
||||
env: TRAVIS_TARGET=steamlink
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
- libc6:i386
|
||||
- libstdc++6:i386
|
||||
- os: linux
|
||||
sudo: required
|
||||
dist: bionic
|
||||
compiler: gcc
|
||||
env: TRAVIS_TARGET=linux
|
||||
addons:
|
||||
apt:
|
||||
sources:
|
||||
- sourceline: 'ppa:savoury1/multimedia'
|
||||
- sourceline: 'ppa:savoury1/ffmpeg4'
|
||||
packages:
|
||||
- qt5-default
|
||||
- qtbase5-dev
|
||||
- qtquickcontrols2-5-dev
|
||||
- libqt5svg5-dev
|
||||
- libgl1-mesa-dev
|
||||
- libegl1-mesa-dev
|
||||
- libxkbcommon-dev
|
||||
- wayland-protocols
|
||||
- libopus-dev
|
||||
- libva-dev
|
||||
- libvdpau-dev
|
||||
- libsdl2-dev
|
||||
- qtdeclarative5-dev
|
||||
- libssl-dev
|
||||
- libsdl2-ttf-dev
|
||||
- libavcodec-dev
|
||||
- libavutil-dev
|
||||
|
||||
install:
|
||||
- '[ "$TRAVIS_TARGET" != osx ] || brew link --force qt'
|
||||
- if [[ "$TRAVIS_TARGET" = "osx" ]]; then nvm use node && npm install --global create-dmg; fi
|
||||
- if [[ "$TRAVIS_TARGET" = "steamlink" ]]; then git clone --depth=1 https://github.com/ValveSoftware/steamlink-sdk.git $HOME/steamlink-sdk; fi
|
||||
|
||||
before_script:
|
||||
- '[ "$TRAVIS_TARGET" = "steamlink" ] || qmake -v'
|
||||
- '[[ "$TRAVIS_TARGET" != linux || "$CXX" != clang++ ]] || export QMAKESPEC=linux-clang'
|
||||
- '[[ "$TRAVIS_TARGET" != linux || "$CXX" != g++ ]] || export QMAKESPEC=linux-g++'
|
||||
- '[ "$TRAVIS_TARGET" != osx ] || export QMAKESPEC=macx-clang'
|
||||
- '[ "$TRAVIS_TARGET" != "steamlink" ] || export STEAMLINK_SDK_PATH=$HOME/steamlink-sdk'
|
||||
|
||||
script:
|
||||
- '[ "$TRAVIS_TARGET" != "linux" ] || qmake moonlight-qt.pro -spec $QMAKESPEC'
|
||||
- '[ "$TRAVIS_TARGET" != "linux" ] || make -j$(nproc) debug'
|
||||
- '[ "$TRAVIS_TARGET" != "linux" ] || make -j$(nproc) release'
|
||||
- '[ "$TRAVIS_TARGET" != "osx" ] || scripts/generate-dmg.sh Release'
|
||||
- '[ "$TRAVIS_TARGET" != "steamlink" ] || scripts/build-steamlink-app.sh'
|
||||
@@ -131,17 +131,22 @@ 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 dList DLLs for Optimus hybrid graphics DDI.
|
||||
// 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
|
||||
@@ -152,6 +157,33 @@ private:
|
||||
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:
|
||||
//
|
||||
|
||||
@@ -2,51 +2,57 @@
|
||||
|
||||
[Moonlight PC](https://moonlight-stream.org) is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield, but built to run on Windows, Mac, and Linux. This client is the successor to [Moonlight Chrome](https://github.com/moonlight-stream/moonlight-chrome) for streaming on PC.
|
||||
|
||||
Moonlight also has mobile versions for [Android](https://github.com/moonlight-stream/moonlight-android) and [iOS](https://github.com/moonlight-stream/moonlight-ios).
|
||||
Moonlight also has mobile versions for [Android](https://github.com/moonlight-stream/moonlight-android) and [iOS](https://github.com/moonlight-stream/moonlight-ios).
|
||||
|
||||
You can follow development on our [Discord server](https://discord.gg/6ERtzFY).
|
||||
You can follow development on our [Discord server](https://moonlight-stream.org/discord).
|
||||
|
||||
[](https://ci.appveyor.com/project/cgutman/moonlight-qt/branch/master)
|
||||
[](https://travis-ci.org/moonlight-stream/moonlight-qt)
|
||||
[](https://ci.appveyor.com/project/cgutman/moonlight-qt/branch/master)
|
||||
[](https://github.com/moonlight-stream/moonlight-qt/releases)
|
||||
|
||||
## Features
|
||||
- Hardware accelerated video decoding on Windows, Mac, and Linux
|
||||
- Supports streaming at up to 120 FPS (high refresh rate monitor recommended)
|
||||
- Supports streaming at 720p, 1080p, 1440p, 4K, and the client PC's native screen resolution
|
||||
- 5.1 surround sound audio
|
||||
- 7.1 surround sound audio support
|
||||
- Support for both pointer capture (for games) and direct mouse control (for remote desktop)
|
||||
- Support for passing system-wide keyboard shortcuts like Alt+Tab to the host in fullscreen mode
|
||||
- Direct touchscreen mouse control
|
||||
- HEVC support for better image quality at reduced bandwidth
|
||||
- Keyboard and mouse support
|
||||
- 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 Build Requirements
|
||||
### Windows Build Requirements
|
||||
* Qt 5.15 SDK or later
|
||||
* Windows 7 or later
|
||||
* [Visual Studio](https://visualstudio.microsoft.com/downloads/) 2017 (Community edition is fine)
|
||||
* Select MSVC Desktop toolchain during Qt installation
|
||||
* [DirectX SDK](https://www.microsoft.com/en-us/download/details.aspx?id=6812)
|
||||
* [Visual Studio 2019](https://visualstudio.microsoft.com/downloads/) (Community edition is fine)
|
||||
* Select **MSVC 2019** option during Qt installation. MinGW is not supported.
|
||||
* [7-Zip](https://www.7-zip.org/) (only if building installers for non-development PCs)
|
||||
* [WiX Toolset](http://wixtoolset.org/releases/) v3.11 or later (only if building installers for non-development PCs)
|
||||
* [WiX Toolset](https://wixtoolset.org/releases/) v3.11 or later (only if building installers for non-development PCs)
|
||||
|
||||
### Mac-specific Build Requirements
|
||||
* macOS Sierra (10.12) or later
|
||||
* Xcode 10
|
||||
### macOS Build Requirements
|
||||
* Qt 5.15 SDK or later
|
||||
* macOS High Sierra (10.13) or later
|
||||
* Xcode 11
|
||||
* [create-dmg](https://github.com/sindresorhus/create-dmg) (only if building DMGs for use on non-development Macs)
|
||||
|
||||
### Linux-specific Build Requirements
|
||||
### Linux/Unix Build Requirements
|
||||
* Qt 5.9 SDK or later
|
||||
* GCC or Clang
|
||||
* Install your distro equivalents of: `openssl-devel qt5-devel SDL2-devel ffmpeg-devel qt5-qtquickcontrols2-devel libva-devel libvdpau-devel opus-devel pulseaudio-libs-devel alsa-lib-devel SDL2_ttf-devel`
|
||||
* FFmpeg 4.0 is required to build. If your distro doesn't package FFmpeg 4.0, you can build and install it from source on http://ffmpeg.org/
|
||||
* Install the required packages:
|
||||
* Debian/Ubuntu: `libegl1-mesa-dev libgl1-mesa-dev libopus-dev libqt5svg5-dev libsdl2-dev libsdl2-ttf-dev libssl-dev libavcodec-dev libva-dev libvdpau-dev libxkbcommon-dev qt5-default qt5-qmake qtbase5-dev qtdeclarative5-dev qtquickcontrols2-5-dev wayland-protocols qml-module-qtquick-controls2 qml-module-qtquick-layouts qml-module-qtquick-window2 qml-module-qtquick2`
|
||||
* RedHat/Fedora: `openssl-devel qt5-devel SDL2-devel ffmpeg-devel qt5-qtquickcontrols2-devel libva-devel libvdpau-devel opus-devel pulseaudio-libs-devel alsa-lib-devel SDL2_ttf-devel`
|
||||
* FFmpeg 4.0+ is required to build. If your distro doesn't package FFmpeg 4.0 or later, you can build and install it from source on https://ffmpeg.org/
|
||||
|
||||
### Steam Link-specific Build Requirements
|
||||
### 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
|
||||
|
||||
@@ -58,7 +64,7 @@ You can follow development on our [Discord server](https://discord.gg/6ERtzFY).
|
||||
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`
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>10.11</string>
|
||||
<string>10.13</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string>NSApplication</string>
|
||||
<key>NSSupportsAutomaticGraphicsSwitching</key>
|
||||
|
||||
+1
-1
Submodule app/SDL_GameControllerDB updated: aeebc86f9f...f0ae9dea50
+52
-29
@@ -33,20 +33,19 @@ DEFINES += QT_DEPRECATED_WARNINGS
|
||||
DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
|
||||
|
||||
win32 {
|
||||
INCLUDEPATH += \
|
||||
$$PWD/../libs/windows/include \
|
||||
$$(DXSDK_DIR)/Include
|
||||
INCLUDEPATH += $$PWD/../libs/windows/include
|
||||
|
||||
contains(QT_ARCH, i386) {
|
||||
LIBS += -L$$PWD/../libs/windows/lib/x86
|
||||
LIBS += -L$$(DXSDK_DIR)/Lib/x86
|
||||
}
|
||||
contains(QT_ARCH, x86_64) {
|
||||
LIBS += -L$$PWD/../libs/windows/lib/x64
|
||||
LIBS += -L$$(DXSDK_DIR)/Lib/x64
|
||||
}
|
||||
contains(QT_ARCH, arm64) {
|
||||
LIBS += -L$$PWD/../libs/windows/lib/arm64
|
||||
}
|
||||
|
||||
LIBS += ws2_32.lib winmm.lib dxva2.lib ole32.lib gdi32.lib user32.lib d3d9.lib dwmapi.lib dbghelp.lib
|
||||
LIBS += ws2_32.lib winmm.lib dxva2.lib ole32.lib gdi32.lib user32.lib d3d9.lib dwmapi.lib dbghelp.lib qwave.lib
|
||||
}
|
||||
macx {
|
||||
INCLUDEPATH += $$PWD/../libs/mac/include
|
||||
@@ -59,20 +58,6 @@ unix:!macx {
|
||||
CONFIG += link_pkgconfig
|
||||
PKGCONFIG += openssl sdl2 SDL2_ttf opus
|
||||
|
||||
# SLAudio is used on Steam Link
|
||||
!config_SL {
|
||||
CONFIG += soundio
|
||||
}
|
||||
|
||||
soundio {
|
||||
packagesExist(libpulse) {
|
||||
PKGCONFIG += libpulse
|
||||
}
|
||||
packagesExist(alsa) {
|
||||
PKGCONFIG += alsa
|
||||
}
|
||||
}
|
||||
|
||||
packagesExist(libavcodec) {
|
||||
PKGCONFIG += libavcodec libavutil
|
||||
CONFIG += ffmpeg
|
||||
@@ -104,9 +89,19 @@ unix:!macx {
|
||||
CONFIG += libdrm
|
||||
}
|
||||
}
|
||||
|
||||
packagesExist(wayland-client) {
|
||||
DEFINES += HAS_WAYLAND
|
||||
PKGCONFIG += wayland-client
|
||||
}
|
||||
|
||||
packagesExist(x11) {
|
||||
DEFINES += HAS_X11
|
||||
PKGCONFIG += x11
|
||||
}
|
||||
}
|
||||
win32 {
|
||||
LIBS += -llibssl -llibcrypto -lSDL2 -lSDL2_ttf -lavcodec -lavutil -lopus -ld3dx9
|
||||
LIBS += -llibssl -llibcrypto -lSDL2 -lSDL2_ttf -lavcodec -lavutil -lopus
|
||||
CONFIG += ffmpeg
|
||||
}
|
||||
win32:!winrt {
|
||||
@@ -123,6 +118,7 @@ macx {
|
||||
}
|
||||
|
||||
SOURCES += \
|
||||
backend/nvapp.cpp \
|
||||
main.cpp \
|
||||
backend/computerseeker.cpp \
|
||||
backend/identitymanager.cpp \
|
||||
@@ -135,8 +131,14 @@ SOURCES += \
|
||||
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 \
|
||||
@@ -148,9 +150,12 @@ SOURCES += \
|
||||
settings/mappingmanager.cpp \
|
||||
gui/sdlgamepadkeynavigation.cpp \
|
||||
streaming/video/overlaymanager.cpp \
|
||||
backend/systemproperties.cpp
|
||||
backend/systemproperties.cpp \
|
||||
wm.cpp
|
||||
|
||||
HEADERS += \
|
||||
backend/nvapp.h \
|
||||
settings/mappingfetcher.h \
|
||||
utils.h \
|
||||
backend/computerseeker.h \
|
||||
backend/identitymanager.h \
|
||||
@@ -164,7 +169,7 @@ HEADERS += \
|
||||
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 \
|
||||
@@ -246,6 +251,16 @@ libdrm {
|
||||
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)
|
||||
|
||||
@@ -297,6 +312,9 @@ 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 =
|
||||
|
||||
@@ -334,11 +352,13 @@ INCLUDEPATH += $$PWD/../h264bitstream/h264bitstream
|
||||
DEPENDPATH += $$PWD/../h264bitstream/h264bitstream
|
||||
|
||||
!winrt {
|
||||
win32:CONFIG(release, debug|release): LIBS += -L$$OUT_PWD/../AntiHooking/release/ -lAntiHooking
|
||||
else:win32:CONFIG(debug, debug|release): LIBS += -L$$OUT_PWD/../AntiHooking/debug/ -lAntiHooking
|
||||
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: {
|
||||
@@ -387,7 +407,10 @@ macx {
|
||||
APP_BUNDLE_FRAMEWORKS.files = $$files(../libs/mac/Frameworks/*.framework, true) $$files(../libs/mac/lib/*.dylib, true)
|
||||
APP_BUNDLE_FRAMEWORKS.path = Contents/Frameworks
|
||||
|
||||
QMAKE_BUNDLE_DATA += APP_BUNDLE_RESOURCES APP_BUNDLE_FRAMEWORKS
|
||||
APP_BUNDLE_PLIST.files = $$OUT_PWD/Info.plist
|
||||
APP_BUNDLE_PLIST.path = Contents
|
||||
|
||||
QMAKE_BUNDLE_DATA += APP_BUNDLE_RESOURCES APP_BUNDLE_FRAMEWORKS APP_BUNDLE_PLIST
|
||||
|
||||
QMAKE_RPATHDIR += @executable_path/../Frameworks
|
||||
}
|
||||
|
||||
@@ -28,13 +28,24 @@ AutoUpdateChecker::AutoUpdateChecker(QObject *parent) :
|
||||
|
||||
void AutoUpdateChecker::start()
|
||||
{
|
||||
#if defined(Q_OS_WIN32) || defined(Q_OS_DARWIN) || defined(STEAM_LINK) // Only run update checker on platforms without auto-update
|
||||
// HACK: Set network accessibility to work around QTBUG-80947
|
||||
#if defined(Q_OS_WIN32) || defined(Q_OS_DARWIN) || defined(STEAM_LINK) || defined(APP_IMAGE) // Only run update checker on platforms without auto-update
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) && QT_VERSION < QT_VERSION_CHECK(5, 15, 1) && !defined(QT_NO_BEARERMANAGEMENT)
|
||||
// HACK: Set network accessibility to work around QTBUG-80947 (introduced in Qt 5.14.0 and fixed in Qt 5.15.1)
|
||||
QT_WARNING_PUSH
|
||||
QT_WARNING_DISABLE_DEPRECATED
|
||||
m_Nam.setNetworkAccessible(QNetworkAccessManager::Accessible);
|
||||
QT_WARNING_POP
|
||||
#endif
|
||||
|
||||
// We'll get a callback when this is finished
|
||||
QUrl url("https://moonlight-stream.org/updates/qt.json");
|
||||
m_Nam.get(QNetworkRequest(url));
|
||||
QNetworkRequest request(url);
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
|
||||
request.setAttribute(QNetworkRequest::Http2AllowedAttribute, true);
|
||||
#else
|
||||
request.setAttribute(QNetworkRequest::HTTP2AllowedAttribute, true);
|
||||
#endif
|
||||
m_Nam.get(request);
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -48,8 +59,10 @@ void AutoUpdateChecker::parseStringToVersionQuad(QString& string, QVector<int>&
|
||||
|
||||
QString AutoUpdateChecker::getPlatform()
|
||||
{
|
||||
#ifdef STEAM_LINK
|
||||
#if defined(STEAM_LINK)
|
||||
return QStringLiteral("steamlink");
|
||||
#elif defined(APP_IMAGE)
|
||||
return QStringLiteral("appimage");
|
||||
#else
|
||||
return QSysInfo::productType();
|
||||
#endif
|
||||
@@ -61,7 +74,12 @@ void AutoUpdateChecker::handleUpdateCheckRequestFinished(QNetworkReply* reply)
|
||||
|
||||
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();
|
||||
|
||||
@@ -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,6 +87,16 @@ 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()) {
|
||||
@@ -109,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"
|
||||
|
||||
@@ -32,7 +33,7 @@ private:
|
||||
|
||||
QString serverInfo;
|
||||
try {
|
||||
serverInfo = http.getServerInfo(NvHTTP::NvLogLevel::NVLL_NONE);
|
||||
serverInfo = http.getServerInfo(NvHTTP::NvLogLevel::NVLL_NONE, true);
|
||||
} catch (...) {
|
||||
return false;
|
||||
}
|
||||
@@ -67,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;
|
||||
}
|
||||
|
||||
@@ -84,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;
|
||||
@@ -209,9 +205,10 @@ void ComputerManager::saveHosts()
|
||||
|
||||
settings.remove(SER_HOSTS);
|
||||
settings.beginWriteArray(SER_HOSTS);
|
||||
for (int i = 0; i < m_KnownHosts.count(); i++) {
|
||||
settings.setArrayIndex(i);
|
||||
m_KnownHosts.value(m_KnownHosts.keys()[i])->serialize(settings);
|
||||
int i = 0;
|
||||
for (const NvComputer* computer : m_KnownHosts) {
|
||||
settings.setArrayIndex(i++);
|
||||
computer->serialize(settings);
|
||||
}
|
||||
settings.endArray();
|
||||
}
|
||||
@@ -398,6 +395,9 @@ public:
|
||||
// Delete the polling entry first. This will stop all polling threads too.
|
||||
delete pollingEntry;
|
||||
|
||||
// Delete cached box art
|
||||
BoxArtManager::deleteBoxArt(m_Computer);
|
||||
|
||||
// Finally, delete the computer itself. This must be done
|
||||
// last because the polling thread might be using it.
|
||||
delete m_Computer;
|
||||
@@ -415,6 +415,28 @@ 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);
|
||||
@@ -514,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());
|
||||
@@ -585,7 +607,7 @@ public:
|
||||
}
|
||||
|
||||
signals:
|
||||
void computerAddCompleted(QVariant success);
|
||||
void computerAddCompleted(QVariant success, QVariant detectedPortBlocking);
|
||||
|
||||
void computerStateChanged(NvComputer* computer);
|
||||
|
||||
@@ -615,7 +637,20 @@ private:
|
||||
return serverInfo;
|
||||
} catch (...) {
|
||||
if (!m_Mdns) {
|
||||
emit computerAddCompleted(false);
|
||||
StreamingPreferences prefs;
|
||||
int portTestResult;
|
||||
|
||||
if (prefs.detectNetworkBlocking) {
|
||||
// We failed to connect to the specified PC. Let's test to make sure this network
|
||||
// isn't blocking Moonlight, so we can tell the user about it.
|
||||
portTestResult = LiTestClientConnectivity("qt.conntest.moonlight-stream.org", 443,
|
||||
ML_PORT_FLAG_TCP_47984 | ML_PORT_FLAG_TCP_47989);
|
||||
}
|
||||
else {
|
||||
portTestResult = 0;
|
||||
}
|
||||
|
||||
emit computerAddCompleted(false, portTestResult != 0 && portTestResult != ML_TEST_RESULT_INCONCLUSIVE);
|
||||
}
|
||||
return QString();
|
||||
}
|
||||
@@ -693,6 +728,12 @@ private:
|
||||
newComputer->manualAddress = m_Address;
|
||||
}
|
||||
|
||||
QHostAddress hostAddress(m_Address);
|
||||
bool addressIsSiteLocalV4 =
|
||||
hostAddress.isInSubnet(QHostAddress("10.0.0.0"), 8) ||
|
||||
hostAddress.isInSubnet(QHostAddress("172.16.0.0"), 12) ||
|
||||
hostAddress.isInSubnet(QHostAddress("192.168.0.0"), 16);
|
||||
|
||||
{
|
||||
// Check if this PC already exists
|
||||
QWriteLocker lock(&m_ComputerManager->m_Lock);
|
||||
@@ -707,7 +748,7 @@ private:
|
||||
|
||||
// For non-mDNS clients, let them know it succeeded
|
||||
if (!m_Mdns) {
|
||||
emit computerAddCompleted(true);
|
||||
emit computerAddCompleted(true, false);
|
||||
}
|
||||
|
||||
// Tell our client if something changed
|
||||
@@ -726,9 +767,24 @@ private:
|
||||
// Drop the lock before notifying
|
||||
lock.unlock();
|
||||
|
||||
// If this wasn't added via mDNS but it is a RFC 1918 IPv4 address and not a VPN,
|
||||
// go ahead and do the STUN request now to populate an external address.
|
||||
if (!m_Mdns && addressIsSiteLocalV4 && !newComputer->isReachableOverVpn()) {
|
||||
quint32 addr;
|
||||
int err = LiFindExternalAddressIP4("stun.moonlight-stream.org", 3478, &addr);
|
||||
if (err == 0) {
|
||||
lock.relock();
|
||||
newComputer->remoteAddress = QHostAddress(qFromBigEndian(addr)).toString();
|
||||
lock.unlock();
|
||||
}
|
||||
else {
|
||||
qWarning() << "STUN failed to get WAN address:" << err;
|
||||
}
|
||||
}
|
||||
|
||||
// For non-mDNS clients, let them know it succeeded
|
||||
if (!m_Mdns) {
|
||||
emit computerAddCompleted(true);
|
||||
emit computerAddCompleted(true, false);
|
||||
}
|
||||
|
||||
// Tell our client about this new PC
|
||||
|
||||
@@ -179,12 +179,16 @@ 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);
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
+41
-19
@@ -1,4 +1,5 @@
|
||||
#include "nvcomputer.h"
|
||||
#include "nvapp.h"
|
||||
|
||||
#include <QUdpSocket>
|
||||
#include <QHostInfo>
|
||||
@@ -14,15 +15,13 @@
|
||||
#define SER_IPV6ADDR "ipv6address"
|
||||
#define SER_APPLIST "apps"
|
||||
#define SER_SRVCERT "srvcert"
|
||||
|
||||
#define SER_APPNAME "name"
|
||||
#define SER_APPID "id"
|
||||
#define SER_APPHDR "hdr"
|
||||
#define SER_CUSTOMNAME "customname"
|
||||
|
||||
NvComputer::NvComputer(QSettings& settings)
|
||||
{
|
||||
this->name = settings.value(SER_NAME).toString();
|
||||
this->uuid = settings.value(SER_UUID).toString();
|
||||
this->hasCustomName = settings.value(SER_CUSTOMNAME).toBool();
|
||||
this->macAddress = settings.value(SER_MAC).toByteArray();
|
||||
this->localAddress = settings.value(SER_LOCALADDR).toString();
|
||||
this->remoteAddress = settings.value(SER_REMOTEADDR).toString();
|
||||
@@ -32,14 +31,9 @@ NvComputer::NvComputer(QSettings& settings)
|
||||
|
||||
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();
|
||||
@@ -57,11 +51,12 @@ 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);
|
||||
@@ -76,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();
|
||||
}
|
||||
@@ -96,6 +88,7 @@ NvComputer::NvComputer(QString address, QString serverInfo, QSslCertificate serv
|
||||
{
|
||||
this->serverCert = serverCert;
|
||||
|
||||
this->hasCustomName = false;
|
||||
this->name = NvHTTP::getXmlString(serverInfo, "hostname");
|
||||
if (this->name.isEmpty()) {
|
||||
this->name = "UNKNOWN";
|
||||
@@ -164,8 +157,9 @@ bool NvComputer::wake()
|
||||
}
|
||||
|
||||
const quint16 WOL_PORTS[] = {
|
||||
7, 9, // Standard WOL ports
|
||||
9, // Standard WOL port (privileged port)
|
||||
47998, 47999, 48000, 48002, 48010, // Ports opened by GFE
|
||||
47009, // Port opened by Moonlight Internet Hosting Tool for WoL (non-privileged port)
|
||||
};
|
||||
|
||||
// Create the WoL payload
|
||||
@@ -313,7 +307,27 @@ bool NvComputer::isReachableOverVpn()
|
||||
}
|
||||
}
|
||||
|
||||
QVector<QString> NvComputer::uniqueAddresses()
|
||||
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;
|
||||
|
||||
@@ -377,7 +391,10 @@ bool NvComputer::update(NvComputer& that)
|
||||
changed = true; \
|
||||
}
|
||||
|
||||
ASSIGN_IF_CHANGED(name);
|
||||
if (!hasCustomName) {
|
||||
// Only overwrite the name if it's not custom
|
||||
ASSIGN_IF_CHANGED(name);
|
||||
}
|
||||
ASSIGN_IF_CHANGED_AND_NONEMPTY(macAddress);
|
||||
ASSIGN_IF_CHANGED_AND_NONEMPTY(localAddress);
|
||||
ASSIGN_IF_CHANGED_AND_NONEMPTY(remoteAddress);
|
||||
@@ -393,7 +410,12 @@ bool NvComputer::update(NvComputer& that)
|
||||
ASSIGN_IF_CHANGED(maxLumaPixelsHEVC);
|
||||
ASSIGN_IF_CHANGED(gpuModel);
|
||||
ASSIGN_IF_CHANGED_AND_NONNULL(serverCert);
|
||||
ASSIGN_IF_CHANGED_AND_NONEMPTY(appList);
|
||||
ASSIGN_IF_CHANGED_AND_NONEMPTY(displayModes);
|
||||
|
||||
if (!that.appList.isEmpty()) {
|
||||
// updateAppList() handles merging client-side attributes
|
||||
updateAppList(that.appList);
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@ class NvComputer
|
||||
private:
|
||||
void sortAppList();
|
||||
|
||||
bool updateAppList(QVector<NvApp> newAppList);
|
||||
|
||||
bool pendingQuit;
|
||||
|
||||
public:
|
||||
@@ -33,10 +35,10 @@ public:
|
||||
isReachableOverVpn();
|
||||
|
||||
QVector<QString>
|
||||
uniqueAddresses();
|
||||
uniqueAddresses() const;
|
||||
|
||||
void
|
||||
serialize(QSettings& settings);
|
||||
serialize(QSettings& settings) const;
|
||||
|
||||
enum PairState
|
||||
{
|
||||
@@ -71,10 +73,11 @@ public:
|
||||
QString manualAddress;
|
||||
QByteArray macAddress;
|
||||
QString name;
|
||||
bool hasCustomName;
|
||||
QString uuid;
|
||||
QSslCertificate serverCert;
|
||||
QVector<NvApp> appList;
|
||||
|
||||
// Synchronization
|
||||
QReadWriteLock lock;
|
||||
mutable QReadWriteLock lock;
|
||||
};
|
||||
|
||||
+47
-46
@@ -12,6 +12,7 @@
|
||||
#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
|
||||
@@ -92,7 +93,7 @@ NvHTTP::getCurrentGame(QString serverInfo)
|
||||
}
|
||||
|
||||
QString
|
||||
NvHTTP::getServerInfo(NvLogLevel logLevel)
|
||||
NvHTTP::getServerInfo(NvLogLevel logLevel, bool fastFail)
|
||||
{
|
||||
QString serverInfo;
|
||||
|
||||
@@ -106,7 +107,7 @@ NvHTTP::getServerInfo(NvLogLevel logLevel)
|
||||
serverInfo = openConnectionToString(m_BaseUrlHttps,
|
||||
"serverinfo",
|
||||
nullptr,
|
||||
REQUEST_TIMEOUT_MS,
|
||||
fastFail ? FAST_FAIL_TIMEOUT_MS : REQUEST_TIMEOUT_MS,
|
||||
logLevel);
|
||||
// Throws if the request failed
|
||||
verifyResponseStatus(serverInfo);
|
||||
@@ -119,7 +120,7 @@ NvHTTP::getServerInfo(NvLogLevel logLevel)
|
||||
serverInfo = openConnectionToString(m_BaseUrlHttp,
|
||||
"serverinfo",
|
||||
nullptr,
|
||||
REQUEST_TIMEOUT_MS,
|
||||
fastFail ? FAST_FAIL_TIMEOUT_MS : REQUEST_TIMEOUT_MS,
|
||||
logLevel);
|
||||
verifyResponseStatus(serverInfo);
|
||||
}
|
||||
@@ -136,7 +137,7 @@ NvHTTP::getServerInfo(NvLogLevel logLevel)
|
||||
serverInfo = openConnectionToString(m_BaseUrlHttp,
|
||||
"serverinfo",
|
||||
nullptr,
|
||||
REQUEST_TIMEOUT_MS,
|
||||
fastFail ? FAST_FAIL_TIMEOUT_MS : REQUEST_TIMEOUT_MS,
|
||||
logLevel);
|
||||
verifyResponseStatus(serverInfo);
|
||||
}
|
||||
@@ -144,30 +145,6 @@ NvHTTP::getServerInfo(NvLogLevel logLevel)
|
||||
return serverInfo;
|
||||
}
|
||||
|
||||
static QString
|
||||
getSurroundAudioInfoString(int audioConfig)
|
||||
{
|
||||
int channelMask;
|
||||
int channelCount;
|
||||
|
||||
switch (audioConfig)
|
||||
{
|
||||
case AUDIO_CONFIGURATION_STEREO:
|
||||
channelCount = 2;
|
||||
channelMask = 0x3;
|
||||
break;
|
||||
case AUDIO_CONFIGURATION_51_SURROUND:
|
||||
channelCount = 6;
|
||||
channelMask = 0xFC;
|
||||
break;
|
||||
default:
|
||||
Q_ASSERT(false);
|
||||
return 0;
|
||||
}
|
||||
|
||||
return QString::number(channelMask << 16 | channelCount);
|
||||
}
|
||||
|
||||
void
|
||||
NvHTTP::launchApp(int appId,
|
||||
PSTREAM_CONFIGURATION streamConfig,
|
||||
@@ -187,9 +164,10 @@ NvHTTP::launchApp(int appId,
|
||||
"&mode="+QString::number(streamConfig->width)+"x"+
|
||||
QString::number(streamConfig->height)+"x"+
|
||||
// Using an FPS value over 60 causes SOPS to default to 720p60,
|
||||
// so force it to 60 when starting. This won't impact our ability
|
||||
// to get > 60 FPS while actually streaming though.
|
||||
QString::number(streamConfig->fps > 60 ? 60 : streamConfig->fps)+
|
||||
// so force it to 0 to ensure the correct resolution is set. We
|
||||
// used to use 60 here but that locked the frame rate to 60 FPS
|
||||
// on GFE 3.20.3.
|
||||
QString::number(streamConfig->fps > 60 ? 0 : streamConfig->fps)+
|
||||
"&additionalStates=1&sops="+QString::number(sops ? 1 : 0)+
|
||||
"&rikey="+QByteArray(streamConfig->remoteInputAesKey, sizeof(streamConfig->remoteInputAesKey)).toHex()+
|
||||
"&rikeyid="+QString::number(riKeyId)+
|
||||
@@ -197,7 +175,7 @@ NvHTTP::launchApp(int appId,
|
||||
"&hdrMode=1&clientHdrCapVersion=0&clientHdrCapSupportedFlagsInUint32=0&clientHdrCapMetaDataId=NV_STATIC_METADATA_TYPE_1&clientHdrCapDisplayData=0x0x0x0x0x0x0x0x0x0x0" :
|
||||
"")+
|
||||
"&localAudioPlayMode="+QString::number(localAudio ? 1 : 0)+
|
||||
"&surroundAudioInfo="+getSurroundAudioInfoString(streamConfig->audioConfiguration)+
|
||||
"&surroundAudioInfo="+QString::number(SURROUNDAUDIOINFO_FROM_AUDIO_CONFIGURATION(streamConfig->audioConfiguration))+
|
||||
"&remoteControllersBitmap="+QString::number(gamepadMask)+
|
||||
"&gcmap="+QString::number(gamepadMask),
|
||||
LAUNCH_TIMEOUT_MS);
|
||||
@@ -219,7 +197,7 @@ NvHTTP::resumeApp(PSTREAM_CONFIGURATION streamConfig)
|
||||
"resume",
|
||||
"rikey="+QString(QByteArray(streamConfig->remoteInputAesKey, sizeof(streamConfig->remoteInputAesKey)).toHex())+
|
||||
"&rikeyid="+QString::number(riKeyId)+
|
||||
"&surroundAudioInfo="+getSurroundAudioInfoString(streamConfig->audioConfiguration),
|
||||
"&surroundAudioInfo="+QString::number(SURROUNDAUDIOINFO_FROM_AUDIO_CONFIGURATION(streamConfig->audioConfiguration)),
|
||||
RESUME_TIMEOUT_MS);
|
||||
|
||||
// Throws if the request failed
|
||||
@@ -255,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();
|
||||
}
|
||||
}
|
||||
@@ -288,8 +266,8 @@ NvHTTP::getAppList()
|
||||
QVector<NvApp> apps;
|
||||
while (!xmlReader.atEnd()) {
|
||||
while (xmlReader.readNextStartElement()) {
|
||||
QStringRef name = xmlReader.name();
|
||||
if (xmlReader.name() == "App") {
|
||||
auto name = xmlReader.name();
|
||||
if (name == QString("App")) {
|
||||
// We must have a valid app before advancing to the next one
|
||||
if (!apps.isEmpty() && !apps.last().isInitialized()) {
|
||||
qWarning() << "Invalid applist XML";
|
||||
@@ -298,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";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -320,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
|
||||
@@ -335,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);
|
||||
}
|
||||
}
|
||||
@@ -424,7 +414,13 @@ NvHTTP::openConnectionToString(QUrl baseUrl,
|
||||
QString ret;
|
||||
|
||||
QTextStream stream(reply);
|
||||
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
stream.setEncoding(QStringConverter::Utf8);
|
||||
#else
|
||||
stream.setCodec("UTF-8");
|
||||
#endif
|
||||
|
||||
ret = stream.readAll();
|
||||
delete reply;
|
||||
|
||||
@@ -454,8 +450,13 @@ NvHTTP::openConnection(QUrl baseUrl,
|
||||
// Add our client certificate
|
||||
request.setSslConfiguration(IdentityManager::get()->getSslConfig());
|
||||
|
||||
// HACK: Set reachability to work around QTBUG-80947
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) && QT_VERSION < QT_VERSION_CHECK(5, 15, 1) && !defined(QT_NO_BEARERMANAGEMENT)
|
||||
// HACK: Set network accessibility to work around QTBUG-80947 (introduced in Qt 5.14.0 and fixed in Qt 5.15.1)
|
||||
QT_WARNING_PUSH
|
||||
QT_WARNING_DISABLE_DEPRECATED
|
||||
m_Nam.setNetworkAccessible(QNetworkAccessManager::Accessible);
|
||||
QT_WARNING_POP
|
||||
#endif
|
||||
|
||||
QNetworkReply* reply = m_Nam.get(request);
|
||||
|
||||
|
||||
+2
-21
@@ -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:
|
||||
@@ -131,7 +112,7 @@ public:
|
||||
getCurrentGame(QString serverInfo);
|
||||
|
||||
QString
|
||||
getServerInfo(NvLogLevel logLevel);
|
||||
getServerInfo(NvLogLevel logLevel, bool fastFail = false);
|
||||
|
||||
static
|
||||
void
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
#include "nvpairingmanager.h"
|
||||
#include "utils.h"
|
||||
|
||||
#include <stdexcept>
|
||||
|
||||
#include <openssl/bio.h>
|
||||
#include <openssl/aes.h>
|
||||
#include <openssl/rand.h>
|
||||
#include <openssl/pem.h>
|
||||
#include <openssl/x509.h>
|
||||
@@ -51,39 +52,63 @@ NvPairingManager::generateRandomBytes(int length)
|
||||
}
|
||||
|
||||
QByteArray
|
||||
NvPairingManager::encrypt(QByteArray plaintext, AES_KEY* key)
|
||||
NvPairingManager::encrypt(const QByteArray& plaintext, const QByteArray& key)
|
||||
{
|
||||
QByteArray ciphertext(plaintext.size(), 0);
|
||||
EVP_CIPHER_CTX* cipher;
|
||||
int ciphertextLen;
|
||||
|
||||
for (int i = 0; i < plaintext.size(); i += 16)
|
||||
{
|
||||
AES_encrypt(reinterpret_cast<unsigned char*>(&plaintext.data()[i]),
|
||||
reinterpret_cast<unsigned char*>(&ciphertext.data()[i]),
|
||||
key);
|
||||
}
|
||||
cipher = EVP_CIPHER_CTX_new();
|
||||
THROW_BAD_ALLOC_IF_NULL(cipher);
|
||||
|
||||
EVP_EncryptInit(cipher, EVP_aes_128_ecb(), reinterpret_cast<const unsigned char*>(key.data()), NULL);
|
||||
EVP_CIPHER_CTX_set_padding(cipher, 0);
|
||||
|
||||
EVP_EncryptUpdate(cipher,
|
||||
reinterpret_cast<unsigned char*>(ciphertext.data()),
|
||||
&ciphertextLen,
|
||||
reinterpret_cast<const unsigned char*>(plaintext.data()),
|
||||
plaintext.length());
|
||||
Q_ASSERT(ciphertextLen == ciphertext.length());
|
||||
|
||||
EVP_CIPHER_CTX_free(cipher);
|
||||
|
||||
return ciphertext;
|
||||
}
|
||||
|
||||
QByteArray
|
||||
NvPairingManager::decrypt(QByteArray ciphertext, AES_KEY* key)
|
||||
NvPairingManager::decrypt(const QByteArray& ciphertext, const QByteArray& key)
|
||||
{
|
||||
QByteArray plaintext(ciphertext.size(), 0);
|
||||
EVP_CIPHER_CTX* cipher;
|
||||
int plaintextLen;
|
||||
|
||||
for (int i = 0; i < plaintext.size(); i += 16)
|
||||
{
|
||||
AES_decrypt(reinterpret_cast<unsigned char*>(&ciphertext.data()[i]),
|
||||
reinterpret_cast<unsigned char*>(&plaintext.data()[i]),
|
||||
key);
|
||||
}
|
||||
cipher = EVP_CIPHER_CTX_new();
|
||||
THROW_BAD_ALLOC_IF_NULL(cipher);
|
||||
|
||||
EVP_DecryptInit(cipher, EVP_aes_128_ecb(), reinterpret_cast<const unsigned char*>(key.data()), NULL);
|
||||
EVP_CIPHER_CTX_set_padding(cipher, 0);
|
||||
|
||||
EVP_DecryptUpdate(cipher,
|
||||
reinterpret_cast<unsigned char*>(plaintext.data()),
|
||||
&plaintextLen,
|
||||
reinterpret_cast<const unsigned char*>(ciphertext.data()),
|
||||
ciphertext.length());
|
||||
Q_ASSERT(plaintextLen == plaintext.length());
|
||||
|
||||
EVP_CIPHER_CTX_free(cipher);
|
||||
|
||||
return plaintext;
|
||||
}
|
||||
|
||||
QByteArray
|
||||
NvPairingManager::getSignatureFromPemCert(QByteArray certificate)
|
||||
NvPairingManager::getSignatureFromPemCert(const QByteArray& certificate)
|
||||
{
|
||||
#if (OPENSSL_VERSION_NUMBER < 0x10100000L)
|
||||
BIO* bio = BIO_new_mem_buf(const_cast<char*>(certificate.data()), -1);
|
||||
#else
|
||||
BIO* bio = BIO_new_mem_buf(certificate.data(), -1);
|
||||
#endif
|
||||
THROW_BAD_ALLOC_IF_NULL(bio);
|
||||
|
||||
X509* cert = PEM_read_bio_X509(bio, nullptr, nullptr, nullptr);
|
||||
@@ -107,9 +132,13 @@ NvPairingManager::getSignatureFromPemCert(QByteArray certificate)
|
||||
}
|
||||
|
||||
bool
|
||||
NvPairingManager::verifySignature(QByteArray data, QByteArray signature, QByteArray serverCertificate)
|
||||
NvPairingManager::verifySignature(const QByteArray& data, const QByteArray& signature, const QByteArray& serverCertificate)
|
||||
{
|
||||
#if (OPENSSL_VERSION_NUMBER < 0x10100000L)
|
||||
BIO* bio = BIO_new_mem_buf(const_cast<char*>(serverCertificate.data()), -1);
|
||||
#else
|
||||
BIO* bio = BIO_new_mem_buf(serverCertificate.data(), -1);
|
||||
#endif
|
||||
THROW_BAD_ALLOC_IF_NULL(bio);
|
||||
|
||||
X509* cert = PEM_read_bio_X509(bio, nullptr, nullptr, nullptr);
|
||||
@@ -123,7 +152,7 @@ NvPairingManager::verifySignature(QByteArray data, QByteArray signature, QByteAr
|
||||
|
||||
EVP_DigestVerifyInit(mdctx, nullptr, EVP_sha256(), nullptr, pubKey);
|
||||
EVP_DigestVerifyUpdate(mdctx, data.data(), data.length());
|
||||
int result = EVP_DigestVerifyFinal(mdctx, reinterpret_cast<unsigned char*>(signature.data()), signature.length());
|
||||
int result = EVP_DigestVerifyFinal(mdctx, reinterpret_cast<unsigned char*>(const_cast<char*>(signature.data())), signature.length());
|
||||
|
||||
EVP_PKEY_free(pubKey);
|
||||
EVP_MD_CTX_destroy(mdctx);
|
||||
@@ -133,7 +162,7 @@ NvPairingManager::verifySignature(QByteArray data, QByteArray signature, QByteAr
|
||||
}
|
||||
|
||||
QByteArray
|
||||
NvPairingManager::signMessage(QByteArray message)
|
||||
NvPairingManager::signMessage(const QByteArray& message)
|
||||
{
|
||||
EVP_MD_CTX *ctx = EVP_MD_CTX_create();
|
||||
THROW_BAD_ALLOC_IF_NULL(ctx);
|
||||
@@ -143,7 +172,7 @@ NvPairingManager::signMessage(QByteArray message)
|
||||
|
||||
EVP_DigestInit_ex(ctx, md, NULL);
|
||||
EVP_DigestSignInit(ctx, NULL, md, NULL, m_PrivateKey);
|
||||
EVP_DigestSignUpdate(ctx, reinterpret_cast<unsigned char*>(message.data()), message.length());
|
||||
EVP_DigestSignUpdate(ctx, reinterpret_cast<unsigned char*>(const_cast<char*>(message.data())), message.length());
|
||||
|
||||
size_t signatureLength = 0;
|
||||
EVP_DigestSignFinal(ctx, NULL, &signatureLength);
|
||||
@@ -157,7 +186,7 @@ NvPairingManager::signMessage(QByteArray message)
|
||||
}
|
||||
|
||||
QByteArray
|
||||
NvPairingManager::saltPin(QByteArray salt, QString pin)
|
||||
NvPairingManager::saltPin(const QByteArray& salt, QString pin)
|
||||
{
|
||||
return QByteArray().append(salt).append(pin.toLatin1());
|
||||
}
|
||||
@@ -186,9 +215,8 @@ NvPairingManager::pair(QString appVersion, QString pin, QSslCertificate& serverC
|
||||
QByteArray salt = generateRandomBytes(16);
|
||||
QByteArray saltedPin = saltPin(salt, pin);
|
||||
|
||||
AES_KEY encKey, decKey;
|
||||
AES_set_decrypt_key(reinterpret_cast<const unsigned char*>(QCryptographicHash::hash(saltedPin, hashAlgo).data()), 128, &decKey);
|
||||
AES_set_encrypt_key(reinterpret_cast<const unsigned char*>(QCryptographicHash::hash(saltedPin, hashAlgo).data()), 128, &encKey);
|
||||
QByteArray aesKey = QCryptographicHash::hash(saltedPin, hashAlgo).data();
|
||||
aesKey.truncate(16);
|
||||
|
||||
QString getCert = m_Http.openConnectionToString(m_Http.m_BaseUrlHttp,
|
||||
"pair",
|
||||
@@ -210,20 +238,21 @@ NvPairingManager::pair(QString appVersion, QString pin, QSslCertificate& serverC
|
||||
return PairState::ALREADY_IN_PROGRESS;
|
||||
}
|
||||
|
||||
serverCert = QSslCertificate(serverCertStr);
|
||||
if (serverCert.isNull()) {
|
||||
Q_ASSERT(!serverCert.isNull());
|
||||
QSslCertificate unverifiedServerCert = QSslCertificate(serverCertStr);
|
||||
if (unverifiedServerCert.isNull()) {
|
||||
Q_ASSERT(!unverifiedServerCert.isNull());
|
||||
|
||||
qCritical() << "Failed to parse plaincert";
|
||||
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, REQUEST_TIMEOUT_MS);
|
||||
return PairState::FAILED;
|
||||
}
|
||||
|
||||
// Pin this cert for TLS
|
||||
m_Http.setServerCert(serverCert);
|
||||
// Pin this cert for TLS until pairing is complete. If successful, we will propagate
|
||||
// the cert into the NvComputer object and persist it.
|
||||
m_Http.setServerCert(unverifiedServerCert);
|
||||
|
||||
QByteArray randomChallenge = generateRandomBytes(16);
|
||||
QByteArray encryptedChallenge = encrypt(randomChallenge, &encKey);
|
||||
QByteArray encryptedChallenge = encrypt(randomChallenge, aesKey);
|
||||
QString challengeXml = m_Http.openConnectionToString(m_Http.m_BaseUrlHttp,
|
||||
"pair",
|
||||
"devicename=roth&updateState=1&clientchallenge=" +
|
||||
@@ -237,7 +266,7 @@ NvPairingManager::pair(QString appVersion, QString pin, QSslCertificate& serverC
|
||||
return PairState::FAILED;
|
||||
}
|
||||
|
||||
QByteArray challengeResponseData = decrypt(m_Http.getXmlStringFromHex(challengeXml, "challengeresponse"), &decKey);
|
||||
QByteArray challengeResponseData = decrypt(m_Http.getXmlStringFromHex(challengeXml, "challengeresponse"), aesKey);
|
||||
QByteArray clientSecretData = generateRandomBytes(16);
|
||||
QByteArray challengeResponse;
|
||||
QByteArray serverResponse(challengeResponseData.data(), hashLength);
|
||||
@@ -258,7 +287,7 @@ NvPairingManager::pair(QString appVersion, QString pin, QSslCertificate& serverC
|
||||
|
||||
QByteArray paddedHash = QCryptographicHash::hash(challengeResponse, hashAlgo);
|
||||
paddedHash.resize(32);
|
||||
QByteArray encryptedChallengeResponseHash = encrypt(paddedHash, &encKey);
|
||||
QByteArray encryptedChallengeResponseHash = encrypt(paddedHash, aesKey);
|
||||
QString respXml = m_Http.openConnectionToString(m_Http.m_BaseUrlHttp,
|
||||
"pair",
|
||||
"devicename=roth&updateState=1&serverchallengeresp=" +
|
||||
@@ -325,5 +354,6 @@ NvPairingManager::pair(QString appVersion, QString pin, QSslCertificate& serverC
|
||||
return PairState::FAILED;
|
||||
}
|
||||
|
||||
serverCert = std::move(unverifiedServerCert);
|
||||
return PairState::PAIRED;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
#include "identitymanager.h"
|
||||
#include "nvhttp.h"
|
||||
|
||||
#include <openssl/aes.h>
|
||||
#include <openssl/x509.h>
|
||||
#include <openssl/evp.h>
|
||||
|
||||
@@ -30,22 +29,22 @@ private:
|
||||
generateRandomBytes(int length);
|
||||
|
||||
QByteArray
|
||||
saltPin(QByteArray salt, QString pin);
|
||||
saltPin(const QByteArray& salt, QString pin);
|
||||
|
||||
QByteArray
|
||||
encrypt(QByteArray plaintext, AES_KEY* key);
|
||||
encrypt(const QByteArray& plaintext, const QByteArray& key);
|
||||
|
||||
QByteArray
|
||||
decrypt(QByteArray ciphertext, AES_KEY* key);
|
||||
decrypt(const QByteArray& ciphertext, const QByteArray& key);
|
||||
|
||||
QByteArray
|
||||
getSignatureFromPemCert(QByteArray certificate);
|
||||
getSignatureFromPemCert(const QByteArray& certificate);
|
||||
|
||||
bool
|
||||
verifySignature(QByteArray data, QByteArray signature, QByteArray serverCertificate);
|
||||
verifySignature(const QByteArray& data, const QByteArray& signature, const QByteArray& serverCertificate);
|
||||
|
||||
QByteArray
|
||||
signMessage(QByteArray message);
|
||||
signMessage(const QByteArray& message);
|
||||
|
||||
NvHTTP m_Http;
|
||||
X509* m_Cert;
|
||||
|
||||
@@ -24,6 +24,9 @@ RichPresenceManager::RichPresenceManager(StreamingPreferences& prefs, QString ga
|
||||
discordPresence.largeImageKey = "icon";
|
||||
Discord_UpdatePresence(&discordPresence);
|
||||
}
|
||||
#else
|
||||
Q_UNUSED(prefs)
|
||||
Q_UNUSED(gameName)
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -1,23 +1,63 @@
|
||||
#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()
|
||||
{
|
||||
isRunningWayland = qgetenv("XDG_SESSION_TYPE") == "wayland";
|
||||
isRunningXWayland = qgetenv("XDG_SESSION_TYPE") == "wayland" && QGuiApplication::platformName() == "xcb";
|
||||
versionString = QString(VERSION_STR);
|
||||
hasWindowManager = WMUtils::isRunningWindowManager();
|
||||
isRunningWayland = WMUtils::isRunningWayland();
|
||||
isRunningXWayland = isRunningWayland && QGuiApplication::platformName() == "xcb";
|
||||
QString nativeArch = QSysInfo::currentCpuArchitecture();
|
||||
|
||||
#ifdef Q_OS_WIN32
|
||||
isWow64 = QSysInfo::currentCpuArchitecture() != QSysInfo::buildCpuArchitecture();
|
||||
{
|
||||
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
|
||||
hasBrowser = true;
|
||||
// Assume we can probably launch a browser if we're in a GUI environment
|
||||
hasBrowser = hasWindowManager;
|
||||
#else
|
||||
hasBrowser = false;
|
||||
#endif
|
||||
@@ -53,13 +93,8 @@ QRect SystemProperties::getNativeResolution(int displayIndex)
|
||||
|
||||
void SystemProperties::querySdlVideoInfo()
|
||||
{
|
||||
monitorDesktopResolutions.clear();
|
||||
monitorNativeResolutions.clear();
|
||||
hasHardwareAcceleration = false;
|
||||
|
||||
// Never let the maximum drop below 60 FPS
|
||||
maximumStreamingFrameRate = 60;
|
||||
|
||||
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s",
|
||||
@@ -67,6 +102,48 @@ void SystemProperties::querySdlVideoInfo()
|
||||
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;
|
||||
@@ -74,7 +151,14 @@ void SystemProperties::querySdlVideoInfo()
|
||||
|
||||
err = SDL_GetDesktopDisplayMode(displayIndex, &desktopMode);
|
||||
if (err == 0) {
|
||||
monitorDesktopResolutions.insert(displayIndex, QRect(0, 0, desktopMode.w, desktopMode.h));
|
||||
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,
|
||||
@@ -83,7 +167,14 @@ void SystemProperties::querySdlVideoInfo()
|
||||
}
|
||||
|
||||
if (StreamUtils::getRealDesktopMode(displayIndex, &desktopMode)) {
|
||||
monitorNativeResolutions.insert(displayIndex, QRect(0, 0, desktopMode.w, desktopMode.h));
|
||||
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;
|
||||
@@ -102,22 +193,5 @@ void SystemProperties::querySdlVideoInfo()
|
||||
}
|
||||
}
|
||||
|
||||
SDL_Window* 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;
|
||||
}
|
||||
|
||||
hasHardwareAcceleration =
|
||||
Session::isHardwareDecodeAvailable(testWindow,
|
||||
StreamingPreferences::VDS_AUTO,
|
||||
VIDEO_FORMAT_H264,
|
||||
1920, 1080, 60);
|
||||
|
||||
SDL_DestroyWindow(testWindow);
|
||||
|
||||
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||
}
|
||||
|
||||
@@ -11,14 +11,20 @@ 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);
|
||||
|
||||
@@ -29,14 +35,19 @@ 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;
|
||||
};
|
||||
|
||||
|
||||
@@ -172,20 +172,31 @@ GlobalCommandLineParser::ParseResult GlobalCommandLineParser::parse(const QStrin
|
||||
parser.addPositionalArgument("action", "Action to execute", "<action>");
|
||||
parser.parse(args);
|
||||
auto posArgs = parser.positionalArguments();
|
||||
QString action = posArgs.isEmpty() ? QString() : posArgs.first().toLower();
|
||||
|
||||
if (action == "") {
|
||||
if (posArgs.isEmpty()) {
|
||||
// This method will not return and terminates the process if --version
|
||||
// or --help is specified
|
||||
parser.handleHelpAndVersionOptions();
|
||||
parser.handleUnknownOptions();
|
||||
return NormalStartRequested;
|
||||
} else if (action == "quit") {
|
||||
return QuitRequested;
|
||||
} else if (action == "stream") {
|
||||
return StreamRequested;
|
||||
} else {
|
||||
parser.showError(QString("Invalid action: %1").arg(action));
|
||||
}
|
||||
else {
|
||||
// If users supply arguments that accept values prior to the "quit"
|
||||
// or "stream" positional arguments, we will not be able to correctly
|
||||
// parse the value out of the input because this QCommandLineParser
|
||||
// doesn't know about all of the options that "quit" and "stream"
|
||||
// commands can accept. To work around this issue, we just look
|
||||
// for "quit" or "stream" positional arguments anywhere.
|
||||
for (int i = 0; i < posArgs.size(); i++) {
|
||||
QString action = posArgs.at(i).toLower();
|
||||
if (action == "quit") {
|
||||
return QuitRequested;
|
||||
} else if (action == "stream") {
|
||||
return StreamRequested;
|
||||
}
|
||||
}
|
||||
|
||||
parser.showError(QString("Invalid action"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,6 +252,7 @@ StreamCommandLineParser::StreamCommandLineParser()
|
||||
m_AudioConfigMap = {
|
||||
{"stereo", StreamingPreferences::AC_STEREO},
|
||||
{"5.1-surround", StreamingPreferences::AC_51_SURROUND},
|
||||
{"7.1-surround", StreamingPreferences::AC_71_SURROUND},
|
||||
};
|
||||
m_VideoCodecMap = {
|
||||
{"auto", StreamingPreferences::VCC_AUTO},
|
||||
@@ -249,8 +261,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},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -280,14 +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("quit-after", "quit app after session");
|
||||
parser.addToggleOption("mouse-acceleration", "mouse acceleration");
|
||||
parser.addToggleOption("absolute-mouse", "remote desktop optimized mouse control");
|
||||
parser.addToggleOption("mouse-buttons-swap", "left and right mouse buttons swap");
|
||||
parser.addToggleOption("touchscreen-trackpad", "touchscreen in trackpad mode");
|
||||
parser.addToggleOption("game-optimization", "game optimizations");
|
||||
parser.addToggleOption("audio-on-host", "audio on host PC");
|
||||
parser.addToggleOption("frame-pacing", "frame pacing");
|
||||
parser.addToggleOption("mute-on-focus-loss", "mute audio when Moonlight window loses focus");
|
||||
parser.addToggleOption("background-gamepad", "background gamepad input");
|
||||
parser.addToggleOption("reverse-scroll-direction", "inverted scroll direction");
|
||||
parser.addToggleOption("swap-gamepad-buttons", "swap A/B and X/Y gamepad buttons (Nintendo-style)");
|
||||
parser.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());
|
||||
|
||||
@@ -345,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"));
|
||||
@@ -364,8 +392,14 @@ void StreamCommandLineParser::parse(const QStringList &args, StreamingPreference
|
||||
// Resolve --quit-after and --no-quit-after options
|
||||
preferences->quitAppAfter = parser.getToggleOptionValue("quit-after", preferences->quitAppAfter);
|
||||
|
||||
// Resolve --mouse-acceleration and --no-mouse-acceleration options
|
||||
preferences->mouseAcceleration = parser.getToggleOptionValue("mouse-acceleration", preferences->mouseAcceleration);
|
||||
// Resolve --absolute-mouse and --no-absolute-mouse options
|
||||
preferences->absoluteMouseMode = parser.getToggleOptionValue("absolute-mouse", preferences->absoluteMouseMode);
|
||||
|
||||
// Resolve --mouse-buttons-swap and --no-mouse-buttons-swap options
|
||||
preferences->swapMouseButtons = parser.getToggleOptionValue("mouse-buttons-swap", preferences->swapMouseButtons);
|
||||
|
||||
// Resolve --touchscreen-trackpad and --no-touchscreen-trackpad options
|
||||
preferences->absoluteTouchMode = !parser.getToggleOptionValue("touchscreen-trackpad", !preferences->absoluteTouchMode);
|
||||
|
||||
// Resolve --game-optimization and --no-game-optimization options
|
||||
preferences->gameOptimizations = parser.getToggleOptionValue("game-optimization", preferences->gameOptimizations);
|
||||
@@ -376,6 +410,21 @@ void StreamCommandLineParser::parse(const QStringList &args, StreamingPreference
|
||||
// Resolve --frame-pacing and --no-frame-pacing options
|
||||
preferences->framePacing = parser.getToggleOptionValue("frame-pacing", preferences->framePacing);
|
||||
|
||||
// Resolve --mute-on-focus-loss and --no-mute-on-focus-loss options
|
||||
preferences->muteOnFocusLoss = parser.getToggleOptionValue("mute-on-focus-loss", preferences->muteOnFocusLoss);
|
||||
|
||||
// Resolve --background-gamepad and --no-background-gamepad options
|
||||
preferences->backgroundGamepad = parser.getToggleOptionValue("background-gamepad", preferences->backgroundGamepad);
|
||||
|
||||
// Resolve --reverse-scroll-direction and --no-reverse-scroll-direction options
|
||||
preferences->reverseScrollDirection = parser.getToggleOptionValue("reverse-scroll-direction", preferences->reverseScrollDirection);
|
||||
|
||||
// Resolve --swap-gamepad-buttons and --no-swap-gamepad-buttons options
|
||||
preferences->swapFaceButtons = parser.getToggleOptionValue("swap-gamepad-buttons", preferences->swapFaceButtons);
|
||||
|
||||
// Resolve --capture-system-keys 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"));
|
||||
|
||||
@@ -73,7 +73,7 @@ public:
|
||||
case Event::ComputerSeekTimedout:
|
||||
if (m_State == StateSeekComputer) {
|
||||
m_State = StateFailure;
|
||||
emit q->failed(QString("Failed to connect to %1").arg(m_ComputerName));
|
||||
emit q->failed(QObject::tr("Failed to connect to %1").arg(m_ComputerName));
|
||||
}
|
||||
break;
|
||||
// Occurs when searched computer is found
|
||||
@@ -85,8 +85,8 @@ public:
|
||||
m_ComputerManager->quitRunningApp(event.computer);
|
||||
} else {
|
||||
m_State = StateFailure;
|
||||
QString msg = QString("Computer %1 has not been paired. "
|
||||
"Please open Moonlight to pair before streaming.")
|
||||
QString msg = QObject::tr("Computer %1 has not been paired. "
|
||||
"Please open Moonlight to pair before streaming.")
|
||||
.arg(event.computer->name);
|
||||
emit q->failed(msg);
|
||||
}
|
||||
@@ -99,7 +99,7 @@ public:
|
||||
QCoreApplication::exit(0);
|
||||
} else {
|
||||
m_State = StateFailure;
|
||||
emit q->failed(QString("Quitting app failed, reason: %1").arg(event.errorMessage));
|
||||
emit q->failed(QObject::tr("Quitting app failed, reason: %1").arg(event.errorMessage));
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -86,8 +86,8 @@ public:
|
||||
emit q->searchingApp();
|
||||
} else {
|
||||
m_State = StateFailure;
|
||||
QString msg = QString("Computer %1 has not been paired. "
|
||||
"Please open Moonlight to pair before streaming.")
|
||||
QString msg = QObject::tr("Computer %1 has not been paired. "
|
||||
"Please open Moonlight to pair before streaming.")
|
||||
.arg(event.computer->name);
|
||||
emit q->failed(msg);
|
||||
}
|
||||
@@ -123,18 +123,18 @@ public:
|
||||
case Event::AppQuitCompleted:
|
||||
if (m_State == StateSeekApp && !event.errorMessage.isEmpty()) {
|
||||
m_State = StateFailure;
|
||||
emit q->failed(QString("Quitting app failed, reason: %1").arg(event.errorMessage));
|
||||
emit q->failed(QObject::tr("Quitting app failed, reason: %1").arg(event.errorMessage));
|
||||
}
|
||||
break;
|
||||
// Occurs when computer or app search timed out
|
||||
case Event::Timedout:
|
||||
if (m_State == StateSeekComputer) {
|
||||
m_State = StateFailure;
|
||||
emit q->failed(QString("Failed to connect to %1").arg(m_ComputerName));
|
||||
emit q->failed(QObject::tr("Failed to connect to %1").arg(m_ComputerName));
|
||||
}
|
||||
if (m_State == StateSeekApp) {
|
||||
m_State = StateFailure;
|
||||
emit q->failed(QString("Failed to find application %1").arg(m_AppName));
|
||||
emit q->failed(QObject::tr("Failed to find application %1").arg(m_AppName));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -7,13 +7,16 @@
|
||||
<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, VDPAU, and NVDEC support</li>
|
||||
<li>5.1 surround sound audio 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 force feedback for up to 4 players</li>
|
||||
</ul>
|
||||
@@ -33,6 +36,103 @@
|
||||
</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>
|
||||
|
||||
@@ -5,9 +5,19 @@
|
||||
# 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
|
||||
exec nice -n -10 ./bin/moonlight > /tmp/moonlight.log
|
||||
echo Launching $BIN
|
||||
exec nice -n -10 ./bin/$BIN > /tmp/moonlight.log
|
||||
|
||||
+91
-37
@@ -9,6 +9,8 @@ CenteredGridView {
|
||||
property int computerIndex
|
||||
property AppModel appModel : createModel()
|
||||
property bool activated
|
||||
property bool showHiddenGames
|
||||
property bool showGames
|
||||
|
||||
id: appGrid
|
||||
focus: true
|
||||
@@ -38,6 +40,19 @@ CenteredGridView {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StackView.onDeactivating: {
|
||||
@@ -48,7 +63,7 @@ CenteredGridView {
|
||||
function createModel()
|
||||
{
|
||||
var model = Qt.createQmlObject('import AppModel 1.0; AppModel {}', parent, '')
|
||||
model.initialize(ComputerManager, computerIndex)
|
||||
model.initialize(ComputerManager, computerIndex, showHiddenGames)
|
||||
return model
|
||||
}
|
||||
|
||||
@@ -58,6 +73,9 @@ CenteredGridView {
|
||||
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
|
||||
|
||||
@@ -108,10 +126,10 @@ CenteredGridView {
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
launchOrResumeSelectedApp()
|
||||
launchOrResumeSelectedApp(true)
|
||||
}
|
||||
|
||||
ToolTip.text: "Resume Game"
|
||||
ToolTip.text: qsTr("Resume Game")
|
||||
ToolTip.delay: 1000
|
||||
ToolTip.timeout: 3000
|
||||
ToolTip.visible: hovered
|
||||
@@ -139,7 +157,7 @@ CenteredGridView {
|
||||
doQuitGame()
|
||||
}
|
||||
|
||||
ToolTip.text: "Quit Game"
|
||||
ToolTip.text: qsTr("Quit Game")
|
||||
ToolTip.delay: 1000
|
||||
ToolTip.timeout: 3000
|
||||
ToolTip.visible: hovered
|
||||
@@ -163,20 +181,27 @@ CenteredGridView {
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
function launchOrResumeSelectedApp()
|
||||
function launchOrResumeSelectedApp(quitExistingApp)
|
||||
{
|
||||
var runningIndex = appModel.getRunningAppIndex()
|
||||
if (runningIndex >= 0 && runningIndex !== index) {
|
||||
quitAppDialog.appName = appModel.getRunningAppName()
|
||||
quitAppDialog.segueToStream = true
|
||||
quitAppDialog.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)
|
||||
}
|
||||
|
||||
@@ -186,7 +211,26 @@ CenteredGridView {
|
||||
// will handle starting the game and clicks on the box art will
|
||||
// be ignored.
|
||||
if (!model.running) {
|
||||
launchOrResumeSelectedApp()
|
||||
launchOrResumeSelectedApp(true)
|
||||
}
|
||||
}
|
||||
|
||||
onPressAndHold: {
|
||||
// popup() ensures the menu appears under the mouse cursor
|
||||
if (appContextMenu.popup) {
|
||||
appContextMenu.popup()
|
||||
}
|
||||
else {
|
||||
// Qt 5.9 doesn't have popup()
|
||||
appContextMenu.open()
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.RightButton;
|
||||
onClicked: {
|
||||
parent.onPressAndHold()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,11 +246,8 @@ CenteredGridView {
|
||||
}
|
||||
|
||||
Keys.onMenuPressed: {
|
||||
if (model.running) {
|
||||
// This will be keyboard/gamepad driven so use
|
||||
// open() instead of popup()
|
||||
appContextMenu.open()
|
||||
}
|
||||
// This will be keyboard/gamepad driven so use open() instead of popup()
|
||||
appContextMenu.open()
|
||||
}
|
||||
|
||||
function doQuitGame() {
|
||||
@@ -219,15 +260,41 @@ CenteredGridView {
|
||||
id: appContextMenu
|
||||
NavigableMenuItem {
|
||||
parentMenu: appContextMenu
|
||||
text: model.running ? "Resume Game" : "Launch Game"
|
||||
onTriggered: launchOrResumeSelectedApp()
|
||||
text: model.running ? qsTr("Resume Game") : qsTr("Launch Game")
|
||||
onTriggered: launchOrResumeSelectedApp(true)
|
||||
}
|
||||
NavigableMenuItem {
|
||||
parentMenu: appContextMenu
|
||||
text: "Quit Game"
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -237,7 +304,7 @@ CenteredGridView {
|
||||
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."
|
||||
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() {
|
||||
@@ -263,18 +330,5 @@ CenteredGridView {
|
||||
onAccepted: quitApp()
|
||||
}
|
||||
|
||||
ScrollBar.vertical: ScrollBar {
|
||||
// Manually hide the scrollbar to prevent it from being drawn on top
|
||||
// of the StreamSegue during the transition. It can sometimes get stuck
|
||||
// there since we're not consistently pumping the event loop while
|
||||
// starting the stream.
|
||||
visible: activated
|
||||
|
||||
parent: appGrid.parent
|
||||
anchors {
|
||||
top: parent.top
|
||||
right: parent.right
|
||||
bottom: parent.bottom
|
||||
}
|
||||
}
|
||||
ScrollBar.vertical: ScrollBar {}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,10 @@ 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
|
||||
@@ -17,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
|
||||
@@ -31,6 +31,10 @@ ComboBox {
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
@@ -38,4 +38,6 @@ GridView {
|
||||
|
||||
updateMargins()
|
||||
}
|
||||
|
||||
boundsBehavior: Flickable.OvershootBounds
|
||||
}
|
||||
|
||||
@@ -6,11 +6,11 @@ import Session 1.0
|
||||
|
||||
Item {
|
||||
function onSearchingComputer() {
|
||||
stageLabel.text = "Establishing connection to PC..."
|
||||
stageLabel.text = qsTr("Establishing connection to PC...")
|
||||
}
|
||||
|
||||
function onQuittingApp() {
|
||||
stageLabel.text = "Quitting app..."
|
||||
stageLabel.text = qsTr("Quitting app...")
|
||||
}
|
||||
|
||||
function onFailure(message) {
|
||||
|
||||
@@ -2,14 +2,15 @@ import QtQuick 2.0
|
||||
import QtQuick.Controls 2.2
|
||||
|
||||
import ComputerManager 1.0
|
||||
import SdlGamepadKeyNavigation 1.0
|
||||
|
||||
Item {
|
||||
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) {
|
||||
@@ -35,6 +36,11 @@ Item {
|
||||
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)
|
||||
@@ -72,7 +78,7 @@ Item {
|
||||
|
||||
NavigableMessageDialog {
|
||||
id: quitAppDialog
|
||||
text:"Are you sure you want to quit " + appName +"? Any unsaved progress will be lost."
|
||||
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 : ""
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import QtQuick 2.0
|
||||
|
||||
Item {
|
||||
objectName: "Gamepad Mapping"
|
||||
objectName: qsTr("Gamepad Mapping")
|
||||
}
|
||||
|
||||
@@ -2,7 +2,8 @@ import QtQuick 2.0
|
||||
import QtQuick.Controls 2.2
|
||||
|
||||
Dialog {
|
||||
parent: overlay
|
||||
// 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)
|
||||
|
||||
@@ -3,6 +3,13 @@ import QtQuick.Controls 2.2
|
||||
|
||||
Menu {
|
||||
onOpened: {
|
||||
itemAt(0).forceActiveFocus(Qt.TabFocusReason)
|
||||
// Give focus to the first visible and enabled menu item
|
||||
for (var i = 0; i < count; i++) {
|
||||
var item = itemAt(i)
|
||||
if (item.visible && item.enabled) {
|
||||
item.forceActiveFocus(Qt.TabFocusReason)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ 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"
|
||||
@@ -19,7 +21,13 @@ NavigableDialog {
|
||||
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"
|
||||
@@ -28,6 +36,7 @@ NavigableDialog {
|
||||
width: 50
|
||||
height: 50
|
||||
}
|
||||
visible: !showSpinner
|
||||
}
|
||||
|
||||
Label {
|
||||
|
||||
+134
-23
@@ -1,5 +1,6 @@
|
||||
import QtQuick 2.9
|
||||
import QtQuick.Controls 2.2
|
||||
import QtQuick.Layouts 1.3
|
||||
|
||||
import ComputerModel 1.0
|
||||
|
||||
@@ -16,7 +17,7 @@ CenteredGridView {
|
||||
topMargin: 20
|
||||
bottomMargin: 5
|
||||
cellWidth: 310; cellHeight: 330;
|
||||
objectName: "Computers"
|
||||
objectName: qsTr("Computers")
|
||||
|
||||
Component.onCompleted: {
|
||||
// Don't show any highlighted item until interacting with them.
|
||||
@@ -25,6 +26,9 @@ CenteredGridView {
|
||||
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)
|
||||
@@ -57,11 +61,18 @@ CenteredGridView {
|
||||
}
|
||||
}
|
||||
|
||||
function addComplete(success)
|
||||
function addComplete(success, detectedPortBlocking)
|
||||
{
|
||||
if (!success) {
|
||||
errorDialog.text = "Unable to connect to the specified PC."
|
||||
errorDialog.helpText = "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()
|
||||
}
|
||||
}
|
||||
@@ -71,6 +82,7 @@ CenteredGridView {
|
||||
var model = Qt.createQmlObject('import ComputerModel 1.0; ComputerModel {}', parent, '')
|
||||
model.initialize(ComputerManager)
|
||||
model.pairingCompleted.connect(pairingComplete)
|
||||
model.connectionTestCompleted.connect(testConnectionDialog.connectionTestComplete)
|
||||
return model
|
||||
}
|
||||
|
||||
@@ -87,8 +99,8 @@ CenteredGridView {
|
||||
Label {
|
||||
height: searchSpinner.height
|
||||
elide: Label.ElideRight
|
||||
text: StreamingPreferences.enableMdns ? "Searching for PCs with NVIDIA GameStream enabled..."
|
||||
: "Automatic PC discovery is disabled. Add your PC manually."
|
||||
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
|
||||
@@ -150,21 +162,54 @@ CenteredGridView {
|
||||
|
||||
NavigableMenu {
|
||||
id: pcContextMenu
|
||||
MenuItem {
|
||||
text: qsTr("PC Status: %1").arg(model.online ? qsTr("Online") : qsTr("Offline"))
|
||||
font.bold: true
|
||||
enabled: false
|
||||
}
|
||||
NavigableMenuItem {
|
||||
parentMenu: pcContextMenu
|
||||
text: "Delete PC"
|
||||
text: qsTr("View All Apps")
|
||||
onTriggered: {
|
||||
var component = Qt.createComponent("AppView.qml")
|
||||
var appView = component.createObject(stackView, {"computerIndex": index, "objectName": model.name, "showHiddenGames": true})
|
||||
stackView.push(appView)
|
||||
}
|
||||
visible: model.online && model.paired
|
||||
}
|
||||
NavigableMenuItem {
|
||||
parentMenu: pcContextMenu
|
||||
text: qsTr("Wake PC")
|
||||
onTriggered: computerModel.wakeComputer(index)
|
||||
visible: !model.online && model.wakeable
|
||||
}
|
||||
NavigableMenuItem {
|
||||
parentMenu: pcContextMenu
|
||||
text: qsTr("Test Network")
|
||||
onTriggered: {
|
||||
computerModel.testConnectionForComputer(index)
|
||||
testConnectionDialog.open()
|
||||
}
|
||||
}
|
||||
|
||||
NavigableMenuItem {
|
||||
parentMenu: pcContextMenu
|
||||
text: qsTr("Rename PC")
|
||||
onTriggered: {
|
||||
renamePcDialog.pcIndex = index
|
||||
renamePcDialog.originalName = model.name
|
||||
renamePcDialog.open()
|
||||
}
|
||||
}
|
||||
NavigableMenuItem {
|
||||
parentMenu: pcContextMenu
|
||||
text: qsTr("Delete PC")
|
||||
onTriggered: {
|
||||
deletePcDialog.pcIndex = index
|
||||
// get confirmation first, actual closing is called from the dialog
|
||||
deletePcDialog.open()
|
||||
}
|
||||
}
|
||||
NavigableMenuItem {
|
||||
parentMenu: pcContextMenu
|
||||
text: "Wake PC"
|
||||
onTriggered: computerModel.wakeComputer(index)
|
||||
visible: !model.online && model.wakeable
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
@@ -177,7 +222,7 @@ CenteredGridView {
|
||||
}
|
||||
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)
|
||||
@@ -188,7 +233,7 @@ CenteredGridView {
|
||||
}
|
||||
else {
|
||||
// cannot pair while something is streaming or attempting to pair
|
||||
errorDialog.text = "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.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()
|
||||
}
|
||||
@@ -243,7 +288,7 @@ CenteredGridView {
|
||||
|
||||
// don't allow edits to the rest of the window while open
|
||||
property string pin : "0000"
|
||||
text:"Please enter " + pin + " on your GameStream PC. This dialog will close when pairing is completed."
|
||||
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
|
||||
@@ -254,7 +299,7 @@ CenteredGridView {
|
||||
id: deletePcDialog
|
||||
// don't allow edits to the rest of the window while open
|
||||
property int pcIndex : -1;
|
||||
text:"Are you sure you want to remove this PC?"
|
||||
text:qsTr("Are you sure you want to remove this PC?")
|
||||
standardButtons: Dialog.Yes | Dialog.No
|
||||
|
||||
function deletePc() {
|
||||
@@ -265,12 +310,78 @@ CenteredGridView {
|
||||
onAccepted: deletePc()
|
||||
}
|
||||
|
||||
ScrollBar.vertical: ScrollBar {
|
||||
parent: pcGrid.parent
|
||||
anchors {
|
||||
top: parent.top
|
||||
right: parent.right
|
||||
bottom: parent.bottom
|
||||
NavigableMessageDialog {
|
||||
id: testConnectionDialog
|
||||
closePolicy: Popup.CloseOnEscape
|
||||
standardButtons: Dialog.Ok
|
||||
|
||||
onAboutToShow: {
|
||||
testConnectionDialog.text = qsTr("Moonlight is testing your network connection to determine if NVIDIA GameStream is blocked.") + "\n\n" + qsTr("This may take a few seconds…")
|
||||
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: {
|
||||
if (editText.text) {
|
||||
computerModel.renameComputer(pcIndex, editText.text)
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Label {
|
||||
text: renamePcDialog.label
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
TextField {
|
||||
id: editText
|
||||
placeholderText: renamePcDialog.originalName
|
||||
Layout.fillWidth: true
|
||||
focus: true
|
||||
|
||||
Keys.onReturnPressed: {
|
||||
renamePcDialog.accept()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ScrollBar.vertical: ScrollBar {}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ Item {
|
||||
property Session nextSession : null
|
||||
property string nextAppName : ""
|
||||
|
||||
property string stageText : "Quitting " + appName + "..."
|
||||
property string stageText : qsTr("Quitting %1...").arg(appName)
|
||||
|
||||
function quitAppCompleted(error)
|
||||
{
|
||||
|
||||
+542
-119
File diff suppressed because it is too large
Load Diff
+41
-8
@@ -8,19 +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
|
||||
|
||||
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)
|
||||
{
|
||||
// Display the error dialog after Session::exec() returns
|
||||
streamSegueErrorDialog.text = "Starting " + stage + " failed: Error " + errorCode
|
||||
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()
|
||||
@@ -62,8 +68,12 @@ Item {
|
||||
window.visible = true
|
||||
}
|
||||
|
||||
function sessionFinished()
|
||||
function sessionFinished(portTestResult)
|
||||
{
|
||||
if (portTestResult !== 0 && portTestResult !== -1 && streamSegueErrorDialog.text) {
|
||||
streamSegueErrorDialog.text += "\n\n" + qsTr("This PC's Internet connection is blocking Moonlight. Streaming over the Internet may not work while connected to this network.")
|
||||
}
|
||||
|
||||
// Enable GUI gamepad usage now
|
||||
SdlGamepadKeyNavigation.enable()
|
||||
|
||||
@@ -93,6 +103,11 @@ Item {
|
||||
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: {
|
||||
@@ -117,9 +132,22 @@ Item {
|
||||
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
|
||||
@@ -130,13 +158,17 @@ Item {
|
||||
// in the hintText control itself to synchronize
|
||||
// with Session.exec() which requires no concurrent
|
||||
// gamepad usage.
|
||||
hintText.text = SdlGamepadKeyNavigation.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"))
|
||||
|
||||
// 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)
|
||||
}
|
||||
@@ -150,6 +182,7 @@ Item {
|
||||
|
||||
BusyIndicator {
|
||||
id: stageSpinner
|
||||
running: false
|
||||
}
|
||||
|
||||
Label {
|
||||
|
||||
+173
-32
@@ -7,7 +7,7 @@ AppModel::AppModel(QObject *parent)
|
||||
this, &AppModel::handleBoxArtLoaded);
|
||||
}
|
||||
|
||||
void AppModel::initialize(ComputerManager* computerManager, int computerIndex)
|
||||
void AppModel::initialize(ComputerManager* computerManager, int computerIndex, bool showHiddenGames)
|
||||
{
|
||||
m_ComputerManager = computerManager;
|
||||
connect(m_ComputerManager, &ComputerManager::computerStateChanged,
|
||||
@@ -15,29 +15,23 @@ void AppModel::initialize(ComputerManager* computerManager, int computerIndex)
|
||||
|
||||
Q_ASSERT(computerIndex < m_ComputerManager->getComputers().count());
|
||||
m_Computer = m_ComputerManager->getComputers().at(computerIndex);
|
||||
m_Apps = m_Computer->appList;
|
||||
m_CurrentGameId = m_Computer->currentGameId;
|
||||
m_ShowHiddenGames = showHiddenGames;
|
||||
|
||||
updateAppList(m_Computer->appList);
|
||||
}
|
||||
|
||||
int AppModel::getRunningAppIndex()
|
||||
int AppModel::getRunningAppId()
|
||||
{
|
||||
if (m_CurrentGameId != 0) {
|
||||
for (int i = 0; i < m_Apps.count(); i++) {
|
||||
if (m_Apps[i].id == m_CurrentGameId) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
return m_CurrentGameId;
|
||||
}
|
||||
|
||||
QString AppModel::getRunningAppName()
|
||||
{
|
||||
if (m_CurrentGameId != 0) {
|
||||
for (int i = 0; i < m_Apps.count(); i++) {
|
||||
if (m_Apps[i].id == m_CurrentGameId) {
|
||||
return m_Apps[i].name;
|
||||
for (int i = 0; i < m_AllApps.count(); i++) {
|
||||
if (m_AllApps[i].id == m_CurrentGameId) {
|
||||
return m_AllApps[i].name;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -47,12 +41,23 @@ QString AppModel::getRunningAppName()
|
||||
|
||||
Session* AppModel::createSessionForApp(int appIndex)
|
||||
{
|
||||
Q_ASSERT(appIndex < m_Apps.count());
|
||||
NvApp app = m_Apps.at(appIndex);
|
||||
Q_ASSERT(appIndex < m_VisibleApps.count());
|
||||
NvApp app = m_VisibleApps.at(appIndex);
|
||||
|
||||
return new Session(m_Computer, app);
|
||||
}
|
||||
|
||||
int AppModel::getDirectLaunchAppIndex()
|
||||
{
|
||||
for (int i = 0; i < m_VisibleApps.count(); i++) {
|
||||
if (m_VisibleApps[i].directLaunch) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
int AppModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
// For list models only the root node (an invalid parent) should return the list's size. For all
|
||||
@@ -60,7 +65,7 @@ int AppModel::rowCount(const QModelIndex &parent) const
|
||||
if (parent.isValid())
|
||||
return 0;
|
||||
|
||||
return m_Apps.count();
|
||||
return m_VisibleApps.count();
|
||||
}
|
||||
|
||||
QVariant AppModel::data(const QModelIndex &index, int role) const
|
||||
@@ -68,8 +73,8 @@ QVariant AppModel::data(const QModelIndex &index, int role) const
|
||||
if (!index.isValid())
|
||||
return QVariant();
|
||||
|
||||
Q_ASSERT(index.row() < m_Apps.count());
|
||||
NvApp app = m_Apps.at(index.row());
|
||||
Q_ASSERT(index.row() < m_VisibleApps.count());
|
||||
NvApp app = m_VisibleApps.at(index.row());
|
||||
|
||||
switch (role)
|
||||
{
|
||||
@@ -80,6 +85,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) {
|
||||
|
||||
+20
-4
@@ -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) {}
|
||||
|
||||
@@ -128,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());
|
||||
@@ -158,3 +206,4 @@ void ComputerModel::handleComputerStateChanged(NvComputer* computer)
|
||||
}
|
||||
}
|
||||
|
||||
#include "computermodel.moc"
|
||||
|
||||
@@ -31,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);
|
||||
|
||||
+86
-24
@@ -17,7 +17,19 @@ ApplicationWindow {
|
||||
width: 1280
|
||||
height: 600
|
||||
|
||||
visibility: StreamingPreferences.startWindowed ? "Windowed" : "Maximized"
|
||||
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
|
||||
@@ -175,6 +187,17 @@ ApplicationWindow {
|
||||
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
|
||||
@@ -194,26 +217,61 @@ 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 === "Computers"
|
||||
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: "Add PC manually" + (newPcShortcut.nativeText ? (" ("+newPcShortcut.nativeText+")") : "")
|
||||
ToolTip.text: qsTr("Add PC manually") + (newPcShortcut.nativeText ? (" ("+newPcShortcut.nativeText+")") : "")
|
||||
|
||||
Shortcut {
|
||||
id: newPcShortcut
|
||||
@@ -253,7 +311,7 @@ ApplicationWindow {
|
||||
|
||||
function updateAvailable(version, url)
|
||||
{
|
||||
ToolTip.text = "Update available for Moonlight: Version " + version
|
||||
ToolTip.text = qsTr("Update available for Moonlight: Version %1").arg(version)
|
||||
updateButton.browserUrl = url
|
||||
updateButton.visible = true
|
||||
}
|
||||
@@ -277,7 +335,7 @@ ApplicationWindow {
|
||||
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
|
||||
@@ -300,11 +358,11 @@ ApplicationWindow {
|
||||
ToolTip.delay: 1000
|
||||
ToolTip.timeout: 3000
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.text: "Gamepad Mapper"
|
||||
ToolTip.text: qsTr("Gamepad Mapper")
|
||||
|
||||
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)
|
||||
@@ -316,7 +374,7 @@ ApplicationWindow {
|
||||
|
||||
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)
|
||||
@@ -331,31 +389,31 @@ 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+")") : "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ErrorMessageDialog {
|
||||
id: noHwDecoderDialog
|
||||
text: "No functioning hardware accelerated H.264 video decoder was detected by Moonlight. " +
|
||||
"Your streaming performance may be severely degraded in this configuration."
|
||||
helpText: "Click the Help button for more information on solving this problem."
|
||||
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"
|
||||
}
|
||||
|
||||
ErrorMessageDialog {
|
||||
id: xWaylandDialog
|
||||
text: "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: "Click the Help button for more information."
|
||||
text: qsTr("Hardware acceleration doesn't work on XWayland. Continuing on XWayland may result in poor streaming performance. " +
|
||||
"Try running with QT_QPA_PLATFORM=wayland or switch to X11.")
|
||||
helpText: qsTr("Click the Help button for more information.")
|
||||
helpUrl: "https://github.com/moonlight-stream/moonlight-docs/wiki/Fixing-Hardware-Decoding-Problems"
|
||||
}
|
||||
|
||||
NavigableMessageDialog {
|
||||
id: wow64Dialog
|
||||
standardButtons: Dialog.Ok | Dialog.Cancel
|
||||
text: "This PC is running a 64-bit version of Windows. Please download the x64 version of Moonlight for the best streaming performance."
|
||||
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");
|
||||
}
|
||||
@@ -364,9 +422,9 @@ ApplicationWindow {
|
||||
ErrorMessageDialog {
|
||||
id: unmappedGamepadDialog
|
||||
property string unmappedGamepads : ""
|
||||
text: "Moonlight detected gamepads without a mapping:\n" + unmappedGamepads
|
||||
text: qsTr("Moonlight detected gamepads without a mapping:") + "\n" + unmappedGamepads
|
||||
helpTextSeparator: "\n\n"
|
||||
helpText: "Click the Help button for information on how to map your gamepads."
|
||||
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"
|
||||
}
|
||||
|
||||
@@ -374,7 +432,7 @@ ApplicationWindow {
|
||||
NavigableMessageDialog {
|
||||
id: quitConfirmationDialog
|
||||
standardButtons: Dialog.Yes | Dialog.No
|
||||
text: "Are you sure you want to quit?"
|
||||
text: qsTr("Are you sure you want to quit?")
|
||||
// For keyboard/gamepad navigation
|
||||
onAccepted: Qt.quit()
|
||||
}
|
||||
@@ -404,7 +462,7 @@ ApplicationWindow {
|
||||
|
||||
NavigableDialog {
|
||||
id: addPcDialog
|
||||
property string label: "Enter the IP address of your GameStream PC:"
|
||||
property string label: qsTr("Enter the IP address of your GameStream PC:")
|
||||
|
||||
standardButtons: Dialog.Ok | Dialog.Cancel
|
||||
|
||||
@@ -413,9 +471,13 @@ ApplicationWindow {
|
||||
editText.forceActiveFocus()
|
||||
}
|
||||
|
||||
onClosed: {
|
||||
editText.clear()
|
||||
}
|
||||
|
||||
onAccepted: {
|
||||
if (editText.text) {
|
||||
ComputerManager.addNewHost(editText.text, false)
|
||||
ComputerManager.addNewHost(editText.text.trim(), false)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <QGuiApplication>
|
||||
#include <QWindow>
|
||||
|
||||
#include "settings/streamingpreferences.h"
|
||||
#include "settings/mappingmanager.h"
|
||||
|
||||
#define AXIS_NAVIGATION_REPEAT_DELAY 150
|
||||
@@ -88,6 +89,7 @@ void SdlGamepadKeyNavigation::disable()
|
||||
void SdlGamepadKeyNavigation::onPollingTimerFired()
|
||||
{
|
||||
SDL_Event event;
|
||||
StreamingPreferences prefs;
|
||||
|
||||
while (SDL_PollEvent(&event)) {
|
||||
switch (event.type) {
|
||||
@@ -104,6 +106,24 @@ 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_UiNavMode) {
|
||||
@@ -205,6 +225,31 @@ void SdlGamepadKeyNavigation::onPollingTimerFired()
|
||||
sendKey(QEvent::Type::KeyRelease, Qt::Key_Right);
|
||||
m_LastAxisNavigationEventTime = SDL_GetTicks();
|
||||
}
|
||||
|
||||
// In UI navigation mode (settings page), use the right stick to scroll
|
||||
if (m_UiNavMode) {
|
||||
short rightX = SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_RIGHTX);
|
||||
short rightY = SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_RIGHTY);
|
||||
|
||||
QPoint wheelDelta;
|
||||
if (rightX > 30000) {
|
||||
wheelDelta.setX(30);
|
||||
}
|
||||
else if (rightX < -30000) {
|
||||
wheelDelta.setX(-30);
|
||||
}
|
||||
|
||||
if (rightY > 30000) {
|
||||
wheelDelta.setY(-30);
|
||||
}
|
||||
else if (rightY < -30000) {
|
||||
wheelDelta.setY(30);
|
||||
}
|
||||
|
||||
if (!wheelDelta.isNull()) {
|
||||
sendWheel(wheelDelta);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -218,6 +263,22 @@ void SdlGamepadKeyNavigation::sendKey(QEvent::Type type, Qt::Key key, Qt::Keyboa
|
||||
}
|
||||
}
|
||||
|
||||
void SdlGamepadKeyNavigation::sendWheel(QPoint& angleDelta)
|
||||
{
|
||||
QGuiApplication* app = static_cast<QGuiApplication*>(QGuiApplication::instance());
|
||||
QWindow* focusWindow = app->focusWindow();
|
||||
if (focusWindow != nullptr) {
|
||||
QPoint mousePos(focusWindow->width() / 2, focusWindow->height() / 2);
|
||||
QPoint globalPos(focusWindow->mapToGlobal(mousePos));
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 12, 0)
|
||||
QWheelEvent wheelEvent(mousePos, globalPos, QPoint(), angleDelta, Qt::NoButton, Qt::NoModifier, Qt::NoScrollPhase, false, Qt::MouseEventSynthesizedByApplication);
|
||||
#else
|
||||
QWheelEvent wheelEvent(mousePos, globalPos, QPoint(), angleDelta, angleDelta.y(), Qt::Vertical, Qt::NoButton, Qt::NoModifier, Qt::NoScrollPhase, Qt::MouseEventSynthesizedByApplication, false);
|
||||
#endif
|
||||
app->sendEvent(focusWindow, &wheelEvent);
|
||||
}
|
||||
}
|
||||
|
||||
void SdlGamepadKeyNavigation::setUiNavMode(bool uiNavMode)
|
||||
{
|
||||
m_UiNavMode = uiNavMode;
|
||||
|
||||
@@ -24,6 +24,7 @@ public:
|
||||
|
||||
private:
|
||||
void sendKey(QEvent::Type type, Qt::Key key, Qt::KeyboardModifiers modifiers = Qt::NoModifier);
|
||||
void sendWheel(QPoint& angleDelta);
|
||||
|
||||
private slots:
|
||||
void onPollingTimerFired();
|
||||
|
||||
Binary file not shown.
@@ -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>
|
||||
+113
-13
@@ -10,6 +10,8 @@
|
||||
#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
|
||||
@@ -21,14 +23,17 @@
|
||||
#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"
|
||||
@@ -72,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;
|
||||
}
|
||||
@@ -187,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;
|
||||
@@ -279,25 +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
|
||||
|
||||
// Enable High DPI support
|
||||
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);
|
||||
// Enable fractional High DPI scaling on Qt 5.14 and later
|
||||
QGuiApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::PassThrough);
|
||||
#endif
|
||||
}
|
||||
else {
|
||||
#ifndef STEAM_LINK
|
||||
if (!qEnvironmentVariableIsSet("QT_QPA_PLATFORM")) {
|
||||
qInfo() << "Unable to detect Wayland or X11, so EGLFS will be used by default. Set QT_QPA_PLATFORM to override this.";
|
||||
qputenv("QT_QPA_PLATFORM", "eglfs");
|
||||
|
||||
if (!qEnvironmentVariableIsSet("QT_QPA_EGLFS_ALWAYS_SET_MODE")) {
|
||||
qInfo() << "Setting display mode by default. Set QT_QPA_EGLFS_ALWAYS_SET_MODE=0 to override this.";
|
||||
|
||||
// The UI doesn't appear on RetroPie without this option.
|
||||
qputenv("QT_QPA_EGLFS_ALWAYS_SET_MODE", "1");
|
||||
}
|
||||
|
||||
if (!QFile("/dev/dri").exists()) {
|
||||
qWarning() << "Unable to find a KMSDRM display device!";
|
||||
qWarning() << "On the Raspberry Pi, you must enable the 'fake KMS' driver in raspi-config to use Moonlight outside of the GUI environment.";
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// This avoids using the default keychain for SSL, which may cause
|
||||
// password prompts on macOS.
|
||||
qputenv("QT_SSL_USE_TEMPORARY_KEYCHAIN", "1");
|
||||
|
||||
#ifdef Q_OS_WIN32
|
||||
#if defined(Q_OS_WIN32) && QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
|
||||
if (!qEnvironmentVariableIsSet("QT_OPENGL")) {
|
||||
// On Windows, use ANGLE so we don't have to load OpenGL
|
||||
// user-mode drivers into our app. OGL drivers (especially Intel)
|
||||
@@ -306,6 +351,16 @@ 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);
|
||||
|
||||
@@ -333,6 +388,17 @@ int main(int argc, char *argv[])
|
||||
// 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",
|
||||
@@ -363,6 +429,19 @@ int main(int argc, char *argv[])
|
||||
// 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
|
||||
@@ -372,14 +451,18 @@ int main(int argc, char *argv[])
|
||||
|
||||
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 (qgetenv("XDG_SESSION_TYPE") == "wayland" && QGuiApplication::platformName() == "xcb") {
|
||||
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. Performance may be worse than X11.");
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Detected Wayland");
|
||||
qputenv("SDL_VIDEODRIVER", "wayland");
|
||||
}
|
||||
|
||||
@@ -397,9 +480,24 @@ int main(int argc, char *argv[])
|
||||
// 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");
|
||||
@@ -431,6 +529,9 @@ int main(int argc, char *argv[])
|
||||
return new StreamingPreferences();
|
||||
});
|
||||
|
||||
// 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")) {
|
||||
@@ -492,7 +593,6 @@ int main(int argc, char *argv[])
|
||||
engine.load(QUrl(QStringLiteral("qrc:/gui/main.qml")));
|
||||
if (engine.rootObjects().isEmpty())
|
||||
return -1;
|
||||
|
||||
int err = app.exec();
|
||||
|
||||
// Give worker tasks time to properly exit. Fixes PendingQuitTask
|
||||
|
||||
Binary file not shown.
+40
-2
@@ -6,6 +6,7 @@
|
||||
#include <QSettings>
|
||||
#include <QCoreApplication>
|
||||
|
||||
QString Path::s_CacheDir;
|
||||
QString Path::s_LogDir;
|
||||
QString Path::s_BoxArtCacheDir;
|
||||
|
||||
@@ -28,11 +29,43 @@ QByteArray Path::readDataFile(QString fileName)
|
||||
return dataFile.readAll();
|
||||
}
|
||||
|
||||
void Path::writeCacheFile(QString fileName, QByteArray data)
|
||||
{
|
||||
QDir cacheDir(s_CacheDir);
|
||||
|
||||
// Create the cache path if it does not exist
|
||||
if (!cacheDir.exists()) {
|
||||
cacheDir.mkpath(".");
|
||||
}
|
||||
|
||||
QFile dataFile(cacheDir.absoluteFilePath(fileName));
|
||||
dataFile.open(QIODevice::WriteOnly);
|
||||
dataFile.write(data);
|
||||
}
|
||||
|
||||
void Path::deleteCacheFile(QString fileName)
|
||||
{
|
||||
QFile dataFile(QDir(s_CacheDir).absoluteFilePath(fileName));
|
||||
dataFile.remove();
|
||||
}
|
||||
|
||||
QFileInfo Path::getCacheFileInfo(QString fileName)
|
||||
{
|
||||
return QFileInfo(QDir(s_CacheDir), fileName);
|
||||
}
|
||||
|
||||
QString Path::getDataFilePath(QString fileName)
|
||||
{
|
||||
QString candidatePath;
|
||||
|
||||
// Check the current directory first
|
||||
// Check the cache location first (used by Path::writeDataFile())
|
||||
candidatePath = QDir(s_CacheDir).absoluteFilePath(fileName);
|
||||
if (QFile::exists(candidatePath)) {
|
||||
qInfo() << "Found" << fileName << "at" << candidatePath;
|
||||
return candidatePath;
|
||||
}
|
||||
|
||||
// Check the current directory
|
||||
candidatePath = QDir(QDir::currentPath()).absoluteFilePath(fileName);
|
||||
if (QFile::exists(candidatePath)) {
|
||||
qInfo() << "Found" << fileName << "at" << candidatePath;
|
||||
@@ -40,7 +73,7 @@ QString Path::getDataFilePath(QString fileName)
|
||||
}
|
||||
|
||||
// Now check the data directories (for Linux, in particular)
|
||||
candidatePath = QStandardPaths::locate(QStandardPaths::DataLocation, fileName);
|
||||
candidatePath = QStandardPaths::locate(QStandardPaths::AppDataLocation, fileName);
|
||||
if (!candidatePath.isEmpty() && QFile::exists(candidatePath)) {
|
||||
qInfo() << "Found" << fileName << "at" << candidatePath;
|
||||
return candidatePath;
|
||||
@@ -64,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
|
||||
@@ -73,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
|
||||
{
|
||||
@@ -10,6 +11,9 @@ public:
|
||||
static QString getBoxArtCacheDir();
|
||||
|
||||
static QByteArray readDataFile(QString fileName);
|
||||
static void writeCacheFile(QString fileName, QByteArray data);
|
||||
static void deleteCacheFile(QString fileName);
|
||||
static QFileInfo getCacheFileInfo(QString fileName);
|
||||
|
||||
// Only safe to use directly for Qt classes
|
||||
static QString getDataFilePath(QString fileName);
|
||||
@@ -17,6 +21,7 @@ public:
|
||||
static void initialize(bool portable);
|
||||
|
||||
private:
|
||||
static QString s_CacheDir;
|
||||
static QString s_LogDir;
|
||||
static QString s_BoxArtCacheDir;
|
||||
};
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 245 240" fill="white"><path class="st0" d="M104.4 103.9c-5.7 0-10.2 5-10.2 11.1s4.6 11.1 10.2 11.1c5.7 0 10.2-5 10.2-11.1.1-6.1-4.5-11.1-10.2-11.1zM140.9 103.9c-5.7 0-10.2 5-10.2 11.1s4.6 11.1 10.2 11.1c5.7 0 10.2-5 10.2-11.1s-4.5-11.1-10.2-11.1z"/><path class="st0" d="M189.5 20h-134C44.2 20 35 29.2 35 40.6v135.2c0 11.4 9.2 20.6 20.5 20.6h113.4l-5.3-18.5 12.8 11.9 12.1 11.2 21.5 19V40.6c0-11.4-9.2-20.6-20.5-20.6zm-38.6 130.6s-3.6-4.3-6.6-8.1c13.1-3.7 18.1-11.9 18.1-11.9-4.1 2.7-8 4.6-11.5 5.9-5 2.1-9.8 3.5-14.5 4.3-9.6 1.8-18.4 1.3-25.9-.1-5.7-1.1-10.6-2.7-14.7-4.3-2.3-.9-4.8-2-7.3-3.4-.3-.2-.6-.3-.9-.5-.2-.1-.3-.2-.4-.3-1.8-1-2.8-1.7-2.8-1.7s4.8 8 17.5 11.8c-3 3.8-6.7 8.3-6.7 8.3-22.1-.7-30.5-15.2-30.5-15.2 0-32.2 14.4-58.3 14.4-58.3 14.4-10.8 28.1-10.5 28.1-10.5l1 1.2c-18 5.2-26.3 13.1-26.3 13.1s2.2-1.2 5.9-2.9c10.7-4.7 19.2-6 22.7-6.3.6-.1 1.1-.2 1.7-.2 6.1-.8 13-1 20.2-.2 9.5 1.1 19.7 3.9 30.1 9.6 0 0-7.9-7.5-24.9-12.7l1.4-1.6s13.7-.3 28.1 10.5c0 0 14.4 26.1 14.4 58.3 0 0-8.5 14.5-30.6 15.2z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24" fill="#FFFFFF"><path d="M0 0h24v24H0V0zm0 0h24v24H0V0z" fill="none"/><path d="M16.59 7.58L10 14.17l-3.59-3.58L5 12l5 5 8-8zM12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"/></svg>
|
||||
|
After Width: | Height: | Size: 339 B |
@@ -15,9 +15,17 @@
|
||||
<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);
|
||||
@@ -58,16 +72,26 @@ void MappingManager::applyMappings()
|
||||
if (!mappingData.isEmpty()) {
|
||||
int newMappings = SDL_GameControllerAddMappingsFromRW(
|
||||
SDL_RWFromConstMem(mappingData.constData(), mappingData.size()), 1);
|
||||
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_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -19,13 +19,23 @@
|
||||
#define SER_UNSUPPORTEDFPS "unsupportedfps"
|
||||
#define SER_MDNS "mdns"
|
||||
#define SER_QUITAPPAFTER "quitAppAfter"
|
||||
#define SER_MOUSEACCELERATION "mouseacceleration"
|
||||
#define SER_ABSMOUSEMODE "mouseacceleration"
|
||||
#define SER_ABSTOUCHMODE "abstouchmode"
|
||||
#define SER_STARTWINDOWED "startwindowed"
|
||||
#define SER_FRAMEPACING "framepacing"
|
||||
#define SER_CONNWARNINGS "connwarnings"
|
||||
#define SER_UIDISPLAYMODE "uidisplaymode"
|
||||
#define SER_RICHPRESENCE "richpresence"
|
||||
#define SER_GAMEPADMOUSE "gamepadmouse"
|
||||
#define SER_DEFAULTVER "defaultver"
|
||||
#define SER_PACKETSIZE "packetsize"
|
||||
#define SER_DETECTNETBLOCKING "detectnetblocking"
|
||||
#define SER_SWAPMOUSEBUTTONS "swapmousebuttons"
|
||||
#define SER_MUTEONFOCUSLOSS "muteonfocusloss"
|
||||
#define SER_BACKGROUNDGAMEPAD "backgroundgamepad"
|
||||
#define SER_REVERSESCROLL "reversescroll"
|
||||
#define SER_SWAPFACEBUTTONS "swapfacebuttons"
|
||||
#define SER_CAPTURESYSKEYS "capturesyskeys"
|
||||
|
||||
#define CURRENT_DEFAULT_VER 1
|
||||
|
||||
@@ -58,12 +68,20 @@ void StreamingPreferences::reload()
|
||||
unsupportedFps = settings.value(SER_UNSUPPORTEDFPS, false).toBool();
|
||||
enableMdns = settings.value(SER_MDNS, true).toBool();
|
||||
quitAppAfter = settings.value(SER_QUITAPPAFTER, false).toBool();
|
||||
mouseAcceleration = settings.value(SER_MOUSEACCELERATION, false).toBool();
|
||||
startWindowed = settings.value(SER_STARTWINDOWED, false).toBool();
|
||||
absoluteMouseMode = settings.value(SER_ABSMOUSEMODE, false).toBool();
|
||||
absoluteTouchMode = settings.value(SER_ABSTOUCHMODE, true).toBool();
|
||||
framePacing = settings.value(SER_FRAMEPACING, false).toBool();
|
||||
connectionWarnings = settings.value(SER_CONNWARNINGS, true).toBool();
|
||||
richPresence = settings.value(SER_RICHPRESENCE, true).toBool();
|
||||
gamepadMouse = settings.value(SER_GAMEPADMOUSE, true).toBool();
|
||||
detectNetworkBlocking = settings.value(SER_DETECTNETBLOCKING, true).toBool();
|
||||
packetSize = settings.value(SER_PACKETSIZE, 0).toInt();
|
||||
swapMouseButtons = settings.value(SER_SWAPMOUSEBUTTONS, false).toBool();
|
||||
muteOnFocusLoss = settings.value(SER_MUTEONFOCUSLOSS, false).toBool();
|
||||
backgroundGamepad = settings.value(SER_BACKGROUNDGAMEPAD, false).toBool();
|
||||
reverseScrollDirection = settings.value(SER_REVERSESCROLL, false).toBool();
|
||||
swapFaceButtons = settings.value(SER_SWAPFACEBUTTONS, false).toBool();
|
||||
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,
|
||||
@@ -74,6 +92,10 @@ void StreamingPreferences::reload()
|
||||
// Try to load from the old preference value too
|
||||
static_cast<int>(settings.value(SER_FULLSCREEN, true).toBool() ?
|
||||
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) {
|
||||
@@ -101,17 +123,26 @@ void StreamingPreferences::save()
|
||||
settings.setValue(SER_UNSUPPORTEDFPS, unsupportedFps);
|
||||
settings.setValue(SER_MDNS, enableMdns);
|
||||
settings.setValue(SER_QUITAPPAFTER, quitAppAfter);
|
||||
settings.setValue(SER_MOUSEACCELERATION, mouseAcceleration);
|
||||
settings.setValue(SER_STARTWINDOWED, startWindowed);
|
||||
settings.setValue(SER_ABSMOUSEMODE, absoluteMouseMode);
|
||||
settings.setValue(SER_ABSTOUCHMODE, absoluteTouchMode);
|
||||
settings.setValue(SER_FRAMEPACING, framePacing);
|
||||
settings.setValue(SER_CONNWARNINGS, connectionWarnings);
|
||||
settings.setValue(SER_RICHPRESENCE, richPresence);
|
||||
settings.setValue(SER_GAMEPADMOUSE, gamepadMouse);
|
||||
settings.setValue(SER_PACKETSIZE, packetSize);
|
||||
settings.setValue(SER_DETECTNETBLOCKING, detectNetworkBlocking);
|
||||
settings.setValue(SER_AUDIOCFG, static_cast<int>(audioConfig));
|
||||
settings.setValue(SER_VIDEOCFG, static_cast<int>(videoCodecConfig));
|
||||
settings.setValue(SER_VIDEODEC, static_cast<int>(videoDecoderSelection));
|
||||
settings.setValue(SER_WINDOWMODE, static_cast<int>(windowMode));
|
||||
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)
|
||||
@@ -121,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) {
|
||||
|
||||
@@ -20,7 +20,8 @@ public:
|
||||
enum AudioConfig
|
||||
{
|
||||
AC_STEREO,
|
||||
AC_51_SURROUND
|
||||
AC_51_SURROUND,
|
||||
AC_71_SURROUND
|
||||
};
|
||||
Q_ENUM(AudioConfig)
|
||||
|
||||
@@ -49,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)
|
||||
@@ -60,17 +69,25 @@ public:
|
||||
Q_PROPERTY(bool unsupportedFps MEMBER unsupportedFps NOTIFY unsupportedFpsChanged)
|
||||
Q_PROPERTY(bool enableMdns MEMBER enableMdns NOTIFY enableMdnsChanged)
|
||||
Q_PROPERTY(bool quitAppAfter MEMBER quitAppAfter NOTIFY quitAppAfterChanged)
|
||||
Q_PROPERTY(bool mouseAcceleration MEMBER mouseAcceleration NOTIFY mouseAccelerationChanged)
|
||||
Q_PROPERTY(bool startWindowed MEMBER startWindowed NOTIFY startWindowedChanged)
|
||||
Q_PROPERTY(bool absoluteMouseMode MEMBER absoluteMouseMode NOTIFY absoluteMouseModeChanged)
|
||||
Q_PROPERTY(bool absoluteTouchMode MEMBER absoluteTouchMode NOTIFY absoluteTouchModeChanged)
|
||||
Q_PROPERTY(bool framePacing MEMBER framePacing NOTIFY framePacingChanged)
|
||||
Q_PROPERTY(bool connectionWarnings MEMBER connectionWarnings NOTIFY connectionWarningsChanged)
|
||||
Q_PROPERTY(bool richPresence MEMBER richPresence NOTIFY richPresenceChanged)
|
||||
Q_PROPERTY(bool gamepadMouse MEMBER gamepadMouse NOTIFY gamepadMouseChanged)
|
||||
Q_PROPERTY(bool detectNetworkBlocking MEMBER detectNetworkBlocking NOTIFY detectNetworkBlockingChanged);
|
||||
Q_PROPERTY(AudioConfig audioConfig MEMBER audioConfig NOTIFY audioConfigChanged)
|
||||
Q_PROPERTY(VideoCodecConfig videoCodecConfig MEMBER videoCodecConfig NOTIFY videoCodecConfigChanged)
|
||||
Q_PROPERTY(VideoDecoderSelection videoDecoderSelection MEMBER videoDecoderSelection NOTIFY videoDecoderSelectionChanged)
|
||||
Q_PROPERTY(WindowMode windowMode MEMBER windowMode NOTIFY windowModeChanged)
|
||||
Q_PROPERTY(WindowMode recommendedFullScreenMode MEMBER recommendedFullScreenMode CONSTANT)
|
||||
Q_PROPERTY(UIDisplayMode uiDisplayMode MEMBER uiDisplayMode NOTIFY uiDisplayModeChanged)
|
||||
Q_PROPERTY(bool swapMouseButtons MEMBER swapMouseButtons NOTIFY mouseButtonsChanged)
|
||||
Q_PROPERTY(bool muteOnFocusLoss MEMBER muteOnFocusLoss NOTIFY muteOnFocusLossChanged)
|
||||
Q_PROPERTY(bool backgroundGamepad MEMBER backgroundGamepad NOTIFY backgroundGamepadChanged)
|
||||
Q_PROPERTY(bool reverseScrollDirection MEMBER reverseScrollDirection NOTIFY reverseScrollDirectionChanged)
|
||||
Q_PROPERTY(bool swapFaceButtons MEMBER swapFaceButtons NOTIFY swapFaceButtonsChanged)
|
||||
Q_PROPERTY(bool captureSysKeys MEMBER captureSysKeys NOTIFY captureSysKeysChanged)
|
||||
|
||||
// Directly accessible members for preferences
|
||||
int width;
|
||||
@@ -84,17 +101,26 @@ public:
|
||||
bool unsupportedFps;
|
||||
bool enableMdns;
|
||||
bool quitAppAfter;
|
||||
bool mouseAcceleration;
|
||||
bool startWindowed;
|
||||
bool absoluteMouseMode;
|
||||
bool absoluteTouchMode;
|
||||
bool framePacing;
|
||||
bool connectionWarnings;
|
||||
bool richPresence;
|
||||
bool gamepadMouse;
|
||||
bool detectNetworkBlocking;
|
||||
bool swapMouseButtons;
|
||||
bool muteOnFocusLoss;
|
||||
bool backgroundGamepad;
|
||||
bool reverseScrollDirection;
|
||||
bool swapFaceButtons;
|
||||
bool captureSysKeys;
|
||||
int packetSize;
|
||||
AudioConfig audioConfig;
|
||||
VideoCodecConfig videoCodecConfig;
|
||||
VideoDecoderSelection videoDecoderSelection;
|
||||
WindowMode windowMode;
|
||||
WindowMode recommendedFullScreenMode;
|
||||
UIDisplayMode uiDisplayMode;
|
||||
|
||||
signals:
|
||||
void displayModeChanged();
|
||||
@@ -106,15 +132,23 @@ signals:
|
||||
void unsupportedFpsChanged();
|
||||
void enableMdnsChanged();
|
||||
void quitAppAfterChanged();
|
||||
void mouseAccelerationChanged();
|
||||
void absoluteMouseModeChanged();
|
||||
void absoluteTouchModeChanged();
|
||||
void audioConfigChanged();
|
||||
void videoCodecConfigChanged();
|
||||
void videoDecoderSelectionChanged();
|
||||
void uiDisplayModeChanged();
|
||||
void windowModeChanged();
|
||||
void startWindowedChanged();
|
||||
void framePacingChanged();
|
||||
void connectionWarningsChanged();
|
||||
void richPresenceChanged();
|
||||
void gamepadMouseChanged();
|
||||
void detectNetworkBlockingChanged();
|
||||
void mouseButtonsChanged();
|
||||
void muteOnFocusLossChanged();
|
||||
void backgroundGamepadChanged();
|
||||
void reverseScrollDirectionChanged();
|
||||
void swapFaceButtonsChanged();
|
||||
void 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);
|
||||
}
|
||||
@@ -80,19 +80,7 @@ int Session::getAudioRendererCapabilities(int audioConfiguration)
|
||||
OPUS_MULTISTREAM_CONFIGURATION opusConfig = {};
|
||||
opusConfig.sampleRate = 48000;
|
||||
opusConfig.samplesPerFrame = 240;
|
||||
|
||||
switch (audioConfiguration)
|
||||
{
|
||||
case AUDIO_CONFIGURATION_STEREO:
|
||||
opusConfig.channelCount = 2;
|
||||
break;
|
||||
case AUDIO_CONFIGURATION_51_SURROUND:
|
||||
opusConfig.channelCount = 6;
|
||||
break;
|
||||
default:
|
||||
SDL_assert(false);
|
||||
return 0;
|
||||
}
|
||||
opusConfig.channelCount = CHANNEL_COUNT_FROM_AUDIO_CONFIGURATION(audioConfiguration);
|
||||
|
||||
IAudioRenderer* audioRenderer = createAudioRenderer(&opusConfig);
|
||||
if (audioRenderer == nullptr) {
|
||||
@@ -113,19 +101,7 @@ bool Session::testAudio(int audioConfiguration)
|
||||
OPUS_MULTISTREAM_CONFIGURATION opusConfig = {};
|
||||
opusConfig.sampleRate = 48000;
|
||||
opusConfig.samplesPerFrame = 240;
|
||||
|
||||
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;
|
||||
}
|
||||
opusConfig.channelCount = CHANNEL_COUNT_FROM_AUDIO_CONFIGURATION(audioConfiguration);
|
||||
|
||||
IAudioRenderer* audioRenderer = createAudioRenderer(&opusConfig);
|
||||
if (audioRenderer == nullptr) {
|
||||
@@ -221,6 +197,11 @@ void Session::arDecodeAndPlaySample(char* sampleData, int sampleLength)
|
||||
|
||||
s_ActiveSession->m_AudioSampleCount++;
|
||||
|
||||
// If audio is muted, don't decode or play the audio
|
||||
if (s_ActiveSession->m_AudioMuted) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (s_ActiveSession->m_AudioRenderer != nullptr) {
|
||||
int desiredSize = sizeof(short) * s_ActiveSession->m_AudioConfig.samplesPerFrame * s_ActiveSession->m_AudioConfig.channelCount;
|
||||
void* buffer = s_ActiveSession->m_AudioRenderer->getAudioBuffer(&desiredSize);
|
||||
@@ -237,7 +218,7 @@ void Session::arDecodeAndPlaySample(char* sampleData, int sampleLength)
|
||||
|
||||
// Update desiredSize with the number of bytes actually populated by the decoding operation
|
||||
if (samplesDecoded > 0) {
|
||||
SDL_assert(desiredSize >= 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 {
|
||||
|
||||
@@ -3,13 +3,12 @@
|
||||
#include <Limelight.h>
|
||||
#include <SDL.h>
|
||||
|
||||
#include <QtGlobal>
|
||||
|
||||
SdlAudioRenderer::SdlAudioRenderer()
|
||||
: m_AudioDevice(0),
|
||||
m_AudioBuffer(nullptr)
|
||||
{
|
||||
SDL_assert(!SDL_WasInit(SDL_INIT_AUDIO));
|
||||
|
||||
if (SDL_InitSubSystem(SDL_INIT_AUDIO) != 0) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"SDL_InitSubSystem(SDL_INIT_AUDIO) failed: %s",
|
||||
@@ -43,7 +42,7 @@ bool SdlAudioRenderer::prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION*
|
||||
return false;
|
||||
}
|
||||
|
||||
m_AudioBuffer = malloc(m_FrameSize);
|
||||
m_AudioBuffer = SDL_malloc(m_FrameSize);
|
||||
if (m_AudioBuffer == nullptr) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Failed to allocate audio buffer");
|
||||
@@ -51,9 +50,9 @@ bool SdlAudioRenderer::prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION*
|
||||
}
|
||||
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Desired audio buffer: %u samples (%lu bytes)",
|
||||
"Desired audio buffer: %u samples (%u bytes)",
|
||||
want.samples,
|
||||
want.samples * sizeof(short) * want.channels);
|
||||
want.samples * (Uint32)sizeof(short) * want.channels);
|
||||
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Obtained audio buffer: %u samples (%u bytes)",
|
||||
@@ -75,7 +74,7 @@ SdlAudioRenderer::~SdlAudioRenderer()
|
||||
}
|
||||
|
||||
if (m_AudioBuffer != nullptr) {
|
||||
free(m_AudioBuffer);
|
||||
SDL_free(m_AudioBuffer);
|
||||
}
|
||||
|
||||
SDL_QuitSubSystem(SDL_INIT_AUDIO);
|
||||
|
||||
@@ -42,26 +42,26 @@ bool SLAudioRenderer::prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* o
|
||||
return true;
|
||||
}
|
||||
|
||||
#define SWAP_CHANNEL(i, j) \
|
||||
tmp = opusConfig->mapping[i]; \
|
||||
opusConfig->mapping[i] = opusConfig->mapping[j]; \
|
||||
opusConfig->mapping[j] = tmp
|
||||
|
||||
void SLAudioRenderer::remapChannels(POPUS_MULTISTREAM_CONFIGURATION opusConfig) {
|
||||
unsigned char tmp;
|
||||
OPUS_MULTISTREAM_CONFIGURATION originalConfig = *opusConfig;
|
||||
|
||||
if (opusConfig->channelCount == 6) {
|
||||
// The Moonlight's default channel order is FL,FR,C,LFE,RL,RR
|
||||
// SLAudio expects FL,C,FR,RL,RR,LFE so we swap the channels around to match
|
||||
// The Moonlight's default channel order is FL,FR,C,LFE,RL,RR,SL,SR
|
||||
// SLAudio expects FL,C,FR,RL,RR,(SL,SR),LFE for 5.1/7.1 so we swap the channels around to match
|
||||
|
||||
// Swap FR and C - now FL,C,FR,LFE,RL,RR
|
||||
SWAP_CHANNEL(1, 2);
|
||||
if (opusConfig->channelCount == 3 || opusConfig->channelCount >= 6) {
|
||||
// Swap FR and C
|
||||
opusConfig->mapping[1] = originalConfig.mapping[2];
|
||||
opusConfig->mapping[2] = originalConfig.mapping[1];
|
||||
}
|
||||
|
||||
// Swap LFE and RR - now FL,C,FR,RR,RL,LFE
|
||||
SWAP_CHANNEL(3, 5);
|
||||
if (opusConfig->channelCount >= 6) {
|
||||
// SLAudio expects the LFE channel at the end
|
||||
opusConfig->mapping[opusConfig->channelCount - 1] = originalConfig.mapping[3];
|
||||
|
||||
// Swap RR and RL - now FL,C,FR,RL,RR,LFE
|
||||
SWAP_CHANNEL(3, 4);
|
||||
// Slide the other channels down
|
||||
memcpy(&opusConfig->mapping[3],
|
||||
&originalConfig.mapping[4],
|
||||
opusConfig->channelCount - 4);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -72,11 +72,17 @@ int SoundIoAudioRenderer::scoreChannelLayout(const struct SoundIoChannelLayout*
|
||||
score++;
|
||||
break;
|
||||
|
||||
// Score layouts using the back L/R as higher
|
||||
// value than those using side L/R.
|
||||
case SoundIoChannelIdBackLeft:
|
||||
case SoundIoChannelIdBackRight:
|
||||
score += 2;
|
||||
if (opusConfig->channelCount == 6) {
|
||||
// Score back channels higher in 5.1 mode to
|
||||
// discourage selection of side channel layouts.
|
||||
score += 2;
|
||||
}
|
||||
else {
|
||||
// Score back channels normally for 7.1 mode
|
||||
score++;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
@@ -211,13 +217,25 @@ bool SoundIoAudioRenderer::prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATI
|
||||
|
||||
m_EffectiveLayout = m_OutputStream->layout;
|
||||
for (int i = 0; i < m_EffectiveLayout.channel_count; i++) {
|
||||
// Fixup the layout to use back L/R so our channel position
|
||||
// logic in sioWriteCallback() works.
|
||||
if (m_EffectiveLayout.channels[i] == SoundIoChannelIdSideLeft) {
|
||||
m_EffectiveLayout.channels[i] = SoundIoChannelIdBackLeft;
|
||||
if (opusConfig->channelCount == 6) {
|
||||
// For 5.1, replace side L/R with back L/R so our channel position
|
||||
// logic in sioWriteCallback() works.
|
||||
if (m_EffectiveLayout.channels[i] == SoundIoChannelIdSideLeft) {
|
||||
m_EffectiveLayout.channels[i] = SoundIoChannelIdBackLeft;
|
||||
}
|
||||
if (m_EffectiveLayout.channels[i] == SoundIoChannelIdSideRight) {
|
||||
m_EffectiveLayout.channels[i] = SoundIoChannelIdBackRight;
|
||||
}
|
||||
}
|
||||
if (m_EffectiveLayout.channels[i] == SoundIoChannelIdSideRight) {
|
||||
m_EffectiveLayout.channels[i] = SoundIoChannelIdBackRight;
|
||||
else if (opusConfig->channelCount == 8) {
|
||||
// For 5.1, replace side L/R with LOC/ROC so our channel position
|
||||
// logic in sioWriteCallback() works.
|
||||
if (m_EffectiveLayout.channels[i] == SoundIoChannelIdSideLeft) {
|
||||
m_EffectiveLayout.channels[i] = SoundIoChannelIdFrontLeftCenter;
|
||||
}
|
||||
if (m_EffectiveLayout.channels[i] == SoundIoChannelIdSideRight) {
|
||||
m_EffectiveLayout.channels[i] = SoundIoChannelIdFrontRightCenter;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -411,9 +429,8 @@ void SoundIoAudioRenderer::sioWriteCallback(SoundIoOutStream* stream, int frameC
|
||||
|
||||
for (int frame = 0; frame < frameCount; frame++) {
|
||||
for (int ch = 0; ch < me->m_EffectiveLayout.channel_count; ch++) {
|
||||
// SoundIoChannelId - 1 happens to match Moonlight's channel layout.
|
||||
// For side L/R, this logic depends on us fixing those up
|
||||
// in m_EffectiveLayout to back L/R.
|
||||
// SoundIoChannelId - 1 happens to match Moonlight's channel layout
|
||||
// after we've applied our fixups to m_EffectiveLayout for 5.1 and 7.1.
|
||||
int readPtrChannel = me->m_EffectiveLayout.channels[ch] - 1;
|
||||
|
||||
if (frame >= framesLeft || readPtrChannel >= me->m_OpusChannelCount) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,107 @@
|
||||
#include "input.h"
|
||||
|
||||
#include <Limelight.h>
|
||||
#include <SDL.h>
|
||||
#include "streaming/streamutils.h"
|
||||
|
||||
#include <QtMath>
|
||||
|
||||
// How long the fingers must be stationary to start a right click
|
||||
#define LONG_PRESS_ACTIVATION_DELAY 650
|
||||
|
||||
// How far the finger can move before it cancels a right click
|
||||
#define LONG_PRESS_ACTIVATION_DELTA 0.01f
|
||||
|
||||
// How long the double tap deadzone stays in effect between touch up and touch down
|
||||
#define DOUBLE_TAP_DEAD_ZONE_DELAY 250
|
||||
|
||||
// How far the finger can move before it can override the double tap deadzone
|
||||
#define DOUBLE_TAP_DEAD_ZONE_DELTA 0.025f
|
||||
|
||||
Uint32 SdlInputHandler::longPressTimerCallback(Uint32, void*)
|
||||
{
|
||||
// Raise the left click and start a right click
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_RIGHT);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void SdlInputHandler::handleAbsoluteFingerEvent(SDL_TouchFingerEvent* event)
|
||||
{
|
||||
// Observations on Windows 10: x and y appear to be relative to 0,0 of the window client area.
|
||||
// Although SDL documentation states they are 0.0 - 1.0 float values, they can actually be higher
|
||||
// or lower than those values as touch events continue for touches started within the client area that
|
||||
// leave the client area during a drag motion.
|
||||
// dx and dy are deltas from the last touch event, not the first touch down.
|
||||
|
||||
// Ignore touch down events with more than one finger
|
||||
if (event->type == SDL_FINGERDOWN && SDL_GetNumTouchFingers(event->touchId) > 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ignore touch move and touch up events from the non-primary finger
|
||||
if (event->type != SDL_FINGERDOWN && event->fingerId != m_LastTouchDownEvent.fingerId) {
|
||||
return;
|
||||
}
|
||||
|
||||
SDL_Rect src, dst;
|
||||
int windowWidth, windowHeight;
|
||||
|
||||
SDL_GetWindowSize(m_Window, &windowWidth, &windowHeight);
|
||||
|
||||
src.x = src.y = 0;
|
||||
src.w = m_StreamWidth;
|
||||
src.h = m_StreamHeight;
|
||||
|
||||
dst.x = dst.y = 0;
|
||||
dst.w = windowWidth;
|
||||
dst.h = windowHeight;
|
||||
|
||||
// Use the stream and window sizes to determine the video region
|
||||
StreamUtils::scaleSourceToDestinationSurface(&src, &dst);
|
||||
|
||||
if (qSqrt(qPow(event->x - m_LastTouchDownEvent.x, 2) + qPow(event->y - m_LastTouchDownEvent.y, 2)) > LONG_PRESS_ACTIVATION_DELTA) {
|
||||
// Moved too far since touch down. Cancel the long press timer.
|
||||
SDL_RemoveTimer(m_LongPressTimer);
|
||||
m_LongPressTimer = 0;
|
||||
}
|
||||
|
||||
// Don't reposition for finger down events within the deadzone. This makes double-clicking easier.
|
||||
if (event->type != SDL_FINGERDOWN ||
|
||||
event->timestamp - m_LastTouchUpEvent.timestamp > DOUBLE_TAP_DEAD_ZONE_DELAY ||
|
||||
qSqrt(qPow(event->x - m_LastTouchUpEvent.x, 2) + qPow(event->y - m_LastTouchUpEvent.y, 2)) > DOUBLE_TAP_DEAD_ZONE_DELTA) {
|
||||
// Scale window-relative events to be video-relative and clamp to video region
|
||||
short x = qMin(qMax((int)(event->x * windowWidth), dst.x), dst.x + dst.w);
|
||||
short y = qMin(qMax((int)(event->y * windowHeight), dst.y), dst.y + dst.h);
|
||||
|
||||
// Update the cursor position relative to the video region
|
||||
LiSendMousePositionEvent(x - dst.x, y - dst.y, dst.w, dst.h);
|
||||
}
|
||||
|
||||
if (event->type == SDL_FINGERDOWN) {
|
||||
m_LastTouchDownEvent = *event;
|
||||
|
||||
// Start/restart the long press timer
|
||||
SDL_RemoveTimer(m_LongPressTimer);
|
||||
m_LongPressTimer = SDL_AddTimer(LONG_PRESS_ACTIVATION_DELAY,
|
||||
longPressTimerCallback,
|
||||
nullptr);
|
||||
|
||||
// Left button down on finger down
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT);
|
||||
}
|
||||
else if (event->type == SDL_FINGERUP) {
|
||||
m_LastTouchUpEvent = *event;
|
||||
|
||||
// Cancel the long press timer
|
||||
SDL_RemoveTimer(m_LongPressTimer);
|
||||
m_LongPressTimer = 0;
|
||||
|
||||
// Left button up on finger up
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
|
||||
|
||||
// Raise right button too in case we triggered a long press gesture
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_RIGHT);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,612 @@
|
||||
#include "streaming/session.h"
|
||||
|
||||
#include <Limelight.h>
|
||||
#include <SDL.h>
|
||||
#include "settings/mappingmanager.h"
|
||||
|
||||
#include <QtMath>
|
||||
|
||||
// How long the Start button must be pressed to toggle mouse emulation
|
||||
#define MOUSE_EMULATION_LONG_PRESS_TIME 750
|
||||
|
||||
// How long between polling the gamepad to send virtual mouse input
|
||||
#define MOUSE_EMULATION_POLLING_INTERVAL 50
|
||||
|
||||
// Determines how fast the mouse will move each interval
|
||||
#define MOUSE_EMULATION_MOTION_MULTIPLIER 4
|
||||
|
||||
// Determines the maximum motion amount before allowing movement
|
||||
#define MOUSE_EMULATION_DEADZONE 2
|
||||
|
||||
// Haptic capabilities (in addition to those from SDL_HapticQuery())
|
||||
#define ML_HAPTIC_GC_RUMBLE (1U << 16)
|
||||
#define ML_HAPTIC_SIMPLE_RUMBLE (1U << 17)
|
||||
|
||||
const int SdlInputHandler::k_ButtonMap[] = {
|
||||
A_FLAG, B_FLAG, X_FLAG, Y_FLAG,
|
||||
BACK_FLAG, SPECIAL_FLAG, PLAY_FLAG,
|
||||
LS_CLK_FLAG, RS_CLK_FLAG,
|
||||
LB_FLAG, RB_FLAG,
|
||||
UP_FLAG, DOWN_FLAG, LEFT_FLAG, RIGHT_FLAG
|
||||
};
|
||||
|
||||
GamepadState*
|
||||
SdlInputHandler::findStateForGamepad(SDL_JoystickID id)
|
||||
{
|
||||
int i;
|
||||
|
||||
for (i = 0; i < MAX_GAMEPADS; i++) {
|
||||
if (m_GamepadState[i].jsId == id) {
|
||||
SDL_assert(!m_MultiController || m_GamepadState[i].index == i);
|
||||
return &m_GamepadState[i];
|
||||
}
|
||||
}
|
||||
|
||||
// This should only happen with > 4 gamepads
|
||||
SDL_assert(SDL_NumJoysticks() > 4);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void SdlInputHandler::sendGamepadState(GamepadState* state)
|
||||
{
|
||||
SDL_assert(m_GamepadMask == 0x1 || m_MultiController);
|
||||
LiSendMultiControllerEvent(state->index,
|
||||
m_GamepadMask,
|
||||
state->buttons,
|
||||
state->lt,
|
||||
state->rt,
|
||||
state->lsX,
|
||||
state->lsY,
|
||||
state->rsX,
|
||||
state->rsY);
|
||||
}
|
||||
|
||||
Uint32 SdlInputHandler::mouseEmulationTimerCallback(Uint32 interval, void *param)
|
||||
{
|
||||
auto gamepad = reinterpret_cast<GamepadState*>(param);
|
||||
|
||||
short rawX;
|
||||
short rawY;
|
||||
|
||||
// Determine which analog stick is currently receiving the strongest input
|
||||
if ((uint32_t)qAbs(gamepad->lsX) + qAbs(gamepad->lsY) > (uint32_t)qAbs(gamepad->rsX) + qAbs(gamepad->rsY)) {
|
||||
rawX = gamepad->lsX;
|
||||
rawY = -gamepad->lsY;
|
||||
}
|
||||
else {
|
||||
rawX = gamepad->rsX;
|
||||
rawY = -gamepad->rsY;
|
||||
}
|
||||
|
||||
float deltaX;
|
||||
float deltaY;
|
||||
|
||||
// Produce a base vector for mouse movement with increased speed as we deviate further from center
|
||||
deltaX = qPow(rawX / 32766.0f * MOUSE_EMULATION_MOTION_MULTIPLIER, 3);
|
||||
deltaY = qPow(rawY / 32766.0f * MOUSE_EMULATION_MOTION_MULTIPLIER, 3);
|
||||
|
||||
// Enforce deadzones
|
||||
deltaX = qAbs(deltaX) > MOUSE_EMULATION_DEADZONE ? deltaX - MOUSE_EMULATION_DEADZONE : 0;
|
||||
deltaY = qAbs(deltaY) > MOUSE_EMULATION_DEADZONE ? deltaY - MOUSE_EMULATION_DEADZONE : 0;
|
||||
|
||||
if (deltaX != 0 || deltaY != 0) {
|
||||
LiSendMouseMoveEvent((short)deltaX, (short)deltaY);
|
||||
}
|
||||
|
||||
return interval;
|
||||
}
|
||||
|
||||
void SdlInputHandler::handleControllerAxisEvent(SDL_ControllerAxisEvent* event)
|
||||
{
|
||||
SDL_JoystickID gameControllerId = event->which;
|
||||
GamepadState* state = findStateForGamepad(gameControllerId);
|
||||
if (state == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Batch all pending axis motion events for this gamepad to save CPU time
|
||||
SDL_Event nextEvent;
|
||||
for (;;) {
|
||||
switch (event->axis)
|
||||
{
|
||||
case SDL_CONTROLLER_AXIS_LEFTX:
|
||||
state->lsX = event->value;
|
||||
break;
|
||||
case SDL_CONTROLLER_AXIS_LEFTY:
|
||||
// Signed values have one more negative value than
|
||||
// positive value, so inverting the sign on -32768
|
||||
// could actually cause the value to overflow and
|
||||
// wrap around to be negative again. Avoid that by
|
||||
// capping the value at 32767.
|
||||
state->lsY = -qMax(event->value, (short)-32767);
|
||||
break;
|
||||
case SDL_CONTROLLER_AXIS_RIGHTX:
|
||||
state->rsX = event->value;
|
||||
break;
|
||||
case SDL_CONTROLLER_AXIS_RIGHTY:
|
||||
state->rsY = -qMax(event->value, (short)-32767);
|
||||
break;
|
||||
case SDL_CONTROLLER_AXIS_TRIGGERLEFT:
|
||||
state->lt = (unsigned char)(event->value * 255UL / 32767);
|
||||
break;
|
||||
case SDL_CONTROLLER_AXIS_TRIGGERRIGHT:
|
||||
state->rt = (unsigned char)(event->value * 255UL / 32767);
|
||||
break;
|
||||
default:
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Unhandled controller axis: %d",
|
||||
event->axis);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for another event to batch with
|
||||
if (SDL_PeepEvents(&nextEvent, 1, SDL_PEEKEVENT, SDL_CONTROLLERAXISMOTION, SDL_CONTROLLERAXISMOTION) <= 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
event = &nextEvent.caxis;
|
||||
if (event->which != gameControllerId) {
|
||||
// Stop batching if a different gamepad interrupts us
|
||||
break;
|
||||
}
|
||||
|
||||
// Remove the next event to batch
|
||||
SDL_PeepEvents(&nextEvent, 1, SDL_GETEVENT, SDL_CONTROLLERAXISMOTION, SDL_CONTROLLERAXISMOTION);
|
||||
}
|
||||
|
||||
// Only send the gamepad state to the host if it's not in mouse emulation mode
|
||||
if (state->mouseEmulationTimer == 0) {
|
||||
sendGamepadState(state);
|
||||
}
|
||||
}
|
||||
|
||||
void SdlInputHandler::handleControllerButtonEvent(SDL_ControllerButtonEvent* event)
|
||||
{
|
||||
GamepadState* state = findStateForGamepad(event->which);
|
||||
if (state == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_SwapFaceButtons) {
|
||||
switch (event->button) {
|
||||
case SDL_CONTROLLER_BUTTON_A:
|
||||
event->button = SDL_CONTROLLER_BUTTON_B;
|
||||
break;
|
||||
case SDL_CONTROLLER_BUTTON_B:
|
||||
event->button = SDL_CONTROLLER_BUTTON_A;
|
||||
break;
|
||||
case SDL_CONTROLLER_BUTTON_X:
|
||||
event->button = SDL_CONTROLLER_BUTTON_Y;
|
||||
break;
|
||||
case SDL_CONTROLLER_BUTTON_Y:
|
||||
event->button = SDL_CONTROLLER_BUTTON_X;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (event->state == SDL_PRESSED) {
|
||||
state->buttons |= k_ButtonMap[event->button];
|
||||
|
||||
if (event->button == SDL_CONTROLLER_BUTTON_START) {
|
||||
state->lastStartDownTime = SDL_GetTicks();
|
||||
}
|
||||
else if (state->mouseEmulationTimer != 0) {
|
||||
if (event->button == SDL_CONTROLLER_BUTTON_A) {
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT);
|
||||
}
|
||||
else if (event->button == SDL_CONTROLLER_BUTTON_B) {
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_RIGHT);
|
||||
}
|
||||
else if (event->button == SDL_CONTROLLER_BUTTON_X) {
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_MIDDLE);
|
||||
}
|
||||
else if (event->button == SDL_CONTROLLER_BUTTON_LEFTSHOULDER) {
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_X1);
|
||||
}
|
||||
else if (event->button == SDL_CONTROLLER_BUTTON_RIGHTSHOULDER) {
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_X2);
|
||||
}
|
||||
else if (event->button == SDL_CONTROLLER_BUTTON_DPAD_UP) {
|
||||
LiSendScrollEvent(1);
|
||||
}
|
||||
else if (event->button == SDL_CONTROLLER_BUTTON_DPAD_DOWN) {
|
||||
LiSendScrollEvent(-1);
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
state->buttons &= ~k_ButtonMap[event->button];
|
||||
|
||||
if (event->button == SDL_CONTROLLER_BUTTON_START) {
|
||||
if (SDL_GetTicks() - state->lastStartDownTime > MOUSE_EMULATION_LONG_PRESS_TIME) {
|
||||
if (state->mouseEmulationTimer != 0) {
|
||||
SDL_RemoveTimer(state->mouseEmulationTimer);
|
||||
state->mouseEmulationTimer = 0;
|
||||
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Mouse emulation deactivated");
|
||||
Session::get()->notifyMouseEmulationMode(false);
|
||||
}
|
||||
else if (m_GamepadMouse) {
|
||||
// Send the start button up event to the host, since we won't do it below
|
||||
sendGamepadState(state);
|
||||
|
||||
state->mouseEmulationTimer = SDL_AddTimer(MOUSE_EMULATION_POLLING_INTERVAL, SdlInputHandler::mouseEmulationTimerCallback, state);
|
||||
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Mouse emulation active");
|
||||
Session::get()->notifyMouseEmulationMode(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (state->mouseEmulationTimer != 0) {
|
||||
if (event->button == SDL_CONTROLLER_BUTTON_A) {
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
|
||||
}
|
||||
else if (event->button == SDL_CONTROLLER_BUTTON_B) {
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_RIGHT);
|
||||
}
|
||||
else if (event->button == SDL_CONTROLLER_BUTTON_X) {
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_MIDDLE);
|
||||
}
|
||||
else if (event->button == SDL_CONTROLLER_BUTTON_LEFTSHOULDER) {
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_X1);
|
||||
}
|
||||
else if (event->button == SDL_CONTROLLER_BUTTON_RIGHTSHOULDER) {
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_X2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle Start+Select+L1+R1 as a gamepad quit combo
|
||||
if (state->buttons == (PLAY_FLAG | BACK_FLAG | LB_FLAG | RB_FLAG)) {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Detected quit gamepad button combo");
|
||||
|
||||
// Push a quit event to the main loop
|
||||
SDL_Event event;
|
||||
event.type = SDL_QUIT;
|
||||
event.quit.timestamp = SDL_GetTicks();
|
||||
SDL_PushEvent(&event);
|
||||
|
||||
// Clear buttons down on this gameapd
|
||||
LiSendMultiControllerEvent(state->index, m_GamepadMask,
|
||||
0, 0, 0, 0, 0, 0, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
// Only send the gamepad state to the host if it's not in mouse emulation mode
|
||||
if (state->mouseEmulationTimer == 0) {
|
||||
sendGamepadState(state);
|
||||
}
|
||||
}
|
||||
|
||||
void SdlInputHandler::handleControllerDeviceEvent(SDL_ControllerDeviceEvent* event)
|
||||
{
|
||||
GamepadState* state;
|
||||
|
||||
if (event->type == SDL_CONTROLLERDEVICEADDED) {
|
||||
int i;
|
||||
const char* name;
|
||||
SDL_GameController* controller;
|
||||
const char* mapping;
|
||||
char guidStr[33];
|
||||
uint32_t hapticCaps;
|
||||
|
||||
controller = SDL_GameControllerOpen(event->which);
|
||||
if (controller == NULL) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Failed to open gamepad: %s",
|
||||
SDL_GetError());
|
||||
return;
|
||||
}
|
||||
|
||||
// We used to use SDL_GameControllerGetPlayerIndex() here but that
|
||||
// can lead to strange issues due to bugs in Windows where an Xbox
|
||||
// controller will join as player 2, even though no player 1 controller
|
||||
// is connected at all. This pretty much screws any attempt to use
|
||||
// the gamepad in single player games, so just assign them in order from 0.
|
||||
i = 0;
|
||||
|
||||
for (; i < MAX_GAMEPADS; i++) {
|
||||
SDL_assert(m_GamepadState[i].controller != controller);
|
||||
if (m_GamepadState[i].controller == NULL) {
|
||||
// Found an empty slot
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (i == MAX_GAMEPADS) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"No open gamepad slots found!");
|
||||
SDL_GameControllerClose(controller);
|
||||
return;
|
||||
}
|
||||
|
||||
state = &m_GamepadState[i];
|
||||
if (m_MultiController) {
|
||||
state->index = i;
|
||||
|
||||
#if SDL_VERSION_ATLEAST(2, 0, 12)
|
||||
// This will change indicators on the controller to show the assigned
|
||||
// player index. For Xbox 360 controllers, that means updating the LED
|
||||
// ring to light up the corresponding quadrant for this player.
|
||||
SDL_GameControllerSetPlayerIndex(controller, state->index);
|
||||
#endif
|
||||
}
|
||||
else {
|
||||
// Always player 1 in single controller mode
|
||||
state->index = 0;
|
||||
}
|
||||
|
||||
state->controller = controller;
|
||||
state->jsId = SDL_JoystickInstanceID(SDL_GameControllerGetJoystick(state->controller));
|
||||
|
||||
#if SDL_VERSION_ATLEAST(2, 0, 9)
|
||||
// Perform a tiny rumble to see if haptics are supported.
|
||||
// NB: We cannot use zeros for rumble intensity or SDL will not actually call the JS driver
|
||||
// and we'll get a (potentially false) success value returned.
|
||||
hapticCaps = SDL_GameControllerRumble(controller, 1, 1, 1) == 0 ?
|
||||
ML_HAPTIC_GC_RUMBLE : 0;
|
||||
#else
|
||||
state->haptic = SDL_HapticOpenFromJoystick(SDL_GameControllerGetJoystick(state->controller));
|
||||
state->hapticEffectId = -1;
|
||||
state->hapticMethod = GAMEPAD_HAPTIC_METHOD_NONE;
|
||||
if (state->haptic != nullptr) {
|
||||
// Query for supported haptic effects
|
||||
hapticCaps = SDL_HapticQuery(state->haptic);
|
||||
hapticCaps |= SDL_HapticRumbleSupported(state->haptic) ?
|
||||
ML_HAPTIC_SIMPLE_RUMBLE : 0;
|
||||
|
||||
if ((SDL_HapticQuery(state->haptic) & SDL_HAPTIC_LEFTRIGHT) == 0) {
|
||||
if (SDL_HapticRumbleSupported(state->haptic)) {
|
||||
if (SDL_HapticRumbleInit(state->haptic) == 0) {
|
||||
state->hapticMethod = GAMEPAD_HAPTIC_METHOD_SIMPLERUMBLE;
|
||||
}
|
||||
}
|
||||
if (state->hapticMethod == GAMEPAD_HAPTIC_METHOD_NONE) {
|
||||
SDL_HapticClose(state->haptic);
|
||||
state->haptic = nullptr;
|
||||
}
|
||||
} else {
|
||||
state->hapticMethod = GAMEPAD_HAPTIC_METHOD_LEFTRIGHT;
|
||||
}
|
||||
}
|
||||
else {
|
||||
hapticCaps = 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
SDL_JoystickGetGUIDString(SDL_JoystickGetGUID(SDL_GameControllerGetJoystick(state->controller)),
|
||||
guidStr, sizeof(guidStr));
|
||||
mapping = SDL_GameControllerMapping(state->controller);
|
||||
name = SDL_GameControllerName(state->controller);
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Gamepad %d (player %d) is: %s (haptic capabilities: 0x%x) (mapping: %s -> %s)",
|
||||
i,
|
||||
state->index,
|
||||
name != nullptr ? name : "<null>",
|
||||
hapticCaps,
|
||||
guidStr,
|
||||
mapping != nullptr ? mapping : "<null>");
|
||||
if (mapping != nullptr) {
|
||||
SDL_free((void*)mapping);
|
||||
}
|
||||
|
||||
// Add this gamepad to the gamepad mask
|
||||
if (m_MultiController) {
|
||||
// NB: Don't assert that it's unset here because we will already
|
||||
// have the mask set for initially attached gamepads to avoid confusing
|
||||
// apps running on the host.
|
||||
m_GamepadMask |= (1 << state->index);
|
||||
}
|
||||
else {
|
||||
SDL_assert(m_GamepadMask == 0x1);
|
||||
}
|
||||
|
||||
// Send an empty event to tell the PC we've arrived
|
||||
sendGamepadState(state);
|
||||
}
|
||||
else if (event->type == SDL_CONTROLLERDEVICEREMOVED) {
|
||||
state = findStateForGamepad(event->which);
|
||||
if (state != NULL) {
|
||||
if (state->mouseEmulationTimer != 0) {
|
||||
Session::get()->notifyMouseEmulationMode(false);
|
||||
SDL_RemoveTimer(state->mouseEmulationTimer);
|
||||
}
|
||||
|
||||
SDL_GameControllerClose(state->controller);
|
||||
|
||||
#if !SDL_VERSION_ATLEAST(2, 0, 9)
|
||||
if (state->haptic != nullptr) {
|
||||
SDL_HapticClose(state->haptic);
|
||||
}
|
||||
#endif
|
||||
|
||||
// Remove this from the gamepad mask in MC-mode
|
||||
if (m_MultiController) {
|
||||
SDL_assert(m_GamepadMask & (1 << state->index));
|
||||
m_GamepadMask &= ~(1 << state->index);
|
||||
}
|
||||
else {
|
||||
SDL_assert(m_GamepadMask == 0x1);
|
||||
}
|
||||
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Gamepad %d is gone",
|
||||
state->index);
|
||||
|
||||
// Send a final event to let the PC know this gamepad is gone
|
||||
LiSendMultiControllerEvent(state->index, m_GamepadMask,
|
||||
0, 0, 0, 0, 0, 0, 0);
|
||||
|
||||
// Clear all remaining state from this slot
|
||||
SDL_memset(state, 0, sizeof(*state));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SdlInputHandler::handleJoystickArrivalEvent(SDL_JoyDeviceEvent* event)
|
||||
{
|
||||
SDL_assert(event->type == SDL_JOYDEVICEADDED);
|
||||
|
||||
if (!SDL_IsGameController(event->which)) {
|
||||
char guidStr[33];
|
||||
SDL_JoystickGetGUIDString(SDL_JoystickGetDeviceGUID(event->which),
|
||||
guidStr, sizeof(guidStr));
|
||||
const char* name = SDL_JoystickNameForIndex(event->which);
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Joystick discovered with no mapping: %s %s",
|
||||
name ? name : "<UNKNOWN>",
|
||||
guidStr);
|
||||
SDL_Joystick* joy = SDL_JoystickOpen(event->which);
|
||||
if (joy != nullptr) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Number of axes: %d | Number of buttons: %d | Number of hats: %d",
|
||||
SDL_JoystickNumAxes(joy), SDL_JoystickNumButtons(joy),
|
||||
SDL_JoystickNumHats(joy));
|
||||
SDL_JoystickClose(joy);
|
||||
}
|
||||
else {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Unable to open joystick for query: %s",
|
||||
SDL_GetError());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SdlInputHandler::rumble(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor)
|
||||
{
|
||||
// Make sure the controller number is within our supported count
|
||||
if (controllerNumber >= MAX_GAMEPADS) {
|
||||
return;
|
||||
}
|
||||
|
||||
#if SDL_VERSION_ATLEAST(2, 0, 9)
|
||||
if (m_GamepadState[controllerNumber].controller != nullptr) {
|
||||
SDL_GameControllerRumble(m_GamepadState[controllerNumber].controller, lowFreqMotor, highFreqMotor, 30000);
|
||||
}
|
||||
#else
|
||||
// Check if the controller supports haptics (and if the controller exists at all)
|
||||
SDL_Haptic* haptic = m_GamepadState[controllerNumber].haptic;
|
||||
if (haptic == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Stop the last effect we played
|
||||
if (m_GamepadState[controllerNumber].hapticMethod == GAMEPAD_HAPTIC_METHOD_LEFTRIGHT) {
|
||||
if (m_GamepadState[controllerNumber].hapticEffectId >= 0) {
|
||||
SDL_HapticDestroyEffect(haptic, m_GamepadState[controllerNumber].hapticEffectId);
|
||||
}
|
||||
} else if (m_GamepadState[controllerNumber].hapticMethod == GAMEPAD_HAPTIC_METHOD_SIMPLERUMBLE) {
|
||||
SDL_HapticRumbleStop(haptic);
|
||||
}
|
||||
|
||||
// If this callback is telling us to stop both motors, don't bother queuing a new effect
|
||||
if (lowFreqMotor == 0 && highFreqMotor == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_GamepadState[controllerNumber].hapticMethod == GAMEPAD_HAPTIC_METHOD_LEFTRIGHT) {
|
||||
SDL_HapticEffect effect;
|
||||
SDL_memset(&effect, 0, sizeof(effect));
|
||||
effect.type = SDL_HAPTIC_LEFTRIGHT;
|
||||
|
||||
// The effect should last until we are instructed to stop or change it
|
||||
effect.leftright.length = SDL_HAPTIC_INFINITY;
|
||||
|
||||
// SDL haptics range from 0-32767 but XInput uses 0-65535, so divide by 2 to correct for SDL's scaling
|
||||
effect.leftright.large_magnitude = lowFreqMotor / 2;
|
||||
effect.leftright.small_magnitude = highFreqMotor / 2;
|
||||
|
||||
// Play the new effect
|
||||
m_GamepadState[controllerNumber].hapticEffectId = SDL_HapticNewEffect(haptic, &effect);
|
||||
if (m_GamepadState[controllerNumber].hapticEffectId >= 0) {
|
||||
SDL_HapticRunEffect(haptic, m_GamepadState[controllerNumber].hapticEffectId, 1);
|
||||
}
|
||||
} else if (m_GamepadState[controllerNumber].hapticMethod == GAMEPAD_HAPTIC_METHOD_SIMPLERUMBLE) {
|
||||
SDL_HapticRumblePlay(haptic,
|
||||
std::min(1.0, (GAMEPAD_HAPTIC_SIMPLE_HIFREQ_MOTOR_WEIGHT*highFreqMotor +
|
||||
GAMEPAD_HAPTIC_SIMPLE_LOWFREQ_MOTOR_WEIGHT*lowFreqMotor) / 65535.0),
|
||||
SDL_HAPTIC_INFINITY);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
QString SdlInputHandler::getUnmappedGamepads()
|
||||
{
|
||||
QString ret;
|
||||
|
||||
if (SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) != 0) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) failed: %s",
|
||||
SDL_GetError());
|
||||
}
|
||||
|
||||
MappingManager mappingManager;
|
||||
mappingManager.applyMappings();
|
||||
|
||||
for (int i = 0; i < SDL_NumJoysticks(); i++) {
|
||||
if (!SDL_IsGameController(i)) {
|
||||
char guidStr[33];
|
||||
SDL_JoystickGetGUIDString(SDL_JoystickGetDeviceGUID(i),
|
||||
guidStr, sizeof(guidStr));
|
||||
const char* name = SDL_JoystickNameForIndex(i);
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Unmapped joystick: %s %s",
|
||||
name ? name : "<UNKNOWN>",
|
||||
guidStr);
|
||||
SDL_Joystick* joy = SDL_JoystickOpen(i);
|
||||
if (joy != nullptr) {
|
||||
int numButtons = SDL_JoystickNumButtons(joy);
|
||||
int numHats = SDL_JoystickNumHats(joy);
|
||||
int numAxes = SDL_JoystickNumAxes(joy);
|
||||
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Number of axes: %d | Number of buttons: %d | Number of hats: %d",
|
||||
numAxes, numButtons, numHats);
|
||||
|
||||
if ((numAxes >= 4 && numAxes <= 8) && numButtons >= 8 && numHats <= 1) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Joystick likely to be an unmapped game controller");
|
||||
if (!ret.isEmpty()) {
|
||||
ret += ", ";
|
||||
}
|
||||
|
||||
ret += name;
|
||||
}
|
||||
|
||||
SDL_JoystickClose(joy);
|
||||
}
|
||||
else {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Unable to open joystick for query: %s",
|
||||
SDL_GetError());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
int SdlInputHandler::getAttachedGamepadMask()
|
||||
{
|
||||
int count;
|
||||
int mask;
|
||||
|
||||
if (!m_MultiController) {
|
||||
// Player 1 is always present in non-MC mode
|
||||
return 0x1;
|
||||
}
|
||||
|
||||
count = mask = 0;
|
||||
for (int i = 0; i < SDL_NumJoysticks(); i++) {
|
||||
if (SDL_IsGameController(i)) {
|
||||
mask |= (1 << count++);
|
||||
}
|
||||
}
|
||||
|
||||
return mask;
|
||||
}
|
||||
@@ -0,0 +1,546 @@
|
||||
#include <Limelight.h>
|
||||
#include <SDL.h>
|
||||
#include "streaming/session.h"
|
||||
#include "settings/mappingmanager.h"
|
||||
#include "path.h"
|
||||
#include "utils.h"
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QDir>
|
||||
|
||||
#define MOUSE_POLLING_INTERVAL 5
|
||||
|
||||
#ifdef Q_OS_WIN32
|
||||
HHOOK g_KeyboardHook;
|
||||
#endif
|
||||
|
||||
SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, NvComputer*, int streamWidth, int streamHeight)
|
||||
: m_MultiController(prefs.multiController),
|
||||
m_GamepadMouse(prefs.gamepadMouse),
|
||||
m_SwapMouseButtons(prefs.swapMouseButtons),
|
||||
m_ReverseScrollDirection(prefs.reverseScrollDirection),
|
||||
m_SwapFaceButtons(prefs.swapFaceButtons),
|
||||
m_MouseMoveTimer(0),
|
||||
m_MousePositionLock(0),
|
||||
m_MouseWasInVideoRegion(false),
|
||||
m_PendingMouseButtonsAllUpOnVideoRegionLeave(false),
|
||||
m_FakeCaptureActive(false),
|
||||
m_CaptureSystemKeysEnabled(prefs.captureSysKeys || !WMUtils::isRunningWindowManager()),
|
||||
m_LongPressTimer(0),
|
||||
m_StreamWidth(streamWidth),
|
||||
m_StreamHeight(streamHeight),
|
||||
m_AbsoluteMouseMode(prefs.absoluteMouseMode),
|
||||
m_AbsoluteTouchMode(prefs.absoluteTouchMode),
|
||||
m_PendingMouseLeaveButtonUp(0),
|
||||
m_LeftButtonReleaseTimer(0),
|
||||
m_RightButtonReleaseTimer(0),
|
||||
m_DragTimer(0),
|
||||
m_DragButton(0),
|
||||
m_NumFingersDown(0)
|
||||
{
|
||||
// Allow gamepad input when the app doesn't have focus if requested
|
||||
SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, prefs.backgroundGamepad ? "1" : "0");
|
||||
|
||||
// If absolute mouse mode is enabled, use relative mode warp (which
|
||||
// is via normal motion events that are influenced by mouse acceleration).
|
||||
// Otherwise, we'll use raw input capture which is straight from the device
|
||||
// without modification by the OS.
|
||||
SDL_SetHintWithPriority(SDL_HINT_MOUSE_RELATIVE_MODE_WARP,
|
||||
prefs.absoluteMouseMode ? "1" : "0",
|
||||
SDL_HINT_OVERRIDE);
|
||||
|
||||
// If we're grabbing system keys, enable the X11 keyboard grab in SDL
|
||||
SDL_SetHintWithPriority(SDL_HINT_GRAB_KEYBOARD,
|
||||
m_CaptureSystemKeysEnabled ? "1" : "0",
|
||||
SDL_HINT_OVERRIDE);
|
||||
|
||||
// Allow clicks to pass through to us when focusing the window. If we're in
|
||||
// absolute mouse mode, this will avoid the user having to click twice to
|
||||
// trigger a click on the host if the Moonlight window is not focused. In
|
||||
// relative mode, the click event will trigger the mouse to be recaptured.
|
||||
SDL_SetHint(SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH, "1");
|
||||
|
||||
// Enabling extended input reports allows rumble to function on Bluetooth PS4/PS5
|
||||
// controllers, but breaks DirectInput applications. We will enable it because
|
||||
// it's likely that working rumble is what the user is expecting. If they don't
|
||||
// want this behavior, they can override it with the environment variable.
|
||||
SDL_SetHint("SDL_JOYSTICK_HIDAPI_PS4_RUMBLE", "1");
|
||||
SDL_SetHint("SDL_JOYSTICK_HIDAPI_PS5_RUMBLE", "1");
|
||||
|
||||
m_OldIgnoreDevices = SDL_GetHint(SDL_HINT_GAMECONTROLLER_IGNORE_DEVICES);
|
||||
m_OldIgnoreDevicesExcept = SDL_GetHint(SDL_HINT_GAMECONTROLLER_IGNORE_DEVICES_EXCEPT);
|
||||
|
||||
QString streamIgnoreDevices = qgetenv("STREAM_GAMECONTROLLER_IGNORE_DEVICES");
|
||||
QString streamIgnoreDevicesExcept = qgetenv("STREAM_GAMECONTROLLER_IGNORE_DEVICES_EXCEPT");
|
||||
|
||||
if (!streamIgnoreDevices.isEmpty() && !streamIgnoreDevices.endsWith(',')) {
|
||||
streamIgnoreDevices += ',';
|
||||
}
|
||||
streamIgnoreDevices += m_OldIgnoreDevices;
|
||||
|
||||
// For SDL_HINT_GAMECONTROLLER_IGNORE_DEVICES, we use the union of SDL_GAMECONTROLLER_IGNORE_DEVICES
|
||||
// and STREAM_GAMECONTROLLER_IGNORE_DEVICES while streaming. STREAM_GAMECONTROLLER_IGNORE_DEVICES_EXCEPT
|
||||
// overrides SDL_GAMECONTROLLER_IGNORE_DEVICES_EXCEPT while streaming.
|
||||
SDL_SetHint(SDL_HINT_GAMECONTROLLER_IGNORE_DEVICES, streamIgnoreDevices.toUtf8());
|
||||
SDL_SetHint(SDL_HINT_GAMECONTROLLER_IGNORE_DEVICES_EXCEPT, streamIgnoreDevicesExcept.toUtf8());
|
||||
|
||||
// We must initialize joystick explicitly before gamecontroller in order
|
||||
// to ensure we receive gamecontroller attach events for gamepads where
|
||||
// SDL doesn't have a built-in mapping. By starting joystick first, we
|
||||
// can allow mapping manager to update the mappings before GC attach
|
||||
// events are generated.
|
||||
SDL_assert(!SDL_WasInit(SDL_INIT_JOYSTICK));
|
||||
if (SDL_InitSubSystem(SDL_INIT_JOYSTICK) != 0) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"SDL_InitSubSystem(SDL_INIT_JOYSTICK) failed: %s",
|
||||
SDL_GetError());
|
||||
}
|
||||
|
||||
MappingManager mappingManager;
|
||||
mappingManager.applyMappings();
|
||||
|
||||
// Flush gamepad arrival and departure events which may be queued before
|
||||
// starting the gamecontroller subsystem again. This prevents us from
|
||||
// receiving duplicate arrival and departure events for the same gamepad.
|
||||
SDL_FlushEvent(SDL_CONTROLLERDEVICEADDED);
|
||||
SDL_FlushEvent(SDL_CONTROLLERDEVICEREMOVED);
|
||||
|
||||
// We need to reinit this each time, since you only get
|
||||
// an initial set of gamepad arrival events once per init.
|
||||
SDL_assert(!SDL_WasInit(SDL_INIT_GAMECONTROLLER));
|
||||
if (SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) != 0) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) failed: %s",
|
||||
SDL_GetError());
|
||||
}
|
||||
|
||||
#if !SDL_VERSION_ATLEAST(2, 0, 9)
|
||||
SDL_assert(!SDL_WasInit(SDL_INIT_HAPTIC));
|
||||
if (SDL_InitSubSystem(SDL_INIT_HAPTIC) != 0) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"SDL_InitSubSystem(SDL_INIT_HAPTIC) failed: %s",
|
||||
SDL_GetError());
|
||||
}
|
||||
#endif
|
||||
|
||||
// Initialize the gamepad mask with currently attached gamepads to avoid
|
||||
// causing gamepads to unexpectedly disappear and reappear on the host
|
||||
// during stream startup as we detect currently attached gamepads one at a time.
|
||||
m_GamepadMask = getAttachedGamepadMask();
|
||||
|
||||
SDL_zero(m_GamepadState);
|
||||
SDL_zero(m_LastTouchDownEvent);
|
||||
SDL_zero(m_LastTouchUpEvent);
|
||||
SDL_zero(m_TouchDownEvent);
|
||||
SDL_zero(m_MousePositionReport);
|
||||
|
||||
SDL_AtomicSet(&m_MouseDeltaX, 0);
|
||||
SDL_AtomicSet(&m_MouseDeltaY, 0);
|
||||
SDL_AtomicSet(&m_MousePositionUpdated, 0);
|
||||
|
||||
Uint32 pollingInterval = QString(qgetenv("MOUSE_POLLING_INTERVAL")).toUInt();
|
||||
if (pollingInterval == 0) {
|
||||
pollingInterval = MOUSE_POLLING_INTERVAL;
|
||||
}
|
||||
else {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Using custom mouse polling interval: %u ms",
|
||||
pollingInterval);
|
||||
}
|
||||
|
||||
m_MouseMoveTimer = SDL_AddTimer(pollingInterval, SdlInputHandler::mouseMoveTimerCallback, this);
|
||||
|
||||
#ifdef Q_OS_WIN32
|
||||
if (m_CaptureSystemKeysEnabled) {
|
||||
// If system key capture is enabled, install the window hook required to intercept
|
||||
// these key presses and block them from the OS itself.
|
||||
g_KeyboardHook = SetWindowsHookEx(WH_KEYBOARD_LL, SdlInputHandler::keyboardHookProc, GetModuleHandle(NULL), 0);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
SdlInputHandler::~SdlInputHandler()
|
||||
{
|
||||
#ifdef Q_OS_WIN32
|
||||
if (g_KeyboardHook != nullptr) {
|
||||
UnhookWindowsHookEx(g_KeyboardHook);
|
||||
g_KeyboardHook = nullptr;
|
||||
|
||||
// If we're terminating because of the user pressing the quit combo,
|
||||
// we won't have a chance to inform SDL as the modifier keys raise.
|
||||
// This will leave stale modifier flags in the SDL_Keyboard global
|
||||
// inside SDL which will show up on our next key event if the user
|
||||
// streams again. Avoid this by explicitly zeroing mod state when
|
||||
// ending the stream.
|
||||
//
|
||||
// This is only needed for the case where we're hooking the keyboard
|
||||
// because we're generating synthetic SDL_KEYDOWN/SDL_KEYUP events
|
||||
// which don't properly maintain SDL's internal keyboard state,
|
||||
// so we're forced to invoke SDL_SetModState() ourselves to set mods.
|
||||
SDL_SetModState(KMOD_NONE);
|
||||
}
|
||||
#endif
|
||||
|
||||
for (int i = 0; i < MAX_GAMEPADS; i++) {
|
||||
if (m_GamepadState[i].mouseEmulationTimer != 0) {
|
||||
Session::get()->notifyMouseEmulationMode(false);
|
||||
SDL_RemoveTimer(m_GamepadState[i].mouseEmulationTimer);
|
||||
}
|
||||
#if !SDL_VERSION_ATLEAST(2, 0, 9)
|
||||
if (m_GamepadState[i].haptic != nullptr) {
|
||||
SDL_HapticClose(m_GamepadState[i].haptic);
|
||||
}
|
||||
#endif
|
||||
if (m_GamepadState[i].controller != nullptr) {
|
||||
SDL_GameControllerClose(m_GamepadState[i].controller);
|
||||
}
|
||||
}
|
||||
|
||||
SDL_RemoveTimer(m_MouseMoveTimer);
|
||||
SDL_RemoveTimer(m_LongPressTimer);
|
||||
SDL_RemoveTimer(m_LeftButtonReleaseTimer);
|
||||
SDL_RemoveTimer(m_RightButtonReleaseTimer);
|
||||
SDL_RemoveTimer(m_DragTimer);
|
||||
|
||||
#if !SDL_VERSION_ATLEAST(2, 0, 9)
|
||||
SDL_QuitSubSystem(SDL_INIT_HAPTIC);
|
||||
SDL_assert(!SDL_WasInit(SDL_INIT_HAPTIC));
|
||||
#endif
|
||||
|
||||
SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER);
|
||||
SDL_assert(!SDL_WasInit(SDL_INIT_GAMECONTROLLER));
|
||||
|
||||
SDL_QuitSubSystem(SDL_INIT_JOYSTICK);
|
||||
SDL_assert(!SDL_WasInit(SDL_INIT_JOYSTICK));
|
||||
|
||||
// Return background event handling to off
|
||||
SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "0");
|
||||
|
||||
// Restore the ignored devices
|
||||
SDL_SetHint(SDL_HINT_GAMECONTROLLER_IGNORE_DEVICES, m_OldIgnoreDevices.toUtf8());
|
||||
SDL_SetHint(SDL_HINT_GAMECONTROLLER_IGNORE_DEVICES_EXCEPT, m_OldIgnoreDevicesExcept.toUtf8());
|
||||
|
||||
#ifdef STEAM_LINK
|
||||
// Hide SDL's cursor on Steam Link after quitting the stream.
|
||||
// FIXME: We should also do this for other situations where SDL
|
||||
// and Qt will draw their own mouse cursors like KMSDRM or RPi
|
||||
// video backends.
|
||||
SDL_ShowCursor(SDL_DISABLE);
|
||||
#endif
|
||||
}
|
||||
|
||||
void SdlInputHandler::setWindow(SDL_Window *window)
|
||||
{
|
||||
m_Window = window;
|
||||
}
|
||||
|
||||
#ifdef Q_OS_WIN32
|
||||
LRESULT CALLBACK SdlInputHandler::keyboardHookProc(int nCode, WPARAM wParam, LPARAM lParam)
|
||||
{
|
||||
if (nCode < 0 || nCode != HC_ACTION) {
|
||||
return CallNextHookEx(g_KeyboardHook, nCode, wParam, lParam);
|
||||
}
|
||||
|
||||
bool keyCaptureActive;
|
||||
SDL_Event event = {};
|
||||
KBDLLHOOKSTRUCT* hookData = (KBDLLHOOKSTRUCT*)lParam;
|
||||
switch (hookData->vkCode) {
|
||||
case VK_LWIN:
|
||||
event.key.keysym.sym = SDLK_LGUI;
|
||||
event.key.keysym.scancode = SDL_SCANCODE_LGUI;
|
||||
break;
|
||||
case VK_RWIN:
|
||||
event.key.keysym.sym = SDLK_RGUI;
|
||||
event.key.keysym.scancode = SDL_SCANCODE_RGUI;
|
||||
break;
|
||||
case VK_LMENU:
|
||||
event.key.keysym.sym = SDLK_LALT;
|
||||
event.key.keysym.scancode = SDL_SCANCODE_LALT;
|
||||
break;
|
||||
case VK_RMENU:
|
||||
event.key.keysym.sym = SDLK_RALT;
|
||||
event.key.keysym.scancode = SDL_SCANCODE_RALT;
|
||||
break;
|
||||
case VK_LCONTROL:
|
||||
event.key.keysym.sym = SDLK_LCTRL;
|
||||
event.key.keysym.scancode = SDL_SCANCODE_LCTRL;
|
||||
break;
|
||||
case VK_RCONTROL:
|
||||
event.key.keysym.sym = SDLK_RCTRL;
|
||||
event.key.keysym.scancode = SDL_SCANCODE_RCTRL;
|
||||
break;
|
||||
default:
|
||||
// Bail quickly if it's not a key we care about
|
||||
goto NextHook;
|
||||
}
|
||||
|
||||
// Make sure we're in a state where we actually want to steal this event (and it is safe to do so)
|
||||
Session* session = Session::get();
|
||||
if (session == nullptr || session->m_InputHandler == nullptr) {
|
||||
goto NextHook;
|
||||
}
|
||||
|
||||
keyCaptureActive = session->m_InputHandler->isSystemKeyCaptureActive();
|
||||
|
||||
// If this is a key we're going to intercept, create the synthetic SDL event.
|
||||
// This is necessary because we are also hiding this key event from ourselves too.
|
||||
if (wParam == WM_KEYDOWN || wParam == WM_SYSKEYDOWN) {
|
||||
event.type = SDL_KEYDOWN;
|
||||
event.key.state = SDL_PRESSED;
|
||||
}
|
||||
else {
|
||||
event.type = SDL_KEYUP;
|
||||
event.key.state = SDL_RELEASED;
|
||||
}
|
||||
|
||||
// Drop the event if it's a key down and capture is not active.
|
||||
// For key up, we'll need to do a little more work to determine if we need to send this
|
||||
// event to SDL in order to keep its internal keyboard modifier state consistent.
|
||||
if (event.type == SDL_KEYDOWN && !keyCaptureActive) {
|
||||
goto NextHook;
|
||||
}
|
||||
|
||||
event.key.keysym.mod = SDL_GetModState();
|
||||
switch (hookData->vkCode) {
|
||||
case VK_LWIN:
|
||||
if (event.key.state == SDL_PRESSED) {
|
||||
event.key.keysym.mod |= KMOD_LGUI;
|
||||
}
|
||||
else {
|
||||
event.key.keysym.mod &= ~KMOD_LGUI;
|
||||
}
|
||||
break;
|
||||
case VK_RWIN:
|
||||
if (event.key.state == SDL_PRESSED) {
|
||||
event.key.keysym.mod |= KMOD_RGUI;
|
||||
}
|
||||
else {
|
||||
event.key.keysym.mod &= ~KMOD_RGUI;
|
||||
}
|
||||
break;
|
||||
case VK_LMENU:
|
||||
if (event.key.state == SDL_PRESSED) {
|
||||
event.key.keysym.mod |= KMOD_LALT;
|
||||
}
|
||||
else {
|
||||
event.key.keysym.mod &= ~KMOD_LALT;
|
||||
}
|
||||
break;
|
||||
case VK_RMENU:
|
||||
if (event.key.state == SDL_PRESSED) {
|
||||
event.key.keysym.mod |= KMOD_RALT;
|
||||
}
|
||||
else {
|
||||
event.key.keysym.mod &= ~KMOD_RALT;
|
||||
}
|
||||
break;
|
||||
case VK_LCONTROL:
|
||||
if (event.key.state == SDL_PRESSED) {
|
||||
event.key.keysym.mod |= KMOD_LCTRL;
|
||||
}
|
||||
else {
|
||||
event.key.keysym.mod &= ~KMOD_LCTRL;
|
||||
}
|
||||
break;
|
||||
case VK_RCONTROL:
|
||||
if (event.key.state == SDL_PRESSED) {
|
||||
event.key.keysym.mod |= KMOD_RCTRL;
|
||||
}
|
||||
else {
|
||||
event.key.keysym.mod &= ~KMOD_RCTRL;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// If the modifier state is unchanged and we're not capturing system keys,
|
||||
// drop the event. If the modifier state is changed, we need to send the
|
||||
// event to update SDL's state even if system key capture is now inactive
|
||||
// (due to focus loss, mouse capture toggled off, etc.) otherwise SDL won't
|
||||
// know that the modifier key has been lifted.
|
||||
if (event.key.keysym.mod == SDL_GetModState() && !keyCaptureActive) {
|
||||
goto NextHook;
|
||||
}
|
||||
|
||||
event.key.timestamp = SDL_GetTicks();
|
||||
event.key.windowID = SDL_GetWindowID(session->m_Window);
|
||||
|
||||
// NOTE: event.key.repeat is not populated in this path!
|
||||
SDL_PushEvent(&event);
|
||||
|
||||
// Synchronize SDL's modifier state with the current state.
|
||||
// SDL won't do this on its own because it will never see the
|
||||
// WM_KEYUP/WM_KEYDOWN events.
|
||||
SDL_SetModState((SDL_Keymod)event.key.keysym.mod);
|
||||
|
||||
// Eat the event only if key capture is active.
|
||||
// If capture is not active and we're just resyncing SDL's modifier state,
|
||||
// we need to ensure the key event is still delivered normally to the
|
||||
// window in focus.
|
||||
if (keyCaptureActive) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
NextHook:
|
||||
return CallNextHookEx(g_KeyboardHook, nCode, wParam, lParam);
|
||||
}
|
||||
#endif
|
||||
|
||||
void SdlInputHandler::raiseAllKeys()
|
||||
{
|
||||
if (m_KeysDown.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Raising %d keys",
|
||||
(int)m_KeysDown.count());
|
||||
|
||||
for (auto keyDown : m_KeysDown) {
|
||||
LiSendKeyboardEvent(keyDown, KEY_ACTION_UP, 0);
|
||||
}
|
||||
|
||||
m_KeysDown.clear();
|
||||
}
|
||||
|
||||
void SdlInputHandler::notifyMouseLeave()
|
||||
{
|
||||
#ifdef Q_OS_WIN32
|
||||
// SDL on Windows doesn't send the mouse button up until the mouse re-enters the window
|
||||
// after leaving it. This breaks some of the Aero snap gestures, so we'll fake it here.
|
||||
if (m_AbsoluteMouseMode && isCaptureActive()) {
|
||||
// NB: Not using SDL_GetGlobalMouseState() because we want our state not the system's
|
||||
Uint32 mouseState = SDL_GetMouseState(nullptr, nullptr);
|
||||
for (Uint32 button = SDL_BUTTON_LEFT; button <= SDL_BUTTON_X2; button++) {
|
||||
if (mouseState & SDL_BUTTON(button)) {
|
||||
m_PendingMouseLeaveButtonUp = button;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void SdlInputHandler::notifyFocusLost()
|
||||
{
|
||||
// Release mouse cursor when another window is activated (e.g. by using ALT+TAB).
|
||||
// This lets user to interact with our window's title bar and with the buttons in it.
|
||||
// Doing this while the window is full-screen breaks the transition out of FS
|
||||
// (desktop and exclusive), so we must check for that before releasing mouse capture.
|
||||
if (!(SDL_GetWindowFlags(m_Window) & SDL_WINDOW_FULLSCREEN) && !m_AbsoluteMouseMode) {
|
||||
setCaptureActive(false);
|
||||
}
|
||||
|
||||
// Raise all keys that are currently pressed. If we don't do this, certain keys
|
||||
// used in shortcuts that cause focus loss (such as Alt+Tab) may get stuck down.
|
||||
raiseAllKeys();
|
||||
}
|
||||
|
||||
bool SdlInputHandler::isCaptureActive()
|
||||
{
|
||||
if (SDL_GetRelativeMouseMode()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Some platforms don't support SDL_SetRelativeMouseMode
|
||||
return m_FakeCaptureActive;
|
||||
}
|
||||
|
||||
bool SdlInputHandler::isSystemKeyCaptureActive()
|
||||
{
|
||||
if (!m_CaptureSystemKeysEnabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_Window == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Uint32 windowFlags = SDL_GetWindowFlags(m_Window);
|
||||
return (windowFlags & SDL_WINDOW_INPUT_FOCUS) &&
|
||||
(windowFlags & SDL_WINDOW_INPUT_GRABBED) &&
|
||||
(windowFlags & SDL_WINDOW_FULLSCREEN_DESKTOP);
|
||||
}
|
||||
|
||||
void SdlInputHandler::setCaptureActive(bool active)
|
||||
{
|
||||
if (active) {
|
||||
// If we're in full-screen exclusive mode, grab the cursor so it can't accidentally leave our window.
|
||||
// If we're in full-screen desktop mode but system key capture is enabled, also grab the cursor (will grab the keyboard too on X11).
|
||||
if (((SDL_GetWindowFlags(m_Window) & SDL_WINDOW_FULLSCREEN_DESKTOP) != 0 && m_CaptureSystemKeysEnabled) ||
|
||||
(SDL_GetWindowFlags(m_Window) & SDL_WINDOW_FULLSCREEN_DESKTOP) == SDL_WINDOW_FULLSCREEN) {
|
||||
SDL_SetWindowGrab(m_Window, SDL_TRUE);
|
||||
}
|
||||
|
||||
if (!m_AbsoluteMouseMode) {
|
||||
// If our window is occluded when mouse is captured, the mouse may
|
||||
// get stuck on top of the occluding window and not be properly
|
||||
// captured. We can avoid this by raising our window before we
|
||||
// capture the mouse.
|
||||
SDL_RaiseWindow(m_Window);
|
||||
}
|
||||
|
||||
// If we're in relative mode, try to activate SDL's relative mouse mode
|
||||
if (m_AbsoluteMouseMode || SDL_SetRelativeMouseMode(SDL_TRUE) < 0) {
|
||||
// Relative mouse mode didn't work or was disabled, so we'll just hide the cursor
|
||||
SDL_ShowCursor(SDL_DISABLE);
|
||||
m_FakeCaptureActive = true;
|
||||
}
|
||||
|
||||
// Synchronize the client and host cursor when activating absolute capture
|
||||
if (m_AbsoluteMouseMode) {
|
||||
int mouseX, mouseY;
|
||||
int windowX, windowY;
|
||||
|
||||
// We have to use SDL_GetGlobalMouseState() because macOS may not reflect
|
||||
// the new position of the mouse when outside the window.
|
||||
SDL_GetGlobalMouseState(&mouseX, &mouseY);
|
||||
|
||||
// Convert global mouse state to window-relative
|
||||
SDL_GetWindowPosition(m_Window, &windowX, &windowY);
|
||||
mouseX -= windowX;
|
||||
mouseY -= windowY;
|
||||
|
||||
if (isMouseInVideoRegion(mouseX, mouseY)) {
|
||||
updateMousePositionReport(mouseX, mouseY);
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (m_FakeCaptureActive) {
|
||||
// Display the cursor again
|
||||
SDL_ShowCursor(SDL_ENABLE);
|
||||
m_FakeCaptureActive = false;
|
||||
}
|
||||
else {
|
||||
SDL_SetRelativeMouseMode(SDL_FALSE);
|
||||
}
|
||||
|
||||
// Allow the cursor to leave the bounds of our window again.
|
||||
SDL_SetWindowGrab(m_Window, SDL_FALSE);
|
||||
}
|
||||
}
|
||||
|
||||
void SdlInputHandler::handleTouchFingerEvent(SDL_TouchFingerEvent* event)
|
||||
{
|
||||
#if SDL_VERSION_ATLEAST(2, 0, 10)
|
||||
if (SDL_GetTouchDeviceType(event->touchId) != SDL_TOUCH_DEVICE_DIRECT) {
|
||||
// Ignore anything that isn't a touchscreen. We may get callbacks
|
||||
// for trackpads, but we want to handle those in the mouse path.
|
||||
return;
|
||||
}
|
||||
#elif defined(Q_OS_DARWIN)
|
||||
// SDL2 sends touch events from trackpads by default on
|
||||
// macOS. This totally screws our actual mouse handling,
|
||||
// so we must explicitly ignore touch events on macOS
|
||||
// until SDL 2.0.10 where we have SDL_GetTouchDeviceType()
|
||||
// to tell them apart.
|
||||
return;
|
||||
#endif
|
||||
|
||||
if (m_AbsoluteTouchMode) {
|
||||
handleAbsoluteFingerEvent(event);
|
||||
}
|
||||
else {
|
||||
handleRelativeFingerEvent(event);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,14 @@
|
||||
|
||||
#include <SDL.h>
|
||||
|
||||
#ifdef Q_OS_WIN32
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <Windows.h>
|
||||
#endif
|
||||
|
||||
#define SDL_CODE_HIDE_CURSOR 1
|
||||
#define SDL_CODE_SHOW_CURSOR 2
|
||||
|
||||
struct GamepadState {
|
||||
SDL_GameController* controller;
|
||||
SDL_JoystickID jsId;
|
||||
@@ -43,6 +51,8 @@ public:
|
||||
|
||||
~SdlInputHandler();
|
||||
|
||||
void setWindow(SDL_Window* window);
|
||||
|
||||
void handleKeyEvent(SDL_KeyboardEvent* event);
|
||||
|
||||
void handleMouseButtonEvent(SDL_MouseButtonEvent* event);
|
||||
@@ -67,10 +77,22 @@ public:
|
||||
|
||||
void raiseAllKeys();
|
||||
|
||||
void notifyMouseLeave();
|
||||
|
||||
void notifyFocusLost();
|
||||
|
||||
bool isCaptureActive();
|
||||
|
||||
bool isSystemKeyCaptureActive();
|
||||
|
||||
void setCaptureActive(bool active);
|
||||
|
||||
bool isMouseInVideoRegion(int mouseX, int mouseY, int windowWidth = -1, int windowHeight = -1);
|
||||
|
||||
void updateMousePositionReport(int mouseX, int mouseY);
|
||||
|
||||
void flushMousePositionUpdate();
|
||||
|
||||
static
|
||||
QString getUnmappedGamepads();
|
||||
|
||||
@@ -80,6 +102,19 @@ private:
|
||||
|
||||
void sendGamepadState(GamepadState* state);
|
||||
|
||||
void handleAbsoluteFingerEvent(SDL_TouchFingerEvent* event);
|
||||
|
||||
void handleRelativeFingerEvent(SDL_TouchFingerEvent* event);
|
||||
|
||||
static
|
||||
Uint32 longPressTimerCallback(Uint32 interval, void* param);
|
||||
|
||||
static
|
||||
Uint32 mouseMoveTimerCallback(Uint32 interval, void* param);
|
||||
|
||||
static
|
||||
Uint32 mouseEmulationTimerCallback(Uint32 interval, void* param);
|
||||
|
||||
static
|
||||
Uint32 releaseLeftButtonTimerCallback(Uint32 interval, void* param);
|
||||
|
||||
@@ -89,31 +124,53 @@ private:
|
||||
static
|
||||
Uint32 dragTimerCallback(Uint32 interval, void* param);
|
||||
|
||||
#ifdef Q_OS_WIN32
|
||||
static
|
||||
Uint32 mouseMoveTimerCallback(Uint32 interval, void* param);
|
||||
|
||||
static
|
||||
Uint32 mouseEmulationTimerCallback(Uint32 interval, void* param);
|
||||
LRESULT CALLBACK keyboardHookProc(int nCode, WPARAM wParam, LPARAM lParam);
|
||||
#endif
|
||||
|
||||
SDL_Window* m_Window;
|
||||
bool m_MultiController;
|
||||
bool m_GamepadMouse;
|
||||
bool m_SwapMouseButtons;
|
||||
bool m_ReverseScrollDirection;
|
||||
bool m_SwapFaceButtons;
|
||||
SDL_TimerID m_MouseMoveTimer;
|
||||
SDL_atomic_t m_MouseDeltaX;
|
||||
SDL_atomic_t m_MouseDeltaY;
|
||||
|
||||
SDL_SpinLock m_MousePositionLock;
|
||||
struct {
|
||||
int x, y;
|
||||
int windowWidth, windowHeight;
|
||||
} m_MousePositionReport;
|
||||
SDL_atomic_t m_MousePositionUpdated;
|
||||
bool m_MouseWasInVideoRegion;
|
||||
bool m_PendingMouseButtonsAllUpOnVideoRegionLeave;
|
||||
|
||||
int m_GamepadMask;
|
||||
GamepadState m_GamepadState[MAX_GAMEPADS];
|
||||
QSet<short> m_KeysDown;
|
||||
bool m_FakeCaptureActive;
|
||||
QString m_OldIgnoreDevices;
|
||||
QString m_OldIgnoreDevicesExcept;
|
||||
bool m_CaptureSystemKeysEnabled;
|
||||
|
||||
SDL_TouchFingerEvent m_LastTouchDownEvent;
|
||||
SDL_TouchFingerEvent m_LastTouchUpEvent;
|
||||
SDL_TimerID m_LongPressTimer;
|
||||
int m_StreamWidth;
|
||||
int m_StreamHeight;
|
||||
bool m_AbsoluteMouseMode;
|
||||
bool m_AbsoluteTouchMode;
|
||||
Uint32 m_PendingMouseLeaveButtonUp;
|
||||
|
||||
SDL_TouchFingerEvent m_TouchDownEvent[MAX_FINGERS];
|
||||
float m_CumulativeDelta[MAX_FINGERS];
|
||||
SDL_TimerID m_LeftButtonReleaseTimer;
|
||||
SDL_TimerID m_RightButtonReleaseTimer;
|
||||
SDL_TimerID m_DragTimer;
|
||||
char m_DragButton;
|
||||
int m_NumFingersDown;
|
||||
int m_StreamWidth;
|
||||
int m_StreamHeight;
|
||||
|
||||
static const int k_ButtonMap[];
|
||||
};
|
||||
@@ -0,0 +1,448 @@
|
||||
#include "streaming/session.h"
|
||||
|
||||
#include <Limelight.h>
|
||||
#include <SDL.h>
|
||||
|
||||
#include <QGuiApplication>
|
||||
|
||||
#define VK_0 0x30
|
||||
#define VK_A 0x41
|
||||
|
||||
// These are real Windows VK_* codes
|
||||
#ifndef VK_F1
|
||||
#define VK_F1 0x70
|
||||
#define VK_F13 0x7C
|
||||
#define VK_NUMPAD0 0x60
|
||||
#endif
|
||||
|
||||
void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
|
||||
{
|
||||
short keyCode;
|
||||
char modifiers;
|
||||
|
||||
// Check for our special key combos
|
||||
if ((event->state == SDL_PRESSED) &&
|
||||
(event->keysym.mod & KMOD_CTRL) &&
|
||||
(event->keysym.mod & KMOD_ALT) &&
|
||||
(event->keysym.mod & KMOD_SHIFT)) {
|
||||
// First we test the SDLK combos for matches,
|
||||
// that way we ensure that latin keyboard users
|
||||
// can match to the key they see on their keyboards.
|
||||
// If nothing matches that, we'll then go on to
|
||||
// checking scancodes so non-latin keyboard users
|
||||
// can have working hotkeys (though possibly in
|
||||
// odd positions). We must do all SDLK tests before
|
||||
// any scancode tests to avoid issues in cases
|
||||
// where the SDLK for one shortcut collides with
|
||||
// the scancode of another.
|
||||
|
||||
// Check for quit combo (Ctrl+Alt+Shift+Q)
|
||||
if (event->keysym.sym == SDLK_q) {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Detected quit key combo (SDLK)");
|
||||
|
||||
// Push a quit event to the main loop
|
||||
SDL_Event event;
|
||||
event.type = SDL_QUIT;
|
||||
event.quit.timestamp = SDL_GetTicks();
|
||||
SDL_PushEvent(&event);
|
||||
return;
|
||||
}
|
||||
// Check for the unbind combo (Ctrl+Alt+Shift+Z) unless on EGLFS which has no window manager
|
||||
else if (event->keysym.sym == SDLK_z && QGuiApplication::platformName() != "eglfs") {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Detected mouse capture toggle combo (SDLK)");
|
||||
|
||||
// Stop handling future input
|
||||
setCaptureActive(!isCaptureActive());
|
||||
|
||||
// Force raise all keys to ensure they aren't stuck,
|
||||
// since we won't get their key up events.
|
||||
raiseAllKeys();
|
||||
return;
|
||||
}
|
||||
// Check for the mouse mode combo (Ctrl+Alt+Shift+M) unless on EGLFS which has no window manager
|
||||
else if (event->keysym.sym == SDLK_m && QGuiApplication::platformName() != "eglfs") {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Detected mouse mode toggle combo (SDLK)");
|
||||
|
||||
// Uncapture input
|
||||
setCaptureActive(false);
|
||||
|
||||
// Toggle mouse mode
|
||||
m_AbsoluteMouseMode = !m_AbsoluteMouseMode;
|
||||
|
||||
// Recapture input
|
||||
setCaptureActive(true);
|
||||
return;
|
||||
}
|
||||
else if (event->keysym.sym == SDLK_x && QGuiApplication::platformName() != "eglfs") {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Detected full-screen toggle combo (SDLK)");
|
||||
Session::s_ActiveSession->toggleFullscreen();
|
||||
|
||||
// Force raise all keys just be safe across this full-screen/windowed
|
||||
// transition just in case key events get lost.
|
||||
raiseAllKeys();
|
||||
return;
|
||||
}
|
||||
// Check for overlay combo (Ctrl+Alt+Shift+S)
|
||||
else if (event->keysym.sym == SDLK_s) {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Detected stats toggle combo (SDLK)");
|
||||
|
||||
// Toggle the stats overlay
|
||||
Session::get()->getOverlayManager().setOverlayState(Overlay::OverlayDebug,
|
||||
!Session::get()->getOverlayManager().isOverlayEnabled(Overlay::OverlayDebug));
|
||||
|
||||
// Force raise all keys just be safe across this full-screen/windowed
|
||||
// transition just in case key events get lost.
|
||||
raiseAllKeys();
|
||||
return;
|
||||
}
|
||||
// Check for mouse show combo (Ctrl+Alt+Shift+C)
|
||||
else if (event->keysym.sym == SDLK_c) {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Detected show mouse combo (SDLK)");
|
||||
|
||||
if (!SDL_GetRelativeMouseMode()) {
|
||||
SDL_ShowCursor(!SDL_ShowCursor(SDL_QUERY));
|
||||
}
|
||||
else {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Cursor can only be shown in remote desktop mouse mode");
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Check for quit combo (Ctrl+Alt+Shift+Q)
|
||||
else if (event->keysym.scancode == SDL_SCANCODE_Q) {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Detected quit key combo (scancode)");
|
||||
|
||||
// Push a quit event to the main loop
|
||||
SDL_Event event;
|
||||
event.type = SDL_QUIT;
|
||||
event.quit.timestamp = SDL_GetTicks();
|
||||
SDL_PushEvent(&event);
|
||||
return;
|
||||
}
|
||||
// Check for the unbind combo (Ctrl+Alt+Shift+Z)
|
||||
else if (event->keysym.scancode == SDL_SCANCODE_Z && QGuiApplication::platformName() != "eglfs") {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Detected mouse capture toggle combo (scancode)");
|
||||
|
||||
// Stop handling future input
|
||||
setCaptureActive(!isCaptureActive());
|
||||
|
||||
// Force raise all keys to ensure they aren't stuck,
|
||||
// since we won't get their key up events.
|
||||
raiseAllKeys();
|
||||
return;
|
||||
}
|
||||
// Check for the full-screen combo (Ctrl+Alt+Shift+X) unless on EGLFS which has no window manager
|
||||
else if (event->keysym.scancode == SDL_SCANCODE_X && QGuiApplication::platformName() != "eglfs") {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Detected full-screen toggle combo (scancode)");
|
||||
Session::s_ActiveSession->toggleFullscreen();
|
||||
|
||||
// Force raise all keys just be safe across this full-screen/windowed
|
||||
// transition just in case key events get lost.
|
||||
raiseAllKeys();
|
||||
return;
|
||||
}
|
||||
// Check for the mouse mode toggle combo (Ctrl+Alt+Shift+M) unless on EGLFS which has no window manager
|
||||
else if (event->keysym.scancode == SDL_SCANCODE_M && QGuiApplication::platformName() != "eglfs") {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Detected mouse mode toggle combo (scancode)");
|
||||
|
||||
// Uncapture input
|
||||
setCaptureActive(false);
|
||||
|
||||
// Toggle mouse mode
|
||||
m_AbsoluteMouseMode = !m_AbsoluteMouseMode;
|
||||
|
||||
// Recapture input
|
||||
setCaptureActive(true);
|
||||
return;
|
||||
}
|
||||
else if (event->keysym.scancode == SDL_SCANCODE_S) {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Detected stats toggle combo (scancode)");
|
||||
|
||||
// Toggle the stats overlay
|
||||
Session::get()->getOverlayManager().setOverlayState(Overlay::OverlayDebug,
|
||||
!Session::get()->getOverlayManager().isOverlayEnabled(Overlay::OverlayDebug));
|
||||
|
||||
// Force raise all keys just be safe across this full-screen/windowed
|
||||
// transition just in case key events get lost.
|
||||
raiseAllKeys();
|
||||
return;
|
||||
}
|
||||
// Check for mouse show combo (Ctrl+Alt+Shift+C)
|
||||
else if (event->keysym.sym == SDL_SCANCODE_C) {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Detected show mouse combo (scancode)");
|
||||
|
||||
if (!SDL_GetRelativeMouseMode()) {
|
||||
SDL_ShowCursor(!SDL_ShowCursor(SDL_QUERY));
|
||||
}
|
||||
else {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Cursor can only be shown in remote desktop mouse mode");
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (event->repeat) {
|
||||
// Ignore repeat key down events
|
||||
SDL_assert(event->state == SDL_PRESSED);
|
||||
return;
|
||||
}
|
||||
|
||||
// Set modifier flags
|
||||
modifiers = 0;
|
||||
if (event->keysym.mod & KMOD_CTRL) {
|
||||
modifiers |= MODIFIER_CTRL;
|
||||
}
|
||||
if (event->keysym.mod & KMOD_ALT) {
|
||||
modifiers |= MODIFIER_ALT;
|
||||
}
|
||||
if (event->keysym.mod & KMOD_SHIFT) {
|
||||
modifiers |= MODIFIER_SHIFT;
|
||||
}
|
||||
if (event->keysym.mod & KMOD_GUI) {
|
||||
if (isSystemKeyCaptureActive()) {
|
||||
modifiers |= MODIFIER_META;
|
||||
}
|
||||
}
|
||||
|
||||
// Set keycode. We explicitly use scancode here because GFE will try to correct
|
||||
// for AZERTY layouts on the host but it depends on receiving VK_ values matching
|
||||
// a QWERTY layout to work.
|
||||
if (event->keysym.scancode >= SDL_SCANCODE_1 && event->keysym.scancode <= SDL_SCANCODE_9) {
|
||||
// SDL defines SDL_SCANCODE_0 > SDL_SCANCODE_9, so we need to handle that manually
|
||||
keyCode = (event->keysym.scancode - SDL_SCANCODE_1) + VK_0 + 1;
|
||||
}
|
||||
else if (event->keysym.scancode >= SDL_SCANCODE_A && event->keysym.scancode <= SDL_SCANCODE_Z) {
|
||||
keyCode = (event->keysym.scancode - SDL_SCANCODE_A) + VK_A;
|
||||
}
|
||||
else if (event->keysym.scancode >= SDL_SCANCODE_F1 && event->keysym.scancode <= SDL_SCANCODE_F12) {
|
||||
keyCode = (event->keysym.scancode - SDL_SCANCODE_F1) + VK_F1;
|
||||
}
|
||||
else if (event->keysym.scancode >= SDL_SCANCODE_F13 && event->keysym.scancode <= SDL_SCANCODE_F24) {
|
||||
keyCode = (event->keysym.scancode - SDL_SCANCODE_F13) + VK_F13;
|
||||
}
|
||||
else if (event->keysym.scancode >= SDL_SCANCODE_KP_1 && event->keysym.scancode <= SDL_SCANCODE_KP_9) {
|
||||
// SDL defines SDL_SCANCODE_KP_0 > SDL_SCANCODE_KP_9, so we need to handle that manually
|
||||
keyCode = (event->keysym.scancode - SDL_SCANCODE_KP_1) + VK_NUMPAD0 + 1;
|
||||
}
|
||||
else {
|
||||
switch (event->keysym.scancode) {
|
||||
case SDL_SCANCODE_BACKSPACE:
|
||||
keyCode = 0x08;
|
||||
break;
|
||||
case SDL_SCANCODE_TAB:
|
||||
keyCode = 0x09;
|
||||
break;
|
||||
case SDL_SCANCODE_CLEAR:
|
||||
keyCode = 0x0C;
|
||||
break;
|
||||
case SDL_SCANCODE_KP_ENTER: // FIXME: Is this correct?
|
||||
case SDL_SCANCODE_RETURN:
|
||||
keyCode = 0x0D;
|
||||
break;
|
||||
case SDL_SCANCODE_PAUSE:
|
||||
keyCode = 0x13;
|
||||
break;
|
||||
case SDL_SCANCODE_CAPSLOCK:
|
||||
keyCode = 0x14;
|
||||
break;
|
||||
case SDL_SCANCODE_ESCAPE:
|
||||
keyCode = 0x1B;
|
||||
break;
|
||||
case SDL_SCANCODE_SPACE:
|
||||
keyCode = 0x20;
|
||||
break;
|
||||
case SDL_SCANCODE_PAGEUP:
|
||||
keyCode = 0x21;
|
||||
break;
|
||||
case SDL_SCANCODE_PAGEDOWN:
|
||||
keyCode = 0x22;
|
||||
break;
|
||||
case SDL_SCANCODE_END:
|
||||
keyCode = 0x23;
|
||||
break;
|
||||
case SDL_SCANCODE_HOME:
|
||||
keyCode = 0x24;
|
||||
break;
|
||||
case SDL_SCANCODE_LEFT:
|
||||
keyCode = 0x25;
|
||||
break;
|
||||
case SDL_SCANCODE_UP:
|
||||
keyCode = 0x26;
|
||||
break;
|
||||
case SDL_SCANCODE_RIGHT:
|
||||
keyCode = 0x27;
|
||||
break;
|
||||
case SDL_SCANCODE_DOWN:
|
||||
keyCode = 0x28;
|
||||
break;
|
||||
case SDL_SCANCODE_SELECT:
|
||||
keyCode = 0x29;
|
||||
break;
|
||||
case SDL_SCANCODE_EXECUTE:
|
||||
keyCode = 0x2B;
|
||||
break;
|
||||
case SDL_SCANCODE_PRINTSCREEN:
|
||||
keyCode = 0x2C;
|
||||
break;
|
||||
case SDL_SCANCODE_INSERT:
|
||||
keyCode = 0x2D;
|
||||
break;
|
||||
case SDL_SCANCODE_DELETE:
|
||||
keyCode = 0x2E;
|
||||
break;
|
||||
case SDL_SCANCODE_HELP:
|
||||
keyCode = 0x2F;
|
||||
break;
|
||||
case SDL_SCANCODE_KP_0:
|
||||
// See comment above about why we only handle SDL_SCANCODE_KP_0 here
|
||||
keyCode = VK_NUMPAD0;
|
||||
break;
|
||||
case SDL_SCANCODE_0:
|
||||
// See comment above about why we only handle SDL_SCANCODE_0 here
|
||||
keyCode = VK_0;
|
||||
break;
|
||||
case SDL_SCANCODE_KP_MULTIPLY:
|
||||
keyCode = 0x6A;
|
||||
break;
|
||||
case SDL_SCANCODE_KP_PLUS:
|
||||
keyCode = 0x6B;
|
||||
break;
|
||||
case SDL_SCANCODE_KP_COMMA:
|
||||
keyCode = 0x6C;
|
||||
break;
|
||||
case SDL_SCANCODE_KP_MINUS:
|
||||
keyCode = 0x6D;
|
||||
break;
|
||||
case SDL_SCANCODE_KP_PERIOD:
|
||||
keyCode = 0x6E;
|
||||
break;
|
||||
case SDL_SCANCODE_KP_DIVIDE:
|
||||
keyCode = 0x6F;
|
||||
break;
|
||||
case SDL_SCANCODE_NUMLOCKCLEAR:
|
||||
keyCode = 0x90;
|
||||
break;
|
||||
case SDL_SCANCODE_SCROLLLOCK:
|
||||
keyCode = 0x91;
|
||||
break;
|
||||
case SDL_SCANCODE_LSHIFT:
|
||||
keyCode = 0xA0;
|
||||
break;
|
||||
case SDL_SCANCODE_RSHIFT:
|
||||
keyCode = 0xA1;
|
||||
break;
|
||||
case SDL_SCANCODE_LCTRL:
|
||||
keyCode = 0xA2;
|
||||
break;
|
||||
case SDL_SCANCODE_RCTRL:
|
||||
keyCode = 0xA3;
|
||||
break;
|
||||
case SDL_SCANCODE_LALT:
|
||||
keyCode = 0xA4;
|
||||
break;
|
||||
case SDL_SCANCODE_RALT:
|
||||
keyCode = 0xA5;
|
||||
break;
|
||||
case SDL_SCANCODE_LGUI:
|
||||
if (!isSystemKeyCaptureActive()) {
|
||||
return;
|
||||
}
|
||||
keyCode = 0x5B;
|
||||
break;
|
||||
case SDL_SCANCODE_RGUI:
|
||||
if (!isSystemKeyCaptureActive()) {
|
||||
return;
|
||||
}
|
||||
keyCode = 0x5C;
|
||||
break;
|
||||
case SDL_SCANCODE_AC_BACK:
|
||||
keyCode = 0xA6;
|
||||
break;
|
||||
case SDL_SCANCODE_AC_FORWARD:
|
||||
keyCode = 0xA7;
|
||||
break;
|
||||
case SDL_SCANCODE_AC_REFRESH:
|
||||
keyCode = 0xA8;
|
||||
break;
|
||||
case SDL_SCANCODE_AC_STOP:
|
||||
keyCode = 0xA9;
|
||||
break;
|
||||
case SDL_SCANCODE_AC_SEARCH:
|
||||
keyCode = 0xAA;
|
||||
break;
|
||||
case SDL_SCANCODE_AC_BOOKMARKS:
|
||||
keyCode = 0xAB;
|
||||
break;
|
||||
case SDL_SCANCODE_AC_HOME:
|
||||
keyCode = 0xAC;
|
||||
break;
|
||||
case SDL_SCANCODE_SEMICOLON:
|
||||
keyCode = 0xBA;
|
||||
break;
|
||||
case SDL_SCANCODE_EQUALS:
|
||||
keyCode = 0xBB;
|
||||
break;
|
||||
case SDL_SCANCODE_COMMA:
|
||||
keyCode = 0xBC;
|
||||
break;
|
||||
case SDL_SCANCODE_MINUS:
|
||||
keyCode = 0xBD;
|
||||
break;
|
||||
case SDL_SCANCODE_PERIOD:
|
||||
keyCode = 0xBE;
|
||||
break;
|
||||
case SDL_SCANCODE_SLASH:
|
||||
keyCode = 0xBF;
|
||||
break;
|
||||
case SDL_SCANCODE_GRAVE:
|
||||
keyCode = 0xC0;
|
||||
break;
|
||||
case SDL_SCANCODE_LEFTBRACKET:
|
||||
keyCode = 0xDB;
|
||||
break;
|
||||
case SDL_SCANCODE_BACKSLASH:
|
||||
keyCode = 0xDC;
|
||||
break;
|
||||
case SDL_SCANCODE_RIGHTBRACKET:
|
||||
keyCode = 0xDD;
|
||||
break;
|
||||
case SDL_SCANCODE_APOSTROPHE:
|
||||
keyCode = 0xDE;
|
||||
break;
|
||||
case SDL_SCANCODE_NONUSBACKSLASH:
|
||||
keyCode = 0xE2;
|
||||
break;
|
||||
default:
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Unhandled button event: %d",
|
||||
event->keysym.scancode);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Track the key state so we always know which keys are down
|
||||
if (event->state == SDL_PRESSED) {
|
||||
m_KeysDown.insert(keyCode);
|
||||
}
|
||||
else {
|
||||
m_KeysDown.remove(keyCode);
|
||||
}
|
||||
|
||||
LiSendKeyboardEvent(0x8000 | keyCode,
|
||||
event->state == SDL_PRESSED ?
|
||||
KEY_ACTION_DOWN : KEY_ACTION_UP,
|
||||
modifiers);
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
#include "input.h"
|
||||
|
||||
#include <Limelight.h>
|
||||
#include <SDL.h>
|
||||
#include "streaming/streamutils.h"
|
||||
|
||||
void SdlInputHandler::handleMouseButtonEvent(SDL_MouseButtonEvent* event)
|
||||
{
|
||||
int button;
|
||||
|
||||
if (event->which == SDL_TOUCH_MOUSEID) {
|
||||
// Ignore synthetic mouse events
|
||||
return;
|
||||
}
|
||||
else if (!isCaptureActive()) {
|
||||
if (event->button == SDL_BUTTON_LEFT && event->state == SDL_RELEASED &&
|
||||
isMouseInVideoRegion(event->x, event->y)) {
|
||||
// Capture the mouse again if clicked when unbound.
|
||||
// We start capture on left button released instead of
|
||||
// pressed to avoid sending an errant mouse button released
|
||||
// event to the host when clicking into our window (since
|
||||
// the pressed event was consumed by this code).
|
||||
setCaptureActive(true);
|
||||
}
|
||||
|
||||
// Not capturing
|
||||
return;
|
||||
}
|
||||
else if (m_AbsoluteMouseMode && !isMouseInVideoRegion(event->x, event->y) && event->state == SDL_PRESSED) {
|
||||
// Ignore button presses outside the video region, but allow button releases
|
||||
return;
|
||||
}
|
||||
|
||||
switch (event->button)
|
||||
{
|
||||
case SDL_BUTTON_LEFT:
|
||||
button = BUTTON_LEFT;
|
||||
break;
|
||||
case SDL_BUTTON_MIDDLE:
|
||||
button = BUTTON_MIDDLE;
|
||||
break;
|
||||
case SDL_BUTTON_RIGHT:
|
||||
button = BUTTON_RIGHT;
|
||||
break;
|
||||
case SDL_BUTTON_X1:
|
||||
button = BUTTON_X1;
|
||||
break;
|
||||
case SDL_BUTTON_X2:
|
||||
button = BUTTON_X2;
|
||||
break;
|
||||
default:
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Unhandled button event: %d",
|
||||
event->button);
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_SwapMouseButtons) {
|
||||
if (button == BUTTON_RIGHT)
|
||||
button = BUTTON_LEFT;
|
||||
else if (button == BUTTON_LEFT)
|
||||
button = BUTTON_RIGHT;
|
||||
}
|
||||
|
||||
// Ensure any pending mouse position update has been sent before we send
|
||||
// our button event. This ensures that the cursor is in the correct location
|
||||
// when the button event is issued.
|
||||
//
|
||||
// On platforms like macOS, the mouse doesn't track when the window isn't
|
||||
// focused. When we gain focus via mouse click, we immediately get a mouse
|
||||
// move event and a mouse button event. If we don't flush here, the button
|
||||
// will probably arrive before the mouse timer issues the position update.
|
||||
flushMousePositionUpdate();
|
||||
|
||||
LiSendMouseButtonEvent(event->state == SDL_PRESSED ?
|
||||
BUTTON_ACTION_PRESS :
|
||||
BUTTON_ACTION_RELEASE,
|
||||
button);
|
||||
}
|
||||
|
||||
void SdlInputHandler::updateMousePositionReport(int mouseX, int mouseY)
|
||||
{
|
||||
int windowWidth, windowHeight;
|
||||
|
||||
// Call SDL_GetWindowSize() before entering the spinlock
|
||||
SDL_GetWindowSize(m_Window, &windowWidth, &windowHeight);
|
||||
|
||||
SDL_AtomicLock(&m_MousePositionLock);
|
||||
m_MousePositionReport.x = mouseX;
|
||||
m_MousePositionReport.y = mouseY;
|
||||
m_MousePositionReport.windowWidth = windowWidth;
|
||||
m_MousePositionReport.windowHeight = windowHeight;
|
||||
SDL_AtomicUnlock(&m_MousePositionLock);
|
||||
SDL_AtomicSet(&m_MousePositionUpdated, 1);
|
||||
}
|
||||
|
||||
void SdlInputHandler::flushMousePositionUpdate()
|
||||
{
|
||||
bool hasNewPosition = SDL_AtomicSet(&m_MousePositionUpdated, 0) != 0;
|
||||
if (hasNewPosition) {
|
||||
// If the lock is held now, the main thread is trying to update
|
||||
// the mouse position. We'll pick up the new position next time.
|
||||
if (SDL_AtomicTryLock(&m_MousePositionLock)) {
|
||||
SDL_Rect src, dst;
|
||||
bool mouseInVideoRegion;
|
||||
|
||||
src.x = src.y = 0;
|
||||
src.w = m_StreamWidth;
|
||||
src.h = m_StreamHeight;
|
||||
|
||||
dst.x = dst.y = 0;
|
||||
dst.w = m_MousePositionReport.windowWidth;
|
||||
dst.h = m_MousePositionReport.windowHeight;
|
||||
|
||||
// Use the stream and window sizes to determine the video region
|
||||
StreamUtils::scaleSourceToDestinationSurface(&src, &dst);
|
||||
|
||||
mouseInVideoRegion = isMouseInVideoRegion(m_MousePositionReport.x,
|
||||
m_MousePositionReport.y,
|
||||
m_MousePositionReport.windowWidth,
|
||||
m_MousePositionReport.windowHeight);
|
||||
|
||||
// Clamp motion to the video region
|
||||
short x = qMin(qMax(m_MousePositionReport.x - dst.x, 0), dst.w);
|
||||
short y = qMin(qMax(m_MousePositionReport.y - dst.y, 0), dst.h);
|
||||
|
||||
// Release the spinlock to unblock the main thread
|
||||
SDL_AtomicUnlock(&m_MousePositionLock);
|
||||
|
||||
// Send the mouse position update if one of the following is true:
|
||||
// a) it is in the video region now
|
||||
// b) it just left the video region (to ensure the mouse is clamped to the video boundary)
|
||||
// c) a mouse button is still down from before the cursor left the video region (to allow smooth dragging)
|
||||
Uint32 buttonState = SDL_GetMouseState(nullptr, nullptr);
|
||||
if (buttonState == 0) {
|
||||
m_PendingMouseButtonsAllUpOnVideoRegionLeave = false;
|
||||
}
|
||||
if (mouseInVideoRegion || m_MouseWasInVideoRegion || m_PendingMouseButtonsAllUpOnVideoRegionLeave) {
|
||||
LiSendMousePositionEvent(x, y, dst.w, dst.h);
|
||||
}
|
||||
|
||||
// Adjust the cursor visibility if applicable
|
||||
if (mouseInVideoRegion ^ m_MouseWasInVideoRegion) {
|
||||
// We must push an event for the main thread to process, because it's not safe
|
||||
// to directly can SDL_ShowCursor() on the arbitrary thread on which this timer
|
||||
// executes.
|
||||
SDL_Event event;
|
||||
event.type = SDL_USEREVENT;
|
||||
event.user.code = mouseInVideoRegion ? SDL_CODE_HIDE_CURSOR : SDL_CODE_SHOW_CURSOR;
|
||||
SDL_PushEvent(&event);
|
||||
|
||||
if (!mouseInVideoRegion && buttonState != 0) {
|
||||
// If we still have a button pressed on leave, wait for that to come up
|
||||
// before we stop sending mouse position events.
|
||||
m_PendingMouseButtonsAllUpOnVideoRegionLeave = true;
|
||||
}
|
||||
}
|
||||
|
||||
m_MouseWasInVideoRegion = mouseInVideoRegion;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SdlInputHandler::handleMouseMotionEvent(SDL_MouseMotionEvent* event)
|
||||
{
|
||||
if (!isCaptureActive()) {
|
||||
// Not capturing
|
||||
return;
|
||||
}
|
||||
else if (event->which == SDL_TOUCH_MOUSEID) {
|
||||
// Ignore synthetic mouse events
|
||||
return;
|
||||
}
|
||||
|
||||
// Batch until the next mouse polling window or we'll get awful
|
||||
// input lag everything except GFE 3.14 and 3.15.
|
||||
if (m_AbsoluteMouseMode) {
|
||||
updateMousePositionReport(event->x, event->y);
|
||||
}
|
||||
else {
|
||||
SDL_AtomicAdd(&m_MouseDeltaX, event->xrel);
|
||||
SDL_AtomicAdd(&m_MouseDeltaY, event->yrel);
|
||||
}
|
||||
}
|
||||
|
||||
void SdlInputHandler::handleMouseWheelEvent(SDL_MouseWheelEvent* event)
|
||||
{
|
||||
if (!isCaptureActive()) {
|
||||
// Not capturing
|
||||
return;
|
||||
}
|
||||
else if (event->which == SDL_TOUCH_MOUSEID) {
|
||||
// Ignore synthetic mouse events
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_AbsoluteMouseMode) {
|
||||
int mouseX, mouseY;
|
||||
SDL_GetMouseState(&mouseX, &mouseY);
|
||||
if (!isMouseInVideoRegion(mouseX, mouseY)) {
|
||||
// Ignore scroll events outside the video region
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (event->y != 0) {
|
||||
// Invert the scroll direction if needed
|
||||
if (m_ReverseScrollDirection) {
|
||||
event->y = -event->y;
|
||||
}
|
||||
|
||||
LiSendScrollEvent((signed char)event->y);
|
||||
}
|
||||
}
|
||||
|
||||
bool SdlInputHandler::isMouseInVideoRegion(int mouseX, int mouseY, int windowWidth, int windowHeight)
|
||||
{
|
||||
SDL_Rect src, dst;
|
||||
|
||||
if (windowWidth < 0 || windowHeight < 0) {
|
||||
SDL_GetWindowSize(m_Window, &windowWidth, &windowHeight);
|
||||
}
|
||||
|
||||
src.x = src.y = 0;
|
||||
src.w = m_StreamWidth;
|
||||
src.h = m_StreamHeight;
|
||||
|
||||
dst.x = dst.y = 0;
|
||||
dst.w = windowWidth;
|
||||
dst.h = windowHeight;
|
||||
|
||||
// Use the stream and window sizes to determine the video region
|
||||
StreamUtils::scaleSourceToDestinationSurface(&src, &dst);
|
||||
|
||||
return (mouseX >= dst.x && mouseX <= dst.x + dst.w) &&
|
||||
(mouseY >= dst.y && mouseY <= dst.y + dst.h);
|
||||
}
|
||||
|
||||
Uint32 SdlInputHandler::mouseMoveTimerCallback(Uint32 interval, void *param)
|
||||
{
|
||||
auto me = reinterpret_cast<SdlInputHandler*>(param);
|
||||
|
||||
short deltaX = (short)SDL_AtomicSet(&me->m_MouseDeltaX, 0);
|
||||
short deltaY = (short)SDL_AtomicSet(&me->m_MouseDeltaY, 0);
|
||||
|
||||
if (deltaX != 0 || deltaY != 0) {
|
||||
LiSendMouseMoveEvent(deltaX, deltaY);
|
||||
}
|
||||
|
||||
// Send mouse position updates if applicable
|
||||
me->flushMousePositionUpdate();
|
||||
|
||||
#ifdef Q_OS_WIN32
|
||||
// See comment in SdlInputHandler::notifyMouseLeave()
|
||||
if (me->m_AbsoluteMouseMode && me->m_PendingMouseLeaveButtonUp != 0 && me->isCaptureActive()) {
|
||||
int mouseX, mouseY;
|
||||
int windowX, windowY;
|
||||
Uint32 mouseState = SDL_GetGlobalMouseState(&mouseX, &mouseY);
|
||||
SDL_GetWindowPosition(me->m_Window, &windowX, &windowY);
|
||||
|
||||
// If the button is now up, send the synthetic mouse up event
|
||||
if ((mouseState & SDL_BUTTON(me->m_PendingMouseLeaveButtonUp)) == 0) {
|
||||
SDL_Event event;
|
||||
|
||||
event.button.type = SDL_MOUSEBUTTONUP;
|
||||
event.button.timestamp = SDL_GetTicks();
|
||||
event.button.windowID = SDL_GetWindowID(me->m_Window);
|
||||
event.button.which = 0;
|
||||
event.button.button = me->m_PendingMouseLeaveButtonUp;
|
||||
event.button.state = SDL_RELEASED;
|
||||
event.button.clicks = 1;
|
||||
event.button.x = mouseX - windowX;
|
||||
event.button.y = mouseY - windowY;
|
||||
SDL_PushEvent(&event);
|
||||
|
||||
me->m_PendingMouseLeaveButtonUp = 0;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
return interval;
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
#include "input.h"
|
||||
|
||||
#include <Limelight.h>
|
||||
#include <SDL.h>
|
||||
|
||||
#include <QtMath>
|
||||
|
||||
// How long the mouse button will be pressed for a tap to click gesture
|
||||
#define TAP_BUTTON_RELEASE_DELAY 100
|
||||
|
||||
// How long the fingers must be stationary to start a drag
|
||||
#define DRAG_ACTIVATION_DELAY 650
|
||||
|
||||
// How far the finger can move before it cancels a drag or tap
|
||||
#define DEAD_ZONE_DELTA 0.01f
|
||||
|
||||
Uint32 SdlInputHandler::releaseLeftButtonTimerCallback(Uint32, void*)
|
||||
{
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
|
||||
return 0;
|
||||
}
|
||||
|
||||
Uint32 SdlInputHandler::releaseRightButtonTimerCallback(Uint32, void*)
|
||||
{
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_RIGHT);
|
||||
return 0;
|
||||
}
|
||||
|
||||
Uint32 SdlInputHandler::dragTimerCallback(Uint32, void *param)
|
||||
{
|
||||
auto me = reinterpret_cast<SdlInputHandler*>(param);
|
||||
|
||||
// Check how many fingers are down now to decide
|
||||
// which button to hold down
|
||||
if (me->m_NumFingersDown == 2) {
|
||||
me->m_DragButton = BUTTON_RIGHT;
|
||||
}
|
||||
else if (me->m_NumFingersDown == 1) {
|
||||
me->m_DragButton = BUTTON_LEFT;
|
||||
}
|
||||
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, me->m_DragButton);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void SdlInputHandler::handleRelativeFingerEvent(SDL_TouchFingerEvent* event)
|
||||
{
|
||||
int fingerIndex = -1;
|
||||
|
||||
// Observations on Windows 10: x and y appear to be relative to 0,0 of the window client area.
|
||||
// Although SDL documentation states they are 0.0 - 1.0 float values, they can actually be higher
|
||||
// or lower than those values as touch events continue for touches started within the client area that
|
||||
// leave the client area during a drag motion.
|
||||
// dx and dy are deltas from the last touch event, not the first touch down.
|
||||
|
||||
// Determine the index of this finger using our list
|
||||
// of fingers that are currently active on screen.
|
||||
// This is also required to handle finger up which
|
||||
// where the finger will not be in SDL_GetTouchFinger()
|
||||
// anymore.
|
||||
if (event->type != SDL_FINGERDOWN) {
|
||||
for (int i = 0; i < MAX_FINGERS; i++) {
|
||||
if (event->fingerId == m_TouchDownEvent[i].fingerId) {
|
||||
fingerIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Resolve the new finger by determining the ID of each
|
||||
// finger on the display.
|
||||
int numTouchFingers = SDL_GetNumTouchFingers(event->touchId);
|
||||
for (int i = 0; i < numTouchFingers; i++) {
|
||||
SDL_Finger* finger = SDL_GetTouchFinger(event->touchId, i);
|
||||
SDL_assert(finger != nullptr);
|
||||
if (finger != nullptr) {
|
||||
if (finger->id == event->fingerId) {
|
||||
fingerIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (fingerIndex < 0 || fingerIndex >= MAX_FINGERS) {
|
||||
// Too many fingers
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle cursor motion based on the position of the
|
||||
// primary finger on screen
|
||||
if (fingerIndex == 0) {
|
||||
// The event x and y values are relative to our window width
|
||||
// and height. However, we want to scale them to be relative
|
||||
// to the host resolution. Fortunately this is easy since we
|
||||
// already have normalized values. We'll just multiply them
|
||||
// by the stream dimensions to get real X and Y values rather
|
||||
// than the client window dimensions.
|
||||
short deltaX = static_cast<short>(event->dx * m_StreamWidth);
|
||||
short deltaY = static_cast<short>(event->dy * m_StreamHeight);
|
||||
if (deltaX != 0 || deltaY != 0) {
|
||||
LiSendMouseMoveEvent(deltaX, deltaY);
|
||||
}
|
||||
}
|
||||
|
||||
// Start a drag timer when primary or secondary
|
||||
// fingers go down
|
||||
if (event->type == SDL_FINGERDOWN &&
|
||||
(fingerIndex == 0 || fingerIndex == 1)) {
|
||||
SDL_RemoveTimer(m_DragTimer);
|
||||
m_DragTimer = SDL_AddTimer(DRAG_ACTIVATION_DELAY,
|
||||
dragTimerCallback,
|
||||
this);
|
||||
}
|
||||
|
||||
if (event->type == SDL_FINGERMOTION) {
|
||||
// If it's outside the deadzone delta, cancel drags and taps
|
||||
if (qSqrt(qPow(event->x - m_TouchDownEvent[fingerIndex].x, 2) +
|
||||
qPow(event->y - m_TouchDownEvent[fingerIndex].y, 2)) > DEAD_ZONE_DELTA) {
|
||||
SDL_RemoveTimer(m_DragTimer);
|
||||
m_DragTimer = 0;
|
||||
|
||||
// This effectively cancels the tap logic below
|
||||
m_TouchDownEvent[fingerIndex].timestamp = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (event->type == SDL_FINGERUP) {
|
||||
// Cancel the drag timer on finger up
|
||||
SDL_RemoveTimer(m_DragTimer);
|
||||
m_DragTimer = 0;
|
||||
|
||||
// Release any drag
|
||||
if (m_DragButton != 0) {
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, m_DragButton);
|
||||
m_DragButton = 0;
|
||||
}
|
||||
// 2 finger tap
|
||||
else if (event->timestamp - m_TouchDownEvent[1].timestamp < 250) {
|
||||
// Zero timestamp of the primary finger to ensure we won't
|
||||
// generate a left click if the primary finger comes up soon.
|
||||
m_TouchDownEvent[0].timestamp = 0;
|
||||
|
||||
// Press down the right mouse button
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_RIGHT);
|
||||
|
||||
// Queue a timer to release it in 100 ms
|
||||
SDL_RemoveTimer(m_RightButtonReleaseTimer);
|
||||
m_RightButtonReleaseTimer = SDL_AddTimer(TAP_BUTTON_RELEASE_DELAY,
|
||||
releaseRightButtonTimerCallback,
|
||||
nullptr);
|
||||
}
|
||||
// 1 finger tap
|
||||
else if (event->timestamp - m_TouchDownEvent[0].timestamp < 250) {
|
||||
// Press down the left mouse button
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT);
|
||||
|
||||
// Queue a timer to release it in 100 ms
|
||||
SDL_RemoveTimer(m_LeftButtonReleaseTimer);
|
||||
m_LeftButtonReleaseTimer = SDL_AddTimer(TAP_BUTTON_RELEASE_DELAY,
|
||||
releaseLeftButtonTimerCallback,
|
||||
nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
m_NumFingersDown = SDL_GetNumTouchFingers(event->touchId);
|
||||
|
||||
if (event->type == SDL_FINGERDOWN) {
|
||||
m_TouchDownEvent[fingerIndex] = *event;
|
||||
}
|
||||
else if (event->type == SDL_FINGERUP) {
|
||||
m_TouchDownEvent[fingerIndex] = {};
|
||||
}
|
||||
}
|
||||
+420
-169
@@ -22,6 +22,8 @@
|
||||
#define ICON_SIZE 64
|
||||
#endif
|
||||
|
||||
#define SDL_CODE_FLUSH_WINDOW_EVENT_BARRIER 100
|
||||
|
||||
#include <openssl/rand.h>
|
||||
|
||||
#include <QtEndian>
|
||||
@@ -30,6 +32,18 @@
|
||||
#include <QSvgRenderer>
|
||||
#include <QPainter>
|
||||
#include <QImage>
|
||||
#include <QGuiApplication>
|
||||
#include <QCursor>
|
||||
|
||||
#define CONN_TEST_SERVER "qt.conntest.moonlight-stream.org"
|
||||
|
||||
// Running the connection process asynchronously seems to reliably
|
||||
// cause a crash in QSGRenderThread on Wayland and strange crashes
|
||||
// elsewhere. Until these are figured out, avoid the async connect
|
||||
// thread on Linux and BSDs.
|
||||
#if !defined(Q_OS_UNIX) || defined(Q_OS_DARWIN)
|
||||
#define USE_ASYNC_CONNECT_THREAD 1
|
||||
#endif
|
||||
|
||||
CONNECTION_LISTENER_CALLBACKS Session::k_ConnCallbacks = {
|
||||
Session::clStageStarting,
|
||||
@@ -37,8 +51,6 @@ CONNECTION_LISTENER_CALLBACKS Session::k_ConnCallbacks = {
|
||||
Session::clStageFailed,
|
||||
nullptr,
|
||||
Session::clConnectionTerminated,
|
||||
nullptr,
|
||||
nullptr,
|
||||
Session::clLogMessage,
|
||||
Session::clRumble,
|
||||
Session::clConnectionStatusUpdate
|
||||
@@ -53,28 +65,69 @@ void Session::clStageStarting(int stage)
|
||||
// which happens to be the main thread, so it's cool to interact
|
||||
// with the GUI in these callbacks.
|
||||
emit s_ActiveSession->stageStarting(QString::fromLocal8Bit(LiGetStageName(stage)));
|
||||
|
||||
#ifndef USE_ASYNC_CONNECT_THREAD
|
||||
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
|
||||
QCoreApplication::sendPostedEvents();
|
||||
#endif
|
||||
}
|
||||
|
||||
void Session::clStageFailed(int stage, long errorCode)
|
||||
void Session::clStageFailed(int stage, int errorCode)
|
||||
{
|
||||
// We know this is called on the same thread as LiStartConnection()
|
||||
// which happens to be the main thread, so it's cool to interact
|
||||
// with the GUI in these callbacks.
|
||||
emit s_ActiveSession->stageFailed(QString::fromLocal8Bit(LiGetStageName(stage)), errorCode);
|
||||
// Perform the port test now, while we're on the async connection thread and not blocking the UI.
|
||||
unsigned int portFlags = LiGetPortFlagsFromStage(stage);
|
||||
s_ActiveSession->m_PortTestResults = LiTestClientConnectivity(CONN_TEST_SERVER, 443, portFlags);
|
||||
|
||||
char failingPorts[128];
|
||||
LiStringifyPortFlags(portFlags, ", ", failingPorts, sizeof(failingPorts));
|
||||
emit s_ActiveSession->stageFailed(QString::fromLocal8Bit(LiGetStageName(stage)), errorCode, QString(failingPorts));
|
||||
|
||||
#ifndef USE_ASYNC_CONNECT_THREAD
|
||||
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
|
||||
QCoreApplication::sendPostedEvents();
|
||||
#endif
|
||||
}
|
||||
|
||||
void Session::clConnectionTerminated(long errorCode)
|
||||
void Session::clConnectionTerminated(int errorCode)
|
||||
{
|
||||
unsigned int portFlags = LiGetPortFlagsFromTerminationErrorCode(errorCode);
|
||||
s_ActiveSession->m_PortTestResults = LiTestClientConnectivity(CONN_TEST_SERVER, 443, portFlags);
|
||||
|
||||
// Display the termination dialog if this was not intended
|
||||
if (errorCode != 0) {
|
||||
switch (errorCode) {
|
||||
case ML_ERROR_GRACEFUL_TERMINATION:
|
||||
break;
|
||||
|
||||
case ML_ERROR_NO_VIDEO_TRAFFIC:
|
||||
s_ActiveSession->m_UnexpectedTermination = true;
|
||||
emit s_ActiveSession->displayLaunchError("Connection terminated");
|
||||
|
||||
char ports[128];
|
||||
SDL_assert(portFlags != 0);
|
||||
LiStringifyPortFlags(portFlags, ", ", ports, sizeof(ports));
|
||||
emit s_ActiveSession->displayLaunchError(tr("No video received from host.") + "\n\n"+
|
||||
tr("Check your firewall and port forwarding rules for port(s): %1").arg(ports));
|
||||
break;
|
||||
|
||||
case ML_ERROR_NO_VIDEO_FRAME:
|
||||
s_ActiveSession->m_UnexpectedTermination = true;
|
||||
emit s_ActiveSession->displayLaunchError(tr("Your network connection isn't performing well. Reduce your video bitrate setting or try a faster connection."));
|
||||
break;
|
||||
|
||||
case ML_ERROR_UNEXPECTED_EARLY_TERMINATION:
|
||||
s_ActiveSession->m_UnexpectedTermination = true;
|
||||
emit s_ActiveSession->displayLaunchError(tr("Something went wrong on your host PC when starting the stream.") + "\n\n" +
|
||||
tr("Make sure you don't have any DRM-protected content open on your host PC. You can also try restarting your host PC.") + "\n\n" +
|
||||
tr("If the issue persists, try reinstalling your GPU drivers and GeForce Experience."));
|
||||
break;
|
||||
|
||||
default:
|
||||
s_ActiveSession->m_UnexpectedTermination = true;
|
||||
emit s_ActiveSession->displayLaunchError(tr("Connection terminated"));
|
||||
break;
|
||||
}
|
||||
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Connection terminated: %ld",
|
||||
"Connection terminated: %d",
|
||||
errorCode);
|
||||
|
||||
// Push a quit event to the main loop
|
||||
@@ -250,6 +303,25 @@ int Session::drSubmitDecodeUnit(PDECODE_UNIT du)
|
||||
}
|
||||
}
|
||||
|
||||
void Session::getDecoderInfo(SDL_Window* window,
|
||||
bool& isHardwareAccelerated, bool& isFullScreenOnly, QSize& maxResolution)
|
||||
{
|
||||
IVideoDecoder* decoder;
|
||||
|
||||
if (!chooseDecoder(StreamingPreferences::VDS_AUTO,
|
||||
window, VIDEO_FORMAT_H264, 1920, 1080, 60,
|
||||
true, false, true, decoder)) {
|
||||
isHardwareAccelerated = isFullScreenOnly = false;
|
||||
return;
|
||||
}
|
||||
|
||||
isHardwareAccelerated = decoder->isHardwareAccelerated();
|
||||
isFullScreenOnly = decoder->isAlwaysFullScreen();
|
||||
maxResolution = decoder->getDecoderMaxResolution();
|
||||
|
||||
delete decoder;
|
||||
}
|
||||
|
||||
bool Session::isHardwareDecodeAvailable(SDL_Window* window,
|
||||
StreamingPreferences::VideoDecoderSelection vds,
|
||||
int videoFormat, int width, int height, int frameRate)
|
||||
@@ -300,6 +372,7 @@ Session::Session(NvComputer* computer, NvApp& app, StreamingPreferences *prefere
|
||||
m_DecoderLock(0),
|
||||
m_NeedsIdr(false),
|
||||
m_AudioDisabled(false),
|
||||
m_AudioMuted(false),
|
||||
m_DisplayOriginX(0),
|
||||
m_DisplayOriginY(0),
|
||||
m_PendingWindowedTransition(false),
|
||||
@@ -307,6 +380,9 @@ Session::Session(NvComputer* computer, NvApp& app, StreamingPreferences *prefere
|
||||
m_InputHandler(nullptr),
|
||||
m_InputHandlerLock(0),
|
||||
m_MouseEmulationRefCount(0),
|
||||
m_FlushingWindowEventsRef(0),
|
||||
m_AsyncConnectionSuccess(false),
|
||||
m_PortTestResults(0),
|
||||
m_OpusDecoder(nullptr),
|
||||
m_AudioRenderer(nullptr),
|
||||
m_AudioSampleCount(0),
|
||||
@@ -334,13 +410,21 @@ bool Session::initialize()
|
||||
}
|
||||
|
||||
// Create a hidden window to use for decoder initialization tests
|
||||
SDL_Window* testWindow = SDL_CreateWindow("", 0, 0, 1280, 720, SDL_WINDOW_HIDDEN);
|
||||
SDL_Window* testWindow = SDL_CreateWindow("", 0, 0, 1280, 720,
|
||||
SDL_WINDOW_HIDDEN | StreamUtils::getPlatformWindowFlags());
|
||||
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 false;
|
||||
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 false;
|
||||
}
|
||||
}
|
||||
|
||||
qInfo() << "Server GPU:" << m_Computer->gpuModel;
|
||||
@@ -375,6 +459,9 @@ bool Session::initialize()
|
||||
case StreamingPreferences::AC_51_SURROUND:
|
||||
m_StreamConfig.audioConfiguration = AUDIO_CONFIGURATION_51_SURROUND;
|
||||
break;
|
||||
case StreamingPreferences::AC_71_SURROUND:
|
||||
m_StreamConfig.audioConfiguration = AUDIO_CONFIGURATION_71_SURROUND;
|
||||
break;
|
||||
}
|
||||
|
||||
LiInitializeAudioCallbacks(&m_AudioCallbacks);
|
||||
@@ -384,8 +471,11 @@ bool Session::initialize()
|
||||
m_AudioCallbacks.capabilities = getAudioRendererCapabilities(m_StreamConfig.audioConfiguration);
|
||||
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Audio configuration: %d",
|
||||
m_StreamConfig.audioConfiguration);
|
||||
"Audio channel count: %d",
|
||||
CHANNEL_COUNT_FROM_AUDIO_CONFIGURATION(m_StreamConfig.audioConfiguration));
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Audio channel mask: %X",
|
||||
CHANNEL_MASK_FROM_AUDIO_CONFIGURATION(m_StreamConfig.audioConfiguration));
|
||||
|
||||
switch (m_Preferences->videoCodecConfig)
|
||||
{
|
||||
@@ -441,6 +531,26 @@ bool Session::initialize()
|
||||
break;
|
||||
}
|
||||
|
||||
#ifdef Q_OS_DARWIN
|
||||
// macOS behaves as if we're capturing system keys when
|
||||
// we enter "real" full-screen mode, so use standard
|
||||
// full-screen as our full-screen flag when capturing keys.
|
||||
if (m_Preferences->captureSysKeys) {
|
||||
m_FullScreenFlag = SDL_WINDOW_FULLSCREEN;
|
||||
}
|
||||
#endif
|
||||
|
||||
#if !SDL_VERSION_ATLEAST(2, 0, 11)
|
||||
// HACK: Using a full-screen window breaks mouse capture on the Pi's LXDE
|
||||
// GUI environment. Force the session to use windowed mode (which won't
|
||||
// really matter anyway because the MMAL renderer always draws full-screen).
|
||||
if (qgetenv("DESKTOP_SESSION") == "LXDE-pi") {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Forcing windowed mode on LXDE-Pi");
|
||||
m_FullScreenFlag = 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
// Check for validation errors/warnings and emit
|
||||
// signals for them, if appropriate
|
||||
bool ret = validateLaunch(testWindow);
|
||||
@@ -474,6 +584,7 @@ void Session::emitLaunchWarning(QString text)
|
||||
// Pump the UI loop while we wait
|
||||
SDL_Delay(5);
|
||||
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
|
||||
QCoreApplication::sendPostedEvents();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -481,15 +592,25 @@ bool Session::validateLaunch(SDL_Window* testWindow)
|
||||
{
|
||||
QStringList warningList;
|
||||
|
||||
if (m_Preferences->absoluteMouseMode && !m_App.isAppCollectorGame) {
|
||||
emitLaunchWarning(tr("Your selection to enable remote desktop mouse mode may cause problems in games."));
|
||||
}
|
||||
|
||||
if (m_Preferences->videoDecoderSelection == StreamingPreferences::VDS_FORCE_SOFTWARE) {
|
||||
emitLaunchWarning("Your settings selection to force software decoding may cause poor streaming performance.");
|
||||
if (m_Preferences->videoCodecConfig == StreamingPreferences::VCC_FORCE_HEVC_HDR) {
|
||||
emitLaunchWarning(tr("HDR is not supported with software decoding."));
|
||||
m_StreamConfig.enableHdr = false;
|
||||
}
|
||||
else {
|
||||
emitLaunchWarning(tr("Your settings selection to force software decoding may cause poor streaming performance."));
|
||||
}
|
||||
}
|
||||
|
||||
if (m_Preferences->unsupportedFps && m_StreamConfig.fps > 60) {
|
||||
emitLaunchWarning("Using unsupported FPS options may cause stuttering or lag.");
|
||||
emitLaunchWarning(tr("Using unsupported FPS options may cause stuttering or lag."));
|
||||
|
||||
if (m_Preferences->enableVsync) {
|
||||
emitLaunchWarning("V-sync will be disabled when streaming at a higher frame rate than the display.");
|
||||
emitLaunchWarning(tr("V-sync will be disabled when streaming at a higher frame rate than the display."));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -506,18 +627,18 @@ bool Session::validateLaunch(SDL_Window* testWindow)
|
||||
m_StreamConfig.height,
|
||||
m_StreamConfig.fps)) {
|
||||
if (hevcForced) {
|
||||
emitLaunchWarning("Using software decoding due to your selection to force HEVC without GPU support. This may cause poor streaming performance.");
|
||||
emitLaunchWarning(tr("Using software decoding due to your selection to force HEVC without GPU support. This may cause poor streaming performance."));
|
||||
}
|
||||
else {
|
||||
emitLaunchWarning("This PC's GPU doesn't support HEVC decoding.");
|
||||
emitLaunchWarning(tr("This PC's GPU doesn't support HEVC decoding."));
|
||||
m_StreamConfig.supportsHevc = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (hevcForced) {
|
||||
if (m_Computer->maxLumaPixelsHEVC == 0) {
|
||||
emitLaunchWarning("Your host PC GPU doesn't support HEVC. "
|
||||
"A GeForce GTX 900-series (Maxwell) or later GPU is required for HEVC streaming.");
|
||||
emitLaunchWarning(tr("Your host PC GPU doesn't support HEVC. "
|
||||
"A GeForce GTX 900-series (Maxwell) or later GPU is required for HEVC streaming."));
|
||||
|
||||
// Moonlight-common-c will handle this case already, but we want
|
||||
// to set this explicitly here so we can do our hardware acceleration
|
||||
@@ -533,12 +654,12 @@ bool Session::validateLaunch(SDL_Window* testWindow)
|
||||
|
||||
// Check that the app supports HDR
|
||||
if (!m_App.hdrSupported) {
|
||||
emitLaunchWarning(m_App.name + " doesn't support HDR10.");
|
||||
emitLaunchWarning(tr("%1 doesn't support HDR10.").arg(m_App.name));
|
||||
}
|
||||
// Check that the server GPU supports HDR
|
||||
else if (!(m_Computer->serverCodecModeSupport & 0x200)) {
|
||||
emitLaunchWarning("Your host PC GPU doesn't support HDR streaming. "
|
||||
"A GeForce GTX 1000-series (Pascal) or later GPU is required for HDR streaming.");
|
||||
emitLaunchWarning(tr("Your host PC GPU doesn't support HDR streaming. "
|
||||
"A GeForce GTX 1000-series (Pascal) or later GPU is required for HDR streaming."));
|
||||
}
|
||||
else if (!isHardwareDecodeAvailable(testWindow,
|
||||
m_Preferences->videoDecoderSelection,
|
||||
@@ -546,7 +667,7 @@ bool Session::validateLaunch(SDL_Window* testWindow)
|
||||
m_StreamConfig.width,
|
||||
m_StreamConfig.height,
|
||||
m_StreamConfig.fps)) {
|
||||
emitLaunchWarning("This PC's GPU doesn't support HEVC Main10 decoding for HDR streaming.");
|
||||
emitLaunchWarning(tr("This PC's GPU doesn't support HEVC Main10 decoding for HDR streaming."));
|
||||
}
|
||||
else {
|
||||
// TODO: Also validate display capabilites
|
||||
@@ -559,7 +680,7 @@ bool Session::validateLaunch(SDL_Window* testWindow)
|
||||
if (m_StreamConfig.width >= 3840) {
|
||||
// Only allow 4K on GFE 3.x+
|
||||
if (m_Computer->gfeVersion.isEmpty() || m_Computer->gfeVersion.startsWith("2.")) {
|
||||
emitLaunchWarning("GeForce Experience 3.0 or higher is required for 4K streaming.");
|
||||
emitLaunchWarning(tr("GeForce Experience 3.0 or higher is required for 4K streaming."));
|
||||
|
||||
m_StreamConfig.width = 1920;
|
||||
m_StreamConfig.height = 1080;
|
||||
@@ -569,24 +690,38 @@ bool Session::validateLaunch(SDL_Window* testWindow)
|
||||
// Test if audio works at the specified audio configuration
|
||||
bool audioTestPassed = testAudio(m_StreamConfig.audioConfiguration);
|
||||
|
||||
// Gracefully degrade to stereo if 5.1 doesn't work
|
||||
if (!audioTestPassed && m_StreamConfig.audioConfiguration == AUDIO_CONFIGURATION_51_SURROUND) {
|
||||
// Gracefully degrade to stereo if surround sound doesn't work
|
||||
if (!audioTestPassed && CHANNEL_COUNT_FROM_AUDIO_CONFIGURATION(m_StreamConfig.audioConfiguration) > 2) {
|
||||
audioTestPassed = testAudio(AUDIO_CONFIGURATION_STEREO);
|
||||
if (audioTestPassed) {
|
||||
m_StreamConfig.audioConfiguration = AUDIO_CONFIGURATION_STEREO;
|
||||
emitLaunchWarning("5.1 surround sound is not supported by the current audio device.");
|
||||
emitLaunchWarning(tr("Your selected surround sound setting is not supported by the current audio device."));
|
||||
}
|
||||
}
|
||||
|
||||
// If nothing worked, warn the user that audio will not work
|
||||
m_AudioDisabled = !audioTestPassed;
|
||||
if (m_AudioDisabled) {
|
||||
emitLaunchWarning("Failed to open audio device. Audio will be unavailable during this session.");
|
||||
emitLaunchWarning(tr("Failed to open audio device. Audio will be unavailable during this session."));
|
||||
}
|
||||
|
||||
// Check for unmapped gamepads
|
||||
if (!SdlInputHandler::getUnmappedGamepads().isEmpty()) {
|
||||
emitLaunchWarning("An attached gamepad has no mapping and won't be usable. Visit the Moonlight help to resolve this.");
|
||||
emitLaunchWarning(tr("An attached gamepad has no mapping and won't be usable. Visit the Moonlight help to resolve this."));
|
||||
}
|
||||
|
||||
// NVENC will fail to initialize when using dimensions over 4096 and H.264.
|
||||
if (m_StreamConfig.width > 4096 || m_StreamConfig.height > 4096) {
|
||||
// Pascal added support for 8K HEVC encoding support. Maxwell 2 could encode HEVC but only up to 4K.
|
||||
// We can't directly identify Pascal, but we can look for HEVC Main10 which was added in the same generation.
|
||||
if (m_Computer->maxLumaPixelsHEVC == 0 || !(m_Computer->serverCodecModeSupport & 0x200)) {
|
||||
emit displayLaunchError(tr("Your host PC's GPU doesn't support streaming video resolutions over 4K."));
|
||||
return false;
|
||||
}
|
||||
else if (!m_StreamConfig.supportsHevc) {
|
||||
emit displayLaunchError(tr("Video resolutions over 4K are only supported by the HEVC codec."));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (m_Preferences->videoDecoderSelection == StreamingPreferences::VDS_FORCE_HARDWARE &&
|
||||
@@ -598,10 +733,10 @@ bool Session::validateLaunch(SDL_Window* testWindow)
|
||||
m_StreamConfig.height,
|
||||
m_StreamConfig.fps)) {
|
||||
if (m_Preferences->videoCodecConfig == StreamingPreferences::VCC_AUTO) {
|
||||
emit displayLaunchError("Your selection to force hardware decoding cannot be satisfied due to missing hardware decoding support on this PC's GPU.");
|
||||
emit displayLaunchError(tr("Your selection to force hardware decoding cannot be satisfied due to missing hardware decoding support on this PC's GPU."));
|
||||
}
|
||||
else {
|
||||
emit displayLaunchError("Your codec selection and force hardware decoding setting are not compatible. This PC's GPU lacks support for decoding your chosen codec.");
|
||||
emit displayLaunchError(tr("Your codec selection and force hardware decoding setting are not compatible. This PC's GPU lacks support for decoding your chosen codec."));
|
||||
}
|
||||
|
||||
// Fail the launch, because we won't manage to get a decoder for the actual stream
|
||||
@@ -638,7 +773,7 @@ private:
|
||||
emit m_Session->quitStarting();
|
||||
}
|
||||
else {
|
||||
emit m_Session->sessionFinished();
|
||||
emit m_Session->sessionFinished(m_Session->m_PortTestResults);
|
||||
}
|
||||
|
||||
// Finish cleanup of the connection state
|
||||
@@ -656,7 +791,7 @@ private:
|
||||
}
|
||||
|
||||
// Session is finished now
|
||||
emit m_Session->sessionFinished();
|
||||
emit m_Session->sessionFinished(m_Session->m_PortTestResults);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -809,10 +944,20 @@ void Session::toggleFullscreen()
|
||||
bool fullScreen = !(SDL_GetWindowFlags(m_Window) & m_FullScreenFlag);
|
||||
|
||||
if (fullScreen) {
|
||||
if ((m_FullScreenFlag == SDL_WINDOW_FULLSCREEN || m_Preferences->captureSysKeys) && m_InputHandler->isCaptureActive()) {
|
||||
// Confine the cursor to the window if we're capturing input while transitioning to full screen.
|
||||
// We also need to grab if we're capturing system keys, because SDL requires window grab to
|
||||
// capture the keyboard on X11.
|
||||
SDL_SetWindowGrab(m_Window, SDL_TRUE);
|
||||
}
|
||||
|
||||
SDL_SetWindowResizable(m_Window, SDL_FALSE);
|
||||
SDL_SetWindowFullscreen(m_Window, m_FullScreenFlag);
|
||||
}
|
||||
else {
|
||||
// Unconfine the cursor
|
||||
SDL_SetWindowGrab(m_Window, SDL_FALSE);
|
||||
|
||||
SDL_SetWindowFullscreen(m_Window, 0);
|
||||
SDL_SetWindowResizable(m_Window, SDL_TRUE);
|
||||
|
||||
@@ -837,39 +982,37 @@ void Session::notifyMouseEmulationMode(bool enabled)
|
||||
}
|
||||
}
|
||||
|
||||
void Session::exec(int displayOriginX, int displayOriginY)
|
||||
class AsyncConnectionStartThread : public QThread
|
||||
{
|
||||
m_DisplayOriginX = displayOriginX;
|
||||
m_DisplayOriginY = displayOriginY;
|
||||
public:
|
||||
AsyncConnectionStartThread(Session* session) :
|
||||
QThread(nullptr),
|
||||
m_Session(session) {}
|
||||
|
||||
// Complete initialization in this deferred context to avoid
|
||||
// calling expensive functions in the constructor (during the
|
||||
// process of loading the StreamSegue).
|
||||
if (!initialize()) {
|
||||
emit sessionFinished();
|
||||
return;
|
||||
void run() override
|
||||
{
|
||||
m_Session->m_AsyncConnectionSuccess = m_Session->startConnectionAsync();
|
||||
}
|
||||
|
||||
Session* m_Session;
|
||||
};
|
||||
|
||||
// Called in a non-main thread
|
||||
bool Session::startConnectionAsync()
|
||||
{
|
||||
// Wait 1.5 seconds before connecting to let the user
|
||||
// have time to read any messages present on the segue
|
||||
#ifdef USE_ASYNC_CONNECT_THREAD
|
||||
SDL_Delay(1500);
|
||||
#else
|
||||
uint32_t start = SDL_GetTicks();
|
||||
while (!SDL_TICKS_PASSED(SDL_GetTicks(), start + 1500)) {
|
||||
// Pump the UI loop while we wait
|
||||
// Pump the UI loop while we wait since we're not async
|
||||
SDL_Delay(5);
|
||||
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
|
||||
QCoreApplication::sendPostedEvents();
|
||||
}
|
||||
|
||||
// Wait for any old session to finish cleanup
|
||||
s_ActiveSessionSemaphore.acquire();
|
||||
|
||||
// We're now active
|
||||
s_ActiveSession = this;
|
||||
|
||||
// Initialize the gamepad code with our preferences
|
||||
StreamingPreferences prefs;
|
||||
m_InputHandler = new SdlInputHandler(prefs, m_Computer,
|
||||
m_StreamConfig.width,
|
||||
m_StreamConfig.height);
|
||||
#endif
|
||||
|
||||
// The UI should have ensured the old game was already quit
|
||||
// if we decide to stream a different game.
|
||||
@@ -886,7 +1029,7 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Found host supported resolution: %dx%d",
|
||||
mode.width, mode.height);
|
||||
enableGameOptimizations = prefs.gameOptimizations;
|
||||
enableGameOptimizations = m_Preferences->gameOptimizations;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -899,23 +1042,15 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
||||
else {
|
||||
http.launchApp(m_App.id, &m_StreamConfig,
|
||||
enableGameOptimizations,
|
||||
prefs.playAudioOnHost,
|
||||
m_Preferences->playAudioOnHost,
|
||||
m_InputHandler->getAttachedGamepadMask());
|
||||
}
|
||||
} catch (const GfeHttpResponseException& e) {
|
||||
delete m_InputHandler;
|
||||
m_InputHandler = nullptr;
|
||||
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||
emit displayLaunchError("GeForce Experience returned error: " + e.toQString());
|
||||
QThreadPool::globalInstance()->start(new DeferredSessionCleanupTask(this));
|
||||
return;
|
||||
emit displayLaunchError(tr("GeForce Experience returned error: %1").arg(e.toQString()));
|
||||
return false;
|
||||
} catch (const QtNetworkReplyException& e) {
|
||||
delete m_InputHandler;
|
||||
m_InputHandler = nullptr;
|
||||
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||
emit displayLaunchError(e.toQString());
|
||||
QThreadPool::globalInstance()->start(new DeferredSessionCleanupTask(this));
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
QByteArray hostnameStr = m_Computer->activeAddress.toLatin1();
|
||||
@@ -934,17 +1069,28 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
||||
hostInfo.serverInfoGfeVersion = siGfeVersion.data();
|
||||
}
|
||||
|
||||
// isReachableOverVpn() does network I/O, so we only attempt to check
|
||||
// VPN reachability if we've already contacted the PC successfully
|
||||
if (m_Computer->isReachableOverVpn()) {
|
||||
// It looks like our route to this PC is over a VPN.
|
||||
// Treat it as remote even if the target address is in RFC 1918 address space.
|
||||
m_StreamConfig.streamingRemotely = STREAM_CFG_REMOTE;
|
||||
m_StreamConfig.packetSize = 1024;
|
||||
if (m_Preferences->packetSize != 0) {
|
||||
// Override default packet size and remote streaming detection
|
||||
// NB: Using STREAM_CFG_AUTO will cap our packet size at 1024 for remote hosts.
|
||||
m_StreamConfig.streamingRemotely = STREAM_CFG_LOCAL;
|
||||
m_StreamConfig.packetSize = m_Preferences->packetSize;
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Using custom packet size: %d bytes",
|
||||
m_Preferences->packetSize);
|
||||
}
|
||||
else {
|
||||
m_StreamConfig.streamingRemotely = STREAM_CFG_AUTO;
|
||||
m_StreamConfig.packetSize = 1392;
|
||||
// isReachableOverVpn() does network I/O, so we only attempt to check
|
||||
// VPN reachability if we've already contacted the PC successfully
|
||||
if (m_Computer->isReachableOverVpn()) {
|
||||
// It looks like our route to this PC is over a VPN.
|
||||
// Treat it as remote even if the target address is in RFC 1918 address space.
|
||||
m_StreamConfig.streamingRemotely = STREAM_CFG_REMOTE;
|
||||
m_StreamConfig.packetSize = 1024;
|
||||
}
|
||||
else {
|
||||
m_StreamConfig.streamingRemotely = STREAM_CFG_AUTO;
|
||||
m_StreamConfig.packetSize = 1392;
|
||||
}
|
||||
}
|
||||
|
||||
int err = LiStartConnection(&hostInfo, &m_StreamConfig, &k_ConnCallbacks,
|
||||
@@ -954,6 +1100,77 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
||||
if (err != 0) {
|
||||
// We already displayed an error dialog in the stage failure
|
||||
// listener.
|
||||
return false;
|
||||
}
|
||||
|
||||
emit connectionStarted();
|
||||
return true;
|
||||
}
|
||||
|
||||
void Session::flushWindowEvents()
|
||||
{
|
||||
// Pump events to ensure all pending OS events are posted
|
||||
SDL_PumpEvents();
|
||||
|
||||
// Insert a barrier to discard any additional window events.
|
||||
// We don't use SDL_FlushEvent() here because it could cause
|
||||
// important events to be lost.
|
||||
m_FlushingWindowEventsRef++;
|
||||
|
||||
// This event will cause us to set m_FlushingWindowEvents back to false.
|
||||
SDL_Event flushEvent = {};
|
||||
flushEvent.type = SDL_USEREVENT;
|
||||
flushEvent.user.code = SDL_CODE_FLUSH_WINDOW_EVENT_BARRIER;
|
||||
SDL_PushEvent(&flushEvent);
|
||||
}
|
||||
|
||||
void Session::exec(int displayOriginX, int displayOriginY)
|
||||
{
|
||||
m_DisplayOriginX = displayOriginX;
|
||||
m_DisplayOriginY = displayOriginY;
|
||||
|
||||
// Complete initialization in this deferred context to avoid
|
||||
// calling expensive functions in the constructor (during the
|
||||
// process of loading the StreamSegue).
|
||||
//
|
||||
// NB: This initializes the SDL video subsystem, so it must be
|
||||
// called on the main thread.
|
||||
if (!initialize()) {
|
||||
emit sessionFinished(0);
|
||||
return;
|
||||
}
|
||||
|
||||
// Wait for any old session to finish cleanup
|
||||
s_ActiveSessionSemaphore.acquire();
|
||||
|
||||
// We're now active
|
||||
s_ActiveSession = this;
|
||||
|
||||
// Initialize the gamepad code with our preferences
|
||||
// NB: m_InputHandler must be initialize before starting the connection.
|
||||
m_InputHandler = new SdlInputHandler(*m_Preferences, m_Computer,
|
||||
m_StreamConfig.width,
|
||||
m_StreamConfig.height);
|
||||
|
||||
// Kick off the async connection thread while we sit here and pump the event loop
|
||||
AsyncConnectionStartThread asyncConnThread(this);
|
||||
#ifdef USE_ASYNC_CONNECT_THREAD
|
||||
asyncConnThread.start();
|
||||
while (!asyncConnThread.wait(10)) {
|
||||
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
|
||||
QCoreApplication::sendPostedEvents();
|
||||
}
|
||||
#else
|
||||
asyncConnThread.run();
|
||||
#endif
|
||||
|
||||
// Pump the event loop one last time to ensure we pick up any events from
|
||||
// the thread that happened while it was in the final successful QThread::wait().
|
||||
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
|
||||
QCoreApplication::sendPostedEvents();
|
||||
|
||||
// If the connection failed, clean up and abort the connection.
|
||||
if (!m_AsyncConnectionSuccess) {
|
||||
delete m_InputHandler;
|
||||
m_InputHandler = nullptr;
|
||||
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||
@@ -961,30 +1178,48 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
||||
return;
|
||||
}
|
||||
|
||||
// Pump the message loop to update the UI
|
||||
emit connectionStarted();
|
||||
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
|
||||
|
||||
int x, y, width, height;
|
||||
getWindowDimensions(x, y, width, height);
|
||||
|
||||
#ifdef STEAM_LINK
|
||||
// We need a little delay before creating the window or we will trigger some kind
|
||||
// of graphics driver bug on Steam Link that causes a jagged overlay to appear in
|
||||
// the top right corner randomly.
|
||||
SDL_Delay(500);
|
||||
#endif
|
||||
|
||||
m_Window = SDL_CreateWindow("Moonlight",
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
SDL_WINDOW_ALLOW_HIGHDPI);
|
||||
SDL_WINDOW_ALLOW_HIGHDPI | StreamUtils::getPlatformWindowFlags());
|
||||
if (!m_Window) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"SDL_CreateWindow() failed: %s",
|
||||
SDL_GetError());
|
||||
delete m_InputHandler;
|
||||
m_InputHandler = nullptr;
|
||||
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||
QThreadPool::globalInstance()->start(new DeferredSessionCleanupTask(this));
|
||||
return;
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"SDL_CreateWindow() failed with platform flags: %s",
|
||||
SDL_GetError());
|
||||
|
||||
m_Window = SDL_CreateWindow("Moonlight",
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
SDL_WINDOW_ALLOW_HIGHDPI);
|
||||
if (!m_Window) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"SDL_CreateWindow() failed: %s",
|
||||
SDL_GetError());
|
||||
|
||||
delete m_InputHandler;
|
||||
m_InputHandler = nullptr;
|
||||
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||
QThreadPool::globalInstance()->start(new DeferredSessionCleanupTask(this));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
m_InputHandler->setWindow(m_Window);
|
||||
|
||||
QSvgRenderer svgIconRenderer(QString(":/res/moonlight.svg"));
|
||||
QImage svgImage(ICON_SIZE, ICON_SIZE, QImage::Format_RGBA8888);
|
||||
svgImage.fill(0);
|
||||
@@ -1029,18 +1264,21 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
||||
SDL_SetWindowFullscreen(m_Window, m_FullScreenFlag);
|
||||
}
|
||||
|
||||
#ifndef QT_DEBUG
|
||||
// Capture the mouse by default on release builds only.
|
||||
// This prevents the mouse from becoming trapped inside
|
||||
// Moonlight when it's halted at a debug break.
|
||||
if (m_Preferences->windowMode != StreamingPreferences::WM_WINDOWED) {
|
||||
// HACK: This doesn't work on Wayland until we render a frame, so
|
||||
// just don't do it for now.
|
||||
if (strcmp(SDL_GetCurrentVideoDriver(), "wayland") != 0) {
|
||||
m_InputHandler->setCaptureActive(true);
|
||||
}
|
||||
bool needsFirstEnterCapture = false;
|
||||
bool needsPostDecoderCreationCapture = false;
|
||||
|
||||
// HACK: For Wayland, we wait until we get the first SDL_WINDOWEVENT_ENTER
|
||||
// event where it seems to work consistently on GNOME. For other platforms,
|
||||
// especially where SDL may call SDL_RecreateWindow(), we must only capture
|
||||
// after the decoder is created.
|
||||
if (strcmp(SDL_GetCurrentVideoDriver(), "wayland") == 0) {
|
||||
// Native Wayland: Capture on SDL_WINDOWEVENT_ENTER
|
||||
needsFirstEnterCapture = true;
|
||||
}
|
||||
else {
|
||||
// X11/XWayland: Capture after decoder creation
|
||||
needsPostDecoderCreationCapture = true;
|
||||
}
|
||||
#endif
|
||||
|
||||
// Stop text input. SDL enables it by default
|
||||
// when we initialize the video subsystem, but this
|
||||
@@ -1051,6 +1289,11 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
||||
// Disable the screen saver
|
||||
SDL_DisableScreenSaver();
|
||||
|
||||
// Hide Qt's fake mouse cursor on EGLFS systems
|
||||
if (QGuiApplication::platformName() == "eglfs") {
|
||||
QGuiApplication::setOverrideCursor(QCursor(Qt::BlankCursor));
|
||||
}
|
||||
|
||||
// Set timer resolution to 1 ms on Windows for greater
|
||||
// sleep precision and more accurate callback timing.
|
||||
SDL_SetHint(SDL_HINT_TIMER_RESOLUTION, "1");
|
||||
@@ -1063,7 +1306,7 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
||||
m_UnexpectedTermination = false;
|
||||
|
||||
// Start rich presence to indicate we're in game
|
||||
RichPresenceManager presence(prefs, m_App.name);
|
||||
RichPresenceManager presence(*m_Preferences, m_App.name);
|
||||
|
||||
// Hijack this thread to be the SDL main thread. We have to do this
|
||||
// because we want to suspend all Qt processing until the stream is over.
|
||||
@@ -1091,48 +1334,47 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
||||
goto DispatchDeferredCleanup;
|
||||
|
||||
case SDL_USEREVENT:
|
||||
SDL_assert(event.user.code == SDL_CODE_FRAME_READY);
|
||||
m_VideoDecoder->renderFrameOnMainThread();
|
||||
switch (event.user.code) {
|
||||
case SDL_CODE_FRAME_READY:
|
||||
m_VideoDecoder->renderFrameOnMainThread();
|
||||
break;
|
||||
case SDL_CODE_HIDE_CURSOR:
|
||||
SDL_ShowCursor(SDL_DISABLE);
|
||||
break;
|
||||
case SDL_CODE_SHOW_CURSOR:
|
||||
SDL_ShowCursor(SDL_ENABLE);
|
||||
break;
|
||||
case SDL_CODE_FLUSH_WINDOW_EVENT_BARRIER:
|
||||
m_FlushingWindowEventsRef--;
|
||||
break;
|
||||
default:
|
||||
SDL_assert(false);
|
||||
}
|
||||
break;
|
||||
|
||||
case SDL_WINDOWEVENT:
|
||||
// Capture mouse cursor when user actives the window by clicking on
|
||||
// window's client area (borders and title bar excluded).
|
||||
// Without this you would have to click the window twice (once to
|
||||
// activate it, second time to enable capture). With this you need to
|
||||
// click it only once.
|
||||
// On Linux, the button press event is delivered after the focus gain
|
||||
// so this is not neccessary (and leads to a click sent to the host
|
||||
// when focusing the window by clicking).
|
||||
// By excluding window's borders and title bar out, lets user still
|
||||
// interact with them without mouse capture kicking in.
|
||||
#if defined(Q_OS_WIN32) || defined(Q_OS_DARWIN)
|
||||
if (event.window.event == SDL_WINDOWEVENT_FOCUS_GAINED) {
|
||||
int mouseX, mouseY;
|
||||
Uint32 mouseState = SDL_GetGlobalMouseState(&mouseX, &mouseY);
|
||||
if (mouseState & SDL_BUTTON(SDL_BUTTON_LEFT)) {
|
||||
int x, y, width, height;
|
||||
SDL_GetWindowPosition(m_Window, &x, &y);
|
||||
SDL_GetWindowSize(m_Window, &width, &height);
|
||||
if (mouseX > x && mouseX < x+width && mouseY > y && mouseY < y+height) {
|
||||
m_InputHandler->setCaptureActive(true);
|
||||
}
|
||||
// Early handling of some events
|
||||
switch (event.window.event) {
|
||||
case SDL_WINDOWEVENT_FOCUS_LOST:
|
||||
if (m_Preferences->muteOnFocusLoss) {
|
||||
m_AudioMuted = true;
|
||||
}
|
||||
m_InputHandler->notifyFocusLost();
|
||||
break;
|
||||
case SDL_WINDOWEVENT_FOCUS_GAINED:
|
||||
if (m_Preferences->muteOnFocusLoss) {
|
||||
m_AudioMuted = false;
|
||||
}
|
||||
break;
|
||||
case SDL_WINDOWEVENT_LEAVE:
|
||||
m_InputHandler->notifyMouseLeave();
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (event.window.event == SDL_WINDOWEVENT_FOCUS_LOST) {
|
||||
// Release mouse cursor when another window is activated (e.g. by using ALT+TAB).
|
||||
// This lets user to interact with our window's title bar and with the buttons in it.
|
||||
// Doing this while the window is full-screen breaks the transition out of FS
|
||||
// (desktop and exclusive), so we must check for that before releasing mouse capture.
|
||||
if (!(SDL_GetWindowFlags(m_Window) & SDL_WINDOW_FULLSCREEN)) {
|
||||
m_InputHandler->setCaptureActive(false);
|
||||
}
|
||||
|
||||
// Raise all keys that are currently pressed. If we don't do this, certain keys
|
||||
// used in shortcuts that cause focus loss (such as Alt+Tab) may get stuck down.
|
||||
m_InputHandler->raiseAllKeys();
|
||||
// Capture the mouse on SDL_WINDOWEVENT_ENTER if needed
|
||||
if (needsFirstEnterCapture && event.window.event == SDL_WINDOWEVENT_ENTER) {
|
||||
m_InputHandler->setCaptureActive(true);
|
||||
needsFirstEnterCapture = false;
|
||||
}
|
||||
|
||||
// We want to recreate the decoder for resizes (full-screen toggles) and the initial shown event.
|
||||
@@ -1161,6 +1403,22 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
||||
SDL_SetWindowPosition(m_Window, x, y);
|
||||
}
|
||||
|
||||
if (m_FlushingWindowEventsRef > 0) {
|
||||
// Ignore window events for renderer reset if flushing
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Dropping window event during flush: %d (%d %d)",
|
||||
event.window.event,
|
||||
event.window.data1,
|
||||
event.window.data2);
|
||||
break;
|
||||
}
|
||||
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Recreating renderer for window event: %d (%d %d)",
|
||||
event.window.event,
|
||||
event.window.data1,
|
||||
event.window.data2);
|
||||
|
||||
// Fall through
|
||||
case SDL_RENDER_DEVICE_RESET:
|
||||
case SDL_RENDER_TARGETS_RESET:
|
||||
@@ -1170,10 +1428,11 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
||||
// Destroy the old decoder
|
||||
delete m_VideoDecoder;
|
||||
|
||||
// Flush any other pending window events that could
|
||||
// send us back here immediately
|
||||
SDL_PumpEvents();
|
||||
SDL_FlushEvent(SDL_WINDOWEVENT);
|
||||
// Insert a barrier to discard any additional window events
|
||||
// that could cause the renderer to be and recreated again.
|
||||
// We don't use SDL_FlushEvent() here because it could cause
|
||||
// important events to be lost.
|
||||
flushWindowEvents();
|
||||
|
||||
// Update the window display mode based on our current monitor
|
||||
currentDisplayIndex = SDL_GetWindowDisplayIndex(m_Window);
|
||||
@@ -1210,9 +1469,17 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
||||
SDL_AtomicUnlock(&m_DecoderLock);
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Failed to recreate decoder after reset");
|
||||
emit displayLaunchError("Unable to initialize video decoder. Please check your streaming settings and try again.");
|
||||
emit displayLaunchError(tr("Unable to initialize video decoder. Please check your streaming settings and try again."));
|
||||
goto DispatchDeferredCleanup;
|
||||
}
|
||||
|
||||
// As of SDL 2.0.12, SDL_RecreateWindow() doesn't carry over mouse capture
|
||||
// or mouse hiding state to the new window. By capturing after the decoder
|
||||
// is set up, this ensures the window re-creation is already done.
|
||||
if (needsPostDecoderCreationCapture) {
|
||||
m_InputHandler->setCaptureActive(true);
|
||||
needsPostDecoderCreationCapture = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Request an IDR frame to complete the reset
|
||||
@@ -1263,6 +1530,9 @@ DispatchDeferredCleanup:
|
||||
m_InputHandler->setCaptureActive(false);
|
||||
SDL_EnableScreenSaver();
|
||||
SDL_SetHint(SDL_HINT_TIMER_RESOLUTION, "0");
|
||||
if (QGuiApplication::platformName() == "eglfs") {
|
||||
QGuiApplication::restoreOverrideCursor();
|
||||
}
|
||||
|
||||
// Raise any keys that are still down
|
||||
m_InputHandler->raiseAllKeys();
|
||||
@@ -1282,25 +1552,6 @@ DispatchDeferredCleanup:
|
||||
m_VideoDecoder = nullptr;
|
||||
SDL_AtomicUnlock(&m_DecoderLock);
|
||||
|
||||
if (strcmp(SDL_GetCurrentVideoDriver(), "wayland") == 0) {
|
||||
// HACK: SDL (as of 2.0.10) has a bug that causes Mutter not to destroy the window
|
||||
// surface when in full-screen unless we render more frames after we request
|
||||
// to exit full-screen. The amount of frames required is variable but 500 ms
|
||||
// of frames seems sufficient in my testing.
|
||||
SDL_SetWindowFullscreen(m_Window, 0);
|
||||
SDL_Renderer* renderer = SDL_CreateRenderer(m_Window, -1, SDL_RENDERER_PRESENTVSYNC);
|
||||
if (renderer != nullptr) {
|
||||
SDL_SetRenderDrawColor(renderer, 0, 0, 0, SDL_ALPHA_OPAQUE);
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
SDL_RenderClear(renderer);
|
||||
SDL_RenderPresent(renderer);
|
||||
SDL_Delay(50);
|
||||
}
|
||||
SDL_DestroyRenderer(renderer);
|
||||
}
|
||||
}
|
||||
|
||||
// This must be called after the decoder is deleted, because
|
||||
// the renderer may want to interact with the window
|
||||
SDL_DestroyWindow(m_Window);
|
||||
|
||||
+22
-8
@@ -5,7 +5,7 @@
|
||||
#include <Limelight.h>
|
||||
#include <opus_multistream.h>
|
||||
#include "settings/streamingpreferences.h"
|
||||
#include "input.h"
|
||||
#include "input/input.h"
|
||||
#include "video/decoder.h"
|
||||
#include "audio/renderers/renderer.h"
|
||||
#include "video/overlaymanager.h"
|
||||
@@ -16,6 +16,7 @@ class Session : public QObject
|
||||
|
||||
friend class SdlInputHandler;
|
||||
friend class DeferredSessionCleanupTask;
|
||||
friend class AsyncConnectionStartThread;
|
||||
|
||||
public:
|
||||
explicit Session(NvComputer* computer, NvApp& app, StreamingPreferences *preferences = nullptr);
|
||||
@@ -25,9 +26,8 @@ public:
|
||||
Q_INVOKABLE void exec(int displayOriginX, int displayOriginY);
|
||||
|
||||
static
|
||||
bool isHardwareDecodeAvailable(SDL_Window* window,
|
||||
StreamingPreferences::VideoDecoderSelection vds,
|
||||
int videoFormat, int width, int height, int frameRate);
|
||||
void getDecoderInfo(SDL_Window* window,
|
||||
bool& isHardwareAccelerated, bool& isFullScreenOnly, QSize& maxResolution);
|
||||
|
||||
static Session* get()
|
||||
{
|
||||
@@ -39,10 +39,12 @@ public:
|
||||
return m_OverlayManager;
|
||||
}
|
||||
|
||||
void flushWindowEvents();
|
||||
|
||||
signals:
|
||||
void stageStarting(QString stage);
|
||||
|
||||
void stageFailed(QString stage, long errorCode);
|
||||
void stageFailed(QString stage, int errorCode, QString failingPorts);
|
||||
|
||||
void connectionStarted();
|
||||
|
||||
@@ -52,11 +54,13 @@ signals:
|
||||
|
||||
void quitStarting();
|
||||
|
||||
void sessionFinished();
|
||||
void sessionFinished(int portTestResult);
|
||||
|
||||
private:
|
||||
bool initialize();
|
||||
|
||||
bool startConnectionAsync();
|
||||
|
||||
bool validateLaunch(SDL_Window* testWindow);
|
||||
|
||||
void emitLaunchWarning(QString text);
|
||||
@@ -78,6 +82,11 @@ private:
|
||||
|
||||
void updateOptimalWindowDisplayMode();
|
||||
|
||||
static
|
||||
bool isHardwareDecodeAvailable(SDL_Window* window,
|
||||
StreamingPreferences::VideoDecoderSelection vds,
|
||||
int videoFormat, int width, int height, int frameRate);
|
||||
|
||||
static
|
||||
bool chooseDecoder(StreamingPreferences::VideoDecoderSelection vds,
|
||||
SDL_Window* window, int videoFormat, int width, int height,
|
||||
@@ -89,10 +98,10 @@ private:
|
||||
void clStageStarting(int stage);
|
||||
|
||||
static
|
||||
void clStageFailed(int stage, long errorCode);
|
||||
void clStageFailed(int stage, int errorCode);
|
||||
|
||||
static
|
||||
void clConnectionTerminated(long errorCode);
|
||||
void clConnectionTerminated(int errorCode);
|
||||
|
||||
static
|
||||
void clLogMessage(const char* format, ...);
|
||||
@@ -134,6 +143,7 @@ private:
|
||||
SDL_SpinLock m_DecoderLock;
|
||||
bool m_NeedsIdr;
|
||||
bool m_AudioDisabled;
|
||||
bool m_AudioMuted;
|
||||
Uint32 m_FullScreenFlag;
|
||||
int m_DisplayOriginX;
|
||||
int m_DisplayOriginY;
|
||||
@@ -142,6 +152,10 @@ private:
|
||||
SdlInputHandler* m_InputHandler;
|
||||
SDL_SpinLock m_InputHandlerLock;
|
||||
int m_MouseEmulationRefCount;
|
||||
int m_FlushingWindowEventsRef;
|
||||
|
||||
bool m_AsyncConnectionSuccess;
|
||||
int m_PortTestResults;
|
||||
|
||||
int m_ActiveVideoFormat;
|
||||
int m_ActiveVideoWidth;
|
||||
|
||||
@@ -6,20 +6,27 @@
|
||||
#include <ApplicationServices/ApplicationServices.h>
|
||||
#endif
|
||||
|
||||
Uint32 StreamUtils::getPlatformWindowFlags()
|
||||
{
|
||||
#ifdef Q_OS_DARWIN
|
||||
return SDL_WINDOW_METAL;
|
||||
#else
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
void StreamUtils::scaleSourceToDestinationSurface(SDL_Rect* src, SDL_Rect* dst)
|
||||
{
|
||||
int dstH = dst->w * src->h / src->w;
|
||||
int dstW = dst->h * src->w / src->h;
|
||||
|
||||
if (dstH > dst->h) {
|
||||
dst->y = 0;
|
||||
dst->x = (dst->w - dstW) / 2;
|
||||
dst->x += (dst->w - dstW) / 2;
|
||||
dst->w = dstW;
|
||||
SDL_assert(dst->w * src->h / src->w <= dst->h);
|
||||
}
|
||||
else {
|
||||
dst->x = 0;
|
||||
dst->y = (dst->h - dstH) / 2;
|
||||
dst->y += (dst->h - dstH) / 2;
|
||||
dst->h = dstH;
|
||||
SDL_assert(dst->h * src->w / src->h <= dst->w);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
class StreamUtils
|
||||
{
|
||||
public:
|
||||
static
|
||||
Uint32 getPlatformWindowFlags();
|
||||
|
||||
static
|
||||
void scaleSourceToDestinationSurface(SDL_Rect* src, SDL_Rect* dst);
|
||||
|
||||
|
||||
@@ -43,8 +43,10 @@ public:
|
||||
virtual ~IVideoDecoder() {}
|
||||
virtual bool initialize(PDECODER_PARAMETERS params) = 0;
|
||||
virtual bool isHardwareAccelerated() = 0;
|
||||
virtual bool isAlwaysFullScreen() = 0;
|
||||
virtual int getDecoderCapabilities() = 0;
|
||||
virtual int getDecoderColorspace() = 0;
|
||||
virtual QSize getDecoderMaxResolution() = 0;
|
||||
virtual int submitDecodeUnit(PDECODE_UNIT du) = 0;
|
||||
virtual void renderFrameOnMainThread() = 0;
|
||||
};
|
||||
|
||||
@@ -28,7 +28,7 @@ bool CUDARenderer::initialize(PDECODER_PARAMETERS)
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CUDARenderer::prepareDecoderContext(AVCodecContext* context)
|
||||
bool CUDARenderer::prepareDecoderContext(AVCodecContext* context, AVDictionary**)
|
||||
{
|
||||
context->hw_device_ctx = av_buffer_ref(m_HwContext);
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ public:
|
||||
CUDARenderer();
|
||||
virtual ~CUDARenderer() override;
|
||||
virtual bool initialize(PDECODER_PARAMETERS) override;
|
||||
virtual bool prepareDecoderContext(AVCodecContext* context) override;
|
||||
virtual bool prepareDecoderContext(AVCodecContext* context, AVDictionary** options) override;
|
||||
virtual void renderFrame(AVFrame* frame) override;
|
||||
virtual bool needsTestFrame() override;
|
||||
virtual bool isDirectRenderingSupported() override;
|
||||
|
||||
@@ -33,7 +33,7 @@ DrmRenderer::~DrmRenderer()
|
||||
}
|
||||
}
|
||||
|
||||
bool DrmRenderer::prepareDecoderContext(AVCodecContext*)
|
||||
bool DrmRenderer::prepareDecoderContext(AVCodecContext*, AVDictionary**)
|
||||
{
|
||||
/* Nothing to do */
|
||||
|
||||
@@ -194,8 +194,19 @@ enum AVPixelFormat DrmRenderer::getPreferredPixelFormat(int)
|
||||
return AV_PIX_FMT_DRM_PRIME;
|
||||
}
|
||||
|
||||
int DrmRenderer::getRendererAttributes()
|
||||
{
|
||||
// This renderer can only draw in full-screen
|
||||
return RENDERER_ATTRIBUTE_FULLSCREEN_ONLY;
|
||||
}
|
||||
|
||||
void DrmRenderer::renderFrame(AVFrame* frame)
|
||||
{
|
||||
if (frame == nullptr) {
|
||||
// End of stream - nothing to do for us
|
||||
return;
|
||||
}
|
||||
|
||||
AVDRMFrameDescriptor* drmFrame = (AVDRMFrameDescriptor*)frame->data[0];
|
||||
int err;
|
||||
uint32_t primeHandle;
|
||||
|
||||
@@ -10,9 +10,10 @@ public:
|
||||
DrmRenderer();
|
||||
virtual ~DrmRenderer() override;
|
||||
virtual bool initialize(PDECODER_PARAMETERS params) override;
|
||||
virtual bool prepareDecoderContext(AVCodecContext* context) override;
|
||||
virtual bool prepareDecoderContext(AVCodecContext* context, AVDictionary** options) override;
|
||||
virtual void renderFrame(AVFrame* frame) override;
|
||||
virtual enum AVPixelFormat getPreferredPixelFormat(int videoFormat) override;
|
||||
virtual int getRendererAttributes() override;
|
||||
|
||||
private:
|
||||
int m_DrmFd;
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// minwindef.h defines min() and max() macros that conflict with
|
||||
// std::numeric_limits, which Qt uses in some of its headers.
|
||||
#define NOMINMAX
|
||||
|
||||
#include <initguid.h>
|
||||
#include "dxva2.h"
|
||||
#include "../ffmpeg.h"
|
||||
@@ -17,23 +21,31 @@ DEFINE_GUID(DXVADDI_Intel_ModeH264_E, 0x604F8E68,0x4951,0x4C54,0x88,0xFE,0xAB,0x
|
||||
|
||||
#define SAFE_COM_RELEASE(x) if (x) { (x)->Release(); }
|
||||
|
||||
typedef struct _VERTEX
|
||||
{
|
||||
float x, y, z, rhw;
|
||||
float tu, tv;
|
||||
} VERTEX, *PVERTEX;
|
||||
|
||||
DXVA2Renderer::DXVA2Renderer() :
|
||||
m_DecService(nullptr),
|
||||
m_Decoder(nullptr),
|
||||
m_SurfacesUsed(0),
|
||||
m_Pool(nullptr),
|
||||
m_OverlayLock(0),
|
||||
m_Device(nullptr),
|
||||
m_RenderTarget(nullptr),
|
||||
m_ProcService(nullptr),
|
||||
m_Processor(nullptr),
|
||||
m_FrameIndex(0),
|
||||
m_DebugOverlayFont(nullptr),
|
||||
m_StatusOverlayFont(nullptr),
|
||||
m_BlockingPresent(false)
|
||||
{
|
||||
RtlZeroMemory(m_DecSurfaces, sizeof(m_DecSurfaces));
|
||||
RtlZeroMemory(&m_DXVAContext, sizeof(m_DXVAContext));
|
||||
|
||||
RtlZeroMemory(m_OverlayVertexBuffers, sizeof(m_OverlayVertexBuffers));
|
||||
RtlZeroMemory(m_OverlayTextures, sizeof(m_OverlayTextures));
|
||||
|
||||
// Use MMCSS scheduling for lower scheduling latency while we're streaming
|
||||
DwmEnableMMCSS(TRUE);
|
||||
}
|
||||
@@ -48,8 +60,14 @@ DXVA2Renderer::~DXVA2Renderer()
|
||||
SAFE_COM_RELEASE(m_RenderTarget);
|
||||
SAFE_COM_RELEASE(m_ProcService);
|
||||
SAFE_COM_RELEASE(m_Processor);
|
||||
SAFE_COM_RELEASE(m_DebugOverlayFont);
|
||||
SAFE_COM_RELEASE(m_StatusOverlayFont);
|
||||
|
||||
for (int i = 0; i < ARRAYSIZE(m_OverlayVertexBuffers); i++) {
|
||||
SAFE_COM_RELEASE(m_OverlayVertexBuffers[i]);
|
||||
}
|
||||
|
||||
for (int i = 0; i < ARRAYSIZE(m_OverlayTextures); i++) {
|
||||
SAFE_COM_RELEASE(m_OverlayTextures[i]);
|
||||
}
|
||||
|
||||
for (int i = 0; i < ARRAYSIZE(m_DecSurfaces); i++) {
|
||||
SAFE_COM_RELEASE(m_DecSurfaces[i]);
|
||||
@@ -80,7 +98,7 @@ AVBufferRef* DXVA2Renderer::ffPoolAlloc(void* opaque, int)
|
||||
return NULL;
|
||||
}
|
||||
|
||||
bool DXVA2Renderer::prepareDecoderContext(AVCodecContext* context)
|
||||
bool DXVA2Renderer::prepareDecoderContext(AVCodecContext* context, AVDictionary**)
|
||||
{
|
||||
// m_DXVAContext.workaround and report_id already initialized elsewhere
|
||||
m_DXVAContext.decoder = m_Decoder;
|
||||
@@ -323,6 +341,23 @@ bool DXVA2Renderer::initializeRenderer()
|
||||
}
|
||||
}
|
||||
|
||||
m_Device->SetRenderState(D3DRS_ZENABLE, D3DZB_FALSE);
|
||||
m_Device->SetRenderState(D3DRS_CULLMODE, D3DCULL_NONE);
|
||||
m_Device->SetRenderState(D3DRS_LIGHTING, FALSE);
|
||||
|
||||
m_Device->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_MODULATE);
|
||||
m_Device->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE);
|
||||
m_Device->SetTextureStageState(0, D3DTSS_COLORARG2, D3DTA_DIFFUSE);
|
||||
|
||||
m_Device->SetSamplerState(0, D3DSAMP_MAGFILTER, D3DTEXF_LINEAR);
|
||||
m_Device->SetSamplerState(0, D3DSAMP_MINFILTER, D3DTEXF_LINEAR);
|
||||
|
||||
m_Device->SetRenderState(D3DRS_ALPHABLENDENABLE, TRUE);
|
||||
m_Device->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA);
|
||||
m_Device->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA);
|
||||
|
||||
m_Device->SetFVF(D3DFVF_XYZRHW | D3DFVF_TEX1);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -347,15 +382,28 @@ bool DXVA2Renderer::isDXVideoProcessorAPIBlacklisted()
|
||||
D3DADAPTER_IDENTIFIER9 id;
|
||||
|
||||
hr = d3d9->GetAdapterIdentifier(caps.AdapterOrdinal, 0, &id);
|
||||
if (SUCCEEDED(hr) && id.VendorId == 0x8086) {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Avoiding IDirectXVideoProcessor API on Intel GPU");
|
||||
if (SUCCEEDED(hr)) {
|
||||
if (id.VendorId == 0x8086) {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Avoiding IDirectXVideoProcessor API on Intel GPU");
|
||||
|
||||
// On Intel GPUs, we can get unwanted video "enhancements" due to post-processing
|
||||
// effects that the GPU driver forces on us. In many cases, this makes the video
|
||||
// actually look worse. We can avoid these by using StretchRect() instead on these
|
||||
// platforms.
|
||||
result = true;
|
||||
// On Intel GPUs, we can get unwanted video "enhancements" due to post-processing
|
||||
// effects that the GPU driver forces on us. In many cases, this makes the video
|
||||
// actually look worse. We can avoid these by using StretchRect() instead on these
|
||||
// platforms.
|
||||
result = true;
|
||||
}
|
||||
else if (id.VendorId == 0x4d4f4351) { // QCOM in ASCII
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Avoiding IDirectXVideoProcessor API on Qualcomm GPU");
|
||||
|
||||
// On Qualcomm GPUs (all D3D9on12 GPUs?), the scaling quality of VideoProcessBlt()
|
||||
// is absolutely horrible. StretchRect() is much much better.
|
||||
result = true;
|
||||
}
|
||||
else {
|
||||
result = false;
|
||||
}
|
||||
}
|
||||
else {
|
||||
result = false;
|
||||
@@ -446,7 +494,7 @@ bool DXVA2Renderer::isDecoderBlacklisted()
|
||||
// https://www.intel.com/content/www/us/en/support/articles/000005654/graphics-drivers.html
|
||||
if (LOWORD(id.DriverVersion.LowPart) < 4836) {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Intel driver version blacklisted for HEVC");
|
||||
"Detected buggy Intel GPU driver installed. Update your Intel GPU driver to enable HEVC!");
|
||||
result = (m_VideoFormat & VIDEO_FORMAT_MASK_H265) != 0;
|
||||
}
|
||||
else {
|
||||
@@ -546,14 +594,34 @@ bool DXVA2Renderer::initializeDevice(SDL_Window* window, bool enableVsync)
|
||||
d3dpp.hDeviceWindow = info.info.win.window;
|
||||
d3dpp.Flags = D3DPRESENTFLAG_VIDEO;
|
||||
|
||||
if (m_VideoFormat == VIDEO_FORMAT_H265_MAIN10) {
|
||||
// Verify 10-bit A2R10G10B10 color support. This is only available
|
||||
// as a display format in full-screen exclusive mode on DX9.
|
||||
hr = d3d9ex->CheckDeviceType(adapterIndex,
|
||||
D3DDEVTYPE_HAL,
|
||||
D3DFMT_A2R10G10B10,
|
||||
D3DFMT_A2R10G10B10,
|
||||
FALSE);
|
||||
if (FAILED(hr)) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"GPU/driver doesn't support A2R10G10B10");
|
||||
d3d9ex->Release();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if ((windowFlags & SDL_WINDOW_FULLSCREEN_DESKTOP) == SDL_WINDOW_FULLSCREEN) {
|
||||
d3dpp.Windowed = false;
|
||||
d3dpp.BackBufferWidth = currentMode.Width;
|
||||
d3dpp.BackBufferHeight = currentMode.Height;
|
||||
d3dpp.FullScreen_RefreshRateInHz = currentMode.RefreshRate;
|
||||
|
||||
// TODO: Need to select 10-bit color format for HDR
|
||||
d3dpp.BackBufferFormat = currentMode.Format;
|
||||
if (m_VideoFormat == VIDEO_FORMAT_H265_MAIN10) {
|
||||
d3dpp.BackBufferFormat = currentMode.Format = D3DFMT_A2R10G10B10;
|
||||
}
|
||||
else {
|
||||
d3dpp.BackBufferFormat = currentMode.Format;
|
||||
}
|
||||
}
|
||||
else {
|
||||
d3dpp.Windowed = true;
|
||||
@@ -722,64 +790,201 @@ void DXVA2Renderer::notifyOverlayUpdated(Overlay::OverlayType type)
|
||||
{
|
||||
HRESULT hr;
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case Overlay::OverlayDebug:
|
||||
if (m_DebugOverlayFont == nullptr) {
|
||||
hr = D3DXCreateFontA(m_Device,
|
||||
Session::get()->getOverlayManager().getOverlayFontSize(Overlay::OverlayDebug),
|
||||
0,
|
||||
FW_HEAVY,
|
||||
1,
|
||||
false,
|
||||
ANSI_CHARSET,
|
||||
OUT_DEFAULT_PRECIS,
|
||||
DEFAULT_QUALITY,
|
||||
DEFAULT_PITCH | FF_DONTCARE,
|
||||
"",
|
||||
&m_DebugOverlayFont);
|
||||
if (FAILED(hr)) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"D3DXCreateFontA() failed: %x",
|
||||
hr);
|
||||
m_DebugOverlayFont = nullptr;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case Overlay::OverlayStatusUpdate:
|
||||
if (m_StatusOverlayFont == nullptr) {
|
||||
hr = D3DXCreateFontA(m_Device,
|
||||
Session::get()->getOverlayManager().getOverlayFontSize(Overlay::OverlayStatusUpdate),
|
||||
0,
|
||||
FW_HEAVY,
|
||||
1,
|
||||
false,
|
||||
ANSI_CHARSET,
|
||||
OUT_DEFAULT_PRECIS,
|
||||
DEFAULT_QUALITY,
|
||||
DEFAULT_PITCH | FF_DONTCARE,
|
||||
"",
|
||||
&m_StatusOverlayFont);
|
||||
if (FAILED(hr)) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"D3DXCreateFontA() failed: %x",
|
||||
hr);
|
||||
m_StatusOverlayFont = nullptr;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
SDL_assert(false);
|
||||
break;
|
||||
SDL_Surface* newSurface = Session::get()->getOverlayManager().getUpdatedOverlaySurface(type);
|
||||
if (newSurface == nullptr && Session::get()->getOverlayManager().isOverlayEnabled(type)) {
|
||||
// The overlay is enabled and there is no new surface. Leave the old texture alone.
|
||||
return;
|
||||
}
|
||||
|
||||
SDL_AtomicLock(&m_OverlayLock);
|
||||
IDirect3DTexture9* oldTexture = m_OverlayTextures[type];
|
||||
m_OverlayTextures[type] = nullptr;
|
||||
|
||||
IDirect3DVertexBuffer9* oldVertexBuffer = m_OverlayVertexBuffers[type];
|
||||
m_OverlayVertexBuffers[type] = nullptr;
|
||||
SDL_AtomicUnlock(&m_OverlayLock);
|
||||
|
||||
SAFE_COM_RELEASE(oldTexture);
|
||||
SAFE_COM_RELEASE(oldVertexBuffer);
|
||||
|
||||
// If the overlay is disabled, we're done
|
||||
if (!Session::get()->getOverlayManager().isOverlayEnabled(type)) {
|
||||
SDL_FreeSurface(newSurface);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a dynamic texture to populate with our pixel data
|
||||
SDL_assert(!SDL_MUSTLOCK(newSurface));
|
||||
IDirect3DTexture9* newTexture = nullptr;
|
||||
hr = m_Device->CreateTexture(newSurface->w,
|
||||
newSurface->h,
|
||||
1,
|
||||
D3DUSAGE_DYNAMIC,
|
||||
D3DFMT_A8R8G8B8,
|
||||
D3DPOOL_DEFAULT,
|
||||
&newTexture,
|
||||
nullptr);
|
||||
if (FAILED(hr)) {
|
||||
SDL_FreeSurface(newSurface);
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"CreateTexture() failed: %x",
|
||||
hr);
|
||||
return;
|
||||
}
|
||||
|
||||
D3DLOCKED_RECT lockedRect;
|
||||
hr = newTexture->LockRect(0, &lockedRect, nullptr, D3DLOCK_DISCARD);
|
||||
if (FAILED(hr)) {
|
||||
SDL_FreeSurface(newSurface);
|
||||
SAFE_COM_RELEASE(newTexture);
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"IDirect3DTexture9::LockRect() failed: %x",
|
||||
hr);
|
||||
return;
|
||||
}
|
||||
|
||||
if (newSurface->pitch == lockedRect.Pitch) {
|
||||
// If the pitch matches, we can take the fast path and use a single copy to transfer the pixels
|
||||
RtlCopyMemory(lockedRect.pBits, newSurface->pixels, newSurface->pitch * newSurface->h);
|
||||
}
|
||||
else {
|
||||
// If the pitch doesn't match, we'll need to copy each row separately
|
||||
int pitch = SDL_min(newSurface->pitch, lockedRect.Pitch);
|
||||
for (int i = 0; i < newSurface->h; i++) {
|
||||
RtlCopyMemory(((PUCHAR)lockedRect.pBits) + (lockedRect.Pitch * i),
|
||||
((PUCHAR)newSurface->pixels) + (newSurface->pitch * i),
|
||||
pitch);
|
||||
}
|
||||
}
|
||||
|
||||
newTexture->UnlockRect(0);
|
||||
|
||||
SDL_FRect renderRect = {};
|
||||
|
||||
if (type == Overlay::OverlayStatusUpdate) {
|
||||
// Bottom Left
|
||||
renderRect.x = 0;
|
||||
renderRect.y = m_DisplayHeight - newSurface->h;
|
||||
}
|
||||
else if (type == Overlay::OverlayDebug) {
|
||||
// Top left
|
||||
renderRect.x = 0;
|
||||
renderRect.y = 0;
|
||||
}
|
||||
|
||||
renderRect.w = newSurface->w;
|
||||
renderRect.h = newSurface->h;
|
||||
|
||||
// The surface is no longer required
|
||||
SDL_FreeSurface(newSurface);
|
||||
newSurface = nullptr;
|
||||
|
||||
VERTEX verts[] =
|
||||
{
|
||||
{renderRect.x, renderRect.y, 0, 1, 0, 0},
|
||||
{renderRect.x, renderRect.y+renderRect.h, 0, 1, 0, 1},
|
||||
{renderRect.x+renderRect.w, renderRect.y+renderRect.h, 0, 1, 1, 1},
|
||||
{renderRect.x+renderRect.w, renderRect.y, 0, 1, 1, 0}
|
||||
};
|
||||
|
||||
IDirect3DVertexBuffer9* newVertexBuffer = nullptr;
|
||||
hr = m_Device->CreateVertexBuffer(sizeof(verts),
|
||||
D3DUSAGE_WRITEONLY,
|
||||
D3DFVF_XYZRHW | D3DFVF_TEX1,
|
||||
D3DPOOL_DEFAULT,
|
||||
&newVertexBuffer,
|
||||
nullptr);
|
||||
if (FAILED(hr)) {
|
||||
SAFE_COM_RELEASE(newTexture);
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"CreateVertexBuffer() failed: %x",
|
||||
hr);
|
||||
return;
|
||||
}
|
||||
|
||||
PVOID targetVertexBuffer = nullptr;
|
||||
hr = newVertexBuffer->Lock(0, 0, &targetVertexBuffer, 0);
|
||||
if (FAILED(hr)) {
|
||||
SAFE_COM_RELEASE(newTexture);
|
||||
SAFE_COM_RELEASE(newVertexBuffer);
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"IDirect3DVertexBuffer9::Lock() failed: %x",
|
||||
hr);
|
||||
return;
|
||||
}
|
||||
|
||||
RtlCopyMemory(targetVertexBuffer, verts, sizeof(verts));
|
||||
|
||||
newVertexBuffer->Unlock();
|
||||
|
||||
SDL_AtomicLock(&m_OverlayLock);
|
||||
m_OverlayVertexBuffers[type] = newVertexBuffer;
|
||||
m_OverlayTextures[type] = newTexture;
|
||||
SDL_AtomicUnlock(&m_OverlayLock);
|
||||
}
|
||||
|
||||
void DXVA2Renderer::renderOverlay(Overlay::OverlayType type)
|
||||
{
|
||||
HRESULT hr;
|
||||
|
||||
if (!Session::get()->getOverlayManager().isOverlayEnabled(type)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the overlay is being updated, just skip rendering it this frame
|
||||
if (!SDL_AtomicTryLock(&m_OverlayLock)) {
|
||||
return;
|
||||
}
|
||||
|
||||
IDirect3DTexture9* overlayTexture = m_OverlayTextures[type];
|
||||
IDirect3DVertexBuffer9* overlayVertexBuffer = m_OverlayVertexBuffers[type];
|
||||
|
||||
if (overlayTexture == nullptr) {
|
||||
SDL_AtomicUnlock(&m_OverlayLock);
|
||||
return;
|
||||
}
|
||||
|
||||
// Reference these objects so they don't immediately go away if the
|
||||
// overlay update thread tries to release them.
|
||||
SDL_assert(overlayVertexBuffer != nullptr);
|
||||
overlayTexture->AddRef();
|
||||
overlayVertexBuffer->AddRef();
|
||||
|
||||
SDL_AtomicUnlock(&m_OverlayLock);
|
||||
|
||||
hr = m_Device->SetTexture(0, overlayTexture);
|
||||
if (FAILED(hr)) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"SetTexture() failed: %x",
|
||||
hr);
|
||||
goto Exit;
|
||||
}
|
||||
|
||||
hr = m_Device->SetStreamSource(0, overlayVertexBuffer, 0, sizeof(VERTEX));
|
||||
if (FAILED(hr)) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"SetStreamSource() failed: %x",
|
||||
hr);
|
||||
goto Exit;
|
||||
}
|
||||
|
||||
hr = m_Device->DrawPrimitive(D3DPT_TRIANGLEFAN, 0, 2);
|
||||
if (FAILED(hr)) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"DrawPrimitive() failed: %x",
|
||||
hr);
|
||||
goto Exit;
|
||||
}
|
||||
|
||||
Exit:
|
||||
overlayTexture->Release();
|
||||
overlayVertexBuffer->Release();
|
||||
}
|
||||
|
||||
int DXVA2Renderer::getDecoderColorspace()
|
||||
{
|
||||
if (isDXVideoProcessorAPIBlacklisted()) {
|
||||
// StretchRect() assumes Rec 601 on Intel GPUs
|
||||
// StretchRect() assumes Rec 601 on Intel and Qualcomm GPUs.
|
||||
return COLORSPACE_REC_601;
|
||||
}
|
||||
else {
|
||||
@@ -792,6 +997,11 @@ int DXVA2Renderer::getDecoderColorspace()
|
||||
|
||||
void DXVA2Renderer::renderFrame(AVFrame *frame)
|
||||
{
|
||||
if (frame == nullptr) {
|
||||
// End of stream - nothing to do for us
|
||||
return;
|
||||
}
|
||||
|
||||
IDirect3DSurface9* surface = reinterpret_cast<IDirect3DSurface9*>(frame->data[3]);
|
||||
HRESULT hr;
|
||||
|
||||
@@ -973,28 +1183,9 @@ void DXVA2Renderer::renderFrame(AVFrame *frame)
|
||||
}
|
||||
}
|
||||
|
||||
if (m_DebugOverlayFont != nullptr) {
|
||||
if (Session::get()->getOverlayManager().isOverlayEnabled(Overlay::OverlayDebug)) {
|
||||
SDL_Color color = Session::get()->getOverlayManager().getOverlayColor(Overlay::OverlayDebug);
|
||||
m_DebugOverlayFont->DrawTextA(nullptr,
|
||||
Session::get()->getOverlayManager().getOverlayText(Overlay::OverlayDebug),
|
||||
-1,
|
||||
&sample.DstRect,
|
||||
DT_LEFT | DT_NOCLIP,
|
||||
D3DCOLOR_ARGB(color.a, color.r, color.g, color.b));
|
||||
}
|
||||
}
|
||||
|
||||
if (m_StatusOverlayFont != nullptr) {
|
||||
if (Session::get()->getOverlayManager().isOverlayEnabled(Overlay::OverlayStatusUpdate)) {
|
||||
SDL_Color color = Session::get()->getOverlayManager().getOverlayColor(Overlay::OverlayStatusUpdate);
|
||||
m_StatusOverlayFont->DrawTextA(nullptr,
|
||||
Session::get()->getOverlayManager().getOverlayText(Overlay::OverlayStatusUpdate),
|
||||
-1,
|
||||
&sample.DstRect,
|
||||
DT_RIGHT | DT_NOCLIP,
|
||||
D3DCOLOR_ARGB(color.a, color.r, color.g, color.b));
|
||||
}
|
||||
// Render overlays on top of the video stream
|
||||
for (int i = 0; i < Overlay::OverlayMax; i++) {
|
||||
renderOverlay((Overlay::OverlayType)i);
|
||||
}
|
||||
|
||||
hr = m_Device->EndScene();
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
#include "pacer/pacer.h"
|
||||
|
||||
#include <d3d9.h>
|
||||
#include <d3dx9.h>
|
||||
#include <dxva2api.h>
|
||||
|
||||
extern "C" {
|
||||
@@ -17,9 +16,9 @@ public:
|
||||
DXVA2Renderer();
|
||||
virtual ~DXVA2Renderer() override;
|
||||
virtual bool initialize(PDECODER_PARAMETERS params) override;
|
||||
virtual bool prepareDecoderContext(AVCodecContext* context) override;
|
||||
virtual bool prepareDecoderContext(AVCodecContext* context, AVDictionary** options) override;
|
||||
virtual void renderFrame(AVFrame* frame) override;
|
||||
virtual void notifyOverlayUpdated(Overlay::OverlayType) override;
|
||||
virtual void notifyOverlayUpdated(Overlay::OverlayType type) override;
|
||||
virtual int getDecoderColorspace() override;
|
||||
|
||||
private:
|
||||
@@ -28,6 +27,7 @@ private:
|
||||
bool initializeDevice(SDL_Window* window, bool enableVsync);
|
||||
bool isDecoderBlacklisted();
|
||||
bool isDXVideoProcessorAPIBlacklisted();
|
||||
void renderOverlay(Overlay::OverlayType type);
|
||||
|
||||
static
|
||||
AVBufferRef* ffPoolAlloc(void* opaque, int size);
|
||||
@@ -53,6 +53,10 @@ private:
|
||||
int m_SurfacesUsed;
|
||||
AVBufferPool* m_Pool;
|
||||
|
||||
SDL_SpinLock m_OverlayLock;
|
||||
IDirect3DVertexBuffer9* m_OverlayVertexBuffers[Overlay::OverlayMax];
|
||||
IDirect3DTexture9* m_OverlayTextures[Overlay::OverlayMax];
|
||||
|
||||
IDirect3DDevice9Ex* m_Device;
|
||||
IDirect3DSurface9* m_RenderTarget;
|
||||
IDirectXVideoProcessorService* m_ProcService;
|
||||
@@ -63,7 +67,5 @@ private:
|
||||
DXVA2_ValueRange m_SaturationRange;
|
||||
DXVA2_VideoDesc m_Desc;
|
||||
REFERENCE_TIME m_FrameIndex;
|
||||
LPD3DXFONT m_DebugOverlayFont;
|
||||
LPD3DXFONT m_StatusOverlayFont;
|
||||
bool m_BlockingPresent;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
// vim: noai:ts=4:sw=4:softtabstop=4:expandtab
|
||||
#include "renderer.h"
|
||||
|
||||
#include <SDL_egl.h>
|
||||
|
||||
static QStringList egl_get_extensions(EGLDisplay dpy) {
|
||||
const auto EGLExtensionsStr = eglQueryString(dpy, EGL_EXTENSIONS);
|
||||
if (!EGLExtensionsStr) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Unable to get EGL extensions");
|
||||
return QStringList();
|
||||
}
|
||||
return QString(EGLExtensionsStr).split(" ");
|
||||
}
|
||||
|
||||
EGLExtensions::EGLExtensions(EGLDisplay dpy) :
|
||||
m_Extensions(egl_get_extensions(dpy))
|
||||
{}
|
||||
|
||||
bool EGLExtensions::isSupported(const QString &extension) const {
|
||||
return m_Extensions.contains(extension);
|
||||
}
|
||||
@@ -0,0 +1,792 @@
|
||||
// vim: noai:ts=4:sw=4:softtabstop=4:expandtab
|
||||
#include "eglvid.h"
|
||||
|
||||
#include "path.h"
|
||||
#include "streaming/session.h"
|
||||
#include "streaming/streamutils.h"
|
||||
|
||||
#include <QDir>
|
||||
|
||||
#include <Limelight.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <SDL_egl.h>
|
||||
#include <SDL_opengles2.h>
|
||||
#include <SDL_render.h>
|
||||
#include <SDL_syswm.h>
|
||||
|
||||
#ifndef EGL_VERSION_1_5
|
||||
typedef intptr_t EGLAttrib;
|
||||
#endif
|
||||
|
||||
#if !defined(EGL_VERSION_1_5) || !defined(EGL_EGL_PROTOTYPES)
|
||||
typedef EGLDisplay (EGLAPIENTRYP PFNEGLGETPLATFORMDISPLAYPROC) (EGLenum platform, void *native_display, const EGLAttrib *attrib_list);
|
||||
#endif
|
||||
|
||||
#ifndef EGL_EXT_platform_base
|
||||
typedef EGLDisplay (EGLAPIENTRYP PFNEGLGETPLATFORMDISPLAYEXTPROC) (EGLenum platform, void *native_display, const EGLint *attrib_list);
|
||||
#endif
|
||||
|
||||
// These are EGL extensions, so some platform headers may not provide them
|
||||
#ifndef EGL_PLATFORM_WAYLAND_KHR
|
||||
#define EGL_PLATFORM_WAYLAND_KHR 0x31D8
|
||||
#endif
|
||||
#ifndef EGL_PLATFORM_X11_KHR
|
||||
#define EGL_PLATFORM_X11_KHR 0x31D5
|
||||
#endif
|
||||
|
||||
typedef struct _OVERLAY_VERTEX
|
||||
{
|
||||
float x, y;
|
||||
float u, v;
|
||||
} OVERLAY_VERTEX, *POVERLAY_VERTEX;
|
||||
|
||||
/* TODO:
|
||||
* - handle more pixel formats
|
||||
* - handle software decoding
|
||||
*/
|
||||
|
||||
/* DOC/misc:
|
||||
* - https://kernel-recipes.org/en/2016/talks/video-and-colorspaces/
|
||||
* - http://www.brucelindbloom.com/
|
||||
* - https://learnopengl.com/Getting-started/Shaders
|
||||
* - https://github.com/stunpix/yuvit
|
||||
* - https://en.wikipedia.org/wiki/YCbCr#ITU-R_BT.601_conversion
|
||||
* - https://www.renesas.com/eu/en/www/doc/application-note/an9717.pdf
|
||||
* - https://www.xilinx.com/support/documentation/application_notes/xapp283.pdf
|
||||
* - https://www.itu.int/dms_pubrec/itu-r/rec/bt/R-REC-BT.709-6-201506-I!!PDF-E.pdf
|
||||
* - https://www.khronos.org/registry/OpenGL/extensions/OES/OES_EGL_image_external.txt
|
||||
* - https://gist.github.com/rexguo/6696123
|
||||
* - https://wiki.libsdl.org/CategoryVideo
|
||||
*/
|
||||
|
||||
#define EGL_LOG(Category, ...) SDL_Log ## Category(\
|
||||
SDL_LOG_CATEGORY_APPLICATION, \
|
||||
"EGLRenderer: " __VA_ARGS__)
|
||||
|
||||
SDL_Window* EGLRenderer::s_LastFailedWindow = nullptr;
|
||||
|
||||
EGLRenderer::EGLRenderer(IFFmpegRenderer *backendRenderer)
|
||||
:
|
||||
m_SwPixelFormat(AV_PIX_FMT_NONE),
|
||||
m_EGLDisplay(EGL_NO_DISPLAY),
|
||||
m_Textures{0},
|
||||
m_OverlayTextures{0},
|
||||
m_OverlayVbos{0},
|
||||
m_OverlayHasValidData{},
|
||||
m_ShaderProgram(0),
|
||||
m_OverlayShaderProgram(0),
|
||||
m_Context(0),
|
||||
m_Window(nullptr),
|
||||
m_Backend(backendRenderer),
|
||||
m_VAO(0),
|
||||
m_ColorSpace(AVCOL_SPC_NB),
|
||||
m_ColorFull(false),
|
||||
m_BlockingSwapBuffers(false),
|
||||
m_glEGLImageTargetTexture2DOES(nullptr),
|
||||
m_glGenVertexArraysOES(nullptr),
|
||||
m_glBindVertexArrayOES(nullptr),
|
||||
m_glDeleteVertexArraysOES(nullptr),
|
||||
m_DummyRenderer(nullptr)
|
||||
{
|
||||
SDL_assert(backendRenderer);
|
||||
SDL_assert(backendRenderer->canExportEGL());
|
||||
|
||||
// Save these global parameters so we can restore them in our destructor
|
||||
SDL_GL_GetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, &m_OldContextProfileMask);
|
||||
SDL_GL_GetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, &m_OldContextMajorVersion);
|
||||
SDL_GL_GetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, &m_OldContextMinorVersion);
|
||||
}
|
||||
|
||||
EGLRenderer::~EGLRenderer()
|
||||
{
|
||||
if (m_Context) {
|
||||
// Reattach the GL context to the main thread for destruction
|
||||
SDL_GL_MakeCurrent(m_Window, m_Context);
|
||||
if (m_ShaderProgram) {
|
||||
glDeleteProgram(m_ShaderProgram);
|
||||
}
|
||||
if (m_OverlayShaderProgram) {
|
||||
glDeleteProgram(m_OverlayShaderProgram);
|
||||
}
|
||||
if (m_VAO) {
|
||||
SDL_assert(m_glDeleteVertexArraysOES != nullptr);
|
||||
m_glDeleteVertexArraysOES(1, &m_VAO);
|
||||
}
|
||||
for (int i = 0; i < EGL_MAX_PLANES; i++) {
|
||||
if (m_Textures[i] != 0) {
|
||||
glDeleteTextures(1, &m_Textures[i]);
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < Overlay::OverlayMax; i++) {
|
||||
if (m_OverlayTextures[i] != 0) {
|
||||
glDeleteTextures(1, &m_OverlayTextures[i]);
|
||||
}
|
||||
if (m_OverlayVbos[i] != 0) {
|
||||
glDeleteBuffers(1, &m_OverlayVbos[i]);
|
||||
}
|
||||
}
|
||||
SDL_GL_DeleteContext(m_Context);
|
||||
}
|
||||
|
||||
if (m_DummyRenderer) {
|
||||
SDL_DestroyRenderer(m_DummyRenderer);
|
||||
}
|
||||
|
||||
// Reset the global properties back to what they were before
|
||||
SDL_SetHint(SDL_HINT_OPENGL_ES_DRIVER, "0");
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, m_OldContextProfileMask);
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, m_OldContextMajorVersion);
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, m_OldContextMinorVersion);
|
||||
}
|
||||
|
||||
bool EGLRenderer::prepareDecoderContext(AVCodecContext*, AVDictionary**)
|
||||
{
|
||||
/* Nothing to do */
|
||||
|
||||
EGL_LOG(Info, "Using EGL renderer");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void EGLRenderer::notifyOverlayUpdated(Overlay::OverlayType type)
|
||||
{
|
||||
// We handle uploading the updated overlay texture in renderOverlay().
|
||||
// notifyOverlayUpdated() is called on an arbitrary thread, which may
|
||||
// not be have the OpenGL context current on it.
|
||||
|
||||
if (!Session::get()->getOverlayManager().isOverlayEnabled(type)) {
|
||||
// If the overlay has been disabled, mark the data as invalid/stale.
|
||||
SDL_AtomicSet(&m_OverlayHasValidData[type], 0);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
bool EGLRenderer::isPixelFormatSupported(int, AVPixelFormat pixelFormat)
|
||||
{
|
||||
// Remember to keep this in sync with EGLRenderer::renderFrame()!
|
||||
switch (pixelFormat)
|
||||
{
|
||||
case AV_PIX_FMT_NV12:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void EGLRenderer::renderOverlay(Overlay::OverlayType type)
|
||||
{
|
||||
// Do nothing if this overlay is disabled
|
||||
if (!Session::get()->getOverlayManager().isOverlayEnabled(type)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Upload a new overlay texture if needed
|
||||
SDL_Surface* newSurface = Session::get()->getOverlayManager().getUpdatedOverlaySurface(type);
|
||||
if (newSurface != nullptr) {
|
||||
SDL_assert(!SDL_MUSTLOCK(newSurface));
|
||||
|
||||
glBindTexture(GL_TEXTURE_2D, m_OverlayTextures[type]);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, newSurface->w, newSurface->h, 0, GL_RGBA, GL_UNSIGNED_BYTE, newSurface->pixels);
|
||||
|
||||
// SDL_FRect wasn't added until 2.0.10
|
||||
struct {
|
||||
float x;
|
||||
float y;
|
||||
float w;
|
||||
float h;
|
||||
} overlayRect;
|
||||
|
||||
// These overlay positions differ from the other renderers because OpenGL
|
||||
// places the origin in the lower-left corner instead of the upper-left.
|
||||
if (type == Overlay::OverlayStatusUpdate) {
|
||||
// Bottom Left
|
||||
overlayRect.x = 0;
|
||||
overlayRect.y = 0;
|
||||
}
|
||||
else if (type == Overlay::OverlayDebug) {
|
||||
// Top left
|
||||
overlayRect.x = 0;
|
||||
overlayRect.y = m_ViewportHeight - newSurface->h;
|
||||
}
|
||||
|
||||
overlayRect.w = newSurface->w;
|
||||
overlayRect.h = newSurface->h;
|
||||
|
||||
SDL_FreeSurface(newSurface);
|
||||
|
||||
// Convert screen space to normalized device coordinates
|
||||
overlayRect.x /= m_ViewportWidth / 2;
|
||||
overlayRect.w /= m_ViewportWidth / 2;
|
||||
overlayRect.y /= m_ViewportHeight / 2;
|
||||
overlayRect.h /= m_ViewportHeight / 2;
|
||||
overlayRect.x -= 1.0f;
|
||||
overlayRect.y -= 1.0f;
|
||||
|
||||
OVERLAY_VERTEX verts[] =
|
||||
{
|
||||
{overlayRect.x + overlayRect.w, overlayRect.y + overlayRect.h, 1.0f, 0.0f},
|
||||
{overlayRect.x, overlayRect.y + overlayRect.h, 0.0f, 0.0f},
|
||||
{overlayRect.x, overlayRect.y, 0.0f, 1.0f},
|
||||
{overlayRect.x, overlayRect.y, 0.0f, 1.0f},
|
||||
{overlayRect.x + overlayRect.w, overlayRect.y, 1.0f, 1.0f},
|
||||
{overlayRect.x + overlayRect.w, overlayRect.y + overlayRect.h, 1.0f, 0.0f}
|
||||
};
|
||||
|
||||
glBindBuffer(GL_ARRAY_BUFFER, m_OverlayVbos[type]);
|
||||
glBufferData(GL_ARRAY_BUFFER, sizeof(verts), verts, GL_STATIC_DRAW);
|
||||
|
||||
SDL_AtomicSet(&m_OverlayHasValidData[type], 1);
|
||||
}
|
||||
|
||||
if (!SDL_AtomicGet(&m_OverlayHasValidData[type])) {
|
||||
// If the overlay is not populated yet or is stale, don't render it.
|
||||
return;
|
||||
}
|
||||
|
||||
glUseProgram(m_OverlayShaderProgram);
|
||||
|
||||
glBindBuffer(GL_ARRAY_BUFFER, m_OverlayVbos[type]);
|
||||
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(OVERLAY_VERTEX), (void*)offsetof(OVERLAY_VERTEX, x));
|
||||
glEnableVertexAttribArray(0);
|
||||
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, sizeof(OVERLAY_VERTEX), (void*)offsetof(OVERLAY_VERTEX, u));
|
||||
glEnableVertexAttribArray(1);
|
||||
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, m_OverlayTextures[type]);
|
||||
glUniform1i(m_OverlayShaderProgramParams[PARAM_TEXTURE], 0);
|
||||
|
||||
glDrawArrays(GL_TRIANGLES, 0, 6);
|
||||
}
|
||||
|
||||
int EGLRenderer::loadAndBuildShader(int shaderType,
|
||||
const char *file) {
|
||||
GLuint shader = glCreateShader(shaderType);
|
||||
if (!shader || shader == GL_INVALID_ENUM) {
|
||||
EGL_LOG(Error, "Can't create shader: %d", glGetError());
|
||||
return 0;
|
||||
}
|
||||
|
||||
auto sourceData = Path::readDataFile(file);
|
||||
GLint len = sourceData.size();
|
||||
const char *buf = sourceData.data();
|
||||
|
||||
glShaderSource(shader, 1, &buf, &len);
|
||||
glCompileShader(shader);
|
||||
GLint status;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
|
||||
if (!status) {
|
||||
char shaderLog[512];
|
||||
glGetShaderInfoLog(shader, sizeof (shaderLog), nullptr, shaderLog);
|
||||
EGL_LOG(Error, "Cannot load shader \"%s\": %s", file, shaderLog);
|
||||
return 0;
|
||||
}
|
||||
|
||||
return shader;
|
||||
}
|
||||
|
||||
bool EGLRenderer::openDisplay(unsigned int platform, void* nativeDisplay)
|
||||
{
|
||||
PFNEGLGETPLATFORMDISPLAYPROC eglGetPlatformDisplayProc;
|
||||
PFNEGLGETPLATFORMDISPLAYEXTPROC eglGetPlatformDisplayEXTProc;
|
||||
|
||||
m_EGLDisplay = EGL_NO_DISPLAY;
|
||||
|
||||
// NB: eglGetPlatformDisplay() and eglGetPlatformDisplayEXT() have slightly different definitions
|
||||
eglGetPlatformDisplayProc = (typeof(eglGetPlatformDisplayProc))eglGetProcAddress("eglGetPlatformDisplay");
|
||||
eglGetPlatformDisplayEXTProc = (typeof(eglGetPlatformDisplayEXTProc))eglGetProcAddress("eglGetPlatformDisplayEXT");
|
||||
|
||||
if (m_EGLDisplay == EGL_NO_DISPLAY) {
|
||||
// eglGetPlatformDisplay() is part of the EGL 1.5 core specification
|
||||
if (eglGetPlatformDisplayProc != nullptr) {
|
||||
m_EGLDisplay = eglGetPlatformDisplayProc(platform, nativeDisplay, nullptr);
|
||||
if (m_EGLDisplay == EGL_NO_DISPLAY) {
|
||||
EGL_LOG(Warn, "eglGetPlatformDisplay() failed: %d", eglGetError());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (m_EGLDisplay == EGL_NO_DISPLAY) {
|
||||
// eglGetPlatformDisplayEXT() is an extension for EGL 1.4
|
||||
const EGLExtensions eglExtensions(EGL_NO_DISPLAY);
|
||||
if (eglExtensions.isSupported("EGL_EXT_platform_base")) {
|
||||
if (eglGetPlatformDisplayEXTProc != nullptr) {
|
||||
m_EGLDisplay = eglGetPlatformDisplayEXTProc(platform, nativeDisplay, nullptr);
|
||||
if (m_EGLDisplay == EGL_NO_DISPLAY) {
|
||||
EGL_LOG(Warn, "eglGetPlatformDisplayEXT() failed: %d", eglGetError());
|
||||
}
|
||||
}
|
||||
else {
|
||||
EGL_LOG(Warn, "EGL_EXT_platform_base supported but no eglGetPlatformDisplayEXT() export!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (m_EGLDisplay == EGL_NO_DISPLAY) {
|
||||
// Finally, if all else fails, use eglGetDisplay()
|
||||
m_EGLDisplay = eglGetDisplay((EGLNativeDisplayType)nativeDisplay);
|
||||
if (m_EGLDisplay == EGL_NO_DISPLAY) {
|
||||
EGL_LOG(Error, "eglGetDisplay() failed: %d", eglGetError());
|
||||
}
|
||||
}
|
||||
|
||||
return m_EGLDisplay != EGL_NO_DISPLAY;
|
||||
}
|
||||
|
||||
unsigned EGLRenderer::compileShader(const char* vertexShaderSrc, const char* fragmentShaderSrc) {
|
||||
unsigned shader = 0;
|
||||
|
||||
GLuint vertexShader = loadAndBuildShader(GL_VERTEX_SHADER, vertexShaderSrc);
|
||||
if (!vertexShader)
|
||||
return false;
|
||||
|
||||
GLuint fragmentShader = loadAndBuildShader(GL_FRAGMENT_SHADER, fragmentShaderSrc);
|
||||
if (!fragmentShader)
|
||||
goto fragError;
|
||||
|
||||
shader = glCreateProgram();
|
||||
if (!shader) {
|
||||
EGL_LOG(Error, "Cannot create shader program");
|
||||
goto progFailCreate;
|
||||
}
|
||||
|
||||
glAttachShader(shader, vertexShader);
|
||||
glAttachShader(shader, fragmentShader);
|
||||
glLinkProgram(shader);
|
||||
int status;
|
||||
glGetProgramiv(shader, GL_LINK_STATUS, &status);
|
||||
if (!status) {
|
||||
char shader_log[512];
|
||||
glGetProgramInfoLog(shader, sizeof (shader_log), nullptr, shader_log);
|
||||
EGL_LOG(Error, "Cannot link shader program: %s", shader_log);
|
||||
glDeleteProgram(shader);
|
||||
shader = 0;
|
||||
}
|
||||
|
||||
progFailCreate:
|
||||
glDeleteShader(fragmentShader);
|
||||
fragError:
|
||||
glDeleteShader(vertexShader);
|
||||
return shader;
|
||||
}
|
||||
|
||||
bool EGLRenderer::compileShaders() {
|
||||
SDL_assert(!m_ShaderProgram);
|
||||
SDL_assert(!m_OverlayShaderProgram);
|
||||
|
||||
SDL_assert(m_SwPixelFormat != AV_PIX_FMT_NONE);
|
||||
|
||||
// XXX: TODO: other formats
|
||||
SDL_assert(m_SwPixelFormat == AV_PIX_FMT_NV12);
|
||||
|
||||
m_ShaderProgram = compileShader("egl.vert", "egl.frag");
|
||||
if (!m_ShaderProgram) {
|
||||
return false;
|
||||
}
|
||||
|
||||
m_ShaderProgramParams[PARAM_YUVMAT] = glGetUniformLocation(m_ShaderProgram, "yuvmat");
|
||||
m_ShaderProgramParams[PARAM_OFFSET] = glGetUniformLocation(m_ShaderProgram, "offset");
|
||||
m_ShaderProgramParams[PARAM_PLANE1] = glGetUniformLocation(m_ShaderProgram, "plane1");
|
||||
m_ShaderProgramParams[PARAM_PLANE2] = glGetUniformLocation(m_ShaderProgram, "plane2");
|
||||
|
||||
m_OverlayShaderProgram = compileShader("egl_overlay.vert", "egl_overlay.frag");
|
||||
if (!m_OverlayShaderProgram) {
|
||||
return false;
|
||||
}
|
||||
|
||||
m_OverlayShaderProgramParams[PARAM_TEXTURE] = glGetUniformLocation(m_OverlayShaderProgram, "uTexture");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool EGLRenderer::initialize(PDECODER_PARAMETERS params)
|
||||
{
|
||||
m_Window = params->window;
|
||||
|
||||
if (params->videoFormat == VIDEO_FORMAT_H265_MAIN10) {
|
||||
// EGL doesn't support rendering YUV 10-bit textures yet
|
||||
return false;
|
||||
}
|
||||
|
||||
// It's not safe to attempt to opportunistically create a GLES2
|
||||
// renderer prior to 2.0.10. If GLES2 isn't available, SDL will
|
||||
// attempt to dereference a null pointer and crash Moonlight.
|
||||
// https://bugzilla.libsdl.org/show_bug.cgi?id=4350
|
||||
// https://hg.libsdl.org/SDL/rev/84618d571795
|
||||
if (!SDL_VERSION_ATLEAST(2, 0, 10)) {
|
||||
EGL_LOG(Error, "Not supported until SDL 2.0.10");
|
||||
return false;
|
||||
}
|
||||
|
||||
// HACK: Work around bug where renderer will repeatedly fail with:
|
||||
// SDL_CreateRenderer() failed: Could not create GLES window surface
|
||||
if (m_Window == s_LastFailedWindow) {
|
||||
EGL_LOG(Error, "SDL_CreateRenderer() already failed on this window!");
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
* Create a dummy renderer in order to craft an accelerated SDL Window.
|
||||
* Request opengl ES 3.0 context, otherwise it will SIGSEGV
|
||||
* https://gitlab.freedesktop.org/mesa/mesa/issues/1011
|
||||
*/
|
||||
SDL_SetHint(SDL_HINT_OPENGL_ES_DRIVER, "1");
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES);
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0);
|
||||
|
||||
int renderIndex;
|
||||
int maxRenderers = SDL_GetNumRenderDrivers();
|
||||
SDL_assert(maxRenderers >= 0);
|
||||
|
||||
SDL_RendererInfo renderInfo;
|
||||
for (renderIndex = 0; renderIndex < maxRenderers; ++renderIndex) {
|
||||
if (SDL_GetRenderDriverInfo(renderIndex, &renderInfo))
|
||||
continue;
|
||||
if (!strcmp(renderInfo.name, "opengles2")) {
|
||||
SDL_assert(renderInfo.flags & SDL_RENDERER_ACCELERATED);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (renderIndex == maxRenderers) {
|
||||
EGL_LOG(Error, "Could not find a suitable SDL_Renderer");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!(m_DummyRenderer = SDL_CreateRenderer(m_Window, renderIndex, SDL_RENDERER_ACCELERATED))) {
|
||||
EGL_LOG(Error, "SDL_CreateRenderer() failed: %s", SDL_GetError());
|
||||
s_LastFailedWindow = m_Window;
|
||||
return false;
|
||||
}
|
||||
|
||||
SDL_SysWMinfo info;
|
||||
SDL_VERSION(&info.version);
|
||||
if (!SDL_GetWindowWMInfo(params->window, &info)) {
|
||||
EGL_LOG(Error, "SDL_GetWindowWMInfo() failed: %s", SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
switch (info.subsystem) {
|
||||
#ifdef SDL_VIDEO_DRIVER_WAYLAND
|
||||
case SDL_SYSWM_WAYLAND:
|
||||
if (!openDisplay(EGL_PLATFORM_WAYLAND_KHR, info.info.wl.display)) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
#endif
|
||||
#ifdef SDL_VIDEO_DRIVER_X11
|
||||
case SDL_SYSWM_X11:
|
||||
if (!openDisplay(EGL_PLATFORM_X11_KHR, info.info.x11.display)) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
#endif
|
||||
default:
|
||||
EGL_LOG(Error, "not compatible with SYSWM");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_EGLDisplay == EGL_NO_DISPLAY) {
|
||||
EGL_LOG(Error, "Cannot get EGL display: %d", eglGetError());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!(m_Context = SDL_GL_CreateContext(params->window))) {
|
||||
EGL_LOG(Error, "Cannot create OpenGL context: %s", SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
if (SDL_GL_MakeCurrent(params->window, m_Context)) {
|
||||
EGL_LOG(Error, "Cannot use created EGL context: %s", SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
|
||||
const EGLExtensions eglExtensions(m_EGLDisplay);
|
||||
if (!eglExtensions.isSupported("EGL_KHR_image_base") &&
|
||||
!eglExtensions.isSupported("EGL_KHR_image")) {
|
||||
EGL_LOG(Error, "EGL_KHR_image unsupported");
|
||||
return false;
|
||||
}
|
||||
else if (!SDL_GL_ExtensionSupported("GL_OES_EGL_image")) {
|
||||
EGL_LOG(Error, "GL_OES_EGL_image unsupported");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!m_Backend->initializeEGL(m_EGLDisplay, eglExtensions))
|
||||
return false;
|
||||
|
||||
if (!(m_glEGLImageTargetTexture2DOES = (typeof(m_glEGLImageTargetTexture2DOES))eglGetProcAddress("glEGLImageTargetTexture2DOES"))) {
|
||||
EGL_LOG(Error,
|
||||
"EGL: cannot retrieve `glEGLImageTargetTexture2DOES` address");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Vertex arrays are an extension on OpenGL ES 2.0
|
||||
if (SDL_GL_ExtensionSupported("GL_OES_vertex_array_object")) {
|
||||
m_glGenVertexArraysOES = (typeof(m_glGenVertexArraysOES))eglGetProcAddress("glGenVertexArraysOES");
|
||||
m_glBindVertexArrayOES = (typeof(m_glBindVertexArrayOES))eglGetProcAddress("glBindVertexArrayOES");
|
||||
m_glDeleteVertexArraysOES = (typeof(m_glDeleteVertexArraysOES))eglGetProcAddress("glDeleteVertexArraysOES");
|
||||
}
|
||||
else {
|
||||
// They are included in OpenGL ES 3.0 as part of the standard
|
||||
m_glGenVertexArraysOES = (typeof(m_glGenVertexArraysOES))eglGetProcAddress("glGenVertexArrays");
|
||||
m_glBindVertexArrayOES = (typeof(m_glBindVertexArrayOES))eglGetProcAddress("glBindVertexArray");
|
||||
m_glDeleteVertexArraysOES = (typeof(m_glDeleteVertexArraysOES))eglGetProcAddress("glDeleteVertexArrays");
|
||||
}
|
||||
|
||||
if (!m_glGenVertexArraysOES || !m_glBindVertexArrayOES || !m_glDeleteVertexArraysOES) {
|
||||
EGL_LOG(Error, "Failed to find VAO functions");
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Compute the video region size in order to keep the aspect ratio of the
|
||||
* video stream.
|
||||
*/
|
||||
SDL_Rect src, dst;
|
||||
src.x = src.y = dst.x = dst.y = 0;
|
||||
src.w = params->width;
|
||||
src.h = params->height;
|
||||
SDL_GetWindowSize(m_Window, &dst.w, &dst.h);
|
||||
StreamUtils::scaleSourceToDestinationSurface(&src, &dst);
|
||||
|
||||
glViewport(dst.x, dst.y, dst.w, dst.h);
|
||||
|
||||
m_ViewportWidth = dst.w;
|
||||
m_ViewportHeight = dst.h;
|
||||
|
||||
glClearColor(0, 0, 0, 1);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
|
||||
if (params->enableVsync) {
|
||||
SDL_GL_SetSwapInterval(1);
|
||||
m_BlockingSwapBuffers = true;
|
||||
} else {
|
||||
SDL_GL_SetSwapInterval(0);
|
||||
}
|
||||
|
||||
SDL_GL_SwapWindow(params->window);
|
||||
|
||||
glGenTextures(EGL_MAX_PLANES, m_Textures);
|
||||
for (size_t i = 0; i < EGL_MAX_PLANES; ++i) {
|
||||
glBindTexture(GL_TEXTURE_EXTERNAL_OES, m_Textures[i]);
|
||||
glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
}
|
||||
|
||||
glGenBuffers(Overlay::OverlayMax, m_OverlayVbos);
|
||||
glGenTextures(Overlay::OverlayMax, m_OverlayTextures);
|
||||
for (size_t i = 0; i < Overlay::OverlayMax; ++i) {
|
||||
glBindTexture(GL_TEXTURE_2D, m_OverlayTextures[i]);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
}
|
||||
|
||||
glEnable(GL_BLEND);
|
||||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||
|
||||
GLenum err = glGetError();
|
||||
if (err != GL_NO_ERROR)
|
||||
EGL_LOG(Error, "OpenGL error: %d", err);
|
||||
|
||||
// Detach the context from this thread, so the render thread can attach it
|
||||
SDL_GL_MakeCurrent(m_Window, nullptr);
|
||||
|
||||
return err == GL_NO_ERROR;
|
||||
}
|
||||
|
||||
const float *EGLRenderer::getColorOffsets() {
|
||||
static const float limitedOffsets[] = { 16.0f / 255.0f, 128.0f / 255.0f, 128.0f / 255.0f };
|
||||
static const float fullOffsets[] = { 0.0f, 128.0f / 255.0f, 128.0f / 255.0f };
|
||||
|
||||
return m_ColorFull ? fullOffsets : limitedOffsets;
|
||||
}
|
||||
|
||||
const float *EGLRenderer::getColorMatrix() {
|
||||
/* The conversion matrices are shamelessly stolen from linux:
|
||||
* drivers/media/platform/imx-pxp.c:pxp_setup_csc
|
||||
*/
|
||||
static const float bt601Lim[] = {
|
||||
1.1644f, 1.1644f, 1.1644f,
|
||||
0.0f, -0.3917f, 2.0172f,
|
||||
1.5960f, -0.8129f, 0.0f
|
||||
};
|
||||
static const float bt601Full[] = {
|
||||
1.0f, 1.0f, 1.0f,
|
||||
0.0f, -0.3441f, 1.7720f,
|
||||
1.4020f, -0.7141f, 0.0f
|
||||
};
|
||||
static const float bt709Lim[] = {
|
||||
1.1644f, 1.1644f, 1.1644f,
|
||||
0.0f, -0.2132f, 2.1124f,
|
||||
1.7927f, -0.5329f, 0.0f
|
||||
};
|
||||
static const float bt709Full[] = {
|
||||
1.0f, 1.0f, 1.0f,
|
||||
0.0f, -0.1873f, 1.8556f,
|
||||
1.5748f, -0.4681f, 0.0f
|
||||
};
|
||||
static const float bt2020Lim[] = {
|
||||
1.1644f, 1.1644f, 1.1644f,
|
||||
0.0f, -0.1874f, 2.1418f,
|
||||
1.6781f, -0.6505f, 0.0f
|
||||
};
|
||||
static const float bt2020Full[] = {
|
||||
1.0f, 1.0f, 1.0f,
|
||||
0.0f, -0.1646f, 1.8814f,
|
||||
1.4746f, -0.5714f, 0.0f
|
||||
};
|
||||
|
||||
switch (m_ColorSpace) {
|
||||
case AVCOL_SPC_SMPTE170M:
|
||||
case AVCOL_SPC_BT470BG:
|
||||
return m_ColorFull ? bt601Full : bt601Lim;
|
||||
case AVCOL_SPC_BT709:
|
||||
return m_ColorFull ? bt709Full : bt709Lim;
|
||||
case AVCOL_SPC_BT2020_NCL:
|
||||
case AVCOL_SPC_BT2020_CL:
|
||||
return m_ColorFull ? bt2020Full : bt2020Lim;
|
||||
default:
|
||||
SDL_assert(false);
|
||||
};
|
||||
|
||||
return bt601Lim;
|
||||
}
|
||||
|
||||
bool EGLRenderer::specialize() {
|
||||
SDL_assert(!m_VAO);
|
||||
|
||||
// Attach our GL context to the render thread
|
||||
SDL_GL_MakeCurrent(m_Window, m_Context);
|
||||
|
||||
if (!compileShaders())
|
||||
return false;
|
||||
|
||||
// The viewport should have the aspect ratio of the video stream
|
||||
static const float vertices[] = {
|
||||
// pos .... // tex coords
|
||||
1.0f, 1.0f, 1.0f, 0.0f,
|
||||
1.0f, -1.0f, 1.0f, 1.0f,
|
||||
-1.0f, -1.0f, 0.0f, 1.0f,
|
||||
-1.0f, 1.0f, 0.0f, 0.0f,
|
||||
|
||||
};
|
||||
static const unsigned int indices[] = {
|
||||
0, 1, 3,
|
||||
1, 2, 3,
|
||||
};
|
||||
|
||||
glUseProgram(m_ShaderProgram);
|
||||
|
||||
unsigned int VBO, EBO;
|
||||
m_glGenVertexArraysOES(1, &m_VAO);
|
||||
glGenBuffers(1, &VBO);
|
||||
glGenBuffers(1, &EBO);
|
||||
|
||||
m_glBindVertexArrayOES(m_VAO);
|
||||
|
||||
glBindBuffer(GL_ARRAY_BUFFER, VBO);
|
||||
glBufferData(GL_ARRAY_BUFFER, sizeof (vertices), vertices, GL_STATIC_DRAW);
|
||||
|
||||
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
|
||||
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof (indices), indices, GL_STATIC_DRAW);
|
||||
|
||||
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)0);
|
||||
glEnableVertexAttribArray(0);
|
||||
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)(2 * sizeof (float)));
|
||||
glEnableVertexAttribArray(1);
|
||||
|
||||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
m_glBindVertexArrayOES(0);
|
||||
|
||||
glDeleteBuffers(1, &VBO);
|
||||
glDeleteBuffers(1, &EBO);
|
||||
|
||||
GLenum err = glGetError();
|
||||
if (err != GL_NO_ERROR) {
|
||||
EGL_LOG(Error, "OpenGL error: %d", err);
|
||||
}
|
||||
|
||||
return err == GL_NO_ERROR;
|
||||
}
|
||||
|
||||
void EGLRenderer::renderFrame(AVFrame* frame)
|
||||
{
|
||||
EGLImage imgs[EGL_MAX_PLANES];
|
||||
|
||||
if (frame == nullptr) {
|
||||
// End of stream - unbind the GL context
|
||||
SDL_GL_MakeCurrent(m_Window, nullptr);
|
||||
return;
|
||||
}
|
||||
|
||||
if (frame->hw_frames_ctx != nullptr) {
|
||||
// Find the native read-back format and load the shader
|
||||
if (m_SwPixelFormat == AV_PIX_FMT_NONE) {
|
||||
auto hwFrameCtx = (AVHWFramesContext*)frame->hw_frames_ctx->data;
|
||||
|
||||
m_SwPixelFormat = hwFrameCtx->sw_format;
|
||||
SDL_assert(m_SwPixelFormat != AV_PIX_FMT_NONE);
|
||||
|
||||
EGL_LOG(Info, "Selected read-back format: %d", m_SwPixelFormat);
|
||||
|
||||
// XXX: TODO: Handle other pixel formats
|
||||
SDL_assert(m_SwPixelFormat == AV_PIX_FMT_NV12);
|
||||
m_ColorSpace = frame->colorspace;
|
||||
m_ColorFull = frame->color_range == AVCOL_RANGE_JPEG;
|
||||
|
||||
if (!specialize()) {
|
||||
m_SwPixelFormat = AV_PIX_FMT_NONE;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
ssize_t plane_count = m_Backend->exportEGLImages(frame, m_EGLDisplay, imgs);
|
||||
if (plane_count < 0)
|
||||
return;
|
||||
for (ssize_t i = 0; i < plane_count; ++i) {
|
||||
glActiveTexture(GL_TEXTURE0 + i);
|
||||
glBindTexture(GL_TEXTURE_EXTERNAL_OES, m_Textures[i]);
|
||||
m_glEGLImageTargetTexture2DOES(GL_TEXTURE_EXTERNAL_OES, imgs[i]);
|
||||
}
|
||||
} else {
|
||||
// TODO: load texture for SW decoding ?
|
||||
EGL_LOG(Error, "EGL rendering only supports hw frames");
|
||||
return;
|
||||
}
|
||||
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
glUseProgram(m_ShaderProgram);
|
||||
m_glBindVertexArrayOES(m_VAO);
|
||||
|
||||
glUniformMatrix3fv(m_ShaderProgramParams[PARAM_YUVMAT], 1, GL_FALSE, getColorMatrix());
|
||||
glUniform3fv(m_ShaderProgramParams[PARAM_OFFSET], 1, getColorOffsets());
|
||||
glUniform1i(m_ShaderProgramParams[PARAM_PLANE1], 0);
|
||||
glUniform1i(m_ShaderProgramParams[PARAM_PLANE2], 1);
|
||||
|
||||
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
|
||||
|
||||
m_glBindVertexArrayOES(0);
|
||||
|
||||
for (int i = 0; i < Overlay::OverlayMax; i++) {
|
||||
renderOverlay((Overlay::OverlayType)i);
|
||||
}
|
||||
|
||||
SDL_GL_SwapWindow(m_Window);
|
||||
|
||||
if (m_BlockingSwapBuffers) {
|
||||
// This glClear() forces us to block until the buffer swap is
|
||||
// complete to continue rendering. Mesa won't actually wait
|
||||
// for the swap with just glFinish() alone. Waiting here keeps us
|
||||
// in lock step with the display refresh rate. If we don't wait
|
||||
// here, we'll stall on the first GL call next frame. Doing the
|
||||
// wait here instead allows more time for a newer frame to arrive
|
||||
// for next renderFrame() call.
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
glFinish();
|
||||
}
|
||||
|
||||
if (frame->hw_frames_ctx != nullptr)
|
||||
m_Backend->freeEGLImages(m_EGLDisplay, imgs);
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
#pragma once
|
||||
|
||||
#include "renderer.h"
|
||||
|
||||
#include <SDL_opengles2.h>
|
||||
#include <SDL_opengles2_gl2ext.h>
|
||||
|
||||
class EGLRenderer : public IFFmpegRenderer {
|
||||
public:
|
||||
EGLRenderer(IFFmpegRenderer *backendRenderer);
|
||||
virtual ~EGLRenderer() override;
|
||||
virtual bool initialize(PDECODER_PARAMETERS params) override;
|
||||
virtual bool prepareDecoderContext(AVCodecContext* context, AVDictionary** options) override;
|
||||
virtual void renderFrame(AVFrame* frame) override;
|
||||
virtual void notifyOverlayUpdated(Overlay::OverlayType) override;
|
||||
virtual bool isPixelFormatSupported(int videoFormat, enum AVPixelFormat pixelFormat) override;
|
||||
|
||||
private:
|
||||
|
||||
void renderOverlay(Overlay::OverlayType type);
|
||||
unsigned compileShader(const char* vertexShaderSrc, const char* fragmentShaderSrc);
|
||||
bool compileShaders();
|
||||
bool specialize();
|
||||
const float *getColorOffsets();
|
||||
const float *getColorMatrix();
|
||||
static int loadAndBuildShader(int shaderType, const char *filename);
|
||||
bool openDisplay(unsigned int platform, void* nativeDisplay);
|
||||
|
||||
int m_ViewportWidth;
|
||||
int m_ViewportHeight;
|
||||
|
||||
int m_SwPixelFormat;
|
||||
void *m_EGLDisplay;
|
||||
unsigned m_Textures[EGL_MAX_PLANES];
|
||||
unsigned m_OverlayTextures[Overlay::OverlayMax];
|
||||
unsigned m_OverlayVbos[Overlay::OverlayMax];
|
||||
SDL_atomic_t m_OverlayHasValidData[Overlay::OverlayMax];
|
||||
unsigned m_ShaderProgram;
|
||||
unsigned m_OverlayShaderProgram;
|
||||
SDL_GLContext m_Context;
|
||||
SDL_Window *m_Window;
|
||||
IFFmpegRenderer *m_Backend;
|
||||
unsigned int m_VAO;
|
||||
int m_ColorSpace;
|
||||
bool m_ColorFull;
|
||||
bool m_BlockingSwapBuffers;
|
||||
PFNGLEGLIMAGETARGETTEXTURE2DOESPROC m_glEGLImageTargetTexture2DOES;
|
||||
PFNGLGENVERTEXARRAYSOESPROC m_glGenVertexArraysOES;
|
||||
PFNGLBINDVERTEXARRAYOESPROC m_glBindVertexArrayOES;
|
||||
PFNGLDELETEVERTEXARRAYSOESPROC m_glDeleteVertexArraysOES;
|
||||
|
||||
#define PARAM_YUVMAT 0
|
||||
#define PARAM_OFFSET 1
|
||||
#define PARAM_PLANE1 2
|
||||
#define PARAM_PLANE2 3
|
||||
int m_ShaderProgramParams[4];
|
||||
|
||||
#define PARAM_TEXTURE 0
|
||||
int m_OverlayShaderProgramParams[1];
|
||||
|
||||
int m_OldContextProfileMask;
|
||||
int m_OldContextMajorVersion;
|
||||
int m_OldContextMinorVersion;
|
||||
|
||||
SDL_Renderer *m_DummyRenderer;
|
||||
|
||||
// HACK: Work around bug where renderer will repeatedly fail with:
|
||||
// SDL_CreateRenderer() failed: Could not create GLES window surface
|
||||
static SDL_Window* s_LastFailedWindow;
|
||||
};
|
||||
@@ -5,9 +5,12 @@
|
||||
|
||||
#include <Limelight.h>
|
||||
|
||||
#include <SDL_syswm.h>
|
||||
|
||||
MmalRenderer::MmalRenderer()
|
||||
: m_Renderer(nullptr),
|
||||
m_InputPort(nullptr)
|
||||
m_InputPort(nullptr),
|
||||
m_BackgroundRenderer(nullptr)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -20,11 +23,22 @@ MmalRenderer::~MmalRenderer()
|
||||
if (m_Renderer != nullptr) {
|
||||
mmal_component_destroy(m_Renderer);
|
||||
}
|
||||
|
||||
if (m_BackgroundRenderer != nullptr) {
|
||||
SDL_DestroyRenderer(m_BackgroundRenderer);
|
||||
}
|
||||
}
|
||||
|
||||
bool MmalRenderer::prepareDecoderContext(AVCodecContext*)
|
||||
bool MmalRenderer::prepareDecoderContext(AVCodecContext* context, AVDictionary** options)
|
||||
{
|
||||
/* Nothing to do */
|
||||
// FFmpeg defaults this to 10 which is too large to fit in the default 64 MB VRAM split.
|
||||
// Reducing to 2 seems to work fine for our bitstreams (max of 1 buffered frame needed).
|
||||
av_dict_set_int(options, "extra_buffers", 2, 0);
|
||||
|
||||
// MMAL seems to dislike certain initial width and height values, but it seems okay
|
||||
// with getting zero for the width and height. We'll zero them all the time to be safe.
|
||||
context->width = 0;
|
||||
context->height = 0;
|
||||
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Using MMAL renderer");
|
||||
@@ -36,6 +50,9 @@ bool MmalRenderer::initialize(PDECODER_PARAMETERS params)
|
||||
{
|
||||
MMAL_STATUS_T status;
|
||||
|
||||
// Clear the background if possible
|
||||
setupBackground(params);
|
||||
|
||||
status = mmal_component_create(MMAL_COMPONENT_DEFAULT_VIDEO_RENDERER, &m_Renderer);
|
||||
if (status != MMAL_SUCCESS) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
@@ -70,19 +87,25 @@ bool MmalRenderer::initialize(PDECODER_PARAMETERS params)
|
||||
}
|
||||
|
||||
{
|
||||
MMAL_DISPLAYREGION_T dr;
|
||||
MMAL_DISPLAYREGION_T dr = {};
|
||||
|
||||
dr.hdr.id = MMAL_PARAMETER_DISPLAYREGION;
|
||||
dr.hdr.size = sizeof(MMAL_DISPLAYREGION_T);
|
||||
|
||||
dr.set = MMAL_DISPLAY_SET_LAYER;
|
||||
dr.layer = 128;
|
||||
|
||||
dr.set |= MMAL_DISPLAY_SET_ALPHA;
|
||||
dr.alpha = 255;
|
||||
|
||||
dr.set |= MMAL_DISPLAY_SET_FULLSCREEN;
|
||||
dr.fullscreen = (SDL_GetWindowFlags(params->window) & SDL_WINDOW_FULLSCREEN) != 0;
|
||||
dr.fullscreen = MMAL_FALSE;
|
||||
|
||||
dr.set |= MMAL_DISPLAY_SET_MODE;
|
||||
dr.mode = MMAL_DISPLAY_MODE_LETTERBOX;
|
||||
|
||||
dr.set |= MMAL_DISPLAY_SET_NOASPECT;
|
||||
dr.noaspect = MMAL_TRUE;
|
||||
|
||||
dr.set |= MMAL_DISPLAY_SET_SRC_RECT;
|
||||
dr.src_rect.x = 0;
|
||||
dr.src_rect.y = 0;
|
||||
dr.src_rect.width = params->width;
|
||||
dr.src_rect.height = params->height;
|
||||
|
||||
{
|
||||
SDL_Rect src, dst;
|
||||
@@ -100,6 +123,14 @@ bool MmalRenderer::initialize(PDECODER_PARAMETERS params)
|
||||
dr.dest_rect.width = dst.w;
|
||||
dr.dest_rect.height = dst.h;
|
||||
}
|
||||
|
||||
status = mmal_port_parameter_set(m_InputPort, &dr.hdr);
|
||||
if (status != MMAL_SUCCESS) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"mmal_port_parameter_set() failed: %x (%s)",
|
||||
status, mmal_status_to_string(status));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
status = mmal_port_enable(m_InputPort, InputPortCallback);
|
||||
@@ -113,6 +144,36 @@ bool MmalRenderer::initialize(PDECODER_PARAMETERS params)
|
||||
return true;
|
||||
}
|
||||
|
||||
void MmalRenderer::setupBackground(PDECODER_PARAMETERS params)
|
||||
{
|
||||
SDL_SysWMinfo info;
|
||||
|
||||
SDL_VERSION(&info.version);
|
||||
|
||||
if (!SDL_GetWindowWMInfo(params->window, &info)) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"SDL_GetWindowWMInfo() failed: %s",
|
||||
SDL_GetError());
|
||||
return;
|
||||
}
|
||||
|
||||
// On X11, we can safely create a renderer and draw a black background.
|
||||
// Due to conflicts with Qt, it's unsafe to do this for KMSDRM.
|
||||
if (info.subsystem == SDL_SYSWM_X11) {
|
||||
m_BackgroundRenderer = SDL_CreateRenderer(params->window, -1, SDL_RENDERER_SOFTWARE);
|
||||
if (m_BackgroundRenderer == nullptr) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"SDL_CreateRenderer() failed: %s",
|
||||
SDL_GetError());
|
||||
return;
|
||||
}
|
||||
|
||||
SDL_SetRenderDrawColor(m_BackgroundRenderer, 0, 0, 0, SDL_ALPHA_OPAQUE);
|
||||
SDL_RenderClear(m_BackgroundRenderer);
|
||||
SDL_RenderPresent(m_BackgroundRenderer);
|
||||
}
|
||||
}
|
||||
|
||||
void MmalRenderer::InputPortCallback(MMAL_PORT_T*, MMAL_BUFFER_HEADER_T* buffer)
|
||||
{
|
||||
mmal_buffer_header_release(buffer);
|
||||
@@ -125,6 +186,12 @@ enum AVPixelFormat MmalRenderer::getPreferredPixelFormat(int videoFormat)
|
||||
return AV_PIX_FMT_MMAL;
|
||||
}
|
||||
|
||||
int MmalRenderer::getRendererAttributes()
|
||||
{
|
||||
// This renderer can only draw in full-screen and maxes out at 1080p
|
||||
return RENDERER_ATTRIBUTE_FULLSCREEN_ONLY | RENDERER_ATTRIBUTE_1080P_MAX;
|
||||
}
|
||||
|
||||
bool MmalRenderer::needsTestFrame()
|
||||
{
|
||||
// We won't be able to decode if the GPU memory is 64 MB or lower,
|
||||
@@ -134,6 +201,11 @@ bool MmalRenderer::needsTestFrame()
|
||||
|
||||
void MmalRenderer::renderFrame(AVFrame* frame)
|
||||
{
|
||||
if (frame == nullptr) {
|
||||
// End of stream - nothing to do for us
|
||||
return;
|
||||
}
|
||||
|
||||
MMAL_BUFFER_HEADER_T* buffer = (MMAL_BUFFER_HEADER_T*)frame->data[3];
|
||||
MMAL_STATUS_T status;
|
||||
|
||||
|
||||
@@ -12,15 +12,20 @@ public:
|
||||
MmalRenderer();
|
||||
virtual ~MmalRenderer() override;
|
||||
virtual bool initialize(PDECODER_PARAMETERS params) override;
|
||||
virtual bool prepareDecoderContext(AVCodecContext* context) override;
|
||||
virtual bool prepareDecoderContext(AVCodecContext* context, AVDictionary** options) override;
|
||||
virtual void renderFrame(AVFrame* frame) override;
|
||||
virtual enum AVPixelFormat getPreferredPixelFormat(int videoFormat) override;
|
||||
virtual bool needsTestFrame() override;
|
||||
virtual int getRendererAttributes() override;
|
||||
|
||||
private:
|
||||
static void InputPortCallback(MMAL_PORT_T* port, MMAL_BUFFER_HEADER_T* buffer);
|
||||
|
||||
void setupBackground(PDECODER_PARAMETERS params);
|
||||
|
||||
MMAL_COMPONENT_T* m_Renderer;
|
||||
MMAL_PORT_T* m_InputPort;
|
||||
|
||||
SDL_Renderer* m_BackgroundRenderer;
|
||||
};
|
||||
|
||||
|
||||
@@ -45,6 +45,10 @@ Pacer::~Pacer()
|
||||
m_RenderQueueNotEmpty.wakeAll();
|
||||
SDL_WaitThread(m_RenderThread, nullptr);
|
||||
}
|
||||
else {
|
||||
// Send a null AVFrame to indicate end of stream on the main thread
|
||||
m_VsyncRenderer->renderFrame(nullptr);
|
||||
}
|
||||
|
||||
// Delete any remaining unconsumed frames
|
||||
while (!m_RenderQueue.isEmpty()) {
|
||||
@@ -106,6 +110,9 @@ int Pacer::renderThread(void* context)
|
||||
me->renderLastFrameAndUnlock();
|
||||
}
|
||||
|
||||
// Send a null AVFrame to indicate end of stream on the render thread
|
||||
me->m_VsyncRenderer->renderFrame(nullptr);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user