Compare commits

..
Author SHA1 Message Date
Cameron Gutman 12b7f6b7a3 Fix appstream metadata release date for v0.8.1 2019-01-29 18:52:01 -08:00
675 changed files with 20656 additions and 47062 deletions
+5 -11
View File
@@ -1,10 +1,8 @@
---
name: Bug report
about: Ask for help on Discord before reporting a bug
about: Create a report to help us address broken functionality
---
**READ ME FIRST!**
This issue tracker is not the right place to ask for troubleshooting help if you're not able to connect or stream at all. For those requests, please use the #pc-help channel on our Discord server where there are many more people who can help (or direct you back here if it looks like a Moonlight bug after all). https://moonlight-stream.org/discord
**Describe the bug**
A clear and concise description of what the bug is.
@@ -18,28 +16,24 @@ If applicable, add screenshots to help explain your problem. If the issue is rel
**Affected games**
List the games you've tried that exhibit the issue. To see if the issue is game-specific, try streaming Steam Big Picture with Moonlight and see if the issue persists there.
**Other Moonlight clients**
- Does the issue occur when using Moonlight on iOS or Android?
**Moonlight settings (please complete the following information)**
- Have any settings been adjusted from defaults?
- If so, which settings have been changed?
- Does the problem still occur after reverting settings back to default?
**Client PC details (please complete the following information)**
- OS: [e.g. Windows 10 1809]
- Moonlight Version: [e.g. v0.9.0]
- OS: [e.g. Windows 10 1803]
- Moonlight Version: [e.g. v0.2.0]
**Server PC details (please complete the following information)**
- OS: [e.g. Windows 10 1809]
- OS: [e.g. Windows 10 1803]
- GeForce Experience version: [e.g. 3.16.0.140]
- Nvidia GPU driver: [e.g. 417.35]
**Moonlight Logs (please attach)**
- On Windows, `Moonlight-###.log` files can be found in `%TEMP%`. Simply type that into the File Explorer path field to navigate there.
- On macOS, `Moonlight-###.log` files can be found in `/tmp`. In Finder, press Cmd+Shift+G, then type `/tmp` to navigate there.
- On Linux with the Flatpak, logs print to the terminal when running with the command: `flatpak run com.moonlight_stream.Moonlight`
- On Linux with the Snap, logs print to the terminal when running with the command: `moonlight`
- On Linux, logs print to the terminal when running via `flatpak run com.moonlight_stream.Moonlight`
**Additional context**
Anything else you think may be relevant to the issue
-1
View File
@@ -1 +0,0 @@
blank_issues_enabled: false
-4
View File
@@ -1,4 +0,0 @@
issuesOpened: >
If this is a question about Moonlight or you need help troubleshooting a streaming problem, please use the help channels on our [Discord server](https://moonlight-stream.org/discord) instead of GitHub issues. There are many more people available on Discord to help you and answer your questions.<br /><br />
This issue tracker should only be used for specific bugs or feature requests.<br /><br />
Thank you, and happy streaming!
+1 -1
View File
@@ -9,7 +9,7 @@
url = https://github.com/gabomdq/SDL_GameControllerDB.git
[submodule "soundio/libsoundio"]
path = soundio/libsoundio
url = https://github.com/cgutman/libsoundio.git
url = https://github.com/andrewrk/libsoundio.git
[submodule "h264bitstream/h264bitstream"]
path = h264bitstream/h264bitstream
url = https://github.com/aizvorski/h264bitstream.git
+50
View File
@@ -0,0 +1,50 @@
language: cpp
git:
depth: 1
matrix:
include:
- os: osx
compiler: clang
- os: linux
sudo: required
dist: xenial
compiler: gcc
addons:
apt:
sources:
- sourceline: 'ppa:beineri/opt-qt596-xenial'
- sourceline: 'ppa:jonathonf/ffmpeg-4'
packages:
- qt59base
- qt59quickcontrols2
- qt59svg
- qt59multimedia
- libesd0-dev
- libgl-dev
- libegl1-mesa-dev
- libxkbcommon-dev
- wayland-protocols
- libopus-dev
- libavcodec-dev
- libavutil-dev
- libva-dev
- libvdpau-dev
install:
- '[ "$TRAVIS_OS_NAME" != osx ] || brew install qt5'
- '[ "$TRAVIS_OS_NAME" != osx ] || brew link --force qt5'
- if [[ "$TRAVIS_OS_NAME" = "linux" ]]; then export SDL2_VER=2.0.8 && wget https://www.libsdl.org/release/SDL2-$SDL2_VER.tar.gz && tar -xvf SDL2-$SDL2_VER.tar.gz && cd SDL2-$SDL2_VER && ./configure && make && sudo make install && cd ..; fi
before_script:
- '[ "$TRAVIS_OS_NAME" != "linux" ] || source /opt/qt59/bin/qt59-env.sh'
- qmake -v
- '[[ "$TRAVIS_OS_NAME" != linux || "$CXX" != clang++ ]] || export QMAKESPEC=linux-clang'
- '[[ "$TRAVIS_OS_NAME" != linux || "$CXX" != g++ ]] || export QMAKESPEC=linux-g++'
- '[ "$TRAVIS_OS_NAME" != osx ] || export QMAKESPEC=macx-clang'
script:
- qmake moonlight-qt.pro -spec $QMAKESPEC
- make debug
- make release
+5 -1
View File
@@ -3,7 +3,11 @@ QT -= core gui
TARGET = AntiHooking
TEMPLATE = lib
include(../globaldefs.pri)
# Support debug and release builds from command line for CI
CONFIG += debug_and_release
# Ensure symbols are always generated
CONFIG += force_debug_info
INCLUDEPATH += $$PWD/../libs/windows/include
contains(QT_ARCH, i386) {
+4 -75
View File
@@ -131,81 +131,10 @@ private:
static LoadLibraryExWFunc s_RealLoadLibraryExW;
static constexpr LPCWSTR k_BlacklistedDlls[] = {
// These A-Volute DLLs shipped with various audio driver packages improperly handle
// D3D9 exclusive fullscreen in a way that causes CreateDeviceEx() to deadlock.
// https://github.com/moonlight-stream/moonlight-qt/issues/102
L"NahimicOSD.dll", // ASUS Sonic Radar 3
L"SSAudioOSD.dll", // SteelSeries headsets
L"SS2OSD.dll", // ASUS Sonic Studio 2
L"Nahimic2OSD.dll",
L"NahimicMSIOSD.dll",
L"nhAsusPhoebusOSD.dll" // ASUS Phoebus
// This DLL has been seen in several crash reports. Some Googling
// suggests it's highly unstable and causes issues in many games.
L"EZFRD32.dll",
L"EZFRD64.dll",
// These are the newer dList DLLs for Optimus hybrid graphics DDI.
// https://docs.microsoft.com/en-us/windows-hardware/drivers/display/hybrid-system-ddi
//
// We forcefully block them from loading because Optimus has a bug that
// deadlocks DXVA2 when we present with D3DPRESENT_DONOTWAIT. This will prevent
// Optimus from ever using the dGPU even if the user has requested it.
// https://github.com/moonlight-stream/moonlight-qt/issues/240
// https://github.com/moonlight-stream/moonlight-qt/issues/235
L"nvdlist.dll",
L"nvdlistx.dll",
// These are the older dList/AppInit DLLs for Optimus hybrid graphics DDI.
// https://docs.microsoft.com/en-us/windows-hardware/drivers/display/hybrid-system-ddi
//
// These seem to cause a crash in PresentEx() in full-screen exclusive mode.
// This block will prevent Optimus from ever using the dGPU even if the user has requested it.
// https://github.com/moonlight-stream/moonlight-qt/issues/386
//
// d3d9!CSwapChain::BltToHybridPrimary+0x200:
// 00007ffa`23f37e58 488b01 mov rax,qword ptr [rcx] ds:00000000`00000038=????????????????
// 00 0000004e`496ff4e0 00007ffa`23f39e2c d3d9!CSwapChain::BltToHybridPrimary+0x200
// 01 0000004e`496ff880 00007ffa`23ee39ce d3d9!CSwapChain::FlipToSurface+0x15c
// 02 0000004e`496ff900 00007ffa`23f4dd75 d3d9!CSwapChain::PresentMain+0x3e13e
// 03 0000004e`496ffab0 00007ffa`23f4dccd d3d9!CBaseDevice::PresentMain+0x9d
// 04 0000004e`496ffb00 00007ff7`8e31016f d3d9!CBaseDevice::PresentEx+0xbd
// 05 0000004e`496ffb50 00007ff7`8e30df1e Moonlight!DXVA2Renderer::renderFrame+0x61f [C:\moonlight-qt\app\streaming\video\ffmpeg-renderers\dxva2.cpp @ 1035]
// 06 0000004e`496ffd50 00007ff7`8e30e46a Moonlight!Pacer::renderFrame+0x3e [C:\moonlight-qt\app\streaming\video\ffmpeg-renderers\pacer\pacer.cpp @ 265]
// 07 (Inline Function) --------`-------- Moonlight!Pacer::renderLastFrameAndUnlock+0x197 [C:\moonlight-qt\app\streaming\video\ffmpeg-renderers\pacer\pacer.cpp @ 156]
// 08 0000004e`496ffda0 00007ffa`14476978 Moonlight!Pacer::renderThread+0x1ca [C:\moonlight-qt\app\streaming\video\ffmpeg-renderers\pacer\pacer.cpp @ 88]
// 09 0000004e`496ffde0 00007ffa`14476ee2 SDL2!SDL_RunThread+0x38 [C:\Users\camer\SDL\src\thread\SDL_thread.c @ 276]
// 0a 0000004e`496ffe10 00007ffa`2aae0e82 SDL2!RunThread+0x12 [C:\Users\camer\SDL\src\thread\windows\SDL_systhread.c @ 83]
// 0b 0000004e`496ffe40 00007ffa`2d627bd4 ucrtbase!thread_start<unsigned int (__cdecl*)(void *),1>+0x42
// 0c 0000004e`496ffe70 00007ffa`2da4ce51 kernel32!BaseThreadInitThunk+0x14
// 0d 0000004e`496ffea0 00000000`00000000 ntdll!RtlUserThreadStart+0x21
//
L"nvinit.dll",
L"nvinitx.dll",
// In some unknown circumstances, RTSS tries to hook in the middle of an instruction, leaving garbage
// code inside d3d9.dll that causes a crash when executed:
//
// 0:000> u
// d3d9!D3D9GetCurrentOwnershipMode+0x5d:
// 00007ff8`95b95861 9b wait
// 00007ff8`95b95862 a7 cmps dword ptr [rsi],dword ptr [rdi] <--- crash happens here
// 00007ff8`95b95863 ff ???
// 00007ff8`95b95864 bfe8ca8a00 mov edi,8ACAE8h
// 00007ff8`95b95869 00eb add bl,ch
// 00007ff8`95b9586b f1 ???
// 00007ff8`95b9586c b808000000 mov eax,8
// 00007ff8`95b95871 ebe6 jmp d3d9!D3D9GetCurrentOwnershipMode+0x55 (00007ff8`95b95859)
//
// Disassembling starting at the exact address of the attempted hook yields the intended jmp instruction
//
// 0:000> u d3d9!D3D9GetCurrentOwnershipMode+0x5c:
// 00007ff8`95b95860 e99ba7ffbf jmp 00007ff8`55b90000
//
// Since the RTSS OSD doesn't even work with DXVA2, we'll just block the hooks entirely.
L"RTSSHooks.dll",
L"RTSSHooks64.dll",
// This DLL shipped with ASUS Sonic Radar 3 improperly handles
// D3D9 exclusive fullscreen in a way that causes CreateDeviceEx()
// to deadlock. https://github.com/moonlight-stream/moonlight-qt/issues/102
L"NahimicOSD.dll"
};
};
+23 -38
View File
@@ -2,71 +2,56 @@
[Moonlight PC](https://moonlight-stream.org) is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield, but built to run on Windows, Mac, and Linux. This client is the successor to [Moonlight Chrome](https://github.com/moonlight-stream/moonlight-chrome) for streaming on PC.
Moonlight also has mobile versions for [Android](https://github.com/moonlight-stream/moonlight-android) and [iOS](https://github.com/moonlight-stream/moonlight-ios).
Moonlight also has mobile versions for [Android](https://github.com/moonlight-stream/moonlight-android) and [iOS](https://github.com/moonlight-stream/moonlight-ios).
You can follow development on our [Discord server](https://moonlight-stream.org/discord).
You can follow development on our [Discord server](https://discord.gg/6ERtzFY).
[![AppVeyor Build Status](https://ci.appveyor.com/api/projects/status/glj5cxqwy2w3bglv/branch/master?svg=true)](https://ci.appveyor.com/project/cgutman/moonlight-qt/branch/master)
[![Downloads](https://img.shields.io/github/downloads/moonlight-stream/moonlight-qt/total)](https://github.com/moonlight-stream/moonlight-qt/releases)
[![Windows AppVeyor Status](https://ci.appveyor.com/api/projects/status/glj5cxqwy2w3bglv/branch/master?svg=true)](https://ci.appveyor.com/project/cgutman/moonlight-qt/branch/master)
[![Mac and Linux Travis CI Status](https://travis-ci.org/moonlight-stream/moonlight-qt.svg?branch=master)](https://travis-ci.org/moonlight-stream/moonlight-qt)
## Features
- Hardware accelerated video decoding on Windows, Mac, and Linux
- Supports streaming at up to 120 FPS (high refresh rate monitor recommended)
- Supports streaming at 720p, 1080p, 1440p, 4K, and the client PC's native screen resolution
- 7.1 surround sound audio support
- Support for both pointer capture (for games) and direct mouse control (for remote desktop)
- Support for passing system-wide keyboard shortcuts like Alt+Tab to the host in fullscreen mode
- Direct touchscreen mouse control
- 5.1 surround sound audio
- HEVC support for better image quality at reduced bandwidth
- Gamepad support with force feedback for up to 4 players
- Keyboard and mouse support
- Gamepad support with SDL gamepad mappings
## Downloads
- [Windows, macOS, and Steam Link](https://github.com/moonlight-stream/moonlight-qt/releases)
- [Snap (for Ubuntu-based Linux distros)](https://snapcraft.io/moonlight)
- [Flatpak (for other Linux distros)](https://flathub.org/apps/details/com.moonlight_stream.Moonlight)
- [AppImage](https://github.com/moonlight-stream/moonlight-qt/releases)
- [Raspberry Pi 4](https://github.com/moonlight-stream/moonlight-docs/wiki/Installing-Moonlight-Qt-on-Raspberry-Pi-4)
- [NVIDIA Jetson and Nintendo Switch (Ubuntu L4T)](https://github.com/moonlight-stream/moonlight-docs/wiki/Installing-Moonlight-Qt-on-Linux4Tegra-(L4T)-Ubuntu)
- [Windows and macOS](https://github.com/moonlight-stream/moonlight-qt/releases)
- [Linux](https://flathub.org/apps/details/com.moonlight_stream.Moonlight)
## Building
### General Build Requirements
* Qt 5.9 SDK or later
### Windows Build Requirements
* Qt 5.15 SDK or later
### Windows-specific Requirements
* Windows 7 or later
* [Visual Studio 2019](https://visualstudio.microsoft.com/downloads/) (Community edition is fine)
* Select **MSVC 2019** option during Qt installation. MinGW is not supported.
* [7-Zip](https://www.7-zip.org/) (only if building installers for non-development PCs)
* [WiX Toolset](https://wixtoolset.org/releases/) v3.11 or later (only if building installers for non-development PCs)
* Visual Studio 2015 or later
* Select MSVC Desktop toolchain during Qt installation
* 7-Zip (only if building installers for non-development PCs)
* WiX Toolset v3.11 or later (only if building installers for non-development PCs)
### macOS Build Requirements
* Qt 5.15 SDK or later
* macOS High Sierra (10.13) or later
* Xcode 11
### Mac-specific Requirements
* macOS Sierra (10.12) or later
* Xcode 10
* [create-dmg](https://github.com/sindresorhus/create-dmg) (only if building DMGs for use on non-development Macs)
### Linux/Unix Build Requirements
* Qt 5.9 SDK or later
### Linux-specific Requirements
* GCC or Clang
* Install the required packages:
* Debian/Ubuntu: `libegl1-mesa-dev libgl1-mesa-dev libopus-dev libqt5svg5-dev libsdl2-dev libsdl2-ttf-dev libssl-dev libavcodec-dev libva-dev libvdpau-dev libxkbcommon-dev qt5-default qt5-qmake qtbase5-dev qtdeclarative5-dev qtquickcontrols2-5-dev wayland-protocols qml-module-qtquick-controls2 qml-module-qtquick-layouts qml-module-qtquick-window2 qml-module-qtquick2`
* RedHat/Fedora: `openssl-devel qt5-devel SDL2-devel ffmpeg-devel qt5-qtquickcontrols2-devel libva-devel libvdpau-devel opus-devel pulseaudio-libs-devel alsa-lib-devel SDL2_ttf-devel`
* FFmpeg 4.0+ is required to build. If your distro doesn't package FFmpeg 4.0 or later, you can build and install it from source on https://ffmpeg.org/
### Steam Link Build Requirements
* [Steam Link SDK](https://github.com/ValveSoftware/steamlink-sdk) cloned on your build system
* STEAMLINK_SDK_PATH environment variable set to the Steam Link SDK path
* Install your distro equivalents of: `openssl-devel qt5-devel SDL2-devel ffmpeg-devel qt5-qtquickcontrols2-devel libva-devel libvdpau-devel opus-devel pulseaudio-libs-devel alsa-lib-devel`
* FFmpeg 4.0 is required to build. If your distro doesn't package FFmpeg 4.0, you can build and install it from source on http://ffmpeg.org/
### Build Setup Steps
1. Install the latest Qt SDK (and optionally, the Qt Creator IDE) from https://www.qt.io/download
* You can install Qt via Homebrew on macOS, but you will need to use `brew install qt --with-debug` to be able to create debug builds of Moonlight.
* You may also use your Linux distro's package manager for the Qt SDK as long as the packages are Qt 5.9 or later.
* This step is not required for building on Steam Link, because the Steam Link SDK includes Qt 5.9.
2. Run `git submodule update --init --recursive` from within `moonlight-qt/`
3. Open the project in Qt Creator or build from qmake on the command line.
* To build a binary for use on non-development machines, use the scripts in the `scripts` folder.
* For Windows builds, use `scripts\build-arch.bat` and `scripts\generate-bundle.bat`. Execute these scripts from the root of the repository within a Qt command prompt. Ensure WiX and 7-Zip binary directories are in your `%PATH%`.
* For Windows builds, use `scripts\generate-installers.bat`. Execute this script from the root of the repository within a Qt command prompt. Ensure WiX and 7-Zip binary directories are in your `%PATH%`.
* For macOS builds, use `scripts/generate-dmg.sh`. Execute this script from the root of the repository and ensure Qt's `bin` folder is in your `$PATH`.
* For Steam Link builds, run `scripts/build-steamlink-app.sh` from the root of the repository.
* To build from the command line for development use, run `qmake moonlight-qt.pro` then `make debug` or `make release`
## Contribute
+1 -1
View File
@@ -17,7 +17,7 @@
<key>CFBundleSignature</key>
<string>????</string>
<key>LSMinimumSystemVersion</key>
<string>10.13</string>
<string>10.11</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
<key>NSSupportsAutomaticGraphicsSwitching</key>
BIN
View File
Binary file not shown.
+55 -139
View File
@@ -8,7 +8,11 @@ unix:!macx {
TARGET = Moonlight
}
include(../globaldefs.pri)
# Support debug and release builds from command line for CI
CONFIG += debug_and_release
# Ensure symbols are always generated
CONFIG += force_debug_info
# Precompile QML files to avoid writing qmlcache on portable versions.
# Since this binds the app against the Qt runtime version, we will only
@@ -41,22 +45,25 @@ win32 {
contains(QT_ARCH, x86_64) {
LIBS += -L$$PWD/../libs/windows/lib/x64
}
contains(QT_ARCH, arm64) {
LIBS += -L$$PWD/../libs/windows/lib/arm64
}
LIBS += ws2_32.lib winmm.lib dxva2.lib ole32.lib gdi32.lib user32.lib d3d9.lib dwmapi.lib dbghelp.lib qwave.lib
LIBS += ws2_32.lib winmm.lib dxva2.lib ole32.lib gdi32.lib user32.lib d3d9.lib dwmapi.lib dbghelp.lib
}
macx {
INCLUDEPATH += $$PWD/../libs/mac/include
INCLUDEPATH += $$PWD/../libs/mac/Frameworks/SDL2.framework/Versions/A/Headers
INCLUDEPATH += $$PWD/../libs/mac/Frameworks/SDL2_ttf.framework/Versions/A/Headers
INCLUDEPATH += $$PWD/../libs/mac/include $$PWD/../libs/mac/Frameworks/SDL2.framework/Versions/A/Headers
LIBS += -L$$PWD/../libs/mac/lib -F$$PWD/../libs/mac/Frameworks
}
unix:!macx {
CONFIG += link_pkgconfig
PKGCONFIG += openssl sdl2 SDL2_ttf opus
CONFIG += link_pkgconfig soundio
PKGCONFIG += openssl sdl2 opus
# For libsoundio
packagesExist(libpulse) {
PKGCONFIG += libpulse
}
packagesExist(alsa) {
PKGCONFIG += alsa
}
packagesExist(libavcodec) {
PKGCONFIG += libavcodec libavutil
@@ -69,56 +76,29 @@ unix:!macx {
packagesExist(libva-wayland) {
CONFIG += libva-wayland
}
packagesExist(libva-drm) {
CONFIG += libva-drm
}
CONFIG += libva
}
packagesExist(vdpau) {
CONFIG += libvdpau
}
packagesExist(mmal) {
PKGCONFIG += mmal
CONFIG += mmal
}
packagesExist(libdrm) {
PKGCONFIG += libdrm
CONFIG += libdrm
}
}
packagesExist(wayland-client) {
DEFINES += HAS_WAYLAND
PKGCONFIG += wayland-client
}
packagesExist(x11) {
DEFINES += HAS_X11
PKGCONFIG += x11
}
}
win32 {
LIBS += -llibssl -llibcrypto -lSDL2 -lSDL2_ttf -lavcodec -lavutil -lopus
CONFIG += ffmpeg
}
win32:!winrt {
CONFIG += soundio discord-rpc
LIBS += -llibssl -llibcrypto -lSDL2 -lavcodec -lavutil -lopus
CONFIG += ffmpeg soundio
}
macx {
LIBS += -lssl -lcrypto -lavcodec.58 -lavutil.56 -lopus -framework SDL2 -framework SDL2_ttf
LIBS += -lobjc -framework VideoToolbox -framework AVFoundation -framework CoreVideo -framework CoreGraphics -framework CoreMedia -framework AppKit -framework Metal
LIBS += -lssl -lcrypto -lavcodec.58 -lavutil.56 -lopus -framework SDL2
LIBS += -lobjc -framework VideoToolbox -framework AVFoundation -framework CoreVideo -framework CoreGraphics -framework CoreMedia -framework AppKit
# For libsoundio
LIBS += -framework CoreAudio -framework AudioUnit
CONFIG += ffmpeg soundio discord-rpc
CONFIG += ffmpeg soundio
}
SOURCES += \
backend/nvapp.cpp \
main.cpp \
backend/computerseeker.cpp \
backend/identitymanager.cpp \
@@ -127,18 +107,11 @@ SOURCES += \
backend/nvpairingmanager.cpp \
backend/computermanager.cpp \
backend/boxartmanager.cpp \
backend/richpresencemanager.cpp \
cli/commandlineparser.cpp \
cli/quitstream.cpp \
cli/startstream.cpp \
settings/mappingfetcher.cpp \
settings/streamingpreferences.cpp \
streaming/input/abstouch.cpp \
streaming/input/gamepad.cpp \
streaming/input/input.cpp \
streaming/input/keyboard.cpp \
streaming/input/mouse.cpp \
streaming/input/reltouch.cpp \
streaming/input.cpp \
streaming/session.cpp \
streaming/audio/audio.cpp \
streaming/audio/renderers/sdlaud.cpp \
@@ -148,14 +121,9 @@ SOURCES += \
backend/autoupdatechecker.cpp \
path.cpp \
settings/mappingmanager.cpp \
gui/sdlgamepadkeynavigation.cpp \
streaming/video/overlaymanager.cpp \
backend/systemproperties.cpp \
wm.cpp
gui/sdlgamepadkeynavigation.cpp
HEADERS += \
backend/nvapp.h \
settings/mappingfetcher.h \
utils.h \
backend/computerseeker.h \
backend/identitymanager.h \
@@ -164,12 +132,11 @@ HEADERS += \
backend/nvpairingmanager.h \
backend/computermanager.h \
backend/boxartmanager.h \
backend/richpresencemanager.h \
cli/commandlineparser.h \
cli/quitstream.h \
cli/startstream.h \
settings/streamingpreferences.h \
streaming/input/input.h \
streaming/input.h \
streaming/session.h \
streaming/audio/renderers/renderer.h \
streaming/audio/renderers/sdl.h \
@@ -180,9 +147,7 @@ HEADERS += \
backend/autoupdatechecker.h \
path.h \
settings/mappingmanager.h \
gui/sdlgamepadkeynavigation.h \
streaming/video/overlaymanager.h \
backend/systemproperties.h
gui/sdlgamepadkeynavigation.h
# Platform-specific renderers and decoders
ffmpeg {
@@ -192,15 +157,13 @@ ffmpeg {
SOURCES += \
streaming/video/ffmpeg.cpp \
streaming/video/ffmpeg-renderers/sdlvid.cpp \
streaming/video/ffmpeg-renderers/cuda.cpp \
streaming/video/ffmpeg-renderers/pacer/pacer.cpp \
streaming/video/ffmpeg-renderers/pacer/nullthreadedvsyncsource.cpp
HEADERS += \
streaming/video/ffmpeg.h \
streaming/video/ffmpeg-renderers/renderer.h \
streaming/video/ffmpeg-renderers/sdlvid.h \
streaming/video/ffmpeg-renderers/cuda.h \
streaming/video/ffmpeg-renderers/pacer/pacer.h \
streaming/video/ffmpeg-renderers/pacer/nullthreadedvsyncsource.h
}
@@ -224,12 +187,6 @@ libva-wayland {
PKGCONFIG += libva-wayland
DEFINES += HAVE_LIBVA_WAYLAND
}
libva-wayland {
message(VAAPI DRM support enabled)
PKGCONFIG += libva-drm
DEFINES += HAVE_LIBVA_DRM
}
libvdpau {
message(VDPAU renderer selected)
@@ -237,44 +194,16 @@ libvdpau {
SOURCES += streaming/video/ffmpeg-renderers/vdpau.cpp
HEADERS += streaming/video/ffmpeg-renderers/vdpau.h
}
mmal {
message(MMAL renderer selected)
config_SLVideo {
message(SLVideo decoder/renderer selected)
DEFINES += HAVE_MMAL
SOURCES += streaming/video/ffmpeg-renderers/mmal.cpp
HEADERS += streaming/video/ffmpeg-renderers/mmal.h
DEFINES += HAVE_SLVIDEO
LIBS += -lSLVideo
SOURCES += streaming/video/sl.cpp
HEADERS += streaming/video/sl.h
}
libdrm {
message(DRM renderer selected)
DEFINES += HAVE_DRM
SOURCES += streaming/video/ffmpeg-renderers/drm.cpp
HEADERS += streaming/video/ffmpeg-renderers/drm.h
}
config_EGL {
message(EGL renderer selected)
CONFIG += egl
DEFINES += HAVE_EGL
SOURCES += \
streaming/video/ffmpeg-renderers/eglvid.cpp \
streaming/video/ffmpeg-renderers/egl_extensions.cpp
HEADERS += streaming/video/ffmpeg-renderers/eglvid.h
}
config_SL {
message(Steam Link build configuration selected)
DEFINES += STEAM_LINK HAVE_SLVIDEO HAVE_SLAUDIO
LIBS += -lSLVideo -lSLAudio
SOURCES += \
streaming/video/slvid.cpp \
streaming/audio/renderers/slaud.cpp
HEADERS += \
streaming/video/slvid.h \
streaming/audio/renderers/slaud.h
}
win32:!winrt {
win32 {
message(DXVA2 renderer selected)
SOURCES += \
@@ -289,10 +218,12 @@ macx {
message(VideoToolbox renderer selected)
SOURCES += \
streaming/video/ffmpeg-renderers/vt.mm
streaming/video/ffmpeg-renderers/vt.mm \
streaming/video/ffmpeg-renderers/pacer/displaylinkvsyncsource.mm
HEADERS += \
streaming/video/ffmpeg-renderers/vt.h
streaming/video/ffmpeg-renderers/vt.h \
streaming/video/ffmpeg-renderers/pacer/displaylinkvsyncsource.h
}
soundio {
message(libsoundio audio renderer selected)
@@ -301,20 +232,11 @@ soundio {
SOURCES += streaming/audio/renderers/soundioaudiorenderer.cpp
HEADERS += streaming/audio/renderers/soundioaudiorenderer.h
}
discord-rpc {
message(Discord integration enabled)
LIBS += -ldiscord-rpc
DEFINES += HAVE_DISCORD
}
RESOURCES += \
resources.qrc \
qml.qrc
TRANSLATIONS += \
languages/qml_fr.ts
# Additional import path used to resolve QML modules in Qt Creator's code model
QML_IMPORT_PATH =
@@ -335,14 +257,12 @@ 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
win32:CONFIG(release, debug|release): LIBS += -L$$OUT_PWD/../soundio/release/ -lsoundio
else:win32:CONFIG(debug, debug|release): LIBS += -L$$OUT_PWD/../soundio/debug/ -lsoundio
else:unix: LIBS += -L$$OUT_PWD/../soundio/ -lsoundio
INCLUDEPATH += $$PWD/../soundio/libsoundio
DEPENDPATH += $$PWD/../soundio/libsoundio
}
INCLUDEPATH += $$PWD/../soundio/libsoundio
DEPENDPATH += $$PWD/../soundio/libsoundio
win32:CONFIG(release, debug|release): LIBS += -L$$OUT_PWD/../h264bitstream/release/ -lh264bitstream
else:win32:CONFIG(debug, debug|release): LIBS += -L$$OUT_PWD/../h264bitstream/debug/ -lh264bitstream
@@ -351,15 +271,11 @@ else:unix: LIBS += -L$$OUT_PWD/../h264bitstream/ -lh264bitstream
INCLUDEPATH += $$PWD/../h264bitstream/h264bitstream
DEPENDPATH += $$PWD/../h264bitstream/h264bitstream
!winrt {
contains(QT_ARCH, i386)|contains(QT_ARCH, x86_64) {
win32:CONFIG(release, debug|release): LIBS += -L$$OUT_PWD/../AntiHooking/release/ -lAntiHooking
else:win32:CONFIG(debug, debug|release): LIBS += -L$$OUT_PWD/../AntiHooking/debug/ -lAntiHooking
win32:CONFIG(release, debug|release): LIBS += -L$$OUT_PWD/../AntiHooking/release/ -lAntiHooking
else:win32:CONFIG(debug, debug|release): LIBS += -L$$OUT_PWD/../AntiHooking/debug/ -lAntiHooking
INCLUDEPATH += $$PWD/../AntiHooking
DEPENDPATH += $$PWD/../AntiHooking
}
}
INCLUDEPATH += $$PWD/../AntiHooking
DEPENDPATH += $$PWD/../AntiHooking
unix:!macx: {
isEmpty(PREFIX) {
@@ -383,7 +299,10 @@ unix:!macx: {
appstream.files = deploy/linux/com.moonlight_stream.Moonlight.appdata.xml
appstream.path = $$PREFIX/$$DATADIR/metainfo/
INSTALLS += target desktop icons appstream
appdata.files = SDL_GameControllerDB/gamecontrollerdb.txt
appdata.path = "$$PREFIX/$$DATADIR/Moonlight Game Streaming Project/Moonlight/"
INSTALLS += target appdata desktop icons appstream
}
win32 {
RC_ICONS = moonlight.ico
@@ -401,16 +320,13 @@ macx {
QMAKE_INFO_PLIST = $$OUT_PWD/Info.plist
APP_BUNDLE_RESOURCES.files = moonlight.icns
APP_BUNDLE_RESOURCES.files = moonlight.icns SDL_GameControllerDB/gamecontrollerdb.txt
APP_BUNDLE_RESOURCES.path = Contents/Resources
APP_BUNDLE_FRAMEWORKS.files = $$files(../libs/mac/Frameworks/*.framework, true) $$files(../libs/mac/lib/*.dylib, true)
APP_BUNDLE_FRAMEWORKS.files = $$files(../libs/mac/Frameworks/*.framework, true)
APP_BUNDLE_FRAMEWORKS.path = Contents/Frameworks
APP_BUNDLE_PLIST.files = $$OUT_PWD/Info.plist
APP_BUNDLE_PLIST.path = Contents
QMAKE_BUNDLE_DATA += APP_BUNDLE_RESOURCES APP_BUNDLE_FRAMEWORKS APP_BUNDLE_PLIST
QMAKE_BUNDLE_DATA += APP_BUNDLE_RESOURCES APP_BUNDLE_FRAMEWORKS
QMAKE_RPATHDIR += @executable_path/../Frameworks
}
+4 -34
View File
@@ -28,24 +28,10 @@ AutoUpdateChecker::AutoUpdateChecker(QObject *parent) :
void AutoUpdateChecker::start()
{
#if defined(Q_OS_WIN32) || defined(Q_OS_DARWIN) || defined(STEAM_LINK) || defined(APP_IMAGE) // Only run update checker on platforms without auto-update
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) && QT_VERSION < QT_VERSION_CHECK(5, 15, 1) && !defined(QT_NO_BEARERMANAGEMENT)
// HACK: Set network accessibility to work around QTBUG-80947 (introduced in Qt 5.14.0 and fixed in Qt 5.15.1)
QT_WARNING_PUSH
QT_WARNING_DISABLE_DEPRECATED
m_Nam.setNetworkAccessible(QNetworkAccessManager::Accessible);
QT_WARNING_POP
#endif
#if defined(Q_OS_WIN32) || defined(Q_OS_DARWIN) // Only run update checker on platforms without auto-update
// We'll get a callback when this is finished
QUrl url("https://moonlight-stream.org/updates/qt.json");
QNetworkRequest request(url);
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
request.setAttribute(QNetworkRequest::Http2AllowedAttribute, true);
#else
request.setAttribute(QNetworkRequest::HTTP2AllowedAttribute, true);
#endif
m_Nam.get(request);
m_Nam.get(QNetworkRequest(url));
#endif
}
@@ -57,29 +43,13 @@ void AutoUpdateChecker::parseStringToVersionQuad(QString& string, QVector<int>&
}
}
QString AutoUpdateChecker::getPlatform()
{
#if defined(STEAM_LINK)
return QStringLiteral("steamlink");
#elif defined(APP_IMAGE)
return QStringLiteral("appimage");
#else
return QSysInfo::productType();
#endif
}
void AutoUpdateChecker::handleUpdateCheckRequestFinished(QNetworkReply* reply)
{
Q_ASSERT(reply->isFinished());
if (reply->error() == QNetworkReply::NoError) {
QTextStream stream(reply);
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
stream.setEncoding(QStringConverter::Utf8);
#else
stream.setCodec("UTF-8");
#endif
// Read all data and queue the reply for deletion
QString jsonString = stream.readAll();
@@ -118,7 +88,7 @@ void AutoUpdateChecker::handleUpdateCheckRequestFinished(QNetworkReply* reply)
}
if (updateObj["arch"] == QSysInfo::buildCpuArchitecture() &&
updateObj["platform"] == getPlatform()) {
updateObj["platform"] == QSysInfo::productType()) {
qDebug() << "Found update manifest match for current platform";
QString latestVersion = updateObj["version"].toString();
@@ -167,7 +137,7 @@ void AutoUpdateChecker::handleUpdateCheckRequestFinished(QNetworkReply* reply)
}
qWarning() << "No entry in update manifest found for current platform: "
<< QSysInfo::buildCpuArchitecture() << getPlatform();
<< QSysInfo::buildCpuArchitecture() << QSysInfo::productType();
}
else {
qWarning() << "Update checking failed with error: " << reply->error();
-2
View File
@@ -20,8 +20,6 @@ private slots:
private:
void parseStringToVersionQuad(QString& string, QVector<int>& version);
QString getPlatform();
QVector<int> m_CurrentVersionQuad;
QNetworkAccessManager m_Nam;
};
+7 -20
View File
@@ -71,10 +71,10 @@ private:
QUrl BoxArtManager::loadBoxArt(NvComputer* computer, NvApp& app)
{
// Try to open the cached file if it exists and contains data
QFile cacheFile(getFilePathForBoxArt(computer, app.id));
if (cacheFile.exists() && cacheFile.size() > 0) {
return QUrl::fromLocalFile(cacheFile.fileName());
// Try to open the cached file
QString cacheFilePath = getFilePathForBoxArt(computer, app.id);
if (QFile::exists(cacheFilePath)) {
return QUrl::fromLocalFile(cacheFilePath);
}
// If we get here, we need to fetch asynchronously.
@@ -87,21 +87,12 @@ QUrl BoxArtManager::loadBoxArt(NvComputer* computer, NvApp& app)
return QUrl("qrc:/res/no_app_image.png");
}
void BoxArtManager::deleteBoxArt(NvComputer* computer)
{
QDir dir(Path::getBoxArtCacheDir());
// Delete everything in this computer's box art directory
if (dir.cd(computer->uuid)) {
dir.removeRecursively();
}
}
void BoxArtManager::handleBoxArtLoadComplete(NvComputer* computer, NvApp app, QUrl image)
{
if (!image.isEmpty()) {
emit boxArtLoadComplete(computer, app, image);
if (image.isEmpty()) {
image = QUrl("qrc:/res/no_app_image.png");
}
emit boxArtLoadComplete(computer, app, image);
}
QUrl BoxArtManager::loadBoxArtFromNetwork(NvComputer* computer, int appId)
@@ -119,10 +110,6 @@ QUrl BoxArtManager::loadBoxArtFromNetwork(NvComputer* computer, int appId)
if (image.save(cachePath)) {
return QUrl::fromLocalFile(cachePath);
}
else {
// A failed save() may leave a zero byte file. Make sure that's removed.
QFile(cachePath).remove();
}
}
return QUrl();
-4
View File
@@ -18,10 +18,6 @@ public:
QUrl
loadBoxArt(NvComputer* computer, NvApp& app);
static
void
deleteBoxArt(NvComputer* computer);
signals:
void
boxArtLoadComplete(NvComputer* computer, NvApp app, QUrl image);
+106 -233
View File
@@ -1,5 +1,4 @@
#include "computermanager.h"
#include "boxartmanager.h"
#include "nvhttp.h"
#include "settings/streamingpreferences.h"
@@ -8,7 +7,6 @@
#include <QThread>
#include <QThreadPool>
#include <QCoreApplication>
#define SER_HOSTS "hosts"
@@ -33,7 +31,7 @@ private:
QString serverInfo;
try {
serverInfo = http.getServerInfo(NvHTTP::NvLogLevel::NVLL_NONE, true);
serverInfo = http.getServerInfo(NvHTTP::NvLogLevel::NONE);
} catch (...) {
return false;
}
@@ -68,7 +66,12 @@ private:
}
QWriteLocker lock(&m_Computer->lock);
changed = m_Computer->updateAppList(appList);
if (m_Computer->appList != appList) {
m_Computer->appList = appList;
m_Computer->sortAppList();
changed = true;
}
return true;
}
@@ -80,7 +83,7 @@ private:
bool stateChanged = false;
bool online = false;
bool wasOnline = m_Computer->state == NvComputer::CS_ONLINE;
for (int i = 0; i < (wasOnline ? TRIES_BEFORE_OFFLINING : 1) && !online; i++) {
for (int i = 0; i < TRIES_BEFORE_OFFLINING && !online; i++) {
for (auto& address : m_Computer->uniqueAddresses()) {
if (isInterruptionRequested()) {
return;
@@ -127,12 +130,8 @@ private:
emit computerStateChanged(m_Computer);
}
// Wait a bit to poll again, but do it in 100 ms chunks
// so we can be interrupted reasonably quickly.
// FIXME: QWaitCondition would be better.
for (int i = 0; i < 30 && !isInterruptionRequested(); i++) {
QThread::msleep(100);
}
// Wait a bit to poll again
QThread::sleep(3);
}
}
@@ -158,13 +157,6 @@ ComputerManager::ComputerManager(QObject *parent)
m_KnownHosts[computer->uuid] = computer;
}
settings.endArray();
// To quit in a timely manner, we must block additional requests
// after we receive the aboutToQuit() signal. This is neccessary
// because NvHTTP uses aboutToQuit() to abort requests in progres
// while quitting, however this is a one time signal - additional
// requests would not be aborted and block termination.
connect(QCoreApplication::instance(), SIGNAL(aboutToQuit()), this, SLOT(handleAboutToQuit()));
}
ComputerManager::~ComputerManager()
@@ -182,19 +174,38 @@ ComputerManager::~ComputerManager()
delete m_MdnsBrowser;
m_MdnsBrowser = nullptr;
// Interrupt polling
for (ComputerPollingEntry* entry : m_PollEntries) {
entry->interrupt();
{
QMutableMapIterator<QString, QThread*> i(m_PollThreads);
// Interrupt all polling threads
while (i.hasNext()) {
i.next();
i.value()->requestInterruption();
}
// Wait for all threads to terminate before destroying
// the NvComputer objects.
i.toFront();
while (i.hasNext()) {
i.next();
QThread* thread = i.value();
thread->wait();
delete thread;
i.remove();
}
}
// Delete all polling entries (and associated threads)
for (ComputerPollingEntry* entry : m_PollEntries) {
delete entry;
}
{
QMutableMapIterator<QString, NvComputer*> i(m_KnownHosts);
// Destroy all NvComputer objects now that polling is halted
for (NvComputer* computer : m_KnownHosts) {
delete computer;
// Destroy all NvComputer objects
while (i.hasNext()) {
i.next();
delete i.value();
i.remove();
}
}
}
@@ -205,50 +216,13 @@ void ComputerManager::saveHosts()
settings.remove(SER_HOSTS);
settings.beginWriteArray(SER_HOSTS);
int i = 0;
for (const NvComputer* computer : m_KnownHosts) {
settings.setArrayIndex(i++);
computer->serialize(settings);
for (int i = 0; i < m_KnownHosts.count(); i++) {
settings.setArrayIndex(i);
m_KnownHosts.value(m_KnownHosts.keys()[i])->serialize(settings);
}
settings.endArray();
}
QHostAddress ComputerManager::getBestGlobalAddressV6(QVector<QHostAddress> &addresses)
{
for (const QHostAddress& address : addresses) {
if (address.protocol() == QAbstractSocket::IPv6Protocol) {
if (address.isInSubnet(QHostAddress("fe80::"), 10)) {
// Link-local
continue;
}
if (address.isInSubnet(QHostAddress("fec0::"), 10)) {
qInfo() << "Ignoring site-local address:" << address;
continue;
}
if (address.isInSubnet(QHostAddress("fc00::"), 7)) {
qInfo() << "Ignoring ULA:" << address;
continue;
}
if (address.isInSubnet(QHostAddress("2002::"), 16)) {
qInfo() << "Ignoring 6to4 address:" << address;
continue;
}
if (address.isInSubnet(QHostAddress("2001::"), 32)) {
qInfo() << "Ignoring Teredo address:" << address;
continue;
}
return address;
}
}
return QHostAddress();
}
void ComputerManager::startPolling()
{
QWriteLocker lock(&m_Lock);
@@ -266,9 +240,9 @@ void ComputerManager::startPolling()
this, [this](const QMdnsEngine::Service& service) {
qInfo() << "Discovered mDNS host:" << service.hostname();
MdnsPendingComputer* pendingComputer = new MdnsPendingComputer(&m_MdnsServer, service);
connect(pendingComputer, &MdnsPendingComputer::resolvedHost,
this, &ComputerManager::handleMdnsServiceResolved);
MdnsPendingComputer* pendingComputer = new MdnsPendingComputer(&m_MdnsServer, &m_MdnsCache, service);
connect(pendingComputer, SIGNAL(resolvedv4(MdnsPendingComputer*,QHostAddress)),
this, SLOT(handleMdnsServiceResolved(MdnsPendingComputer*,QHostAddress)));
m_PendingResolution.append(pendingComputer);
});
}
@@ -291,58 +265,24 @@ void ComputerManager::startPollingComputer(NvComputer* computer)
return;
}
ComputerPollingEntry* pollingEntry;
if (!m_PollEntries.contains(computer->uuid)) {
pollingEntry = m_PollEntries[computer->uuid] = new ComputerPollingEntry();
}
else {
pollingEntry = m_PollEntries[computer->uuid];
if (m_PollThreads.contains(computer->uuid)) {
Q_ASSERT(m_PollThreads.value(computer->uuid)->isRunning());
return;
}
if (!pollingEntry->isActive()) {
PcMonitorThread* thread = new PcMonitorThread(computer);
connect(thread, SIGNAL(computerStateChanged(NvComputer*)),
this, SLOT(handleComputerStateChanged(NvComputer*)));
pollingEntry->setActiveThread(thread);
thread->start();
}
PcMonitorThread* thread = new PcMonitorThread(computer);
connect(thread, SIGNAL(computerStateChanged(NvComputer*)),
this, SLOT(handleComputerStateChanged(NvComputer*)));
m_PollThreads[computer->uuid] = thread;
thread->start();
}
void ComputerManager::handleMdnsServiceResolved(MdnsPendingComputer* computer,
QVector<QHostAddress>& addresses)
const QHostAddress& address)
{
QHostAddress v6Global = getBestGlobalAddressV6(addresses);
bool added = false;
qInfo() << "Resolved" << computer->hostname() << "to" << address.toString();
// Add the host using the IPv4 address
for (const QHostAddress& address : addresses) {
if (address.protocol() == QAbstractSocket::IPv4Protocol) {
// NB: We don't just call addNewHost() here with v6Global because the IPv6
// address may not be reachable (if the user hasn't installed the IPv6 helper yet
// or if this host lacks outbound IPv6 capability). We want to add IPv6 even if
// it's not currently reachable.
addNewHost(address.toString(), true, v6Global);
added = true;
break;
}
}
if (!added) {
// If we get here, there wasn't an IPv4 address so we'll do it v6-only
for (const QHostAddress& address : addresses) {
if (address.protocol() == QAbstractSocket::IPv6Protocol) {
// Use a link-local or site-local address for the "local address"
if (address.isInSubnet(QHostAddress("fe80::"), 10) ||
address.isInSubnet(QHostAddress("fec0::"), 10) ||
address.isInSubnet(QHostAddress("fc00::"), 7)) {
addNewHost(address.toString(), true, v6Global);
added = true;
break;
}
}
}
}
addNewHost(address.toString(), true);
m_PendingResolution.removeOne(computer);
computer->deleteLater();
@@ -377,14 +317,14 @@ public:
void run()
{
ComputerPollingEntry* pollingEntry;
QThread* pollingThread;
// Only do the minimum amount of work while holding the writer lock.
// We must release it before calling saveHosts().
{
QWriteLocker lock(&m_ComputerManager->m_Lock);
pollingEntry = m_ComputerManager->m_PollEntries.take(m_Computer->uuid);
pollingThread = m_ComputerManager->m_PollThreads.take(m_Computer->uuid);
m_ComputerManager->m_KnownHosts.remove(m_Computer->uuid);
}
@@ -392,11 +332,18 @@ public:
// Persist the new host list
m_ComputerManager->saveHosts();
// Delete the polling entry first. This will stop all polling threads too.
delete pollingEntry;
// Delete the polling thread
if (pollingThread != nullptr) {
pollingThread->requestInterruption();
// Delete cached box art
BoxArtManager::deleteBoxArt(m_Computer);
// We must wait here because we're going to delete computer
// and we can't do that out from underneath the poller.
pollingThread->wait();
// The thread is safe to delete now
Q_ASSERT(pollingThread->isFinished());
delete pollingThread;
}
// Finally, delete the computer itself. This must be done
// last because the polling thread might be using it.
@@ -415,39 +362,6 @@ void ComputerManager::deleteHost(NvComputer* computer)
QThreadPool::globalInstance()->start(new DeferredHostDeletionTask(this, computer));
}
void ComputerManager::renameHost(NvComputer* computer, QString name)
{
{
QWriteLocker(&computer->lock);
computer->name = name;
computer->hasCustomName = true;
}
// Notify the UI of the state change
handleComputerStateChanged(computer);
}
void ComputerManager::clientSideAttributeUpdated(NvComputer* computer)
{
// Persist the change
saveHosts();
// Notify the UI of the state change
handleComputerStateChanged(computer);
}
void ComputerManager::handleAboutToQuit()
{
QWriteLocker lock(&m_Lock);
// Interrupt polling threads immediately, so they
// avoid making additional requests while quitting
for (ComputerPollingEntry* entry : m_PollEntries) {
entry->interrupt();
}
}
class PendingPairingTask : public QObject, public QRunnable
{
Q_OBJECT
@@ -487,7 +401,7 @@ private:
break;
}
} catch (const GfeHttpResponseException& e) {
emit pairingCompleted(m_Computer, "GeForce Experience returned error: " + e.toQString());
emit pairingCompleted(m_Computer, e.toQString());
} catch (const QtNetworkReplyException& e) {
emit pairingCompleted(m_Computer, e.toQString());
}
@@ -536,8 +450,8 @@ private:
}
if (e.getStatusCode() == 599) {
// 599 is a special code we make a custom message for
emit quitAppFailed(tr("The running game wasn't started by this PC. "
"You must quit the game on the host PC manually or use the device that originally started the game."));
emit quitAppFailed("The running game wasn't started by this PC. "
"You must quit the game on the host PC manually or use the device that originally started the game.");
}
else {
emit quitAppFailed(e.toQString());
@@ -584,8 +498,20 @@ void ComputerManager::stopPollingAsync()
m_MdnsBrowser = nullptr;
// Interrupt all threads, but don't wait for them to terminate
for (ComputerPollingEntry* entry : m_PollEntries) {
entry->interrupt();
QMutableMapIterator<QString, QThread*> i(m_PollThreads);
while (i.hasNext()) {
i.next();
QThread* thread = i.value();
// Let this thread delete itself when it's finished
connect(thread, SIGNAL(finished()),
thread, SLOT(deleteLater()));
// The threads will delete themselves when they terminate,
// but we remove them from the polling threads map here.
i.value()->requestInterruption();
i.remove();
}
}
@@ -594,10 +520,9 @@ class PendingAddTask : public QObject, public QRunnable
Q_OBJECT
public:
PendingAddTask(ComputerManager* computerManager, QString address, QHostAddress mdnsIpv6Address, bool mdns)
PendingAddTask(ComputerManager* computerManager, QString address, bool mdns)
: m_ComputerManager(computerManager),
m_Address(address),
m_MdnsIpv6Address(mdnsIpv6Address),
m_Mdns(mdns)
{
connect(this, &PendingAddTask::computerAddCompleted,
@@ -607,7 +532,7 @@ public:
}
signals:
void computerAddCompleted(QVariant success, QVariant detectedPortBlocking);
void computerAddCompleted(QVariant success);
void computerStateChanged(NvComputer* computer);
@@ -621,12 +546,12 @@ private:
// around this issue, we will issue the request again after a few seconds if
// we see a ServiceUnavailableError error.
try {
serverInfo = http.getServerInfo(NvHTTP::NVLL_VERBOSE);
serverInfo = http.getServerInfo(NvHTTP::NvLogLevel::VERBOSE);
} catch (const QtNetworkReplyException& e) {
if (e.getError() == QNetworkReply::ServiceUnavailableError) {
qWarning() << "Retrying request in 5 seconds after ServiceUnavailableError";
QThread::sleep(5);
serverInfo = http.getServerInfo(NvHTTP::NVLL_VERBOSE);
serverInfo = http.getServerInfo(NvHTTP::NvLogLevel::VERBOSE);
qInfo() << "Retry successful";
}
else {
@@ -637,20 +562,7 @@ private:
return serverInfo;
} catch (...) {
if (!m_Mdns) {
StreamingPreferences prefs;
int portTestResult;
if (prefs.detectNetworkBlocking) {
// We failed to connect to the specified PC. Let's test to make sure this network
// isn't blocking Moonlight, so we can tell the user about it.
portTestResult = LiTestClientConnectivity("qt.conntest.moonlight-stream.org", 443,
ML_PORT_FLAG_TCP_47984 | ML_PORT_FLAG_TCP_47989);
}
else {
portTestResult = 0;
}
emit computerAddCompleted(false, portTestResult != 0 && portTestResult != ML_TEST_RESULT_INCONCLUSIVE);
emit computerAddCompleted(false);
}
return QString();
}
@@ -660,21 +572,16 @@ private:
{
NvHTTP http(m_Address, QSslCertificate());
qInfo() << "Processing new PC at" << m_Address << "from" << (m_Mdns ? "mDNS" : "user") << m_MdnsIpv6Address;
qInfo() << "Processing new PC at" << m_Address << "from" << (m_Mdns ? "mDNS" : "user");
// Perform initial serverinfo fetch over HTTP since we don't know which cert to use
QString serverInfo = fetchServerInfo(http);
if (serverInfo.isEmpty() && !m_MdnsIpv6Address.isNull()) {
// Retry using the global IPv6 address if the IPv4 or link-local IPv6 address fails
http.setAddress(m_MdnsIpv6Address.toString());
serverInfo = fetchServerInfo(http);
}
if (serverInfo.isEmpty()) {
return;
}
// Create initial newComputer using HTTP serverinfo with no pinned cert
NvComputer* newComputer = new NvComputer(http.address(), serverInfo, QSslCertificate());
NvComputer* newComputer = new NvComputer(m_Address, serverInfo, QSslCertificate());
// Check if we have a record of this host UUID to pull the pinned cert
NvComputer* existingComputer;
@@ -694,46 +601,28 @@ private:
}
// Update the polled computer with the HTTPS information
NvComputer httpsComputer(http.address(), serverInfo, QSslCertificate());
NvComputer httpsComputer(m_Address, serverInfo, QSslCertificate());
newComputer->update(httpsComputer);
}
// Update addresses depending on the context
if (m_Mdns) {
// Only update local address if we actually reached the PC via this address.
// If we reached it via the IPv6 address after the local address failed,
// don't store the non-working local address.
if (http.address() == m_Address) {
newComputer->localAddress = m_Address;
}
newComputer->localAddress = m_Address;
// Get the WAN IP address using STUN if we're on mDNS over IPv4
if (QHostAddress(newComputer->localAddress).protocol() == QAbstractSocket::IPv4Protocol) {
quint32 addr;
int err = LiFindExternalAddressIP4("stun.moonlight-stream.org", 3478, &addr);
if (err == 0) {
newComputer->remoteAddress = QHostAddress(qFromBigEndian(addr)).toString();
}
else {
qWarning() << "STUN failed to get WAN address:" << err;
}
// Get the WAN IP address using STUN if we're on mDNS
quint32 addr;
int err = LiFindExternalAddressIP4("stun.moonlight-stream.org", 3478, &addr);
if (err == 0) {
newComputer->remoteAddress = QHostAddress(qFromBigEndian(addr)).toString();
}
if (!m_MdnsIpv6Address.isNull()) {
Q_ASSERT(m_MdnsIpv6Address.protocol() == QAbstractSocket::IPv6Protocol);
newComputer->ipv6Address = m_MdnsIpv6Address.toString();
else {
qWarning() << "STUN failed to get WAN address:" << err;
}
}
else {
newComputer->manualAddress = m_Address;
}
QHostAddress hostAddress(m_Address);
bool addressIsSiteLocalV4 =
hostAddress.isInSubnet(QHostAddress("10.0.0.0"), 8) ||
hostAddress.isInSubnet(QHostAddress("172.16.0.0"), 12) ||
hostAddress.isInSubnet(QHostAddress("192.168.0.0"), 16);
{
// Check if this PC already exists
QWriteLocker lock(&m_ComputerManager->m_Lock);
@@ -748,7 +637,7 @@ private:
// For non-mDNS clients, let them know it succeeded
if (!m_Mdns) {
emit computerAddCompleted(true, false);
emit computerAddCompleted(true);
}
// Tell our client if something changed
@@ -767,24 +656,9 @@ private:
// Drop the lock before notifying
lock.unlock();
// If this wasn't added via mDNS but it is a RFC 1918 IPv4 address and not a VPN,
// go ahead and do the STUN request now to populate an external address.
if (!m_Mdns && addressIsSiteLocalV4 && !newComputer->isReachableOverVpn()) {
quint32 addr;
int err = LiFindExternalAddressIP4("stun.moonlight-stream.org", 3478, &addr);
if (err == 0) {
lock.relock();
newComputer->remoteAddress = QHostAddress(qFromBigEndian(addr)).toString();
lock.unlock();
}
else {
qWarning() << "STUN failed to get WAN address:" << err;
}
}
// For non-mDNS clients, let them know it succeeded
if (!m_Mdns) {
emit computerAddCompleted(true, false);
emit computerAddCompleted(true);
}
// Tell our client about this new PC
@@ -795,15 +669,14 @@ private:
ComputerManager* m_ComputerManager;
QString m_Address;
QHostAddress m_MdnsIpv6Address;
bool m_Mdns;
};
void ComputerManager::addNewHost(QString address, bool mdns, QHostAddress mdnsIpv6Address)
void ComputerManager::addNewHost(QString address, bool mdns)
{
// Punt to a worker thread to avoid stalling the
// UI while waiting for serverinfo query to complete
PendingAddTask* addTask = new PendingAddTask(this, address, mdnsIpv6Address, mdns);
PendingAddTask* addTask = new PendingAddTask(this, address, mdns);
QThreadPool::globalInstance()->start(addTask);
}
+14 -123
View File
@@ -13,7 +13,6 @@
#include <QReadWriteLock>
#include <QSettings>
#include <QRunnable>
#include <QTimer>
class MdnsPendingComputer : public QObject
{
@@ -21,18 +20,13 @@ class MdnsPendingComputer : public QObject
public:
explicit MdnsPendingComputer(QMdnsEngine::Server* server,
QMdnsEngine::Cache* cache,
const QMdnsEngine::Service& service)
: m_Hostname(service.hostname()),
m_Server(server),
m_Resolver(nullptr)
m_Resolver(server, m_Hostname, cache)
{
// Start resolving
resolve();
}
virtual ~MdnsPendingComputer()
{
delete m_Resolver;
connect(&m_Resolver, SIGNAL(resolved(QHostAddress)),
this, SLOT(handleResolved(QHostAddress)));
}
QString hostname()
@@ -41,115 +35,20 @@ public:
}
private slots:
void handleResolvedTimeout()
void handleResolved(const QHostAddress& address)
{
if (m_Addresses.isEmpty()) {
// Try again
qInfo() << "Resolving" << hostname() << "timed out. Retrying...";
resolve();
if (address.protocol() == QAbstractSocket::IPv4Protocol) {
m_Resolver.disconnect();
emit resolvedv4(this, address);
}
else {
Q_ASSERT(!m_Addresses.isEmpty());
emit resolvedHost(this, m_Addresses);
}
}
void handleResolvedAddress(const QHostAddress& address)
{
qInfo() << "Resolved" << hostname() << "to" << address;
m_Addresses.push_back(address);
}
signals:
void resolvedHost(MdnsPendingComputer*,QVector<QHostAddress>&);
void resolvedv4(MdnsPendingComputer*, const QHostAddress&);
private:
void resolve()
{
delete m_Resolver;
m_Resolver = new QMdnsEngine::Resolver(m_Server, m_Hostname);
connect(m_Resolver, &QMdnsEngine::Resolver::resolved,
this, &MdnsPendingComputer::handleResolvedAddress);
QTimer::singleShot(2000, this, SLOT(handleResolvedTimeout()));
}
QByteArray m_Hostname;
QMdnsEngine::Server* m_Server;
QMdnsEngine::Resolver* m_Resolver;
QVector<QHostAddress> m_Addresses;
};
class ComputerPollingEntry
{
public:
ComputerPollingEntry()
: m_ActiveThread(nullptr)
{
}
virtual ~ComputerPollingEntry()
{
interrupt();
// interrupt() should have taken care of this
Q_ASSERT(m_ActiveThread == nullptr);
for (QThread* thread : m_InactiveList) {
thread->wait();
delete thread;
}
}
bool isActive()
{
cleanInactiveList();
return m_ActiveThread != nullptr;
}
void setActiveThread(QThread* thread)
{
cleanInactiveList();
Q_ASSERT(!isActive());
m_ActiveThread = thread;
}
void interrupt()
{
cleanInactiveList();
if (m_ActiveThread != nullptr) {
// Interrupt the active thread
m_ActiveThread->requestInterruption();
// Place it on the inactive list awaiting death
m_InactiveList.append(m_ActiveThread);
m_ActiveThread = nullptr;
}
}
private:
void cleanInactiveList()
{
QMutableListIterator<QThread*> i(m_InactiveList);
// Reap any threads that have finished
while (i.hasNext()) {
i.next();
QThread* thread = i.value();
if (thread->isFinished()) {
delete thread;
i.remove();
}
}
}
QThread* m_ActiveThread;
QList<QThread*> m_InactiveList;
QMdnsEngine::Resolver m_Resolver;
};
class ComputerManager : public QObject
@@ -168,7 +67,7 @@ public:
Q_INVOKABLE void stopPollingAsync();
Q_INVOKABLE void addNewHost(QString address, bool mdns, QHostAddress mdnsIpv6Address = QHostAddress());
Q_INVOKABLE void addNewHost(QString address, bool mdns);
void pairHost(NvComputer* computer, QString pin);
@@ -179,37 +78,29 @@ public:
// computer is deleted inside this call
void deleteHost(NvComputer* computer);
void renameHost(NvComputer* computer, QString name);
void clientSideAttributeUpdated(NvComputer* computer);
signals:
void computerStateChanged(NvComputer* computer);
void pairingCompleted(NvComputer* computer, QString error);
void computerAddCompleted(QVariant success, QVariant detectedPortBlocking);
void computerAddCompleted(QVariant success);
void quitAppCompleted(QVariant error);
private slots:
void handleAboutToQuit();
void handleComputerStateChanged(NvComputer* computer);
void handleMdnsServiceResolved(MdnsPendingComputer* computer, QVector<QHostAddress>& addresses);
void handleMdnsServiceResolved(MdnsPendingComputer* computer, const QHostAddress& address);
private:
void saveHosts();
QHostAddress getBestGlobalAddressV6(QVector<QHostAddress>& addresses);
void startPollingComputer(NvComputer* computer);
int m_PollingRef;
QReadWriteLock m_Lock;
QMap<QString, NvComputer*> m_KnownHosts;
QMap<QString, ComputerPollingEntry*> m_PollEntries;
QMap<QString, QThread*> m_PollThreads;
QMdnsEngine::Server m_MdnsServer;
QMdnsEngine::Browser* m_MdnsBrowser;
QMdnsEngine::Cache m_MdnsCache;
+5 -12
View File
@@ -39,18 +39,11 @@ void ComputerSeeker::onComputerUpdated(NvComputer *computer)
bool ComputerSeeker::matchComputer(NvComputer *computer) const
{
QString value = m_ComputerName.toLower();
if (computer->name.toLower() == value || computer->uuid.toLower() == value) {
return true;
}
for (const QString& addr : computer->uniqueAddresses()) {
if (addr.toLower() == value) {
return true;
}
}
return false;
return computer->name.toLower() == value ||
computer->localAddress.toLower() == value ||
computer->remoteAddress.toLower() == value ||
computer->manualAddress.toLower() == value ||
computer->uuid.toLower() == value;
}
bool ComputerSeeker::isOnline(NvComputer *computer) const
+15 -12
View File
@@ -5,6 +5,7 @@
#include <openssl/pem.h>
#include <openssl/rsa.h>
#include <openssl/bn.h>
#include <openssl/x509.h>
#include <openssl/rand.h>
@@ -31,19 +32,20 @@ void IdentityManager::createCredentials(QSettings& settings)
X509* cert = X509_new();
THROW_BAD_ALLOC_IF_NULL(cert);
EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, NULL);
THROW_BAD_ALLOC_IF_NULL(ctx);
EVP_PKEY_keygen_init(ctx);
EVP_PKEY_CTX_set_rsa_keygen_bits(ctx, 2048);
// pk must be initialized on input
EVP_PKEY* pk = NULL;
EVP_PKEY_keygen(ctx, &pk);
EVP_PKEY_CTX_free(ctx);
EVP_PKEY* pk = EVP_PKEY_new();
THROW_BAD_ALLOC_IF_NULL(pk);
BIGNUM* bne = BN_new();
THROW_BAD_ALLOC_IF_NULL(bne);
RSA* rsa = RSA_new();
THROW_BAD_ALLOC_IF_NULL(rsa);
BN_set_word(bne, RSA_F4);
RSA_generate_key_ex(rsa, 2048, bne, nullptr);
EVP_PKEY_assign_RSA(pk, rsa);
X509_set_version(cert, 2);
ASN1_INTEGER_set(X509_get_serialNumber(cert), 0);
#if OPENSSL_VERSION_NUMBER < 0x10100000L
@@ -73,7 +75,7 @@ void IdentityManager::createCredentials(QSettings& settings)
-1, -1, 0);
X509_set_issuer_name(cert, name);
X509_sign(cert, pk, EVP_sha256());
X509_sign(cert, pk, EVP_sha1());
BIO* biokey = BIO_new(BIO_s_mem());
THROW_BAD_ALLOC_IF_NULL(biokey);
@@ -92,6 +94,7 @@ void IdentityManager::createCredentials(QSettings& settings)
X509_free(cert);
EVP_PKEY_free(pk);
BN_free(bne);
BIO_free(biokey);
BIO_free(biocert);
-28
View File
@@ -1,28 +0,0 @@
#include "nvapp.h"
#define SER_APPNAME "name"
#define SER_APPID "id"
#define SER_APPHDR "hdr"
#define SER_APPCOLLECTOR "appcollector"
#define SER_HIDDEN "hidden"
#define SER_DIRECTLAUNCH "directlaunch"
NvApp::NvApp(QSettings& settings)
{
name = settings.value(SER_APPNAME).toString();
id = settings.value(SER_APPID).toInt();
hdrSupported = settings.value(SER_APPHDR).toBool();
isAppCollectorGame = settings.value(SER_APPCOLLECTOR).toBool();
hidden = settings.value(SER_HIDDEN).toBool();
directLaunch = settings.value(SER_DIRECTLAUNCH).toBool();
}
void NvApp::serialize(QSettings& settings) const
{
settings.setValue(SER_APPNAME, name);
settings.setValue(SER_APPID, id);
settings.setValue(SER_APPHDR, hdrSupported);
settings.setValue(SER_APPCOLLECTOR, isAppCollectorGame);
settings.setValue(SER_HIDDEN, hidden);
settings.setValue(SER_DIRECTLAUNCH, directLaunch);
}
-42
View File
@@ -1,42 +0,0 @@
#pragma once
#include <QSettings>
class NvApp
{
public:
NvApp() {}
explicit NvApp(QSettings& settings);
bool operator==(const NvApp& other) const
{
return id == other.id &&
name == other.name &&
hdrSupported == other.hdrSupported &&
isAppCollectorGame == other.isAppCollectorGame &&
hidden == other.hidden &&
directLaunch == other.directLaunch;
}
bool operator!=(const NvApp& other) const
{
return !operator==(other);
}
bool isInitialized()
{
return id != 0 && !name.isEmpty();
}
void
serialize(QSettings& settings) const;
int id = 0;
QString name;
bool hdrSupported = false;
bool isAppCollectorGame = false;
bool hidden = false;
bool directLaunch = false;
};
Q_DECLARE_METATYPE(NvApp)
+20 -128
View File
@@ -1,10 +1,8 @@
#include "nvcomputer.h"
#include "nvapp.h"
#include <QUdpSocket>
#include <QHostInfo>
#include <QNetworkInterface>
#include <QNetworkProxy>
#define SER_NAME "hostname"
#define SER_UUID "uuid"
@@ -12,28 +10,33 @@
#define SER_LOCALADDR "localaddress"
#define SER_REMOTEADDR "remoteaddress"
#define SER_MANUALADDR "manualaddress"
#define SER_IPV6ADDR "ipv6address"
#define SER_APPLIST "apps"
#define SER_SRVCERT "srvcert"
#define SER_CUSTOMNAME "customname"
#define SER_APPNAME "name"
#define SER_APPID "id"
#define SER_APPHDR "hdr"
NvComputer::NvComputer(QSettings& settings)
{
this->name = settings.value(SER_NAME).toString();
this->uuid = settings.value(SER_UUID).toString();
this->hasCustomName = settings.value(SER_CUSTOMNAME).toBool();
this->macAddress = settings.value(SER_MAC).toByteArray();
this->localAddress = settings.value(SER_LOCALADDR).toString();
this->remoteAddress = settings.value(SER_REMOTEADDR).toString();
this->ipv6Address = settings.value(SER_IPV6ADDR).toString();
this->manualAddress = settings.value(SER_MANUALADDR).toString();
this->serverCert = QSslCertificate(settings.value(SER_SRVCERT).toByteArray());
int appCount = settings.beginReadArray(SER_APPLIST);
for (int i = 0; i < appCount; i++) {
NvApp app;
settings.setArrayIndex(i);
NvApp app(settings);
app.name = settings.value(SER_APPNAME).toString();
app.id = settings.value(SER_APPID).toInt();
app.hdrSupported = settings.value(SER_APPHDR).toBool();
this->appList.append(app);
}
settings.endArray();
@@ -51,17 +54,15 @@ NvComputer::NvComputer(QSettings& settings)
this->gpuModel = nullptr;
}
void NvComputer::serialize(QSettings& settings) const
void NvComputer::serialize(QSettings& settings)
{
QReadLocker lock(&this->lock);
settings.setValue(SER_NAME, name);
settings.setValue(SER_CUSTOMNAME, hasCustomName);
settings.setValue(SER_UUID, uuid);
settings.setValue(SER_MAC, macAddress);
settings.setValue(SER_LOCALADDR, localAddress);
settings.setValue(SER_REMOTEADDR, remoteAddress);
settings.setValue(SER_IPV6ADDR, ipv6Address);
settings.setValue(SER_MANUALADDR, manualAddress);
settings.setValue(SER_SRVCERT, serverCert.toPem());
@@ -71,7 +72,10 @@ void NvComputer::serialize(QSettings& settings) const
settings.beginWriteArray(SER_APPLIST);
for (int i = 0; i < appList.count(); i++) {
settings.setArrayIndex(i);
appList[i].serialize(settings);
settings.setValue(SER_APPNAME, appList[i].name);
settings.setValue(SER_APPID, appList[i].id);
settings.setValue(SER_APPHDR, appList[i].hdrSupported);
}
settings.endArray();
}
@@ -88,7 +92,6 @@ NvComputer::NvComputer(QString address, QString serverInfo, QSslCertificate serv
{
this->serverCert = serverCert;
this->hasCustomName = false;
this->name = NvHTTP::getXmlString(serverInfo, "hostname");
if (this->name.isEmpty()) {
this->name = "UNKNOWN";
@@ -126,12 +129,7 @@ NvComputer::NvComputer(QString address, QString serverInfo, QSslCertificate serv
mode2.width * mode2.height * mode2.refreshRate;
});
// We can get an IPv4 loopback address if we're using the GS IPv6 Forwarder
this->localAddress = NvHTTP::getXmlString(serverInfo, "LocalIP");
if (this->localAddress.startsWith("127.")) {
this->localAddress = QString();
}
this->remoteAddress = NvHTTP::getXmlString(serverInfo, "ExternalIP");
this->pairState = NvHTTP::getXmlString(serverInfo, "PairStatus") == "1" ?
PS_PAIRED : PS_NOT_PAIRED;
@@ -157,9 +155,8 @@ bool NvComputer::wake()
}
const quint16 WOL_PORTS[] = {
9, // Standard WOL port (privileged port)
47998, 47999, 48000, 48002, 48010, // Ports opened by GFE
47009, // Port opened by Moonlight Internet Hosting Tool for WoL (non-privileged port)
7, 9, // Standard WOL ports
47998, 47999, 48000, // Ports opened by GFE
};
// Create the WoL payload
@@ -232,102 +229,7 @@ bool NvComputer::wake()
return success;
}
bool NvComputer::isReachableOverVpn()
{
if (activeAddress.isEmpty()) {
return false;
}
QTcpSocket s;
s.setProxy(QNetworkProxy::NoProxy);
s.connectToHost(activeAddress, 47984);
if (s.waitForConnected(3000)) {
Q_ASSERT(!s.localAddress().isNull());
for (const QNetworkInterface& nic : QNetworkInterface::allInterfaces()) {
// Ensure the interface is up
if ((nic.flags() & QNetworkInterface::IsUp) == 0) {
continue;
}
for (const QNetworkAddressEntry& addr : nic.addressEntries()) {
if (addr.ip() == s.localAddress()) {
qInfo() << "Found matching interface:" << nic.humanReadableName() << nic.hardwareAddress() << nic.flags();
#if QT_VERSION >= QT_VERSION_CHECK(5, 11, 0)
qInfo() << "Interface Type:" << nic.type();
qInfo() << "Interface MTU:" << nic.maximumTransmissionUnit();
if (nic.type() == QNetworkInterface::Virtual ||
nic.type() == QNetworkInterface::Ppp) {
// Treat PPP and virtual interfaces as likely VPNs
return true;
}
if (nic.maximumTransmissionUnit() != 0 && nic.maximumTransmissionUnit() < 1500) {
// Treat MTUs under 1500 as likely VPNs
return true;
}
#endif
if (nic.flags() & QNetworkInterface::IsPointToPoint) {
// Treat point-to-point links as likely VPNs.
// This check detects OpenVPN on Unix-like OSes.
return true;
}
if (nic.hardwareAddress().startsWith("00:FF", Qt::CaseInsensitive)) {
// OpenVPN TAP interfaces have a MAC address starting with 00:FF on Windows
return true;
}
if (nic.humanReadableName().startsWith("ZeroTier")) {
// ZeroTier interfaces always start with "ZeroTier"
return true;
}
if (nic.humanReadableName().contains("VPN")) {
// This one is just a final heuristic if all else fails
return true;
}
// Didn't meet any of our VPN heuristics
return false;
}
}
}
qWarning() << "No match found for address:" << s.localAddress();
return false;
}
else {
// If we fail to connect, just pretend that it's not a VPN
return false;
}
}
bool NvComputer::updateAppList(QVector<NvApp> newAppList) {
if (appList == newAppList) {
return false;
}
// Propagate client-side attributes to the new app list
for (const NvApp& existingApp : appList) {
for (NvApp& newApp : newAppList) {
if (existingApp.id == newApp.id) {
newApp.hidden = existingApp.hidden;
newApp.directLaunch = existingApp.directLaunch;
}
}
}
appList = newAppList;
sortAppList();
return true;
}
QVector<QString> NvComputer::uniqueAddresses() const
QVector<QString> NvComputer::uniqueAddresses()
{
QVector<QString> uniqueAddressList;
@@ -335,7 +237,6 @@ QVector<QString> NvComputer::uniqueAddresses() const
uniqueAddressList.append(activeAddress);
uniqueAddressList.append(localAddress);
uniqueAddressList.append(remoteAddress);
uniqueAddressList.append(ipv6Address);
uniqueAddressList.append(manualAddress);
// Prune duplicates (always giving precedence to the first)
@@ -391,14 +292,10 @@ bool NvComputer::update(NvComputer& that)
changed = true; \
}
if (!hasCustomName) {
// Only overwrite the name if it's not custom
ASSIGN_IF_CHANGED(name);
}
ASSIGN_IF_CHANGED(name);
ASSIGN_IF_CHANGED_AND_NONEMPTY(macAddress);
ASSIGN_IF_CHANGED_AND_NONEMPTY(localAddress);
ASSIGN_IF_CHANGED_AND_NONEMPTY(remoteAddress);
ASSIGN_IF_CHANGED_AND_NONEMPTY(ipv6Address);
ASSIGN_IF_CHANGED_AND_NONEMPTY(manualAddress);
ASSIGN_IF_CHANGED(pairState);
ASSIGN_IF_CHANGED(serverCodecModeSupport);
@@ -410,12 +307,7 @@ bool NvComputer::update(NvComputer& that)
ASSIGN_IF_CHANGED(maxLumaPixelsHEVC);
ASSIGN_IF_CHANGED(gpuModel);
ASSIGN_IF_CHANGED_AND_NONNULL(serverCert);
ASSIGN_IF_CHANGED_AND_NONEMPTY(appList);
ASSIGN_IF_CHANGED_AND_NONEMPTY(displayModes);
if (!that.appList.isEmpty()) {
// updateAppList() handles merging client-side attributes
updateAppList(that.appList);
}
return changed;
}
+3 -10
View File
@@ -16,8 +16,6 @@ class NvComputer
private:
void sortAppList();
bool updateAppList(QVector<NvApp> newAppList);
bool pendingQuit;
public:
@@ -31,14 +29,11 @@ public:
bool
wake();
bool
isReachableOverVpn();
QVector<QString>
uniqueAddresses() const;
uniqueAddresses();
void
serialize(QSettings& settings) const;
serialize(QSettings& settings);
enum PairState
{
@@ -69,15 +64,13 @@ public:
// Persisted traits
QString localAddress;
QString remoteAddress;
QString ipv6Address;
QString manualAddress;
QByteArray macAddress;
QString name;
bool hasCustomName;
QString uuid;
QSslCertificate serverCert;
QVector<NvApp> appList;
// Synchronization
mutable QReadWriteLock lock;
QReadWriteLock lock;
};
+73 -100
View File
@@ -12,27 +12,27 @@
#include <QtEndian>
#include <QNetworkProxy>
#define FAST_FAIL_TIMEOUT_MS 2000
#define REQUEST_TIMEOUT_MS 5000
#define LAUNCH_TIMEOUT_MS 120000
#define RESUME_TIMEOUT_MS 30000
#define QUIT_TIMEOUT_MS 30000
NvHTTP::NvHTTP(QString address, QSslCertificate serverCert) :
m_Address(address),
m_ServerCert(serverCert)
{
Q_ASSERT(!address.isEmpty());
m_BaseUrlHttp.setScheme("http");
m_BaseUrlHttps.setScheme("https");
m_BaseUrlHttp.setHost(address);
m_BaseUrlHttps.setHost(address);
m_BaseUrlHttp.setPort(47989);
m_BaseUrlHttps.setPort(47984);
setAddress(address);
// Never use a proxy server
QNetworkProxy noProxy(QNetworkProxy::NoProxy);
m_Nam.setProxy(noProxy);
connect(&m_Nam, &QNetworkAccessManager::sslErrors, this, &NvHTTP::handleSslErrors);
}
void NvHTTP::setServerCert(QSslCertificate serverCert)
@@ -40,21 +40,6 @@ void NvHTTP::setServerCert(QSslCertificate serverCert)
m_ServerCert = serverCert;
}
void NvHTTP::setAddress(QString address)
{
Q_ASSERT(!address.isEmpty());
m_Address = address;
m_BaseUrlHttp.setHost(address);
m_BaseUrlHttps.setHost(address);
}
QString NvHTTP::address()
{
return m_Address;
}
QVector<int>
NvHTTP::parseQuad(QString quad)
{
@@ -93,7 +78,7 @@ NvHTTP::getCurrentGame(QString serverInfo)
}
QString
NvHTTP::getServerInfo(NvLogLevel logLevel, bool fastFail)
NvHTTP::getServerInfo(NvLogLevel logLevel)
{
QString serverInfo;
@@ -107,7 +92,7 @@ NvHTTP::getServerInfo(NvLogLevel logLevel, bool fastFail)
serverInfo = openConnectionToString(m_BaseUrlHttps,
"serverinfo",
nullptr,
fastFail ? FAST_FAIL_TIMEOUT_MS : REQUEST_TIMEOUT_MS,
REQUEST_TIMEOUT_MS,
logLevel);
// Throws if the request failed
verifyResponseStatus(serverInfo);
@@ -120,7 +105,7 @@ NvHTTP::getServerInfo(NvLogLevel logLevel, bool fastFail)
serverInfo = openConnectionToString(m_BaseUrlHttp,
"serverinfo",
nullptr,
fastFail ? FAST_FAIL_TIMEOUT_MS : REQUEST_TIMEOUT_MS,
REQUEST_TIMEOUT_MS,
logLevel);
verifyResponseStatus(serverInfo);
}
@@ -137,7 +122,7 @@ NvHTTP::getServerInfo(NvLogLevel logLevel, bool fastFail)
serverInfo = openConnectionToString(m_BaseUrlHttp,
"serverinfo",
nullptr,
fastFail ? FAST_FAIL_TIMEOUT_MS : REQUEST_TIMEOUT_MS,
REQUEST_TIMEOUT_MS,
logLevel);
verifyResponseStatus(serverInfo);
}
@@ -145,6 +130,30 @@ NvHTTP::getServerInfo(NvLogLevel logLevel, bool fastFail)
return serverInfo;
}
static QString
getSurroundAudioInfoString(int audioConfig)
{
int channelMask;
int channelCount;
switch (audioConfig)
{
case AUDIO_CONFIGURATION_STEREO:
channelCount = 2;
channelMask = 0x3;
break;
case AUDIO_CONFIGURATION_51_SURROUND:
channelCount = 6;
channelMask = 0xFC;
break;
default:
Q_ASSERT(false);
return 0;
}
return QString::number(channelMask << 16 | channelCount);
}
void
NvHTTP::launchApp(int appId,
PSTREAM_CONFIGURATION streamConfig,
@@ -163,11 +172,7 @@ NvHTTP::launchApp(int appId,
"appid="+QString::number(appId)+
"&mode="+QString::number(streamConfig->width)+"x"+
QString::number(streamConfig->height)+"x"+
// Using an FPS value over 60 causes SOPS to default to 720p60,
// so force it to 0 to ensure the correct resolution is set. We
// used to use 60 here but that locked the frame rate to 60 FPS
// on GFE 3.20.3.
QString::number(streamConfig->fps > 60 ? 0 : streamConfig->fps)+
QString::number(streamConfig->fps)+
"&additionalStates=1&sops="+QString::number(sops ? 1 : 0)+
"&rikey="+QByteArray(streamConfig->remoteInputAesKey, sizeof(streamConfig->remoteInputAesKey)).toHex()+
"&rikeyid="+QString::number(riKeyId)+
@@ -175,7 +180,7 @@ NvHTTP::launchApp(int appId,
"&hdrMode=1&clientHdrCapVersion=0&clientHdrCapSupportedFlagsInUint32=0&clientHdrCapMetaDataId=NV_STATIC_METADATA_TYPE_1&clientHdrCapDisplayData=0x0x0x0x0x0x0x0x0x0x0" :
"")+
"&localAudioPlayMode="+QString::number(localAudio ? 1 : 0)+
"&surroundAudioInfo="+QString::number(SURROUNDAUDIOINFO_FROM_AUDIO_CONFIGURATION(streamConfig->audioConfiguration))+
"&surroundAudioInfo="+getSurroundAudioInfoString(streamConfig->audioConfiguration)+
"&remoteControllersBitmap="+QString::number(gamepadMask)+
"&gcmap="+QString::number(gamepadMask),
LAUNCH_TIMEOUT_MS);
@@ -197,7 +202,7 @@ NvHTTP::resumeApp(PSTREAM_CONFIGURATION streamConfig)
"resume",
"rikey="+QString(QByteArray(streamConfig->remoteInputAesKey, sizeof(streamConfig->remoteInputAesKey)).toHex())+
"&rikeyid="+QString::number(riKeyId)+
"&surroundAudioInfo="+QString::number(SURROUNDAUDIOINFO_FROM_AUDIO_CONFIGURATION(streamConfig->audioConfiguration)),
"&surroundAudioInfo="+getSurroundAudioInfoString(streamConfig->audioConfiguration),
RESUME_TIMEOUT_MS);
// Throws if the request failed
@@ -218,7 +223,7 @@ NvHTTP::quitApp()
// Newer GFE versions will just return success even if quitting fails
// if we're not the original requestor.
if (getCurrentGame(getServerInfo(NvHTTP::NVLL_ERROR)) != 0) {
if (getCurrentGame(getServerInfo(NvHTTP::ERROR)) != 0) {
// Generate a synthetic GfeResponseException letting the caller know
// that they can't kill someone else's stream.
throw GfeHttpResponseException(599, "");
@@ -233,17 +238,17 @@ NvHTTP::getDisplayModeList(QString serverInfo)
while (!xmlReader.atEnd()) {
while (xmlReader.readNextStartElement()) {
auto name = xmlReader.name();
if (name == QString("DisplayMode")) {
QStringRef name = xmlReader.name();
if (xmlReader.name() == "DisplayMode") {
modes.append(NvDisplayMode());
}
else if (name == QString("Width")) {
else if (xmlReader.name() == "Width") {
modes.last().width = xmlReader.readElementText().toInt();
}
else if (name == QString("Height")) {
else if (xmlReader.name() == "Height") {
modes.last().height = xmlReader.readElementText().toInt();
}
else if (name == QString("RefreshRate")) {
else if (xmlReader.name() == "RefreshRate") {
modes.last().refreshRate = xmlReader.readElementText().toInt();
}
}
@@ -259,15 +264,15 @@ NvHTTP::getAppList()
"applist",
nullptr,
REQUEST_TIMEOUT_MS,
NvLogLevel::NVLL_ERROR);
NvLogLevel::ERROR);
verifyResponseStatus(appxml);
QXmlStreamReader xmlReader(appxml);
QVector<NvApp> apps;
while (!xmlReader.atEnd()) {
while (xmlReader.readNextStartElement()) {
auto name = xmlReader.name();
if (name == QString("App")) {
QStringRef name = xmlReader.name();
if (xmlReader.name() == "App") {
// We must have a valid app before advancing to the next one
if (!apps.isEmpty() && !apps.last().isInitialized()) {
qWarning() << "Invalid applist XML";
@@ -276,18 +281,15 @@ NvHTTP::getAppList()
}
apps.append(NvApp());
}
else if (name == QString("AppTitle")) {
else if (xmlReader.name() == "AppTitle") {
apps.last().name = xmlReader.readElementText();
}
else if (name == QString("ID")) {
else if (xmlReader.name() == "ID") {
apps.last().id = xmlReader.readElementText().toInt();
}
else if (name == QString("IsHdrSupported")) {
else if (xmlReader.name() == "IsHdrSupported") {
apps.last().hdrSupported = xmlReader.readElementText() == "1";
}
else if (name == QString("IsAppCollectorGame")) {
apps.last().isAppCollectorGame = xmlReader.readElementText() == "1";
}
}
}
@@ -301,12 +303,9 @@ NvHTTP::verifyResponseStatus(QString xml)
while (xmlReader.readNextStartElement())
{
if (xmlReader.name() == QString("root"))
if (xmlReader.name() == "root")
{
// Status code can be 0xFFFFFFFF in some rare cases on GFE 3.20.3, and
// QString::toInt() will fail in that case, so use QString::toUInt()
// and cast the result to an int instead.
int statusCode = (int)xmlReader.attributes().value("status_code").toUInt();
int statusCode = xmlReader.attributes().value("status_code").toInt();
if (statusCode == 200)
{
// Successful
@@ -319,12 +318,6 @@ NvHTTP::verifyResponseStatus(QString xml)
// 401 is expected for unpaired PCs when we fetch serverinfo over HTTPS
qWarning() << "Request failed:" << statusCode << statusMessage;
}
if (statusCode == -1 && statusMessage == "Invalid") {
// Special case handling an audio capture error which GFE doesn't
// provide any useful status message for.
statusCode = 418;
statusMessage = tr("Missing audio capture device. Reinstalling GeForce Experience should resolve this error.");
}
throw GfeHttpResponseException(statusCode, statusMessage);
}
}
@@ -339,7 +332,7 @@ NvHTTP::getBoxArt(int appId)
"appid="+QString::number(appId)+
"&AssetType=2&AssetIdx=0",
REQUEST_TIMEOUT_MS,
NvLogLevel::NVLL_VERBOSE);
NvLogLevel::VERBOSE);
QImage image = QImageReader(reply).read();
delete reply;
@@ -381,28 +374,6 @@ NvHTTP::getXmlString(QString xml,
return nullptr;
}
void NvHTTP::handleSslErrors(QNetworkReply* reply, const QList<QSslError>& errors)
{
bool ignoreErrors = true;
if (m_ServerCert.isNull()) {
// We should never make an HTTPS request without a cert
Q_ASSERT(!m_ServerCert.isNull());
return;
}
for (auto error : errors) {
if (m_ServerCert != error.certificate()) {
ignoreErrors = false;
break;
}
}
if (ignoreErrors) {
reply->ignoreSslErrors(errors);
}
}
QString
NvHTTP::openConnectionToString(QUrl baseUrl,
QString command,
@@ -414,13 +385,7 @@ NvHTTP::openConnectionToString(QUrl baseUrl,
QString ret;
QTextStream stream(reply);
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
stream.setEncoding(QStringConverter::Utf8);
#else
stream.setCodec("UTF-8");
#endif
ret = stream.readAll();
delete reply;
@@ -445,29 +410,37 @@ NvHTTP::openConnection(QUrl baseUrl,
QUuid::createUuid().toRfc4122().toHex() +
((arguments != nullptr) ? ("&" + arguments) : ""));
QNetworkRequest request(url);
QNetworkRequest request = QNetworkRequest(url);
// Add our client certificate
request.setSslConfiguration(IdentityManager::get()->getSslConfig());
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) && QT_VERSION < QT_VERSION_CHECK(5, 15, 1) && !defined(QT_NO_BEARERMANAGEMENT)
// HACK: Set network accessibility to work around QTBUG-80947 (introduced in Qt 5.14.0 and fixed in Qt 5.15.1)
QT_WARNING_PUSH
QT_WARNING_DISABLE_DEPRECATED
m_Nam.setNetworkAccessible(QNetworkAccessManager::Accessible);
QT_WARNING_POP
#endif
QNetworkReply* reply = m_Nam.get(request);
// Assert that we always have a server cert for HTTPS, since
// the request will fail anyway if we do not.
Q_ASSERT(!m_ServerCert.isNull() || baseUrl == m_BaseUrlHttp);
if (!m_ServerCert.isNull()) {
// Pin the server certificate received during pairing
QList<QSslError> expectedSslErrors;
expectedSslErrors.append(QSslError(QSslError::HostNameMismatch, m_ServerCert));
expectedSslErrors.append(QSslError(QSslError::SelfSignedCertificate, m_ServerCert));
// The SecureTransport backend for Qt TLS on macOS throws CertificateUntrusted
// instead of SelfSignedCertificate, so we will need to allow that error too.
expectedSslErrors.append(QSslError(QSslError::CertificateUntrusted, m_ServerCert));
reply->ignoreSslErrors(expectedSslErrors);
}
// Run the request with a timeout if requested
QEventLoop loop;
connect(reply, SIGNAL(finished()), &loop, SLOT(quit()));
connect(QCoreApplication::instance(), SIGNAL(aboutToQuit()), &loop, SLOT(quit()));
QObject::connect(reply, SIGNAL(finished()), &loop, SLOT(quit()));
if (timeoutMs) {
QTimer::singleShot(timeoutMs, &loop, SLOT(quit()));
}
if (logLevel >= NvLogLevel::NVLL_VERBOSE) {
if (logLevel >= NvLogLevel::VERBOSE) {
qInfo() << "Executing request:" << url.toString();
}
loop.exec(QEventLoop::ExcludeUserInputEvents);
@@ -475,7 +448,7 @@ NvHTTP::openConnection(QUrl baseUrl,
// Abort the request if it timed out
if (!reply->isFinished())
{
if (logLevel >= NvLogLevel::NVLL_ERROR) {
if (logLevel >= NvLogLevel::ERROR) {
qWarning() << "Aborting timed out request for" << url.toString();
}
reply->abort();
@@ -488,7 +461,7 @@ NvHTTP::openConnection(QUrl baseUrl,
// Handle error
if (reply->error() != QNetworkReply::NoError)
{
if (logLevel >= NvLogLevel::NVLL_ERROR) {
if (logLevel >= NvLogLevel::ERROR) {
qWarning() << command << " request failed with error " << reply->error();
}
+26 -16
View File
@@ -1,7 +1,6 @@
#pragma once
#include "identitymanager.h"
#include "nvapp.h"
#include <Limelight.h>
@@ -9,6 +8,26 @@
#include <QNetworkAccessManager>
#include <QNetworkReply>
class NvApp
{
public:
bool operator==(const NvApp& other) const
{
return id == other.id;
}
bool isInitialized()
{
return id != 0 && !name.isEmpty();
}
int id;
QString name;
bool hdrSupported;
};
Q_DECLARE_METATYPE(NvApp)
class NvDisplayMode
{
public:
@@ -94,15 +113,13 @@ private:
QString m_ErrorText;
};
class NvHTTP : public QObject
class NvHTTP
{
Q_OBJECT
public:
enum NvLogLevel {
NVLL_NONE,
NVLL_ERROR,
NVLL_VERBOSE
NONE,
ERROR,
VERBOSE
};
explicit NvHTTP(QString address, QSslCertificate serverCert);
@@ -112,7 +129,7 @@ public:
getCurrentGame(QString serverInfo);
QString
getServerInfo(NvLogLevel logLevel, bool fastFail = false);
getServerInfo(NvLogLevel logLevel);
static
void
@@ -133,14 +150,10 @@ public:
QString command,
QString arguments,
int timeoutMs,
NvLogLevel logLevel = NvLogLevel::NVLL_VERBOSE);
NvLogLevel logLevel = NvLogLevel::VERBOSE);
void setServerCert(QSslCertificate serverCert);
void setAddress(QString address);
QString address();
static
QVector<int>
parseQuad(QString quad);
@@ -171,9 +184,6 @@ public:
QUrl m_BaseUrlHttp;
QUrl m_BaseUrlHttps;
private:
void
handleSslErrors(QNetworkReply* reply, const QList<QSslError>& errors);
QNetworkReply*
openConnection(QUrl baseUrl,
QString command,
+32 -64
View File
@@ -1,9 +1,8 @@
#include "nvpairingmanager.h"
#include "utils.h"
#include <stdexcept>
#include <openssl/bio.h>
#include <openssl/aes.h>
#include <openssl/rand.h>
#include <openssl/pem.h>
#include <openssl/x509.h>
@@ -52,63 +51,39 @@ NvPairingManager::generateRandomBytes(int length)
}
QByteArray
NvPairingManager::encrypt(const QByteArray& plaintext, const QByteArray& key)
NvPairingManager::encrypt(QByteArray plaintext, AES_KEY* key)
{
QByteArray ciphertext(plaintext.size(), 0);
EVP_CIPHER_CTX* cipher;
int ciphertextLen;
cipher = EVP_CIPHER_CTX_new();
THROW_BAD_ALLOC_IF_NULL(cipher);
EVP_EncryptInit(cipher, EVP_aes_128_ecb(), reinterpret_cast<const unsigned char*>(key.data()), NULL);
EVP_CIPHER_CTX_set_padding(cipher, 0);
EVP_EncryptUpdate(cipher,
reinterpret_cast<unsigned char*>(ciphertext.data()),
&ciphertextLen,
reinterpret_cast<const unsigned char*>(plaintext.data()),
plaintext.length());
Q_ASSERT(ciphertextLen == ciphertext.length());
EVP_CIPHER_CTX_free(cipher);
for (int i = 0; i < plaintext.size(); i += 16)
{
AES_encrypt(reinterpret_cast<unsigned char*>(&plaintext.data()[i]),
reinterpret_cast<unsigned char*>(&ciphertext.data()[i]),
key);
}
return ciphertext;
}
QByteArray
NvPairingManager::decrypt(const QByteArray& ciphertext, const QByteArray& key)
NvPairingManager::decrypt(QByteArray ciphertext, AES_KEY* key)
{
QByteArray plaintext(ciphertext.size(), 0);
EVP_CIPHER_CTX* cipher;
int plaintextLen;
cipher = EVP_CIPHER_CTX_new();
THROW_BAD_ALLOC_IF_NULL(cipher);
EVP_DecryptInit(cipher, EVP_aes_128_ecb(), reinterpret_cast<const unsigned char*>(key.data()), NULL);
EVP_CIPHER_CTX_set_padding(cipher, 0);
EVP_DecryptUpdate(cipher,
reinterpret_cast<unsigned char*>(plaintext.data()),
&plaintextLen,
reinterpret_cast<const unsigned char*>(ciphertext.data()),
ciphertext.length());
Q_ASSERT(plaintextLen == plaintext.length());
EVP_CIPHER_CTX_free(cipher);
for (int i = 0; i < plaintext.size(); i += 16)
{
AES_decrypt(reinterpret_cast<unsigned char*>(&ciphertext.data()[i]),
reinterpret_cast<unsigned char*>(&plaintext.data()[i]),
key);
}
return plaintext;
}
QByteArray
NvPairingManager::getSignatureFromPemCert(const QByteArray& certificate)
NvPairingManager::getSignatureFromPemCert(QByteArray certificate)
{
#if (OPENSSL_VERSION_NUMBER < 0x10100000L)
BIO* bio = BIO_new_mem_buf(const_cast<char*>(certificate.data()), -1);
#else
BIO* bio = BIO_new_mem_buf(certificate.data(), -1);
#endif
THROW_BAD_ALLOC_IF_NULL(bio);
X509* cert = PEM_read_bio_X509(bio, nullptr, nullptr, nullptr);
@@ -132,13 +107,9 @@ NvPairingManager::getSignatureFromPemCert(const QByteArray& certificate)
}
bool
NvPairingManager::verifySignature(const QByteArray& data, const QByteArray& signature, const QByteArray& serverCertificate)
NvPairingManager::verifySignature(QByteArray data, QByteArray signature, QByteArray serverCertificate)
{
#if (OPENSSL_VERSION_NUMBER < 0x10100000L)
BIO* bio = BIO_new_mem_buf(const_cast<char*>(serverCertificate.data()), -1);
#else
BIO* bio = BIO_new_mem_buf(serverCertificate.data(), -1);
#endif
THROW_BAD_ALLOC_IF_NULL(bio);
X509* cert = PEM_read_bio_X509(bio, nullptr, nullptr, nullptr);
@@ -152,7 +123,7 @@ NvPairingManager::verifySignature(const QByteArray& data, const QByteArray& sign
EVP_DigestVerifyInit(mdctx, nullptr, EVP_sha256(), nullptr, pubKey);
EVP_DigestVerifyUpdate(mdctx, data.data(), data.length());
int result = EVP_DigestVerifyFinal(mdctx, reinterpret_cast<unsigned char*>(const_cast<char*>(signature.data())), signature.length());
int result = EVP_DigestVerifyFinal(mdctx, reinterpret_cast<unsigned char*>(signature.data()), signature.length());
EVP_PKEY_free(pubKey);
EVP_MD_CTX_destroy(mdctx);
@@ -162,7 +133,7 @@ NvPairingManager::verifySignature(const QByteArray& data, const QByteArray& sign
}
QByteArray
NvPairingManager::signMessage(const QByteArray& message)
NvPairingManager::signMessage(QByteArray message)
{
EVP_MD_CTX *ctx = EVP_MD_CTX_create();
THROW_BAD_ALLOC_IF_NULL(ctx);
@@ -172,7 +143,7 @@ NvPairingManager::signMessage(const QByteArray& message)
EVP_DigestInit_ex(ctx, md, NULL);
EVP_DigestSignInit(ctx, NULL, md, NULL, m_PrivateKey);
EVP_DigestSignUpdate(ctx, reinterpret_cast<unsigned char*>(const_cast<char*>(message.data())), message.length());
EVP_DigestSignUpdate(ctx, reinterpret_cast<unsigned char*>(message.data()), message.length());
size_t signatureLength = 0;
EVP_DigestSignFinal(ctx, NULL, &signatureLength);
@@ -186,7 +157,7 @@ NvPairingManager::signMessage(const QByteArray& message)
}
QByteArray
NvPairingManager::saltPin(const QByteArray& salt, QString pin)
NvPairingManager::saltPin(QByteArray salt, QString pin)
{
return QByteArray().append(salt).append(pin.toLatin1());
}
@@ -215,8 +186,9 @@ NvPairingManager::pair(QString appVersion, QString pin, QSslCertificate& serverC
QByteArray salt = generateRandomBytes(16);
QByteArray saltedPin = saltPin(salt, pin);
QByteArray aesKey = QCryptographicHash::hash(saltedPin, hashAlgo).data();
aesKey.truncate(16);
AES_KEY encKey, decKey;
AES_set_decrypt_key(reinterpret_cast<const unsigned char*>(QCryptographicHash::hash(saltedPin, hashAlgo).data()), 128, &decKey);
AES_set_encrypt_key(reinterpret_cast<const unsigned char*>(QCryptographicHash::hash(saltedPin, hashAlgo).data()), 128, &encKey);
QString getCert = m_Http.openConnectionToString(m_Http.m_BaseUrlHttp,
"pair",
@@ -238,21 +210,20 @@ NvPairingManager::pair(QString appVersion, QString pin, QSslCertificate& serverC
return PairState::ALREADY_IN_PROGRESS;
}
QSslCertificate unverifiedServerCert = QSslCertificate(serverCertStr);
if (unverifiedServerCert.isNull()) {
Q_ASSERT(!unverifiedServerCert.isNull());
serverCert = QSslCertificate(serverCertStr);
if (serverCert.isNull()) {
Q_ASSERT(!serverCert.isNull());
qCritical() << "Failed to parse plaincert";
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, REQUEST_TIMEOUT_MS);
return PairState::FAILED;
}
// Pin this cert for TLS until pairing is complete. If successful, we will propagate
// the cert into the NvComputer object and persist it.
m_Http.setServerCert(unverifiedServerCert);
// Pin this cert for TLS
m_Http.setServerCert(serverCert);
QByteArray randomChallenge = generateRandomBytes(16);
QByteArray encryptedChallenge = encrypt(randomChallenge, aesKey);
QByteArray encryptedChallenge = encrypt(randomChallenge, &encKey);
QString challengeXml = m_Http.openConnectionToString(m_Http.m_BaseUrlHttp,
"pair",
"devicename=roth&updateState=1&clientchallenge=" +
@@ -266,7 +237,7 @@ NvPairingManager::pair(QString appVersion, QString pin, QSslCertificate& serverC
return PairState::FAILED;
}
QByteArray challengeResponseData = decrypt(m_Http.getXmlStringFromHex(challengeXml, "challengeresponse"), aesKey);
QByteArray challengeResponseData = decrypt(m_Http.getXmlStringFromHex(challengeXml, "challengeresponse"), &decKey);
QByteArray clientSecretData = generateRandomBytes(16);
QByteArray challengeResponse;
QByteArray serverResponse(challengeResponseData.data(), hashLength);
@@ -285,9 +256,7 @@ NvPairingManager::pair(QString appVersion, QString pin, QSslCertificate& serverC
challengeResponse.append(reinterpret_cast<char*>(asnSignature->data), asnSignature->length);
challengeResponse.append(clientSecretData);
QByteArray paddedHash = QCryptographicHash::hash(challengeResponse, hashAlgo);
paddedHash.resize(32);
QByteArray encryptedChallengeResponseHash = encrypt(paddedHash, aesKey);
QByteArray encryptedChallengeResponseHash = encrypt(QCryptographicHash::hash(challengeResponse, hashAlgo), &encKey);
QString respXml = m_Http.openConnectionToString(m_Http.m_BaseUrlHttp,
"pair",
"devicename=roth&updateState=1&serverchallengeresp=" +
@@ -354,6 +323,5 @@ NvPairingManager::pair(QString appVersion, QString pin, QSslCertificate& serverC
return PairState::FAILED;
}
serverCert = std::move(unverifiedServerCert);
return PairState::PAIRED;
}
+7 -6
View File
@@ -3,6 +3,7 @@
#include "identitymanager.h"
#include "nvhttp.h"
#include <openssl/aes.h>
#include <openssl/x509.h>
#include <openssl/evp.h>
@@ -29,22 +30,22 @@ private:
generateRandomBytes(int length);
QByteArray
saltPin(const QByteArray& salt, QString pin);
saltPin(QByteArray salt, QString pin);
QByteArray
encrypt(const QByteArray& plaintext, const QByteArray& key);
encrypt(QByteArray plaintext, AES_KEY* key);
QByteArray
decrypt(const QByteArray& ciphertext, const QByteArray& key);
decrypt(QByteArray ciphertext, AES_KEY* key);
QByteArray
getSignatureFromPemCert(const QByteArray& certificate);
getSignatureFromPemCert(QByteArray certificate);
bool
verifySignature(const QByteArray& data, const QByteArray& signature, const QByteArray& serverCertificate);
verifySignature(QByteArray data, QByteArray signature, QByteArray serverCertificate);
QByteArray
signMessage(const QByteArray& message);
signMessage(QByteArray message);
NvHTTP m_Http;
X509* m_Cert;
-67
View File
@@ -1,67 +0,0 @@
#include "richpresencemanager.h"
#include <QDebug>
RichPresenceManager::RichPresenceManager(StreamingPreferences& prefs, QString gameName)
: m_DiscordActive(false)
{
#ifdef HAVE_DISCORD
if (prefs.richPresence) {
DiscordEventHandlers handlers = {};
handlers.ready = discordReady;
handlers.disconnected = discordDisconnected;
handlers.errored = discordErrored;
Discord_Initialize("594668102021677159", &handlers, 0, nullptr);
m_DiscordActive = true;
}
if (m_DiscordActive) {
QByteArray stateStr = (QString("Streaming ") + gameName).toUtf8();
DiscordRichPresence discordPresence = {};
discordPresence.state = stateStr.data();
discordPresence.startTimestamp = time(nullptr);
discordPresence.largeImageKey = "icon";
Discord_UpdatePresence(&discordPresence);
}
#else
Q_UNUSED(prefs)
Q_UNUSED(gameName)
#endif
}
RichPresenceManager::~RichPresenceManager()
{
#ifdef HAVE_DISCORD
if (m_DiscordActive) {
Discord_ClearPresence();
Discord_Shutdown();
}
#endif
}
void RichPresenceManager::runCallbacks()
{
#ifdef HAVE_DISCORD
if (m_DiscordActive) {
Discord_RunCallbacks();
}
#endif
}
#ifdef HAVE_DISCORD
void RichPresenceManager::discordReady(const DiscordUser* request)
{
qInfo() << "Discord integration ready for user:" << request->username;
}
void RichPresenceManager::discordDisconnected(int errorCode, const char *message)
{
qInfo() << "Discord integration disconnected:" << errorCode << message;
}
void RichPresenceManager::discordErrored(int errorCode, const char *message)
{
qWarning() << "Discord integration error:" << errorCode << message;
}
#endif
-26
View File
@@ -1,26 +0,0 @@
#pragma once
#include "settings/streamingpreferences.h"
#ifdef HAVE_DISCORD
#include <discord_rpc.h>
#endif
class RichPresenceManager
{
public:
RichPresenceManager(StreamingPreferences& prefs, QString gameName);
~RichPresenceManager();
void runCallbacks();
private:
#ifdef HAVE_DISCORD
static void discordReady(const DiscordUser* request);
static void discordDisconnected(int errorCode, const char* message);
static void discordErrored(int errorCode, const char* message);
#endif
bool m_DiscordActive;
};
-197
View File
@@ -1,197 +0,0 @@
#include "systemproperties.h"
#include "utils.h"
#include <QGuiApplication>
#include "streaming/session.h"
#include "streaming/streamutils.h"
#ifdef Q_OS_WIN32
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
#endif
SystemProperties::SystemProperties()
{
versionString = QString(VERSION_STR);
hasWindowManager = WMUtils::isRunningWindowManager();
isRunningWayland = WMUtils::isRunningWayland();
isRunningXWayland = isRunningWayland && QGuiApplication::platformName() == "xcb";
QString nativeArch = QSysInfo::currentCpuArchitecture();
#ifdef Q_OS_WIN32
{
USHORT processArch, machineArch;
// Use IsWow64Process2 on TH2 and later, because it supports ARM64
auto fnIsWow64Process2 = (decltype(IsWow64Process2)*)GetProcAddress(GetModuleHandleA("kernel32.dll"), "IsWow64Process2");
if (fnIsWow64Process2 != nullptr && fnIsWow64Process2(GetCurrentProcess(), &processArch, &machineArch)) {
switch (machineArch) {
case IMAGE_FILE_MACHINE_I386:
nativeArch = "i386";
break;
case IMAGE_FILE_MACHINE_AMD64:
nativeArch = "x86_64";
break;
case IMAGE_FILE_MACHINE_ARM64:
nativeArch = "arm64";
break;
}
}
isWow64 = nativeArch != QSysInfo::buildCpuArchitecture();
}
#else
isWow64 = false;
#endif
if (nativeArch == "i386") {
friendlyNativeArchName = "x86";
}
else if (nativeArch == "x86_64") {
friendlyNativeArchName = "x64";
}
else {
friendlyNativeArchName = nativeArch.toUpper();
}
#ifndef STEAM_LINK
// Assume we can probably launch a browser if we're in a GUI environment
hasBrowser = hasWindowManager;
#else
hasBrowser = false;
#endif
#ifdef HAVE_DISCORD
hasDiscordIntegration = true;
#else
hasDiscordIntegration = false;
#endif
unmappedGamepads = SdlInputHandler::getUnmappedGamepads();
// Populate data that requires talking to SDL. We do it all in one shot
// and cache the results to speed up future queries on this data.
querySdlVideoInfo();
Q_ASSERT(maximumStreamingFrameRate >= 60);
Q_ASSERT(!monitorDesktopResolutions.isEmpty());
Q_ASSERT(!monitorNativeResolutions.isEmpty());
}
QRect SystemProperties::getDesktopResolution(int displayIndex)
{
// Returns default constructed QRect if out of bounds
return monitorDesktopResolutions.value(displayIndex);
}
QRect SystemProperties::getNativeResolution(int displayIndex)
{
// Returns default constructed QRect if out of bounds
return monitorNativeResolutions.value(displayIndex);
}
void SystemProperties::querySdlVideoInfo()
{
hasHardwareAcceleration = false;
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s",
SDL_GetError());
return;
}
// Update display related attributes (max FPS, native resolution, etc).
refreshDisplays();
SDL_Window* testWindow = SDL_CreateWindow("", 0, 0, 1280, 720,
SDL_WINDOW_HIDDEN | StreamUtils::getPlatformWindowFlags());
if (!testWindow) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Failed to create test window with platform flags: %s",
SDL_GetError());
testWindow = SDL_CreateWindow("", 0, 0, 1280, 720, SDL_WINDOW_HIDDEN);
if (!testWindow) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to create window for hardware decode test: %s",
SDL_GetError());
SDL_QuitSubSystem(SDL_INIT_VIDEO);
return;
}
}
Session::getDecoderInfo(testWindow, hasHardwareAcceleration, rendererAlwaysFullScreen, maximumResolution);
SDL_DestroyWindow(testWindow);
SDL_QuitSubSystem(SDL_INIT_VIDEO);
}
void SystemProperties::refreshDisplays()
{
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s",
SDL_GetError());
return;
}
monitorDesktopResolutions.clear();
monitorNativeResolutions.clear();
// Never let the maximum drop below 60 FPS
maximumStreamingFrameRate = 60;
SDL_DisplayMode bestMode;
for (int displayIndex = 0; displayIndex < SDL_GetNumVideoDisplays(); displayIndex++) {
SDL_DisplayMode desktopMode;
int err;
err = SDL_GetDesktopDisplayMode(displayIndex, &desktopMode);
if (err == 0) {
if (desktopMode.w <= 8192 && desktopMode.h <= 8192) {
monitorDesktopResolutions.insert(displayIndex, QRect(0, 0, desktopMode.w, desktopMode.h));
}
else {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Skipping resolution over 8K: %dx%d",
desktopMode.w, desktopMode.h);
}
}
else {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_GetDesktopDisplayMode() failed: %s",
SDL_GetError());
}
if (StreamUtils::getRealDesktopMode(displayIndex, &desktopMode)) {
if (desktopMode.w <= 8192 && desktopMode.h <= 8192) {
monitorNativeResolutions.insert(displayIndex, QRect(0, 0, desktopMode.w, desktopMode.h));
}
else {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Skipping resolution over 8K: %dx%d",
desktopMode.w, desktopMode.h);
}
// Start at desktop mode and work our way up
bestMode = desktopMode;
for (int i = 0; i < SDL_GetNumDisplayModes(displayIndex); i++) {
SDL_DisplayMode mode;
if (SDL_GetDisplayMode(displayIndex, i, &mode) == 0) {
if (mode.w == desktopMode.w && mode.h == desktopMode.h) {
if (mode.refresh_rate > bestMode.refresh_rate) {
bestMode = mode;
}
}
}
}
maximumStreamingFrameRate = qMax(maximumStreamingFrameRate, bestMode.refresh_rate);
}
}
SDL_QuitSubSystem(SDL_INIT_VIDEO);
}
-53
View File
@@ -1,53 +0,0 @@
#pragma once
#include <QObject>
#include <QRect>
class SystemProperties : public QObject
{
Q_OBJECT
public:
SystemProperties();
Q_PROPERTY(bool hasHardwareAcceleration MEMBER hasHardwareAcceleration CONSTANT)
Q_PROPERTY(bool rendererAlwaysFullScreen MEMBER rendererAlwaysFullScreen CONSTANT)
Q_PROPERTY(bool isRunningWayland MEMBER isRunningWayland CONSTANT)
Q_PROPERTY(bool isRunningXWayland MEMBER isRunningXWayland CONSTANT)
Q_PROPERTY(bool isWow64 MEMBER isWow64 CONSTANT)
Q_PROPERTY(QString friendlyNativeArchName MEMBER friendlyNativeArchName CONSTANT)
Q_PROPERTY(bool hasWindowManager MEMBER hasWindowManager CONSTANT)
Q_PROPERTY(bool hasBrowser MEMBER hasBrowser CONSTANT)
Q_PROPERTY(bool hasDiscordIntegration MEMBER hasDiscordIntegration CONSTANT)
Q_PROPERTY(QString unmappedGamepads MEMBER unmappedGamepads NOTIFY unmappedGamepadsChanged)
Q_PROPERTY(int maximumStreamingFrameRate MEMBER maximumStreamingFrameRate CONSTANT)
Q_PROPERTY(QSize maximumResolution MEMBER maximumResolution CONSTANT)
Q_PROPERTY(QString versionString MEMBER versionString CONSTANT)
Q_INVOKABLE void refreshDisplays();
Q_INVOKABLE QRect getDesktopResolution(int displayIndex);
Q_INVOKABLE QRect getNativeResolution(int displayIndex);
signals:
void unmappedGamepadsChanged();
private:
void querySdlVideoInfo();
bool hasHardwareAcceleration;
bool rendererAlwaysFullScreen;
bool isRunningWayland;
bool isRunningXWayland;
bool isWow64;
QString friendlyNativeArchName;
bool hasWindowManager;
bool hasBrowser;
bool hasDiscordIntegration;
QString unmappedGamepads;
int maximumStreamingFrameRate;
QSize maximumResolution;
QList<QRect> monitorDesktopResolutions;
QList<QRect> monitorNativeResolutions;
QString versionString;
};
+14 -63
View File
@@ -59,7 +59,7 @@ public:
void showMessage(QString message, MessageType type) const
{
#if defined(Q_OS_WIN32)
#if defined(Q_OS_WIN)
UINT flags = MB_OK | MB_TOPMOST | MB_SETFOREGROUND;
flags |= (type == Info ? MB_ICONINFORMATION : MB_ICONERROR);
QString title = "Moonlight";
@@ -172,31 +172,20 @@ GlobalCommandLineParser::ParseResult GlobalCommandLineParser::parse(const QStrin
parser.addPositionalArgument("action", "Action to execute", "<action>");
parser.parse(args);
auto posArgs = parser.positionalArguments();
QString action = posArgs.isEmpty() ? QString() : posArgs.first().toLower();
if (posArgs.isEmpty()) {
if (action == "") {
// This method will not return and terminates the process if --version
// or --help is specified
parser.handleHelpAndVersionOptions();
parser.handleUnknownOptions();
return NormalStartRequested;
}
else {
// If users supply arguments that accept values prior to the "quit"
// or "stream" positional arguments, we will not be able to correctly
// parse the value out of the input because this QCommandLineParser
// doesn't know about all of the options that "quit" and "stream"
// commands can accept. To work around this issue, we just look
// for "quit" or "stream" positional arguments anywhere.
for (int i = 0; i < posArgs.size(); i++) {
QString action = posArgs.at(i).toLower();
if (action == "quit") {
return QuitRequested;
} else if (action == "stream") {
return StreamRequested;
}
}
parser.showError(QString("Invalid action"));
} else if (action == "quit") {
return QuitRequested;
} else if (action == "stream") {
return StreamRequested;
} else {
parser.showError(QString("Invalid action: %1").arg(action));
}
}
@@ -252,7 +241,6 @@ StreamCommandLineParser::StreamCommandLineParser()
m_AudioConfigMap = {
{"stereo", StreamingPreferences::AC_STEREO},
{"5.1-surround", StreamingPreferences::AC_51_SURROUND},
{"7.1-surround", StreamingPreferences::AC_71_SURROUND},
};
m_VideoCodecMap = {
{"auto", StreamingPreferences::VCC_AUTO},
@@ -261,8 +249,8 @@ StreamCommandLineParser::StreamCommandLineParser()
};
m_VideoDecoderMap = {
{"auto", StreamingPreferences::VDS_AUTO},
{"software", StreamingPreferences::VDS_FORCE_SOFTWARE},
{"hardware", StreamingPreferences::VDS_FORCE_HARDWARE},
{"software", StreamingPreferences::VDS_FORCE_HARDWARE},
{"hardware", StreamingPreferences::VDS_FORCE_SOFTWARE},
};
}
@@ -292,22 +280,14 @@ void StreamCommandLineParser::parse(const QStringList &args, StreamingPreference
parser.addToggleOption("vsync", "V-Sync");
parser.addValueOption("fps", "FPS");
parser.addValueOption("bitrate", "bitrate in Kbps");
parser.addValueOption("packet-size", "video packet size");
parser.addChoiceOption("display-mode", "display mode", m_WindowModeMap.keys());
parser.addChoiceOption("audio-config", "audio config", m_AudioConfigMap.keys());
parser.addToggleOption("multi-controller", "multiple controller support");
parser.addToggleOption("quit-after", "quit app after session");
parser.addToggleOption("absolute-mouse", "remote desktop optimized mouse control");
parser.addToggleOption("mouse-buttons-swap", "left and right mouse buttons swap");
parser.addToggleOption("touchscreen-trackpad", "touchscreen in trackpad mode");
parser.addToggleOption("mouse-acceleration", "mouse acceleration");
parser.addToggleOption("game-optimization", "game optimizations");
parser.addToggleOption("audio-on-host", "audio on host PC");
parser.addToggleOption("frame-pacing", "frame pacing");
parser.addToggleOption("mute-on-focus-loss", "mute audio when Moonlight window loses focus");
parser.addToggleOption("background-gamepad", "background gamepad input");
parser.addToggleOption("reverse-scroll-direction", "inverted scroll direction");
parser.addToggleOption("swap-gamepad-buttons", "swap A/B and X/Y gamepad buttons (Nintendo-style)");
parser.addToggleOption("capture-system-keys", "capture system key combos in fullscreen mode");
parser.addChoiceOption("video-codec", "video codec", m_VideoCodecMap.keys());
parser.addChoiceOption("video-decoder", "video decoder", m_VideoDecoderMap.keys());
@@ -365,14 +345,6 @@ void StreamCommandLineParser::parse(const QStringList &args, StreamingPreference
preferences->width, preferences->height, preferences->fps);
}
// Resolve --packet-size option
if (parser.isSet("packet-size")) {
preferences->packetSize = parser.getIntOption("packet-size");
if (preferences->packetSize < 1024) {
parser.showError("Packet size must be greater than 1024 bytes");
}
}
// Resolve --display option
if (parser.isSet("display-mode")) {
preferences->windowMode = mapValue(m_WindowModeMap, parser.getChoiceOptionValue("display-mode"));
@@ -392,14 +364,8 @@ void StreamCommandLineParser::parse(const QStringList &args, StreamingPreference
// Resolve --quit-after and --no-quit-after options
preferences->quitAppAfter = parser.getToggleOptionValue("quit-after", preferences->quitAppAfter);
// Resolve --absolute-mouse and --no-absolute-mouse options
preferences->absoluteMouseMode = parser.getToggleOptionValue("absolute-mouse", preferences->absoluteMouseMode);
// Resolve --mouse-buttons-swap and --no-mouse-buttons-swap options
preferences->swapMouseButtons = parser.getToggleOptionValue("mouse-buttons-swap", preferences->swapMouseButtons);
// Resolve --touchscreen-trackpad and --no-touchscreen-trackpad options
preferences->absoluteTouchMode = !parser.getToggleOptionValue("touchscreen-trackpad", !preferences->absoluteTouchMode);
// Resolve --mouse-acceleration and --no-mouse-acceleration options
preferences->mouseAcceleration = parser.getToggleOptionValue("mouse-acceleration", preferences->mouseAcceleration);
// Resolve --game-optimization and --no-game-optimization options
preferences->gameOptimizations = parser.getToggleOptionValue("game-optimization", preferences->gameOptimizations);
@@ -410,21 +376,6 @@ void StreamCommandLineParser::parse(const QStringList &args, StreamingPreference
// Resolve --frame-pacing and --no-frame-pacing options
preferences->framePacing = parser.getToggleOptionValue("frame-pacing", preferences->framePacing);
// Resolve --mute-on-focus-loss and --no-mute-on-focus-loss options
preferences->muteOnFocusLoss = parser.getToggleOptionValue("mute-on-focus-loss", preferences->muteOnFocusLoss);
// Resolve --background-gamepad and --no-background-gamepad options
preferences->backgroundGamepad = parser.getToggleOptionValue("background-gamepad", preferences->backgroundGamepad);
// Resolve --reverse-scroll-direction and --no-reverse-scroll-direction options
preferences->reverseScrollDirection = parser.getToggleOptionValue("reverse-scroll-direction", preferences->reverseScrollDirection);
// Resolve --swap-gamepad-buttons and --no-swap-gamepad-buttons options
preferences->swapFaceButtons = parser.getToggleOptionValue("swap-gamepad-buttons", preferences->swapFaceButtons);
// Resolve --capture-system-keys and --no-capture-system-keys options
preferences->captureSysKeys = parser.getToggleOptionValue("capture-system-keys", preferences->captureSysKeys);
// Resolve --video-codec option
if (parser.isSet("video-codec")) {
preferences->videoCodecConfig = mapValue(m_VideoCodecMap, parser.getChoiceOptionValue("video-codec"));
+4 -4
View File
@@ -73,7 +73,7 @@ public:
case Event::ComputerSeekTimedout:
if (m_State == StateSeekComputer) {
m_State = StateFailure;
emit q->failed(QObject::tr("Failed to connect to %1").arg(m_ComputerName));
emit q->failed(QString("Failed to connect to %1").arg(m_ComputerName));
}
break;
// Occurs when searched computer is found
@@ -85,8 +85,8 @@ public:
m_ComputerManager->quitRunningApp(event.computer);
} else {
m_State = StateFailure;
QString msg = QObject::tr("Computer %1 has not been paired. "
"Please open Moonlight to pair before streaming.")
QString msg = QString("Computer %1 has not been paired. "
"Please open Moonlight to pair before streaming.")
.arg(event.computer->name);
emit q->failed(msg);
}
@@ -99,7 +99,7 @@ public:
QCoreApplication::exit(0);
} else {
m_State = StateFailure;
emit q->failed(QObject::tr("Quitting app failed, reason: %1").arg(event.errorMessage));
emit q->failed(QString("Quitting app failed, reason: %1").arg(event.errorMessage));
}
}
break;
+5 -5
View File
@@ -86,8 +86,8 @@ public:
emit q->searchingApp();
} else {
m_State = StateFailure;
QString msg = QObject::tr("Computer %1 has not been paired. "
"Please open Moonlight to pair before streaming.")
QString msg = QString("Computer %1 has not been paired. "
"Please open Moonlight to pair before streaming.")
.arg(event.computer->name);
emit q->failed(msg);
}
@@ -123,18 +123,18 @@ public:
case Event::AppQuitCompleted:
if (m_State == StateSeekApp && !event.errorMessage.isEmpty()) {
m_State = StateFailure;
emit q->failed(QObject::tr("Quitting app failed, reason: %1").arg(event.errorMessage));
emit q->failed(QString("Quitting app failed, reason: %1").arg(event.errorMessage));
}
break;
// Occurs when computer or app search timed out
case Event::Timedout:
if (m_State == StateSeekComputer) {
m_State = StateFailure;
emit q->failed(QObject::tr("Failed to connect to %1").arg(m_ComputerName));
emit q->failed(QString("Failed to connect to %1").arg(m_ComputerName));
}
if (m_State == StateSeekApp) {
m_State = StateFailure;
emit q->failed(QObject::tr("Failed to find application %1").arg(m_AppName));
emit q->failed(QString("Failed to find application %1").arg(m_AppName));
}
break;
}
@@ -7,18 +7,15 @@
<summary>Play games remotely from your NVIDIA GameStream-enabled PC</summary>
<description>
<p>Moonlight can stream games and other applications from a PC with an NVIDIA GeForce GTX/RTX or NVIDIA Quadro GPU and GeForce/Quadro Experience installed.</p>
<p>Moonlight can stream games and other applications from a PC with an NVIDIA GeForce GTX 600-series or higher GPU and GeForce Experience installed.</p>
<p>Features include:</p>
<ul>
<li>Streaming at up to 4K resolution</li>
<li>Support for up to 120 FPS streaming (high refresh rate monitor recommended)</li>
<li>Hardware accelerated video decoding with VAAPI, VDPAU, and NVDEC support</li>
<li>7.1 surround sound audio support</li>
<li>Support for both pointer capture (for games) and direct mouse control (for remote desktop)</li>
<li>Support for passing system-wide keyboard shortcuts like Alt+Tab to the host in fullscreen mode on X11</li>
<li>Direct touchscreen mouse control</li>
<li>Hardware accelerated video decoding with VAAPI and VDPAU support</li>
<li>5.1 surround sound audio support</li>
<li>HEVC support for better video compression efficiency</li>
<li>Gamepad support with force feedback for up to 4 players</li>
<li>Gamepad support with SDL mapping compatibility</li>
</ul>
</description>
@@ -36,238 +33,6 @@
</screenshots>
<releases>
<release version="3.0.0" date="2021-01-17">
<description>
<p>New features:</p>
<ul>
<li>Added the ability to define a custom streaming resolution using the UI</li>
<li>Added the option to capture system key combinations (Alt+Tab, Ctrl+Shift+Esc, Win+Tab, etc) in fullscreen mode</li>
<li>Updated gamepad mappings are automatically downloaded each time Moonlight launches</li>
<li>The performance overlay is now supported for Linux clients using VAAPI and VDPAU hardware acceleration</li>
<li>Added the option to enable "Direct Launch" which immediately launches the specified game when clicking on a PC</li>
<li>Added the option to mute audio when the Moonlight window loses input focus</li>
<li>Added the option to toggle background gamepad input</li>
<li>Added the option to invert scroll direction</li>
<li>Added the option to swap gamepad button layout to Nintendo-style</li>
<li>Added the option to start the Moonlight UI in fullscreen mode</li>
<li>The failing TCP or UDP ports are now displayed when the connection fails</li>
</ul>
<p>Bugfixes:</p>
<ul>
<li>Fixed PS4 controllers not reading inputs during haptics in some games</li>
<li>Fixed some slow decoders causing the receive time value to increase rather than the decode time</li>
<li>Native resolution options now refresh each time the settings page is opened, rather than once per launch of Moonlight</li>
<li>Fixed no video or audio while streaming over Internet connections that drop packets with QoS tagging enabled</li>
<li>Improved connection testing accuracy for connections with low MTUs</li>
<li>Fixed inability to open an app's context menu with a gamepad</li>
</ul>
</description>
</release>
<release version="2.2.0" date="2020-09-10">
<description>
<p>New features:</p>
<ul>
<li>Added the ability to hide unwanted apps and games</li>
<li>Added a network test to determine if your Internet connection is blocking Moonlight</li>
<li>Added the option to swap mouse buttons</li>
<li>Added codec and resolution to the on-screen performance stats</li>
<li>Added a warning if the connection is too unstable to receive any complete video frames</li>
<li>Added a Discord link to the toolbar in settings</li>
<li>Enabled the new EGL renderer by default on Linux</li>
</ul>
<p>Bugfixes:</p>
<ul>
<li>Fixed streaming over networks that block QoS-tagged packets</li>
<li>Selecting a frame rate higher than 60 FPS no longer sets the host desktop to 720p</li>
<li>Right-side modifier keys no longer press the corresponding left-side keys too</li>
<li>Fixed mouse position never reaching the edge of the screen with certain desktop resolutions</li>
<li>Improved synchronization between host and client mouse cursor in remote desktop mouse mode</li>
<li>Cached box art is now deleted when the PC is removed from Moonlight</li>
<li>Fixed several CLI options not properly overriding GUI options</li>
<li>Updated included gamepad mappings</li>
<li>Updated to Qt 5.15.1</li>
</ul>
</description>
</release>
<release version="2.1.0" date="2020-05-17">
<description>
<p>New features:</p>
<ul>
<li>Remote desktop mode now allows interaction with the host without having to click to focus the Moonlight window first</li>
<li>Added a new EGL renderer for improved performance on Wayland and XWayland</li>
<li>Added the option to toggle between direct touchscreen mode and the old trackpad mode</li>
<li>Added the option to rename host PCs</li>
<li>Added the ability to scroll the settings page with the right analog stick</li>
<li>Various UI improvements</li>
</ul>
<p>Bugfixes:</p>
<ul>
<li>Fixed unreliable mouse capture when the stream begins</li>
<li>Fixed cursor being trapped in full-screen mode even when the mouse wasn't captured</li>
<li>Fixed key presses not being sent to the host if the Super/Meta/Windows key is pressed</li>
<li>Updated included gamepad mappings</li>
</ul>
</description>
</release>
<release version="2.0.0" date="2020-04-28">
<description>
<p>New features:</p>
<ul>
<li>Remote desktop-optimized mouse mode which doesn't require capturing the mouse cursor</li>
<li>Touchscreen input now directly controls the mouse cursor</li>
<li>7.1 surround sound support</li>
</ul>
<p>Bugfixes:</p>
<ul>
<li>Improved performance on some lossy networks</li>
<li>Fixed disconnect delay when streaming from GeForce Experience 3.20.3.63</li>
<li>Fixed frame rate not exceeding 60 when streaming from GeForce Experience 3.20.3.63</li>
<li>Fixed parsing of command-line options when passing flags prior to the action</li>
<li>Fixed an issue that may cause excessive mDNS network traffic while Moonlight is running</li>
<li>Fixed hardware decoding on Intel GPUs running Mesa 20.0 and/or the Iris driver</li>
<li>Fixed hardware decoding on Comet Lake and later Intel GPUs using the iHD VA-API driver</li>
<li>Fixed hardware decoding on AMD Radeon GPUs with XWayland</li>
<li>Fixed a crash when using the iHD VA-API driver on Wayland</li>
<li>Fixed delay after exiting the stream on Wayland</li>
<li>Updated included gamepad mappings</li>
</ul>
</description>
</release>
<release version="1.2.0" date="2019-12-17">
<description>
<p>Bugfixes:</p>
<ul>
<li>Improved video encoding efficiency</li>
<li>Improved performance when streaming over a VPN</li>
<li>Fixed RTSP handshake error on certain networks</li>
<li>Fixed rumble not working on certain gamepads</li>
<li>Fixed pairing error when double-clicking on an unpaired PC</li>
<li>Updated included SDL gamepad mappings</li>
</ul>
</description>
</release>
<release version="1.1.1" date="2019-09-29">
<description>
<p>Bugfixes:</p>
<ul>
<li>Improved reliability of public IP address detection</li>
<li>Fixed connecting to a host PC with multiple network connections</li>
<li>Fixed a hang after exiting a full-screen stream with GNOME on Wayland</li>
<li>Fixed gamepad hotplugging</li>
<li>Updated to SDL 2.0.10</li>
<li>Updated to Qt 5.13.1</li>
</ul>
</description>
</release>
<release version="1.1.0" date="2019-08-05">
<description>
<p>New features:</p>
<ul>
<li>Added support for NVDEC for NVIDIA GPUs on Wayland</li>
<li>Added an option to disable gamepad mouse mode for games that require holding the Start button</li>
<li>Added automatic detection of IPv6 addresses for Internet streaming over IPv6 networks</li>
</ul>
<p>Bugfixes:</p>
<ul>
<li>Improved speed and reliability of automatic PC detection on the local network</li>
<li>Fixed audio latency increasing under certain network conditions</li>
<li>Fixed 5.1 surround sound sometimes causing no audio to play when streaming over the Internet</li>
<li>Fixed graceful termination of games not being detected properly on GFE 3.19</li>
<li>Fixed several audio issues on Linux by switching back to SDL for audio</li>
<li>Changed client certificate signature algorithm from SHA-1 to SHA-256 for Debian Buster compatibility</li>
<li>Updated included gamepad mappings</li>
</ul>
</description>
</release>
<release version="1.0.1" date="2019-05-27">
<description>
<p>Bugfixes:</p>
<ul>
<li>Fixed broken video on GeForce Experience 3.19</li>
</ul>
</description>
</release>
<release version="1.0.0" date="2019-05-21">
<description>
<p>New features:</p>
<ul>
<li>Mouse mode for gamepads (Press and hold Start to toggle)</li>
<li>Improved gamepad UI navigation</li>
</ul>
<p>Bugfixes:</p>
<ul>
<li>Fixed duplicate non-working gamepads appearing on the host</li>
<li>Fixed excessive frame queue delay on macOS</li>
<li>Fixed excessive audio latency on Steam Link</li>
<li>Fixed hiding mouse cursor after streaming on Steam Link</li>
<li>Fixed incorrect receive time stats on 32-bit platforms</li>
<li>Added UDP ports 48002 and 48010 for Wake-on-LAN</li>
<li>Updated included gamepad mappings</li>
</ul>
</description>
</release>
<release version="0.10.1" date="2019-04-28">
<description>
<p>New features:</p>
<ul>
<li>Video performance optimizations</li>
<li>VAAPI support on Wayland</li>
</ul>
<p>Bugfixes:</p>
<ul>
<li>Fixed a crash when attempting to dismiss the connection terminated dialog</li>
<li>Fixed a crash when using the overlay on the software decoder</li>
<li>Fixed software decoder not displaying output in some cases</li>
<li>Fixed some minor UI issues on the PC grid</li>
<li>Improved visibility of toolbar highlights</li>
</ul>
</description>
</release>
<release version="0.10.0" date="2019-04-07">
<description>
<p>New features:</p>
<ul>
<li>Redesigned App Grid with larger game artwork and less blank space</li>
<li>Redesigned UI dialogs using Material Design</li>
<li>Added a connection warning overlay (which can be disabled in settings)</li>
<li>Streaming window no longer minimizes when it loses focus in full-screen mode</li>
<li>The Steam Link device is now a supported platform</li>
</ul>
<p>Bugfixes:</p>
<ul>
<li>Fixed several bugs that could cause UI hangs or lag</li>
<li>Fixed rumble support on gamepads with limited rumble motor control</li>
<li>Fixed increased memory usage if the software renderer gets delayed</li>
<li>Fixed crashes on certain desktop configurations when loading the UI</li>
<li>Fixed handling of pairing certificates with a bogus date</li>
</ul>
</description>
</release>
<release version="0.9.1" date="2019-02-17">
<description>
<p>New features:</p>
<ul>
<li>Added gamepad rumble support</li>
<li>Added a dedicated quit button to the running app tile</li>
<li>Reduced video latency on Windows and Linux</li>
<li>Added a performance overlay for software decoding</li>
</ul>
<p>Bugfixes:</p>
<ul>
<li>Fixed delayed first video frame with GFE 3.17</li>
<li>Fixed erroneously setting host resolution to 720p in certain configurations</li>
<li>The connection termination dialog is no longer displayed for intentional termination</li>
<li>Fixed some gamepads only functioning for UI navigation and not in game</li>
<li>Fixed audio latency increasing when switching audio devices</li>
<li>Fixed VDPAU scaling behavior on AMD cards</li>
<li>Fixed several bugs in gamepad navigation behavior</li>
<li>Work around a bug in QGnomePlatform that breaks dialog rendering on Linux</li>
<li>Updated included SDL gamepad mappings</li>
<li>Fixed hotkey behavior on non-latin keyboards</li>
<li>Fixed incorrect frame drop percentages in debug stats</li>
</ul>
</description>
</release>
<release version="0.8.1" date="2019-01-08">
<description>
<p>New features:</p>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

-23
View File
@@ -1,23 +0,0 @@
#!/bin/sh
# The default HOME is not persistent, so override
# it to a path on the onboard flash. Otherwise our
# pairing data will be lost each reboot.
HOME=/usr/local/moonlight
# For a seamless transition across the Qt 5.9 -> Qt 5.14 boundary,
# we bundle binaries built with both Qt 5.9 and Qt 5.14 and pick
# the correct one at runtime.
if [ -d "/usr/local/Qt-5.14.1" ]; then
BIN="moonlight"
else
BIN="moonlight59"
fi
# Renice PE_Single_CPU which seems to host A/V stuff
renice -10 -p $(pidof PE_Single_CPU)
# Renice Moonlight itself to avoid preemption by background tasks
# Write output to a logfile in /tmp
echo Launching $BIN
exec nice -n -10 ./bin/$BIN > /tmp/moonlight.log
-3
View File
@@ -1,3 +0,0 @@
name=Moonlight
icon=moonlight.png
run=moonlight.sh
+111 -225
View File
@@ -1,23 +1,29 @@
import QtQuick 2.9
import QtQuick.Dialogs 1.2
import QtQuick.Controls 2.2
import AppModel 1.0
import ComputerManager 1.0
import SdlGamepadKeyNavigation 1.0
CenteredGridView {
GridView {
property int computerIndex
property AppModel appModel : createModel()
property bool activated
property bool showHiddenGames
property bool showGames
id: appGrid
focus: true
activeFocusOnTab: true
topMargin: 20
bottomMargin: 5
cellWidth: 230; cellHeight: 297;
anchors.fill: parent
anchors.leftMargin: (parent.width % (cellWidth + anchors.rightMargin)) / 2
anchors.topMargin: 20
anchors.rightMargin: 5
anchors.bottomMargin: 5
cellWidth: 225; cellHeight: 385;
// The StackView will trigger a visibility change when
// we're pushed onto it, causing our onVisibleChanged
// routine to run, but only if we start as invisible
visible: false
function computerLost()
{
@@ -26,286 +32,154 @@ CenteredGridView {
}
Component.onCompleted: {
// Don't show any highlighted item until interacting with them.
// We do this here instead of onActivated to avoid losing the user's
// selection when backing out of a different page of the app.
// Don't show any highlighted item until interacting with them
currentIndex = -1
}
StackView.onActivated: {
appModel.computerLost.connect(computerLost)
activated = true
// Highlight the first item if a gamepad is connected
if (currentIndex == -1 && SdlGamepadKeyNavigation.getConnectedGamepads() > 0) {
currentIndex = 0
}
if (!showGames && !showHiddenGames) {
// Check if there's a direct launch app
var directLaunchAppIndex = model.getDirectLaunchAppIndex();
if (directLaunchAppIndex >= 0) {
// Start the direct launch app if nothing else is running
currentIndex = directLaunchAppIndex
currentItem.launchOrResumeSelectedApp(false)
// Set showGames so we will not loop when the stream ends
showGames = true
}
}
SdlGamepadKeyNavigation {
id: gamepadKeyNav
}
StackView.onDeactivating: {
appModel.computerLost.disconnect(computerLost)
activated = false
onVisibleChanged: {
if (visible) {
appModel.computerLost.connect(computerLost)
gamepadKeyNav.enable()
}
else {
appModel.computerLost.disconnect(computerLost)
gamepadKeyNav.disable()
}
}
function createModel()
{
var model = Qt.createQmlObject('import AppModel 1.0; AppModel {}', parent, '')
model.initialize(ComputerManager, computerIndex, showHiddenGames)
model.initialize(ComputerManager, computerIndex)
return model
}
model: appModel
delegate: NavigableItemDelegate {
width: 220; height: 287;
width: 200; height: 335;
grid: appGrid
// Dim the app if it's hidden
opacity: model.hidden ? 0.4 : 1.0
Image {
id: appIcon
anchors.horizontalCenter: parent.horizontalCenter;
y: 20
source: model.boxart
sourceSize {
width: 150
height: 200
}
width: sourceSize.width
height: sourceSize.height
fillMode: Image.Pad
}
Image {
property bool isPlaceholder: false
id: appIcon
anchors.horizontalCenter: parent.horizontalCenter
y: 10
source: model.boxart
onSourceSizeChanged: {
if ((sourceSize.width == 130 && sourceSize.height == 180) || // GFE 2.0 placeholder image
(sourceSize.width == 628 && sourceSize.height == 888) || // GFE 3.0 placeholder image
(sourceSize.width == 200 && sourceSize.height == 266)) // Our no_app_image.png
{
isPlaceholder = true
}
else
{
isPlaceholder = false
}
width = 200
height = 267
id: runningIcon
anchors.centerIn: appIcon
visible: model.running
source: "qrc:/res/baseline-play_circle_filled_white-48px.svg"
sourceSize {
width: 75
height: 75
}
}
Label {
id: appNameText
text: model.name
width: parent.width
height: 125
anchors.top: appIcon.bottom
font.pointSize: 22
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.Wrap
elide: Text.ElideRight
// Display a tooltip with the full name if it's truncated
ToolTip.text: model.name
ToolTip.delay: 1000
ToolTip.timeout: 5000
ToolTip.visible: (parent.hovered || parent.highlighted) && (!appNameText.visible || appNameText.truncated)
ToolTip.visible: (parent.hovered || parent.highlighted) && truncated
}
ToolButton {
id: resumeButton
anchors.horizontalCenterOffset: appIcon.isPlaceholder ? -47 : 0
anchors.verticalCenterOffset: appIcon.isPlaceholder ? -75 : -60
anchors.centerIn: appIcon
visible: model.running
implicitWidth: 125
implicitHeight: 125
Image {
source: "qrc:/res/baseline-play_circle_filled_white-48px.svg"
anchors.centerIn: parent
sourceSize {
width: 75
height: 75
}
}
onClicked: {
launchOrResumeSelectedApp(true)
}
ToolTip.text: qsTr("Resume Game")
ToolTip.delay: 1000
ToolTip.timeout: 3000
ToolTip.visible: hovered
}
ToolButton {
id: quitButton
anchors.horizontalCenterOffset: appIcon.isPlaceholder ? 47 : 0
anchors.verticalCenterOffset: appIcon.isPlaceholder ? -75 : 60
anchors.centerIn: appIcon
visible: model.running
implicitWidth: 125
implicitHeight: 125
Image {
source: "qrc:/res/baseline-cancel-24px.svg"
anchors.centerIn: parent
sourceSize {
width: 75
height: 75
}
}
onClicked: {
doQuitGame()
}
ToolTip.text: qsTr("Quit Game")
ToolTip.delay: 1000
ToolTip.timeout: 3000
ToolTip.visible: hovered
}
Label {
id: appNameText
visible: appIcon.isPlaceholder
text: model.name
width: appIcon.width
height: model.running ? 175 : appIcon.height
leftPadding: 20
rightPadding: 20
anchors.left: appIcon.left
anchors.right: appIcon.right
anchors.bottom: appIcon.bottom
font.pointSize: 22
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.Wrap
elide: Text.ElideRight
}
function launchOrResumeSelectedApp(quitExistingApp)
function launchOrResumeSelectedApp()
{
var runningId = appModel.getRunningAppId()
if (runningId !== 0 && runningId !== model.appid) {
if (quitExistingApp) {
quitAppDialog.appName = appModel.getRunningAppName()
quitAppDialog.segueToStream = true
quitAppDialog.nextAppName = model.name
quitAppDialog.nextAppIndex = index
quitAppDialog.open()
}
var runningIndex = appModel.getRunningAppIndex()
if (runningIndex >= 0 && runningIndex !== index) {
quitAppDialog.appName = appModel.getRunningAppName()
quitAppDialog.segueToStream = true
quitAppDialog.nextAppName = model.name
quitAppDialog.nextAppIndex = index
quitAppDialog.open()
return
}
var component = Qt.createComponent("StreamSegue.qml")
var segue = component.createObject(stackView, {
"appName": model.name,
"session": appModel.createSessionForApp(index),
"isResume": runningId === model.appid
})
var segue = component.createObject(stackView, {"appName": model.name, "session": appModel.createSessionForApp(index)})
stackView.push(segue)
}
onClicked: {
// Only allow clicking on the box art for non-running games.
// For running games, buttons will appear to resume or quit which
// will handle starting the game and clicks on the box art will
// be ignored.
if (!model.running) {
launchOrResumeSelectedApp(true)
}
}
onPressAndHold: {
// popup() ensures the menu appears under the mouse cursor
if (appContextMenu.popup) {
appContextMenu.popup()
}
else {
// Qt 5.9 doesn't have popup()
appContextMenu.open()
}
// Nothing is running or this app is running
launchOrResumeSelectedApp()
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.RightButton;
acceptedButtons: Qt.RightButton
onClicked: {
parent.onPressAndHold()
}
}
Keys.onReturnPressed: {
// Open the app context menu if activated via the gamepad or keyboard
// for running games. If the game isn't running, the above onClicked
// method will handle the launch.
if (model.running) {
// This will be keyboard/gamepad driven so use
// open() instead of popup()
appContextMenu.open()
// popup() ensures the menu appears under the mouse cursor
if (appContextMenu.popup) {
appContextMenu.popup()
}
else {
// Qt 5.9 doesn't have popup()
appContextMenu.open()
}
}
}
Keys.onMenuPressed: {
// This will be keyboard/gamepad driven so use open() instead of popup()
// We must use open() here so the menu is positioned on
// the ItemDelegate and not where the mouse cursor is
appContextMenu.open()
}
function doQuitGame() {
quitAppDialog.appName = appModel.getRunningAppName()
quitAppDialog.segueToStream = false
quitAppDialog.open()
}
NavigableMenu {
Menu {
id: appContextMenu
NavigableMenuItem {
parentMenu: appContextMenu
text: model.running ? qsTr("Resume Game") : qsTr("Launch Game")
onTriggered: launchOrResumeSelectedApp(true)
text: model.running ? "Resume Game" : "Launch Game"
onTriggered: {
appContextMenu.close()
launchOrResumeSelectedApp()
}
}
NavigableMenuItem {
parentMenu: appContextMenu
text: qsTr("Quit Game")
onTriggered: doQuitGame()
text: "Quit Game"
onTriggered: {
quitAppDialog.appName = appModel.getRunningAppName()
quitAppDialog.segueToStream = false
quitAppDialog.open()
}
visible: model.running
}
NavigableMenuItem {
parentMenu: appContextMenu
checkable: true
checked: model.directLaunch
text: qsTr("Direct Launch")
onTriggered: appModel.setAppDirectLaunch(model.index, !model.directLaunch)
enabled: !model.hidden
ToolTip.text: qsTr("Launch this app immediately when the host is selected, bypassing the app selection grid.")
ToolTip.delay: 1000
ToolTip.timeout: 3000
ToolTip.visible: hovered
}
NavigableMenuItem {
parentMenu: appContextMenu
checkable: true
checked: model.hidden
text: qsTr("Hide Game")
onTriggered: appModel.setAppHidden(model.index, !model.hidden)
enabled: model.hidden || (!model.running && !model.directLaunch)
ToolTip.text: qsTr("Hide this game from the app grid. To access hidden games, right-click on the host and choose %1.").arg(qsTr("View All Apps"))
ToolTip.delay: 1000
ToolTip.timeout: 5000
ToolTip.visible: hovered
}
}
}
NavigableMessageDialog {
MessageDialog {
id: quitAppDialog
modality:Qt.WindowModal
property string appName : ""
property bool segueToStream : false
property string nextAppName: ""
property int nextAppIndex: 0
text:qsTr("Are you sure you want to quit %1? Any unsaved progress will be lost.").arg(appName)
standardButtons: Dialog.Yes | Dialog.No
text:"Are you sure you want to quit " + appName +"? Any unsaved progress will be lost."
standardButtons: StandardButton.Yes | StandardButton.No
function quitApp() {
var component = Qt.createComponent("QuitSegue.qml")
@@ -327,8 +201,20 @@ CenteredGridView {
appModel.quitRunningApp()
}
onYes: quitApp()
// For keyboard/gamepad navigation
onAccepted: quitApp()
}
ScrollBar.vertical: ScrollBar {}
ScrollBar.vertical: ScrollBar {
parent: appGrid.parent
anchors {
top: appGrid.top
left: appGrid.right
bottom: appGrid.bottom
leftMargin: -10
}
}
}
+4 -19
View File
@@ -1,15 +1,11 @@
import QtQuick 2.9
import QtQuick.Controls 2.2
import SdlGamepadKeyNavigation 1.0
// https://stackoverflow.com/questions/45029968/how-do-i-set-the-combobox-width-to-fit-the-largest-item
ComboBox {
property int textWidth
property int desiredWidth : leftPadding + textWidth + indicator.width + rightPadding
property int maximumWidth : parent.width
implicitWidth: desiredWidth < maximumWidth ? desiredWidth : maximumWidth
implicitWidth: leftPadding + textWidth + indicator.width + rightPadding
TextMetrics {
id: popupMetrics
@@ -19,7 +15,9 @@ ComboBox {
id: textMetrics
}
function recalculateWidth() {
// We call this every time the options change (and init)
// so we can adjust the combo box width here too
onActivated: {
textMetrics.font = font
popupMetrics.font = popup.font
textWidth = 0
@@ -30,17 +28,4 @@ ComboBox {
textWidth = Math.max(popupMetrics.width, textWidth)
}
}
// We call this every time the options change (and init)
// so we can adjust the combo box width here too
onActivated: recalculateWidth()
popup.onAboutToShow: {
// Switch to normal navigation for combo boxes
SdlGamepadKeyNavigation.setUiNavMode(false)
}
popup.onAboutToHide: {
SdlGamepadKeyNavigation.setUiNavMode(true)
}
}
-43
View File
@@ -1,43 +0,0 @@
import QtQuick 2.9
import QtQuick.Controls 2.2
GridView {
// Detect Qt 5.11 or earlier using presence of synchronousDrag.
// Prior to 5.12, the leftMargin and rightMargin values did not work.
property bool hasBrokenMargins: this.synchronousDrag === undefined
property int minMargin: 10
property real availableWidth: (parent.width - 2 * minMargin)
property int itemsPerRow: availableWidth / cellWidth
property real horizontalMargin: itemsPerRow < count && availableWidth >= cellWidth ?
(availableWidth % cellWidth) / 2 : minMargin
function updateMargins() {
leftMargin = horizontalMargin
rightMargin = horizontalMargin
if (hasBrokenMargins) {
anchors.leftMargin = leftMargin
anchors.rightMargin = rightMargin
}
}
onHorizontalMarginChanged: {
updateMargins()
}
Component.onCompleted: {
if (hasBrokenMargins) {
// This will cause an anchor conflict with the parent StackView
// which breaks animation, but otherwise the grid will not be
// centered in the window.
anchors.fill = parent
anchors.topMargin = topMargin
anchors.bottomMargin = bottomMargin
}
updateMargins()
}
boundsBehavior: Flickable.OvershootBounds
}
+20 -7
View File
@@ -1,16 +1,19 @@
import QtQuick 2.0
import QtQuick.Controls 2.2
import QtQuick.Dialogs 1.2
import ComputerManager 1.0
import Session 1.0
Item {
anchors.fill: parent
function onSearchingComputer() {
stageLabel.text = qsTr("Establishing connection to PC...")
stageLabel.text = "Establishing connection to PC..."
}
function onQuittingApp() {
stageLabel.text = qsTr("Quitting app...")
stageLabel.text = "Quitting app..."
}
function onFailure(message) {
@@ -18,8 +21,13 @@ Item {
errorDialog.open()
}
StackView.onActivated: {
if (!launcher.isExecuted()) {
// The StackView will trigger a visibility change when
// we're pushed onto it, causing our onVisibleChanged
// routine to run, but only if we start as invisible
visible: false
onVisibleChanged: {
if (visible && !launcher.isExecuted()) {
toolBar.visible = false
launcher.searchingComputer.connect(onSearchingComputer)
launcher.quittingApp.connect(onQuittingApp)
@@ -47,11 +55,16 @@ Item {
}
}
ErrorMessageDialog {
MessageDialog {
id: errorDialog
modality:Qt.WindowModal
icon: StandardIcon.Critical
standardButtons: StandardButton.Ok
onClosed: {
Qt.quit()
onVisibleChanged: {
if (!visible) {
Qt.quit()
}
}
}
}
+46 -18
View File
@@ -1,16 +1,19 @@
import QtQml 2.2
import QtQuick 2.0
import QtQuick.Controls 2.2
import QtQuick.Dialogs 1.2
import ComputerManager 1.0
import SdlGamepadKeyNavigation 1.0
Item {
visible: false
function onSearchingComputer() {
stageLabel.text = qsTr("Establishing connection to PC...")
stageLabel.text = "Establishing connection to PC..."
}
function onSearchingApp() {
stageLabel.text = qsTr("Loading app list...")
stageLabel.text = "Loading app list..."
}
function onSessionCreated(appName, session) {
@@ -33,14 +36,9 @@ Item {
quitAppDialog.open()
}
StackView.onActivated: {
if (!launcher.isExecuted()) {
onVisibleChanged: {
if (visible && !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)
@@ -68,18 +66,26 @@ Item {
}
}
ErrorMessageDialog {
MessageDialog {
id: errorDialog
onClosed: {
Qt.quit();
modality:Qt.WindowModal
icon: StandardIcon.Critical
standardButtons: StandardButton.Ok | StandardButton.Help
onHelp: {
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Troubleshooting");
}
onVisibleChanged: {
if (!visible) {
Qt.quit();
}
}
}
NavigableMessageDialog {
MessageDialog {
id: quitAppDialog
text:qsTr("Are you sure you want to quit %1? Any unsaved progress will be lost.").arg(appName)
standardButtons: Dialog.Yes | Dialog.No
modality:Qt.WindowModal
text:"Are you sure you want to quit " + appName +"? Any unsaved progress will be lost."
standardButtons: StandardButton.Yes | StandardButton.No
property string appName : ""
function quitApp() {
@@ -90,7 +96,29 @@ Item {
launcher.quitRunningApp()
}
onYes: quitApp()
// For keyboard/gamepad navigation
onAccepted: quitApp()
onRejected: Qt.quit()
// Exit process if app quit is rejected (reacts also to closing of the
// dialog from title bar's close button).
// Note: this depends on undocumented behavior of visibleChanged()
// signal being emitted before yes() or accepted() has been emitted.
onVisibleChanged: {
if (!visible) {
quitTimer.start()
}
}
Component.onCompleted: {
yes.connect(quitTimer.stop)
accepted.connect(quitTimer.stop)
}
}
Timer {
id: quitTimer
interval: 100
onTriggered: Qt.quit()
}
}
-8
View File
@@ -1,8 +0,0 @@
import QtQuick 2.0
import QtQuick.Controls 2.2
import SystemProperties 1.0
NavigableMessageDialog {
standardButtons: Dialog.Ok | (SystemProperties.hasBrowser ? Dialog.Help : 0)
}
+1 -1
View File
@@ -1,5 +1,5 @@
import QtQuick 2.0
Item {
objectName: qsTr("Gamepad Mapping")
objectName: "Gamepad Mapping"
}
-18
View File
@@ -1,18 +0,0 @@
import QtQuick 2.0
import QtQuick.Controls 2.2
Dialog {
// We should use Overlay.overlay here but that's not available in Qt 5.9 :(
parent: ApplicationWindow.contentItem
x: Math.round((parent.width - width) / 2)
y: Math.round((parent.height - height) / 2)
onAboutToHide: {
// We must force focus back to the last item for platforms without
// support for more than one active window like Steam Link. If
// we don't, gamepad and keyboard navigation will break after a
// dialog appears.
stackView.forceActiveFocus()
}
}
-15
View File
@@ -1,15 +0,0 @@
import QtQuick 2.0
import QtQuick.Controls 2.2
Menu {
onOpened: {
// Give focus to the first visible and enabled menu item
for (var i = 0; i < count; i++) {
var item = itemAt(i)
if (item.visible && item.enabled) {
item.forceActiveFocus(Qt.TabFocusReason)
break
}
}
}
}
-16
View File
@@ -2,27 +2,11 @@ import QtQuick 2.0
import QtQuick.Controls 2.2
MenuItem {
// Qt 5.10 has a menu property, but we need to support 5.9
// so we must make our own.
property Menu parentMenu
// Ensure focus can't be given to an invisible item
enabled: visible
height: visible ? implicitHeight : 0
focusPolicy: visible ? Qt.TabFocus : Qt.NoFocus
onTriggered: {
// We must close the context menu first or
// it can steal focus from any dialogs that
// onTriggered may spawn.
parentMenu.close()
}
Keys.onReturnPressed: {
triggered()
}
Keys.onEscapePressed: {
parentMenu.close()
}
}
-74
View File
@@ -1,74 +0,0 @@
import QtQuick 2.0
import QtQuick.Controls 2.2
import QtQuick.Layouts 1.2
NavigableDialog {
id: dialog
property alias text: dialogLabel.dialogText
property alias showSpinner: dialogSpinner.visible
property alias imageSrc: dialogImage.source
property string helpText
property string helpUrl : "https://github.com/moonlight-stream/moonlight-docs/wiki/Troubleshooting"
property string helpTextSeparator : " "
onOpened: {
// Force keyboard focus on the label so keyboard navigation works
dialogLabel.forceActiveFocus()
}
RowLayout {
spacing: 10
BusyIndicator {
id: dialogSpinner
visible: false
}
Image {
id: dialogImage
source: (standardButtons & Dialog.Yes) ?
"qrc:/res/baseline-help_outline-24px.svg" :
"qrc:/res/baseline-error_outline-24px.svg"
sourceSize {
// The icon should be square so use the height as the width too
width: 50
height: 50
}
visible: !showSpinner
}
Label {
property string dialogText
id: dialogLabel
text: dialogText + ((helpText && (standardButtons & Dialog.Help)) ? (helpTextSeparator + helpText) : "")
wrapMode: Text.Wrap
elide: Label.ElideRight
// Cap the width so the dialog doesn't grow horizontally forever. This
// will cause word wrap to kick in.
Layout.maximumWidth: 400
Layout.maximumHeight: 400
Keys.onReturnPressed: {
accept()
}
Keys.onEscapePressed: {
reject()
}
}
}
footer: DialogButtonBox {
id: dialogButtonBox
standardButtons: dialog.standardButtons
onHelpRequested: {
Qt.openUrlExternally(helpUrl)
close()
}
}
}
-19
View File
@@ -2,27 +2,8 @@ import QtQuick 2.0
import QtQuick.Controls 2.2
ToolButton {
property string iconSource
activeFocusOnTab: true
// FIXME: We're using an Image here rather than icon.source because
// icons don't work on Qt 5.9 LTS.
Image {
id: image
source: iconSource
anchors.centerIn: parent.background
sourceSize {
width: parent.background.width * 1.10
height: parent.background.height * 1.10
}
}
// This determines the size of the Material highlight. We increase it
// from the default because we use larger than normal icons for TV readability.
background.width: (parent.height - parent.anchors.bottomMargin - parent.anchors.topMargin) * 0.60
background.height: (parent.height - parent.anchors.bottomMargin - parent.anchors.topMargin) * 0.60
Keys.onReturnPressed: {
clicked()
}
+118 -203
View File
@@ -1,6 +1,8 @@
import QtQuick 2.9
import QtQuick.Controls 2.2
import QtQuick.Dialogs 1.2
import QtQuick.Layouts 1.3
import QtQuick.Window 2.2
import ComputerModel 1.0
@@ -8,44 +10,48 @@ import ComputerManager 1.0
import StreamingPreferences 1.0
import SdlGamepadKeyNavigation 1.0
CenteredGridView {
GridView {
property ComputerModel computerModel : createModel()
id: pcGrid
focus: true
activeFocusOnTab: true
topMargin: 20
bottomMargin: 5
cellWidth: 310; cellHeight: 330;
objectName: qsTr("Computers")
anchors.fill: parent
anchors.leftMargin: (parent.width % (cellWidth + anchors.rightMargin)) / 2
anchors.topMargin: 20
anchors.rightMargin: 5
anchors.bottomMargin: 5
cellWidth: 350; cellHeight: 350;
objectName: "Computers"
Component.onCompleted: {
// Don't show any highlighted item until interacting with them.
// We do this here instead of onActivated to avoid losing the user's
// selection when backing out of a different page of the app.
currentIndex = -1
// The StackView will trigger a visibility change when
// we're pushed onto it, causing our onVisibleChanged
// routine to run, but only if we start as invisible
visible: false
StreamingPreferences {
id: prefs
}
// Note: Any initialization done here that is critical for streaming must
// also be done in CliStartStreamSegue.qml, since this code does not run
// for command-line initiated streams.
StackView.onActivated: {
// Setup signals on CM
ComputerManager.computerAddCompleted.connect(addComplete)
SdlGamepadKeyNavigation {
id: gamepadKeyNav
}
// This is a bit of a hack to do this here as opposed to main.qml, but
// we need it enabled before calling getConnectedGamepads() and PcView
// is never destroyed, so it should be okay.
SdlGamepadKeyNavigation.enable()
// Highlight the first item if a gamepad is connected
if (currentIndex == -1 && SdlGamepadKeyNavigation.getConnectedGamepads() > 0) {
currentIndex = 0
onVisibleChanged: {
if (visible) {
gamepadKeyNav.enable()
}
else {
gamepadKeyNav.disable()
}
}
StackView.onDeactivating: {
ComputerManager.computerAddCompleted.disconnect(addComplete)
Component.onCompleted: {
// Setup signals on CM
ComputerManager.computerAddCompleted.connect(addComplete)
// Don't show any highlighted item until interacting with them
currentIndex = -1
}
function pairingComplete(error)
@@ -56,23 +62,14 @@ CenteredGridView {
// Display a failed dialog if we got an error
if (error !== undefined) {
errorDialog.text = error
errorDialog.helpText = ""
errorDialog.open()
}
}
function addComplete(success, detectedPortBlocking)
function addComplete(success)
{
if (!success) {
errorDialog.text = qsTr("Unable to connect to the specified PC.")
if (detectedPortBlocking) {
errorDialog.text += "\n\n" + qsTr("This PC's Internet connection is blocking Moonlight. Streaming over the Internet may not work while connected to this network.")
}
else {
errorDialog.helpText = qsTr("Click the Help button for possible solutions.")
}
errorDialog.text = "Unable to connect to the specified PC. Click the Help button for possible solutions."
errorDialog.open()
}
}
@@ -82,41 +79,21 @@ CenteredGridView {
var model = Qt.createQmlObject('import ComputerModel 1.0; ComputerModel {}', parent, '')
model.initialize(ComputerManager)
model.pairingCompleted.connect(pairingComplete)
model.connectionTestCompleted.connect(testConnectionDialog.connectionTestComplete)
return model
}
Row {
anchors.centerIn: parent
spacing: 5
visible: pcGrid.count === 0
BusyIndicator {
id: searchSpinner
visible: StreamingPreferences.enableMdns
}
Label {
height: searchSpinner.height
elide: Label.ElideRight
text: StreamingPreferences.enableMdns ? qsTr("Searching for PCs on your local network with NVIDIA GameStream enabled...")
: qsTr("Automatic PC discovery is disabled. Add your PC manually.")
font.pointSize: 20
verticalAlignment: Text.AlignVCenter
wrapMode: Text.Wrap
}
}
model: computerModel
delegate: NavigableItemDelegate {
width: 300; height: 320;
width: 300; height: 300;
grid: pcGrid
Image {
id: pcIcon
anchors.horizontalCenter: parent.horizontalCenter
source: "qrc:/res/desktop_windows-48px.svg"
source: {
model.addPc ? "qrc:/res/ic_add_to_queue_white_48px.svg" : "qrc:/res/ic_tv_white_48px.svg"
}
sourceSize {
width: 200
height: 200
@@ -128,8 +105,8 @@ CenteredGridView {
id: stateIcon
anchors.horizontalCenter: pcIcon.horizontalCenter
anchors.verticalCenter: pcIcon.verticalCenter
anchors.verticalCenterOffset: -15
visible: !model.statusUnknown && (!model.online || !model.paired)
anchors.verticalCenterOffset: -10
visible: !model.addPc && !model.statusUnknown && (!model.online || !model.paired)
source: !model.online ? "qrc:/res/baseline-warning-24px.svg" : "qrc:/res/baseline-lock-24px.svg"
sourceSize {
width: 75
@@ -141,10 +118,10 @@ CenteredGridView {
id: statusUnknownSpinner
anchors.horizontalCenter: pcIcon.horizontalCenter
anchors.verticalCenter: pcIcon.verticalCenter
anchors.verticalCenterOffset: -15
anchors.verticalCenterOffset: -10
width: 75
height: 75
visible: model.statusUnknown
visible: !model.addPc && model.statusUnknown
}
Label {
@@ -153,57 +130,20 @@ CenteredGridView {
width: parent.width
anchors.top: pcIcon.bottom
anchors.bottom: parent.bottom
font.pointSize: 36
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.Wrap
elide: Text.ElideRight
}
NavigableMenu {
Menu {
id: pcContextMenu
MenuItem {
text: qsTr("PC Status: %1").arg(model.online ? qsTr("Online") : qsTr("Offline"))
font.bold: true
enabled: false
}
NavigableMenuItem {
parentMenu: pcContextMenu
text: qsTr("View All Apps")
onTriggered: {
var component = Qt.createComponent("AppView.qml")
var appView = component.createObject(stackView, {"computerIndex": index, "objectName": model.name, "showHiddenGames": true})
stackView.push(appView)
}
visible: model.online && model.paired
}
NavigableMenuItem {
parentMenu: pcContextMenu
text: qsTr("Wake PC")
text: "Wake PC"
onTriggered: computerModel.wakeComputer(index)
visible: !model.online && model.wakeable
visible: !model.addPc && !model.online && model.wakeable
}
NavigableMenuItem {
parentMenu: pcContextMenu
text: qsTr("Test Network")
onTriggered: {
computerModel.testConnectionForComputer(index)
testConnectionDialog.open()
}
}
NavigableMenuItem {
parentMenu: pcContextMenu
text: qsTr("Rename PC")
onTriggered: {
renamePcDialog.pcIndex = index
renamePcDialog.originalName = model.name
renamePcDialog.open()
}
}
NavigableMenuItem {
parentMenu: pcContextMenu
text: qsTr("Delete PC")
text: "Delete PC"
onTriggered: {
deletePcDialog.pcIndex = index
// get confirmation first, actual closing is called from the dialog
@@ -213,7 +153,10 @@ CenteredGridView {
}
onClicked: {
if (model.online) {
if (model.addPc) {
addPcDialog.open()
}
else if (model.online) {
if (model.paired) {
// go to game view
var component = Qt.createComponent("AppView.qml")
@@ -222,7 +165,7 @@ CenteredGridView {
}
else {
if (!model.busy) {
var pin = computerModel.generatePinString()
var pin = ("0000" + Math.floor(Math.random() * 10000)).slice(-4)
// Kick off pairing in the background
computerModel.pairComputer(index, pin)
@@ -233,8 +176,7 @@ CenteredGridView {
}
else {
// cannot pair while something is streaming or attempting to pair
errorDialog.text = qsTr("You cannot pair while a previous session is still running on the host PC. Quit any running games or reboot the host PC, then try pairing again.")
errorDialog.helpText = ""
errorDialog.text = "This PC is currently busy. Make sure to quit any running games and try again."
errorDialog.open()
}
}
@@ -244,144 +186,117 @@ CenteredGridView {
}
}
onPressAndHold: {
// popup() ensures the menu appears under the mouse cursor
if (pcContextMenu.popup) {
pcContextMenu.popup()
}
else {
// Qt 5.9 doesn't have popup()
pcContextMenu.open()
}
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.RightButton;
onClicked: {
parent.onPressAndHold()
if (!model.addPc) {
// popup() ensures the menu appears under the mouse cursor
if (pcContextMenu.popup) {
pcContextMenu.popup()
}
else {
// Qt 5.9 doesn't have popup()
pcContextMenu.open()
}
}
}
}
Keys.onMenuPressed: {
// We must use open() here so the menu is positioned on
// the ItemDelegate and not where the mouse cursor is
pcContextMenu.open()
if (!model.addPc) {
// We must use open() here so the menu is positioned on
// the ItemDelegate and not where the mouse cursor is
pcContextMenu.open()
}
}
}
ErrorMessageDialog {
MessageDialog {
id: errorDialog
// Using Setup-Guide here instead of Troubleshooting because it's likely that users
// will arrive here by forgetting to enable GameStream or not forwarding ports.
helpUrl: "https://github.com/moonlight-stream/moonlight-docs/wiki/Setup-Guide"
// don't allow edits to the rest of the window while open
modality:Qt.WindowModal
icon: StandardIcon.Critical
standardButtons: StandardButton.Ok | StandardButton.Help
onHelp: {
// Using Setup-Guide here instead of Troubleshooting because it's likely that users
// will arrive here by forgetting to enable GameStream or not forwarding ports.
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Setup-Guide");
}
}
NavigableMessageDialog {
MessageDialog {
id: pairDialog
// Pairing dialog must be modal to prevent double-clicks from triggering
// pairing twice
modal: true
closePolicy: Popup.CloseOnEscape
// don't allow edits to the rest of the window while open
modality:Qt.WindowModal
property string pin : "0000"
text:qsTr("Please enter %1 on your GameStream PC. This dialog will close when pairing is completed.").arg(pin)
standardButtons: Dialog.Cancel
text:"Please enter " + pin + " on your GameStream PC. This dialog will close when pairing is completed."
standardButtons: StandardButton.Cancel
onRejected: {
// FIXME: We should interrupt pairing here
}
}
NavigableMessageDialog {
MessageDialog {
id: deletePcDialog
// don't allow edits to the rest of the window while open
modality:Qt.WindowModal
property int pcIndex : -1;
text:qsTr("Are you sure you want to remove this PC?")
standardButtons: Dialog.Yes | Dialog.No
text:"Are you sure you want to remove this PC?"
standardButtons: StandardButton.Yes | StandardButton.No
function deletePc() {
console.log("deleting PC pairing for PC at index: " + pcIndex)
computerModel.deleteComputer(pcIndex);
}
onYes: deletePc()
// For keyboard/gamepad activation
onAccepted: deletePc()
}
NavigableMessageDialog {
id: testConnectionDialog
closePolicy: Popup.CloseOnEscape
standardButtons: Dialog.Ok
Dialog {
id: addPcDialog
property string label: "Enter the IP address of your GameStream PC"
property string hint: "192.168.1.100"
property alias editText : editTextItem
onAboutToShow: {
testConnectionDialog.text = qsTr("Moonlight is testing your network connection to determine if NVIDIA GameStream is blocked.") + "\n\n" + qsTr("This may take a few seconds…")
showSpinner = true
standardButtons: StandardButton.Ok | StandardButton.Cancel
onVisibleChanged: {
editTextItem.focus = true
editTextItem.selectAll()
}
function connectionTestComplete(result, blockedPorts)
{
if (result === -1) {
text = qsTr("The network test could not be performed because none of Moonlight's connection testing servers were reachable from this PC. Check your Internet connection or try again later.")
imageSrc = "qrc:/res/baseline-warning-24px.svg"
}
else if (result === 0) {
text = qsTr("This network does not appear to be blocking Moonlight. If you still have trouble connecting, check your PC's firewall settings.") + "\n\n" + qsTr("If you are trying to stream over the Internet, install the Moonlight Internet Hosting Tool on your gaming PC and run the included Internet Streaming Tester to check your gaming PC's Internet connection.")
imageSrc = "qrc:/res/baseline-check_circle_outline-24px.svg"
}
else {
text = qsTr("Your PC's current network connection seems to be blocking Moonlight. Streaming over the Internet may not work while connected to this network.") + "\n\n" + qsTr("The following network ports were blocked:") + "\n"
text += blockedPorts
imageSrc = "qrc:/res/baseline-error_outline-24px.svg"
}
// Stop showing the spinner and show the image instead
showSpinner = false
}
}
NavigableDialog {
id: renamePcDialog
property string label: qsTr("Enter the new name for this PC:")
property string originalName
property int pcIndex : -1;
standardButtons: Dialog.Ok | Dialog.Cancel
onOpened: {
// Force keyboard focus on the textbox so keyboard navigation works
editText.forceActiveFocus()
}
onClosed: {
editText.clear()
}
onAccepted: {
if (editText.text) {
computerModel.renameComputer(pcIndex, editText.text)
}
ComputerManager.addNewHost(editText.text, false)
}
ColumnLayout {
Label {
text: renamePcDialog.label
font.bold: true
Text {
id: addPcDialogLabel
text: addPcDialog.label
}
Rectangle {
implicitWidth: parent.parent.width
height: editTextItem.height
TextField {
id: editText
placeholderText: renamePcDialog.originalName
Layout.fillWidth: true
focus: true
Keys.onReturnPressed: {
renamePcDialog.accept()
TextInput {
id: editTextItem
inputMethodHints: Qt.ImhPreferUppercase
text: addPcDialog.hint
}
}
}
}
ScrollBar.vertical: ScrollBar {}
ScrollBar.vertical: ScrollBar {
parent: pcGrid.parent
anchors {
top: pcGrid.top
left: pcGrid.right
bottom: pcGrid.bottom
leftMargin: -10
}
}
}
+26 -21
View File
@@ -1,5 +1,6 @@
import QtQuick 2.0
import QtQuick.Controls 2.2
import QtQuick.Dialogs 1.2
import ComputerManager 1.0
import Session 1.0
@@ -9,7 +10,14 @@ Item {
property Session nextSession : null
property string nextAppName : ""
property string stageText : qsTr("Quitting %1...").arg(appName)
property string stageText : "Quitting " + appName + "..."
anchors.fill: parent
// The StackView will trigger a visibility change when
// we're pushed onto it, causing our onVisibleChanged
// routine to run, but only if we start as invisible
visible: false
function quitAppCompleted(error)
{
@@ -19,34 +27,28 @@ Item {
errorDialog.open()
}
// Exit this view
stackView.pop()
// If we're supposed to launch another game after this, do so now
if (error === undefined && nextSession !== null) {
var component = Qt.createComponent("StreamSegue.qml")
var segue = component.createObject(stackView, {"appName": nextAppName, "session": nextSession})
stackView.replace(segue)
stackView.push(segue)
}
}
onVisibleChanged: {
if (visible) {
// Connect the quit completion signal
ComputerManager.quitAppCompleted.connect(quitAppCompleted)
}
else {
// Exit this view
stackView.pop()
// Disconnect the signal
ComputerManager.quitAppCompleted.disconnect(quitAppCompleted)
}
}
StackView.onActivated: {
// Hide the toolbar before we start loading
toolBar.visible = false
// Connect the quit completion signal
ComputerManager.quitAppCompleted.connect(quitAppCompleted)
}
StackView.onDeactivating: {
// Show the toolbar again
toolBar.visible = true
// Disconnect the signal
ComputerManager.quitAppCompleted.disconnect(quitAppCompleted)
}
Row {
anchors.centerIn: parent
spacing: 5
@@ -66,7 +68,10 @@ Item {
}
}
ErrorMessageDialog {
MessageDialog {
id: errorDialog
modality:Qt.WindowModal
icon: StandardIcon.Critical
standardButtons: StandardButton.Ok
}
}
+168 -645
View File
File diff suppressed because it is too large Load Diff
+66 -86
View File
@@ -1,32 +1,35 @@
import QtQuick 2.0
import QtQuick.Controls 2.2
import QtQuick.Dialogs 1.2
import QtQuick.Window 2.2
import ComputerManager 1.0
import SdlGamepadKeyNavigation 1.0
import Session 1.0
Item {
property Session session
property string appName
property string stageText : isResume ? qsTr("Resuming %1...").arg(appName) :
qsTr("Starting %1...").arg(appName)
property bool isResume : false
property string stageText : "Starting " + appName + "..."
property bool quitAfter : false
anchors.fill: parent
// The StackView will trigger a visibility change when
// we're pushed onto it, causing our onVisibleChanged
// routine to run, but only if we start as invisible
visible: false
function stageStarting(stage)
{
// Update the spinner text
stageText = qsTr("Starting %1...").arg(stage)
stageText = "Starting " + stage + "..."
}
function stageFailed(stage, errorCode, failingPorts)
function stageFailed(stage, errorCode)
{
// Display the error dialog after Session::exec() returns
streamSegueErrorDialog.text = qsTr("Starting %1 failed: Error %2").arg(stage).arg(errorCode)
if (failingPorts) {
streamSegueErrorDialog.text += "\n\n" + qsTr("Check your firewall and port forwarding rules for port(s): %1").arg(failingPorts)
}
errorDialog.text = "Starting " + stage + " failed: Error " + errorCode
errorDialog.open()
}
function connectionStarted()
@@ -35,7 +38,6 @@ Item {
// see them briefly when we pop off the StackView
stageSpinner.visible = false
stageLabel.visible = false
hintText.visible = false
// Hide the window now that streaming has begun
window.visible = false
@@ -44,7 +46,7 @@ Item {
function displayLaunchError(text)
{
// Display the error dialog after Session::exec() returns
streamSegueErrorDialog.text = text
errorDialog.text = text
}
function displayLaunchWarning(text)
@@ -52,7 +54,7 @@ Item {
// This toast appears for 3 seconds, just shorter than how long
// Session will wait for it to be displayed. This gives it time
// to transition to invisible before continuing.
var toast = Qt.createQmlObject('import QtQuick.Controls 2.2; ToolTip {}', parent, '')
var toast = Qt.createQmlObject('import QtQuick.Controls 2.3; ToolTip {}', parent, '')
toast.text = text
toast.timeout = 3000
toast.visible = true
@@ -68,20 +70,12 @@ Item {
window.visible = true
}
function sessionFinished(portTestResult)
function sessionFinished()
{
if (portTestResult !== 0 && portTestResult !== -1 && streamSegueErrorDialog.text) {
streamSegueErrorDialog.text += "\n\n" + qsTr("This PC's Internet connection is blocking Moonlight. Streaming over the Internet may not work while connected to this network.")
}
// Enable GUI gamepad usage now
SdlGamepadKeyNavigation.enable()
if (quitAfter) {
if (streamSegueErrorDialog.text) {
if (errorDialog.text) {
// Quit when the error dialog is acknowledged
streamSegueErrorDialog.quitAfter = quitAfter
streamSegueErrorDialog.open()
errorDialog.open()
}
else {
// Quit immediately
@@ -98,82 +92,48 @@ Item {
// the Qt UI is visible again to prevent losing
// focus on the dialog which would impact gamepad
// users.
if (streamSegueErrorDialog.text) {
streamSegueErrorDialog.quitAfter = quitAfter
streamSegueErrorDialog.open()
if (errorDialog.text) {
errorDialog.open()
}
}
// Garbage collect the Session object since it's pretty heavyweight
// and keeps other libraries (like SDL_TTF) around until it is deleted.
session = null
gc()
}
StackView.onDeactivating: {
// Show the toolbar again when popped off the stack
toolBar.visible = true
// Enable GUI gamepad usage now
SdlGamepadKeyNavigation.enable()
// It's important that we don't call enable() here
// or it may interfere with the Session instance
// getting notified of initial connected gamepads.
SdlGamepadKeyNavigation {
id: gamepadKeyNav
}
StackView.onActivated: {
// Hide the toolbar before we start loading
toolBar.visible = false
onVisibleChanged: {
if (visible) {
// Hide the toolbar before we start loading
toolBar.visible = false
// Hook up our signals
session.stageStarting.connect(stageStarting)
session.stageFailed.connect(stageFailed)
session.connectionStarted.connect(connectionStarted)
session.displayLaunchError.connect(displayLaunchError)
session.displayLaunchWarning.connect(displayLaunchWarning)
session.quitStarting.connect(quitStarting)
session.sessionFinished.connect(sessionFinished)
// Kick off the stream
spinnerTimer.start()
streamLoader.active = true
}
Timer {
id: spinnerTimer
// Display the spinner appearance a bit to allow us to reach
// the code in Session.exec() that pumps the event loop.
// If we display it immediately, it will briefly hang in the
// middle of the animation on Windows, which looks very
// obviously broken.
interval: 100
onTriggered: stageSpinner.running = true
}
Loader {
id: streamLoader
active: false
asynchronous: true
onLoaded: {
// Set the hint text. We do this here rather than
// in the hintText control itself to synchronize
// with Session.exec() which requires no concurrent
// gamepad usage.
hintText.text = qsTr("Tip:") + " " + qsTr("Press %1 to disconnect your session").arg(SdlGamepadKeyNavigation.getConnectedGamepads() > 0 ?
qsTr("Start+Select+L1+R1") : qsTr("Ctrl+Alt+Shift+Q"))
hintText.text = gamepadKeyNav.getConnectedGamepads() > 0 ?
"Tip: Press Start+Select+L1+R1 to disconnect your session" :
"Tip: Press Ctrl+Alt+Shift+Q to disconnect your session"
// Stop GUI gamepad usage now
SdlGamepadKeyNavigation.disable()
// Garbage collect QML stuff before we start streaming,
// since we'll probably be streaming for a while and we
// won't be able to GC during the stream.
gc()
// Hook up our signals
session.stageStarting.connect(stageStarting)
session.stageFailed.connect(stageFailed)
session.connectionStarted.connect(connectionStarted)
session.displayLaunchError.connect(displayLaunchError)
session.displayLaunchWarning.connect(displayLaunchWarning)
session.quitStarting.connect(quitStarting)
session.sessionFinished.connect(sessionFinished)
// Run the streaming session to completion
session.exec(Screen.virtualX, Screen.virtualY)
}
sourceComponent: Item {}
else if (!quitAfter) {
// Show the toolbar again when we become hidden
toolBar.visible = true
}
}
Row {
@@ -182,7 +142,6 @@ Item {
BusyIndicator {
id: stageSpinner
running: false
}
Label {
@@ -206,4 +165,25 @@ Item {
wrapMode: Text.Wrap
}
MessageDialog {
id: errorDialog
modality:Qt.WindowModal
icon: StandardIcon.Critical
standardButtons: StandardButton.Ok | StandardButton.Help
onAccepted: {
if (quitAfter) {
Qt.quit()
}
}
onHelp: {
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Troubleshooting");
if (quitAfter) {
Qt.quit()
}
}
}
}
+32 -173
View File
@@ -7,7 +7,7 @@ AppModel::AppModel(QObject *parent)
this, &AppModel::handleBoxArtLoaded);
}
void AppModel::initialize(ComputerManager* computerManager, int computerIndex, bool showHiddenGames)
void AppModel::initialize(ComputerManager* computerManager, int computerIndex)
{
m_ComputerManager = computerManager;
connect(m_ComputerManager, &ComputerManager::computerStateChanged,
@@ -15,23 +15,29 @@ void AppModel::initialize(ComputerManager* computerManager, int computerIndex, b
Q_ASSERT(computerIndex < m_ComputerManager->getComputers().count());
m_Computer = m_ComputerManager->getComputers().at(computerIndex);
m_Apps = m_Computer->appList;
m_CurrentGameId = m_Computer->currentGameId;
m_ShowHiddenGames = showHiddenGames;
updateAppList(m_Computer->appList);
}
int AppModel::getRunningAppId()
int AppModel::getRunningAppIndex()
{
return m_CurrentGameId;
if (m_CurrentGameId != 0) {
for (int i = 0; i < m_Apps.count(); i++) {
if (m_Apps[i].id == m_CurrentGameId) {
return i;
}
}
}
return -1;
}
QString AppModel::getRunningAppName()
{
if (m_CurrentGameId != 0) {
for (int i = 0; i < m_AllApps.count(); i++) {
if (m_AllApps[i].id == m_CurrentGameId) {
return m_AllApps[i].name;
for (int i = 0; i < m_Apps.count(); i++) {
if (m_Apps[i].id == m_CurrentGameId) {
return m_Apps[i].name;
}
}
}
@@ -41,23 +47,12 @@ QString AppModel::getRunningAppName()
Session* AppModel::createSessionForApp(int appIndex)
{
Q_ASSERT(appIndex < m_VisibleApps.count());
NvApp app = m_VisibleApps.at(appIndex);
Q_ASSERT(appIndex < m_Apps.count());
NvApp app = m_Apps.at(appIndex);
return new Session(m_Computer, app);
}
int AppModel::getDirectLaunchAppIndex()
{
for (int i = 0; i < m_VisibleApps.count(); i++) {
if (m_VisibleApps[i].directLaunch) {
return i;
}
}
return -1;
}
int AppModel::rowCount(const QModelIndex &parent) const
{
// For list models only the root node (an invalid parent) should return the list's size. For all
@@ -65,7 +60,7 @@ int AppModel::rowCount(const QModelIndex &parent) const
if (parent.isValid())
return 0;
return m_VisibleApps.count();
return m_Apps.count();
}
QVariant AppModel::data(const QModelIndex &index, int role) const
@@ -73,8 +68,8 @@ QVariant AppModel::data(const QModelIndex &index, int role) const
if (!index.isValid())
return QVariant();
Q_ASSERT(index.row() < m_VisibleApps.count());
NvApp app = m_VisibleApps.at(index.row());
Q_ASSERT(index.row() < m_Apps.count());
NvApp app = m_Apps.at(index.row());
switch (role)
{
@@ -85,12 +80,6 @@ QVariant AppModel::data(const QModelIndex &index, int role) const
case BoxArtRole:
// FIXME: const-correctness
return const_cast<BoxArtManager&>(m_BoxArtManager).loadBoxArt(m_Computer, app);
case HiddenRole:
return app.hidden;
case AppIdRole:
return app.id;
case DirectLaunchRole:
return app.directLaunch;
default:
return QVariant();
}
@@ -103,9 +92,6 @@ QHash<int, QByteArray> AppModel::roleNames() const
names[NameRole] = "name";
names[RunningRole] = "running";
names[BoxArtRole] = "boxart";
names[HiddenRole] = "hidden";
names[AppIdRole] = "appid";
names[DirectLaunchRole] = "directLaunch";
return names;
}
@@ -115,138 +101,6 @@ void AppModel::quitRunningApp()
m_ComputerManager->quitRunningApp(m_Computer);
}
bool AppModel::isAppCurrentlyVisible(const NvApp& app)
{
for (const NvApp& visibleApp : m_VisibleApps) {
if (app.id == visibleApp.id) {
return true;
}
}
return false;
}
QVector<NvApp> AppModel::getVisibleApps(const QVector<NvApp>& appList)
{
QVector<NvApp> visibleApps;
for (const NvApp& app : appList) {
// Don't immediately hide games that were previously visible. This
// allows users to easily uncheck the "Hide App" checkbox if they
// check it by mistake.
if (m_ShowHiddenGames || !app.hidden || isAppCurrentlyVisible(app)) {
visibleApps.append(app);
}
}
return visibleApps;
}
void AppModel::updateAppList(QVector<NvApp> newList)
{
m_AllApps = newList;
QVector<NvApp> newVisibleList = getVisibleApps(newList);
// Process removals and updates first
for (int i = 0; i < m_VisibleApps.count(); i++) {
const NvApp& existingApp = m_VisibleApps.at(i);
bool found = false;
for (const NvApp& newApp : newVisibleList) {
if (existingApp.id == newApp.id) {
// If the data changed, update it in our list
if (existingApp != newApp) {
m_VisibleApps.replace(i, newApp);
emit dataChanged(createIndex(i, 0), createIndex(i, 0));
}
found = true;
break;
}
}
if (!found) {
beginRemoveRows(QModelIndex(), i, i);
m_VisibleApps.removeAt(i);
endRemoveRows();
i--;
}
}
// Process additions now
for (const NvApp& newApp : newVisibleList) {
int insertionIndex = m_VisibleApps.size();
bool found = false;
for (int i = 0; i < m_VisibleApps.count(); i++) {
const NvApp& existingApp = m_VisibleApps.at(i);
if (existingApp.id == newApp.id) {
found = true;
break;
}
else if (existingApp.name.toLower() > newApp.name.toLower()) {
insertionIndex = i;
break;
}
}
if (!found) {
beginInsertRows(QModelIndex(), insertionIndex, insertionIndex);
m_VisibleApps.insert(insertionIndex, newApp);
endInsertRows();
}
}
Q_ASSERT(newVisibleList == m_VisibleApps);
}
void AppModel::setAppHidden(int appIndex, bool hidden)
{
Q_ASSERT(appIndex < m_VisibleApps.count());
int appId = m_VisibleApps.at(appIndex).id;
{
QWriteLocker lock(&m_Computer->lock);
for (NvApp& app : m_Computer->appList) {
if (app.id == appId) {
app.hidden = hidden;
break;
}
}
}
m_ComputerManager->clientSideAttributeUpdated(m_Computer);
}
void AppModel::setAppDirectLaunch(int appIndex, bool directLaunch)
{
Q_ASSERT(appIndex < m_VisibleApps.count());
int appId = m_VisibleApps.at(appIndex).id;
{
QWriteLocker lock(&m_Computer->lock);
for (NvApp& app : m_Computer->appList) {
if (directLaunch) {
// We must clear direct launch from all other apps
// to set it on the new app.
app.directLaunch = app.id == appId;
}
else if (app.id == appId) {
// If we're clearing direct launch, we're done once we
// find our matching app ID.
app.directLaunch = false;
break;
}
}
}
m_ComputerManager->clientSideAttributeUpdated(m_Computer);
}
void AppModel::handleComputerStateChanged(NvComputer* computer)
{
// Ignore updates for computers that aren't ours
@@ -265,15 +119,20 @@ void AppModel::handleComputerStateChanged(NvComputer* computer)
// First, process additions/removals from the app list. This
// is required because the new game may now be running, so
// we can't check that first.
if (computer->appList != m_AllApps) {
updateAppList(computer->appList);
if (computer->appList != m_Apps) {
// Just reset the whole thing if the list changes
beginResetModel();
m_Apps = computer->appList;
m_CurrentGameId = computer->currentGameId;
endResetModel();
return;
}
// Finally, process changes to the active app
if (computer->currentGameId != m_CurrentGameId) {
// First, invalidate the running state of newly running game
for (int i = 0; i < m_VisibleApps.count(); i++) {
if (m_VisibleApps[i].id == computer->currentGameId) {
for (int i = 0; i < m_Apps.count(); i++) {
if (m_Apps[i].id == computer->currentGameId) {
emit dataChanged(createIndex(i, 0),
createIndex(i, 0),
QVector<int>() << RunningRole);
@@ -283,8 +142,8 @@ void AppModel::handleComputerStateChanged(NvComputer* computer)
// Next, invalidate the running state of the old game (if it exists)
if (m_CurrentGameId != 0) {
for (int i = 0; i < m_VisibleApps.count(); i++) {
if (m_VisibleApps[i].id == m_CurrentGameId) {
for (int i = 0; i < m_Apps.count(); i++) {
if (m_Apps[i].id == m_CurrentGameId) {
emit dataChanged(createIndex(i, 0),
createIndex(i, 0),
QVector<int>() << RunningRole);
@@ -302,7 +161,7 @@ void AppModel::handleBoxArtLoaded(NvComputer* computer, NvApp app, QUrl /* image
{
Q_ASSERT(computer == m_Computer);
int index = m_VisibleApps.indexOf(app);
int index = m_Apps.indexOf(app);
// Make sure we're not delivering a callback to an app that's already been removed
if (index >= 0) {
+4 -20
View File
@@ -14,32 +14,23 @@ class AppModel : public QAbstractListModel
{
NameRole = Qt::UserRole,
RunningRole,
BoxArtRole,
HiddenRole,
AppIdRole,
DirectLaunchRole
BoxArtRole
};
public:
explicit AppModel(QObject *parent = nullptr);
// Must be called before any QAbstractListModel functions
Q_INVOKABLE void initialize(ComputerManager* computerManager, int computerIndex, bool showHiddenGames);
Q_INVOKABLE void initialize(ComputerManager* computerManager, int computerIndex);
Q_INVOKABLE Session* createSessionForApp(int appIndex);
Q_INVOKABLE int getDirectLaunchAppIndex();
Q_INVOKABLE int getRunningAppId();
Q_INVOKABLE int getRunningAppIndex();
Q_INVOKABLE QString getRunningAppName();
Q_INVOKABLE void quitRunningApp();
Q_INVOKABLE void setAppHidden(int appIndex, bool hidden);
Q_INVOKABLE void setAppDirectLaunch(int appIndex, bool directLaunch);
QVariant data(const QModelIndex &index, int role) const override;
int rowCount(const QModelIndex &parent) const override;
@@ -55,16 +46,9 @@ signals:
void computerLost();
private:
void updateAppList(QVector<NvApp> newList);
QVector<NvApp> getVisibleApps(const QVector<NvApp>& appList);
bool isAppCurrentlyVisible(const NvApp& app);
NvComputer* m_Computer;
BoxArtManager m_BoxArtManager;
ComputerManager* m_ComputerManager;
QVector<NvApp> m_VisibleApps, m_AllApps;
QVector<NvApp> m_Apps;
int m_CurrentGameId;
bool m_ShowHiddenGames;
};
+17 -50
View File
@@ -2,8 +2,6 @@
#include <QThreadPool>
#include <random>
ComputerModel::ComputerModel(QObject* object)
: QAbstractListModel(object) {}
@@ -24,6 +22,18 @@ QVariant ComputerModel::data(const QModelIndex& index, int role) const
return QVariant();
}
if (index.row() == m_Computers.count()) {
// We insert a synthetic item at the end for the Add PC option
switch (role) {
case NameRole:
return "Add PC";
case AddPcRole:
return true;
default:
return QVariant();
}
}
Q_ASSERT(index.row() < m_Computers.count());
NvComputer* computer = m_Computers[index.row()];
@@ -40,6 +50,8 @@ QVariant ComputerModel::data(const QModelIndex& index, int role) const
return computer->currentGameId != 0;
case WakeableRole:
return !computer->macAddress.isEmpty();
case AddPcRole:
return false;
case StatusUnknownRole:
return computer->state == NvComputer::CS_UNKNOWN;
default:
@@ -55,7 +67,8 @@ int ComputerModel::rowCount(const QModelIndex& parent) const
return 0;
}
return m_Computers.count();
// Add PC placeholder counts as 1
return m_Computers.count() + 1;
}
QHash<int, QByteArray> ComputerModel::roleNames() const
@@ -66,6 +79,7 @@ QHash<int, QByteArray> ComputerModel::roleNames() const
names[OnlineRole] = "online";
names[PairedRole] = "paired";
names[BusyRole] = "busy";
names[AddPcRole] = "addPc";
names[WakeableRole] = "wakeable";
names[StatusUnknownRole] = "statusUnknown";
@@ -130,52 +144,6 @@ void ComputerModel::wakeComputer(int computerIndex)
QThreadPool::globalInstance()->start(wakeTask);
}
void ComputerModel::renameComputer(int computerIndex, QString name)
{
Q_ASSERT(computerIndex < m_Computers.count());
m_ComputerManager->renameHost(m_Computers[computerIndex], name);
}
// TODO: Use QRandomGenerator when we drop Qt 5.9 support
QString ComputerModel::generatePinString()
{
std::uniform_int_distribution<int> dist(0, 9999);
std::random_device rd;
std::mt19937 engine(rd());
return QString::asprintf("%04u", dist(engine));
}
class DeferredTestConnectionTask : public QObject, public QRunnable
{
Q_OBJECT
public:
void run()
{
unsigned int portTestResult = LiTestClientConnectivity("qt.conntest.moonlight-stream.org", 443, ML_PORT_FLAG_ALL);
if (portTestResult == ML_TEST_RESULT_INCONCLUSIVE) {
emit connectionTestCompleted(-1, QString());
}
else {
char blockedPorts[512];
LiStringifyPortFlags(portTestResult, "\n", blockedPorts, sizeof(blockedPorts));
emit connectionTestCompleted(portTestResult, QString(blockedPorts));
}
}
signals:
void connectionTestCompleted(int result, QString blockedPorts);
};
void ComputerModel::testConnectionForComputer(int)
{
DeferredTestConnectionTask* testConnectionTask = new DeferredTestConnectionTask();
QObject::connect(testConnectionTask, &DeferredTestConnectionTask::connectionTestCompleted,
this, &ComputerModel::connectionTestCompleted);
QThreadPool::globalInstance()->start(testConnectionTask);
}
void ComputerModel::pairComputer(int computerIndex, QString pin)
{
Q_ASSERT(computerIndex < m_Computers.count());
@@ -206,4 +174,3 @@ void ComputerModel::handleComputerStateChanged(NvComputer* computer)
}
}
#include "computermodel.moc"
+2 -8
View File
@@ -14,7 +14,8 @@ class ComputerModel : public QAbstractListModel
PairedRole,
BusyRole,
WakeableRole,
StatusUnknownRole
StatusUnknownRole,
AddPcRole
};
public:
@@ -31,21 +32,14 @@ public:
Q_INVOKABLE void deleteComputer(int computerIndex);
Q_INVOKABLE QString generatePinString();
Q_INVOKABLE void pairComputer(int computerIndex, QString pin);
Q_INVOKABLE void testConnectionForComputer(int computerIndex);
Q_INVOKABLE void wakeComputer(int computerIndex);
Q_INVOKABLE void renameComputer(int computerIndex, QString name);
Q_INVOKABLE Session* createSessionForCurrentGame(int computerIndex);
signals:
void pairingCompleted(QVariant error);
void connectionTestCompleted(int result, QString blockedPorts);
private slots:
void handleComputerStateChanged(NvComputer* computer);
+116 -204
View File
@@ -1,13 +1,14 @@
import QtQuick 2.9
import QtQuick.Controls 2.2
import QtQuick.Dialogs 1.2
import QtQuick.Layouts 1.3
import QtQuick.Window 2.2
import QtQuick.Controls.Material 2.1
import ComputerManager 1.0
import AutoUpdateChecker 1.0
import StreamingPreferences 1.0
import SystemProperties 1.0
import SdlGamepadKeyNavigation 1.0
ApplicationWindow {
property bool pollingActive: false
@@ -17,19 +18,14 @@ ApplicationWindow {
width: 1280
height: 600
visibility: {
if (SystemProperties.hasWindowManager) {
if (StreamingPreferences.uiDisplayMode == StreamingPreferences.UI_WINDOWED) return "Windowed"
else if (StreamingPreferences.uiDisplayMode == StreamingPreferences.UI_MAXIMIZED) return "Maximized"
else if (StreamingPreferences.uiDisplayMode == StreamingPreferences.UI_FULLSCREEN) return "FullScreen"
} else {
return "FullScreen"
}
visibility: prefs.startWindowed ? "Windowed" : "Maximized"
Material.theme: Material.Dark
Material.accent: Material.Purple
StreamingPreferences {
id: prefs
}
// This configures the maximum width of the singleton attached QML ToolTip. If left unconstrained,
// it will never insert a line break and just extend on forever.
ToolTip.toolTip.contentWidth: ToolTip.toolTip.implicitContentWidth < 400 ? ToolTip.toolTip.implicitContentWidth : 400
StackView {
id: stackView
@@ -146,20 +142,19 @@ ApplicationWindow {
// onAfterRendering call and potentially reenter this code.
initialized = true;
if (SystemProperties.isWow64) {
if (prefs.isRunningWayland()) {
waylandDialog.open()
}
else if (prefs.isWow64()) {
wow64Dialog.open()
}
else if (!SystemProperties.hasHardwareAcceleration) {
if (SystemProperties.isRunningXWayland) {
xWaylandDialog.open()
}
else {
noHwDecoderDialog.open()
}
else if (!prefs.hasAnyHardwareAcceleration()) {
noHwDecoderDialog.open()
}
if (SystemProperties.unmappedGamepads) {
unmappedGamepadDialog.unmappedGamepads = SystemProperties.unmappedGamepads
var unmappedGamepads = prefs.getUnmappedGamepads()
if (unmappedGamepads) {
unmappedGamepadDialog.unmappedGamepads = unmappedGamepads
unmappedGamepadDialog.open()
}
}
@@ -183,32 +178,28 @@ ApplicationWindow {
header: ToolBar {
id: toolBar
height: 60
anchors.topMargin: 5
anchors.bottomMargin: 5
Label {
id: titleLabel
visible: toolBar.width > 700
anchors.fill: parent
text: stackView.currentItem.objectName
font.pointSize: 20
elide: Label.ElideRight
horizontalAlignment: Qt.AlignHCenter
verticalAlignment: Qt.AlignVCenter
}
RowLayout {
spacing: 20
anchors.leftMargin: 10
anchors.rightMargin: 10
anchors.fill: parent
NavigableToolButton {
// Only make the button visible if the user has navigated somewhere.
visible: stackView.depth > 1
iconSource: "qrc:/res/arrow_left.svg"
// FIXME: We're using an Image here rather than icon.source because
// icons don't work on Qt 5.9 LTS.
Image {
source: "qrc:/res/arrow_left.svg"
anchors.centerIn: parent
sourceSize {
// The icon should be square so use the height as the width too
width: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
height: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
}
}
onClicked: stackView.pop()
@@ -217,75 +208,14 @@ ApplicationWindow {
}
}
// This label will appear when the window gets too small and
// we need to ensure the toolbar controls don't collide
Label {
id: titleRowLabel
font.pointSize: titleLabel.font.pointSize
id: titleLabel
text: stackView.currentItem.objectName
font.pointSize: 15
elide: Label.ElideRight
horizontalAlignment: Qt.AlignHCenter
verticalAlignment: Qt.AlignVCenter
Layout.fillWidth: true
// We need this label to always be visible so it can occupy
// the remaining space in the RowLayout. To "hide" it, we
// just set the text to empty string.
text: !titleLabel.visible ? stackView.currentItem.objectName : ""
}
Label {
id: versionLabel
visible: stackView.currentItem.objectName === qsTr("Settings")
text: qsTr("Version %1").arg(SystemProperties.versionString)
font.pointSize: 12
horizontalAlignment: Qt.AlignRight
verticalAlignment: Qt.AlignVCenter
}
NavigableToolButton {
id: discordButton
visible: SystemProperties.hasBrowser &&
stackView.currentItem.objectName === qsTr("Settings")
iconSource: "qrc:/res/Discord-Logo-White.svg"
ToolTip.delay: 1000
ToolTip.timeout: 3000
ToolTip.visible: hovered
ToolTip.text: qsTr("Join our community on Discord")
// TODO need to make sure browser is brought to foreground.
onClicked: Qt.openUrlExternally("https://moonlight-stream.org/discord");
Keys.onDownPressed: {
stackView.currentItem.forceActiveFocus(Qt.TabFocus)
}
}
NavigableToolButton {
id: addPcButton
visible: stackView.currentItem.objectName === qsTr("Computers")
iconSource: "qrc:/res/ic_add_to_queue_white_48px.svg"
ToolTip.delay: 1000
ToolTip.timeout: 3000
ToolTip.visible: hovered
ToolTip.text: qsTr("Add PC manually") + (newPcShortcut.nativeText ? (" ("+newPcShortcut.nativeText+")") : "")
Shortcut {
id: newPcShortcut
sequence: StandardKey.New
onActivated: addPcButton.clicked()
}
onClicked: {
addPcDialog.open()
}
Keys.onDownPressed: {
stackView.currentItem.forceActiveFocus(Qt.TabFocus)
}
}
NavigableToolButton {
@@ -293,25 +223,28 @@ ApplicationWindow {
id: updateButton
iconSource: "qrc:/res/update.svg"
Image {
source: "qrc:/res/update.svg"
anchors.centerIn: parent
sourceSize {
width: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
height: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
}
}
ToolTip.delay: 1000
ToolTip.timeout: 3000
ToolTip.visible: hovered || visible
ToolTip.visible: hovered
// Invisible until we get a callback notifying us that
// an update is available
visible: false
onClicked: {
if (SystemProperties.hasBrowser) {
Qt.openUrlExternally(browserUrl);
}
}
onClicked: Qt.openUrlExternally(browserUrl);
function updateAvailable(version, url)
{
ToolTip.text = qsTr("Update available for Moonlight: Version %1").arg(version)
ToolTip.text = "Update available for Moonlight: Version " + version
updateButton.browserUrl = url
updateButton.visible = true
}
@@ -328,14 +261,20 @@ ApplicationWindow {
NavigableToolButton {
id: helpButton
visible: SystemProperties.hasBrowser
iconSource: "qrc:/res/question_mark.svg"
Image {
source: "qrc:/res/question_mark.svg"
anchors.centerIn: parent
sourceSize {
width: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
height: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
}
}
ToolTip.delay: 1000
ToolTip.timeout: 3000
ToolTip.visible: hovered
ToolTip.text: qsTr("Help") + (helpShortcut.nativeText ? (" ("+helpShortcut.nativeText+")") : "")
ToolTip.text: "Help" + (helpShortcut.nativeText ? (" ("+helpShortcut.nativeText+")") : "")
Shortcut {
id: helpShortcut
@@ -358,11 +297,18 @@ ApplicationWindow {
ToolTip.delay: 1000
ToolTip.timeout: 3000
ToolTip.visible: hovered
ToolTip.text: qsTr("Gamepad Mapper")
ToolTip.text: "Gamepad Mapper"
iconSource: "qrc:/res/ic_videogame_asset_white_48px.svg"
Image {
source: "qrc:/res/ic_videogame_asset_white_48px.svg"
anchors.centerIn: parent
sourceSize {
width: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
height: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
}
}
onClicked: navigateTo("qrc:/gui/GamepadMapper.qml", qsTr("Gamepad Mapping"))
onClicked: navigateTo("qrc:/gui/GamepadMapper.qml", "Gamepad Mapping")
Keys.onDownPressed: {
stackView.currentItem.forceActiveFocus(Qt.TabFocus)
@@ -372,9 +318,16 @@ ApplicationWindow {
NavigableToolButton {
id: settingsButton
iconSource: "qrc:/res/settings.svg"
Image {
source: "qrc:/res/settings.svg"
anchors.centerIn: parent
sourceSize {
width: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
height: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
}
}
onClicked: navigateTo("qrc:/gui/SettingsView.qml", qsTr("Settings"))
onClicked: navigateTo("qrc:/gui/SettingsView.qml", "Settings")
Keys.onDownPressed: {
stackView.currentItem.forceActiveFocus(Qt.TabFocus)
@@ -389,113 +342,72 @@ ApplicationWindow {
ToolTip.delay: 1000
ToolTip.timeout: 3000
ToolTip.visible: hovered
ToolTip.text: qsTr("Settings") + (settingsShortcut.nativeText ? (" ("+settingsShortcut.nativeText+")") : "")
ToolTip.text: "Settings" + (settingsShortcut.nativeText ? (" ("+settingsShortcut.nativeText+")") : "")
}
}
}
ErrorMessageDialog {
MessageDialog {
id: noHwDecoderDialog
text: qsTr("No functioning hardware accelerated H.264 video decoder was detected by Moonlight. " +
"Your streaming performance may be severely degraded in this configuration.")
helpText: qsTr("Click the Help button for more information on solving this problem.")
helpUrl: "https://github.com/moonlight-stream/moonlight-docs/wiki/Fixing-Hardware-Decoding-Problems"
modality:Qt.WindowModal
icon: StandardIcon.Warning
standardButtons: StandardButton.Ok | StandardButton.Help
text: "No functioning hardware accelerated H.264 video decoder was detected by Moonlight. " +
"Your streaming performance may be severely degraded in this configuration. " +
"Click the Help button for more information on solving this problem."
onHelp: {
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Fixing-Hardware-Decoding-Problems");
}
}
ErrorMessageDialog {
id: xWaylandDialog
text: qsTr("Hardware acceleration doesn't work on XWayland. Continuing on XWayland may result in poor streaming performance. " +
"Try running with QT_QPA_PLATFORM=wayland or switch to X11.")
helpText: qsTr("Click the Help button for more information.")
helpUrl: "https://github.com/moonlight-stream/moonlight-docs/wiki/Fixing-Hardware-Decoding-Problems"
MessageDialog {
id: waylandDialog
modality:Qt.WindowModal
icon: StandardIcon.Warning
standardButtons: StandardButton.Ok | StandardButton.Help
text: "Moonlight does not support hardware acceleration on Wayland. Continuing on Wayland may result in poor streaming performance. " +
"Please switch to an X session for optimal performance."
onHelp: {
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Fixing-Hardware-Decoding-Problems");
}
}
NavigableMessageDialog {
MessageDialog {
id: wow64Dialog
standardButtons: Dialog.Ok | Dialog.Cancel
text: qsTr("This version of Moonlight isn't optimized for your PC. Please download the '%1' version of Moonlight for the best streaming performance.").arg(SystemProperties.friendlyNativeArchName)
modality:Qt.WindowModal
icon: StandardIcon.Warning
standardButtons: StandardButton.Ok | StandardButton.Cancel
text: "This PC is running a 64-bit version of Windows. Please download the x64 version of Moonlight for the best streaming performance."
onAccepted: {
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-qt/releases");
}
}
ErrorMessageDialog {
MessageDialog {
id: unmappedGamepadDialog
property string unmappedGamepads : ""
text: qsTr("Moonlight detected gamepads without a mapping:") + "\n" + unmappedGamepads
helpTextSeparator: "\n\n"
helpText: qsTr("Click the Help button for information on how to map your gamepads.")
helpUrl: "https://github.com/moonlight-stream/moonlight-docs/wiki/Gamepad-Mapping"
modality:Qt.WindowModal
icon: StandardIcon.Warning
standardButtons: StandardButton.Ok | StandardButton.Help
text: "Moonlight detected gamepads without a proper mapping. " +
"The following gamepads will not function until this is resolved: " + unmappedGamepads + "\n\n" +
"Click the Help button for information on how to map your gamepads."
onHelp: {
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Gamepad-Mapping");
}
}
// This dialog appears when quitting via keyboard or gamepad button
NavigableMessageDialog {
MessageDialog {
id: quitConfirmationDialog
standardButtons: Dialog.Yes | Dialog.No
text: qsTr("Are you sure you want to quit?")
modality:Qt.WindowModal
icon: StandardIcon.Warning
standardButtons: StandardButton.Yes | StandardButton.No
text: "Are you sure you want to quit?"
onYes: Qt.quit()
// For keyboard/gamepad navigation
onAccepted: Qt.quit()
}
// HACK: This belongs in StreamSegue but keeping a dialog around after the parent
// dies can trigger bugs in Qt 5.12 that cause the app to crash. For now, we will
// host this dialog in a QML component that is never destroyed.
//
// To repro: Start a stream, cut the network connection to trigger the "Connection
// terminated" dialog, wait until the app grid times out back to the PC grid, then
// try to dismiss the dialog.
ErrorMessageDialog {
id: streamSegueErrorDialog
property bool quitAfter: false
onClosed: {
if (quitAfter) {
Qt.quit()
}
// StreamSegue assumes its dialog will be re-created each time we
// start streaming, so fake it by wiping out the text each time.
text = ""
}
}
NavigableDialog {
id: addPcDialog
property string label: qsTr("Enter the IP address of your GameStream PC:")
standardButtons: Dialog.Ok | Dialog.Cancel
onOpened: {
// Force keyboard focus on the textbox so keyboard navigation works
editText.forceActiveFocus()
}
onClosed: {
editText.clear()
}
onAccepted: {
if (editText.text) {
ComputerManager.addNewHost(editText.text.trim(), false)
}
}
ColumnLayout {
Label {
text: addPcDialog.label
font.bold: true
}
TextField {
id: editText
Layout.fillWidth: true
focus: true
Keys.onReturnPressed: {
addPcDialog.accept()
}
}
}
}
}
+43 -76
View File
@@ -4,14 +4,13 @@
#include <QGuiApplication>
#include <QWindow>
#include "settings/streamingpreferences.h"
#include "settings/mappingmanager.h"
#define AXIS_NAVIGATION_REPEAT_DELAY 150
SdlGamepadKeyNavigation::SdlGamepadKeyNavigation()
: m_Enabled(false),
m_UiNavMode(false),
m_SettingsMode(false),
m_LastAxisNavigationEventTime(0)
{
m_PollingTimer = new QTimer(this);
@@ -89,16 +88,9 @@ void SdlGamepadKeyNavigation::disable()
void SdlGamepadKeyNavigation::onPollingTimerFired()
{
SDL_Event event;
StreamingPreferences prefs;
while (SDL_PollEvent(&event)) {
switch (event.type) {
case SDL_QUIT:
// SDL may send us a quit event since we initialize
// the video subsystem on startup. If we get one,
// forward it on for Qt to take care of.
QCoreApplication::instance()->quit();
break;
case SDL_CONTROLLERBUTTONDOWN:
case SDL_CONTROLLERBUTTONUP:
{
@@ -106,27 +98,9 @@ void SdlGamepadKeyNavigation::onPollingTimerFired()
event.type == SDL_CONTROLLERBUTTONDOWN ?
QEvent::Type::KeyPress : QEvent::Type::KeyRelease;
// Swap face buttons if needed
if (prefs.swapFaceButtons) {
switch (event.cbutton.button) {
case SDL_CONTROLLER_BUTTON_A:
event.cbutton.button = SDL_CONTROLLER_BUTTON_B;
break;
case SDL_CONTROLLER_BUTTON_B:
event.cbutton.button = SDL_CONTROLLER_BUTTON_A;
break;
case SDL_CONTROLLER_BUTTON_X:
event.cbutton.button = SDL_CONTROLLER_BUTTON_Y;
break;
case SDL_CONTROLLER_BUTTON_Y:
event.cbutton.button = SDL_CONTROLLER_BUTTON_X;
break;
}
}
switch (event.cbutton.button) {
case SDL_CONTROLLER_BUTTON_DPAD_UP:
if (m_UiNavMode) {
if (m_SettingsMode) {
// Back-tab
sendKey(type, Qt::Key_Tab, Qt::ShiftModifier);
}
@@ -135,7 +109,7 @@ void SdlGamepadKeyNavigation::onPollingTimerFired()
}
break;
case SDL_CONTROLLER_BUTTON_DPAD_DOWN:
if (m_UiNavMode) {
if (m_SettingsMode) {
sendKey(type, Qt::Key_Tab);
}
else {
@@ -144,12 +118,24 @@ void SdlGamepadKeyNavigation::onPollingTimerFired()
break;
case SDL_CONTROLLER_BUTTON_DPAD_LEFT:
sendKey(type, Qt::Key_Left);
if (m_SettingsMode) {
// Some settings controls respond to left/right (like the slider)
// and others respond to up/down (like combo boxes). They seem to
// be mutually exclusive though so let's just send both.
sendKey(type, Qt::Key_Up);
}
break;
case SDL_CONTROLLER_BUTTON_DPAD_RIGHT:
sendKey(type, Qt::Key_Right);
if (m_SettingsMode) {
// Some settings controls respond to left/right (like the slider)
// and others respond to up/down (like combo boxes). They seem to
// be mutually exclusive though so let's just send both.
sendKey(type, Qt::Key_Down);
}
break;
case SDL_CONTROLLER_BUTTON_A:
if (m_UiNavMode) {
if (m_SettingsMode) {
sendKey(type, Qt::Key_Space);
}
else {
@@ -191,7 +177,7 @@ void SdlGamepadKeyNavigation::onPollingTimerFired()
// Do nothing
}
else if (leftY < -30000) {
if (m_UiNavMode) {
if (m_SettingsMode) {
// Back-tab
sendKey(QEvent::Type::KeyPress, Qt::Key_Tab, Qt::ShiftModifier);
sendKey(QEvent::Type::KeyRelease, Qt::Key_Tab, Qt::ShiftModifier);
@@ -204,7 +190,7 @@ void SdlGamepadKeyNavigation::onPollingTimerFired()
m_LastAxisNavigationEventTime = SDL_GetTicks();
}
else if (leftY > 30000) {
if (m_UiNavMode) {
if (m_SettingsMode) {
sendKey(QEvent::Type::KeyPress, Qt::Key_Tab);
sendKey(QEvent::Type::KeyRelease, Qt::Key_Tab);
}
@@ -218,38 +204,29 @@ void SdlGamepadKeyNavigation::onPollingTimerFired()
else if (leftX < -30000) {
sendKey(QEvent::Type::KeyPress, Qt::Key_Left);
sendKey(QEvent::Type::KeyRelease, Qt::Key_Left);
if (m_SettingsMode) {
// Some settings controls respond to left/right (like the slider)
// and others respond to up/down (like combo boxes). They seem to
// be mutually exclusive though so let's just send both.
sendKey(QEvent::Type::KeyPress, Qt::Key_Up);
sendKey(QEvent::Type::KeyRelease, Qt::Key_Up);
}
m_LastAxisNavigationEventTime = SDL_GetTicks();
}
else if (leftX > 30000) {
sendKey(QEvent::Type::KeyPress, Qt::Key_Right);
sendKey(QEvent::Type::KeyRelease, Qt::Key_Right);
if (m_SettingsMode) {
// Some settings controls respond to left/right (like the slider)
// and others respond to up/down (like combo boxes). They seem to
// be mutually exclusive though so let's just send both.
sendKey(QEvent::Type::KeyPress, Qt::Key_Down);
sendKey(QEvent::Type::KeyRelease, Qt::Key_Down);
}
m_LastAxisNavigationEventTime = SDL_GetTicks();
}
// In UI navigation mode (settings page), use the right stick to scroll
if (m_UiNavMode) {
short rightX = SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_RIGHTX);
short rightY = SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_RIGHTY);
QPoint wheelDelta;
if (rightX > 30000) {
wheelDelta.setX(30);
}
else if (rightX < -30000) {
wheelDelta.setX(-30);
}
if (rightY > 30000) {
wheelDelta.setY(-30);
}
else if (rightY < -30000) {
wheelDelta.setY(30);
}
if (!wheelDelta.isNull()) {
sendWheel(wheelDelta);
}
}
}
}
@@ -263,30 +240,19 @@ void SdlGamepadKeyNavigation::sendKey(QEvent::Type type, Qt::Key key, Qt::Keyboa
}
}
void SdlGamepadKeyNavigation::sendWheel(QPoint& angleDelta)
void SdlGamepadKeyNavigation::setSettingsMode(bool settingsMode)
{
QGuiApplication* app = static_cast<QGuiApplication*>(QGuiApplication::instance());
QWindow* focusWindow = app->focusWindow();
if (focusWindow != nullptr) {
QPoint mousePos(focusWindow->width() / 2, focusWindow->height() / 2);
QPoint globalPos(focusWindow->mapToGlobal(mousePos));
#if QT_VERSION >= QT_VERSION_CHECK(5, 12, 0)
QWheelEvent wheelEvent(mousePos, globalPos, QPoint(), angleDelta, Qt::NoButton, Qt::NoModifier, Qt::NoScrollPhase, false, Qt::MouseEventSynthesizedByApplication);
#else
QWheelEvent wheelEvent(mousePos, globalPos, QPoint(), angleDelta, angleDelta.y(), Qt::Vertical, Qt::NoButton, Qt::NoModifier, Qt::NoScrollPhase, Qt::MouseEventSynthesizedByApplication, false);
#endif
app->sendEvent(focusWindow, &wheelEvent);
}
}
void SdlGamepadKeyNavigation::setUiNavMode(bool uiNavMode)
{
m_UiNavMode = uiNavMode;
m_SettingsMode = settingsMode;
}
int SdlGamepadKeyNavigation::getConnectedGamepads()
{
Q_ASSERT(m_Enabled);
if (SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) failed: %s",
SDL_GetError());
return 0;
}
int count = 0;
for (int i = 0; i < SDL_NumJoysticks(); i++) {
@@ -295,5 +261,6 @@ int SdlGamepadKeyNavigation::getConnectedGamepads()
}
}
SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER);
return count;
}
+2 -3
View File
@@ -18,13 +18,12 @@ public:
Q_INVOKABLE void disable();
Q_INVOKABLE void setUiNavMode(bool settingsMode);
Q_INVOKABLE void setSettingsMode(bool settingsMode);
Q_INVOKABLE int getConnectedGamepads();
private:
void sendKey(QEvent::Type type, Qt::Key key, Qt::KeyboardModifiers modifiers = Qt::NoModifier);
void sendWheel(QPoint& angleDelta);
private slots:
void onPollingTimerFired();
@@ -33,6 +32,6 @@ private:
QTimer* m_PollingTimer;
QList<SDL_GameController*> m_Gamepads;
bool m_Enabled;
bool m_UiNavMode;
bool m_SettingsMode;
Uint32 m_LastAxisNavigationEventTime;
};
Binary file not shown.
-824
View File
@@ -1,824 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1" language="fr_FR">
<context>
<name>AppView</name>
<message>
<location filename="../gui/AppView.qml" line="118"/>
<location filename="../gui/AppView.qml" line="242"/>
<source>Resume Game</source>
<translation>Reprendre le jeu</translation>
</message>
<message>
<location filename="../gui/AppView.qml" line="146"/>
<location filename="../gui/AppView.qml" line="247"/>
<source>Quit Game</source>
<translation>Quitter le jeu</translation>
</message>
<message>
<location filename="../gui/AppView.qml" line="242"/>
<source>Launch Game</source>
<translation>Lancer le jeu</translation>
</message>
<message>
<location filename="../gui/AppView.qml" line="255"/>
<source>Hide Game</source>
<translation>Cacher le jeu</translation>
</message>
<message>
<location filename="../gui/AppView.qml" line="268"/>
<source>Are you sure you want to quit %1? Any unsaved progress will be lost.</source>
<translation>Vous êtes sûr de vouloir arrêter %1? Tout progrès non enregistré sera perdu.</translation>
</message>
</context>
<context>
<name>CliQuitStreamSegue</name>
<message>
<location filename="../gui/CliQuitStreamSegue.qml" line="9"/>
<source>Establishing connection to PC...</source>
<translation>connexion au PC...</translation>
</message>
<message>
<location filename="../gui/CliQuitStreamSegue.qml" line="13"/>
<source>Quitting app...</source>
<translation>Fermeture de l&apos;app...</translation>
</message>
</context>
<context>
<name>CliStartStreamSegue</name>
<message>
<location filename="../gui/CliStartStreamSegue.qml" line="9"/>
<source>Establishing connection to PC...</source>
<translation>connexion au PC...</translation>
</message>
<message>
<location filename="../gui/CliStartStreamSegue.qml" line="13"/>
<source>Loading app list...</source>
<translation>Chargement de la liste des applications...</translation>
</message>
<message>
<location filename="../gui/CliStartStreamSegue.qml" line="81"/>
<source>Are you sure you want to quit %1? Any unsaved progress will be lost.</source>
<translation>Vous êtes sûr de vouloir arrêter %1? Tout progrès non enregistré sera perdu.</translation>
</message>
</context>
<context>
<name>GamepadMapper</name>
<message>
<location filename="../gui/GamepadMapper.qml" line="4"/>
<source>Gamepad Mapping</source>
<translation>Configuration du controleur</translation>
</message>
</context>
<context>
<name>NvHTTP</name>
<message>
<location filename="../backend/nvhttp.cpp" line="326"/>
<source>Missing audio capture device. Reinstalling GeForce Experience should resolve this error.</source>
<translation>Dispositif de capture audio manquant. La réinstallation de GeForce Experience devrait résoudre cette erreur.</translation>
</message>
</context>
<context>
<name>PcView</name>
<message>
<location filename="../gui/PcView.qml" line="20"/>
<source>Computers</source>
<translation>Ordinateurs</translation>
</message>
<message>
<location filename="../gui/PcView.qml" line="67"/>
<source>Unable to connect to the specified PC.</source>
<translation>Impossible de se connecter au PC spécifié.</translation>
</message>
<message>
<location filename="../gui/PcView.qml" line="70"/>
<source>This PC&apos;s Internet connection is blocking Moonlight. Streaming over the Internet may not work while connected to this network.</source>
<translation>La connexion Internet de ce PC bloque Moonlight. Le streaming sur Internet peut ne pas fonctionner lorsqu&apos;il est connecté à ce réseau.</translation>
</message>
<message>
<location filename="../gui/PcView.qml" line="73"/>
<source>Click the Help button for possible solutions.</source>
<translation>Cliquez sur le bouton Aide pour connaître les solutions possibles.</translation>
</message>
<message>
<source>Searching for PCs with NVIDIA GameStream enabled...</source>
<translation type="vanished">La recherche de PC avec NVIDIA GameStream activé...</translation>
</message>
<message>
<location filename="../gui/PcView.qml" line="103"/>
<source>Automatic PC discovery is disabled. Add your PC manually.</source>
<translation>La découverte automatique des PC est désactivée. Ajoutez votre PC manuellement.</translation>
</message>
<message>
<location filename="../gui/PcView.qml" line="167"/>
<source>View Apps</source>
<translation>Voir les applications</translation>
</message>
<message>
<location filename="../gui/PcView.qml" line="177"/>
<source>View Hidden Apps</source>
<translation>Voir les applications cachées</translation>
</message>
<message>
<location filename="../gui/PcView.qml" line="187"/>
<source>Wake PC</source>
<translation>Réveiller le PC</translation>
</message>
<message>
<location filename="../gui/PcView.qml" line="193"/>
<source>Test Network</source>
<translation>Test Réseau</translation>
</message>
<message>
<location filename="../gui/PcView.qml" line="202"/>
<source>Rename PC</source>
<translation>Renommer le PC</translation>
</message>
<message>
<location filename="../gui/PcView.qml" line="211"/>
<source>Delete PC</source>
<translation>Supprimer le PC</translation>
</message>
<message>
<location filename="../gui/PcView.qml" line="241"/>
<source>You cannot pair while a previous session is still running on the host PC. Quit any running games or reboot the host PC, then try pairing again.</source>
<translation>Vous ne pouvez pas effectuer de couplage alors qu&apos;une session précédente est encore en cours sur le PC hôte. Fermez les jeux en cours ou redémarrez le PC hôte, puis essayez à nouveau d&apos;effectuer l&apos;appariement.</translation>
</message>
<message>
<location filename="../gui/PcView.qml" line="296"/>
<source>Please enter %1 on your GameStream PC. This dialog will close when pairing is completed.</source>
<translation>Veuillez saisir %1 sur votre PC GameStream. Cette boîte de dialogue se fermera lorsque l&apos;appariement sera terminé.</translation>
</message>
<message>
<location filename="../gui/PcView.qml" line="307"/>
<source>Are you sure you want to remove this PC?</source>
<translation>Êtes-vous sûr de vouloir supprimer ce PC ?</translation>
</message>
<message>
<location filename="../gui/PcView.qml" line="324"/>
<source>Moonlight is testing your network connection to determine if NVIDIA GameStream is blocked.</source>
<translation>Moonlight teste votre connexion réseau pour déterminer si NVIDIA GameStream est bloqué.</translation>
</message>
<message>
<location filename="../gui/PcView.qml" line="324"/>
<source>This may take a few seconds…</source>
<translation>Cela peut prendre quelques secondes...</translation>
</message>
<message>
<location filename="../gui/PcView.qml" line="335"/>
<source>This network does not appear to be blocking Moonlight. If you still have trouble connecting, check your PC&apos;s firewall settings.</source>
<translation>Ce réseau ne semble pas bloquer Moonlight. Si vous avez toujours des problèmes de connexion, vérifiez les paramètres du pare-feu de votre PC.</translation>
</message>
<message>
<location filename="../gui/PcView.qml" line="335"/>
<source>If you are trying to stream over the Internet, install the Moonlight Internet Hosting Tool on your gaming PC and run the included Internet Streaming Tester to check your gaming PC&apos;s Internet connection.</source>
<translation>Si vous essayez d&apos;utiliser la diffusion en continu sur Internet, installez l&apos;outil d&apos;hébergement Internet Moonlight sur votre PC de jeu et exécutez le testeur de diffusion en continu sur Internet inclus pour vérifier la connexion Internet de votre PC de jeu.</translation>
</message>
<message>
<location filename="../gui/PcView.qml" line="339"/>
<source>Your PC&apos;s current network connection seems to be blocking Moonlight. Streaming over the Internet may not work while connected to this network.</source>
<translation>La connexion réseau actuelle de votre PC semble bloquer le Moonlight. Il se peut que la diffusion en continu sur Internet ne fonctionne pas lorsque vous êtes connecté à ce réseau.</translation>
</message>
<message>
<location filename="../gui/PcView.qml" line="339"/>
<source>The following network ports were blocked:</source>
<translation>Les ports de réseau suivants ont été bloqués :</translation>
</message>
<message>
<location filename="../gui/PcView.qml" line="331"/>
<source>The network test could not be performed because none of Moonlight&apos;s connection testing servers were reachable from this PC. Check your Internet connection or try again later.</source>
<translation>Le test de réseau n&apos;a pas pu être effectué car aucun des serveurs de test de connexion de Moonlight n&apos;était accessible depuis ce PC. Vérifiez votre connexion Internet ou réessayez plus tard.</translation>
</message>
<message>
<location filename="../gui/PcView.qml" line="102"/>
<source>Searching for PCs on your local network with NVIDIA GameStream enabled...</source>
<translation>Recherche de PC sur votre réseau local avec NVIDIA GameStream activé...</translation>
</message>
<message>
<location filename="../gui/PcView.qml" line="351"/>
<source>Enter the new name for this PC:</source>
<translation>Saisissez le nouveau nom de ce PC :</translation>
</message>
</context>
<context>
<name>PendingQuitTask</name>
<message>
<location filename="../backend/computermanager.cpp" line="539"/>
<source>The running game wasn&apos;t started by this PC. You must quit the game on the host PC manually or use the device that originally started the game.</source>
<translation>Le jeu en cours d’exécution n&apos;a pas été démarré par ce PC. Vous devez fermer le jeu sur le PC hôte manuellement ou utiliser l&apos;appareil qui a démarré le jeu à l&apos;origine.</translation>
</message>
</context>
<context>
<name>QObject</name>
<message>
<location filename="../cli/quitstream.cpp" line="76"/>
<location filename="../cli/startstream.cpp" line="133"/>
<source>Failed to connect to %1</source>
<translation>N&apos;a pas pu se connecter à %1</translation>
</message>
<message>
<location filename="../cli/quitstream.cpp" line="88"/>
<location filename="../cli/startstream.cpp" line="89"/>
<source>Computer %1 has not been paired. Please open Moonlight to pair before streaming.</source>
<translation>Le %1 de l&apos;ordinateur n&apos;a pas été jumelé. Veuillez ouvrir Moonlight pour jumeler avant le streaming.</translation>
</message>
<message>
<location filename="../cli/quitstream.cpp" line="102"/>
<location filename="../cli/startstream.cpp" line="126"/>
<source>Quitting app failed, reason: %1</source>
<translation>La fermeture de l&apos;application a échoué, raison : %1</translation>
</message>
<message>
<location filename="../cli/startstream.cpp" line="137"/>
<source>Failed to find application %1</source>
<translation>N&apos;a pas trouvé l&apos;application %1</translation>
</message>
</context>
<context>
<name>QuitSegue</name>
<message>
<location filename="../gui/QuitSegue.qml" line="12"/>
<source>Quitting %1...</source>
<translation>Fermeture %1...</translation>
</message>
</context>
<context>
<name>Session</name>
<message>
<location filename="../streaming/session.cpp" line="97"/>
<source>No video received from host. Check the host PC&apos;s firewall and port forwarding rules.</source>
<translation>Aucune vidéo reçue de l&apos;hôte. Vérifiez le pare-feu et les règles de transfert de port du PC hôte.</translation>
</message>
<message>
<location filename="../streaming/session.cpp" line="102"/>
<source>Your network connection isn&apos;t performing well. Reduce your video bitrate setting or try a faster connection.</source>
<translation>Votre connexion réseau ne fonctionne pas bien. Réduisez votre débit vidéo ou essayez une connexion plus rapide.</translation>
</message>
<message>
<location filename="../streaming/session.cpp" line="107"/>
<source>Connection terminated</source>
<translation>Connexion terminée</translation>
</message>
<message>
<location filename="../streaming/session.cpp" line="567"/>
<source>Your selection to enable remote desktop mouse mode may cause problems in games.</source>
<translation>Votre sélection pour activer le mode de souris de bureau à distance peut causer des problèmes dans les jeux.</translation>
</message>
<message>
<location filename="../streaming/session.cpp" line="572"/>
<source>HDR is not supported with software decoding.</source>
<translation>Le HDR n&apos;est pas pris en charge par le décodage logiciel.</translation>
</message>
<message>
<location filename="../streaming/session.cpp" line="576"/>
<source>Your settings selection to force software decoding may cause poor streaming performance.</source>
<translation>La sélection de vos paramètres pour forcer le décodage du logiciel peut entraîner de mauvaises performances de streaming.</translation>
</message>
<message>
<location filename="../streaming/session.cpp" line="581"/>
<source>Using unsupported FPS options may cause stuttering or lag.</source>
<translation>L&apos;utilisation d&apos;options de FPS non prises en charge peut entraîner un stuttering ou des lag.</translation>
</message>
<message>
<location filename="../streaming/session.cpp" line="584"/>
<source>V-sync will be disabled when streaming at a higher frame rate than the display.</source>
<translation>La synchronisation vertical sera désactivée lors de la diffusion en continu à une fréquence d&apos;images supérieure à celle de l&apos;affichage.</translation>
</message>
<message>
<location filename="../streaming/session.cpp" line="601"/>
<source>Using software decoding due to your selection to force HEVC without GPU support. This may cause poor streaming performance.</source>
<translation>Utilisation du décodage logiciel dû à votre sélection pour forcer les HEVC sans support GPU. Cela peut entraîner de mauvaises performances de streaming.</translation>
</message>
<message>
<location filename="../streaming/session.cpp" line="604"/>
<source>This PC&apos;s GPU doesn&apos;t support HEVC decoding.</source>
<translation>Le GPU de ce PC ne prend pas en charge le décodage HEVC.</translation>
</message>
<message>
<location filename="../streaming/session.cpp" line="611"/>
<source>Your host PC GPU doesn&apos;t support HEVC. A GeForce GTX 900-series (Maxwell) or later GPU is required for HEVC streaming.</source>
<translation>Le GPU de votre PC hôte ne prend pas en charge les HEVC. Un GPU GeForce GTX série 900 (Maxwell) ou plus récent est nécessaire pour le streaming HEVC.</translation>
</message>
<message>
<location filename="../streaming/session.cpp" line="628"/>
<source>%1 doesn&apos;t support HDR10.</source>
<translation>%1 ne prend pas en charge le HDR10.</translation>
</message>
<message>
<location filename="../streaming/session.cpp" line="632"/>
<source>Your host PC GPU doesn&apos;t support HDR streaming. A GeForce GTX 1000-series (Pascal) or later GPU is required for HDR streaming.</source>
<translation>Le GPU de votre PC hôte ne prend pas en charge le streaming HDR. Un GPU GeForce GTX série 1000 (Pascal) ou plus récent est nécessaire pour la diffusion HDR.</translation>
</message>
<message>
<location filename="../streaming/session.cpp" line="641"/>
<source>This PC&apos;s GPU doesn&apos;t support HEVC Main10 decoding for HDR streaming.</source>
<translation>Le GPU de ce PC ne prend pas en charge le décodage HEVC pour le streaming HDR.</translation>
</message>
<message>
<location filename="../streaming/session.cpp" line="654"/>
<source>GeForce Experience 3.0 or higher is required for 4K streaming.</source>
<translation>Une expérience GeForce 3.0 ou supérieure est requise pour le streaming 4K.</translation>
</message>
<message>
<location filename="../streaming/session.cpp" line="669"/>
<source>Your selected surround sound setting is not supported by the current audio device.</source>
<translation>Le réglage du son surround que vous avez sélectionné n&apos;est pas pris en charge par l&apos;appareil audio actuel.</translation>
</message>
<message>
<location filename="../streaming/session.cpp" line="676"/>
<source>Failed to open audio device. Audio will be unavailable during this session.</source>
<translation>Echec de l&apos;ouverture de l&apos;appareil audio. L&apos;audio ne sera pas disponible pendant cette session.</translation>
</message>
<message>
<location filename="../streaming/session.cpp" line="681"/>
<source>An attached gamepad has no mapping and won&apos;t be usable. Visit the Moonlight help to resolve this.</source>
<translation>Une manette de jeun n&apos;a pas était configurée et ne sera pas utilisable. Visitez le Moonlight pour aider à résoudre ce problème.</translation>
</message>
<message>
<location filename="../streaming/session.cpp" line="687"/>
<source>Your host PC&apos;s GPU doesn&apos;t support streaming video resolutions over 4K.</source>
<translation>Le GPU de votre PC hôte ne prend pas en charge les résolutions vidéo en continu supérieures à 4K.</translation>
</message>
<message>
<location filename="../streaming/session.cpp" line="691"/>
<source>Video resolutions over 4K are only supported by the HEVC codec.</source>
<translation>Les résolutions vidéo supérieures à 4K sont uniquement prises en charge par le codec HEVC.</translation>
</message>
<message>
<location filename="../streaming/session.cpp" line="705"/>
<source>Your selection to force hardware decoding cannot be satisfied due to missing hardware decoding support on this PC&apos;s GPU.</source>
<translation>Votre choix pour forcer le décodage matériel ne peut être satisfait en raison de l&apos;absence de support de décodage matériel sur le GPU de ce PC.</translation>
</message>
<message>
<location filename="../streaming/session.cpp" line="708"/>
<source>Your codec selection and force hardware decoding setting are not compatible. This PC&apos;s GPU lacks support for decoding your chosen codec.</source>
<translation>Votre sélection de codecs et le réglage du décodage matériel ne sont pas compatibles. Le GPU de ce PC ne prend pas en charge le décodage du codec que vous avez choisi.</translation>
</message>
<message>
<location filename="../streaming/session.cpp" line="1016"/>
<source>GeForce Experience returned error: %1</source>
<translation>GeForce Experience a renvoyé l&apos;erreur : %1</translation>
</message>
<message>
<location filename="../streaming/session.cpp" line="1397"/>
<source>Unable to initialize video decoder. Please check your streaming settings and try again.</source>
<translation>Impossible d&apos;initialiser le décodeur vidéo. Veuillez vérifier vos paramètres de diffusion en continu et réessayer.</translation>
</message>
</context>
<context>
<name>SettingsView</name>
<message>
<location filename="../gui/SettingsView.qml" line="11"/>
<source>Settings</source>
<translation>Paramètres</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="63"/>
<source>Resolution and FPS</source>
<translation>Résolution et FPS</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="71"/>
<source>Setting values too high for your PC or network connection may cause lag, stuttering, or errors.</source>
<translation>Le fait de fixer des valeurs trop élevées pour votre PC ou votre connexion réseau peut entraîner un décalage, des bégaiements ou des erreurs.</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="53"/>
<source>Basic Settings</source>
<translation>Paramètres de base</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="173"/>
<source>720p</source>
<translation>720p</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="178"/>
<source>1080p</source>
<translation>1080p</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="183"/>
<source>1440p</source>
<translation>1440p</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="188"/>
<source>4K</source>
<translation>4K</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="218"/>
<location filename="../gui/SettingsView.qml" line="219"/>
<location filename="../gui/SettingsView.qml" line="239"/>
<location filename="../gui/SettingsView.qml" line="242"/>
<source>%1 FPS</source>
<translation>%1 FPS</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="224"/>
<location filename="../gui/SettingsView.qml" line="227"/>
<location filename="../gui/SettingsView.qml" line="236"/>
<location filename="../gui/SettingsView.qml" line="249"/>
<location filename="../gui/SettingsView.qml" line="252"/>
<source>%1 FPS (Unsupported)</source>
<translation>%1 FPS (Non supporté)</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="305"/>
<source>Video bitrate:</source>
<translation>Bitrate vidéo :</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="313"/>
<source>Lower the bitrate on slower connections. Raise the bitrate to increase image quality.</source>
<translation>Baissez le débit binaire sur les connexions plus lentes. Augmentez le débit binaire pour améliorer la qualité de l&apos;image.</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="331"/>
<source>Video bitrate: %1 Mbps</source>
<translation>Bitrate vidéo : %1 Mbps</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="339"/>
<source>Display mode</source>
<translation>Mode d&apos;affichage</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="351"/>
<source> (Recommended)</source>
<translation> (Recommandé)</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="380"/>
<source>Full-screen</source>
<translation>Plein écran</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="384"/>
<source>Borderless windowed</source>
<translation>fenêtré sans bordure</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="388"/>
<source>Windowed</source>
<translation>Fenêtré</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="399"/>
<source>Full-screen generally provides the best performance, but borderless windowed may work better with features like macOS Spaces, Alt+Tab, screenshot tools, on-screen overlays, etc.</source>
<translation>Le plein écran offre généralement les meilleures performances, mais les fenêtres sans bordure peuvent fonctionner mieux avec des fonctionnalités telles que MacOS Spaces, Alt+Tab, les outils de capture d&apos;écran, les superpositions à l&apos;écran, etc.</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="406"/>
<source>V-Sync</source>
<translation>synchronisation verticale</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="416"/>
<source>Disabling V-Sync allows sub-frame rendering latency, but it can display visible tearing</source>
<translation>La désactivation de V-Sync permet une latence de rendu des sous-images plus faible, mais elle peut afficher des déchirures visibles</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="423"/>
<source>Frame pacing</source>
<translation>Frame pacing</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="433"/>
<source>Frame pacing reduces micro-stutter by delaying frames that come in too early</source>
<translation>Frame pacing réduit les micro-bégaiements en retardant les images qui arrivent trop tôt</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="443"/>
<source>Audio Settings</source>
<translation>Réglages audio</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="478"/>
<source>Stereo</source>
<translation>Stereo</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="482"/>
<source>5.1 surround sound</source>
<translation>5.1 son surround</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="486"/>
<source>7.1 surround sound</source>
<translation>7.1 son surround</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="503"/>
<source>UI Settings</source>
<translation>Réglages de l&apos;interface utilisateur</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="565"/>
<source>Input Settings</source>
<translation>Paramètres d&apos;entrée</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="585"/>
<source>Forces a single gamepad to always stay connected to the host, even if no gamepads are actually connected to this PC.</source>
<translation>Forces a single gamepad to always stay connected to the host, even if no gamepads are actually connected to this PC.</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="604"/>
<source>This enables mouse control without capturing the client&apos;s mouse cursor. It will not work in most games.</source>
<translation>Cela permet de contrôler la souris sans avoir à capturer le curseur du client. Cela ne fonctionne pas dans la plupart des jeux.</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="605"/>
<source>You can toggle this while streaming using Ctrl+Alt+Shift+M.</source>
<translation>Vous pouvez le basculer pendant la diffusion en utilisant Ctrl+Alt+Shift+M.</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="665"/>
<source>Host Settings</source>
<translation>Paramètres de l&apos;hôte</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="716"/>
<source>Advanced Settings</source>
<translation>Paramètres avancés</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="453"/>
<source>Audio configuration</source>
<translation>Configuration audio</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="513"/>
<source>Maximize Moonlight window on startup</source>
<translation>Maximiser la fenêtre Moonlight au démarrage</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="525"/>
<source>Show connection quality warnings</source>
<translation>Afficher les avertissements sur la qualité de la connexion</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="537"/>
<source>Discord Rich Presence integration</source>
<translation>Intégration de la présence Discord Rich</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="547"/>
<source>Updates your Discord status to display the name of the game you&apos;re streaming.</source>
<translation>Met à jour votre statut Discord pour afficher le nom du jeu que vous diffusez en continu.</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="575"/>
<source>Force gamepad #1 always present</source>
<translation>Forcé la manette de jeu Force #1 à être toujours présente</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="586"/>
<source>Only enable this option when streaming a game that doesn&apos;t support gamepads being connected after startup.</source>
<translation>N&apos;activez cette option que lorsque vous diffusez en continu un jeu qui ne prend pas en charge la connexion des manettes de jeu après le démarrage.</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="593"/>
<source>Optimize mouse for remote desktop instead of games</source>
<translation>Optimiser la souris pour le bureau à distance plutôt que pour les jeux</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="612"/>
<source>Use touchscreen as a trackpad</source>
<translation>Utiliser l&apos;écran tactile comme trackpad</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="622"/>
<source>When checked, the touchscreen acts like a trackpad. When unchecked, the touchscreen will directly control the mouse pointer.</source>
<translation>Lorsqu&apos;on le vérifie, l&apos;écran tactile agit comme un pavé tactile. Lorsqu&apos;il n&apos;est pas coché, l&apos;écran tactile contrôle directement le pointeur de la souris.</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="629"/>
<source>Gamepad mouse mode support</source>
<translation>Prise en charge du mode souris de la manette de jeu</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="639"/>
<source>When enabled, holding the Start button will toggle mouse mode</source>
<translation>Lorsqu&apos;il est activé, le maintien du bouton &quot;Start&quot; permet de passer en mode souris</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="646"/>
<source>Swap mouse buttons</source>
<translation>Échanger les boutons de la souris</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="656"/>
<source>When checked, swap the left and right mouse buttons</source>
<translation>Lorsque la case est cochée, permuter les boutons gauche et droit de la souris</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="675"/>
<source>Optimize game settings for streaming</source>
<translation>Optimiser les paramètres du jeu pour la diffusion en continu</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="686"/>
<source>Play audio on host PC</source>
<translation>Lecture audio sur le PC hôte</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="697"/>
<source>Quit app on host PC after ending stream</source>
<translation>Quitter l&apos;application sur le PC hôte après avoir terminé le flux</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="707"/>
<source>This will close the app or game you are streaming when you end your stream. You will lose any unsaved progress!</source>
<translation>Cela fermera l&apos;application ou le jeu que vous êtes en train de diffuser lorsque vous aurez terminé votre flux. Vous perdrez tout progrès non sauvegardé !</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="726"/>
<source>Video decoder</source>
<translation>Décodeur vidéo</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="751"/>
<location filename="../gui/SettingsView.qml" line="797"/>
<source>Automatic (Recommended)</source>
<translation>Automatique (recommandé)</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="755"/>
<source>Force software decoding</source>
<translation>Forcer le décodage des logiciels</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="759"/>
<source>Force hardware decoding</source>
<translation>Forcer le décodage du matériel</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="772"/>
<source>Video codec</source>
<translation>Codec vidéo</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="801"/>
<source>H.264</source>
<translation>H.264</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="805"/>
<source>HEVC (H.265)</source>
<translation>HEVC (H.265)</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="822"/>
<source>Unlock unsupported FPS options</source>
<translation>Débloquer les options non supportées du FPS</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="841"/>
<source>Automatically find PCs on the local network (Recommended)</source>
<translation>Trouver automatiquement les PC sur le réseau local (recommandé)</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="866"/>
<source>Automatically detect blocked connections (Recommended)</source>
<translation>Détecter automatiquement les connexions bloquées (recommandé)</translation>
</message>
</context>
<context>
<name>StreamSegue</name>
<message>
<location filename="../gui/StreamSegue.qml" line="11"/>
<location filename="../gui/StreamSegue.qml" line="17"/>
<source>Starting %1...</source>
<translation>Démarrage de %1...</translation>
</message>
<message>
<location filename="../gui/StreamSegue.qml" line="23"/>
<source>Starting %1 failed: Error %2</source>
<translation>Démarrage de %1... echec: erreur %2</translation>
</message>
<message>
<location filename="../gui/StreamSegue.qml" line="68"/>
<source>This PC&apos;s Internet connection is blocking Moonlight. Streaming over the Internet may not work while connected to this network.</source>
<translation>La connexion Internet de ce PC bloque Moonlight. Le streaming sur Internet peut ne pas fonctionner lorsqu&apos;il est connecté à ce réseau.</translation>
</message>
<message>
<location filename="../gui/StreamSegue.qml" line="150"/>
<source>Tip:</source>
<translation>Aide:</translation>
</message>
<message>
<location filename="../gui/StreamSegue.qml" line="150"/>
<source>Press %1 to disconnect your session</source>
<translation>Appuyez sur %1 pour déconnecter votre session</translation>
</message>
<message>
<location filename="../gui/StreamSegue.qml" line="151"/>
<source>Start+Select+L1+R1</source>
<translation>Start+Select+L1+R1</translation>
</message>
<message>
<location filename="../gui/StreamSegue.qml" line="151"/>
<source>Ctrl+Alt+Shift+Q</source>
<translation>Ctrl + Alt + Shift + Q</translation>
</message>
</context>
<context>
<name>main</name>
<message>
<location filename="../gui/main.qml" line="226"/>
<location filename="../gui/main.qml" line="236"/>
<location filename="../gui/main.qml" line="365"/>
<location filename="../gui/main.qml" line="380"/>
<source>Settings</source>
<translation>Paramètres</translation>
</message>
<message>
<location filename="../gui/main.qml" line="227"/>
<source>Version %1</source>
<translation>Version %1</translation>
</message>
<message>
<location filename="../gui/main.qml" line="243"/>
<source>Join our community on Discord</source>
<translation>Rejoignez notre communauté sur Discord</translation>
</message>
<message>
<location filename="../gui/main.qml" line="255"/>
<source>Computers</source>
<translation>Ordinateurs</translation>
</message>
<message>
<location filename="../gui/main.qml" line="262"/>
<source>Add PC manually</source>
<translation>Ajouter un PC manuellement</translation>
</message>
<message>
<location filename="../gui/main.qml" line="302"/>
<source>Update available for Moonlight: Version %1</source>
<translation>Mise à jour disponible pour Moonlight : Version %1</translation>
</message>
<message>
<location filename="../gui/main.qml" line="349"/>
<source>Gamepad Mapper</source>
<translation>configuration du controleur</translation>
</message>
<message>
<location filename="../gui/main.qml" line="353"/>
<source>Gamepad Mapping</source>
<translation>configuration du controleur</translation>
</message>
<message>
<location filename="../gui/main.qml" line="387"/>
<source>No functioning hardware accelerated H.264 video decoder was detected by Moonlight. Your streaming performance may be severely degraded in this configuration.</source>
<translation>Aucun décodeur vidéo H.264 accéléré par le matériel n&apos;a été détecté par Moonlight. Dans cette configuration, les performances de votre streaming peuvent être fortement dégradées.</translation>
</message>
<message>
<location filename="../gui/main.qml" line="395"/>
<source>Hardware acceleration doesn&apos;t work on XWayland. Continuing on XWayland may result in poor streaming performance. Try running with QT_QPA_PLATFORM=wayland or switch to X11.</source>
<translation>L&apos;accélération matérielle ne fonctionne pas sur XWayland. Continuer sur XWayland peut entraîner de mauvaises performances de streaming. Essayez de fonctionner avec QT_QPA_PLATFORM=wayland ou passez à X11.</translation>
</message>
<message>
<location filename="../gui/main.qml" line="413"/>
<source>Moonlight detected gamepads without a mapping:</source>
<translation>Moonlight a détecté des manettes de jeu san configuration:</translation>
</message>
<message>
<location filename="../gui/main.qml" line="326"/>
<source>Help</source>
<translation>Aide</translation>
</message>
<message>
<location filename="../gui/main.qml" line="389"/>
<source>Click the Help button for more information on solving this problem.</source>
<translation>Cliquez sur le bouton Aide pour obtenir plus d&apos;informations sur la manière de résoudre ce problème.</translation>
</message>
<message>
<location filename="../gui/main.qml" line="397"/>
<source>Click the Help button for more information.</source>
<translation>Cliquez sur le bouton Aide pour plus d&apos;informations.</translation>
</message>
<message>
<location filename="../gui/main.qml" line="404"/>
<source>This PC is running a 64-bit version of Windows. Please download the x64 version of Moonlight for the best streaming performance.</source>
<translation>Ce PC fonctionne avec une version 64 bits de Windows. Veuillez télécharger la version x64 de Moonlight pour obtenir les meilleures performances de diffusion en continu.</translation>
</message>
<message>
<location filename="../gui/main.qml" line="415"/>
<source>Click the Help button for information on how to map your gamepads.</source>
<translation>Cliquez sur le bouton Aide pour obtenir des informations sur la configuration de vos manettes de jeu.</translation>
</message>
<message>
<location filename="../gui/main.qml" line="423"/>
<source>Are you sure you want to quit?</source>
<translation>Vous êtes sûr de vouloir quitter?</translation>
</message>
<message>
<location filename="../gui/main.qml" line="453"/>
<source>Enter the IP address of your GameStream PC:</source>
<translation>Saisissez l&apos;adresse IP de votre PC GameStream :</translation>
</message>
</context>
</TS>
+40 -245
View File
@@ -6,12 +6,6 @@
#include <QMutex>
#include <QtDebug>
#include <QNetworkProxyFactory>
#include <QPalette>
#include <QFont>
#include <QCursor>
#include <QElapsedTimer>
#include <QFile>
#include<QTranslator>
// Don't let SDL hook our main function, since Qt is already
// doing the same thing. This needs to be before any headers
@@ -23,21 +17,17 @@
#include "streaming/video/ffmpeg.h"
#endif
#if defined(Q_OS_WIN32) && defined(Q_PROCESSOR_X86)
#ifdef Q_OS_WIN32
#include "antihookingprotection.h"
#elif defined(Q_OS_LINUX)
#include <openssl/ssl.h>
#endif
#include "cli/quitstream.h"
#include "cli/startstream.h"
#include "cli/commandlineparser.h"
#include "path.h"
#include "utils.h"
#include "gui/computermodel.h"
#include "gui/appmodel.h"
#include "backend/autoupdatechecker.h"
#include "backend/systemproperties.h"
#include "streaming/session.h"
#include "settings/streamingpreferences.h"
#include "gui/sdlgamepadkeynavigation.h"
@@ -58,7 +48,7 @@
#endif
#ifdef USE_CUSTOM_LOGGER
static QElapsedTimer s_LoggerTime;
static QTime s_LoggerTime;
static QTextStream s_LoggerStream(stdout);
static QMutex s_LoggerLock;
#ifdef LOG_TO_FILE
@@ -77,12 +67,7 @@ void logToLoggerStream(QString& message)
return;
}
else if (s_LogLinesWritten == MAX_LOG_LINES) {
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_LoggerStream << "Log size limit reached!" << endl;
s_LogLimitReached = true;
return;
}
@@ -151,6 +136,14 @@ void qtLogToDiskHandler(QtMsgType type, const QMessageLogContext&, const QString
break;
}
// HACK: Avoid printing this internal Qt warning which seems to print every time we
// start our event loop for the QNetworkReply. I have found this warning in many
// unrelated bug reports, but none actually tackle this warning itself. It seems to be
// new in Qt 5.12.
if (msg.startsWith("QNetworkReplyHttpImplPrivate::_q_startOperation was called more than once")) {
return;
}
QTime logTime = QTime::fromMSecsSinceStartOfDay(s_LoggerTime.elapsed());
QString txt = QString("%1 - Qt %2: %3\n").arg(logTime.toString()).arg(typeTxt).arg(msg);
@@ -197,7 +190,7 @@ LONG WINAPI UnhandledExceptionHandler(struct _EXCEPTION_POINTERS *ExceptionInfo)
WCHAR dmpFileName[MAX_PATH];
swprintf_s(dmpFileName, L"%ls\\Moonlight-%I64u.dmp",
(PWCHAR)QDir::toNativeSeparators(Path::getLogDir()).utf16(), QDateTime::currentSecsSinceEpoch());
QString qDmpFileName = QString::fromUtf16((const char16_t*)dmpFileName);
QString qDmpFileName = QString::fromUtf16((unsigned short*)dmpFileName);
HANDLE dumpHandle = CreateFileW(dmpFileName, GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
if (dumpHandle != INVALID_HANDLE_VALUE) {
MINIDUMP_EXCEPTION_INFORMATION info;
@@ -238,8 +231,6 @@ LONG WINAPI UnhandledExceptionHandler(struct _EXCEPTION_POINTERS *ExceptionInfo)
int main(int argc, char *argv[])
{
SDL_SetMainReady();
// Set the app version for the QCommandLineParser's showVersion() command
QCoreApplication::setApplicationVersion(VERSION_STR);
@@ -289,60 +280,19 @@ int main(int argc, char *argv[])
SetUnhandledExceptionFilter(UnhandledExceptionHandler);
#endif
#if defined(Q_OS_WIN32) && defined(Q_PROCESSOR_X86)
#ifdef Q_OS_WIN32
// Force AntiHooking.dll to be statically imported and loaded
// by ntdll on x86/x64 platforms by calling a dummy function.
// AntiHooking.dll is not currently built for ARM64.
// by ntdll by calling a dummy function.
AntiHookingDummyImport();
#elif defined(Q_OS_LINUX)
// Force libssl.so to be directly linked to our binary, so
// linuxdeployqt can find it and include it in our AppImage.
// QtNetwork will pull it in via dlopen().
SSL_free(nullptr);
#endif
// Avoid using High DPI on EGLFS. It breaks font rendering.
// https://bugreports.qt.io/browse/QTBUG-64377
//
// NB: We can't use QGuiApplication::platformName() here because it is only
// set once the QGuiApplication is created, which is too late to enable High DPI :(
if (WMUtils::isRunningWindowManager()) {
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
// Enable High DPI support on Qt 5.x. It is always enabled on Qt 6.0
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
#endif
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
// Enable fractional High DPI scaling on Qt 5.14 and later
QGuiApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::PassThrough);
#endif
}
else {
#ifndef STEAM_LINK
if (!qEnvironmentVariableIsSet("QT_QPA_PLATFORM")) {
qInfo() << "Unable to detect Wayland or X11, so EGLFS will be used by default. Set QT_QPA_PLATFORM to override this.";
qputenv("QT_QPA_PLATFORM", "eglfs");
if (!qEnvironmentVariableIsSet("QT_QPA_EGLFS_ALWAYS_SET_MODE")) {
qInfo() << "Setting display mode by default. Set QT_QPA_EGLFS_ALWAYS_SET_MODE=0 to override this.";
// The UI doesn't appear on RetroPie without this option.
qputenv("QT_QPA_EGLFS_ALWAYS_SET_MODE", "1");
}
if (!QFile("/dev/dri").exists()) {
qWarning() << "Unable to find a KMSDRM display device!";
qWarning() << "On the Raspberry Pi, you must enable the 'fake KMS' driver in raspi-config to use Moonlight outside of the GUI environment.";
}
}
#endif
}
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
// This avoids using the default keychain for SSL, which may cause
// password prompts on macOS.
qputenv("QT_SSL_USE_TEMPORARY_KEYCHAIN", "1");
qputenv("QT_SSL_USE_TEMPORARY_KEYCHAIN", QByteArray("1"));
#if defined(Q_OS_WIN32) && QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
#ifdef Q_OS_WIN32
if (!qEnvironmentVariableIsSet("QT_OPENGL")) {
// On Windows, use ANGLE so we don't have to load OpenGL
// user-mode drivers into our app. OGL drivers (especially Intel)
@@ -351,16 +301,6 @@ int main(int argc, char *argv[])
}
#endif
#if defined(Q_OS_DARWIN) && QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
if (!qEnvironmentVariableIsSet("QSG_RHI_BACKEND")) {
// The Metal backend in Qt 6.0.0 causes really strange issues transitioning to
// full-screen in our SDL window (hangs in SDL's Cocoa_SetWindowFullscreenSpace())
// and breaks drawing our status updates in the StreamSegue, so use OpenGL like
// Qt 5 does until we figure out the cause of these issues.
qputenv("QSG_RHI_BACKEND", "opengl");
}
#endif
// We don't want system proxies to apply to us
QNetworkProxyFactory::setUseSystemConfiguration(false);
@@ -371,137 +311,15 @@ int main(int argc, char *argv[])
// Register custom metatypes for use in signals
qRegisterMetaType<NvApp>("NvApp");
SDL_version compileVersion;
SDL_VERSION(&compileVersion);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Compiled with SDL %d.%d.%d",
compileVersion.major, compileVersion.minor, compileVersion.patch);
SDL_version runtimeVersion;
SDL_GetVersion(&runtimeVersion);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Running with SDL %d.%d.%d",
runtimeVersion.major, runtimeVersion.minor, runtimeVersion.patch);
// Allow the display to sleep by default. We will manually use SDL_DisableScreenSaver()
// and SDL_EnableScreenSaver() when appropriate. This hint must be set before
// initializing the SDL video subsystem to have any effect.
SDL_SetHint(SDL_HINT_VIDEO_ALLOW_SCREENSAVER, "1");
// For SDL backends that support it, use double buffering instead of triple buffering
// to save a frame of latency. This doesn't matter for MMAL or DRM renderers since they
// are drawing directly to the screen without involving SDL, but it may matter for other
// future KMSDRM platforms that use SDL for rendering.
SDL_SetHint(SDL_HINT_VIDEO_DOUBLE_BUFFER, "1");
// Use Direct3D 9Ex to avoid a deadlock caused by the D3D device being reset when
// the user triggers a UAC prompt. This option controls the software/SDL renderer.
// The DXVA2 renderer uses Direct3D 9Ex itself directly.
SDL_SetHint("SDL_WINDOWS_USE_D3D9EX", "1");
if (SDL_InitSubSystem(SDL_INIT_TIMER) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_TIMER) failed: %s",
SDL_GetError());
return -1;
}
#ifdef STEAM_LINK
// Steam Link requires that we initialize video before creating our
// QGuiApplication in order to configure the framebuffer correctly.
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s",
SDL_GetError());
return -1;
}
#endif
// Use atexit() to ensure SDL_Quit() is called. This avoids
// racing with object destruction where SDL may be used.
atexit(SDL_Quit);
// Avoid the default behavior of changing the timer resolution to 1 ms.
// We don't want this all the time that Moonlight is open. We will set
// it manually when we start streaming.
SDL_SetHint(SDL_HINT_TIMER_RESOLUTION, "0");
// Disable minimize on focus loss by default. Users seem to want this off by default.
SDL_SetHint(SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS, "0");
// SDL 2.0.12 changes the default behavior to use the button label rather than the button
// position as most other software does. Set this back to 0 to stay consistent with prior
// releases of Moonlight.
SDL_SetHint("SDL_GAMECONTROLLER_USE_BUTTON_LABELS", "0");
// Disable relative mouse scaling to renderer size or logical DPI. We want to send
// the mouse motion exactly how it was given to us.
SDL_SetHint("SDL_MOUSE_RELATIVE_SCALING", "0");
// Set our app name for SDL to use with PulseAudio. This matches what we provide
// as our app name to libsoundio too.
SDL_SetHint("SDL_AUDIO_DEVICE_APP_NAME", "Moonlight");
#ifdef QT_DEBUG
// Allow thread naming using exceptions on debug builds. SDL doesn't use SEH
// when throwing the exceptions, so we don't enable it for release builds out
// of caution.
SDL_SetHint(SDL_HINT_WINDOWS_DISABLE_THREAD_NAMING, "0");
#endif
QGuiApplication app(argc, argv);
QTranslator translator;
qDebug() << "Translation loaded:" << translator.load(QString(":/languages/qml_") + QLocale::system().name());
app.installTranslator(&translator);
// After the QGuiApplication is created, the platform stuff will be initialized
// and we can set the SDL video driver to match Qt.
if (WMUtils::isRunningWayland() && QGuiApplication::platformName() == "xcb") {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Detected XWayland. This will probably break hardware decoding! Try running with QT_QPA_PLATFORM=wayland or switch to X11.");
}
else if (QGuiApplication::platformName().startsWith("wayland")) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Detected Wayland");
qputenv("SDL_VIDEODRIVER", "wayland");
}
#ifdef STEAM_LINK
// Qt 5.9 from the Steam Link SDK is not able to load any fonts
// since the Steam Link doesn't include any of the ones it looks
// for. We know it has NotoSans so we will explicitly ask for that.
if (app.font().family().isEmpty()) {
qWarning() << "SL HACK: No default font - using NotoSans";
QFont fon("NotoSans");
app.setFont(fon);
}
// Move the mouse to the bottom right so it's invisible when using
// gamepad-only navigation.
QCursor().setPos(0xFFFF, 0xFFFF);
#elif !SDL_VERSION_ATLEAST(2, 0, 11) && defined(Q_OS_LINUX) && (defined(__arm__) || defined(__aarch64__))
if (qgetenv("SDL_VIDEO_GL_DRIVER").isEmpty() && QGuiApplication::platformName() == "eglfs") {
// Look for Raspberry Pi GLES libraries. SDL 2.0.10 and earlier needs some help finding
// the correct libraries for the KMSDRM backend if not compiled with the RPI backend enabled.
if (SDL_LoadObject("libbrcmGLESv2.so") != nullptr) {
qputenv("SDL_VIDEO_GL_DRIVER", "libbrcmGLESv2.so");
}
else if (SDL_LoadObject("/opt/vc/lib/libbrcmGLESv2.so") != nullptr) {
qputenv("SDL_VIDEO_GL_DRIVER", "/opt/vc/lib/libbrcmGLESv2.so");
}
}
#endif
#ifndef Q_OS_DARWIN
// Set the window icon except on macOS where we want to keep the
// modified macOS 11 style rounded corner icon.
app.setWindowIcon(QIcon(":/res/moonlight.svg"));
#endif
// Register our C++ types for QML
qmlRegisterType<ComputerModel>("ComputerModel", 1, 0, "ComputerModel");
qmlRegisterType<AppModel>("AppModel", 1, 0, "AppModel");
qmlRegisterType<StreamingPreferences>("StreamingPreferences", 1, 0, "StreamingPreferences");
qmlRegisterType<SdlGamepadKeyNavigation>("SdlGamepadKeyNavigation", 1, 0, "SdlGamepadKeyNavigation");
qmlRegisterUncreatableType<Session>("Session", 1, 0, "Session", "Session cannot be created from QML");
qmlRegisterSingletonType<ComputerManager>("ComputerManager", 1, 0,
"ComputerManager",
@@ -513,48 +331,8 @@ int main(int argc, char *argv[])
[](QQmlEngine*, QJSEngine*) -> QObject* {
return new AutoUpdateChecker();
});
qmlRegisterSingletonType<SystemProperties>("SystemProperties", 1, 0,
"SystemProperties",
[](QQmlEngine*, QJSEngine*) -> QObject* {
return new SystemProperties();
});
qmlRegisterSingletonType<SdlGamepadKeyNavigation>("SdlGamepadKeyNavigation", 1, 0,
"SdlGamepadKeyNavigation",
[](QQmlEngine*, QJSEngine*) -> QObject* {
return new SdlGamepadKeyNavigation();
});
qmlRegisterSingletonType<StreamingPreferences>("StreamingPreferences", 1, 0,
"StreamingPreferences",
[](QQmlEngine*, QJSEngine*) -> QObject* {
return new StreamingPreferences();
});
// Create the identity manager on the main thread
IdentityManager::get();
#ifndef Q_OS_WINRT
// Use the dense material dark theme by default
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_STYLE")) {
qputenv("QT_QUICK_CONTROLS_STYLE", "Material");
}
#else
// Use universal dark on WinRT
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_STYLE")) {
qputenv("QT_QUICK_CONTROLS_STYLE", "Universal");
}
#endif
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_MATERIAL_THEME")) {
qputenv("QT_QUICK_CONTROLS_MATERIAL_THEME", "Dark");
}
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_MATERIAL_ACCENT")) {
qputenv("QT_QUICK_CONTROLS_MATERIAL_ACCENT", "Purple");
}
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_MATERIAL_VARIANT")) {
qputenv("QT_QUICK_CONTROLS_MATERIAL_VARIANT", "Dense");
}
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_UNIVERSAL_THEME")) {
qputenv("QT_QUICK_CONTROLS_UNIVERSAL_THEME", "Dark");
}
QQuickStyle::setStyle("Material");
QQmlApplicationEngine engine;
QString initialView;
@@ -562,11 +340,11 @@ int main(int argc, char *argv[])
GlobalCommandLineParser parser;
switch (parser.parse(app.arguments())) {
case GlobalCommandLineParser::NormalStartRequested:
initialView = "qrc:/gui/PcView.qml";
initialView = "PcView.qml";
break;
case GlobalCommandLineParser::StreamRequested:
{
initialView = "qrc:/gui/CliStartStreamSegue.qml";
initialView = "CliStartStreamSegue.qml";
StreamingPreferences* preferences = new StreamingPreferences(&app);
StreamCommandLineParser streamParser;
streamParser.parse(app.arguments(), preferences);
@@ -578,7 +356,7 @@ int main(int argc, char *argv[])
}
case GlobalCommandLineParser::QuitRequested:
{
initialView = "qrc:/gui/CliQuitStreamSegue.qml";
initialView = "CliQuitStreamSegue.qml";
QuitCommandLineParser quitParser;
quitParser.parse(app.arguments());
auto launcher = new CliQuitStream::Launcher(quitParser.getHost(), &app);
@@ -593,6 +371,23 @@ int main(int argc, char *argv[])
engine.load(QUrl(QStringLiteral("qrc:/gui/main.qml")));
if (engine.rootObjects().isEmpty())
return -1;
SDL_SetMainReady();
if (SDL_Init(SDL_INIT_TIMER) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_Init() failed: %s",
SDL_GetError());
}
// Use atexit() to ensure SDL_Quit() is called. This avoids
// racing with object destruction where SDL may be used.
atexit(SDL_Quit);
// Avoid the default behavior of changing the timer resolution to 1 ms.
// We don't want this all the time that Moonlight is open. We will set
// it manually when we start streaming.
SDL_SetHint(SDL_HINT_TIMER_RESOLUTION, "0");
int err = app.exec();
// Give worker tasks time to properly exit. Fixes PendingQuitTask
Binary file not shown.
+18 -57
View File
@@ -6,9 +6,9 @@
#include <QSettings>
#include <QCoreApplication>
QString Path::s_CacheDir;
QString Path::s_LogDir;
QString Path::s_BoxArtCacheDir;
const QString Path::k_GameControllerMappingFile("gamecontrollerdb.txt");
QString Path::getLogDir()
{
@@ -22,74 +22,40 @@ QString Path::getBoxArtCacheDir()
return s_BoxArtCacheDir;
}
QByteArray Path::readDataFile(QString fileName)
{
QFile dataFile(getDataFilePath(fileName));
dataFile.open(QIODevice::ReadOnly);
return dataFile.readAll();
}
void Path::writeCacheFile(QString fileName, QByteArray data)
{
QDir cacheDir(s_CacheDir);
// Create the cache path if it does not exist
if (!cacheDir.exists()) {
cacheDir.mkpath(".");
}
QFile dataFile(cacheDir.absoluteFilePath(fileName));
dataFile.open(QIODevice::WriteOnly);
dataFile.write(data);
}
void Path::deleteCacheFile(QString fileName)
{
QFile dataFile(QDir(s_CacheDir).absoluteFilePath(fileName));
dataFile.remove();
}
QFileInfo Path::getCacheFileInfo(QString fileName)
{
return QFileInfo(QDir(s_CacheDir), fileName);
}
QString Path::getDataFilePath(QString fileName)
QString Path::getGamepadMappingFile()
{
QString candidatePath;
// Check the cache location first (used by Path::writeDataFile())
candidatePath = QDir(s_CacheDir).absoluteFilePath(fileName);
// Check the current directory first
candidatePath = QDir(QDir::currentPath()).absoluteFilePath(k_GameControllerMappingFile);
if (QFile::exists(candidatePath)) {
qInfo() << "Found" << fileName << "at" << candidatePath;
return candidatePath;
}
// Check the current directory
candidatePath = QDir(QDir::currentPath()).absoluteFilePath(fileName);
if (QFile::exists(candidatePath)) {
qInfo() << "Found" << fileName << "at" << candidatePath;
return candidatePath;
}
// Now check the data directories (for Linux, in particular)
candidatePath = QStandardPaths::locate(QStandardPaths::AppDataLocation, fileName);
candidatePath = QStandardPaths::locate(QStandardPaths::DataLocation, k_GameControllerMappingFile);
if (!candidatePath.isEmpty() && QFile::exists(candidatePath)) {
qInfo() << "Found" << fileName << "at" << candidatePath;
return candidatePath;
}
// Now try the directory of our app installation (for Windows, if current dir doesn't find it)
candidatePath = QDir(QCoreApplication::applicationDirPath()).absoluteFilePath(fileName);
candidatePath = QDir(QCoreApplication::applicationDirPath()).absoluteFilePath(k_GameControllerMappingFile);
if (QFile::exists(candidatePath)) {
qInfo() << "Found" << fileName << "at" << candidatePath;
return candidatePath;
}
// Return the QRC embedded copy
candidatePath = ":/data/" + fileName;
qInfo() << "Found" << fileName << "at" << candidatePath;
return QString(candidatePath);
// Now try the Resources folder in our app bundle for macOS
QDir dir = QDir(QCoreApplication::applicationDirPath());
dir.cdUp();
dir.cd("Resources");
dir.filePath(k_GameControllerMappingFile);
candidatePath = dir.absoluteFilePath(k_GameControllerMappingFile);
if (QFile::exists(candidatePath)) {
return candidatePath;
}
// Couldn't find it
return QString();
}
void Path::initialize(bool portable)
@@ -97,10 +63,6 @@ void Path::initialize(bool portable)
if (portable) {
s_LogDir = QDir::currentPath();
s_BoxArtCacheDir = QDir::currentPath() + "/boxart";
// In order for the If-Modified-Since logic to work in MappingFetcher,
// the cache directory must be different than the current directory.
s_CacheDir = QDir::currentPath() + "/cache";
}
else {
#ifdef Q_OS_DARWIN
@@ -110,7 +72,6 @@ void Path::initialize(bool portable)
#else
s_LogDir = QDir::tempPath();
#endif
s_CacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation);
s_BoxArtCacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/boxart";
}
}
+2 -9
View File
@@ -1,7 +1,6 @@
#pragma once
#include <QString>
#include <QFileInfo>
class Path
{
@@ -10,18 +9,12 @@ public:
static QString getBoxArtCacheDir();
static QByteArray readDataFile(QString fileName);
static void writeCacheFile(QString fileName, QByteArray data);
static void deleteCacheFile(QString fileName);
static QFileInfo getCacheFileInfo(QString fileName);
// Only safe to use directly for Qt classes
static QString getDataFilePath(QString fileName);
static QString getGamepadMappingFile();
static void initialize(bool portable);
private:
static QString s_CacheDir;
static QString s_LogDir;
static QString s_BoxArtCacheDir;
static const QString k_GameControllerMappingFile;
};
-5
View File
@@ -13,10 +13,5 @@
<file>gui/CliQuitStreamSegue.qml</file>
<file>gui/CliStartStreamSegue.qml</file>
<file>gui/AutoResizingComboBox.qml</file>
<file>gui/NavigableMenu.qml</file>
<file>gui/ErrorMessageDialog.qml</file>
<file>gui/NavigableMessageDialog.qml</file>
<file>gui/NavigableDialog.qml</file>
<file>gui/CenteredGridView.qml</file>
</qresource>
</RCC>
-1
View File
@@ -1 +0,0 @@
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 245 240" fill="white"><path class="st0" d="M104.4 103.9c-5.7 0-10.2 5-10.2 11.1s4.6 11.1 10.2 11.1c5.7 0 10.2-5 10.2-11.1.1-6.1-4.5-11.1-10.2-11.1zM140.9 103.9c-5.7 0-10.2 5-10.2 11.1s4.6 11.1 10.2 11.1c5.7 0 10.2-5 10.2-11.1s-4.5-11.1-10.2-11.1z"/><path class="st0" d="M189.5 20h-134C44.2 20 35 29.2 35 40.6v135.2c0 11.4 9.2 20.6 20.5 20.6h113.4l-5.3-18.5 12.8 11.9 12.1 11.2 21.5 19V40.6c0-11.4-9.2-20.6-20.5-20.6zm-38.6 130.6s-3.6-4.3-6.6-8.1c13.1-3.7 18.1-11.9 18.1-11.9-4.1 2.7-8 4.6-11.5 5.9-5 2.1-9.8 3.5-14.5 4.3-9.6 1.8-18.4 1.3-25.9-.1-5.7-1.1-10.6-2.7-14.7-4.3-2.3-.9-4.8-2-7.3-3.4-.3-.2-.6-.3-.9-.5-.2-.1-.3-.2-.4-.3-1.8-1-2.8-1.7-2.8-1.7s4.8 8 17.5 11.8c-3 3.8-6.7 8.3-6.7 8.3-22.1-.7-30.5-15.2-30.5-15.2 0-32.2 14.4-58.3 14.4-58.3 14.4-10.8 28.1-10.5 28.1-10.5l1 1.2c-18 5.2-26.3 13.1-26.3 13.1s2.2-1.2 5.9-2.9c10.7-4.7 19.2-6 22.7-6.3.6-.1 1.1-.2 1.7-.2 6.1-.8 13-1 20.2-.2 9.5 1.1 19.7 3.9 30.1 9.6 0 0-7.9-7.5-24.9-12.7l1.4-1.6s13.7-.3 28.1 10.5c0 0 14.4 26.1 14.4 58.3 0 0-8.5 14.5-30.6 15.2z"/></svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="#fff"><path d="M12 2C6.47 2 2 6.47 2 12s4.47 10 10 10 10-4.47 10-10S17.53 2 12 2zm5 13.59L15.59 17 12 13.41 8.41 17 7 15.59 10.59 12 7 8.41 8.41 7 12 10.59 15.59 7 17 8.41 13.41 12 17 15.59z"/><path d="M0 0h24v24H0z" fill="none"/></svg>

Before

Width:  |  Height:  |  Size: 325 B

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24" fill="#FFFFFF"><path d="M0 0h24v24H0V0zm0 0h24v24H0V0z" fill="none"/><path d="M16.59 7.58L10 14.17l-3.59-3.58L5 12l5 5 8-8zM12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"/></svg>

Before

Width:  |  Height:  |  Size: 339 B

-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="#FFFFFF"><path fill="none" d="M0 0h24v24H0V0z"/><path d="M11 15h2v2h-2zm0-8h2v6h-2zm.99-5C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"/></svg>

Before

Width:  |  Height:  |  Size: 323 B

-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="#FFFFFF"><path fill="none" d="M0 0h24v24H0z"/><path d="M11 18h2v-2h-2v2zm1-16C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm0-14c-2.21 0-4 1.79-4 4h2c0-1.1.9-2 2-2s2 .9 2 2c0 2-3 1.75-3 5h2c0-2.25 3-2.5 3-5 0-2.21-1.79-4-4-4z"/></svg>

Before

Width:  |  Height:  |  Size: 397 B

-4
View File
@@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="#FFFFFF" width="48" height="48" viewBox="0 0 24 24">
<path fill="none" d="M0 0h24v24H0V0z"/>
<path d="M21 2H3c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h7v2H8v2h8v-2h-2v-2h7c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 14H3V4h18v12z"/>
</svg>

Before

Width:  |  Height:  |  Size: 276 B

@@ -0,0 +1,4 @@
<svg fill="#FFFFFF" height="48" viewBox="0 0 24 24" width="48" xmlns="http://www.w3.org/2000/svg">
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M7 11v2h10v-2H7zm5-9C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z"/>
</svg>

After

Width:  |  Height:  |  Size: 303 B

@@ -0,0 +1,9 @@
<svg fill="#FFFFFF" height="48" viewBox="0 0 24 24" width="48" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<path d="M0 0h24v24H0V0z" id="a"/>
</defs>
<clipPath id="b">
<use overflow="visible" xlink:href="#a"/>
</clipPath>
<path clip-path="url(#b)" d="M21 3H3c-1.11 0-2 .89-2 2v12c0 1.1.89 2 2 2h5v2h8v-2h5c1.1 0 1.99-.9 1.99-2L23 5c0-1.11-.9-2-2-2zm0 14H3V5h18v12zm-5-7v2H8v-2h8z"/>
</svg>

After

Width:  |  Height:  |  Size: 467 B

+4
View File
@@ -0,0 +1,4 @@
<svg fill="#FFFFFF" height="48" viewBox="0 0 24 24" width="48" xmlns="http://www.w3.org/2000/svg">
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M21 3H3c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h5v2h8v-2h5c1.1 0 1.99-.9 1.99-2L23 5c0-1.1-.9-2-2-2zm0 14H3V5h18v12z"/>
</svg>

After

Width:  |  Height:  |  Size: 273 B

+3 -16
View File
@@ -1,31 +1,18 @@
<RCC>
<qresource prefix="/">
<file>res/ic_remove_circle_outline_white_48px.svg</file>
<file>res/ic_remove_from_queue_white_48px.svg</file>
<file>res/ic_videogame_asset_white_48px.svg</file>
<file>res/desktop_windows-48px.svg</file>
<file>res/ic_tv_white_48px.svg</file>
<file>res/ic_add_to_queue_white_48px.svg</file>
<file>res/baseline-lock-24px.svg</file>
<file>res/baseline-play_circle_filled_white-48px.svg</file>
<file>res/baseline-warning-24px.svg</file>
<file>res/baseline-cancel-24px.svg</file>
<file>res/no_app_image.png</file>
<file>res/settings.svg</file>
<file>res/arrow_left.svg</file>
<file>res/question_mark.svg</file>
<file>res/moonlight.svg</file>
<file>res/update.svg</file>
<file>res/baseline-help_outline-24px.svg</file>
<file>res/baseline-error_outline-24px.svg</file>
<file>res/baseline-check_circle_outline-24px.svg</file>
<file>res/Discord-Logo-White.svg</file>
<file>languages/qml_fr.ts</file>
<file>languages/qml_fr.qm</file>
</qresource>
<qresource prefix="/data">
<file alias="gamecontrollerdb.txt">SDL_GameControllerDB/gamecontrollerdb.txt</file>
<file alias="ModeSeven.ttf">ModeSeven.ttf</file>
<file alias="egl.frag">shaders/egl.frag</file>
<file alias="egl.vert">shaders/egl.vert</file>
<file alias="egl_overlay.frag">shaders/egl_overlay.frag</file>
<file alias="egl_overlay.vert">shaders/egl_overlay.vert</file>
</qresource>
</RCC>
-91
View File
@@ -1,91 +0,0 @@
#include "mappingfetcher.h"
#include "path.h"
#include <QNetworkReply>
MappingFetcher::MappingFetcher(QObject *parent) :
QObject(parent),
m_Nam(this)
{
// Never communicate over HTTP
m_Nam.setStrictTransportSecurityEnabled(true);
// Allow HTTP redirects
m_Nam.setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy);
connect(&m_Nam, SIGNAL(finished(QNetworkReply*)),
this, SLOT(handleMappingListFetched(QNetworkReply*)));
}
void MappingFetcher::start()
{
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) && QT_VERSION < QT_VERSION_CHECK(5, 15, 1) && !defined(QT_NO_BEARERMANAGEMENT)
// HACK: Set network accessibility to work around QTBUG-80947 (introduced in Qt 5.14.0 and fixed in Qt 5.15.1)
QT_WARNING_PUSH
QT_WARNING_DISABLE_DEPRECATED
m_Nam.setNetworkAccessible(QNetworkAccessManager::Accessible);
QT_WARNING_POP
#endif
QUrl url("https://moonlight-stream.org/SDL_GameControllerDB/gamecontrollerdb.txt");
QNetworkRequest request(url);
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
request.setAttribute(QNetworkRequest::Http2AllowedAttribute, true);
#else
request.setAttribute(QNetworkRequest::HTTP2AllowedAttribute, true);
#endif
// Qt 5.12 introduced QNetworkRequest::IfModifiedSinceHeader. We _could_ implement it
// by hand (including QDateTime conversion to the correct string form) for earlier Qt
// versions, but that's a pain and this is just an optimization anyway. We'll do a bit
// of extra work on those legacy Qt versions by fetching the GCDB each time we launch.
#if QT_VERSION >= QT_VERSION_CHECK(5, 12, 0)
// Only download the file if it's newer than what we have
QFileInfo existingFileInfo = Path::getCacheFileInfo("gamecontrollerdb.txt");
if (existingFileInfo.exists()) {
// Make sure the cached file looks reasonable. It should have some data and
// the last modified time should not be in the future.
QDateTime lastModifiedTime = existingFileInfo.lastModified().toUTC();
if (existingFileInfo.size() > 0 && lastModifiedTime <= QDateTime::currentDateTimeUtc()) {
request.setHeader(QNetworkRequest::IfModifiedSinceHeader, existingFileInfo.lastModified().toUTC());
}
else {
Path::deleteCacheFile("gamecontrollerdb.txt");
}
}
#endif
// We'll get a callback when this is finished
m_Nam.get(request);
}
void MappingFetcher::handleMappingListFetched(QNetworkReply* reply)
{
Q_ASSERT(reply->isFinished());
if (reply->error() == QNetworkReply::NoError) {
// Queue the reply for deletion
reply->deleteLater();
// If we get a 304 back, Qt will happily just tell us our request was
// successful and let us try to write our empty response to disk. Check
// the status code directly to prevent this.
if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute) == 304) {
qInfo() << "Gamepad mappings are up to date";
}
else {
// Update the cached data on disk for next call to applyMappings()
QByteArray data = reply->readAll();
if (!data.isEmpty()) {
Path::writeCacheFile("gamecontrollerdb.txt", data);
}
qInfo() << "Downloaded updated gamepad mappings";
}
}
else {
qWarning() << "Failed to download updated gamepad mappings:" << reply->error();
reply->deleteLater();
}
}
-20
View File
@@ -1,20 +0,0 @@
#pragma once
#include <QObject>
#include <QNetworkAccessManager>
class MappingFetcher : public QObject
{
Q_OBJECT
public:
explicit MappingFetcher(QObject *parent = nullptr);
void start();
private slots:
void handleMappingListFetched(QNetworkReply* reply);
private:
QNetworkAccessManager m_Nam;
};
+17 -36
View File
@@ -10,18 +10,10 @@
#define SER_GUID "guid"
#define SER_MAPPING "mapping"
MappingFetcher* MappingManager::s_MappingFetcher;
MappingManager::MappingManager()
{
QSettings settings;
// Load updated mappings from the Internet once per Moonlight launch
if (s_MappingFetcher == nullptr) {
s_MappingFetcher = new MappingFetcher();
s_MappingFetcher->start();
}
// First load existing saved mappings. This ensures the user's
// hints can always override the old data.
int mappingCount = settings.beginReadArray(SER_GAMEPADMAPPING);
@@ -34,13 +26,7 @@ MappingManager::MappingManager()
settings.endArray();
// Finally load mappings from SDL_HINT_GAMECONTROLLERCONFIG
QStringList sdlMappings =
QString::fromLocal8Bit(SDL_GetHint(SDL_HINT_GAMECONTROLLERCONFIG))
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
.split('\n', Qt::SkipEmptyParts);
#else
.split('\n', QString::SkipEmptyParts);
#endif
QStringList sdlMappings = QString::fromLocal8Bit(SDL_GetHint(SDL_HINT_GAMECONTROLLERCONFIG)).split('\n', QString::SkipEmptyParts);
for (QString sdlMapping : sdlMappings) {
SdlGamepadMapping mapping(sdlMapping);
addMapping(mapping);
@@ -68,34 +54,29 @@ void MappingManager::save()
void MappingManager::applyMappings()
{
QByteArray mappingData = Path::readDataFile("gamecontrollerdb.txt");
if (!mappingData.isEmpty()) {
int newMappings = SDL_GameControllerAddMappingsFromRW(
SDL_RWFromConstMem(mappingData.constData(), mappingData.size()), 1);
QString mappingFile = Path::getGamepadMappingFile();
if (!mappingFile.isEmpty()) {
std::string mappingFileNative = QDir::toNativeSeparators(mappingFile).toStdString();
if (newMappings > 0) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Loading gamepad mappings from: %s",
mappingFileNative.c_str());
int newMappings = SDL_GameControllerAddMappingsFromFile(mappingFileNative.c_str());
if (newMappings < 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Error loading gamepad mappings: %s",
SDL_GetError());
}
else {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Loaded %d new gamepad mappings",
newMappings);
}
else {
if (newMappings < 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Error loading gamepad mappings: %s",
SDL_GetError());
}
else if (newMappings == 0) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"0 new mappings found in gamecontrollerdb.txt. Is it corrupt?");
}
// Try deleting the cached mapping list just in case it's corrupt
Path::deleteCacheFile("gamecontrollerdb.txt");
}
}
else {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Unable to load gamepad mapping file");
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"No gamepad mapping file found");
}
QList<SdlGamepadMapping> mappings = m_Mappings.values();
-4
View File
@@ -1,7 +1,5 @@
#pragma once
#include "mappingfetcher.h"
#include <QSettings>
class SdlGamepadMapping
@@ -72,7 +70,5 @@ public:
private:
QMap<QString, SdlGamepadMapping> m_Mappings;
static MappingFetcher* s_MappingFetcher;
};
+130 -75
View File
@@ -1,4 +1,6 @@
#include "streamingpreferences.h"
#include "streaming/session.h"
#include "streaming/streamutils.h"
#include <QSettings>
@@ -19,25 +21,9 @@
#define SER_UNSUPPORTEDFPS "unsupportedfps"
#define SER_MDNS "mdns"
#define SER_QUITAPPAFTER "quitAppAfter"
#define SER_ABSMOUSEMODE "mouseacceleration"
#define SER_ABSTOUCHMODE "abstouchmode"
#define SER_MOUSEACCELERATION "mouseacceleration"
#define SER_STARTWINDOWED "startwindowed"
#define SER_FRAMEPACING "framepacing"
#define SER_CONNWARNINGS "connwarnings"
#define SER_UIDISPLAYMODE "uidisplaymode"
#define SER_RICHPRESENCE "richpresence"
#define SER_GAMEPADMOUSE "gamepadmouse"
#define SER_DEFAULTVER "defaultver"
#define SER_PACKETSIZE "packetsize"
#define SER_DETECTNETBLOCKING "detectnetblocking"
#define SER_SWAPMOUSEBUTTONS "swapmousebuttons"
#define SER_MUTEONFOCUSLOSS "muteonfocusloss"
#define SER_BACKGROUNDGAMEPAD "backgroundgamepad"
#define SER_REVERSESCROLL "reversescroll"
#define SER_SWAPFACEBUTTONS "swapfacebuttons"
#define SER_CAPTURESYSKEYS "capturesyskeys"
#define CURRENT_DEFAULT_VER 1
StreamingPreferences::StreamingPreferences(QObject *parent)
: QObject(parent)
@@ -49,14 +35,6 @@ void StreamingPreferences::reload()
{
QSettings settings;
int defaultVer = settings.value(SER_DEFAULTVER, 0).toInt();
#ifdef Q_OS_DARWIN
recommendedFullScreenMode = WindowMode::WM_FULLSCREEN_DESKTOP;
#else
recommendedFullScreenMode = WindowMode::WM_FULLSCREEN;
#endif
width = settings.value(SER_WIDTH, 1280).toInt();
height = settings.value(SER_HEIGHT, 720).toInt();
fps = settings.value(SER_FPS, 60).toInt();
@@ -68,20 +46,9 @@ void StreamingPreferences::reload()
unsupportedFps = settings.value(SER_UNSUPPORTEDFPS, false).toBool();
enableMdns = settings.value(SER_MDNS, true).toBool();
quitAppAfter = settings.value(SER_QUITAPPAFTER, false).toBool();
absoluteMouseMode = settings.value(SER_ABSMOUSEMODE, false).toBool();
absoluteTouchMode = settings.value(SER_ABSTOUCHMODE, true).toBool();
mouseAcceleration = settings.value(SER_MOUSEACCELERATION, false).toBool();
startWindowed = settings.value(SER_STARTWINDOWED, false).toBool();
framePacing = settings.value(SER_FRAMEPACING, false).toBool();
connectionWarnings = settings.value(SER_CONNWARNINGS, true).toBool();
richPresence = settings.value(SER_RICHPRESENCE, true).toBool();
gamepadMouse = settings.value(SER_GAMEPADMOUSE, true).toBool();
detectNetworkBlocking = settings.value(SER_DETECTNETBLOCKING, true).toBool();
packetSize = settings.value(SER_PACKETSIZE, 0).toInt();
swapMouseButtons = settings.value(SER_SWAPMOUSEBUTTONS, false).toBool();
muteOnFocusLoss = settings.value(SER_MUTEONFOCUSLOSS, false).toBool();
backgroundGamepad = settings.value(SER_BACKGROUNDGAMEPAD, false).toBool();
reverseScrollDirection = settings.value(SER_REVERSESCROLL, false).toBool();
swapFaceButtons = settings.value(SER_SWAPFACEBUTTONS, false).toBool();
captureSysKeys = settings.value(SER_CAPTURESYSKEYS, false).toBool();
audioConfig = static_cast<AudioConfig>(settings.value(SER_AUDIOCFG,
static_cast<int>(AudioConfig::AC_STEREO)).toInt());
videoCodecConfig = static_cast<VideoCodecConfig>(settings.value(SER_VIDEOCFG,
@@ -91,21 +58,7 @@ void StreamingPreferences::reload()
windowMode = static_cast<WindowMode>(settings.value(SER_WINDOWMODE,
// Try to load from the old preference value too
static_cast<int>(settings.value(SER_FULLSCREEN, true).toBool() ?
recommendedFullScreenMode : WindowMode::WM_WINDOWED)).toInt());
uiDisplayMode = static_cast<UIDisplayMode>(settings.value(SER_UIDISPLAYMODE,
static_cast<int>(settings.value(SER_STARTWINDOWED, true).toBool() ? UIDisplayMode::UI_WINDOWED
: UIDisplayMode::UI_MAXIMIZED)).toInt());
// Perform default settings updates as required based on last default version
if (defaultVer == 0) {
#ifdef Q_OS_DARWIN
// Update window mode setting on macOS from full-screen (old default) to borderless windowed (new default)
if (windowMode == WindowMode::WM_FULLSCREEN) {
windowMode = WindowMode::WM_FULLSCREEN_DESKTOP;
}
#endif
}
WindowMode::WM_FULLSCREEN : WindowMode::WM_WINDOWED)).toInt());
}
void StreamingPreferences::save()
@@ -123,26 +76,134 @@ void StreamingPreferences::save()
settings.setValue(SER_UNSUPPORTEDFPS, unsupportedFps);
settings.setValue(SER_MDNS, enableMdns);
settings.setValue(SER_QUITAPPAFTER, quitAppAfter);
settings.setValue(SER_ABSMOUSEMODE, absoluteMouseMode);
settings.setValue(SER_ABSTOUCHMODE, absoluteTouchMode);
settings.setValue(SER_MOUSEACCELERATION, mouseAcceleration);
settings.setValue(SER_STARTWINDOWED, startWindowed);
settings.setValue(SER_FRAMEPACING, framePacing);
settings.setValue(SER_CONNWARNINGS, connectionWarnings);
settings.setValue(SER_RICHPRESENCE, richPresence);
settings.setValue(SER_GAMEPADMOUSE, gamepadMouse);
settings.setValue(SER_PACKETSIZE, packetSize);
settings.setValue(SER_DETECTNETBLOCKING, detectNetworkBlocking);
settings.setValue(SER_AUDIOCFG, static_cast<int>(audioConfig));
settings.setValue(SER_VIDEOCFG, static_cast<int>(videoCodecConfig));
settings.setValue(SER_VIDEODEC, static_cast<int>(videoDecoderSelection));
settings.setValue(SER_WINDOWMODE, static_cast<int>(windowMode));
settings.setValue(SER_UIDISPLAYMODE, static_cast<int>(uiDisplayMode));
settings.setValue(SER_DEFAULTVER, CURRENT_DEFAULT_VER);
settings.setValue(SER_SWAPMOUSEBUTTONS, swapMouseButtons);
settings.setValue(SER_MUTEONFOCUSLOSS, muteOnFocusLoss);
settings.setValue(SER_BACKGROUNDGAMEPAD, backgroundGamepad);
settings.setValue(SER_REVERSESCROLL, reverseScrollDirection);
settings.setValue(SER_SWAPFACEBUTTONS, swapFaceButtons);
settings.setValue(SER_CAPTURESYSKEYS, captureSysKeys);
}
bool StreamingPreferences::hasAnyHardwareAcceleration()
{
// Always use VDS_AUTO to avoid spamming the user with warnings
// if they've forced software decoding.
return Session::isHardwareDecodeAvailable(VDS_AUTO,
VIDEO_FORMAT_H264,
1920, 1080, 60);
}
bool StreamingPreferences::isRunningWayland()
{
return qgetenv("XDG_SESSION_TYPE") == QByteArray("wayland");
}
bool StreamingPreferences::isWow64()
{
#ifdef Q_OS_WIN32
return QSysInfo::currentCpuArchitecture() != QSysInfo::buildCpuArchitecture();
#else
return false;
#endif
}
QString StreamingPreferences::getUnmappedGamepads()
{
return SdlInputHandler::getUnmappedGamepads();
}
int StreamingPreferences::getMaximumStreamingFrameRate()
{
// Never let the maximum drop below 60 FPS
int maxFrameRate = 60;
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s",
SDL_GetError());
return maxFrameRate;
}
SDL_DisplayMode mode, bestMode, desktopMode;
for (int displayIndex = 0; displayIndex < SDL_GetNumVideoDisplays(); displayIndex++) {
// Get the native desktop resolution
if (StreamUtils::getRealDesktopMode(displayIndex, &desktopMode)) {
// Start at desktop mode and work our way up
bestMode = desktopMode;
for (int i = 0; i < SDL_GetNumDisplayModes(displayIndex); i++) {
if (SDL_GetDisplayMode(displayIndex, i, &mode) == 0) {
if (mode.w == desktopMode.w && mode.h == desktopMode.h) {
if (mode.refresh_rate > bestMode.refresh_rate) {
bestMode = mode;
}
}
}
}
maxFrameRate = qMax(maxFrameRate, bestMode.refresh_rate);
}
}
SDL_QuitSubSystem(SDL_INIT_VIDEO);
return maxFrameRate;
}
QRect StreamingPreferences::getDesktopResolution(int displayIndex)
{
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s",
SDL_GetError());
return QRect();
}
if (displayIndex >= SDL_GetNumVideoDisplays()) {
SDL_QuitSubSystem(SDL_INIT_VIDEO);
return QRect();
}
SDL_DisplayMode mode;
int err = SDL_GetDesktopDisplayMode(displayIndex, &mode);
SDL_QuitSubSystem(SDL_INIT_VIDEO);
if (err == 0) {
return QRect(0, 0, mode.w, mode.h);
}
else {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_GetDesktopDisplayMode() failed: %s",
SDL_GetError());
return QRect();
}
}
QRect StreamingPreferences::getNativeResolution(int displayIndex)
{
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s",
SDL_GetError());
return QRect();
}
if (displayIndex >= SDL_GetNumVideoDisplays()) {
SDL_QuitSubSystem(SDL_INIT_VIDEO);
return QRect();
}
SDL_DisplayMode mode;
bool success = StreamUtils::getRealDesktopMode(displayIndex, &mode);
SDL_QuitSubSystem(SDL_INIT_VIDEO);
if (success) {
return QRect(0, 0, mode.w, mode.h);
}
else {
return QRect();
}
}
int StreamingPreferences::getDefaultBitrate(int width, int height, int fps)
@@ -152,14 +213,8 @@ int StreamingPreferences::getDefaultBitrate(int width, int height, int fps)
// we don't want to bump those 16:10 resolutions up to the
// next 16:9 slot.
if (width * height <= 640 * 360) {
return static_cast<int>(1000 * (fps / 30.0));
}
else if (width * height <= 854 * 480) {
return static_cast<int>(1500 * (fps / 30.0));
}
// This covers 1280x720 and 1280x800 too
else if (width * height <= 1366 * 768) {
if (width * height <= 1366 * 768) {
return static_cast<int>(5000 * (fps / 30.0));
}
else if (width * height <= 1920 * 1200) {
+21 -52
View File
@@ -15,13 +15,26 @@ public:
Q_INVOKABLE void save();
Q_INVOKABLE static bool hasAnyHardwareAcceleration();
Q_INVOKABLE static bool isRunningWayland();
Q_INVOKABLE static bool isWow64();
Q_INVOKABLE static int getMaximumStreamingFrameRate();
Q_INVOKABLE QRect getDesktopResolution(int displayIndex);
Q_INVOKABLE QRect getNativeResolution(int displayIndex);
Q_INVOKABLE QString getUnmappedGamepads();
void reload();
enum AudioConfig
{
AC_STEREO,
AC_51_SURROUND,
AC_71_SURROUND
AC_51_SURROUND
};
Q_ENUM(AudioConfig)
@@ -50,14 +63,6 @@ public:
};
Q_ENUM(WindowMode)
enum UIDisplayMode
{
UI_WINDOWED,
UI_MAXIMIZED,
UI_FULLSCREEN
};
Q_ENUM(UIDisplayMode)
Q_PROPERTY(int width MEMBER width NOTIFY displayModeChanged)
Q_PROPERTY(int height MEMBER height NOTIFY displayModeChanged)
Q_PROPERTY(int fps MEMBER fps NOTIFY displayModeChanged)
@@ -69,25 +74,13 @@ public:
Q_PROPERTY(bool unsupportedFps MEMBER unsupportedFps NOTIFY unsupportedFpsChanged)
Q_PROPERTY(bool enableMdns MEMBER enableMdns NOTIFY enableMdnsChanged)
Q_PROPERTY(bool quitAppAfter MEMBER quitAppAfter NOTIFY quitAppAfterChanged)
Q_PROPERTY(bool absoluteMouseMode MEMBER absoluteMouseMode NOTIFY absoluteMouseModeChanged)
Q_PROPERTY(bool absoluteTouchMode MEMBER absoluteTouchMode NOTIFY absoluteTouchModeChanged)
Q_PROPERTY(bool mouseAcceleration MEMBER mouseAcceleration NOTIFY mouseAccelerationChanged)
Q_PROPERTY(bool startWindowed MEMBER startWindowed NOTIFY startWindowedChanged)
Q_PROPERTY(bool framePacing MEMBER framePacing NOTIFY framePacingChanged)
Q_PROPERTY(bool connectionWarnings MEMBER connectionWarnings NOTIFY connectionWarningsChanged)
Q_PROPERTY(bool richPresence MEMBER richPresence NOTIFY richPresenceChanged)
Q_PROPERTY(bool gamepadMouse MEMBER gamepadMouse NOTIFY gamepadMouseChanged)
Q_PROPERTY(bool detectNetworkBlocking MEMBER detectNetworkBlocking NOTIFY detectNetworkBlockingChanged);
Q_PROPERTY(AudioConfig audioConfig MEMBER audioConfig NOTIFY audioConfigChanged)
Q_PROPERTY(VideoCodecConfig videoCodecConfig MEMBER videoCodecConfig NOTIFY videoCodecConfigChanged)
Q_PROPERTY(VideoDecoderSelection videoDecoderSelection MEMBER videoDecoderSelection NOTIFY videoDecoderSelectionChanged)
Q_PROPERTY(WindowMode windowMode MEMBER windowMode NOTIFY windowModeChanged)
Q_PROPERTY(WindowMode recommendedFullScreenMode MEMBER recommendedFullScreenMode CONSTANT)
Q_PROPERTY(UIDisplayMode uiDisplayMode MEMBER uiDisplayMode NOTIFY uiDisplayModeChanged)
Q_PROPERTY(bool swapMouseButtons MEMBER swapMouseButtons NOTIFY mouseButtonsChanged)
Q_PROPERTY(bool muteOnFocusLoss MEMBER muteOnFocusLoss NOTIFY muteOnFocusLossChanged)
Q_PROPERTY(bool backgroundGamepad MEMBER backgroundGamepad NOTIFY backgroundGamepadChanged)
Q_PROPERTY(bool reverseScrollDirection MEMBER reverseScrollDirection NOTIFY reverseScrollDirectionChanged)
Q_PROPERTY(bool swapFaceButtons MEMBER swapFaceButtons NOTIFY swapFaceButtonsChanged)
Q_PROPERTY(bool captureSysKeys MEMBER captureSysKeys NOTIFY captureSysKeysChanged)
// Directly accessible members for preferences
int width;
@@ -101,26 +94,13 @@ public:
bool unsupportedFps;
bool enableMdns;
bool quitAppAfter;
bool absoluteMouseMode;
bool absoluteTouchMode;
bool mouseAcceleration;
bool startWindowed;
bool framePacing;
bool connectionWarnings;
bool richPresence;
bool gamepadMouse;
bool detectNetworkBlocking;
bool swapMouseButtons;
bool muteOnFocusLoss;
bool backgroundGamepad;
bool reverseScrollDirection;
bool swapFaceButtons;
bool captureSysKeys;
int packetSize;
AudioConfig audioConfig;
VideoCodecConfig videoCodecConfig;
VideoDecoderSelection videoDecoderSelection;
WindowMode windowMode;
WindowMode recommendedFullScreenMode;
UIDisplayMode uiDisplayMode;
signals:
void displayModeChanged();
@@ -132,23 +112,12 @@ signals:
void unsupportedFpsChanged();
void enableMdnsChanged();
void quitAppAfterChanged();
void absoluteMouseModeChanged();
void absoluteTouchModeChanged();
void mouseAccelerationChanged();
void audioConfigChanged();
void videoCodecConfigChanged();
void videoDecoderSelectionChanged();
void uiDisplayModeChanged();
void windowModeChanged();
void startWindowedChanged();
void framePacingChanged();
void connectionWarningsChanged();
void richPresenceChanged();
void gamepadMouseChanged();
void detectNetworkBlockingChanged();
void mouseButtonsChanged();
void muteOnFocusLossChanged();
void backgroundGamepadChanged();
void reverseScrollDirectionChanged();
void swapFaceButtonsChanged();
void captureSysKeysChanged();
};
-21
View File
@@ -1,21 +0,0 @@
#version 300 es
#extension GL_OES_EGL_image_external : require
precision mediump float;
out vec4 FragColor;
in vec2 vTextCoord;
uniform mat3 yuvmat;
uniform vec3 offset;
uniform samplerExternalOES plane1;
uniform samplerExternalOES plane2;
void main() {
vec3 YCbCr = vec3(
texture2D(plane1, vTextCoord)[0],
texture2D(plane2, vTextCoord).xy
);
YCbCr -= offset;
FragColor = vec4(clamp(yuvmat * YCbCr, 0.0, 1.0), 1.0f);
}
-10
View File
@@ -1,10 +0,0 @@
#version 300 es
layout (location = 0) in vec2 aPosition; // 2D: X,Y
layout (location = 1) in vec2 aTexCoord;
out vec2 vTextCoord;
void main() {
vTextCoord = aTexCoord;
gl_Position = vec4(aPosition, 0, 1);
}
-11
View File
@@ -1,11 +0,0 @@
precision mediump float;
uniform sampler2D uTexture;
varying vec2 vTexCoord;
void main() {
vec4 abgr = texture2D(uTexture, vTexCoord);
gl_FragColor = abgr;
gl_FragColor.r = abgr.b;
gl_FragColor.b = abgr.r;
}
-8
View File
@@ -1,8 +0,0 @@
attribute vec2 aPosition; // 2D: X,Y
attribute vec2 aTexCoord;
varying vec2 vTexCoord;
void main() {
vTexCoord = aTexCoord;
gl_Position = vec4(aPosition, 0, 1);
}
+60 -162
View File
@@ -5,112 +5,55 @@
#include "renderers/soundioaudiorenderer.h"
#endif
#ifdef HAVE_SLAUDIO
#include "renderers/slaud.h"
#endif
#include "renderers/sdl.h"
#include <Limelight.h>
#define TRY_INIT_RENDERER(renderer, opusConfig) \
{ \
IAudioRenderer* __renderer = new renderer(); \
if (__renderer->prepareForPlayback(opusConfig)) \
return __renderer; \
delete __renderer; \
}
IAudioRenderer* Session::createAudioRenderer(const POPUS_MULTISTREAM_CONFIGURATION opusConfig)
IAudioRenderer* Session::createAudioRenderer()
{
// Handle explicit ML_AUDIO setting and fail if the requested backend fails
QString mlAudio = qgetenv("ML_AUDIO").toLower();
if (mlAudio == "sdl") {
TRY_INIT_RENDERER(SdlAudioRenderer, opusConfig)
return nullptr;
}
#ifdef HAVE_SOUNDIO
else if (mlAudio == "libsoundio") {
TRY_INIT_RENDERER(SoundIoAudioRenderer, opusConfig)
return nullptr;
}
#endif
#if defined(HAVE_SLAUDIO)
else if (mlAudio == "slaudio") {
TRY_INIT_RENDERER(SLAudioRenderer, opusConfig)
return nullptr;
}
#endif
else if (!mlAudio.isEmpty()) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Unknown audio backend: %s",
SDL_getenv("ML_AUDIO"));
return nullptr;
if (qgetenv("ML_AUDIO") == "SDL") {
return new SdlAudioRenderer();
}
// -------------- Automatic backend selection below this line ---------------
#if defined(HAVE_SLAUDIO)
// Steam Link should always have SLAudio
TRY_INIT_RENDERER(SLAudioRenderer, opusConfig)
#endif
#if !defined(Q_OS_WIN32) && !defined(Q_OS_DARWIN)
// Linux defaults to SDL due to persistent glitching issues under libsoundio.
// Platforms that libsoundio doesn't support also default to SDL.
TRY_INIT_RENDERER(SdlAudioRenderer, opusConfig)
#ifdef HAVE_SOUNDIO
TRY_INIT_RENDERER(SoundIoAudioRenderer, opusConfig)
#endif
return new SoundIoAudioRenderer();
#else
// Windows and macOS default to libsoundio and fall back to SDL
#ifdef HAVE_SOUNDIO
TRY_INIT_RENDERER(SoundIoAudioRenderer, opusConfig)
return new SdlAudioRenderer();
#endif
TRY_INIT_RENDERER(SdlAudioRenderer, opusConfig)
#endif
return nullptr;
}
int Session::getAudioRendererCapabilities(int audioConfiguration)
{
// Build a fake OPUS_MULTISTREAM_CONFIGURATION to give
// the renderer the channel count and sample rate.
OPUS_MULTISTREAM_CONFIGURATION opusConfig = {};
opusConfig.sampleRate = 48000;
opusConfig.samplesPerFrame = 240;
opusConfig.channelCount = CHANNEL_COUNT_FROM_AUDIO_CONFIGURATION(audioConfiguration);
IAudioRenderer* audioRenderer = createAudioRenderer(&opusConfig);
if (audioRenderer == nullptr) {
return 0;
}
int caps = audioRenderer->getCapabilities();
delete audioRenderer;
return caps;
}
bool Session::testAudio(int audioConfiguration)
{
// Build a fake OPUS_MULTISTREAM_CONFIGURATION to give
// the renderer the channel count and sample rate.
OPUS_MULTISTREAM_CONFIGURATION opusConfig = {};
opusConfig.sampleRate = 48000;
opusConfig.samplesPerFrame = 240;
opusConfig.channelCount = CHANNEL_COUNT_FROM_AUDIO_CONFIGURATION(audioConfiguration);
IAudioRenderer* audioRenderer;
IAudioRenderer* audioRenderer = createAudioRenderer(&opusConfig);
audioRenderer = createAudioRenderer();
if (audioRenderer == nullptr) {
return false;
}
// Build a fake OPUS_MULTISTREAM_CONFIGURATION to give
// the renderer the channel count and sample rate.
OPUS_MULTISTREAM_CONFIGURATION opusConfig = {};
opusConfig.sampleRate = 48000;
switch (audioConfiguration)
{
case AUDIO_CONFIGURATION_STEREO:
opusConfig.channelCount = 2;
break;
case AUDIO_CONFIGURATION_51_SURROUND:
opusConfig.channelCount = 6;
break;
default:
SDL_assert(false);
return false;
}
bool ret = audioRenderer->prepareForPlayback(&opusConfig);
delete audioRenderer;
return true;
return ret;
}
int Session::arInit(int /* audioConfiguration */,
@@ -121,34 +64,30 @@ int Session::arInit(int /* audioConfiguration */,
SDL_memcpy(&s_ActiveSession->m_AudioConfig, opusConfig, sizeof(*opusConfig));
s_ActiveSession->m_AudioRenderer = s_ActiveSession->createAudioRenderer(&s_ActiveSession->m_AudioConfig);
if (s_ActiveSession->m_AudioRenderer == nullptr) {
return -2;
}
// Allow the chosen renderer to remap Opus channels as needed to ensure proper output
s_ActiveSession->m_AudioRenderer->remapChannels(&s_ActiveSession->m_AudioConfig);
// Create the Opus decoder with the renderer's preferred channel mapping
s_ActiveSession->m_OpusDecoder =
opus_multistream_decoder_create(s_ActiveSession->m_AudioConfig.sampleRate,
s_ActiveSession->m_AudioConfig.channelCount,
s_ActiveSession->m_AudioConfig.streams,
s_ActiveSession->m_AudioConfig.coupledStreams,
s_ActiveSession->m_AudioConfig.mapping,
opus_multistream_decoder_create(opusConfig->sampleRate,
opusConfig->channelCount,
opusConfig->streams,
opusConfig->coupledStreams,
opusConfig->mapping,
&error);
if (s_ActiveSession->m_OpusDecoder == NULL) {
delete s_ActiveSession->m_AudioRenderer;
s_ActiveSession->m_AudioRenderer = nullptr;
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to create decoder: %d",
error);
return -1;
}
s_ActiveSession->m_AudioRenderer = s_ActiveSession->createAudioRenderer();
if (!s_ActiveSession->m_AudioRenderer->prepareForPlayback(opusConfig)) {
delete s_ActiveSession->m_AudioRenderer;
opus_multistream_decoder_destroy(s_ActiveSession->m_OpusDecoder);
return -2;
}
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Audio stream has %d channels",
s_ActiveSession->m_AudioConfig.channelCount);
opusConfig->channelCount);
return 0;
}
@@ -166,11 +105,8 @@ void Session::arDecodeAndPlaySample(char* sampleData, int sampleLength)
{
int samplesDecoded;
#ifndef STEAM_LINK
// Set this thread to high priority to reduce the chance of missing
// our sample delivery time. On Steam Link, this causes starvation
// of other threads due to severely restricted CPU time available,
// so we will skip it on that platform.
// Set this thread to high priority to reduce
// the chance of missing our sample delivery time
if (s_ActiveSession->m_AudioSampleCount == 0) {
if (SDL_SetThreadPriority(SDL_THREAD_PRIORITY_HIGH) < 0) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
@@ -178,59 +114,29 @@ void Session::arDecodeAndPlaySample(char* sampleData, int sampleLength)
SDL_GetError());
}
}
#endif
// See if we need to drop this sample
if (s_ActiveSession->m_DropAudioEndTime != 0) {
if (SDL_TICKS_PASSED(SDL_GetTicks(), s_ActiveSession->m_DropAudioEndTime)) {
// Avoid calling SDL_GetTicks() now
s_ActiveSession->m_DropAudioEndTime = 0;
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Audio drop window has ended");
}
else {
// We're still in the drop window
return;
}
}
s_ActiveSession->m_AudioSampleCount++;
// If audio is muted, don't decode or play the audio
if (s_ActiveSession->m_AudioMuted) {
return;
}
if (s_ActiveSession->m_AudioRenderer != nullptr) {
int desiredSize = sizeof(short) * s_ActiveSession->m_AudioConfig.samplesPerFrame * s_ActiveSession->m_AudioConfig.channelCount;
void* buffer = s_ActiveSession->m_AudioRenderer->getAudioBuffer(&desiredSize);
if (buffer == nullptr) {
return;
}
samplesDecoded = opus_multistream_decode(s_ActiveSession->m_OpusDecoder,
(unsigned char*)sampleData,
sampleLength,
(short*)buffer,
desiredSize / sizeof(short) / s_ActiveSession->m_AudioConfig.channelCount,
s_ActiveSession->m_OpusDecodeBuffer,
SAMPLES_PER_FRAME,
0);
// Update desiredSize with the number of bytes actually populated by the decoding operation
if (samplesDecoded > 0) {
SDL_assert(desiredSize >= (int)(sizeof(short) * samplesDecoded * s_ActiveSession->m_AudioConfig.channelCount));
desiredSize = sizeof(short) * samplesDecoded * s_ActiveSession->m_AudioConfig.channelCount;
}
else {
desiredSize = 0;
}
if (!s_ActiveSession->m_AudioRenderer->submitAudio(s_ActiveSession->m_OpusDecodeBuffer,
static_cast<int>(
sizeof(short) *
samplesDecoded *
s_ActiveSession->m_AudioConfig.channelCount))) {
if (!s_ActiveSession->m_AudioRenderer->submitAudio(desiredSize)) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Reinitializing audio renderer after failure");
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Reinitializing audio renderer after failure");
delete s_ActiveSession->m_AudioRenderer;
s_ActiveSession->m_AudioRenderer = nullptr;
delete s_ActiveSession->m_AudioRenderer;
s_ActiveSession->m_AudioRenderer = nullptr;
}
}
}
@@ -239,18 +145,10 @@ void Session::arDecodeAndPlaySample(char* sampleData, int sampleLength)
// safe to reinitialize here because we can't be torn down while
// the audio decoder/playback thread is still alive.
if (s_ActiveSession->m_AudioRenderer == nullptr && (s_ActiveSession->m_AudioSampleCount % 200) == 0) {
// Since we're doing this inline and audio initialization takes time, we need
// to drop samples to account for the time we've spent blocking audio rendering
// so we return to real-time playback and don't accumulate latency.
Uint32 audioReinitStartTime = SDL_GetTicks();
s_ActiveSession->m_AudioRenderer = s_ActiveSession->createAudioRenderer(&s_ActiveSession->m_AudioConfig);
Uint32 audioReinitStopTime = SDL_GetTicks();
s_ActiveSession->m_DropAudioEndTime = audioReinitStopTime + (audioReinitStopTime - audioReinitStartTime);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Audio reinitialization took %d ms - starting drop window",
audioReinitStopTime - audioReinitStartTime);
s_ActiveSession->m_AudioRenderer = s_ActiveSession->createAudioRenderer();
if (!s_ActiveSession->m_AudioRenderer->prepareForPlayback(&s_ActiveSession->m_AudioConfig)) {
delete s_ActiveSession->m_AudioRenderer;
s_ActiveSession->m_AudioRenderer = nullptr;
}
}
}
+4 -15
View File
@@ -2,6 +2,9 @@
#include <Limelight.h>
#define MAX_CHANNELS 6
#define SAMPLES_PER_FRAME 240
class IAudioRenderer
{
public:
@@ -9,20 +12,6 @@ public:
virtual bool prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig) = 0;
virtual void* getAudioBuffer(int* size) = 0;
// Return false if an unrecoverable error has occurred and the renderer must be reinitialized
virtual bool submitAudio(int bytesWritten) = 0;
virtual int getCapabilities() = 0;
virtual void remapChannels(POPUS_MULTISTREAM_CONFIGURATION) {
// Use default channel mapping:
// 0 - Front Left
// 1 - Front Right
// 2 - Center
// 3 - LFE
// 4 - Surround Left
// 5 - Surround Right
}
virtual bool submitAudio(short* audioBuffer, int audioSize) = 0;
};
+1 -7
View File
@@ -12,14 +12,8 @@ public:
virtual bool prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig);
virtual void* getAudioBuffer(int* size);
virtual bool submitAudio(int bytesWritten);
virtual int getCapabilities();
virtual bool submitAudio(short* audioBuffer, int audioSize);
private:
SDL_AudioDeviceID m_AudioDevice;
void* m_AudioBuffer;
int m_FrameSize;
};
+9 -49
View File
@@ -3,12 +3,12 @@
#include <Limelight.h>
#include <SDL.h>
#include <QtGlobal>
SdlAudioRenderer::SdlAudioRenderer()
: m_AudioDevice(0),
m_AudioBuffer(nullptr)
: m_AudioDevice(0)
{
SDL_assert(!SDL_WasInit(SDL_INIT_AUDIO));
if (SDL_InitSubSystem(SDL_INIT_AUDIO) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_AUDIO) failed: %s",
@@ -30,29 +30,21 @@ bool SdlAudioRenderer::prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION*
// frames contain a non-power of 2 number of samples,
// so the slop would require buffering another full frame.
// Specifying non-Po2 seems to work for our supported platforms.
want.samples = opusConfig->samplesPerFrame;
m_FrameSize = opusConfig->samplesPerFrame * sizeof(short) * opusConfig->channelCount;
want.samples = SAMPLES_PER_FRAME;
m_AudioDevice = SDL_OpenAudioDevice(NULL, 0, &want, &have, 0);
if (m_AudioDevice == 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to open audio device: %s",
SDL_GetError());
return false;
}
m_AudioBuffer = SDL_malloc(m_FrameSize);
if (m_AudioBuffer == nullptr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to allocate audio buffer");
SDL_QuitSubSystem(SDL_INIT_AUDIO);
return false;
}
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Desired audio buffer: %u samples (%u bytes)",
"Desired audio buffer: %u samples (%lu bytes)",
want.samples,
want.samples * (Uint32)sizeof(short) * want.channels);
want.samples * sizeof(short) * want.channels);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Obtained audio buffer: %u samples (%u bytes)",
@@ -73,39 +65,13 @@ SdlAudioRenderer::~SdlAudioRenderer()
SDL_CloseAudioDevice(m_AudioDevice);
}
if (m_AudioBuffer != nullptr) {
SDL_free(m_AudioBuffer);
}
SDL_QuitSubSystem(SDL_INIT_AUDIO);
SDL_assert(!SDL_WasInit(SDL_INIT_AUDIO));
}
void* SdlAudioRenderer::getAudioBuffer(int*)
bool SdlAudioRenderer::submitAudio(short* audioBuffer, int audioSize)
{
return m_AudioBuffer;
}
bool SdlAudioRenderer::submitAudio(int bytesWritten)
{
if (bytesWritten == 0) {
// Nothing to do
return true;
}
// Don't queue if there's already more than 30 ms of audio data waiting
// in Moonlight's audio queue.
if (LiGetPendingAudioDuration() > 30) {
return true;
}
// Provide backpressure on the queue to ensure too many frames don't build up
// in SDL's audio queue.
while (SDL_GetQueuedAudioSize(m_AudioDevice) / m_FrameSize > 10) {
SDL_Delay(1);
}
if (SDL_QueueAudio(m_AudioDevice, m_AudioBuffer, bytesWritten) < 0) {
if (SDL_QueueAudio(m_AudioDevice, audioBuffer, audioSize) < 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to queue audio sample: %s",
SDL_GetError());
@@ -113,9 +79,3 @@ 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;
}
-145
View File
@@ -1,145 +0,0 @@
#include "slaud.h"
#include <SDL.h>
SLAudioRenderer::SLAudioRenderer()
: m_AudioContext(nullptr),
m_AudioStream(nullptr),
m_AudioBuffer(nullptr)
{
SLAudio_SetLogFunction(SLAudioRenderer::slLogCallback, nullptr);
}
bool SLAudioRenderer::prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig)
{
m_AudioContext = SLAudio_CreateContext();
if (m_AudioContext == nullptr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SLAudio_CreateContext() failed");
return false;
}
// This number is pretty conservative (especially for surround), but
// it's hard to avoid since we get crushed by CPU limitations.
m_MaxQueuedAudioMs = 40 * opusConfig->channelCount / 2;
m_AudioBufferSize = opusConfig->samplesPerFrame * sizeof(short) * opusConfig->channelCount;
m_AudioStream = SLAudio_CreateStream(m_AudioContext,
opusConfig->sampleRate,
opusConfig->channelCount,
m_AudioBufferSize,
1);
if (m_AudioStream == nullptr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SLAudio_CreateStream() failed");
return false;
}
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Using SLAudio renderer with %d samples per frame",
opusConfig->samplesPerFrame);
return true;
}
void SLAudioRenderer::remapChannels(POPUS_MULTISTREAM_CONFIGURATION opusConfig) {
OPUS_MULTISTREAM_CONFIGURATION originalConfig = *opusConfig;
// The Moonlight's default channel order is FL,FR,C,LFE,RL,RR,SL,SR
// SLAudio expects FL,C,FR,RL,RR,(SL,SR),LFE for 5.1/7.1 so we swap the channels around to match
if (opusConfig->channelCount == 3 || opusConfig->channelCount >= 6) {
// Swap FR and C
opusConfig->mapping[1] = originalConfig.mapping[2];
opusConfig->mapping[2] = originalConfig.mapping[1];
}
if (opusConfig->channelCount >= 6) {
// SLAudio expects the LFE channel at the end
opusConfig->mapping[opusConfig->channelCount - 1] = originalConfig.mapping[3];
// Slide the other channels down
memcpy(&opusConfig->mapping[3],
&originalConfig.mapping[4],
opusConfig->channelCount - 4);
}
}
void* SLAudioRenderer::getAudioBuffer(int* size)
{
SDL_assert(*size == m_AudioBufferSize);
if (m_AudioBuffer == nullptr) {
m_AudioBuffer = SLAudio_BeginFrame(m_AudioStream);
}
return m_AudioBuffer;
}
SLAudioRenderer::~SLAudioRenderer()
{
if (m_AudioBuffer != nullptr) {
memset(m_AudioBuffer, 0, m_AudioBufferSize);
SLAudio_SubmitFrame(m_AudioStream);
}
if (m_AudioStream != nullptr) {
SLAudio_FreeStream(m_AudioStream);
}
if (m_AudioContext != nullptr) {
SLAudio_FreeContext(m_AudioContext);
}
}
bool SLAudioRenderer::submitAudio(int bytesWritten)
{
if (bytesWritten == 0) {
// This buffer will be reused next time
return true;
}
if (LiGetPendingAudioDuration() < m_MaxQueuedAudioMs) {
SLAudio_SubmitFrame(m_AudioStream);
m_AudioBuffer = nullptr;
}
else {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Too many queued audio frames: %d",
LiGetPendingAudioFrames());
}
return true;
}
int SLAudioRenderer::getCapabilities()
{
return CAPABILITY_SLOW_OPUS_DECODER | CAPABILITY_SUPPORTS_ARBITRARY_AUDIO_DURATION;
}
void SLAudioRenderer::slLogCallback(void*, ESLAudioLog logLevel, const char *message)
{
SDL_LogPriority priority;
switch (logLevel)
{
case k_ESLAudioLogError:
priority = SDL_LOG_PRIORITY_ERROR;
break;
case k_ESLAudioLogWarning:
priority = SDL_LOG_PRIORITY_WARN;
break;
case k_ESLAudioLogInfo:
priority = SDL_LOG_PRIORITY_INFO;
break;
default:
case k_ESLAudioLogDebug:
priority = SDL_LOG_PRIORITY_DEBUG;
break;
}
SDL_LogMessage(SDL_LOG_CATEGORY_APPLICATION,
priority,
"SLAudio: %s",
message);
}
-32
View File
@@ -1,32 +0,0 @@
#pragma once
#include "renderer.h"
#include <SLAudio.h>
class SLAudioRenderer : public IAudioRenderer
{
public:
SLAudioRenderer();
virtual ~SLAudioRenderer();
virtual bool prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig);
virtual void* getAudioBuffer(int* size);
virtual bool submitAudio(int bytesWritten);
virtual int getCapabilities();
virtual void remapChannels(POPUS_MULTISTREAM_CONFIGURATION opusConfig);
private:
static void slLogCallback(void* context, ESLAudioLog logLevel, const char* message);
CSLAudioContext* m_AudioContext;
CSLAudioStream* m_AudioStream;
void* m_AudioBuffer;
int m_AudioBufferSize;
int m_MaxQueuedAudioMs;
};
@@ -4,13 +4,29 @@
#include <QtGlobal>
// GFE sends us packets in 5 ms chunks
const double SoundIoAudioRenderer::k_RawSampleLengthSec = 0.005;
#ifdef Q_OS_LINUX
// PulseAudio and ALSA require more than just 5 ms samples
// for some reason, so write a minimum of 25 ms each time to
// prevent underruns on Bluetooth.
// https://github.com/moonlight-stream/moonlight-qt/issues/147
// https://github.com/moonlight-stream/moonlight-qt/issues/157
const double SoundIoAudioRenderer::k_MinSampleLengthSec = 0.025;
#else
// This determines the size of the buffers we'll
// get from CoreAudio. It is also the minimum
// size that we will write when called to fill a buffer.
const double SoundIoAudioRenderer::k_MinSampleLengthSec = k_RawSampleLengthSec;
#endif
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)
{
@@ -72,17 +88,11 @@ int SoundIoAudioRenderer::scoreChannelLayout(const struct SoundIoChannelLayout*
score++;
break;
// Score layouts using the back L/R as higher
// value than those using side L/R.
case SoundIoChannelIdBackLeft:
case SoundIoChannelIdBackRight:
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++;
}
score += 2;
break;
default:
@@ -162,11 +172,9 @@ bool SoundIoAudioRenderer::prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATI
return false;
}
m_AudioPacketDuration = (opusConfig->samplesPerFrame / (opusConfig->sampleRate / 1000)) / 1000.0;
m_OutputStream->format = SoundIoFormatS16NE;
m_OutputStream->sample_rate = opusConfig->sampleRate;
m_OutputStream->software_latency = m_AudioPacketDuration;
m_OutputStream->software_latency = k_MinSampleLengthSec;
m_OutputStream->name = "Moonlight";
m_OutputStream->userdata = this;
m_OutputStream->error_callback = sioErrorCallback;
@@ -217,50 +225,42 @@ bool SoundIoAudioRenderer::prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATI
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;
}
// Fixup the layout to use back L/R so our channel position
// logic in sioWriteCallback() works.
if (m_EffectiveLayout.channels[i] == SoundIoChannelIdSideLeft) {
m_EffectiveLayout.channels[i] = SoundIoChannelIdBackLeft;
}
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;
}
if (m_EffectiveLayout.channels[i] == SoundIoChannelIdSideRight) {
m_EffectiveLayout.channels[i] = SoundIoChannelIdBackRight;
}
}
int packetsToBuffer;
#ifdef Q_OS_LINUX
// PulseAudio and ALSA need the large buffer (see comment on k_MinSampleLengthSec),
// so we need a buffer at least double that size to allow packets to arrive
// while we're writing to the sink.
packetsToBuffer = (int)(k_MinSampleLengthSec / k_RawSampleLengthSec) * 2;
#else
if (m_SoundIo->current_backend == SoundIoBackendWasapi) {
// 15 ms buffer seems to be fine for WASAPI
packetsToBuffer = (int)ceil(0.015 / m_AudioPacketDuration);
packetsToBuffer = 3;
}
else {
// 30 ms buffer on CoreAudio to avoid glitching on macOS
packetsToBuffer = (int)ceil(0.030 / m_AudioPacketDuration);
packetsToBuffer = 6;
}
// Always buffer at least 2 packets
packetsToBuffer = qMax(2, packetsToBuffer);
#endif
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Audio buffer size: %f seconds",
packetsToBuffer * m_AudioPacketDuration);
"Audio buffer size: %d packets",
packetsToBuffer);
m_RingBuffer = soundio_ring_buffer_create(m_SoundIo,
m_OutputStream->bytes_per_sample *
m_OpusChannelCount *
opusConfig->samplesPerFrame *
SAMPLES_PER_FRAME *
packetsToBuffer);
if (m_RingBuffer == nullptr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
@@ -284,8 +284,15 @@ bool SoundIoAudioRenderer::prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATI
return true;
}
void* SoundIoAudioRenderer::getAudioBuffer(int* size)
bool SoundIoAudioRenderer::submitAudio(short* audioBuffer, int audioSize)
{
if (m_Errored) {
return false;
}
// Flush events to update with new device arrivals
soundio_flush_events(m_SoundIo);
// 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
@@ -293,36 +300,13 @@ void* SoundIoAudioRenderer::getAudioBuffer(int* size)
// 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);
int bytesToWrite = qMin(audioSize, (bytesFree / bytesPerFrame) * bytesPerFrame);
memcpy(soundio_ring_buffer_write_ptr(m_RingBuffer), audioBuffer, bytesToWrite);
soundio_ring_buffer_advance_write_ptr(m_RingBuffer, bytesToWrite);
return true;
}
int SoundIoAudioRenderer::getCapabilities()
{
// TODO: Tweak buffer sizes then re-enable arbitrary audio duration
return CAPABILITY_DIRECT_SUBMIT /* | CAPABILITY_SUPPORTS_ARBITRARY_AUDIO_DURATION */;
}
void SoundIoAudioRenderer::sioErrorCallback(SoundIoOutStream* stream, int err)
{
auto me = reinterpret_cast<SoundIoAudioRenderer*>(stream->userdata);
@@ -387,19 +371,15 @@ void SoundIoAudioRenderer::sioWriteCallback(SoundIoOutStream* stream, int frameC
(me->m_OpusChannelCount * stream->bytes_per_sample);
int bytesRead = 0;
// Clamp framesLeft to frameCountMax
framesLeft = qMin(framesLeft, frameCountMax);
// 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 = qMax(frameCountMin, (int)(stream->sample_rate * k_MinSampleLengthSec));
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);
@@ -429,8 +409,9 @@ void SoundIoAudioRenderer::sioWriteCallback(SoundIoOutStream* stream, int frameC
for (int frame = 0; frame < frameCount; frame++) {
for (int ch = 0; ch < me->m_EffectiveLayout.channel_count; ch++) {
// SoundIoChannelId - 1 happens to match Moonlight's channel layout
// after we've applied our fixups to m_EffectiveLayout for 5.1 and 7.1.
// SoundIoChannelId - 1 happens to match Moonlight's channel layout.
// For side L/R, this logic depends on us fixing those up
// in m_EffectiveLayout to back L/R.
int readPtrChannel = me->m_EffectiveLayout.channels[ch] - 1;
if (frame >= framesLeft || readPtrChannel >= me->m_OpusChannelCount) {
@@ -13,11 +13,7 @@ public:
virtual bool prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig);
virtual void* getAudioBuffer(int* size);
virtual bool submitAudio(int bytesWritten);
virtual int getCapabilities();
virtual bool submitAudio(short* audioBuffer, int audioSize);
private:
int scoreChannelLayout(const struct SoundIoChannelLayout* layout, const OPUS_MULTISTREAM_CONFIGURATION* opusConfig);
@@ -36,7 +32,9 @@ private:
struct SoundIoOutStream* m_OutputStream;
struct SoundIoRingBuffer* m_RingBuffer;
struct SoundIoChannelLayout m_EffectiveLayout;
double m_AudioPacketDuration;
double m_Latency;
bool m_Errored;
static const double k_RawSampleLengthSec;
static const double k_MinSampleLengthSec;
};
+997
View File
@@ -0,0 +1,997 @@
#include <Limelight.h>
#include <SDL.h>
#include "streaming/session.h"
#include "settings/mappingmanager.h"
#include "path.h"
#include <QtGlobal>
#include <QDir>
#define VK_0 0x30
#define VK_A 0x41
// These are real Windows VK_* codes
#ifndef VK_F1
#define VK_F1 0x70
#define VK_F13 0x7C
#define VK_NUMPAD0 0x60
#endif
#define MOUSE_POLLING_INTERVAL 5
// How long the mouse button will be pressed for a tap to click gesture
#define TAP_BUTTON_RELEASE_DELAY 100
// How long the fingers must be stationary to start a drag
#define DRAG_ACTIVATION_DELAY 650
// How far the finger can move before it cancels a drag or tap
#define DEAD_ZONE_DELTA 0.1f
const int SdlInputHandler::k_ButtonMap[] = {
A_FLAG, B_FLAG, X_FLAG, Y_FLAG,
BACK_FLAG, SPECIAL_FLAG, PLAY_FLAG,
LS_CLK_FLAG, RS_CLK_FLAG,
LB_FLAG, RB_FLAG,
UP_FLAG, DOWN_FLAG, LEFT_FLAG, RIGHT_FLAG
};
SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, NvComputer*, int streamWidth, int streamHeight)
: m_MultiController(prefs.multiController),
m_MouseMoveTimer(0),
m_LeftButtonReleaseTimer(0),
m_RightButtonReleaseTimer(0),
m_DragTimer(0),
m_DragButton(0),
m_NumFingersDown(0),
m_StreamWidth(streamWidth),
m_StreamHeight(streamHeight)
{
// Allow gamepad input when the app doesn't have focus
SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1");
// If mouse acceleration is enabled, use relative mode warp (which
// is via normal motion events that are influenced by mouse acceleration).
// Otherwise, we'll use raw input capture which is straight from the device
// without modification by the OS.
SDL_SetHint(SDL_HINT_MOUSE_RELATIVE_MODE_WARP,
prefs.mouseAcceleration ? "1" : "0");
// We need to reinit this each time, since you only get
// an initial set of gamepad arrival events once per init.
SDL_assert(!SDL_WasInit(SDL_INIT_GAMECONTROLLER));
if (SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) failed: %s",
SDL_GetError());
}
MappingManager mappingManager;
mappingManager.applyMappings();
// Initialize the gamepad mask with currently attached gamepads to avoid
// causing gamepads to unexpectedly disappear and reappear on the host
// during stream startup as we detect currently attached gamepads one at a time.
m_GamepadMask = getAttachedGamepadMask();
SDL_zero(m_GamepadState);
SDL_zero(m_TouchDownEvent);
SDL_zero(m_CumulativeDelta);
SDL_AtomicSet(&m_MouseDeltaX, 0);
SDL_AtomicSet(&m_MouseDeltaY, 0);
m_MouseMoveTimer = SDL_AddTimer(MOUSE_POLLING_INTERVAL, SdlInputHandler::mouseMoveTimerCallback, this);
}
SdlInputHandler::~SdlInputHandler()
{
for (int i = 0; i < MAX_GAMEPADS; i++) {
if (m_GamepadState[i].controller != nullptr) {
SDL_GameControllerClose(m_GamepadState[i].controller);
}
}
SDL_RemoveTimer(m_MouseMoveTimer);
SDL_RemoveTimer(m_LeftButtonReleaseTimer);
SDL_RemoveTimer(m_RightButtonReleaseTimer);
SDL_RemoveTimer(m_DragTimer);
SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER);
SDL_assert(!SDL_WasInit(SDL_INIT_GAMECONTROLLER));
}
void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
{
short keyCode;
char modifiers;
// Check for our special key combos
if ((event->state == SDL_PRESSED) &&
(event->keysym.mod & KMOD_CTRL) &&
(event->keysym.mod & KMOD_ALT) &&
(event->keysym.mod & KMOD_SHIFT)) {
// Check for quit combo (Ctrl+Alt+Shift+Q)
if (event->keysym.sym == SDLK_q) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Detected quit key combo");
// Push a quit event to the main loop
SDL_Event event;
event.type = SDL_QUIT;
event.quit.timestamp = SDL_GetTicks();
SDL_PushEvent(&event);
return;
}
// Check for the unbind combo (Ctrl+Alt+Shift+Z)
else if (event->keysym.sym == SDLK_z) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Detected mouse capture toggle combo");
// Stop handling future input
SDL_SetRelativeMouseMode((SDL_bool)!SDL_GetRelativeMouseMode());
// Force raise all keys to ensure they aren't stuck,
// since we won't get their key up events.
raiseAllKeys();
return;
}
// Check for the full-screen combo (Ctrl+Alt+Shift+X)
else if (event->keysym.sym == SDLK_x) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Detected full-screen toggle combo");
Session::s_ActiveSession->toggleFullscreen();
// Force raise all keys just be safe across this full-screen/windowed
// transition just in case key events get lost.
raiseAllKeys();
return;
}
}
if (!SDL_GetRelativeMouseMode()) {
// Not capturing
return;
}
// Set modifier flags
modifiers = 0;
if (event->keysym.mod & KMOD_CTRL) {
modifiers |= MODIFIER_CTRL;
}
if (event->keysym.mod & KMOD_ALT) {
modifiers |= MODIFIER_ALT;
}
if (event->keysym.mod & KMOD_SHIFT) {
modifiers |= MODIFIER_SHIFT;
}
// Set keycode. We explicitly use scancode here because GFE will try to correct
// for AZERTY layouts on the host but it depends on receiving VK_ values matching
// a QWERTY layout to work.
if (event->keysym.scancode >= SDL_SCANCODE_1 && event->keysym.scancode <= SDL_SCANCODE_9) {
// SDL defines SDL_SCANCODE_0 > SDL_SCANCODE_9, so we need to handle that manually
keyCode = (event->keysym.scancode - SDL_SCANCODE_1) + VK_0 + 1;
}
else if (event->keysym.scancode >= SDL_SCANCODE_A && event->keysym.scancode <= SDL_SCANCODE_Z) {
keyCode = (event->keysym.scancode - SDL_SCANCODE_A) + VK_A;
}
else if (event->keysym.scancode >= SDL_SCANCODE_F1 && event->keysym.scancode <= SDL_SCANCODE_F12) {
keyCode = (event->keysym.scancode - SDL_SCANCODE_F1) + VK_F1;
}
else if (event->keysym.scancode >= SDL_SCANCODE_F13 && event->keysym.scancode <= SDL_SCANCODE_F24) {
keyCode = (event->keysym.scancode - SDL_SCANCODE_F13) + VK_F13;
}
else if (event->keysym.scancode >= SDL_SCANCODE_KP_1 && event->keysym.scancode <= SDL_SCANCODE_KP_9) {
// SDL defines SDL_SCANCODE_KP_0 > SDL_SCANCODE_KP_9, so we need to handle that manually
keyCode = (event->keysym.scancode - SDL_SCANCODE_KP_1) + VK_NUMPAD0 + 1;
}
else {
switch (event->keysym.scancode) {
case SDL_SCANCODE_BACKSPACE:
keyCode = 0x08;
break;
case SDL_SCANCODE_TAB:
keyCode = 0x09;
break;
case SDL_SCANCODE_CLEAR:
keyCode = 0x0C;
break;
case SDL_SCANCODE_KP_ENTER: // FIXME: Is this correct?
case SDL_SCANCODE_RETURN:
keyCode = 0x0D;
break;
case SDL_SCANCODE_PAUSE:
keyCode = 0x13;
break;
case SDL_SCANCODE_CAPSLOCK:
keyCode = 0x14;
break;
case SDL_SCANCODE_ESCAPE:
keyCode = 0x1B;
break;
case SDL_SCANCODE_SPACE:
keyCode = 0x20;
break;
case SDL_SCANCODE_PAGEUP:
keyCode = 0x21;
break;
case SDL_SCANCODE_PAGEDOWN:
keyCode = 0x22;
break;
case SDL_SCANCODE_END:
keyCode = 0x23;
break;
case SDL_SCANCODE_HOME:
keyCode = 0x24;
break;
case SDL_SCANCODE_LEFT:
keyCode = 0x25;
break;
case SDL_SCANCODE_UP:
keyCode = 0x26;
break;
case SDL_SCANCODE_RIGHT:
keyCode = 0x27;
break;
case SDL_SCANCODE_DOWN:
keyCode = 0x28;
break;
case SDL_SCANCODE_SELECT:
keyCode = 0x29;
break;
case SDL_SCANCODE_EXECUTE:
keyCode = 0x2B;
break;
case SDL_SCANCODE_PRINTSCREEN:
keyCode = 0x2C;
break;
case SDL_SCANCODE_INSERT:
keyCode = 0x2D;
break;
case SDL_SCANCODE_DELETE:
keyCode = 0x2E;
break;
case SDL_SCANCODE_HELP:
keyCode = 0x2F;
break;
case SDL_SCANCODE_KP_0:
// See comment above about why we only handle SDL_SCANCODE_KP_0 here
keyCode = VK_NUMPAD0;
break;
case SDL_SCANCODE_0:
// See comment above about why we only handle SDL_SCANCODE_0 here
keyCode = VK_0;
break;
case SDL_SCANCODE_KP_MULTIPLY:
keyCode = 0x6A;
break;
case SDL_SCANCODE_KP_PLUS:
keyCode = 0x6B;
break;
case SDL_SCANCODE_KP_COMMA:
keyCode = 0x6C;
break;
case SDL_SCANCODE_KP_MINUS:
keyCode = 0x6D;
break;
case SDL_SCANCODE_KP_PERIOD:
keyCode = 0x6E;
break;
case SDL_SCANCODE_KP_DIVIDE:
keyCode = 0x6F;
break;
case SDL_SCANCODE_NUMLOCKCLEAR:
keyCode = 0x90;
break;
case SDL_SCANCODE_SCROLLLOCK:
keyCode = 0x91;
break;
case SDL_SCANCODE_LSHIFT:
keyCode = 0xA0;
break;
case SDL_SCANCODE_RSHIFT:
keyCode = 0xA1;
break;
case SDL_SCANCODE_LCTRL:
keyCode = 0xA2;
break;
case SDL_SCANCODE_RCTRL:
keyCode = 0xA3;
break;
case SDL_SCANCODE_LALT:
keyCode = 0xA4;
break;
case SDL_SCANCODE_RALT:
keyCode = 0xA5;
break;
case SDL_SCANCODE_AC_BACK:
keyCode = 0xA6;
break;
case SDL_SCANCODE_AC_FORWARD:
keyCode = 0xA7;
break;
case SDL_SCANCODE_AC_REFRESH:
keyCode = 0xA8;
break;
case SDL_SCANCODE_AC_STOP:
keyCode = 0xA9;
break;
case SDL_SCANCODE_AC_SEARCH:
keyCode = 0xAA;
break;
case SDL_SCANCODE_AC_BOOKMARKS:
keyCode = 0xAB;
break;
case SDL_SCANCODE_AC_HOME:
keyCode = 0xAC;
break;
case SDL_SCANCODE_SEMICOLON:
keyCode = 0xBA;
break;
case SDL_SCANCODE_EQUALS:
keyCode = 0xBB;
break;
case SDL_SCANCODE_COMMA:
keyCode = 0xBC;
break;
case SDL_SCANCODE_MINUS:
keyCode = 0xBD;
break;
case SDL_SCANCODE_PERIOD:
keyCode = 0xBE;
break;
case SDL_SCANCODE_SLASH:
keyCode = 0xBF;
break;
case SDL_SCANCODE_GRAVE:
keyCode = 0xC0;
break;
case SDL_SCANCODE_LEFTBRACKET:
keyCode = 0xDB;
break;
case SDL_SCANCODE_BACKSLASH:
keyCode = 0xDC;
break;
case SDL_SCANCODE_RIGHTBRACKET:
keyCode = 0xDD;
break;
case SDL_SCANCODE_APOSTROPHE:
keyCode = 0xDE;
break;
case SDL_SCANCODE_NONUSBACKSLASH:
keyCode = 0xE2;
break;
default:
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Unhandled button event: %d",
event->keysym.scancode);
return;
}
}
// Track the key state so we always know which keys are down
if (event->state == SDL_PRESSED) {
m_KeysDown.insert(keyCode);
}
else {
m_KeysDown.remove(keyCode);
}
LiSendKeyboardEvent(keyCode,
event->state == SDL_PRESSED ?
KEY_ACTION_DOWN : KEY_ACTION_UP,
modifiers);
}
void SdlInputHandler::handleMouseButtonEvent(SDL_MouseButtonEvent* event)
{
int button;
// Capture the mouse again if clicked when unbound.
// We start capture on left button released instead of
// pressed to avoid sending an errant mouse button released
// event to the host when clicking into our window (since
// the pressed event was consumed by this code).
if (event->button == SDL_BUTTON_LEFT &&
event->state == SDL_RELEASED &&
!SDL_GetRelativeMouseMode()) {
SDL_SetRelativeMouseMode(SDL_TRUE);
return;
}
else if (!SDL_GetRelativeMouseMode()) {
// Not capturing
return;
}
else if (event->which == SDL_TOUCH_MOUSEID) {
// Ignore synthetic mouse events
return;
}
switch (event->button)
{
case SDL_BUTTON_LEFT:
button = BUTTON_LEFT;
break;
case SDL_BUTTON_MIDDLE:
button = BUTTON_MIDDLE;
break;
case SDL_BUTTON_RIGHT:
button = BUTTON_RIGHT;
break;
case SDL_BUTTON_X1:
button = BUTTON_X1;
break;
case SDL_BUTTON_X2:
button = BUTTON_X2;
break;
default:
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Unhandled button event: %d",
event->button);
return;
}
LiSendMouseButtonEvent(event->state == SDL_PRESSED ?
BUTTON_ACTION_PRESS :
BUTTON_ACTION_RELEASE,
button);
}
void SdlInputHandler::handleMouseMotionEvent(SDL_MouseMotionEvent* event)
{
if (!SDL_GetRelativeMouseMode()) {
// Not capturing
return;
}
else if (event->which == SDL_TOUCH_MOUSEID) {
// Ignore synthetic mouse events
return;
}
// Batch until the next mouse polling window or we'll get awful
// input lag everything except GFE 3.14 and 3.15.
SDL_AtomicAdd(&m_MouseDeltaX, event->xrel);
SDL_AtomicAdd(&m_MouseDeltaY, event->yrel);
}
void SdlInputHandler::handleMouseWheelEvent(SDL_MouseWheelEvent* event)
{
if (!SDL_GetRelativeMouseMode()) {
// Not capturing
return;
}
else if (event->which == SDL_TOUCH_MOUSEID) {
// Ignore synthetic mouse events
return;
}
if (event->y != 0) {
LiSendScrollEvent((signed char)event->y);
}
}
GamepadState*
SdlInputHandler::findStateForGamepad(SDL_JoystickID id)
{
int i;
for (i = 0; i < MAX_GAMEPADS; i++) {
if (m_GamepadState[i].jsId == id) {
SDL_assert(!m_MultiController || m_GamepadState[i].index == i);
return &m_GamepadState[i];
}
}
// This should only happen with > 4 gamepads
SDL_assert(SDL_NumJoysticks() > 4);
return nullptr;
}
void SdlInputHandler::sendGamepadState(GamepadState* state)
{
SDL_assert(m_GamepadMask == 0x1 || m_MultiController);
LiSendMultiControllerEvent(state->index,
m_GamepadMask,
state->buttons,
state->lt,
state->rt,
state->lsX,
state->lsY,
state->rsX,
state->rsY);
}
Uint32 SdlInputHandler::releaseLeftButtonTimerCallback(Uint32, void*)
{
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
return 0;
}
Uint32 SdlInputHandler::releaseRightButtonTimerCallback(Uint32, void*)
{
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_RIGHT);
return 0;
}
Uint32 SdlInputHandler::dragTimerCallback(Uint32, void *param)
{
auto me = reinterpret_cast<SdlInputHandler*>(param);
// Check how many fingers are down now to decide
// which button to hold down
if (me->m_NumFingersDown == 2) {
me->m_DragButton = BUTTON_RIGHT;
}
else if (me->m_NumFingersDown == 1) {
me->m_DragButton = BUTTON_LEFT;
}
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, me->m_DragButton);
return 0;
}
Uint32 SdlInputHandler::mouseMoveTimerCallback(Uint32 interval, void *param)
{
auto me = reinterpret_cast<SdlInputHandler*>(param);
short deltaX = (short)SDL_AtomicSet(&me->m_MouseDeltaX, 0);
short deltaY = (short)SDL_AtomicSet(&me->m_MouseDeltaY, 0);
if (deltaX != 0 || deltaY != 0) {
LiSendMouseMoveEvent(deltaX, deltaY);
}
return interval;
}
void SdlInputHandler::handleControllerAxisEvent(SDL_ControllerAxisEvent* event)
{
GamepadState* state = findStateForGamepad(event->which);
if (state == NULL) {
return;
}
switch (event->axis)
{
case SDL_CONTROLLER_AXIS_LEFTX:
state->lsX = event->value;
break;
case SDL_CONTROLLER_AXIS_LEFTY:
// Signed values have one more negative value than
// positive value, so inverting the sign on -32768
// could actually cause the value to overflow and
// wrap around to be negative again. Avoid that by
// capping the value at 32767.
state->lsY = -qMax(event->value, (short)-32767);
break;
case SDL_CONTROLLER_AXIS_RIGHTX:
state->rsX = event->value;
break;
case SDL_CONTROLLER_AXIS_RIGHTY:
state->rsY = -qMax(event->value, (short)-32767);
break;
case SDL_CONTROLLER_AXIS_TRIGGERLEFT:
state->lt = (unsigned char)(event->value * 255UL / 32767);
break;
case SDL_CONTROLLER_AXIS_TRIGGERRIGHT:
state->rt = (unsigned char)(event->value * 255UL / 32767);
break;
default:
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Unhandled controller axis: %d",
event->axis);
return;
}
sendGamepadState(state);
}
void SdlInputHandler::handleControllerButtonEvent(SDL_ControllerButtonEvent* event)
{
GamepadState* state = findStateForGamepad(event->which);
if (state == NULL) {
return;
}
if (event->state == SDL_PRESSED) {
state->buttons |= k_ButtonMap[event->button];
}
else {
state->buttons &= ~k_ButtonMap[event->button];
}
// Handle Start+Select+L1+R1 as a gamepad quit combo
if (state->buttons == (PLAY_FLAG | BACK_FLAG | LB_FLAG | RB_FLAG)) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Detected quit gamepad button combo");
// Push a quit event to the main loop
SDL_Event event;
event.type = SDL_QUIT;
event.quit.timestamp = SDL_GetTicks();
SDL_PushEvent(&event);
// Clear buttons down on this gameapd
LiSendMultiControllerEvent(state->index, m_GamepadMask,
0, 0, 0, 0, 0, 0, 0);
return;
}
sendGamepadState(state);
}
void SdlInputHandler::handleControllerDeviceEvent(SDL_ControllerDeviceEvent* event)
{
GamepadState* state;
if (event->type == SDL_CONTROLLERDEVICEADDED) {
int i;
const char* name;
SDL_GameController* controller;
const char* mapping;
char guidStr[33];
controller = SDL_GameControllerOpen(event->which);
if (controller == NULL) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to open gamepad: %s",
SDL_GetError());
return;
}
#if SDL_VERSION_ATLEAST(2, 0, 9)
// Determine this player's preferred index
i = SDL_GameControllerGetPlayerIndex(controller);
if (i < 0 || i >= MAX_GAMEPADS || m_GamepadState[i].controller != NULL) {
// If the player index is unavailable or invalid, start searching from 0
i = 0;
}
#else
// SDL 2.0.8 and earlier has no player number hints
i = 0;
#endif
for (; i < MAX_GAMEPADS; i++) {
if (m_GamepadState[i].controller == NULL) {
// Found an empty slot
break;
}
}
if (i == MAX_GAMEPADS) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"No open gamepad slots found!");
SDL_GameControllerClose(controller);
return;
}
state = &m_GamepadState[i];
if (m_MultiController) {
state->index = i;
}
else {
// Always player 1 in single controller mode
state->index = 0;
}
state->controller = controller;
state->jsId = SDL_JoystickInstanceID(SDL_GameControllerGetJoystick(state->controller));
SDL_JoystickGetGUIDString(SDL_JoystickGetGUID(SDL_GameControllerGetJoystick(state->controller)),
guidStr, sizeof(guidStr));
mapping = SDL_GameControllerMapping(state->controller);
name = SDL_GameControllerName(state->controller);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Gamepad %d (player %d) is: %s (%s -> %s)",
i,
state->index,
name != nullptr ? name : "<null>",
guidStr,
mapping != nullptr ? mapping : "<null>");
if (mapping != nullptr) {
SDL_free((void*)mapping);
}
// Add this gamepad to the gamepad mask
if (m_MultiController) {
// NB: Don't assert that it's unset here because we will already
// have the mask set for initially attached gamepads to avoid confusing
// apps running on the host.
m_GamepadMask |= (1 << state->index);
}
else {
SDL_assert(m_GamepadMask == 0x1);
}
// Send an empty event to tell the PC we've arrived
sendGamepadState(state);
}
else if (event->type == SDL_CONTROLLERDEVICEREMOVED) {
state = findStateForGamepad(event->which);
if (state != NULL) {
SDL_GameControllerClose(state->controller);
// Remove this from the gamepad mask in MC-mode
if (m_MultiController) {
SDL_assert(m_GamepadMask & (1 << state->index));
m_GamepadMask &= ~(1 << state->index);
}
else {
SDL_assert(m_GamepadMask == 0x1);
}
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Gamepad %d is gone",
state->index);
// Send a final event to let the PC know this gamepad is gone
LiSendMultiControllerEvent(state->index, m_GamepadMask,
0, 0, 0, 0, 0, 0, 0);
// Clear all remaining state from this slot
SDL_memset(state, 0, sizeof(*state));
}
}
}
void SdlInputHandler::handleJoystickArrivalEvent(SDL_JoyDeviceEvent* event)
{
SDL_assert(event->type == SDL_JOYDEVICEADDED);
if (!SDL_IsGameController(event->which)) {
char guidStr[33];
SDL_JoystickGetGUIDString(SDL_JoystickGetDeviceGUID(event->which),
guidStr, sizeof(guidStr));
const char* name = SDL_JoystickNameForIndex(event->which);
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Joystick discovered with no mapping: %s %s",
name ? name : "<UNKNOWN>",
guidStr);
SDL_Joystick* joy = SDL_JoystickOpen(event->which);
if (joy != nullptr) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Number of axes: %d | Number of buttons: %d | Number of hats: %d",
SDL_JoystickNumAxes(joy), SDL_JoystickNumButtons(joy),
SDL_JoystickNumHats(joy));
SDL_JoystickClose(joy);
}
else {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Unable to open joystick for query: %s",
SDL_GetError());
}
}
}
void SdlInputHandler::handleTouchFingerEvent(SDL_TouchFingerEvent* event)
{
int fingerIndex = -1;
// Observations on Windows 10: x and y appear to be relative to 0,0 of the window client area.
// Although SDL documentation states they are 0.0 - 1.0 float values, they can actually be higher
// or lower than those values as touch events continue for touches started within the client area that
// leave the client area during a drag motion.
// dx and dy are deltas from the last touch event, not the first touch down.
// Determine the index of this finger using our list
// of fingers that are currently active on screen.
// This is also required to handle finger up which
// where the finger will not be in SDL_GetTouchFinger()
// anymore.
if (event->type != SDL_FINGERDOWN) {
for (int i = 0; i < MAX_FINGERS; i++) {
if (event->fingerId == m_TouchDownEvent[i].fingerId) {
fingerIndex = i;
break;
}
}
}
else {
// Resolve the new finger by determining the ID of each
// finger on the display.
int numTouchFingers = SDL_GetNumTouchFingers(event->touchId);
for (int i = 0; i < numTouchFingers; i++) {
SDL_Finger* finger = SDL_GetTouchFinger(event->touchId, i);
SDL_assert(finger != nullptr);
if (finger != nullptr) {
if (finger->id == event->fingerId) {
fingerIndex = i;
break;
}
}
}
}
if (fingerIndex < 0 || fingerIndex >= MAX_FINGERS) {
// Too many fingers
return;
}
// Handle cursor motion based on the position of the
// primary finger on screen
if (fingerIndex == 0) {
// The event x and y values are relative to our window width
// and height. However, we want to scale them to be relative
// to the host resolution. Fortunately this is easy since we
// already have normalized values. We'll just multiply them
// by the stream dimensions to get real X and Y values rather
// than the client window dimensions.
short deltaX = static_cast<short>(event->dx * m_StreamWidth);
short deltaY = static_cast<short>(event->dy * m_StreamHeight);
if (deltaX != 0 || deltaY != 0) {
LiSendMouseMoveEvent(deltaX, deltaY);
}
}
// Start a drag timer when primary or secondary
// fingers go down
if (event->type == SDL_FINGERDOWN &&
(fingerIndex == 0 || fingerIndex == 1)) {
SDL_RemoveTimer(m_DragTimer);
m_DragTimer = SDL_AddTimer(DRAG_ACTIVATION_DELAY,
dragTimerCallback,
this);
}
if (event->type == SDL_FINGERMOTION) {
// Count the total cumulative dx/dy that the finger
// has moved.
m_CumulativeDelta[fingerIndex] += qAbs(event->x);
m_CumulativeDelta[fingerIndex] += qAbs(event->y);
// If it's outside the deadzone delta, cancel drags and taps
if (m_CumulativeDelta[fingerIndex] > DEAD_ZONE_DELTA) {
SDL_RemoveTimer(m_DragTimer);
m_DragTimer = 0;
// This effectively cancels the tap logic below
m_TouchDownEvent[fingerIndex].timestamp = 0;
}
}
if (event->type == SDL_FINGERUP) {
// Cancel the drag timer on finger up
SDL_RemoveTimer(m_DragTimer);
m_DragTimer = 0;
// Release any drag
if (m_DragButton != 0) {
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, m_DragButton);
m_DragButton = 0;
}
// 2 finger tap
else if (event->timestamp - m_TouchDownEvent[1].timestamp < 250) {
// Zero timestamp of the primary finger to ensure we won't
// generate a left click if the primary finger comes up soon.
m_TouchDownEvent[0].timestamp = 0;
// Press down the right mouse button
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_RIGHT);
// Queue a timer to release it in 100 ms
SDL_RemoveTimer(m_RightButtonReleaseTimer);
m_RightButtonReleaseTimer = SDL_AddTimer(TAP_BUTTON_RELEASE_DELAY,
releaseRightButtonTimerCallback,
nullptr);
}
// 1 finger tap
else if (event->timestamp - m_TouchDownEvent[0].timestamp < 250) {
// Press down the left mouse button
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT);
// Queue a timer to release it in 100 ms
SDL_RemoveTimer(m_LeftButtonReleaseTimer);
m_LeftButtonReleaseTimer = SDL_AddTimer(TAP_BUTTON_RELEASE_DELAY,
releaseLeftButtonTimerCallback,
nullptr);
}
}
m_NumFingersDown = SDL_GetNumTouchFingers(event->touchId);
if (event->type == SDL_FINGERDOWN) {
m_TouchDownEvent[fingerIndex] = *event;
m_CumulativeDelta[fingerIndex] = 0;
}
else if (event->type == SDL_FINGERUP) {
m_TouchDownEvent[fingerIndex] = {};
}
}
int SdlInputHandler::getAttachedGamepadMask()
{
int count;
int mask;
if (!m_MultiController) {
// Player 1 is always present in non-MC mode
return 0x1;
}
// TODO: Use SDL_GameControllerGetPlayerIndex() for accurate indexes?
count = mask = 0;
for (int i = 0; i < SDL_NumJoysticks(); i++) {
if (SDL_IsGameController(i)) {
mask |= (1 << count++);
}
}
return mask;
}
void SdlInputHandler::raiseAllKeys()
{
if (m_KeysDown.isEmpty()) {
return;
}
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Raising %d keys",
m_KeysDown.count());
for (auto keyDown : m_KeysDown) {
LiSendKeyboardEvent(keyDown, KEY_ACTION_UP, 0);
}
m_KeysDown.clear();
}
QString SdlInputHandler::getUnmappedGamepads()
{
QString ret;
if (SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) failed: %s",
SDL_GetError());
}
MappingManager mappingManager;
mappingManager.applyMappings();
for (int i = 0; i < SDL_NumJoysticks(); i++) {
if (!SDL_IsGameController(i)) {
char guidStr[33];
SDL_JoystickGetGUIDString(SDL_JoystickGetDeviceGUID(i),
guidStr, sizeof(guidStr));
const char* name = SDL_JoystickNameForIndex(i);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Unmapped joystick: %s %s",
name ? name : "<UNKNOWN>",
guidStr);
SDL_Joystick* joy = SDL_JoystickOpen(i);
if (joy != nullptr) {
int numButtons = SDL_JoystickNumButtons(joy);
int numHats = SDL_JoystickNumHats(joy);
int numAxes = SDL_JoystickNumAxes(joy);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Number of axes: %d | Number of buttons: %d | Number of hats: %d",
numAxes, numButtons, numHats);
if ((numAxes >= 4 && numAxes <= 8) && numButtons >= 8 && numHats <= 1) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Joystick likely to be an unmapped game controller");
if (!ret.isEmpty()) {
ret += ", ";
}
ret += name;
}
SDL_JoystickClose(joy);
}
else {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Unable to open joystick for query: %s",
SDL_GetError());
}
}
}
SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER);
return ret;
}
+92
View File
@@ -0,0 +1,92 @@
#pragma once
#include "settings/streamingpreferences.h"
#include "backend/computermanager.h"
#include <SDL.h>
struct GamepadState {
SDL_GameController* controller;
SDL_JoystickID jsId;
short index;
short buttons;
short lsX, lsY;
short rsX, rsY;
unsigned char lt, rt;
};
#define MAX_GAMEPADS 4
#define MAX_FINGERS 2
class SdlInputHandler
{
public:
explicit SdlInputHandler(StreamingPreferences& prefs, NvComputer* computer,
int streamWidth, int streamHeight);
~SdlInputHandler();
void handleKeyEvent(SDL_KeyboardEvent* event);
void handleMouseButtonEvent(SDL_MouseButtonEvent* event);
void handleMouseMotionEvent(SDL_MouseMotionEvent* event);
void handleMouseWheelEvent(SDL_MouseWheelEvent* event);
void handleControllerAxisEvent(SDL_ControllerAxisEvent* event);
void handleControllerButtonEvent(SDL_ControllerButtonEvent* event);
void handleControllerDeviceEvent(SDL_ControllerDeviceEvent* event);
void handleJoystickArrivalEvent(SDL_JoyDeviceEvent* event);
void handleTouchFingerEvent(SDL_TouchFingerEvent* event);
int getAttachedGamepadMask();
void raiseAllKeys();
static
QString getUnmappedGamepads();
private:
GamepadState*
findStateForGamepad(SDL_JoystickID id);
void sendGamepadState(GamepadState* state);
static
Uint32 releaseLeftButtonTimerCallback(Uint32 interval, void* param);
static
Uint32 releaseRightButtonTimerCallback(Uint32 interval, void* param);
static
Uint32 dragTimerCallback(Uint32 interval, void* param);
static
Uint32 mouseMoveTimerCallback(Uint32 interval, void* param);
bool m_MultiController;
SDL_TimerID m_MouseMoveTimer;
SDL_atomic_t m_MouseDeltaX;
SDL_atomic_t m_MouseDeltaY;
int m_GamepadMask;
GamepadState m_GamepadState[MAX_GAMEPADS];
QSet<short> m_KeysDown;
SDL_TouchFingerEvent m_TouchDownEvent[MAX_FINGERS];
float m_CumulativeDelta[MAX_FINGERS];
SDL_TimerID m_LeftButtonReleaseTimer;
SDL_TimerID m_RightButtonReleaseTimer;
SDL_TimerID m_DragTimer;
char m_DragButton;
int m_NumFingersDown;
int m_StreamWidth;
int m_StreamHeight;
static const int k_ButtonMap[];
};
-107
View File
@@ -1,107 +0,0 @@
#include "input.h"
#include <Limelight.h>
#include <SDL.h>
#include "streaming/streamutils.h"
#include <QtMath>
// How long the fingers must be stationary to start a right click
#define LONG_PRESS_ACTIVATION_DELAY 650
// How far the finger can move before it cancels a right click
#define LONG_PRESS_ACTIVATION_DELTA 0.01f
// How long the double tap deadzone stays in effect between touch up and touch down
#define DOUBLE_TAP_DEAD_ZONE_DELAY 250
// How far the finger can move before it can override the double tap deadzone
#define DOUBLE_TAP_DEAD_ZONE_DELTA 0.025f
Uint32 SdlInputHandler::longPressTimerCallback(Uint32, void*)
{
// Raise the left click and start a right click
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_RIGHT);
return 0;
}
void SdlInputHandler::handleAbsoluteFingerEvent(SDL_TouchFingerEvent* event)
{
// Observations on Windows 10: x and y appear to be relative to 0,0 of the window client area.
// Although SDL documentation states they are 0.0 - 1.0 float values, they can actually be higher
// or lower than those values as touch events continue for touches started within the client area that
// leave the client area during a drag motion.
// dx and dy are deltas from the last touch event, not the first touch down.
// Ignore touch down events with more than one finger
if (event->type == SDL_FINGERDOWN && SDL_GetNumTouchFingers(event->touchId) > 1) {
return;
}
// Ignore touch move and touch up events from the non-primary finger
if (event->type != SDL_FINGERDOWN && event->fingerId != m_LastTouchDownEvent.fingerId) {
return;
}
SDL_Rect src, dst;
int windowWidth, windowHeight;
SDL_GetWindowSize(m_Window, &windowWidth, &windowHeight);
src.x = src.y = 0;
src.w = m_StreamWidth;
src.h = m_StreamHeight;
dst.x = dst.y = 0;
dst.w = windowWidth;
dst.h = windowHeight;
// Use the stream and window sizes to determine the video region
StreamUtils::scaleSourceToDestinationSurface(&src, &dst);
if (qSqrt(qPow(event->x - m_LastTouchDownEvent.x, 2) + qPow(event->y - m_LastTouchDownEvent.y, 2)) > LONG_PRESS_ACTIVATION_DELTA) {
// Moved too far since touch down. Cancel the long press timer.
SDL_RemoveTimer(m_LongPressTimer);
m_LongPressTimer = 0;
}
// Don't reposition for finger down events within the deadzone. This makes double-clicking easier.
if (event->type != SDL_FINGERDOWN ||
event->timestamp - m_LastTouchUpEvent.timestamp > DOUBLE_TAP_DEAD_ZONE_DELAY ||
qSqrt(qPow(event->x - m_LastTouchUpEvent.x, 2) + qPow(event->y - m_LastTouchUpEvent.y, 2)) > DOUBLE_TAP_DEAD_ZONE_DELTA) {
// Scale window-relative events to be video-relative and clamp to video region
short x = qMin(qMax((int)(event->x * windowWidth), dst.x), dst.x + dst.w);
short y = qMin(qMax((int)(event->y * windowHeight), dst.y), dst.y + dst.h);
// Update the cursor position relative to the video region
LiSendMousePositionEvent(x - dst.x, y - dst.y, dst.w, dst.h);
}
if (event->type == SDL_FINGERDOWN) {
m_LastTouchDownEvent = *event;
// Start/restart the long press timer
SDL_RemoveTimer(m_LongPressTimer);
m_LongPressTimer = SDL_AddTimer(LONG_PRESS_ACTIVATION_DELAY,
longPressTimerCallback,
nullptr);
// Left button down on finger down
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT);
}
else if (event->type == SDL_FINGERUP) {
m_LastTouchUpEvent = *event;
// Cancel the long press timer
SDL_RemoveTimer(m_LongPressTimer);
m_LongPressTimer = 0;
// Left button up on finger up
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
// Raise right button too in case we triggered a long press gesture
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_RIGHT);
}
}

Some files were not shown because too many files have changed in this diff Show More