mirror of
https://github.com/moonlight-stream/moonlight-qt.git
synced 2026-10-22 16:17:20 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c523570e5 | ||
|
|
047e5bdd81 | ||
|
|
6b1559d397 | ||
|
|
cbeb23f16a | ||
|
|
1af2742013 | ||
|
|
8048938158 | ||
|
|
3689c7f982 | ||
|
|
96c7057fee | ||
|
|
df7c4c82a9 | ||
|
|
18d8db338b | ||
|
|
9637639e31 | ||
|
|
badaaece04 | ||
|
|
56119eebae | ||
|
|
c17c8e2a2c | ||
|
|
e45a60f2ed | ||
|
|
ebe9356937 | ||
|
|
0094305299 | ||
|
|
b27050d3bf | ||
|
|
d941e596ad | ||
|
|
ddaec92da5 | ||
|
|
1c0a4c92a8 | ||
|
|
383c00afbd | ||
|
|
d3b2bff8ad | ||
|
|
77aaa6141d | ||
|
|
2bb1af61d0 | ||
|
|
40b29472b3 | ||
|
|
5d5029de59 | ||
|
|
cc96e6a222 | ||
|
|
581c9438ca | ||
|
|
54bd427f26 | ||
|
|
c55d184195 | ||
|
|
b279f94789 | ||
|
|
f3a897e99a | ||
|
|
392b1b5904 | ||
|
|
4daa8066a6 | ||
|
|
8a89951286 | ||
|
|
c41ed55291 | ||
|
|
caba0499d7 | ||
|
|
13deacfc3b | ||
|
|
7231085456 | ||
|
|
663bb4aa10 | ||
|
|
18721b4542 | ||
|
|
19232722e2 | ||
|
|
d2eca68484 | ||
|
|
091167478e | ||
|
|
547b8abc56 | ||
|
|
dbfac45d59 | ||
|
|
086f6de867 | ||
|
|
0f93091848 | ||
|
|
ac19f62ed9 | ||
|
|
8fcef63890 | ||
|
|
34cd2c1afa | ||
|
|
5d1a19d49c | ||
|
|
2fbb320539 | ||
|
|
b50e5ed7e6 | ||
|
|
26c82ca72c | ||
|
|
f8a8a431a8 | ||
|
|
fd27867195 | ||
|
|
5e2a627795 | ||
|
|
e081401c2d | ||
|
|
e62a268aac | ||
|
|
140a9e5191 | ||
|
|
51b5f8046f | ||
|
|
91b965d1aa | ||
|
|
f8faccee59 | ||
|
|
6ac46a36a7 | ||
|
|
27572ba4bc | ||
|
|
21be832161 | ||
|
|
5769632793 | ||
|
|
9efd959ed8 | ||
|
|
5c72afe17e | ||
|
|
dc24319333 | ||
|
|
1acbdf94aa | ||
|
|
44229dfe51 | ||
|
|
13206227d9 | ||
|
|
e3e8a57df6 | ||
|
|
7afff80984 | ||
|
|
500522418e | ||
|
|
5ccac5a5c3 | ||
|
|
69822f4401 | ||
|
|
f290cfbd35 | ||
|
|
fae30f59ce | ||
|
|
a15f303c0a | ||
|
|
5e85038d78 | ||
|
|
4e7b35c54e | ||
|
|
48dde310ae | ||
|
|
6b041e7657 | ||
|
|
69576ee1e1 | ||
|
|
bd3105f62e | ||
|
|
02e3f0e71b | ||
|
|
74bf981f3d | ||
|
|
32f0678692 | ||
|
|
e74753bec1 | ||
|
|
4a8c9ad17f | ||
|
|
0c6c882f50 | ||
|
|
579f25c1ba | ||
|
|
d5eb5c8816 | ||
|
|
26d04b5f9c | ||
|
|
a0b9684504 | ||
|
|
402b6d1f0c | ||
|
|
e87a5fa354 | ||
|
|
63574f643e | ||
|
|
1d6b1020c3 | ||
|
|
96337a3230 | ||
|
|
950c59d8b4 | ||
|
|
9cfd095281 | ||
|
|
996e049863 | ||
|
|
6e42817c3e | ||
|
|
7758344a57 | ||
|
|
b5df566b6b | ||
|
|
e21fd19a70 | ||
|
|
98672c8e44 | ||
|
|
2e92f0640b | ||
|
|
d6b314f573 | ||
|
|
d7a150ffa8 | ||
|
|
955da6be30 | ||
|
|
b5f8e6738a | ||
|
|
d7fa8c9795 | ||
|
|
acb7a5b3c8 | ||
|
|
783a57ef89 | ||
|
|
d33ccce683 | ||
|
|
d722bf10b9 | ||
|
|
41337475a2 | ||
|
|
f504a069d7 | ||
|
|
96f79688dd | ||
|
|
c330898aeb | ||
|
|
94952cf0a4 | ||
|
|
a3c249abe9 | ||
|
|
c26562fab8 | ||
|
|
b7cbd99212 | ||
|
|
b53ef35a85 | ||
|
|
1ebb5fefb1 | ||
|
|
164b3edd41 | ||
|
|
819ea36cca | ||
|
|
088c9b9e4e | ||
|
|
ab25a9b8ee | ||
|
|
b2aea9f8f0 | ||
|
|
5399b95936 | ||
|
|
1d5c120748 | ||
|
|
6be7301e69 | ||
|
|
fff9f2b17d | ||
|
|
87a7d2e45c | ||
|
|
58803ef40d | ||
|
|
b27ca993aa | ||
|
|
28ecc6bcbf | ||
|
|
30c5e3f237 | ||
|
|
6a25a3a3d0 | ||
|
|
9a84c0ae30 | ||
|
|
133cc5457d | ||
|
|
05e4c5b507 | ||
|
|
8372717a09 | ||
|
|
5f3f2a96fd | ||
|
|
b83c6f0c28 | ||
|
|
36dc0f3e3c | ||
|
|
b4edde6f90 | ||
|
|
395eb5581c | ||
|
|
6dab251055 | ||
|
|
3bd6a6a614 | ||
|
|
93d21a0d1d | ||
|
|
2c2a73e320 | ||
|
|
a88a3c9ea3 | ||
|
|
e4df70cd56 | ||
|
|
90f7d35150 | ||
|
|
75b19a60c1 | ||
|
|
9fc921f49f | ||
|
|
4b9e90ac72 | ||
|
|
36dd6c83e0 | ||
|
|
d790313221 | ||
|
|
c3895f06c0 | ||
|
|
76e81fa651 | ||
|
|
d63a1b0eb0 | ||
|
|
4dffac3c78 | ||
|
|
88391b0274 | ||
|
|
c3cea0238e | ||
|
|
5bf4266973 | ||
|
|
ed57c54d06 | ||
|
|
01936e000e | ||
|
|
80137848dc | ||
|
|
149575e0c4 | ||
|
|
6a66b462ce | ||
|
|
1e31f6fe77 | ||
|
|
f173b3ff09 | ||
|
|
baffab62ce | ||
|
|
d141cec2a8 | ||
|
|
df87ac3810 | ||
|
|
46960ce0a4 | ||
|
|
522206b0bc | ||
|
|
24d60dd3ab | ||
|
|
dbfdc2fd14 | ||
|
|
a97912e297 | ||
|
|
cedba501ee | ||
|
|
ac947d3bda | ||
|
|
243ba371b3 | ||
|
|
3af24eafbb | ||
|
|
69b6022afe | ||
|
|
1cd66419d4 | ||
|
|
5309a02f6d | ||
|
|
541784382a | ||
|
|
2a7fae2c82 | ||
|
|
ed3a5448b1 | ||
|
|
2eb4769bbc | ||
|
|
02019000df | ||
|
|
6262f51c96 | ||
|
|
d38ff5759a | ||
|
|
f6de2e4dd3 | ||
|
|
3959f447d4 |
No files matched your search
@@ -1,10 +1,12 @@
|
|||||||
---
|
---
|
||||||
name: Bug report
|
name: Bug report
|
||||||
about: Ask for help on Discord before reporting a bug
|
about: Follow the troubleshooting guide before reporting a bug
|
||||||
|
|
||||||
---
|
---
|
||||||
**READ ME FIRST!**
|
**READ ME FIRST!**
|
||||||
This issue tracker is not the right place to ask for troubleshooting help if you're not able to connect or stream at all. For those requests, please use the #pc-help channel on our Discord server where there are many more people who can help (or direct you back here if it looks like a Moonlight bug after all). https://moonlight-stream.org/discord
|
If you're here because something basic is not working (like gamepad input, video, or similar), it's probably something specific to your setup, so make sure you've gone through the Troubleshooting Guide first: https://github.com/moonlight-stream/moonlight-docs/wiki/Troubleshooting
|
||||||
|
|
||||||
|
If you still have trouble with basic functionality after following the guide, join our Discord server where there are many other volunteers who can help (or direct you back here if it looks like a Moonlight bug after all). https://moonlight-stream.org/discord
|
||||||
|
|
||||||
**Describe the bug**
|
**Describe the bug**
|
||||||
A clear and concise description of what the bug is.
|
A clear and concise description of what the bug is.
|
||||||
@@ -26,14 +28,21 @@ List the games you've tried that exhibit the issue. To see if the issue is game-
|
|||||||
- If so, which settings have been changed?
|
- If so, which settings have been changed?
|
||||||
- Does the problem still occur after reverting settings back to default?
|
- Does the problem still occur after reverting settings back to default?
|
||||||
|
|
||||||
|
**Gamepad-related issues (please complete if problem is gamepad-related)**
|
||||||
|
- Do you have any gamepads connected to your host PC directly?
|
||||||
|
- Does the problem still remain if you stream the desktop and use https://html5gamepad.com to test your gamepad?
|
||||||
|
- Instructions for streaming the desktop can be found here: https://github.com/moonlight-stream/moonlight-docs/wiki/Setup-Guide
|
||||||
|
|
||||||
**Client PC details (please complete the following information)**
|
**Client PC details (please complete the following information)**
|
||||||
- OS: [e.g. Windows 10 1809]
|
- OS: [e.g. Windows 10 1809]
|
||||||
- Moonlight Version: [e.g. v0.9.0]
|
- Moonlight Version: [e.g. v0.9.0]
|
||||||
|
- GPU: [e.g. Intel HD Graphics 520]
|
||||||
|
|
||||||
**Server PC details (please complete the following information)**
|
**Server PC details (please complete the following information)**
|
||||||
- OS: [e.g. Windows 10 1809]
|
- OS: [e.g. Windows 10 1809]
|
||||||
- GeForce Experience version: [e.g. 3.16.0.140]
|
- GeForce Experience version: [e.g. 3.16.0.140]
|
||||||
- Nvidia GPU driver: [e.g. 417.35]
|
- Nvidia GPU driver: [e.g. 417.35]
|
||||||
|
- Antivirus and firewall software: [e.g. Windows Defender and Windows Firewall]
|
||||||
|
|
||||||
**Moonlight Logs (please attach)**
|
**Moonlight Logs (please attach)**
|
||||||
- On Windows, `Moonlight-###.log` files can be found in `%TEMP%`. Simply type that into the File Explorer path field to navigate there.
|
- On Windows, `Moonlight-###.log` files can be found in `%TEMP%`. Simply type that into the File Explorer path field to navigate there.
|
||||||
|
|||||||
@@ -13,3 +13,7 @@
|
|||||||
[submodule "h264bitstream/h264bitstream"]
|
[submodule "h264bitstream/h264bitstream"]
|
||||||
path = h264bitstream/h264bitstream
|
path = h264bitstream/h264bitstream
|
||||||
url = https://github.com/aizvorski/h264bitstream.git
|
url = https://github.com/aizvorski/h264bitstream.git
|
||||||
|
[submodule "libs"]
|
||||||
|
path = libs
|
||||||
|
url = https://github.com/cgutman/moonlight-qt-prebuilts.git
|
||||||
|
shallow = true
|
||||||
@@ -4,10 +4,11 @@
|
|||||||
|
|
||||||
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://moonlight-stream.org/discord).
|
You can follow development on our [Discord server](https://moonlight-stream.org/discord) and help translate Moonlight into your language on [Weblate](https://hosted.weblate.org/projects/moonlight/moonlight-qt/).
|
||||||
|
|
||||||
[](https://ci.appveyor.com/project/cgutman/moonlight-qt/branch/master)
|
[](https://ci.appveyor.com/project/cgutman/moonlight-qt/branch/master)
|
||||||
[](https://github.com/moonlight-stream/moonlight-qt/releases)
|
[](https://github.com/moonlight-stream/moonlight-qt/releases)
|
||||||
|
[](https://hosted.weblate.org/projects/moonlight/moonlight-qt/)
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
- Hardware accelerated video decoding on Windows, Mac, and Linux
|
- Hardware accelerated video decoding on Windows, Mac, and Linux
|
||||||
@@ -15,7 +16,7 @@ You can follow development on our [Discord server](https://moonlight-stream.org/
|
|||||||
- Supports streaming at 720p, 1080p, 1440p, 4K, and the client PC's native screen resolution
|
- Supports streaming at 720p, 1080p, 1440p, 4K, and the client PC's native screen resolution
|
||||||
- 7.1 surround sound audio support
|
- 7.1 surround sound audio support
|
||||||
- Support for both pointer capture (for games) and direct mouse control (for remote desktop)
|
- 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
|
- Support for passing system-wide keyboard shortcuts like Alt+Tab to the host
|
||||||
- Direct touchscreen mouse control
|
- Direct touchscreen mouse control
|
||||||
- HEVC support for better image quality at reduced bandwidth
|
- HEVC support for better image quality at reduced bandwidth
|
||||||
- Gamepad support with force feedback for up to 4 players
|
- Gamepad support with force feedback for up to 4 players
|
||||||
@@ -28,6 +29,12 @@ You can follow development on our [Discord server](https://moonlight-stream.org/
|
|||||||
- [Raspberry Pi 4](https://github.com/moonlight-stream/moonlight-docs/wiki/Installing-Moonlight-Qt-on-Raspberry-Pi-4)
|
- [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)
|
- [NVIDIA Jetson and Nintendo Switch (Ubuntu L4T)](https://github.com/moonlight-stream/moonlight-docs/wiki/Installing-Moonlight-Qt-on-Linux4Tegra-(L4T)-Ubuntu)
|
||||||
|
|
||||||
|
#### Special Thanks
|
||||||
|
|
||||||
|
[](https://cloudsmith.com)
|
||||||
|
|
||||||
|
Hosting for Moonlight's Raspberry Pi and L4T package repositories is graciously provided for free by [Cloudsmith](https://cloudsmith.com).
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
|
|
||||||
### Windows Build Requirements
|
### Windows Build Requirements
|
||||||
@@ -49,7 +56,7 @@ You can follow development on our [Discord server](https://moonlight-stream.org/
|
|||||||
* GCC or Clang
|
* GCC or Clang
|
||||||
* Install the required packages:
|
* Install the required packages:
|
||||||
* Debian/Ubuntu: `libegl1-mesa-dev libgl1-mesa-dev libopus-dev libqt5svg5-dev libsdl2-dev libsdl2-ttf-dev libssl-dev libavcodec-dev libva-dev libvdpau-dev libxkbcommon-dev qt5-default qt5-qmake qtbase5-dev qtdeclarative5-dev qtquickcontrols2-5-dev wayland-protocols qml-module-qtquick-controls2 qml-module-qtquick-layouts qml-module-qtquick-window2 qml-module-qtquick2`
|
* Debian/Ubuntu: `libegl1-mesa-dev libgl1-mesa-dev libopus-dev libqt5svg5-dev libsdl2-dev libsdl2-ttf-dev libssl-dev libavcodec-dev libva-dev libvdpau-dev libxkbcommon-dev 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`
|
* RedHat/Fedora: `openssl-devel SDL2-devel SDL2_ttf-devel ffmpeg-devel qt5-qtsvg-devel qt5-qtquickcontrols2-devel libva-devel libvdpau-devel opus-devel pulseaudio-libs-devel alsa-lib-devel`
|
||||||
* FFmpeg 4.0+ is required to build. If your distro doesn't package FFmpeg 4.0 or later, you can build and install it from source on https://ffmpeg.org/
|
* FFmpeg 4.0+ is required to build. If your distro doesn't package FFmpeg 4.0 or later, you can build and install it from source on https://ffmpeg.org/
|
||||||
|
|
||||||
### Steam Link Build Requirements
|
### Steam Link Build Requirements
|
||||||
@@ -68,6 +75,8 @@ You can follow development on our [Discord server](https://moonlight-stream.org/
|
|||||||
* 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 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.
|
* For Steam Link builds, run `scripts/build-steamlink-app.sh` from the root of the repository.
|
||||||
* To build from the command line for development use, run `qmake moonlight-qt.pro` then `make debug` or `make release`
|
* To build from the command line for development use, run `qmake moonlight-qt.pro` then `make debug` or `make release`
|
||||||
|
* To create an embedded build for a single-purpose device, use `qmake "CONFIG+=embedded" moonlight-qt.pro` and build normally.
|
||||||
|
* This build will lack windowed mode, Discord/Help links, and other features that don't make sense on an embedded device.
|
||||||
|
|
||||||
## Contribute
|
## Contribute
|
||||||
1. Fork us
|
1. Fork us
|
||||||
|
|||||||
+1
-1
Submodule app/SDL_GameControllerDB updated: f0ae9dea50...376c576317.
+26
-3
@@ -45,7 +45,7 @@ win32 {
|
|||||||
LIBS += -L$$PWD/../libs/windows/lib/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 qwave.lib
|
LIBS += ws2_32.lib winmm.lib dxva2.lib ole32.lib gdi32.lib user32.lib d3d9.lib dwmapi.lib dbghelp.lib
|
||||||
}
|
}
|
||||||
macx {
|
macx {
|
||||||
INCLUDEPATH += $$PWD/../libs/mac/include
|
INCLUDEPATH += $$PWD/../libs/mac/include
|
||||||
@@ -131,9 +131,11 @@ SOURCES += \
|
|||||||
cli/commandlineparser.cpp \
|
cli/commandlineparser.cpp \
|
||||||
cli/quitstream.cpp \
|
cli/quitstream.cpp \
|
||||||
cli/startstream.cpp \
|
cli/startstream.cpp \
|
||||||
|
settings/compatfetcher.cpp \
|
||||||
settings/mappingfetcher.cpp \
|
settings/mappingfetcher.cpp \
|
||||||
settings/streamingpreferences.cpp \
|
settings/streamingpreferences.cpp \
|
||||||
streaming/input/abstouch.cpp \
|
streaming/input/abstouch.cpp \
|
||||||
|
streaming/input/clipboard.cpp \
|
||||||
streaming/input/gamepad.cpp \
|
streaming/input/gamepad.cpp \
|
||||||
streaming/input/input.cpp \
|
streaming/input/input.cpp \
|
||||||
streaming/input/keyboard.cpp \
|
streaming/input/keyboard.cpp \
|
||||||
@@ -155,6 +157,7 @@ SOURCES += \
|
|||||||
|
|
||||||
HEADERS += \
|
HEADERS += \
|
||||||
backend/nvapp.h \
|
backend/nvapp.h \
|
||||||
|
settings/compatfetcher.h \
|
||||||
settings/mappingfetcher.h \
|
settings/mappingfetcher.h \
|
||||||
utils.h \
|
utils.h \
|
||||||
backend/computerseeker.h \
|
backend/computerseeker.h \
|
||||||
@@ -250,6 +253,12 @@ libdrm {
|
|||||||
DEFINES += HAVE_DRM
|
DEFINES += HAVE_DRM
|
||||||
SOURCES += streaming/video/ffmpeg-renderers/drm.cpp
|
SOURCES += streaming/video/ffmpeg-renderers/drm.cpp
|
||||||
HEADERS += streaming/video/ffmpeg-renderers/drm.h
|
HEADERS += streaming/video/ffmpeg-renderers/drm.h
|
||||||
|
|
||||||
|
linux {
|
||||||
|
message(Master hooks enabled)
|
||||||
|
SOURCES += masterhook.c
|
||||||
|
LIBS += -ldl
|
||||||
|
}
|
||||||
}
|
}
|
||||||
config_EGL {
|
config_EGL {
|
||||||
message(EGL renderer selected)
|
message(EGL renderer selected)
|
||||||
@@ -264,7 +273,7 @@ config_EGL {
|
|||||||
config_SL {
|
config_SL {
|
||||||
message(Steam Link build configuration selected)
|
message(Steam Link build configuration selected)
|
||||||
|
|
||||||
DEFINES += STEAM_LINK HAVE_SLVIDEO HAVE_SLAUDIO
|
DEFINES += EMBEDDED_BUILD STEAM_LINK HAVE_SLVIDEO HAVE_SLAUDIO
|
||||||
LIBS += -lSLVideo -lSLAudio
|
LIBS += -lSLVideo -lSLAudio
|
||||||
|
|
||||||
SOURCES += \
|
SOURCES += \
|
||||||
@@ -307,13 +316,27 @@ discord-rpc {
|
|||||||
LIBS += -ldiscord-rpc
|
LIBS += -ldiscord-rpc
|
||||||
DEFINES += HAVE_DISCORD
|
DEFINES += HAVE_DISCORD
|
||||||
}
|
}
|
||||||
|
embedded {
|
||||||
|
message(Embedded build)
|
||||||
|
|
||||||
|
DEFINES += EMBEDDED_BUILD
|
||||||
|
}
|
||||||
|
|
||||||
RESOURCES += \
|
RESOURCES += \
|
||||||
resources.qrc \
|
resources.qrc \
|
||||||
qml.qrc
|
qml.qrc
|
||||||
|
|
||||||
TRANSLATIONS += \
|
TRANSLATIONS += \
|
||||||
languages/qml_fr.ts
|
languages/qml_zh_cn.ts \
|
||||||
|
languages/qml_de.ts \
|
||||||
|
languages/qml_fr.ts \
|
||||||
|
languages/qml_nb_NO.ts \
|
||||||
|
languages/qml_ru.ts \
|
||||||
|
languages/qml_es.ts \
|
||||||
|
languages/qml_ja.ts \
|
||||||
|
languages/qml_vi.ts \
|
||||||
|
languages/qml_th.ts \
|
||||||
|
languages/qml_ko.ts
|
||||||
|
|
||||||
# Additional import path used to resolve QML modules in Qt Creator's code model
|
# Additional import path used to resolve QML modules in Qt Creator's code model
|
||||||
QML_IMPORT_PATH =
|
QML_IMPORT_PATH =
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ AutoUpdateChecker::AutoUpdateChecker(QObject *parent) :
|
|||||||
// Allow HTTP redirects
|
// Allow HTTP redirects
|
||||||
m_Nam.setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy);
|
m_Nam.setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy);
|
||||||
|
|
||||||
connect(&m_Nam, SIGNAL(finished(QNetworkReply*)),
|
connect(&m_Nam, &QNetworkAccessManager::finished,
|
||||||
this, SLOT(handleUpdateCheckRequestFinished(QNetworkReply*)));
|
this, &AutoUpdateChecker::handleUpdateCheckRequestFinished);
|
||||||
|
|
||||||
QString currentVersion(VERSION_STR);
|
QString currentVersion(VERSION_STR);
|
||||||
qDebug() << "Current Moonlight version:" << currentVersion;
|
qDebug() << "Current Moonlight version:" << currentVersion;
|
||||||
@@ -63,11 +63,41 @@ QString AutoUpdateChecker::getPlatform()
|
|||||||
return QStringLiteral("steamlink");
|
return QStringLiteral("steamlink");
|
||||||
#elif defined(APP_IMAGE)
|
#elif defined(APP_IMAGE)
|
||||||
return QStringLiteral("appimage");
|
return QStringLiteral("appimage");
|
||||||
|
#elif defined(Q_OS_DARWIN) && QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||||
|
// Qt 6 changed this from 'osx' to 'macos'. Use the old one
|
||||||
|
// to be consistent (and not require another entry in the manifest).
|
||||||
|
return QStringLiteral("osx");
|
||||||
#else
|
#else
|
||||||
return QSysInfo::productType();
|
return QSysInfo::productType();
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int AutoUpdateChecker::compareVersion(QVector<int>& version1, QVector<int>& version2) {
|
||||||
|
for (int i = 0;; i++) {
|
||||||
|
int v1Val = 0;
|
||||||
|
int v2Val = 0;
|
||||||
|
|
||||||
|
// Treat missing decimal places as 0
|
||||||
|
if (i < version1.count()) {
|
||||||
|
v1Val = version1[i];
|
||||||
|
}
|
||||||
|
if (i < version2.count()) {
|
||||||
|
v2Val = version2[i];
|
||||||
|
}
|
||||||
|
if (i >= version1.count() && i >= version2.count()) {
|
||||||
|
// Equal versions
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (v1Val < v2Val) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
else if (v1Val > v2Val) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void AutoUpdateChecker::handleUpdateCheckRequestFinished(QNetworkReply* reply)
|
void AutoUpdateChecker::handleUpdateCheckRequestFinished(QNetworkReply* reply)
|
||||||
{
|
{
|
||||||
Q_ASSERT(reply->isFinished());
|
Q_ASSERT(reply->isFinished());
|
||||||
@@ -119,46 +149,47 @@ void AutoUpdateChecker::handleUpdateCheckRequestFinished(QNetworkReply* reply)
|
|||||||
|
|
||||||
if (updateObj["arch"] == QSysInfo::buildCpuArchitecture() &&
|
if (updateObj["arch"] == QSysInfo::buildCpuArchitecture() &&
|
||||||
updateObj["platform"] == getPlatform()) {
|
updateObj["platform"] == getPlatform()) {
|
||||||
|
|
||||||
|
// Check the kernel version minimum if one exists
|
||||||
|
if (updateObj.contains("kernel_version_at_least") && updateObj["kernel_version_at_least"].isString()) {
|
||||||
|
QVector<int> requiredVersionQuad;
|
||||||
|
QVector<int> actualVersionQuad;
|
||||||
|
|
||||||
|
QString requiredVersion = updateObj["kernel_version_at_least"].toString();
|
||||||
|
QString actualVersion = QSysInfo::kernelVersion();
|
||||||
|
parseStringToVersionQuad(requiredVersion, requiredVersionQuad);
|
||||||
|
parseStringToVersionQuad(actualVersion, actualVersionQuad);
|
||||||
|
|
||||||
|
if (compareVersion(actualVersionQuad, requiredVersionQuad) < 0) {
|
||||||
|
qDebug() << "Skipping manifest entry due to kernel version (" << actualVersion << "<" << requiredVersion << ")";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
qDebug() << "Found update manifest match for current platform";
|
qDebug() << "Found update manifest match for current platform";
|
||||||
|
|
||||||
QString latestVersion = updateObj["version"].toString();
|
QString latestVersion = updateObj["version"].toString();
|
||||||
qDebug() << "Latest version of Moonlight for this platform is:" << latestVersion;
|
qDebug() << "Latest version of Moonlight for this platform is:" << latestVersion;
|
||||||
|
|
||||||
QVector<int> latestVersionQuad;
|
QVector<int> latestVersionQuad;
|
||||||
|
|
||||||
parseStringToVersionQuad(latestVersion, latestVersionQuad);
|
parseStringToVersionQuad(latestVersion, latestVersionQuad);
|
||||||
|
|
||||||
for (int i = 0;; i++) {
|
int res = compareVersion(m_CurrentVersionQuad, latestVersionQuad);
|
||||||
int latestVer = 0;
|
if (res < 0) {
|
||||||
int currentVer = 0;
|
// m_CurrentVersionQuad < latestVersionQuad
|
||||||
|
qDebug() << "Update available";
|
||||||
// Treat missing decimal places as 0
|
emit onUpdateAvailable(updateObj["version"].toString(),
|
||||||
if (i < latestVersionQuad.count()) {
|
updateObj["browser_url"].toString());
|
||||||
latestVer = latestVersionQuad[i];
|
return;
|
||||||
}
|
}
|
||||||
if (i < m_CurrentVersionQuad.count()) {
|
else if (res > 0) {
|
||||||
currentVer = m_CurrentVersionQuad[i];
|
qDebug() << "Update manifest version lower than current version";
|
||||||
}
|
return;
|
||||||
if (i >= latestVersionQuad.count() && i >= m_CurrentVersionQuad.count()) {
|
}
|
||||||
break;
|
else {
|
||||||
}
|
qDebug() << "Update manifest version equal to current version";
|
||||||
|
return;
|
||||||
if (currentVer < latestVer) {
|
|
||||||
qDebug() << "Update available";
|
|
||||||
|
|
||||||
emit onUpdateAvailable(updateObj["version"].toString(),
|
|
||||||
updateObj["browser_url"].toString());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
else if (currentVer > latestVer) {
|
|
||||||
qDebug() << "Update manifest version lower than current version";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
qDebug() << "Update manifest version equal to current version";
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -166,8 +197,8 @@ void AutoUpdateChecker::handleUpdateCheckRequestFinished(QNetworkReply* reply)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
qWarning() << "No entry in update manifest found for current platform: "
|
qWarning() << "No entry in update manifest found for current platform:"
|
||||||
<< QSysInfo::buildCpuArchitecture() << getPlatform();
|
<< QSysInfo::buildCpuArchitecture() << getPlatform() << QSysInfo::kernelVersion();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
qWarning() << "Update checking failed with error: " << reply->error();
|
qWarning() << "Update checking failed with error: " << reply->error();
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ private slots:
|
|||||||
private:
|
private:
|
||||||
void parseStringToVersionQuad(QString& string, QVector<int>& version);
|
void parseStringToVersionQuad(QString& string, QVector<int>& version);
|
||||||
|
|
||||||
|
int compareVersion(QVector<int>& version1, QVector<int>& version2);
|
||||||
|
|
||||||
QString getPlatform();
|
QString getPlatform();
|
||||||
|
|
||||||
QVector<int> m_CurrentVersionQuad;
|
QVector<int> m_CurrentVersionQuad;
|
||||||
|
|||||||
@@ -46,8 +46,8 @@ public:
|
|||||||
m_Computer(computer),
|
m_Computer(computer),
|
||||||
m_App(app)
|
m_App(app)
|
||||||
{
|
{
|
||||||
connect(this, SIGNAL(boxArtFetchCompleted(NvComputer*,NvApp,QUrl)),
|
connect(this, &NetworkBoxArtLoadTask::boxArtFetchCompleted,
|
||||||
boxArtManager, SLOT(handleBoxArtLoadComplete(NvComputer*,NvApp,QUrl)));
|
boxArtManager, &BoxArtManager::handleBoxArtLoadComplete);
|
||||||
}
|
}
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
|
|||||||
@@ -146,7 +146,8 @@ private:
|
|||||||
ComputerManager::ComputerManager(QObject *parent)
|
ComputerManager::ComputerManager(QObject *parent)
|
||||||
: QObject(parent),
|
: QObject(parent),
|
||||||
m_PollingRef(0),
|
m_PollingRef(0),
|
||||||
m_MdnsBrowser(nullptr)
|
m_MdnsBrowser(nullptr),
|
||||||
|
m_CompatFetcher(nullptr)
|
||||||
{
|
{
|
||||||
QSettings settings;
|
QSettings settings;
|
||||||
|
|
||||||
@@ -159,12 +160,15 @@ ComputerManager::ComputerManager(QObject *parent)
|
|||||||
}
|
}
|
||||||
settings.endArray();
|
settings.endArray();
|
||||||
|
|
||||||
|
// Fetch latest compatibility data asynchronously
|
||||||
|
m_CompatFetcher.start();
|
||||||
|
|
||||||
// To quit in a timely manner, we must block additional requests
|
// To quit in a timely manner, we must block additional requests
|
||||||
// after we receive the aboutToQuit() signal. This is neccessary
|
// after we receive the aboutToQuit() signal. This is neccessary
|
||||||
// because NvHTTP uses aboutToQuit() to abort requests in progres
|
// because NvHTTP uses aboutToQuit() to abort requests in progres
|
||||||
// while quitting, however this is a one time signal - additional
|
// while quitting, however this is a one time signal - additional
|
||||||
// requests would not be aborted and block termination.
|
// requests would not be aborted and block termination.
|
||||||
connect(QCoreApplication::instance(), SIGNAL(aboutToQuit()), this, SLOT(handleAboutToQuit()));
|
connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this, &ComputerManager::handleAboutToQuit);
|
||||||
}
|
}
|
||||||
|
|
||||||
ComputerManager::~ComputerManager()
|
ComputerManager::~ComputerManager()
|
||||||
@@ -302,8 +306,8 @@ void ComputerManager::startPollingComputer(NvComputer* computer)
|
|||||||
|
|
||||||
if (!pollingEntry->isActive()) {
|
if (!pollingEntry->isActive()) {
|
||||||
PcMonitorThread* thread = new PcMonitorThread(computer);
|
PcMonitorThread* thread = new PcMonitorThread(computer);
|
||||||
connect(thread, SIGNAL(computerStateChanged(NvComputer*)),
|
connect(thread, &PcMonitorThread::computerStateChanged,
|
||||||
this, SLOT(handleComputerStateChanged(NvComputer*)));
|
this, &ComputerManager::handleComputerStateChanged);
|
||||||
pollingEntry->setActiveThread(thread);
|
pollingEntry->setActiveThread(thread);
|
||||||
thread->start();
|
thread->start();
|
||||||
}
|
}
|
||||||
@@ -337,7 +341,6 @@ void ComputerManager::handleMdnsServiceResolved(MdnsPendingComputer* computer,
|
|||||||
address.isInSubnet(QHostAddress("fec0::"), 10) ||
|
address.isInSubnet(QHostAddress("fec0::"), 10) ||
|
||||||
address.isInSubnet(QHostAddress("fc00::"), 7)) {
|
address.isInSubnet(QHostAddress("fc00::"), 7)) {
|
||||||
addNewHost(address.toString(), true, v6Global);
|
addNewHost(address.toString(), true, v6Global);
|
||||||
added = true;
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -418,7 +421,7 @@ void ComputerManager::deleteHost(NvComputer* computer)
|
|||||||
void ComputerManager::renameHost(NvComputer* computer, QString name)
|
void ComputerManager::renameHost(NvComputer* computer, QString name)
|
||||||
{
|
{
|
||||||
{
|
{
|
||||||
QWriteLocker(&computer->lock);
|
QWriteLocker lock(&computer->lock);
|
||||||
|
|
||||||
computer->name = name;
|
computer->name = name;
|
||||||
computer->hasCustomName = true;
|
computer->hasCustomName = true;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include "nvcomputer.h"
|
#include "nvcomputer.h"
|
||||||
#include "nvpairingmanager.h"
|
#include "nvpairingmanager.h"
|
||||||
|
#include "settings/compatfetcher.h"
|
||||||
|
|
||||||
#include <qmdnsengine/server.h>
|
#include <qmdnsengine/server.h>
|
||||||
#include <qmdnsengine/cache.h>
|
#include <qmdnsengine/cache.h>
|
||||||
@@ -70,7 +71,7 @@ private:
|
|||||||
m_Resolver = new QMdnsEngine::Resolver(m_Server, m_Hostname);
|
m_Resolver = new QMdnsEngine::Resolver(m_Server, m_Hostname);
|
||||||
connect(m_Resolver, &QMdnsEngine::Resolver::resolved,
|
connect(m_Resolver, &QMdnsEngine::Resolver::resolved,
|
||||||
this, &MdnsPendingComputer::handleResolvedAddress);
|
this, &MdnsPendingComputer::handleResolvedAddress);
|
||||||
QTimer::singleShot(2000, this, SLOT(handleResolvedTimeout()));
|
QTimer::singleShot(2000, this, &MdnsPendingComputer::handleResolvedTimeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
QByteArray m_Hostname;
|
QByteArray m_Hostname;
|
||||||
@@ -214,4 +215,5 @@ private:
|
|||||||
QMdnsEngine::Browser* m_MdnsBrowser;
|
QMdnsEngine::Browser* m_MdnsBrowser;
|
||||||
QMdnsEngine::Cache m_MdnsCache;
|
QMdnsEngine::Cache m_MdnsCache;
|
||||||
QVector<MdnsPendingComputer*> m_PendingResolution;
|
QVector<MdnsPendingComputer*> m_PendingResolution;
|
||||||
|
CompatFetcher m_CompatFetcher;
|
||||||
};
|
};
|
||||||
@@ -143,7 +143,10 @@ QSslKey
|
|||||||
IdentityManager::getSslKey()
|
IdentityManager::getSslKey()
|
||||||
{
|
{
|
||||||
if (m_CachedSslKey.isNull()) {
|
if (m_CachedSslKey.isNull()) {
|
||||||
BIO* bio = BIO_new_mem_buf(m_CachedPrivateKey.data(), -1);
|
// This seemingly useless const_cast is required for old OpenSSL headers
|
||||||
|
// where BIO_new_mem_buf's parameter is not declared const like those on
|
||||||
|
// the Steam Link hardware.
|
||||||
|
BIO* bio = BIO_new_mem_buf(const_cast<char*>(m_CachedPrivateKey.constData()), -1);
|
||||||
THROW_BAD_ALLOC_IF_NULL(bio);
|
THROW_BAD_ALLOC_IF_NULL(bio);
|
||||||
|
|
||||||
EVP_PKEY* pk = PEM_read_bio_PrivateKey(bio, nullptr, nullptr, nullptr);
|
EVP_PKEY* pk = PEM_read_bio_PrivateKey(bio, nullptr, nullptr, nullptr);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#include "nvcomputer.h"
|
#include "nvcomputer.h"
|
||||||
#include "nvapp.h"
|
#include "nvapp.h"
|
||||||
|
#include "settings/compatfetcher.h"
|
||||||
|
|
||||||
#include <QUdpSocket>
|
#include <QUdpSocket>
|
||||||
#include <QHostInfo>
|
#include <QHostInfo>
|
||||||
@@ -30,6 +31,7 @@ NvComputer::NvComputer(QSettings& settings)
|
|||||||
this->serverCert = QSslCertificate(settings.value(SER_SRVCERT).toByteArray());
|
this->serverCert = QSslCertificate(settings.value(SER_SRVCERT).toByteArray());
|
||||||
|
|
||||||
int appCount = settings.beginReadArray(SER_APPLIST);
|
int appCount = settings.beginReadArray(SER_APPLIST);
|
||||||
|
this->appList.reserve(appCount);
|
||||||
for (int i = 0; i < appCount; i++) {
|
for (int i = 0; i < appCount; i++) {
|
||||||
settings.setArrayIndex(i);
|
settings.setArrayIndex(i);
|
||||||
|
|
||||||
@@ -49,6 +51,7 @@ NvComputer::NvComputer(QSettings& settings)
|
|||||||
this->serverCodecModeSupport = 0;
|
this->serverCodecModeSupport = 0;
|
||||||
this->pendingQuit = false;
|
this->pendingQuit = false;
|
||||||
this->gpuModel = nullptr;
|
this->gpuModel = nullptr;
|
||||||
|
this->isSupportedServerVersion = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void NvComputer::serialize(QSettings& settings) const
|
void NvComputer::serialize(QSettings& settings) const
|
||||||
@@ -98,7 +101,7 @@ NvComputer::NvComputer(QString address, QString serverInfo, QSslCertificate serv
|
|||||||
QString newMacString = NvHTTP::getXmlString(serverInfo, "mac");
|
QString newMacString = NvHTTP::getXmlString(serverInfo, "mac");
|
||||||
if (newMacString != "00:00:00:00:00:00") {
|
if (newMacString != "00:00:00:00:00:00") {
|
||||||
QStringList macOctets = newMacString.split(':');
|
QStringList macOctets = newMacString.split(':');
|
||||||
for (QString macOctet : macOctets) {
|
for (const QString& macOctet : macOctets) {
|
||||||
this->macAddress.append((char) macOctet.toInt(nullptr, 16));
|
this->macAddress.append((char) macOctet.toInt(nullptr, 16));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -142,6 +145,7 @@ NvComputer::NvComputer(QString address, QString serverInfo, QSslCertificate serv
|
|||||||
this->activeAddress = address;
|
this->activeAddress = address;
|
||||||
this->state = NvComputer::CS_ONLINE;
|
this->state = NvComputer::CS_ONLINE;
|
||||||
this->pendingQuit = false;
|
this->pendingQuit = false;
|
||||||
|
this->isSupportedServerVersion = CompatFetcher::isGfeVersionSupported(this->gfeVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool NvComputer::wake()
|
bool NvComputer::wake()
|
||||||
@@ -407,6 +411,7 @@ bool NvComputer::update(NvComputer& that)
|
|||||||
ASSIGN_IF_CHANGED(state);
|
ASSIGN_IF_CHANGED(state);
|
||||||
ASSIGN_IF_CHANGED(gfeVersion);
|
ASSIGN_IF_CHANGED(gfeVersion);
|
||||||
ASSIGN_IF_CHANGED(appVersion);
|
ASSIGN_IF_CHANGED(appVersion);
|
||||||
|
ASSIGN_IF_CHANGED(isSupportedServerVersion);
|
||||||
ASSIGN_IF_CHANGED(maxLumaPixelsHEVC);
|
ASSIGN_IF_CHANGED(maxLumaPixelsHEVC);
|
||||||
ASSIGN_IF_CHANGED(gpuModel);
|
ASSIGN_IF_CHANGED(gpuModel);
|
||||||
ASSIGN_IF_CHANGED_AND_NONNULL(serverCert);
|
ASSIGN_IF_CHANGED_AND_NONNULL(serverCert);
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ public:
|
|||||||
int maxLumaPixelsHEVC;
|
int maxLumaPixelsHEVC;
|
||||||
int serverCodecModeSupport;
|
int serverCodecModeSupport;
|
||||||
QString gpuModel;
|
QString gpuModel;
|
||||||
|
bool isSupportedServerVersion;
|
||||||
|
|
||||||
// Persisted traits
|
// Persisted traits
|
||||||
QString localAddress;
|
QString localAddress;
|
||||||
|
|||||||
+11
-5
@@ -67,7 +67,8 @@ NvHTTP::parseQuad(QString quad)
|
|||||||
}
|
}
|
||||||
|
|
||||||
QStringList parts = quad.split(".");
|
QStringList parts = quad.split(".");
|
||||||
for (int i = 0; i < 4; i++)
|
ret.reserve(parts.length());
|
||||||
|
for (int i = 0; i < parts.length(); i++)
|
||||||
{
|
{
|
||||||
ret.append(parts.at(i).toInt());
|
ret.append(parts.at(i).toInt());
|
||||||
}
|
}
|
||||||
@@ -391,7 +392,7 @@ void NvHTTP::handleSslErrors(QNetworkReply* reply, const QList<QSslError>& error
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (auto error : errors) {
|
for (const QSslError& error : errors) {
|
||||||
if (m_ServerCert != error.certificate()) {
|
if (m_ServerCert != error.certificate()) {
|
||||||
ignoreErrors = false;
|
ignoreErrors = false;
|
||||||
break;
|
break;
|
||||||
@@ -450,6 +451,11 @@ NvHTTP::openConnection(QUrl baseUrl,
|
|||||||
// Add our client certificate
|
// Add our client certificate
|
||||||
request.setSslConfiguration(IdentityManager::get()->getSslConfig());
|
request.setSslConfiguration(IdentityManager::get()->getSslConfig());
|
||||||
|
|
||||||
|
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||||
|
// Disable HTTP/2 (GFE 3.22 doesn't like it) and Qt 6 enables it by default
|
||||||
|
request.setAttribute(QNetworkRequest::Http2AllowedAttribute, false);
|
||||||
|
#endif
|
||||||
|
|
||||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) && QT_VERSION < QT_VERSION_CHECK(5, 15, 1) && !defined(QT_NO_BEARERMANAGEMENT)
|
#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)
|
// 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_PUSH
|
||||||
@@ -462,10 +468,10 @@ NvHTTP::openConnection(QUrl baseUrl,
|
|||||||
|
|
||||||
// Run the request with a timeout if requested
|
// Run the request with a timeout if requested
|
||||||
QEventLoop loop;
|
QEventLoop loop;
|
||||||
connect(reply, SIGNAL(finished()), &loop, SLOT(quit()));
|
connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
|
||||||
connect(QCoreApplication::instance(), SIGNAL(aboutToQuit()), &loop, SLOT(quit()));
|
connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, &loop, &QEventLoop::quit);
|
||||||
if (timeoutMs) {
|
if (timeoutMs) {
|
||||||
QTimer::singleShot(timeoutMs, &loop, SLOT(quit()));
|
QTimer::singleShot(timeoutMs, &loop, &QEventLoop::quit);
|
||||||
}
|
}
|
||||||
if (logLevel >= NvLogLevel::NVLL_VERBOSE) {
|
if (logLevel >= NvLogLevel::NVLL_VERBOSE) {
|
||||||
qInfo() << "Executing request:" << url.toString();
|
qInfo() << "Executing request:" << url.toString();
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ public:
|
|||||||
int height;
|
int height;
|
||||||
int refreshRate;
|
int refreshRate;
|
||||||
};
|
};
|
||||||
|
Q_DECLARE_TYPEINFO(NvDisplayMode, Q_PRIMITIVE_TYPE);
|
||||||
|
|
||||||
class GfeHttpResponseException : public std::exception
|
class GfeHttpResponseException : public std::exception
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -215,7 +215,7 @@ NvPairingManager::pair(QString appVersion, QString pin, QSslCertificate& serverC
|
|||||||
QByteArray salt = generateRandomBytes(16);
|
QByteArray salt = generateRandomBytes(16);
|
||||||
QByteArray saltedPin = saltPin(salt, pin);
|
QByteArray saltedPin = saltPin(salt, pin);
|
||||||
|
|
||||||
QByteArray aesKey = QCryptographicHash::hash(saltedPin, hashAlgo).data();
|
QByteArray aesKey = QCryptographicHash::hash(saltedPin, hashAlgo).constData();
|
||||||
aesKey.truncate(16);
|
aesKey.truncate(16);
|
||||||
|
|
||||||
QString getCert = m_Http.openConnectionToString(m_Http.m_BaseUrlHttp,
|
QString getCert = m_Http.openConnectionToString(m_Http.m_BaseUrlHttp,
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
SystemProperties::SystemProperties()
|
SystemProperties::SystemProperties()
|
||||||
{
|
{
|
||||||
versionString = QString(VERSION_STR);
|
versionString = QString(VERSION_STR);
|
||||||
hasWindowManager = WMUtils::isRunningWindowManager();
|
hasDesktopEnvironment = WMUtils::isRunningDesktopEnvironment();
|
||||||
isRunningWayland = WMUtils::isRunningWayland();
|
isRunningWayland = WMUtils::isRunningWayland();
|
||||||
isRunningXWayland = isRunningWayland && QGuiApplication::platformName() == "xcb";
|
isRunningXWayland = isRunningWayland && QGuiApplication::platformName() == "xcb";
|
||||||
QString nativeArch = QSysInfo::currentCpuArchitecture();
|
QString nativeArch = QSysInfo::currentCpuArchitecture();
|
||||||
@@ -55,12 +55,8 @@ SystemProperties::SystemProperties()
|
|||||||
friendlyNativeArchName = nativeArch.toUpper();
|
friendlyNativeArchName = nativeArch.toUpper();
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifndef STEAM_LINK
|
|
||||||
// Assume we can probably launch a browser if we're in a GUI environment
|
// Assume we can probably launch a browser if we're in a GUI environment
|
||||||
hasBrowser = hasWindowManager;
|
hasBrowser = hasDesktopEnvironment;
|
||||||
#else
|
|
||||||
hasBrowser = false;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef HAVE_DISCORD
|
#ifdef HAVE_DISCORD
|
||||||
hasDiscordIntegration = true;
|
hasDiscordIntegration = true;
|
||||||
@@ -91,7 +87,36 @@ QRect SystemProperties::getNativeResolution(int displayIndex)
|
|||||||
return monitorNativeResolutions.value(displayIndex);
|
return monitorNativeResolutions.value(displayIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class QuerySdlVideoThread : public QThread
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
QuerySdlVideoThread(SystemProperties* me) :
|
||||||
|
QThread(nullptr),
|
||||||
|
m_Me(me) {}
|
||||||
|
|
||||||
|
void run() override
|
||||||
|
{
|
||||||
|
m_Me->querySdlVideoInfoInternal();
|
||||||
|
}
|
||||||
|
|
||||||
|
SystemProperties* m_Me;
|
||||||
|
};
|
||||||
|
|
||||||
void SystemProperties::querySdlVideoInfo()
|
void SystemProperties::querySdlVideoInfo()
|
||||||
|
{
|
||||||
|
if (WMUtils::isRunningX11() || WMUtils::isRunningWayland()) {
|
||||||
|
// Use a separate thread to temporarily initialize SDL
|
||||||
|
// video to avoid stomping on Qt's X11 and OGL state.
|
||||||
|
QuerySdlVideoThread thread(this);
|
||||||
|
thread.start();
|
||||||
|
thread.wait();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
querySdlVideoInfoInternal();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SystemProperties::querySdlVideoInfoInternal()
|
||||||
{
|
{
|
||||||
hasHardwareAcceleration = false;
|
hasHardwareAcceleration = false;
|
||||||
|
|
||||||
@@ -103,7 +128,8 @@ void SystemProperties::querySdlVideoInfo()
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update display related attributes (max FPS, native resolution, etc).
|
// Update display related attributes (max FPS, native resolution, etc).
|
||||||
refreshDisplays();
|
// We call the internal variant because we're already in a safe thread context.
|
||||||
|
refreshDisplaysInternal();
|
||||||
|
|
||||||
SDL_Window* testWindow = SDL_CreateWindow("", 0, 0, 1280, 720,
|
SDL_Window* testWindow = SDL_CreateWindow("", 0, 0, 1280, 720,
|
||||||
SDL_WINDOW_HIDDEN | StreamUtils::getPlatformWindowFlags());
|
SDL_WINDOW_HIDDEN | StreamUtils::getPlatformWindowFlags());
|
||||||
@@ -129,7 +155,36 @@ void SystemProperties::querySdlVideoInfo()
|
|||||||
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class RefreshDisplaysThread : public QThread
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
RefreshDisplaysThread(SystemProperties* me) :
|
||||||
|
QThread(nullptr),
|
||||||
|
m_Me(me) {}
|
||||||
|
|
||||||
|
void run() override
|
||||||
|
{
|
||||||
|
m_Me->refreshDisplaysInternal();
|
||||||
|
}
|
||||||
|
|
||||||
|
SystemProperties* m_Me;
|
||||||
|
};
|
||||||
|
|
||||||
void SystemProperties::refreshDisplays()
|
void SystemProperties::refreshDisplays()
|
||||||
|
{
|
||||||
|
if (WMUtils::isRunningX11() || WMUtils::isRunningWayland()) {
|
||||||
|
// Use a separate thread to temporarily initialize SDL
|
||||||
|
// video to avoid stomping on Qt's X11 and OGL state.
|
||||||
|
RefreshDisplaysThread thread(this);
|
||||||
|
thread.start();
|
||||||
|
thread.wait();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
refreshDisplaysInternal();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SystemProperties::refreshDisplaysInternal()
|
||||||
{
|
{
|
||||||
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
|
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ class SystemProperties : public QObject
|
|||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
|
friend class QuerySdlVideoThread;
|
||||||
|
friend class RefreshDisplaysThread;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
SystemProperties();
|
SystemProperties();
|
||||||
|
|
||||||
@@ -16,7 +19,7 @@ public:
|
|||||||
Q_PROPERTY(bool isRunningXWayland MEMBER isRunningXWayland CONSTANT)
|
Q_PROPERTY(bool isRunningXWayland MEMBER isRunningXWayland CONSTANT)
|
||||||
Q_PROPERTY(bool isWow64 MEMBER isWow64 CONSTANT)
|
Q_PROPERTY(bool isWow64 MEMBER isWow64 CONSTANT)
|
||||||
Q_PROPERTY(QString friendlyNativeArchName MEMBER friendlyNativeArchName CONSTANT)
|
Q_PROPERTY(QString friendlyNativeArchName MEMBER friendlyNativeArchName CONSTANT)
|
||||||
Q_PROPERTY(bool hasWindowManager MEMBER hasWindowManager CONSTANT)
|
Q_PROPERTY(bool hasDesktopEnvironment MEMBER hasDesktopEnvironment CONSTANT)
|
||||||
Q_PROPERTY(bool hasBrowser MEMBER hasBrowser CONSTANT)
|
Q_PROPERTY(bool hasBrowser MEMBER hasBrowser CONSTANT)
|
||||||
Q_PROPERTY(bool hasDiscordIntegration MEMBER hasDiscordIntegration CONSTANT)
|
Q_PROPERTY(bool hasDiscordIntegration MEMBER hasDiscordIntegration CONSTANT)
|
||||||
Q_PROPERTY(QString unmappedGamepads MEMBER unmappedGamepads NOTIFY unmappedGamepadsChanged)
|
Q_PROPERTY(QString unmappedGamepads MEMBER unmappedGamepads NOTIFY unmappedGamepadsChanged)
|
||||||
@@ -33,6 +36,8 @@ signals:
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
void querySdlVideoInfo();
|
void querySdlVideoInfo();
|
||||||
|
void querySdlVideoInfoInternal();
|
||||||
|
void refreshDisplaysInternal();
|
||||||
|
|
||||||
bool hasHardwareAcceleration;
|
bool hasHardwareAcceleration;
|
||||||
bool rendererAlwaysFullScreen;
|
bool rendererAlwaysFullScreen;
|
||||||
@@ -40,7 +45,7 @@ private:
|
|||||||
bool isRunningXWayland;
|
bool isRunningXWayland;
|
||||||
bool isWow64;
|
bool isWow64;
|
||||||
QString friendlyNativeArchName;
|
QString friendlyNativeArchName;
|
||||||
bool hasWindowManager;
|
bool hasDesktopEnvironment;
|
||||||
bool hasBrowser;
|
bool hasBrowser;
|
||||||
bool hasDiscordIntegration;
|
bool hasDiscordIntegration;
|
||||||
QString unmappedGamepads;
|
QString unmappedGamepads;
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ public:
|
|||||||
|
|
||||||
void handleUnknownOptions()
|
void handleUnknownOptions()
|
||||||
{
|
{
|
||||||
if (unknownOptionNames().length()) {
|
if (!unknownOptionNames().isEmpty()) {
|
||||||
showError(QString("Unknown options: %1").arg(unknownOptionNames().join(", ")));
|
showError(QString("Unknown options: %1").arg(unknownOptionNames().join(", ")));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -264,6 +264,11 @@ StreamCommandLineParser::StreamCommandLineParser()
|
|||||||
{"software", StreamingPreferences::VDS_FORCE_SOFTWARE},
|
{"software", StreamingPreferences::VDS_FORCE_SOFTWARE},
|
||||||
{"hardware", StreamingPreferences::VDS_FORCE_HARDWARE},
|
{"hardware", StreamingPreferences::VDS_FORCE_HARDWARE},
|
||||||
};
|
};
|
||||||
|
m_CaptureSysKeysModeMap = {
|
||||||
|
{"never", StreamingPreferences::CSK_OFF},
|
||||||
|
{"fullscreen", StreamingPreferences::CSK_FULLSCREEN},
|
||||||
|
{"always", StreamingPreferences::CSK_ALWAYS},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
StreamCommandLineParser::~StreamCommandLineParser()
|
StreamCommandLineParser::~StreamCommandLineParser()
|
||||||
@@ -307,7 +312,7 @@ void StreamCommandLineParser::parse(const QStringList &args, StreamingPreference
|
|||||||
parser.addToggleOption("background-gamepad", "background gamepad input");
|
parser.addToggleOption("background-gamepad", "background gamepad input");
|
||||||
parser.addToggleOption("reverse-scroll-direction", "inverted scroll direction");
|
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("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("capture-system-keys", "capture system key combos", m_CaptureSysKeysModeMap.keys());
|
||||||
parser.addChoiceOption("video-codec", "video codec", m_VideoCodecMap.keys());
|
parser.addChoiceOption("video-codec", "video codec", m_VideoCodecMap.keys());
|
||||||
parser.addChoiceOption("video-decoder", "video decoder", m_VideoDecoderMap.keys());
|
parser.addChoiceOption("video-decoder", "video decoder", m_VideoDecoderMap.keys());
|
||||||
|
|
||||||
@@ -320,25 +325,21 @@ void StreamCommandLineParser::parse(const QStringList &args, StreamingPreference
|
|||||||
// Resolve display's width and height
|
// Resolve display's width and height
|
||||||
QRegularExpression resolutionRexExp("^(720|1080|1440|4K|resolution)$");
|
QRegularExpression resolutionRexExp("^(720|1080|1440|4K|resolution)$");
|
||||||
QStringList resoOptions = parser.optionNames().filter(resolutionRexExp);
|
QStringList resoOptions = parser.optionNames().filter(resolutionRexExp);
|
||||||
bool displaySet = resoOptions.length();
|
bool displaySet = !resoOptions.isEmpty();
|
||||||
if (displaySet) {
|
if (displaySet) {
|
||||||
QString name = resoOptions.last();
|
QString name = resoOptions.last();
|
||||||
if (name == "720") {
|
if (name == "720") {
|
||||||
preferences->width = 1280;
|
preferences->width = 1280;
|
||||||
preferences->height = 720;
|
preferences->height = 720;
|
||||||
displaySet = true;
|
|
||||||
} else if (name == "1080") {
|
} else if (name == "1080") {
|
||||||
preferences->width = 1920;
|
preferences->width = 1920;
|
||||||
preferences->height = 1080;
|
preferences->height = 1080;
|
||||||
displaySet = true;
|
|
||||||
} else if (name == "1440") {
|
} else if (name == "1440") {
|
||||||
preferences->width = 2560;
|
preferences->width = 2560;
|
||||||
preferences->height = 1440;
|
preferences->height = 1440;
|
||||||
displaySet = true;
|
|
||||||
} else if (name == "4K") {
|
} else if (name == "4K") {
|
||||||
preferences->width = 3840;
|
preferences->width = 3840;
|
||||||
preferences->height = 2160;
|
preferences->height = 2160;
|
||||||
displaySet = true;
|
|
||||||
} else if (name == "resolution") {
|
} else if (name == "resolution") {
|
||||||
auto resolution = parser.getResolutionOptionValue(name);
|
auto resolution = parser.getResolutionOptionValue(name);
|
||||||
preferences->width = resolution.first;
|
preferences->width = resolution.first;
|
||||||
@@ -349,8 +350,8 @@ void StreamCommandLineParser::parse(const QStringList &args, StreamingPreference
|
|||||||
// Resolve --fps option
|
// Resolve --fps option
|
||||||
if (parser.isSet("fps")) {
|
if (parser.isSet("fps")) {
|
||||||
preferences->fps = parser.getIntOption("fps");
|
preferences->fps = parser.getIntOption("fps");
|
||||||
if (!inRange(preferences->fps, 30, 120)) {
|
if (!inRange(preferences->fps, 30, 240)) {
|
||||||
parser.showError("FPS must be in range: 30 - 120");
|
parser.showError("FPS must be in range: 30 - 240");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -422,8 +423,10 @@ void StreamCommandLineParser::parse(const QStringList &args, StreamingPreference
|
|||||||
// Resolve --swap-gamepad-buttons and --no-swap-gamepad-buttons options
|
// Resolve --swap-gamepad-buttons and --no-swap-gamepad-buttons options
|
||||||
preferences->swapFaceButtons = parser.getToggleOptionValue("swap-gamepad-buttons", preferences->swapFaceButtons);
|
preferences->swapFaceButtons = parser.getToggleOptionValue("swap-gamepad-buttons", preferences->swapFaceButtons);
|
||||||
|
|
||||||
// Resolve --capture-system-keys and --no-capture-system-keys options
|
// Resolve --capture-system-keys option
|
||||||
preferences->captureSysKeys = parser.getToggleOptionValue("capture-system-keys", preferences->captureSysKeys);
|
if (parser.isSet("capture-system-keys")) {
|
||||||
|
preferences->captureSysKeysMode = mapValue(m_CaptureSysKeysModeMap, parser.getChoiceOptionValue("capture-system-keys"));
|
||||||
|
}
|
||||||
|
|
||||||
// Resolve --video-codec option
|
// Resolve --video-codec option
|
||||||
if (parser.isSet("video-codec")) {
|
if (parser.isSet("video-codec")) {
|
||||||
|
|||||||
@@ -53,4 +53,5 @@ private:
|
|||||||
QMap<QString, StreamingPreferences::AudioConfig> m_AudioConfigMap;
|
QMap<QString, StreamingPreferences::AudioConfig> m_AudioConfigMap;
|
||||||
QMap<QString, StreamingPreferences::VideoCodecConfig> m_VideoCodecMap;
|
QMap<QString, StreamingPreferences::VideoCodecConfig> m_VideoCodecMap;
|
||||||
QMap<QString, StreamingPreferences::VideoDecoderSelection> m_VideoDecoderMap;
|
QMap<QString, StreamingPreferences::VideoDecoderSelection> m_VideoDecoderMap;
|
||||||
|
QMap<QString, StreamingPreferences::CaptureSysKeysMode> m_CaptureSysKeysModeMap;
|
||||||
};
|
};
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
<li>Hardware accelerated video decoding with VAAPI, VDPAU, and NVDEC support</li>
|
<li>Hardware accelerated video decoding with VAAPI, VDPAU, and NVDEC support</li>
|
||||||
<li>7.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 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>Support for passing system-wide keyboard shortcuts like Alt+Tab to the host</li>
|
||||||
<li>Direct touchscreen mouse control</li>
|
<li>Direct touchscreen mouse control</li>
|
||||||
<li>HEVC support for better video compression efficiency</li>
|
<li>HEVC support for better video compression efficiency</li>
|
||||||
<li>Gamepad support with force feedback for up to 4 players</li>
|
<li>Gamepad support with force feedback for up to 4 players</li>
|
||||||
@@ -36,6 +36,71 @@
|
|||||||
</screenshots>
|
</screenshots>
|
||||||
|
|
||||||
<releases>
|
<releases>
|
||||||
|
<release version="3.1.4" date="2021-06-25">
|
||||||
|
<description>
|
||||||
|
<p>Changes:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Added network latency statistics to the performance overlay</li>
|
||||||
|
<li>Improved audio quality on unreliable connections (requires GeForce Experience 3.19 or later)</li>
|
||||||
|
<li>Reduced video frame drops on unreliable connections (requires GeForce Experience 3.22 or later)</li>
|
||||||
|
<li>Updated community-contributed translations from Weblate</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release version="3.1.3" date="2021-05-07">
|
||||||
|
<description>
|
||||||
|
<p>Changes:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Fixed an issue that could cause excessive audio latency on some connections</li>
|
||||||
|
<li>Updated community-contributed translations from Weblate</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release version="3.1.2" date="2021-05-03">
|
||||||
|
<description>
|
||||||
|
<p>Changes:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Improved connection reliability and performance with GeForce Experience 3.22</li>
|
||||||
|
<li>Added support for audio encryption with GeForce Experience 3.22</li>
|
||||||
|
<li>Added compatibility update checks for future versions of GeForce Experience</li>
|
||||||
|
<li>Fixed keyboard capture on X11</li>
|
||||||
|
<li>Updated community-contributed translations from Weblate</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release version="3.1.1" date="2021-04-09">
|
||||||
|
<description>
|
||||||
|
<p>Changes:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Added support for streaming from GeForce Experience 3.22</li>
|
||||||
|
<li>Added support for AMF-encoded HEVC streams from servers like Sunshine and OpenStream</li>
|
||||||
|
<li>Added new community-contributed translations from Weblate</li>
|
||||||
|
<li>Fixed KMSDRM rendering with buffer modifiers on embedded devices</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release version="3.1.0" date="2021-03-14">
|
||||||
|
<description>
|
||||||
|
<p>New features:</p>
|
||||||
|
<ul>
|
||||||
|
<li>System keyboard shortcuts (Alt+Tab, Super key, Ctrl+Shift+Esc, etc.) can now be (optionally) captured in windowed mode</li>
|
||||||
|
<li>System keyboard shortcuts can now be captured under Wayland</li>
|
||||||
|
<li>Added support for typing the client's clipboard text on the host by pressing Ctrl+Alt+Shift+V</li>
|
||||||
|
<li>Added Ctrl+Alt+Shift+D key combo to minimize the streaming window</li>
|
||||||
|
<li>Added Ctrl+Alt+Shift+C key combo to show the local cursor while streaming in remote desktop mouse mode</li>
|
||||||
|
<li>Added Select+L1+R1+X gamepad button combo to toggle performance overlay</li>
|
||||||
|
<li>Added translations for Simplified Chinese and German</li>
|
||||||
|
<li>Added a language selector on the settings page</li>
|
||||||
|
</ul>
|
||||||
|
<p>Bugfixes:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Improved performance of loading the app grid</li>
|
||||||
|
<li>Fixed video scaling in High DPI mode on Wayland</li>
|
||||||
|
<li>Fixed window icons on Wayland</li>
|
||||||
|
<li>Fixed number pad enter key functionality in the UI</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
<release version="3.0.0" date="2021-01-17">
|
<release version="3.0.0" date="2021-01-17">
|
||||||
<description>
|
<description>
|
||||||
<p>New features:</p>
|
<p>New features:</p>
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 13 KiB |
@@ -5,19 +5,9 @@
|
|||||||
# pairing data will be lost each reboot.
|
# pairing data will be lost each reboot.
|
||||||
HOME=/usr/local/moonlight
|
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 PE_Single_CPU which seems to host A/V stuff
|
||||||
renice -10 -p $(pidof PE_Single_CPU)
|
renice -10 -p $(pidof PE_Single_CPU)
|
||||||
|
|
||||||
# Renice Moonlight itself to avoid preemption by background tasks
|
# Renice Moonlight itself to avoid preemption by background tasks
|
||||||
# Write output to a logfile in /tmp
|
# Write output to a logfile in /tmp
|
||||||
echo Launching $BIN
|
exec nice -n -10 ./bin/moonlight > /tmp/moonlight.log
|
||||||
exec nice -n -10 ./bin/$BIN > /tmp/moonlight.log
|
|
||||||
+136
-100
@@ -73,6 +73,9 @@ CenteredGridView {
|
|||||||
width: 220; height: 287;
|
width: 220; height: 287;
|
||||||
grid: appGrid
|
grid: appGrid
|
||||||
|
|
||||||
|
property alias appContextMenu: appContextMenuLoader.item
|
||||||
|
property alias appNameText: appNameTextLoader.item
|
||||||
|
|
||||||
// Dim the app if it's hidden
|
// Dim the app if it's hidden
|
||||||
opacity: model.hidden ? 0.4 : 1.0
|
opacity: model.hidden ? 0.4 : 1.0
|
||||||
|
|
||||||
@@ -85,9 +88,14 @@ CenteredGridView {
|
|||||||
source: model.boxart
|
source: model.boxart
|
||||||
|
|
||||||
onSourceSizeChanged: {
|
onSourceSizeChanged: {
|
||||||
if ((sourceSize.width == 130 && sourceSize.height == 180) || // GFE 2.0 placeholder image
|
// Nearly all of Nvidia's official box art does not match the dimensions of placeholder
|
||||||
(sourceSize.width == 628 && sourceSize.height == 888) || // GFE 3.0 placeholder image
|
// images, however the one known exeception is Overcooked. Therefore, we only execute
|
||||||
(sourceSize.width == 200 && sourceSize.height == 266)) // Our no_app_image.png
|
// the image size checks if this is not an app collector game. We know the officially
|
||||||
|
// supported games all have box art, so this check is not required.
|
||||||
|
if (!model.isAppCollectorGame &&
|
||||||
|
((sourceSize.width == 130 && sourceSize.height == 180) || // GFE 2.0 placeholder image
|
||||||
|
(sourceSize.width == 628 && sourceSize.height == 888) || // GFE 3.0 placeholder image
|
||||||
|
(sourceSize.width == 200 && sourceSize.height == 266))) // Our no_app_image.png
|
||||||
{
|
{
|
||||||
isPlaceholder = true
|
isPlaceholder = true
|
||||||
}
|
}
|
||||||
@@ -104,81 +112,94 @@ CenteredGridView {
|
|||||||
ToolTip.text: model.name
|
ToolTip.text: model.name
|
||||||
ToolTip.delay: 1000
|
ToolTip.delay: 1000
|
||||||
ToolTip.timeout: 5000
|
ToolTip.timeout: 5000
|
||||||
ToolTip.visible: (parent.hovered || parent.highlighted) && (!appNameText.visible || appNameText.truncated)
|
ToolTip.visible: (parent.hovered || parent.highlighted) && (!appNameText || appNameText.truncated)
|
||||||
}
|
}
|
||||||
|
|
||||||
ToolButton {
|
Loader {
|
||||||
id: resumeButton
|
active: model.running
|
||||||
anchors.horizontalCenterOffset: appIcon.isPlaceholder ? -47 : 0
|
asynchronous: true
|
||||||
anchors.verticalCenterOffset: appIcon.isPlaceholder ? -75 : -60
|
anchors.fill: appIcon
|
||||||
anchors.centerIn: appIcon
|
|
||||||
visible: model.running
|
|
||||||
implicitWidth: 125
|
|
||||||
implicitHeight: 125
|
|
||||||
|
|
||||||
Image {
|
sourceComponent: Item {
|
||||||
source: "qrc:/res/baseline-play_circle_filled_white-48px.svg"
|
ToolButton {
|
||||||
anchors.centerIn: parent
|
anchors.horizontalCenterOffset: appIcon.isPlaceholder ? -47 : 0
|
||||||
sourceSize {
|
anchors.verticalCenterOffset: appIcon.isPlaceholder ? -75 : -60
|
||||||
width: 75
|
anchors.centerIn: parent
|
||||||
height: 75
|
implicitWidth: 125
|
||||||
|
implicitHeight: 125
|
||||||
|
|
||||||
|
Image {
|
||||||
|
source: "qrc:/res/baseline-play_circle_filled_white-48px.svg"
|
||||||
|
anchors.centerIn: parent
|
||||||
|
sourceSize {
|
||||||
|
width: 75
|
||||||
|
height: 75
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
launchOrResumeSelectedApp(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
ToolTip.text: qsTr("Resume Game")
|
||||||
|
ToolTip.delay: 1000
|
||||||
|
ToolTip.timeout: 3000
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
}
|
||||||
|
|
||||||
|
ToolButton {
|
||||||
|
anchors.horizontalCenterOffset: appIcon.isPlaceholder ? 47 : 0
|
||||||
|
anchors.verticalCenterOffset: appIcon.isPlaceholder ? -75 : 60
|
||||||
|
anchors.centerIn: parent
|
||||||
|
implicitWidth: 125
|
||||||
|
implicitHeight: 125
|
||||||
|
|
||||||
|
Image {
|
||||||
|
source: "qrc:/res/baseline-cancel-24px.svg"
|
||||||
|
anchors.centerIn: parent
|
||||||
|
sourceSize {
|
||||||
|
width: 75
|
||||||
|
height: 75
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
doQuitGame()
|
||||||
|
}
|
||||||
|
|
||||||
|
ToolTip.text: qsTr("Quit Game")
|
||||||
|
ToolTip.delay: 1000
|
||||||
|
ToolTip.timeout: 3000
|
||||||
|
ToolTip.visible: hovered
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onClicked: {
|
|
||||||
launchOrResumeSelectedApp(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
ToolTip.text: qsTr("Resume Game")
|
|
||||||
ToolTip.delay: 1000
|
|
||||||
ToolTip.timeout: 3000
|
|
||||||
ToolTip.visible: hovered
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ToolButton {
|
Loader {
|
||||||
id: quitButton
|
id: appNameTextLoader
|
||||||
anchors.horizontalCenterOffset: appIcon.isPlaceholder ? 47 : 0
|
active: appIcon.isPlaceholder
|
||||||
anchors.verticalCenterOffset: appIcon.isPlaceholder ? -75 : 60
|
|
||||||
anchors.centerIn: appIcon
|
|
||||||
visible: model.running
|
|
||||||
implicitWidth: 125
|
|
||||||
implicitHeight: 125
|
|
||||||
|
|
||||||
Image {
|
// This loader is not asynchronous to avoid noticeable differences
|
||||||
source: "qrc:/res/baseline-cancel-24px.svg"
|
// in the time in which the text loads for each game.
|
||||||
anchors.centerIn: parent
|
|
||||||
sourceSize {
|
|
||||||
width: 75
|
|
||||||
height: 75
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onClicked: {
|
|
||||||
doQuitGame()
|
|
||||||
}
|
|
||||||
|
|
||||||
ToolTip.text: qsTr("Quit Game")
|
|
||||||
ToolTip.delay: 1000
|
|
||||||
ToolTip.timeout: 3000
|
|
||||||
ToolTip.visible: hovered
|
|
||||||
}
|
|
||||||
|
|
||||||
Label {
|
|
||||||
id: appNameText
|
|
||||||
visible: appIcon.isPlaceholder
|
|
||||||
text: model.name
|
|
||||||
width: appIcon.width
|
width: appIcon.width
|
||||||
height: model.running ? 175 : appIcon.height
|
height: model.running ? 175 : appIcon.height
|
||||||
leftPadding: 20
|
|
||||||
rightPadding: 20
|
|
||||||
anchors.left: appIcon.left
|
anchors.left: appIcon.left
|
||||||
anchors.right: appIcon.right
|
anchors.right: appIcon.right
|
||||||
anchors.bottom: appIcon.bottom
|
anchors.bottom: appIcon.bottom
|
||||||
font.pointSize: 22
|
|
||||||
verticalAlignment: Text.AlignVCenter
|
sourceComponent: Label {
|
||||||
horizontalAlignment: Text.AlignHCenter
|
id: appNameText
|
||||||
wrapMode: Text.Wrap
|
text: model.name
|
||||||
elide: Text.ElideRight
|
font.pointSize: 22
|
||||||
|
leftPadding: 20
|
||||||
|
rightPadding: 20
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function launchOrResumeSelectedApp(quitExistingApp)
|
function launchOrResumeSelectedApp(quitExistingApp)
|
||||||
@@ -245,6 +266,17 @@ CenteredGridView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Keys.onEnterPressed: {
|
||||||
|
// Open the app context menu if activated via the gamepad or keyboard
|
||||||
|
// for running games. If the game isn't running, the above onClicked
|
||||||
|
// method will handle the launch.
|
||||||
|
if (model.running) {
|
||||||
|
// This will be keyboard/gamepad driven so use
|
||||||
|
// open() instead of popup()
|
||||||
|
appContextMenu.open()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Keys.onMenuPressed: {
|
Keys.onMenuPressed: {
|
||||||
// This will be keyboard/gamepad driven so use open() instead of popup()
|
// This will be keyboard/gamepad driven so use open() instead of popup()
|
||||||
appContextMenu.open()
|
appContextMenu.open()
|
||||||
@@ -256,44 +288,48 @@ CenteredGridView {
|
|||||||
quitAppDialog.open()
|
quitAppDialog.open()
|
||||||
}
|
}
|
||||||
|
|
||||||
NavigableMenu {
|
Loader {
|
||||||
id: appContextMenu
|
id: appContextMenuLoader
|
||||||
NavigableMenuItem {
|
asynchronous: true
|
||||||
parentMenu: appContextMenu
|
sourceComponent: NavigableMenu {
|
||||||
text: model.running ? qsTr("Resume Game") : qsTr("Launch Game")
|
id: appContextMenu
|
||||||
onTriggered: launchOrResumeSelectedApp(true)
|
NavigableMenuItem {
|
||||||
}
|
parentMenu: appContextMenu
|
||||||
NavigableMenuItem {
|
text: model.running ? qsTr("Resume Game") : qsTr("Launch Game")
|
||||||
parentMenu: appContextMenu
|
onTriggered: launchOrResumeSelectedApp(true)
|
||||||
text: qsTr("Quit Game")
|
}
|
||||||
onTriggered: doQuitGame()
|
NavigableMenuItem {
|
||||||
visible: model.running
|
parentMenu: appContextMenu
|
||||||
}
|
text: qsTr("Quit Game")
|
||||||
NavigableMenuItem {
|
onTriggered: doQuitGame()
|
||||||
parentMenu: appContextMenu
|
visible: model.running
|
||||||
checkable: true
|
}
|
||||||
checked: model.directLaunch
|
NavigableMenuItem {
|
||||||
text: qsTr("Direct Launch")
|
parentMenu: appContextMenu
|
||||||
onTriggered: appModel.setAppDirectLaunch(model.index, !model.directLaunch)
|
checkable: true
|
||||||
enabled: !model.hidden
|
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.text: qsTr("Launch this app immediately when the host is selected, bypassing the app selection grid.")
|
||||||
ToolTip.delay: 1000
|
ToolTip.delay: 1000
|
||||||
ToolTip.timeout: 3000
|
ToolTip.timeout: 3000
|
||||||
ToolTip.visible: hovered
|
ToolTip.visible: hovered
|
||||||
}
|
}
|
||||||
NavigableMenuItem {
|
NavigableMenuItem {
|
||||||
parentMenu: appContextMenu
|
parentMenu: appContextMenu
|
||||||
checkable: true
|
checkable: true
|
||||||
checked: model.hidden
|
checked: model.hidden
|
||||||
text: qsTr("Hide Game")
|
text: qsTr("Hide Game")
|
||||||
onTriggered: appModel.setAppHidden(model.index, !model.hidden)
|
onTriggered: appModel.setAppHidden(model.index, !model.hidden)
|
||||||
enabled: model.hidden || (!model.running && !model.directLaunch)
|
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.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.delay: 1000
|
||||||
ToolTip.timeout: 5000
|
ToolTip.timeout: 5000
|
||||||
ToolTip.visible: hovered
|
ToolTip.visible: hovered
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,4 +21,7 @@ ItemDelegate {
|
|||||||
Keys.onReturnPressed: {
|
Keys.onReturnPressed: {
|
||||||
clicked()
|
clicked()
|
||||||
}
|
}
|
||||||
|
Keys.onEnterPressed: {
|
||||||
|
clicked()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -22,6 +22,10 @@ MenuItem {
|
|||||||
triggered()
|
triggered()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Keys.onEnterPressed: {
|
||||||
|
triggered()
|
||||||
|
}
|
||||||
|
|
||||||
Keys.onEscapePressed: {
|
Keys.onEscapePressed: {
|
||||||
parentMenu.close()
|
parentMenu.close()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,6 +56,10 @@ NavigableDialog {
|
|||||||
accept()
|
accept()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Keys.onEnterPressed: {
|
||||||
|
accept()
|
||||||
|
}
|
||||||
|
|
||||||
Keys.onEscapePressed: {
|
Keys.onEscapePressed: {
|
||||||
reject()
|
reject()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,10 @@ ToolButton {
|
|||||||
clicked()
|
clicked()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Keys.onEnterPressed: {
|
||||||
|
clicked()
|
||||||
|
}
|
||||||
|
|
||||||
Keys.onRightPressed: {
|
Keys.onRightPressed: {
|
||||||
nextItemInFocusChain(true).forceActiveFocus(Qt.TabFocus)
|
nextItemInFocusChain(true).forceActiveFocus(Qt.TabFocus)
|
||||||
}
|
}
|
||||||
|
|||||||
+60
-45
@@ -113,6 +113,8 @@ CenteredGridView {
|
|||||||
width: 300; height: 320;
|
width: 300; height: 320;
|
||||||
grid: pcGrid
|
grid: pcGrid
|
||||||
|
|
||||||
|
property alias pcContextMenu : pcContextMenuLoader.item
|
||||||
|
|
||||||
Image {
|
Image {
|
||||||
id: pcIcon
|
id: pcIcon
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
@@ -160,61 +162,70 @@ CenteredGridView {
|
|||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
}
|
}
|
||||||
|
|
||||||
NavigableMenu {
|
Loader {
|
||||||
id: pcContextMenu
|
id: pcContextMenuLoader
|
||||||
MenuItem {
|
asynchronous: true
|
||||||
text: qsTr("PC Status: %1").arg(model.online ? qsTr("Online") : qsTr("Offline"))
|
sourceComponent: NavigableMenu {
|
||||||
font.bold: true
|
id: pcContextMenu
|
||||||
enabled: false
|
MenuItem {
|
||||||
}
|
text: qsTr("PC Status: %1").arg(model.online ? qsTr("Online") : qsTr("Offline"))
|
||||||
NavigableMenuItem {
|
font.bold: true
|
||||||
parentMenu: pcContextMenu
|
enabled: false
|
||||||
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
|
||||||
NavigableMenuItem {
|
text: qsTr("View All Apps")
|
||||||
parentMenu: pcContextMenu
|
onTriggered: {
|
||||||
text: qsTr("Wake PC")
|
var component = Qt.createComponent("AppView.qml")
|
||||||
onTriggered: computerModel.wakeComputer(index)
|
var appView = component.createObject(stackView, {"computerIndex": index, "objectName": model.name, "showHiddenGames": true})
|
||||||
visible: !model.online && model.wakeable
|
stackView.push(appView)
|
||||||
}
|
}
|
||||||
NavigableMenuItem {
|
visible: model.online && model.paired
|
||||||
parentMenu: pcContextMenu
|
}
|
||||||
text: qsTr("Test Network")
|
NavigableMenuItem {
|
||||||
onTriggered: {
|
parentMenu: pcContextMenu
|
||||||
computerModel.testConnectionForComputer(index)
|
text: qsTr("Wake PC")
|
||||||
testConnectionDialog.open()
|
onTriggered: computerModel.wakeComputer(index)
|
||||||
|
visible: !model.online && model.wakeable
|
||||||
|
}
|
||||||
|
NavigableMenuItem {
|
||||||
|
parentMenu: pcContextMenu
|
||||||
|
text: qsTr("Test Network")
|
||||||
|
onTriggered: {
|
||||||
|
computerModel.testConnectionForComputer(index)
|
||||||
|
testConnectionDialog.open()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
NavigableMenuItem {
|
NavigableMenuItem {
|
||||||
parentMenu: pcContextMenu
|
parentMenu: pcContextMenu
|
||||||
text: qsTr("Rename PC")
|
text: qsTr("Rename PC")
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
renamePcDialog.pcIndex = index
|
renamePcDialog.pcIndex = index
|
||||||
renamePcDialog.originalName = model.name
|
renamePcDialog.originalName = model.name
|
||||||
renamePcDialog.open()
|
renamePcDialog.open()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
NavigableMenuItem {
|
||||||
NavigableMenuItem {
|
parentMenu: pcContextMenu
|
||||||
parentMenu: pcContextMenu
|
text: qsTr("Delete PC")
|
||||||
text: qsTr("Delete PC")
|
onTriggered: {
|
||||||
onTriggered: {
|
deletePcDialog.pcIndex = index
|
||||||
deletePcDialog.pcIndex = index
|
// get confirmation first, actual closing is called from the dialog
|
||||||
// get confirmation first, actual closing is called from the dialog
|
deletePcDialog.open()
|
||||||
deletePcDialog.open()
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if (model.online) {
|
if (model.online) {
|
||||||
if (model.paired) {
|
if (!model.serverSupported) {
|
||||||
|
errorDialog.text = qsTr("The version of GeForce Experience on %1 is not supported by this build of Moonlight. You must update Moonlight to stream from %1.").arg(model.name)
|
||||||
|
errorDialog.helpText = ""
|
||||||
|
errorDialog.open()
|
||||||
|
}
|
||||||
|
else if (model.paired) {
|
||||||
// go to game view
|
// go to game view
|
||||||
var component = Qt.createComponent("AppView.qml")
|
var component = Qt.createComponent("AppView.qml")
|
||||||
var appView = component.createObject(stackView, {"computerIndex": index, "objectName": model.name})
|
var appView = component.createObject(stackView, {"computerIndex": index, "objectName": model.name})
|
||||||
@@ -379,6 +390,10 @@ CenteredGridView {
|
|||||||
Keys.onReturnPressed: {
|
Keys.onReturnPressed: {
|
||||||
renamePcDialog.accept()
|
renamePcDialog.accept()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Keys.onEnterPressed: {
|
||||||
|
renamePcDialog.accept()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+205
-46
@@ -358,6 +358,10 @@ Flickable {
|
|||||||
Keys.onReturnPressed: {
|
Keys.onReturnPressed: {
|
||||||
customResolutionDialog.accept()
|
customResolutionDialog.accept()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Keys.onEnterPressed: {
|
||||||
|
customResolutionDialog.accept()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
@@ -382,6 +386,10 @@ Flickable {
|
|||||||
Keys.onReturnPressed: {
|
Keys.onReturnPressed: {
|
||||||
customResolutionDialog.accept()
|
customResolutionDialog.accept()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Keys.onEnterPressed: {
|
||||||
|
customResolutionDialog.accept()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -519,12 +527,17 @@ Flickable {
|
|||||||
text: qsTr("Display mode")
|
text: qsTr("Display mode")
|
||||||
font.pointSize: 12
|
font.pointSize: 12
|
||||||
wrapMode: Text.Wrap
|
wrapMode: Text.Wrap
|
||||||
visible: SystemProperties.hasWindowManager
|
visible: SystemProperties.hasDesktopEnvironment
|
||||||
}
|
}
|
||||||
|
|
||||||
AutoResizingComboBox {
|
AutoResizingComboBox {
|
||||||
// ignore setting the index at first, and actually set it when the component is loaded
|
// ignore setting the index at first, and actually set it when the component is loaded
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
|
if (!visible) {
|
||||||
|
// Do nothing if the control won't even be visible
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Set the recommended option based on the OS
|
// Set the recommended option based on the OS
|
||||||
for (var i = 0; i < windowModeListModel.count; i++) {
|
for (var i = 0; i < windowModeListModel.count; i++) {
|
||||||
var thisWm = windowModeListModel.get(i).val;
|
var thisWm = windowModeListModel.get(i).val;
|
||||||
@@ -537,22 +550,20 @@ Flickable {
|
|||||||
|
|
||||||
currentIndex = 0
|
currentIndex = 0
|
||||||
|
|
||||||
if (SystemProperties.hasWindowManager && !SystemProperties.rendererAlwaysFullScreen) {
|
var savedWm = StreamingPreferences.windowMode
|
||||||
var savedWm = StreamingPreferences.windowMode
|
for (var i = 0; i < windowModeListModel.count; i++) {
|
||||||
for (var i = 0; i < windowModeListModel.count; i++) {
|
var thisWm = windowModeListModel.get(i).val;
|
||||||
var thisWm = windowModeListModel.get(i).val;
|
if (savedWm === thisWm) {
|
||||||
if (savedWm === thisWm) {
|
currentIndex = i
|
||||||
currentIndex = i
|
break
|
||||||
break
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
activated(currentIndex)
|
activated(currentIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
id: windowModeComboBox
|
id: windowModeComboBox
|
||||||
visible: SystemProperties.hasWindowManager
|
visible: SystemProperties.hasDesktopEnvironment
|
||||||
enabled: !SystemProperties.rendererAlwaysFullScreen
|
enabled: !SystemProperties.rendererAlwaysFullScreen
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
textRole: "text"
|
textRole: "text"
|
||||||
@@ -697,7 +708,7 @@ Flickable {
|
|||||||
width: parent.width
|
width: parent.width
|
||||||
text: qsTr("Mute audio stream when Moonlight is not the active window")
|
text: qsTr("Mute audio stream when Moonlight is not the active window")
|
||||||
font.pointSize: 12
|
font.pointSize: 12
|
||||||
visible: SystemProperties.hasWindowManager
|
visible: SystemProperties.hasDesktopEnvironment
|
||||||
checked: StreamingPreferences.muteOnFocusLoss
|
checked: StreamingPreferences.muteOnFocusLoss
|
||||||
onCheckedChanged: {
|
onCheckedChanged: {
|
||||||
StreamingPreferences.muteOnFocusLoss = checked
|
StreamingPreferences.muteOnFocusLoss = checked
|
||||||
@@ -724,37 +735,132 @@ Flickable {
|
|||||||
|
|
||||||
Label {
|
Label {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
id: uiDisplayModeTitle
|
id: languageTitle
|
||||||
text: qsTr("GUI display mode")
|
text: qsTr("Language")
|
||||||
font.pointSize: 12
|
font.pointSize: 12
|
||||||
wrapMode: Text.Wrap
|
wrapMode: Text.Wrap
|
||||||
visible: SystemProperties.hasWindowManager
|
|
||||||
}
|
}
|
||||||
|
|
||||||
AutoResizingComboBox {
|
AutoResizingComboBox {
|
||||||
// ignore setting the index at first, and actually set it when the component is loaded
|
// ignore setting the index at first, and actually set it when the component is loaded
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
if (SystemProperties.hasWindowManager) {
|
var saved_language = StreamingPreferences.language
|
||||||
var saved_uidisplaymode = StreamingPreferences.uiDisplayMode
|
currentIndex = 0
|
||||||
currentIndex = 0
|
for (var i = 0; i < languageListModel.count; i++) {
|
||||||
for (var i = 0; i < uiDisplayModeListModel.count; i++) {
|
var el_language = languageListModel.get(i).val;
|
||||||
var el_uidisplaymode = uiDisplayModeListModel.get(i).val;
|
if (saved_language === el_language) {
|
||||||
if (saved_uidisplaymode === el_uidisplaymode) {
|
currentIndex = i
|
||||||
currentIndex = i
|
break
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
// Full-screen is always selected when there is no window manager
|
activated(currentIndex)
|
||||||
currentIndex = 2
|
}
|
||||||
|
|
||||||
|
id: languageComboBox
|
||||||
|
textRole: "text"
|
||||||
|
model: ListModel {
|
||||||
|
id: languageListModel
|
||||||
|
ListElement {
|
||||||
|
text: qsTr("Automatic")
|
||||||
|
val: StreamingPreferences.LANG_AUTO
|
||||||
|
}
|
||||||
|
ListElement {
|
||||||
|
text: "Deutsch" //German
|
||||||
|
val: StreamingPreferences.LANG_DE
|
||||||
|
}
|
||||||
|
ListElement {
|
||||||
|
text: "English"
|
||||||
|
val: StreamingPreferences.LANG_EN
|
||||||
|
}
|
||||||
|
ListElement {
|
||||||
|
text: "Français" //French
|
||||||
|
val: StreamingPreferences.LANG_FR
|
||||||
|
}
|
||||||
|
ListElement {
|
||||||
|
text: "简体中文" //Simplified Chinese
|
||||||
|
val: StreamingPreferences.LANG_ZH_CN
|
||||||
|
}
|
||||||
|
ListElement {
|
||||||
|
text: "Norwegian Bokmål"
|
||||||
|
val: StreamingPreferences.LANG_NB_NO
|
||||||
|
}
|
||||||
|
ListElement {
|
||||||
|
text: "русский" // Russian
|
||||||
|
val: StreamingPreferences.LANG_RU
|
||||||
|
}
|
||||||
|
// Don't list Spanish until it is more complete
|
||||||
|
/*ListElement {
|
||||||
|
text: "Español" // Spanish
|
||||||
|
val: StreamingPreferences.LANG_ES
|
||||||
|
}*/
|
||||||
|
ListElement {
|
||||||
|
text: "日本語" // Japanese
|
||||||
|
val: StreamingPreferences.LANG_JA
|
||||||
|
}
|
||||||
|
ListElement {
|
||||||
|
text: "Tiếng Việt" // Vietnamese
|
||||||
|
val: StreamingPreferences.LANG_VI
|
||||||
|
}
|
||||||
|
ListElement {
|
||||||
|
text: "ภาษาไทย" // Thai
|
||||||
|
val: StreamingPreferences.LANG_TH
|
||||||
|
}
|
||||||
|
ListElement {
|
||||||
|
text: "한국어" // Korean
|
||||||
|
val: StreamingPreferences.LANG_KO
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ::onActivated must be used, as it only listens for when the index is changed by a human
|
||||||
|
onActivated : {
|
||||||
|
// Retranslating is expensive, so only do it if the language actually changed
|
||||||
|
var new_language = languageListModel.get(currentIndex).val
|
||||||
|
if (StreamingPreferences.language !== new_language) {
|
||||||
|
StreamingPreferences.language = languageListModel.get(currentIndex).val
|
||||||
|
if (!StreamingPreferences.retranslate()) {
|
||||||
|
ToolTip.show(qsTr("You must restart Moonlight for this change to take effect"), 5000)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Force the back operation to pop any AppView pages that exist.
|
||||||
|
// The AppView stops working after retranslate() for some reason.
|
||||||
|
window.clearOnBack = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
width: parent.width
|
||||||
|
id: uiDisplayModeTitle
|
||||||
|
text: qsTr("GUI display mode")
|
||||||
|
font.pointSize: 12
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
visible: SystemProperties.hasDesktopEnvironment
|
||||||
|
}
|
||||||
|
|
||||||
|
AutoResizingComboBox {
|
||||||
|
// ignore setting the index at first, and actually set it when the component is loaded
|
||||||
|
Component.onCompleted: {
|
||||||
|
if (!visible) {
|
||||||
|
// Do nothing if the control won't even be visible
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var saved_uidisplaymode = StreamingPreferences.uiDisplayMode
|
||||||
|
currentIndex = 0
|
||||||
|
for (var i = 0; i < uiDisplayModeListModel.count; i++) {
|
||||||
|
var el_uidisplaymode = uiDisplayModeListModel.get(i).val;
|
||||||
|
if (saved_uidisplaymode === el_uidisplaymode) {
|
||||||
|
currentIndex = i
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
activated(currentIndex)
|
activated(currentIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
id: uiDisplayModeComboBox
|
id: uiDisplayModeComboBox
|
||||||
visible: SystemProperties.hasWindowManager
|
visible: SystemProperties.hasDesktopEnvironment
|
||||||
textRole: "text"
|
textRole: "text"
|
||||||
model: ListModel {
|
model: ListModel {
|
||||||
id: uiDisplayModeListModel
|
id: uiDisplayModeListModel
|
||||||
@@ -833,7 +939,6 @@ Flickable {
|
|||||||
width: parent.width
|
width: parent.width
|
||||||
text: qsTr("Optimize mouse for remote desktop instead of games")
|
text: qsTr("Optimize mouse for remote desktop instead of games")
|
||||||
font.pointSize: 12
|
font.pointSize: 12
|
||||||
visible: SystemProperties.hasWindowManager
|
|
||||||
checked: StreamingPreferences.absoluteMouseMode
|
checked: StreamingPreferences.absoluteMouseMode
|
||||||
onCheckedChanged: {
|
onCheckedChanged: {
|
||||||
StreamingPreferences.absoluteMouseMode = checked
|
StreamingPreferences.absoluteMouseMode = checked
|
||||||
@@ -847,25 +952,79 @@ Flickable {
|
|||||||
qsTr("NOTE: Due to a bug in GeForce Experience, this option may not work properly if your host PC has multiple monitors.")
|
qsTr("NOTE: Due to a bug in GeForce Experience, this option may not work properly if your host PC has multiple monitors.")
|
||||||
}
|
}
|
||||||
|
|
||||||
CheckBox {
|
Row {
|
||||||
id: captureSysKeysCheck
|
spacing: 5
|
||||||
hoverEnabled: true
|
|
||||||
width: parent.width
|
width: parent.width
|
||||||
text: qsTr("Capture system keyboard shortcuts ") +
|
|
||||||
(!SystemProperties.isRunningWayland ? qsTr("while streaming in fullscreen") :
|
CheckBox {
|
||||||
qsTr("(unsupported on Wayland)"))
|
id: captureSysKeysCheck
|
||||||
font.pointSize: 12
|
hoverEnabled: true
|
||||||
enabled: SystemProperties.hasWindowManager && !SystemProperties.isRunningWayland
|
text: qsTr("Capture system keyboard shortcuts")
|
||||||
checked: (StreamingPreferences.captureSysKeys || !SystemProperties.hasWindowManager) && !SystemProperties.isRunningWayland
|
font.pointSize: 12
|
||||||
onCheckedChanged: {
|
enabled: SystemProperties.hasDesktopEnvironment
|
||||||
StreamingPreferences.captureSysKeys = checked
|
checked: StreamingPreferences.captureSysKeysMode !== StreamingPreferences.CSK_OFF || !SystemProperties.hasDesktopEnvironment
|
||||||
|
|
||||||
|
ToolTip.delay: 1000
|
||||||
|
ToolTip.timeout: 10000
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
ToolTip.text: qsTr("This enables the capture of system-wide keyboard shortcuts like Alt+Tab that would normally be handled by the client OS while streaming.") + "\n\n" +
|
||||||
|
qsTr("NOTE: Certain keyboard shortcuts like Ctrl+Alt+Del on Windows cannot be intercepted by any application, including Moonlight.")
|
||||||
}
|
}
|
||||||
|
|
||||||
ToolTip.delay: 1000
|
AutoResizingComboBox {
|
||||||
ToolTip.timeout: 10000
|
// ignore setting the index at first, and actually set it when the component is loaded
|
||||||
ToolTip.visible: hovered
|
Component.onCompleted: {
|
||||||
ToolTip.text: qsTr("This enables the capture of system-wide keyboard shortcuts like Alt+Tab that would normally be handled by the client OS while streaming in fullscreen.") + "\n\n" +
|
if (!visible) {
|
||||||
qsTr("NOTE: Certain keyboard shortcuts like Ctrl+Alt+Del on Windows cannot be intercepted by any application, including Moonlight.")
|
// Do nothing if the control won't even be visible
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var saved_syskeysmode = StreamingPreferences.captureSysKeysMode
|
||||||
|
currentIndex = 0
|
||||||
|
for (var i = 0; i < captureSysKeysModeListModel.count; i++) {
|
||||||
|
var el_syskeysmode = captureSysKeysModeListModel.get(i).val;
|
||||||
|
if (saved_syskeysmode === el_syskeysmode) {
|
||||||
|
currentIndex = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
activated(currentIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
enabled: captureSysKeysCheck.checked && captureSysKeysCheck.enabled
|
||||||
|
textRole: "text"
|
||||||
|
model: ListModel {
|
||||||
|
id: captureSysKeysModeListModel
|
||||||
|
ListElement {
|
||||||
|
text: qsTr("in fullscreen")
|
||||||
|
val: StreamingPreferences.CSK_FULLSCREEN
|
||||||
|
}
|
||||||
|
ListElement {
|
||||||
|
text: qsTr("always")
|
||||||
|
val: StreamingPreferences.CSK_ALWAYS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updatePref() {
|
||||||
|
if (!enabled) {
|
||||||
|
StreamingPreferences.captureSysKeysMode = StreamingPreferences.CSK_OFF
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
StreamingPreferences.captureSysKeysMode = captureSysKeysModeListModel.get(currentIndex).val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ::onActivated must be used, as it only listens for when the index is changed by a human
|
||||||
|
onActivated: {
|
||||||
|
updatePref()
|
||||||
|
}
|
||||||
|
|
||||||
|
// This handles transition of the checkbox state
|
||||||
|
onEnabledChanged: {
|
||||||
|
updatePref()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
CheckBox {
|
CheckBox {
|
||||||
@@ -980,7 +1139,7 @@ Flickable {
|
|||||||
width: parent.width
|
width: parent.width
|
||||||
text: qsTr("Process gamepad input when Moonlight is in the background")
|
text: qsTr("Process gamepad input when Moonlight is in the background")
|
||||||
font.pointSize: 12
|
font.pointSize: 12
|
||||||
visible: SystemProperties.hasWindowManager
|
visible: SystemProperties.hasDesktopEnvironment
|
||||||
checked: StreamingPreferences.backgroundGamepad
|
checked: StreamingPreferences.backgroundGamepad
|
||||||
onCheckedChanged: {
|
onCheckedChanged: {
|
||||||
StreamingPreferences.backgroundGamepad = checked
|
StreamingPreferences.backgroundGamepad = checked
|
||||||
|
|||||||
@@ -91,6 +91,8 @@ QVariant AppModel::data(const QModelIndex &index, int role) const
|
|||||||
return app.id;
|
return app.id;
|
||||||
case DirectLaunchRole:
|
case DirectLaunchRole:
|
||||||
return app.directLaunch;
|
return app.directLaunch;
|
||||||
|
case AppCollectorGameRole:
|
||||||
|
return app.isAppCollectorGame;
|
||||||
default:
|
default:
|
||||||
return QVariant();
|
return QVariant();
|
||||||
}
|
}
|
||||||
@@ -106,6 +108,7 @@ QHash<int, QByteArray> AppModel::roleNames() const
|
|||||||
names[HiddenRole] = "hidden";
|
names[HiddenRole] = "hidden";
|
||||||
names[AppIdRole] = "appid";
|
names[AppIdRole] = "appid";
|
||||||
names[DirectLaunchRole] = "directLaunch";
|
names[DirectLaunchRole] = "directLaunch";
|
||||||
|
names[AppCollectorGameRole] = "appCollectorGame";
|
||||||
|
|
||||||
return names;
|
return names;
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -17,7 +17,8 @@ class AppModel : public QAbstractListModel
|
|||||||
BoxArtRole,
|
BoxArtRole,
|
||||||
HiddenRole,
|
HiddenRole,
|
||||||
AppIdRole,
|
AppIdRole,
|
||||||
DirectLaunchRole
|
DirectLaunchRole,
|
||||||
|
AppCollectorGameRole,
|
||||||
};
|
};
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ QVariant ComputerModel::data(const QModelIndex& index, int role) const
|
|||||||
return !computer->macAddress.isEmpty();
|
return !computer->macAddress.isEmpty();
|
||||||
case StatusUnknownRole:
|
case StatusUnknownRole:
|
||||||
return computer->state == NvComputer::CS_UNKNOWN;
|
return computer->state == NvComputer::CS_UNKNOWN;
|
||||||
|
case ServerSupportedRole:
|
||||||
|
return computer->isSupportedServerVersion;
|
||||||
default:
|
default:
|
||||||
return QVariant();
|
return QVariant();
|
||||||
}
|
}
|
||||||
@@ -68,6 +70,7 @@ QHash<int, QByteArray> ComputerModel::roleNames() const
|
|||||||
names[BusyRole] = "busy";
|
names[BusyRole] = "busy";
|
||||||
names[WakeableRole] = "wakeable";
|
names[WakeableRole] = "wakeable";
|
||||||
names[StatusUnknownRole] = "statusUnknown";
|
names[StatusUnknownRole] = "statusUnknown";
|
||||||
|
names[ServerSupportedRole] = "serverSupported";
|
||||||
|
|
||||||
return names;
|
return names;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ class ComputerModel : public QAbstractListModel
|
|||||||
PairedRole,
|
PairedRole,
|
||||||
BusyRole,
|
BusyRole,
|
||||||
WakeableRole,
|
WakeableRole,
|
||||||
StatusUnknownRole
|
StatusUnknownRole,
|
||||||
|
ServerSupportedRole
|
||||||
};
|
};
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
|||||||
+24
-4
@@ -12,13 +12,18 @@ import SdlGamepadKeyNavigation 1.0
|
|||||||
ApplicationWindow {
|
ApplicationWindow {
|
||||||
property bool pollingActive: false
|
property bool pollingActive: false
|
||||||
|
|
||||||
|
// Set by SettingsView to force the back operation to pop all
|
||||||
|
// pages except the initial view. This is required when doing
|
||||||
|
// a retranslate() because AppView breaks for some reason.
|
||||||
|
property bool clearOnBack: false
|
||||||
|
|
||||||
id: window
|
id: window
|
||||||
visible: true
|
visible: true
|
||||||
width: 1280
|
width: 1280
|
||||||
height: 600
|
height: 600
|
||||||
|
|
||||||
visibility: {
|
visibility: {
|
||||||
if (SystemProperties.hasWindowManager) {
|
if (SystemProperties.hasDesktopEnvironment) {
|
||||||
if (StreamingPreferences.uiDisplayMode == StreamingPreferences.UI_WINDOWED) return "Windowed"
|
if (StreamingPreferences.uiDisplayMode == StreamingPreferences.UI_WINDOWED) return "Windowed"
|
||||||
else if (StreamingPreferences.uiDisplayMode == StreamingPreferences.UI_MAXIMIZED) return "Maximized"
|
else if (StreamingPreferences.uiDisplayMode == StreamingPreferences.UI_MAXIMIZED) return "Maximized"
|
||||||
else if (StreamingPreferences.uiDisplayMode == StreamingPreferences.UI_FULLSCREEN) return "FullScreen"
|
else if (StreamingPreferences.uiDisplayMode == StreamingPreferences.UI_FULLSCREEN) return "FullScreen"
|
||||||
@@ -31,6 +36,17 @@ ApplicationWindow {
|
|||||||
// it will never insert a line break and just extend on forever.
|
// it will never insert a line break and just extend on forever.
|
||||||
ToolTip.toolTip.contentWidth: ToolTip.toolTip.implicitContentWidth < 400 ? ToolTip.toolTip.implicitContentWidth : 400
|
ToolTip.toolTip.contentWidth: ToolTip.toolTip.implicitContentWidth < 400 ? ToolTip.toolTip.implicitContentWidth : 400
|
||||||
|
|
||||||
|
function goBack() {
|
||||||
|
if (clearOnBack) {
|
||||||
|
// Pop all items except the first one
|
||||||
|
stackView.pop(null)
|
||||||
|
clearOnBack = false
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
stackView.pop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
StackView {
|
StackView {
|
||||||
id: stackView
|
id: stackView
|
||||||
initialItem: initialView
|
initialItem: initialView
|
||||||
@@ -46,7 +62,7 @@ ApplicationWindow {
|
|||||||
|
|
||||||
Keys.onEscapePressed: {
|
Keys.onEscapePressed: {
|
||||||
if (depth > 1) {
|
if (depth > 1) {
|
||||||
stackView.pop()
|
goBack()
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
quitConfirmationDialog.open()
|
quitConfirmationDialog.open()
|
||||||
@@ -55,7 +71,7 @@ ApplicationWindow {
|
|||||||
|
|
||||||
Keys.onBackPressed: {
|
Keys.onBackPressed: {
|
||||||
if (depth > 1) {
|
if (depth > 1) {
|
||||||
stackView.pop()
|
goBack()
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
quitConfirmationDialog.open()
|
quitConfirmationDialog.open()
|
||||||
@@ -210,7 +226,7 @@ ApplicationWindow {
|
|||||||
|
|
||||||
iconSource: "qrc:/res/arrow_left.svg"
|
iconSource: "qrc:/res/arrow_left.svg"
|
||||||
|
|
||||||
onClicked: stackView.pop()
|
onClicked: goBack()
|
||||||
|
|
||||||
Keys.onDownPressed: {
|
Keys.onDownPressed: {
|
||||||
stackView.currentItem.forceActiveFocus(Qt.TabFocus)
|
stackView.currentItem.forceActiveFocus(Qt.TabFocus)
|
||||||
@@ -495,6 +511,10 @@ ApplicationWindow {
|
|||||||
Keys.onReturnPressed: {
|
Keys.onReturnPressed: {
|
||||||
addPcDialog.accept()
|
addPcDialog.accept()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Keys.onEnterPressed: {
|
||||||
|
addPcDialog.accept()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ SdlGamepadKeyNavigation::SdlGamepadKeyNavigation()
|
|||||||
m_LastAxisNavigationEventTime(0)
|
m_LastAxisNavigationEventTime(0)
|
||||||
{
|
{
|
||||||
m_PollingTimer = new QTimer(this);
|
m_PollingTimer = new QTimer(this);
|
||||||
connect(m_PollingTimer, SIGNAL(timeout()), this, SLOT(onPollingTimerFired()));
|
connect(m_PollingTimer, &QTimer::timeout, this, &SdlGamepadKeyNavigation::onPollingTimerFired);
|
||||||
}
|
}
|
||||||
|
|
||||||
SdlGamepadKeyNavigation::~SdlGamepadKeyNavigation()
|
SdlGamepadKeyNavigation::~SdlGamepadKeyNavigation()
|
||||||
|
|||||||
Binary file not shown.
File diff suppressed because it is too large.
Load diff
Binary file not shown.
File diff suppressed because it is too large.
Load diff
Binary file not shown.
+439
-207
File diff suppressed because it is too large.
Load diff
Binary file not shown.
File diff suppressed because it is too large.
Load diff
Binary file not shown.
File diff suppressed because it is too large.
Load diff
Binary file not shown.
File diff suppressed because it is too large.
Load diff
Binary file not shown.
File diff suppressed because it is too large.
Load diff
Binary file not shown.
File diff suppressed because it is too large.
Load diff
Binary file not shown.
File diff suppressed because it is too large.
Load diff
Binary file not shown.
@@ -0,0 +1,1000 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!DOCTYPE TS>
|
||||||
|
<TS version="2.1" language="zh_CN">
|
||||||
|
<context>
|
||||||
|
<name>AppView</name>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/AppView.qml" line="144"/>
|
||||||
|
<location filename="../gui/AppView.qml" line="298"/>
|
||||||
|
<source>Resume Game</source>
|
||||||
|
<translation>继续游戏</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/AppView.qml" line="170"/>
|
||||||
|
<location filename="../gui/AppView.qml" line="303"/>
|
||||||
|
<source>Quit Game</source>
|
||||||
|
<translation>退出游戏</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/AppView.qml" line="298"/>
|
||||||
|
<source>Launch Game</source>
|
||||||
|
<translation>启动游戏</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/AppView.qml" line="311"/>
|
||||||
|
<source>Direct Launch</source>
|
||||||
|
<translation>直接启动</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/AppView.qml" line="315"/>
|
||||||
|
<source>Launch this app immediately when the host is selected, bypassing the app selection grid.</source>
|
||||||
|
<translation>选择目标计算机后立即启动此应用程序,绕过应用程序选择网格。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/AppView.qml" line="324"/>
|
||||||
|
<source>Hide Game</source>
|
||||||
|
<translation>隐藏游戏</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/AppView.qml" line="328"/>
|
||||||
|
<source>Hide this game from the app grid. To access hidden games, right-click on the host and choose %1.</source>
|
||||||
|
<translation>从应用程序网格中隐藏此游戏。要访问隐藏的游戏,请右键单击目标计算机并选择 %1。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/AppView.qml" line="328"/>
|
||||||
|
<source>View All Apps</source>
|
||||||
|
<translation>查看所有应用程序</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/AppView.qml" line="343"/>
|
||||||
|
<source>Are you sure you want to quit %1? Any unsaved progress will be lost.</source>
|
||||||
|
<translation>你确定要退出 %1 吗?所有未保存的游戏进度都将丢失。</translation>
|
||||||
|
</message>
|
||||||
|
</context>
|
||||||
|
<context>
|
||||||
|
<name>CliQuitStreamSegue</name>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/CliQuitStreamSegue.qml" line="9"/>
|
||||||
|
<source>Establishing connection to PC...</source>
|
||||||
|
<translation>正在建立与目标计算机的连接...</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/CliQuitStreamSegue.qml" line="13"/>
|
||||||
|
<source>Quitting app...</source>
|
||||||
|
<translation>应用程序退出中...</translation>
|
||||||
|
</message>
|
||||||
|
</context>
|
||||||
|
<context>
|
||||||
|
<name>CliStartStreamSegue</name>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/CliStartStreamSegue.qml" line="9"/>
|
||||||
|
<source>Establishing connection to PC...</source>
|
||||||
|
<translation>正在建立与目标计算机的连接...</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/CliStartStreamSegue.qml" line="13"/>
|
||||||
|
<source>Loading app list...</source>
|
||||||
|
<translation>应用程序列表加载中...</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/CliStartStreamSegue.qml" line="81"/>
|
||||||
|
<source>Are you sure you want to quit %1? Any unsaved progress will be lost.</source>
|
||||||
|
<translation>你确定要退出 %1 吗?任何未保存的进度都将丢失。</translation>
|
||||||
|
</message>
|
||||||
|
</context>
|
||||||
|
<context>
|
||||||
|
<name>GamepadMapper</name>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/GamepadMapper.qml" line="4"/>
|
||||||
|
<source>Gamepad Mapping</source>
|
||||||
|
<translation>手柄按键映射</translation>
|
||||||
|
</message>
|
||||||
|
</context>
|
||||||
|
<context>
|
||||||
|
<name>NvHTTP</name>
|
||||||
|
<message>
|
||||||
|
<location filename="../backend/nvhttp.cpp" line="327"/>
|
||||||
|
<source>Missing audio capture device. Reinstalling GeForce Experience should resolve this error.</source>
|
||||||
|
<translation>缺少音频捕获设备。重新安装 GeForce Experience 可以解决此错误。</translation>
|
||||||
|
</message>
|
||||||
|
</context>
|
||||||
|
<context>
|
||||||
|
<name>PcView</name>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/PcView.qml" line="20"/>
|
||||||
|
<source>Computers</source>
|
||||||
|
<translation>计算机列表</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/PcView.qml" line="67"/>
|
||||||
|
<source>Unable to connect to the specified PC.</source>
|
||||||
|
<translation>无法连接到目标计算机。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/PcView.qml" line="70"/>
|
||||||
|
<source>This PC's Internet connection is blocking Moonlight. Streaming over the Internet may not work while connected to this network.</source>
|
||||||
|
<translation>此计算机的互联网连接被阻塞。当连接到此网络时,互联网上的流式传输可能无法工作。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/PcView.qml" line="73"/>
|
||||||
|
<source>Click the Help button for possible solutions.</source>
|
||||||
|
<translation>单击帮助按钮以获取可能的解决方案。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/PcView.qml" line="102"/>
|
||||||
|
<source>Searching for PCs on your local network with NVIDIA GameStream enabled...</source>
|
||||||
|
<translation>正在本地网络上搜索启用 NVIDIA GameStrea 的计算机...</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/PcView.qml" line="103"/>
|
||||||
|
<source>Automatic PC discovery is disabled. Add your PC manually.</source>
|
||||||
|
<translation>已禁用计算机自动发现。需要手动添加计算机。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/PcView.qml" line="171"/>
|
||||||
|
<source>PC Status: %1</source>
|
||||||
|
<translation>计算机状态: %1</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/PcView.qml" line="171"/>
|
||||||
|
<source>Online</source>
|
||||||
|
<translation>在线</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/PcView.qml" line="171"/>
|
||||||
|
<source>Offline</source>
|
||||||
|
<translation>离线</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/PcView.qml" line="177"/>
|
||||||
|
<source>View All Apps</source>
|
||||||
|
<translation>查看所有应用程序</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/PcView.qml" line="187"/>
|
||||||
|
<source>Wake PC</source>
|
||||||
|
<translation>唤醒目标计算机</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/PcView.qml" line="193"/>
|
||||||
|
<source>Test Network</source>
|
||||||
|
<translation>网络测试</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/PcView.qml" line="202"/>
|
||||||
|
<source>Rename PC</source>
|
||||||
|
<translation>重命名计算机</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/PcView.qml" line="211"/>
|
||||||
|
<source>Delete PC</source>
|
||||||
|
<translation>删除计算机</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/PcView.qml" line="224"/>
|
||||||
|
<source>The version of GeForce Experience on %1 is not supported by this build of Moonlight. You must update Moonlight to stream from %1.</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/PcView.qml" line="247"/>
|
||||||
|
<source>You cannot pair while a previous session is still running on the host PC. Quit any running games or reboot the host PC, then try pairing again.</source>
|
||||||
|
<translation>当上一个会话仍在运行时,无法进行配对。请退出所有正在运行的游戏或重新启动目标计算机,然后重试配对。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/PcView.qml" line="302"/>
|
||||||
|
<source>Please enter %1 on your GameStream PC. This dialog will close when pairing is completed.</source>
|
||||||
|
<translation>请在启用 GameStream 的计算机上输入 %1。配对完成后,此对话框将关闭。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/PcView.qml" line="313"/>
|
||||||
|
<source>Are you sure you want to remove this PC?</source>
|
||||||
|
<translation>确定要移除这个计算机吗?</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/PcView.qml" line="330"/>
|
||||||
|
<source>Moonlight is testing your network connection to determine if NVIDIA GameStream is blocked.</source>
|
||||||
|
<translation>Moonlight 正在测试你的网络连接,以检测 NVIDIA GameStream 是否被阻止。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/PcView.qml" line="330"/>
|
||||||
|
<source>This may take a few seconds…</source>
|
||||||
|
<translation>这可能需要几秒钟……</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/PcView.qml" line="337"/>
|
||||||
|
<source>The network test could not be performed because none of Moonlight's connection testing servers were reachable from this PC. Check your Internet connection or try again later.</source>
|
||||||
|
<translation>无法执行网络测试,因为此计算机无法访问 Moonlight 的连接测试服务器。请检查你的互联网连接或稍后再试。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/PcView.qml" line="341"/>
|
||||||
|
<source>This network does not appear to be blocking Moonlight. If you still have trouble connecting, check your PC's firewall settings.</source>
|
||||||
|
<translation>此网络似乎没有阻止 Moonlight。如果仍然无法连接,请检查计算机的防火墙设置。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/PcView.qml" line="341"/>
|
||||||
|
<source>If you are trying to stream over the Internet, install the Moonlight Internet Hosting Tool on your gaming PC and run the included Internet Streaming Tester to check your gaming PC's Internet connection.</source>
|
||||||
|
<translation>如果你想通过互联网进行流式传输,请在你的目标计算机上安装 Moonlight Internet Hosting 工具,并运行包含的互联网流式传输测试程序来检查你目标计算机的互联网连接。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/PcView.qml" line="345"/>
|
||||||
|
<source>Your PC's current network connection seems to be blocking Moonlight. Streaming over the Internet may not work while connected to this network.</source>
|
||||||
|
<translation>此计算机当前的网络连接似乎被阻塞了。当连接到此网络时,互联网上的流式传输可能无法工作。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/PcView.qml" line="345"/>
|
||||||
|
<source>The following network ports were blocked:</source>
|
||||||
|
<translation>下列网络端口被阻止:</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/PcView.qml" line="357"/>
|
||||||
|
<source>Enter the new name for this PC:</source>
|
||||||
|
<translation>输入这台计算机的新名称:</translation>
|
||||||
|
</message>
|
||||||
|
</context>
|
||||||
|
<context>
|
||||||
|
<name>PendingQuitTask</name>
|
||||||
|
<message>
|
||||||
|
<location filename="../backend/computermanager.cpp" line="542"/>
|
||||||
|
<source>The running game wasn't started by this PC. You must quit the game on the host PC manually or use the device that originally started the game.</source>
|
||||||
|
<translation>正在运行的游戏不是由这台计算机启动的。你必须在目标计算机上手动退出游戏或使用最初启动游戏的设备。</translation>
|
||||||
|
</message>
|
||||||
|
</context>
|
||||||
|
<context>
|
||||||
|
<name>QObject</name>
|
||||||
|
<message>
|
||||||
|
<location filename="../cli/quitstream.cpp" line="76"/>
|
||||||
|
<location filename="../cli/startstream.cpp" line="133"/>
|
||||||
|
<source>Failed to connect to %1</source>
|
||||||
|
<translation>无法连接到 %1</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../cli/quitstream.cpp" line="88"/>
|
||||||
|
<location filename="../cli/startstream.cpp" line="89"/>
|
||||||
|
<source>Computer %1 has not been paired. Please open Moonlight to pair before streaming.</source>
|
||||||
|
<translation>计算机 %1 尚未配对。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../cli/quitstream.cpp" line="102"/>
|
||||||
|
<location filename="../cli/startstream.cpp" line="126"/>
|
||||||
|
<source>Quitting app failed, reason: %1</source>
|
||||||
|
<translation>退出应用程序失败,原因: %1</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../cli/startstream.cpp" line="137"/>
|
||||||
|
<source>Failed to find application %1</source>
|
||||||
|
<translation>找不到应用程序 %1</translation>
|
||||||
|
</message>
|
||||||
|
</context>
|
||||||
|
<context>
|
||||||
|
<name>QuitSegue</name>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/QuitSegue.qml" line="12"/>
|
||||||
|
<source>Quitting %1...</source>
|
||||||
|
<translation>正在退出 %1...</translation>
|
||||||
|
</message>
|
||||||
|
</context>
|
||||||
|
<context>
|
||||||
|
<name>Session</name>
|
||||||
|
<message>
|
||||||
|
<location filename="../streaming/session.cpp" line="89"/>
|
||||||
|
<source>No video received from host.</source>
|
||||||
|
<translation>没有从目标计算机接收到视频画面。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../streaming/session.cpp" line="90"/>
|
||||||
|
<source>Check your firewall and port forwarding rules for port(s): %1</source>
|
||||||
|
<translation>检查防火墙和端口转发规则: %1</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../streaming/session.cpp" line="95"/>
|
||||||
|
<source>Your network connection isn't performing well. Reduce your video bitrate setting or try a faster connection.</source>
|
||||||
|
<translation>网络连接性能不佳。请降低视频比特率或尝试更快的网络连接。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../streaming/session.cpp" line="100"/>
|
||||||
|
<source>Something went wrong on your host PC when starting the stream.</source>
|
||||||
|
<translation>目标计算机启动流式传输时出现问题。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../streaming/session.cpp" line="101"/>
|
||||||
|
<source>Make sure you don't have any DRM-protected content open on your host PC. You can also try restarting your host PC.</source>
|
||||||
|
<translation>确保目标计算机上没有打开任何受 DRM 保护的内容。你也可以尝试重新启动目标计算机。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../streaming/session.cpp" line="102"/>
|
||||||
|
<source>If the issue persists, try reinstalling your GPU drivers and GeForce Experience.</source>
|
||||||
|
<translation>如果问题仍然存在,请尝试重新安装显卡驱动程序和 GeForce Experience。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../streaming/session.cpp" line="107"/>
|
||||||
|
<source>Connection terminated</source>
|
||||||
|
<translation>会话已终止</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../streaming/session.cpp" line="592"/>
|
||||||
|
<source>The version of GeForce Experience on %1 is not supported by this build of Moonlight. You must update Moonlight to stream from %1.</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../streaming/session.cpp" line="597"/>
|
||||||
|
<source>Your selection to enable remote desktop mouse mode may cause problems in games.</source>
|
||||||
|
<translation>启用远程桌面鼠标模式可能会导致游戏出现问题。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../streaming/session.cpp" line="602"/>
|
||||||
|
<source>HDR is not supported with software decoding.</source>
|
||||||
|
<translation>软件解码不支持 HDR。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../streaming/session.cpp" line="606"/>
|
||||||
|
<source>Your settings selection to force software decoding may cause poor streaming performance.</source>
|
||||||
|
<translation>你选择的用于强制软件解码的设置可能会导致流式传输性能不佳。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../streaming/session.cpp" line="611"/>
|
||||||
|
<source>Using unsupported FPS options may cause stuttering or lag.</source>
|
||||||
|
<translation>使用不受支持的 FPS 选项可能会导致卡顿或延迟。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../streaming/session.cpp" line="614"/>
|
||||||
|
<source>V-sync will be disabled when streaming at a higher frame rate than the display.</source>
|
||||||
|
<translation>当流式传输的帧率高于显示时,垂直同步将被禁用。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../streaming/session.cpp" line="631"/>
|
||||||
|
<source>Using software decoding due to your selection to force HEVC without GPU support. This may cause poor streaming performance.</source>
|
||||||
|
<translation>根据你的选择使用软件解码,以在没有显卡支持的情况下强制 HEVC。这可能会导致流式传输性能不佳。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../streaming/session.cpp" line="634"/>
|
||||||
|
<source>This PC's GPU doesn't support HEVC decoding.</source>
|
||||||
|
<translation>此计算机的显卡不支持 HEVC 解码。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../streaming/session.cpp" line="641"/>
|
||||||
|
<source>Your host PC GPU doesn't support HEVC. A GeForce GTX 900-series (Maxwell) or later GPU is required for HEVC streaming.</source>
|
||||||
|
<translation>目标计算机的显卡不支持 HEVC。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../streaming/session.cpp" line="658"/>
|
||||||
|
<source>%1 doesn't support HDR10.</source>
|
||||||
|
<translation>%1 不支持 HDR10。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../streaming/session.cpp" line="662"/>
|
||||||
|
<source>Your host PC GPU doesn't support HDR streaming. A GeForce GTX 1000-series (Pascal) or later GPU is required for HDR streaming.</source>
|
||||||
|
<translation>目标计算机的显卡不支持 HDR 流。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../streaming/session.cpp" line="671"/>
|
||||||
|
<source>This PC's GPU doesn't support HEVC Main10 decoding for HDR streaming.</source>
|
||||||
|
<translation>此计算机的显卡不支持 HEVC Main10 解码 HDR 流。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../streaming/session.cpp" line="684"/>
|
||||||
|
<source>GeForce Experience 3.0 or higher is required for 4K streaming.</source>
|
||||||
|
<translation>4K 流式传输需要 GeForce Experience 3.0 或更高版本。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../streaming/session.cpp" line="699"/>
|
||||||
|
<source>Your selected surround sound setting is not supported by the current audio device.</source>
|
||||||
|
<translation>当前音频设备不支持你选择的环绕声设置。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../streaming/session.cpp" line="706"/>
|
||||||
|
<source>Failed to open audio device. Audio will be unavailable during this session.</source>
|
||||||
|
<translation>无法打开音频设备。音频在此会话期间将不可用。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../streaming/session.cpp" line="711"/>
|
||||||
|
<source>An attached gamepad has no mapping and won't be usable. Visit the Moonlight help to resolve this.</source>
|
||||||
|
<translation>附加的手柄没有映射,因此无法使用。请访问 Moonlight 帮助解决此问题。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../streaming/session.cpp" line="719"/>
|
||||||
|
<source>Your host PC's GPU doesn't support streaming video resolutions over 4K.</source>
|
||||||
|
<translation>目标计算机的显卡不支持 4K 以上的流式传输视频分辨率。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../streaming/session.cpp" line="723"/>
|
||||||
|
<source>Video resolutions over 4K are only supported by the HEVC codec.</source>
|
||||||
|
<translation>只有 HEVC 编解码器才支持 4K 以上的视频分辨率。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../streaming/session.cpp" line="737"/>
|
||||||
|
<source>Your selection to force hardware decoding cannot be satisfied due to missing hardware decoding support on this PC's GPU.</source>
|
||||||
|
<translation>由于此计算机的显卡上缺少硬件解码支持,无法满足强制硬件解码的选择。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../streaming/session.cpp" line="740"/>
|
||||||
|
<source>Your codec selection and force hardware decoding setting are not compatible. This PC's GPU lacks support for decoding your chosen codec.</source>
|
||||||
|
<translation>你选择的编解码器和强制硬件解码设置不兼容。此计算机的显卡不支持解码你选择的编解码器。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../streaming/session.cpp" line="1041"/>
|
||||||
|
<source>GeForce Experience returned error: %1</source>
|
||||||
|
<translation>GeForce Experience 返回错误: %1</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../streaming/session.cpp" line="1519"/>
|
||||||
|
<source>Unable to initialize video decoder. Please check your streaming settings and try again.</source>
|
||||||
|
<translation>无法初始化视频解码器。请检查你的流式传输设置,然后重试。</translation>
|
||||||
|
</message>
|
||||||
|
</context>
|
||||||
|
<context>
|
||||||
|
<name>SettingsView</name>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="12"/>
|
||||||
|
<source>Settings</source>
|
||||||
|
<translation>设置</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="60"/>
|
||||||
|
<source>Basic Settings</source>
|
||||||
|
<translation>基本设置</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="70"/>
|
||||||
|
<source>Resolution and FPS</source>
|
||||||
|
<translation>分辨率和 FPS</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="78"/>
|
||||||
|
<source>Setting values too high for your PC or network connection may cause lag, stuttering, or errors.</source>
|
||||||
|
<translation>如果你的计算机或网络连接设置的值过高,可能会导致延迟,卡顿或错误。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="209"/>
|
||||||
|
<source>720p</source>
|
||||||
|
<translation>720p</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="215"/>
|
||||||
|
<source>1080p</source>
|
||||||
|
<translation>1080p</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="221"/>
|
||||||
|
<source>1440p</source>
|
||||||
|
<translation>1440p</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="227"/>
|
||||||
|
<source>4K</source>
|
||||||
|
<translation>4K</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="331"/>
|
||||||
|
<source>Custom resolutions are not officially supported by GeForce Experience, so it will not set your host display resolution. You will need to set it manually while in game.</source>
|
||||||
|
<translation>自定义分辨率不受 GeForce Experience 的正式支持,因此不会设置目标计算机的分辨率。你需要在游戏中手动设置。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="332"/>
|
||||||
|
<source>Resolutions that are not supported by your client or host PC may cause streaming errors.</source>
|
||||||
|
<translation>使用当前计算机或目标计算机不支持的分辨率可能会导致流式传输错误。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="338"/>
|
||||||
|
<source>Enter a custom resolution:</source>
|
||||||
|
<translation>输入自定义分辨率:</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="406"/>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="407"/>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="427"/>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="430"/>
|
||||||
|
<source>%1 FPS</source>
|
||||||
|
<translation>%1 FPS</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="412"/>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="415"/>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="424"/>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="437"/>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="440"/>
|
||||||
|
<source>%1 FPS (Unsupported)</source>
|
||||||
|
<translation>%1 FPS (不支持)</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="493"/>
|
||||||
|
<source>Video bitrate:</source>
|
||||||
|
<translation>视频比特率:</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="501"/>
|
||||||
|
<source>Lower the bitrate on slower connections. Raise the bitrate to increase image quality.</source>
|
||||||
|
<translation>在较慢的连接上降低比特率。提高比特率以提高图像质量。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="519"/>
|
||||||
|
<source>Video bitrate: %1 Mbps</source>
|
||||||
|
<translation>视频比特率: %1 Mbps</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="527"/>
|
||||||
|
<source>Display mode</source>
|
||||||
|
<translation>显示模式</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="545"/>
|
||||||
|
<source> (Recommended)</source>
|
||||||
|
<translation> (推荐)</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="573"/>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="868"/>
|
||||||
|
<source>Fullscreen</source>
|
||||||
|
<translation>全屏</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="577"/>
|
||||||
|
<source>Borderless windowed</source>
|
||||||
|
<translation>无边框窗口</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="581"/>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="860"/>
|
||||||
|
<source>Windowed</source>
|
||||||
|
<translation>窗口化</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="592"/>
|
||||||
|
<source>Fullscreen generally provides the best performance, but borderless windowed may work better with features like macOS Spaces, Alt+Tab, screenshot tools, on-screen overlays, etc.</source>
|
||||||
|
<translation>全屏通常提供最好的性能,但是无边框窗口可以更好地与 macOS Spaces、Alt+Tab、屏幕截图工具、屏幕覆盖等功能配合使用。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="599"/>
|
||||||
|
<source>V-Sync</source>
|
||||||
|
<translation>垂直同步</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="609"/>
|
||||||
|
<source>Disabling V-Sync allows sub-frame rendering latency, but it can display visible tearing</source>
|
||||||
|
<translation>禁用垂直同步可允许子帧渲染延迟,但会显示可见的撕裂</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="616"/>
|
||||||
|
<source>Frame pacing</source>
|
||||||
|
<translation>帧速调节</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="626"/>
|
||||||
|
<source>Frame pacing reduces micro-stutter by delaying frames that come in too early</source>
|
||||||
|
<translation>通过延迟过早进入的帧来减少卡顿</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="636"/>
|
||||||
|
<source>Audio Settings</source>
|
||||||
|
<translation>音频设置</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="646"/>
|
||||||
|
<source>Audio configuration</source>
|
||||||
|
<translation>音频配置</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="671"/>
|
||||||
|
<source>Stereo</source>
|
||||||
|
<translation>立体声</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="675"/>
|
||||||
|
<source>5.1 surround sound</source>
|
||||||
|
<translation>5.1 环绕声</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="679"/>
|
||||||
|
<source>7.1 surround sound</source>
|
||||||
|
<translation>7.1 环绕声</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="693"/>
|
||||||
|
<source>Mute host PC speakers while streaming</source>
|
||||||
|
<translation>流式传输启动时将目标计算机的扬声器静音</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="703"/>
|
||||||
|
<source>You must restart any game currently in progress for this setting to take effect</source>
|
||||||
|
<translation>要使此设置生效,必须重新启动当前正在进行的所有游戏</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="709"/>
|
||||||
|
<source>Mute audio stream when Moonlight is not the active window</source>
|
||||||
|
<translation>当 Moonlight 失去焦点时静音</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="720"/>
|
||||||
|
<source>Mutes Moonlight's audio when you Alt+Tab out of the stream or click on a different window.</source>
|
||||||
|
<translation>当你用 Alt+Tab 键离开流式传输或者点击其他窗口时,Moonlight 将静音。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="729"/>
|
||||||
|
<source>UI Settings</source>
|
||||||
|
<translation>界面设置</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="739"/>
|
||||||
|
<source>Language</source>
|
||||||
|
<translation>语言</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="765"/>
|
||||||
|
<source>Automatic</source>
|
||||||
|
<translation>自动化</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="813"/>
|
||||||
|
<source>You must restart Moonlight for this change to take effect</source>
|
||||||
|
<translation>你必须重新启动 Moonlight 才能使更改生效</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="827"/>
|
||||||
|
<source>GUI display mode</source>
|
||||||
|
<translation>窗口显示模式</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="864"/>
|
||||||
|
<source>Maximized</source>
|
||||||
|
<translation>最大化</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="881"/>
|
||||||
|
<source>Show connection quality warnings</source>
|
||||||
|
<translation>显示连接质量警告</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="893"/>
|
||||||
|
<source>Discord Rich Presence integration</source>
|
||||||
|
<translation>Discord Rich Presence 集成</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="903"/>
|
||||||
|
<source>Updates your Discord status to display the name of the game you're streaming.</source>
|
||||||
|
<translation>更新你的 Discord 状态,以显示正在运行的游戏名称。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="921"/>
|
||||||
|
<source>Input Settings</source>
|
||||||
|
<translation>输入设置</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="932"/>
|
||||||
|
<source>Optimize mouse for remote desktop instead of games</source>
|
||||||
|
<translation>为远程桌面而不是游戏优化鼠标</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="942"/>
|
||||||
|
<source>This enables seamless mouse control without capturing the client's mouse cursor. It is ideal for remote desktop usage but will not work in most games.</source>
|
||||||
|
<translation>这可以实现无缝鼠标控制,而无需捕获客户端的鼠标光标。这是理想的远程桌面使用方案,但不会在大多数游戏中工作。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="943"/>
|
||||||
|
<source>You can toggle this while streaming using Ctrl+Alt+Shift+M.</source>
|
||||||
|
<translation>你可以使用 Ctrl+Alt+Shift+M 切换鼠标控制模式。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="944"/>
|
||||||
|
<source>NOTE: Due to a bug in GeForce Experience, this option may not work properly if your host PC has multiple monitors.</source>
|
||||||
|
<translation>注意:由于 GeForce Experience 的 Bug,如果你的目标计算机有多个显示器,则此选项可能无法正常工作。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="954"/>
|
||||||
|
<source>Capture system keyboard shortcuts</source>
|
||||||
|
<translation>捕获系统快捷键</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="962"/>
|
||||||
|
<source>This enables the capture of system-wide keyboard shortcuts like Alt+Tab that would normally be handled by the client OS while streaming.</source>
|
||||||
|
<translation>这样可以捕获系统范围的键盘快捷键,例如 Alt + Tab,这些快捷键通常在流式传输时由客户端系统处理。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<source>Capture system keyboard shortcuts while streaming in fullscreen</source>
|
||||||
|
<translation type="vanished">在全屏流式传输时捕获系统键盘快捷键</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<source>This enables the capture of system-wide keyboard shortcuts like Alt+Tab that would normally be handled by the client OS while streaming in fullscreen.</source>
|
||||||
|
<translation type="vanished">这允许捕获系统范围内的键盘快捷键(如Alt+Tab),这些快捷键通常在全屏流式传输时由客户端操作系统处理。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="963"/>
|
||||||
|
<source>NOTE: Certain keyboard shortcuts like Ctrl+Alt+Del on Windows cannot be intercepted by any application, including Moonlight.</source>
|
||||||
|
<translation>注意:Windows 上的某些键盘快捷键(如 Ctrl+Alt+Del)不能被任何应用程序截获,包括 Moonlight。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="992"/>
|
||||||
|
<source>in fullscreen</source>
|
||||||
|
<translation>全屏</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="996"/>
|
||||||
|
<source>always</source>
|
||||||
|
<translation>总是</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="1026"/>
|
||||||
|
<source>Use touchscreen as a virtual trackpad</source>
|
||||||
|
<translation>将触摸屏用作虚拟触控板</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="1036"/>
|
||||||
|
<source>When checked, the touchscreen acts like a trackpad. When unchecked, the touchscreen will directly control the mouse pointer.</source>
|
||||||
|
<translation>选中时,触摸屏就像一个触控板。取消选中时,触摸屏将直接控制鼠标指针。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="1043"/>
|
||||||
|
<source>Swap left and right mouse buttons</source>
|
||||||
|
<translation>交换鼠标左键和右键</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="1055"/>
|
||||||
|
<source>Reverse mouse scrolling direction</source>
|
||||||
|
<translation>反转鼠标滚动方向</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="1069"/>
|
||||||
|
<source>Gamepad Settings</source>
|
||||||
|
<translation>手柄设置</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="1079"/>
|
||||||
|
<source>Swap A/B and X/Y gamepad buttons</source>
|
||||||
|
<translation>交换手柄的 A/B 和 X/Y 按钮</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="1097"/>
|
||||||
|
<source>This switches gamepads into a Nintendo-style button layout</source>
|
||||||
|
<translation>这会将手柄切换为任天堂风格的按钮布局</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="1103"/>
|
||||||
|
<source>Force gamepad #1 always connected</source>
|
||||||
|
<translation>强制手柄 #1 始终连接</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="1113"/>
|
||||||
|
<source>Forces a single gamepad to always stay connected to the host, even if no gamepads are actually connected to this PC.</source>
|
||||||
|
<translation>强制单个手柄始终与目标计算机保持连接,即使没有任何手柄实际连接到此计算机。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="1114"/>
|
||||||
|
<source>Only enable this option when streaming a game that doesn't support gamepads being connected after startup.</source>
|
||||||
|
<translation>仅在游戏启动后不支持手柄连接时才启用此选项。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="1121"/>
|
||||||
|
<source>Enable mouse control with gamepads by holding the 'Start' button</source>
|
||||||
|
<translation>按住手柄的“Start”按钮启用鼠标控制</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="1132"/>
|
||||||
|
<source>Process gamepad input when Moonlight is in the background</source>
|
||||||
|
<translation>当 Moonlight 在后台时处理手柄输入</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="1143"/>
|
||||||
|
<source>Allows Moonlight to capture gamepad inputs even if it's not the current window in focus</source>
|
||||||
|
<translation>即使不是焦点窗口,也允许 Moonlight 捕获手柄的输入</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="1152"/>
|
||||||
|
<source>Host Settings</source>
|
||||||
|
<translation>目标计算机设置</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="1162"/>
|
||||||
|
<source>Optimize game settings for streaming</source>
|
||||||
|
<translation>优化游戏设置以进行流式传输</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="1173"/>
|
||||||
|
<source>Quit app on host PC after ending stream</source>
|
||||||
|
<translation>流式传输结束后退出目标计算机上的应用程序</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="1183"/>
|
||||||
|
<source>This will close the app or game you are streaming when you end your stream. You will lose any unsaved progress!</source>
|
||||||
|
<translation>当你结束流式传输时,这将关闭正在运行的应用程序或游戏。这会导致你失去所有未保存的游戏进度!</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="1192"/>
|
||||||
|
<source>Advanced Settings</source>
|
||||||
|
<translation>高级设置</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="1202"/>
|
||||||
|
<source>Video decoder</source>
|
||||||
|
<translation>视频解码器</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="1227"/>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="1273"/>
|
||||||
|
<source>Automatic (Recommended)</source>
|
||||||
|
<translation>自动 (推荐)</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="1231"/>
|
||||||
|
<source>Force software decoding</source>
|
||||||
|
<translation>强制软件解码</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="1235"/>
|
||||||
|
<source>Force hardware decoding</source>
|
||||||
|
<translation>强制硬件解码</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="1248"/>
|
||||||
|
<source>Video codec</source>
|
||||||
|
<translation>视频编解码器</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="1277"/>
|
||||||
|
<source>H.264</source>
|
||||||
|
<translation>H.264</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="1281"/>
|
||||||
|
<source>HEVC (H.265)</source>
|
||||||
|
<translation>HEVC (H.265)</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="1298"/>
|
||||||
|
<source>Unlock unsupported FPS options</source>
|
||||||
|
<translation>解锁不受支持的 FPS 选项</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="1317"/>
|
||||||
|
<source>Automatically find PCs on the local network (Recommended)</source>
|
||||||
|
<translation>自动在本地网络上查找计算机 (推荐)</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/SettingsView.qml" line="1342"/>
|
||||||
|
<source>Automatically detect blocked connections (Recommended)</source>
|
||||||
|
<translation>自动检测被阻止的连接 (推荐)</translation>
|
||||||
|
</message>
|
||||||
|
</context>
|
||||||
|
<context>
|
||||||
|
<name>StreamSegue</name>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/StreamSegue.qml" line="11"/>
|
||||||
|
<source>Resuming %1...</source>
|
||||||
|
<translation>正在恢复 %1...</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/StreamSegue.qml" line="12"/>
|
||||||
|
<location filename="../gui/StreamSegue.qml" line="19"/>
|
||||||
|
<source>Starting %1...</source>
|
||||||
|
<translation>正在启动 %1...</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/StreamSegue.qml" line="25"/>
|
||||||
|
<source>Starting %1 failed: Error %2</source>
|
||||||
|
<translation>启动 %1 失败: 错误 %2</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/StreamSegue.qml" line="28"/>
|
||||||
|
<source>Check your firewall and port forwarding rules for port(s): %1</source>
|
||||||
|
<translation>检查防火墙和端口转发规则: %1</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/StreamSegue.qml" line="74"/>
|
||||||
|
<source>This PC's Internet connection is blocking Moonlight. Streaming over the Internet may not work while connected to this network.</source>
|
||||||
|
<translation>此计算机的互联网连接被阻塞。当连接到此网络时,互联网上的流式传输可能无法工作。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/StreamSegue.qml" line="161"/>
|
||||||
|
<source>Tip:</source>
|
||||||
|
<translation>提示:</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/StreamSegue.qml" line="161"/>
|
||||||
|
<source>Press %1 to disconnect your session</source>
|
||||||
|
<translation>按 %1 来断开会话</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/StreamSegue.qml" line="162"/>
|
||||||
|
<source>Start+Select+L1+R1</source>
|
||||||
|
<translation>Start+Select+L1+R1</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/StreamSegue.qml" line="162"/>
|
||||||
|
<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="254"/>
|
||||||
|
<location filename="../gui/main.qml" line="264"/>
|
||||||
|
<location filename="../gui/main.qml" line="393"/>
|
||||||
|
<location filename="../gui/main.qml" line="408"/>
|
||||||
|
<source>Settings</source>
|
||||||
|
<translation>设置</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/main.qml" line="255"/>
|
||||||
|
<source>Version %1</source>
|
||||||
|
<translation>版本 %1</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/main.qml" line="271"/>
|
||||||
|
<source>Join our community on Discord</source>
|
||||||
|
<translation>加入我们的社区</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/main.qml" line="283"/>
|
||||||
|
<source>Computers</source>
|
||||||
|
<translation>计算机列表</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/main.qml" line="290"/>
|
||||||
|
<source>Add PC manually</source>
|
||||||
|
<translation>手动添加计算机</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/main.qml" line="330"/>
|
||||||
|
<source>Update available for Moonlight: Version %1</source>
|
||||||
|
<translation>Moonlight 可用更新:版本 %1</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/main.qml" line="354"/>
|
||||||
|
<source>Help</source>
|
||||||
|
<translation>帮助</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/main.qml" line="377"/>
|
||||||
|
<source>Gamepad Mapper</source>
|
||||||
|
<translation>手柄按键映射</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/main.qml" line="381"/>
|
||||||
|
<source>Gamepad Mapping</source>
|
||||||
|
<translation>手柄按键映射</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/main.qml" line="415"/>
|
||||||
|
<source>No functioning hardware accelerated H.264 video decoder was detected by Moonlight. Your streaming performance may be severely degraded in this configuration.</source>
|
||||||
|
<translation>Moonlight 未检测到功能正常的硬件加速 H.264 视频解码器。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/main.qml" line="417"/>
|
||||||
|
<source>Click the Help button for more information on solving this problem.</source>
|
||||||
|
<translation>单击帮助按钮以获取有关解决此问题的更多信息。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/main.qml" line="423"/>
|
||||||
|
<source>Hardware acceleration doesn't work on XWayland. Continuing on XWayland may result in poor streaming performance. Try running with QT_QPA_PLATFORM=wayland or switch to X11.</source>
|
||||||
|
<translation>硬件加速在 XWayland 上不起作用。在 XWayland 上继续操作可能会导致流式传输性能下降。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/main.qml" line="425"/>
|
||||||
|
<source>Click the Help button for more information.</source>
|
||||||
|
<translation>单击帮助按钮以获取更多信息。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/main.qml" line="432"/>
|
||||||
|
<source>This version of Moonlight isn't optimized for your PC. Please download the '%1' version of Moonlight for the best streaming performance.</source>
|
||||||
|
<translation>此版本的 Moonlight 尚未针对你的计算机优化。请下载“%1”版本的 Moonlight,以获得最佳的流式传输性能。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/main.qml" line="441"/>
|
||||||
|
<source>Moonlight detected gamepads without a mapping:</source>
|
||||||
|
<translation>Moonlight 检测到没有映射的手柄:</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/main.qml" line="443"/>
|
||||||
|
<source>Click the Help button for information on how to map your gamepads.</source>
|
||||||
|
<translation>单击帮助按钮以获取有关如何映射手柄的信息。</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/main.qml" line="451"/>
|
||||||
|
<source>Are you sure you want to quit?</source>
|
||||||
|
<translation>确定要退出吗?</translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<location filename="../gui/main.qml" line="481"/>
|
||||||
|
<source>Enter the IP address of your GameStream PC:</source>
|
||||||
|
<translation>输入目标计算机的 IP 地址:</translation>
|
||||||
|
</message>
|
||||||
|
</context>
|
||||||
|
</TS>
|
||||||
+23
-14
@@ -11,7 +11,6 @@
|
|||||||
#include <QCursor>
|
#include <QCursor>
|
||||||
#include <QElapsedTimer>
|
#include <QElapsedTimer>
|
||||||
#include <QFile>
|
#include <QFile>
|
||||||
#include<QTranslator>
|
|
||||||
|
|
||||||
// Don't let SDL hook our main function, since Qt is already
|
// Don't let SDL hook our main function, since Qt is already
|
||||||
// doing the same thing. This needs to be before any headers
|
// doing the same thing. This needs to be before any headers
|
||||||
@@ -351,14 +350,16 @@ int main(int argc, char *argv[])
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if defined(Q_OS_DARWIN) && QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
#if !defined(Q_OS_WIN32) || QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||||
if (!qEnvironmentVariableIsSet("QSG_RHI_BACKEND")) {
|
// Moonlight requires the non-threaded renderer because we depend
|
||||||
// The Metal backend in Qt 6.0.0 causes really strange issues transitioning to
|
// on being able to control the render thread by blocking in the
|
||||||
// full-screen in our SDL window (hangs in SDL's Cocoa_SetWindowFullscreenSpace())
|
// main thread (and pumping events from the main thread when needed).
|
||||||
// and breaks drawing our status updates in the StreamSegue, so use OpenGL like
|
// That doesn't work with the threaded renderer which causes all
|
||||||
// Qt 5 does until we figure out the cause of these issues.
|
// sorts of odd behavior depending on the platform.
|
||||||
qputenv("QSG_RHI_BACKEND", "opengl");
|
//
|
||||||
}
|
// NB: Windows defaults to the "windows" non-threaded render loop on
|
||||||
|
// Qt 5 and the threaded render loop on Qt 6.
|
||||||
|
qputenv("QSG_RENDER_LOOP", "basic");
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// We don't want system proxies to apply to us
|
// We don't want system proxies to apply to us
|
||||||
@@ -394,6 +395,9 @@ int main(int argc, char *argv[])
|
|||||||
// future KMSDRM platforms that use SDL for rendering.
|
// future KMSDRM platforms that use SDL for rendering.
|
||||||
SDL_SetHint(SDL_HINT_VIDEO_DOUBLE_BUFFER, "1");
|
SDL_SetHint(SDL_HINT_VIDEO_DOUBLE_BUFFER, "1");
|
||||||
|
|
||||||
|
// We use MMAL to render on Raspberry Pi, so we do not require DRM master.
|
||||||
|
SDL_SetHint("SDL_KMSDRM_REQUIRE_DRM_MASTER", "0");
|
||||||
|
|
||||||
// Use Direct3D 9Ex to avoid a deadlock caused by the D3D device being reset when
|
// 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 user triggers a UAC prompt. This option controls the software/SDL renderer.
|
||||||
// The DXVA2 renderer uses Direct3D 9Ex itself directly.
|
// The DXVA2 renderer uses Direct3D 9Ex itself directly.
|
||||||
@@ -451,9 +455,9 @@ int main(int argc, char *argv[])
|
|||||||
|
|
||||||
QGuiApplication app(argc, argv);
|
QGuiApplication app(argc, argv);
|
||||||
|
|
||||||
QTranslator translator;
|
// Apply the initial translation based on user preference
|
||||||
qDebug() << "Translation loaded:" << translator.load(QString(":/languages/qml_") + QLocale::system().name());
|
StreamingPreferences prefs;
|
||||||
app.installTranslator(&translator);
|
prefs.retranslate();
|
||||||
|
|
||||||
// After the QGuiApplication is created, the platform stuff will be initialized
|
// After the QGuiApplication is created, the platform stuff will be initialized
|
||||||
// and we can set the SDL video driver to match Qt.
|
// and we can set the SDL video driver to match Qt.
|
||||||
@@ -499,6 +503,11 @@ int main(int argc, char *argv[])
|
|||||||
app.setWindowIcon(QIcon(":/res/moonlight.svg"));
|
app.setWindowIcon(QIcon(":/res/moonlight.svg"));
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
// This is necessary to show our icon correctly on Wayland
|
||||||
|
app.setDesktopFileName("com.moonlight_stream.Moonlight.desktop");
|
||||||
|
qputenv("SDL_VIDEO_WAYLAND_WMCLASS", "com.moonlight_stream.Moonlight");
|
||||||
|
qputenv("SDL_VIDEO_X11_WMCLASS", "com.moonlight_stream.Moonlight");
|
||||||
|
|
||||||
// Register our C++ types for QML
|
// Register our C++ types for QML
|
||||||
qmlRegisterType<ComputerModel>("ComputerModel", 1, 0, "ComputerModel");
|
qmlRegisterType<ComputerModel>("ComputerModel", 1, 0, "ComputerModel");
|
||||||
qmlRegisterType<AppModel>("AppModel", 1, 0, "AppModel");
|
qmlRegisterType<AppModel>("AppModel", 1, 0, "AppModel");
|
||||||
@@ -525,8 +534,8 @@ int main(int argc, char *argv[])
|
|||||||
});
|
});
|
||||||
qmlRegisterSingletonType<StreamingPreferences>("StreamingPreferences", 1, 0,
|
qmlRegisterSingletonType<StreamingPreferences>("StreamingPreferences", 1, 0,
|
||||||
"StreamingPreferences",
|
"StreamingPreferences",
|
||||||
[](QQmlEngine*, QJSEngine*) -> QObject* {
|
[](QQmlEngine* qmlEngine, QJSEngine*) -> QObject* {
|
||||||
return new StreamingPreferences();
|
return new StreamingPreferences(qmlEngine);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create the identity manager on the main thread
|
// Create the identity manager on the main thread
|
||||||
|
|||||||
@@ -0,0 +1,146 @@
|
|||||||
|
// This file contains hooks for several functions that allow
|
||||||
|
// Qt and SDL to more or less share DRM master ownership.
|
||||||
|
//
|
||||||
|
// This technique requires Linux v5.8 or later, or for Moonlight
|
||||||
|
// to run as root (with CAP_SYS_ADMIN). Prior to Linux v5.8,
|
||||||
|
// DRM_IOCTL_DROP_MASTER required CAP_SYS_ADMIN, which prevents
|
||||||
|
// our trick from working (without root, that is).
|
||||||
|
//
|
||||||
|
// The specific kernel change required to run without root is:
|
||||||
|
// https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=45bc3d26c95a8fc63a7d8668ca9e57ef0883351c
|
||||||
|
|
||||||
|
#define _GNU_SOURCE
|
||||||
|
|
||||||
|
#include <SDL.h>
|
||||||
|
#include <dlfcn.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
|
||||||
|
#include <xf86drm.h>
|
||||||
|
#include <xf86drmMode.h>
|
||||||
|
|
||||||
|
// We require SDL 2.0.15+ to hook because it supports sharing
|
||||||
|
// the DRM FD with our code. This avoids having multiple DRM FDs
|
||||||
|
// in flight at the same time which would significantly complicate
|
||||||
|
// the logic here because we'd need to figure out exactly which FD
|
||||||
|
// should be the master at any given time. With the position of our
|
||||||
|
// hooks, that is definitely not trivial.
|
||||||
|
#if SDL_VERSION_ATLEAST(2, 0, 15)
|
||||||
|
|
||||||
|
// Qt's DRM master FD grabbed by our hook
|
||||||
|
int g_QtDrmMasterFd = -1;
|
||||||
|
struct stat64 g_DrmMasterStat;
|
||||||
|
|
||||||
|
// The DRM master FD created for SDL
|
||||||
|
int g_SdlDrmMasterFd = -1;
|
||||||
|
|
||||||
|
// This hook will handle legacy DRM rendering
|
||||||
|
int drmModeSetCrtc(int fd, uint32_t crtcId, uint32_t bufferId,
|
||||||
|
uint32_t x, uint32_t y, uint32_t *connectors, int count,
|
||||||
|
drmModeModeInfoPtr mode)
|
||||||
|
{
|
||||||
|
// Grab the first DRM FD that makes it in here. This will be the Qt
|
||||||
|
// EGLFS backend's DRM FD, which we will need later.
|
||||||
|
if (g_QtDrmMasterFd == -1) {
|
||||||
|
g_QtDrmMasterFd = fd;
|
||||||
|
fstat64(g_QtDrmMasterFd, &g_DrmMasterStat);
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Captured Qt EGLFS DRM master fd (legacy): %d",
|
||||||
|
g_QtDrmMasterFd);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call into the real thing
|
||||||
|
return ((typeof(drmModeSetCrtc)*)dlsym(RTLD_NEXT, "drmModeSetCrtc"))(fd, crtcId, bufferId, x, y, connectors, count, mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
// This hook will handle atomic DRM rendering
|
||||||
|
int drmModeAtomicCommit(int fd, drmModeAtomicReqPtr req,
|
||||||
|
uint32_t flags, void *user_data)
|
||||||
|
{
|
||||||
|
// Grab the first DRM FD that makes it in here. This will be the Qt
|
||||||
|
// EGLFS backend's DRM FD, which we will need later.
|
||||||
|
if (g_QtDrmMasterFd == -1) {
|
||||||
|
g_QtDrmMasterFd = fd;
|
||||||
|
fstat64(g_QtDrmMasterFd, &g_DrmMasterStat);
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Captured Qt EGLFS DRM master fd (atomic): %d",
|
||||||
|
g_QtDrmMasterFd);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call into the real thing
|
||||||
|
return ((typeof(drmModeAtomicCommit)*)dlsym(RTLD_NEXT, "drmModeAtomicCommit"))(fd, req, flags, user_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// This hook will handle SDL's open() on the DRM device. We just need to
|
||||||
|
// hook this variant of open(), since that's what SDL uses. When we see
|
||||||
|
// the open a FD for the same card as the Qt DRM master FD, we'll drop
|
||||||
|
// master on the Qt FD to allow the new FD to have master.
|
||||||
|
int open(const char *pathname, int flags)
|
||||||
|
{
|
||||||
|
// Call the real thing to do the open operation
|
||||||
|
int fd = ((typeof(open)*)dlsym(RTLD_NEXT, "open"))(pathname, flags);
|
||||||
|
|
||||||
|
// If the file was successfully opened and we have a DRM master FD,
|
||||||
|
// check if the FD we just opened is a DRM device.
|
||||||
|
if (fd >= 0 && g_QtDrmMasterFd != -1) {
|
||||||
|
if (strncmp(pathname, "/dev/dri/card", 13) == 0) {
|
||||||
|
// It's a DRM device, but is it _our_ DRM device?
|
||||||
|
struct stat64 fdstat;
|
||||||
|
|
||||||
|
fstat64(fd, &fdstat);
|
||||||
|
if (g_DrmMasterStat.st_dev == fdstat.st_dev &&
|
||||||
|
g_DrmMasterStat.st_ino == fdstat.st_ino) {
|
||||||
|
// It is our device. Time to do the magic!
|
||||||
|
|
||||||
|
// This code assumes SDL only ever opens a single FD
|
||||||
|
// for a given DRM device.
|
||||||
|
SDL_assert(g_SdlDrmMasterFd == -1);
|
||||||
|
|
||||||
|
// Drop master on Qt's FD so we can pick it up for SDL.
|
||||||
|
if (drmDropMaster(g_QtDrmMasterFd) < 0) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Failed to drop master on Qt DRM FD: %d",
|
||||||
|
errno);
|
||||||
|
// Hope for the best
|
||||||
|
return fd;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We are not allowed to call drmSetMaster() without CAP_SYS_ADMIN,
|
||||||
|
// but since we just dropped the master, we can become master by
|
||||||
|
// simply creating a new FD. Let's do it.
|
||||||
|
close(fd);
|
||||||
|
fd = ((typeof(open)*)dlsym(RTLD_NEXT, "open"))(pathname, flags);
|
||||||
|
g_SdlDrmMasterFd = fd;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fd;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Our close() hook handles restoring DRM master to the Qt FD
|
||||||
|
// after SDL closes its DRM FD.
|
||||||
|
int close(int fd)
|
||||||
|
{
|
||||||
|
// Call the real thing
|
||||||
|
int ret = ((typeof(close)*)dlsym(RTLD_NEXT, "close"))(fd);
|
||||||
|
if (ret == 0) {
|
||||||
|
// If we just closed the SDL DRM master FD, restore master
|
||||||
|
// to the Qt DRM FD. This works because the Qt DRM master FD
|
||||||
|
// was master once before, so we can set it as master again
|
||||||
|
// using drmSetMaster() without CAP_SYS_ADMIN.
|
||||||
|
if (g_SdlDrmMasterFd != -1 && fd == g_SdlDrmMasterFd) {
|
||||||
|
if (drmSetMaster(g_QtDrmMasterFd) < 0) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Failed to restore master to Qt DRM FD: %d",
|
||||||
|
errno);
|
||||||
|
}
|
||||||
|
|
||||||
|
g_SdlDrmMasterFd = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
+22
-2
@@ -17,14 +17,34 @@
|
|||||||
<file>res/baseline-error_outline-24px.svg</file>
|
<file>res/baseline-error_outline-24px.svg</file>
|
||||||
<file>res/baseline-check_circle_outline-24px.svg</file>
|
<file>res/baseline-check_circle_outline-24px.svg</file>
|
||||||
<file>res/Discord-Logo-White.svg</file>
|
<file>res/Discord-Logo-White.svg</file>
|
||||||
|
<file>languages/qml_de.ts</file>
|
||||||
|
<file>languages/qml_de.qm</file>
|
||||||
<file>languages/qml_fr.ts</file>
|
<file>languages/qml_fr.ts</file>
|
||||||
<file>languages/qml_fr.qm</file>
|
<file>languages/qml_fr.qm</file>
|
||||||
|
<file>languages/qml_zh_cn.ts</file>
|
||||||
|
<file>languages/qml_zh_cn.qm</file>
|
||||||
|
<file>languages/qml_nb_NO.ts</file>
|
||||||
|
<file>languages/qml_nb_NO.qm</file>
|
||||||
|
<file>languages/qml_ru.ts</file>
|
||||||
|
<file>languages/qml_ru.qm</file>
|
||||||
|
<file>languages/qml_es.ts</file>
|
||||||
|
<file>languages/qml_es.qm</file>
|
||||||
|
<file>languages/qml_ja.ts</file>
|
||||||
|
<file>languages/qml_ja.qm</file>
|
||||||
|
<file>languages/qml_vi.ts</file>
|
||||||
|
<file>languages/qml_vi.qm</file>
|
||||||
|
<file>languages/qml_th.ts</file>
|
||||||
|
<file>languages/qml_th.qm</file>
|
||||||
|
<file>languages/qml_ko.ts</file>
|
||||||
|
<file>languages/qml_ko.qm</file>
|
||||||
</qresource>
|
</qresource>
|
||||||
<qresource prefix="/data">
|
<qresource prefix="/data">
|
||||||
<file alias="gamecontrollerdb.txt">SDL_GameControllerDB/gamecontrollerdb.txt</file>
|
<file alias="gamecontrollerdb.txt">SDL_GameControllerDB/gamecontrollerdb.txt</file>
|
||||||
<file alias="ModeSeven.ttf">ModeSeven.ttf</file>
|
<file alias="ModeSeven.ttf">ModeSeven.ttf</file>
|
||||||
<file alias="egl.frag">shaders/egl.frag</file>
|
<file alias="egl_nv12.frag">shaders/egl_nv12.frag</file>
|
||||||
<file alias="egl.vert">shaders/egl.vert</file>
|
<file alias="egl_nv12.vert">shaders/egl_nv12.vert</file>
|
||||||
|
<file alias="egl_opaque.frag">shaders/egl_opaque.frag</file>
|
||||||
|
<file alias="egl_opaque.vert">shaders/egl_opaque.vert</file>
|
||||||
<file alias="egl_overlay.frag">shaders/egl_overlay.frag</file>
|
<file alias="egl_overlay.frag">shaders/egl_overlay.frag</file>
|
||||||
<file alias="egl_overlay.vert">shaders/egl_overlay.vert</file>
|
<file alias="egl_overlay.vert">shaders/egl_overlay.vert</file>
|
||||||
</qresource>
|
</qresource>
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
#include "compatfetcher.h"
|
||||||
|
#include "path.h"
|
||||||
|
|
||||||
|
#include <QNetworkReply>
|
||||||
|
#include <QSettings>
|
||||||
|
|
||||||
|
#define COMPAT_VERSION "v1"
|
||||||
|
#define COMPAT_KEY "latestsupportedversion-"
|
||||||
|
|
||||||
|
CompatFetcher::CompatFetcher(QObject *parent) :
|
||||||
|
QObject(parent),
|
||||||
|
m_Nam(this)
|
||||||
|
{
|
||||||
|
// Never communicate over HTTP
|
||||||
|
m_Nam.setStrictTransportSecurityEnabled(true);
|
||||||
|
|
||||||
|
// Allow HTTP redirects
|
||||||
|
m_Nam.setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy);
|
||||||
|
|
||||||
|
connect(&m_Nam, &QNetworkAccessManager::finished,
|
||||||
|
this, &CompatFetcher::handleCompatInfoFetched);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CompatFetcher::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/compatibility/" COMPAT_VERSION);
|
||||||
|
QNetworkRequest request(url);
|
||||||
|
|
||||||
|
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
|
||||||
|
request.setAttribute(QNetworkRequest::Http2AllowedAttribute, true);
|
||||||
|
#else
|
||||||
|
request.setAttribute(QNetworkRequest::HTTP2AllowedAttribute, true);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// We'll get a callback when this is finished
|
||||||
|
m_Nam.get(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CompatFetcher::isGfeVersionSupported(QString gfeVersion)
|
||||||
|
{
|
||||||
|
QSettings settings;
|
||||||
|
|
||||||
|
if (gfeVersion.trimmed().isEmpty()) {
|
||||||
|
// If we don't have a GFE version, just allow it
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString latestSupportedVersion = settings.value(COMPAT_KEY COMPAT_VERSION).toString();
|
||||||
|
if (latestSupportedVersion.isEmpty()) {
|
||||||
|
// We don't have compat data yet, so just assume it's supported
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
QStringList latestSupportedVersionQuad = latestSupportedVersion.split('.');
|
||||||
|
QStringList gfeVersionQuad = gfeVersion.split('.');
|
||||||
|
|
||||||
|
if (gfeVersionQuad.count() <= 1) {
|
||||||
|
qWarning() << "Failed to parse GFE version:" << gfeVersion;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (latestSupportedVersionQuad.count() <= 1) {
|
||||||
|
qWarning() << "Failed to parse latest supported version:" << latestSupportedVersion;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0;; i++) {
|
||||||
|
int actualVerVal = 0;
|
||||||
|
int latestSupportedVal = 0;
|
||||||
|
|
||||||
|
// Treat missing decimal places as 0
|
||||||
|
if (i < gfeVersionQuad.count()) {
|
||||||
|
bool ok;
|
||||||
|
|
||||||
|
actualVerVal = gfeVersionQuad[i].toInt(&ok);
|
||||||
|
if (!ok || actualVerVal < 0) {
|
||||||
|
// Return true to be safe
|
||||||
|
qWarning() << "Failed to parse GFE version:" << gfeVersion;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (i < latestSupportedVersionQuad.count()) {
|
||||||
|
bool ok;
|
||||||
|
|
||||||
|
latestSupportedVal = latestSupportedVersionQuad[i].toInt(&ok);
|
||||||
|
if (!ok || latestSupportedVal < 0) {
|
||||||
|
// Return true to be safe
|
||||||
|
qWarning() << "Failed to parse latest supported version:" << latestSupportedVersion;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i >= gfeVersionQuad.count() && i >= latestSupportedVersionQuad.count()) {
|
||||||
|
// Equal versions - this is fine
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (actualVerVal < latestSupportedVal) {
|
||||||
|
// Actual version is lower than latest supported - this is fine
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else if (actualVerVal > latestSupportedVal) {
|
||||||
|
// Actual version is greater than latest supported - this is bad
|
||||||
|
qWarning() << "GFE version" << gfeVersion << "is not supported by this version of Moonlight";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CompatFetcher::handleCompatInfoFetched(QNetworkReply* reply)
|
||||||
|
{
|
||||||
|
Q_ASSERT(reply->isFinished());
|
||||||
|
|
||||||
|
if (reply->error() == QNetworkReply::NoError) {
|
||||||
|
// Queue the reply for deletion
|
||||||
|
reply->deleteLater();
|
||||||
|
|
||||||
|
QString version = QString(reply->readAll()).trimmed();
|
||||||
|
|
||||||
|
QSettings settings;
|
||||||
|
settings.setValue(COMPAT_KEY COMPAT_VERSION, version);
|
||||||
|
|
||||||
|
qInfo() << "Latest supported GFE server:" << version;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
qWarning() << "Failed to download latest compatibility data:" << reply->error();
|
||||||
|
reply->deleteLater();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QNetworkAccessManager>
|
||||||
|
|
||||||
|
class CompatFetcher : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit CompatFetcher(QObject *parent = nullptr);
|
||||||
|
|
||||||
|
void start();
|
||||||
|
|
||||||
|
static bool isGfeVersionSupported(QString gfeVersion);
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void handleCompatInfoFetched(QNetworkReply* reply);
|
||||||
|
|
||||||
|
private:
|
||||||
|
QNetworkAccessManager m_Nam;
|
||||||
|
};
|
||||||
@@ -13,8 +13,8 @@ MappingFetcher::MappingFetcher(QObject *parent) :
|
|||||||
// Allow HTTP redirects
|
// Allow HTTP redirects
|
||||||
m_Nam.setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy);
|
m_Nam.setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy);
|
||||||
|
|
||||||
connect(&m_Nam, SIGNAL(finished(QNetworkReply*)),
|
connect(&m_Nam, &QNetworkAccessManager::finished,
|
||||||
this, SLOT(handleMappingListFetched(QNetworkReply*)));
|
this, &MappingFetcher::handleMappingListFetched);
|
||||||
}
|
}
|
||||||
|
|
||||||
void MappingFetcher::start()
|
void MappingFetcher::start()
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ MappingManager::MappingManager()
|
|||||||
#else
|
#else
|
||||||
.split('\n', QString::SkipEmptyParts);
|
.split('\n', QString::SkipEmptyParts);
|
||||||
#endif
|
#endif
|
||||||
for (QString sdlMapping : sdlMappings) {
|
for (const QString& sdlMapping : sdlMappings) {
|
||||||
SdlGamepadMapping mapping(sdlMapping);
|
SdlGamepadMapping mapping(sdlMapping);
|
||||||
addMapping(mapping);
|
addMapping(mapping);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
#include "streamingpreferences.h"
|
#include "streamingpreferences.h"
|
||||||
|
|
||||||
#include <QSettings>
|
#include <QSettings>
|
||||||
|
#include <QTranslator>
|
||||||
|
#include <QCoreApplication>
|
||||||
|
#include <QLocale>
|
||||||
|
|
||||||
|
#include <QtDebug>
|
||||||
|
|
||||||
#define SER_STREAMSETTINGS "streamsettings"
|
#define SER_STREAMSETTINGS "streamsettings"
|
||||||
#define SER_WIDTH "width"
|
#define SER_WIDTH "width"
|
||||||
@@ -36,11 +41,20 @@
|
|||||||
#define SER_REVERSESCROLL "reversescroll"
|
#define SER_REVERSESCROLL "reversescroll"
|
||||||
#define SER_SWAPFACEBUTTONS "swapfacebuttons"
|
#define SER_SWAPFACEBUTTONS "swapfacebuttons"
|
||||||
#define SER_CAPTURESYSKEYS "capturesyskeys"
|
#define SER_CAPTURESYSKEYS "capturesyskeys"
|
||||||
|
#define SER_LANGUAGE "language"
|
||||||
|
|
||||||
#define CURRENT_DEFAULT_VER 1
|
#define CURRENT_DEFAULT_VER 1
|
||||||
|
|
||||||
StreamingPreferences::StreamingPreferences(QObject *parent)
|
StreamingPreferences::StreamingPreferences(QObject *parent)
|
||||||
: QObject(parent)
|
: QObject(parent),
|
||||||
|
m_QmlEngine(nullptr)
|
||||||
|
{
|
||||||
|
reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
StreamingPreferences::StreamingPreferences(QQmlEngine *qmlEngine, QObject *parent)
|
||||||
|
: QObject(parent),
|
||||||
|
m_QmlEngine(qmlEngine)
|
||||||
{
|
{
|
||||||
reload();
|
reload();
|
||||||
}
|
}
|
||||||
@@ -81,7 +95,8 @@ void StreamingPreferences::reload()
|
|||||||
backgroundGamepad = settings.value(SER_BACKGROUNDGAMEPAD, false).toBool();
|
backgroundGamepad = settings.value(SER_BACKGROUNDGAMEPAD, false).toBool();
|
||||||
reverseScrollDirection = settings.value(SER_REVERSESCROLL, false).toBool();
|
reverseScrollDirection = settings.value(SER_REVERSESCROLL, false).toBool();
|
||||||
swapFaceButtons = settings.value(SER_SWAPFACEBUTTONS, false).toBool();
|
swapFaceButtons = settings.value(SER_SWAPFACEBUTTONS, false).toBool();
|
||||||
captureSysKeys = settings.value(SER_CAPTURESYSKEYS, false).toBool();
|
captureSysKeysMode = static_cast<CaptureSysKeysMode>(settings.value(SER_CAPTURESYSKEYS,
|
||||||
|
static_cast<int>(CaptureSysKeysMode::CSK_OFF)).toInt());
|
||||||
audioConfig = static_cast<AudioConfig>(settings.value(SER_AUDIOCFG,
|
audioConfig = static_cast<AudioConfig>(settings.value(SER_AUDIOCFG,
|
||||||
static_cast<int>(AudioConfig::AC_STEREO)).toInt());
|
static_cast<int>(AudioConfig::AC_STEREO)).toInt());
|
||||||
videoCodecConfig = static_cast<VideoCodecConfig>(settings.value(SER_VIDEOCFG,
|
videoCodecConfig = static_cast<VideoCodecConfig>(settings.value(SER_VIDEOCFG,
|
||||||
@@ -95,6 +110,8 @@ void StreamingPreferences::reload()
|
|||||||
uiDisplayMode = static_cast<UIDisplayMode>(settings.value(SER_UIDISPLAYMODE,
|
uiDisplayMode = static_cast<UIDisplayMode>(settings.value(SER_UIDISPLAYMODE,
|
||||||
static_cast<int>(settings.value(SER_STARTWINDOWED, true).toBool() ? UIDisplayMode::UI_WINDOWED
|
static_cast<int>(settings.value(SER_STARTWINDOWED, true).toBool() ? UIDisplayMode::UI_WINDOWED
|
||||||
: UIDisplayMode::UI_MAXIMIZED)).toInt());
|
: UIDisplayMode::UI_MAXIMIZED)).toInt());
|
||||||
|
language = static_cast<Language>(settings.value(SER_LANGUAGE,
|
||||||
|
static_cast<int>(Language::LANG_AUTO)).toInt());
|
||||||
|
|
||||||
|
|
||||||
// Perform default settings updates as required based on last default version
|
// Perform default settings updates as required based on last default version
|
||||||
@@ -108,6 +125,90 @@ void StreamingPreferences::reload()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool StreamingPreferences::retranslate()
|
||||||
|
{
|
||||||
|
static QTranslator* translator = nullptr;
|
||||||
|
|
||||||
|
#if QT_VERSION < QT_VERSION_CHECK(5, 10, 0)
|
||||||
|
if (m_QmlEngine != nullptr) {
|
||||||
|
// Dynamic retranslation is not supported until Qt 5.10
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
QTranslator* newTranslator = new QTranslator();
|
||||||
|
QString languageSuffix = getSuffixFromLanguage(language);
|
||||||
|
|
||||||
|
// Remove the old translator, even if we can't load a new one.
|
||||||
|
// Otherwise we'll be stuck with the old translated values instead
|
||||||
|
// of defaulting to English.
|
||||||
|
if (translator != nullptr) {
|
||||||
|
QCoreApplication::removeTranslator(translator);
|
||||||
|
delete translator;
|
||||||
|
translator = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newTranslator->load(QString(":/languages/qml_") + languageSuffix)) {
|
||||||
|
qInfo() << "Successfully loaded translation for " << languageSuffix;
|
||||||
|
|
||||||
|
translator = newTranslator;
|
||||||
|
QCoreApplication::installTranslator(translator);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
qInfo() << "No translation available for " << languageSuffix;
|
||||||
|
delete newTranslator;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_QmlEngine != nullptr) {
|
||||||
|
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
|
||||||
|
// This is a dynamic retranslation from the settings page.
|
||||||
|
// We have to kick the QML engine into reloading our text.
|
||||||
|
m_QmlEngine->retranslate();
|
||||||
|
#else
|
||||||
|
// Unreachable below Qt 5.10 due to the check above
|
||||||
|
Q_ASSERT(false);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// This is a translation from a non-QML context, which means
|
||||||
|
// it is probably app startup. There's nothing to refresh.
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString StreamingPreferences::getSuffixFromLanguage(StreamingPreferences::Language lang)
|
||||||
|
{
|
||||||
|
switch (lang)
|
||||||
|
{
|
||||||
|
case LANG_DE:
|
||||||
|
return "de";
|
||||||
|
case LANG_EN:
|
||||||
|
return "en";
|
||||||
|
case LANG_FR:
|
||||||
|
return "fr";
|
||||||
|
case LANG_ZH_CN:
|
||||||
|
return "zh_cn";
|
||||||
|
case LANG_NB_NO:
|
||||||
|
return "nb_NO";
|
||||||
|
case LANG_RU:
|
||||||
|
return "ru";
|
||||||
|
case LANG_ES:
|
||||||
|
return "es";
|
||||||
|
case LANG_JA:
|
||||||
|
return "ja";
|
||||||
|
case LANG_VI:
|
||||||
|
return "vi";
|
||||||
|
case LANG_TH:
|
||||||
|
return "th";
|
||||||
|
case LANG_KO:
|
||||||
|
return "ko";
|
||||||
|
case LANG_AUTO:
|
||||||
|
default:
|
||||||
|
return QLocale::system().name();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void StreamingPreferences::save()
|
void StreamingPreferences::save()
|
||||||
{
|
{
|
||||||
QSettings settings;
|
QSettings settings;
|
||||||
@@ -136,13 +237,14 @@ void StreamingPreferences::save()
|
|||||||
settings.setValue(SER_VIDEODEC, static_cast<int>(videoDecoderSelection));
|
settings.setValue(SER_VIDEODEC, static_cast<int>(videoDecoderSelection));
|
||||||
settings.setValue(SER_WINDOWMODE, static_cast<int>(windowMode));
|
settings.setValue(SER_WINDOWMODE, static_cast<int>(windowMode));
|
||||||
settings.setValue(SER_UIDISPLAYMODE, static_cast<int>(uiDisplayMode));
|
settings.setValue(SER_UIDISPLAYMODE, static_cast<int>(uiDisplayMode));
|
||||||
|
settings.setValue(SER_LANGUAGE, static_cast<int>(language));
|
||||||
settings.setValue(SER_DEFAULTVER, CURRENT_DEFAULT_VER);
|
settings.setValue(SER_DEFAULTVER, CURRENT_DEFAULT_VER);
|
||||||
settings.setValue(SER_SWAPMOUSEBUTTONS, swapMouseButtons);
|
settings.setValue(SER_SWAPMOUSEBUTTONS, swapMouseButtons);
|
||||||
settings.setValue(SER_MUTEONFOCUSLOSS, muteOnFocusLoss);
|
settings.setValue(SER_MUTEONFOCUSLOSS, muteOnFocusLoss);
|
||||||
settings.setValue(SER_BACKGROUNDGAMEPAD, backgroundGamepad);
|
settings.setValue(SER_BACKGROUNDGAMEPAD, backgroundGamepad);
|
||||||
settings.setValue(SER_REVERSESCROLL, reverseScrollDirection);
|
settings.setValue(SER_REVERSESCROLL, reverseScrollDirection);
|
||||||
settings.setValue(SER_SWAPFACEBUTTONS, swapFaceButtons);
|
settings.setValue(SER_SWAPFACEBUTTONS, swapFaceButtons);
|
||||||
settings.setValue(SER_CAPTURESYSKEYS, captureSysKeys);
|
settings.setValue(SER_CAPTURESYSKEYS, captureSysKeysMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
int StreamingPreferences::getDefaultBitrate(int width, int height, int fps)
|
int StreamingPreferences::getDefaultBitrate(int width, int height, int fps)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
#include <QRect>
|
#include <QRect>
|
||||||
|
#include <QQmlEngine>
|
||||||
|
|
||||||
class StreamingPreferences : public QObject
|
class StreamingPreferences : public QObject
|
||||||
{
|
{
|
||||||
@@ -9,6 +10,7 @@ class StreamingPreferences : public QObject
|
|||||||
|
|
||||||
public:
|
public:
|
||||||
StreamingPreferences(QObject *parent = nullptr);
|
StreamingPreferences(QObject *parent = nullptr);
|
||||||
|
StreamingPreferences(QQmlEngine *qmlEngine, QObject *parent = nullptr);
|
||||||
|
|
||||||
Q_INVOKABLE static int
|
Q_INVOKABLE static int
|
||||||
getDefaultBitrate(int width, int height, int fps);
|
getDefaultBitrate(int width, int height, int fps);
|
||||||
@@ -58,6 +60,34 @@ public:
|
|||||||
};
|
};
|
||||||
Q_ENUM(UIDisplayMode)
|
Q_ENUM(UIDisplayMode)
|
||||||
|
|
||||||
|
// New entries must go at the end of the enum
|
||||||
|
// to avoid renumbering existing entries (which
|
||||||
|
// would affect existing user preferences).
|
||||||
|
enum Language
|
||||||
|
{
|
||||||
|
LANG_AUTO,
|
||||||
|
LANG_EN,
|
||||||
|
LANG_FR,
|
||||||
|
LANG_ZH_CN,
|
||||||
|
LANG_DE,
|
||||||
|
LANG_NB_NO,
|
||||||
|
LANG_RU,
|
||||||
|
LANG_ES,
|
||||||
|
LANG_JA,
|
||||||
|
LANG_VI,
|
||||||
|
LANG_TH,
|
||||||
|
LANG_KO
|
||||||
|
};
|
||||||
|
Q_ENUM(Language);
|
||||||
|
|
||||||
|
enum CaptureSysKeysMode
|
||||||
|
{
|
||||||
|
CSK_OFF,
|
||||||
|
CSK_FULLSCREEN,
|
||||||
|
CSK_ALWAYS,
|
||||||
|
};
|
||||||
|
Q_ENUM(CaptureSysKeysMode);
|
||||||
|
|
||||||
Q_PROPERTY(int width MEMBER width NOTIFY displayModeChanged)
|
Q_PROPERTY(int width MEMBER width NOTIFY displayModeChanged)
|
||||||
Q_PROPERTY(int height MEMBER height NOTIFY displayModeChanged)
|
Q_PROPERTY(int height MEMBER height NOTIFY displayModeChanged)
|
||||||
Q_PROPERTY(int fps MEMBER fps NOTIFY displayModeChanged)
|
Q_PROPERTY(int fps MEMBER fps NOTIFY displayModeChanged)
|
||||||
@@ -87,7 +117,10 @@ public:
|
|||||||
Q_PROPERTY(bool backgroundGamepad MEMBER backgroundGamepad NOTIFY backgroundGamepadChanged)
|
Q_PROPERTY(bool backgroundGamepad MEMBER backgroundGamepad NOTIFY backgroundGamepadChanged)
|
||||||
Q_PROPERTY(bool reverseScrollDirection MEMBER reverseScrollDirection NOTIFY reverseScrollDirectionChanged)
|
Q_PROPERTY(bool reverseScrollDirection MEMBER reverseScrollDirection NOTIFY reverseScrollDirectionChanged)
|
||||||
Q_PROPERTY(bool swapFaceButtons MEMBER swapFaceButtons NOTIFY swapFaceButtonsChanged)
|
Q_PROPERTY(bool swapFaceButtons MEMBER swapFaceButtons NOTIFY swapFaceButtonsChanged)
|
||||||
Q_PROPERTY(bool captureSysKeys MEMBER captureSysKeys NOTIFY captureSysKeysChanged)
|
Q_PROPERTY(CaptureSysKeysMode captureSysKeysMode MEMBER captureSysKeysMode NOTIFY captureSysKeysModeChanged)
|
||||||
|
Q_PROPERTY(Language language MEMBER language NOTIFY languageChanged);
|
||||||
|
|
||||||
|
Q_INVOKABLE bool retranslate();
|
||||||
|
|
||||||
// Directly accessible members for preferences
|
// Directly accessible members for preferences
|
||||||
int width;
|
int width;
|
||||||
@@ -113,7 +146,6 @@ public:
|
|||||||
bool backgroundGamepad;
|
bool backgroundGamepad;
|
||||||
bool reverseScrollDirection;
|
bool reverseScrollDirection;
|
||||||
bool swapFaceButtons;
|
bool swapFaceButtons;
|
||||||
bool captureSysKeys;
|
|
||||||
int packetSize;
|
int packetSize;
|
||||||
AudioConfig audioConfig;
|
AudioConfig audioConfig;
|
||||||
VideoCodecConfig videoCodecConfig;
|
VideoCodecConfig videoCodecConfig;
|
||||||
@@ -121,6 +153,8 @@ public:
|
|||||||
WindowMode windowMode;
|
WindowMode windowMode;
|
||||||
WindowMode recommendedFullScreenMode;
|
WindowMode recommendedFullScreenMode;
|
||||||
UIDisplayMode uiDisplayMode;
|
UIDisplayMode uiDisplayMode;
|
||||||
|
Language language;
|
||||||
|
CaptureSysKeysMode captureSysKeysMode;
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void displayModeChanged();
|
void displayModeChanged();
|
||||||
@@ -149,6 +183,12 @@ signals:
|
|||||||
void backgroundGamepadChanged();
|
void backgroundGamepadChanged();
|
||||||
void reverseScrollDirectionChanged();
|
void reverseScrollDirectionChanged();
|
||||||
void swapFaceButtonsChanged();
|
void swapFaceButtonsChanged();
|
||||||
void captureSysKeysChanged();
|
void captureSysKeysModeChanged();
|
||||||
|
void languageChanged();
|
||||||
|
|
||||||
|
private:
|
||||||
|
QString getSuffixFromLanguage(Language lang);
|
||||||
|
|
||||||
|
QQmlEngine* m_QmlEngine;
|
||||||
};
|
};
|
||||||
|
|
||||||
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,12 @@
|
|||||||
|
#version 300 es
|
||||||
|
#extension GL_OES_EGL_image_external : require
|
||||||
|
precision mediump float;
|
||||||
|
out vec4 FragColor;
|
||||||
|
|
||||||
|
in vec2 vTextCoord;
|
||||||
|
|
||||||
|
uniform samplerExternalOES uTexture;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
FragColor = texture2D(uTexture, vTextCoord);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
#include "qtaud.h"
|
|
||||||
|
|
||||||
#include <QDebug>
|
|
||||||
|
|
||||||
QtAudioRenderer::QtAudioRenderer()
|
|
||||||
: m_AudioOutput(nullptr),
|
|
||||||
m_OutputDevice(nullptr)
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
QtAudioRenderer::~QtAudioRenderer()
|
|
||||||
{
|
|
||||||
delete m_AudioOutput;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool QtAudioRenderer::prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig)
|
|
||||||
{
|
|
||||||
QAudioFormat format;
|
|
||||||
|
|
||||||
// testAudio() assumes this sample rate
|
|
||||||
Q_ASSERT(opusConfig->sampleRate == 48000);
|
|
||||||
|
|
||||||
format.setSampleRate(opusConfig->sampleRate);
|
|
||||||
format.setChannelCount(opusConfig->channelCount);
|
|
||||||
format.setSampleSize(16);
|
|
||||||
format.setSampleType(QAudioFormat::SignedInt);
|
|
||||||
format.setCodec("audio/pcm");
|
|
||||||
|
|
||||||
m_AudioOutput = new QAudioOutput(format);
|
|
||||||
|
|
||||||
// Buffer 40 ms of audio data
|
|
||||||
m_AudioOutput->setBufferSize(format.bytesForDuration(40000));
|
|
||||||
|
|
||||||
m_OutputDevice = m_AudioOutput->start();
|
|
||||||
if (m_OutputDevice == nullptr) {
|
|
||||||
qWarning() << "Audio start failed:" << m_AudioOutput->error();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check what buffer size was really used after starting
|
|
||||||
qInfo() << "Audio stream buffer:" << m_AudioOutput->bufferSize() << "bytes";
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void QtAudioRenderer::submitAudio(short* audioBuffer, int audioSize)
|
|
||||||
{
|
|
||||||
m_OutputDevice->write((const char *)audioBuffer, audioSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool QtAudioRenderer::testAudio(int audioConfiguration)
|
|
||||||
{
|
|
||||||
QAudioFormat format;
|
|
||||||
|
|
||||||
format.setSampleRate(48000);
|
|
||||||
format.setSampleSize(16);
|
|
||||||
format.setSampleType(QAudioFormat::SignedInt);
|
|
||||||
format.setCodec("audio/pcm");
|
|
||||||
|
|
||||||
switch (audioConfiguration) {
|
|
||||||
case AUDIO_CONFIGURATION_STEREO:
|
|
||||||
format.setChannelCount(2);
|
|
||||||
break;
|
|
||||||
case AUDIO_CONFIGURATION_51_SURROUND:
|
|
||||||
format.setChannelCount(6);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
Q_ASSERT(false);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (QAudioDeviceInfo::defaultOutputDevice().isFormatSupported(format)) {
|
|
||||||
qInfo() << "Audio test successful with" << format.channelCount() << "channels";
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
qWarning() << "Audio test failed with" << format.channelCount() << "channels";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int QtAudioRenderer::detectAudioConfiguration()
|
|
||||||
{
|
|
||||||
int preferredChannelCount = QAudioDeviceInfo::defaultOutputDevice().preferredFormat().channelCount();
|
|
||||||
|
|
||||||
qInfo() << "Audio output device prefers" << preferredChannelCount << "channel configuration";
|
|
||||||
|
|
||||||
// We can better downmix 5.1 to quad than we can upmix stereo
|
|
||||||
if (preferredChannelCount > 2) {
|
|
||||||
return AUDIO_CONFIGURATION_51_SURROUND;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return AUDIO_CONFIGURATION_STEREO;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "renderer.h"
|
|
||||||
#include <QAudioOutput>
|
|
||||||
|
|
||||||
class QtAudioRenderer : public IAudioRenderer
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
QtAudioRenderer();
|
|
||||||
|
|
||||||
virtual ~QtAudioRenderer();
|
|
||||||
|
|
||||||
virtual bool prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig);
|
|
||||||
|
|
||||||
virtual void submitAudio(short* audioBuffer, int audioSize);
|
|
||||||
|
|
||||||
virtual bool testAudio(int audioConfiguration);
|
|
||||||
|
|
||||||
virtual int detectAudioConfiguration();
|
|
||||||
|
|
||||||
private:
|
|
||||||
QAudioOutput* m_AudioOutput;
|
|
||||||
QIODevice* m_OutputDevice;
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,190 @@
|
|||||||
|
#include "input.h"
|
||||||
|
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
int SdlInputHandler::clipboardThreadProc(void *ptr)
|
||||||
|
{
|
||||||
|
auto me = (SdlInputHandler*)ptr;
|
||||||
|
|
||||||
|
while (!SDL_AtomicGet(&me->m_ShutdownClipboardThread)) {
|
||||||
|
QString clipboardData;
|
||||||
|
|
||||||
|
SDL_LockMutex(me->m_ClipboardLock);
|
||||||
|
for (;;) {
|
||||||
|
clipboardData = me->m_ClipboardData;
|
||||||
|
me->m_ClipboardData.clear();
|
||||||
|
if (!clipboardData.isEmpty() || SDL_AtomicGet(&me->m_ShutdownClipboardThread)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
SDL_CondWait(me->m_ClipboardHasData, me->m_ClipboardLock);
|
||||||
|
}
|
||||||
|
SDL_UnlockMutex(me->m_ClipboardLock);
|
||||||
|
|
||||||
|
// We might get here on shutdown, so don't send text if there's
|
||||||
|
// nothing to send.
|
||||||
|
if (!clipboardData.isEmpty()) {
|
||||||
|
me->sendText(clipboardData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#define MAP_KEY(c, sc) \
|
||||||
|
case c: \
|
||||||
|
event.key.keysym.scancode = sc; \
|
||||||
|
break
|
||||||
|
|
||||||
|
#define MAP_KEY_SHIFT(c, sc) \
|
||||||
|
case c: \
|
||||||
|
event.key.keysym.scancode = sc; \
|
||||||
|
event.key.keysym.mod = KMOD_SHIFT; \
|
||||||
|
break
|
||||||
|
|
||||||
|
void SdlInputHandler::sendText(QString& string)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < string.size(); i++) {
|
||||||
|
char16_t c = string[i].unicode();
|
||||||
|
SDL_Event event = {};
|
||||||
|
|
||||||
|
// If we're sending a very long string, we might get a termination request
|
||||||
|
// while we're in the middle of sending a string. In that case, just bail.
|
||||||
|
if (SDL_AtomicGet(&m_ShutdownClipboardThread)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (c >= 'A' && c <= 'Z') {
|
||||||
|
event.key.keysym.scancode = (SDL_Scancode)((c - 'A') + SDL_SCANCODE_A);
|
||||||
|
event.key.keysym.mod = KMOD_SHIFT;
|
||||||
|
}
|
||||||
|
else if (c >= 'a' && c <= 'z') {
|
||||||
|
event.key.keysym.scancode = (SDL_Scancode)((c - 'a') + SDL_SCANCODE_A);
|
||||||
|
}
|
||||||
|
else if (c >= '1' && c <= '9') {
|
||||||
|
event.key.keysym.scancode = (SDL_Scancode)((c - '1') + SDL_SCANCODE_1);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Fixup some Unicode characters to be expressible in ASCII
|
||||||
|
switch (c) {
|
||||||
|
case u'\U0000201C': // Left "
|
||||||
|
case u'\U0000201D': // Right "
|
||||||
|
c = '"';
|
||||||
|
break;
|
||||||
|
|
||||||
|
case u'\U00002018': // Left '
|
||||||
|
case u'\U00002019': // Right '
|
||||||
|
c = '\'';
|
||||||
|
break;
|
||||||
|
|
||||||
|
case u'\U00002013': // Smart -
|
||||||
|
c = '-';
|
||||||
|
break;
|
||||||
|
|
||||||
|
case u'\U00002026': // Ellipsis (...)
|
||||||
|
// Convert this character into 3 periods
|
||||||
|
string.insert(i+1, '.');
|
||||||
|
string.insert(i+1, '.');
|
||||||
|
c = '.';
|
||||||
|
break;
|
||||||
|
|
||||||
|
case u'\U000000A0': // Non-breaking space
|
||||||
|
c = ' ';
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Nothing
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
switch (c) {
|
||||||
|
|
||||||
|
// Handle CRLF separately to avoid duplicate newlines
|
||||||
|
case '\r':
|
||||||
|
if (string[i + 1] == '\n') {
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
event.key.keysym.scancode = SDL_SCANCODE_RETURN;
|
||||||
|
break;
|
||||||
|
|
||||||
|
MAP_KEY('\b', SDL_SCANCODE_BACKSPACE);
|
||||||
|
MAP_KEY('\n', SDL_SCANCODE_RETURN);
|
||||||
|
MAP_KEY('\t', SDL_SCANCODE_TAB);
|
||||||
|
|
||||||
|
MAP_KEY(' ', SDL_SCANCODE_SPACE);
|
||||||
|
MAP_KEY_SHIFT('!', SDL_SCANCODE_1);
|
||||||
|
MAP_KEY_SHIFT('"', SDL_SCANCODE_APOSTROPHE);
|
||||||
|
MAP_KEY_SHIFT('#', SDL_SCANCODE_3);
|
||||||
|
MAP_KEY_SHIFT('$', SDL_SCANCODE_4);
|
||||||
|
MAP_KEY_SHIFT('%', SDL_SCANCODE_5);
|
||||||
|
MAP_KEY_SHIFT('&', SDL_SCANCODE_7);
|
||||||
|
MAP_KEY('\'', SDL_SCANCODE_APOSTROPHE);
|
||||||
|
MAP_KEY_SHIFT('(', SDL_SCANCODE_9);
|
||||||
|
MAP_KEY_SHIFT(')', SDL_SCANCODE_0);
|
||||||
|
MAP_KEY_SHIFT('*', SDL_SCANCODE_8);
|
||||||
|
MAP_KEY_SHIFT('+', SDL_SCANCODE_EQUALS);
|
||||||
|
MAP_KEY(',', SDL_SCANCODE_COMMA);
|
||||||
|
MAP_KEY('-', SDL_SCANCODE_MINUS);
|
||||||
|
MAP_KEY('.', SDL_SCANCODE_PERIOD);
|
||||||
|
MAP_KEY('/', SDL_SCANCODE_SLASH);
|
||||||
|
MAP_KEY('0', SDL_SCANCODE_0);
|
||||||
|
|
||||||
|
MAP_KEY_SHIFT(':', SDL_SCANCODE_SEMICOLON);
|
||||||
|
MAP_KEY(';', SDL_SCANCODE_SEMICOLON);
|
||||||
|
MAP_KEY_SHIFT('<', SDL_SCANCODE_COMMA);
|
||||||
|
MAP_KEY('=', SDL_SCANCODE_EQUALS);
|
||||||
|
MAP_KEY_SHIFT('>', SDL_SCANCODE_PERIOD);
|
||||||
|
MAP_KEY_SHIFT('?', SDL_SCANCODE_SLASH);
|
||||||
|
MAP_KEY_SHIFT('@', SDL_SCANCODE_2);
|
||||||
|
|
||||||
|
MAP_KEY('[', SDL_SCANCODE_LEFTBRACKET);
|
||||||
|
MAP_KEY('\\', SDL_SCANCODE_BACKSLASH);
|
||||||
|
MAP_KEY(']', SDL_SCANCODE_RIGHTBRACKET);
|
||||||
|
MAP_KEY_SHIFT('^', SDL_SCANCODE_6);
|
||||||
|
MAP_KEY_SHIFT('_', SDL_SCANCODE_MINUS);
|
||||||
|
MAP_KEY('`', SDL_SCANCODE_GRAVE);
|
||||||
|
|
||||||
|
MAP_KEY_SHIFT('{', SDL_SCANCODE_LEFTBRACKET);
|
||||||
|
MAP_KEY_SHIFT('|', SDL_SCANCODE_BACKSLASH);
|
||||||
|
MAP_KEY_SHIFT('}', SDL_SCANCODE_RIGHTBRACKET);
|
||||||
|
MAP_KEY_SHIFT('~', SDL_SCANCODE_GRAVE);
|
||||||
|
|
||||||
|
default:
|
||||||
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Pasting text - non-ASCII character value 'U+%x' ignored",
|
||||||
|
c);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key.keysym.mod & KMOD_SHIFT) {
|
||||||
|
SDL_Event modifierEvent = {};
|
||||||
|
modifierEvent.type = SDL_KEYDOWN;
|
||||||
|
modifierEvent.key.state = SDL_PRESSED;
|
||||||
|
modifierEvent.key.keysym.scancode = SDL_SCANCODE_LSHIFT;
|
||||||
|
handleKeyEvent(&modifierEvent.key);
|
||||||
|
|
||||||
|
SDL_Delay(10);
|
||||||
|
}
|
||||||
|
|
||||||
|
event.type = SDL_KEYDOWN;
|
||||||
|
event.key.state = SDL_PRESSED;
|
||||||
|
handleKeyEvent(&event.key);
|
||||||
|
|
||||||
|
SDL_Delay(10);
|
||||||
|
|
||||||
|
event.type = SDL_KEYUP;
|
||||||
|
event.key.state = SDL_RELEASED;
|
||||||
|
handleKeyEvent(&event.key);
|
||||||
|
|
||||||
|
if (event.key.keysym.mod & KMOD_SHIFT) {
|
||||||
|
SDL_Event modifierEvent = {};
|
||||||
|
modifierEvent.type = SDL_KEYUP;
|
||||||
|
modifierEvent.key.state = SDL_RELEASED;
|
||||||
|
modifierEvent.key.keysym.scancode = SDL_SCANCODE_LSHIFT;
|
||||||
|
handleKeyEvent(&modifierEvent.key);
|
||||||
|
|
||||||
|
SDL_Delay(10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -269,7 +269,22 @@ void SdlInputHandler::handleControllerButtonEvent(SDL_ControllerButtonEvent* eve
|
|||||||
event.quit.timestamp = SDL_GetTicks();
|
event.quit.timestamp = SDL_GetTicks();
|
||||||
SDL_PushEvent(&event);
|
SDL_PushEvent(&event);
|
||||||
|
|
||||||
// Clear buttons down on this gameapd
|
// Clear buttons down on this gamepad
|
||||||
|
LiSendMultiControllerEvent(state->index, m_GamepadMask,
|
||||||
|
0, 0, 0, 0, 0, 0, 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle Select+L1+R1+X as a gamepad overlay combo
|
||||||
|
if (state->buttons == (BACK_FLAG | LB_FLAG | RB_FLAG | X_FLAG)) {
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Detected stats toggle gamepad combo");
|
||||||
|
|
||||||
|
// Toggle the stats overlay
|
||||||
|
Session::get()->getOverlayManager().setOverlayState(Overlay::OverlayDebug,
|
||||||
|
!Session::get()->getOverlayManager().isOverlayEnabled(Overlay::OverlayDebug));
|
||||||
|
|
||||||
|
// Clear buttons down on this gamepad
|
||||||
LiSendMultiControllerEvent(state->index, m_GamepadMask,
|
LiSendMultiControllerEvent(state->index, m_GamepadMask,
|
||||||
0, 0, 0, 0, 0, 0, 0);
|
0, 0, 0, 0, 0, 0, 0);
|
||||||
return;
|
return;
|
||||||
|
|||||||
+227
-208
@@ -5,15 +5,16 @@
|
|||||||
#include "path.h"
|
#include "path.h"
|
||||||
#include "utils.h"
|
#include "utils.h"
|
||||||
|
|
||||||
|
#ifdef Q_OS_WIN32
|
||||||
|
#include <SDL_syswm.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
#include <QtGlobal>
|
#include <QtGlobal>
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
|
#include <QGuiApplication>
|
||||||
|
|
||||||
#define MOUSE_POLLING_INTERVAL 5
|
#define MOUSE_POLLING_INTERVAL 5
|
||||||
|
|
||||||
#ifdef Q_OS_WIN32
|
|
||||||
HHOOK g_KeyboardHook;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, NvComputer*, int streamWidth, int streamHeight)
|
SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, NvComputer*, int streamWidth, int streamHeight)
|
||||||
: m_MultiController(prefs.multiController),
|
: m_MultiController(prefs.multiController),
|
||||||
m_GamepadMouse(prefs.gamepadMouse),
|
m_GamepadMouse(prefs.gamepadMouse),
|
||||||
@@ -25,19 +26,25 @@ SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, NvComputer*, int s
|
|||||||
m_MouseWasInVideoRegion(false),
|
m_MouseWasInVideoRegion(false),
|
||||||
m_PendingMouseButtonsAllUpOnVideoRegionLeave(false),
|
m_PendingMouseButtonsAllUpOnVideoRegionLeave(false),
|
||||||
m_FakeCaptureActive(false),
|
m_FakeCaptureActive(false),
|
||||||
m_CaptureSystemKeysEnabled(prefs.captureSysKeys || !WMUtils::isRunningWindowManager()),
|
m_CaptureSystemKeysMode(prefs.captureSysKeysMode),
|
||||||
|
m_MouseCursorCapturedVisibilityState(SDL_DISABLE),
|
||||||
m_LongPressTimer(0),
|
m_LongPressTimer(0),
|
||||||
m_StreamWidth(streamWidth),
|
m_StreamWidth(streamWidth),
|
||||||
m_StreamHeight(streamHeight),
|
m_StreamHeight(streamHeight),
|
||||||
m_AbsoluteMouseMode(prefs.absoluteMouseMode),
|
m_AbsoluteMouseMode(prefs.absoluteMouseMode),
|
||||||
m_AbsoluteTouchMode(prefs.absoluteTouchMode),
|
m_AbsoluteTouchMode(prefs.absoluteTouchMode),
|
||||||
m_PendingMouseLeaveButtonUp(0),
|
|
||||||
m_LeftButtonReleaseTimer(0),
|
m_LeftButtonReleaseTimer(0),
|
||||||
m_RightButtonReleaseTimer(0),
|
m_RightButtonReleaseTimer(0),
|
||||||
m_DragTimer(0),
|
m_DragTimer(0),
|
||||||
m_DragButton(0),
|
m_DragButton(0),
|
||||||
m_NumFingersDown(0)
|
m_NumFingersDown(0),
|
||||||
|
m_ClipboardData()
|
||||||
{
|
{
|
||||||
|
// System keys are always captured when running without a DE
|
||||||
|
if (!WMUtils::isRunningDesktopEnvironment()) {
|
||||||
|
m_CaptureSystemKeysMode = StreamingPreferences::CSK_ALWAYS;
|
||||||
|
}
|
||||||
|
|
||||||
// Allow gamepad input when the app doesn't have focus if requested
|
// Allow gamepad input when the app doesn't have focus if requested
|
||||||
SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, prefs.backgroundGamepad ? "1" : "0");
|
SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, prefs.backgroundGamepad ? "1" : "0");
|
||||||
|
|
||||||
@@ -49,10 +56,15 @@ SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, NvComputer*, int s
|
|||||||
prefs.absoluteMouseMode ? "1" : "0",
|
prefs.absoluteMouseMode ? "1" : "0",
|
||||||
SDL_HINT_OVERRIDE);
|
SDL_HINT_OVERRIDE);
|
||||||
|
|
||||||
// If we're grabbing system keys, enable the X11 keyboard grab in SDL
|
#if !SDL_VERSION_ATLEAST(2, 0, 15)
|
||||||
|
// For older versions of SDL (2.0.14 and earlier), use SDL_HINT_GRAB_KEYBOARD
|
||||||
SDL_SetHintWithPriority(SDL_HINT_GRAB_KEYBOARD,
|
SDL_SetHintWithPriority(SDL_HINT_GRAB_KEYBOARD,
|
||||||
m_CaptureSystemKeysEnabled ? "1" : "0",
|
m_CaptureSystemKeysMode != StreamingPreferences::CSK_OFF ? "1" : "0",
|
||||||
SDL_HINT_OVERRIDE);
|
SDL_HINT_OVERRIDE);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Opt-out of SDL's built-in Alt+Tab handling while keyboard grab is enabled
|
||||||
|
SDL_SetHint("SDL_ALLOW_ALT_TAB_WHILE_GRABBED", "0");
|
||||||
|
|
||||||
// Allow clicks to pass through to us when focusing the window. If we're in
|
// 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
|
// absolute mouse mode, this will avoid the user having to click twice to
|
||||||
@@ -67,6 +79,47 @@ SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, NvComputer*, int s
|
|||||||
SDL_SetHint("SDL_JOYSTICK_HIDAPI_PS4_RUMBLE", "1");
|
SDL_SetHint("SDL_JOYSTICK_HIDAPI_PS4_RUMBLE", "1");
|
||||||
SDL_SetHint("SDL_JOYSTICK_HIDAPI_PS5_RUMBLE", "1");
|
SDL_SetHint("SDL_JOYSTICK_HIDAPI_PS5_RUMBLE", "1");
|
||||||
|
|
||||||
|
// Populate special key combo configuration
|
||||||
|
m_SpecialKeyCombos[KeyComboQuit].keyCombo = KeyComboQuit;
|
||||||
|
m_SpecialKeyCombos[KeyComboQuit].keyCode = SDLK_q;
|
||||||
|
m_SpecialKeyCombos[KeyComboQuit].scanCode = SDL_SCANCODE_Q;
|
||||||
|
m_SpecialKeyCombos[KeyComboQuit].enabled = true;
|
||||||
|
|
||||||
|
m_SpecialKeyCombos[KeyComboUngrabInput].keyCombo = KeyComboUngrabInput;
|
||||||
|
m_SpecialKeyCombos[KeyComboUngrabInput].keyCode = SDLK_z;
|
||||||
|
m_SpecialKeyCombos[KeyComboUngrabInput].scanCode = SDL_SCANCODE_Z;
|
||||||
|
m_SpecialKeyCombos[KeyComboUngrabInput].enabled = QGuiApplication::platformName() != "eglfs";
|
||||||
|
|
||||||
|
m_SpecialKeyCombos[KeyComboToggleFullScreen].keyCombo = KeyComboToggleFullScreen;
|
||||||
|
m_SpecialKeyCombos[KeyComboToggleFullScreen].keyCode = SDLK_x;
|
||||||
|
m_SpecialKeyCombos[KeyComboToggleFullScreen].scanCode = SDL_SCANCODE_X;
|
||||||
|
m_SpecialKeyCombos[KeyComboToggleFullScreen].enabled = QGuiApplication::platformName() != "eglfs";
|
||||||
|
|
||||||
|
m_SpecialKeyCombos[KeyComboToggleStatsOverlay].keyCombo = KeyComboToggleStatsOverlay;
|
||||||
|
m_SpecialKeyCombos[KeyComboToggleStatsOverlay].keyCode = SDLK_s;
|
||||||
|
m_SpecialKeyCombos[KeyComboToggleStatsOverlay].scanCode = SDL_SCANCODE_S;
|
||||||
|
m_SpecialKeyCombos[KeyComboToggleStatsOverlay].enabled = true;
|
||||||
|
|
||||||
|
m_SpecialKeyCombos[KeyComboToggleMouseMode].keyCombo = KeyComboToggleMouseMode;
|
||||||
|
m_SpecialKeyCombos[KeyComboToggleMouseMode].keyCode = SDLK_m;
|
||||||
|
m_SpecialKeyCombos[KeyComboToggleMouseMode].scanCode = SDL_SCANCODE_M;
|
||||||
|
m_SpecialKeyCombos[KeyComboToggleMouseMode].enabled = true;
|
||||||
|
|
||||||
|
m_SpecialKeyCombos[KeyComboToggleCursorHide].keyCombo = KeyComboToggleCursorHide;
|
||||||
|
m_SpecialKeyCombos[KeyComboToggleCursorHide].keyCode = SDLK_c;
|
||||||
|
m_SpecialKeyCombos[KeyComboToggleCursorHide].scanCode = SDL_SCANCODE_C;
|
||||||
|
m_SpecialKeyCombos[KeyComboToggleCursorHide].enabled = true;
|
||||||
|
|
||||||
|
m_SpecialKeyCombos[KeyComboToggleMinimize].keyCombo = KeyComboToggleMinimize;
|
||||||
|
m_SpecialKeyCombos[KeyComboToggleMinimize].keyCode = SDLK_d;
|
||||||
|
m_SpecialKeyCombos[KeyComboToggleMinimize].scanCode = SDL_SCANCODE_D;
|
||||||
|
m_SpecialKeyCombos[KeyComboToggleMinimize].enabled = QGuiApplication::platformName() != "eglfs";
|
||||||
|
|
||||||
|
m_SpecialKeyCombos[KeyComboPasteText].keyCombo = KeyComboPasteText;
|
||||||
|
m_SpecialKeyCombos[KeyComboPasteText].keyCode = SDLK_v;
|
||||||
|
m_SpecialKeyCombos[KeyComboPasteText].scanCode = SDL_SCANCODE_V;
|
||||||
|
m_SpecialKeyCombos[KeyComboPasteText].enabled = true;
|
||||||
|
|
||||||
m_OldIgnoreDevices = SDL_GetHint(SDL_HINT_GAMECONTROLLER_IGNORE_DEVICES);
|
m_OldIgnoreDevices = SDL_GetHint(SDL_HINT_GAMECONTROLLER_IGNORE_DEVICES);
|
||||||
m_OldIgnoreDevicesExcept = SDL_GetHint(SDL_HINT_GAMECONTROLLER_IGNORE_DEVICES_EXCEPT);
|
m_OldIgnoreDevicesExcept = SDL_GetHint(SDL_HINT_GAMECONTROLLER_IGNORE_DEVICES_EXCEPT);
|
||||||
|
|
||||||
@@ -123,6 +176,10 @@ SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, NvComputer*, int s
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifdef Q_OS_DARWIN
|
||||||
|
CGSGetGlobalHotKeyOperatingMode(_CGSDefaultConnection(), &m_OldHotKeyMode);
|
||||||
|
#endif
|
||||||
|
|
||||||
// Initialize the gamepad mask with currently attached gamepads to avoid
|
// Initialize the gamepad mask with currently attached gamepads to avoid
|
||||||
// causing gamepads to unexpectedly disappear and reappear on the host
|
// causing gamepads to unexpectedly disappear and reappear on the host
|
||||||
// during stream startup as we detect currently attached gamepads one at a time.
|
// during stream startup as we detect currently attached gamepads one at a time.
|
||||||
@@ -150,37 +207,16 @@ SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, NvComputer*, int s
|
|||||||
|
|
||||||
m_MouseMoveTimer = SDL_AddTimer(pollingInterval, SdlInputHandler::mouseMoveTimerCallback, this);
|
m_MouseMoveTimer = SDL_AddTimer(pollingInterval, SdlInputHandler::mouseMoveTimerCallback, this);
|
||||||
|
|
||||||
#ifdef Q_OS_WIN32
|
// Initialize state used by the clipboard thread before we start it
|
||||||
if (m_CaptureSystemKeysEnabled) {
|
SDL_AtomicSet(&m_ShutdownClipboardThread, 0);
|
||||||
// If system key capture is enabled, install the window hook required to intercept
|
m_ClipboardHasData = SDL_CreateCond();
|
||||||
// these key presses and block them from the OS itself.
|
m_ClipboardLock = SDL_CreateMutex();
|
||||||
g_KeyboardHook = SetWindowsHookEx(WH_KEYBOARD_LL, SdlInputHandler::keyboardHookProc, GetModuleHandle(NULL), 0);
|
|
||||||
}
|
m_ClipboardThread = SDL_CreateThread(SdlInputHandler::clipboardThreadProc, "Clipboard Sender", this);
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SdlInputHandler::~SdlInputHandler()
|
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++) {
|
for (int i = 0; i < MAX_GAMEPADS; i++) {
|
||||||
if (m_GamepadState[i].mouseEmulationTimer != 0) {
|
if (m_GamepadState[i].mouseEmulationTimer != 0) {
|
||||||
Session::get()->notifyMouseEmulationMode(false);
|
Session::get()->notifyMouseEmulationMode(false);
|
||||||
@@ -202,6 +238,21 @@ SdlInputHandler::~SdlInputHandler()
|
|||||||
SDL_RemoveTimer(m_RightButtonReleaseTimer);
|
SDL_RemoveTimer(m_RightButtonReleaseTimer);
|
||||||
SDL_RemoveTimer(m_DragTimer);
|
SDL_RemoveTimer(m_DragTimer);
|
||||||
|
|
||||||
|
// Wake up the clipboard thread to terminate it
|
||||||
|
SDL_AtomicSet(&m_ShutdownClipboardThread, 1);
|
||||||
|
SDL_CondBroadcast(m_ClipboardHasData);
|
||||||
|
|
||||||
|
// Wait for it to terminate
|
||||||
|
SDL_WaitThread(m_ClipboardThread, nullptr);
|
||||||
|
|
||||||
|
// Now we can safely clean up its resources
|
||||||
|
SDL_DestroyCond(m_ClipboardHasData);
|
||||||
|
SDL_DestroyMutex(m_ClipboardLock);
|
||||||
|
|
||||||
|
#ifdef Q_OS_DARWIN
|
||||||
|
CGSSetGlobalHotKeyOperatingMode(_CGSDefaultConnection(), m_OldHotKeyMode);
|
||||||
|
#endif
|
||||||
|
|
||||||
#if !SDL_VERSION_ATLEAST(2, 0, 9)
|
#if !SDL_VERSION_ATLEAST(2, 0, 9)
|
||||||
SDL_QuitSubSystem(SDL_INIT_HAPTIC);
|
SDL_QuitSubSystem(SDL_INIT_HAPTIC);
|
||||||
SDL_assert(!SDL_WasInit(SDL_INIT_HAPTIC));
|
SDL_assert(!SDL_WasInit(SDL_INIT_HAPTIC));
|
||||||
@@ -234,157 +285,6 @@ void SdlInputHandler::setWindow(SDL_Window *window)
|
|||||||
m_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()
|
void SdlInputHandler::raiseAllKeys()
|
||||||
{
|
{
|
||||||
if (m_KeysDown.isEmpty()) {
|
if (m_KeysDown.isEmpty()) {
|
||||||
@@ -404,20 +304,22 @@ void SdlInputHandler::raiseAllKeys()
|
|||||||
|
|
||||||
void SdlInputHandler::notifyMouseLeave()
|
void SdlInputHandler::notifyMouseLeave()
|
||||||
{
|
{
|
||||||
#ifdef Q_OS_WIN32
|
|
||||||
// SDL on Windows doesn't send the mouse button up until the mouse re-enters the window
|
// 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.
|
// after leaving it. This breaks some of the Aero snap gestures, so we'll capture it to
|
||||||
|
// allow us to receive the mouse button up events later.
|
||||||
|
//
|
||||||
|
// On macOS and X11, capturing the mouse allows us to receive mouse motion outside the
|
||||||
|
// window (button up already worked without capture).
|
||||||
if (m_AbsoluteMouseMode && isCaptureActive()) {
|
if (m_AbsoluteMouseMode && isCaptureActive()) {
|
||||||
// NB: Not using SDL_GetGlobalMouseState() because we want our state not the system's
|
// NB: Not using SDL_GetGlobalMouseState() because we want our state not the system's
|
||||||
Uint32 mouseState = SDL_GetMouseState(nullptr, nullptr);
|
Uint32 mouseState = SDL_GetMouseState(nullptr, nullptr);
|
||||||
for (Uint32 button = SDL_BUTTON_LEFT; button <= SDL_BUTTON_X2; button++) {
|
for (Uint32 button = SDL_BUTTON_LEFT; button <= SDL_BUTTON_X2; button++) {
|
||||||
if (mouseState & SDL_BUTTON(button)) {
|
if (mouseState & SDL_BUTTON(button)) {
|
||||||
m_PendingMouseLeaveButtonUp = button;
|
SDL_CaptureMouse(SDL_TRUE);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void SdlInputHandler::notifyFocusLost()
|
void SdlInputHandler::notifyFocusLost()
|
||||||
@@ -430,11 +332,27 @@ void SdlInputHandler::notifyFocusLost()
|
|||||||
setCaptureActive(false);
|
setCaptureActive(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef Q_OS_DARWIN
|
||||||
|
// Ungrab the keyboard
|
||||||
|
updateKeyboardGrabState();
|
||||||
|
#endif
|
||||||
|
|
||||||
// Raise all keys that are currently pressed. If we don't do this, certain keys
|
// 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.
|
// used in shortcuts that cause focus loss (such as Alt+Tab) may get stuck down.
|
||||||
raiseAllKeys();
|
raiseAllKeys();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SdlInputHandler::notifyFocusGained()
|
||||||
|
{
|
||||||
|
#ifdef Q_OS_DARWIN
|
||||||
|
// Re-grab the keyboard if it was grabbed before focus loss
|
||||||
|
// FIXME: We only do this on macOS because we get a spurious
|
||||||
|
// focus gain when in SDL_WINDOW_FULLSCREEN_DESKTOP on Windows
|
||||||
|
// immediately after losing focus by clicking on another window.
|
||||||
|
updateKeyboardGrabState();
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
bool SdlInputHandler::isCaptureActive()
|
bool SdlInputHandler::isCaptureActive()
|
||||||
{
|
{
|
||||||
if (SDL_GetRelativeMouseMode()) {
|
if (SDL_GetRelativeMouseMode()) {
|
||||||
@@ -445,9 +363,69 @@ bool SdlInputHandler::isCaptureActive()
|
|||||||
return m_FakeCaptureActive;
|
return m_FakeCaptureActive;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SdlInputHandler::updateKeyboardGrabState()
|
||||||
|
{
|
||||||
|
if (m_CaptureSystemKeysMode == StreamingPreferences::CSK_OFF) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool shouldGrab = isCaptureActive();
|
||||||
|
Uint32 windowFlags = SDL_GetWindowFlags(m_Window);
|
||||||
|
if (m_CaptureSystemKeysMode == StreamingPreferences::CSK_FULLSCREEN &&
|
||||||
|
!(windowFlags & SDL_WINDOW_FULLSCREEN)) {
|
||||||
|
// Ungrab if it's fullscreen only and we left fullscreen
|
||||||
|
shouldGrab = false;
|
||||||
|
}
|
||||||
|
#ifdef Q_OS_DARWIN
|
||||||
|
else if (!(windowFlags & SDL_WINDOW_INPUT_FOCUS)) {
|
||||||
|
// Ungrab if we lose input focus on macOS. SDL will handle
|
||||||
|
// this internally for platforms where we use the SDL
|
||||||
|
// SDL_SetWindowKeyboardGrab() API exclusively.
|
||||||
|
//
|
||||||
|
// NB: On X11, we may not have input focus at the time of
|
||||||
|
// the initial keyboard grab update, so we must not enable
|
||||||
|
// this codepath on Linux.
|
||||||
|
shouldGrab = false;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Don't close the window on Alt+F4 when keyboard grab is enabled
|
||||||
|
SDL_SetHint(SDL_HINT_WINDOWS_NO_CLOSE_ON_ALT_F4, shouldGrab ? "1" : "0");
|
||||||
|
|
||||||
|
if (shouldGrab) {
|
||||||
|
#if SDL_VERSION_ATLEAST(2, 0, 15)
|
||||||
|
// On SDL 2.0.15, we can get keyboard-only grab on Win32, X11, and Wayland.
|
||||||
|
// This does nothing on macOS but it sets the SDL_WINDOW_KEYBOARD_GRABBED flag
|
||||||
|
// that we look for to see if keyboard capture is enabled. We'll handle macOS
|
||||||
|
// ourselves below using the private CGSSetGlobalHotKeyOperatingMode() API.
|
||||||
|
SDL_SetWindowKeyboardGrab(m_Window, SDL_TRUE);
|
||||||
|
#else
|
||||||
|
// If we're in full-screen desktop mode and SDL doesn't have keyboard grab yet,
|
||||||
|
// grab the cursor (will grab the keyboard too on X11).
|
||||||
|
if (SDL_GetWindowFlags(m_Window) & SDL_WINDOW_FULLSCREEN) {
|
||||||
|
SDL_SetWindowGrab(m_Window, SDL_TRUE);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
#ifdef Q_OS_DARWIN
|
||||||
|
// SDL doesn't support this private macOS API
|
||||||
|
CGSSetGlobalHotKeyOperatingMode(_CGSDefaultConnection(), CGSGlobalHotKeyDisable);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
#if SDL_VERSION_ATLEAST(2, 0, 15)
|
||||||
|
// Allow the keyboard to leave the window
|
||||||
|
SDL_SetWindowKeyboardGrab(m_Window, SDL_FALSE);
|
||||||
|
#endif
|
||||||
|
#ifdef Q_OS_DARWIN
|
||||||
|
// SDL doesn't support this private macOS API
|
||||||
|
CGSSetGlobalHotKeyOperatingMode(_CGSDefaultConnection(), m_OldHotKeyMode);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bool SdlInputHandler::isSystemKeyCaptureActive()
|
bool SdlInputHandler::isSystemKeyCaptureActive()
|
||||||
{
|
{
|
||||||
if (!m_CaptureSystemKeysEnabled) {
|
if (m_CaptureSystemKeysMode == StreamingPreferences::CSK_OFF) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -456,33 +434,66 @@ bool SdlInputHandler::isSystemKeyCaptureActive()
|
|||||||
}
|
}
|
||||||
|
|
||||||
Uint32 windowFlags = SDL_GetWindowFlags(m_Window);
|
Uint32 windowFlags = SDL_GetWindowFlags(m_Window);
|
||||||
return (windowFlags & SDL_WINDOW_INPUT_FOCUS) &&
|
if (!(windowFlags & SDL_WINDOW_INPUT_FOCUS)
|
||||||
(windowFlags & SDL_WINDOW_INPUT_GRABBED) &&
|
#if SDL_VERSION_ATLEAST(2, 0, 15)
|
||||||
(windowFlags & SDL_WINDOW_FULLSCREEN_DESKTOP);
|
|| !(windowFlags & SDL_WINDOW_KEYBOARD_GRABBED)
|
||||||
|
#else
|
||||||
|
|| !(windowFlags & SDL_WINDOW_INPUT_GRABBED)
|
||||||
|
#endif
|
||||||
|
)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_CaptureSystemKeysMode == StreamingPreferences::CSK_FULLSCREEN &&
|
||||||
|
!(windowFlags & SDL_WINDOW_FULLSCREEN)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void SdlInputHandler::setCaptureActive(bool active)
|
void SdlInputHandler::setCaptureActive(bool active)
|
||||||
{
|
{
|
||||||
if (active) {
|
if (active) {
|
||||||
// If we're in full-screen exclusive mode, grab the cursor so it can't accidentally leave our window.
|
#if defined(Q_OS_WIN32)
|
||||||
// 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 our window is occluded when mouse is captured, the mouse may
|
||||||
if (((SDL_GetWindowFlags(m_Window) & SDL_WINDOW_FULLSCREEN_DESKTOP) != 0 && m_CaptureSystemKeysEnabled) ||
|
// get stuck on top of the occluding window and not be properly
|
||||||
(SDL_GetWindowFlags(m_Window) & SDL_WINDOW_FULLSCREEN_DESKTOP) == SDL_WINDOW_FULLSCREEN) {
|
// captured. We can avoid this by raising our window before we
|
||||||
SDL_SetWindowGrab(m_Window, SDL_TRUE);
|
// capture the mouse. This can also cause problems when keyboard
|
||||||
}
|
// capture is enabled (modifiers will be eaten by our background window).
|
||||||
|
// We can't use SDL_RaiseWindow() because it doesn't let us know if
|
||||||
|
// SetForegroundWindow() failed. In the failure case, we must not
|
||||||
|
// enable capture to avoid trapping the mouse and/or keyboard.
|
||||||
|
{
|
||||||
|
SDL_SysWMinfo info;
|
||||||
|
|
||||||
if (!m_AbsoluteMouseMode) {
|
SDL_VERSION(&info.version);
|
||||||
// If our window is occluded when mouse is captured, the mouse may
|
SDL_GetWindowWMInfo(m_Window, &info);
|
||||||
// get stuck on top of the occluding window and not be properly
|
|
||||||
// captured. We can avoid this by raising our window before we
|
SDL_assert(info.subsystem == SDL_SYSWM_WINDOWS);
|
||||||
// capture the mouse.
|
|
||||||
SDL_RaiseWindow(m_Window);
|
if (!SetForegroundWindow(info.info.win.window)) {
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Not enabling input capture - window is not foreground");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// If we're in full-screen exclusive mode, grab the cursor so it can't accidentally leave our window.
|
||||||
|
if ((SDL_GetWindowFlags(m_Window) & SDL_WINDOW_FULLSCREEN_DESKTOP) == SDL_WINDOW_FULLSCREEN) {
|
||||||
|
#if SDL_VERSION_ATLEAST(2, 0, 15)
|
||||||
|
SDL_SetWindowMouseGrab(m_Window, SDL_TRUE);
|
||||||
|
#else
|
||||||
|
SDL_SetWindowGrab(m_Window, SDL_TRUE);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we're in relative mode, try to activate SDL's relative mouse mode
|
// If we're in relative mode, try to activate SDL's relative mouse mode
|
||||||
if (m_AbsoluteMouseMode || SDL_SetRelativeMouseMode(SDL_TRUE) < 0) {
|
if (m_AbsoluteMouseMode || SDL_SetRelativeMouseMode(SDL_TRUE) < 0) {
|
||||||
// Relative mouse mode didn't work or was disabled, so we'll just hide the cursor
|
// Relative mouse mode didn't work or was disabled, so we'll just hide the cursor
|
||||||
SDL_ShowCursor(SDL_DISABLE);
|
SDL_ShowCursor(m_MouseCursorCapturedVisibilityState);
|
||||||
m_FakeCaptureActive = true;
|
m_FakeCaptureActive = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -515,9 +526,17 @@ void SdlInputHandler::setCaptureActive(bool active)
|
|||||||
SDL_SetRelativeMouseMode(SDL_FALSE);
|
SDL_SetRelativeMouseMode(SDL_FALSE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if SDL_VERSION_ATLEAST(2, 0, 15)
|
||||||
|
// Allow the cursor to leave the bounds of our window again.
|
||||||
|
SDL_SetWindowMouseGrab(m_Window, SDL_FALSE);
|
||||||
|
#else
|
||||||
// Allow the cursor to leave the bounds of our window again.
|
// Allow the cursor to leave the bounds of our window again.
|
||||||
SDL_SetWindowGrab(m_Window, SDL_FALSE);
|
SDL_SetWindowGrab(m_Window, SDL_FALSE);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Now update the keyboard grab
|
||||||
|
updateKeyboardGrabState();
|
||||||
}
|
}
|
||||||
|
|
||||||
void SdlInputHandler::handleTouchFingerEvent(SDL_TouchFingerEvent* event)
|
void SdlInputHandler::handleTouchFingerEvent(SDL_TouchFingerEvent* event)
|
||||||
|
|||||||
+60
-10
@@ -5,13 +5,9 @@
|
|||||||
|
|
||||||
#include <SDL.h>
|
#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_HIDE_CURSOR 1
|
||||||
#define SDL_CODE_SHOW_CURSOR 2
|
#define SDL_CODE_SHOW_CURSOR 2
|
||||||
|
#define SDL_CODE_UNCAPTURE_MOUSE 3
|
||||||
|
|
||||||
struct GamepadState {
|
struct GamepadState {
|
||||||
SDL_GameController* controller;
|
SDL_GameController* controller;
|
||||||
@@ -33,6 +29,25 @@ struct GamepadState {
|
|||||||
unsigned char lt, rt;
|
unsigned char lt, rt;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#ifdef Q_OS_DARWIN
|
||||||
|
#include <CoreGraphics/CGError.h>
|
||||||
|
extern "C" {
|
||||||
|
typedef int CGSConnection;
|
||||||
|
typedef enum {
|
||||||
|
CGSGlobalHotKeyEnable = 0,
|
||||||
|
CGSGlobalHotKeyDisable = 1,
|
||||||
|
} CGSGlobalHotKeyOperatingMode;
|
||||||
|
|
||||||
|
extern CGSConnection _CGSDefaultConnection(void);
|
||||||
|
|
||||||
|
extern CGError CGSGetGlobalHotKeyOperatingMode(CGSConnection connection,
|
||||||
|
CGSGlobalHotKeyOperatingMode* mode);
|
||||||
|
|
||||||
|
extern CGError CGSSetGlobalHotKeyOperatingMode(CGSConnection connection,
|
||||||
|
CGSGlobalHotKeyOperatingMode mode);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
#define MAX_GAMEPADS 4
|
#define MAX_GAMEPADS 4
|
||||||
#define MAX_FINGERS 2
|
#define MAX_FINGERS 2
|
||||||
|
|
||||||
@@ -69,6 +84,8 @@ public:
|
|||||||
|
|
||||||
void handleJoystickArrivalEvent(SDL_JoyDeviceEvent* event);
|
void handleJoystickArrivalEvent(SDL_JoyDeviceEvent* event);
|
||||||
|
|
||||||
|
void sendText(QString& string);
|
||||||
|
|
||||||
void rumble(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor);
|
void rumble(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor);
|
||||||
|
|
||||||
void handleTouchFingerEvent(SDL_TouchFingerEvent* event);
|
void handleTouchFingerEvent(SDL_TouchFingerEvent* event);
|
||||||
@@ -81,6 +98,8 @@ public:
|
|||||||
|
|
||||||
void notifyFocusLost();
|
void notifyFocusLost();
|
||||||
|
|
||||||
|
void notifyFocusGained();
|
||||||
|
|
||||||
bool isCaptureActive();
|
bool isCaptureActive();
|
||||||
|
|
||||||
bool isSystemKeyCaptureActive();
|
bool isSystemKeyCaptureActive();
|
||||||
@@ -93,10 +112,24 @@ public:
|
|||||||
|
|
||||||
void flushMousePositionUpdate();
|
void flushMousePositionUpdate();
|
||||||
|
|
||||||
|
void updateKeyboardGrabState();
|
||||||
|
|
||||||
static
|
static
|
||||||
QString getUnmappedGamepads();
|
QString getUnmappedGamepads();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
enum KeyCombo {
|
||||||
|
KeyComboQuit,
|
||||||
|
KeyComboUngrabInput,
|
||||||
|
KeyComboToggleFullScreen,
|
||||||
|
KeyComboToggleStatsOverlay,
|
||||||
|
KeyComboToggleMouseMode,
|
||||||
|
KeyComboToggleCursorHide,
|
||||||
|
KeyComboToggleMinimize,
|
||||||
|
KeyComboPasteText,
|
||||||
|
KeyComboMax
|
||||||
|
};
|
||||||
|
|
||||||
GamepadState*
|
GamepadState*
|
||||||
findStateForGamepad(SDL_JoystickID id);
|
findStateForGamepad(SDL_JoystickID id);
|
||||||
|
|
||||||
@@ -106,6 +139,8 @@ private:
|
|||||||
|
|
||||||
void handleRelativeFingerEvent(SDL_TouchFingerEvent* event);
|
void handleRelativeFingerEvent(SDL_TouchFingerEvent* event);
|
||||||
|
|
||||||
|
void performSpecialKeyCombo(KeyCombo combo);
|
||||||
|
|
||||||
static
|
static
|
||||||
Uint32 longPressTimerCallback(Uint32 interval, void* param);
|
Uint32 longPressTimerCallback(Uint32 interval, void* param);
|
||||||
|
|
||||||
@@ -124,10 +159,8 @@ private:
|
|||||||
static
|
static
|
||||||
Uint32 dragTimerCallback(Uint32 interval, void* param);
|
Uint32 dragTimerCallback(Uint32 interval, void* param);
|
||||||
|
|
||||||
#ifdef Q_OS_WIN32
|
|
||||||
static
|
static
|
||||||
LRESULT CALLBACK keyboardHookProc(int nCode, WPARAM wParam, LPARAM lParam);
|
int clipboardThreadProc(void *ptr);
|
||||||
#endif
|
|
||||||
|
|
||||||
SDL_Window* m_Window;
|
SDL_Window* m_Window;
|
||||||
bool m_MultiController;
|
bool m_MultiController;
|
||||||
@@ -154,7 +187,19 @@ private:
|
|||||||
bool m_FakeCaptureActive;
|
bool m_FakeCaptureActive;
|
||||||
QString m_OldIgnoreDevices;
|
QString m_OldIgnoreDevices;
|
||||||
QString m_OldIgnoreDevicesExcept;
|
QString m_OldIgnoreDevicesExcept;
|
||||||
bool m_CaptureSystemKeysEnabled;
|
StreamingPreferences::CaptureSysKeysMode m_CaptureSystemKeysMode;
|
||||||
|
int m_MouseCursorCapturedVisibilityState;
|
||||||
|
|
||||||
|
#ifdef Q_OS_DARWIN
|
||||||
|
CGSGlobalHotKeyOperatingMode m_OldHotKeyMode;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
struct {
|
||||||
|
KeyCombo keyCombo;
|
||||||
|
SDL_Keycode keyCode;
|
||||||
|
SDL_Scancode scanCode;
|
||||||
|
bool enabled;
|
||||||
|
} m_SpecialKeyCombos[KeyComboMax];
|
||||||
|
|
||||||
SDL_TouchFingerEvent m_LastTouchDownEvent;
|
SDL_TouchFingerEvent m_LastTouchDownEvent;
|
||||||
SDL_TouchFingerEvent m_LastTouchUpEvent;
|
SDL_TouchFingerEvent m_LastTouchUpEvent;
|
||||||
@@ -163,7 +208,6 @@ private:
|
|||||||
int m_StreamHeight;
|
int m_StreamHeight;
|
||||||
bool m_AbsoluteMouseMode;
|
bool m_AbsoluteMouseMode;
|
||||||
bool m_AbsoluteTouchMode;
|
bool m_AbsoluteTouchMode;
|
||||||
Uint32 m_PendingMouseLeaveButtonUp;
|
|
||||||
|
|
||||||
SDL_TouchFingerEvent m_TouchDownEvent[MAX_FINGERS];
|
SDL_TouchFingerEvent m_TouchDownEvent[MAX_FINGERS];
|
||||||
SDL_TimerID m_LeftButtonReleaseTimer;
|
SDL_TimerID m_LeftButtonReleaseTimer;
|
||||||
@@ -172,5 +216,11 @@ private:
|
|||||||
char m_DragButton;
|
char m_DragButton;
|
||||||
int m_NumFingersDown;
|
int m_NumFingersDown;
|
||||||
|
|
||||||
|
SDL_Thread* m_ClipboardThread;
|
||||||
|
SDL_atomic_t m_ShutdownClipboardThread;
|
||||||
|
QString m_ClipboardData;
|
||||||
|
SDL_cond* m_ClipboardHasData;
|
||||||
|
SDL_mutex* m_ClipboardLock;
|
||||||
|
|
||||||
static const int k_ButtonMap[];
|
static const int k_ButtonMap[];
|
||||||
};
|
};
|
||||||
+142
-168
@@ -3,8 +3,6 @@
|
|||||||
#include <Limelight.h>
|
#include <Limelight.h>
|
||||||
#include <SDL.h>
|
#include <SDL.h>
|
||||||
|
|
||||||
#include <QGuiApplication>
|
|
||||||
|
|
||||||
#define VK_0 0x30
|
#define VK_0 0x30
|
||||||
#define VK_A 0x41
|
#define VK_A 0x41
|
||||||
|
|
||||||
@@ -15,13 +13,134 @@
|
|||||||
#define VK_NUMPAD0 0x60
|
#define VK_NUMPAD0 0x60
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
void SdlInputHandler::performSpecialKeyCombo(KeyCombo combo)
|
||||||
|
{
|
||||||
|
switch (combo) {
|
||||||
|
case KeyComboQuit:
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Detected quit key combo");
|
||||||
|
|
||||||
|
// Push a quit event to the main loop
|
||||||
|
SDL_Event event;
|
||||||
|
event.type = SDL_QUIT;
|
||||||
|
event.quit.timestamp = SDL_GetTicks();
|
||||||
|
SDL_PushEvent(&event);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case KeyComboUngrabInput:
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Detected mouse capture toggle combo");
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case KeyComboToggleFullScreen:
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Detected full-screen toggle combo");
|
||||||
|
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();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case KeyComboToggleStatsOverlay:
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Detected stats toggle combo");
|
||||||
|
|
||||||
|
// Toggle the stats overlay
|
||||||
|
Session::get()->getOverlayManager().setOverlayState(Overlay::OverlayDebug,
|
||||||
|
!Session::get()->getOverlayManager().isOverlayEnabled(Overlay::OverlayDebug));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case KeyComboToggleMouseMode:
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Detected mouse mode toggle combo");
|
||||||
|
|
||||||
|
// Uncapture input
|
||||||
|
setCaptureActive(false);
|
||||||
|
|
||||||
|
// Toggle mouse mode
|
||||||
|
m_AbsoluteMouseMode = !m_AbsoluteMouseMode;
|
||||||
|
|
||||||
|
// Recapture input
|
||||||
|
setCaptureActive(true);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case KeyComboToggleCursorHide:
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Detected show mouse combo");
|
||||||
|
|
||||||
|
if (!SDL_GetRelativeMouseMode()) {
|
||||||
|
m_MouseCursorCapturedVisibilityState = !m_MouseCursorCapturedVisibilityState;
|
||||||
|
SDL_ShowCursor(m_MouseCursorCapturedVisibilityState);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Cursor can only be shown in remote desktop mouse mode");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case KeyComboToggleMinimize:
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Detected minimize combo");
|
||||||
|
SDL_MinimizeWindow(m_Window);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case KeyComboPasteText:
|
||||||
|
{
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Detected type clipboard text combo");
|
||||||
|
|
||||||
|
// Force raise all keys to ensure that none of them interfere
|
||||||
|
// with the text we're going to type.
|
||||||
|
raiseAllKeys();
|
||||||
|
|
||||||
|
const char* text;
|
||||||
|
if (SDL_HasClipboardText() && (text = SDL_GetClipboardText()) != nullptr) {
|
||||||
|
// Append this data to the clipboard data string for the thread to process
|
||||||
|
SDL_LockMutex(m_ClipboardLock);
|
||||||
|
m_ClipboardData.append(text);
|
||||||
|
SDL_CondSignal(m_ClipboardHasData);
|
||||||
|
SDL_UnlockMutex(m_ClipboardLock);
|
||||||
|
|
||||||
|
// SDL_GetClipboardText() allocates, so we must free
|
||||||
|
SDL_free((void*)text);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"No text in clipboard to paste!");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
Q_UNREACHABLE();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#define IS_SYNTHETIC_KEY_EVENT(x) ((x)->timestamp == 0)
|
||||||
|
|
||||||
void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
|
void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
|
||||||
{
|
{
|
||||||
short keyCode;
|
short keyCode;
|
||||||
char modifiers;
|
char modifiers;
|
||||||
|
|
||||||
|
if (event->repeat) {
|
||||||
|
// Ignore repeat key down events
|
||||||
|
SDL_assert(event->state == SDL_PRESSED);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Check for our special key combos
|
// Check for our special key combos
|
||||||
if ((event->state == SDL_PRESSED) &&
|
// Ignore timestamp == 0 which are sent from our keyboard code.
|
||||||
|
if (!IS_SYNTHETIC_KEY_EVENT(event) &&
|
||||||
|
(event->state == SDL_PRESSED) &&
|
||||||
(event->keysym.mod & KMOD_CTRL) &&
|
(event->keysym.mod & KMOD_CTRL) &&
|
||||||
(event->keysym.mod & KMOD_ALT) &&
|
(event->keysym.mod & KMOD_ALT) &&
|
||||||
(event->keysym.mod & KMOD_SHIFT)) {
|
(event->keysym.mod & KMOD_SHIFT)) {
|
||||||
@@ -36,168 +155,19 @@ void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
|
|||||||
// where the SDLK for one shortcut collides with
|
// where the SDLK for one shortcut collides with
|
||||||
// the scancode of another.
|
// the scancode of another.
|
||||||
|
|
||||||
// Check for quit combo (Ctrl+Alt+Shift+Q)
|
for (int i = 0; i < KeyComboMax; i++) {
|
||||||
if (event->keysym.sym == SDLK_q) {
|
if (m_SpecialKeyCombos[i].enabled && event->keysym.sym == m_SpecialKeyCombos[i].keyCode) {
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
performSpecialKeyCombo(m_SpecialKeyCombos[i].keyCombo);
|
||||||
"Detected quit key combo (SDLK)");
|
return;
|
||||||
|
|
||||||
// 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");
|
for (int i = 0; i < KeyComboMax; i++) {
|
||||||
|
if (m_SpecialKeyCombos[i].enabled && event->keysym.scancode == m_SpecialKeyCombos[i].scanCode) {
|
||||||
|
performSpecialKeyCombo(m_SpecialKeyCombos[i].keyCombo);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
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
|
// Set modifier flags
|
||||||
@@ -433,12 +403,16 @@ void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Track the key state so we always know which keys are down
|
// If this is a synthetic keypress from the clipboard code,
|
||||||
if (event->state == SDL_PRESSED) {
|
// this will be on a non-main thread, so don't touch m_KeysDown.
|
||||||
m_KeysDown.insert(keyCode);
|
if (!IS_SYNTHETIC_KEY_EVENT(event)) {
|
||||||
}
|
// Track the key state so we always know which keys are down
|
||||||
else {
|
if (event->state == SDL_PRESSED) {
|
||||||
m_KeysDown.remove(keyCode);
|
m_KeysDown.insert(keyCode);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
m_KeysDown.remove(keyCode);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LiSendKeyboardEvent(0x8000 | keyCode,
|
LiSendKeyboardEvent(0x8000 | keyCode,
|
||||||
|
|||||||
@@ -133,7 +133,15 @@ void SdlInputHandler::flushMousePositionUpdate()
|
|||||||
// c) a mouse button is still down from before the cursor left the video region (to allow smooth dragging)
|
// 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);
|
Uint32 buttonState = SDL_GetMouseState(nullptr, nullptr);
|
||||||
if (buttonState == 0) {
|
if (buttonState == 0) {
|
||||||
m_PendingMouseButtonsAllUpOnVideoRegionLeave = false;
|
if (m_PendingMouseButtonsAllUpOnVideoRegionLeave) {
|
||||||
|
// Tell the main thread to stop capturing the mouse now
|
||||||
|
SDL_Event event;
|
||||||
|
event.type = SDL_USEREVENT;
|
||||||
|
event.user.code = SDL_CODE_UNCAPTURE_MOUSE;
|
||||||
|
SDL_PushEvent(&event);
|
||||||
|
|
||||||
|
m_PendingMouseButtonsAllUpOnVideoRegionLeave = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (mouseInVideoRegion || m_MouseWasInVideoRegion || m_PendingMouseButtonsAllUpOnVideoRegionLeave) {
|
if (mouseInVideoRegion || m_MouseWasInVideoRegion || m_PendingMouseButtonsAllUpOnVideoRegionLeave) {
|
||||||
LiSendMousePositionEvent(x, y, dst.w, dst.h);
|
LiSendMousePositionEvent(x, y, dst.w, dst.h);
|
||||||
@@ -146,7 +154,8 @@ void SdlInputHandler::flushMousePositionUpdate()
|
|||||||
// executes.
|
// executes.
|
||||||
SDL_Event event;
|
SDL_Event event;
|
||||||
event.type = SDL_USEREVENT;
|
event.type = SDL_USEREVENT;
|
||||||
event.user.code = mouseInVideoRegion ? SDL_CODE_HIDE_CURSOR : SDL_CODE_SHOW_CURSOR;
|
event.user.code = (mouseInVideoRegion && m_MouseCursorCapturedVisibilityState == SDL_DISABLE) ?
|
||||||
|
SDL_CODE_HIDE_CURSOR : SDL_CODE_SHOW_CURSOR;
|
||||||
SDL_PushEvent(&event);
|
SDL_PushEvent(&event);
|
||||||
|
|
||||||
if (!mouseInVideoRegion && buttonState != 0) {
|
if (!mouseInVideoRegion && buttonState != 0) {
|
||||||
@@ -250,33 +259,5 @@ Uint32 SdlInputHandler::mouseMoveTimerCallback(Uint32 interval, void *param)
|
|||||||
// Send mouse position updates if applicable
|
// Send mouse position updates if applicable
|
||||||
me->flushMousePositionUpdate();
|
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;
|
return interval;
|
||||||
}
|
}
|
||||||
+109
-62
@@ -37,14 +37,6 @@
|
|||||||
|
|
||||||
#define CONN_TEST_SERVER "qt.conntest.moonlight-stream.org"
|
#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 = {
|
CONNECTION_LISTENER_CALLBACKS Session::k_ConnCallbacks = {
|
||||||
Session::clStageStarting,
|
Session::clStageStarting,
|
||||||
nullptr,
|
nullptr,
|
||||||
@@ -65,11 +57,6 @@ void Session::clStageStarting(int stage)
|
|||||||
// which happens to be the main thread, so it's cool to interact
|
// which happens to be the main thread, so it's cool to interact
|
||||||
// with the GUI in these callbacks.
|
// with the GUI in these callbacks.
|
||||||
emit s_ActiveSession->stageStarting(QString::fromLocal8Bit(LiGetStageName(stage)));
|
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, int errorCode)
|
void Session::clStageFailed(int stage, int errorCode)
|
||||||
@@ -81,11 +68,6 @@ void Session::clStageFailed(int stage, int errorCode)
|
|||||||
char failingPorts[128];
|
char failingPorts[128];
|
||||||
LiStringifyPortFlags(portFlags, ", ", failingPorts, sizeof(failingPorts));
|
LiStringifyPortFlags(portFlags, ", ", failingPorts, sizeof(failingPorts));
|
||||||
emit s_ActiveSession->stageFailed(QString::fromLocal8Bit(LiGetStageName(stage)), errorCode, QString(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(int errorCode)
|
void Session::clConnectionTerminated(int errorCode)
|
||||||
@@ -199,6 +181,11 @@ bool Session::chooseDecoder(StreamingPreferences::VideoDecoderSelection vds,
|
|||||||
{
|
{
|
||||||
DECODER_PARAMETERS params;
|
DECODER_PARAMETERS params;
|
||||||
|
|
||||||
|
// We should never have vsync enabled for test-mode.
|
||||||
|
// It introduces unnecessary delay for renderers that may
|
||||||
|
// block while waiting for a backbuffer swap.
|
||||||
|
SDL_assert(!enableVsync || !testOnly);
|
||||||
|
|
||||||
params.width = width;
|
params.width = width;
|
||||||
params.height = height;
|
params.height = height;
|
||||||
params.frameRate = frameRate;
|
params.frameRate = frameRate;
|
||||||
@@ -310,7 +297,7 @@ void Session::getDecoderInfo(SDL_Window* window,
|
|||||||
|
|
||||||
if (!chooseDecoder(StreamingPreferences::VDS_AUTO,
|
if (!chooseDecoder(StreamingPreferences::VDS_AUTO,
|
||||||
window, VIDEO_FORMAT_H264, 1920, 1080, 60,
|
window, VIDEO_FORMAT_H264, 1920, 1080, 60,
|
||||||
true, false, true, decoder)) {
|
false, false, true, decoder)) {
|
||||||
isHardwareAccelerated = isFullScreenOnly = false;
|
isHardwareAccelerated = isFullScreenOnly = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -328,7 +315,7 @@ bool Session::isHardwareDecodeAvailable(SDL_Window* window,
|
|||||||
{
|
{
|
||||||
IVideoDecoder* decoder;
|
IVideoDecoder* decoder;
|
||||||
|
|
||||||
if (!chooseDecoder(vds, window, videoFormat, width, height, frameRate, true, false, true, decoder)) {
|
if (!chooseDecoder(vds, window, videoFormat, width, height, frameRate, false, false, true, decoder)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -350,7 +337,7 @@ bool Session::populateDecoderProperties(SDL_Window* window)
|
|||||||
m_StreamConfig.width,
|
m_StreamConfig.width,
|
||||||
m_StreamConfig.height,
|
m_StreamConfig.height,
|
||||||
m_StreamConfig.fps,
|
m_StreamConfig.fps,
|
||||||
true, false, true, decoder)) {
|
false, false, true, decoder)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -358,6 +345,10 @@ bool Session::populateDecoderProperties(SDL_Window* window)
|
|||||||
|
|
||||||
m_StreamConfig.colorSpace = decoder->getDecoderColorspace();
|
m_StreamConfig.colorSpace = decoder->getDecoderColorspace();
|
||||||
|
|
||||||
|
if (decoder->isAlwaysFullScreen()) {
|
||||||
|
m_IsFullScreen = true;
|
||||||
|
}
|
||||||
|
|
||||||
delete decoder;
|
delete decoder;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -365,6 +356,7 @@ bool Session::populateDecoderProperties(SDL_Window* window)
|
|||||||
|
|
||||||
Session::Session(NvComputer* computer, NvApp& app, StreamingPreferences *preferences)
|
Session::Session(NvComputer* computer, NvApp& app, StreamingPreferences *preferences)
|
||||||
: m_Preferences(preferences ? preferences : new StreamingPreferences(this)),
|
: m_Preferences(preferences ? preferences : new StreamingPreferences(this)),
|
||||||
|
m_IsFullScreen(m_Preferences->windowMode != StreamingPreferences::WM_WINDOWED || !WMUtils::isRunningDesktopEnvironment()),
|
||||||
m_Computer(computer),
|
m_Computer(computer),
|
||||||
m_App(app),
|
m_App(app),
|
||||||
m_Window(nullptr),
|
m_Window(nullptr),
|
||||||
@@ -441,6 +433,12 @@ bool Session::initialize()
|
|||||||
m_StreamConfig.bitrate = m_Preferences->bitrateKbps;
|
m_StreamConfig.bitrate = m_Preferences->bitrateKbps;
|
||||||
m_StreamConfig.hevcBitratePercentageMultiplier = 75;
|
m_StreamConfig.hevcBitratePercentageMultiplier = 75;
|
||||||
|
|
||||||
|
#ifndef STEAM_LINK
|
||||||
|
// Enable audio encryption as long as we're not on Steam Link.
|
||||||
|
// That hardware can hardly handle Opus decoding at all.
|
||||||
|
m_StreamConfig.encryptionFlags = ENCFLG_AUDIO;
|
||||||
|
#endif
|
||||||
|
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"Video bitrate: %d kbps",
|
"Video bitrate: %d kbps",
|
||||||
m_StreamConfig.bitrate);
|
m_StreamConfig.bitrate);
|
||||||
@@ -524,22 +522,17 @@ bool Session::initialize()
|
|||||||
{
|
{
|
||||||
default:
|
default:
|
||||||
case StreamingPreferences::WM_FULLSCREEN_DESKTOP:
|
case StreamingPreferences::WM_FULLSCREEN_DESKTOP:
|
||||||
m_FullScreenFlag = SDL_WINDOW_FULLSCREEN_DESKTOP;
|
// Only use full-screen desktop mode if we're running a desktop environment
|
||||||
break;
|
if (WMUtils::isRunningDesktopEnvironment()) {
|
||||||
|
m_FullScreenFlag = SDL_WINDOW_FULLSCREEN_DESKTOP;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Fall-through
|
||||||
case StreamingPreferences::WM_FULLSCREEN:
|
case StreamingPreferences::WM_FULLSCREEN:
|
||||||
m_FullScreenFlag = SDL_WINDOW_FULLSCREEN;
|
m_FullScreenFlag = SDL_WINDOW_FULLSCREEN;
|
||||||
break;
|
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)
|
#if !SDL_VERSION_ATLEAST(2, 0, 11)
|
||||||
// HACK: Using a full-screen window breaks mouse capture on the Pi's LXDE
|
// 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
|
// GUI environment. Force the session to use windowed mode (which won't
|
||||||
@@ -581,10 +574,13 @@ void Session::emitLaunchWarning(QString text)
|
|||||||
// to allow it to transition off the screen before continuing.
|
// to allow it to transition off the screen before continuing.
|
||||||
uint32_t start = SDL_GetTicks();
|
uint32_t start = SDL_GetTicks();
|
||||||
while (!SDL_TICKS_PASSED(SDL_GetTicks(), start + 3500)) {
|
while (!SDL_TICKS_PASSED(SDL_GetTicks(), start + 3500)) {
|
||||||
// Pump the UI loop while we wait
|
|
||||||
SDL_Delay(5);
|
SDL_Delay(5);
|
||||||
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
|
|
||||||
QCoreApplication::sendPostedEvents();
|
if (!m_ThreadedExec) {
|
||||||
|
// Pump the UI loop while we wait if we're on the main thread
|
||||||
|
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
|
||||||
|
QCoreApplication::sendPostedEvents();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -592,6 +588,11 @@ bool Session::validateLaunch(SDL_Window* testWindow)
|
|||||||
{
|
{
|
||||||
QStringList warningList;
|
QStringList warningList;
|
||||||
|
|
||||||
|
if (!m_Computer->isSupportedServerVersion) {
|
||||||
|
emit displayLaunchError(tr("The version of GeForce Experience on %1 is not supported by this build of Moonlight. You must update Moonlight to stream from %1.").arg(m_Computer->name));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (m_Preferences->absoluteMouseMode && !m_App.isAppCollectorGame) {
|
if (m_Preferences->absoluteMouseMode && !m_App.isAppCollectorGame) {
|
||||||
emitLaunchWarning(tr("Your selection to enable remote desktop mouse mode may cause problems in games."));
|
emitLaunchWarning(tr("Your selection to enable remote desktop mouse mode may cause problems in games."));
|
||||||
}
|
}
|
||||||
@@ -835,7 +836,7 @@ void Session::getWindowDimensions(int& x, int& y,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fullScreen = (m_Preferences->windowMode != StreamingPreferences::WM_WINDOWED);
|
fullScreen = m_IsFullScreen;
|
||||||
}
|
}
|
||||||
|
|
||||||
SDL_Rect usableBounds;
|
SDL_Rect usableBounds;
|
||||||
@@ -944,10 +945,8 @@ void Session::toggleFullscreen()
|
|||||||
bool fullScreen = !(SDL_GetWindowFlags(m_Window) & m_FullScreenFlag);
|
bool fullScreen = !(SDL_GetWindowFlags(m_Window) & m_FullScreenFlag);
|
||||||
|
|
||||||
if (fullScreen) {
|
if (fullScreen) {
|
||||||
if ((m_FullScreenFlag == SDL_WINDOW_FULLSCREEN || m_Preferences->captureSysKeys) && m_InputHandler->isCaptureActive()) {
|
if (m_FullScreenFlag == SDL_WINDOW_FULLSCREEN && m_InputHandler->isCaptureActive()) {
|
||||||
// Confine the cursor to the window if we're capturing input while transitioning to full screen.
|
// 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_SetWindowGrab(m_Window, SDL_TRUE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -964,6 +963,9 @@ void Session::toggleFullscreen()
|
|||||||
// Reposition the window when the resize is complete
|
// Reposition the window when the resize is complete
|
||||||
m_PendingWindowedTransition = true;
|
m_PendingWindowedTransition = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Input handler might need to start/stop keyboard grab after changing modes
|
||||||
|
m_InputHandler->updateKeyboardGrabState();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Session::notifyMouseEmulationMode(bool enabled)
|
void Session::notifyMouseEmulationMode(bool enabled)
|
||||||
@@ -1002,17 +1004,7 @@ bool Session::startConnectionAsync()
|
|||||||
{
|
{
|
||||||
// Wait 1.5 seconds before connecting to let the user
|
// Wait 1.5 seconds before connecting to let the user
|
||||||
// have time to read any messages present on the segue
|
// have time to read any messages present on the segue
|
||||||
#ifdef USE_ASYNC_CONNECT_THREAD
|
|
||||||
SDL_Delay(1500);
|
SDL_Delay(1500);
|
||||||
#else
|
|
||||||
uint32_t start = SDL_GetTicks();
|
|
||||||
while (!SDL_TICKS_PASSED(SDL_GetTicks(), start + 1500)) {
|
|
||||||
// Pump the UI loop while we wait since we're not async
|
|
||||||
SDL_Delay(5);
|
|
||||||
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
|
|
||||||
QCoreApplication::sendPostedEvents();
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// The UI should have ensured the old game was already quit
|
// The UI should have ensured the old game was already quit
|
||||||
// if we decide to stream a different game.
|
// if we decide to stream a different game.
|
||||||
@@ -1124,11 +1116,58 @@ void Session::flushWindowEvents()
|
|||||||
SDL_PushEvent(&flushEvent);
|
SDL_PushEvent(&flushEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ExecThread : public QThread
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
ExecThread(Session* session) :
|
||||||
|
QThread(nullptr),
|
||||||
|
m_Session(session) {}
|
||||||
|
|
||||||
|
void run() override
|
||||||
|
{
|
||||||
|
m_Session->execInternal();
|
||||||
|
}
|
||||||
|
|
||||||
|
Session* m_Session;
|
||||||
|
};
|
||||||
|
|
||||||
void Session::exec(int displayOriginX, int displayOriginY)
|
void Session::exec(int displayOriginX, int displayOriginY)
|
||||||
{
|
{
|
||||||
m_DisplayOriginX = displayOriginX;
|
m_DisplayOriginX = displayOriginX;
|
||||||
m_DisplayOriginY = displayOriginY;
|
m_DisplayOriginY = displayOriginY;
|
||||||
|
|
||||||
|
// Use a separate thread for the streaming session on X11 or Wayland
|
||||||
|
// to ensure we don't stomp on Qt's GL context. This breaks when using
|
||||||
|
// the Qt EGLFS backend, so we will restrict this to X11
|
||||||
|
m_ThreadedExec = WMUtils::isRunningX11() || WMUtils::isRunningWayland();
|
||||||
|
|
||||||
|
if (m_ThreadedExec) {
|
||||||
|
// Run the streaming session on a separate thread for Linux/BSD
|
||||||
|
ExecThread execThread(this);
|
||||||
|
execThread.start();
|
||||||
|
|
||||||
|
// Until the SDL streaming window is created, we should continue
|
||||||
|
// to update the Qt UI to allow warning messages to display and
|
||||||
|
// make sure that the Qt window can hide itself.
|
||||||
|
while (!execThread.wait(10) && m_Window == nullptr) {
|
||||||
|
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
|
||||||
|
QCoreApplication::sendPostedEvents();
|
||||||
|
}
|
||||||
|
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
|
||||||
|
QCoreApplication::sendPostedEvents();
|
||||||
|
|
||||||
|
// SDL is in charge now. Wait until the streaming thread exits
|
||||||
|
// to further update the Qt window.
|
||||||
|
execThread.wait();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Run the streaming session on the main thread for Windows and macOS
|
||||||
|
execInternal();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Session::execInternal()
|
||||||
|
{
|
||||||
// Complete initialization in this deferred context to avoid
|
// Complete initialization in this deferred context to avoid
|
||||||
// calling expensive functions in the constructor (during the
|
// calling expensive functions in the constructor (during the
|
||||||
// process of loading the StreamSegue).
|
// process of loading the StreamSegue).
|
||||||
@@ -1152,22 +1191,26 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
|||||||
m_StreamConfig.width,
|
m_StreamConfig.width,
|
||||||
m_StreamConfig.height);
|
m_StreamConfig.height);
|
||||||
|
|
||||||
// Kick off the async connection thread while we sit here and pump the event loop
|
|
||||||
AsyncConnectionStartThread asyncConnThread(this);
|
AsyncConnectionStartThread asyncConnThread(this);
|
||||||
#ifdef USE_ASYNC_CONNECT_THREAD
|
if (!m_ThreadedExec) {
|
||||||
asyncConnThread.start();
|
// Kick off the async connection thread while we sit here and pump the event loop
|
||||||
while (!asyncConnThread.wait(10)) {
|
asyncConnThread.start();
|
||||||
|
while (!asyncConnThread.wait(10)) {
|
||||||
|
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
|
||||||
|
QCoreApplication::sendPostedEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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::processEvents(QEventLoop::ExcludeUserInputEvents);
|
||||||
QCoreApplication::sendPostedEvents();
|
QCoreApplication::sendPostedEvents();
|
||||||
}
|
}
|
||||||
#else
|
else {
|
||||||
asyncConnThread.run();
|
// We're already in a separate thread so run the connection operations
|
||||||
#endif
|
// synchronously and don't pump the event loop. The main thread is already
|
||||||
|
// pumping the event loop for us.
|
||||||
// Pump the event loop one last time to ensure we pick up any events from
|
asyncConnThread.run();
|
||||||
// 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 the connection failed, clean up and abort the connection.
|
||||||
if (!m_AsyncConnectionSuccess) {
|
if (!m_AsyncConnectionSuccess) {
|
||||||
@@ -1244,7 +1287,7 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
|||||||
// For non-full screen windows, call getWindowDimensions()
|
// For non-full screen windows, call getWindowDimensions()
|
||||||
// again after creating a window to allow it to account
|
// again after creating a window to allow it to account
|
||||||
// for window chrome size.
|
// for window chrome size.
|
||||||
if (m_Preferences->windowMode == StreamingPreferences::WM_WINDOWED) {
|
if (!m_IsFullScreen) {
|
||||||
getWindowDimensions(x, y, width, height);
|
getWindowDimensions(x, y, width, height);
|
||||||
|
|
||||||
// We must set the size before the position because centering
|
// We must set the size before the position because centering
|
||||||
@@ -1344,6 +1387,9 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
|||||||
case SDL_CODE_SHOW_CURSOR:
|
case SDL_CODE_SHOW_CURSOR:
|
||||||
SDL_ShowCursor(SDL_ENABLE);
|
SDL_ShowCursor(SDL_ENABLE);
|
||||||
break;
|
break;
|
||||||
|
case SDL_CODE_UNCAPTURE_MOUSE:
|
||||||
|
SDL_CaptureMouse(SDL_FALSE);
|
||||||
|
break;
|
||||||
case SDL_CODE_FLUSH_WINDOW_EVENT_BARRIER:
|
case SDL_CODE_FLUSH_WINDOW_EVENT_BARRIER:
|
||||||
m_FlushingWindowEventsRef--;
|
m_FlushingWindowEventsRef--;
|
||||||
break;
|
break;
|
||||||
@@ -1365,6 +1411,7 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
|||||||
if (m_Preferences->muteOnFocusLoss) {
|
if (m_Preferences->muteOnFocusLoss) {
|
||||||
m_AudioMuted = false;
|
m_AudioMuted = false;
|
||||||
}
|
}
|
||||||
|
m_InputHandler->notifyFocusGained();
|
||||||
break;
|
break;
|
||||||
case SDL_WINDOWEVENT_LEAVE:
|
case SDL_WINDOWEVENT_LEAVE:
|
||||||
m_InputHandler->notifyMouseLeave();
|
m_InputHandler->notifyMouseLeave();
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ class Session : public QObject
|
|||||||
friend class SdlInputHandler;
|
friend class SdlInputHandler;
|
||||||
friend class DeferredSessionCleanupTask;
|
friend class DeferredSessionCleanupTask;
|
||||||
friend class AsyncConnectionStartThread;
|
friend class AsyncConnectionStartThread;
|
||||||
|
friend class ExecThread;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit Session(NvComputer* computer, NvApp& app, StreamingPreferences *preferences = nullptr);
|
explicit Session(NvComputer* computer, NvApp& app, StreamingPreferences *preferences = nullptr);
|
||||||
@@ -57,6 +58,8 @@ signals:
|
|||||||
void sessionFinished(int portTestResult);
|
void sessionFinished(int portTestResult);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
void execInternal();
|
||||||
|
|
||||||
bool initialize();
|
bool initialize();
|
||||||
|
|
||||||
bool startConnectionAsync();
|
bool startConnectionAsync();
|
||||||
@@ -133,6 +136,7 @@ private:
|
|||||||
int drSubmitDecodeUnit(PDECODE_UNIT du);
|
int drSubmitDecodeUnit(PDECODE_UNIT du);
|
||||||
|
|
||||||
StreamingPreferences* m_Preferences;
|
StreamingPreferences* m_Preferences;
|
||||||
|
bool m_IsFullScreen;
|
||||||
STREAM_CONFIGURATION m_StreamConfig;
|
STREAM_CONFIGURATION m_StreamConfig;
|
||||||
DECODER_RENDERER_CALLBACKS m_VideoCallbacks;
|
DECODER_RENDERER_CALLBACKS m_VideoCallbacks;
|
||||||
AUDIO_RENDERER_CALLBACKS m_AudioCallbacks;
|
AUDIO_RENDERER_CALLBACKS m_AudioCallbacks;
|
||||||
@@ -147,6 +151,7 @@ private:
|
|||||||
Uint32 m_FullScreenFlag;
|
Uint32 m_FullScreenFlag;
|
||||||
int m_DisplayOriginX;
|
int m_DisplayOriginX;
|
||||||
int m_DisplayOriginY;
|
int m_DisplayOriginY;
|
||||||
|
bool m_ThreadedExec;
|
||||||
bool m_PendingWindowedTransition;
|
bool m_PendingWindowedTransition;
|
||||||
bool m_UnexpectedTermination;
|
bool m_UnexpectedTermination;
|
||||||
SdlInputHandler* m_InputHandler;
|
SdlInputHandler* m_InputHandler;
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ typedef struct _VIDEO_STATS {
|
|||||||
uint32_t totalDecodeTime;
|
uint32_t totalDecodeTime;
|
||||||
uint32_t totalPacerTime;
|
uint32_t totalPacerTime;
|
||||||
uint32_t totalRenderTime;
|
uint32_t totalRenderTime;
|
||||||
|
uint32_t lastRtt;
|
||||||
|
uint32_t lastRttVariance;
|
||||||
float totalFps;
|
float totalFps;
|
||||||
float receivedFps;
|
float receivedFps;
|
||||||
float decodedFps;
|
float decodedFps;
|
||||||
|
|||||||
@@ -14,12 +14,24 @@ extern "C" {
|
|||||||
|
|
||||||
#include <Limelight.h>
|
#include <Limelight.h>
|
||||||
|
|
||||||
|
#include <SDL_syswm.h>
|
||||||
|
|
||||||
DrmRenderer::DrmRenderer()
|
DrmRenderer::DrmRenderer()
|
||||||
: m_DrmFd(-1),
|
: m_HwContext(nullptr),
|
||||||
|
m_DrmFd(-1),
|
||||||
|
m_SdlOwnsDrmFd(false),
|
||||||
|
m_SupportsDirectRendering(false),
|
||||||
m_CrtcId(0),
|
m_CrtcId(0),
|
||||||
m_PlaneId(0),
|
m_PlaneId(0),
|
||||||
m_CurrentFbId(0)
|
m_CurrentFbId(0)
|
||||||
{
|
{
|
||||||
|
#ifdef HAVE_EGL
|
||||||
|
m_EGLExtDmaBuf = false;
|
||||||
|
m_eglCreateImage = nullptr;
|
||||||
|
m_eglCreateImageKHR = nullptr;
|
||||||
|
m_eglDestroyImage = nullptr;
|
||||||
|
m_eglDestroyImageKHR = nullptr;
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
DrmRenderer::~DrmRenderer()
|
DrmRenderer::~DrmRenderer()
|
||||||
@@ -28,14 +40,22 @@ DrmRenderer::~DrmRenderer()
|
|||||||
drmModeRmFB(m_DrmFd, m_CurrentFbId);
|
drmModeRmFB(m_DrmFd, m_CurrentFbId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (m_DrmFd != -1) {
|
if (m_HwContext != nullptr) {
|
||||||
|
av_buffer_unref(&m_HwContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!m_SdlOwnsDrmFd && m_DrmFd != -1) {
|
||||||
close(m_DrmFd);
|
close(m_DrmFd);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool DrmRenderer::prepareDecoderContext(AVCodecContext*, AVDictionary**)
|
bool DrmRenderer::prepareDecoderContext(AVCodecContext* context, AVDictionary** options)
|
||||||
{
|
{
|
||||||
/* Nothing to do */
|
// The out-of-tree LibreELEC patches use this option to control the type of the V4L2
|
||||||
|
// buffers that we get back. We only support NV12 buffers now.
|
||||||
|
av_dict_set_int(options, "pixel_format", AV_PIX_FMT_NV12, 0);
|
||||||
|
|
||||||
|
context->hw_device_ctx = av_buffer_ref(m_HwContext);
|
||||||
|
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"Using DRM renderer");
|
"Using DRM renderer");
|
||||||
@@ -43,33 +63,89 @@ bool DrmRenderer::prepareDecoderContext(AVCodecContext*, AVDictionary**)
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool DrmRenderer::initialize(PDECODER_PARAMETERS)
|
bool DrmRenderer::initialize(PDECODER_PARAMETERS params)
|
||||||
{
|
{
|
||||||
const char* device = SDL_getenv("DRM_DEV");
|
|
||||||
int i;
|
int i;
|
||||||
|
|
||||||
if (device == nullptr) {
|
#if SDL_VERSION_ATLEAST(2, 0, 15)
|
||||||
device = "/dev/dri/card0";
|
SDL_SysWMinfo info;
|
||||||
}
|
|
||||||
|
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_VERSION(&info.version);
|
||||||
"Opening DRM device: %s",
|
|
||||||
device);
|
|
||||||
|
|
||||||
m_DrmFd = open(device, O_RDWR | O_CLOEXEC);
|
if (!SDL_GetWindowWMInfo(params->window, &info)) {
|
||||||
if (m_DrmFd < 0) {
|
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"Failed to open DRM device: %d",
|
"SDL_GetWindowWMInfo() failed: %s",
|
||||||
errno);
|
SDL_GetError());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (info.subsystem == SDL_SYSWM_KMSDRM) {
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Sharing DRM FD with SDL");
|
||||||
|
|
||||||
|
SDL_assert(info.info.kmsdrm.drm_fd >= 0);
|
||||||
|
m_DrmFd = info.info.kmsdrm.drm_fd;
|
||||||
|
m_SdlOwnsDrmFd = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
const char* device = SDL_getenv("DRM_DEV");
|
||||||
|
|
||||||
|
if (device == nullptr) {
|
||||||
|
device = "/dev/dri/card0";
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Opening DRM device: %s",
|
||||||
|
device);
|
||||||
|
|
||||||
|
m_DrmFd = open(device, O_RDWR | O_CLOEXEC);
|
||||||
|
if (m_DrmFd < 0) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Failed to open DRM device: %d",
|
||||||
|
errno);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the device context first because it is needed whether we can
|
||||||
|
// actually use direct rendering or not.
|
||||||
|
m_HwContext = av_hwdevice_ctx_alloc(AV_HWDEVICE_TYPE_DRM);
|
||||||
|
if (m_HwContext == nullptr) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"av_hwdevice_ctx_alloc(DRM) failed");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
AVHWDeviceContext* deviceContext = (AVHWDeviceContext*)m_HwContext->data;
|
||||||
|
AVDRMDeviceContext* drmDeviceContext = (AVDRMDeviceContext*)deviceContext->hwctx;
|
||||||
|
|
||||||
|
drmDeviceContext->fd = m_DrmFd;
|
||||||
|
|
||||||
|
int err = av_hwdevice_ctx_init(m_HwContext);
|
||||||
|
if (err < 0) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"av_hwdevice_ctx_init(DRM) failed: %d",
|
||||||
|
err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef HAVE_EGL
|
||||||
|
// Still return true if we fail to initialize DRM direct rendering
|
||||||
|
// stuff, since we have EGL that we can use for indirect rendering.
|
||||||
|
const bool DIRECT_RENDERING_INIT_FAILED = true;
|
||||||
|
#else
|
||||||
|
// Fail if we can't initialize direct rendering and we don't have EGL.
|
||||||
|
const bool DIRECT_RENDERING_INIT_FAILED = false;
|
||||||
|
#endif
|
||||||
|
|
||||||
drmModeRes* resources = drmModeGetResources(m_DrmFd);
|
drmModeRes* resources = drmModeGetResources(m_DrmFd);
|
||||||
if (resources == nullptr) {
|
if (resources == nullptr) {
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"drmModeGetResources() failed: %d",
|
"drmModeGetResources() failed: %d",
|
||||||
errno);
|
errno);
|
||||||
return false;
|
return DIRECT_RENDERING_INIT_FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Look for a connected connector and get the associated encoder
|
// Look for a connected connector and get the associated encoder
|
||||||
@@ -89,7 +165,7 @@ bool DrmRenderer::initialize(PDECODER_PARAMETERS)
|
|||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"No connected displays found!");
|
"No connected displays found!");
|
||||||
drmModeFreeResources(resources);
|
drmModeFreeResources(resources);
|
||||||
return false;
|
return DIRECT_RENDERING_INIT_FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now find the CRTC from the encoder
|
// Now find the CRTC from the encoder
|
||||||
@@ -109,7 +185,7 @@ bool DrmRenderer::initialize(PDECODER_PARAMETERS)
|
|||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"DRM encoder not found!");
|
"DRM encoder not found!");
|
||||||
drmModeFreeResources(resources);
|
drmModeFreeResources(resources);
|
||||||
return false;
|
return DIRECT_RENDERING_INIT_FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
int crtcIndex = -1;
|
int crtcIndex = -1;
|
||||||
@@ -130,7 +206,7 @@ bool DrmRenderer::initialize(PDECODER_PARAMETERS)
|
|||||||
if (crtcIndex == -1) {
|
if (crtcIndex == -1) {
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"Failed to get CRTC!");
|
"Failed to get CRTC!");
|
||||||
return false;
|
return DIRECT_RENDERING_INIT_FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
drmSetClientCap(m_DrmFd, DRM_CLIENT_CAP_UNIVERSAL_PLANES, 1);
|
drmSetClientCap(m_DrmFd, DRM_CLIENT_CAP_UNIVERSAL_PLANES, 1);
|
||||||
@@ -140,7 +216,7 @@ bool DrmRenderer::initialize(PDECODER_PARAMETERS)
|
|||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"drmGetPlaneResources() failed: %d",
|
"drmGetPlaneResources() failed: %d",
|
||||||
errno);
|
errno);
|
||||||
return false;
|
return DIRECT_RENDERING_INIT_FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find an NV12 overlay plane to render on
|
// Find an NV12 overlay plane to render on
|
||||||
@@ -185,6 +261,15 @@ bool DrmRenderer::initialize(PDECODER_PARAMETERS)
|
|||||||
|
|
||||||
drmModeFreePlaneResources(planeRes);
|
drmModeFreePlaneResources(planeRes);
|
||||||
|
|
||||||
|
if (m_PlaneId == 0) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Failed to find suitable NV12 overlay plane!");
|
||||||
|
return DIRECT_RENDERING_INIT_FAILED;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we got this far, we can do direct rendering via the DRM FD.
|
||||||
|
m_SupportsDirectRendering = true;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,6 +298,8 @@ void DrmRenderer::renderFrame(AVFrame* frame)
|
|||||||
uint32_t handles[4] = {};
|
uint32_t handles[4] = {};
|
||||||
uint32_t pitches[4] = {};
|
uint32_t pitches[4] = {};
|
||||||
uint32_t offsets[4] = {};
|
uint32_t offsets[4] = {};
|
||||||
|
uint64_t modifiers[4] = {};
|
||||||
|
uint32_t flags = 0;
|
||||||
|
|
||||||
SDL_Rect src, dst;
|
SDL_Rect src, dst;
|
||||||
|
|
||||||
@@ -234,12 +321,18 @@ void DrmRenderer::renderFrame(AVFrame* frame)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pass along the modifiers to DRM if there are some in the descriptor
|
||||||
|
if (drmFrame->objects[0].format_modifier != DRM_FORMAT_MOD_INVALID) {
|
||||||
|
flags |= DRM_MODE_FB_MODIFIERS;
|
||||||
|
}
|
||||||
|
|
||||||
SDL_assert(drmFrame->nb_layers == 1);
|
SDL_assert(drmFrame->nb_layers == 1);
|
||||||
SDL_assert(drmFrame->layers[0].nb_planes == 2);
|
SDL_assert(drmFrame->layers[0].nb_planes == 2);
|
||||||
for (int i = 0; i < drmFrame->layers[0].nb_planes; i++) {
|
for (int i = 0; i < drmFrame->layers[0].nb_planes; i++) {
|
||||||
handles[i] = primeHandle;
|
handles[i] = primeHandle;
|
||||||
pitches[i] = drmFrame->layers[0].planes[i].pitch;
|
pitches[i] = drmFrame->layers[0].planes[i].pitch;
|
||||||
offsets[i] = drmFrame->layers[0].planes[i].offset;
|
offsets[i] = drmFrame->layers[0].planes[i].offset;
|
||||||
|
modifiers[i] = drmFrame->objects[0].format_modifier;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remember the last FB object we created so we can free it
|
// Remember the last FB object we created so we can free it
|
||||||
@@ -247,12 +340,15 @@ void DrmRenderer::renderFrame(AVFrame* frame)
|
|||||||
uint32_t lastFbId = m_CurrentFbId;
|
uint32_t lastFbId = m_CurrentFbId;
|
||||||
|
|
||||||
// Create a frame buffer object from the PRIME buffer
|
// Create a frame buffer object from the PRIME buffer
|
||||||
err = drmModeAddFB2(m_DrmFd, frame->width, frame->height,
|
// NB: It is an error to pass modifiers without DRM_MODE_FB_MODIFIERS set.
|
||||||
drmFrame->layers[0].format,
|
err = drmModeAddFB2WithModifiers(m_DrmFd, frame->width, frame->height,
|
||||||
handles, pitches, offsets, &m_CurrentFbId, 0);
|
drmFrame->layers[0].format,
|
||||||
|
handles, pitches, offsets,
|
||||||
|
(flags & DRM_MODE_FB_MODIFIERS) ? modifiers : NULL,
|
||||||
|
&m_CurrentFbId, flags);
|
||||||
if (err < 0) {
|
if (err < 0) {
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"drmModeAddFB2() failed: %d",
|
"drmModeAddFB2WithModifiers() failed: %d",
|
||||||
errno);
|
errno);
|
||||||
m_CurrentFbId = lastFbId;
|
m_CurrentFbId = lastFbId;
|
||||||
return;
|
return;
|
||||||
@@ -277,3 +373,200 @@ void DrmRenderer::renderFrame(AVFrame* frame)
|
|||||||
// Free the previous FB object which has now been superseded
|
// Free the previous FB object which has now been superseded
|
||||||
drmModeRmFB(m_DrmFd, lastFbId);
|
drmModeRmFB(m_DrmFd, lastFbId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool DrmRenderer::needsTestFrame()
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DrmRenderer::isDirectRenderingSupported()
|
||||||
|
{
|
||||||
|
return m_SupportsDirectRendering;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef HAVE_EGL
|
||||||
|
|
||||||
|
bool DrmRenderer::canExportEGL() {
|
||||||
|
if (qgetenv("DRM_FORCE_DIRECT") == "1") {
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Using direct rendering due to environment variable");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"DRM backend supports exporting EGLImage");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
AVPixelFormat DrmRenderer::getEGLImagePixelFormat() {
|
||||||
|
// This tells EGLRenderer to treat the EGLImage as a single opaque texture
|
||||||
|
return AV_PIX_FMT_DRM_PRIME;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DrmRenderer::initializeEGL(EGLDisplay,
|
||||||
|
const EGLExtensions &ext) {
|
||||||
|
if (!ext.isSupported("EGL_EXT_image_dma_buf_import")) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"DRM-EGL: DMABUF unsupported");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_EGLExtDmaBuf = ext.isSupported("EGL_EXT_image_dma_buf_import_modifiers");
|
||||||
|
|
||||||
|
// NB: eglCreateImage() and eglCreateImageKHR() have slightly different definitions
|
||||||
|
m_eglCreateImage = (typeof(m_eglCreateImage))eglGetProcAddress("eglCreateImage");
|
||||||
|
m_eglCreateImageKHR = (typeof(m_eglCreateImageKHR))eglGetProcAddress("eglCreateImageKHR");
|
||||||
|
m_eglDestroyImage = (typeof(m_eglDestroyImage))eglGetProcAddress("eglDestroyImage");
|
||||||
|
m_eglDestroyImageKHR = (typeof(m_eglDestroyImageKHR))eglGetProcAddress("eglDestroyImageKHR");
|
||||||
|
|
||||||
|
if (!(m_eglCreateImage && m_eglDestroyImage) &&
|
||||||
|
!(m_eglCreateImageKHR && m_eglDestroyImageKHR)) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Missing eglCreateImage()/eglDestroyImage() in EGL driver");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
ssize_t DrmRenderer::exportEGLImages(AVFrame *frame, EGLDisplay dpy,
|
||||||
|
EGLImage images[EGL_MAX_PLANES]) {
|
||||||
|
AVDRMFrameDescriptor* drmFrame = (AVDRMFrameDescriptor*)frame->data[0];
|
||||||
|
|
||||||
|
memset(images, 0, sizeof(EGLImage) * EGL_MAX_PLANES);
|
||||||
|
|
||||||
|
SDL_assert(drmFrame->nb_objects == 1);
|
||||||
|
SDL_assert(drmFrame->nb_layers == 1);
|
||||||
|
|
||||||
|
// Max 30 attributes (1 key + 1 value for each)
|
||||||
|
const int MAX_ATTRIB_COUNT = 30 * 2;
|
||||||
|
EGLAttrib attribs[MAX_ATTRIB_COUNT] = {
|
||||||
|
EGL_LINUX_DRM_FOURCC_EXT, (EGLAttrib)drmFrame->layers[0].format,
|
||||||
|
EGL_WIDTH, frame->width,
|
||||||
|
EGL_HEIGHT, frame->height,
|
||||||
|
};
|
||||||
|
int attribIndex = 6;
|
||||||
|
|
||||||
|
for (int i = 0; i < drmFrame->layers[0].nb_planes; ++i) {
|
||||||
|
const auto &plane = drmFrame->layers[0].planes[i];
|
||||||
|
const auto &object = drmFrame->objects[plane.object_index];
|
||||||
|
|
||||||
|
switch (i) {
|
||||||
|
case 0:
|
||||||
|
attribs[attribIndex++] = EGL_DMA_BUF_PLANE0_FD_EXT;
|
||||||
|
attribs[attribIndex++] = object.fd;
|
||||||
|
attribs[attribIndex++] = EGL_DMA_BUF_PLANE0_OFFSET_EXT;
|
||||||
|
attribs[attribIndex++] = plane.offset;
|
||||||
|
attribs[attribIndex++] = EGL_DMA_BUF_PLANE0_PITCH_EXT;
|
||||||
|
attribs[attribIndex++] = plane.pitch;
|
||||||
|
if (m_EGLExtDmaBuf && object.format_modifier != DRM_FORMAT_MOD_INVALID) {
|
||||||
|
attribs[attribIndex++] = EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT;
|
||||||
|
attribs[attribIndex++] = (EGLint)(object.format_modifier & 0xFFFFFFFF);
|
||||||
|
attribs[attribIndex++] = EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT;
|
||||||
|
attribs[attribIndex++] = (EGLint)(object.format_modifier >> 32);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 1:
|
||||||
|
attribs[attribIndex++] = EGL_DMA_BUF_PLANE1_FD_EXT;
|
||||||
|
attribs[attribIndex++] = object.fd;
|
||||||
|
attribs[attribIndex++] = EGL_DMA_BUF_PLANE1_OFFSET_EXT;
|
||||||
|
attribs[attribIndex++] = plane.offset;
|
||||||
|
attribs[attribIndex++] = EGL_DMA_BUF_PLANE1_PITCH_EXT;
|
||||||
|
attribs[attribIndex++] = plane.pitch;
|
||||||
|
if (m_EGLExtDmaBuf && object.format_modifier != DRM_FORMAT_MOD_INVALID) {
|
||||||
|
attribs[attribIndex++] = EGL_DMA_BUF_PLANE1_MODIFIER_LO_EXT;
|
||||||
|
attribs[attribIndex++] = (EGLint)(object.format_modifier & 0xFFFFFFFF);
|
||||||
|
attribs[attribIndex++] = EGL_DMA_BUF_PLANE1_MODIFIER_HI_EXT;
|
||||||
|
attribs[attribIndex++] = (EGLint)(object.format_modifier >> 32);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 2:
|
||||||
|
attribs[attribIndex++] = EGL_DMA_BUF_PLANE2_FD_EXT;
|
||||||
|
attribs[attribIndex++] = object.fd;
|
||||||
|
attribs[attribIndex++] = EGL_DMA_BUF_PLANE2_OFFSET_EXT;
|
||||||
|
attribs[attribIndex++] = plane.offset;
|
||||||
|
attribs[attribIndex++] = EGL_DMA_BUF_PLANE2_PITCH_EXT;
|
||||||
|
attribs[attribIndex++] = plane.pitch;
|
||||||
|
if (m_EGLExtDmaBuf && object.format_modifier != DRM_FORMAT_MOD_INVALID) {
|
||||||
|
attribs[attribIndex++] = EGL_DMA_BUF_PLANE2_MODIFIER_LO_EXT;
|
||||||
|
attribs[attribIndex++] = (EGLint)(object.format_modifier & 0xFFFFFFFF);
|
||||||
|
attribs[attribIndex++] = EGL_DMA_BUF_PLANE2_MODIFIER_HI_EXT;
|
||||||
|
attribs[attribIndex++] = (EGLint)(object.format_modifier >> 32);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 3:
|
||||||
|
attribs[attribIndex++] = EGL_DMA_BUF_PLANE3_FD_EXT;
|
||||||
|
attribs[attribIndex++] = object.fd;
|
||||||
|
attribs[attribIndex++] = EGL_DMA_BUF_PLANE3_OFFSET_EXT;
|
||||||
|
attribs[attribIndex++] = plane.offset;
|
||||||
|
attribs[attribIndex++] = EGL_DMA_BUF_PLANE3_PITCH_EXT;
|
||||||
|
attribs[attribIndex++] = plane.pitch;
|
||||||
|
if (m_EGLExtDmaBuf && object.format_modifier != DRM_FORMAT_MOD_INVALID) {
|
||||||
|
attribs[attribIndex++] = EGL_DMA_BUF_PLANE3_MODIFIER_LO_EXT;
|
||||||
|
attribs[attribIndex++] = (EGLint)(object.format_modifier & 0xFFFFFFFF);
|
||||||
|
attribs[attribIndex++] = EGL_DMA_BUF_PLANE3_MODIFIER_HI_EXT;
|
||||||
|
attribs[attribIndex++] = (EGLint)(object.format_modifier >> 32);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
Q_UNREACHABLE();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Terminate the attribute list
|
||||||
|
attribs[attribIndex++] = EGL_NONE;
|
||||||
|
SDL_assert(attribIndex <= MAX_ATTRIB_COUNT);
|
||||||
|
|
||||||
|
// Our EGLImages are non-planar, so we only populate the first entry
|
||||||
|
if (m_eglCreateImage) {
|
||||||
|
images[0] = m_eglCreateImage(dpy, EGL_NO_CONTEXT,
|
||||||
|
EGL_LINUX_DMA_BUF_EXT,
|
||||||
|
nullptr, attribs);
|
||||||
|
if (!images[0]) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"eglCreateImage() Failed: %d", eglGetError());
|
||||||
|
goto fail;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Cast the EGLAttrib array elements to EGLint for the KHR extension
|
||||||
|
EGLint intAttribs[MAX_ATTRIB_COUNT];
|
||||||
|
for (int i = 0; i < MAX_ATTRIB_COUNT; i++) {
|
||||||
|
intAttribs[i] = (EGLint)attribs[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
images[0] = m_eglCreateImageKHR(dpy, EGL_NO_CONTEXT,
|
||||||
|
EGL_LINUX_DMA_BUF_EXT,
|
||||||
|
nullptr, intAttribs);
|
||||||
|
if (!images[0]) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"eglCreateImageKHR() Failed: %d", eglGetError());
|
||||||
|
goto fail;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
|
||||||
|
fail:
|
||||||
|
freeEGLImages(dpy, images);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DrmRenderer::freeEGLImages(EGLDisplay dpy, EGLImage images[EGL_MAX_PLANES]) {
|
||||||
|
if (m_eglDestroyImage) {
|
||||||
|
m_eglDestroyImage(dpy, images[0]);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
m_eglDestroyImageKHR(dpy, images[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Our EGLImages are non-planar
|
||||||
|
SDL_assert(images[1] == 0);
|
||||||
|
SDL_assert(images[2] == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -14,12 +14,32 @@ public:
|
|||||||
virtual void renderFrame(AVFrame* frame) override;
|
virtual void renderFrame(AVFrame* frame) override;
|
||||||
virtual enum AVPixelFormat getPreferredPixelFormat(int videoFormat) override;
|
virtual enum AVPixelFormat getPreferredPixelFormat(int videoFormat) override;
|
||||||
virtual int getRendererAttributes() override;
|
virtual int getRendererAttributes() override;
|
||||||
|
virtual bool needsTestFrame() override;
|
||||||
|
virtual bool isDirectRenderingSupported() override;
|
||||||
|
#ifdef HAVE_EGL
|
||||||
|
virtual bool canExportEGL() override;
|
||||||
|
virtual AVPixelFormat getEGLImagePixelFormat() override;
|
||||||
|
virtual bool initializeEGL(EGLDisplay dpy, const EGLExtensions &ext) override;
|
||||||
|
virtual ssize_t exportEGLImages(AVFrame *frame, EGLDisplay dpy, EGLImage images[EGL_MAX_PLANES]) override;
|
||||||
|
virtual void freeEGLImages(EGLDisplay dpy, EGLImage[EGL_MAX_PLANES]) override;
|
||||||
|
#endif
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
AVBufferRef* m_HwContext;
|
||||||
int m_DrmFd;
|
int m_DrmFd;
|
||||||
|
bool m_SdlOwnsDrmFd;
|
||||||
|
bool m_SupportsDirectRendering;
|
||||||
uint32_t m_CrtcId;
|
uint32_t m_CrtcId;
|
||||||
uint32_t m_PlaneId;
|
uint32_t m_PlaneId;
|
||||||
uint32_t m_CurrentFbId;
|
uint32_t m_CurrentFbId;
|
||||||
SDL_Rect m_OutputRect;
|
SDL_Rect m_OutputRect;
|
||||||
|
|
||||||
|
#ifdef HAVE_EGL
|
||||||
|
bool m_EGLExtDmaBuf;
|
||||||
|
PFNEGLCREATEIMAGEPROC m_eglCreateImage;
|
||||||
|
PFNEGLDESTROYIMAGEPROC m_eglDestroyImage;
|
||||||
|
PFNEGLCREATEIMAGEKHRPROC m_eglCreateImageKHR;
|
||||||
|
PFNEGLDESTROYIMAGEKHRPROC m_eglDestroyImageKHR;
|
||||||
|
#endif
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
static QStringList egl_get_extensions(EGLDisplay dpy) {
|
static QStringList egl_get_extensions(EGLDisplay dpy) {
|
||||||
const auto EGLExtensionsStr = eglQueryString(dpy, EGL_EXTENSIONS);
|
const auto EGLExtensionsStr = eglQueryString(dpy, EGL_EXTENSIONS);
|
||||||
if (!EGLExtensionsStr) {
|
if (!EGLExtensionsStr) {
|
||||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Unable to get EGL extensions");
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Unable to get EGL extensions: %d", eglGetError());
|
||||||
return QStringList();
|
return QStringList();
|
||||||
}
|
}
|
||||||
return QString(EGLExtensionsStr).split(" ");
|
return QString(EGLExtensionsStr).split(" ");
|
||||||
|
|||||||
@@ -15,18 +15,6 @@
|
|||||||
#include <SDL_render.h>
|
#include <SDL_render.h>
|
||||||
#include <SDL_syswm.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
|
// These are EGL extensions, so some platform headers may not provide them
|
||||||
#ifndef EGL_PLATFORM_WAYLAND_KHR
|
#ifndef EGL_PLATFORM_WAYLAND_KHR
|
||||||
#define EGL_PLATFORM_WAYLAND_KHR 0x31D8
|
#define EGL_PLATFORM_WAYLAND_KHR 0x31D8
|
||||||
@@ -34,6 +22,9 @@ typedef EGLDisplay (EGLAPIENTRYP PFNEGLGETPLATFORMDISPLAYEXTPROC) (EGLenum platf
|
|||||||
#ifndef EGL_PLATFORM_X11_KHR
|
#ifndef EGL_PLATFORM_X11_KHR
|
||||||
#define EGL_PLATFORM_X11_KHR 0x31D5
|
#define EGL_PLATFORM_X11_KHR 0x31D5
|
||||||
#endif
|
#endif
|
||||||
|
#ifndef EGL_PLATFORM_GBM_KHR
|
||||||
|
#define EGL_PLATFORM_GBM_KHR 0x31D7
|
||||||
|
#endif
|
||||||
|
|
||||||
typedef struct _OVERLAY_VERTEX
|
typedef struct _OVERLAY_VERTEX
|
||||||
{
|
{
|
||||||
@@ -68,7 +59,7 @@ SDL_Window* EGLRenderer::s_LastFailedWindow = nullptr;
|
|||||||
|
|
||||||
EGLRenderer::EGLRenderer(IFFmpegRenderer *backendRenderer)
|
EGLRenderer::EGLRenderer(IFFmpegRenderer *backendRenderer)
|
||||||
:
|
:
|
||||||
m_SwPixelFormat(AV_PIX_FMT_NONE),
|
m_EGLImagePixelFormat(AV_PIX_FMT_NONE),
|
||||||
m_EGLDisplay(EGL_NO_DISPLAY),
|
m_EGLDisplay(EGL_NO_DISPLAY),
|
||||||
m_Textures{0},
|
m_Textures{0},
|
||||||
m_OverlayTextures{0},
|
m_OverlayTextures{0},
|
||||||
@@ -83,6 +74,7 @@ EGLRenderer::EGLRenderer(IFFmpegRenderer *backendRenderer)
|
|||||||
m_ColorSpace(AVCOL_SPC_NB),
|
m_ColorSpace(AVCOL_SPC_NB),
|
||||||
m_ColorFull(false),
|
m_ColorFull(false),
|
||||||
m_BlockingSwapBuffers(false),
|
m_BlockingSwapBuffers(false),
|
||||||
|
m_LastFrame(av_frame_alloc()),
|
||||||
m_glEGLImageTargetTexture2DOES(nullptr),
|
m_glEGLImageTargetTexture2DOES(nullptr),
|
||||||
m_glGenVertexArraysOES(nullptr),
|
m_glGenVertexArraysOES(nullptr),
|
||||||
m_glBindVertexArrayOES(nullptr),
|
m_glBindVertexArrayOES(nullptr),
|
||||||
@@ -133,6 +125,8 @@ EGLRenderer::~EGLRenderer()
|
|||||||
SDL_DestroyRenderer(m_DummyRenderer);
|
SDL_DestroyRenderer(m_DummyRenderer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
av_frame_free(&m_LastFrame);
|
||||||
|
|
||||||
// Reset the global properties back to what they were before
|
// Reset the global properties back to what they were before
|
||||||
SDL_SetHint(SDL_HINT_OPENGL_ES_DRIVER, "0");
|
SDL_SetHint(SDL_HINT_OPENGL_ES_DRIVER, "0");
|
||||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, m_OldContextProfileMask);
|
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, m_OldContextProfileMask);
|
||||||
@@ -162,16 +156,16 @@ void EGLRenderer::notifyOverlayUpdated(Overlay::OverlayType type)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool EGLRenderer::isPixelFormatSupported(int, AVPixelFormat pixelFormat)
|
bool EGLRenderer::isPixelFormatSupported(int videoFormat, AVPixelFormat pixelFormat)
|
||||||
{
|
{
|
||||||
// Remember to keep this in sync with EGLRenderer::renderFrame()!
|
// Pixel format support should be determined by the backend renderer
|
||||||
switch (pixelFormat)
|
return m_Backend->isPixelFormatSupported(videoFormat, pixelFormat);
|
||||||
{
|
}
|
||||||
case AV_PIX_FMT_NV12:
|
|
||||||
return true;
|
AVPixelFormat EGLRenderer::getPreferredPixelFormat(int videoFormat)
|
||||||
default:
|
{
|
||||||
return false;
|
// Pixel format preference should be determined by the backend renderer
|
||||||
}
|
return m_Backend->getPreferredPixelFormat(videoFormat);
|
||||||
}
|
}
|
||||||
|
|
||||||
void EGLRenderer::renderOverlay(Overlay::OverlayType type)
|
void EGLRenderer::renderOverlay(Overlay::OverlayType type)
|
||||||
@@ -195,7 +189,7 @@ void EGLRenderer::renderOverlay(Overlay::OverlayType type)
|
|||||||
float y;
|
float y;
|
||||||
float w;
|
float w;
|
||||||
float h;
|
float h;
|
||||||
} overlayRect;
|
} overlayRect = {};
|
||||||
|
|
||||||
// These overlay positions differ from the other renderers because OpenGL
|
// These overlay positions differ from the other renderers because OpenGL
|
||||||
// places the origin in the lower-left corner instead of the upper-left.
|
// places the origin in the lower-left corner instead of the upper-left.
|
||||||
@@ -208,6 +202,8 @@ void EGLRenderer::renderOverlay(Overlay::OverlayType type)
|
|||||||
// Top left
|
// Top left
|
||||||
overlayRect.x = 0;
|
overlayRect.x = 0;
|
||||||
overlayRect.y = m_ViewportHeight - newSurface->h;
|
overlayRect.y = m_ViewportHeight - newSurface->h;
|
||||||
|
} else {
|
||||||
|
SDL_assert(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
overlayRect.w = newSurface->w;
|
overlayRect.w = newSurface->w;
|
||||||
@@ -254,7 +250,7 @@ void EGLRenderer::renderOverlay(Overlay::OverlayType type)
|
|||||||
|
|
||||||
glActiveTexture(GL_TEXTURE0);
|
glActiveTexture(GL_TEXTURE0);
|
||||||
glBindTexture(GL_TEXTURE_2D, m_OverlayTextures[type]);
|
glBindTexture(GL_TEXTURE_2D, m_OverlayTextures[type]);
|
||||||
glUniform1i(m_OverlayShaderProgramParams[PARAM_TEXTURE], 0);
|
glUniform1i(m_OverlayShaderProgramParams[OVERLAY_PARAM_TEXTURE], 0);
|
||||||
|
|
||||||
glDrawArrays(GL_TRIANGLES, 0, 6);
|
glDrawArrays(GL_TRIANGLES, 0, 6);
|
||||||
}
|
}
|
||||||
@@ -374,27 +370,42 @@ bool EGLRenderer::compileShaders() {
|
|||||||
SDL_assert(!m_ShaderProgram);
|
SDL_assert(!m_ShaderProgram);
|
||||||
SDL_assert(!m_OverlayShaderProgram);
|
SDL_assert(!m_OverlayShaderProgram);
|
||||||
|
|
||||||
SDL_assert(m_SwPixelFormat != AV_PIX_FMT_NONE);
|
SDL_assert(m_EGLImagePixelFormat != AV_PIX_FMT_NONE);
|
||||||
|
|
||||||
// XXX: TODO: other formats
|
// XXX: TODO: other formats
|
||||||
SDL_assert(m_SwPixelFormat == AV_PIX_FMT_NV12);
|
if (m_EGLImagePixelFormat == AV_PIX_FMT_NV12) {
|
||||||
|
m_ShaderProgram = compileShader("egl_nv12.vert", "egl_nv12.frag");
|
||||||
|
if (!m_ShaderProgram) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
m_ShaderProgram = compileShader("egl.vert", "egl.frag");
|
m_ShaderProgramParams[NV12_PARAM_YUVMAT] = glGetUniformLocation(m_ShaderProgram, "yuvmat");
|
||||||
if (!m_ShaderProgram) {
|
m_ShaderProgramParams[NV12_PARAM_OFFSET] = glGetUniformLocation(m_ShaderProgram, "offset");
|
||||||
|
m_ShaderProgramParams[NV12_PARAM_PLANE1] = glGetUniformLocation(m_ShaderProgram, "plane1");
|
||||||
|
m_ShaderProgramParams[NV12_PARAM_PLANE2] = glGetUniformLocation(m_ShaderProgram, "plane2");
|
||||||
|
}
|
||||||
|
else if (m_EGLImagePixelFormat == AV_PIX_FMT_DRM_PRIME) {
|
||||||
|
m_ShaderProgram = compileShader("egl_opaque.vert", "egl_opaque.frag");
|
||||||
|
if (!m_ShaderProgram) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_ShaderProgramParams[OPAQUE_PARAM_TEXTURE] = glGetUniformLocation(m_ShaderProgram, "uTexture");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Unsupported EGL pixel format: %d",
|
||||||
|
m_EGLImagePixelFormat);
|
||||||
|
SDL_assert(false);
|
||||||
return false;
|
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");
|
m_OverlayShaderProgram = compileShader("egl_overlay.vert", "egl_overlay.frag");
|
||||||
if (!m_OverlayShaderProgram) {
|
if (!m_OverlayShaderProgram) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
m_OverlayShaderProgramParams[PARAM_TEXTURE] = glGetUniformLocation(m_OverlayShaderProgram, "uTexture");
|
m_OverlayShaderProgramParams[OVERLAY_PARAM_TEXTURE] = glGetUniformLocation(m_OverlayShaderProgram, "uTexture");
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -479,6 +490,13 @@ bool EGLRenderer::initialize(PDECODER_PARAMETERS params)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
#endif
|
||||||
|
#if SDL_VERSION_ATLEAST(2, 0, 15) && defined(SDL_VIDEO_DRIVER_KMSDRM)
|
||||||
|
case SDL_SYSWM_KMSDRM:
|
||||||
|
if (!openDisplay(EGL_PLATFORM_GBM_KHR, info.info.kmsdrm.gbm_dev)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
break;
|
||||||
#endif
|
#endif
|
||||||
default:
|
default:
|
||||||
EGL_LOG(Error, "not compatible with SYSWM");
|
EGL_LOG(Error, "not compatible with SYSWM");
|
||||||
@@ -544,7 +562,7 @@ bool EGLRenderer::initialize(PDECODER_PARAMETERS params)
|
|||||||
src.x = src.y = dst.x = dst.y = 0;
|
src.x = src.y = dst.x = dst.y = 0;
|
||||||
src.w = params->width;
|
src.w = params->width;
|
||||||
src.h = params->height;
|
src.h = params->height;
|
||||||
SDL_GetWindowSize(m_Window, &dst.w, &dst.h);
|
SDL_GL_GetDrawableSize(m_Window, &dst.w, &dst.h);
|
||||||
StreamUtils::scaleSourceToDestinationSurface(&src, &dst);
|
StreamUtils::scaleSourceToDestinationSurface(&src, &dst);
|
||||||
|
|
||||||
glViewport(dst.x, dst.y, dst.w, dst.h);
|
glViewport(dst.x, dst.y, dst.w, dst.h);
|
||||||
@@ -557,7 +575,17 @@ bool EGLRenderer::initialize(PDECODER_PARAMETERS params)
|
|||||||
|
|
||||||
if (params->enableVsync) {
|
if (params->enableVsync) {
|
||||||
SDL_GL_SetSwapInterval(1);
|
SDL_GL_SetSwapInterval(1);
|
||||||
m_BlockingSwapBuffers = true;
|
|
||||||
|
#if SDL_VERSION_ATLEAST(2, 0, 15) && defined(SDL_VIDEO_DRIVER_KMSDRM)
|
||||||
|
// The SDL KMSDRM backend already enforces double buffering (due to
|
||||||
|
// SDL_HINT_VIDEO_DOUBLE_BUFFER=1), so calling glFinish() after
|
||||||
|
// SDL_GL_SwapWindow() will block an extra frame and lock rendering
|
||||||
|
// at 1/2 the display refresh rate.
|
||||||
|
if (info.subsystem != SDL_SYSWM_KMSDRM)
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
m_BlockingSwapBuffers = true;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
SDL_GL_SetSwapInterval(0);
|
SDL_GL_SetSwapInterval(0);
|
||||||
}
|
}
|
||||||
@@ -648,7 +676,18 @@ const float *EGLRenderer::getColorMatrix() {
|
|||||||
case AVCOL_SPC_BT2020_CL:
|
case AVCOL_SPC_BT2020_CL:
|
||||||
return m_ColorFull ? bt2020Full : bt2020Lim;
|
return m_ColorFull ? bt2020Full : bt2020Lim;
|
||||||
default:
|
default:
|
||||||
SDL_assert(false);
|
// Some backends don't populate this, so we'll assume
|
||||||
|
// the host gave us what we asked for by default.
|
||||||
|
switch (getDecoderColorspace()) {
|
||||||
|
case COLORSPACE_REC_601:
|
||||||
|
return m_ColorFull ? bt601Full : bt601Lim;
|
||||||
|
case COLORSPACE_REC_709:
|
||||||
|
return m_ColorFull ? bt709Full : bt709Lim;
|
||||||
|
case COLORSPACE_REC_2020:
|
||||||
|
return m_ColorFull ? bt2020Full : bt2020Lim;
|
||||||
|
default:
|
||||||
|
SDL_assert(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return bt601Lim;
|
return bt601Lim;
|
||||||
@@ -721,49 +760,49 @@ void EGLRenderer::renderFrame(AVFrame* frame)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (frame->hw_frames_ctx != nullptr) {
|
// Find the native read-back format and load the shaders
|
||||||
// Find the native read-back format and load the shader
|
if (m_EGLImagePixelFormat == AV_PIX_FMT_NONE) {
|
||||||
if (m_SwPixelFormat == AV_PIX_FMT_NONE) {
|
m_EGLImagePixelFormat = m_Backend->getEGLImagePixelFormat();
|
||||||
auto hwFrameCtx = (AVHWFramesContext*)frame->hw_frames_ctx->data;
|
EGL_LOG(Info, "EGLImage pixel format: %d", m_EGLImagePixelFormat);
|
||||||
|
|
||||||
m_SwPixelFormat = hwFrameCtx->sw_format;
|
SDL_assert(m_EGLImagePixelFormat != AV_PIX_FMT_NONE);
|
||||||
SDL_assert(m_SwPixelFormat != AV_PIX_FMT_NONE);
|
|
||||||
|
|
||||||
EGL_LOG(Info, "Selected read-back format: %d", m_SwPixelFormat);
|
m_ColorSpace = frame->colorspace;
|
||||||
|
|
||||||
// XXX: TODO: Handle other pixel formats
|
// This handles the case where the color range is unknown,
|
||||||
SDL_assert(m_SwPixelFormat == AV_PIX_FMT_NV12);
|
// so that we use Limited color range which is the default
|
||||||
m_ColorSpace = frame->colorspace;
|
// behavior for Moonlight.
|
||||||
m_ColorFull = frame->color_range == AVCOL_RANGE_JPEG;
|
m_ColorFull = frame->color_range == AVCOL_RANGE_JPEG;
|
||||||
|
|
||||||
if (!specialize()) {
|
if (!specialize()) {
|
||||||
m_SwPixelFormat = AV_PIX_FMT_NONE;
|
m_EGLImagePixelFormat = AV_PIX_FMT_NONE;
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ssize_t plane_count = m_Backend->exportEGLImages(frame, m_EGLDisplay, imgs);
|
|
||||||
if (plane_count < 0)
|
|
||||||
return;
|
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");
|
ssize_t plane_count = m_Backend->exportEGLImages(frame, m_EGLDisplay, imgs);
|
||||||
|
if (plane_count < 0)
|
||||||
return;
|
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]);
|
||||||
}
|
}
|
||||||
|
|
||||||
glClear(GL_COLOR_BUFFER_BIT);
|
glClear(GL_COLOR_BUFFER_BIT);
|
||||||
glUseProgram(m_ShaderProgram);
|
glUseProgram(m_ShaderProgram);
|
||||||
m_glBindVertexArrayOES(m_VAO);
|
m_glBindVertexArrayOES(m_VAO);
|
||||||
|
|
||||||
glUniformMatrix3fv(m_ShaderProgramParams[PARAM_YUVMAT], 1, GL_FALSE, getColorMatrix());
|
// Bind parameters for the shaders
|
||||||
glUniform3fv(m_ShaderProgramParams[PARAM_OFFSET], 1, getColorOffsets());
|
if (m_EGLImagePixelFormat == AV_PIX_FMT_NV12) {
|
||||||
glUniform1i(m_ShaderProgramParams[PARAM_PLANE1], 0);
|
glUniformMatrix3fv(m_ShaderProgramParams[NV12_PARAM_YUVMAT], 1, GL_FALSE, getColorMatrix());
|
||||||
glUniform1i(m_ShaderProgramParams[PARAM_PLANE2], 1);
|
glUniform3fv(m_ShaderProgramParams[NV12_PARAM_OFFSET], 1, getColorOffsets());
|
||||||
|
glUniform1i(m_ShaderProgramParams[NV12_PARAM_PLANE1], 0);
|
||||||
|
glUniform1i(m_ShaderProgramParams[NV12_PARAM_PLANE2], 1);
|
||||||
|
}
|
||||||
|
else if (m_EGLImagePixelFormat == AV_PIX_FMT_DRM_PRIME) {
|
||||||
|
glUniform1i(m_ShaderProgramParams[OPAQUE_PARAM_TEXTURE], 0);
|
||||||
|
}
|
||||||
|
|
||||||
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
|
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
|
||||||
|
|
||||||
@@ -787,6 +826,31 @@ void EGLRenderer::renderFrame(AVFrame* frame)
|
|||||||
glFinish();
|
glFinish();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (frame->hw_frames_ctx != nullptr)
|
m_Backend->freeEGLImages(m_EGLDisplay, imgs);
|
||||||
m_Backend->freeEGLImages(m_EGLDisplay, imgs);
|
|
||||||
|
// Free the DMA-BUF backing the last frame now that it is definitely
|
||||||
|
// no longer being used anymore. While the PRIME FD stays around until
|
||||||
|
// EGL is done with it, the memory backing it may be reused by FFmpeg
|
||||||
|
// before the GPU has read it. This is particularly noticeable on the
|
||||||
|
// RK3288-based TinkerBoard when V-Sync is disabled.
|
||||||
|
av_frame_unref(m_LastFrame);
|
||||||
|
av_frame_move_ref(m_LastFrame, frame);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool EGLRenderer::testRenderFrame(AVFrame* frame)
|
||||||
|
{
|
||||||
|
EGLImage imgs[EGL_MAX_PLANES];
|
||||||
|
|
||||||
|
// Make sure we can get working EGLImages from the backend renderer.
|
||||||
|
// Some devices (Raspberry Pi) will happily decode into DRM formats that
|
||||||
|
// its own GL implementation won't accept in eglCreateImage().
|
||||||
|
ssize_t plane_count = m_Backend->exportEGLImages(frame, m_EGLDisplay, imgs);
|
||||||
|
if (plane_count <= 0) {
|
||||||
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Backend failed to export EGL image for test frame");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_Backend->freeEGLImages(m_EGLDisplay, imgs);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
@@ -12,8 +12,10 @@ public:
|
|||||||
virtual bool initialize(PDECODER_PARAMETERS params) override;
|
virtual bool initialize(PDECODER_PARAMETERS params) override;
|
||||||
virtual bool prepareDecoderContext(AVCodecContext* context, AVDictionary** options) override;
|
virtual bool prepareDecoderContext(AVCodecContext* context, AVDictionary** options) override;
|
||||||
virtual void renderFrame(AVFrame* frame) override;
|
virtual void renderFrame(AVFrame* frame) override;
|
||||||
|
virtual bool testRenderFrame(AVFrame* frame) override;
|
||||||
virtual void notifyOverlayUpdated(Overlay::OverlayType) override;
|
virtual void notifyOverlayUpdated(Overlay::OverlayType) override;
|
||||||
virtual bool isPixelFormatSupported(int videoFormat, enum AVPixelFormat pixelFormat) override;
|
virtual bool isPixelFormatSupported(int videoFormat, enum AVPixelFormat pixelFormat) override;
|
||||||
|
virtual AVPixelFormat getPreferredPixelFormat(int videoFormat) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|
||||||
@@ -29,7 +31,7 @@ private:
|
|||||||
int m_ViewportWidth;
|
int m_ViewportWidth;
|
||||||
int m_ViewportHeight;
|
int m_ViewportHeight;
|
||||||
|
|
||||||
int m_SwPixelFormat;
|
AVPixelFormat m_EGLImagePixelFormat;
|
||||||
void *m_EGLDisplay;
|
void *m_EGLDisplay;
|
||||||
unsigned m_Textures[EGL_MAX_PLANES];
|
unsigned m_Textures[EGL_MAX_PLANES];
|
||||||
unsigned m_OverlayTextures[Overlay::OverlayMax];
|
unsigned m_OverlayTextures[Overlay::OverlayMax];
|
||||||
@@ -44,18 +46,20 @@ private:
|
|||||||
int m_ColorSpace;
|
int m_ColorSpace;
|
||||||
bool m_ColorFull;
|
bool m_ColorFull;
|
||||||
bool m_BlockingSwapBuffers;
|
bool m_BlockingSwapBuffers;
|
||||||
|
AVFrame* m_LastFrame;
|
||||||
PFNGLEGLIMAGETARGETTEXTURE2DOESPROC m_glEGLImageTargetTexture2DOES;
|
PFNGLEGLIMAGETARGETTEXTURE2DOESPROC m_glEGLImageTargetTexture2DOES;
|
||||||
PFNGLGENVERTEXARRAYSOESPROC m_glGenVertexArraysOES;
|
PFNGLGENVERTEXARRAYSOESPROC m_glGenVertexArraysOES;
|
||||||
PFNGLBINDVERTEXARRAYOESPROC m_glBindVertexArrayOES;
|
PFNGLBINDVERTEXARRAYOESPROC m_glBindVertexArrayOES;
|
||||||
PFNGLDELETEVERTEXARRAYSOESPROC m_glDeleteVertexArraysOES;
|
PFNGLDELETEVERTEXARRAYSOESPROC m_glDeleteVertexArraysOES;
|
||||||
|
|
||||||
#define PARAM_YUVMAT 0
|
#define NV12_PARAM_YUVMAT 0
|
||||||
#define PARAM_OFFSET 1
|
#define NV12_PARAM_OFFSET 1
|
||||||
#define PARAM_PLANE1 2
|
#define NV12_PARAM_PLANE1 2
|
||||||
#define PARAM_PLANE2 3
|
#define NV12_PARAM_PLANE2 3
|
||||||
|
#define OPAQUE_PARAM_TEXTURE 0
|
||||||
int m_ShaderProgramParams[4];
|
int m_ShaderProgramParams[4];
|
||||||
|
|
||||||
#define PARAM_TEXTURE 0
|
#define OVERLAY_PARAM_TEXTURE 0
|
||||||
int m_OverlayShaderProgramParams[1];
|
int m_OverlayShaderProgramParams[1];
|
||||||
|
|
||||||
int m_OldContextProfileMask;
|
int m_OldContextProfileMask;
|
||||||
|
|||||||
@@ -10,7 +10,12 @@
|
|||||||
MmalRenderer::MmalRenderer()
|
MmalRenderer::MmalRenderer()
|
||||||
: m_Renderer(nullptr),
|
: m_Renderer(nullptr),
|
||||||
m_InputPort(nullptr),
|
m_InputPort(nullptr),
|
||||||
m_BackgroundRenderer(nullptr)
|
m_BackgroundRenderer(nullptr),
|
||||||
|
m_Window(nullptr),
|
||||||
|
m_VideoWidth(0),
|
||||||
|
m_VideoHeight(0),
|
||||||
|
m_LastWindowPosX(-1),
|
||||||
|
m_LastWindowPosY(-1)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,10 +51,67 @@ bool MmalRenderer::prepareDecoderContext(AVCodecContext* context, AVDictionary**
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void MmalRenderer::updateDisplayRegion()
|
||||||
|
{
|
||||||
|
MMAL_STATUS_T status;
|
||||||
|
int currentPosX, currentPosY;
|
||||||
|
MMAL_DISPLAYREGION_T dr;
|
||||||
|
|
||||||
|
dr.hdr.id = MMAL_PARAMETER_DISPLAYREGION;
|
||||||
|
dr.hdr.size = sizeof(MMAL_DISPLAYREGION_T);
|
||||||
|
dr.set = MMAL_DISPLAY_SET_DEST_RECT;
|
||||||
|
|
||||||
|
SDL_GetWindowPosition(m_Window, ¤tPosX, ¤tPosY);
|
||||||
|
|
||||||
|
if ((SDL_GetWindowFlags(m_Window) & SDL_WINDOW_INPUT_FOCUS) == 0) {
|
||||||
|
dr.dest_rect.x = 0;
|
||||||
|
dr.dest_rect.y = 0;
|
||||||
|
dr.dest_rect.width = 0;
|
||||||
|
dr.dest_rect.height = 0;
|
||||||
|
|
||||||
|
// Force a re-evaluation next time
|
||||||
|
m_LastWindowPosX = -1;
|
||||||
|
m_LastWindowPosY = -1;
|
||||||
|
}
|
||||||
|
else if (m_LastWindowPosX != currentPosX || m_LastWindowPosY != currentPosY) {
|
||||||
|
SDL_Rect src, dst;
|
||||||
|
src.x = src.y = 0;
|
||||||
|
src.w = m_VideoWidth;
|
||||||
|
src.h = m_VideoHeight;
|
||||||
|
dst.x = dst.y = 0;
|
||||||
|
SDL_GetWindowSize(m_Window, &dst.w, &dst.h);
|
||||||
|
|
||||||
|
StreamUtils::scaleSourceToDestinationSurface(&src, &dst);
|
||||||
|
|
||||||
|
dr.dest_rect.x = currentPosX + dst.x;
|
||||||
|
dr.dest_rect.y = currentPosY + dst.y;
|
||||||
|
dr.dest_rect.width = dst.w;
|
||||||
|
dr.dest_rect.height = dst.h;
|
||||||
|
|
||||||
|
m_LastWindowPosX = currentPosX;
|
||||||
|
m_LastWindowPosY = currentPosY;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Nothing to do
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
status = mmal_port_parameter_set(m_InputPort, &dr.hdr);
|
||||||
|
if (status != MMAL_SUCCESS) {
|
||||||
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"mmal_port_parameter_set() failed: %x (%s)",
|
||||||
|
status, mmal_status_to_string(status));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bool MmalRenderer::initialize(PDECODER_PARAMETERS params)
|
bool MmalRenderer::initialize(PDECODER_PARAMETERS params)
|
||||||
{
|
{
|
||||||
MMAL_STATUS_T status;
|
MMAL_STATUS_T status;
|
||||||
|
|
||||||
|
m_Window = params->window;
|
||||||
|
m_VideoWidth = params->width;
|
||||||
|
m_VideoHeight = params->height;
|
||||||
|
|
||||||
// Clear the background if possible
|
// Clear the background if possible
|
||||||
setupBackground(params);
|
setupBackground(params);
|
||||||
|
|
||||||
@@ -107,23 +169,6 @@ bool MmalRenderer::initialize(PDECODER_PARAMETERS params)
|
|||||||
dr.src_rect.width = params->width;
|
dr.src_rect.width = params->width;
|
||||||
dr.src_rect.height = params->height;
|
dr.src_rect.height = params->height;
|
||||||
|
|
||||||
{
|
|
||||||
SDL_Rect src, dst;
|
|
||||||
src.x = src.y = 0;
|
|
||||||
src.w = params->width;
|
|
||||||
src.h = params->height;
|
|
||||||
dst.x = dst.y = 0;
|
|
||||||
SDL_GetWindowSize(params->window, &dst.w, &dst.h);
|
|
||||||
|
|
||||||
StreamUtils::scaleSourceToDestinationSurface(&src, &dst);
|
|
||||||
|
|
||||||
dr.set |= MMAL_DISPLAY_SET_DEST_RECT;
|
|
||||||
dr.dest_rect.x = dst.x;
|
|
||||||
dr.dest_rect.y = dst.y;
|
|
||||||
dr.dest_rect.width = dst.w;
|
|
||||||
dr.dest_rect.height = dst.h;
|
|
||||||
}
|
|
||||||
|
|
||||||
status = mmal_port_parameter_set(m_InputPort, &dr.hdr);
|
status = mmal_port_parameter_set(m_InputPort, &dr.hdr);
|
||||||
if (status != MMAL_SUCCESS) {
|
if (status != MMAL_SUCCESS) {
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
@@ -131,6 +176,9 @@ bool MmalRenderer::initialize(PDECODER_PARAMETERS params)
|
|||||||
status, mmal_status_to_string(status));
|
status, mmal_status_to_string(status));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set the destination display region
|
||||||
|
updateDisplayRegion();
|
||||||
}
|
}
|
||||||
|
|
||||||
status = mmal_port_enable(m_InputPort, InputPortCallback);
|
status = mmal_port_enable(m_InputPort, InputPortCallback);
|
||||||
@@ -179,17 +227,16 @@ void MmalRenderer::InputPortCallback(MMAL_PORT_T*, MMAL_BUFFER_HEADER_T* buffer)
|
|||||||
mmal_buffer_header_release(buffer);
|
mmal_buffer_header_release(buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
enum AVPixelFormat MmalRenderer::getPreferredPixelFormat(int videoFormat)
|
enum AVPixelFormat MmalRenderer::getPreferredPixelFormat(int)
|
||||||
{
|
{
|
||||||
// Opaque MMAL buffers
|
// Opaque MMAL buffers
|
||||||
SDL_assert(videoFormat == VIDEO_FORMAT_H264);
|
|
||||||
return AV_PIX_FMT_MMAL;
|
return AV_PIX_FMT_MMAL;
|
||||||
}
|
}
|
||||||
|
|
||||||
int MmalRenderer::getRendererAttributes()
|
int MmalRenderer::getRendererAttributes()
|
||||||
{
|
{
|
||||||
// This renderer can only draw in full-screen and maxes out at 1080p
|
// This renderer maxes out at 1080p
|
||||||
return RENDERER_ATTRIBUTE_FULLSCREEN_ONLY | RENDERER_ATTRIBUTE_1080P_MAX;
|
return RENDERER_ATTRIBUTE_1080P_MAX;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool MmalRenderer::needsTestFrame()
|
bool MmalRenderer::needsTestFrame()
|
||||||
@@ -209,6 +256,9 @@ void MmalRenderer::renderFrame(AVFrame* frame)
|
|||||||
MMAL_BUFFER_HEADER_T* buffer = (MMAL_BUFFER_HEADER_T*)frame->data[3];
|
MMAL_BUFFER_HEADER_T* buffer = (MMAL_BUFFER_HEADER_T*)frame->data[3];
|
||||||
MMAL_STATUS_T status;
|
MMAL_STATUS_T status;
|
||||||
|
|
||||||
|
// Update the destination display region in case the window moved
|
||||||
|
updateDisplayRegion();
|
||||||
|
|
||||||
status = mmal_port_send_buffer(m_InputPort, buffer);
|
status = mmal_port_send_buffer(m_InputPort, buffer);
|
||||||
if (status != MMAL_SUCCESS) {
|
if (status != MMAL_SUCCESS) {
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
|||||||
@@ -22,10 +22,14 @@ private:
|
|||||||
static void InputPortCallback(MMAL_PORT_T* port, MMAL_BUFFER_HEADER_T* buffer);
|
static void InputPortCallback(MMAL_PORT_T* port, MMAL_BUFFER_HEADER_T* buffer);
|
||||||
|
|
||||||
void setupBackground(PDECODER_PARAMETERS params);
|
void setupBackground(PDECODER_PARAMETERS params);
|
||||||
|
void updateDisplayRegion();
|
||||||
|
|
||||||
MMAL_COMPONENT_T* m_Renderer;
|
MMAL_COMPONENT_T* m_Renderer;
|
||||||
MMAL_PORT_T* m_InputPort;
|
MMAL_PORT_T* m_InputPort;
|
||||||
|
|
||||||
SDL_Renderer* m_BackgroundRenderer;
|
SDL_Renderer* m_BackgroundRenderer;
|
||||||
|
SDL_Window* m_Window;
|
||||||
|
int m_VideoWidth, m_VideoHeight;
|
||||||
|
int m_LastWindowPosX, m_LastWindowPosY;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -10,9 +10,60 @@ extern "C" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#ifdef HAVE_EGL
|
#ifdef HAVE_EGL
|
||||||
// SDL_egl.h have too many conflicts, we will do without
|
#define MESA_EGL_NO_X11_HEADERS
|
||||||
typedef void *EGLDisplay;
|
#define EGL_NO_X11
|
||||||
|
#include <SDL_egl.h>
|
||||||
|
|
||||||
|
#ifndef EGL_VERSION_1_5
|
||||||
|
typedef intptr_t EGLAttrib;
|
||||||
typedef void *EGLImage;
|
typedef void *EGLImage;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if !defined(EGL_VERSION_1_5) || !defined(EGL_EGL_PROTOTYPES)
|
||||||
|
typedef EGLImage (EGLAPIENTRYP PFNEGLCREATEIMAGEPROC) (EGLDisplay dpy, EGLContext ctx, EGLenum target, EGLClientBuffer buffer, const EGLAttrib *attrib_list);
|
||||||
|
typedef EGLBoolean (EGLAPIENTRYP PFNEGLDESTROYIMAGEPROC) (EGLDisplay dpy, EGLImage image);
|
||||||
|
typedef EGLDisplay (EGLAPIENTRYP PFNEGLGETPLATFORMDISPLAYPROC) (EGLenum platform, void *native_display, const EGLAttrib *attrib_list);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if !defined(EGL_KHR_image) || !defined(EGL_EGLEXT_PROTOTYPES)
|
||||||
|
// EGL_KHR_image technically uses EGLImageKHR instead of EGLImage, but they're compatible
|
||||||
|
// so we swap them here to avoid mixing them all over the place
|
||||||
|
typedef EGLImage (EGLAPIENTRYP PFNEGLCREATEIMAGEKHRPROC) (EGLDisplay dpy, EGLContext ctx, EGLenum target, EGLClientBuffer buffer, const EGLint *attrib_list);
|
||||||
|
typedef EGLBoolean (EGLAPIENTRYP PFNEGLDESTROYIMAGEKHRPROC) (EGLDisplay dpy, EGLImage image);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if !defined(EGL_EXT_platform_base) || !defined(EGL_EGLEXT_PROTOTYPES)
|
||||||
|
typedef EGLDisplay (EGLAPIENTRYP PFNEGLGETPLATFORMDISPLAYEXTPROC) (EGLenum platform, void *native_display, const EGLint *attrib_list);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef EGL_EXT_image_dma_buf_import
|
||||||
|
#define EGL_LINUX_DMA_BUF_EXT 0x3270
|
||||||
|
#define EGL_LINUX_DRM_FOURCC_EXT 0x3271
|
||||||
|
#define EGL_DMA_BUF_PLANE0_FD_EXT 0x3272
|
||||||
|
#define EGL_DMA_BUF_PLANE0_OFFSET_EXT 0x3273
|
||||||
|
#define EGL_DMA_BUF_PLANE0_PITCH_EXT 0x3274
|
||||||
|
#define EGL_DMA_BUF_PLANE1_FD_EXT 0x3275
|
||||||
|
#define EGL_DMA_BUF_PLANE1_OFFSET_EXT 0x3276
|
||||||
|
#define EGL_DMA_BUF_PLANE1_PITCH_EXT 0x3277
|
||||||
|
#define EGL_DMA_BUF_PLANE2_FD_EXT 0x3278
|
||||||
|
#define EGL_DMA_BUF_PLANE2_OFFSET_EXT 0x3279
|
||||||
|
#define EGL_DMA_BUF_PLANE2_PITCH_EXT 0x327A
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef EGL_EXT_image_dma_buf_import_modifiers
|
||||||
|
#define EGL_DMA_BUF_PLANE3_FD_EXT 0x3440
|
||||||
|
#define EGL_DMA_BUF_PLANE3_OFFSET_EXT 0x3441
|
||||||
|
#define EGL_DMA_BUF_PLANE3_PITCH_EXT 0x3442
|
||||||
|
#define EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT 0x3443
|
||||||
|
#define EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT 0x3444
|
||||||
|
#define EGL_DMA_BUF_PLANE1_MODIFIER_LO_EXT 0x3445
|
||||||
|
#define EGL_DMA_BUF_PLANE1_MODIFIER_HI_EXT 0x3446
|
||||||
|
#define EGL_DMA_BUF_PLANE2_MODIFIER_LO_EXT 0x3447
|
||||||
|
#define EGL_DMA_BUF_PLANE2_MODIFIER_HI_EXT 0x3448
|
||||||
|
#define EGL_DMA_BUF_PLANE3_MODIFIER_LO_EXT 0x3449
|
||||||
|
#define EGL_DMA_BUF_PLANE3_MODIFIER_HI_EXT 0x344A
|
||||||
|
#endif
|
||||||
|
|
||||||
#define EGL_MAX_PLANES 4
|
#define EGL_MAX_PLANES 4
|
||||||
|
|
||||||
class EGLExtensions {
|
class EGLExtensions {
|
||||||
@@ -35,6 +86,13 @@ public:
|
|||||||
virtual bool prepareDecoderContext(AVCodecContext* context, AVDictionary** options) = 0;
|
virtual bool prepareDecoderContext(AVCodecContext* context, AVDictionary** options) = 0;
|
||||||
virtual void renderFrame(AVFrame* frame) = 0;
|
virtual void renderFrame(AVFrame* frame) = 0;
|
||||||
|
|
||||||
|
virtual bool testRenderFrame(AVFrame*) {
|
||||||
|
// If the renderer doesn't provide an explicit test routine,
|
||||||
|
// we will always assume that any returned AVFrame can be
|
||||||
|
// rendered successfully.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
virtual bool needsTestFrame() {
|
virtual bool needsTestFrame() {
|
||||||
// No test frame required by default
|
// No test frame required by default
|
||||||
return false;
|
return false;
|
||||||
@@ -65,7 +123,7 @@ public:
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
virtual enum AVPixelFormat getPreferredPixelFormat(int videoFormat) {
|
virtual AVPixelFormat getPreferredPixelFormat(int videoFormat) {
|
||||||
if (videoFormat == VIDEO_FORMAT_H265_MAIN10) {
|
if (videoFormat == VIDEO_FORMAT_H265_MAIN10) {
|
||||||
// 10-bit YUV 4:2:0
|
// 10-bit YUV 4:2:0
|
||||||
return AV_PIX_FMT_P010;
|
return AV_PIX_FMT_P010;
|
||||||
@@ -76,7 +134,7 @@ public:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
virtual bool isPixelFormatSupported(int videoFormat, enum AVPixelFormat pixelFormat) {
|
virtual bool isPixelFormatSupported(int videoFormat, AVPixelFormat pixelFormat) {
|
||||||
// By default, we only support the preferred pixel format
|
// By default, we only support the preferred pixel format
|
||||||
return getPreferredPixelFormat(videoFormat) == pixelFormat;
|
return getPreferredPixelFormat(videoFormat) == pixelFormat;
|
||||||
}
|
}
|
||||||
@@ -92,6 +150,10 @@ public:
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
virtual AVPixelFormat getEGLImagePixelFormat() {
|
||||||
|
return AV_PIX_FMT_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
virtual bool initializeEGL(EGLDisplay,
|
virtual bool initializeEGL(EGLDisplay,
|
||||||
const EGLExtensions &) {
|
const EGLExtensions &) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -351,3 +351,36 @@ Exit:
|
|||||||
av_frame_free(&swFrame);
|
av_frame_free(&swFrame);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool SdlRenderer::testRenderFrame(AVFrame* frame)
|
||||||
|
{
|
||||||
|
// If we are acting as the frontend for a hardware
|
||||||
|
// accelerated decoder, we'll need to read the frame
|
||||||
|
// back to render it. Test that this can be done
|
||||||
|
// for the given frame successfully.
|
||||||
|
if (frame->hw_frames_ctx != nullptr) {
|
||||||
|
auto hwFrameCtx = (AVHWFramesContext*)frame->hw_frames_ctx->data;
|
||||||
|
|
||||||
|
AVFrame* swFrame = av_frame_alloc();
|
||||||
|
if (swFrame == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
swFrame->width = frame->width;
|
||||||
|
swFrame->height = frame->height;
|
||||||
|
swFrame->format = hwFrameCtx->sw_format;
|
||||||
|
|
||||||
|
int err = av_hwframe_transfer_data(swFrame, frame, 0);
|
||||||
|
|
||||||
|
av_frame_free(&swFrame);
|
||||||
|
|
||||||
|
if (err != 0) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"av_hwframe_transfer_data() failed: %d",
|
||||||
|
err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ public:
|
|||||||
virtual void renderFrame(AVFrame* frame) override;
|
virtual void renderFrame(AVFrame* frame) override;
|
||||||
virtual bool isRenderThreadSupported() override;
|
virtual bool isRenderThreadSupported() override;
|
||||||
virtual bool isPixelFormatSupported(int videoFormat, enum AVPixelFormat pixelFormat) override;
|
virtual bool isPixelFormatSupported(int videoFormat, enum AVPixelFormat pixelFormat) override;
|
||||||
|
virtual bool testRenderFrame(AVFrame* frame) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void renderOverlay(Overlay::OverlayType type);
|
void renderOverlay(Overlay::OverlayType type);
|
||||||
|
|||||||
@@ -5,22 +5,23 @@
|
|||||||
#include <streaming/streamutils.h>
|
#include <streaming/streamutils.h>
|
||||||
|
|
||||||
#include <SDL_syswm.h>
|
#include <SDL_syswm.h>
|
||||||
#ifdef HAVE_EGL
|
|
||||||
#include <SDL_egl.h>
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
|
|
||||||
VAAPIRenderer::VAAPIRenderer()
|
VAAPIRenderer::VAAPIRenderer()
|
||||||
: m_HwContext(nullptr),
|
: m_HwContext(nullptr),
|
||||||
m_DrmFd(-1),
|
|
||||||
m_BlacklistedForDirectRendering(false)
|
m_BlacklistedForDirectRendering(false)
|
||||||
{
|
{
|
||||||
#ifdef HAVE_EGL
|
#ifdef HAVE_EGL
|
||||||
m_PrimeDescriptor.num_layers = 0;
|
m_PrimeDescriptor.num_layers = 0;
|
||||||
m_PrimeDescriptor.num_objects = 0;
|
m_PrimeDescriptor.num_objects = 0;
|
||||||
m_EGLExtDmaBuf = false;
|
m_EGLExtDmaBuf = false;
|
||||||
|
|
||||||
|
m_eglCreateImage = nullptr;
|
||||||
|
m_eglCreateImageKHR = nullptr;
|
||||||
|
m_eglDestroyImage = nullptr;
|
||||||
|
m_eglDestroyImageKHR = nullptr;
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,10 +40,6 @@ VAAPIRenderer::~VAAPIRenderer()
|
|||||||
vaTerminate(display);
|
vaTerminate(display);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (m_DrmFd != -1) {
|
|
||||||
close(m_DrmFd);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
VADisplay
|
VADisplay
|
||||||
@@ -90,41 +87,17 @@ VAAPIRenderer::openDisplay(SDL_Window* window)
|
|||||||
return nullptr;
|
return nullptr;
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
// TODO: Upstream a better solution for SDL_GetWindowWMInfo on KMSDRM
|
#if defined(SDL_VIDEO_DRIVER_KMSDRM) && defined(HAVE_LIBVA_DRM) && SDL_VERSION_ATLEAST(2, 0, 15)
|
||||||
else if (strcmp(SDL_GetCurrentVideoDriver(), "KMSDRM") == 0) {
|
else if (info.subsystem == SDL_SYSWM_KMSDRM) {
|
||||||
#ifdef HAVE_LIBVA_DRM
|
SDL_assert(info.info.kmsdrm.drm_fd >= 0);
|
||||||
if (m_DrmFd < 0) {
|
display = vaGetDisplayDRM(info.info.kmsdrm.drm_fd);
|
||||||
const char* device = SDL_getenv("DRM_DEV");
|
|
||||||
|
|
||||||
if (device == nullptr) {
|
|
||||||
device = "/dev/dri/card0";
|
|
||||||
}
|
|
||||||
|
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Opening DRM device: %s",
|
|
||||||
device);
|
|
||||||
|
|
||||||
m_DrmFd = open(device, O_RDWR | O_CLOEXEC);
|
|
||||||
if (m_DrmFd < 0) {
|
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Failed to open DRM device: %d",
|
|
||||||
errno);
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
display = vaGetDisplayDRM(m_DrmFd);
|
|
||||||
if (display == nullptr) {
|
if (display == nullptr) {
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"Unable to open DRM display for VAAPI");
|
"Unable to open DRM display for VAAPI");
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
#else
|
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Moonlight not compiled with VAAPI DRM support!");
|
|
||||||
return nullptr;
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
else {
|
else {
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"Unsupported VAAPI rendering subsystem: %d",
|
"Unsupported VAAPI rendering subsystem: %d",
|
||||||
@@ -259,12 +232,12 @@ VAAPIRenderer::initialize(PDECODER_PARAMETERS params)
|
|||||||
"Driver: %s",
|
"Driver: %s",
|
||||||
vendorString ? vendorString : "<unknown>");
|
vendorString ? vendorString : "<unknown>");
|
||||||
|
|
||||||
// AMD's Gallium VAAPI driver has a nasty memory leak that causes memory
|
// Older versions of the Gallium VAAPI driver have a nasty memory leak that
|
||||||
// to be leaked for each submitted frame. The Flatpak runtime has a VDPAU
|
// causes memory to be leaked for each submitted frame. I believe this is
|
||||||
// driver in place that works well, so use that instead on AMD systems. If
|
// resolved in the libva2 drivers (VAAPI 1.x).
|
||||||
// we're using Wayland, we have no choice but to use VAAPI because VDPAU
|
// If we're using Wayland, we have no choice but to use VAAPI because VDPAU
|
||||||
// is only supported under X11 or XWayland.
|
// is only supported under X11 or XWayland.
|
||||||
if (qgetenv("FORCE_VAAPI") != "1" && !WMUtils::isRunningWayland()) {
|
if (major == 0 && qgetenv("FORCE_VAAPI") != "1" && !WMUtils::isRunningWayland()) {
|
||||||
if (vendorStr.contains("AMD", Qt::CaseInsensitive) ||
|
if (vendorStr.contains("AMD", Qt::CaseInsensitive) ||
|
||||||
vendorStr.contains("Radeon", Qt::CaseInsensitive)) {
|
vendorStr.contains("Radeon", Qt::CaseInsensitive)) {
|
||||||
// Fail and let VDPAU pick this up
|
// Fail and let VDPAU pick this up
|
||||||
@@ -510,6 +483,10 @@ VAAPIRenderer::canExportEGL() {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AVPixelFormat VAAPIRenderer::getEGLImagePixelFormat() {
|
||||||
|
return AV_PIX_FMT_NV12;
|
||||||
|
}
|
||||||
|
|
||||||
bool
|
bool
|
||||||
VAAPIRenderer::initializeEGL(EGLDisplay,
|
VAAPIRenderer::initializeEGL(EGLDisplay,
|
||||||
const EGLExtensions &ext) {
|
const EGLExtensions &ext) {
|
||||||
@@ -519,6 +496,20 @@ VAAPIRenderer::initializeEGL(EGLDisplay,
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
m_EGLExtDmaBuf = ext.isSupported("EGL_EXT_image_dma_buf_import_modifiers");
|
m_EGLExtDmaBuf = ext.isSupported("EGL_EXT_image_dma_buf_import_modifiers");
|
||||||
|
|
||||||
|
// NB: eglCreateImage() and eglCreateImageKHR() have slightly different definitions
|
||||||
|
m_eglCreateImage = (typeof(m_eglCreateImage))eglGetProcAddress("eglCreateImage");
|
||||||
|
m_eglCreateImageKHR = (typeof(m_eglCreateImageKHR))eglGetProcAddress("eglCreateImageKHR");
|
||||||
|
m_eglDestroyImage = (typeof(m_eglDestroyImage))eglGetProcAddress("eglDestroyImage");
|
||||||
|
m_eglDestroyImageKHR = (typeof(m_eglDestroyImageKHR))eglGetProcAddress("eglDestroyImageKHR");
|
||||||
|
|
||||||
|
if (!(m_eglCreateImage && m_eglDestroyImage) &&
|
||||||
|
!(m_eglCreateImageKHR && m_eglDestroyImageKHR)) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Missing eglCreateImage()/eglDestroyImage() in EGL driver");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -554,33 +545,54 @@ VAAPIRenderer::exportEGLImages(AVFrame *frame, EGLDisplay dpy,
|
|||||||
const auto &layer = m_PrimeDescriptor.layers[i];
|
const auto &layer = m_PrimeDescriptor.layers[i];
|
||||||
const auto &object = m_PrimeDescriptor.objects[layer.object_index[0]];
|
const auto &object = m_PrimeDescriptor.objects[layer.object_index[0]];
|
||||||
|
|
||||||
EGLAttrib attribs[17] = {
|
const int EGL_ATTRIB_COUNT = 17;
|
||||||
EGL_LINUX_DRM_FOURCC_EXT, (EGLint)layer.drm_format,
|
EGLAttrib attribs[EGL_ATTRIB_COUNT] = {
|
||||||
|
EGL_LINUX_DRM_FOURCC_EXT, layer.drm_format,
|
||||||
EGL_WIDTH, i == 0 ? frame->width : frame->width / 2,
|
EGL_WIDTH, i == 0 ? frame->width : frame->width / 2,
|
||||||
EGL_HEIGHT, i == 0 ? frame->height : frame->height / 2,
|
EGL_HEIGHT, i == 0 ? frame->height : frame->height / 2,
|
||||||
EGL_DMA_BUF_PLANE0_FD_EXT, object.fd,
|
EGL_DMA_BUF_PLANE0_FD_EXT, object.fd,
|
||||||
EGL_DMA_BUF_PLANE0_OFFSET_EXT, (EGLint)layer.offset[0],
|
EGL_DMA_BUF_PLANE0_OFFSET_EXT, layer.offset[0],
|
||||||
EGL_DMA_BUF_PLANE0_PITCH_EXT, (EGLint)layer.pitch[0],
|
EGL_DMA_BUF_PLANE0_PITCH_EXT, layer.pitch[0],
|
||||||
EGL_NONE,
|
EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT, (EGLint)object.drm_format_modifier,
|
||||||
|
EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT, (EGLint)(object.drm_format_modifier >> 32),
|
||||||
|
EGL_NONE
|
||||||
};
|
};
|
||||||
if (m_EGLExtDmaBuf) {
|
|
||||||
const EGLAttrib extra[] = {
|
// Cut off the attribute array before the modifiers if they aren't supported
|
||||||
EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT,
|
if (!m_EGLExtDmaBuf) {
|
||||||
(EGLint)object.drm_format_modifier,
|
SDL_assert(attribs[12] == EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT);
|
||||||
EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT,
|
SDL_assert(attribs[14] == EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT);
|
||||||
(EGLint)(object.drm_format_modifier >> 32),
|
SDL_assert(attribs[16] == EGL_NONE);
|
||||||
EGL_NONE,
|
attribs[12] = EGL_NONE;
|
||||||
};
|
|
||||||
memcpy((void *)(&attribs[12]), (void *)extra, sizeof (extra));
|
|
||||||
}
|
}
|
||||||
images[i] = eglCreateImage(dpy, EGL_NO_CONTEXT,
|
|
||||||
EGL_LINUX_DMA_BUF_EXT,
|
if (m_eglCreateImage) {
|
||||||
nullptr, attribs);
|
images[i] = m_eglCreateImage(dpy, EGL_NO_CONTEXT,
|
||||||
if (!images[i]) {
|
EGL_LINUX_DMA_BUF_EXT,
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
nullptr, attribs);
|
||||||
"eglCreateImage() Failed: %d", eglGetError());
|
if (!images[i]) {
|
||||||
goto create_image_fail;
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"eglCreateImage() Failed: %d", eglGetError());
|
||||||
|
goto create_image_fail;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
// Cast the EGLAttrib array elements to EGLint for the KHR extension
|
||||||
|
EGLint intAttribs[EGL_ATTRIB_COUNT];
|
||||||
|
for (int i = 0; i < EGL_ATTRIB_COUNT; i++) {
|
||||||
|
intAttribs[i] = (EGLint)attribs[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
images[i] = m_eglCreateImageKHR(dpy, EGL_NO_CONTEXT,
|
||||||
|
EGL_LINUX_DMA_BUF_EXT,
|
||||||
|
nullptr, intAttribs);
|
||||||
|
if (!images[i]) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"eglCreateImageKHR() Failed: %d", eglGetError());
|
||||||
|
goto create_image_fail;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
++count;
|
++count;
|
||||||
}
|
}
|
||||||
return count;
|
return count;
|
||||||
@@ -595,7 +607,12 @@ sync_fail:
|
|||||||
void
|
void
|
||||||
VAAPIRenderer::freeEGLImages(EGLDisplay dpy, EGLImage images[EGL_MAX_PLANES]) {
|
VAAPIRenderer::freeEGLImages(EGLDisplay dpy, EGLImage images[EGL_MAX_PLANES]) {
|
||||||
for (size_t i = 0; i < m_PrimeDescriptor.num_layers; ++i) {
|
for (size_t i = 0; i < m_PrimeDescriptor.num_layers; ++i) {
|
||||||
eglDestroyImage(dpy, images[i]);
|
if (m_eglDestroyImage) {
|
||||||
|
m_eglDestroyImage(dpy, images[i]);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
m_eglDestroyImageKHR(dpy, images[i]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for (size_t i = 0; i < m_PrimeDescriptor.num_objects; ++i) {
|
for (size_t i = 0; i < m_PrimeDescriptor.num_objects; ++i) {
|
||||||
close(m_PrimeDescriptor.objects[i].fd);
|
close(m_PrimeDescriptor.objects[i].fd);
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ public:
|
|||||||
virtual int getDecoderColorspace() override;
|
virtual int getDecoderColorspace() override;
|
||||||
#ifdef HAVE_EGL
|
#ifdef HAVE_EGL
|
||||||
virtual bool canExportEGL() override;
|
virtual bool canExportEGL() override;
|
||||||
|
virtual AVPixelFormat getEGLImagePixelFormat() override;
|
||||||
virtual bool initializeEGL(EGLDisplay dpy, const EGLExtensions &ext) override;
|
virtual bool initializeEGL(EGLDisplay dpy, const EGLExtensions &ext) override;
|
||||||
virtual ssize_t exportEGLImages(AVFrame *frame, EGLDisplay dpy, EGLImage images[EGL_MAX_PLANES]) override;
|
virtual ssize_t exportEGLImages(AVFrame *frame, EGLDisplay dpy, EGLImage images[EGL_MAX_PLANES]) override;
|
||||||
virtual void freeEGLImages(EGLDisplay dpy, EGLImage[EGL_MAX_PLANES]) override;
|
virtual void freeEGLImages(EGLDisplay dpy, EGLImage[EGL_MAX_PLANES]) override;
|
||||||
@@ -54,7 +55,6 @@ private:
|
|||||||
|
|
||||||
int m_WindowSystem;
|
int m_WindowSystem;
|
||||||
AVBufferRef* m_HwContext;
|
AVBufferRef* m_HwContext;
|
||||||
int m_DrmFd;
|
|
||||||
bool m_BlacklistedForDirectRendering;
|
bool m_BlacklistedForDirectRendering;
|
||||||
|
|
||||||
#ifdef HAVE_LIBVA_X11
|
#ifdef HAVE_LIBVA_X11
|
||||||
@@ -69,5 +69,9 @@ private:
|
|||||||
#ifdef HAVE_EGL
|
#ifdef HAVE_EGL
|
||||||
VADRMPRIMESurfaceDescriptor m_PrimeDescriptor;
|
VADRMPRIMESurfaceDescriptor m_PrimeDescriptor;
|
||||||
bool m_EGLExtDmaBuf;
|
bool m_EGLExtDmaBuf;
|
||||||
|
PFNEGLCREATEIMAGEPROC m_eglCreateImage;
|
||||||
|
PFNEGLDESTROYIMAGEPROC m_eglDestroyImage;
|
||||||
|
PFNEGLCREATEIMAGEKHRPROC m_eglCreateImageKHR;
|
||||||
|
PFNEGLDESTROYIMAGEKHRPROC m_eglDestroyImageKHR;
|
||||||
#endif
|
#endif
|
||||||
};
|
};
|
||||||
@@ -281,8 +281,8 @@ public:
|
|||||||
// Queue this sample for the next v-sync
|
// Queue this sample for the next v-sync
|
||||||
CMSampleTimingInfo timingInfo = {
|
CMSampleTimingInfo timingInfo = {
|
||||||
.duration = kCMTimeInvalid,
|
.duration = kCMTimeInvalid,
|
||||||
|
.presentationTimeStamp = CMClockMakeHostTimeFromSystemUnits(mach_absolute_time()),
|
||||||
.decodeTimeStamp = kCMTimeInvalid,
|
.decodeTimeStamp = kCMTimeInvalid,
|
||||||
.presentationTimeStamp = CMClockMakeHostTimeFromSystemUnits(mach_absolute_time())
|
|
||||||
};
|
};
|
||||||
|
|
||||||
CMSampleBufferRef sampleBuffer;
|
CMSampleBufferRef sampleBuffer;
|
||||||
@@ -365,6 +365,7 @@ public:
|
|||||||
if ([device.name containsString:@" 5"]) {
|
if ([device.name containsString:@" 5"]) {
|
||||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"No HEVC Main10 support on Skylake iGPU");
|
"No HEVC Main10 support on Skylake iGPU");
|
||||||
|
[device release];
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -375,10 +376,13 @@ public:
|
|||||||
[device.name containsString:@" M3"]) {
|
[device.name containsString:@" M3"]) {
|
||||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"No HEVC Main10 support on AMD GPUs until Polaris");
|
"No HEVC Main10 support on AMD GPUs until Polaris");
|
||||||
|
[device release];
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[device release];
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
+199
-128
@@ -51,7 +51,7 @@ bool FFmpegVideoDecoder::isHardwareAccelerated()
|
|||||||
|
|
||||||
bool FFmpegVideoDecoder::isAlwaysFullScreen()
|
bool FFmpegVideoDecoder::isAlwaysFullScreen()
|
||||||
{
|
{
|
||||||
return m_BackendRenderer->getRendererAttributes() & RENDERER_ATTRIBUTE_FULLSCREEN_ONLY;
|
return m_FrontendRenderer->getRendererAttributes() & RENDERER_ATTRIBUTE_FULLSCREEN_ONLY;
|
||||||
}
|
}
|
||||||
|
|
||||||
int FFmpegVideoDecoder::getDecoderCapabilities()
|
int FFmpegVideoDecoder::getDecoderCapabilities()
|
||||||
@@ -117,7 +117,8 @@ enum AVPixelFormat FFmpegVideoDecoder::ffGetFormat(AVCodecContext* context,
|
|||||||
}
|
}
|
||||||
|
|
||||||
FFmpegVideoDecoder::FFmpegVideoDecoder(bool testOnly)
|
FFmpegVideoDecoder::FFmpegVideoDecoder(bool testOnly)
|
||||||
: m_VideoDecoderCtx(nullptr),
|
: m_Pkt(av_packet_alloc()),
|
||||||
|
m_VideoDecoderCtx(nullptr),
|
||||||
m_DecodeBuffer(1024 * 1024, 0),
|
m_DecodeBuffer(1024 * 1024, 0),
|
||||||
m_HwDecodeCfg(nullptr),
|
m_HwDecodeCfg(nullptr),
|
||||||
m_BackendRenderer(nullptr),
|
m_BackendRenderer(nullptr),
|
||||||
@@ -132,8 +133,6 @@ FFmpegVideoDecoder::FFmpegVideoDecoder(bool testOnly)
|
|||||||
m_NeedsSpsFixup(false),
|
m_NeedsSpsFixup(false),
|
||||||
m_TestOnly(testOnly)
|
m_TestOnly(testOnly)
|
||||||
{
|
{
|
||||||
av_init_packet(&m_Pkt);
|
|
||||||
|
|
||||||
SDL_zero(m_ActiveWndVideoStats);
|
SDL_zero(m_ActiveWndVideoStats);
|
||||||
SDL_zero(m_LastWndVideoStats);
|
SDL_zero(m_LastWndVideoStats);
|
||||||
SDL_zero(m_GlobalVideoStats);
|
SDL_zero(m_GlobalVideoStats);
|
||||||
@@ -151,6 +150,8 @@ FFmpegVideoDecoder::~FFmpegVideoDecoder()
|
|||||||
// to preserve the log level across reset() during
|
// to preserve the log level across reset() during
|
||||||
// test initialization.
|
// test initialization.
|
||||||
av_log_set_level(AV_LOG_INFO);
|
av_log_set_level(AV_LOG_INFO);
|
||||||
|
|
||||||
|
av_packet_free(&m_Pkt);
|
||||||
}
|
}
|
||||||
|
|
||||||
IFFmpegRenderer* FFmpegVideoDecoder::getBackendRenderer()
|
IFFmpegRenderer* FFmpegVideoDecoder::getBackendRenderer()
|
||||||
@@ -192,17 +193,22 @@ void FFmpegVideoDecoder::reset()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool FFmpegVideoDecoder::createFrontendRenderer(PDECODER_PARAMETERS params)
|
bool FFmpegVideoDecoder::createFrontendRenderer(PDECODER_PARAMETERS params, bool eglOnly)
|
||||||
{
|
{
|
||||||
|
if (eglOnly) {
|
||||||
#ifdef HAVE_EGL
|
#ifdef HAVE_EGL
|
||||||
if (m_BackendRenderer->canExportEGL()) {
|
if (m_BackendRenderer->canExportEGL()) {
|
||||||
m_FrontendRenderer = new EGLRenderer(m_BackendRenderer);
|
m_FrontendRenderer = new EGLRenderer(m_BackendRenderer);
|
||||||
if (m_FrontendRenderer->initialize(params)) {
|
if (m_FrontendRenderer->initialize(params)) {
|
||||||
return true;
|
return true;
|
||||||
|
}
|
||||||
|
delete m_FrontendRenderer;
|
||||||
|
m_FrontendRenderer = nullptr;
|
||||||
}
|
}
|
||||||
delete m_FrontendRenderer;
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
|
// If we made it here, we failed to create the EGLRenderer
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (m_BackendRenderer->isDirectRenderingSupported()) {
|
if (m_BackendRenderer->isDirectRenderingSupported()) {
|
||||||
// The backend renderer can render to the display
|
// The backend renderer can render to the display
|
||||||
@@ -220,13 +226,13 @@ bool FFmpegVideoDecoder::createFrontendRenderer(PDECODER_PARAMETERS params)
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool FFmpegVideoDecoder::completeInitialization(AVCodec* decoder, PDECODER_PARAMETERS params, bool testFrame)
|
bool FFmpegVideoDecoder::completeInitialization(const AVCodec* decoder, PDECODER_PARAMETERS params, bool testFrame, bool eglOnly)
|
||||||
{
|
{
|
||||||
// In test-only mode, we should only see test frames
|
// In test-only mode, we should only see test frames
|
||||||
SDL_assert(!m_TestOnly || testFrame);
|
SDL_assert(!m_TestOnly || testFrame);
|
||||||
|
|
||||||
// Create the frontend renderer based on the capabilities of the backend renderer
|
// Create the frontend renderer based on the capabilities of the backend renderer
|
||||||
if (!createFrontendRenderer(params)) {
|
if (!createFrontendRenderer(params, eglOnly)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -311,16 +317,16 @@ bool FFmpegVideoDecoder::completeInitialization(AVCodec* decoder, PDECODER_PARAM
|
|||||||
if (testFrame) {
|
if (testFrame) {
|
||||||
switch (params->videoFormat) {
|
switch (params->videoFormat) {
|
||||||
case VIDEO_FORMAT_H264:
|
case VIDEO_FORMAT_H264:
|
||||||
m_Pkt.data = (uint8_t*)k_H264TestFrame;
|
m_Pkt->data = (uint8_t*)k_H264TestFrame;
|
||||||
m_Pkt.size = sizeof(k_H264TestFrame);
|
m_Pkt->size = sizeof(k_H264TestFrame);
|
||||||
break;
|
break;
|
||||||
case VIDEO_FORMAT_H265:
|
case VIDEO_FORMAT_H265:
|
||||||
m_Pkt.data = (uint8_t*)k_HEVCMainTestFrame;
|
m_Pkt->data = (uint8_t*)k_HEVCMainTestFrame;
|
||||||
m_Pkt.size = sizeof(k_HEVCMainTestFrame);
|
m_Pkt->size = sizeof(k_HEVCMainTestFrame);
|
||||||
break;
|
break;
|
||||||
case VIDEO_FORMAT_H265_MAIN10:
|
case VIDEO_FORMAT_H265_MAIN10:
|
||||||
m_Pkt.data = (uint8_t*)k_HEVCMain10TestFrame;
|
m_Pkt->data = (uint8_t*)k_HEVCMain10TestFrame;
|
||||||
m_Pkt.size = sizeof(k_HEVCMain10TestFrame);
|
m_Pkt->size = sizeof(k_HEVCMain10TestFrame);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
@@ -341,7 +347,7 @@ bool FFmpegVideoDecoder::completeInitialization(AVCodec* decoder, PDECODER_PARAM
|
|||||||
for (int retries = 0; retries < 5; retries++) {
|
for (int retries = 0; retries < 5; retries++) {
|
||||||
// Most FFmpeg decoders process input using a "push" model.
|
// Most FFmpeg decoders process input using a "push" model.
|
||||||
// We'll see those fail here if the format is not supported.
|
// We'll see those fail here if the format is not supported.
|
||||||
err = avcodec_send_packet(m_VideoDecoderCtx, &m_Pkt);
|
err = avcodec_send_packet(m_VideoDecoderCtx, m_Pkt);
|
||||||
if (err < 0) {
|
if (err < 0) {
|
||||||
av_frame_free(&frame);
|
av_frame_free(&frame);
|
||||||
char errorstring[512];
|
char errorstring[512];
|
||||||
@@ -364,6 +370,14 @@ bool FFmpegVideoDecoder::completeInitialization(AVCodec* decoder, PDECODER_PARAM
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Allow the renderer to do any validation it wants on this frame
|
||||||
|
if (!m_FrontendRenderer->testRenderFrame(frame)) {
|
||||||
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Render test failed");
|
||||||
|
av_frame_free(&frame);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
av_frame_free(&frame);
|
av_frame_free(&frame);
|
||||||
if (err < 0) {
|
if (err < 0) {
|
||||||
char errorstring[512];
|
char errorstring[512];
|
||||||
@@ -404,6 +418,16 @@ void FFmpegVideoDecoder::addVideoStats(VIDEO_STATS& src, VIDEO_STATS& dst)
|
|||||||
dst.totalPacerTime += src.totalPacerTime;
|
dst.totalPacerTime += src.totalPacerTime;
|
||||||
dst.totalRenderTime += src.totalRenderTime;
|
dst.totalRenderTime += src.totalRenderTime;
|
||||||
|
|
||||||
|
if (!LiGetEstimatedRttInfo(&dst.lastRtt, &dst.lastRttVariance)) {
|
||||||
|
dst.lastRtt = 0;
|
||||||
|
dst.lastRttVariance = 0;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Our logic to determine if RTT is valid depends on us never
|
||||||
|
// getting an RTT of 0. ENet currently ensures RTTs are >= 1.
|
||||||
|
SDL_assert(dst.lastRtt > 0);
|
||||||
|
}
|
||||||
|
|
||||||
Uint32 now = SDL_GetTicks();
|
Uint32 now = SDL_GetTicks();
|
||||||
|
|
||||||
// Initialize the measurement start point if this is the first video stat window
|
// Initialize the measurement start point if this is the first video stat window
|
||||||
@@ -468,16 +492,25 @@ void FFmpegVideoDecoder::stringifyVideoStats(VIDEO_STATS& stats, char* output)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (stats.renderedFrames != 0) {
|
if (stats.renderedFrames != 0) {
|
||||||
|
char rttString[32];
|
||||||
|
|
||||||
|
if (stats.lastRtt != 0) {
|
||||||
|
sprintf(rttString, "%u ms (variance: %u ms)", stats.lastRtt, stats.lastRttVariance);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
sprintf(rttString, "N/A");
|
||||||
|
}
|
||||||
|
|
||||||
offset += sprintf(&output[offset],
|
offset += sprintf(&output[offset],
|
||||||
"Frames dropped by your network connection: %.2f%%\n"
|
"Frames dropped by your network connection: %.2f%%\n"
|
||||||
"Frames dropped due to network jitter: %.2f%%\n"
|
"Frames dropped due to network jitter: %.2f%%\n"
|
||||||
"Average receive time: %.2f ms\n"
|
"Average network latency: %s\n"
|
||||||
"Average decoding time: %.2f ms\n"
|
"Average decoding time: %.2f ms\n"
|
||||||
"Average frame queue delay: %.2f ms\n"
|
"Average frame queue delay: %.2f ms\n"
|
||||||
"Average rendering time (including monitor V-sync latency): %.2f ms\n",
|
"Average rendering time (including monitor V-sync latency): %.2f ms\n",
|
||||||
(float)stats.networkDroppedFrames / stats.totalFrames * 100,
|
(float)stats.networkDroppedFrames / stats.totalFrames * 100,
|
||||||
(float)stats.pacerDroppedFrames / stats.decodedFrames * 100,
|
(float)stats.pacerDroppedFrames / stats.decodedFrames * 100,
|
||||||
(float)stats.totalReassemblyTime / stats.receivedFrames,
|
rttString,
|
||||||
(float)stats.totalDecodeTime / stats.decodedFrames,
|
(float)stats.totalDecodeTime / stats.decodedFrames,
|
||||||
(float)stats.totalPacerTime / stats.renderedFrames,
|
(float)stats.totalPacerTime / stats.renderedFrames,
|
||||||
(float)stats.totalRenderTime / stats.renderedFrames);
|
(float)stats.totalRenderTime / stats.renderedFrames);
|
||||||
@@ -522,6 +555,10 @@ IFFmpegRenderer* FFmpegVideoDecoder::createHwAccelRenderer(const AVCodecHWConfig
|
|||||||
#ifdef HAVE_LIBVDPAU
|
#ifdef HAVE_LIBVDPAU
|
||||||
case AV_HWDEVICE_TYPE_VDPAU:
|
case AV_HWDEVICE_TYPE_VDPAU:
|
||||||
return new VDPAURenderer();
|
return new VDPAURenderer();
|
||||||
|
#endif
|
||||||
|
#ifdef HAVE_DRM
|
||||||
|
case AV_HWDEVICE_TYPE_DRM:
|
||||||
|
return new DrmRenderer();
|
||||||
#endif
|
#endif
|
||||||
default:
|
default:
|
||||||
return nullptr;
|
return nullptr;
|
||||||
@@ -545,47 +582,140 @@ IFFmpegRenderer* FFmpegVideoDecoder::createHwAccelRenderer(const AVCodecHWConfig
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool FFmpegVideoDecoder::tryInitializeRenderer(AVCodec* decoder,
|
bool FFmpegVideoDecoder::tryInitializeRenderer(const AVCodec* decoder,
|
||||||
PDECODER_PARAMETERS params,
|
PDECODER_PARAMETERS params,
|
||||||
const AVCodecHWConfig* hwConfig,
|
const AVCodecHWConfig* hwConfig,
|
||||||
std::function<IFFmpegRenderer*()> createRendererFunc)
|
std::function<IFFmpegRenderer*()> createRendererFunc)
|
||||||
{
|
{
|
||||||
m_BackendRenderer = createRendererFunc();
|
|
||||||
m_HwDecodeCfg = hwConfig;
|
m_HwDecodeCfg = hwConfig;
|
||||||
|
|
||||||
if (m_BackendRenderer != nullptr &&
|
// i == 0 - Indirect via EGL frontend with zero-copy DMA-BUF passing
|
||||||
m_BackendRenderer->initialize(params) &&
|
// i == 1 - Direct rendering or indirect via SDL read-back
|
||||||
completeInitialization(decoder, params, m_TestOnly || m_BackendRenderer->needsTestFrame())) {
|
#ifdef HAVE_EGL
|
||||||
if (m_TestOnly) {
|
for (int i = 0; i < 2; i++) {
|
||||||
// This decoder is only for testing capabilities, so don't bother
|
#else
|
||||||
// creating a usable renderer
|
for (int i = 1; i < 2; i++) {
|
||||||
return true;
|
#endif
|
||||||
}
|
SDL_assert(m_BackendRenderer == nullptr);
|
||||||
|
if ((m_BackendRenderer = createRendererFunc()) != nullptr &&
|
||||||
if (m_BackendRenderer->needsTestFrame()) {
|
m_BackendRenderer->initialize(params) &&
|
||||||
// The test worked, so now let's initialize it for real
|
completeInitialization(decoder, params, m_TestOnly || m_BackendRenderer->needsTestFrame(), i == 0 /* EGL */)) {
|
||||||
reset();
|
if (m_TestOnly) {
|
||||||
if ((m_BackendRenderer = createRendererFunc()) != nullptr &&
|
// This decoder is only for testing capabilities, so don't bother
|
||||||
m_BackendRenderer->initialize(params) &&
|
// creating a usable renderer
|
||||||
completeInitialization(decoder, params, false)) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
SDL_LogCritical(SDL_LOG_CATEGORY_APPLICATION,
|
if (m_BackendRenderer->needsTestFrame()) {
|
||||||
"Decoder failed to initialize after successful test");
|
// The test worked, so now let's initialize it for real
|
||||||
reset();
|
reset();
|
||||||
|
if ((m_BackendRenderer = createRendererFunc()) != nullptr &&
|
||||||
|
m_BackendRenderer->initialize(params) &&
|
||||||
|
completeInitialization(decoder, params, false, i == 0 /* EGL */)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
SDL_LogCritical(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Decoder failed to initialize after successful test");
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// No test required. Good to go now.
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// No test required. Good to go now.
|
// Failed to initialize, so keep looking
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// reset() must be called before we reach this point!
|
||||||
|
SDL_assert(m_BackendRenderer == nullptr);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
#define TRY_PREFERRED_PIXEL_FORMAT(RENDERER_TYPE) \
|
||||||
|
{ \
|
||||||
|
RENDERER_TYPE renderer; \
|
||||||
|
if (renderer.getPreferredPixelFormat(params->videoFormat) == decoder->pix_fmts[i]) { \
|
||||||
|
if (tryInitializeRenderer(decoder, params, nullptr, \
|
||||||
|
[]() -> IFFmpegRenderer* { return new RENDERER_TYPE(); })) { \
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, \
|
||||||
|
"Chose " #RENDERER_TYPE " for codec %s due to preferred pixel format: 0x%x", \
|
||||||
|
decoder->name, decoder->pix_fmts[i]); \
|
||||||
|
return true; \
|
||||||
|
} \
|
||||||
|
} \
|
||||||
|
}
|
||||||
|
|
||||||
|
#define TRY_SUPPORTED_PIXEL_FORMAT(RENDERER_TYPE) \
|
||||||
|
{ \
|
||||||
|
RENDERER_TYPE renderer; \
|
||||||
|
if (renderer.isPixelFormatSupported(params->videoFormat, decoder->pix_fmts[i])) { \
|
||||||
|
if (tryInitializeRenderer(decoder, params, nullptr, \
|
||||||
|
[]() -> IFFmpegRenderer* { return new RENDERER_TYPE(); })) { \
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, \
|
||||||
|
"Chose " #RENDERER_TYPE " for codec %s due to compatible pixel format: 0x%x", \
|
||||||
|
decoder->name, decoder->pix_fmts[i]); \
|
||||||
|
return true; \
|
||||||
|
} \
|
||||||
|
} \
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FFmpegVideoDecoder::tryInitializeRendererForDecoderByName(const char *decoderName,
|
||||||
|
PDECODER_PARAMETERS params)
|
||||||
|
{
|
||||||
|
const AVCodec* decoder = avcodec_find_decoder_by_name(decoderName);
|
||||||
|
if (decoder == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This might be a hwaccel decoder, so try any hw configs first
|
||||||
|
for (int i = 0;; i++) {
|
||||||
|
const AVCodecHWConfig *config = avcodec_get_hw_config(decoder, i);
|
||||||
|
if (!config) {
|
||||||
|
// No remaing hwaccel options
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize the hardware codec and submit a test frame if the renderer needs it
|
||||||
|
if (tryInitializeRenderer(decoder, params, config,
|
||||||
|
[config]() -> IFFmpegRenderer* { return createHwAccelRenderer(config, 0); })) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
// Failed to initialize or test frame failed, so keep looking
|
if (decoder->pix_fmts == NULL) {
|
||||||
reset();
|
// Supported output pixel formats are unknown. We'll just try SDL and hope it can cope.
|
||||||
|
return tryInitializeRenderer(decoder, params, nullptr,
|
||||||
|
[]() -> IFFmpegRenderer* { return new SdlRenderer(); });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if any of our decoders prefer any of the pixel formats first
|
||||||
|
for (int i = 0; decoder->pix_fmts[i] != AV_PIX_FMT_NONE; i++) {
|
||||||
|
#ifdef HAVE_DRM
|
||||||
|
TRY_PREFERRED_PIXEL_FORMAT(DrmRenderer);
|
||||||
|
#endif
|
||||||
|
#ifdef HAVE_MMAL
|
||||||
|
TRY_PREFERRED_PIXEL_FORMAT(MmalRenderer);
|
||||||
|
#endif
|
||||||
|
TRY_PREFERRED_PIXEL_FORMAT(SdlRenderer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nothing prefers any of them. Let's see if anyone will tolerate one.
|
||||||
|
for (int i = 0; decoder->pix_fmts[i] != AV_PIX_FMT_NONE; i++) {
|
||||||
|
#ifdef HAVE_DRM
|
||||||
|
TRY_SUPPORTED_PIXEL_FORMAT(DrmRenderer);
|
||||||
|
#endif
|
||||||
|
#ifdef HAVE_MMAL
|
||||||
|
TRY_SUPPORTED_PIXEL_FORMAT(MmalRenderer);
|
||||||
|
#endif
|
||||||
|
TRY_SUPPORTED_PIXEL_FORMAT(SdlRenderer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we made it here, we couldn't find anything
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -595,21 +725,18 @@ bool FFmpegVideoDecoder::initialize(PDECODER_PARAMETERS params)
|
|||||||
av_log_set_level(AV_LOG_DEBUG);
|
av_log_set_level(AV_LOG_DEBUG);
|
||||||
|
|
||||||
// First try decoders that the user has manually specified via environment variables.
|
// First try decoders that the user has manually specified via environment variables.
|
||||||
// These must output surfaces in one of the formats that the SDL renderer supports,
|
// These must output surfaces in one of the formats that one of our renderers supports,
|
||||||
// which is currently:
|
// which is currently:
|
||||||
// - AV_PIX_FMT_YUV420P (preferred)
|
// - AV_PIX_FMT_DRM_PRIME
|
||||||
|
// - AV_PIX_FMT_MMAL
|
||||||
|
// - AV_PIX_FMT_YUV420P
|
||||||
// - AV_PIX_FMT_NV12
|
// - AV_PIX_FMT_NV12
|
||||||
// - AV_PIX_FMT_NV21
|
// - AV_PIX_FMT_NV21
|
||||||
// These formats should cover most/all decoders that output in a standard YUV format.
|
|
||||||
{
|
{
|
||||||
QString h264DecoderHint = qgetenv("H264_DECODER_HINT");
|
QString h264DecoderHint = qgetenv("H264_DECODER_HINT");
|
||||||
if (!h264DecoderHint.isEmpty() && (params->videoFormat & VIDEO_FORMAT_MASK_H264)) {
|
if (!h264DecoderHint.isEmpty() && (params->videoFormat & VIDEO_FORMAT_MASK_H264)) {
|
||||||
QByteArray decoderString = h264DecoderHint.toLocal8Bit();
|
QByteArray decoderString = h264DecoderHint.toLocal8Bit();
|
||||||
AVCodec* customAvcDecoder = avcodec_find_decoder_by_name(decoderString.constData());
|
if (tryInitializeRendererForDecoderByName(decoderString.constData(), params)) {
|
||||||
|
|
||||||
if (customAvcDecoder != nullptr &&
|
|
||||||
tryInitializeRenderer(customAvcDecoder, params, nullptr,
|
|
||||||
[]() -> IFFmpegRenderer* { return new SdlRenderer(); })) {
|
|
||||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"Using custom H.264 decoder (H264_DECODER_HINT): %s",
|
"Using custom H.264 decoder (H264_DECODER_HINT): %s",
|
||||||
decoderString.constData());
|
decoderString.constData());
|
||||||
@@ -626,11 +753,7 @@ bool FFmpegVideoDecoder::initialize(PDECODER_PARAMETERS params)
|
|||||||
QString hevcDecoderHint = qgetenv("HEVC_DECODER_HINT");
|
QString hevcDecoderHint = qgetenv("HEVC_DECODER_HINT");
|
||||||
if (!hevcDecoderHint.isEmpty() && (params->videoFormat & VIDEO_FORMAT_MASK_H265)) {
|
if (!hevcDecoderHint.isEmpty() && (params->videoFormat & VIDEO_FORMAT_MASK_H265)) {
|
||||||
QByteArray decoderString = hevcDecoderHint.toLocal8Bit();
|
QByteArray decoderString = hevcDecoderHint.toLocal8Bit();
|
||||||
AVCodec* customHevcDecoder = avcodec_find_decoder_by_name(decoderString.constData());
|
if (tryInitializeRendererForDecoderByName(decoderString.constData(), params)) {
|
||||||
|
|
||||||
if (customHevcDecoder != nullptr &&
|
|
||||||
tryInitializeRenderer(customHevcDecoder, params, nullptr,
|
|
||||||
[]() -> IFFmpegRenderer* { return new SdlRenderer(); })) {
|
|
||||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"Using custom HEVC decoder (HEVC_DECODER_HINT): %s",
|
"Using custom HEVC decoder (HEVC_DECODER_HINT): %s",
|
||||||
decoderString.constData());
|
decoderString.constData());
|
||||||
@@ -644,7 +767,7 @@ bool FFmpegVideoDecoder::initialize(PDECODER_PARAMETERS params)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AVCodec* decoder;
|
const AVCodec* decoder;
|
||||||
|
|
||||||
if (params->videoFormat & VIDEO_FORMAT_MASK_H264) {
|
if (params->videoFormat & VIDEO_FORMAT_MASK_H264) {
|
||||||
decoder = avcodec_find_decoder(AV_CODEC_ID_H264);
|
decoder = avcodec_find_decoder(AV_CODEC_ID_H264);
|
||||||
@@ -682,74 +805,22 @@ bool FFmpegVideoDecoder::initialize(PDECODER_PARAMETERS params)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Continue with special non-hwaccel hardware decoders
|
// Continue with special non-hwaccel hardware decoders
|
||||||
|
|
||||||
#ifdef HAVE_MMAL
|
|
||||||
// MMAL is the decoder for the Raspberry Pi
|
|
||||||
if (params->videoFormat & VIDEO_FORMAT_MASK_H264) {
|
if (params->videoFormat & VIDEO_FORMAT_MASK_H264) {
|
||||||
AVCodec* mmalDecoder = avcodec_find_decoder_by_name("h264_mmal");
|
QList<const char *> knownAvcCodecs = { "h264_mmal", "h264_rkmpp", "h264_nvmpi", "h264_v4l2m2m" };
|
||||||
if (mmalDecoder != nullptr &&
|
for (const char* codec : knownAvcCodecs) {
|
||||||
tryInitializeRenderer(mmalDecoder, params, nullptr,
|
if (tryInitializeRendererForDecoderByName(codec, params)) {
|
||||||
[]() -> IFFmpegRenderer* { return new MmalRenderer(); })) {
|
return true;
|
||||||
return true;
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
else {
|
||||||
|
QList<const char *> knownHevcCodecs = { "hevc_rkmpp", "hevc_nvmpi", "hevc_v4l2m2m" };
|
||||||
#ifdef HAVE_DRM
|
for (const char* codec : knownHevcCodecs) {
|
||||||
{
|
if (tryInitializeRendererForDecoderByName(codec, params)) {
|
||||||
// RKMPP is a hardware accelerated decoder that outputs DRI PRIME buffers
|
return true;
|
||||||
AVCodec* rkmppDecoder;
|
}
|
||||||
|
|
||||||
if (params->videoFormat & VIDEO_FORMAT_MASK_H264) {
|
|
||||||
rkmppDecoder = avcodec_find_decoder_by_name("h264_rkmpp");
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
rkmppDecoder = avcodec_find_decoder_by_name("hevc_rkmpp");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rkmppDecoder != nullptr &&
|
|
||||||
tryInitializeRenderer(rkmppDecoder, params, nullptr,
|
|
||||||
[]() -> IFFmpegRenderer* { return new DrmRenderer(); })) {
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef Q_OS_LINUX
|
|
||||||
{
|
|
||||||
AVCodec* nvmpiDecoder;
|
|
||||||
|
|
||||||
if (params->videoFormat & VIDEO_FORMAT_MASK_H264) {
|
|
||||||
nvmpiDecoder = avcodec_find_decoder_by_name("h264_nvmpi");
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
nvmpiDecoder = avcodec_find_decoder_by_name("hevc_nvmpi");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nvmpiDecoder != nullptr &&
|
|
||||||
tryInitializeRenderer(nvmpiDecoder, params, nullptr,
|
|
||||||
[]() -> IFFmpegRenderer* { return new SdlRenderer(); })) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
AVCodec* v4l2Decoder;
|
|
||||||
|
|
||||||
if (params->videoFormat & VIDEO_FORMAT_MASK_H264) {
|
|
||||||
v4l2Decoder = avcodec_find_decoder_by_name("h264_v4l2m2m");
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
v4l2Decoder = avcodec_find_decoder_by_name("hevc_v4l2m2m");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (v4l2Decoder != nullptr &&
|
|
||||||
tryInitializeRenderer(v4l2Decoder, params, nullptr,
|
|
||||||
[]() -> IFFmpegRenderer* { return new SdlRenderer(); })) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Look for the first matching hwaccel hardware decoder (pass 1)
|
// Look for the first matching hwaccel hardware decoder (pass 1)
|
||||||
// This picks up "second-tier" hwaccels like CUDA.
|
// This picks up "second-tier" hwaccels like CUDA.
|
||||||
@@ -881,19 +952,19 @@ int FFmpegVideoDecoder::submitDecodeUnit(PDECODE_UNIT du)
|
|||||||
entry = entry->next;
|
entry = entry->next;
|
||||||
}
|
}
|
||||||
|
|
||||||
m_Pkt.data = reinterpret_cast<uint8_t*>(m_DecodeBuffer.data());
|
m_Pkt->data = reinterpret_cast<uint8_t*>(m_DecodeBuffer.data());
|
||||||
m_Pkt.size = offset;
|
m_Pkt->size = offset;
|
||||||
|
|
||||||
if (du->frameType == FRAME_TYPE_IDR) {
|
if (du->frameType == FRAME_TYPE_IDR) {
|
||||||
m_Pkt.flags = AV_PKT_FLAG_KEY;
|
m_Pkt->flags = AV_PKT_FLAG_KEY;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
m_Pkt.flags = 0;
|
m_Pkt->flags = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
m_ActiveWndVideoStats.totalReassemblyTime += du->enqueueTimeMs - du->receiveTimeMs;
|
m_ActiveWndVideoStats.totalReassemblyTime += du->enqueueTimeMs - du->receiveTimeMs;
|
||||||
|
|
||||||
err = avcodec_send_packet(m_VideoDecoderCtx, &m_Pkt);
|
err = avcodec_send_packet(m_VideoDecoderCtx, m_Pkt);
|
||||||
if (err < 0) {
|
if (err < 0) {
|
||||||
char errorstring[512];
|
char errorstring[512];
|
||||||
av_strerror(err, errorstring, sizeof(errorstring));
|
av_strerror(err, errorstring, sizeof(errorstring));
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ public:
|
|||||||
virtual IFFmpegRenderer* getBackendRenderer();
|
virtual IFFmpegRenderer* getBackendRenderer();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
bool completeInitialization(AVCodec* decoder, PDECODER_PARAMETERS params, bool testFrame);
|
bool completeInitialization(const AVCodec* decoder, PDECODER_PARAMETERS params, bool testFrame, bool eglOnly);
|
||||||
|
|
||||||
void stringifyVideoStats(VIDEO_STATS& stats, char* output);
|
void stringifyVideoStats(VIDEO_STATS& stats, char* output);
|
||||||
|
|
||||||
@@ -34,9 +34,12 @@ private:
|
|||||||
|
|
||||||
void addVideoStats(VIDEO_STATS& src, VIDEO_STATS& dst);
|
void addVideoStats(VIDEO_STATS& src, VIDEO_STATS& dst);
|
||||||
|
|
||||||
bool createFrontendRenderer(PDECODER_PARAMETERS params);
|
bool createFrontendRenderer(PDECODER_PARAMETERS params, bool eglOnly);
|
||||||
|
|
||||||
bool tryInitializeRenderer(AVCodec* decoder,
|
bool tryInitializeRendererForDecoderByName(const char* decoderName,
|
||||||
|
PDECODER_PARAMETERS params);
|
||||||
|
|
||||||
|
bool tryInitializeRenderer(const AVCodec* decoder,
|
||||||
PDECODER_PARAMETERS params,
|
PDECODER_PARAMETERS params,
|
||||||
const AVCodecHWConfig* hwConfig,
|
const AVCodecHWConfig* hwConfig,
|
||||||
std::function<IFFmpegRenderer*()> createRendererFunc);
|
std::function<IFFmpegRenderer*()> createRendererFunc);
|
||||||
@@ -51,7 +54,7 @@ private:
|
|||||||
enum AVPixelFormat ffGetFormat(AVCodecContext* context,
|
enum AVPixelFormat ffGetFormat(AVCodecContext* context,
|
||||||
const enum AVPixelFormat* pixFmts);
|
const enum AVPixelFormat* pixFmts);
|
||||||
|
|
||||||
AVPacket m_Pkt;
|
AVPacket* m_Pkt;
|
||||||
AVCodecContext* m_VideoDecoderCtx;
|
AVCodecContext* m_VideoDecoderCtx;
|
||||||
QByteArray m_DecodeBuffer;
|
QByteArray m_DecodeBuffer;
|
||||||
const AVCodecHWConfig* m_HwDecodeCfg;
|
const AVCodecHWConfig* m_HwDecodeCfg;
|
||||||
|
|||||||
@@ -7,4 +7,5 @@ namespace WMUtils {
|
|||||||
bool isRunningX11();
|
bool isRunningX11();
|
||||||
bool isRunningWayland();
|
bool isRunningWayland();
|
||||||
bool isRunningWindowManager();
|
bool isRunningWindowManager();
|
||||||
|
bool isRunningDesktopEnvironment();
|
||||||
}
|
}
|
||||||
+1
-1
@@ -1 +1 @@
|
|||||||
3.0.0
|
3.1.4
|
||||||
+58
-8
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
#include "utils.h"
|
#include "utils.h"
|
||||||
|
|
||||||
|
#include <SDL.h>
|
||||||
|
|
||||||
#ifdef HAS_X11
|
#ifdef HAS_X11
|
||||||
#include <X11/Xlib.h>
|
#include <X11/Xlib.h>
|
||||||
#endif
|
#endif
|
||||||
@@ -10,14 +12,30 @@
|
|||||||
#include <wayland-client.h>
|
#include <wayland-client.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#define VALUE_SET 0x01
|
||||||
|
#define VALUE_TRUE 0x02
|
||||||
|
|
||||||
bool WMUtils::isRunningX11()
|
bool WMUtils::isRunningX11()
|
||||||
{
|
{
|
||||||
#ifdef HAS_X11
|
#ifdef HAS_X11
|
||||||
Display* display = XOpenDisplay(nullptr);
|
static SDL_atomic_t isRunningOnX11;
|
||||||
if (display != nullptr) {
|
|
||||||
XCloseDisplay(display);
|
// If the value is not set yet, populate it now.
|
||||||
return true;
|
int val = SDL_AtomicGet(&isRunningOnX11);
|
||||||
|
if (!(val & VALUE_SET)) {
|
||||||
|
Display* display = XOpenDisplay(nullptr);
|
||||||
|
if (display != nullptr) {
|
||||||
|
XCloseDisplay(display);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate the value to return and have for next time.
|
||||||
|
// This can race with another thread populating the same data,
|
||||||
|
// but that's no big deal.
|
||||||
|
val = VALUE_SET | ((display != nullptr) ? VALUE_TRUE : 0);
|
||||||
|
SDL_AtomicSet(&isRunningOnX11, val);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return !!(val & VALUE_TRUE);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@@ -26,11 +44,24 @@ bool WMUtils::isRunningX11()
|
|||||||
bool WMUtils::isRunningWayland()
|
bool WMUtils::isRunningWayland()
|
||||||
{
|
{
|
||||||
#ifdef HAS_WAYLAND
|
#ifdef HAS_WAYLAND
|
||||||
struct wl_display* display = wl_display_connect(nullptr);
|
static SDL_atomic_t isRunningOnWayland;
|
||||||
if (display != nullptr) {
|
|
||||||
wl_display_disconnect(display);
|
// If the value is not set yet, populate it now.
|
||||||
return true;
|
int val = SDL_AtomicGet(&isRunningOnWayland);
|
||||||
|
if (!(val & VALUE_SET)) {
|
||||||
|
struct wl_display* display = wl_display_connect(nullptr);
|
||||||
|
if (display != nullptr) {
|
||||||
|
wl_display_disconnect(display);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate the value to return and have for next time.
|
||||||
|
// This can race with another thread populating the same data,
|
||||||
|
// but that's no big deal.
|
||||||
|
val = VALUE_SET | ((display != nullptr) ? VALUE_TRUE : 0);
|
||||||
|
SDL_AtomicSet(&isRunningOnWayland, val);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return !!(val & VALUE_TRUE);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@@ -46,3 +77,22 @@ bool WMUtils::isRunningWindowManager()
|
|||||||
return WMUtils::isRunningWayland() || WMUtils::isRunningX11();
|
return WMUtils::isRunningWayland() || WMUtils::isRunningX11();
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool WMUtils::isRunningDesktopEnvironment()
|
||||||
|
{
|
||||||
|
if (qEnvironmentVariableIsSet("HAS_DESKTOP_ENVIRONMENT")) {
|
||||||
|
return qEnvironmentVariableIntValue("HAS_DESKTOP_ENVIRONMENT");
|
||||||
|
}
|
||||||
|
|
||||||
|
#if defined(Q_OS_WIN) || defined(Q_OS_DARWIN)
|
||||||
|
// Windows and macOS are always running a desktop environment
|
||||||
|
return true;
|
||||||
|
#elif defined(EMBEDDED_BUILD)
|
||||||
|
// Embedded systems don't run desktop environments
|
||||||
|
return false;
|
||||||
|
#else
|
||||||
|
// On non-embedded systems, assume we have a desktop environment
|
||||||
|
// if we have a WM running.
|
||||||
|
return isRunningWindowManager();
|
||||||
|
#endif
|
||||||
|
}
|
||||||
+11
-11
@@ -10,29 +10,29 @@ environment:
|
|||||||
BUILD_TARGET: macos
|
BUILD_TARGET: macos
|
||||||
- APPVEYOR_BUILD_WORKER_IMAGE: Ubuntu1604
|
- APPVEYOR_BUILD_WORKER_IMAGE: Ubuntu1604
|
||||||
BUILD_TARGET: steamlink
|
BUILD_TARGET: steamlink
|
||||||
- APPVEYOR_BUILD_WORKER_IMAGE: Ubuntu1604
|
- APPVEYOR_BUILD_WORKER_IMAGE: Ubuntu
|
||||||
BUILD_TARGET: linux
|
BUILD_TARGET: linux
|
||||||
FFMPEG_CONFIGURE_ARGS: --enable-pic --disable-static --enable-shared --disable-all --enable-avcodec --enable-decoder=h264 --enable-decoder=hevc --enable-nvdec --enable-hwaccel=h264_nvdec --enable-hwaccel=hevc_nvdec --enable-hwaccel=h264_vaapi --enable-hwaccel=hevc_vaapi --enable-hwaccel=h264_vdpau --enable-hwaccel=hevc_vdpau
|
FFMPEG_CONFIGURE_ARGS: --enable-pic --disable-static --enable-shared --disable-all --enable-avcodec --enable-decoder=h264 --enable-decoder=hevc --enable-nvdec --enable-hwaccel=h264_nvdec --enable-hwaccel=hevc_nvdec --enable-hwaccel=h264_vaapi --enable-hwaccel=hevc_vaapi --enable-hwaccel=h264_vdpau --enable-hwaccel=hevc_vdpau
|
||||||
|
|
||||||
install:
|
install:
|
||||||
- sh: '[ "$BUILD_TARGET" != macos ] || nvm use node'
|
- sh: '[ "$BUILD_TARGET" != macos ] || nvm use node'
|
||||||
- sh: '[ "$BUILD_TARGET" != macos ] || npm install --global create-dmg'
|
- sh: '[ "$BUILD_TARGET" != macos ] || npm install --global create-dmg'
|
||||||
- sh: '[ "$BUILD_TARGET" != macos ] || brew install qt'
|
|
||||||
- sh: '[ "$BUILD_TARGET" != macos ] || brew link --force qt'
|
|
||||||
- sh: '[ "$BUILD_TARGET" != steamlink ] || sudo apt install -y libc6:i386 libstdc++6:i386'
|
- sh: '[ "$BUILD_TARGET" != steamlink ] || sudo apt install -y libc6:i386 libstdc++6:i386'
|
||||||
- sh: '[ "$BUILD_TARGET" != steamlink ] || git clone --depth=1 https://github.com/ValveSoftware/steamlink-sdk.git $HOME/steamlink-sdk'
|
- sh: '[ "$BUILD_TARGET" != steamlink ] || git clone --depth=1 https://github.com/ValveSoftware/steamlink-sdk.git $HOME/steamlink-sdk'
|
||||||
- sh: '[ "$BUILD_TARGET" != linux ] || sudo add-apt-repository ppa:beineri/opt-qt-5.14.2-xenial'
|
- sh: '[ "$BUILD_TARGET" != linux ] || sudo add-apt-repository ppa:beineri/opt-qt-5.15.2-bionic'
|
||||||
- sh: '[ "$BUILD_TARGET" != linux ] || sudo apt update'
|
- sh: '[ "$BUILD_TARGET" != linux ] || sudo apt update || true'
|
||||||
- sh: '[ "$BUILD_TARGET" != linux ] || sudo apt install -y qt514base qt514quickcontrols2 qt514svg qt514wayland nasm libgbm-dev libdrm-dev libfreetype6-dev libasound2-dev libdbus-1-dev libegl1-mesa-dev libgl1-mesa-dev libgles2-mesa-dev libglu1-mesa-dev libibus-1.0-dev libpulse-dev libudev-dev libx11-dev libxcursor-dev libxext-dev libxi-dev libxinerama-dev libxkbcommon-dev libxrandr-dev libxss-dev libxt-dev libxv-dev libxxf86vm-dev wayland-protocols libopus-dev libvdpau-dev'
|
- sh: '[ "$BUILD_TARGET" != linux ] || sudo apt install -y qt515base qt515quickcontrols2 qt515svg qt515wayland nasm libgbm-dev libdrm-dev libfreetype6-dev libasound2-dev libdbus-1-dev libegl1-mesa-dev libgl1-mesa-dev libgles2-mesa-dev libglu1-mesa-dev libibus-1.0-dev libpulse-dev libudev-dev libx11-dev libxcursor-dev libxext-dev libxi-dev libxinerama-dev libxkbcommon-dev libxrandr-dev libxss-dev libxt-dev libxv-dev libxxf86vm-dev wayland-protocols libopus-dev libvdpau-dev'
|
||||||
- sh: 'if [[ "$BUILD_TARGET" = linux ]]; then export SDL2_VER=2.0.14 && wget https://www.libsdl.org/release/SDL2-$SDL2_VER.tar.gz && tar -xvf SDL2-$SDL2_VER.tar.gz && cd SDL2-$SDL2_VER && ./configure && make -j$(nproc) && sudo make install && cd ..; fi'
|
- sh: 'if [[ "$BUILD_TARGET" = linux ]]; then export SDL2_REV=dfa64eadd38a6a8125290dcf121f1bc285e98dad && git clone https://github.com/libsdl-org/SDL.git SDL2 && cd SDL2 && git checkout $SDL2_REV && ./configure --enable-video-kmsdrm && make -j$(nproc) && sudo make install && cd ..; fi'
|
||||||
- sh: 'if [[ "$BUILD_TARGET" = linux ]]; then export SDL2_TTF_VER=2.0.15 && wget https://www.libsdl.org/projects/SDL_ttf/release/SDL2_ttf-$SDL2_TTF_VER.tar.gz && tar -xvf SDL2_ttf-$SDL2_TTF_VER.tar.gz && cd SDL2_ttf-$SDL2_TTF_VER && ./configure && make -j$(nproc) && sudo make install && cd ..; fi'
|
- sh: 'if [[ "$BUILD_TARGET" = linux ]]; then export SDL2_TTF_VER=2.0.15 && wget https://www.libsdl.org/projects/SDL_ttf/release/SDL2_ttf-$SDL2_TTF_VER.tar.gz && tar -xvf SDL2_ttf-$SDL2_TTF_VER.tar.gz && cd SDL2_ttf-$SDL2_TTF_VER && ./configure && make -j$(nproc) && sudo make install && cd ..; fi'
|
||||||
- sh: 'if [[ "$BUILD_TARGET" = linux ]]; then export LIBVA_VER=2.10.0 && wget https://github.com/intel/libva/releases/download/$LIBVA_VER/libva-$LIBVA_VER.tar.bz2 && tar -xvf libva-$LIBVA_VER.tar.bz2 && cd libva-$LIBVA_VER && ./configure && make -j$(nproc) && sudo make install && cd ..; fi'
|
- sh: 'if [[ "$BUILD_TARGET" = linux ]]; then export LIBVA_VER=2.12.0 && git clone https://github.com/intel/libva.git && cd libva && git checkout $LIBVA_VER && ./autogen.sh && ./configure && make -j$(nproc) && sudo make install && cd ..; fi'
|
||||||
- sh: 'if [[ "$BUILD_TARGET" = linux ]]; then export NVHDR_VER=8.1.24.12 && wget https://github.com/FFmpeg/nv-codec-headers/releases/download/n$NVHDR_VER/nv-codec-headers-$NVHDR_VER.tar.gz && tar -xvf nv-codec-headers-$NVHDR_VER.tar.gz && cd nv-codec-headers-$NVHDR_VER && sudo make install && cd ..; fi'
|
- sh: 'if [[ "$BUILD_TARGET" = linux ]]; then export NVHDR_VER=8.1.24.12 && wget https://github.com/FFmpeg/nv-codec-headers/releases/download/n$NVHDR_VER/nv-codec-headers-$NVHDR_VER.tar.gz && tar -xvf nv-codec-headers-$NVHDR_VER.tar.gz && cd nv-codec-headers-$NVHDR_VER && sudo make install && cd ..; fi'
|
||||||
- sh: 'if [[ "$BUILD_TARGET" = linux ]]; then export FFMPEG_VER=4.3.1 && wget https://ffmpeg.org/releases/ffmpeg-$FFMPEG_VER.tar.bz2 && tar -xvf ffmpeg-$FFMPEG_VER.tar.bz2 && cd ffmpeg-$FFMPEG_VER && ./configure $FFMPEG_CONFIGURE_ARGS && make -j$(nproc) && sudo make install && sudo ldconfig && cd ..; fi'
|
- sh: 'if [[ "$BUILD_TARGET" = linux ]]; then export FFMPEG_VER=4.3.1 && wget https://ffmpeg.org/releases/ffmpeg-$FFMPEG_VER.tar.bz2 && tar -xvf ffmpeg-$FFMPEG_VER.tar.bz2 && cd ffmpeg-$FFMPEG_VER && ./configure $FFMPEG_CONFIGURE_ARGS && make -j$(nproc) && sudo make install && sudo ldconfig && cd ..; fi'
|
||||||
- sh: 'if [[ "$BUILD_TARGET" = linux ]]; then mkdir $HOME/bin && wget -O $HOME/bin/linuxdeployqt https://github.com/probonopd/linuxdeployqt/releases/download/continuous/linuxdeployqt-continuous-x86_64.AppImage && chmod a+x $HOME/bin/linuxdeployqt; fi'
|
- sh: 'if [[ "$BUILD_TARGET" = linux ]]; then mkdir $HOME/bin && wget -O $HOME/bin/linuxdeployqt https://github.com/probonopd/linuxdeployqt/releases/download/continuous/linuxdeployqt-continuous-x86_64.AppImage && chmod a+x $HOME/bin/linuxdeployqt; fi'
|
||||||
|
|
||||||
before_build:
|
before_build:
|
||||||
- 'git submodule update --init --recursive'
|
- 'git -c submodule.libs.update=none submodule update --init --recursive'
|
||||||
|
- cmd: 'git submodule update --init --recursive'
|
||||||
|
- sh: '[ "$BUILD_TARGET" != macos ] || git submodule update --init --recursive'
|
||||||
- cmd: 'set OLDPATH=%PATH%'
|
- cmd: 'set OLDPATH=%PATH%'
|
||||||
- cmd: 'set /p VERSION=<app\version.txt'
|
- cmd: 'set /p VERSION=<app\version.txt'
|
||||||
- sh: 'export VERSION=`cat app/version.txt`'
|
- sh: 'export VERSION=`cat app/version.txt`'
|
||||||
@@ -43,9 +43,9 @@ build_script:
|
|||||||
- cmd: 'set PATH=%OLDPATH%;%QTDIR%\msvc2019\bin;%ProgramFiles(x86)%\WiX Toolset v3.11\bin'
|
- cmd: 'set PATH=%OLDPATH%;%QTDIR%\msvc2019\bin;%ProgramFiles(x86)%\WiX Toolset v3.11\bin'
|
||||||
- cmd: 'scripts\build-arch.bat Release x86'
|
- cmd: 'scripts\build-arch.bat Release x86'
|
||||||
- cmd: 'scripts\generate-bundle.bat Release'
|
- cmd: 'scripts\generate-bundle.bat Release'
|
||||||
- sh: '[ "$BUILD_TARGET" != linux ] || source /opt/qt514/bin/qt514-env.sh'
|
- sh: '[ "$BUILD_TARGET" != linux ] || source /opt/qt515/bin/qt515-env.sh'
|
||||||
- sh: '[ "$BUILD_TARGET" != linux ] || PATH=$PATH:$HOME/bin scripts/build-appimage.sh'
|
- sh: '[ "$BUILD_TARGET" != linux ] || PATH=$PATH:$HOME/bin scripts/build-appimage.sh'
|
||||||
- sh: '[ "$BUILD_TARGET" != macos ] || scripts/generate-dmg.sh Release'
|
- sh: '[ "$BUILD_TARGET" != macos ] || PATH=$PATH:$HOME/Qt/5.15/clang_64/bin scripts/generate-dmg.sh Release'
|
||||||
- sh: '[ "$BUILD_TARGET" != steamlink ] || STEAMLINK_SDK_PATH=$HOME/steamlink-sdk scripts/build-steamlink-app.sh'
|
- sh: '[ "$BUILD_TARGET" != steamlink ] || STEAMLINK_SDK_PATH=$HOME/steamlink-sdk scripts/build-steamlink-app.sh'
|
||||||
|
|
||||||
after_build:
|
after_build:
|
||||||
|
|||||||
Submodule
+1
Submodule libs added at c5149747ff.
Loaded 100 of 811 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user