Compare commits

..
1 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
804 changed files with 184908 additions and 42054 deletions

No files matched your search

+5 -20
View File
@@ -1,12 +1,8 @@
--- ---
name: Bug report name: Bug report
about: Follow the troubleshooting guide before reporting a bug about: Create a report to help us address broken functionality
--- ---
**READ ME FIRST!**
If you're here because something basic is not working (like gamepad input, video, or similar), it's probably something specific to your setup, so make sure you've gone through the Troubleshooting Guide first: https://github.com/moonlight-stream/moonlight-docs/wiki/Troubleshooting
If you still have trouble with basic functionality after following the guide, join our Discord server where there are many other volunteers who can help (or direct you back here if it looks like a Moonlight bug after all). https://moonlight-stream.org/discord
**Describe the bug** **Describe the bug**
A clear and concise description of what the bug is. A clear and concise description of what the bug is.
@@ -20,35 +16,24 @@ If applicable, add screenshots to help explain your problem. If the issue is rel
**Affected games** **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. 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)** **Moonlight settings (please complete the following information)**
- Have any settings been adjusted from defaults? - Have any settings been adjusted from defaults?
- If so, which settings have been changed? - If so, which settings have been changed?
- Does the problem still occur after reverting settings back to default? - Does the problem still occur after reverting settings back to default?
**Gamepad-related issues (please complete if problem is gamepad-related)**
- Do you have any gamepads connected to your host PC directly?
- Does the problem still remain if you stream the desktop and use https://html5gamepad.com to test your gamepad?
- Instructions for streaming the desktop can be found here: https://github.com/moonlight-stream/moonlight-docs/wiki/Setup-Guide
**Client PC details (please complete the following information)** **Client PC details (please complete the following information)**
- OS: [e.g. Windows 10 1809] - OS: [e.g. Windows 10 1803]
- Moonlight Version: [e.g. v0.9.0] - Moonlight Version: [e.g. v0.2.0]
- GPU: [e.g. Intel HD Graphics 520]
**Server PC details (please complete the following information)** **Server PC details (please complete the following information)**
- OS: [e.g. Windows 10 1809] - OS: [e.g. Windows 10 1803]
- GeForce Experience version: [e.g. 3.16.0.140] - GeForce Experience version: [e.g. 3.16.0.140]
- Nvidia GPU driver: [e.g. 417.35] - Nvidia GPU driver: [e.g. 417.35]
- Antivirus and firewall software: [e.g. Windows Defender and Windows Firewall]
**Moonlight Logs (please attach)** **Moonlight Logs (please attach)**
- On Windows, `Moonlight-###.log` files can be found in `%TEMP%`. Simply type that into the File Explorer path field to navigate there. - On Windows, `Moonlight-###.log` files can be found in `%TEMP%`. Simply type that into the File Explorer path field to navigate there.
- On macOS, `Moonlight-###.log` files can be found in `/tmp`. In Finder, press Cmd+Shift+G, then type `/tmp` 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, logs print to the terminal when running via `flatpak run com.moonlight_stream.Moonlight`
- On Linux with the Snap, logs print to the terminal when running with the command: `moonlight`
**Additional context** **Additional context**
Anything else you think may be relevant to the issue 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 -5
View File
@@ -9,11 +9,7 @@
url = https://github.com/gabomdq/SDL_GameControllerDB.git url = https://github.com/gabomdq/SDL_GameControllerDB.git
[submodule "soundio/libsoundio"] [submodule "soundio/libsoundio"]
path = soundio/libsoundio path = soundio/libsoundio
url = https://github.com/cgutman/libsoundio.git url = https://github.com/andrewrk/libsoundio.git
[submodule "h264bitstream/h264bitstream"] [submodule "h264bitstream/h264bitstream"]
path = h264bitstream/h264bitstream path = h264bitstream/h264bitstream
url = https://github.com/aizvorski/h264bitstream.git url = https://github.com/aizvorski/h264bitstream.git
[submodule "libs"]
path = libs
url = https://github.com/cgutman/moonlight-qt-prebuilts.git
shallow = true
+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
+6 -5
View File
@@ -3,7 +3,11 @@ QT -= core gui
TARGET = AntiHooking TARGET = AntiHooking
TEMPLATE = lib 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 INCLUDEPATH += $$PWD/../libs/windows/include
contains(QT_ARCH, i386) { contains(QT_ARCH, i386) {
@@ -12,11 +16,8 @@ contains(QT_ARCH, i386) {
contains(QT_ARCH, x86_64) { contains(QT_ARCH, x86_64) {
LIBS += -L$$PWD/../libs/windows/lib/x64 LIBS += -L$$PWD/../libs/windows/lib/x64
} }
contains(QT_ARCH, arm64) {
LIBS += -L$$PWD/../libs/windows/lib/arm64
}
LIBS += -ldetours LIBS += -lNktHookLib
DEFINES += ANTIHOOKING_LIBRARY DEFINES += ANTIHOOKING_LIBRARY
SOURCES += antihookingprotection.cpp SOURCES += antihookingprotection.cpp
HEADERS += antihookingprotection.h HEADERS += antihookingprotection.h
+25 -119
View File
@@ -1,9 +1,6 @@
#include "antihookingprotection.h" #include "antihookingprotection.h"
#define WIN32_LEAN_AND_MEAN #include <NktHookLib.h>
#include <Windows.h>
#include <detours.h>
typedef HMODULE (WINAPI *LoadLibraryAFunc)(LPCSTR lpLibFileName); typedef HMODULE (WINAPI *LoadLibraryAFunc)(LPCSTR lpLibFileName);
typedef HMODULE (WINAPI *LoadLibraryWFunc)(LPCWSTR lpLibFileName); typedef HMODULE (WINAPI *LoadLibraryWFunc)(LPCWSTR lpLibFileName);
@@ -15,35 +12,25 @@ class AntiHookingProtection
public: public:
static void enable() static void enable()
{ {
DetourTransactionBegin(); #ifdef QT_DEBUG
DetourUpdateThread(GetCurrentThread()); s_HookManager.SetEnableDebugOutput(true);
#endif
s_RealLoadLibraryA = LoadLibraryA; HINSTANCE kernel32Handle = NktHookLibHelpers::GetModuleBaseAddress(L"kernel32.dll");
DetourAttach(&(PVOID&)s_RealLoadLibraryA, LoadLibraryAHook); SIZE_T hookId;
s_RealLoadLibraryW = LoadLibraryW; s_HookManager.Hook(&hookId, (LPVOID*)&s_RealLoadLibraryA,
DetourAttach(&(PVOID&)s_RealLoadLibraryW, LoadLibraryWHook); NktHookLibHelpers::GetProcedureAddress(kernel32Handle, "LoadLibraryA"),
(LPVOID)AntiHookingProtection::LoadLibraryAHook);
s_RealLoadLibraryExA = LoadLibraryExA; s_HookManager.Hook(&hookId, (LPVOID*)&s_RealLoadLibraryW,
DetourAttach(&(PVOID&)s_RealLoadLibraryExA, LoadLibraryExAHook); NktHookLibHelpers::GetProcedureAddress(kernel32Handle, "LoadLibraryW"),
(LPVOID)AntiHookingProtection::LoadLibraryWHook);
s_RealLoadLibraryExW = LoadLibraryExW; s_HookManager.Hook(&hookId, (LPVOID*)&s_RealLoadLibraryExA,
DetourAttach(&(PVOID&)s_RealLoadLibraryExW, LoadLibraryExWHook); NktHookLibHelpers::GetProcedureAddress(kernel32Handle, "LoadLibraryExA"),
(LPVOID)AntiHookingProtection::LoadLibraryExAHook);
DetourTransactionCommit(); s_HookManager.Hook(&hookId, (LPVOID*)&s_RealLoadLibraryExW,
} NktHookLibHelpers::GetProcedureAddress(kernel32Handle, "LoadLibraryExW"),
(LPVOID)AntiHookingProtection::LoadLibraryExWHook);
static void disable()
{
DetourTransactionBegin();
DetourUpdateThread(GetCurrentThread());
DetourDetach(&(PVOID&)s_RealLoadLibraryA, LoadLibraryAHook);
DetourDetach(&(PVOID&)s_RealLoadLibraryW, LoadLibraryWHook);
DetourDetach(&(PVOID&)s_RealLoadLibraryExA, LoadLibraryExAHook);
DetourDetach(&(PVOID&)s_RealLoadLibraryExW, LoadLibraryExWHook);
DetourTransactionCommit();
} }
private: private:
@@ -137,90 +124,21 @@ private:
return s_RealLoadLibraryExW(lpLibFileName, hFile, dwFlags); return s_RealLoadLibraryExW(lpLibFileName, hFile, dwFlags);
} }
static CNktHookLib s_HookManager;
static LoadLibraryAFunc s_RealLoadLibraryA; static LoadLibraryAFunc s_RealLoadLibraryA;
static LoadLibraryWFunc s_RealLoadLibraryW; static LoadLibraryWFunc s_RealLoadLibraryW;
static LoadLibraryExAFunc s_RealLoadLibraryExA; static LoadLibraryExAFunc s_RealLoadLibraryExA;
static LoadLibraryExWFunc s_RealLoadLibraryExW; static LoadLibraryExWFunc s_RealLoadLibraryExW;
static constexpr LPCWSTR k_BlacklistedDlls[] = { static constexpr LPCWSTR k_BlacklistedDlls[] = {
// These A-Volute DLLs shipped with various audio driver packages improperly handle // This DLL shipped with ASUS Sonic Radar 3 improperly handles
// D3D9 exclusive fullscreen in a way that causes CreateDeviceEx() to deadlock. // D3D9 exclusive fullscreen in a way that causes CreateDeviceEx()
// https://github.com/moonlight-stream/moonlight-qt/issues/102 // to deadlock. https://github.com/moonlight-stream/moonlight-qt/issues/102
L"NahimicOSD.dll", // ASUS Sonic Radar 3 L"NahimicOSD.dll"
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",
}; };
}; };
CNktHookLib AntiHookingProtection::s_HookManager;
LoadLibraryAFunc AntiHookingProtection::s_RealLoadLibraryA; LoadLibraryAFunc AntiHookingProtection::s_RealLoadLibraryA;
LoadLibraryWFunc AntiHookingProtection::s_RealLoadLibraryW; LoadLibraryWFunc AntiHookingProtection::s_RealLoadLibraryW;
LoadLibraryExAFunc AntiHookingProtection::s_RealLoadLibraryExA; LoadLibraryExAFunc AntiHookingProtection::s_RealLoadLibraryExA;
@@ -229,26 +147,14 @@ LoadLibraryExWFunc AntiHookingProtection::s_RealLoadLibraryExW;
AH_EXPORT void AntiHookingDummyImport() {} AH_EXPORT void AntiHookingDummyImport() {}
extern "C" extern "C"
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved) BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID)
{ {
if (DetourIsHelperProcess()) {
return TRUE;
}
switch (fdwReason) switch (fdwReason)
{ {
case DLL_PROCESS_ATTACH: case DLL_PROCESS_ATTACH:
DetourRestoreAfterWith();
AntiHookingProtection::enable(); AntiHookingProtection::enable();
DisableThreadLibraryCalls(hinstDLL); DisableThreadLibraryCalls(hinstDLL);
break; break;
case DLL_PROCESS_DETACH:
// Ignore DLL_PROCESS_DETACH on process exit. No need to waste time
// unhooking everything if the whole process is being destroyed.
if (lpvReserved == NULL) {
AntiHookingProtection::disable();
}
break;
} }
return TRUE; return TRUE;
+24 -49
View File
@@ -1,83 +1,58 @@
# Moonlight PC # Moonlight PC
[Moonlight PC](https://moonlight-stream.org) is an open source PC client for NVIDIA GameStream, as used by the NVIDIA Shield. [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) and help translate Moonlight into your language on [Weblate](https://hosted.weblate.org/projects/moonlight/moonlight-qt/). 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) [![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)
[![Downloads](https://img.shields.io/github/downloads/moonlight-stream/moonlight-qt/total)](https://github.com/moonlight-stream/moonlight-qt/releases) [![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)
[![Translation Status](https://hosted.weblate.org/widgets/moonlight/-/moonlight-qt/svg-badge.svg)](https://hosted.weblate.org/projects/moonlight/moonlight-qt/)
## Features ## Features
- Hardware accelerated video decoding on Windows, Mac, and Linux - Hardware accelerated video decoding on Windows, Mac, and Linux
- Supports streaming at up to 120 FPS (high refresh rate monitor recommended) - 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 - Supports streaming at 720p, 1080p, 1440p, 4K, and the client PC's native screen resolution
- HDR streaming support - 5.1 surround sound audio
- 7.1 surround sound audio support
- Support for both pointer capture (for games) and direct mouse control (for remote desktop)
- Support for passing system-wide keyboard shortcuts like Alt+Tab to the host
- Direct touchscreen mouse control
- HEVC support for better image quality at reduced bandwidth - HEVC support for better image quality at reduced bandwidth
- Gamepad support with force feedback for up to 4 players - Keyboard and mouse support
- Gamepad support with SDL gamepad mappings
## Downloads ## Downloads
- [Windows, macOS, and Steam Link](https://github.com/moonlight-stream/moonlight-qt/releases) - [Windows and macOS](https://github.com/moonlight-stream/moonlight-qt/releases)
- [Snap (for Ubuntu-based Linux distros)](https://snapcraft.io/moonlight) - [Linux](https://flathub.org/apps/details/com.moonlight_stream.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)
#### Special Thanks
[![Hosted By: Cloudsmith](https://img.shields.io/badge/OSS%20hosting%20by-cloudsmith-blue?logo=cloudsmith&style=flat-square)](https://cloudsmith.com)
Hosting for Moonlight's Raspberry Pi and L4T package repositories is graciously provided for free by [Cloudsmith](https://cloudsmith.com).
## Building ## Building
### General Build Requirements
* Qt 5.9 SDK or later
### Windows Build Requirements ### Windows-specific Requirements
* Qt 5.15 SDK or later
* Windows 7 or later * Windows 7 or later
* [Visual Studio 2019](https://visualstudio.microsoft.com/downloads/) (Community edition is fine) * Visual Studio 2015 or later
* Select **MSVC 2019** option during Qt installation. MinGW is not supported. * Select MSVC Desktop toolchain during Qt installation
* [7-Zip](https://www.7-zip.org/) (only if building installers for non-development PCs) * 7-Zip (only if building installers for non-development PCs)
* [WiX Toolset](https://wixtoolset.org/releases/v3-14-0-5722/) v3.14.0.5722 or later (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 ### Mac-specific Requirements
* Qt 6.2 SDK or later * macOS Sierra (10.12) or later
* macOS Mojave (10.14) or later * Xcode 10
* Xcode 13
* [create-dmg](https://github.com/sindresorhus/create-dmg) (only if building DMGs for use on non-development Macs) * [create-dmg](https://github.com/sindresorhus/create-dmg) (only if building DMGs for use on non-development Macs)
### Linux/Unix Build Requirements ### Linux-specific Requirements
* Qt 5.9 SDK or later
* GCC or Clang * GCC or Clang
* Install the required packages: * 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`
* 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` * 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/
* RedHat/Fedora: `openssl-devel SDL2-devel SDL2_ttf-devel ffmpeg-devel qt5-qtsvg-devel qt5-qtquickcontrols2-devel libva-devel libvdpau-devel opus-devel pulseaudio-libs-devel alsa-lib-devel`
* FFmpeg 4.0+ is required to build. If your distro doesn't package FFmpeg 4.0 or later, you can build and install it from source on https://ffmpeg.org/
### Steam Link Build Requirements
* [Steam Link SDK](https://github.com/ValveSoftware/steamlink-sdk) cloned on your build system
* STEAMLINK_SDK_PATH environment variable set to the Steam Link SDK path
### Build Setup Steps ### Build Setup Steps
1. Install the latest Qt SDK (and optionally, the Qt Creator IDE) from https://www.qt.io/download 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 can install Qt via Homebrew on macOS, but you will need to use `brew install qt --with-debug` to be able to create debug builds of Moonlight.
* You may also use your Linux distro's package manager for the Qt SDK as long as the packages are Qt 5.9 or later. * You may also use your Linux distro's package manager for the Qt SDK as long as the packages are Qt 5.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/` 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. 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. * 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 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` * To build from the command line for development use, run `qmake moonlight-qt.pro` then `make debug` or `make release`
* To create an embedded build for a single-purpose device, use `qmake "CONFIG+=embedded" moonlight-qt.pro` and build normally.
* This build will lack windowed mode, Discord/Help links, and other features that don't make sense on an embedded device.
## Contribute ## Contribute
1. Fork us 1. Fork us
+1 -1
View File
@@ -17,7 +17,7 @@
<key>CFBundleSignature</key> <key>CFBundleSignature</key>
<string>????</string> <string>????</string>
<key>LSMinimumSystemVersion</key> <key>LSMinimumSystemVersion</key>
<string>10.14</string> <string>10.11</string>
<key>NSPrincipalClass</key> <key>NSPrincipalClass</key>
<string>NSApplication</string> <string>NSApplication</string>
<key>NSSupportsAutomaticGraphicsSwitching</key> <key>NSSupportsAutomaticGraphicsSwitching</key>
BIN
View File
Binary file not shown.
+55 -189
View File
@@ -8,7 +8,11 @@ unix:!macx {
TARGET = Moonlight 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. # Precompile QML files to avoid writing qmlcache on portable versions.
# Since this binds the app against the Qt runtime version, we will only # Since this binds the app against the Qt runtime version, we will only
@@ -33,32 +37,33 @@ DEFINES += QT_DEPRECATED_WARNINGS
DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
win32 { win32 {
INCLUDEPATH += $$PWD/../libs/windows/include
contains(QT_ARCH, i386) { contains(QT_ARCH, i386) {
LIBS += -L$$PWD/../libs/windows/lib/x86 LIBS += -L$$PWD/../libs/windows/lib/x86
INCLUDEPATH += $$PWD/../libs/windows/include/x86
} }
contains(QT_ARCH, x86_64) { contains(QT_ARCH, x86_64) {
LIBS += -L$$PWD/../libs/windows/lib/x64 LIBS += -L$$PWD/../libs/windows/lib/x64
INCLUDEPATH += $$PWD/../libs/windows/include/x64
}
contains(QT_ARCH, arm64) {
LIBS += -L$$PWD/../libs/windows/lib/arm64
INCLUDEPATH += $$PWD/../libs/windows/include/arm64
} }
INCLUDEPATH += $$PWD/../libs/windows/include
LIBS += ws2_32.lib winmm.lib dxva2.lib ole32.lib gdi32.lib user32.lib d3d9.lib dwmapi.lib dbghelp.lib LIBS += ws2_32.lib winmm.lib dxva2.lib ole32.lib gdi32.lib user32.lib d3d9.lib dwmapi.lib dbghelp.lib
} }
macx { macx {
INCLUDEPATH += $$PWD/../libs/mac/include INCLUDEPATH += $$PWD/../libs/mac/include $$PWD/../libs/mac/Frameworks/SDL2.framework/Versions/A/Headers
INCLUDEPATH += $$PWD/../libs/mac/Frameworks/SDL2.framework/Versions/A/Headers
INCLUDEPATH += $$PWD/../libs/mac/Frameworks/SDL2_ttf.framework/Versions/A/Headers
LIBS += -L$$PWD/../libs/mac/lib -F$$PWD/../libs/mac/Frameworks LIBS += -L$$PWD/../libs/mac/lib -F$$PWD/../libs/mac/Frameworks
} }
unix:!macx { unix:!macx {
CONFIG += link_pkgconfig CONFIG += link_pkgconfig soundio
PKGCONFIG += openssl sdl2 SDL2_ttf opus PKGCONFIG += openssl sdl2 opus
# For libsoundio
packagesExist(libpulse) {
PKGCONFIG += libpulse
}
packagesExist(alsa) {
PKGCONFIG += alsa
}
packagesExist(libavcodec) { packagesExist(libavcodec) {
PKGCONFIG += libavcodec libavutil PKGCONFIG += libavcodec libavutil
@@ -71,62 +76,29 @@ unix:!macx {
packagesExist(libva-wayland) { packagesExist(libva-wayland) {
CONFIG += libva-wayland CONFIG += libva-wayland
} }
packagesExist(libva-drm) {
CONFIG += libva-drm
}
CONFIG += libva CONFIG += libva
} }
packagesExist(vdpau) { packagesExist(vdpau) {
CONFIG += libvdpau CONFIG += libvdpau
} }
packagesExist(mmal) {
PKGCONFIG += mmal
CONFIG += mmal
}
packagesExist(libdrm) {
PKGCONFIG += libdrm
CONFIG += libdrm
}
packagesExist(ffnvcodec) {
PKGCONFIG += ffnvcodec
CONFIG += cuda
}
}
packagesExist(wayland-client) {
DEFINES += HAS_WAYLAND
PKGCONFIG += wayland-client
}
packagesExist(x11) {
DEFINES += HAS_X11
PKGCONFIG += x11
} }
} }
win32 { win32 {
LIBS += -llibssl -llibcrypto -lSDL2 -lSDL2_ttf -lavcodec -lavutil -lopus -ldxgi -ld3d11 LIBS += -llibssl -llibcrypto -lSDL2 -lavcodec -lavutil -lopus
CONFIG += ffmpeg CONFIG += ffmpeg soundio
}
win32:!winrt {
CONFIG += soundio discord-rpc
} }
macx { macx {
LIBS += -lssl -lcrypto -lavcodec.59 -lavutil.57 -lopus -framework SDL2 -framework SDL2_ttf LIBS += -lssl -lcrypto -lavcodec.58 -lavutil.56 -lopus -framework SDL2
LIBS += -lobjc -framework VideoToolbox -framework AVFoundation -framework CoreVideo -framework CoreGraphics -framework CoreMedia -framework AppKit -framework Metal LIBS += -lobjc -framework VideoToolbox -framework AVFoundation -framework CoreVideo -framework CoreGraphics -framework CoreMedia -framework AppKit
# For libsoundio # For libsoundio
LIBS += -framework CoreAudio -framework AudioUnit LIBS += -framework CoreAudio -framework AudioUnit
CONFIG += ffmpeg soundio discord-rpc CONFIG += ffmpeg soundio
} }
SOURCES += \ SOURCES += \
backend/nvaddress.cpp \
backend/nvapp.cpp \
main.cpp \ main.cpp \
backend/computerseeker.cpp \ backend/computerseeker.cpp \
backend/identitymanager.cpp \ backend/identitymanager.cpp \
@@ -135,19 +107,11 @@ SOURCES += \
backend/nvpairingmanager.cpp \ backend/nvpairingmanager.cpp \
backend/computermanager.cpp \ backend/computermanager.cpp \
backend/boxartmanager.cpp \ backend/boxartmanager.cpp \
backend/richpresencemanager.cpp \
cli/commandlineparser.cpp \ cli/commandlineparser.cpp \
cli/quitstream.cpp \ cli/quitstream.cpp \
cli/startstream.cpp \ cli/startstream.cpp \
settings/compatfetcher.cpp \
settings/mappingfetcher.cpp \
settings/streamingpreferences.cpp \ settings/streamingpreferences.cpp \
streaming/input/abstouch.cpp \ streaming/input.cpp \
streaming/input/gamepad.cpp \
streaming/input/input.cpp \
streaming/input/keyboard.cpp \
streaming/input/mouse.cpp \
streaming/input/reltouch.cpp \
streaming/session.cpp \ streaming/session.cpp \
streaming/audio/audio.cpp \ streaming/audio/audio.cpp \
streaming/audio/renderers/sdlaud.cpp \ streaming/audio/renderers/sdlaud.cpp \
@@ -157,16 +121,9 @@ SOURCES += \
backend/autoupdatechecker.cpp \ backend/autoupdatechecker.cpp \
path.cpp \ path.cpp \
settings/mappingmanager.cpp \ settings/mappingmanager.cpp \
gui/sdlgamepadkeynavigation.cpp \ gui/sdlgamepadkeynavigation.cpp
streaming/video/overlaymanager.cpp \
backend/systemproperties.cpp \
wm.cpp
HEADERS += \ HEADERS += \
backend/nvaddress.h \
backend/nvapp.h \
settings/compatfetcher.h \
settings/mappingfetcher.h \
utils.h \ utils.h \
backend/computerseeker.h \ backend/computerseeker.h \
backend/identitymanager.h \ backend/identitymanager.h \
@@ -175,12 +132,11 @@ HEADERS += \
backend/nvpairingmanager.h \ backend/nvpairingmanager.h \
backend/computermanager.h \ backend/computermanager.h \
backend/boxartmanager.h \ backend/boxartmanager.h \
backend/richpresencemanager.h \
cli/commandlineparser.h \ cli/commandlineparser.h \
cli/quitstream.h \ cli/quitstream.h \
cli/startstream.h \ cli/startstream.h \
settings/streamingpreferences.h \ settings/streamingpreferences.h \
streaming/input/input.h \ streaming/input.h \
streaming/session.h \ streaming/session.h \
streaming/audio/renderers/renderer.h \ streaming/audio/renderers/renderer.h \
streaming/audio/renderers/sdl.h \ streaming/audio/renderers/sdl.h \
@@ -191,9 +147,7 @@ HEADERS += \
backend/autoupdatechecker.h \ backend/autoupdatechecker.h \
path.h \ path.h \
settings/mappingmanager.h \ settings/mappingmanager.h \
gui/sdlgamepadkeynavigation.h \ gui/sdlgamepadkeynavigation.h
streaming/video/overlaymanager.h \
backend/systemproperties.h
# Platform-specific renderers and decoders # Platform-specific renderers and decoders
ffmpeg { ffmpeg {
@@ -206,10 +160,10 @@ ffmpeg {
streaming/video/ffmpeg-renderers/pacer/pacer.cpp \ streaming/video/ffmpeg-renderers/pacer/pacer.cpp \
streaming/video/ffmpeg-renderers/pacer/nullthreadedvsyncsource.cpp streaming/video/ffmpeg-renderers/pacer/nullthreadedvsyncsource.cpp
HEADERS += \ HEADERS += \
streaming/video/ffmpeg.h \ streaming/video/ffmpeg.h \
streaming/video/ffmpeg-renderers/renderer.h \ streaming/video/ffmpeg-renderers/renderer.h \
streaming/video/ffmpeg-renderers/sdlvid.h \
streaming/video/ffmpeg-renderers/pacer/pacer.h \ streaming/video/ffmpeg-renderers/pacer/pacer.h \
streaming/video/ffmpeg-renderers/pacer/nullthreadedvsyncsource.h streaming/video/ffmpeg-renderers/pacer/nullthreadedvsyncsource.h
} }
@@ -233,12 +187,6 @@ libva-wayland {
PKGCONFIG += libva-wayland PKGCONFIG += libva-wayland
DEFINES += HAVE_LIBVA_WAYLAND DEFINES += HAVE_LIBVA_WAYLAND
} }
libva-wayland {
message(VAAPI DRM support enabled)
PKGCONFIG += libva-drm
DEFINES += HAVE_LIBVA_DRM
}
libvdpau { libvdpau {
message(VDPAU renderer selected) message(VDPAU renderer selected)
@@ -246,80 +194,36 @@ libvdpau {
SOURCES += streaming/video/ffmpeg-renderers/vdpau.cpp SOURCES += streaming/video/ffmpeg-renderers/vdpau.cpp
HEADERS += streaming/video/ffmpeg-renderers/vdpau.h HEADERS += streaming/video/ffmpeg-renderers/vdpau.h
} }
mmal { config_SLVideo {
message(MMAL renderer selected) message(SLVideo decoder/renderer selected)
DEFINES += HAVE_MMAL DEFINES += HAVE_SLVIDEO
SOURCES += streaming/video/ffmpeg-renderers/mmal.cpp LIBS += -lSLVideo
HEADERS += streaming/video/ffmpeg-renderers/mmal.h
}
libdrm {
message(DRM renderer selected)
DEFINES += HAVE_DRM SOURCES += streaming/video/sl.cpp
SOURCES += streaming/video/ffmpeg-renderers/drm.cpp HEADERS += streaming/video/sl.h
HEADERS += streaming/video/ffmpeg-renderers/drm.h
linux {
message(Master hooks enabled)
SOURCES += masterhook.c
LIBS += -ldl
}
}
cuda {
message(CUDA support enabled)
DEFINES += HAVE_CUDA
SOURCES += streaming/video/ffmpeg-renderers/cuda.cpp
HEADERS += streaming/video/ffmpeg-renderers/cuda.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 += EMBEDDED_BUILD 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 { win32 {
HEADERS += streaming/video/ffmpeg-renderers/dxutil.h message(DXVA2 renderer selected)
}
win32:!winrt {
message(DXVA2 and D3D11VA renderers selected)
SOURCES += \ SOURCES += \
streaming/video/ffmpeg-renderers/dxva2.cpp \ streaming/video/ffmpeg-renderers/dxva2.cpp \
streaming/video/ffmpeg-renderers/d3d11va.cpp \
streaming/video/ffmpeg-renderers/pacer/dxvsyncsource.cpp streaming/video/ffmpeg-renderers/pacer/dxvsyncsource.cpp
HEADERS += \ HEADERS += \
streaming/video/ffmpeg-renderers/dxva2.h \ streaming/video/ffmpeg-renderers/dxva2.h \
streaming/video/ffmpeg-renderers/d3d11va.h \
streaming/video/ffmpeg-renderers/pacer/dxvsyncsource.h streaming/video/ffmpeg-renderers/pacer/dxvsyncsource.h
} }
macx { macx {
message(VideoToolbox renderer selected) message(VideoToolbox renderer selected)
SOURCES += \ SOURCES += \
streaming/video/ffmpeg-renderers/vt.mm streaming/video/ffmpeg-renderers/vt.mm \
streaming/video/ffmpeg-renderers/pacer/displaylinkvsyncsource.mm
HEADERS += \ HEADERS += \
streaming/video/ffmpeg-renderers/vt.h streaming/video/ffmpeg-renderers/vt.h \
streaming/video/ffmpeg-renderers/pacer/displaylinkvsyncsource.h
} }
soundio { soundio {
message(libsoundio audio renderer selected) message(libsoundio audio renderer selected)
@@ -328,45 +232,11 @@ soundio {
SOURCES += streaming/audio/renderers/soundioaudiorenderer.cpp SOURCES += streaming/audio/renderers/soundioaudiorenderer.cpp
HEADERS += streaming/audio/renderers/soundioaudiorenderer.h HEADERS += streaming/audio/renderers/soundioaudiorenderer.h
} }
discord-rpc {
message(Discord integration enabled)
LIBS += -ldiscord-rpc
DEFINES += HAVE_DISCORD
}
embedded {
message(Embedded build)
DEFINES += EMBEDDED_BUILD
}
RESOURCES += \ RESOURCES += \
resources.qrc \ resources.qrc \
qml.qrc qml.qrc
TRANSLATIONS += \
languages/qml_zh_cn.ts \
languages/qml_de.ts \
languages/qml_fr.ts \
languages/qml_nb_NO.ts \
languages/qml_ru.ts \
languages/qml_es.ts \
languages/qml_ja.ts \
languages/qml_vi.ts \
languages/qml_th.ts \
languages/qml_ko.ts \
languages/qml_hu.ts \
languages/qml_nl.ts \
languages/qml_sv.ts \
languages/qml_tr.ts \
languages/qml_uk.ts \
languages/qml_zh_Hant.ts \
languages/qml_el.ts \
languages/qml_hi.ts \
languages/qml_it.ts \
languages/qml_pt.ts \
languages/qml_pt_BR.ts
# Additional import path used to resolve QML modules in Qt Creator's code model # Additional import path used to resolve QML modules in Qt Creator's code model
QML_IMPORT_PATH = QML_IMPORT_PATH =
@@ -387,14 +257,12 @@ else:unix: LIBS += -L$$OUT_PWD/../qmdnsengine/ -lqmdnsengine
INCLUDEPATH += $$PWD/../qmdnsengine/qmdnsengine/src/include $$PWD/../qmdnsengine INCLUDEPATH += $$PWD/../qmdnsengine/qmdnsengine/src/include $$PWD/../qmdnsengine
DEPENDPATH += $$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
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:win32:CONFIG(debug, debug|release): LIBS += -L$$OUT_PWD/../soundio/debug/ -lsoundio else:unix: LIBS += -L$$OUT_PWD/../soundio/ -lsoundio
else:unix: LIBS += -L$$OUT_PWD/../soundio/ -lsoundio
INCLUDEPATH += $$PWD/../soundio/libsoundio INCLUDEPATH += $$PWD/../soundio/libsoundio
DEPENDPATH += $$PWD/../soundio/libsoundio DEPENDPATH += $$PWD/../soundio/libsoundio
}
win32:CONFIG(release, debug|release): LIBS += -L$$OUT_PWD/../h264bitstream/release/ -lh264bitstream win32:CONFIG(release, debug|release): LIBS += -L$$OUT_PWD/../h264bitstream/release/ -lh264bitstream
else:win32:CONFIG(debug, debug|release): LIBS += -L$$OUT_PWD/../h264bitstream/debug/ -lh264bitstream else:win32:CONFIG(debug, debug|release): LIBS += -L$$OUT_PWD/../h264bitstream/debug/ -lh264bitstream
@@ -403,13 +271,11 @@ else:unix: LIBS += -L$$OUT_PWD/../h264bitstream/ -lh264bitstream
INCLUDEPATH += $$PWD/../h264bitstream/h264bitstream INCLUDEPATH += $$PWD/../h264bitstream/h264bitstream
DEPENDPATH += $$PWD/../h264bitstream/h264bitstream DEPENDPATH += $$PWD/../h264bitstream/h264bitstream
!winrt { win32:CONFIG(release, debug|release): LIBS += -L$$OUT_PWD/../AntiHooking/release/ -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
else:win32:CONFIG(debug, debug|release): LIBS += -L$$OUT_PWD/../AntiHooking/debug/ -lAntiHooking
INCLUDEPATH += $$PWD/../AntiHooking INCLUDEPATH += $$PWD/../AntiHooking
DEPENDPATH += $$PWD/../AntiHooking DEPENDPATH += $$PWD/../AntiHooking
}
unix:!macx: { unix:!macx: {
isEmpty(PREFIX) { isEmpty(PREFIX) {
@@ -433,7 +299,10 @@ unix:!macx: {
appstream.files = deploy/linux/com.moonlight_stream.Moonlight.appdata.xml appstream.files = deploy/linux/com.moonlight_stream.Moonlight.appdata.xml
appstream.path = $$PREFIX/$$DATADIR/metainfo/ 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 { win32 {
RC_ICONS = moonlight.ico RC_ICONS = moonlight.ico
@@ -451,16 +320,13 @@ macx {
QMAKE_INFO_PLIST = $$OUT_PWD/Info.plist 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_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_FRAMEWORKS.path = Contents/Frameworks
APP_BUNDLE_PLIST.files = $$OUT_PWD/Info.plist QMAKE_BUNDLE_DATA += APP_BUNDLE_RESOURCES APP_BUNDLE_FRAMEWORKS
APP_BUNDLE_PLIST.path = Contents
QMAKE_BUNDLE_DATA += APP_BUNDLE_RESOURCES APP_BUNDLE_FRAMEWORKS APP_BUNDLE_PLIST
QMAKE_RPATHDIR += @executable_path/../Frameworks QMAKE_RPATHDIR += @executable_path/../Frameworks
} }
+38 -99
View File
@@ -15,8 +15,8 @@ AutoUpdateChecker::AutoUpdateChecker(QObject *parent) :
// Allow HTTP redirects // Allow HTTP redirects
m_Nam.setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy); m_Nam.setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy);
connect(&m_Nam, &QNetworkAccessManager::finished, connect(&m_Nam, SIGNAL(finished(QNetworkReply*)),
this, &AutoUpdateChecker::handleUpdateCheckRequestFinished); this, SLOT(handleUpdateCheckRequestFinished(QNetworkReply*)));
QString currentVersion(VERSION_STR); QString currentVersion(VERSION_STR);
qDebug() << "Current Moonlight version:" << currentVersion; qDebug() << "Current Moonlight version:" << currentVersion;
@@ -28,24 +28,10 @@ AutoUpdateChecker::AutoUpdateChecker(QObject *parent) :
void AutoUpdateChecker::start() 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 defined(Q_OS_WIN32) || defined(Q_OS_DARWIN) // Only run update checker on platforms without auto-update
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) && QT_VERSION < QT_VERSION_CHECK(5, 15, 1) && !defined(QT_NO_BEARERMANAGEMENT)
// HACK: Set network accessibility to work around QTBUG-80947 (introduced in Qt 5.14.0 and fixed in Qt 5.15.1)
QT_WARNING_PUSH
QT_WARNING_DISABLE_DEPRECATED
m_Nam.setNetworkAccessible(QNetworkAccessManager::Accessible);
QT_WARNING_POP
#endif
// We'll get a callback when this is finished // We'll get a callback when this is finished
QUrl url("https://moonlight-stream.org/updates/qt.json"); QUrl url("https://moonlight-stream.org/updates/qt.json");
QNetworkRequest request(url); m_Nam.get(QNetworkRequest(url));
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
request.setAttribute(QNetworkRequest::Http2AllowedAttribute, true);
#else
request.setAttribute(QNetworkRequest::HTTP2AllowedAttribute, true);
#endif
m_Nam.get(request);
#endif #endif
} }
@@ -57,59 +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");
#elif defined(Q_OS_DARWIN) && QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
// Qt 6 changed this from 'osx' to 'macos'. Use the old one
// to be consistent (and not require another entry in the manifest).
return QStringLiteral("osx");
#else
return QSysInfo::productType();
#endif
}
int AutoUpdateChecker::compareVersion(QVector<int>& version1, QVector<int>& version2) {
for (int i = 0;; i++) {
int v1Val = 0;
int v2Val = 0;
// Treat missing decimal places as 0
if (i < version1.count()) {
v1Val = version1[i];
}
if (i < version2.count()) {
v2Val = version2[i];
}
if (i >= version1.count() && i >= version2.count()) {
// Equal versions
return 0;
}
if (v1Val < v2Val) {
return -1;
}
else if (v1Val > v2Val) {
return 1;
}
}
}
void AutoUpdateChecker::handleUpdateCheckRequestFinished(QNetworkReply* reply) void AutoUpdateChecker::handleUpdateCheckRequestFinished(QNetworkReply* reply)
{ {
Q_ASSERT(reply->isFinished()); Q_ASSERT(reply->isFinished());
if (reply->error() == QNetworkReply::NoError) { if (reply->error() == QNetworkReply::NoError) {
QTextStream stream(reply); QTextStream stream(reply);
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
stream.setEncoding(QStringConverter::Utf8);
#else
stream.setCodec("UTF-8"); stream.setCodec("UTF-8");
#endif
// Read all data and queue the reply for deletion // Read all data and queue the reply for deletion
QString jsonString = stream.readAll(); QString jsonString = stream.readAll();
@@ -148,48 +88,47 @@ void AutoUpdateChecker::handleUpdateCheckRequestFinished(QNetworkReply* reply)
} }
if (updateObj["arch"] == QSysInfo::buildCpuArchitecture() && if (updateObj["arch"] == QSysInfo::buildCpuArchitecture() &&
updateObj["platform"] == getPlatform()) { updateObj["platform"] == QSysInfo::productType()) {
// Check the kernel version minimum if one exists
if (updateObj.contains("kernel_version_at_least") && updateObj["kernel_version_at_least"].isString()) {
QVector<int> requiredVersionQuad;
QVector<int> actualVersionQuad;
QString requiredVersion = updateObj["kernel_version_at_least"].toString();
QString actualVersion = QSysInfo::kernelVersion();
parseStringToVersionQuad(requiredVersion, requiredVersionQuad);
parseStringToVersionQuad(actualVersion, actualVersionQuad);
if (compareVersion(actualVersionQuad, requiredVersionQuad) < 0) {
qDebug() << "Skipping manifest entry due to kernel version (" << actualVersion << "<" << requiredVersion << ")";
continue;
}
}
qDebug() << "Found update manifest match for current platform"; qDebug() << "Found update manifest match for current platform";
QString latestVersion = updateObj["version"].toString(); QString latestVersion = updateObj["version"].toString();
qDebug() << "Latest version of Moonlight for this platform is:" << latestVersion; qDebug() << "Latest version of Moonlight for this platform is:" << latestVersion;
QVector<int> latestVersionQuad; QVector<int> latestVersionQuad;
parseStringToVersionQuad(latestVersion, latestVersionQuad); parseStringToVersionQuad(latestVersion, latestVersionQuad);
int res = compareVersion(m_CurrentVersionQuad, latestVersionQuad); for (int i = 0;; i++) {
if (res < 0) { int latestVer = 0;
// m_CurrentVersionQuad < latestVersionQuad int currentVer = 0;
qDebug() << "Update available";
emit onUpdateAvailable(updateObj["version"].toString(), // Treat missing decimal places as 0
updateObj["browser_url"].toString()); if (i < latestVersionQuad.count()) {
return; latestVer = latestVersionQuad[i];
} }
else if (res > 0) { if (i < m_CurrentVersionQuad.count()) {
qDebug() << "Update manifest version lower than current version"; currentVer = m_CurrentVersionQuad[i];
return; }
} if (i >= latestVersionQuad.count() && i >= m_CurrentVersionQuad.count()) {
else { break;
qDebug() << "Update manifest version equal to current version"; }
return;
if (currentVer < latestVer) {
qDebug() << "Update available";
emit onUpdateAvailable(updateObj["version"].toString(),
updateObj["browser_url"].toString());
return;
}
else if (currentVer > latestVer) {
qDebug() << "Update manifest version lower than current version";
return;
}
} }
qDebug() << "Update manifest version equal to current version";
return;
} }
} }
else { else {
@@ -197,8 +136,8 @@ void AutoUpdateChecker::handleUpdateCheckRequestFinished(QNetworkReply* reply)
} }
} }
qWarning() << "No entry in update manifest found for current platform:" qWarning() << "No entry in update manifest found for current platform: "
<< QSysInfo::buildCpuArchitecture() << getPlatform() << QSysInfo::kernelVersion(); << QSysInfo::buildCpuArchitecture() << QSysInfo::productType();
} }
else { else {
qWarning() << "Update checking failed with error: " << reply->error(); qWarning() << "Update checking failed with error: " << reply->error();
-4
View File
@@ -20,10 +20,6 @@ private slots:
private: private:
void parseStringToVersionQuad(QString& string, QVector<int>& version); void parseStringToVersionQuad(QString& string, QVector<int>& version);
int compareVersion(QVector<int>& version1, QVector<int>& version2);
QString getPlatform();
QVector<int> m_CurrentVersionQuad; QVector<int> m_CurrentVersionQuad;
QNetworkAccessManager m_Nam; QNetworkAccessManager m_Nam;
}; };
+10 -23
View File
@@ -46,8 +46,8 @@ public:
m_Computer(computer), m_Computer(computer),
m_App(app) m_App(app)
{ {
connect(this, &NetworkBoxArtLoadTask::boxArtFetchCompleted, connect(this, SIGNAL(boxArtFetchCompleted(NvComputer*,NvApp,QUrl)),
boxArtManager, &BoxArtManager::handleBoxArtLoadComplete); boxArtManager, SLOT(handleBoxArtLoadComplete(NvComputer*,NvApp,QUrl)));
} }
signals: signals:
@@ -71,10 +71,10 @@ private:
QUrl BoxArtManager::loadBoxArt(NvComputer* computer, NvApp& app) QUrl BoxArtManager::loadBoxArt(NvComputer* computer, NvApp& app)
{ {
// Try to open the cached file if it exists and contains data // Try to open the cached file
QFile cacheFile(getFilePathForBoxArt(computer, app.id)); QString cacheFilePath = getFilePathForBoxArt(computer, app.id);
if (cacheFile.exists() && cacheFile.size() > 0) { if (QFile::exists(cacheFilePath)) {
return QUrl::fromLocalFile(cacheFile.fileName()); return QUrl::fromLocalFile(cacheFilePath);
} }
// If we get here, we need to fetch asynchronously. // If we get here, we need to fetch asynchronously.
@@ -87,26 +87,17 @@ QUrl BoxArtManager::loadBoxArt(NvComputer* computer, NvApp& app)
return QUrl("qrc:/res/no_app_image.png"); 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) void BoxArtManager::handleBoxArtLoadComplete(NvComputer* computer, NvApp app, QUrl image)
{ {
if (!image.isEmpty()) { if (image.isEmpty()) {
emit boxArtLoadComplete(computer, app, image); image = QUrl("qrc:/res/no_app_image.png");
} }
emit boxArtLoadComplete(computer, app, image);
} }
QUrl BoxArtManager::loadBoxArtFromNetwork(NvComputer* computer, int appId) QUrl BoxArtManager::loadBoxArtFromNetwork(NvComputer* computer, int appId)
{ {
NvHTTP http(computer); NvHTTP http(computer->activeAddress, computer->serverCert);
QString cachePath = getFilePathForBoxArt(computer, appId); QString cachePath = getFilePathForBoxArt(computer, appId);
QImage image; QImage image;
@@ -119,10 +110,6 @@ QUrl BoxArtManager::loadBoxArtFromNetwork(NvComputer* computer, int appId)
if (image.save(cachePath)) { if (image.save(cachePath)) {
return QUrl::fromLocalFile(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(); return QUrl();
-4
View File
@@ -18,10 +18,6 @@ public:
QUrl QUrl
loadBoxArt(NvComputer* computer, NvApp& app); loadBoxArt(NvComputer* computer, NvApp& app);
static
void
deleteBoxArt(NvComputer* computer);
signals: signals:
void void
boxArtLoadComplete(NvComputer* computer, NvApp app, QUrl image); boxArtLoadComplete(NvComputer* computer, NvApp app, QUrl image);
+118 -259
View File
@@ -1,5 +1,4 @@
#include "computermanager.h" #include "computermanager.h"
#include "boxartmanager.h"
#include "nvhttp.h" #include "nvhttp.h"
#include "settings/streamingpreferences.h" #include "settings/streamingpreferences.h"
@@ -8,7 +7,6 @@
#include <QThread> #include <QThread>
#include <QThreadPool> #include <QThreadPool>
#include <QCoreApplication>
#define SER_HOSTS "hosts" #define SER_HOSTS "hosts"
@@ -27,18 +25,18 @@ public:
} }
private: private:
bool tryPollComputer(NvAddress address, bool& changed) bool tryPollComputer(QString address, bool& changed)
{ {
NvHTTP http(address, 0, m_Computer->serverCert); NvHTTP http(address, m_Computer->serverCert);
QString serverInfo; QString serverInfo;
try { try {
serverInfo = http.getServerInfo(NvHTTP::NvLogLevel::NVLL_NONE, true); serverInfo = http.getServerInfo(NvHTTP::NvLogLevel::NONE);
} catch (...) { } catch (...) {
return false; return false;
} }
NvComputer newState(http, serverInfo); NvComputer newState(address, serverInfo, QSslCertificate());
// Ensure the machine that responded is the one we intended to contact // Ensure the machine that responded is the one we intended to contact
if (m_Computer->uuid != newState.uuid) { if (m_Computer->uuid != newState.uuid) {
@@ -52,7 +50,9 @@ private:
bool updateAppList(bool& changed) bool updateAppList(bool& changed)
{ {
NvHTTP http(m_Computer); Q_ASSERT(m_Computer->activeAddress != nullptr);
NvHTTP http(m_Computer->activeAddress, m_Computer->serverCert);
QVector<NvApp> appList; QVector<NvApp> appList;
@@ -66,7 +66,12 @@ private:
} }
QWriteLocker lock(&m_Computer->lock); 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; return true;
} }
@@ -78,7 +83,7 @@ private:
bool stateChanged = false; bool stateChanged = false;
bool online = false; bool online = false;
bool wasOnline = m_Computer->state == NvComputer::CS_ONLINE; 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()) { for (auto& address : m_Computer->uniqueAddresses()) {
if (isInterruptionRequested()) { if (isInterruptionRequested()) {
return; return;
@@ -86,7 +91,7 @@ private:
if (tryPollComputer(address, stateChanged)) { if (tryPollComputer(address, stateChanged)) {
if (!wasOnline) { if (!wasOnline) {
qInfo() << m_Computer->name << "is now online at" << m_Computer->activeAddress.toString(); qInfo() << m_Computer->name << "is now online at" << m_Computer->activeAddress;
} }
online = true; online = true;
break; break;
@@ -125,12 +130,8 @@ private:
emit computerStateChanged(m_Computer); emit computerStateChanged(m_Computer);
} }
// Wait a bit to poll again, but do it in 100 ms chunks // Wait a bit to poll again
// so we can be interrupted reasonably quickly. QThread::sleep(3);
// FIXME: QWaitCondition would be better.
for (int i = 0; i < 30 && !isInterruptionRequested(); i++) {
QThread::msleep(100);
}
} }
} }
@@ -144,8 +145,7 @@ private:
ComputerManager::ComputerManager(QObject *parent) ComputerManager::ComputerManager(QObject *parent)
: QObject(parent), : QObject(parent),
m_PollingRef(0), m_PollingRef(0),
m_MdnsBrowser(nullptr), m_MdnsBrowser(nullptr)
m_CompatFetcher(nullptr)
{ {
QSettings settings; QSettings settings;
@@ -157,16 +157,6 @@ ComputerManager::ComputerManager(QObject *parent)
m_KnownHosts[computer->uuid] = computer; m_KnownHosts[computer->uuid] = computer;
} }
settings.endArray(); settings.endArray();
// Fetch latest compatibility data asynchronously
m_CompatFetcher.start();
// 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(), &QCoreApplication::aboutToQuit, this, &ComputerManager::handleAboutToQuit);
} }
ComputerManager::~ComputerManager() ComputerManager::~ComputerManager()
@@ -184,19 +174,38 @@ ComputerManager::~ComputerManager()
delete m_MdnsBrowser; delete m_MdnsBrowser;
m_MdnsBrowser = nullptr; m_MdnsBrowser = nullptr;
// Interrupt polling {
for (ComputerPollingEntry* entry : m_PollEntries) { QMutableMapIterator<QString, QThread*> i(m_PollThreads);
entry->interrupt();
// 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) { QMutableMapIterator<QString, NvComputer*> i(m_KnownHosts);
delete entry;
}
// Destroy all NvComputer objects now that polling is halted // Destroy all NvComputer objects
for (NvComputer* computer : m_KnownHosts) { while (i.hasNext()) {
delete computer; i.next();
delete i.value();
i.remove();
}
} }
} }
@@ -207,50 +216,13 @@ void ComputerManager::saveHosts()
settings.remove(SER_HOSTS); settings.remove(SER_HOSTS);
settings.beginWriteArray(SER_HOSTS); settings.beginWriteArray(SER_HOSTS);
int i = 0; for (int i = 0; i < m_KnownHosts.count(); i++) {
for (const NvComputer* computer : m_KnownHosts) { settings.setArrayIndex(i);
settings.setArrayIndex(i++); m_KnownHosts.value(m_KnownHosts.keys()[i])->serialize(settings);
computer->serialize(settings);
} }
settings.endArray(); 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() void ComputerManager::startPolling()
{ {
QWriteLocker lock(&m_Lock); QWriteLocker lock(&m_Lock);
@@ -268,9 +240,9 @@ void ComputerManager::startPolling()
this, [this](const QMdnsEngine::Service& service) { this, [this](const QMdnsEngine::Service& service) {
qInfo() << "Discovered mDNS host:" << service.hostname(); qInfo() << "Discovered mDNS host:" << service.hostname();
MdnsPendingComputer* pendingComputer = new MdnsPendingComputer(&m_MdnsServer, service); MdnsPendingComputer* pendingComputer = new MdnsPendingComputer(&m_MdnsServer, &m_MdnsCache, service);
connect(pendingComputer, &MdnsPendingComputer::resolvedHost, connect(pendingComputer, SIGNAL(resolvedv4(MdnsPendingComputer*,QHostAddress)),
this, &ComputerManager::handleMdnsServiceResolved); this, SLOT(handleMdnsServiceResolved(MdnsPendingComputer*,QHostAddress)));
m_PendingResolution.append(pendingComputer); m_PendingResolution.append(pendingComputer);
}); });
} }
@@ -293,57 +265,24 @@ void ComputerManager::startPollingComputer(NvComputer* computer)
return; return;
} }
ComputerPollingEntry* pollingEntry; if (m_PollThreads.contains(computer->uuid)) {
Q_ASSERT(m_PollThreads.value(computer->uuid)->isRunning());
if (!m_PollEntries.contains(computer->uuid)) { return;
pollingEntry = m_PollEntries[computer->uuid] = new ComputerPollingEntry();
}
else {
pollingEntry = m_PollEntries[computer->uuid];
} }
if (!pollingEntry->isActive()) { PcMonitorThread* thread = new PcMonitorThread(computer);
PcMonitorThread* thread = new PcMonitorThread(computer); connect(thread, SIGNAL(computerStateChanged(NvComputer*)),
connect(thread, &PcMonitorThread::computerStateChanged, this, SLOT(handleComputerStateChanged(NvComputer*)));
this, &ComputerManager::handleComputerStateChanged); m_PollThreads[computer->uuid] = thread;
pollingEntry->setActiveThread(thread); thread->start();
thread->start();
}
} }
void ComputerManager::handleMdnsServiceResolved(MdnsPendingComputer* computer, void ComputerManager::handleMdnsServiceResolved(MdnsPendingComputer* computer,
QVector<QHostAddress>& addresses) const QHostAddress& address)
{ {
QHostAddress v6Global = getBestGlobalAddressV6(addresses); qInfo() << "Resolved" << computer->hostname() << "to" << address.toString();
bool added = false;
// Add the host using the IPv4 address addNewHost(address.toString(), true);
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(NvAddress(address, computer->port()), true, NvAddress(v6Global, computer->port()));
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(NvAddress(address, computer->port()), true, NvAddress(v6Global, computer->port()));
break;
}
}
}
}
m_PendingResolution.removeOne(computer); m_PendingResolution.removeOne(computer);
computer->deleteLater(); computer->deleteLater();
@@ -378,14 +317,14 @@ public:
void run() void run()
{ {
ComputerPollingEntry* pollingEntry; QThread* pollingThread;
// Only do the minimum amount of work while holding the writer lock. // Only do the minimum amount of work while holding the writer lock.
// We must release it before calling saveHosts(). // We must release it before calling saveHosts().
{ {
QWriteLocker lock(&m_ComputerManager->m_Lock); 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); m_ComputerManager->m_KnownHosts.remove(m_Computer->uuid);
} }
@@ -393,11 +332,18 @@ public:
// Persist the new host list // Persist the new host list
m_ComputerManager->saveHosts(); m_ComputerManager->saveHosts();
// Delete the polling entry first. This will stop all polling threads too. // Delete the polling thread
delete pollingEntry; if (pollingThread != nullptr) {
pollingThread->requestInterruption();
// Delete cached box art // We must wait here because we're going to delete computer
BoxArtManager::deleteBoxArt(m_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 // Finally, delete the computer itself. This must be done
// last because the polling thread might be using it. // last because the polling thread might be using it.
@@ -416,39 +362,6 @@ void ComputerManager::deleteHost(NvComputer* computer)
QThreadPool::globalInstance()->start(new DeferredHostDeletionTask(this, computer)); QThreadPool::globalInstance()->start(new DeferredHostDeletionTask(this, computer));
} }
void ComputerManager::renameHost(NvComputer* computer, QString name)
{
{
QWriteLocker lock(&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 class PendingPairingTask : public QObject, public QRunnable
{ {
Q_OBJECT Q_OBJECT
@@ -468,7 +381,7 @@ signals:
private: private:
void run() void run()
{ {
NvPairingManager pairingManager(m_Computer); NvPairingManager pairingManager(m_Computer->activeAddress);
try { try {
NvPairingManager::PairState result = pairingManager.pair(m_Computer->appVersion, m_Pin, m_Computer->serverCert); NvPairingManager::PairState result = pairingManager.pair(m_Computer->appVersion, m_Pin, m_Computer->serverCert);
@@ -488,7 +401,7 @@ private:
break; break;
} }
} catch (const GfeHttpResponseException& e) { } catch (const GfeHttpResponseException& e) {
emit pairingCompleted(m_Computer, "GeForce Experience returned error: " + e.toQString()); emit pairingCompleted(m_Computer, e.toQString());
} catch (const QtNetworkReplyException& e) { } catch (const QtNetworkReplyException& e) {
emit pairingCompleted(m_Computer, e.toQString()); emit pairingCompleted(m_Computer, e.toQString());
} }
@@ -524,7 +437,7 @@ signals:
private: private:
void run() void run()
{ {
NvHTTP http(m_Computer); NvHTTP http(m_Computer->activeAddress, m_Computer->serverCert);
try { try {
if (m_Computer->currentGameId != 0) { if (m_Computer->currentGameId != 0) {
@@ -537,8 +450,8 @@ private:
} }
if (e.getStatusCode() == 599) { if (e.getStatusCode() == 599) {
// 599 is a special code we make a custom message for // 599 is a special code we make a custom message for
emit quitAppFailed(tr("The running game wasn't started by this PC. " 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.")); "You must quit the game on the host PC manually or use the device that originally started the game.");
} }
else { else {
emit quitAppFailed(e.toQString()); emit quitAppFailed(e.toQString());
@@ -585,20 +498,20 @@ void ComputerManager::stopPollingAsync()
m_MdnsBrowser = nullptr; m_MdnsBrowser = nullptr;
// Interrupt all threads, but don't wait for them to terminate // Interrupt all threads, but don't wait for them to terminate
for (ComputerPollingEntry* entry : m_PollEntries) { QMutableMapIterator<QString, QThread*> i(m_PollThreads);
entry->interrupt(); while (i.hasNext()) {
} i.next();
}
void ComputerManager::addNewHostManually(QString address) QThread* thread = i.value();
{
QUrl url = QUrl::fromUserInput(address); // Let this thread delete itself when it's finished
if (url.isValid() && !url.host().isEmpty()) { connect(thread, SIGNAL(finished()),
// If there wasn't a port specified, use the default thread, SLOT(deleteLater()));
addNewHost(NvAddress(url.host(), url.port(DEFAULT_HTTP_PORT)), false);
} // The threads will delete themselves when they terminate,
else { // but we remove them from the polling threads map here.
emit computerAddCompleted(false, false); i.value()->requestInterruption();
i.remove();
} }
} }
@@ -607,10 +520,9 @@ class PendingAddTask : public QObject, public QRunnable
Q_OBJECT Q_OBJECT
public: public:
PendingAddTask(ComputerManager* computerManager, NvAddress address, NvAddress mdnsIpv6Address, bool mdns) PendingAddTask(ComputerManager* computerManager, QString address, bool mdns)
: m_ComputerManager(computerManager), : m_ComputerManager(computerManager),
m_Address(address), m_Address(address),
m_MdnsIpv6Address(mdnsIpv6Address),
m_Mdns(mdns) m_Mdns(mdns)
{ {
connect(this, &PendingAddTask::computerAddCompleted, connect(this, &PendingAddTask::computerAddCompleted,
@@ -620,7 +532,7 @@ public:
} }
signals: signals:
void computerAddCompleted(QVariant success, QVariant detectedPortBlocking); void computerAddCompleted(QVariant success);
void computerStateChanged(NvComputer* computer); void computerStateChanged(NvComputer* computer);
@@ -634,12 +546,12 @@ private:
// around this issue, we will issue the request again after a few seconds if // around this issue, we will issue the request again after a few seconds if
// we see a ServiceUnavailableError error. // we see a ServiceUnavailableError error.
try { try {
serverInfo = http.getServerInfo(NvHTTP::NVLL_VERBOSE); serverInfo = http.getServerInfo(NvHTTP::NvLogLevel::VERBOSE);
} catch (const QtNetworkReplyException& e) { } catch (const QtNetworkReplyException& e) {
if (e.getError() == QNetworkReply::ServiceUnavailableError) { if (e.getError() == QNetworkReply::ServiceUnavailableError) {
qWarning() << "Retrying request in 5 seconds after ServiceUnavailableError"; qWarning() << "Retrying request in 5 seconds after ServiceUnavailableError";
QThread::sleep(5); QThread::sleep(5);
serverInfo = http.getServerInfo(NvHTTP::NVLL_VERBOSE); serverInfo = http.getServerInfo(NvHTTP::NvLogLevel::VERBOSE);
qInfo() << "Retry successful"; qInfo() << "Retry successful";
} }
else { else {
@@ -650,20 +562,7 @@ private:
return serverInfo; return serverInfo;
} catch (...) { } catch (...) {
if (!m_Mdns) { if (!m_Mdns) {
StreamingPreferences prefs; emit computerAddCompleted(false);
int portTestResult;
if (prefs.detectNetworkBlocking) {
// We failed to connect to the specified PC. Let's test to make sure this network
// isn't blocking Moonlight, so we can tell the user about it.
portTestResult = LiTestClientConnectivity("qt.conntest.moonlight-stream.org", 443,
ML_PORT_FLAG_TCP_47984 | ML_PORT_FLAG_TCP_47989);
}
else {
portTestResult = 0;
}
emit computerAddCompleted(false, portTestResult != 0 && portTestResult != ML_TEST_RESULT_INCONCLUSIVE);
} }
return QString(); return QString();
} }
@@ -671,23 +570,18 @@ private:
void run() void run()
{ {
NvHTTP http(m_Address, 0, QSslCertificate()); NvHTTP http(m_Address, QSslCertificate());
qInfo() << "Processing new PC at" << m_Address.toString() << "from" << (m_Mdns ? "mDNS" : "user") << "with IPv6 address" << m_MdnsIpv6Address.toString(); 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 // Perform initial serverinfo fetch over HTTP since we don't know which cert to use
QString serverInfo = fetchServerInfo(http); 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);
serverInfo = fetchServerInfo(http);
}
if (serverInfo.isEmpty()) { if (serverInfo.isEmpty()) {
return; return;
} }
// Create initial newComputer using HTTP serverinfo with no pinned cert // Create initial newComputer using HTTP serverinfo with no pinned cert
NvComputer* newComputer = new NvComputer(http, serverInfo); NvComputer* newComputer = new NvComputer(m_Address, serverInfo, QSslCertificate());
// Check if we have a record of this host UUID to pull the pinned cert // Check if we have a record of this host UUID to pull the pinned cert
NvComputer* existingComputer; NvComputer* existingComputer;
@@ -701,53 +595,34 @@ private:
// Fetch serverinfo again over HTTPS with the pinned cert // Fetch serverinfo again over HTTPS with the pinned cert
if (existingComputer != nullptr) { if (existingComputer != nullptr) {
Q_ASSERT(http.httpsPort() != 0);
serverInfo = fetchServerInfo(http); serverInfo = fetchServerInfo(http);
if (serverInfo.isEmpty()) { if (serverInfo.isEmpty()) {
return; return;
} }
// Update the polled computer with the HTTPS information // Update the polled computer with the HTTPS information
NvComputer httpsComputer(http, serverInfo); NvComputer httpsComputer(m_Address, serverInfo, QSslCertificate());
newComputer->update(httpsComputer); newComputer->update(httpsComputer);
} }
// Update addresses depending on the context // Update addresses depending on the context
if (m_Mdns) { if (m_Mdns) {
// Only update local address if we actually reached the PC via this address. newComputer->localAddress = m_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;
}
// Get the WAN IP address using STUN if we're on mDNS over IPv4 // Get the WAN IP address using STUN if we're on mDNS
if (QHostAddress(newComputer->localAddress.address()).protocol() == QAbstractSocket::IPv4Protocol) { quint32 addr;
quint32 addr; int err = LiFindExternalAddressIP4("stun.moonlight-stream.org", 3478, &addr);
int err = LiFindExternalAddressIP4("stun.moonlight-stream.org", 3478, &addr); if (err == 0) {
if (err == 0) { newComputer->remoteAddress = QHostAddress(qFromBigEndian(addr)).toString();
newComputer->setRemoteAddress(QHostAddress(qFromBigEndian(addr)));
}
else {
qWarning() << "STUN failed to get WAN address:" << err;
}
} }
else {
if (!m_MdnsIpv6Address.isNull()) { qWarning() << "STUN failed to get WAN address:" << err;
Q_ASSERT(QHostAddress(m_MdnsIpv6Address.address()).protocol() == QAbstractSocket::IPv6Protocol);
newComputer->ipv6Address = m_MdnsIpv6Address;
} }
} }
else { else {
newComputer->manualAddress = m_Address; newComputer->manualAddress = m_Address;
} }
QHostAddress hostAddress(m_Address.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 // Check if this PC already exists
QWriteLocker lock(&m_ComputerManager->m_Lock); QWriteLocker lock(&m_ComputerManager->m_Lock);
@@ -762,12 +637,12 @@ private:
// For non-mDNS clients, let them know it succeeded // For non-mDNS clients, let them know it succeeded
if (!m_Mdns) { if (!m_Mdns) {
emit computerAddCompleted(true, false); emit computerAddCompleted(true);
} }
// Tell our client if something changed // Tell our client if something changed
if (changed) { if (changed) {
qInfo() << existingComputer->name << "is now at" << existingComputer->activeAddress.toString(); qInfo() << existingComputer->name << "is now at" << existingComputer->activeAddress;
emit computerStateChanged(existingComputer); emit computerStateChanged(existingComputer);
} }
} }
@@ -781,24 +656,9 @@ private:
// Drop the lock before notifying // Drop the lock before notifying
lock.unlock(); 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->setRemoteAddress(QHostAddress(qFromBigEndian(addr)));
lock.unlock();
}
else {
qWarning() << "STUN failed to get WAN address:" << err;
}
}
// For non-mDNS clients, let them know it succeeded // For non-mDNS clients, let them know it succeeded
if (!m_Mdns) { if (!m_Mdns) {
emit computerAddCompleted(true, false); emit computerAddCompleted(true);
} }
// Tell our client about this new PC // Tell our client about this new PC
@@ -808,16 +668,15 @@ private:
} }
ComputerManager* m_ComputerManager; ComputerManager* m_ComputerManager;
NvAddress m_Address; QString m_Address;
NvAddress m_MdnsIpv6Address;
bool m_Mdns; bool m_Mdns;
}; };
void ComputerManager::addNewHost(NvAddress address, bool mdns, NvAddress mdnsIpv6Address) void ComputerManager::addNewHost(QString address, bool mdns)
{ {
// Punt to a worker thread to avoid stalling the // Punt to a worker thread to avoid stalling the
// UI while waiting for serverinfo query to complete // 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); QThreadPool::globalInstance()->start(addTask);
} }
+14 -134
View File
@@ -2,7 +2,6 @@
#include "nvcomputer.h" #include "nvcomputer.h"
#include "nvpairingmanager.h" #include "nvpairingmanager.h"
#include "settings/compatfetcher.h"
#include <qmdnsengine/server.h> #include <qmdnsengine/server.h>
#include <qmdnsengine/cache.h> #include <qmdnsengine/cache.h>
@@ -14,7 +13,6 @@
#include <QReadWriteLock> #include <QReadWriteLock>
#include <QSettings> #include <QSettings>
#include <QRunnable> #include <QRunnable>
#include <QTimer>
class MdnsPendingComputer : public QObject class MdnsPendingComputer : public QObject
{ {
@@ -22,19 +20,13 @@ class MdnsPendingComputer : public QObject
public: public:
explicit MdnsPendingComputer(QMdnsEngine::Server* server, explicit MdnsPendingComputer(QMdnsEngine::Server* server,
QMdnsEngine::Cache* cache,
const QMdnsEngine::Service& service) const QMdnsEngine::Service& service)
: m_Hostname(service.hostname()), : m_Hostname(service.hostname()),
m_Port(service.port()), m_Resolver(server, m_Hostname, cache)
m_Server(server),
m_Resolver(nullptr)
{ {
// Start resolving connect(&m_Resolver, SIGNAL(resolved(QHostAddress)),
resolve(); this, SLOT(handleResolved(QHostAddress)));
}
virtual ~MdnsPendingComputer()
{
delete m_Resolver;
} }
QString hostname() QString hostname()
@@ -42,122 +34,21 @@ public:
return m_Hostname; return m_Hostname;
} }
uint16_t port()
{
return m_Port;
}
private slots: private slots:
void handleResolvedTimeout() void handleResolved(const QHostAddress& address)
{ {
if (m_Addresses.isEmpty()) { if (address.protocol() == QAbstractSocket::IPv4Protocol) {
// Try again m_Resolver.disconnect();
qInfo() << "Resolving" << hostname() << "timed out. Retrying..."; emit resolvedv4(this, address);
resolve();
} }
else {
Q_ASSERT(!m_Addresses.isEmpty());
emit resolvedHost(this, m_Addresses);
}
}
void handleResolvedAddress(const QHostAddress& address)
{
qInfo() << "Resolved" << hostname() << "to" << address;
m_Addresses.push_back(address);
} }
signals: signals:
void resolvedHost(MdnsPendingComputer*,QVector<QHostAddress>&); void resolvedv4(MdnsPendingComputer*, const QHostAddress&);
private: 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, &MdnsPendingComputer::handleResolvedTimeout);
}
QByteArray m_Hostname; QByteArray m_Hostname;
uint16_t m_Port; QMdnsEngine::Resolver m_Resolver;
QMdnsEngine::Server* m_Server;
QMdnsEngine::Resolver* m_Resolver;
QVector<QHostAddress> m_Addresses;
};
class ComputerPollingEntry
{
public:
ComputerPollingEntry()
: m_ActiveThread(nullptr)
{
}
virtual ~ComputerPollingEntry()
{
interrupt();
// interrupt() should have taken care of this
Q_ASSERT(m_ActiveThread == nullptr);
for (QThread* thread : m_InactiveList) {
thread->wait();
delete thread;
}
}
bool isActive()
{
cleanInactiveList();
return m_ActiveThread != nullptr;
}
void setActiveThread(QThread* thread)
{
cleanInactiveList();
Q_ASSERT(!isActive());
m_ActiveThread = thread;
}
void interrupt()
{
cleanInactiveList();
if (m_ActiveThread != nullptr) {
// Interrupt the active thread
m_ActiveThread->requestInterruption();
// Place it on the inactive list awaiting death
m_InactiveList.append(m_ActiveThread);
m_ActiveThread = nullptr;
}
}
private:
void cleanInactiveList()
{
QMutableListIterator<QThread*> i(m_InactiveList);
// Reap any threads that have finished
while (i.hasNext()) {
i.next();
QThread* thread = i.value();
if (thread->isFinished()) {
delete thread;
i.remove();
}
}
}
QThread* m_ActiveThread;
QList<QThread*> m_InactiveList;
}; };
class ComputerManager : public QObject class ComputerManager : public QObject
@@ -176,9 +67,7 @@ public:
Q_INVOKABLE void stopPollingAsync(); Q_INVOKABLE void stopPollingAsync();
Q_INVOKABLE void addNewHostManually(QString address); Q_INVOKABLE void addNewHost(QString address, bool mdns);
void addNewHost(NvAddress address, bool mdns, NvAddress mdnsIpv6Address = NvAddress());
void pairHost(NvComputer* computer, QString pin); void pairHost(NvComputer* computer, QString pin);
@@ -189,40 +78,31 @@ public:
// computer is deleted inside this call // computer is deleted inside this call
void deleteHost(NvComputer* computer); void deleteHost(NvComputer* computer);
void renameHost(NvComputer* computer, QString name);
void clientSideAttributeUpdated(NvComputer* computer);
signals: signals:
void computerStateChanged(NvComputer* computer); void computerStateChanged(NvComputer* computer);
void pairingCompleted(NvComputer* computer, QString error); void pairingCompleted(NvComputer* computer, QString error);
void computerAddCompleted(QVariant success, QVariant detectedPortBlocking); void computerAddCompleted(QVariant success);
void quitAppCompleted(QVariant error); void quitAppCompleted(QVariant error);
private slots: private slots:
void handleAboutToQuit();
void handleComputerStateChanged(NvComputer* computer); void handleComputerStateChanged(NvComputer* computer);
void handleMdnsServiceResolved(MdnsPendingComputer* computer, QVector<QHostAddress>& addresses); void handleMdnsServiceResolved(MdnsPendingComputer* computer, const QHostAddress& address);
private: private:
void saveHosts(); void saveHosts();
QHostAddress getBestGlobalAddressV6(QVector<QHostAddress>& addresses);
void startPollingComputer(NvComputer* computer); void startPollingComputer(NvComputer* computer);
int m_PollingRef; int m_PollingRef;
QReadWriteLock m_Lock; QReadWriteLock m_Lock;
QMap<QString, NvComputer*> m_KnownHosts; QMap<QString, NvComputer*> m_KnownHosts;
QMap<QString, ComputerPollingEntry*> m_PollEntries; QMap<QString, QThread*> m_PollThreads;
QMdnsEngine::Server m_MdnsServer; QMdnsEngine::Server m_MdnsServer;
QMdnsEngine::Browser* m_MdnsBrowser; QMdnsEngine::Browser* m_MdnsBrowser;
QMdnsEngine::Cache m_MdnsCache; QMdnsEngine::Cache m_MdnsCache;
QVector<MdnsPendingComputer*> m_PendingResolution; QVector<MdnsPendingComputer*> m_PendingResolution;
CompatFetcher m_CompatFetcher;
}; };
+6 -13
View File
@@ -20,7 +20,7 @@ void ComputerSeeker::start(int timeout)
// if m_ComputerName is UUID, or the name that doesn't resolve to an IP // if m_ComputerName is UUID, or the name that doesn't resolve to an IP
// address) and by polling it using mDNS, hopefully one of these methods // address) and by polling it using mDNS, hopefully one of these methods
// would find the host // would find the host
m_ComputerManager->addNewHostManually(m_ComputerName); m_ComputerManager->addNewHost(m_ComputerName, false);
m_ComputerManager->startPolling(); m_ComputerManager->startPolling();
} }
@@ -39,18 +39,11 @@ void ComputerSeeker::onComputerUpdated(NvComputer *computer)
bool ComputerSeeker::matchComputer(NvComputer *computer) const bool ComputerSeeker::matchComputer(NvComputer *computer) const
{ {
QString value = m_ComputerName.toLower(); QString value = m_ComputerName.toLower();
return computer->name.toLower() == value ||
if (computer->name.toLower() == value || computer->uuid.toLower() == value) { computer->localAddress.toLower() == value ||
return true; computer->remoteAddress.toLower() == value ||
} computer->manualAddress.toLower() == value ||
computer->uuid.toLower() == value;
for (const NvAddress& addr : computer->uniqueAddresses()) {
if (addr.address().toLower() == value || addr.toString().toLower() == value) {
return true;
}
}
return false;
} }
bool ComputerSeeker::isOnline(NvComputer *computer) const bool ComputerSeeker::isOnline(NvComputer *computer) const
+16 -16
View File
@@ -5,6 +5,7 @@
#include <openssl/pem.h> #include <openssl/pem.h>
#include <openssl/rsa.h> #include <openssl/rsa.h>
#include <openssl/bn.h>
#include <openssl/x509.h> #include <openssl/x509.h>
#include <openssl/rand.h> #include <openssl/rand.h>
@@ -31,19 +32,20 @@ void IdentityManager::createCredentials(QSettings& settings)
X509* cert = X509_new(); X509* cert = X509_new();
THROW_BAD_ALLOC_IF_NULL(cert); THROW_BAD_ALLOC_IF_NULL(cert);
EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, NULL); EVP_PKEY* pk = EVP_PKEY_new();
THROW_BAD_ALLOC_IF_NULL(ctx);
EVP_PKEY_keygen_init(ctx);
EVP_PKEY_CTX_set_rsa_keygen_bits(ctx, 2048);
// pk must be initialized on input
EVP_PKEY* pk = NULL;
EVP_PKEY_keygen(ctx, &pk);
EVP_PKEY_CTX_free(ctx);
THROW_BAD_ALLOC_IF_NULL(pk); 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); X509_set_version(cert, 2);
ASN1_INTEGER_set(X509_get_serialNumber(cert), 0); ASN1_INTEGER_set(X509_get_serialNumber(cert), 0);
#if OPENSSL_VERSION_NUMBER < 0x10100000L #if OPENSSL_VERSION_NUMBER < 0x10100000L
@@ -73,7 +75,7 @@ void IdentityManager::createCredentials(QSettings& settings)
-1, -1, 0); -1, -1, 0);
X509_set_issuer_name(cert, name); 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()); BIO* biokey = BIO_new(BIO_s_mem());
THROW_BAD_ALLOC_IF_NULL(biokey); THROW_BAD_ALLOC_IF_NULL(biokey);
@@ -92,6 +94,7 @@ void IdentityManager::createCredentials(QSettings& settings)
X509_free(cert); X509_free(cert);
EVP_PKEY_free(pk); EVP_PKEY_free(pk);
BN_free(bne);
BIO_free(biokey); BIO_free(biokey);
BIO_free(biocert); BIO_free(biocert);
@@ -143,10 +146,7 @@ QSslKey
IdentityManager::getSslKey() IdentityManager::getSslKey()
{ {
if (m_CachedSslKey.isNull()) { if (m_CachedSslKey.isNull()) {
// This seemingly useless const_cast is required for old OpenSSL headers BIO* bio = BIO_new_mem_buf(m_CachedPrivateKey.data(), -1);
// where BIO_new_mem_buf's parameter is not declared const like those on
// the Steam Link hardware.
BIO* bio = BIO_new_mem_buf(const_cast<char*>(m_CachedPrivateKey.constData()), -1);
THROW_BAD_ALLOC_IF_NULL(bio); THROW_BAD_ALLOC_IF_NULL(bio);
EVP_PKEY* pk = PEM_read_bio_PrivateKey(bio, nullptr, nullptr, nullptr); EVP_PKEY* pk = PEM_read_bio_PrivateKey(bio, nullptr, nullptr, nullptr);
-65
View File
@@ -1,65 +0,0 @@
#include "nvaddress.h"
#include <QHostAddress>
NvAddress::NvAddress()
{
setAddress(nullptr);
setPort(0);
}
NvAddress::NvAddress(QString addr, uint16_t port)
{
setAddress(addr);
setPort(port);
}
NvAddress::NvAddress(QHostAddress addr, uint16_t port)
{
setAddress(addr);
setPort(port);
}
uint16_t NvAddress::port() const
{
return m_Port;
}
QString NvAddress::address() const
{
return m_Address;
}
void NvAddress::setPort(uint16_t port)
{
m_Port = port;
}
void NvAddress::setAddress(QString addr)
{
m_Address = addr;
}
void NvAddress::setAddress(QHostAddress addr)
{
m_Address = addr.toString();
}
bool NvAddress::isNull() const
{
return m_Address.isEmpty();
}
QString NvAddress::toString() const
{
if (m_Address.isEmpty()) {
return "<NULL>";
}
if (QHostAddress(m_Address).protocol() == QAbstractSocket::IPv6Protocol) {
return QString("[%1]:%2").arg(m_Address).arg(m_Port);
}
else {
return QString("%1:%2").arg(m_Address).arg(m_Port);
}
}
-39
View File
@@ -1,39 +0,0 @@
#pragma once
#include <QHostAddress>
#define DEFAULT_HTTP_PORT 47989
#define DEFAULT_HTTPS_PORT 47984
class NvAddress
{
public:
NvAddress();
explicit NvAddress(QString addr, uint16_t port);
explicit NvAddress(QHostAddress addr, uint16_t port);
uint16_t port() const;
void setPort(uint16_t port);
QString address() const;
void setAddress(QString addr);
void setAddress(QHostAddress addr);
bool isNull() const;
QString toString() const;
bool operator==(const NvAddress& other) const
{
return m_Address == other.m_Address &&
m_Port == other.m_Port;
}
bool operator!=(const NvAddress& other) const
{
return !operator==(other);
}
private:
QString m_Address;
uint16_t m_Port;
};
-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)
+39 -196
View File
@@ -1,54 +1,48 @@
#include "nvcomputer.h" #include "nvcomputer.h"
#include "nvapp.h"
#include "settings/compatfetcher.h"
#include <QUdpSocket> #include <QUdpSocket>
#include <QHostInfo> #include <QHostInfo>
#include <QNetworkInterface> #include <QNetworkInterface>
#include <QNetworkProxy>
#define SER_NAME "hostname" #define SER_NAME "hostname"
#define SER_UUID "uuid" #define SER_UUID "uuid"
#define SER_MAC "mac" #define SER_MAC "mac"
#define SER_LOCALADDR "localaddress" #define SER_LOCALADDR "localaddress"
#define SER_LOCALPORT "localport"
#define SER_REMOTEADDR "remoteaddress" #define SER_REMOTEADDR "remoteaddress"
#define SER_REMOTEPORT "remoteport"
#define SER_MANUALADDR "manualaddress" #define SER_MANUALADDR "manualaddress"
#define SER_MANUALPORT "manualport"
#define SER_IPV6ADDR "ipv6address"
#define SER_IPV6PORT "ipv6port"
#define SER_APPLIST "apps" #define SER_APPLIST "apps"
#define SER_SRVCERT "srvcert" #define SER_SRVCERT "srvcert"
#define SER_CUSTOMNAME "customname"
#define SER_APPNAME "name"
#define SER_APPID "id"
#define SER_APPHDR "hdr"
NvComputer::NvComputer(QSettings& settings) NvComputer::NvComputer(QSettings& settings)
{ {
this->name = settings.value(SER_NAME).toString(); this->name = settings.value(SER_NAME).toString();
this->uuid = settings.value(SER_UUID).toString(); this->uuid = settings.value(SER_UUID).toString();
this->hasCustomName = settings.value(SER_CUSTOMNAME).toBool();
this->macAddress = settings.value(SER_MAC).toByteArray(); this->macAddress = settings.value(SER_MAC).toByteArray();
this->localAddress = NvAddress(settings.value(SER_LOCALADDR).toString(), this->localAddress = settings.value(SER_LOCALADDR).toString();
settings.value(SER_LOCALPORT, QVariant(DEFAULT_HTTP_PORT)).toUInt()); this->remoteAddress = settings.value(SER_REMOTEADDR).toString();
this->remoteAddress = NvAddress(settings.value(SER_REMOTEADDR).toString(), this->manualAddress = settings.value(SER_MANUALADDR).toString();
settings.value(SER_REMOTEPORT, QVariant(DEFAULT_HTTP_PORT)).toUInt());
this->ipv6Address = NvAddress(settings.value(SER_IPV6ADDR).toString(),
settings.value(SER_IPV6PORT, QVariant(DEFAULT_HTTP_PORT)).toUInt());
this->manualAddress = NvAddress(settings.value(SER_MANUALADDR).toString(),
settings.value(SER_MANUALPORT, QVariant(DEFAULT_HTTP_PORT)).toUInt());
this->serverCert = QSslCertificate(settings.value(SER_SRVCERT).toByteArray()); this->serverCert = QSslCertificate(settings.value(SER_SRVCERT).toByteArray());
int appCount = settings.beginReadArray(SER_APPLIST); int appCount = settings.beginReadArray(SER_APPLIST);
this->appList.reserve(appCount);
for (int i = 0; i < appCount; i++) { for (int i = 0; i < appCount; i++) {
NvApp app;
settings.setArrayIndex(i); 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); this->appList.append(app);
} }
settings.endArray(); settings.endArray();
sortAppList(); sortAppList();
this->activeAddress = nullptr;
this->currentGameId = 0; this->currentGameId = 0;
this->pairState = PS_UNKNOWN; this->pairState = PS_UNKNOWN;
this->state = CS_UNKNOWN; this->state = CS_UNKNOWN;
@@ -58,34 +52,18 @@ NvComputer::NvComputer(QSettings& settings)
this->serverCodecModeSupport = 0; this->serverCodecModeSupport = 0;
this->pendingQuit = false; this->pendingQuit = false;
this->gpuModel = nullptr; this->gpuModel = nullptr;
this->isSupportedServerVersion = true;
this->externalPort = this->remoteAddress.port();
this->activeHttpsPort = 0;
} }
void NvComputer::setRemoteAddress(QHostAddress address) void NvComputer::serialize(QSettings& settings)
{
Q_ASSERT(this->externalPort != 0);
this->remoteAddress = NvAddress(address, this->externalPort);
}
void NvComputer::serialize(QSettings& settings) const
{ {
QReadLocker lock(&this->lock); QReadLocker lock(&this->lock);
settings.setValue(SER_NAME, name); settings.setValue(SER_NAME, name);
settings.setValue(SER_CUSTOMNAME, hasCustomName);
settings.setValue(SER_UUID, uuid); settings.setValue(SER_UUID, uuid);
settings.setValue(SER_MAC, macAddress); settings.setValue(SER_MAC, macAddress);
settings.setValue(SER_LOCALADDR, localAddress.address()); settings.setValue(SER_LOCALADDR, localAddress);
settings.setValue(SER_LOCALPORT, localAddress.port()); settings.setValue(SER_REMOTEADDR, remoteAddress);
settings.setValue(SER_REMOTEADDR, remoteAddress.address()); settings.setValue(SER_MANUALADDR, manualAddress);
settings.setValue(SER_REMOTEPORT, remoteAddress.port());
settings.setValue(SER_IPV6ADDR, ipv6Address.address());
settings.setValue(SER_IPV6PORT, ipv6Address.port());
settings.setValue(SER_MANUALADDR, manualAddress.address());
settings.setValue(SER_MANUALPORT, manualAddress.port());
settings.setValue(SER_SRVCERT, serverCert.toPem()); settings.setValue(SER_SRVCERT, serverCert.toPem());
// Avoid deleting an existing applist if we couldn't get one // Avoid deleting an existing applist if we couldn't get one
@@ -94,7 +72,10 @@ void NvComputer::serialize(QSettings& settings) const
settings.beginWriteArray(SER_APPLIST); settings.beginWriteArray(SER_APPLIST);
for (int i = 0; i < appList.count(); i++) { for (int i = 0; i < appList.count(); i++) {
settings.setArrayIndex(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(); settings.endArray();
} }
@@ -107,11 +88,10 @@ void NvComputer::sortAppList()
}); });
} }
NvComputer::NvComputer(NvHTTP& http, QString serverInfo) NvComputer::NvComputer(QString address, QString serverInfo, QSslCertificate serverCert)
{ {
this->serverCert = http.serverCert(); this->serverCert = serverCert;
this->hasCustomName = false;
this->name = NvHTTP::getXmlString(serverInfo, "hostname"); this->name = NvHTTP::getXmlString(serverInfo, "hostname");
if (this->name.isEmpty()) { if (this->name.isEmpty()) {
this->name = "UNKNOWN"; this->name = "UNKNOWN";
@@ -121,7 +101,7 @@ NvComputer::NvComputer(NvHTTP& http, QString serverInfo)
QString newMacString = NvHTTP::getXmlString(serverInfo, "mac"); QString newMacString = NvHTTP::getXmlString(serverInfo, "mac");
if (newMacString != "00:00:00:00:00:00") { if (newMacString != "00:00:00:00:00:00") {
QStringList macOctets = newMacString.split(':'); QStringList macOctets = newMacString.split(':');
for (const QString& macOctet : macOctets) { for (QString macOctet : macOctets) {
this->macAddress.append((char) macOctet.toInt(nullptr, 16)); this->macAddress.append((char) macOctet.toInt(nullptr, 16));
} }
} }
@@ -149,42 +129,17 @@ NvComputer::NvComputer(NvHTTP& http, QString serverInfo)
mode2.width * mode2.height * mode2.refreshRate; 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");
this->localAddress = NvAddress(NvHTTP::getXmlString(serverInfo, "LocalIP"), http.httpPort()); this->remoteAddress = NvHTTP::getXmlString(serverInfo, "ExternalIP");
if (this->localAddress.address().startsWith("127.")) {
this->localAddress = NvAddress();
}
QString httpsPort = NvHTTP::getXmlString(serverInfo, "HttpsPort");
if (httpsPort.isEmpty() || (this->activeHttpsPort = httpsPort.toUShort()) == 0) {
this->activeHttpsPort = DEFAULT_HTTPS_PORT;
}
// This is an extension which is not present in GFE. It is present for Sunshine to be able
// to support dynamic HTTP WAN ports without requiring the user to manually enter the port.
QString remotePortStr = NvHTTP::getXmlString(serverInfo, "ExternalPort");
if (remotePortStr.isEmpty() || (this->externalPort = remotePortStr.toUShort()) == 0) {
this->externalPort = DEFAULT_HTTP_PORT;
}
QString remoteAddress = NvHTTP::getXmlString(serverInfo, "ExternalIP");
if (!remoteAddress.isEmpty()) {
this->remoteAddress = NvAddress(remoteAddress, this->externalPort);
}
else {
this->remoteAddress = NvAddress();
}
this->pairState = NvHTTP::getXmlString(serverInfo, "PairStatus") == "1" ? this->pairState = NvHTTP::getXmlString(serverInfo, "PairStatus") == "1" ?
PS_PAIRED : PS_NOT_PAIRED; PS_PAIRED : PS_NOT_PAIRED;
this->currentGameId = NvHTTP::getCurrentGame(serverInfo); this->currentGameId = NvHTTP::getCurrentGame(serverInfo);
this->appVersion = NvHTTP::getXmlString(serverInfo, "appversion"); this->appVersion = NvHTTP::getXmlString(serverInfo, "appversion");
this->gfeVersion = NvHTTP::getXmlString(serverInfo, "GfeVersion"); this->gfeVersion = NvHTTP::getXmlString(serverInfo, "GfeVersion");
this->gpuModel = NvHTTP::getXmlString(serverInfo, "gputype"); this->gpuModel = NvHTTP::getXmlString(serverInfo, "gputype");
this->activeAddress = http.address(); this->activeAddress = address;
this->state = NvComputer::CS_ONLINE; this->state = NvComputer::CS_ONLINE;
this->pendingQuit = false; this->pendingQuit = false;
this->isSupportedServerVersion = CompatFetcher::isGfeVersionSupported(this->gfeVersion);
} }
bool NvComputer::wake() bool NvComputer::wake()
@@ -200,9 +155,8 @@ bool NvComputer::wake()
} }
const quint16 WOL_PORTS[] = { const quint16 WOL_PORTS[] = {
9, // Standard WOL port (privileged port) 7, 9, // Standard WOL ports
47998, 47999, 48000, 48002, 48010, // Ports opened by GFE 47998, 47999, 48000, // Ports opened by GFE
47009, // Port opened by Moonlight Internet Hosting Tool for WoL (non-privileged port)
}; };
// Create the WoL payload // Create the WoL payload
@@ -216,10 +170,7 @@ bool NvComputer::wake()
// Add the addresses that we know this host to be // Add the addresses that we know this host to be
// and broadcast addresses for this link just in // and broadcast addresses for this link just in
// case the host has timed out in ARP entries. // case the host has timed out in ARP entries.
QVector<QString> addressList; QVector<QString> addressList = uniqueAddresses();
for (const NvAddress& addr : uniqueAddresses()) {
addressList.append(addr.address());
}
addressList.append("255.255.255.255"); addressList.append("255.255.255.255");
// Try to broadcast on all available NICs // Try to broadcast on all available NICs
@@ -278,115 +229,19 @@ bool NvComputer::wake()
return success; return success;
} }
bool NvComputer::isReachableOverVpn() QVector<QString> NvComputer::uniqueAddresses()
{ {
if (activeAddress.isNull()) { QVector<QString> uniqueAddressList;
return false;
}
QTcpSocket s;
s.setProxy(QNetworkProxy::NoProxy);
s.connectToHost(activeAddress.address(), activeAddress.port());
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<NvAddress> NvComputer::uniqueAddresses() const
{
QVector<NvAddress> uniqueAddressList;
// Start with addresses correctly ordered // Start with addresses correctly ordered
uniqueAddressList.append(activeAddress); uniqueAddressList.append(activeAddress);
uniqueAddressList.append(localAddress); uniqueAddressList.append(localAddress);
uniqueAddressList.append(remoteAddress); uniqueAddressList.append(remoteAddress);
uniqueAddressList.append(ipv6Address);
uniqueAddressList.append(manualAddress); uniqueAddressList.append(manualAddress);
// Prune duplicates (always giving precedence to the first) // Prune duplicates (always giving precedence to the first)
for (int i = 0; i < uniqueAddressList.count(); i++) { for (int i = 0; i < uniqueAddressList.count(); i++) {
if (uniqueAddressList[i].isNull()) { if (uniqueAddressList[i].isEmpty()) {
uniqueAddressList.remove(i); uniqueAddressList.remove(i);
i--; i--;
continue; continue;
@@ -437,17 +292,11 @@ bool NvComputer::update(NvComputer& that)
changed = true; \ changed = true; \
} }
if (!hasCustomName) { ASSIGN_IF_CHANGED(name);
// Only overwrite the name if it's not custom
ASSIGN_IF_CHANGED(name);
}
ASSIGN_IF_CHANGED_AND_NONEMPTY(macAddress); ASSIGN_IF_CHANGED_AND_NONEMPTY(macAddress);
ASSIGN_IF_CHANGED_AND_NONNULL(localAddress); ASSIGN_IF_CHANGED_AND_NONEMPTY(localAddress);
ASSIGN_IF_CHANGED_AND_NONNULL(remoteAddress); ASSIGN_IF_CHANGED_AND_NONEMPTY(remoteAddress);
ASSIGN_IF_CHANGED_AND_NONNULL(ipv6Address); ASSIGN_IF_CHANGED_AND_NONEMPTY(manualAddress);
ASSIGN_IF_CHANGED_AND_NONNULL(manualAddress);
ASSIGN_IF_CHANGED(activeHttpsPort);
ASSIGN_IF_CHANGED(externalPort);
ASSIGN_IF_CHANGED(pairState); ASSIGN_IF_CHANGED(pairState);
ASSIGN_IF_CHANGED(serverCodecModeSupport); ASSIGN_IF_CHANGED(serverCodecModeSupport);
ASSIGN_IF_CHANGED(currentGameId); ASSIGN_IF_CHANGED(currentGameId);
@@ -455,16 +304,10 @@ bool NvComputer::update(NvComputer& that)
ASSIGN_IF_CHANGED(state); ASSIGN_IF_CHANGED(state);
ASSIGN_IF_CHANGED(gfeVersion); ASSIGN_IF_CHANGED(gfeVersion);
ASSIGN_IF_CHANGED(appVersion); ASSIGN_IF_CHANGED(appVersion);
ASSIGN_IF_CHANGED(isSupportedServerVersion);
ASSIGN_IF_CHANGED(maxLumaPixelsHEVC); ASSIGN_IF_CHANGED(maxLumaPixelsHEVC);
ASSIGN_IF_CHANGED(gpuModel); ASSIGN_IF_CHANGED(gpuModel);
ASSIGN_IF_CHANGED_AND_NONNULL(serverCert); ASSIGN_IF_CHANGED_AND_NONNULL(serverCert);
ASSIGN_IF_CHANGED_AND_NONEMPTY(appList);
ASSIGN_IF_CHANGED_AND_NONEMPTY(displayModes); ASSIGN_IF_CHANGED_AND_NONEMPTY(displayModes);
if (!that.appList.isEmpty()) {
// updateAppList() handles merging client-side attributes
updateAppList(that.appList);
}
return changed; return changed;
} }
+9 -25
View File
@@ -1,7 +1,6 @@
#pragma once #pragma once
#include "nvhttp.h" #include "nvhttp.h"
#include "nvaddress.h"
#include <QThread> #include <QThread>
#include <QReadWriteLock> #include <QReadWriteLock>
@@ -17,32 +16,24 @@ class NvComputer
private: private:
void sortAppList(); void sortAppList();
bool updateAppList(QVector<NvApp> newAppList);
bool pendingQuit; bool pendingQuit;
public: public:
explicit NvComputer(NvHTTP& http, QString serverInfo); explicit NvComputer(QString address, QString serverInfo, QSslCertificate serverCert);
explicit NvComputer(QSettings& settings); explicit NvComputer(QSettings& settings);
void
setRemoteAddress(QHostAddress);
bool bool
update(NvComputer& that); update(NvComputer& that);
bool bool
wake(); wake();
bool QVector<QString>
isReachableOverVpn(); uniqueAddresses();
QVector<NvAddress>
uniqueAddresses() const;
void void
serialize(QSettings& settings) const; serialize(QSettings& settings);
enum PairState enum PairState
{ {
@@ -61,8 +52,7 @@ public:
// Ephemeral traits // Ephemeral traits
ComputerState state; ComputerState state;
PairState pairState; PairState pairState;
NvAddress activeAddress; QString activeAddress;
uint16_t activeHttpsPort;
int currentGameId; int currentGameId;
QString gfeVersion; QString gfeVersion;
QString appVersion; QString appVersion;
@@ -70,23 +60,17 @@ public:
int maxLumaPixelsHEVC; int maxLumaPixelsHEVC;
int serverCodecModeSupport; int serverCodecModeSupport;
QString gpuModel; QString gpuModel;
bool isSupportedServerVersion;
// Persisted traits // Persisted traits
NvAddress localAddress; QString localAddress;
NvAddress remoteAddress; QString remoteAddress;
NvAddress ipv6Address; QString manualAddress;
NvAddress manualAddress;
QByteArray macAddress; QByteArray macAddress;
QString name; QString name;
bool hasCustomName;
QString uuid; QString uuid;
QSslCertificate serverCert; QSslCertificate serverCert;
QVector<NvApp> appList; QVector<NvApp> appList;
// Synchronization // Synchronization
mutable QReadWriteLock lock; QReadWriteLock lock;
private:
uint16_t externalPort;
}; };
+84 -173
View File
@@ -1,4 +1,4 @@
#include "nvcomputer.h" #include "nvhttp.h"
#include <Limelight.h> #include <Limelight.h>
#include <QDebug> #include <QDebug>
@@ -12,32 +12,27 @@
#include <QtEndian> #include <QtEndian>
#include <QNetworkProxy> #include <QNetworkProxy>
#define FAST_FAIL_TIMEOUT_MS 2000
#define REQUEST_TIMEOUT_MS 5000 #define REQUEST_TIMEOUT_MS 5000
#define LAUNCH_TIMEOUT_MS 120000 #define LAUNCH_TIMEOUT_MS 120000
#define RESUME_TIMEOUT_MS 30000 #define RESUME_TIMEOUT_MS 30000
#define QUIT_TIMEOUT_MS 30000 #define QUIT_TIMEOUT_MS 30000
NvHTTP::NvHTTP(NvAddress address, uint16_t httpsPort, QSslCertificate serverCert) : NvHTTP::NvHTTP(QString address, QSslCertificate serverCert) :
m_Address(address),
m_ServerCert(serverCert) m_ServerCert(serverCert)
{ {
Q_ASSERT(!address.isEmpty());
m_BaseUrlHttp.setScheme("http"); m_BaseUrlHttp.setScheme("http");
m_BaseUrlHttps.setScheme("https"); m_BaseUrlHttps.setScheme("https");
m_BaseUrlHttp.setHost(address);
setAddress(address); m_BaseUrlHttps.setHost(address);
setHttpsPort(httpsPort); m_BaseUrlHttp.setPort(47989);
m_BaseUrlHttps.setPort(47984);
// Never use a proxy server // Never use a proxy server
QNetworkProxy noProxy(QNetworkProxy::NoProxy); QNetworkProxy noProxy(QNetworkProxy::NoProxy);
m_Nam.setProxy(noProxy); m_Nam.setProxy(noProxy);
connect(&m_Nam, &QNetworkAccessManager::sslErrors, this, &NvHTTP::handleSslErrors);
}
NvHTTP::NvHTTP(NvComputer* computer) :
NvHTTP(computer->activeAddress, computer->activeHttpsPort, computer->serverCert)
{
} }
void NvHTTP::setServerCert(QSslCertificate serverCert) void NvHTTP::setServerCert(QSslCertificate serverCert)
@@ -45,43 +40,6 @@ void NvHTTP::setServerCert(QSslCertificate serverCert)
m_ServerCert = serverCert; m_ServerCert = serverCert;
} }
void NvHTTP::setAddress(NvAddress address)
{
Q_ASSERT(!address.isNull());
m_Address = address;
m_BaseUrlHttp.setHost(address.address());
m_BaseUrlHttps.setHost(address.address());
m_BaseUrlHttp.setPort(address.port());
}
void NvHTTP::setHttpsPort(uint16_t port)
{
m_BaseUrlHttps.setPort(port);
}
NvAddress NvHTTP::address()
{
return m_Address;
}
QSslCertificate NvHTTP::serverCert()
{
return m_ServerCert;
}
uint16_t NvHTTP::httpPort()
{
return m_BaseUrlHttp.port();
}
uint16_t NvHTTP::httpsPort()
{
return m_BaseUrlHttps.port();
}
QVector<int> QVector<int>
NvHTTP::parseQuad(QString quad) NvHTTP::parseQuad(QString quad)
{ {
@@ -94,8 +52,7 @@ NvHTTP::parseQuad(QString quad)
} }
QStringList parts = quad.split("."); QStringList parts = quad.split(".");
ret.reserve(parts.length()); for (int i = 0; i < 4; i++)
for (int i = 0; i < parts.length(); i++)
{ {
ret.append(parts.at(i).toInt()); ret.append(parts.at(i).toInt());
} }
@@ -121,12 +78,12 @@ NvHTTP::getCurrentGame(QString serverInfo)
} }
QString QString
NvHTTP::getServerInfo(NvLogLevel logLevel, bool fastFail) NvHTTP::getServerInfo(NvLogLevel logLevel)
{ {
QString serverInfo; QString serverInfo;
// Check if we have a pinned cert and HTTPS port for this host yet // Check if we have a pinned cert for this host yet
if (!m_ServerCert.isNull() && httpsPort() != 0) if (!m_ServerCert.isNull())
{ {
try try
{ {
@@ -135,7 +92,7 @@ NvHTTP::getServerInfo(NvLogLevel logLevel, bool fastFail)
serverInfo = openConnectionToString(m_BaseUrlHttps, serverInfo = openConnectionToString(m_BaseUrlHttps,
"serverinfo", "serverinfo",
nullptr, nullptr,
fastFail ? FAST_FAIL_TIMEOUT_MS : REQUEST_TIMEOUT_MS, REQUEST_TIMEOUT_MS,
logLevel); logLevel);
// Throws if the request failed // Throws if the request failed
verifyResponseStatus(serverInfo); verifyResponseStatus(serverInfo);
@@ -148,7 +105,7 @@ NvHTTP::getServerInfo(NvLogLevel logLevel, bool fastFail)
serverInfo = openConnectionToString(m_BaseUrlHttp, serverInfo = openConnectionToString(m_BaseUrlHttp,
"serverinfo", "serverinfo",
nullptr, nullptr,
fastFail ? FAST_FAIL_TIMEOUT_MS : REQUEST_TIMEOUT_MS, REQUEST_TIMEOUT_MS,
logLevel); logLevel);
verifyResponseStatus(serverInfo); verifyResponseStatus(serverInfo);
} }
@@ -161,38 +118,48 @@ NvHTTP::getServerInfo(NvLogLevel logLevel, bool fastFail)
} }
else else
{ {
// Only use HTTP prior to pairing or fetching HTTPS port // Only use HTTP prior to pairing
serverInfo = openConnectionToString(m_BaseUrlHttp, serverInfo = openConnectionToString(m_BaseUrlHttp,
"serverinfo", "serverinfo",
nullptr, nullptr,
fastFail ? FAST_FAIL_TIMEOUT_MS : REQUEST_TIMEOUT_MS, REQUEST_TIMEOUT_MS,
logLevel); logLevel);
verifyResponseStatus(serverInfo); verifyResponseStatus(serverInfo);
// Populate the HTTPS port
uint16_t httpsPort = getXmlString(serverInfo, "HttpsPort").toUShort();
if (httpsPort == 0) {
httpsPort = DEFAULT_HTTPS_PORT;
}
setHttpsPort(httpsPort);
// If we just needed to determine the HTTPS port, we'll try again over
// HTTPS now that we have the port number
if (!m_ServerCert.isNull()) {
return getServerInfo(logLevel, fastFail);
}
} }
return serverInfo; return serverInfo;
} }
static QString
getSurroundAudioInfoString(int audioConfig)
{
int channelMask;
int channelCount;
switch (audioConfig)
{
case AUDIO_CONFIGURATION_STEREO:
channelCount = 2;
channelMask = 0x3;
break;
case AUDIO_CONFIGURATION_51_SURROUND:
channelCount = 6;
channelMask = 0xFC;
break;
default:
Q_ASSERT(false);
return 0;
}
return QString::number(channelMask << 16 | channelCount);
}
void void
NvHTTP::launchApp(int appId, NvHTTP::launchApp(int appId,
PSTREAM_CONFIGURATION streamConfig, PSTREAM_CONFIGURATION streamConfig,
bool sops, bool sops,
bool localAudio, bool localAudio,
int gamepadMask, int gamepadMask)
QString& rtspSessionUrl)
{ {
int riKeyId; int riKeyId;
@@ -205,11 +172,7 @@ NvHTTP::launchApp(int appId,
"appid="+QString::number(appId)+ "appid="+QString::number(appId)+
"&mode="+QString::number(streamConfig->width)+"x"+ "&mode="+QString::number(streamConfig->width)+"x"+
QString::number(streamConfig->height)+"x"+ QString::number(streamConfig->height)+"x"+
// Using an FPS value over 60 causes SOPS to default to 720p60, QString::number(streamConfig->fps)+
// so force it to 0 to ensure the correct resolution is set. We
// used to use 60 here but that locked the frame rate to 60 FPS
// on GFE 3.20.3.
QString::number(streamConfig->fps > 60 ? 0 : streamConfig->fps)+
"&additionalStates=1&sops="+QString::number(sops ? 1 : 0)+ "&additionalStates=1&sops="+QString::number(sops ? 1 : 0)+
"&rikey="+QByteArray(streamConfig->remoteInputAesKey, sizeof(streamConfig->remoteInputAesKey)).toHex()+ "&rikey="+QByteArray(streamConfig->remoteInputAesKey, sizeof(streamConfig->remoteInputAesKey)).toHex()+
"&rikeyid="+QString::number(riKeyId)+ "&rikeyid="+QString::number(riKeyId)+
@@ -217,21 +180,17 @@ NvHTTP::launchApp(int appId,
"&hdrMode=1&clientHdrCapVersion=0&clientHdrCapSupportedFlagsInUint32=0&clientHdrCapMetaDataId=NV_STATIC_METADATA_TYPE_1&clientHdrCapDisplayData=0x0x0x0x0x0x0x0x0x0x0" : "&hdrMode=1&clientHdrCapVersion=0&clientHdrCapSupportedFlagsInUint32=0&clientHdrCapMetaDataId=NV_STATIC_METADATA_TYPE_1&clientHdrCapDisplayData=0x0x0x0x0x0x0x0x0x0x0" :
"")+ "")+
"&localAudioPlayMode="+QString::number(localAudio ? 1 : 0)+ "&localAudioPlayMode="+QString::number(localAudio ? 1 : 0)+
"&surroundAudioInfo="+QString::number(SURROUNDAUDIOINFO_FROM_AUDIO_CONFIGURATION(streamConfig->audioConfiguration))+ "&surroundAudioInfo="+getSurroundAudioInfoString(streamConfig->audioConfiguration)+
"&remoteControllersBitmap="+QString::number(gamepadMask)+ "&remoteControllersBitmap="+QString::number(gamepadMask)+
"&gcmap="+QString::number(gamepadMask), "&gcmap="+QString::number(gamepadMask),
LAUNCH_TIMEOUT_MS); LAUNCH_TIMEOUT_MS);
qInfo() << "Launch response:" << response;
// Throws if the request failed // Throws if the request failed
verifyResponseStatus(response); verifyResponseStatus(response);
rtspSessionUrl = getXmlString(response, "sessionUrl0");
} }
void void
NvHTTP::resumeApp(PSTREAM_CONFIGURATION streamConfig, QString& rtspSessionUrl) NvHTTP::resumeApp(PSTREAM_CONFIGURATION streamConfig)
{ {
int riKeyId; int riKeyId;
@@ -243,15 +202,11 @@ NvHTTP::resumeApp(PSTREAM_CONFIGURATION streamConfig, QString& rtspSessionUrl)
"resume", "resume",
"rikey="+QString(QByteArray(streamConfig->remoteInputAesKey, sizeof(streamConfig->remoteInputAesKey)).toHex())+ "rikey="+QString(QByteArray(streamConfig->remoteInputAesKey, sizeof(streamConfig->remoteInputAesKey)).toHex())+
"&rikeyid="+QString::number(riKeyId)+ "&rikeyid="+QString::number(riKeyId)+
"&surroundAudioInfo="+QString::number(SURROUNDAUDIOINFO_FROM_AUDIO_CONFIGURATION(streamConfig->audioConfiguration)), "&surroundAudioInfo="+getSurroundAudioInfoString(streamConfig->audioConfiguration),
RESUME_TIMEOUT_MS); RESUME_TIMEOUT_MS);
qInfo() << "Resume response:" << response;
// Throws if the request failed // Throws if the request failed
verifyResponseStatus(response); verifyResponseStatus(response);
rtspSessionUrl = getXmlString(response, "sessionUrl0");
} }
void void
@@ -263,14 +218,12 @@ NvHTTP::quitApp()
nullptr, nullptr,
QUIT_TIMEOUT_MS); QUIT_TIMEOUT_MS);
qInfo() << "Quit response:" << response;
// Throws if the request failed // Throws if the request failed
verifyResponseStatus(response); verifyResponseStatus(response);
// Newer GFE versions will just return success even if quitting fails // Newer GFE versions will just return success even if quitting fails
// if we're not the original requestor. // 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 // Generate a synthetic GfeResponseException letting the caller know
// that they can't kill someone else's stream. // that they can't kill someone else's stream.
throw GfeHttpResponseException(599, ""); throw GfeHttpResponseException(599, "");
@@ -285,17 +238,17 @@ NvHTTP::getDisplayModeList(QString serverInfo)
while (!xmlReader.atEnd()) { while (!xmlReader.atEnd()) {
while (xmlReader.readNextStartElement()) { while (xmlReader.readNextStartElement()) {
auto name = xmlReader.name(); QStringRef name = xmlReader.name();
if (name == QString("DisplayMode")) { if (xmlReader.name() == "DisplayMode") {
modes.append(NvDisplayMode()); modes.append(NvDisplayMode());
} }
else if (name == QString("Width")) { else if (xmlReader.name() == "Width") {
modes.last().width = xmlReader.readElementText().toInt(); modes.last().width = xmlReader.readElementText().toInt();
} }
else if (name == QString("Height")) { else if (xmlReader.name() == "Height") {
modes.last().height = xmlReader.readElementText().toInt(); modes.last().height = xmlReader.readElementText().toInt();
} }
else if (name == QString("RefreshRate")) { else if (xmlReader.name() == "RefreshRate") {
modes.last().refreshRate = xmlReader.readElementText().toInt(); modes.last().refreshRate = xmlReader.readElementText().toInt();
} }
} }
@@ -311,15 +264,15 @@ NvHTTP::getAppList()
"applist", "applist",
nullptr, nullptr,
REQUEST_TIMEOUT_MS, REQUEST_TIMEOUT_MS,
NvLogLevel::NVLL_ERROR); NvLogLevel::ERROR);
verifyResponseStatus(appxml); verifyResponseStatus(appxml);
QXmlStreamReader xmlReader(appxml); QXmlStreamReader xmlReader(appxml);
QVector<NvApp> apps; QVector<NvApp> apps;
while (!xmlReader.atEnd()) { while (!xmlReader.atEnd()) {
while (xmlReader.readNextStartElement()) { while (xmlReader.readNextStartElement()) {
auto name = xmlReader.name(); QStringRef name = xmlReader.name();
if (name == QString("App")) { if (xmlReader.name() == "App") {
// We must have a valid app before advancing to the next one // We must have a valid app before advancing to the next one
if (!apps.isEmpty() && !apps.last().isInitialized()) { if (!apps.isEmpty() && !apps.last().isInitialized()) {
qWarning() << "Invalid applist XML"; qWarning() << "Invalid applist XML";
@@ -328,18 +281,15 @@ NvHTTP::getAppList()
} }
apps.append(NvApp()); apps.append(NvApp());
} }
else if (name == QString("AppTitle")) { else if (xmlReader.name() == "AppTitle") {
apps.last().name = xmlReader.readElementText(); apps.last().name = xmlReader.readElementText();
} }
else if (name == QString("ID")) { else if (xmlReader.name() == "ID") {
apps.last().id = xmlReader.readElementText().toInt(); apps.last().id = xmlReader.readElementText().toInt();
} }
else if (name == QString("IsHdrSupported")) { else if (xmlReader.name() == "IsHdrSupported") {
apps.last().hdrSupported = xmlReader.readElementText() == "1"; apps.last().hdrSupported = xmlReader.readElementText() == "1";
} }
else if (name == QString("IsAppCollectorGame")) {
apps.last().isAppCollectorGame = xmlReader.readElementText() == "1";
}
} }
} }
@@ -353,12 +303,9 @@ NvHTTP::verifyResponseStatus(QString xml)
while (xmlReader.readNextStartElement()) 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 int statusCode = xmlReader.attributes().value("status_code").toInt();
// QString::toInt() will fail in that case, so use QString::toUInt()
// and cast the result to an int instead.
int statusCode = (int)xmlReader.attributes().value("status_code").toUInt();
if (statusCode == 200) if (statusCode == 200)
{ {
// Successful // Successful
@@ -371,18 +318,10 @@ NvHTTP::verifyResponseStatus(QString xml)
// 401 is expected for unpaired PCs when we fetch serverinfo over HTTPS // 401 is expected for unpaired PCs when we fetch serverinfo over HTTPS
qWarning() << "Request failed:" << statusCode << statusMessage; 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); throw GfeHttpResponseException(statusCode, statusMessage);
} }
} }
} }
throw GfeHttpResponseException(-1, "Malformed GFE XML (missing root element)");
} }
QImage QImage
@@ -393,7 +332,7 @@ NvHTTP::getBoxArt(int appId)
"appid="+QString::number(appId)+ "appid="+QString::number(appId)+
"&AssetType=2&AssetIdx=0", "&AssetType=2&AssetIdx=0",
REQUEST_TIMEOUT_MS, REQUEST_TIMEOUT_MS,
NvLogLevel::NVLL_VERBOSE); NvLogLevel::VERBOSE);
QImage image = QImageReader(reply).read(); QImage image = QImageReader(reply).read();
delete reply; delete reply;
@@ -435,28 +374,6 @@ NvHTTP::getXmlString(QString xml,
return nullptr; 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 (const QSslError& error : errors) {
if (m_ServerCert != error.certificate()) {
ignoreErrors = false;
break;
}
}
if (ignoreErrors) {
reply->ignoreSslErrors(errors);
}
}
QString QString
NvHTTP::openConnectionToString(QUrl baseUrl, NvHTTP::openConnectionToString(QUrl baseUrl,
QString command, QString command,
@@ -468,13 +385,7 @@ NvHTTP::openConnectionToString(QUrl baseUrl,
QString ret; QString ret;
QTextStream stream(reply); QTextStream stream(reply);
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
stream.setEncoding(QStringConverter::Utf8);
#else
stream.setCodec("UTF-8"); stream.setCodec("UTF-8");
#endif
ret = stream.readAll(); ret = stream.readAll();
delete reply; delete reply;
@@ -488,9 +399,6 @@ NvHTTP::openConnection(QUrl baseUrl,
int timeoutMs, int timeoutMs,
NvLogLevel logLevel) NvLogLevel logLevel)
{ {
// Port must be set
Q_ASSERT(baseUrl.port(0) != 0);
// Build a URL for the request // Build a URL for the request
QUrl url(baseUrl); QUrl url(baseUrl);
url.setPath("/" + command); url.setPath("/" + command);
@@ -502,34 +410,37 @@ NvHTTP::openConnection(QUrl baseUrl,
QUuid::createUuid().toRfc4122().toHex() + QUuid::createUuid().toRfc4122().toHex() +
((arguments != nullptr) ? ("&" + arguments) : "")); ((arguments != nullptr) ? ("&" + arguments) : ""));
QNetworkRequest request(url); QNetworkRequest request = QNetworkRequest(url);
// Add our client certificate // Add our client certificate
request.setSslConfiguration(IdentityManager::get()->getSslConfig()); request.setSslConfiguration(IdentityManager::get()->getSslConfig());
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
// Disable HTTP/2 (GFE 3.22 doesn't like it) and Qt 6 enables it by default
request.setAttribute(QNetworkRequest::Http2AllowedAttribute, false);
#endif
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) && QT_VERSION < QT_VERSION_CHECK(5, 15, 1) && !defined(QT_NO_BEARERMANAGEMENT)
// 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); 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 // Run the request with a timeout if requested
QEventLoop loop; QEventLoop loop;
connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); QObject::connect(reply, SIGNAL(finished()), &loop, SLOT(quit()));
connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, &loop, &QEventLoop::quit);
if (timeoutMs) { if (timeoutMs) {
QTimer::singleShot(timeoutMs, &loop, &QEventLoop::quit); QTimer::singleShot(timeoutMs, &loop, SLOT(quit()));
} }
if (logLevel >= NvLogLevel::NVLL_VERBOSE) { if (logLevel >= NvLogLevel::VERBOSE) {
qInfo() << "Executing request:" << url.toString(); qInfo() << "Executing request:" << url.toString();
} }
loop.exec(QEventLoop::ExcludeUserInputEvents); loop.exec(QEventLoop::ExcludeUserInputEvents);
@@ -537,7 +448,7 @@ NvHTTP::openConnection(QUrl baseUrl,
// Abort the request if it timed out // Abort the request if it timed out
if (!reply->isFinished()) if (!reply->isFinished())
{ {
if (logLevel >= NvLogLevel::NVLL_ERROR) { if (logLevel >= NvLogLevel::ERROR) {
qWarning() << "Aborting timed out request for" << url.toString(); qWarning() << "Aborting timed out request for" << url.toString();
} }
reply->abort(); reply->abort();
@@ -550,7 +461,7 @@ NvHTTP::openConnection(QUrl baseUrl,
// Handle error // Handle error
if (reply->error() != QNetworkReply::NoError) if (reply->error() != QNetworkReply::NoError)
{ {
if (logLevel >= NvLogLevel::NVLL_ERROR) { if (logLevel >= NvLogLevel::ERROR) {
qWarning() << command << " request failed with error " << reply->error(); qWarning() << command << " request failed with error " << reply->error();
} }
+29 -34
View File
@@ -1,8 +1,6 @@
#pragma once #pragma once
#include "identitymanager.h" #include "identitymanager.h"
#include "nvapp.h"
#include "nvaddress.h"
#include <Limelight.h> #include <Limelight.h>
@@ -10,7 +8,25 @@
#include <QNetworkAccessManager> #include <QNetworkAccessManager>
#include <QNetworkReply> #include <QNetworkReply>
class NvComputer; 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 class NvDisplayMode
{ {
@@ -26,7 +42,6 @@ public:
int height; int height;
int refreshRate; int refreshRate;
}; };
Q_DECLARE_TYPEINFO(NvDisplayMode, Q_PRIMITIVE_TYPE);
class GfeHttpResponseException : public std::exception class GfeHttpResponseException : public std::exception
{ {
@@ -98,27 +113,23 @@ private:
QString m_ErrorText; QString m_ErrorText;
}; };
class NvHTTP : public QObject class NvHTTP
{ {
Q_OBJECT
public: public:
enum NvLogLevel { enum NvLogLevel {
NVLL_NONE, NONE,
NVLL_ERROR, ERROR,
NVLL_VERBOSE VERBOSE
}; };
explicit NvHTTP(NvAddress address, uint16_t httpsPort, QSslCertificate serverCert); explicit NvHTTP(QString address, QSslCertificate serverCert);
explicit NvHTTP(NvComputer* computer);
static static
int int
getCurrentGame(QString serverInfo); getCurrentGame(QString serverInfo);
QString QString
getServerInfo(NvLogLevel logLevel, bool fastFail = false); getServerInfo(NvLogLevel logLevel);
static static
void void
@@ -139,21 +150,10 @@ public:
QString command, QString command,
QString arguments, QString arguments,
int timeoutMs, int timeoutMs,
NvLogLevel logLevel = NvLogLevel::NVLL_VERBOSE); NvLogLevel logLevel = NvLogLevel::VERBOSE);
void setServerCert(QSslCertificate serverCert); void setServerCert(QSslCertificate serverCert);
void setAddress(NvAddress address);
void setHttpsPort(uint16_t port);
NvAddress address();
QSslCertificate serverCert();
uint16_t httpPort();
uint16_t httpsPort();
static static
QVector<int> QVector<int>
parseQuad(QString quad); parseQuad(QString quad);
@@ -162,16 +162,14 @@ public:
quitApp(); quitApp();
void void
resumeApp(PSTREAM_CONFIGURATION streamConfig, resumeApp(PSTREAM_CONFIGURATION streamConfig);
QString& rtspSessionUrl);
void void
launchApp(int appId, launchApp(int appId,
PSTREAM_CONFIGURATION streamConfig, PSTREAM_CONFIGURATION streamConfig,
bool sops, bool sops,
bool localAudio, bool localAudio,
int gamepadMask, int gamepadMask);
QString& rtspSessionUrl);
QVector<NvApp> QVector<NvApp>
getAppList(); getAppList();
@@ -186,9 +184,6 @@ public:
QUrl m_BaseUrlHttp; QUrl m_BaseUrlHttp;
QUrl m_BaseUrlHttps; QUrl m_BaseUrlHttps;
private: private:
void
handleSslErrors(QNetworkReply* reply, const QList<QSslError>& errors);
QNetworkReply* QNetworkReply*
openConnection(QUrl baseUrl, openConnection(QUrl baseUrl,
QString command, QString command,
@@ -196,7 +191,7 @@ private:
int timeoutMs, int timeoutMs,
NvLogLevel logLevel); NvLogLevel logLevel);
NvAddress m_Address; QString m_Address;
QNetworkAccessManager m_Nam; QNetworkAccessManager m_Nam;
QSslCertificate m_ServerCert; QSslCertificate m_ServerCert;
}; };
+39 -67
View File
@@ -1,9 +1,8 @@
#include "nvpairingmanager.h" #include "nvpairingmanager.h"
#include "utils.h" #include "utils.h"
#include <stdexcept>
#include <openssl/bio.h> #include <openssl/bio.h>
#include <openssl/aes.h>
#include <openssl/rand.h> #include <openssl/rand.h>
#include <openssl/pem.h> #include <openssl/pem.h>
#include <openssl/x509.h> #include <openssl/x509.h>
@@ -11,8 +10,8 @@
#define REQUEST_TIMEOUT_MS 5000 #define REQUEST_TIMEOUT_MS 5000
NvPairingManager::NvPairingManager(NvComputer* computer) : NvPairingManager::NvPairingManager(QString address) :
m_Http(computer) m_Http(address, QSslCertificate())
{ {
QByteArray cert = IdentityManager::get()->getCertificate(); QByteArray cert = IdentityManager::get()->getCertificate();
BIO *bio = BIO_new_mem_buf(cert.data(), -1); BIO *bio = BIO_new_mem_buf(cert.data(), -1);
@@ -52,63 +51,39 @@ NvPairingManager::generateRandomBytes(int length)
} }
QByteArray QByteArray
NvPairingManager::encrypt(const QByteArray& plaintext, const QByteArray& key) NvPairingManager::encrypt(QByteArray plaintext, AES_KEY* key)
{ {
QByteArray ciphertext(plaintext.size(), 0); QByteArray ciphertext(plaintext.size(), 0);
EVP_CIPHER_CTX* cipher;
int ciphertextLen;
cipher = EVP_CIPHER_CTX_new(); for (int i = 0; i < plaintext.size(); i += 16)
THROW_BAD_ALLOC_IF_NULL(cipher); {
AES_encrypt(reinterpret_cast<unsigned char*>(&plaintext.data()[i]),
EVP_EncryptInit(cipher, EVP_aes_128_ecb(), reinterpret_cast<const unsigned char*>(key.data()), NULL); reinterpret_cast<unsigned char*>(&ciphertext.data()[i]),
EVP_CIPHER_CTX_set_padding(cipher, 0); key);
}
EVP_EncryptUpdate(cipher,
reinterpret_cast<unsigned char*>(ciphertext.data()),
&ciphertextLen,
reinterpret_cast<const unsigned char*>(plaintext.data()),
plaintext.length());
Q_ASSERT(ciphertextLen == ciphertext.length());
EVP_CIPHER_CTX_free(cipher);
return ciphertext; return ciphertext;
} }
QByteArray QByteArray
NvPairingManager::decrypt(const QByteArray& ciphertext, const QByteArray& key) NvPairingManager::decrypt(QByteArray ciphertext, AES_KEY* key)
{ {
QByteArray plaintext(ciphertext.size(), 0); QByteArray plaintext(ciphertext.size(), 0);
EVP_CIPHER_CTX* cipher;
int plaintextLen;
cipher = EVP_CIPHER_CTX_new(); for (int i = 0; i < plaintext.size(); i += 16)
THROW_BAD_ALLOC_IF_NULL(cipher); {
AES_decrypt(reinterpret_cast<unsigned char*>(&ciphertext.data()[i]),
EVP_DecryptInit(cipher, EVP_aes_128_ecb(), reinterpret_cast<const unsigned char*>(key.data()), NULL); reinterpret_cast<unsigned char*>(&plaintext.data()[i]),
EVP_CIPHER_CTX_set_padding(cipher, 0); key);
}
EVP_DecryptUpdate(cipher,
reinterpret_cast<unsigned char*>(plaintext.data()),
&plaintextLen,
reinterpret_cast<const unsigned char*>(ciphertext.data()),
ciphertext.length());
Q_ASSERT(plaintextLen == plaintext.length());
EVP_CIPHER_CTX_free(cipher);
return plaintext; return plaintext;
} }
QByteArray 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); BIO* bio = BIO_new_mem_buf(certificate.data(), -1);
#endif
THROW_BAD_ALLOC_IF_NULL(bio); THROW_BAD_ALLOC_IF_NULL(bio);
X509* cert = PEM_read_bio_X509(bio, nullptr, nullptr, nullptr); X509* cert = PEM_read_bio_X509(bio, nullptr, nullptr, nullptr);
@@ -132,13 +107,9 @@ NvPairingManager::getSignatureFromPemCert(const QByteArray& certificate)
} }
bool 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); BIO* bio = BIO_new_mem_buf(serverCertificate.data(), -1);
#endif
THROW_BAD_ALLOC_IF_NULL(bio); THROW_BAD_ALLOC_IF_NULL(bio);
X509* cert = PEM_read_bio_X509(bio, nullptr, nullptr, nullptr); 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_DigestVerifyInit(mdctx, nullptr, EVP_sha256(), nullptr, pubKey);
EVP_DigestVerifyUpdate(mdctx, data.data(), data.length()); 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_PKEY_free(pubKey);
EVP_MD_CTX_destroy(mdctx); EVP_MD_CTX_destroy(mdctx);
@@ -162,13 +133,17 @@ NvPairingManager::verifySignature(const QByteArray& data, const QByteArray& sign
} }
QByteArray QByteArray
NvPairingManager::signMessage(const QByteArray& message) NvPairingManager::signMessage(QByteArray message)
{ {
EVP_MD_CTX *ctx = EVP_MD_CTX_create(); EVP_MD_CTX *ctx = EVP_MD_CTX_create();
THROW_BAD_ALLOC_IF_NULL(ctx); THROW_BAD_ALLOC_IF_NULL(ctx);
EVP_DigestSignInit(ctx, NULL, EVP_sha256(), NULL, m_PrivateKey); const EVP_MD *md = EVP_get_digestbyname("SHA256");
EVP_DigestSignUpdate(ctx, reinterpret_cast<unsigned char*>(const_cast<char*>(message.data())), message.length()); THROW_BAD_ALLOC_IF_NULL(md);
EVP_DigestInit_ex(ctx, md, NULL);
EVP_DigestSignInit(ctx, NULL, md, NULL, m_PrivateKey);
EVP_DigestSignUpdate(ctx, reinterpret_cast<unsigned char*>(message.data()), message.length());
size_t signatureLength = 0; size_t signatureLength = 0;
EVP_DigestSignFinal(ctx, NULL, &signatureLength); EVP_DigestSignFinal(ctx, NULL, &signatureLength);
@@ -182,7 +157,7 @@ NvPairingManager::signMessage(const QByteArray& message)
} }
QByteArray QByteArray
NvPairingManager::saltPin(const QByteArray& salt, QString pin) NvPairingManager::saltPin(QByteArray salt, QString pin)
{ {
return QByteArray().append(salt).append(pin.toLatin1()); return QByteArray().append(salt).append(pin.toLatin1());
} }
@@ -211,8 +186,9 @@ NvPairingManager::pair(QString appVersion, QString pin, QSslCertificate& serverC
QByteArray salt = generateRandomBytes(16); QByteArray salt = generateRandomBytes(16);
QByteArray saltedPin = saltPin(salt, pin); QByteArray saltedPin = saltPin(salt, pin);
QByteArray aesKey = QCryptographicHash::hash(saltedPin, hashAlgo).constData(); AES_KEY encKey, decKey;
aesKey.truncate(16); 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, QString getCert = m_Http.openConnectionToString(m_Http.m_BaseUrlHttp,
"pair", "pair",
@@ -234,21 +210,20 @@ NvPairingManager::pair(QString appVersion, QString pin, QSslCertificate& serverC
return PairState::ALREADY_IN_PROGRESS; return PairState::ALREADY_IN_PROGRESS;
} }
QSslCertificate unverifiedServerCert = QSslCertificate(serverCertStr); serverCert = QSslCertificate(serverCertStr);
if (unverifiedServerCert.isNull()) { if (serverCert.isNull()) {
Q_ASSERT(!unverifiedServerCert.isNull()); Q_ASSERT(!serverCert.isNull());
qCritical() << "Failed to parse plaincert"; qCritical() << "Failed to parse plaincert";
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, REQUEST_TIMEOUT_MS); m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, REQUEST_TIMEOUT_MS);
return PairState::FAILED; return PairState::FAILED;
} }
// Pin this cert for TLS until pairing is complete. If successful, we will propagate // Pin this cert for TLS
// the cert into the NvComputer object and persist it. m_Http.setServerCert(serverCert);
m_Http.setServerCert(unverifiedServerCert);
QByteArray randomChallenge = generateRandomBytes(16); QByteArray randomChallenge = generateRandomBytes(16);
QByteArray encryptedChallenge = encrypt(randomChallenge, aesKey); QByteArray encryptedChallenge = encrypt(randomChallenge, &encKey);
QString challengeXml = m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, QString challengeXml = m_Http.openConnectionToString(m_Http.m_BaseUrlHttp,
"pair", "pair",
"devicename=roth&updateState=1&clientchallenge=" + "devicename=roth&updateState=1&clientchallenge=" +
@@ -262,7 +237,7 @@ NvPairingManager::pair(QString appVersion, QString pin, QSslCertificate& serverC
return PairState::FAILED; 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 clientSecretData = generateRandomBytes(16);
QByteArray challengeResponse; QByteArray challengeResponse;
QByteArray serverResponse(challengeResponseData.data(), hashLength); QByteArray serverResponse(challengeResponseData.data(), hashLength);
@@ -281,9 +256,7 @@ NvPairingManager::pair(QString appVersion, QString pin, QSslCertificate& serverC
challengeResponse.append(reinterpret_cast<char*>(asnSignature->data), asnSignature->length); challengeResponse.append(reinterpret_cast<char*>(asnSignature->data), asnSignature->length);
challengeResponse.append(clientSecretData); challengeResponse.append(clientSecretData);
QByteArray paddedHash = QCryptographicHash::hash(challengeResponse, hashAlgo); QByteArray encryptedChallengeResponseHash = encrypt(QCryptographicHash::hash(challengeResponse, hashAlgo), &encKey);
paddedHash.resize(32);
QByteArray encryptedChallengeResponseHash = encrypt(paddedHash, aesKey);
QString respXml = m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, QString respXml = m_Http.openConnectionToString(m_Http.m_BaseUrlHttp,
"pair", "pair",
"devicename=roth&updateState=1&serverchallengeresp=" + "devicename=roth&updateState=1&serverchallengeresp=" +
@@ -350,6 +323,5 @@ NvPairingManager::pair(QString appVersion, QString pin, QSslCertificate& serverC
return PairState::FAILED; return PairState::FAILED;
} }
serverCert = std::move(unverifiedServerCert);
return PairState::PAIRED; return PairState::PAIRED;
} }
+8 -7
View File
@@ -3,6 +3,7 @@
#include "identitymanager.h" #include "identitymanager.h"
#include "nvhttp.h" #include "nvhttp.h"
#include <openssl/aes.h>
#include <openssl/x509.h> #include <openssl/x509.h>
#include <openssl/evp.h> #include <openssl/evp.h>
@@ -17,7 +18,7 @@ public:
ALREADY_IN_PROGRESS ALREADY_IN_PROGRESS
}; };
explicit NvPairingManager(NvComputer* computer); explicit NvPairingManager(QString address);
~NvPairingManager(); ~NvPairingManager();
@@ -29,22 +30,22 @@ private:
generateRandomBytes(int length); generateRandomBytes(int length);
QByteArray QByteArray
saltPin(const QByteArray& salt, QString pin); saltPin(QByteArray salt, QString pin);
QByteArray QByteArray
encrypt(const QByteArray& plaintext, const QByteArray& key); encrypt(QByteArray plaintext, AES_KEY* key);
QByteArray QByteArray
decrypt(const QByteArray& ciphertext, const QByteArray& key); decrypt(QByteArray ciphertext, AES_KEY* key);
QByteArray QByteArray
getSignatureFromPemCert(const QByteArray& certificate); getSignatureFromPemCert(QByteArray certificate);
bool bool
verifySignature(const QByteArray& data, const QByteArray& signature, const QByteArray& serverCertificate); verifySignature(QByteArray data, QByteArray signature, QByteArray serverCertificate);
QByteArray QByteArray
signMessage(const QByteArray& message); signMessage(QByteArray message);
NvHTTP m_Http; NvHTTP m_Http;
X509* m_Cert; 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;
};
-252
View File
@@ -1,252 +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);
hasDesktopEnvironment = WMUtils::isRunningDesktopEnvironment();
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();
}
// Assume we can probably launch a browser if we're in a GUI environment
hasBrowser = hasDesktopEnvironment;
#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);
}
class QuerySdlVideoThread : public QThread
{
public:
QuerySdlVideoThread(SystemProperties* me) :
QThread(nullptr),
m_Me(me) {}
void run() override
{
m_Me->querySdlVideoInfoInternal();
}
SystemProperties* m_Me;
};
void SystemProperties::querySdlVideoInfo()
{
if (WMUtils::isRunningX11() || WMUtils::isRunningWayland()) {
// Use a separate thread to temporarily initialize SDL
// video to avoid stomping on Qt's X11 and OGL state.
QuerySdlVideoThread thread(this);
thread.start();
thread.wait();
}
else {
querySdlVideoInfoInternal();
}
}
void SystemProperties::querySdlVideoInfoInternal()
{
hasHardwareAcceleration = false;
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).
// We call the internal variant because we're already in a safe thread context.
refreshDisplaysInternal();
SDL_Window* testWindow = SDL_CreateWindow("", 0, 0, 1280, 720,
SDL_WINDOW_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, supportsHdr, maximumResolution);
SDL_DestroyWindow(testWindow);
SDL_QuitSubSystem(SDL_INIT_VIDEO);
}
class RefreshDisplaysThread : public QThread
{
public:
RefreshDisplaysThread(SystemProperties* me) :
QThread(nullptr),
m_Me(me) {}
void run() override
{
m_Me->refreshDisplaysInternal();
}
SystemProperties* m_Me;
};
void SystemProperties::refreshDisplays()
{
if (WMUtils::isRunningX11() || WMUtils::isRunningWayland()) {
// Use a separate thread to temporarily initialize SDL
// video to avoid stomping on Qt's X11 and OGL state.
RefreshDisplaysThread thread(this);
thread.start();
thread.wait();
}
else {
refreshDisplaysInternal();
}
}
void SystemProperties::refreshDisplaysInternal()
{
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
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);
}
-60
View File
@@ -1,60 +0,0 @@
#pragma once
#include <QObject>
#include <QRect>
class SystemProperties : public QObject
{
Q_OBJECT
friend class QuerySdlVideoThread;
friend class RefreshDisplaysThread;
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 hasDesktopEnvironment MEMBER hasDesktopEnvironment 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_PROPERTY(bool supportsHdr MEMBER supportsHdr CONSTANT)
Q_INVOKABLE void refreshDisplays();
Q_INVOKABLE QRect getDesktopResolution(int displayIndex);
Q_INVOKABLE QRect getNativeResolution(int displayIndex);
signals:
void unmappedGamepadsChanged();
private:
void querySdlVideoInfo();
void querySdlVideoInfoInternal();
void refreshDisplaysInternal();
bool hasHardwareAcceleration;
bool rendererAlwaysFullScreen;
bool isRunningWayland;
bool isRunningXWayland;
bool isWow64;
QString friendlyNativeArchName;
bool hasDesktopEnvironment;
bool hasBrowser;
bool hasDiscordIntegration;
QString unmappedGamepads;
int maximumStreamingFrameRate;
QSize maximumResolution;
QList<QRect> monitorDesktopResolutions;
QList<QRect> monitorNativeResolutions;
QString versionString;
bool supportsHdr;
};
+22 -78
View File
@@ -52,14 +52,14 @@ public:
void handleUnknownOptions() void handleUnknownOptions()
{ {
if (!unknownOptionNames().isEmpty()) { if (unknownOptionNames().length()) {
showError(QString("Unknown options: %1").arg(unknownOptionNames().join(", "))); showError(QString("Unknown options: %1").arg(unknownOptionNames().join(", ")));
} }
} }
void showMessage(QString message, MessageType type) const 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; UINT flags = MB_OK | MB_TOPMOST | MB_SETFOREGROUND;
flags |= (type == Info ? MB_ICONINFORMATION : MB_ICONERROR); flags |= (type == Info ? MB_ICONINFORMATION : MB_ICONERROR);
QString title = "Moonlight"; QString title = "Moonlight";
@@ -172,31 +172,20 @@ GlobalCommandLineParser::ParseResult GlobalCommandLineParser::parse(const QStrin
parser.addPositionalArgument("action", "Action to execute", "<action>"); parser.addPositionalArgument("action", "Action to execute", "<action>");
parser.parse(args); parser.parse(args);
auto posArgs = parser.positionalArguments(); 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 // This method will not return and terminates the process if --version
// or --help is specified // or --help is specified
parser.handleHelpAndVersionOptions(); parser.handleHelpAndVersionOptions();
parser.handleUnknownOptions(); parser.handleUnknownOptions();
return NormalStartRequested; return NormalStartRequested;
} } else if (action == "quit") {
else { return QuitRequested;
// If users supply arguments that accept values prior to the "quit" } else if (action == "stream") {
// or "stream" positional arguments, we will not be able to correctly return StreamRequested;
// parse the value out of the input because this QCommandLineParser } else {
// doesn't know about all of the options that "quit" and "stream" parser.showError(QString("Invalid action: %1").arg(action));
// 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"));
} }
} }
@@ -252,7 +241,6 @@ StreamCommandLineParser::StreamCommandLineParser()
m_AudioConfigMap = { m_AudioConfigMap = {
{"stereo", StreamingPreferences::AC_STEREO}, {"stereo", StreamingPreferences::AC_STEREO},
{"5.1-surround", StreamingPreferences::AC_51_SURROUND}, {"5.1-surround", StreamingPreferences::AC_51_SURROUND},
{"7.1-surround", StreamingPreferences::AC_71_SURROUND},
}; };
m_VideoCodecMap = { m_VideoCodecMap = {
{"auto", StreamingPreferences::VCC_AUTO}, {"auto", StreamingPreferences::VCC_AUTO},
@@ -261,13 +249,8 @@ StreamCommandLineParser::StreamCommandLineParser()
}; };
m_VideoDecoderMap = { m_VideoDecoderMap = {
{"auto", StreamingPreferences::VDS_AUTO}, {"auto", StreamingPreferences::VDS_AUTO},
{"software", StreamingPreferences::VDS_FORCE_SOFTWARE}, {"software", StreamingPreferences::VDS_FORCE_HARDWARE},
{"hardware", StreamingPreferences::VDS_FORCE_HARDWARE}, {"hardware", StreamingPreferences::VDS_FORCE_SOFTWARE},
};
m_CaptureSysKeysModeMap = {
{"never", StreamingPreferences::CSK_OFF},
{"fullscreen", StreamingPreferences::CSK_FULLSCREEN},
{"always", StreamingPreferences::CSK_ALWAYS},
}; };
} }
@@ -297,23 +280,14 @@ void StreamCommandLineParser::parse(const QStringList &args, StreamingPreference
parser.addToggleOption("vsync", "V-Sync"); parser.addToggleOption("vsync", "V-Sync");
parser.addValueOption("fps", "FPS"); parser.addValueOption("fps", "FPS");
parser.addValueOption("bitrate", "bitrate in Kbps"); parser.addValueOption("bitrate", "bitrate in Kbps");
parser.addValueOption("packet-size", "video packet size");
parser.addChoiceOption("display-mode", "display mode", m_WindowModeMap.keys()); parser.addChoiceOption("display-mode", "display mode", m_WindowModeMap.keys());
parser.addChoiceOption("audio-config", "audio config", m_AudioConfigMap.keys()); parser.addChoiceOption("audio-config", "audio config", m_AudioConfigMap.keys());
parser.addToggleOption("multi-controller", "multiple controller support"); parser.addToggleOption("multi-controller", "multiple controller support");
parser.addToggleOption("quit-after", "quit app after session"); parser.addToggleOption("quit-after", "quit app after session");
parser.addToggleOption("absolute-mouse", "remote desktop optimized mouse control"); parser.addToggleOption("mouse-acceleration", "mouse acceleration");
parser.addToggleOption("mouse-buttons-swap", "left and right mouse buttons swap");
parser.addToggleOption("touchscreen-trackpad", "touchscreen in trackpad mode");
parser.addToggleOption("game-optimization", "game optimizations"); parser.addToggleOption("game-optimization", "game optimizations");
parser.addToggleOption("audio-on-host", "audio on host PC"); parser.addToggleOption("audio-on-host", "audio on host PC");
parser.addToggleOption("frame-pacing", "frame pacing"); 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("keep-awake", "prevent display sleep while streaming");
parser.addChoiceOption("capture-system-keys", "capture system key combos", m_CaptureSysKeysModeMap.keys());
parser.addChoiceOption("video-codec", "video codec", m_VideoCodecMap.keys()); parser.addChoiceOption("video-codec", "video codec", m_VideoCodecMap.keys());
parser.addChoiceOption("video-decoder", "video decoder", m_VideoDecoderMap.keys()); parser.addChoiceOption("video-decoder", "video decoder", m_VideoDecoderMap.keys());
@@ -326,21 +300,25 @@ void StreamCommandLineParser::parse(const QStringList &args, StreamingPreference
// Resolve display's width and height // Resolve display's width and height
QRegularExpression resolutionRexExp("^(720|1080|1440|4K|resolution)$"); QRegularExpression resolutionRexExp("^(720|1080|1440|4K|resolution)$");
QStringList resoOptions = parser.optionNames().filter(resolutionRexExp); QStringList resoOptions = parser.optionNames().filter(resolutionRexExp);
bool displaySet = !resoOptions.isEmpty(); bool displaySet = resoOptions.length();
if (displaySet) { if (displaySet) {
QString name = resoOptions.last(); QString name = resoOptions.last();
if (name == "720") { if (name == "720") {
preferences->width = 1280; preferences->width = 1280;
preferences->height = 720; preferences->height = 720;
displaySet = true;
} else if (name == "1080") { } else if (name == "1080") {
preferences->width = 1920; preferences->width = 1920;
preferences->height = 1080; preferences->height = 1080;
displaySet = true;
} else if (name == "1440") { } else if (name == "1440") {
preferences->width = 2560; preferences->width = 2560;
preferences->height = 1440; preferences->height = 1440;
displaySet = true;
} else if (name == "4K") { } else if (name == "4K") {
preferences->width = 3840; preferences->width = 3840;
preferences->height = 2160; preferences->height = 2160;
displaySet = true;
} else if (name == "resolution") { } else if (name == "resolution") {
auto resolution = parser.getResolutionOptionValue(name); auto resolution = parser.getResolutionOptionValue(name);
preferences->width = resolution.first; preferences->width = resolution.first;
@@ -351,8 +329,8 @@ void StreamCommandLineParser::parse(const QStringList &args, StreamingPreference
// Resolve --fps option // Resolve --fps option
if (parser.isSet("fps")) { if (parser.isSet("fps")) {
preferences->fps = parser.getIntOption("fps"); preferences->fps = parser.getIntOption("fps");
if (!inRange(preferences->fps, 30, 240)) { if (!inRange(preferences->fps, 30, 120)) {
parser.showError("FPS must be in range: 30 - 240"); parser.showError("FPS must be in range: 30 - 120");
} }
} }
@@ -367,14 +345,6 @@ void StreamCommandLineParser::parse(const QStringList &args, StreamingPreference
preferences->width, preferences->height, preferences->fps); 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 // Resolve --display option
if (parser.isSet("display-mode")) { if (parser.isSet("display-mode")) {
preferences->windowMode = mapValue(m_WindowModeMap, parser.getChoiceOptionValue("display-mode")); preferences->windowMode = mapValue(m_WindowModeMap, parser.getChoiceOptionValue("display-mode"));
@@ -394,14 +364,8 @@ void StreamCommandLineParser::parse(const QStringList &args, StreamingPreference
// Resolve --quit-after and --no-quit-after options // Resolve --quit-after and --no-quit-after options
preferences->quitAppAfter = parser.getToggleOptionValue("quit-after", preferences->quitAppAfter); preferences->quitAppAfter = parser.getToggleOptionValue("quit-after", preferences->quitAppAfter);
// Resolve --absolute-mouse and --no-absolute-mouse options // Resolve --mouse-acceleration and --no-mouse-acceleration options
preferences->absoluteMouseMode = parser.getToggleOptionValue("absolute-mouse", preferences->absoluteMouseMode); preferences->mouseAcceleration = parser.getToggleOptionValue("mouse-acceleration", preferences->mouseAcceleration);
// Resolve --mouse-buttons-swap and --no-mouse-buttons-swap options
preferences->swapMouseButtons = parser.getToggleOptionValue("mouse-buttons-swap", preferences->swapMouseButtons);
// Resolve --touchscreen-trackpad and --no-touchscreen-trackpad options
preferences->absoluteTouchMode = !parser.getToggleOptionValue("touchscreen-trackpad", !preferences->absoluteTouchMode);
// Resolve --game-optimization and --no-game-optimization options // Resolve --game-optimization and --no-game-optimization options
preferences->gameOptimizations = parser.getToggleOptionValue("game-optimization", preferences->gameOptimizations); preferences->gameOptimizations = parser.getToggleOptionValue("game-optimization", preferences->gameOptimizations);
@@ -412,26 +376,6 @@ void StreamCommandLineParser::parse(const QStringList &args, StreamingPreference
// Resolve --frame-pacing and --no-frame-pacing options // Resolve --frame-pacing and --no-frame-pacing options
preferences->framePacing = parser.getToggleOptionValue("frame-pacing", preferences->framePacing); 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 --keep-awake and --no-keep-awake options
preferences->keepAwake = parser.getToggleOptionValue("keep-awake", preferences->keepAwake);
// Resolve --capture-system-keys option
if (parser.isSet("capture-system-keys")) {
preferences->captureSysKeysMode = mapValue(m_CaptureSysKeysModeMap, parser.getChoiceOptionValue("capture-system-keys"));
}
// Resolve --video-codec option // Resolve --video-codec option
if (parser.isSet("video-codec")) { if (parser.isSet("video-codec")) {
preferences->videoCodecConfig = mapValue(m_VideoCodecMap, parser.getChoiceOptionValue("video-codec")); preferences->videoCodecConfig = mapValue(m_VideoCodecMap, parser.getChoiceOptionValue("video-codec"));
-1
View File
@@ -53,5 +53,4 @@ private:
QMap<QString, StreamingPreferences::AudioConfig> m_AudioConfigMap; QMap<QString, StreamingPreferences::AudioConfig> m_AudioConfigMap;
QMap<QString, StreamingPreferences::VideoCodecConfig> m_VideoCodecMap; QMap<QString, StreamingPreferences::VideoCodecConfig> m_VideoCodecMap;
QMap<QString, StreamingPreferences::VideoDecoderSelection> m_VideoDecoderMap; QMap<QString, StreamingPreferences::VideoDecoderSelection> m_VideoDecoderMap;
QMap<QString, StreamingPreferences::CaptureSysKeysMode> m_CaptureSysKeysModeMap;
}; };
+4 -4
View File
@@ -73,7 +73,7 @@ public:
case Event::ComputerSeekTimedout: case Event::ComputerSeekTimedout:
if (m_State == StateSeekComputer) { if (m_State == StateSeekComputer) {
m_State = StateFailure; 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; break;
// Occurs when searched computer is found // Occurs when searched computer is found
@@ -85,8 +85,8 @@ public:
m_ComputerManager->quitRunningApp(event.computer); m_ComputerManager->quitRunningApp(event.computer);
} else { } else {
m_State = StateFailure; m_State = StateFailure;
QString msg = QObject::tr("Computer %1 has not been paired. " QString msg = QString("Computer %1 has not been paired. "
"Please open Moonlight to pair before streaming.") "Please open Moonlight to pair before streaming.")
.arg(event.computer->name); .arg(event.computer->name);
emit q->failed(msg); emit q->failed(msg);
} }
@@ -99,7 +99,7 @@ public:
QCoreApplication::exit(0); QCoreApplication::exit(0);
} else { } else {
m_State = StateFailure; 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; break;
+5 -5
View File
@@ -86,8 +86,8 @@ public:
emit q->searchingApp(); emit q->searchingApp();
} else { } else {
m_State = StateFailure; m_State = StateFailure;
QString msg = QObject::tr("Computer %1 has not been paired. " QString msg = QString("Computer %1 has not been paired. "
"Please open Moonlight to pair before streaming.") "Please open Moonlight to pair before streaming.")
.arg(event.computer->name); .arg(event.computer->name);
emit q->failed(msg); emit q->failed(msg);
} }
@@ -123,18 +123,18 @@ public:
case Event::AppQuitCompleted: case Event::AppQuitCompleted:
if (m_State == StateSeekApp && !event.errorMessage.isEmpty()) { if (m_State == StateSeekApp && !event.errorMessage.isEmpty()) {
m_State = StateFailure; 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; break;
// Occurs when computer or app search timed out // Occurs when computer or app search timed out
case Event::Timedout: case Event::Timedout:
if (m_State == StateSeekComputer) { if (m_State == StateSeekComputer) {
m_State = StateFailure; 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) { if (m_State == StateSeekApp) {
m_State = StateFailure; 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; break;
} }
@@ -7,18 +7,15 @@
<summary>Play games remotely from your NVIDIA GameStream-enabled PC</summary> <summary>Play games remotely from your NVIDIA GameStream-enabled PC</summary>
<description> <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> <p>Features include:</p>
<ul> <ul>
<li>Streaming at up to 4K resolution</li> <li>Streaming at up to 4K resolution</li>
<li>Support for up to 120 FPS streaming (high refresh rate monitor recommended)</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>Hardware accelerated video decoding with VAAPI and VDPAU support</li>
<li>7.1 surround sound audio support</li> <li>5.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</li>
<li>Direct touchscreen mouse control</li>
<li>HEVC support for better video compression efficiency</li> <li>HEVC support for better video compression efficiency</li>
<li>Gamepad support with force feedback for up to 4 players</li> <li>Gamepad support with SDL mapping compatibility</li>
</ul> </ul>
</description> </description>
@@ -36,345 +33,6 @@
</screenshots> </screenshots>
<releases> <releases>
<release version="4.0.0" date="2022-04-02">
<description>
<p>New features:</p>
<ul>
<li>Added a new key combo for locking the mouse cursor to the window (Ctrl+Alt+Shift+L)</li>
<li>Added an option to avoid keeping the client display awake while streaming</li>
</ul>
<p>Behavior Changes:</p>
<ul>
<li>HDR streaming is now a separate option rather than being part of the Video Codec override dropdown</li>
<li>HDR streaming is no longer restricted to apps that are certified by NVIDIA as compatible with GameStream HDR. Some newer games may require an HDR display or EDID emulator dongle connected to your host PC for HDR options to be available.</li>
</ul>
<p>Bugfixes:</p>
<ul>
<li>Fixed loss of all audio if the active audio device is unplugged while streaming</li>
<li>Fixed the GUI being unresponsive after manually quitting when connection to the host is lost</li>
<li>Fixed a rare crash if a controller disconnects right as rumble is starting</li>
<li>Fixed text rendering on the overlay UI when using SDL_ttf 2.0.18 on Linux</li>
<li>Updated included gamepad mappings</li>
<li>Updated community-contributed translations from Weblate</li>
</ul>
</description>
</release>
<release version="3.2.0" date="2022-01-18">
<description>
<p>New features:</p>
<ul>
<li>Reduced CPU usage while streaming</li>
<li>Non-ASCII characters are now supported when pasting clipboard data to the host</li>
<li>Added support for zero-copy NVDEC rendering for Nvidia Wayland systems</li>
<li>Significantly improved performance with asynchronous decoders like V4L2M2M on Linux</li>
<li>Added support for color compression planes on newer Intel systems when using VAAPI decoding</li>
<li>Added support for copy-back rendering in non-default pixel formats (like the Raspberry Pi SAND format)</li>
</ul>
<p>Bugfixes:</p>
<ul>
<li>Fixed unexpected GUI input after using the gamepad quit combo</li>
<li>Updated included gamepad mappings</li>
<li>Updated community-contributed translations from Weblate</li>
</ul>
</description>
</release>
<release version="3.1.4" date="2021-06-25">
<description>
<p>Changes:</p>
<ul>
<li>Added network latency statistics to the performance overlay</li>
<li>Improved audio quality on unreliable connections (requires GeForce Experience 3.19 or later)</li>
<li>Reduced video frame drops on unreliable connections (requires GeForce Experience 3.22 or later)</li>
<li>Updated community-contributed translations from Weblate</li>
</ul>
</description>
</release>
<release version="3.1.3" date="2021-05-07">
<description>
<p>Changes:</p>
<ul>
<li>Fixed an issue that could cause excessive audio latency on some connections</li>
<li>Updated community-contributed translations from Weblate</li>
</ul>
</description>
</release>
<release version="3.1.2" date="2021-05-03">
<description>
<p>Changes:</p>
<ul>
<li>Improved connection reliability and performance with GeForce Experience 3.22</li>
<li>Added support for audio encryption with GeForce Experience 3.22</li>
<li>Added compatibility update checks for future versions of GeForce Experience</li>
<li>Fixed keyboard capture on X11</li>
<li>Updated community-contributed translations from Weblate</li>
</ul>
</description>
</release>
<release version="3.1.1" date="2021-04-09">
<description>
<p>Changes:</p>
<ul>
<li>Added support for streaming from GeForce Experience 3.22</li>
<li>Added support for AMF-encoded HEVC streams from servers like Sunshine and OpenStream</li>
<li>Added new community-contributed translations from Weblate</li>
<li>Fixed KMSDRM rendering with buffer modifiers on embedded devices</li>
</ul>
</description>
</release>
<release version="3.1.0" date="2021-03-14">
<description>
<p>New features:</p>
<ul>
<li>System keyboard shortcuts (Alt+Tab, Super key, Ctrl+Shift+Esc, etc.) can now be (optionally) captured in windowed mode</li>
<li>System keyboard shortcuts can now be captured under Wayland</li>
<li>Added support for typing the client's clipboard text on the host by pressing Ctrl+Alt+Shift+V</li>
<li>Added Ctrl+Alt+Shift+D key combo to minimize the streaming window</li>
<li>Added Ctrl+Alt+Shift+C key combo to show the local cursor while streaming in remote desktop mouse mode</li>
<li>Added Select+L1+R1+X gamepad button combo to toggle performance overlay</li>
<li>Added translations for Simplified Chinese and German</li>
<li>Added a language selector on the settings page</li>
</ul>
<p>Bugfixes:</p>
<ul>
<li>Improved performance of loading the app grid</li>
<li>Fixed video scaling in High DPI mode on Wayland</li>
<li>Fixed window icons on Wayland</li>
<li>Fixed number pad enter key functionality in the UI</li>
</ul>
</description>
</release>
<release version="3.0.0" date="2021-01-17">
<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"> <release version="0.8.1" date="2019-01-08">
<description> <description>
<p>New features:</p> <p>New features:</p>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

-13
View File
@@ -1,13 +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
# Renice PE_Single_CPU which seems to host A/V stuff
renice -10 -p $(pidof PE_Single_CPU)
# Renice Moonlight itself to avoid preemption by background tasks
# Write output to a logfile in /tmp
exec nice -n -10 ./bin/moonlight > /tmp/moonlight.log
-3
View File
@@ -1,3 +0,0 @@
name=Moonlight
icon=moonlight.png
run=moonlight.sh
+114 -264
View File
@@ -1,23 +1,29 @@
import QtQuick 2.9 import QtQuick 2.9
import QtQuick.Dialogs 1.2
import QtQuick.Controls 2.2 import QtQuick.Controls 2.2
import AppModel 1.0 import AppModel 1.0
import ComputerManager 1.0 import ComputerManager 1.0
import SdlGamepadKeyNavigation 1.0 import SdlGamepadKeyNavigation 1.0
CenteredGridView { GridView {
property int computerIndex property int computerIndex
property AppModel appModel : createModel() property AppModel appModel : createModel()
property bool activated
property bool showHiddenGames
property bool showGames
id: appGrid id: appGrid
focus: true focus: true
activeFocusOnTab: true activeFocusOnTab: true
topMargin: 20 anchors.fill: parent
bottomMargin: 5 anchors.leftMargin: (parent.width % (cellWidth + anchors.rightMargin)) / 2
cellWidth: 230; cellHeight: 297; 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() function computerLost()
{ {
@@ -26,322 +32,154 @@ CenteredGridView {
} }
Component.onCompleted: { Component.onCompleted: {
// Don't show any highlighted item until interacting with them. // Don't show any highlighted item until interacting with them
// We do this here instead of onActivated to avoid losing the user's
// selection when backing out of a different page of the app.
currentIndex = -1 currentIndex = -1
} }
StackView.onActivated: { SdlGamepadKeyNavigation {
appModel.computerLost.connect(computerLost) id: gamepadKeyNav
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
}
}
} }
StackView.onDeactivating: { onVisibleChanged: {
appModel.computerLost.disconnect(computerLost) if (visible) {
activated = false appModel.computerLost.connect(computerLost)
gamepadKeyNav.enable()
}
else {
appModel.computerLost.disconnect(computerLost)
gamepadKeyNav.disable()
}
} }
function createModel() function createModel()
{ {
var model = Qt.createQmlObject('import AppModel 1.0; AppModel {}', parent, '') var model = Qt.createQmlObject('import AppModel 1.0; AppModel {}', parent, '')
model.initialize(ComputerManager, computerIndex, showHiddenGames) model.initialize(ComputerManager, computerIndex)
return model return model
} }
model: appModel model: appModel
delegate: NavigableItemDelegate { delegate: NavigableItemDelegate {
width: 220; height: 287; width: 200; height: 335;
grid: appGrid grid: appGrid
property alias appContextMenu: appContextMenuLoader.item Image {
property alias appNameText: appNameTextLoader.item id: appIcon
anchors.horizontalCenter: parent.horizontalCenter;
// Dim the app if it's hidden y: 20
opacity: model.hidden ? 0.4 : 1.0 source: model.boxart
sourceSize {
width: 150
height: 200
}
width: sourceSize.width
height: sourceSize.height
fillMode: Image.Pad
}
Image { Image {
property bool isPlaceholder: false id: runningIcon
anchors.centerIn: appIcon
id: appIcon visible: model.running
anchors.horizontalCenter: parent.horizontalCenter source: "qrc:/res/baseline-play_circle_filled_white-48px.svg"
y: 10 sourceSize {
source: model.boxart width: 75
height: 75
onSourceSizeChanged: {
// Nearly all of Nvidia's official box art does not match the dimensions of placeholder
// images, however the one known exeception is Overcooked. Therefore, we only execute
// the image size checks if this is not an app collector game. We know the officially
// supported games all have box art, so this check is not required.
if (!model.isAppCollectorGame &&
((sourceSize.width == 130 && sourceSize.height == 180) || // GFE 2.0 placeholder image
(sourceSize.width == 628 && sourceSize.height == 888) || // GFE 3.0 placeholder image
(sourceSize.width == 200 && sourceSize.height == 266))) // Our no_app_image.png
{
isPlaceholder = true
}
else
{
isPlaceholder = false
}
width = 200
height = 267
} }
}
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 // Display a tooltip with the full name if it's truncated
ToolTip.text: model.name ToolTip.text: model.name
ToolTip.delay: 1000 ToolTip.delay: 1000
ToolTip.timeout: 5000 ToolTip.timeout: 5000
ToolTip.visible: (parent.hovered || parent.highlighted) && (!appNameText || appNameText.truncated) ToolTip.visible: (parent.hovered || parent.highlighted) && truncated
} }
Loader { function launchOrResumeSelectedApp()
active: model.running
asynchronous: true
anchors.fill: appIcon
sourceComponent: Item {
ToolButton {
anchors.horizontalCenterOffset: appIcon.isPlaceholder ? -47 : 0
anchors.verticalCenterOffset: appIcon.isPlaceholder ? -75 : -60
anchors.centerIn: parent
implicitWidth: 125
implicitHeight: 125
Image {
source: "qrc:/res/baseline-play_circle_filled_white-48px.svg"
anchors.centerIn: parent
sourceSize {
width: 75
height: 75
}
}
onClicked: {
launchOrResumeSelectedApp(true)
}
ToolTip.text: qsTr("Resume Game")
ToolTip.delay: 1000
ToolTip.timeout: 3000
ToolTip.visible: hovered
}
ToolButton {
anchors.horizontalCenterOffset: appIcon.isPlaceholder ? 47 : 0
anchors.verticalCenterOffset: appIcon.isPlaceholder ? -75 : 60
anchors.centerIn: parent
implicitWidth: 125
implicitHeight: 125
Image {
source: "qrc:/res/baseline-cancel-24px.svg"
anchors.centerIn: parent
sourceSize {
width: 75
height: 75
}
}
onClicked: {
doQuitGame()
}
ToolTip.text: qsTr("Quit Game")
ToolTip.delay: 1000
ToolTip.timeout: 3000
ToolTip.visible: hovered
}
}
}
Loader {
id: appNameTextLoader
active: appIcon.isPlaceholder
// This loader is not asynchronous to avoid noticeable differences
// in the time in which the text loads for each game.
width: appIcon.width
height: model.running ? 175 : appIcon.height
anchors.left: appIcon.left
anchors.right: appIcon.right
anchors.bottom: appIcon.bottom
sourceComponent: Label {
id: appNameText
text: model.name
font.pointSize: 22
leftPadding: 20
rightPadding: 20
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.Wrap
elide: Text.ElideRight
}
}
function launchOrResumeSelectedApp(quitExistingApp)
{ {
var runningId = appModel.getRunningAppId() var runningIndex = appModel.getRunningAppIndex()
if (runningId !== 0 && runningId !== model.appid) { if (runningIndex >= 0 && runningIndex !== index) {
if (quitExistingApp) { quitAppDialog.appName = appModel.getRunningAppName()
quitAppDialog.appName = appModel.getRunningAppName() quitAppDialog.segueToStream = true
quitAppDialog.segueToStream = true quitAppDialog.nextAppName = model.name
quitAppDialog.nextAppName = model.name quitAppDialog.nextAppIndex = index
quitAppDialog.nextAppIndex = index quitAppDialog.open()
quitAppDialog.open()
}
return return
} }
var component = Qt.createComponent("StreamSegue.qml") var component = Qt.createComponent("StreamSegue.qml")
var segue = component.createObject(stackView, { var segue = component.createObject(stackView, {"appName": model.name, "session": appModel.createSessionForApp(index)})
"appName": model.name,
"session": appModel.createSessionForApp(index),
"isResume": runningId === model.appid
})
stackView.push(segue) stackView.push(segue)
} }
onClicked: { onClicked: {
// Only allow clicking on the box art for non-running games. // Nothing is running or this app is running
// For running games, buttons will appear to resume or quit which launchOrResumeSelectedApp()
// will handle starting the game and clicks on the box art will
// be ignored.
if (!model.running) {
launchOrResumeSelectedApp(true)
}
}
onPressAndHold: {
// popup() ensures the menu appears under the mouse cursor
if (appContextMenu.popup) {
appContextMenu.popup()
}
else {
// Qt 5.9 doesn't have popup()
appContextMenu.open()
}
} }
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
acceptedButtons: Qt.RightButton; acceptedButtons: Qt.RightButton
onClicked: { onClicked: {
parent.onPressAndHold() // popup() ensures the menu appears under the mouse cursor
} if (appContextMenu.popup) {
} appContextMenu.popup()
}
Keys.onReturnPressed: { else {
// Open the app context menu if activated via the gamepad or keyboard // Qt 5.9 doesn't have popup()
// for running games. If the game isn't running, the above onClicked appContextMenu.open()
// method will handle the launch. }
if (model.running) {
// This will be keyboard/gamepad driven so use
// open() instead of popup()
appContextMenu.open()
}
}
Keys.onEnterPressed: {
// Open the app context menu if activated via the gamepad or keyboard
// for running games. If the game isn't running, the above onClicked
// method will handle the launch.
if (model.running) {
// This will be keyboard/gamepad driven so use
// open() instead of popup()
appContextMenu.open()
} }
} }
Keys.onMenuPressed: { Keys.onMenuPressed: {
// This will be keyboard/gamepad driven so use open() instead of popup() // We must use open() here so the menu is positioned on
// the ItemDelegate and not where the mouse cursor is
appContextMenu.open() appContextMenu.open()
} }
function doQuitGame() { Menu {
quitAppDialog.appName = appModel.getRunningAppName() id: appContextMenu
quitAppDialog.segueToStream = false NavigableMenuItem {
quitAppDialog.open() text: model.running ? "Resume Game" : "Launch Game"
} onTriggered: {
appContextMenu.close()
Loader { launchOrResumeSelectedApp()
id: appContextMenuLoader
asynchronous: true
sourceComponent: NavigableMenu {
id: appContextMenu
NavigableMenuItem {
parentMenu: appContextMenu
text: model.running ? qsTr("Resume Game") : qsTr("Launch Game")
onTriggered: launchOrResumeSelectedApp(true)
} }
NavigableMenuItem { }
parentMenu: appContextMenu NavigableMenuItem {
text: qsTr("Quit Game") text: "Quit Game"
onTriggered: doQuitGame() onTriggered: {
visible: model.running quitAppDialog.appName = appModel.getRunningAppName()
} quitAppDialog.segueToStream = false
NavigableMenuItem { quitAppDialog.open()
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
} }
visible: model.running
} }
} }
} }
NavigableMessageDialog { MessageDialog {
id: quitAppDialog id: quitAppDialog
modality:Qt.WindowModal
property string appName : "" property string appName : ""
property bool segueToStream : false property bool segueToStream : false
property string nextAppName: "" property string nextAppName: ""
property int nextAppIndex: 0 property int nextAppIndex: 0
text:qsTr("Are you sure you want to quit %1? Any unsaved progress will be lost.").arg(appName) text:"Are you sure you want to quit " + appName +"? Any unsaved progress will be lost."
standardButtons: Dialog.Yes | Dialog.No standardButtons: StandardButton.Yes | StandardButton.No
function quitApp() { function quitApp() {
var component = Qt.createComponent("QuitSegue.qml") var component = Qt.createComponent("QuitSegue.qml")
@@ -363,8 +201,20 @@ CenteredGridView {
appModel.quitRunningApp() appModel.quitRunningApp()
} }
onYes: quitApp()
// For keyboard/gamepad navigation
onAccepted: quitApp() 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 2.9
import QtQuick.Controls 2.2 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 // https://stackoverflow.com/questions/45029968/how-do-i-set-the-combobox-width-to-fit-the-largest-item
ComboBox { ComboBox {
property int textWidth 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 { TextMetrics {
id: popupMetrics id: popupMetrics
@@ -19,7 +15,9 @@ ComboBox {
id: textMetrics 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 textMetrics.font = font
popupMetrics.font = popup.font popupMetrics.font = popup.font
textWidth = 0 textWidth = 0
@@ -30,17 +28,4 @@ ComboBox {
textWidth = Math.max(popupMetrics.width, textWidth) 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 2.0
import QtQuick.Controls 2.2 import QtQuick.Controls 2.2
import QtQuick.Dialogs 1.2
import ComputerManager 1.0 import ComputerManager 1.0
import Session 1.0 import Session 1.0
Item { Item {
anchors.fill: parent
function onSearchingComputer() { function onSearchingComputer() {
stageLabel.text = qsTr("Establishing connection to PC...") stageLabel.text = "Establishing connection to PC..."
} }
function onQuittingApp() { function onQuittingApp() {
stageLabel.text = qsTr("Quitting app...") stageLabel.text = "Quitting app..."
} }
function onFailure(message) { function onFailure(message) {
@@ -18,8 +21,13 @@ Item {
errorDialog.open() errorDialog.open()
} }
StackView.onActivated: { // The StackView will trigger a visibility change when
if (!launcher.isExecuted()) { // 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 toolBar.visible = false
launcher.searchingComputer.connect(onSearchingComputer) launcher.searchingComputer.connect(onSearchingComputer)
launcher.quittingApp.connect(onQuittingApp) launcher.quittingApp.connect(onQuittingApp)
@@ -47,11 +55,16 @@ Item {
} }
} }
ErrorMessageDialog { MessageDialog {
id: errorDialog id: errorDialog
modality:Qt.WindowModal
icon: StandardIcon.Critical
standardButtons: StandardButton.Ok
onClosed: { onVisibleChanged: {
Qt.quit() if (!visible) {
Qt.quit()
}
} }
} }
} }
+46 -18
View File
@@ -1,16 +1,19 @@
import QtQml 2.2
import QtQuick 2.0 import QtQuick 2.0
import QtQuick.Controls 2.2 import QtQuick.Controls 2.2
import QtQuick.Dialogs 1.2
import ComputerManager 1.0 import ComputerManager 1.0
import SdlGamepadKeyNavigation 1.0
Item { Item {
visible: false
function onSearchingComputer() { function onSearchingComputer() {
stageLabel.text = qsTr("Establishing connection to PC...") stageLabel.text = "Establishing connection to PC..."
} }
function onSearchingApp() { function onSearchingApp() {
stageLabel.text = qsTr("Loading app list...") stageLabel.text = "Loading app list..."
} }
function onSessionCreated(appName, session) { function onSessionCreated(appName, session) {
@@ -33,14 +36,9 @@ Item {
quitAppDialog.open() quitAppDialog.open()
} }
StackView.onActivated: { onVisibleChanged: {
if (!launcher.isExecuted()) { if (visible && !launcher.isExecuted()) {
toolBar.visible = false 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.searchingComputer.connect(onSearchingComputer)
launcher.searchingApp.connect(onSearchingApp) launcher.searchingApp.connect(onSearchingApp)
launcher.sessionCreated.connect(onSessionCreated) launcher.sessionCreated.connect(onSessionCreated)
@@ -68,18 +66,26 @@ Item {
} }
} }
ErrorMessageDialog { MessageDialog {
id: errorDialog id: errorDialog
modality:Qt.WindowModal
onClosed: { icon: StandardIcon.Critical
Qt.quit(); 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 id: quitAppDialog
text:qsTr("Are you sure you want to quit %1? Any unsaved progress will be lost.").arg(appName) modality:Qt.WindowModal
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
property string appName : "" property string appName : ""
function quitApp() { function quitApp() {
@@ -90,7 +96,29 @@ Item {
launcher.quitRunningApp() launcher.quitRunningApp()
} }
onYes: quitApp()
// For keyboard/gamepad navigation
onAccepted: quitApp() 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 import QtQuick 2.0
Item { 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()
}
}
-3
View File
@@ -21,7 +21,4 @@ ItemDelegate {
Keys.onReturnPressed: { Keys.onReturnPressed: {
clicked() clicked()
} }
Keys.onEnterPressed: {
clicked()
}
} }
-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
}
}
}
}
-20
View File
@@ -2,31 +2,11 @@ import QtQuick 2.0
import QtQuick.Controls 2.2 import QtQuick.Controls 2.2
MenuItem { 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 // Ensure focus can't be given to an invisible item
enabled: visible enabled: visible
height: visible ? implicitHeight : 0 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: { Keys.onReturnPressed: {
triggered() triggered()
} }
Keys.onEnterPressed: {
triggered()
}
Keys.onEscapePressed: {
parentMenu.close()
}
} }
-78
View File
@@ -1,78 +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.onEnterPressed: {
accept()
}
Keys.onEscapePressed: {
reject()
}
}
}
footer: DialogButtonBox {
id: dialogButtonBox
standardButtons: dialog.standardButtons
onHelpRequested: {
Qt.openUrlExternally(helpUrl)
close()
}
}
}
-23
View File
@@ -1,36 +1,13 @@
import QtQuick 2.0 import QtQuick 2.0
import QtQuick.Controls 2.2 import QtQuick.Controls 2.2
import QtQuick.Layouts 1.3
ToolButton { ToolButton {
property string iconSource
activeFocusOnTab: true 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.
Layout.preferredHeight: parent.height
Keys.onReturnPressed: { Keys.onReturnPressed: {
clicked() clicked()
} }
Keys.onEnterPressed: {
clicked()
}
Keys.onRightPressed: { Keys.onRightPressed: {
nextItemInFocusChain(true).forceActiveFocus(Qt.TabFocus) nextItemInFocusChain(true).forceActiveFocus(Qt.TabFocus)
} }
+128 -228
View File
@@ -1,6 +1,8 @@
import QtQuick 2.9 import QtQuick 2.9
import QtQuick.Controls 2.2 import QtQuick.Controls 2.2
import QtQuick.Dialogs 1.2
import QtQuick.Layouts 1.3 import QtQuick.Layouts 1.3
import QtQuick.Window 2.2
import ComputerModel 1.0 import ComputerModel 1.0
@@ -8,44 +10,48 @@ import ComputerManager 1.0
import StreamingPreferences 1.0 import StreamingPreferences 1.0
import SdlGamepadKeyNavigation 1.0 import SdlGamepadKeyNavigation 1.0
CenteredGridView { GridView {
property ComputerModel computerModel : createModel() property ComputerModel computerModel : createModel()
id: pcGrid id: pcGrid
focus: true focus: true
activeFocusOnTab: true activeFocusOnTab: true
topMargin: 20 anchors.fill: parent
bottomMargin: 5 anchors.leftMargin: (parent.width % (cellWidth + anchors.rightMargin)) / 2
cellWidth: 310; cellHeight: 330; anchors.topMargin: 20
objectName: qsTr("Computers") anchors.rightMargin: 5
anchors.bottomMargin: 5
cellWidth: 350; cellHeight: 350;
objectName: "Computers"
Component.onCompleted: { // The StackView will trigger a visibility change when
// Don't show any highlighted item until interacting with them. // we're pushed onto it, causing our onVisibleChanged
// We do this here instead of onActivated to avoid losing the user's // routine to run, but only if we start as invisible
// selection when backing out of a different page of the app. visible: false
currentIndex = -1
StreamingPreferences {
id: prefs
} }
// Note: Any initialization done here that is critical for streaming must SdlGamepadKeyNavigation {
// also be done in CliStartStreamSegue.qml, since this code does not run id: gamepadKeyNav
// for command-line initiated streams. }
StackView.onActivated: {
// Setup signals on CM
ComputerManager.computerAddCompleted.connect(addComplete)
// This is a bit of a hack to do this here as opposed to main.qml, but onVisibleChanged: {
// we need it enabled before calling getConnectedGamepads() and PcView if (visible) {
// is never destroyed, so it should be okay. gamepadKeyNav.enable()
SdlGamepadKeyNavigation.enable() }
else {
// Highlight the first item if a gamepad is connected gamepadKeyNav.disable()
if (currentIndex == -1 && SdlGamepadKeyNavigation.getConnectedGamepads() > 0) {
currentIndex = 0
} }
} }
StackView.onDeactivating: { Component.onCompleted: {
ComputerManager.computerAddCompleted.disconnect(addComplete) // Setup signals on CM
ComputerManager.computerAddCompleted.connect(addComplete)
// Don't show any highlighted item until interacting with them
currentIndex = -1
} }
function pairingComplete(error) function pairingComplete(error)
@@ -56,23 +62,14 @@ CenteredGridView {
// Display a failed dialog if we got an error // Display a failed dialog if we got an error
if (error !== undefined) { if (error !== undefined) {
errorDialog.text = error errorDialog.text = error
errorDialog.helpText = ""
errorDialog.open() errorDialog.open()
} }
} }
function addComplete(success, detectedPortBlocking) function addComplete(success)
{ {
if (!success) { if (!success) {
errorDialog.text = qsTr("Unable to connect to the specified PC.") errorDialog.text = "Unable to connect to the specified PC. Click the Help button for possible solutions."
if (detectedPortBlocking) {
errorDialog.text += "\n\n" + qsTr("This PC's Internet connection is blocking Moonlight. Streaming over the Internet may not work while connected to this network.")
}
else {
errorDialog.helpText = qsTr("Click the Help button for possible solutions.")
}
errorDialog.open() errorDialog.open()
} }
} }
@@ -82,43 +79,21 @@ CenteredGridView {
var model = Qt.createQmlObject('import ComputerModel 1.0; ComputerModel {}', parent, '') var model = Qt.createQmlObject('import ComputerModel 1.0; ComputerModel {}', parent, '')
model.initialize(ComputerManager) model.initialize(ComputerManager)
model.pairingCompleted.connect(pairingComplete) model.pairingCompleted.connect(pairingComplete)
model.connectionTestCompleted.connect(testConnectionDialog.connectionTestComplete)
return model 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 model: computerModel
delegate: NavigableItemDelegate { delegate: NavigableItemDelegate {
width: 300; height: 320; width: 300; height: 300;
grid: pcGrid grid: pcGrid
property alias pcContextMenu : pcContextMenuLoader.item
Image { Image {
id: pcIcon id: pcIcon
anchors.horizontalCenter: parent.horizontalCenter 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 { sourceSize {
width: 200 width: 200
height: 200 height: 200
@@ -130,8 +105,8 @@ CenteredGridView {
id: stateIcon id: stateIcon
anchors.horizontalCenter: pcIcon.horizontalCenter anchors.horizontalCenter: pcIcon.horizontalCenter
anchors.verticalCenter: pcIcon.verticalCenter anchors.verticalCenter: pcIcon.verticalCenter
anchors.verticalCenterOffset: -15 anchors.verticalCenterOffset: -10
visible: !model.statusUnknown && (!model.online || !model.paired) visible: !model.addPc && !model.statusUnknown && (!model.online || !model.paired)
source: !model.online ? "qrc:/res/baseline-warning-24px.svg" : "qrc:/res/baseline-lock-24px.svg" source: !model.online ? "qrc:/res/baseline-warning-24px.svg" : "qrc:/res/baseline-lock-24px.svg"
sourceSize { sourceSize {
width: 75 width: 75
@@ -143,10 +118,10 @@ CenteredGridView {
id: statusUnknownSpinner id: statusUnknownSpinner
anchors.horizontalCenter: pcIcon.horizontalCenter anchors.horizontalCenter: pcIcon.horizontalCenter
anchors.verticalCenter: pcIcon.verticalCenter anchors.verticalCenter: pcIcon.verticalCenter
anchors.verticalCenterOffset: -15 anchors.verticalCenterOffset: -10
width: 75 width: 75
height: 75 height: 75
visible: model.statusUnknown visible: !model.addPc && model.statusUnknown
} }
Label { Label {
@@ -155,77 +130,34 @@ CenteredGridView {
width: parent.width width: parent.width
anchors.top: pcIcon.bottom anchors.top: pcIcon.bottom
anchors.bottom: parent.bottom
font.pointSize: 36 font.pointSize: 36
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
wrapMode: Text.Wrap wrapMode: Text.Wrap
elide: Text.ElideRight
} }
Loader { Menu {
id: pcContextMenuLoader id: pcContextMenu
asynchronous: true NavigableMenuItem {
sourceComponent: NavigableMenu { text: "Wake PC"
id: pcContextMenu onTriggered: computerModel.wakeComputer(index)
MenuItem { visible: !model.addPc && !model.online && model.wakeable
text: qsTr("PC Status: %1").arg(model.online ? qsTr("Online") : qsTr("Offline")) }
font.bold: true NavigableMenuItem {
enabled: false text: "Delete PC"
} onTriggered: {
NavigableMenuItem { deletePcDialog.pcIndex = index
parentMenu: pcContextMenu // get confirmation first, actual closing is called from the dialog
text: qsTr("View All Apps") deletePcDialog.open()
onTriggered: {
var component = Qt.createComponent("AppView.qml")
var appView = component.createObject(stackView, {"computerIndex": index, "objectName": model.name, "showHiddenGames": true})
stackView.push(appView)
}
visible: model.online && model.paired
}
NavigableMenuItem {
parentMenu: pcContextMenu
text: qsTr("Wake PC")
onTriggered: computerModel.wakeComputer(index)
visible: !model.online && model.wakeable
}
NavigableMenuItem {
parentMenu: pcContextMenu
text: qsTr("Test Network")
onTriggered: {
computerModel.testConnectionForComputer(index)
testConnectionDialog.open()
}
}
NavigableMenuItem {
parentMenu: pcContextMenu
text: qsTr("Rename PC")
onTriggered: {
renamePcDialog.pcIndex = index
renamePcDialog.originalName = model.name
renamePcDialog.open()
}
}
NavigableMenuItem {
parentMenu: pcContextMenu
text: qsTr("Delete PC")
onTriggered: {
deletePcDialog.pcIndex = index
// get confirmation first, actual closing is called from the dialog
deletePcDialog.open()
}
} }
} }
} }
onClicked: { onClicked: {
if (model.online) { if (model.addPc) {
if (!model.serverSupported) { addPcDialog.open()
errorDialog.text = qsTr("The version of GeForce Experience on %1 is not supported by this build of Moonlight. You must update Moonlight to stream from %1.").arg(model.name) }
errorDialog.helpText = "" else if (model.online) {
errorDialog.open() if (model.paired) {
}
else if (model.paired) {
// go to game view // go to game view
var component = Qt.createComponent("AppView.qml") var component = Qt.createComponent("AppView.qml")
var appView = component.createObject(stackView, {"computerIndex": index, "objectName": model.name}) var appView = component.createObject(stackView, {"computerIndex": index, "objectName": model.name})
@@ -233,7 +165,7 @@ CenteredGridView {
} }
else { else {
if (!model.busy) { if (!model.busy) {
var pin = computerModel.generatePinString() var pin = ("0000" + Math.floor(Math.random() * 10000)).slice(-4)
// Kick off pairing in the background // Kick off pairing in the background
computerModel.pairComputer(index, pin) computerModel.pairComputer(index, pin)
@@ -244,8 +176,7 @@ CenteredGridView {
} }
else { else {
// cannot pair while something is streaming or attempting to pair // 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.text = "This PC is currently busy. Make sure to quit any running games and try again."
errorDialog.helpText = ""
errorDialog.open() errorDialog.open()
} }
} }
@@ -255,148 +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 { MouseArea {
anchors.fill: parent anchors.fill: parent
acceptedButtons: Qt.RightButton; acceptedButtons: Qt.RightButton;
onClicked: { 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: { Keys.onMenuPressed: {
// We must use open() here so the menu is positioned on if (!model.addPc) {
// the ItemDelegate and not where the mouse cursor is // We must use open() here so the menu is positioned on
pcContextMenu.open() // the ItemDelegate and not where the mouse cursor is
pcContextMenu.open()
}
} }
} }
ErrorMessageDialog { MessageDialog {
id: errorDialog id: errorDialog
// don't allow edits to the rest of the window while open
// Using Setup-Guide here instead of Troubleshooting because it's likely that users modality:Qt.WindowModal
// will arrive here by forgetting to enable GameStream or not forwarding ports. icon: StandardIcon.Critical
helpUrl: "https://github.com/moonlight-stream/moonlight-docs/wiki/Setup-Guide" 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 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 // don't allow edits to the rest of the window while open
modality:Qt.WindowModal
property string pin : "0000" property string pin : "0000"
text:qsTr("Please enter %1 on your GameStream PC. This dialog will close when pairing is completed.").arg(pin) text:"Please enter " + pin + " on your GameStream PC. This dialog will close when pairing is completed."
standardButtons: Dialog.Cancel standardButtons: StandardButton.Cancel
onRejected: { onRejected: {
// FIXME: We should interrupt pairing here // FIXME: We should interrupt pairing here
} }
} }
NavigableMessageDialog { MessageDialog {
id: deletePcDialog id: deletePcDialog
// don't allow edits to the rest of the window while open // don't allow edits to the rest of the window while open
modality:Qt.WindowModal
property int pcIndex : -1; property int pcIndex : -1;
text:qsTr("Are you sure you want to remove this PC?") text:"Are you sure you want to remove this PC?"
standardButtons: Dialog.Yes | Dialog.No standardButtons: StandardButton.Yes | StandardButton.No
function deletePc() { function deletePc() {
console.log("deleting PC pairing for PC at index: " + pcIndex) console.log("deleting PC pairing for PC at index: " + pcIndex)
computerModel.deleteComputer(pcIndex); computerModel.deleteComputer(pcIndex);
} }
onYes: deletePc()
// For keyboard/gamepad activation
onAccepted: deletePc() onAccepted: deletePc()
} }
NavigableMessageDialog { Dialog {
id: testConnectionDialog id: addPcDialog
closePolicy: Popup.CloseOnEscape property string label: "Enter the IP address of your GameStream PC"
standardButtons: Dialog.Ok property string hint: "192.168.1.100"
property alias editText : editTextItem
onAboutToShow: { standardButtons: StandardButton.Ok | StandardButton.Cancel
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…") onVisibleChanged: {
showSpinner = true 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: { onAccepted: {
if (editText.text) { ComputerManager.addNewHost(editText.text, false)
computerModel.renameComputer(pcIndex, editText.text)
}
} }
ColumnLayout { ColumnLayout {
Label { Text {
text: renamePcDialog.label id: addPcDialogLabel
font.bold: true text: addPcDialog.label
} }
Rectangle {
implicitWidth: parent.parent.width
height: editTextItem.height
TextField { TextInput {
id: editText id: editTextItem
placeholderText: renamePcDialog.originalName inputMethodHints: Qt.ImhPreferUppercase
Layout.fillWidth: true text: addPcDialog.hint
focus: true
Keys.onReturnPressed: {
renamePcDialog.accept()
}
Keys.onEnterPressed: {
renamePcDialog.accept()
} }
} }
} }
} }
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 2.0
import QtQuick.Controls 2.2 import QtQuick.Controls 2.2
import QtQuick.Dialogs 1.2
import ComputerManager 1.0 import ComputerManager 1.0
import Session 1.0 import Session 1.0
@@ -9,7 +10,14 @@ Item {
property Session nextSession : null property Session nextSession : null
property string nextAppName : "" 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) function quitAppCompleted(error)
{ {
@@ -19,34 +27,28 @@ Item {
errorDialog.open() errorDialog.open()
} }
// Exit this view
stackView.pop()
// If we're supposed to launch another game after this, do so now // If we're supposed to launch another game after this, do so now
if (error === undefined && nextSession !== null) { if (error === undefined && nextSession !== null) {
var component = Qt.createComponent("StreamSegue.qml") var component = Qt.createComponent("StreamSegue.qml")
var segue = component.createObject(stackView, {"appName": nextAppName, "session": nextSession}) 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 { else {
// Exit this view // Disconnect the signal
stackView.pop() 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 { Row {
anchors.centerIn: parent anchors.centerIn: parent
spacing: 5 spacing: 5
@@ -66,7 +68,10 @@ Item {
} }
} }
ErrorMessageDialog { MessageDialog {
id: errorDialog id: errorDialog
modality:Qt.WindowModal
icon: StandardIcon.Critical
standardButtons: StandardButton.Ok
} }
} }
+167 -918
View File
File diff suppressed because it is too large. Load diff
+66 -90
View File
@@ -1,32 +1,35 @@
import QtQuick 2.0 import QtQuick 2.0
import QtQuick.Controls 2.2 import QtQuick.Controls 2.2
import QtQuick.Dialogs 1.2
import QtQuick.Window 2.2 import QtQuick.Window 2.2
import ComputerManager 1.0
import SdlGamepadKeyNavigation 1.0 import SdlGamepadKeyNavigation 1.0
import Session 1.0 import Session 1.0
Item { Item {
property Session session property Session session
property string appName property string appName
property string stageText : isResume ? qsTr("Resuming %1...").arg(appName) : property string stageText : "Starting " + appName + "..."
qsTr("Starting %1...").arg(appName)
property bool isResume : false
property bool quitAfter : false property bool quitAfter : false
anchors.fill: parent
// The StackView will trigger a visibility change when
// we're pushed onto it, causing our onVisibleChanged
// routine to run, but only if we start as invisible
visible: false
function stageStarting(stage) function stageStarting(stage)
{ {
// Update the spinner text // 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 errorDialog.text = "Starting " + stage + " failed: Error " + errorCode
streamSegueErrorDialog.text = qsTr("Starting %1 failed: Error %2").arg(stage).arg(errorCode) errorDialog.open()
if (failingPorts) {
streamSegueErrorDialog.text += "\n\n" + qsTr("Check your firewall and port forwarding rules for port(s): %1").arg(failingPorts)
}
} }
function connectionStarted() function connectionStarted()
@@ -35,7 +38,6 @@ Item {
// see them briefly when we pop off the StackView // see them briefly when we pop off the StackView
stageSpinner.visible = false stageSpinner.visible = false
stageLabel.visible = false stageLabel.visible = false
hintText.visible = false
// Hide the window now that streaming has begun // Hide the window now that streaming has begun
window.visible = false window.visible = false
@@ -44,7 +46,7 @@ Item {
function displayLaunchError(text) function displayLaunchError(text)
{ {
// Display the error dialog after Session::exec() returns // Display the error dialog after Session::exec() returns
streamSegueErrorDialog.text = text errorDialog.text = text
} }
function displayLaunchWarning(text) function displayLaunchWarning(text)
@@ -52,7 +54,7 @@ Item {
// This toast appears for 3 seconds, just shorter than how long // This toast appears for 3 seconds, just shorter than how long
// Session will wait for it to be displayed. This gives it time // Session will wait for it to be displayed. This gives it time
// to transition to invisible before continuing. // 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.text = text
toast.timeout = 3000 toast.timeout = 3000
toast.visible = true toast.visible = true
@@ -68,20 +70,12 @@ Item {
window.visible = true 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 (quitAfter) {
if (streamSegueErrorDialog.text) { if (errorDialog.text) {
// Quit when the error dialog is acknowledged // Quit when the error dialog is acknowledged
streamSegueErrorDialog.quitAfter = quitAfter errorDialog.open()
streamSegueErrorDialog.open()
} }
else { else {
// Quit immediately // Quit immediately
@@ -98,86 +92,48 @@ Item {
// the Qt UI is visible again to prevent losing // the Qt UI is visible again to prevent losing
// focus on the dialog which would impact gamepad // focus on the dialog which would impact gamepad
// users. // users.
if (streamSegueErrorDialog.text) { if (errorDialog.text) {
streamSegueErrorDialog.quitAfter = quitAfter errorDialog.open()
streamSegueErrorDialog.open()
} }
} }
} }
function sessionReadyForDeletion() // It's important that we don't call enable() here
{ // or it may interfere with the Session instance
// Garbage collect the Session object since it's pretty heavyweight // getting notified of initial connected gamepads.
// and keeps other libraries (like SDL_TTF) around until it is deleted. SdlGamepadKeyNavigation {
session = null id: gamepadKeyNav
gc()
} }
StackView.onDeactivating: { onVisibleChanged: {
// Show the toolbar again when popped off the stack if (visible) {
toolBar.visible = true // Hide the toolbar before we start loading
toolBar.visible = false
// Enable GUI gamepad usage now
SdlGamepadKeyNavigation.enable()
}
StackView.onActivated: {
// Hide the toolbar before we start loading
toolBar.visible = false
// Hook up our signals
session.stageStarting.connect(stageStarting)
session.stageFailed.connect(stageFailed)
session.connectionStarted.connect(connectionStarted)
session.displayLaunchError.connect(displayLaunchError)
session.displayLaunchWarning.connect(displayLaunchWarning)
session.quitStarting.connect(quitStarting)
session.sessionFinished.connect(sessionFinished)
session.readyForDeletion.connect(sessionReadyForDeletion)
// 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 // Set the hint text. We do this here rather than
// in the hintText control itself to synchronize // in the hintText control itself to synchronize
// with Session.exec() which requires no concurrent // with Session.exec() which requires no concurrent
// gamepad usage. // gamepad usage.
hintText.text = qsTr("Tip:") + " " + qsTr("Press %1 to disconnect your session").arg(SdlGamepadKeyNavigation.getConnectedGamepads() > 0 ? hintText.text = gamepadKeyNav.getConnectedGamepads() > 0 ?
qsTr("Start+Select+L1+R1") : qsTr("Ctrl+Alt+Shift+Q")) "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 // Hook up our signals
SdlGamepadKeyNavigation.disable() session.stageStarting.connect(stageStarting)
session.stageFailed.connect(stageFailed)
// Garbage collect QML stuff before we start streaming, session.connectionStarted.connect(connectionStarted)
// since we'll probably be streaming for a while and we session.displayLaunchError.connect(displayLaunchError)
// won't be able to GC during the stream. session.displayLaunchWarning.connect(displayLaunchWarning)
gc() session.quitStarting.connect(quitStarting)
session.sessionFinished.connect(sessionFinished)
// Run the streaming session to completion // Run the streaming session to completion
session.exec(Screen.virtualX, Screen.virtualY) session.exec(Screen.virtualX, Screen.virtualY)
} }
else if (!quitAfter) {
sourceComponent: Item {} // Show the toolbar again when we become hidden
toolBar.visible = true
}
} }
Row { Row {
@@ -186,7 +142,6 @@ Item {
BusyIndicator { BusyIndicator {
id: stageSpinner id: stageSpinner
running: false
} }
Label { Label {
@@ -210,4 +165,25 @@ Item {
wrapMode: Text.Wrap 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 -176
View File
@@ -7,7 +7,7 @@ AppModel::AppModel(QObject *parent)
this, &AppModel::handleBoxArtLoaded); this, &AppModel::handleBoxArtLoaded);
} }
void AppModel::initialize(ComputerManager* computerManager, int computerIndex, bool showHiddenGames) void AppModel::initialize(ComputerManager* computerManager, int computerIndex)
{ {
m_ComputerManager = computerManager; m_ComputerManager = computerManager;
connect(m_ComputerManager, &ComputerManager::computerStateChanged, connect(m_ComputerManager, &ComputerManager::computerStateChanged,
@@ -15,23 +15,29 @@ void AppModel::initialize(ComputerManager* computerManager, int computerIndex, b
Q_ASSERT(computerIndex < m_ComputerManager->getComputers().count()); Q_ASSERT(computerIndex < m_ComputerManager->getComputers().count());
m_Computer = m_ComputerManager->getComputers().at(computerIndex); m_Computer = m_ComputerManager->getComputers().at(computerIndex);
m_Apps = m_Computer->appList;
m_CurrentGameId = m_Computer->currentGameId; 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() QString AppModel::getRunningAppName()
{ {
if (m_CurrentGameId != 0) { if (m_CurrentGameId != 0) {
for (int i = 0; i < m_AllApps.count(); i++) { for (int i = 0; i < m_Apps.count(); i++) {
if (m_AllApps[i].id == m_CurrentGameId) { if (m_Apps[i].id == m_CurrentGameId) {
return m_AllApps[i].name; return m_Apps[i].name;
} }
} }
} }
@@ -41,23 +47,12 @@ QString AppModel::getRunningAppName()
Session* AppModel::createSessionForApp(int appIndex) Session* AppModel::createSessionForApp(int appIndex)
{ {
Q_ASSERT(appIndex < m_VisibleApps.count()); Q_ASSERT(appIndex < m_Apps.count());
NvApp app = m_VisibleApps.at(appIndex); NvApp app = m_Apps.at(appIndex);
return new Session(m_Computer, app); 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 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 // 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()) if (parent.isValid())
return 0; return 0;
return m_VisibleApps.count(); return m_Apps.count();
} }
QVariant AppModel::data(const QModelIndex &index, int role) const 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()) if (!index.isValid())
return QVariant(); return QVariant();
Q_ASSERT(index.row() < m_VisibleApps.count()); Q_ASSERT(index.row() < m_Apps.count());
NvApp app = m_VisibleApps.at(index.row()); NvApp app = m_Apps.at(index.row());
switch (role) switch (role)
{ {
@@ -85,14 +80,6 @@ QVariant AppModel::data(const QModelIndex &index, int role) const
case BoxArtRole: case BoxArtRole:
// FIXME: const-correctness // FIXME: const-correctness
return const_cast<BoxArtManager&>(m_BoxArtManager).loadBoxArt(m_Computer, app); 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;
case AppCollectorGameRole:
return app.isAppCollectorGame;
default: default:
return QVariant(); return QVariant();
} }
@@ -105,10 +92,6 @@ QHash<int, QByteArray> AppModel::roleNames() const
names[NameRole] = "name"; names[NameRole] = "name";
names[RunningRole] = "running"; names[RunningRole] = "running";
names[BoxArtRole] = "boxart"; names[BoxArtRole] = "boxart";
names[HiddenRole] = "hidden";
names[AppIdRole] = "appid";
names[DirectLaunchRole] = "directLaunch";
names[AppCollectorGameRole] = "appCollectorGame";
return names; return names;
} }
@@ -118,138 +101,6 @@ void AppModel::quitRunningApp()
m_ComputerManager->quitRunningApp(m_Computer); 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) void AppModel::handleComputerStateChanged(NvComputer* computer)
{ {
// Ignore updates for computers that aren't ours // Ignore updates for computers that aren't ours
@@ -268,15 +119,20 @@ void AppModel::handleComputerStateChanged(NvComputer* computer)
// First, process additions/removals from the app list. This // First, process additions/removals from the app list. This
// is required because the new game may now be running, so // is required because the new game may now be running, so
// we can't check that first. // we can't check that first.
if (computer->appList != m_AllApps) { if (computer->appList != m_Apps) {
updateAppList(computer->appList); // 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 // Finally, process changes to the active app
if (computer->currentGameId != m_CurrentGameId) { if (computer->currentGameId != m_CurrentGameId) {
// First, invalidate the running state of newly running game // First, invalidate the running state of newly running game
for (int i = 0; i < m_VisibleApps.count(); i++) { for (int i = 0; i < m_Apps.count(); i++) {
if (m_VisibleApps[i].id == computer->currentGameId) { if (m_Apps[i].id == computer->currentGameId) {
emit dataChanged(createIndex(i, 0), emit dataChanged(createIndex(i, 0),
createIndex(i, 0), createIndex(i, 0),
QVector<int>() << RunningRole); QVector<int>() << RunningRole);
@@ -286,8 +142,8 @@ void AppModel::handleComputerStateChanged(NvComputer* computer)
// Next, invalidate the running state of the old game (if it exists) // Next, invalidate the running state of the old game (if it exists)
if (m_CurrentGameId != 0) { if (m_CurrentGameId != 0) {
for (int i = 0; i < m_VisibleApps.count(); i++) { for (int i = 0; i < m_Apps.count(); i++) {
if (m_VisibleApps[i].id == m_CurrentGameId) { if (m_Apps[i].id == m_CurrentGameId) {
emit dataChanged(createIndex(i, 0), emit dataChanged(createIndex(i, 0),
createIndex(i, 0), createIndex(i, 0),
QVector<int>() << RunningRole); QVector<int>() << RunningRole);
@@ -305,7 +161,7 @@ void AppModel::handleBoxArtLoaded(NvComputer* computer, NvApp app, QUrl /* image
{ {
Q_ASSERT(computer == m_Computer); 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 // Make sure we're not delivering a callback to an app that's already been removed
if (index >= 0) { if (index >= 0) {
+4 -21
View File
@@ -14,33 +14,23 @@ class AppModel : public QAbstractListModel
{ {
NameRole = Qt::UserRole, NameRole = Qt::UserRole,
RunningRole, RunningRole,
BoxArtRole, BoxArtRole
HiddenRole,
AppIdRole,
DirectLaunchRole,
AppCollectorGameRole,
}; };
public: public:
explicit AppModel(QObject *parent = nullptr); explicit AppModel(QObject *parent = nullptr);
// Must be called before any QAbstractListModel functions // 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 Session* createSessionForApp(int appIndex);
Q_INVOKABLE int getDirectLaunchAppIndex(); Q_INVOKABLE int getRunningAppIndex();
Q_INVOKABLE int getRunningAppId();
Q_INVOKABLE QString getRunningAppName(); Q_INVOKABLE QString getRunningAppName();
Q_INVOKABLE void quitRunningApp(); 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; QVariant data(const QModelIndex &index, int role) const override;
int rowCount(const QModelIndex &parent) const override; int rowCount(const QModelIndex &parent) const override;
@@ -56,16 +46,9 @@ signals:
void computerLost(); void computerLost();
private: private:
void updateAppList(QVector<NvApp> newList);
QVector<NvApp> getVisibleApps(const QVector<NvApp>& appList);
bool isAppCurrentlyVisible(const NvApp& app);
NvComputer* m_Computer; NvComputer* m_Computer;
BoxArtManager m_BoxArtManager; BoxArtManager m_BoxArtManager;
ComputerManager* m_ComputerManager; ComputerManager* m_ComputerManager;
QVector<NvApp> m_VisibleApps, m_AllApps; QVector<NvApp> m_Apps;
int m_CurrentGameId; int m_CurrentGameId;
bool m_ShowHiddenGames;
}; };
+17 -53
View File
@@ -2,8 +2,6 @@
#include <QThreadPool> #include <QThreadPool>
#include <random>
ComputerModel::ComputerModel(QObject* object) ComputerModel::ComputerModel(QObject* object)
: QAbstractListModel(object) {} : QAbstractListModel(object) {}
@@ -24,6 +22,18 @@ QVariant ComputerModel::data(const QModelIndex& index, int role) const
return QVariant(); 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()); Q_ASSERT(index.row() < m_Computers.count());
NvComputer* computer = m_Computers[index.row()]; NvComputer* computer = m_Computers[index.row()];
@@ -40,10 +50,10 @@ QVariant ComputerModel::data(const QModelIndex& index, int role) const
return computer->currentGameId != 0; return computer->currentGameId != 0;
case WakeableRole: case WakeableRole:
return !computer->macAddress.isEmpty(); return !computer->macAddress.isEmpty();
case AddPcRole:
return false;
case StatusUnknownRole: case StatusUnknownRole:
return computer->state == NvComputer::CS_UNKNOWN; return computer->state == NvComputer::CS_UNKNOWN;
case ServerSupportedRole:
return computer->isSupportedServerVersion;
default: default:
return QVariant(); return QVariant();
} }
@@ -57,7 +67,8 @@ int ComputerModel::rowCount(const QModelIndex& parent) const
return 0; return 0;
} }
return m_Computers.count(); // Add PC placeholder counts as 1
return m_Computers.count() + 1;
} }
QHash<int, QByteArray> ComputerModel::roleNames() const QHash<int, QByteArray> ComputerModel::roleNames() const
@@ -68,9 +79,9 @@ QHash<int, QByteArray> ComputerModel::roleNames() const
names[OnlineRole] = "online"; names[OnlineRole] = "online";
names[PairedRole] = "paired"; names[PairedRole] = "paired";
names[BusyRole] = "busy"; names[BusyRole] = "busy";
names[AddPcRole] = "addPc";
names[WakeableRole] = "wakeable"; names[WakeableRole] = "wakeable";
names[StatusUnknownRole] = "statusUnknown"; names[StatusUnknownRole] = "statusUnknown";
names[ServerSupportedRole] = "serverSupported";
return names; return names;
} }
@@ -133,52 +144,6 @@ void ComputerModel::wakeComputer(int computerIndex)
QThreadPool::globalInstance()->start(wakeTask); 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) void ComputerModel::pairComputer(int computerIndex, QString pin)
{ {
Q_ASSERT(computerIndex < m_Computers.count()); Q_ASSERT(computerIndex < m_Computers.count());
@@ -209,4 +174,3 @@ void ComputerModel::handleComputerStateChanged(NvComputer* computer)
} }
} }
#include "computermodel.moc"
+1 -8
View File
@@ -15,7 +15,7 @@ class ComputerModel : public QAbstractListModel
BusyRole, BusyRole,
WakeableRole, WakeableRole,
StatusUnknownRole, StatusUnknownRole,
ServerSupportedRole AddPcRole
}; };
public: public:
@@ -32,21 +32,14 @@ public:
Q_INVOKABLE void deleteComputer(int computerIndex); Q_INVOKABLE void deleteComputer(int computerIndex);
Q_INVOKABLE QString generatePinString();
Q_INVOKABLE void pairComputer(int computerIndex, QString pin); Q_INVOKABLE void pairComputer(int computerIndex, QString pin);
Q_INVOKABLE void testConnectionForComputer(int computerIndex);
Q_INVOKABLE void wakeComputer(int computerIndex); Q_INVOKABLE void wakeComputer(int computerIndex);
Q_INVOKABLE void renameComputer(int computerIndex, QString name);
Q_INVOKABLE Session* createSessionForCurrentGame(int computerIndex); Q_INVOKABLE Session* createSessionForCurrentGame(int computerIndex);
signals: signals:
void pairingCompleted(QVariant error); void pairingCompleted(QVariant error);
void connectionTestCompleted(int result, QString blockedPorts);
private slots: private slots:
void handleComputerStateChanged(NvComputer* computer); void handleComputerStateChanged(NvComputer* computer);
+119 -227
View File
@@ -1,50 +1,30 @@
import QtQuick 2.9 import QtQuick 2.9
import QtQuick.Controls 2.2 import QtQuick.Controls 2.2
import QtQuick.Dialogs 1.2
import QtQuick.Layouts 1.3 import QtQuick.Layouts 1.3
import QtQuick.Window 2.2 import QtQuick.Window 2.2
import QtQuick.Controls.Material 2.1
import ComputerManager 1.0 import ComputerManager 1.0
import AutoUpdateChecker 1.0 import AutoUpdateChecker 1.0
import StreamingPreferences 1.0 import StreamingPreferences 1.0
import SystemProperties 1.0
import SdlGamepadKeyNavigation 1.0
ApplicationWindow { ApplicationWindow {
property bool pollingActive: false property bool pollingActive: false
// Set by SettingsView to force the back operation to pop all
// pages except the initial view. This is required when doing
// a retranslate() because AppView breaks for some reason.
property bool clearOnBack: false
id: window id: window
visible: true visible: true
width: 1280 width: 1280
height: 600 height: 600
visibility: { visibility: prefs.startWindowed ? "Windowed" : "Maximized"
if (SystemProperties.hasDesktopEnvironment) {
if (StreamingPreferences.uiDisplayMode == StreamingPreferences.UI_WINDOWED) return "Windowed"
else if (StreamingPreferences.uiDisplayMode == StreamingPreferences.UI_MAXIMIZED) return "Maximized"
else if (StreamingPreferences.uiDisplayMode == StreamingPreferences.UI_FULLSCREEN) return "FullScreen"
} else {
return "FullScreen"
}
}
// This configures the maximum width of the singleton attached QML ToolTip. If left unconstrained,
// it will never insert a line break and just extend on forever.
ToolTip.toolTip.contentWidth: ToolTip.toolTip.implicitContentWidth < 400 ? ToolTip.toolTip.implicitContentWidth : 400
function goBack() { Material.theme: Material.Dark
if (clearOnBack) { Material.accent: Material.Purple
// Pop all items except the first one
stackView.pop(null) StreamingPreferences {
clearOnBack = false id: prefs
}
else {
stackView.pop()
}
} }
StackView { StackView {
@@ -62,7 +42,7 @@ ApplicationWindow {
Keys.onEscapePressed: { Keys.onEscapePressed: {
if (depth > 1) { if (depth > 1) {
goBack() stackView.pop()
} }
else { else {
quitConfirmationDialog.open() quitConfirmationDialog.open()
@@ -71,7 +51,7 @@ ApplicationWindow {
Keys.onBackPressed: { Keys.onBackPressed: {
if (depth > 1) { if (depth > 1) {
goBack() stackView.pop()
} }
else { else {
quitConfirmationDialog.open() quitConfirmationDialog.open()
@@ -162,20 +142,19 @@ ApplicationWindow {
// onAfterRendering call and potentially reenter this code. // onAfterRendering call and potentially reenter this code.
initialized = true; initialized = true;
if (SystemProperties.isWow64) { if (prefs.isRunningWayland()) {
waylandDialog.open()
}
else if (prefs.isWow64()) {
wow64Dialog.open() wow64Dialog.open()
} }
else if (!SystemProperties.hasHardwareAcceleration) { else if (!prefs.hasAnyHardwareAcceleration()) {
if (SystemProperties.isRunningXWayland) { noHwDecoderDialog.open()
xWaylandDialog.open()
}
else {
noHwDecoderDialog.open()
}
} }
if (SystemProperties.unmappedGamepads) { var unmappedGamepads = prefs.getUnmappedGamepads()
unmappedGamepadDialog.unmappedGamepads = SystemProperties.unmappedGamepads if (unmappedGamepads) {
unmappedGamepadDialog.unmappedGamepads = unmappedGamepads
unmappedGamepadDialog.open() unmappedGamepadDialog.open()
} }
} }
@@ -199,109 +178,44 @@ ApplicationWindow {
header: ToolBar { header: ToolBar {
id: toolBar id: toolBar
height: 60
anchors.topMargin: 5 anchors.topMargin: 5
anchors.bottomMargin: 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 { RowLayout {
spacing: 10 spacing: 20
anchors.leftMargin: 10
anchors.rightMargin: 10
anchors.fill: parent anchors.fill: parent
NavigableToolButton { NavigableToolButton {
// Only make the button visible if the user has navigated somewhere. // Only make the button visible if the user has navigated somewhere.
visible: stackView.depth > 1 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: goBack() onClicked: stackView.pop()
Keys.onDownPressed: { Keys.onDownPressed: {
stackView.currentItem.forceActiveFocus(Qt.TabFocus) stackView.currentItem.forceActiveFocus(Qt.TabFocus)
} }
} }
// This label will appear when the window gets too small and
// we need to ensure the toolbar controls don't collide
Label { Label {
id: titleRowLabel id: titleLabel
font.pointSize: titleLabel.font.pointSize text: stackView.currentItem.objectName
font.pointSize: 15
elide: Label.ElideRight elide: Label.ElideRight
horizontalAlignment: Qt.AlignHCenter horizontalAlignment: Qt.AlignHCenter
verticalAlignment: Qt.AlignVCenter verticalAlignment: Qt.AlignVCenter
Layout.fillWidth: true 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 { NavigableToolButton {
@@ -309,25 +223,28 @@ ApplicationWindow {
id: updateButton 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.delay: 1000
ToolTip.timeout: 3000 ToolTip.timeout: 3000
ToolTip.visible: hovered || visible ToolTip.visible: hovered
// Invisible until we get a callback notifying us that // Invisible until we get a callback notifying us that
// an update is available // an update is available
visible: false visible: false
onClicked: { onClicked: Qt.openUrlExternally(browserUrl);
if (SystemProperties.hasBrowser) {
Qt.openUrlExternally(browserUrl);
}
}
function updateAvailable(version, url) 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.browserUrl = url
updateButton.visible = true updateButton.visible = true
} }
@@ -344,14 +261,20 @@ ApplicationWindow {
NavigableToolButton { NavigableToolButton {
id: helpButton 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.delay: 1000
ToolTip.timeout: 3000 ToolTip.timeout: 3000
ToolTip.visible: hovered ToolTip.visible: hovered
ToolTip.text: qsTr("Help") + (helpShortcut.nativeText ? (" ("+helpShortcut.nativeText+")") : "") ToolTip.text: "Help" + (helpShortcut.nativeText ? (" ("+helpShortcut.nativeText+")") : "")
Shortcut { Shortcut {
id: helpShortcut id: helpShortcut
@@ -374,11 +297,18 @@ ApplicationWindow {
ToolTip.delay: 1000 ToolTip.delay: 1000
ToolTip.timeout: 3000 ToolTip.timeout: 3000
ToolTip.visible: hovered 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: { Keys.onDownPressed: {
stackView.currentItem.forceActiveFocus(Qt.TabFocus) stackView.currentItem.forceActiveFocus(Qt.TabFocus)
@@ -388,9 +318,16 @@ ApplicationWindow {
NavigableToolButton { NavigableToolButton {
id: settingsButton 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: { Keys.onDownPressed: {
stackView.currentItem.forceActiveFocus(Qt.TabFocus) stackView.currentItem.forceActiveFocus(Qt.TabFocus)
@@ -405,117 +342,72 @@ ApplicationWindow {
ToolTip.delay: 1000 ToolTip.delay: 1000
ToolTip.timeout: 3000 ToolTip.timeout: 3000
ToolTip.visible: hovered ToolTip.visible: hovered
ToolTip.text: qsTr("Settings") + (settingsShortcut.nativeText ? (" ("+settingsShortcut.nativeText+")") : "") ToolTip.text: "Settings" + (settingsShortcut.nativeText ? (" ("+settingsShortcut.nativeText+")") : "")
} }
} }
} }
ErrorMessageDialog { MessageDialog {
id: noHwDecoderDialog id: noHwDecoderDialog
text: qsTr("No functioning hardware accelerated video decoder was detected by Moonlight. " + modality:Qt.WindowModal
"Your streaming performance may be severely degraded in this configuration.") icon: StandardIcon.Warning
helpText: qsTr("Click the Help button for more information on solving this problem.") standardButtons: StandardButton.Ok | StandardButton.Help
helpUrl: "https://github.com/moonlight-stream/moonlight-docs/wiki/Fixing-Hardware-Decoding-Problems" 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 { MessageDialog {
id: xWaylandDialog id: waylandDialog
text: qsTr("Hardware acceleration doesn't work on XWayland. Continuing on XWayland may result in poor streaming performance. " + modality:Qt.WindowModal
"Try running with QT_QPA_PLATFORM=wayland or switch to X11.") icon: StandardIcon.Warning
helpText: qsTr("Click the Help button for more information.") standardButtons: StandardButton.Ok | StandardButton.Help
helpUrl: "https://github.com/moonlight-stream/moonlight-docs/wiki/Fixing-Hardware-Decoding-Problems" 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 id: wow64Dialog
standardButtons: Dialog.Ok | Dialog.Cancel modality:Qt.WindowModal
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) 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: { onAccepted: {
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-qt/releases"); Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-qt/releases");
} }
} }
ErrorMessageDialog { MessageDialog {
id: unmappedGamepadDialog id: unmappedGamepadDialog
property string unmappedGamepads : "" property string unmappedGamepads : ""
text: qsTr("Moonlight detected gamepads without a mapping:") + "\n" + unmappedGamepads modality:Qt.WindowModal
helpTextSeparator: "\n\n" icon: StandardIcon.Warning
helpText: qsTr("Click the Help button for information on how to map your gamepads.") standardButtons: StandardButton.Ok | StandardButton.Help
helpUrl: "https://github.com/moonlight-stream/moonlight-docs/wiki/Gamepad-Mapping" 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 // This dialog appears when quitting via keyboard or gamepad button
NavigableMessageDialog { MessageDialog {
id: quitConfirmationDialog id: quitConfirmationDialog
standardButtons: Dialog.Yes | Dialog.No modality:Qt.WindowModal
text: qsTr("Are you sure you want to quit?") icon: StandardIcon.Warning
standardButtons: StandardButton.Yes | StandardButton.No
text: "Are you sure you want to quit?"
onYes: Qt.quit()
// For keyboard/gamepad navigation // For keyboard/gamepad navigation
onAccepted: Qt.quit() 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.addNewHostManually(editText.text.trim())
}
}
ColumnLayout {
Label {
text: addPcDialog.label
font.bold: true
}
TextField {
id: editText
Layout.fillWidth: true
focus: true
Keys.onReturnPressed: {
addPcDialog.accept()
}
Keys.onEnterPressed: {
addPcDialog.accept()
}
}
}
}
} }
+44 -91
View File
@@ -4,19 +4,17 @@
#include <QGuiApplication> #include <QGuiApplication>
#include <QWindow> #include <QWindow>
#include "settings/streamingpreferences.h"
#include "settings/mappingmanager.h" #include "settings/mappingmanager.h"
#define AXIS_NAVIGATION_REPEAT_DELAY 150 #define AXIS_NAVIGATION_REPEAT_DELAY 150
SdlGamepadKeyNavigation::SdlGamepadKeyNavigation() SdlGamepadKeyNavigation::SdlGamepadKeyNavigation()
: m_Enabled(false), : m_Enabled(false),
m_UiNavMode(false), m_SettingsMode(false),
m_FirstPoll(false),
m_LastAxisNavigationEventTime(0) m_LastAxisNavigationEventTime(0)
{ {
m_PollingTimer = new QTimer(this); m_PollingTimer = new QTimer(this);
connect(m_PollingTimer, &QTimer::timeout, this, &SdlGamepadKeyNavigation::onPollingTimerFired); connect(m_PollingTimer, SIGNAL(timeout()), this, SLOT(onPollingTimerFired()));
} }
SdlGamepadKeyNavigation::~SdlGamepadKeyNavigation() SdlGamepadKeyNavigation::~SdlGamepadKeyNavigation()
@@ -63,9 +61,6 @@ void SdlGamepadKeyNavigation::enable()
} }
} }
// Flush events on the first poll
m_FirstPoll = true;
// Poll every 50 ms for a new joystick event // Poll every 50 ms for a new joystick event
m_PollingTimer->start(50); m_PollingTimer->start(50);
@@ -93,26 +88,9 @@ void SdlGamepadKeyNavigation::disable()
void SdlGamepadKeyNavigation::onPollingTimerFired() void SdlGamepadKeyNavigation::onPollingTimerFired()
{ {
SDL_Event event; SDL_Event event;
StreamingPreferences prefs;
// Discard any pending button events on the first poll to avoid picking up
// stale input data from the stream session (like the quit combo).
if (m_FirstPoll) {
SDL_PumpEvents();
SDL_FlushEvent(SDL_CONTROLLERBUTTONDOWN);
SDL_FlushEvent(SDL_CONTROLLERBUTTONUP);
m_FirstPoll = false;
}
while (SDL_PollEvent(&event)) { while (SDL_PollEvent(&event)) {
switch (event.type) { 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_CONTROLLERBUTTONDOWN:
case SDL_CONTROLLERBUTTONUP: case SDL_CONTROLLERBUTTONUP:
{ {
@@ -120,27 +98,9 @@ void SdlGamepadKeyNavigation::onPollingTimerFired()
event.type == SDL_CONTROLLERBUTTONDOWN ? event.type == SDL_CONTROLLERBUTTONDOWN ?
QEvent::Type::KeyPress : QEvent::Type::KeyRelease; 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) { switch (event.cbutton.button) {
case SDL_CONTROLLER_BUTTON_DPAD_UP: case SDL_CONTROLLER_BUTTON_DPAD_UP:
if (m_UiNavMode) { if (m_SettingsMode) {
// Back-tab // Back-tab
sendKey(type, Qt::Key_Tab, Qt::ShiftModifier); sendKey(type, Qt::Key_Tab, Qt::ShiftModifier);
} }
@@ -149,7 +109,7 @@ void SdlGamepadKeyNavigation::onPollingTimerFired()
} }
break; break;
case SDL_CONTROLLER_BUTTON_DPAD_DOWN: case SDL_CONTROLLER_BUTTON_DPAD_DOWN:
if (m_UiNavMode) { if (m_SettingsMode) {
sendKey(type, Qt::Key_Tab); sendKey(type, Qt::Key_Tab);
} }
else { else {
@@ -158,12 +118,24 @@ void SdlGamepadKeyNavigation::onPollingTimerFired()
break; break;
case SDL_CONTROLLER_BUTTON_DPAD_LEFT: case SDL_CONTROLLER_BUTTON_DPAD_LEFT:
sendKey(type, Qt::Key_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; break;
case SDL_CONTROLLER_BUTTON_DPAD_RIGHT: case SDL_CONTROLLER_BUTTON_DPAD_RIGHT:
sendKey(type, Qt::Key_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; break;
case SDL_CONTROLLER_BUTTON_A: case SDL_CONTROLLER_BUTTON_A:
if (m_UiNavMode) { if (m_SettingsMode) {
sendKey(type, Qt::Key_Space); sendKey(type, Qt::Key_Space);
} }
else { else {
@@ -205,7 +177,7 @@ void SdlGamepadKeyNavigation::onPollingTimerFired()
// Do nothing // Do nothing
} }
else if (leftY < -30000) { else if (leftY < -30000) {
if (m_UiNavMode) { if (m_SettingsMode) {
// Back-tab // Back-tab
sendKey(QEvent::Type::KeyPress, Qt::Key_Tab, Qt::ShiftModifier); sendKey(QEvent::Type::KeyPress, Qt::Key_Tab, Qt::ShiftModifier);
sendKey(QEvent::Type::KeyRelease, Qt::Key_Tab, Qt::ShiftModifier); sendKey(QEvent::Type::KeyRelease, Qt::Key_Tab, Qt::ShiftModifier);
@@ -218,7 +190,7 @@ void SdlGamepadKeyNavigation::onPollingTimerFired()
m_LastAxisNavigationEventTime = SDL_GetTicks(); m_LastAxisNavigationEventTime = SDL_GetTicks();
} }
else if (leftY > 30000) { else if (leftY > 30000) {
if (m_UiNavMode) { if (m_SettingsMode) {
sendKey(QEvent::Type::KeyPress, Qt::Key_Tab); sendKey(QEvent::Type::KeyPress, Qt::Key_Tab);
sendKey(QEvent::Type::KeyRelease, Qt::Key_Tab); sendKey(QEvent::Type::KeyRelease, Qt::Key_Tab);
} }
@@ -232,38 +204,29 @@ void SdlGamepadKeyNavigation::onPollingTimerFired()
else if (leftX < -30000) { else if (leftX < -30000) {
sendKey(QEvent::Type::KeyPress, Qt::Key_Left); sendKey(QEvent::Type::KeyPress, Qt::Key_Left);
sendKey(QEvent::Type::KeyRelease, 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(); m_LastAxisNavigationEventTime = SDL_GetTicks();
} }
else if (leftX > 30000) { else if (leftX > 30000) {
sendKey(QEvent::Type::KeyPress, Qt::Key_Right); sendKey(QEvent::Type::KeyPress, Qt::Key_Right);
sendKey(QEvent::Type::KeyRelease, 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(); 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);
}
}
} }
} }
@@ -277,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()); m_SettingsMode = settingsMode;
QWindow* focusWindow = app->focusWindow();
if (focusWindow != nullptr) {
QPoint mousePos(focusWindow->width() / 2, focusWindow->height() / 2);
QPoint globalPos(focusWindow->mapToGlobal(mousePos));
#if QT_VERSION >= QT_VERSION_CHECK(5, 12, 0)
QWheelEvent wheelEvent(mousePos, globalPos, QPoint(), angleDelta, Qt::NoButton, Qt::NoModifier, Qt::NoScrollPhase, false, Qt::MouseEventSynthesizedByApplication);
#else
QWheelEvent wheelEvent(mousePos, globalPos, QPoint(), angleDelta, angleDelta.y(), Qt::Vertical, Qt::NoButton, Qt::NoModifier, Qt::NoScrollPhase, Qt::MouseEventSynthesizedByApplication, false);
#endif
app->sendEvent(focusWindow, &wheelEvent);
}
}
void SdlGamepadKeyNavigation::setUiNavMode(bool uiNavMode)
{
m_UiNavMode = uiNavMode;
} }
int SdlGamepadKeyNavigation::getConnectedGamepads() 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; int count = 0;
for (int i = 0; i < SDL_NumJoysticks(); i++) { for (int i = 0; i < SDL_NumJoysticks(); i++) {
@@ -309,5 +261,6 @@ int SdlGamepadKeyNavigation::getConnectedGamepads()
} }
} }
SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER);
return count; return count;
} }
+2 -4
View File
@@ -18,13 +18,12 @@ public:
Q_INVOKABLE void disable(); Q_INVOKABLE void disable();
Q_INVOKABLE void setUiNavMode(bool settingsMode); Q_INVOKABLE void setSettingsMode(bool settingsMode);
Q_INVOKABLE int getConnectedGamepads(); Q_INVOKABLE int getConnectedGamepads();
private: private:
void sendKey(QEvent::Type type, Qt::Key key, Qt::KeyboardModifiers modifiers = Qt::NoModifier); void sendKey(QEvent::Type type, Qt::Key key, Qt::KeyboardModifiers modifiers = Qt::NoModifier);
void sendWheel(QPoint& angleDelta);
private slots: private slots:
void onPollingTimerFired(); void onPollingTimerFired();
@@ -33,7 +32,6 @@ private:
QTimer* m_PollingTimer; QTimer* m_PollingTimer;
QList<SDL_GameController*> m_Gamepads; QList<SDL_GameController*> m_Gamepads;
bool m_Enabled; bool m_Enabled;
bool m_UiNavMode; bool m_SettingsMode;
bool m_FirstPoll;
Uint32 m_LastAxisNavigationEventTime; Uint32 m_LastAxisNavigationEventTime;
}; };
Binary file not shown.
File diff suppressed because it is too large. Load diff
Binary file not shown.
File diff suppressed because it is too large. Load diff
Binary file not shown.
File diff suppressed because it is too large. Load diff
Binary file not shown.
File diff suppressed because it is too large. Load diff
Binary file not shown.
File diff suppressed because it is too large. Load diff
Binary file not shown.
File diff suppressed because it is too large. Load diff
Binary file not shown.
File diff suppressed because it is too large. Load diff
Binary file not shown.
File diff suppressed because it is too large. Load diff
Binary file not shown.
File diff suppressed because it is too large. Load diff
Binary file not shown.
File diff suppressed because it is too large. Load diff
Binary file not shown.
File diff suppressed because it is too large. Load diff
Binary file not shown.
File diff suppressed because it is too large. Load diff
Binary file not shown.
File diff suppressed because it is too large. Load diff
Binary file not shown.
File diff suppressed because it is too large. Load diff
Binary file not shown.
File diff suppressed because it is too large. Load diff
Binary file not shown.
File diff suppressed because it is too large. Load diff
Binary file not shown.
File diff suppressed because it is too large. Load diff
Loaded 100 of 804 files, more files were not shown because too many files have changed in this diff. Show more