mirror of
https://github.com/moonlight-stream/moonlight-qt.git
synced 2025-10-26 11:19:26 +00:00
Compare commits
154 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d544c1ce4 | ||
|
|
ac7696ea8f | ||
|
|
4d303cebee | ||
|
|
bd6235efba | ||
|
|
b1232e0ed4 | ||
|
|
c35f7086a0 | ||
|
|
c7bc76325f | ||
|
|
ccaca68570 | ||
|
|
57db20016a | ||
|
|
bdb6d02dac | ||
|
|
9bcc6291be | ||
|
|
4ec549650d | ||
|
|
14027f3c74 | ||
|
|
fb9a164111 | ||
|
|
200cab9d17 | ||
|
|
c52a57f0ec | ||
|
|
b4dc7ca7cb | ||
|
|
00c1dd0d0d | ||
|
|
e6e91ca88b | ||
|
|
260a0e0ae2 | ||
|
|
3f8f4744c5 | ||
|
|
4bbd02fb2d | ||
|
|
c5ca672865 | ||
|
|
4688f3650c | ||
|
|
2550f416f4 | ||
|
|
579ad25a01 | ||
|
|
1144dbccb3 | ||
|
|
ff81f74391 | ||
|
|
c0d38ee78f | ||
|
|
c9cb64f90b | ||
|
|
93e597a93f | ||
|
|
82b33c033a | ||
|
|
be266d3349 | ||
|
|
c618a0b5df | ||
|
|
a20d429bc1 | ||
|
|
2b3e0803de | ||
|
|
4189903233 | ||
|
|
06b5c4631f | ||
|
|
997c4aa0ae | ||
|
|
f4343c5f29 | ||
|
|
3cdea6b039 | ||
|
|
ac2e10c712 | ||
|
|
61fa45ad21 | ||
|
|
f362e76127 | ||
|
|
749c69fc76 | ||
|
|
5dca30def1 | ||
|
|
502783a86b | ||
|
|
45989fdd6e | ||
|
|
490aa5082f | ||
|
|
ae1c65805c | ||
|
|
9cb4105aec | ||
|
|
1fd545ae1f | ||
|
|
3194cb09d8 | ||
|
|
f9bb45579b | ||
|
|
f7b2edc8e3 | ||
|
|
c9a3946d80 | ||
|
|
0c8354336b | ||
|
|
1bf86f52d3 | ||
|
|
504d42865d | ||
|
|
65c04fd560 | ||
|
|
11dc244857 | ||
|
|
b9cab4cac5 | ||
|
|
65647a32f4 | ||
|
|
777502b495 | ||
|
|
ce4309694f | ||
|
|
ccc64a5629 | ||
|
|
25f6c020b6 | ||
|
|
6a95742a92 | ||
|
|
1dbdcb5279 | ||
|
|
9ce0537587 | ||
|
|
76d03f015d | ||
|
|
edbc24fa3e | ||
|
|
d4fd50b973 | ||
|
|
d5ab795b36 | ||
|
|
c5fc5220f3 | ||
|
|
28dea31533 | ||
|
|
8ce6a9585f | ||
|
|
47452371db | ||
|
|
4001b05fca | ||
|
|
75e917622e | ||
|
|
75359bb1c4 | ||
|
|
c93f7e7385 | ||
|
|
48bcd725c9 | ||
|
|
e807a52cfa | ||
|
|
9c9bfd8428 | ||
|
|
29b1304337 | ||
|
|
cc0b574bb1 | ||
|
|
7a769172a1 | ||
|
|
d9c7a245ef | ||
|
|
469a5c78f1 | ||
|
|
fabb4fdadc | ||
|
|
9491884cb4 | ||
|
|
bf51577787 | ||
|
|
4c5bcee8dc | ||
|
|
fd70865026 | ||
|
|
351aaa6759 | ||
|
|
59bc625cc7 | ||
|
|
b2f765e8ef | ||
|
|
bdd9a3a994 | ||
|
|
2ecafabcab | ||
|
|
dd2a99a96b | ||
|
|
9ff2ac0974 | ||
|
|
5760d08c33 | ||
|
|
edd7a134d8 | ||
|
|
ffa87c5f01 | ||
|
|
ff7e61c6d9 | ||
|
|
6ad96fba42 | ||
|
|
85856114b2 | ||
|
|
7decfae792 | ||
|
|
b6008b15dc | ||
|
|
260ec3d80d | ||
|
|
7ec2e50334 | ||
|
|
e532b9167a | ||
|
|
52d5890372 | ||
|
|
f34d11994f | ||
|
|
359c92340d | ||
|
|
15e337fff8 | ||
|
|
707dd3cb83 | ||
|
|
98f6a09991 | ||
|
|
f1d0e97681 | ||
|
|
103f988dbf | ||
|
|
2257cb0cef | ||
|
|
b6a3369243 | ||
|
|
4af9623727 | ||
|
|
515db03fe5 | ||
|
|
e44d097683 | ||
|
|
208d048358 | ||
|
|
9936085aee | ||
|
|
3279d9c3f6 | ||
|
|
e571d5833c | ||
|
|
3531fe0a4f | ||
|
|
286676e5c1 | ||
|
|
6ce02616f0 | ||
|
|
13880353d8 | ||
|
|
ec69dad8d7 | ||
|
|
72ae324d71 | ||
|
|
901cbd255c | ||
|
|
2a63ad53d7 | ||
|
|
9b3d4c1ad7 | ||
|
|
054e334066 | ||
|
|
6d023c2dfa | ||
|
|
0e2d5bf441 | ||
|
|
023b6b2772 | ||
|
|
6f39d120cb | ||
|
|
9cf305865b | ||
|
|
6d47287b60 | ||
|
|
6b11f43302 | ||
|
|
60fb6881b2 | ||
|
|
b79c116bd9 | ||
|
|
895d0a6bf3 | ||
|
|
5a1ef55767 | ||
|
|
76deafbd7b | ||
|
|
ae2693a860 | ||
|
|
0783b28ba6 |
9
.github/dependabot.yml
vendored
Normal file
9
.github/dependabot.yml
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "gitsubmodule"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
3
.gitmodules
vendored
3
.gitmodules
vendored
@ -7,9 +7,6 @@
|
||||
[submodule "app/SDL_GameControllerDB"]
|
||||
path = app/SDL_GameControllerDB
|
||||
url = https://github.com/gabomdq/SDL_GameControllerDB.git
|
||||
[submodule "soundio/libsoundio"]
|
||||
path = soundio/libsoundio
|
||||
url = https://github.com/cgutman/libsoundio.git
|
||||
[submodule "h264bitstream/h264bitstream"]
|
||||
path = h264bitstream/h264bitstream
|
||||
url = https://github.com/aizvorski/h264bitstream.git
|
||||
|
||||
@ -207,6 +207,28 @@ private:
|
||||
// Since the RTSS OSD doesn't even work with DXVA2, we'll just block the hooks entirely.
|
||||
L"RTSSHooks.dll",
|
||||
L"RTSSHooks64.dll",
|
||||
|
||||
// Bandicam's Vulkan layer DLL crashes during destruction of the Vulkan swapchain
|
||||
// bdcamvk64+0x10b73:
|
||||
// 00007ffa`be3b0b73 498b5630 mov rdx,qword ptr [r14+30h] ds:00000000`00000030=????????????????
|
||||
//
|
||||
// bdcamvk64+0x10b73
|
||||
// libplacebo_357!vk_sw_destroy+0x14f [D:\a\moonlight-deps\moonlight-deps\libplacebo\src\vulkan\swapchain.c @ 460]
|
||||
// libplacebo_357!pl_swapchain_destroy+0x1a [D:\a\moonlight-deps\moonlight-deps\libplacebo\src\swapchain.c @ 30]
|
||||
// Moonlight!PlVkRenderer::~PlVkRenderer+0x159 [D:\a\moonlight-qt\app\streaming\video\ffmpeg-renderers\plvk.cpp @ 140]
|
||||
// Moonlight!PlVkRenderer::`scalar deleting destructor'+0x17
|
||||
// Moonlight!FFmpegVideoDecoder::reset+0x203 [D:\a\moonlight-qt\app\streaming\video\ffmpeg.cpp @ 301]
|
||||
// Moonlight!FFmpegVideoDecoder::~FFmpegVideoDecoder+0x22 [D:\a\moonlight-qt\app\streaming\video\ffmpeg.cpp @ 257]
|
||||
// Moonlight!FFmpegVideoDecoder::`scalar deleting destructor'+0x17
|
||||
// Moonlight!Session::getDecoderInfo+0x19a [D:\a\moonlight-qt\app\streaming\session.cpp @ 415]
|
||||
// Moonlight!SystemProperties::querySdlVideoInfoInternal+0x134 [D:\a\moonlight-qt\app\backend\systemproperties.cpp @ 161]
|
||||
// Moonlight!SystemProperties::querySdlVideoInfo+0x97 [D:\a\moonlight-qt\app\backend\systemproperties.cpp @ 123]
|
||||
// Moonlight!SystemProperties::SystemProperties+0x4bf [D:\a\moonlight-qt\app\backend\systemproperties.cpp @ 75]
|
||||
//
|
||||
// https://github.com/moonlight-stream/moonlight-qt/issues/1425
|
||||
// https://github.com/moonlight-stream/moonlight-qt/issues/1579
|
||||
L"bdcamvk32.dll",
|
||||
L"bdcamvk64.dll",
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
18
README.md
18
README.md
@ -13,6 +13,7 @@ You can follow development on our [Discord server](https://moonlight-stream.org/
|
||||
## Features
|
||||
- Hardware accelerated video decoding on Windows, Mac, and Linux
|
||||
- H.264, HEVC, and AV1 codec support (AV1 requires Sunshine and a supported host GPU)
|
||||
- YUV 4:4:4 support (Sunshine only)
|
||||
- HDR streaming support
|
||||
- 7.1 surround sound audio support
|
||||
- 10-point multitouch support (Sunshine only)
|
||||
@ -39,7 +40,7 @@ Hosting for Moonlight's Debian and L4T package repositories is graciously provid
|
||||
## Building
|
||||
|
||||
### Windows Build Requirements
|
||||
* Qt 5.15 SDK or later. Qt 6 is also supported for x64 and ARM64 builds.
|
||||
* Qt 6.7 SDK or later (earlier versions may work but are not officially supported)
|
||||
* [Visual Studio 2022](https://visualstudio.microsoft.com/downloads/) (Community edition is fine)
|
||||
* Select **MSVC** option during Qt installation. MinGW is not supported.
|
||||
* [7-Zip](https://www.7-zip.org/) (only if building installers for non-development PCs)
|
||||
@ -48,12 +49,12 @@ Hosting for Moonlight's Debian and L4T package repositories is graciously provid
|
||||
* Alternatively, run `dism /online /add-capability /capabilityname:Tools.Graphics.DirectX~~~~0.0.1.0` and reboot.
|
||||
|
||||
### macOS Build Requirements
|
||||
* Qt 6.4 SDK or later
|
||||
* Xcode 13 or later
|
||||
* Qt 6.7 SDK or later (earlier versions may work but are not officially supported)
|
||||
* Xcode 14 or later (earlier versions may work but are not officially supported)
|
||||
* [create-dmg](https://github.com/sindresorhus/create-dmg) (only if building DMGs for use on non-development Macs)
|
||||
|
||||
### Linux/Unix Build Requirements
|
||||
* Qt 6 is recommended, but Qt 5.9 or later is also supported (replace `qmake6` with `qmake` when using Qt 5).
|
||||
* Qt 6 is recommended, but Qt 5.12 or later is also supported (replace `qmake6` with `qmake` when using Qt 5).
|
||||
* GCC or Clang
|
||||
* FFmpeg 4.0 or later
|
||||
* Install the required packages:
|
||||
@ -71,10 +72,17 @@ Hosting for Moonlight's Debian and L4T package repositories is graciously provid
|
||||
* [Steam Link SDK](https://github.com/ValveSoftware/steamlink-sdk) cloned on your build system
|
||||
* STEAMLINK_SDK_PATH environment variable set to the Steam Link SDK path
|
||||
|
||||
**Steam Link Hardware Limitations**
|
||||
Moonlight builds for Steam Link are subject to hardware limitations of the Steam Link device:
|
||||
* Maximum resolution: **1080p (1920x1080)**
|
||||
* Maximum framerate: **60 FPS**
|
||||
* Maximum video bitrate: **40 Mbps**
|
||||
* **HDR streaming is not supported** on the original hardware
|
||||
|
||||
### Build Setup Steps
|
||||
1. Install the latest Qt SDK (and optionally, the Qt Creator IDE) from https://www.qt.io/download
|
||||
* You can install Qt via Homebrew on macOS, but you will need to use `brew install qt --with-debug` to be able to create debug builds of Moonlight.
|
||||
* You may also use your Linux distro's package manager for the Qt SDK as long as the packages are Qt 5.9 or later.
|
||||
* You may also use your Linux distro's package manager for the Qt SDK as long as the packages are Qt 5.12 or later.
|
||||
* This step is not required for building on Steam Link, because the Steam Link SDK includes Qt 5.14.
|
||||
2. Run `git submodule update --init --recursive` from within `moonlight-qt/`
|
||||
3. Open the project in Qt Creator or build from qmake on the command line.
|
||||
|
||||
@ -2,8 +2,12 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>GCSupportsGameMode</key>
|
||||
<true/>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>Moonlight</string>
|
||||
<key>LSSupportsGameMode</key>
|
||||
<true/>
|
||||
<key>CFBundleGetInfoString</key>
|
||||
<string>Stream games and other applications from another PC</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
@ -44,5 +48,7 @@
|
||||
<string>VERSION</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Moonlight</string>
|
||||
<key>LSApplicationCategoryType</key>
|
||||
<string>public.app-category.games</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@ -1 +1 @@
|
||||
Subproject commit e5a5fa2ac6e645d72c619ea99520a3a4586ee005
|
||||
Subproject commit 38fc811c715365e963a6942092cae147eddddc90
|
||||
67
app/SDL_compat.h
Normal file
67
app/SDL_compat.h
Normal file
@ -0,0 +1,67 @@
|
||||
//
|
||||
// Compatibility header for older version of SDL.
|
||||
// Include this instead of SDL.h directly.
|
||||
//
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <SDL.h>
|
||||
|
||||
// SDL_FRect wasn't added until 2.0.10
|
||||
#if !SDL_VERSION_ATLEAST(2, 0, 10)
|
||||
typedef struct SDL_FRect
|
||||
{
|
||||
float x;
|
||||
float y;
|
||||
float w;
|
||||
float h;
|
||||
} SDL_FRect;
|
||||
#endif
|
||||
|
||||
#ifndef SDL_HINT_VIDEO_X11_FORCE_EGL
|
||||
#define SDL_HINT_VIDEO_X11_FORCE_EGL "SDL_VIDEO_X11_FORCE_EGL"
|
||||
#endif
|
||||
|
||||
#ifndef SDL_HINT_KMSDRM_REQUIRE_DRM_MASTER
|
||||
#define SDL_HINT_KMSDRM_REQUIRE_DRM_MASTER "SDL_KMSDRM_REQUIRE_DRM_MASTER"
|
||||
#endif
|
||||
|
||||
#ifndef SDL_HINT_ALLOW_ALT_TAB_WHILE_GRABBED
|
||||
#define SDL_HINT_ALLOW_ALT_TAB_WHILE_GRABBED "SDL_ALLOW_ALT_TAB_WHILE_GRABBED"
|
||||
#endif
|
||||
|
||||
#ifndef SDL_HINT_JOYSTICK_HIDAPI_PS4_RUMBLE
|
||||
#define SDL_HINT_JOYSTICK_HIDAPI_PS4_RUMBLE "SDL_JOYSTICK_HIDAPI_PS4_RUMBLE"
|
||||
#endif
|
||||
|
||||
#ifndef SDL_HINT_JOYSTICK_HIDAPI_PS5_RUMBLE
|
||||
#define SDL_HINT_JOYSTICK_HIDAPI_PS5_RUMBLE "SDL_JOYSTICK_HIDAPI_PS5_RUMBLE"
|
||||
#endif
|
||||
|
||||
#ifndef SDL_HINT_WINDOWS_USE_D3D9EX
|
||||
#define SDL_HINT_WINDOWS_USE_D3D9EX "SDL_WINDOWS_USE_D3D9EX"
|
||||
#endif
|
||||
|
||||
#ifndef SDL_HINT_GAMECONTROLLER_USE_BUTTON_LABELS
|
||||
#define SDL_HINT_GAMECONTROLLER_USE_BUTTON_LABELS "SDL_GAMECONTROLLER_USE_BUTTON_LABELS"
|
||||
#endif
|
||||
|
||||
#ifndef SDL_HINT_MOUSE_RELATIVE_SCALING
|
||||
#define SDL_HINT_MOUSE_RELATIVE_SCALING "SDL_MOUSE_RELATIVE_SCALING"
|
||||
#endif
|
||||
|
||||
#ifndef SDL_HINT_AUDIO_DEVICE_APP_NAME
|
||||
#define SDL_HINT_AUDIO_DEVICE_APP_NAME "SDL_AUDIO_DEVICE_APP_NAME"
|
||||
#endif
|
||||
|
||||
#ifndef SDL_HINT_APP_NAME
|
||||
#define SDL_HINT_APP_NAME "SDL_APP_NAME"
|
||||
#endif
|
||||
|
||||
#ifndef SDL_HINT_MOUSE_AUTO_CAPTURE
|
||||
#define SDL_HINT_MOUSE_AUTO_CAPTURE "SDL_MOUSE_AUTO_CAPTURE"
|
||||
#endif
|
||||
|
||||
#ifndef SDL_HINT_VIDEO_WAYLAND_EMULATE_MOUSE_WARP
|
||||
#define SDL_HINT_VIDEO_WAYLAND_EMULATE_MOUSE_WARP "SDL_VIDEO_WAYLAND_EMULATE_MOUSE_WARP"
|
||||
#endif
|
||||
58
app/app.pro
58
app/app.pro
@ -69,7 +69,12 @@ macx:!disable-prebuilts {
|
||||
|
||||
unix:if(!macx|disable-prebuilts) {
|
||||
CONFIG += link_pkgconfig
|
||||
PKGCONFIG += openssl sdl2 SDL2_ttf opus
|
||||
PKGCONFIG += openssl sdl2 SDL2_ttf
|
||||
|
||||
# We have our own optimized libopus.a for Steam Link
|
||||
if(!config_SL|disable-prebuilts) {
|
||||
PKGCONFIG += opus
|
||||
}
|
||||
|
||||
!disable-ffmpeg {
|
||||
packagesExist(libavcodec) {
|
||||
@ -117,7 +122,9 @@ unix:if(!macx|disable-prebuilts) {
|
||||
}
|
||||
}
|
||||
|
||||
!disable-cuda {
|
||||
# Disabled by default due to reliability issues. See #1314.
|
||||
# CUDA interop is superseded by VDPAU and Vulkan Video.
|
||||
enable-cuda {
|
||||
packagesExist(ffnvcodec) {
|
||||
PKGCONFIG += ffnvcodec
|
||||
CONFIG += cuda
|
||||
@ -152,20 +159,16 @@ win32 {
|
||||
CONFIG += ffmpeg libplacebo
|
||||
}
|
||||
win32:!winrt {
|
||||
CONFIG += soundio discord-rpc
|
||||
CONFIG += discord-rpc
|
||||
}
|
||||
macx {
|
||||
!disable-prebuilts {
|
||||
LIBS += -lssl.3 -lcrypto.3 -lavcodec.61 -lavutil.59 -lswscale.8 -lopus -framework SDL2 -framework SDL2_ttf
|
||||
LIBS += -lssl.3 -lcrypto.3 -lavcodec.62 -lavutil.60 -lswscale.9 -lopus -framework SDL2 -framework SDL2_ttf
|
||||
CONFIG += discord-rpc
|
||||
}
|
||||
|
||||
LIBS += -lobjc -framework VideoToolbox -framework AVFoundation -framework CoreVideo -framework CoreGraphics -framework CoreMedia -framework AppKit -framework Metal -framework QuartzCore
|
||||
|
||||
# For libsoundio
|
||||
LIBS += -framework CoreAudio -framework AudioUnit
|
||||
|
||||
CONFIG += ffmpeg soundio
|
||||
CONFIG += ffmpeg
|
||||
}
|
||||
|
||||
SOURCES += \
|
||||
@ -209,6 +212,7 @@ SOURCES += \
|
||||
wm.cpp
|
||||
|
||||
HEADERS += \
|
||||
SDL_compat.h \
|
||||
backend/nvaddress.h \
|
||||
backend/nvapp.h \
|
||||
cli/pair.h \
|
||||
@ -323,9 +327,11 @@ libdrm {
|
||||
HEADERS += streaming/video/ffmpeg-renderers/drm.h
|
||||
|
||||
linux {
|
||||
message(Master hooks enabled)
|
||||
SOURCES += masterhook.c masterhook_internal.c
|
||||
LIBS += -ldl
|
||||
!disable-masterhooks {
|
||||
message(Master hooks enabled)
|
||||
SOURCES += masterhook.c masterhook_internal.c
|
||||
LIBS += -ldl -pthread
|
||||
}
|
||||
}
|
||||
}
|
||||
cuda {
|
||||
@ -364,6 +370,13 @@ config_EGL {
|
||||
config_SL {
|
||||
message(Steam Link build configuration selected)
|
||||
|
||||
!disable-prebuilts {
|
||||
# Link against our NEON-optimized libopus build
|
||||
LIBS += -L$$PWD/../libs/steamlink/lib
|
||||
INCLUDEPATH += $$PWD/../libs/steamlink/include
|
||||
LIBS += -lopus -larmasm -lNE10
|
||||
}
|
||||
|
||||
DEFINES += EMBEDDED_BUILD STEAM_LINK HAVE_SLVIDEO HAVE_SLAUDIO
|
||||
LIBS += -lSLVideo -lSLAudio
|
||||
|
||||
@ -401,13 +414,6 @@ macx {
|
||||
HEADERS += \
|
||||
streaming/video/ffmpeg-renderers/vt.h
|
||||
}
|
||||
soundio {
|
||||
message(libsoundio audio renderer selected)
|
||||
|
||||
DEFINES += HAVE_SOUNDIO SOUNDIO_STATIC_LIBRARY
|
||||
SOURCES += streaming/audio/renderers/soundioaudiorenderer.cpp
|
||||
HEADERS += streaming/audio/renderers/soundioaudiorenderer.h
|
||||
}
|
||||
discord-rpc {
|
||||
message(Discord integration enabled)
|
||||
|
||||
@ -473,7 +479,10 @@ TRANSLATIONS += \
|
||||
languages/qml_he.ts \
|
||||
languages/qml_ckb.ts \
|
||||
languages/qml_lt.ts \
|
||||
languages/qml_et.ts
|
||||
languages/qml_et.ts \
|
||||
languages/qml_bg.ts \
|
||||
languages/qml_eo.ts \
|
||||
languages/qml_ta.ts
|
||||
|
||||
# Additional import path used to resolve QML modules in Qt Creator's code model
|
||||
QML_IMPORT_PATH =
|
||||
@ -495,15 +504,6 @@ else:unix: LIBS += -L$$OUT_PWD/../qmdnsengine/ -lqmdnsengine
|
||||
INCLUDEPATH += $$PWD/../qmdnsengine/qmdnsengine/src/include $$PWD/../qmdnsengine
|
||||
DEPENDPATH += $$PWD/../qmdnsengine/qmdnsengine/src/include $$PWD/../qmdnsengine
|
||||
|
||||
soundio {
|
||||
win32:CONFIG(release, debug|release): LIBS += -L$$OUT_PWD/../soundio/release/ -lsoundio
|
||||
else:win32:CONFIG(debug, debug|release): LIBS += -L$$OUT_PWD/../soundio/debug/ -lsoundio
|
||||
else:unix: LIBS += -L$$OUT_PWD/../soundio/ -lsoundio
|
||||
|
||||
INCLUDEPATH += $$PWD/../soundio/libsoundio
|
||||
DEPENDPATH += $$PWD/../soundio/libsoundio
|
||||
}
|
||||
|
||||
win32:CONFIG(release, debug|release): LIBS += -L$$OUT_PWD/../h264bitstream/release/ -lh264bitstream
|
||||
else:win32:CONFIG(debug, debug|release): LIBS += -L$$OUT_PWD/../h264bitstream/debug/ -lh264bitstream
|
||||
else:unix: LIBS += -L$$OUT_PWD/../h264bitstream/ -lh264bitstream
|
||||
|
||||
@ -9,8 +9,7 @@
|
||||
#include <QThread>
|
||||
#include <QThreadPool>
|
||||
#include <QCoreApplication>
|
||||
|
||||
#include <random>
|
||||
#include <QRandomGenerator>
|
||||
|
||||
#define SER_HOSTS "hosts"
|
||||
#define SER_HOSTS_BACKUP "hostsbackup"
|
||||
@ -30,9 +29,9 @@ public:
|
||||
}
|
||||
|
||||
private:
|
||||
bool tryPollComputer(NvAddress address, bool& changed)
|
||||
bool tryPollComputer(QNetworkAccessManager* nam, NvAddress address, bool& changed)
|
||||
{
|
||||
NvHTTP http(address, 0, m_Computer->serverCert);
|
||||
NvHTTP http(address, 0, m_Computer->serverCert, nam);
|
||||
|
||||
QString serverInfo;
|
||||
try {
|
||||
@ -53,9 +52,9 @@ private:
|
||||
return true;
|
||||
}
|
||||
|
||||
bool updateAppList(bool& changed)
|
||||
bool updateAppList(QNetworkAccessManager* nam, bool& changed)
|
||||
{
|
||||
NvHTTP http(m_Computer);
|
||||
NvHTTP http(m_Computer, nam);
|
||||
|
||||
QVector<NvApp> appList;
|
||||
|
||||
@ -75,6 +74,21 @@ private:
|
||||
|
||||
void run() override
|
||||
{
|
||||
// Reduce the power and performance impact of our
|
||||
// computer status polling while it's running.
|
||||
setPriority(QThread::LowPriority);
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 9, 0)
|
||||
setServiceLevel(QThread::QualityOfService::Eco);
|
||||
#endif
|
||||
|
||||
// Share the QNetworkAccessManager to conserve resources when polling.
|
||||
// Each instance creates a worker thread, so sharing them ensures that
|
||||
// we are not spamming a new thread for every single polling attempt.
|
||||
//
|
||||
// Since QThread inherit the priority of the current thread, this also
|
||||
// ensures that the NAM's worker thread will inherit our lower priority.
|
||||
QNetworkAccessManager nam;
|
||||
|
||||
// Always fetch the applist the first time
|
||||
int pollsSinceLastAppListFetch = POLLS_PER_APPLIST_FETCH;
|
||||
while (!isInterruptionRequested()) {
|
||||
@ -87,7 +101,7 @@ private:
|
||||
return;
|
||||
}
|
||||
|
||||
if (tryPollComputer(address, stateChanged)) {
|
||||
if (tryPollComputer(&nam, address, stateChanged)) {
|
||||
if (!wasOnline) {
|
||||
qInfo() << m_Computer->name << "is now online at" << m_Computer->activeAddress.toString();
|
||||
}
|
||||
@ -118,7 +132,7 @@ private:
|
||||
stateChanged = false;
|
||||
}
|
||||
|
||||
if (updateAppList(stateChanged)) {
|
||||
if (updateAppList(&nam, stateChanged)) {
|
||||
pollsSinceLastAppListFetch = 0;
|
||||
}
|
||||
}
|
||||
@ -715,6 +729,10 @@ void ComputerManager::addNewHostManually(QString address)
|
||||
// If there wasn't a port specified, use the default
|
||||
addNewHost(NvAddress(url.host(), url.port(DEFAULT_HTTP_PORT)), false);
|
||||
}
|
||||
else if (QHostAddress(address).protocol() == QAbstractSocket::IPv6Protocol) {
|
||||
// The user specified an IPv6 literal without URL escaping, so use the default port
|
||||
addNewHost(NvAddress(address, DEFAULT_HTTP_PORT), false);
|
||||
}
|
||||
else {
|
||||
emit computerAddCompleted(false, false);
|
||||
}
|
||||
@ -966,14 +984,9 @@ void ComputerManager::addNewHost(NvAddress address, bool mdns, NvAddress mdnsIpv
|
||||
QThreadPool::globalInstance()->start(addTask);
|
||||
}
|
||||
|
||||
// TODO: Use QRandomGenerator when we drop Qt 5.9 support
|
||||
QString ComputerManager::generatePinString()
|
||||
{
|
||||
std::uniform_int_distribution<int> dist(0, 9999);
|
||||
std::random_device rd;
|
||||
std::mt19937 engine(rd());
|
||||
|
||||
return QString::asprintf("%04u", dist(engine));
|
||||
return QString::asprintf("%04u", QRandomGenerator::system()->bounded(10000));
|
||||
}
|
||||
|
||||
#include "computermanager.moc"
|
||||
|
||||
@ -18,7 +18,8 @@
|
||||
#define RESUME_TIMEOUT_MS 30000
|
||||
#define QUIT_TIMEOUT_MS 30000
|
||||
|
||||
NvHTTP::NvHTTP(NvAddress address, uint16_t httpsPort, QSslCertificate serverCert) :
|
||||
NvHTTP::NvHTTP(NvAddress address, uint16_t httpsPort, QSslCertificate serverCert, QNetworkAccessManager* nam) :
|
||||
m_Nam(nam ? nam : new QNetworkAccessManager(this)),
|
||||
m_ServerCert(serverCert)
|
||||
{
|
||||
m_BaseUrlHttp.setScheme("http");
|
||||
@ -29,13 +30,11 @@ NvHTTP::NvHTTP(NvAddress address, uint16_t httpsPort, QSslCertificate serverCert
|
||||
|
||||
// Never use a proxy server
|
||||
QNetworkProxy noProxy(QNetworkProxy::NoProxy);
|
||||
m_Nam.setProxy(noProxy);
|
||||
|
||||
connect(&m_Nam, &QNetworkAccessManager::sslErrors, this, &NvHTTP::handleSslErrors);
|
||||
m_Nam->setProxy(noProxy);
|
||||
}
|
||||
|
||||
NvHTTP::NvHTTP(NvComputer* computer) :
|
||||
NvHTTP(computer->activeAddress, computer->activeHttpsPort, computer->serverCert)
|
||||
NvHTTP::NvHTTP(NvComputer* computer, QNetworkAccessManager* nam) :
|
||||
NvHTTP(computer->activeAddress, computer->activeHttpsPort, computer->serverCert, nam)
|
||||
{
|
||||
|
||||
}
|
||||
@ -304,8 +303,7 @@ NvHTTP::getAppList()
|
||||
// We must have a valid app before advancing to the next one
|
||||
if (!apps.isEmpty() && !apps.last().isInitialized()) {
|
||||
qWarning() << "Invalid applist XML";
|
||||
Q_ASSERT(false);
|
||||
return QVector<NvApp>();
|
||||
throw std::runtime_error("Invalid applist XML");
|
||||
}
|
||||
apps.append(NvApp());
|
||||
}
|
||||
@ -493,15 +491,15 @@ NvHTTP::openConnection(QUrl baseUrl,
|
||||
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)
|
||||
// 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
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 3, 0)
|
||||
// Use fine-grained idle timeouts to avoid calling QNetworkAccessManager::clearAccessCache(),
|
||||
// which tears down the NAM's global thread each time. We must not keep persistent connections
|
||||
// or GFE will puke.
|
||||
request.setAttribute(QNetworkRequest::ConnectionCacheExpiryTimeoutSecondsAttribute, 0);
|
||||
#endif
|
||||
|
||||
QNetworkReply* reply = m_Nam.get(request);
|
||||
auto sslErrorsConnection = connect(m_Nam, &QNetworkAccessManager::sslErrors, this, &NvHTTP::handleSslErrors);
|
||||
QNetworkReply* reply = m_Nam->get(request);
|
||||
|
||||
// Run the request with a timeout if requested
|
||||
QEventLoop loop;
|
||||
@ -524,9 +522,11 @@ NvHTTP::openConnection(QUrl baseUrl,
|
||||
reply->abort();
|
||||
}
|
||||
|
||||
// We must clear out cached authentication and connections or
|
||||
// GFE will puke next time
|
||||
m_Nam.clearAccessCache();
|
||||
#if QT_VERSION < QT_VERSION_CHECK(6, 3, 0)
|
||||
// If we couldn't use fine-grained connection idle timeouts, kill them all now
|
||||
m_Nam->clearAccessCache();
|
||||
#endif
|
||||
disconnect(sslErrorsConnection);
|
||||
|
||||
// Handle error
|
||||
if (reply->error() != QNetworkReply::NoError)
|
||||
|
||||
@ -109,9 +109,9 @@ public:
|
||||
NVLL_VERBOSE
|
||||
};
|
||||
|
||||
explicit NvHTTP(NvAddress address, uint16_t httpsPort, QSslCertificate serverCert);
|
||||
explicit NvHTTP(NvAddress address, uint16_t httpsPort, QSslCertificate serverCert, QNetworkAccessManager* nam = nullptr);
|
||||
|
||||
explicit NvHTTP(NvComputer* computer);
|
||||
explicit NvHTTP(NvComputer* computer, QNetworkAccessManager* nam = nullptr);
|
||||
|
||||
static
|
||||
int
|
||||
@ -196,6 +196,6 @@ private:
|
||||
NvLogLevel logLevel);
|
||||
|
||||
NvAddress m_Address;
|
||||
QNetworkAccessManager m_Nam;
|
||||
QNetworkAccessManager* m_Nam;
|
||||
QSslCertificate m_ServerCert;
|
||||
};
|
||||
|
||||
@ -59,13 +59,6 @@ public:
|
||||
|
||||
void showMessage(QString message, MessageType type) const
|
||||
{
|
||||
#if defined(Q_OS_WIN32)
|
||||
UINT flags = MB_OK | MB_TOPMOST | MB_SETFOREGROUND;
|
||||
flags |= (type == Info ? MB_ICONINFORMATION : MB_ICONERROR);
|
||||
QString title = "Moonlight";
|
||||
MessageBoxW(nullptr, reinterpret_cast<const wchar_t *>(message.utf16()),
|
||||
reinterpret_cast<const wchar_t *>(title.utf16()), flags);
|
||||
#endif
|
||||
message = message.endsWith('\n') ? message : message + '\n';
|
||||
fputs(qPrintable(message), type == Info ? stdout : stderr);
|
||||
}
|
||||
@ -113,7 +106,7 @@ public:
|
||||
|
||||
QPair<int,int> getResolutionOptionValue(QString name) const
|
||||
{
|
||||
QRegularExpression re("^(\\d+)x(\\d+)$", QRegularExpression::CaseInsensitiveOption);
|
||||
static QRegularExpression re("^(\\d+)x(\\d+)$", QRegularExpression::CaseInsensitiveOption);
|
||||
auto match = re.match(value(name));
|
||||
if (!match.hasMatch()) {
|
||||
showError(QString("Invalid %1 format: %2").arg(name, value(name)));
|
||||
@ -386,7 +379,7 @@ void StreamCommandLineParser::parse(const QStringList &args, StreamingPreference
|
||||
parser.handleUnknownOptions();
|
||||
|
||||
// Resolve display's width and height
|
||||
QRegularExpression resolutionRexExp("^(720|1080|1440|4K|resolution)$");
|
||||
static QRegularExpression resolutionRexExp("^(720|1080|1440|4K|resolution)$");
|
||||
QStringList resoOptions = parser.optionNames().filter(resolutionRexExp);
|
||||
bool displaySet = !resoOptions.isEmpty();
|
||||
if (displaySet) {
|
||||
|
||||
@ -5,10 +5,8 @@
|
||||
#include "backend/computerseeker.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QTimer>
|
||||
|
||||
#define COMPUTER_SEEK_TIMEOUT 30000
|
||||
#define APP_SEEK_TIMEOUT 10000
|
||||
|
||||
namespace CliListApps
|
||||
{
|
||||
@ -16,8 +14,7 @@ namespace CliListApps
|
||||
enum State {
|
||||
StateInit,
|
||||
StateSeekComputer,
|
||||
StateListApp,
|
||||
StateSeekApp,
|
||||
StateListApps,
|
||||
StateFailure,
|
||||
};
|
||||
|
||||
@ -26,7 +23,6 @@ class Event
|
||||
public:
|
||||
enum Type {
|
||||
ComputerFound,
|
||||
ComputerUpdated,
|
||||
ComputerSeekTimedout,
|
||||
Executed,
|
||||
};
|
||||
@ -66,9 +62,6 @@ public:
|
||||
q, &Launcher::onComputerSeekTimeout);
|
||||
m_ComputerSeeker->start(COMPUTER_SEEK_TIMEOUT);
|
||||
|
||||
q->connect(m_ComputerManager, &ComputerManager::computerStateChanged,
|
||||
q, &Launcher::onComputerUpdated);
|
||||
|
||||
m_BoxArtManager = new BoxArtManager(q);
|
||||
|
||||
if (m_Arguments.isVerbose()) {
|
||||
@ -88,12 +81,28 @@ public:
|
||||
case Event::ComputerFound:
|
||||
if (m_State == StateSeekComputer) {
|
||||
if (event.computer->pairState == NvComputer::PS_PAIRED) {
|
||||
m_State = StateSeekApp;
|
||||
m_State = StateListApps;
|
||||
m_Computer = event.computer;
|
||||
m_TimeoutTimer->start(APP_SEEK_TIMEOUT);
|
||||
|
||||
if (m_Arguments.isVerbose()) {
|
||||
fprintf(stdout, "Loading app list...\n");
|
||||
}
|
||||
|
||||
// To avoid race conditions where ComputerSeeker stops async polling, but
|
||||
// ComputerManager is yet to update the app list, we will explicitly fetch the latest app list.
|
||||
// Otherwise, it becomes complicated as we would have to guess whether ComputerManager
|
||||
// would emit 1 signal (the list did not change) or 2 signals (indicating that the list has changed)
|
||||
try {
|
||||
NvHTTP http{m_Computer};
|
||||
|
||||
const auto appList = http.getAppList();
|
||||
m_Arguments.isPrintCSV() ? printAppsCSV(appList) : printApps(appList);
|
||||
|
||||
QCoreApplication::exit(0);
|
||||
} catch (std::exception& exception) {
|
||||
fprintf(stderr, "%s\n", exception.what());
|
||||
QCoreApplication::exit(1);
|
||||
}
|
||||
} else {
|
||||
m_State = StateFailure;
|
||||
fprintf(stderr, "%s\n", qPrintable(QObject::tr("Computer %1 has not been paired. "
|
||||
@ -104,14 +113,6 @@ public:
|
||||
}
|
||||
}
|
||||
break;
|
||||
// Occurs when a computer is updated
|
||||
case Event::ComputerUpdated:
|
||||
if (m_State == StateSeekApp) {
|
||||
m_Arguments.isPrintCSV() ? printAppsCSV(m_Computer->appList) : printApps(m_Computer->appList);
|
||||
|
||||
QCoreApplication::exit(0);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@ -146,7 +147,6 @@ public:
|
||||
BoxArtManager *m_BoxArtManager;
|
||||
NvComputer *m_Computer;
|
||||
State m_State;
|
||||
QTimer *m_TimeoutTimer;
|
||||
ListCommandLineParser m_Arguments;
|
||||
};
|
||||
|
||||
@ -157,11 +157,7 @@ Launcher::Launcher(QString computer, ListCommandLineParser arguments, QObject *p
|
||||
Q_D(Launcher);
|
||||
d->m_ComputerName = computer;
|
||||
d->m_State = StateInit;
|
||||
d->m_TimeoutTimer = new QTimer(this);
|
||||
d->m_TimeoutTimer->setSingleShot(true);
|
||||
d->m_Arguments = arguments;
|
||||
connect(d->m_TimeoutTimer, &QTimer::timeout,
|
||||
this, &Launcher::onComputerSeekTimeout);
|
||||
}
|
||||
|
||||
Launcher::~Launcher()
|
||||
@ -197,12 +193,4 @@ void Launcher::onComputerSeekTimeout()
|
||||
d->handleEvent(event);
|
||||
}
|
||||
|
||||
void Launcher::onComputerUpdated(NvComputer *computer)
|
||||
{
|
||||
Q_D(Launcher);
|
||||
Event event(Event::ComputerUpdated);
|
||||
event.computer = computer;
|
||||
d->handleEvent(event);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -27,7 +27,6 @@ public:
|
||||
|
||||
private slots:
|
||||
void onComputerFound(NvComputer *computer);
|
||||
void onComputerUpdated(NvComputer *computer);
|
||||
void onComputerSeekTimeout();
|
||||
|
||||
private:
|
||||
|
||||
@ -5,5 +5,5 @@ Exec=moonlight
|
||||
Icon=moonlight
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Categories=Qt;Game;
|
||||
Categories=Qt;Game;Network;RemoteAccess;
|
||||
Keywords=nvidia;gamestream;stream;sunshine;remote play;
|
||||
|
||||
@ -14,4 +14,4 @@ renice -10 -p $(pidof PE_Single_CPU)
|
||||
|
||||
# Renice Moonlight itself to avoid preemption by background tasks
|
||||
# Write output to a logfile in /tmp
|
||||
exec nice -n -10 ./bin/moonlight > /tmp/moonlight.log
|
||||
exec nice -n -10 ./bin/moonlight > /tmp/moonlight.log 2>&1
|
||||
|
||||
@ -38,7 +38,7 @@ CenteredGridView {
|
||||
activated = true
|
||||
|
||||
// Highlight the first item if a gamepad is connected
|
||||
if (currentIndex == -1 && SdlGamepadKeyNavigation.getConnectedGamepads() > 0) {
|
||||
if (currentIndex === -1 && SdlGamepadKeyNavigation.getConnectedGamepads() > 0) {
|
||||
currentIndex = 0
|
||||
}
|
||||
|
||||
@ -94,9 +94,9 @@ CenteredGridView {
|
||||
// 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
|
||||
((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
|
||||
}
|
||||
@ -129,14 +129,9 @@ CenteredGridView {
|
||||
implicitWidth: 85
|
||||
implicitHeight: 85
|
||||
|
||||
Image {
|
||||
source: "qrc:/res/play_arrow_FILL1_wght700_GRAD200_opsz48.svg"
|
||||
anchors.centerIn: parent
|
||||
sourceSize {
|
||||
width: 75
|
||||
height: 75
|
||||
}
|
||||
}
|
||||
icon.source: "qrc:/res/play_arrow_FILL1_wght700_GRAD200_opsz48.svg"
|
||||
icon.width: 75
|
||||
icon.height: 75
|
||||
|
||||
onClicked: {
|
||||
launchOrResumeSelectedApp(true)
|
||||
@ -157,14 +152,9 @@ CenteredGridView {
|
||||
implicitWidth: 85
|
||||
implicitHeight: 85
|
||||
|
||||
Image {
|
||||
source: "qrc:/res/stop_FILL1_wght700_GRAD200_opsz48.svg"
|
||||
anchors.centerIn: parent
|
||||
sourceSize {
|
||||
width: 75
|
||||
height: 75
|
||||
}
|
||||
}
|
||||
icon.source: "qrc:/res/stop_FILL1_wght700_GRAD200_opsz48.svg"
|
||||
icon.width: 75
|
||||
icon.height: 75
|
||||
|
||||
onClicked: {
|
||||
doQuitGame()
|
||||
@ -299,18 +289,15 @@ CenteredGridView {
|
||||
sourceComponent: NavigableMenu {
|
||||
id: appContextMenu
|
||||
NavigableMenuItem {
|
||||
parentMenu: appContextMenu
|
||||
text: model.running ? qsTr("Resume Game") : qsTr("Launch Game")
|
||||
onTriggered: launchOrResumeSelectedApp(true)
|
||||
}
|
||||
NavigableMenuItem {
|
||||
parentMenu: appContextMenu
|
||||
text: qsTr("Quit Game")
|
||||
onTriggered: doQuitGame()
|
||||
visible: model.running
|
||||
}
|
||||
NavigableMenuItem {
|
||||
parentMenu: appContextMenu
|
||||
checkable: true
|
||||
checked: model.directLaunch
|
||||
text: qsTr("Direct Launch")
|
||||
@ -323,7 +310,6 @@ CenteredGridView {
|
||||
ToolTip.visible: hovered
|
||||
}
|
||||
NavigableMenuItem {
|
||||
parentMenu: appContextMenu
|
||||
checkable: true
|
||||
checked: model.hidden
|
||||
text: qsTr("Hide Game")
|
||||
@ -339,6 +325,19 @@ CenteredGridView {
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: 5
|
||||
visible: appGrid.count === 0
|
||||
|
||||
Label {
|
||||
text: qsTr("This computer doesn't seem to have any applications or some applications are hidden")
|
||||
font.pointSize: 20
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
}
|
||||
|
||||
NavigableMessageDialog {
|
||||
id: quitAppDialog
|
||||
property string appName : ""
|
||||
|
||||
@ -2,10 +2,6 @@ import QtQuick 2.9
|
||||
import QtQuick.Controls 2.2
|
||||
|
||||
GridView {
|
||||
// Detect Qt 5.11 or earlier using presence of synchronousDrag.
|
||||
// Prior to 5.12, the leftMargin and rightMargin values did not work.
|
||||
property bool hasBrokenMargins: this.synchronousDrag === undefined
|
||||
|
||||
property int minMargin: 10
|
||||
property real availableWidth: (parent.width - 2 * minMargin)
|
||||
property int itemsPerRow: availableWidth / cellWidth
|
||||
@ -15,11 +11,6 @@ GridView {
|
||||
function updateMargins() {
|
||||
leftMargin = horizontalMargin
|
||||
rightMargin = horizontalMargin
|
||||
|
||||
if (hasBrokenMargins) {
|
||||
anchors.leftMargin = leftMargin
|
||||
anchors.rightMargin = rightMargin
|
||||
}
|
||||
}
|
||||
|
||||
onHorizontalMarginChanged: {
|
||||
@ -27,15 +18,6 @@ GridView {
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
if (hasBrokenMargins) {
|
||||
// This will cause an anchor conflict with the parent StackView
|
||||
// which breaks animation, but otherwise the grid will not be
|
||||
// centered in the window.
|
||||
anchors.fill = parent
|
||||
anchors.topMargin = topMargin
|
||||
anchors.bottomMargin = bottomMargin
|
||||
}
|
||||
|
||||
updateMargins()
|
||||
}
|
||||
|
||||
|
||||
@ -2,7 +2,6 @@ import QtQuick 2.0
|
||||
import QtQuick.Controls 2.2
|
||||
|
||||
import ComputerManager 1.0
|
||||
import SdlGamepadKeyNavigation 1.0
|
||||
|
||||
Item {
|
||||
function onSearchingComputer() {
|
||||
@ -39,10 +38,6 @@ Item {
|
||||
if (!launcher.isExecuted()) {
|
||||
toolBar.visible = false
|
||||
|
||||
// Normally this is enabled by PcView, but we will won't
|
||||
// load PcView when streaming from the command-line.
|
||||
SdlGamepadKeyNavigation.enable()
|
||||
|
||||
launcher.searchingComputer.connect(onSearchingComputer)
|
||||
launcher.pairing.connect(onPairing)
|
||||
launcher.failed.connect(onFailed)
|
||||
@ -58,6 +53,7 @@ Item {
|
||||
|
||||
BusyIndicator {
|
||||
id: stageSpinner
|
||||
running: visible
|
||||
}
|
||||
|
||||
Label {
|
||||
|
||||
@ -34,6 +34,7 @@ Item {
|
||||
|
||||
BusyIndicator {
|
||||
id: stageSpinner
|
||||
running: visible
|
||||
}
|
||||
|
||||
Label {
|
||||
|
||||
@ -2,7 +2,6 @@ import QtQuick 2.0
|
||||
import QtQuick.Controls 2.2
|
||||
|
||||
import ComputerManager 1.0
|
||||
import SdlGamepadKeyNavigation 1.0
|
||||
|
||||
Item {
|
||||
function onSearchingComputer() {
|
||||
@ -38,10 +37,6 @@ Item {
|
||||
if (!launcher.isExecuted()) {
|
||||
toolBar.visible = false
|
||||
|
||||
// Normally this is enabled by PcView, but we will won't
|
||||
// load PcView when streaming from the command-line.
|
||||
SdlGamepadKeyNavigation.enable()
|
||||
|
||||
launcher.searchingComputer.connect(onSearchingComputer)
|
||||
launcher.searchingApp.connect(onSearchingApp)
|
||||
launcher.sessionCreated.connect(onSessionCreated)
|
||||
@ -57,6 +52,7 @@ Item {
|
||||
|
||||
BusyIndicator {
|
||||
id: stageSpinner
|
||||
running: visible
|
||||
}
|
||||
|
||||
Label {
|
||||
|
||||
@ -2,10 +2,6 @@ import QtQuick 2.0
|
||||
import QtQuick.Controls 2.2
|
||||
|
||||
MenuItem {
|
||||
// Qt 5.10 has a menu property, but we need to support 5.9
|
||||
// so we must make our own.
|
||||
property Menu parentMenu
|
||||
|
||||
// Ensure focus can't be given to an invisible item
|
||||
enabled: visible
|
||||
height: visible ? implicitHeight : 0
|
||||
@ -15,7 +11,7 @@ MenuItem {
|
||||
// We must close the context menu first or
|
||||
// it can steal focus from any dialogs that
|
||||
// onTriggered may spawn.
|
||||
parentMenu.close()
|
||||
menu.close()
|
||||
}
|
||||
|
||||
Keys.onReturnPressed: {
|
||||
@ -27,6 +23,6 @@ MenuItem {
|
||||
}
|
||||
|
||||
Keys.onEscapePressed: {
|
||||
parentMenu.close()
|
||||
menu.close()
|
||||
}
|
||||
}
|
||||
|
||||
@ -24,6 +24,7 @@ NavigableDialog {
|
||||
BusyIndicator {
|
||||
id: dialogSpinner
|
||||
visible: false
|
||||
running: visible
|
||||
}
|
||||
|
||||
Image {
|
||||
|
||||
@ -7,17 +7,9 @@ ToolButton {
|
||||
|
||||
activeFocusOnTab: true
|
||||
|
||||
// FIXME: We're using an Image here rather than icon.source because
|
||||
// icons don't work on Qt 5.9 LTS.
|
||||
Image {
|
||||
id: image
|
||||
source: iconSource
|
||||
anchors.centerIn: parent.background
|
||||
sourceSize {
|
||||
width: parent.background.width * 1.10
|
||||
height: parent.background.height * 1.10
|
||||
}
|
||||
}
|
||||
icon.source: iconSource
|
||||
icon.width: background.width
|
||||
icon.height: background.height
|
||||
|
||||
// This determines the size of the Material highlight. We increase it
|
||||
// from the default because we use larger than normal icons for TV readability.
|
||||
|
||||
@ -34,13 +34,8 @@ CenteredGridView {
|
||||
// Setup signals on CM
|
||||
ComputerManager.computerAddCompleted.connect(addComplete)
|
||||
|
||||
// This is a bit of a hack to do this here as opposed to main.qml, but
|
||||
// we need it enabled before calling getConnectedGamepads() and PcView
|
||||
// is never destroyed, so it should be okay.
|
||||
SdlGamepadKeyNavigation.enable()
|
||||
|
||||
// Highlight the first item if a gamepad is connected
|
||||
if (currentIndex == -1 && SdlGamepadKeyNavigation.getConnectedGamepads() > 0) {
|
||||
if (currentIndex === -1 && SdlGamepadKeyNavigation.getConnectedGamepads() > 0) {
|
||||
currentIndex = 0
|
||||
}
|
||||
}
|
||||
@ -95,6 +90,7 @@ CenteredGridView {
|
||||
BusyIndicator {
|
||||
id: searchSpinner
|
||||
visible: StreamingPreferences.enableMdns
|
||||
running: visible
|
||||
}
|
||||
|
||||
Label {
|
||||
@ -148,6 +144,7 @@ CenteredGridView {
|
||||
width: 75
|
||||
height: 75
|
||||
visible: model.statusUnknown
|
||||
running: visible
|
||||
}
|
||||
|
||||
Label {
|
||||
@ -174,7 +171,6 @@ CenteredGridView {
|
||||
enabled: false
|
||||
}
|
||||
NavigableMenuItem {
|
||||
parentMenu: pcContextMenu
|
||||
text: qsTr("View All Apps")
|
||||
onTriggered: {
|
||||
var component = Qt.createComponent("AppView.qml")
|
||||
@ -184,13 +180,11 @@ CenteredGridView {
|
||||
visible: model.online && model.paired
|
||||
}
|
||||
NavigableMenuItem {
|
||||
parentMenu: pcContextMenu
|
||||
text: qsTr("Wake PC")
|
||||
onTriggered: computerModel.wakeComputer(index)
|
||||
visible: !model.online && model.wakeable
|
||||
}
|
||||
NavigableMenuItem {
|
||||
parentMenu: pcContextMenu
|
||||
text: qsTr("Test Network")
|
||||
onTriggered: {
|
||||
computerModel.testConnectionForComputer(index)
|
||||
@ -199,7 +193,6 @@ CenteredGridView {
|
||||
}
|
||||
|
||||
NavigableMenuItem {
|
||||
parentMenu: pcContextMenu
|
||||
text: qsTr("Rename PC")
|
||||
onTriggered: {
|
||||
renamePcDialog.pcIndex = index
|
||||
@ -208,7 +201,6 @@ CenteredGridView {
|
||||
}
|
||||
}
|
||||
NavigableMenuItem {
|
||||
parentMenu: pcContextMenu
|
||||
text: qsTr("Delete PC")
|
||||
onTriggered: {
|
||||
deletePcDialog.pcIndex = index
|
||||
@ -217,7 +209,6 @@ CenteredGridView {
|
||||
}
|
||||
}
|
||||
NavigableMenuItem {
|
||||
parentMenu: pcContextMenu
|
||||
text: qsTr("View Details")
|
||||
onTriggered: {
|
||||
showPcDetailsDialog.pcDetails = model.details
|
||||
|
||||
@ -60,6 +60,7 @@ Item {
|
||||
|
||||
BusyIndicator {
|
||||
id: stageSpinner
|
||||
running: visible
|
||||
}
|
||||
|
||||
Label {
|
||||
|
||||
@ -281,11 +281,13 @@ Flickable {
|
||||
StreamingPreferences.width = selectedWidth
|
||||
StreamingPreferences.height = selectedHeight
|
||||
|
||||
StreamingPreferences.bitrateKbps = StreamingPreferences.getDefaultBitrate(StreamingPreferences.width,
|
||||
StreamingPreferences.height,
|
||||
StreamingPreferences.fps,
|
||||
StreamingPreferences.enableYUV444);
|
||||
slider.value = StreamingPreferences.bitrateKbps
|
||||
if (StreamingPreferences.autoAdjustBitrate) {
|
||||
StreamingPreferences.bitrateKbps = StreamingPreferences.getDefaultBitrate(StreamingPreferences.width,
|
||||
StreamingPreferences.height,
|
||||
StreamingPreferences.fps,
|
||||
StreamingPreferences.enableYUV444);
|
||||
slider.value = StreamingPreferences.bitrateKbps
|
||||
}
|
||||
}
|
||||
|
||||
lastIndexValue = currentIndex
|
||||
@ -447,11 +449,13 @@ Flickable {
|
||||
if (StreamingPreferences.fps !== selectedFps) {
|
||||
StreamingPreferences.fps = selectedFps
|
||||
|
||||
StreamingPreferences.bitrateKbps = StreamingPreferences.getDefaultBitrate(StreamingPreferences.width,
|
||||
StreamingPreferences.height,
|
||||
StreamingPreferences.fps,
|
||||
StreamingPreferences.enableYUV444);
|
||||
slider.value = StreamingPreferences.bitrateKbps
|
||||
if (StreamingPreferences.autoAdjustBitrate) {
|
||||
StreamingPreferences.bitrateKbps = StreamingPreferences.getDefaultBitrate(StreamingPreferences.width,
|
||||
StreamingPreferences.height,
|
||||
StreamingPreferences.fps,
|
||||
StreamingPreferences.enableYUV444);
|
||||
slider.value = StreamingPreferences.bitrateKbps
|
||||
}
|
||||
}
|
||||
|
||||
lastIndexValue = currentIndex
|
||||
@ -678,26 +682,47 @@ Flickable {
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
|
||||
Slider {
|
||||
id: slider
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: 5
|
||||
|
||||
value: StreamingPreferences.bitrateKbps
|
||||
Slider {
|
||||
id: slider
|
||||
|
||||
stepSize: 500
|
||||
from : 500
|
||||
to: StreamingPreferences.unlockBitrate ? 500000 : 150000
|
||||
value: StreamingPreferences.bitrateKbps
|
||||
|
||||
snapMode: "SnapOnRelease"
|
||||
width: Math.min(bitrateDesc.implicitWidth, parent.width)
|
||||
stepSize: 500
|
||||
from : 500
|
||||
to: StreamingPreferences.unlockBitrate ? 500000 : 150000
|
||||
|
||||
onValueChanged: {
|
||||
bitrateTitle.text = qsTr("Video bitrate: %1 Mbps").arg(value / 1000.0)
|
||||
StreamingPreferences.bitrateKbps = value
|
||||
snapMode: "SnapOnRelease"
|
||||
width: Math.min(bitrateDesc.implicitWidth, parent.width - (resetBitrateButton.visible ? resetBitrateButton.width + parent.spacing : 0))
|
||||
|
||||
onValueChanged: {
|
||||
bitrateTitle.text = qsTr("Video bitrate: %1 Mbps").arg(value / 1000.0)
|
||||
StreamingPreferences.bitrateKbps = value
|
||||
}
|
||||
|
||||
onMoved: {
|
||||
StreamingPreferences.autoAdjustBitrate = false
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
// Refresh the text after translations change
|
||||
languageChanged.connect(valueChanged)
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
// Refresh the text after translations change
|
||||
languageChanged.connect(valueChanged)
|
||||
Button {
|
||||
id: resetBitrateButton
|
||||
text: qsTr("Use Default (%1 Mbps)").arg(StreamingPreferences.getDefaultBitrate(StreamingPreferences.width, StreamingPreferences.height, StreamingPreferences.fps, StreamingPreferences.enableYUV444) / 1000.0)
|
||||
visible: StreamingPreferences.bitrateKbps !== StreamingPreferences.getDefaultBitrate(StreamingPreferences.width, StreamingPreferences.height, StreamingPreferences.fps, StreamingPreferences.enableYUV444)
|
||||
onClicked: {
|
||||
var defaultBitrate = StreamingPreferences.getDefaultBitrate(StreamingPreferences.width, StreamingPreferences.height, StreamingPreferences.fps, StreamingPreferences.enableYUV444)
|
||||
StreamingPreferences.bitrateKbps = defaultBitrate
|
||||
StreamingPreferences.autoAdjustBitrate = true
|
||||
slider.value = defaultBitrate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1112,6 +1137,18 @@ Flickable {
|
||||
text: "Eesti" // Estonian
|
||||
val: StreamingPreferences.LANG_ET
|
||||
} */
|
||||
ListElement {
|
||||
text: "Български" // Bulgarian
|
||||
val: StreamingPreferences.LANG_BG
|
||||
}
|
||||
/* ListElement {
|
||||
text: "Esperanto"
|
||||
val: StreamingPreferences.LANG_EO
|
||||
} */
|
||||
ListElement {
|
||||
text: "தமிழ்" // Tamil
|
||||
val: StreamingPreferences.LANG_TA
|
||||
}
|
||||
}
|
||||
// ::onActivated must be used, as it only listens for when the index is changed by a human
|
||||
onActivated : {
|
||||
@ -1199,6 +1236,17 @@ Flickable {
|
||||
}
|
||||
}
|
||||
|
||||
CheckBox {
|
||||
id: configurationWarningsCheck
|
||||
width: parent.width
|
||||
text: qsTr("Show configuration warnings")
|
||||
font.pointSize: 12
|
||||
checked: StreamingPreferences.configurationWarnings
|
||||
onCheckedChanged: {
|
||||
StreamingPreferences.configurationWarnings = checked
|
||||
}
|
||||
}
|
||||
|
||||
CheckBox {
|
||||
visible: SystemProperties.hasDiscordIntegration
|
||||
id: discordPresenceCheck
|
||||
@ -1616,11 +1664,13 @@ Flickable {
|
||||
// This is called on init, so only reset to default bitrate when checked state changes.
|
||||
if (StreamingPreferences.enableYUV444 != checked) {
|
||||
StreamingPreferences.enableYUV444 = checked
|
||||
StreamingPreferences.bitrateKbps = StreamingPreferences.getDefaultBitrate(StreamingPreferences.width,
|
||||
StreamingPreferences.height,
|
||||
StreamingPreferences.fps,
|
||||
StreamingPreferences.enableYUV444);
|
||||
slider.value = StreamingPreferences.bitrateKbps
|
||||
if (StreamingPreferences.autoAdjustBitrate) {
|
||||
StreamingPreferences.bitrateKbps = StreamingPreferences.getDefaultBitrate(StreamingPreferences.width,
|
||||
StreamingPreferences.height,
|
||||
StreamingPreferences.fps,
|
||||
StreamingPreferences.enableYUV444);
|
||||
slider.value = StreamingPreferences.bitrateKbps
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -76,7 +76,7 @@ Item {
|
||||
streamSegueErrorDialog.text += "\n\n" + qsTr("This PC's Internet connection is blocking Moonlight. Streaming over the Internet may not work while connected to this network.")
|
||||
}
|
||||
|
||||
// Enable GUI gamepad usage now
|
||||
// Re-enable GUI gamepad usage now
|
||||
SdlGamepadKeyNavigation.enable()
|
||||
|
||||
if (quitAfter) {
|
||||
@ -119,7 +119,7 @@ Item {
|
||||
// Show the toolbar again when popped off the stack
|
||||
toolBar.visible = true
|
||||
|
||||
// Enable GUI gamepad usage now
|
||||
// Re-enable GUI gamepad usage now
|
||||
SdlGamepadKeyNavigation.enable()
|
||||
}
|
||||
|
||||
@ -151,7 +151,7 @@ Item {
|
||||
// middle of the animation on Windows, which looks very
|
||||
// obviously broken.
|
||||
interval: 100
|
||||
onTriggered: stageSpinner.running = true
|
||||
onTriggered: stageSpinner.visible = true
|
||||
}
|
||||
|
||||
Loader {
|
||||
@ -176,7 +176,7 @@ Item {
|
||||
gc()
|
||||
|
||||
// Run the streaming session to completion
|
||||
session.exec(Window.window)
|
||||
session.exec(window)
|
||||
}
|
||||
|
||||
sourceComponent: Item {}
|
||||
@ -188,7 +188,8 @@ Item {
|
||||
|
||||
BusyIndicator {
|
||||
id: stageSpinner
|
||||
running: false
|
||||
running: visible
|
||||
visible: false
|
||||
}
|
||||
|
||||
Label {
|
||||
|
||||
@ -22,7 +22,8 @@ ApplicationWindow {
|
||||
width: 1280
|
||||
height: 600
|
||||
|
||||
Component.onCompleted: {
|
||||
// This function runs prior to creation of the initial StackView item
|
||||
function doEarlyInit() {
|
||||
// Override the background color to Material 2 colors for Qt 6.5+
|
||||
// in order to improve contrast between GFE's placeholder box art
|
||||
// and the background of the app grid.
|
||||
@ -30,6 +31,10 @@ ApplicationWindow {
|
||||
Material.background = "#303030"
|
||||
}
|
||||
|
||||
SdlGamepadKeyNavigation.enable()
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
// Show the window according to the user's preferences
|
||||
if (SystemProperties.hasDesktopEnvironment) {
|
||||
if (StreamingPreferences.uiDisplayMode == StreamingPreferences.UI_MAXIMIZED) {
|
||||
@ -49,7 +54,7 @@ ApplicationWindow {
|
||||
if (SystemProperties.isWow64) {
|
||||
wow64Dialog.open()
|
||||
}
|
||||
else if (!SystemProperties.hasHardwareAcceleration) {
|
||||
else if (!SystemProperties.hasHardwareAcceleration && StreamingPreferences.videoDecoderSelection !== StreamingPreferences.VDS_FORCE_SOFTWARE) {
|
||||
if (SystemProperties.isRunningXWayland) {
|
||||
xWaylandDialog.open()
|
||||
}
|
||||
@ -64,9 +69,19 @@ ApplicationWindow {
|
||||
}
|
||||
}
|
||||
|
||||
// It would be better to use TextMetrics here, but it always lays out
|
||||
// the text slightly more compactly than real Text does in ToolTip,
|
||||
// causing unexpected line breaks to be inserted
|
||||
Text {
|
||||
id: tooltipTextLayoutHelper
|
||||
visible: false
|
||||
font: ToolTip.toolTip.font
|
||||
text: ToolTip.toolTip.text
|
||||
}
|
||||
|
||||
// This configures the maximum width of the singleton attached QML ToolTip. If left unconstrained,
|
||||
// it will never insert a line break and just extend on forever.
|
||||
ToolTip.toolTip.contentWidth: ToolTip.toolTip.implicitContentWidth < 400 ? ToolTip.toolTip.implicitContentWidth : 400
|
||||
ToolTip.toolTip.contentWidth: Math.min(tooltipTextLayoutHelper.width, 400)
|
||||
|
||||
function goBack() {
|
||||
if (clearOnBack) {
|
||||
@ -81,10 +96,16 @@ ApplicationWindow {
|
||||
|
||||
StackView {
|
||||
id: stackView
|
||||
initialItem: initialView
|
||||
anchors.fill: parent
|
||||
focus: true
|
||||
|
||||
Component.onCompleted: {
|
||||
// Perform our early initialization before constructing
|
||||
// the initial view and pushing it to the StackView
|
||||
doEarlyInit()
|
||||
push(initialView)
|
||||
}
|
||||
|
||||
onCurrentItemChanged: {
|
||||
// Ensure focus travels to the next view when going back
|
||||
if (currentItem) {
|
||||
@ -158,6 +179,9 @@ ApplicationWindow {
|
||||
pollingActive = true
|
||||
}
|
||||
}
|
||||
|
||||
// Poll for gamepad input only when the window is in focus
|
||||
SdlGamepadKeyNavigation.notifyWindowFocus(visible && active)
|
||||
}
|
||||
|
||||
onActiveChanged: {
|
||||
@ -176,22 +200,15 @@ ApplicationWindow {
|
||||
// if focus does not return within a few minutes.
|
||||
inactivityTimer.restart()
|
||||
}
|
||||
}
|
||||
|
||||
// Workaround for lack of instanceof in Qt 5.9.
|
||||
//
|
||||
// Based on https://stackoverflow.com/questions/13923794/how-to-do-a-is-a-typeof-or-instanceof-in-qml
|
||||
function qmltypeof(obj, className) { // QtObject, string -> bool
|
||||
// className plus "(" is the class instance without modification
|
||||
// className plus "_QML" is the class instance with user-defined properties
|
||||
var str = obj.toString();
|
||||
return str.startsWith(className + "(") || str.startsWith(className + "_QML");
|
||||
// Poll for gamepad input only when the window is in focus
|
||||
SdlGamepadKeyNavigation.notifyWindowFocus(visible && active)
|
||||
}
|
||||
|
||||
function navigateTo(url, objectType)
|
||||
{
|
||||
var existingItem = stackView.find(function(item, index) {
|
||||
return qmltypeof(item, objectType)
|
||||
return item instanceof objectType
|
||||
})
|
||||
|
||||
if (existingItem !== null) {
|
||||
@ -258,7 +275,7 @@ ApplicationWindow {
|
||||
|
||||
Label {
|
||||
id: versionLabel
|
||||
visible: qmltypeof(stackView.currentItem, "SettingsView")
|
||||
visible: stackView.currentItem instanceof SettingsView
|
||||
text: qsTr("Version %1").arg(SystemProperties.versionString)
|
||||
font.pointSize: 12
|
||||
horizontalAlignment: Qt.AlignRight
|
||||
@ -268,7 +285,7 @@ ApplicationWindow {
|
||||
NavigableToolButton {
|
||||
id: discordButton
|
||||
visible: SystemProperties.hasBrowser &&
|
||||
qmltypeof(stackView.currentItem, "SettingsView")
|
||||
stackView.currentItem instanceof SettingsView
|
||||
|
||||
iconSource: "qrc:/res/discord.svg"
|
||||
|
||||
@ -287,7 +304,7 @@ ApplicationWindow {
|
||||
|
||||
NavigableToolButton {
|
||||
id: addPcButton
|
||||
visible: qmltypeof(stackView.currentItem, "PcView")
|
||||
visible: stackView.currentItem instanceof PcView
|
||||
|
||||
iconSource: "qrc:/res/ic_add_to_queue_white_48px.svg"
|
||||
|
||||
@ -385,7 +402,7 @@ ApplicationWindow {
|
||||
|
||||
iconSource: "qrc:/res/ic_videogame_asset_white_48px.svg"
|
||||
|
||||
onClicked: navigateTo("qrc:/gui/GamepadMapper.qml", "GamepadMapper")
|
||||
onClicked: navigateTo("qrc:/gui/GamepadMapper.qml", GamepadMapper)
|
||||
|
||||
Keys.onDownPressed: {
|
||||
stackView.currentItem.forceActiveFocus(Qt.TabFocus)
|
||||
@ -397,7 +414,7 @@ ApplicationWindow {
|
||||
|
||||
iconSource: "qrc:/res/settings.svg"
|
||||
|
||||
onClicked: navigateTo("qrc:/gui/SettingsView.qml", "SettingsView")
|
||||
onClicked: navigateTo("qrc:/gui/SettingsView.qml", SettingsView)
|
||||
|
||||
Keys.onDownPressed: {
|
||||
stackView.currentItem.forceActiveFocus(Qt.TabFocus)
|
||||
|
||||
@ -13,6 +13,7 @@ SdlGamepadKeyNavigation::SdlGamepadKeyNavigation(StreamingPreferences* prefs)
|
||||
m_Enabled(false),
|
||||
m_UiNavMode(false),
|
||||
m_FirstPoll(false),
|
||||
m_HasFocus(false),
|
||||
m_LastAxisNavigationEventTime(0)
|
||||
{
|
||||
m_PollingTimer = new QTimer(this);
|
||||
@ -54,7 +55,8 @@ void SdlGamepadKeyNavigation::enable()
|
||||
SDL_FlushEvent(SDL_CONTROLLERDEVICEADDED);
|
||||
|
||||
// Open all currently attached game controllers
|
||||
for (int i = 0; i < SDL_NumJoysticks(); i++) {
|
||||
int numJoysticks = SDL_NumJoysticks();
|
||||
for (int i = 0; i < numJoysticks; i++) {
|
||||
if (SDL_IsGameController(i)) {
|
||||
SDL_GameController* gc = SDL_GameControllerOpen(i);
|
||||
if (gc != nullptr) {
|
||||
@ -63,13 +65,10 @@ void SdlGamepadKeyNavigation::enable()
|
||||
}
|
||||
}
|
||||
|
||||
// Flush events on the first poll
|
||||
m_FirstPoll = true;
|
||||
|
||||
// Poll every 50 ms for a new joystick event
|
||||
m_PollingTimer->start(50);
|
||||
|
||||
m_Enabled = true;
|
||||
|
||||
// Start the polling timer if the window is focused
|
||||
updateTimerState();
|
||||
}
|
||||
|
||||
void SdlGamepadKeyNavigation::disable()
|
||||
@ -78,7 +77,9 @@ void SdlGamepadKeyNavigation::disable()
|
||||
return;
|
||||
}
|
||||
|
||||
m_PollingTimer->stop();
|
||||
m_Enabled = false;
|
||||
updateTimerState();
|
||||
Q_ASSERT(!m_PollingTimer->isActive());
|
||||
|
||||
while (!m_Gamepads.isEmpty()) {
|
||||
SDL_GameControllerClose(m_Gamepads[0]);
|
||||
@ -86,8 +87,12 @@ void SdlGamepadKeyNavigation::disable()
|
||||
}
|
||||
|
||||
SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER);
|
||||
}
|
||||
|
||||
m_Enabled = false;
|
||||
void SdlGamepadKeyNavigation::notifyWindowFocus(bool hasFocus)
|
||||
{
|
||||
m_HasFocus = hasFocus;
|
||||
updateTimerState();
|
||||
}
|
||||
|
||||
void SdlGamepadKeyNavigation::onPollingTimerFired()
|
||||
@ -261,6 +266,20 @@ void SdlGamepadKeyNavigation::sendKey(QEvent::Type type, Qt::Key key, Qt::Keyboa
|
||||
}
|
||||
}
|
||||
|
||||
void SdlGamepadKeyNavigation::updateTimerState()
|
||||
{
|
||||
if (m_PollingTimer->isActive() && (!m_HasFocus || !m_Enabled)) {
|
||||
m_PollingTimer->stop();
|
||||
}
|
||||
else if (!m_PollingTimer->isActive() && m_HasFocus && m_Enabled) {
|
||||
// Flush events on the first poll
|
||||
m_FirstPoll = true;
|
||||
|
||||
// Poll every 50 ms for a new joystick event
|
||||
m_PollingTimer->start(50);
|
||||
}
|
||||
}
|
||||
|
||||
void SdlGamepadKeyNavigation::setUiNavMode(bool uiNavMode)
|
||||
{
|
||||
m_UiNavMode = uiNavMode;
|
||||
@ -271,7 +290,8 @@ int SdlGamepadKeyNavigation::getConnectedGamepads()
|
||||
Q_ASSERT(m_Enabled);
|
||||
|
||||
int count = 0;
|
||||
for (int i = 0; i < SDL_NumJoysticks(); i++) {
|
||||
int numJoysticks = SDL_NumJoysticks();
|
||||
for (int i = 0; i < numJoysticks; i++) {
|
||||
if (SDL_IsGameController(i)) {
|
||||
count++;
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
#include <QTimer>
|
||||
#include <QEvent>
|
||||
|
||||
#include <SDL.h>
|
||||
#include "SDL_compat.h"
|
||||
|
||||
#include "settings/streamingpreferences.h"
|
||||
|
||||
@ -20,6 +20,8 @@ public:
|
||||
|
||||
Q_INVOKABLE void disable();
|
||||
|
||||
Q_INVOKABLE void notifyWindowFocus(bool hasFocus);
|
||||
|
||||
Q_INVOKABLE void setUiNavMode(bool settingsMode);
|
||||
|
||||
Q_INVOKABLE int getConnectedGamepads();
|
||||
@ -27,6 +29,8 @@ public:
|
||||
private:
|
||||
void sendKey(QEvent::Type type, Qt::Key key, Qt::KeyboardModifiers modifiers = Qt::NoModifier);
|
||||
|
||||
void updateTimerState();
|
||||
|
||||
private slots:
|
||||
void onPollingTimerFired();
|
||||
|
||||
@ -37,5 +41,6 @@ private:
|
||||
bool m_Enabled;
|
||||
bool m_UiNavMode;
|
||||
bool m_FirstPoll;
|
||||
bool m_HasFocus;
|
||||
Uint32 m_LastAxisNavigationEventTime;
|
||||
};
|
||||
|
||||
BIN
app/languages/qml_bg.qm
Normal file
BIN
app/languages/qml_bg.qm
Normal file
Binary file not shown.
1336
app/languages/qml_bg.ts
Normal file
1336
app/languages/qml_bg.ts
Normal file
File diff suppressed because it is too large
Load Diff
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
File diff suppressed because it is too large
Load Diff
BIN
app/languages/qml_eo.qm
Normal file
BIN
app/languages/qml_eo.qm
Normal file
Binary file not shown.
1336
app/languages/qml_eo.ts
Normal file
1336
app/languages/qml_eo.ts
Normal file
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
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
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
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
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
BIN
app/languages/qml_ta.qm
Normal file
BIN
app/languages/qml_ta.qm
Normal file
Binary file not shown.
1336
app/languages/qml_ta.ts
Normal file
1336
app/languages/qml_ta.ts
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
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
172
app/main.cpp
172
app/main.cpp
@ -17,7 +17,7 @@
|
||||
// doing the same thing. This needs to be before any headers
|
||||
// that might include SDL.h themselves.
|
||||
#define SDL_MAIN_HANDLED
|
||||
#include <SDL.h>
|
||||
#include "SDL_compat.h"
|
||||
|
||||
#ifdef HAVE_FFMPEG
|
||||
#include "streaming/video/ffmpeg.h"
|
||||
@ -60,28 +60,55 @@
|
||||
// Log to console for debug Mac builds
|
||||
#endif
|
||||
|
||||
// StreamUtils::setAsyncLogging() exposes control of this to the Session
|
||||
// class to enable async logging once the stream has started.
|
||||
//
|
||||
// FIXME: Clean this up
|
||||
QAtomicInt g_AsyncLoggingEnabled;
|
||||
|
||||
static QElapsedTimer s_LoggerTime;
|
||||
static QTextStream s_LoggerStream(stderr);
|
||||
static QMutex s_LoggerLock;
|
||||
static QThreadPool s_LoggerThread;
|
||||
static QMutex s_SyncLoggerMutex;
|
||||
static bool s_SuppressVerboseOutput;
|
||||
static QRegularExpression k_RikeyRegex("&rikey=\\w+");
|
||||
static QRegularExpression k_RikeyIdRegex("&rikeyid=[\\d-]+");
|
||||
#ifdef LOG_TO_FILE
|
||||
// Max log file size of 10 MB
|
||||
#define MAX_LOG_SIZE_BYTES (10 * 1024 * 1024)
|
||||
static int s_LogBytesWritten = 0;
|
||||
static bool s_LogLimitReached = false;
|
||||
static const uint64_t k_MaxLogSizeBytes = 10 * 1024 * 1024;
|
||||
static QAtomicInteger<uint64_t> s_LogBytesWritten = 0;
|
||||
static QFile* s_LoggerFile;
|
||||
#endif
|
||||
|
||||
class LoggerTask : public QRunnable
|
||||
{
|
||||
public:
|
||||
LoggerTask(const QString& msg) : m_Msg(msg)
|
||||
{
|
||||
setAutoDelete(true);
|
||||
}
|
||||
|
||||
void run() override
|
||||
{
|
||||
// QTextStream is not thread-safe, so we must lock. This will generally
|
||||
// only contend in synchronous logging mode or during a transition
|
||||
// between synchronous and asynchronous. Asynchronous won't contend in
|
||||
// the common case because we only have a single logging thread.
|
||||
QMutexLocker locker(&s_SyncLoggerMutex);
|
||||
s_LoggerStream << m_Msg;
|
||||
s_LoggerStream.flush();
|
||||
}
|
||||
|
||||
private:
|
||||
QString m_Msg;
|
||||
};
|
||||
|
||||
void logToLoggerStream(QString& message)
|
||||
{
|
||||
QMutexLocker lock(&s_LoggerLock);
|
||||
|
||||
#if defined(QT_DEBUG) && defined(Q_OS_WIN32)
|
||||
// Output log messages to a debugger if attached
|
||||
if (IsDebuggerPresent()) {
|
||||
static QString lineBuffer;
|
||||
thread_local QString lineBuffer;
|
||||
lineBuffer += message;
|
||||
if (message.endsWith('\n')) {
|
||||
OutputDebugStringW(lineBuffer.toStdWString().c_str());
|
||||
@ -95,26 +122,24 @@ void logToLoggerStream(QString& message)
|
||||
message.replace(k_RikeyIdRegex, "&rikeyid=REDACTED");
|
||||
|
||||
#ifdef LOG_TO_FILE
|
||||
if (s_LogLimitReached) {
|
||||
auto oldLogSize = s_LogBytesWritten.fetchAndAddRelaxed(message.size());
|
||||
if (oldLogSize >= k_MaxLogSizeBytes) {
|
||||
return;
|
||||
}
|
||||
else if (s_LogBytesWritten >= MAX_LOG_SIZE_BYTES) {
|
||||
s_LoggerStream << "Log size limit reached!";
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
|
||||
s_LoggerStream << Qt::endl;
|
||||
#else
|
||||
s_LoggerStream << endl;
|
||||
#endif
|
||||
s_LogLimitReached = true;
|
||||
return;
|
||||
}
|
||||
else {
|
||||
s_LogBytesWritten += message.size();
|
||||
else if (oldLogSize >= k_MaxLogSizeBytes - message.size()) {
|
||||
// Write one final message
|
||||
message = "Log size limit reached!";
|
||||
}
|
||||
#endif
|
||||
|
||||
s_LoggerStream << message;
|
||||
s_LoggerStream.flush();
|
||||
if (g_AsyncLoggingEnabled) {
|
||||
// Queue the log message to be written asynchronously
|
||||
s_LoggerThread.start(new LoggerTask(message));
|
||||
}
|
||||
else {
|
||||
// Log the message immediately
|
||||
LoggerTask(message).run();
|
||||
}
|
||||
}
|
||||
|
||||
void sdlLogToDiskHandler(void*, int category, SDL_LogPriority priority, const char* message)
|
||||
@ -284,6 +309,11 @@ LONG WINAPI UnhandledExceptionHandler(struct _EXCEPTION_POINTERS *ExceptionInfo)
|
||||
qCritical() << "Unhandled exception! Failed to open dump file:" << qDmpFileName << "with error" << GetLastError();
|
||||
}
|
||||
|
||||
// Sleep for a moment to allow the logging thread to finish up before crashing
|
||||
if (g_AsyncLoggingEnabled) {
|
||||
Sleep(500);
|
||||
}
|
||||
|
||||
// Let the program crash and WER collect a dump
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
}
|
||||
@ -344,10 +374,20 @@ int main(int argc, char *argv[])
|
||||
}
|
||||
#endif
|
||||
|
||||
// Serialize log messages on a single thread
|
||||
s_LoggerThread.setMaxThreadCount(1);
|
||||
s_LoggerTime.start();
|
||||
qInstallMessageHandler(qtLogToDiskHandler);
|
||||
SDL_LogSetOutputFunction(sdlLogToDiskHandler, nullptr);
|
||||
|
||||
// Register our logger with all libraries
|
||||
#if SDL_VERSION_ATLEAST(3, 0, 0)
|
||||
SDL_SetLogOutputFunction(sdlLogToDiskHandler, nullptr);
|
||||
#else
|
||||
SDL_LogOutputFunction oldSdlLogFn;
|
||||
void* oldSdlLogUserdata;
|
||||
SDL_LogGetOutputFunction(&oldSdlLogFn, &oldSdlLogUserdata);
|
||||
SDL_LogSetOutputFunction(sdlLogToDiskHandler, nullptr);
|
||||
#endif
|
||||
qInstallMessageHandler(qtLogToDiskHandler);
|
||||
#ifdef HAVE_FFMPEG
|
||||
av_log_set_callback(ffmpegLogToDiskHandler);
|
||||
#endif
|
||||
@ -435,7 +475,7 @@ int main(int argc, char *argv[])
|
||||
#endif
|
||||
}
|
||||
|
||||
#if !defined(Q_PROCESSOR_X86) && defined(SDL_HINT_VIDEO_X11_FORCE_EGL)
|
||||
#ifndef Q_PROCESSOR_X86
|
||||
// Some ARM and RISC-V embedded devices don't have working GLX which can cause
|
||||
// SDL to fail to find a working OpenGL implementation at all. Let's force EGL
|
||||
// on non-x86 platforms, since GLX is deprecated anyway.
|
||||
@ -491,12 +531,12 @@ int main(int argc, char *argv[])
|
||||
SDL_SetHint(SDL_HINT_VIDEO_ALLOW_SCREENSAVER, "1");
|
||||
|
||||
// We use MMAL to render on Raspberry Pi, so we do not require DRM master.
|
||||
SDL_SetHint("SDL_KMSDRM_REQUIRE_DRM_MASTER", "0");
|
||||
SDL_SetHint(SDL_HINT_KMSDRM_REQUIRE_DRM_MASTER, "0");
|
||||
|
||||
// Use Direct3D 9Ex to avoid a deadlock caused by the D3D device being reset when
|
||||
// the user triggers a UAC prompt. This option controls the software/SDL renderer.
|
||||
// The DXVA2 renderer uses Direct3D 9Ex itself directly.
|
||||
SDL_SetHint("SDL_WINDOWS_USE_D3D9EX", "1");
|
||||
SDL_SetHint(SDL_HINT_WINDOWS_USE_D3D9EX, "1");
|
||||
|
||||
if (SDL_InitSubSystem(SDL_INIT_TIMER) != 0) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
@ -531,28 +571,28 @@ int main(int argc, char *argv[])
|
||||
// SDL 2.0.12 changes the default behavior to use the button label rather than the button
|
||||
// position as most other software does. Set this back to 0 to stay consistent with prior
|
||||
// releases of Moonlight.
|
||||
SDL_SetHint("SDL_GAMECONTROLLER_USE_BUTTON_LABELS", "0");
|
||||
SDL_SetHint(SDL_HINT_GAMECONTROLLER_USE_BUTTON_LABELS, "0");
|
||||
|
||||
// Disable relative mouse scaling to renderer size or logical DPI. We want to send
|
||||
// the mouse motion exactly how it was given to us.
|
||||
SDL_SetHint("SDL_MOUSE_RELATIVE_SCALING", "0");
|
||||
SDL_SetHint(SDL_HINT_MOUSE_RELATIVE_SCALING, "0");
|
||||
|
||||
// Set our app name for SDL to use with PulseAudio and PipeWire. This matches what we
|
||||
// provide as our app name to libsoundio too. On SDL 2.0.18+, SDL_APP_NAME is also used
|
||||
// for screensaver inhibitor reporting.
|
||||
SDL_SetHint("SDL_AUDIO_DEVICE_APP_NAME", "Moonlight");
|
||||
SDL_SetHint("SDL_APP_NAME", "Moonlight");
|
||||
SDL_SetHint(SDL_HINT_AUDIO_DEVICE_APP_NAME, "Moonlight");
|
||||
SDL_SetHint(SDL_HINT_APP_NAME, "Moonlight");
|
||||
|
||||
// We handle capturing the mouse ourselves when it leaves the window, so we don't need
|
||||
// SDL doing it for us behind our backs.
|
||||
SDL_SetHint("SDL_MOUSE_AUTO_CAPTURE", "0");
|
||||
SDL_SetHint(SDL_HINT_MOUSE_AUTO_CAPTURE, "0");
|
||||
|
||||
// SDL will try to lock the mouse cursor on Wayland if it's not visible in order to
|
||||
// support applications that assume they can warp the cursor (which isn't possible
|
||||
// on Wayland). We don't want this behavior because it interferes with seamless mouse
|
||||
// mode when toggling between windowed and fullscreen modes by unexpectedly locking
|
||||
// the mouse cursor.
|
||||
SDL_SetHint("SDL_VIDEO_WAYLAND_EMULATE_MOUSE_WARP", "0");
|
||||
SDL_SetHint(SDL_HINT_VIDEO_WAYLAND_EMULATE_MOUSE_WARP, "0");
|
||||
|
||||
#ifdef QT_DEBUG
|
||||
// Allow thread naming using exceptions on debug builds. SDL doesn't use SEH
|
||||
@ -571,31 +611,32 @@ int main(int argc, char *argv[])
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef Q_OS_WIN32
|
||||
// If we don't have stdout or stderr handles (which will normally be the case
|
||||
// since we're a /SUBSYSTEM:WINDOWS app), attach to our parent console and use
|
||||
// that for stdout and stderr.
|
||||
//
|
||||
// If we do have stdout or stderr handles, that means the user has used standard
|
||||
// handle redirection. In that case, we don't want to override those handles.
|
||||
if (AttachConsole(ATTACH_PARENT_PROCESS)) {
|
||||
// If we didn't have an old stdout/stderr handle, use the new CONOUT$ handle
|
||||
if (IS_UNSPECIFIED_HANDLE(oldConOut)) {
|
||||
freopen("CONOUT$", "w", stdout);
|
||||
setvbuf(stdout, NULL, _IONBF, 0);
|
||||
}
|
||||
if (IS_UNSPECIFIED_HANDLE(oldConErr)) {
|
||||
freopen("CONOUT$", "w", stderr);
|
||||
setvbuf(stderr, NULL, _IONBF, 0);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
GlobalCommandLineParser parser;
|
||||
GlobalCommandLineParser::ParseResult commandLineParserResult = parser.parse(app.arguments());
|
||||
switch (commandLineParserResult) {
|
||||
case GlobalCommandLineParser::ListRequested:
|
||||
// Don't log to the console since it will jumble the command output
|
||||
s_SuppressVerboseOutput = true;
|
||||
#ifdef Q_OS_WIN32
|
||||
// If we don't have stdout or stderr handles (which will normally be the case
|
||||
// since we're a /SUBSYSTEM:WINDOWS app), attach to our parent console and use
|
||||
// that for stdout and stderr.
|
||||
//
|
||||
// If we do have stdout or stderr handles, that means the user has used standard
|
||||
// handle redirection. In that case, we don't want to override those handles.
|
||||
if (AttachConsole(ATTACH_PARENT_PROCESS)) {
|
||||
// If we didn't have an old stdout/stderr handle, use the new CONOUT$ handle
|
||||
if (IS_UNSPECIFIED_HANDLE(oldConOut)) {
|
||||
freopen("CONOUT$", "w", stdout);
|
||||
setvbuf(stdout, NULL, _IONBF, 0);
|
||||
}
|
||||
if (IS_UNSPECIFIED_HANDLE(oldConErr)) {
|
||||
freopen("CONOUT$", "w", stderr);
|
||||
setvbuf(stderr, NULL, _IONBF, 0);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
@ -669,7 +710,7 @@ int main(int argc, char *argv[])
|
||||
#endif
|
||||
|
||||
// This is necessary to show our icon correctly on Wayland
|
||||
app.setDesktopFileName("com.moonlight_stream.Moonlight.desktop");
|
||||
app.setDesktopFileName("com.moonlight_stream.Moonlight");
|
||||
qputenv("SDL_VIDEO_WAYLAND_WMCLASS", "com.moonlight_stream.Moonlight");
|
||||
qputenv("SDL_VIDEO_X11_WMCLASS", "com.moonlight_stream.Moonlight");
|
||||
|
||||
@ -719,6 +760,12 @@ int main(int argc, char *argv[])
|
||||
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_MATERIAL_VARIANT")) {
|
||||
qputenv("QT_QUICK_CONTROLS_MATERIAL_VARIANT", "Dense");
|
||||
}
|
||||
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_MATERIAL_PRIMARY")) {
|
||||
// Qt 6.9 began to use a different shade of Material.Indigo when we use a dark theme
|
||||
// (which is all the time). The new color looks washed out, so manually specify the
|
||||
// old primary color unless the user overrides it themselves.
|
||||
qputenv("QT_QUICK_CONTROLS_MATERIAL_PRIMARY", "#3F51B5");
|
||||
}
|
||||
|
||||
QQmlApplicationEngine engine;
|
||||
QString initialView;
|
||||
@ -784,6 +831,23 @@ int main(int argc, char *argv[])
|
||||
// sometimes freezing and blocking process exit.
|
||||
QThreadPool::globalInstance()->waitForDone(30000);
|
||||
|
||||
// Restore the default logger for all libraries before shutting down ours
|
||||
#if SDL_VERSION_ATLEAST(3, 0, 0)
|
||||
SDL_SetLogOutputFunction(SDL_GetDefaultLogOutputFunction(), nullptr);
|
||||
#else
|
||||
SDL_LogSetOutputFunction(oldSdlLogFn, oldSdlLogUserdata);
|
||||
#endif
|
||||
qInstallMessageHandler(nullptr);
|
||||
#ifdef HAVE_FFMPEG
|
||||
av_log_set_callback(av_log_default_callback);
|
||||
#endif
|
||||
|
||||
// We should not be in async logging mode anymore
|
||||
Q_ASSERT(g_AsyncLoggingEnabled == 0);
|
||||
|
||||
// Wait for pending log messages to be printed
|
||||
s_LoggerThread.waitForDone();
|
||||
|
||||
#ifdef Q_OS_WIN32
|
||||
// Without an explicit flush, console redirection for the list command
|
||||
// doesn't work reliably (sometimes the target file contains no text).
|
||||
|
||||
154
app/masterhook.c
154
app/masterhook.c
@ -9,17 +9,21 @@
|
||||
// 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
|
||||
|
||||
#ifndef _GNU_SOURCE
|
||||
#define _GNU_SOURCE
|
||||
#endif
|
||||
|
||||
// NOTE: This file MUST NOT include fcntl.h due to open() -> open64()
|
||||
// redirection that happens when _FILE_OFFSET_BITS=64!
|
||||
// See masterhook_internal.c for details.
|
||||
|
||||
#include <SDL.h>
|
||||
#include "SDL_compat.h"
|
||||
#include <dlfcn.h>
|
||||
#include <unistd.h>
|
||||
#include <errno.h>
|
||||
#include <stdbool.h>
|
||||
#include <sys/stat.h>
|
||||
#include <pthread.h>
|
||||
|
||||
#include <xf86drm.h>
|
||||
#include <xf86drmMode.h>
|
||||
@ -32,12 +36,44 @@
|
||||
// hooks, that is definitely not trivial.
|
||||
#if SDL_VERSION_ATLEAST(2, 0, 15)
|
||||
|
||||
// We don't include fcntl.h, so we have to define this ourselves
|
||||
typedef int (*fn_open_t)(const char *pathname, int flags, ...);
|
||||
|
||||
// Pointers to the real implementations of these libdrm functions
|
||||
static pthread_once_t s_InitDrmFunctions = PTHREAD_ONCE_INIT;
|
||||
static typeof(drmModeSetCrtc)* fn_drmModeSetCrtc;
|
||||
static typeof(drmModePageFlip)* fn_drmModePageFlip;
|
||||
static typeof(drmModeAtomicCommit)* fn_drmModeAtomicCommit;
|
||||
|
||||
static void lookupRealDrmFunctions() {
|
||||
fn_drmModeSetCrtc = dlsym(RTLD_NEXT, "drmModeSetCrtc");
|
||||
fn_drmModePageFlip = dlsym(RTLD_NEXT, "drmModePageFlip");
|
||||
fn_drmModeAtomicCommit = dlsym(RTLD_NEXT, "drmModeAtomicCommit");
|
||||
}
|
||||
|
||||
// Pointers to the real implementations of these libc functions
|
||||
static pthread_once_t s_InitLibCFunctions = PTHREAD_ONCE_INIT;
|
||||
static fn_open_t *fn_open;
|
||||
static fn_open_t *fn_open64;
|
||||
static typeof(close) *fn_close;
|
||||
|
||||
static void lookupRealLibCFunctions() {
|
||||
fn_open = dlsym(RTLD_NEXT, "open");
|
||||
fn_open64 = dlsym(RTLD_NEXT, "open64");
|
||||
fn_close = dlsym(RTLD_NEXT, "close");
|
||||
}
|
||||
|
||||
// Qt's DRM master FD grabbed by our hook
|
||||
int g_QtDrmMasterFd = -1;
|
||||
struct stat g_DrmMasterStat;
|
||||
|
||||
// The DRM master FD created for SDL
|
||||
int g_SdlDrmMasterFd = -1;
|
||||
// Last CRTC state for us to restore later
|
||||
drmModeCrtcPtr g_QtCrtcState;
|
||||
uint32_t* g_QtCrtcConnectors;
|
||||
int g_QtCrtcConnectorCount;
|
||||
|
||||
bool removeSdlFd(int fd);
|
||||
int takeMasterFromSdlFd(void);
|
||||
|
||||
int drmIsMaster(int fd)
|
||||
{
|
||||
@ -51,6 +87,9 @@ int drmModeSetCrtc(int fd, uint32_t crtcId, uint32_t bufferId,
|
||||
uint32_t x, uint32_t y, uint32_t *connectors, int count,
|
||||
drmModeModeInfoPtr mode)
|
||||
{
|
||||
// Lookup the real libdrm function pointers if we haven't yet
|
||||
pthread_once(&s_InitDrmFunctions, lookupRealDrmFunctions);
|
||||
|
||||
// Grab the first DRM Master FD that makes it in here. This will be the Qt
|
||||
// EGLFS backend's DRM FD, on which we will call drmDropMaster() later.
|
||||
if (g_QtDrmMasterFd == -1 && drmIsMaster(fd)) {
|
||||
@ -62,13 +101,53 @@ int drmModeSetCrtc(int fd, uint32_t crtcId, uint32_t bufferId,
|
||||
}
|
||||
|
||||
// Call into the real thing
|
||||
return ((typeof(drmModeSetCrtc)*)dlsym(RTLD_NEXT, __FUNCTION__))(fd, crtcId, bufferId, x, y, connectors, count, mode);
|
||||
int err = fn_drmModeSetCrtc(fd, crtcId, bufferId, x, y, connectors, count, mode);
|
||||
if (err == 0 && fd == g_QtDrmMasterFd) {
|
||||
// Free old CRTC state (if any)
|
||||
if (g_QtCrtcState) {
|
||||
drmModeFreeCrtc(g_QtCrtcState);
|
||||
}
|
||||
if (g_QtCrtcConnectors) {
|
||||
free(g_QtCrtcConnectors);
|
||||
}
|
||||
|
||||
// Store the CRTC configuration so we can restore it later
|
||||
g_QtCrtcState = drmModeGetCrtc(fd, crtcId);
|
||||
g_QtCrtcConnectors = calloc(count, sizeof(*g_QtCrtcConnectors));
|
||||
memcpy(g_QtCrtcConnectors, connectors, count * sizeof(*connectors));
|
||||
g_QtCrtcConnectorCount = count;
|
||||
}
|
||||
return err;
|
||||
}
|
||||
|
||||
// This hook will temporarily retake DRM master to allow Qt to render while SDL has a DRM FD open
|
||||
int drmModePageFlip(int fd, uint32_t crtc_id, uint32_t fb_id, uint32_t flags, void *user_data)
|
||||
{
|
||||
// Lookup the real libdrm function pointers if we haven't yet
|
||||
pthread_once(&s_InitDrmFunctions, lookupRealDrmFunctions);
|
||||
|
||||
// Call into the real thing
|
||||
int err = fn_drmModePageFlip(fd, crtc_id, fb_id, flags, user_data);
|
||||
if (err == -EACCES && fd == g_QtDrmMasterFd) {
|
||||
// If SDL took master from us, try to grab it back temporarily
|
||||
int oldMasterFd = takeMasterFromSdlFd();
|
||||
drmSetMaster(fd);
|
||||
err = fn_drmModePageFlip(fd, crtc_id, fb_id, flags, user_data);
|
||||
drmDropMaster(fd);
|
||||
if (oldMasterFd != -1) {
|
||||
drmSetMaster(oldMasterFd);
|
||||
}
|
||||
}
|
||||
return err;
|
||||
}
|
||||
|
||||
// This hook will handle atomic DRM rendering
|
||||
int drmModeAtomicCommit(int fd, drmModeAtomicReqPtr req,
|
||||
uint32_t flags, void *user_data)
|
||||
{
|
||||
// Lookup the real libdrm function pointers if we haven't yet
|
||||
pthread_once(&s_InitDrmFunctions, lookupRealDrmFunctions);
|
||||
|
||||
// Grab the first DRM Master FD that makes it in here. This will be the Qt
|
||||
// EGLFS backend's DRM FD, on which we will call drmDropMaster() later.
|
||||
if (g_QtDrmMasterFd == -1 && drmIsMaster(fd)) {
|
||||
@ -80,29 +159,46 @@ int drmModeAtomicCommit(int fd, drmModeAtomicReqPtr req,
|
||||
}
|
||||
|
||||
// Call into the real thing
|
||||
return ((typeof(drmModeAtomicCommit)*)dlsym(RTLD_NEXT, __FUNCTION__))(fd, req, flags, user_data);
|
||||
int err = fn_drmModeAtomicCommit(fd, req, flags, user_data);
|
||||
if (err == -EACCES && fd == g_QtDrmMasterFd) {
|
||||
// If SDL took master from us, try to grab it back temporarily
|
||||
int oldMasterFd = takeMasterFromSdlFd();
|
||||
drmSetMaster(fd);
|
||||
err = fn_drmModeAtomicCommit(fd, req, flags, user_data);
|
||||
drmDropMaster(fd);
|
||||
if (oldMasterFd != -1) {
|
||||
drmSetMaster(oldMasterFd);
|
||||
}
|
||||
}
|
||||
return err;
|
||||
}
|
||||
|
||||
// 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 openHook(const char *funcname, const char *pathname, int flags, va_list va);
|
||||
int openHook(fn_open_t *real_open, typeof(close) *real_close, const char *pathname, int flags, va_list va);
|
||||
|
||||
int open(const char *pathname, int flags, ...)
|
||||
{
|
||||
// Lookup the real libc functions if we haven't yet
|
||||
pthread_once(&s_InitLibCFunctions, lookupRealLibCFunctions);
|
||||
|
||||
va_list va;
|
||||
va_start(va, flags);
|
||||
int fd = openHook(__FUNCTION__, pathname, flags, va);
|
||||
int fd = openHook(fn_open, fn_close, pathname, flags, va);
|
||||
va_end(va);
|
||||
return fd;
|
||||
}
|
||||
|
||||
int open64(const char *pathname, int flags, ...)
|
||||
{
|
||||
// Lookup the real libc functions if we haven't yet
|
||||
pthread_once(&s_InitLibCFunctions, lookupRealLibCFunctions);
|
||||
|
||||
va_list va;
|
||||
va_start(va, flags);
|
||||
int fd = openHook(__FUNCTION__, pathname, flags, va);
|
||||
int fd = openHook(fn_open64, fn_close, pathname, flags, va);
|
||||
va_end(va);
|
||||
return fd;
|
||||
}
|
||||
@ -111,23 +207,43 @@ int open64(const char *pathname, int flags, ...)
|
||||
// after SDL closes its DRM FD.
|
||||
int close(int fd)
|
||||
{
|
||||
// Lookup the real libc functions if we haven't yet
|
||||
pthread_once(&s_InitLibCFunctions, lookupRealLibCFunctions);
|
||||
|
||||
// Remove this entry from the SDL FD table
|
||||
bool lastSdlFd = removeSdlFd(fd);
|
||||
|
||||
// Call the real thing
|
||||
int ret = ((typeof(close)*)dlsym(RTLD_NEXT, __FUNCTION__))(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) {
|
||||
int ret = fn_close(fd);
|
||||
|
||||
// If we closed the last SDL FD, restore master to the Qt FD
|
||||
if (ret == 0 && lastSdlFd) {
|
||||
if (drmSetMaster(g_QtDrmMasterFd) < 0) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Failed to restore master to Qt DRM FD: %d",
|
||||
errno);
|
||||
}
|
||||
|
||||
// Reset the CRTC state to how Qt configured it
|
||||
if (g_QtCrtcState) {
|
||||
SDL_assert(fn_drmModeSetCrtc != NULL);
|
||||
int err = fn_drmModeSetCrtc(g_QtDrmMasterFd,
|
||||
g_QtCrtcState->crtc_id,
|
||||
g_QtCrtcState->buffer_id,
|
||||
g_QtCrtcState->x,
|
||||
g_QtCrtcState->y,
|
||||
g_QtCrtcConnectors,
|
||||
g_QtCrtcConnectorCount,
|
||||
g_QtCrtcState->mode_valid ?
|
||||
&g_QtCrtcState->mode : NULL);
|
||||
if (err < 0) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Failed to restore master to Qt DRM FD: %d",
|
||||
"Failed to restore CRTC state to Qt DRM FD: %d",
|
||||
errno);
|
||||
}
|
||||
|
||||
g_SdlDrmMasterFd = -1;
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
@ -2,14 +2,17 @@
|
||||
// which must be in a separate compilation unit due to fcntl.h doing
|
||||
// unwanted redirection of open() to open64().
|
||||
|
||||
#ifndef _GNU_SOURCE
|
||||
#define _GNU_SOURCE
|
||||
#endif
|
||||
|
||||
#include <SDL.h>
|
||||
#include <dlfcn.h>
|
||||
#include "SDL_compat.h"
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <errno.h>
|
||||
#include <stdbool.h>
|
||||
#include <sys/stat.h>
|
||||
#include <pthread.h>
|
||||
|
||||
#include <xf86drm.h>
|
||||
#include <xf86drmMode.h>
|
||||
@ -28,9 +31,75 @@
|
||||
|
||||
extern int g_QtDrmMasterFd;
|
||||
extern struct stat g_DrmMasterStat;
|
||||
extern int g_SdlDrmMasterFd;
|
||||
|
||||
int openHook(const char *funcname, const char *pathname, int flags, va_list va)
|
||||
#define MAX_SDL_FD_COUNT 8
|
||||
int g_SdlDrmMasterFds[MAX_SDL_FD_COUNT];
|
||||
int g_SdlDrmMasterFdCount = 0;
|
||||
pthread_mutex_t g_FdTableLock = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
// Caller must hold g_FdTableLock
|
||||
int getSdlFdEntryIndex(bool unused)
|
||||
{
|
||||
for (int i = 0; i < MAX_SDL_FD_COUNT; i++) {
|
||||
// We slightly bend the FD rules here by treating 0
|
||||
// as invalid since that's our global default value.
|
||||
if (unused && g_SdlDrmMasterFds[i] <= 0) {
|
||||
return i;
|
||||
}
|
||||
else if (!unused && g_SdlDrmMasterFds[i] > 0) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Returns true if the final SDL FD was removed
|
||||
bool removeSdlFd(int fd)
|
||||
{
|
||||
pthread_mutex_lock(&g_FdTableLock);
|
||||
if (g_SdlDrmMasterFdCount != 0) {
|
||||
// Clear the entry for this fd from the table
|
||||
for (int i = 0; i < MAX_SDL_FD_COUNT; i++) {
|
||||
if (fd == g_SdlDrmMasterFds[i]) {
|
||||
g_SdlDrmMasterFds[i] = -1;
|
||||
g_SdlDrmMasterFdCount--;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (g_SdlDrmMasterFdCount == 0) {
|
||||
pthread_mutex_unlock(&g_FdTableLock);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
pthread_mutex_unlock(&g_FdTableLock);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Returns the previous master FD or -1 if none
|
||||
int takeMasterFromSdlFd()
|
||||
{
|
||||
int fd = -1;
|
||||
|
||||
// Since all SDL FDs are actually dups of each other
|
||||
// we can take master from any one of them.
|
||||
pthread_mutex_lock(&g_FdTableLock);
|
||||
int fdIndex = getSdlFdEntryIndex(false);
|
||||
if (fdIndex != -1) {
|
||||
fd = g_SdlDrmMasterFds[fdIndex];
|
||||
}
|
||||
pthread_mutex_unlock(&g_FdTableLock);
|
||||
|
||||
if (fd >= 0 && drmDropMaster(fd) == 0) {
|
||||
return fd;
|
||||
}
|
||||
else {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
int openHook(typeof(open) *real_open, typeof(close) *real_close, const char *pathname, int flags, va_list va)
|
||||
{
|
||||
int fd;
|
||||
mode_t mode;
|
||||
@ -38,11 +107,11 @@ int openHook(const char *funcname, const char *pathname, int flags, va_list va)
|
||||
// Call the real thing to do the open operation
|
||||
if (__OPEN_NEEDS_MODE(flags)) {
|
||||
mode = va_arg(va, mode_t);
|
||||
fd = ((typeof(open)*)dlsym(RTLD_NEXT, funcname))(pathname, flags, mode);
|
||||
fd = real_open(pathname, flags, mode);
|
||||
}
|
||||
else {
|
||||
mode = 0;
|
||||
fd = ((typeof(open)*)dlsym(RTLD_NEXT, funcname))(pathname, flags);
|
||||
fd = real_open(pathname, flags);
|
||||
}
|
||||
|
||||
// If the file was successfully opened and we have a DRM master FD,
|
||||
@ -55,32 +124,67 @@ int openHook(const char *funcname, const char *pathname, int flags, va_list va)
|
||||
fstat(fd, &fdstat);
|
||||
if (g_DrmMasterStat.st_dev == fdstat.st_dev &&
|
||||
g_DrmMasterStat.st_ino == fdstat.st_ino) {
|
||||
int freeFdIndex;
|
||||
int allocatedFdIndex;
|
||||
|
||||
// It is our device. Time to do the magic!
|
||||
pthread_mutex_lock(&g_FdTableLock);
|
||||
|
||||
// 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) {
|
||||
// Get a free index for us to put the new entry
|
||||
freeFdIndex = getSdlFdEntryIndex(true);
|
||||
if (freeFdIndex < 0) {
|
||||
pthread_mutex_unlock(&g_FdTableLock);
|
||||
SDL_assert(freeFdIndex >= 0);
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Failed to drop master on Qt DRM FD: %d",
|
||||
errno);
|
||||
"No unused SDL FD table entries!");
|
||||
// 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);
|
||||
if (__OPEN_NEEDS_MODE(flags)) {
|
||||
fd = ((typeof(open)*)dlsym(RTLD_NEXT, funcname))(pathname, flags, mode);
|
||||
// Check if we have an allocated entry already
|
||||
allocatedFdIndex = getSdlFdEntryIndex(false);
|
||||
if (allocatedFdIndex >= 0) {
|
||||
// Close fd that we opened earlier (skipping our close() hook)
|
||||
real_close(fd);
|
||||
|
||||
// dup() an existing FD into the unused slot
|
||||
fd = dup(g_SdlDrmMasterFds[allocatedFdIndex]);
|
||||
}
|
||||
else {
|
||||
fd = ((typeof(open)*)dlsym(RTLD_NEXT, funcname))(pathname, flags);
|
||||
// Drop master on Qt's FD so we can pick it up for SDL.
|
||||
if (drmDropMaster(g_QtDrmMasterFd) < 0) {
|
||||
pthread_mutex_unlock(&g_FdTableLock);
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Failed to drop master on Qt DRM FD: %d",
|
||||
errno);
|
||||
// Hope for the best
|
||||
return fd;
|
||||
}
|
||||
|
||||
// Close fd that we opened earlier (skipping our close() hook)
|
||||
real_close(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.
|
||||
if (__OPEN_NEEDS_MODE(flags)) {
|
||||
fd = real_open(pathname, flags, mode);
|
||||
}
|
||||
else {
|
||||
fd = real_open(pathname, flags);
|
||||
}
|
||||
}
|
||||
g_SdlDrmMasterFd = fd;
|
||||
|
||||
if (fd >= 0) {
|
||||
// Start with DRM master on the new FD
|
||||
drmSetMaster(fd);
|
||||
|
||||
// Insert the FD into the table
|
||||
g_SdlDrmMasterFds[freeFdIndex] = fd;
|
||||
g_SdlDrmMasterFdCount++;
|
||||
}
|
||||
|
||||
pthread_mutex_unlock(&g_FdTableLock);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -32,7 +32,9 @@ QString Path::getQmlCacheDir()
|
||||
QByteArray Path::readDataFile(QString fileName)
|
||||
{
|
||||
QFile dataFile(getDataFilePath(fileName));
|
||||
dataFile.open(QIODevice::ReadOnly);
|
||||
if (!dataFile.open(QIODevice::ReadOnly)) {
|
||||
return {};
|
||||
}
|
||||
return dataFile.readAll();
|
||||
}
|
||||
|
||||
@ -46,8 +48,9 @@ void Path::writeCacheFile(QString fileName, QByteArray data)
|
||||
}
|
||||
|
||||
QFile dataFile(cacheDir.absoluteFilePath(fileName));
|
||||
dataFile.open(QIODevice::WriteOnly);
|
||||
dataFile.write(data);
|
||||
if (dataFile.open(QIODevice::WriteOnly)) {
|
||||
dataFile.write(data);
|
||||
}
|
||||
}
|
||||
|
||||
void Path::deleteCacheFile(QString fileName)
|
||||
|
||||
@ -70,6 +70,12 @@
|
||||
<file>languages/qml_lt.qm</file>
|
||||
<file>languages/qml_et.ts</file>
|
||||
<file>languages/qml_et.qm</file>
|
||||
<file>languages/qml_bg.ts</file>
|
||||
<file>languages/qml_bg.qm</file>
|
||||
<file>languages/qml_eo.ts</file>
|
||||
<file>languages/qml_eo.qm</file>
|
||||
<file>languages/qml_ta.ts</file>
|
||||
<file>languages/qml_ta.qm</file>
|
||||
<!-- Don't include pt_BR until it is more complete -->
|
||||
<!--file>languages/qml_pt_BR.qm</file-->
|
||||
<!--file>languages/qml_pt_BR.ts</file-->
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
|
||||
#include <QDir>
|
||||
|
||||
#include <SDL.h>
|
||||
#include "SDL_compat.h"
|
||||
|
||||
#define SER_GAMEPADMAPPING "gcmapping"
|
||||
|
||||
|
||||
@ -16,6 +16,7 @@
|
||||
#define SER_FPS "fps"
|
||||
#define SER_BITRATE "bitrate"
|
||||
#define SER_UNLOCK_BITRATE "unlockbitrate"
|
||||
#define SER_AUTOADJUSTBITRATE "autoadjustbitrate"
|
||||
#define SER_FULLSCREEN "fullscreen"
|
||||
#define SER_VSYNC "vsync"
|
||||
#define SER_GAMEOPTS "gameopts"
|
||||
@ -34,6 +35,7 @@
|
||||
#define SER_STARTWINDOWED "startwindowed"
|
||||
#define SER_FRAMEPACING "framepacing"
|
||||
#define SER_CONNWARNINGS "connwarnings"
|
||||
#define SER_CONFWARNINGS "confwarnings"
|
||||
#define SER_UIDISPLAYMODE "uidisplaymode"
|
||||
#define SER_RICHPRESENCE "richpresence"
|
||||
#define SER_GAMEPADMOUSE "gamepadmouse"
|
||||
@ -122,6 +124,7 @@ void StreamingPreferences::reload()
|
||||
enableYUV444 = settings.value(SER_YUV444, false).toBool();
|
||||
bitrateKbps = settings.value(SER_BITRATE, getDefaultBitrate(width, height, fps, enableYUV444)).toInt();
|
||||
unlockBitrate = settings.value(SER_UNLOCK_BITRATE, false).toBool();
|
||||
autoAdjustBitrate = settings.value(SER_AUTOADJUSTBITRATE, true).toBool();
|
||||
enableVsync = settings.value(SER_VSYNC, true).toBool();
|
||||
gameOptimizations = settings.value(SER_GAMEOPTS, true).toBool();
|
||||
playAudioOnHost = settings.value(SER_HOSTAUDIO, false).toBool();
|
||||
@ -132,6 +135,7 @@ void StreamingPreferences::reload()
|
||||
absoluteTouchMode = settings.value(SER_ABSTOUCHMODE, true).toBool();
|
||||
framePacing = settings.value(SER_FRAMEPACING, false).toBool();
|
||||
connectionWarnings = settings.value(SER_CONNWARNINGS, true).toBool();
|
||||
configurationWarnings = settings.value(SER_CONFWARNINGS, true).toBool();
|
||||
richPresence = settings.value(SER_RICHPRESENCE, true).toBool();
|
||||
gamepadMouse = settings.value(SER_GAMEPADMOUSE, true).toBool();
|
||||
detectNetworkBlocking = settings.value(SER_DETECTNETBLOCKING, true).toBool();
|
||||
@ -297,6 +301,12 @@ QString StreamingPreferences::getSuffixFromLanguage(StreamingPreferences::Langua
|
||||
return "lt";
|
||||
case LANG_ET:
|
||||
return "et";
|
||||
case LANG_BG:
|
||||
return "bg";
|
||||
case LANG_EO:
|
||||
return "eo";
|
||||
case LANG_TA:
|
||||
return "ta";
|
||||
case LANG_AUTO:
|
||||
default:
|
||||
return QLocale::system().name();
|
||||
@ -312,6 +322,7 @@ void StreamingPreferences::save()
|
||||
settings.setValue(SER_FPS, fps);
|
||||
settings.setValue(SER_BITRATE, bitrateKbps);
|
||||
settings.setValue(SER_UNLOCK_BITRATE, unlockBitrate);
|
||||
settings.setValue(SER_AUTOADJUSTBITRATE, autoAdjustBitrate);
|
||||
settings.setValue(SER_VSYNC, enableVsync);
|
||||
settings.setValue(SER_GAMEOPTS, gameOptimizations);
|
||||
settings.setValue(SER_HOSTAUDIO, playAudioOnHost);
|
||||
@ -322,6 +333,7 @@ void StreamingPreferences::save()
|
||||
settings.setValue(SER_ABSTOUCHMODE, absoluteTouchMode);
|
||||
settings.setValue(SER_FRAMEPACING, framePacing);
|
||||
settings.setValue(SER_CONNWARNINGS, connectionWarnings);
|
||||
settings.setValue(SER_CONFWARNINGS, configurationWarnings);
|
||||
settings.setValue(SER_RICHPRESENCE, richPresence);
|
||||
settings.setValue(SER_GAMEPADMOUSE, gamepadMouse);
|
||||
settings.setValue(SER_PACKETSIZE, packetSize);
|
||||
|
||||
@ -94,6 +94,9 @@ public:
|
||||
LANG_CKB,
|
||||
LANG_LT,
|
||||
LANG_ET,
|
||||
LANG_BG,
|
||||
LANG_EO,
|
||||
LANG_TA,
|
||||
};
|
||||
Q_ENUM(Language);
|
||||
|
||||
@ -110,6 +113,7 @@ public:
|
||||
Q_PROPERTY(int fps MEMBER fps NOTIFY displayModeChanged)
|
||||
Q_PROPERTY(int bitrateKbps MEMBER bitrateKbps NOTIFY bitrateChanged)
|
||||
Q_PROPERTY(bool unlockBitrate MEMBER unlockBitrate NOTIFY unlockBitrateChanged)
|
||||
Q_PROPERTY(bool autoAdjustBitrate MEMBER autoAdjustBitrate NOTIFY autoAdjustBitrateChanged)
|
||||
Q_PROPERTY(bool enableVsync MEMBER enableVsync NOTIFY enableVsyncChanged)
|
||||
Q_PROPERTY(bool gameOptimizations MEMBER gameOptimizations NOTIFY gameOptimizationsChanged)
|
||||
Q_PROPERTY(bool playAudioOnHost MEMBER playAudioOnHost NOTIFY playAudioOnHostChanged)
|
||||
@ -120,6 +124,7 @@ public:
|
||||
Q_PROPERTY(bool absoluteTouchMode MEMBER absoluteTouchMode NOTIFY absoluteTouchModeChanged)
|
||||
Q_PROPERTY(bool framePacing MEMBER framePacing NOTIFY framePacingChanged)
|
||||
Q_PROPERTY(bool connectionWarnings MEMBER connectionWarnings NOTIFY connectionWarningsChanged)
|
||||
Q_PROPERTY(bool configurationWarnings MEMBER configurationWarnings NOTIFY configurationWarningsChanged)
|
||||
Q_PROPERTY(bool richPresence MEMBER richPresence NOTIFY richPresenceChanged)
|
||||
Q_PROPERTY(bool gamepadMouse MEMBER gamepadMouse NOTIFY gamepadMouseChanged)
|
||||
Q_PROPERTY(bool detectNetworkBlocking MEMBER detectNetworkBlocking NOTIFY detectNetworkBlockingChanged)
|
||||
@ -149,6 +154,7 @@ public:
|
||||
int fps;
|
||||
int bitrateKbps;
|
||||
bool unlockBitrate;
|
||||
bool autoAdjustBitrate;
|
||||
bool enableVsync;
|
||||
bool gameOptimizations;
|
||||
bool playAudioOnHost;
|
||||
@ -159,6 +165,7 @@ public:
|
||||
bool absoluteTouchMode;
|
||||
bool framePacing;
|
||||
bool connectionWarnings;
|
||||
bool configurationWarnings;
|
||||
bool richPresence;
|
||||
bool gamepadMouse;
|
||||
bool detectNetworkBlocking;
|
||||
@ -185,6 +192,7 @@ signals:
|
||||
void displayModeChanged();
|
||||
void bitrateChanged();
|
||||
void unlockBitrateChanged();
|
||||
void autoAdjustBitrateChanged();
|
||||
void enableVsyncChanged();
|
||||
void gameOptimizationsChanged();
|
||||
void playAudioOnHostChanged();
|
||||
@ -203,6 +211,7 @@ signals:
|
||||
void windowModeChanged();
|
||||
void framePacingChanged();
|
||||
void connectionWarningsChanged();
|
||||
void configurationWarningsChanged();
|
||||
void richPresenceChanged();
|
||||
void gamepadMouseChanged();
|
||||
void detectNetworkBlockingChanged();
|
||||
|
||||
@ -1,10 +1,6 @@
|
||||
#include "../session.h"
|
||||
#include "renderers/renderer.h"
|
||||
|
||||
#ifdef HAVE_SOUNDIO
|
||||
#include "renderers/soundioaudiorenderer.h"
|
||||
#endif
|
||||
|
||||
#ifdef HAVE_SLAUDIO
|
||||
#include "renderers/slaud.h"
|
||||
#endif
|
||||
@ -29,12 +25,6 @@ IAudioRenderer* Session::createAudioRenderer(const POPUS_MULTISTREAM_CONFIGURATI
|
||||
TRY_INIT_RENDERER(SdlAudioRenderer, opusConfig)
|
||||
return nullptr;
|
||||
}
|
||||
#ifdef HAVE_SOUNDIO
|
||||
else if (mlAudio == "libsoundio") {
|
||||
TRY_INIT_RENDERER(SoundIoAudioRenderer, opusConfig)
|
||||
return nullptr;
|
||||
}
|
||||
#endif
|
||||
#if defined(HAVE_SLAUDIO)
|
||||
else if (mlAudio == "slaudio") {
|
||||
TRY_INIT_RENDERER(SLAudioRenderer, opusConfig)
|
||||
@ -55,11 +45,8 @@ IAudioRenderer* Session::createAudioRenderer(const POPUS_MULTISTREAM_CONFIGURATI
|
||||
TRY_INIT_RENDERER(SLAudioRenderer, opusConfig)
|
||||
#endif
|
||||
|
||||
// Default to SDL and use libsoundio as a fallback
|
||||
// Default to SDL
|
||||
TRY_INIT_RENDERER(SdlAudioRenderer, opusConfig)
|
||||
#ifdef HAVE_SOUNDIO
|
||||
TRY_INIT_RENDERER(SoundIoAudioRenderer, opusConfig)
|
||||
#endif
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
@ -108,21 +95,15 @@ bool Session::initializeAudioRenderer()
|
||||
|
||||
int Session::getAudioRendererCapabilities(int audioConfiguration)
|
||||
{
|
||||
// Build a fake OPUS_MULTISTREAM_CONFIGURATION to give
|
||||
// the renderer the channel count and sample rate.
|
||||
OPUS_MULTISTREAM_CONFIGURATION opusConfig = {};
|
||||
opusConfig.sampleRate = 48000;
|
||||
opusConfig.samplesPerFrame = 240;
|
||||
opusConfig.channelCount = CHANNEL_COUNT_FROM_AUDIO_CONFIGURATION(audioConfiguration);
|
||||
int caps = 0;
|
||||
|
||||
IAudioRenderer* audioRenderer = createAudioRenderer(&opusConfig);
|
||||
if (audioRenderer == nullptr) {
|
||||
return 0;
|
||||
}
|
||||
// All audio renderers support arbitrary audio duration
|
||||
caps |= CAPABILITY_SUPPORTS_ARBITRARY_AUDIO_DURATION;
|
||||
|
||||
int caps = audioRenderer->getCapabilities();
|
||||
|
||||
delete audioRenderer;
|
||||
#ifdef STEAM_LINK
|
||||
// Steam Link devices have slow Opus decoders
|
||||
caps |= CAPABILITY_SLOW_OPUS_DECODER;
|
||||
#endif
|
||||
|
||||
return caps;
|
||||
}
|
||||
|
||||
@ -15,8 +15,6 @@ public:
|
||||
// Return false if an unrecoverable error has occurred and the renderer must be reinitialized
|
||||
virtual bool submitAudio(int bytesWritten) = 0;
|
||||
|
||||
virtual int getCapabilities() = 0;
|
||||
|
||||
virtual void remapChannels(POPUS_MULTISTREAM_CONFIGURATION) {
|
||||
// Use default channel mapping:
|
||||
// 0 - Front Left
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "renderer.h"
|
||||
#include <SDL.h>
|
||||
#include "SDL_compat.h"
|
||||
|
||||
class SdlAudioRenderer : public IAudioRenderer
|
||||
{
|
||||
@ -16,8 +16,6 @@ public:
|
||||
|
||||
virtual bool submitAudio(int bytesWritten);
|
||||
|
||||
virtual int getCapabilities();
|
||||
|
||||
virtual AudioFormat getAudioBufferFormat();
|
||||
|
||||
private:
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
#include "sdl.h"
|
||||
|
||||
#include <Limelight.h>
|
||||
#include <SDL.h>
|
||||
|
||||
SdlAudioRenderer::SdlAudioRenderer()
|
||||
: m_AudioDevice(0),
|
||||
@ -31,14 +30,7 @@ bool SdlAudioRenderer::prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION*
|
||||
// to mitigate this issue. Otherwise, we will buffer up to 3 frames of audio which
|
||||
// is 15 ms at regular 5 ms frames and 30 ms at 10 ms frames for slow connections.
|
||||
// The buffering helps avoid audio underruns due to network jitter.
|
||||
#ifndef Q_OS_DARWIN
|
||||
want.samples = SDL_max(480, opusConfig->samplesPerFrame * 3);
|
||||
#else
|
||||
// HACK: Changing the buffer size can lead to Bluetooth HFP
|
||||
// audio issues on macOS, so we're leaving this alone.
|
||||
// https://github.com/moonlight-stream/moonlight-qt/issues/1071
|
||||
want.samples = SDL_max(480, opusConfig->samplesPerFrame);
|
||||
#endif
|
||||
|
||||
m_FrameSize = opusConfig->samplesPerFrame *
|
||||
opusConfig->channelCount *
|
||||
@ -140,12 +132,6 @@ bool SdlAudioRenderer::submitAudio(int bytesWritten)
|
||||
return true;
|
||||
}
|
||||
|
||||
int SdlAudioRenderer::getCapabilities()
|
||||
{
|
||||
// Direct submit can't be used because we use LiGetPendingAudioDuration()
|
||||
return CAPABILITY_SUPPORTS_ARBITRARY_AUDIO_DURATION;
|
||||
}
|
||||
|
||||
IAudioRenderer::AudioFormat SdlAudioRenderer::getAudioBufferFormat()
|
||||
{
|
||||
return AudioFormat::Float32NE;
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
#include "slaud.h"
|
||||
|
||||
#include <SDL.h>
|
||||
#include "SDL_compat.h"
|
||||
|
||||
SLAudioRenderer::SLAudioRenderer()
|
||||
: m_AudioContext(nullptr),
|
||||
@ -114,11 +114,6 @@ bool SLAudioRenderer::submitAudio(int bytesWritten)
|
||||
return true;
|
||||
}
|
||||
|
||||
int SLAudioRenderer::getCapabilities()
|
||||
{
|
||||
return CAPABILITY_SLOW_OPUS_DECODER | CAPABILITY_SUPPORTS_ARBITRARY_AUDIO_DURATION;
|
||||
}
|
||||
|
||||
IAudioRenderer::AudioFormat SLAudioRenderer::getAudioBufferFormat()
|
||||
{
|
||||
return AudioFormat::Sint16NE;
|
||||
|
||||
@ -16,8 +16,6 @@ public:
|
||||
|
||||
virtual bool submitAudio(int bytesWritten);
|
||||
|
||||
virtual int getCapabilities();
|
||||
|
||||
virtual AudioFormat getAudioBufferFormat();
|
||||
|
||||
virtual void remapChannels(POPUS_MULTISTREAM_CONFIGURATION opusConfig);
|
||||
|
||||
@ -1,487 +0,0 @@
|
||||
#include "soundioaudiorenderer.h"
|
||||
|
||||
#include <SDL.h>
|
||||
|
||||
#include <QtGlobal>
|
||||
|
||||
SoundIoAudioRenderer::SoundIoAudioRenderer()
|
||||
: m_OpusChannelCount(0),
|
||||
m_SoundIo(nullptr),
|
||||
m_Device(nullptr),
|
||||
m_OutputStream(nullptr),
|
||||
m_RingBuffer(nullptr),
|
||||
m_AudioPacketDuration(0),
|
||||
m_Latency(0),
|
||||
m_Errored(false)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
SoundIoAudioRenderer::~SoundIoAudioRenderer()
|
||||
{
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Audio latency: %f",
|
||||
m_Latency);
|
||||
|
||||
if (m_OutputStream != nullptr) {
|
||||
soundio_outstream_destroy(m_OutputStream);
|
||||
}
|
||||
|
||||
// Must be destroyed after the stream is stopped
|
||||
// or we could still get sioWriteCallback() calls.
|
||||
if (m_RingBuffer != nullptr) {
|
||||
soundio_ring_buffer_destroy(m_RingBuffer);
|
||||
}
|
||||
|
||||
if (m_Device != nullptr) {
|
||||
soundio_device_unref(m_Device);
|
||||
}
|
||||
|
||||
if (m_SoundIo != nullptr) {
|
||||
soundio_destroy(m_SoundIo);
|
||||
}
|
||||
}
|
||||
|
||||
int SoundIoAudioRenderer::scoreChannelLayout(const struct SoundIoChannelLayout* layout, const OPUS_MULTISTREAM_CONFIGURATION* opusConfig)
|
||||
{
|
||||
int score = 50;
|
||||
|
||||
// Compute a score for this layout based on how many matching channels
|
||||
// we find (or acceptable alternatives).
|
||||
for (int i = 0; i < layout->channel_count; i++) {
|
||||
if (opusConfig->channelCount >= 2) {
|
||||
switch (layout->channels[i]) {
|
||||
case SoundIoChannelIdFrontLeft:
|
||||
case SoundIoChannelIdFrontRight:
|
||||
score += 2;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (opusConfig->channelCount >= 6) {
|
||||
switch (layout->channels[i]) {
|
||||
case SoundIoChannelIdFrontCenter:
|
||||
case SoundIoChannelIdLfe:
|
||||
score += 2;
|
||||
break;
|
||||
|
||||
case SoundIoChannelIdSideLeft:
|
||||
case SoundIoChannelIdSideRight:
|
||||
score++;
|
||||
break;
|
||||
|
||||
case SoundIoChannelIdBackLeft:
|
||||
case SoundIoChannelIdBackRight:
|
||||
if (opusConfig->channelCount == 6) {
|
||||
// Score back channels higher in 5.1 mode to
|
||||
// discourage selection of side channel layouts.
|
||||
score += 2;
|
||||
}
|
||||
else {
|
||||
// Score back channels normally for 7.1 mode
|
||||
score++;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Now subtract the difference between the desired and actual channel count
|
||||
// to punish layouts that have extra unused speakers.
|
||||
if (opusConfig->channelCount < layout->channel_count) {
|
||||
score -= layout->channel_count - opusConfig->channelCount;
|
||||
}
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
bool SoundIoAudioRenderer::prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig)
|
||||
{
|
||||
m_SoundIo = soundio_create();
|
||||
if (m_SoundIo == nullptr) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"soundio_create() failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
m_SoundIo->app_name = "Moonlight";
|
||||
m_SoundIo->userdata = this;
|
||||
m_SoundIo->on_backend_disconnect = sioBackendDisconnect;
|
||||
m_SoundIo->on_devices_change = sioDevicesChanged;
|
||||
|
||||
int err = soundio_connect(m_SoundIo);
|
||||
if (err != SoundIoErrorNone) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"soundio_connect() failed: %s",
|
||||
soundio_strerror(err));
|
||||
return false;
|
||||
}
|
||||
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Audio backend: %s",
|
||||
soundio_backend_name(m_SoundIo->current_backend));
|
||||
|
||||
// Don't continue if we could only open the dummy backend
|
||||
if (m_SoundIo->current_backend == SoundIoBackendDummy) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Flush events to update with new device arrivals
|
||||
soundio_flush_events(m_SoundIo);
|
||||
|
||||
// Remember the actual channel count for later
|
||||
m_OpusChannelCount = opusConfig->channelCount;
|
||||
|
||||
int outputDeviceIndex = soundio_default_output_device_index(m_SoundIo);
|
||||
if (outputDeviceIndex < 0) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"No output device found");
|
||||
return false;
|
||||
}
|
||||
|
||||
m_Device = soundio_get_output_device(m_SoundIo, outputDeviceIndex);
|
||||
if (m_Device == nullptr) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"soundio_get_output_device() failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Selected audio device: %s",
|
||||
m_Device->name);
|
||||
|
||||
m_OutputStream = soundio_outstream_create(m_Device);
|
||||
if (m_OutputStream == nullptr) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"soundio_outstream_create() failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
m_AudioPacketDuration = (opusConfig->samplesPerFrame / (opusConfig->sampleRate / 1000)) / 1000.0;
|
||||
|
||||
m_OutputStream->format = SoundIoFormatFloat32NE;
|
||||
m_OutputStream->sample_rate = opusConfig->sampleRate;
|
||||
m_OutputStream->software_latency = m_AudioPacketDuration;
|
||||
m_OutputStream->name = "Moonlight";
|
||||
m_OutputStream->userdata = this;
|
||||
m_OutputStream->error_callback = sioErrorCallback;
|
||||
m_OutputStream->write_callback = sioWriteCallback;
|
||||
|
||||
SoundIoChannelLayout bestLayout = m_Device->current_layout;
|
||||
for (int i = 0; i < m_Device->layout_count; i++) {
|
||||
if (scoreChannelLayout(&bestLayout, opusConfig) <
|
||||
scoreChannelLayout(&m_Device->layouts[i], opusConfig)) {
|
||||
bestLayout = m_Device->layouts[i];
|
||||
}
|
||||
}
|
||||
|
||||
if (bestLayout.channel_count < opusConfig->channelCount) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"No compatible channel layouts found. Some channels may not be played!");
|
||||
}
|
||||
|
||||
m_OutputStream->layout = bestLayout;
|
||||
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Native layout: %s (%d channels)",
|
||||
m_OutputStream->layout.name ?
|
||||
m_OutputStream->layout.name : "<UNKNOWN>",
|
||||
m_OutputStream->layout.channel_count);
|
||||
|
||||
err = soundio_outstream_open(m_OutputStream);
|
||||
if (err != SoundIoErrorNone) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"soundio_outstream_open() failed: %s",
|
||||
soundio_strerror(err));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_OutputStream->layout_error) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Channel layout failed: %s",
|
||||
soundio_strerror(m_OutputStream->layout_error));
|
||||
|
||||
// ALSA through PulseAudio appears to fail snd_pcm_set_chmap()
|
||||
// even after claiming the layout is supported (and even on totally
|
||||
// standard layouts like Stereo). We'll just ignore this for ALSA
|
||||
// and only bail if we get an actual failure out of one of these APIs.
|
||||
if (m_SoundIo->current_backend != SoundIoBackendAlsa) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
m_EffectiveLayout = m_OutputStream->layout;
|
||||
for (int i = 0; i < m_EffectiveLayout.channel_count; i++) {
|
||||
if (opusConfig->channelCount == 6) {
|
||||
// For 5.1, replace side L/R with back L/R so our channel position
|
||||
// logic in sioWriteCallback() works.
|
||||
if (m_EffectiveLayout.channels[i] == SoundIoChannelIdSideLeft) {
|
||||
m_EffectiveLayout.channels[i] = SoundIoChannelIdBackLeft;
|
||||
}
|
||||
if (m_EffectiveLayout.channels[i] == SoundIoChannelIdSideRight) {
|
||||
m_EffectiveLayout.channels[i] = SoundIoChannelIdBackRight;
|
||||
}
|
||||
}
|
||||
else if (opusConfig->channelCount == 8) {
|
||||
// For 5.1, replace side L/R with LOC/ROC so our channel position
|
||||
// logic in sioWriteCallback() works.
|
||||
if (m_EffectiveLayout.channels[i] == SoundIoChannelIdSideLeft) {
|
||||
m_EffectiveLayout.channels[i] = SoundIoChannelIdFrontLeftCenter;
|
||||
}
|
||||
if (m_EffectiveLayout.channels[i] == SoundIoChannelIdSideRight) {
|
||||
m_EffectiveLayout.channels[i] = SoundIoChannelIdFrontRightCenter;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int packetsToBuffer;
|
||||
|
||||
if (m_SoundIo->current_backend == SoundIoBackendWasapi) {
|
||||
// 15 ms buffer seems to be fine for WASAPI
|
||||
packetsToBuffer = (int)ceil(0.015 / m_AudioPacketDuration);
|
||||
}
|
||||
else {
|
||||
// 30 ms buffer on CoreAudio to avoid glitching on macOS
|
||||
packetsToBuffer = (int)ceil(0.030 / m_AudioPacketDuration);
|
||||
}
|
||||
|
||||
// Always buffer at least 2 packets
|
||||
packetsToBuffer = qMax(2, packetsToBuffer);
|
||||
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Audio buffer size: %f seconds",
|
||||
packetsToBuffer * m_AudioPacketDuration);
|
||||
|
||||
m_RingBuffer = soundio_ring_buffer_create(m_SoundIo,
|
||||
m_OutputStream->bytes_per_sample *
|
||||
m_OpusChannelCount *
|
||||
opusConfig->samplesPerFrame *
|
||||
packetsToBuffer);
|
||||
if (m_RingBuffer == nullptr) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"soundio_ring_buffer_create() failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
err = soundio_outstream_start(m_OutputStream);
|
||||
if (err != SoundIoErrorNone) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"soundio_outstream_start() failed: %s",
|
||||
soundio_strerror(err));
|
||||
return false;
|
||||
}
|
||||
|
||||
// HACK: For some reason, a constant latency hangs around in the audio pipeline
|
||||
// unless we wait for the audio stream to drain before actually submitting any samples.
|
||||
// This is a gross hack, but it works remarkably well.
|
||||
SDL_Delay(500);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void* SoundIoAudioRenderer::getAudioBuffer(int* size)
|
||||
{
|
||||
// We must always write a full frame of audio. If we don't,
|
||||
// the reader will get out of sync with the writer and our
|
||||
// channels will get all mixed up. To ensure this is always
|
||||
// the case, round our bytes free down to the next multiple
|
||||
// of our frame size.
|
||||
int bytesFree = soundio_ring_buffer_free_count(m_RingBuffer);
|
||||
int bytesPerFrame = m_OpusChannelCount * m_OutputStream->bytes_per_sample;
|
||||
*size = qMin(*size, (bytesFree / bytesPerFrame) * bytesPerFrame);
|
||||
return soundio_ring_buffer_write_ptr(m_RingBuffer);
|
||||
}
|
||||
|
||||
bool SoundIoAudioRenderer::submitAudio(int bytesWritten)
|
||||
{
|
||||
if (m_Errored) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (bytesWritten == 0) {
|
||||
// Nothing to do
|
||||
return true;
|
||||
}
|
||||
|
||||
// Flush events to update with new device arrivals
|
||||
soundio_flush_events(m_SoundIo);
|
||||
|
||||
// Advance the write pointer
|
||||
soundio_ring_buffer_advance_write_ptr(m_RingBuffer, bytesWritten);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
int SoundIoAudioRenderer::getCapabilities()
|
||||
{
|
||||
// TODO: Tweak buffer sizes then re-enable arbitrary audio duration
|
||||
return CAPABILITY_DIRECT_SUBMIT /* | CAPABILITY_SUPPORTS_ARBITRARY_AUDIO_DURATION */;
|
||||
}
|
||||
|
||||
IAudioRenderer::AudioFormat SoundIoAudioRenderer::getAudioBufferFormat()
|
||||
{
|
||||
return AudioFormat::Float32NE;
|
||||
}
|
||||
|
||||
void SoundIoAudioRenderer::sioErrorCallback(SoundIoOutStream* stream, int err)
|
||||
{
|
||||
auto me = reinterpret_cast<SoundIoAudioRenderer*>(stream->userdata);
|
||||
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Audio rendering error: %s",
|
||||
soundio_strerror(err));
|
||||
|
||||
// Trigger reinitialization
|
||||
me->m_Errored = true;
|
||||
}
|
||||
|
||||
void SoundIoAudioRenderer::sioBackendDisconnect(SoundIo* soundio, int err)
|
||||
{
|
||||
auto me = reinterpret_cast<SoundIoAudioRenderer*>(soundio->userdata);
|
||||
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Audio backend disconnected: %s",
|
||||
soundio_strerror(err));
|
||||
|
||||
// Trigger reinitialization
|
||||
me->m_Errored = true;
|
||||
}
|
||||
|
||||
void SoundIoAudioRenderer::sioDevicesChanged(SoundIo* soundio)
|
||||
{
|
||||
auto me = reinterpret_cast<SoundIoAudioRenderer*>(soundio->userdata);
|
||||
|
||||
if (me->m_Device == nullptr) {
|
||||
// Ignore calls that take place during initialization
|
||||
return;
|
||||
}
|
||||
|
||||
int outputDeviceIndex = soundio_default_output_device_index(soundio);
|
||||
if (outputDeviceIndex >= 0) {
|
||||
struct SoundIoDevice* outputDevice = soundio_get_output_device(soundio, outputDeviceIndex);
|
||||
if (outputDevice == nullptr) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"soundio_get_output_device() failed");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!soundio_device_equal(outputDevice, me->m_Device)) {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Default audio output device changed");
|
||||
|
||||
// Trigger reinitialization
|
||||
me->m_Errored = true;
|
||||
}
|
||||
|
||||
soundio_device_unref(outputDevice);
|
||||
}
|
||||
}
|
||||
|
||||
// bytes_per_frame should never be used on the ring buffer! It's not always
|
||||
// the same number of bytes per frames as the output stream!
|
||||
void SoundIoAudioRenderer::sioWriteCallback(SoundIoOutStream* stream, int frameCountMin, int frameCountMax)
|
||||
{
|
||||
auto me = reinterpret_cast<SoundIoAudioRenderer*>(stream->userdata);
|
||||
char* readPtr = soundio_ring_buffer_read_ptr(me->m_RingBuffer);
|
||||
int framesLeft = soundio_ring_buffer_fill_count(me->m_RingBuffer) /
|
||||
(me->m_OpusChannelCount * stream->bytes_per_sample);
|
||||
int bytesRead = 0;
|
||||
|
||||
// Ensure we always write at least a buffer, even if it's silence, to avoid
|
||||
// busy looping when no audio data is available while libsoundio tries to keep
|
||||
// us from starving the output device.
|
||||
frameCountMin = qMax(frameCountMin, (int)(stream->sample_rate * me->m_AudioPacketDuration));
|
||||
|
||||
// Clamp frameCountMax to at least 2 packets or 20 ms to stop our latency from growing if audio packets lag.
|
||||
// This makes sure that we never increase our latency far beyond what the sink is consuming.
|
||||
frameCountMax = qMin(frameCountMax, (int)(stream->sample_rate * qMax(me->m_AudioPacketDuration * 2, 0.020)));
|
||||
frameCountMin = qMin(frameCountMin, frameCountMax);
|
||||
|
||||
// Clamp framesLeft to frameCountMax
|
||||
framesLeft = qMin(framesLeft, frameCountMax);
|
||||
|
||||
// Track latency on queueing-based backends
|
||||
if (me->m_SoundIo->current_backend != SoundIoBackendCoreAudio && me->m_SoundIo->current_backend != SoundIoBackendJack) {
|
||||
soundio_outstream_get_latency(stream, &me->m_Latency);
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
int frameCount;
|
||||
int err;
|
||||
struct SoundIoChannelArea* areas;
|
||||
|
||||
// Always meet the minimum but don't write more than that
|
||||
// if we'll have to insert silence
|
||||
frameCount = qMax(framesLeft, frameCountMin);
|
||||
|
||||
if (frameCount == 0) {
|
||||
// Nothing more to write
|
||||
break;
|
||||
}
|
||||
|
||||
err = soundio_outstream_begin_write(stream, &areas, &frameCount);
|
||||
if (err != SoundIoErrorNone) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"soundio_outstream_begin_write() failed: %s",
|
||||
soundio_strerror(err));
|
||||
break;
|
||||
}
|
||||
|
||||
for (int frame = 0; frame < frameCount; frame++) {
|
||||
for (int ch = 0; ch < me->m_EffectiveLayout.channel_count; ch++) {
|
||||
// SoundIoChannelId - 1 happens to match Moonlight's channel layout
|
||||
// after we've applied our fixups to m_EffectiveLayout for 5.1 and 7.1.
|
||||
int readPtrChannel = me->m_EffectiveLayout.channels[ch] - 1;
|
||||
|
||||
if (frame >= framesLeft || readPtrChannel >= me->m_OpusChannelCount) {
|
||||
// Write silence if we have no buffered frames left or
|
||||
// nothing in the audio stream for this channel
|
||||
memset(areas[ch].ptr, 0, stream->bytes_per_sample);
|
||||
}
|
||||
else {
|
||||
// Write audio data from our ring buffer
|
||||
memcpy(areas[ch].ptr,
|
||||
&readPtr[readPtrChannel * stream->bytes_per_sample],
|
||||
stream->bytes_per_sample);
|
||||
}
|
||||
|
||||
areas[ch].ptr += areas[ch].step;
|
||||
}
|
||||
|
||||
// Move on to the next frame if we aren't inserting silence
|
||||
if (frame < framesLeft) {
|
||||
readPtr += stream->bytes_per_sample * me->m_OpusChannelCount;
|
||||
bytesRead += stream->bytes_per_sample * me->m_OpusChannelCount;
|
||||
}
|
||||
}
|
||||
|
||||
err = soundio_outstream_end_write(stream);
|
||||
if (err != SoundIoErrorNone && err != SoundIoErrorUnderflow) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"soundio_outstream_end_write() failed: %s",
|
||||
soundio_strerror(err));
|
||||
break;
|
||||
}
|
||||
|
||||
if (framesLeft >= frameCount) {
|
||||
framesLeft -= frameCount;
|
||||
}
|
||||
else {
|
||||
framesLeft = 0;
|
||||
}
|
||||
|
||||
if (frameCountMin >= frameCount) {
|
||||
frameCountMin -= frameCount;
|
||||
}
|
||||
else {
|
||||
frameCountMin = 0;
|
||||
}
|
||||
}
|
||||
|
||||
soundio_ring_buffer_advance_read_ptr(me->m_RingBuffer, bytesRead);
|
||||
}
|
||||
@ -1,44 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "renderer.h"
|
||||
|
||||
#include <soundio/soundio.h>
|
||||
|
||||
class SoundIoAudioRenderer : public IAudioRenderer
|
||||
{
|
||||
public:
|
||||
SoundIoAudioRenderer();
|
||||
|
||||
~SoundIoAudioRenderer();
|
||||
|
||||
virtual bool prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig);
|
||||
|
||||
virtual void* getAudioBuffer(int* size);
|
||||
|
||||
virtual bool submitAudio(int bytesWritten);
|
||||
|
||||
virtual int getCapabilities();
|
||||
|
||||
virtual AudioFormat getAudioBufferFormat();
|
||||
|
||||
private:
|
||||
int scoreChannelLayout(const struct SoundIoChannelLayout* layout, const OPUS_MULTISTREAM_CONFIGURATION* opusConfig);
|
||||
|
||||
static void sioErrorCallback(struct SoundIoOutStream* stream, int err);
|
||||
|
||||
static void sioWriteCallback(struct SoundIoOutStream* stream, int frameCountMin, int frameCountMax);
|
||||
|
||||
static void sioBackendDisconnect(struct SoundIo* soundio, int err);
|
||||
|
||||
static void sioDevicesChanged(SoundIo* soundio);
|
||||
|
||||
int m_OpusChannelCount;
|
||||
struct SoundIo* m_SoundIo;
|
||||
struct SoundIoDevice* m_Device;
|
||||
struct SoundIoOutStream* m_OutputStream;
|
||||
struct SoundIoRingBuffer* m_RingBuffer;
|
||||
struct SoundIoChannelLayout m_EffectiveLayout;
|
||||
double m_AudioPacketDuration;
|
||||
double m_Latency;
|
||||
bool m_Errored;
|
||||
};
|
||||
@ -1,7 +1,7 @@
|
||||
#include "input.h"
|
||||
|
||||
#include <Limelight.h>
|
||||
#include <SDL.h>
|
||||
#include "SDL_compat.h"
|
||||
#include <SDL_syswm.h>
|
||||
#include "streaming/streamutils.h"
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
#include "streaming/session.h"
|
||||
|
||||
#include <Limelight.h>
|
||||
#include <SDL.h>
|
||||
#include "SDL_compat.h"
|
||||
#include "settings/mappingmanager.h"
|
||||
|
||||
#include <QtMath>
|
||||
@ -900,6 +900,22 @@ void SdlInputHandler::setControllerLED(uint16_t controllerNumber, uint8_t r, uin
|
||||
#endif
|
||||
}
|
||||
|
||||
void SdlInputHandler::setAdaptiveTriggers(uint16_t controllerNumber, DualSenseOutputReport *report){
|
||||
|
||||
#if SDL_VERSION_ATLEAST(2, 0, 16)
|
||||
// Make sure the controller number is within our supported count
|
||||
if (controllerNumber <= MAX_GAMEPADS &&
|
||||
// and we have a valid controller
|
||||
m_GamepadState[controllerNumber].controller != nullptr &&
|
||||
// and it's a PS5 controller
|
||||
SDL_GameControllerGetType(m_GamepadState[controllerNumber].controller) == SDL_CONTROLLER_TYPE_PS5) {
|
||||
SDL_GameControllerSendEffect(m_GamepadState[controllerNumber].controller, report, sizeof(*report));
|
||||
}
|
||||
#endif
|
||||
|
||||
SDL_free(report);
|
||||
}
|
||||
|
||||
QString SdlInputHandler::getUnmappedGamepads()
|
||||
{
|
||||
QString ret;
|
||||
@ -913,7 +929,8 @@ QString SdlInputHandler::getUnmappedGamepads()
|
||||
MappingManager mappingManager;
|
||||
mappingManager.applyMappings();
|
||||
|
||||
for (int i = 0; i < SDL_NumJoysticks(); i++) {
|
||||
int numJoysticks = SDL_NumJoysticks();
|
||||
for (int i = 0; i < numJoysticks; i++) {
|
||||
if (!SDL_IsGameController(i)) {
|
||||
char guidStr[33];
|
||||
SDL_JoystickGetGUIDString(SDL_JoystickGetDeviceGUID(i),
|
||||
@ -973,7 +990,8 @@ int SdlInputHandler::getAttachedGamepadMask()
|
||||
}
|
||||
|
||||
count = mask = 0;
|
||||
for (int i = 0; i < SDL_NumJoysticks(); i++) {
|
||||
int numJoysticks = SDL_NumJoysticks();
|
||||
for (int i = 0; i < numJoysticks; i++) {
|
||||
if (SDL_IsGameController(i)) {
|
||||
char guidStr[33];
|
||||
SDL_JoystickGetGUIDString(SDL_JoystickGetDeviceGUID(i),
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
#include <Limelight.h>
|
||||
#include <SDL.h>
|
||||
#include "SDL_compat.h"
|
||||
#include "streaming/session.h"
|
||||
#include "settings/mappingmanager.h"
|
||||
#include "path.h"
|
||||
@ -50,7 +50,7 @@ SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, int streamWidth, i
|
||||
#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");
|
||||
SDL_SetHint(SDL_HINT_ALLOW_ALT_TAB_WHILE_GRABBED, "0");
|
||||
|
||||
// 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
|
||||
@ -62,8 +62,8 @@ SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, int streamWidth, i
|
||||
// controllers, but breaks DirectInput applications. We will enable it because
|
||||
// it's likely that working rumble is what the user is expecting. If they don't
|
||||
// want this behavior, they can override it with the environment variable.
|
||||
SDL_SetHint("SDL_JOYSTICK_HIDAPI_PS4_RUMBLE", "1");
|
||||
SDL_SetHint("SDL_JOYSTICK_HIDAPI_PS5_RUMBLE", "1");
|
||||
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS4_RUMBLE, "1");
|
||||
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS5_RUMBLE, "1");
|
||||
|
||||
// Populate special key combo configuration
|
||||
m_SpecialKeyCombos[KeyComboQuit].keyCombo = KeyComboQuit;
|
||||
@ -111,6 +111,11 @@ SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, int streamWidth, i
|
||||
m_SpecialKeyCombos[KeyComboTogglePointerRegionLock].scanCode = SDL_SCANCODE_L;
|
||||
m_SpecialKeyCombos[KeyComboTogglePointerRegionLock].enabled = true;
|
||||
|
||||
m_SpecialKeyCombos[KeyComboQuitAndExit].keyCombo = KeyComboQuitAndExit;
|
||||
m_SpecialKeyCombos[KeyComboQuitAndExit].keyCode = SDLK_e;
|
||||
m_SpecialKeyCombos[KeyComboQuitAndExit].scanCode = SDL_SCANCODE_E;
|
||||
m_SpecialKeyCombos[KeyComboQuitAndExit].enabled = true;
|
||||
|
||||
m_OldIgnoreDevices = SDL_GetHint(SDL_HINT_GAMECONTROLLER_IGNORE_DEVICES);
|
||||
m_OldIgnoreDevicesExcept = SDL_GetHint(SDL_HINT_GAMECONTROLLER_IGNORE_DEVICES_EXCEPT);
|
||||
|
||||
@ -296,6 +301,21 @@ void SdlInputHandler::notifyFocusLost()
|
||||
// Raise all keys that are currently pressed. If we don't do this, certain keys
|
||||
// used in shortcuts that cause focus loss (such as Alt+Tab) may get stuck down.
|
||||
raiseAllKeys();
|
||||
|
||||
#ifdef Q_OS_WIN32
|
||||
// Re-enable text input when window loses focus as a workaround for an SDL bug.
|
||||
// See #1617 for details.
|
||||
SDL_StartTextInput();
|
||||
#endif
|
||||
}
|
||||
|
||||
void SdlInputHandler::notifyFocusGained()
|
||||
{
|
||||
#ifdef Q_OS_WIN32
|
||||
// Disable text input when window gains focus to prevent IME popup interference.
|
||||
// See #1617 for details.
|
||||
SDL_StopTextInput();
|
||||
#endif
|
||||
}
|
||||
|
||||
bool SdlInputHandler::isCaptureActive()
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
#include "settings/streamingpreferences.h"
|
||||
#include "backend/computermanager.h"
|
||||
|
||||
#include <SDL.h>
|
||||
#include "SDL_compat.h"
|
||||
|
||||
struct GamepadState {
|
||||
SDL_GameController* controller;
|
||||
@ -38,6 +38,37 @@ struct GamepadState {
|
||||
unsigned char lt, rt;
|
||||
};
|
||||
|
||||
|
||||
struct DualSenseOutputReport{
|
||||
uint8_t validFlag0;
|
||||
uint8_t validFlag1;
|
||||
|
||||
/* For DualShock 4 compatibility mode. */
|
||||
uint8_t motorRight;
|
||||
uint8_t motorLeft;
|
||||
|
||||
/* Audio controls */
|
||||
uint8_t reserved[4];
|
||||
uint8_t muteButtonLed;
|
||||
|
||||
uint8_t powerSaveControl;
|
||||
uint8_t rightTriggerEffectType;
|
||||
uint8_t rightTriggerEffect[DS_EFFECT_PAYLOAD_SIZE];
|
||||
uint8_t leftTriggerEffectType;
|
||||
uint8_t leftTriggerEffect[DS_EFFECT_PAYLOAD_SIZE];
|
||||
uint8_t reserved2[6];
|
||||
|
||||
/* LEDs and lightbar */
|
||||
uint8_t validFlag2;
|
||||
uint8_t reserved3[2];
|
||||
uint8_t lightbarSetup;
|
||||
uint8_t ledBrightness;
|
||||
uint8_t playerLeds;
|
||||
uint8_t lightbarRed;
|
||||
uint8_t lightbarGreen;
|
||||
uint8_t lightbarBlue;
|
||||
};
|
||||
|
||||
// activeGamepadMask is a short, so we're bounded by the number of mask bits
|
||||
#define MAX_GAMEPADS 16
|
||||
|
||||
@ -95,6 +126,8 @@ public:
|
||||
|
||||
void setControllerLED(uint16_t controllerNumber, uint8_t r, uint8_t g, uint8_t b);
|
||||
|
||||
void setAdaptiveTriggers(uint16_t controllerNumber, DualSenseOutputReport *report);
|
||||
|
||||
void handleTouchFingerEvent(SDL_TouchFingerEvent* event);
|
||||
|
||||
int getAttachedGamepadMask();
|
||||
@ -105,6 +138,8 @@ public:
|
||||
|
||||
void notifyFocusLost();
|
||||
|
||||
void notifyFocusGained();
|
||||
|
||||
bool isCaptureActive();
|
||||
|
||||
bool isSystemKeyCaptureActive();
|
||||
@ -131,6 +166,7 @@ private:
|
||||
KeyComboToggleMinimize,
|
||||
KeyComboPasteText,
|
||||
KeyComboTogglePointerRegionLock,
|
||||
KeyComboQuitAndExit,
|
||||
KeyComboMax
|
||||
};
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
#include "streaming/session.h"
|
||||
|
||||
#include <Limelight.h>
|
||||
#include <SDL.h>
|
||||
#include "SDL_compat.h"
|
||||
|
||||
#define VK_0 0x30
|
||||
#define VK_A 0x41
|
||||
@ -139,6 +139,20 @@ void SdlInputHandler::performSpecialKeyCombo(KeyCombo combo)
|
||||
updatePointerRegionLock();
|
||||
break;
|
||||
|
||||
case KeyComboQuitAndExit:
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Detected quitAndExit key combo");
|
||||
|
||||
// Indicate that we want to exit afterwards
|
||||
Session::get()->setShouldExitAfterQuit();
|
||||
|
||||
// Push a quit event to the main loop
|
||||
SDL_Event quitExitEvent;
|
||||
quitExitEvent.type = SDL_QUIT;
|
||||
quitExitEvent.quit.timestamp = SDL_GetTicks();
|
||||
SDL_PushEvent(&quitExitEvent);
|
||||
break;
|
||||
|
||||
default:
|
||||
Q_UNREACHABLE();
|
||||
}
|
||||
@ -148,6 +162,7 @@ void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
|
||||
{
|
||||
short keyCode;
|
||||
char modifiers;
|
||||
bool shouldNotConvertToScanCodeOnServer = false;
|
||||
|
||||
if (event->repeat) {
|
||||
// Ignore repeat key down events
|
||||
@ -402,6 +417,8 @@ void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
|
||||
case SDL_SCANCODE_LEFTBRACKET:
|
||||
keyCode = 0xDB;
|
||||
break;
|
||||
case SDL_SCANCODE_INTERNATIONAL3:
|
||||
shouldNotConvertToScanCodeOnServer = true;
|
||||
case SDL_SCANCODE_BACKSLASH:
|
||||
keyCode = 0xDC;
|
||||
break;
|
||||
@ -411,9 +428,17 @@ void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
|
||||
case SDL_SCANCODE_APOSTROPHE:
|
||||
keyCode = 0xDE;
|
||||
break;
|
||||
case SDL_SCANCODE_INTERNATIONAL1:
|
||||
shouldNotConvertToScanCodeOnServer = true;
|
||||
case SDL_SCANCODE_NONUSBACKSLASH:
|
||||
keyCode = 0xE2;
|
||||
break;
|
||||
case SDL_SCANCODE_LANG1:
|
||||
keyCode = 0x1C;
|
||||
break;
|
||||
case SDL_SCANCODE_LANG2:
|
||||
keyCode = 0x1D;
|
||||
break;
|
||||
default:
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Unhandled button event: %d",
|
||||
@ -430,8 +455,9 @@ void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
|
||||
m_KeysDown.remove(keyCode);
|
||||
}
|
||||
|
||||
LiSendKeyboardEvent(0x8000 | keyCode,
|
||||
LiSendKeyboardEvent2(0x8000 | keyCode,
|
||||
event->state == SDL_PRESSED ?
|
||||
KEY_ACTION_DOWN : KEY_ACTION_UP,
|
||||
modifiers);
|
||||
modifiers,
|
||||
shouldNotConvertToScanCodeOnServer ? SS_KBE_FLAG_NON_NORMALIZED : 0);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user