Compare commits

..
1 Commits
Author SHA1 Message Date
Cameron Gutman b23a36908c Add EGLFS GBM and Wayland support to AppImage 2022-04-09 18:23:09 -05:00
230 changed files with 12575 additions and 36147 deletions

No files matched your search

+3 -4
View File
@@ -37,13 +37,12 @@ List the games you've tried that exhibit the issue. To see if the issue is game-
- OS: [e.g. Windows 10 1809] - OS: [e.g. Windows 10 1809]
- Moonlight Version: [e.g. v0.9.0] - Moonlight Version: [e.g. v0.9.0]
- GPU: [e.g. Intel HD Graphics 520] - GPU: [e.g. Intel HD Graphics 520]
- Linux package type (if applicable): [e.g. Flatpak]
**Server PC details (please complete the following information)** **Server PC details (please complete the following information)**
- OS: [e.g. Windows 10 1809] - OS: [e.g. Windows 10 1809]
- Sunshine or GeForce Experience version: [e.g. Sunshine v0.21.0] - GeForce Experience version: [e.g. 3.16.0.140]
- GPU: [e.g. AMD Radeon RX 7900 XT] - Nvidia GPU driver: [e.g. 417.35]
- GPU driver: [e.g. 24.1.1] - 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.
+11
View File
@@ -158,6 +158,17 @@ private:
L"EZFRD32.dll", L"EZFRD32.dll",
L"EZFRD64.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. // 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 // https://docs.microsoft.com/en-us/windows-hardware/drivers/display/hybrid-system-ddi
// //
+24 -33
View File
@@ -1,6 +1,6 @@
# Moonlight PC # Moonlight PC
[Moonlight PC](https://moonlight-stream.org) is an open source PC client for NVIDIA GameStream and [Sunshine](https://github.com/LizardByte/Sunshine). [Moonlight PC](https://moonlight-stream.org) is an open source PC client for NVIDIA GameStream, as used by the NVIDIA Shield.
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).
@@ -12,61 +12,53 @@ You can follow development on our [Discord server](https://moonlight-stream.org/
## Features ## Features
- Hardware accelerated video decoding on Windows, Mac, and Linux - Hardware accelerated video decoding on Windows, Mac, and Linux
- H.264, HEVC, and AV1 codec support (AV1 requires Sunshine and a supported host GPU) - Supports streaming at up to 120 FPS (high refresh rate monitor recommended)
- YUV 4:4:4 support (Sunshine only) - Supports streaming at 720p, 1080p, 1440p, 4K, and the client PC's native screen resolution
- HDR streaming support - HDR streaming support
- 7.1 surround sound audio support - 7.1 surround sound audio support
- 10-point multitouch support (Sunshine only)
- Gamepad support with force feedback and motion controls for up to 16 players
- Support for both pointer capture (for games) and direct mouse control (for remote desktop) - Support for both pointer capture (for games) and direct mouse control (for remote desktop)
- Support for passing system-wide keyboard shortcuts like Alt+Tab to the host - 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
- Gamepad support with force feedback for up to 4 players
## Downloads ## Downloads
- [Windows, macOS, and Steam Link](https://github.com/moonlight-stream/moonlight-qt/releases) - [Windows, macOS, and Steam Link](https://github.com/moonlight-stream/moonlight-qt/releases)
- [Snap (for Ubuntu-based Linux distros)](https://snapcraft.io/moonlight) - [Snap (for Ubuntu-based Linux distros)](https://snapcraft.io/moonlight)
- [Flatpak (for other Linux distros)](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) - [AppImage](https://github.com/moonlight-stream/moonlight-qt/releases)
- [Raspberry Pi 4 and 5](https://github.com/moonlight-stream/moonlight-docs/wiki/Installing-Moonlight-Qt-on-Raspberry-Pi-4) - [Raspberry Pi 4](https://github.com/moonlight-stream/moonlight-docs/wiki/Installing-Moonlight-Qt-on-Raspberry-Pi-4)
- [Generic ARM 32-bit and 64-bit Debian packages](https://github.com/moonlight-stream/moonlight-docs/wiki/Installing-Moonlight-Qt-on-ARM%E2%80%90based-Single-Board-Computers) (not for Raspberry Pi)
- [Experimental RISC-V Debian packages](https://github.com/moonlight-stream/moonlight-docs/wiki/Installing-Moonlight-Qt-on-RISC%E2%80%90V-Single-Board-Computers)
- [NVIDIA Jetson and Nintendo Switch (Ubuntu L4T)](https://github.com/moonlight-stream/moonlight-docs/wiki/Installing-Moonlight-Qt-on-Linux4Tegra-(L4T)-Ubuntu) - [NVIDIA Jetson and Nintendo Switch (Ubuntu L4T)](https://github.com/moonlight-stream/moonlight-docs/wiki/Installing-Moonlight-Qt-on-Linux4Tegra-(L4T)-Ubuntu)
#### Special Thanks #### Special Thanks
[![Hosted By: Cloudsmith](https://img.shields.io/badge/OSS%20hosting%20by-cloudsmith-blue?logo=cloudsmith&style=flat-square)](https://cloudsmith.com) [![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 Debian and L4T package repositories is graciously provided for free by [Cloudsmith](https://cloudsmith.com). Hosting for Moonlight's Raspberry Pi and L4T package repositories is graciously provided for free by [Cloudsmith](https://cloudsmith.com).
## Building ## Building
### Windows Build Requirements ### Windows Build Requirements
* Qt 6.7 SDK or later (earlier versions may work but are not officially supported) * Qt 5.15 SDK or later
* [Visual Studio 2022](https://visualstudio.microsoft.com/downloads/) (Community edition is fine) * Windows 7 or later
* Select **MSVC** option during Qt installation. MinGW is not supported. * [Visual Studio 2019](https://visualstudio.microsoft.com/downloads/) (Community edition is fine)
* Select **MSVC 2019** option during Qt installation. MinGW is not supported.
* [7-Zip](https://www.7-zip.org/) (only if building installers for non-development PCs) * [7-Zip](https://www.7-zip.org/) (only if building installers for non-development PCs)
* Graphics Tools (only if running debug builds) * [WiX Toolset](https://wixtoolset.org/releases/v3-14-0-5722/) v3.14.0.5722 or later (only if building installers for non-development PCs)
* Install "Graphics Tools" in the Optional Features page of the Windows Settings app.
* Alternatively, run `dism /online /add-capability /capabilityname:Tools.Graphics.DirectX~~~~0.0.1.0` and reboot.
### macOS Build Requirements ### macOS Build Requirements
* Qt 6.7 SDK or later (earlier versions may work but are not officially supported) * Qt 6.2 SDK or later
* Xcode 14 or later (earlier versions may work but are not officially supported) * macOS Mojave (10.14) or later
* 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/Unix Build Requirements
* Qt 6 is recommended, but Qt 5.9 or later is also supported (replace `qmake6` with `qmake` when using Qt 5). * Qt 5.9 SDK or later
* GCC or Clang * GCC or Clang
* FFmpeg 4.0 or later
* Install the required packages: * Install the required packages:
* Debian/Ubuntu: * 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`
* Base Requirements: `libegl1-mesa-dev libgl1-mesa-dev libopus-dev libsdl2-dev libsdl2-ttf-dev libssl-dev libavcodec-dev libavformat-dev libswscale-dev libva-dev libvdpau-dev libxkbcommon-dev wayland-protocols libdrm-dev` * 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`
* Qt 6 (Recommended): `qt6-base-dev qt6-declarative-dev libqt6svg6-dev qml6-module-qtquick-controls qml6-module-qtquick-templates qml6-module-qtquick-layouts qml6-module-qtqml-workerscript qml6-module-qtquick-window qml6-module-qtquick` * 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/
* Qt 5: `qtbase5-dev qt5-qmake qtdeclarative5-dev qtquickcontrols2-5-dev qml-module-qtquick-controls2 qml-module-qtquick-layouts qml-module-qtquick-window2 qml-module-qtquick2 qtwayland5`
* RedHat/Fedora (RPM Fusion repo required):
* Base Requirements: `openssl-devel SDL2-devel SDL2_ttf-devel ffmpeg-devel libva-devel libvdpau-devel opus-devel pulseaudio-libs-devel alsa-lib-devel libdrm-devel`
* Qt 6 (Recommended): `qt6-qtsvg-devel qt6-qtdeclarative-devel`
* Qt 5: `qt5-qtsvg-devel qt5-qtquickcontrols2-devel`
* Building the Vulkan renderer requires a `libplacebo-dev`/`libplacebo-devel` version of at least v7.349.0 and FFmpeg 6.1 or later.
### Steam Link Build Requirements ### Steam Link Build Requirements
* [Steam Link SDK](https://github.com/ValveSoftware/steamlink-sdk) cloned on your build system * [Steam Link SDK](https://github.com/ValveSoftware/steamlink-sdk) cloned on your build system
@@ -76,17 +68,16 @@ Hosting for Moonlight's Debian and L4T package repositories is graciously provid
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.14. * 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 7-Zip binary directory is on your `%PATH%`. * For Windows builds, use `scripts\build-arch.bat` and `scripts\generate-bundle.bat`. Execute these scripts from the root of the repository within a Qt command prompt. Ensure WiX and 7-Zip binary directories are in your `%PATH%`.
* For macOS builds, use `scripts/generate-dmg.sh`. Execute this script from the root of the repository and ensure Qt's `bin` folder is in your `$PATH`. * For macOS builds, use `scripts/generate-dmg.sh`. Execute this script from the root of the repository and ensure Qt's `bin` folder is in your `$PATH`.
* For Steam Link builds, run `scripts/build-steamlink-app.sh` from the root of the repository. * For Steam Link builds, run `scripts/build-steamlink-app.sh` from the root of the repository.
* To build from the command line for development use on macOS or Linux, run `qmake6 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 `qmake6 "CONFIG+=embedded" moonlight-qt.pro` and build normally. * 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. * This build will lack windowed mode, Discord/Help links, and other features that don't make sense on an embedded device.
* For platforms with poor GPU performance, add `"CONFIG+=gpuslow"` to prefer direct KMSDRM rendering over GL/Vulkan renderers. Direct KMSDRM rendering can use dedicated YUV/RGB conversion and scaling hardware rather than slower GPU shaders for these operations.
## Contribute ## Contribute
1. Fork us 1. Fork us
+2 -18
View File
@@ -5,7 +5,7 @@
<key>CFBundleExecutable</key> <key>CFBundleExecutable</key>
<string>Moonlight</string> <string>Moonlight</string>
<key>CFBundleGetInfoString</key> <key>CFBundleGetInfoString</key>
<string>Stream games and other applications from another PC</string> <string>Stream games from your NVIDIA GameStream-enabled PC</string>
<key>CFBundleIconFile</key> <key>CFBundleIconFile</key>
<string>moonlight</string> <string>moonlight</string>
<key>CFBundleIdentifier</key> <key>CFBundleIdentifier</key>
@@ -17,27 +17,11 @@
<key>CFBundleSignature</key> <key>CFBundleSignature</key>
<string>????</string> <string>????</string>
<key>LSMinimumSystemVersion</key> <key>LSMinimumSystemVersion</key>
<string>11.0.0</string> <string>10.14</string>
<key>NSPrincipalClass</key> <key>NSPrincipalClass</key>
<string>NSApplication</string> <string>NSApplication</string>
<key>NSSupportsAutomaticGraphicsSwitching</key> <key>NSSupportsAutomaticGraphicsSwitching</key>
<true/> <true/>
<key>GCSupportedGameControllers</key>
<array>
<dict>
<key>ProfileName</key>
<string>ExtendedGamepad</string>
</dict>
</array>
<key>GCSupportsControllerUserInteraction</key>
<true/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>NSLocalNetworkUsageDescription</key>
<string>Moonlight uses the local network to connect to your gaming PC for streaming.</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>VERSION</string> <string>VERSION</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
-320
View File
@@ -1,320 +0,0 @@
#include "SDL_compat.h"
#include <SDL_syswm.h>
void* SDLC_Win32_GetHwnd(SDL_Window* window)
{
#ifdef SDL_VIDEO_DRIVER_WINDOWS
SDL_SysWMinfo info;
SDL_VERSION(&info.version);
if (!SDL_GetWindowWMInfo(window, &info)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_GetWindowWMInfo() failed: %s",
SDL_GetError());
return NULL;
}
if (info.subsystem == SDL_SYSWM_WINDOWS) {
return info.info.win.window;
}
#else
(void)window;
#endif
return NULL;
}
void* SDLC_MacOS_GetWindow(SDL_Window* window)
{
#ifdef SDL_VIDEO_DRIVER_COCOA
SDL_SysWMinfo info;
SDL_VERSION(&info.version);
if (!SDL_GetWindowWMInfo(window, &info)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_GetWindowWMInfo() failed: %s",
SDL_GetError());
return NULL;
}
if (info.subsystem == SDL_SYSWM_COCOA) {
return info.info.cocoa.window;
}
#else
(void)window;
#endif
return NULL;
}
void* SDLC_X11_GetDisplay(SDL_Window* window)
{
#ifdef SDL_VIDEO_DRIVER_X11
SDL_SysWMinfo info;
SDL_VERSION(&info.version);
if (!SDL_GetWindowWMInfo(window, &info)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_GetWindowWMInfo() failed: %s",
SDL_GetError());
return NULL;
}
if (info.subsystem == SDL_SYSWM_X11) {
return info.info.x11.display;
}
#else
(void)window;
#endif
return NULL;
}
unsigned long SDLC_X11_GetWindow(SDL_Window* window)
{
#ifdef SDL_VIDEO_DRIVER_X11
SDL_SysWMinfo info;
SDL_VERSION(&info.version);
if (!SDL_GetWindowWMInfo(window, &info)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_GetWindowWMInfo() failed: %s",
SDL_GetError());
return 0;
}
if (info.subsystem == SDL_SYSWM_X11) {
return info.info.x11.window;
}
#else
(void)window;
#endif
return 0;
}
void* SDLC_Wayland_GetDisplay(SDL_Window* window)
{
#ifdef SDL_VIDEO_DRIVER_WAYLAND
SDL_SysWMinfo info;
SDL_VERSION(&info.version);
if (!SDL_GetWindowWMInfo(window, &info)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_GetWindowWMInfo() failed: %s",
SDL_GetError());
return NULL;
}
if (info.subsystem == SDL_SYSWM_WAYLAND) {
return info.info.wl.display;
}
#else
(void)window;
#endif
return NULL;
}
void* SDLC_Wayland_GetSurface(SDL_Window* window)
{
#ifdef SDL_VIDEO_DRIVER_WAYLAND
SDL_SysWMinfo info;
SDL_VERSION(&info.version);
if (!SDL_GetWindowWMInfo(window, &info)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_GetWindowWMInfo() failed: %s",
SDL_GetError());
return NULL;
}
if (info.subsystem == SDL_SYSWM_WAYLAND) {
return info.info.wl.surface;
}
#else
(void)window;
#endif
return NULL;
}
int SDLC_KMSDRM_GetFd(SDL_Window* window)
{
#if defined(SDL_VIDEO_DRIVER_KMSDRM) && SDL_VERSION_ATLEAST(2, 0, 15)
SDL_SysWMinfo info;
SDL_VERSION(&info.version);
if (!SDL_GetWindowWMInfo(window, &info)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_GetWindowWMInfo() failed: %s",
SDL_GetError());
return -1;
}
if (info.subsystem == SDL_SYSWM_KMSDRM) {
return info.info.kmsdrm.drm_fd;
}
#else
(void)window;
#endif
return -1;
}
int SDLC_KMSDRM_GetDevIndex(SDL_Window* window)
{
#if defined(SDL_VIDEO_DRIVER_KMSDRM) && SDL_VERSION_ATLEAST(2, 0, 15)
SDL_SysWMinfo info;
SDL_VERSION(&info.version);
if (!SDL_GetWindowWMInfo(window, &info)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_GetWindowWMInfo() failed: %s",
SDL_GetError());
return -1;
}
if (info.subsystem == SDL_SYSWM_KMSDRM) {
return info.info.kmsdrm.dev_index;
}
#else
(void)window;
#endif
return -1;
}
SDLC_VideoDriver SDLC_GetVideoDriver(void)
{
#if defined(SDL_VIDEO_DRIVER_WINDOWS)
return SDLC_VIDEO_WIN32;
#elif defined(SDL_VIDEO_DRIVER_COCOA)
return SDLC_VIDEO_MACOS;
#else
const char* videoDriver = SDL_GetCurrentVideoDriver();
if (SDL_strcmp(videoDriver, "x11") == 0) {
return SDLC_VIDEO_X11;
}
else if (SDL_strcmp(videoDriver, "wayland") == 0) {
return SDLC_VIDEO_WAYLAND;
}
else if (SDL_strcmp(videoDriver, "kmsdrm") == 0) {
return SDLC_VIDEO_KMSDRM;
}
else {
SDL_assert(0);
return SDLC_VIDEO_UNKNOWN;
}
#endif
}
bool SDLC_IsFullscreen(SDL_Window* window)
{
return !!(SDL_GetWindowFlags(window) & SDL_WINDOW_FULLSCREEN);
}
bool SDLC_IsFullscreenExclusive(SDL_Window* window)
{
return (SDL_GetWindowFlags(window) & SDL_WINDOW_FULLSCREEN_DESKTOP) == SDL_WINDOW_FULLSCREEN;
}
bool SDLC_IsFullscreenDesktop(SDL_Window* window)
{
return (SDL_GetWindowFlags(window) & SDL_WINDOW_FULLSCREEN_DESKTOP) == SDL_WINDOW_FULLSCREEN_DESKTOP;
}
void SDLC_EnterFullscreen(SDL_Window* window, bool exclusive)
{
SDL_SetWindowFullscreen(window, exclusive ? SDL_WINDOW_FULLSCREEN : SDL_WINDOW_FULLSCREEN_DESKTOP);
}
void SDLC_LeaveFullscreen(SDL_Window* window)
{
SDL_SetWindowFullscreen(window, 0);
}
SDL_Window* SDLC_CreateWindowWithFallback(const char *title,
int x, int y, int w, int h,
Uint32 requiredFlags,
Uint32 optionalFlags)
{
SDL_Window* window = SDL_CreateWindow(title, x, y, w, h, requiredFlags | optionalFlags);
if (!window) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Failed to create window with optional flags: %s",
SDL_GetError());
// Try the fallback flags now
window = SDL_CreateWindow(title, x, y, w, h, requiredFlags);
}
return window;
}
void SDLC_FlushWindowEvents(void)
{
SDL_FlushEvent(SDL_WINDOWEVENT);
}
SDL_JoystickID* SDL_GetGamepads(int *count)
{
int numJoysticks = SDL_NumJoysticks();
SDL_JoystickID* ids = SDL_calloc(numJoysticks + 1, sizeof(SDL_JoystickID));
int numGamepads = 0;
for (int i = 0; i < numJoysticks; i++) {
if (SDL_IsGameController(i)) {
ids[numGamepads++] = i;
}
}
if (count != NULL) {
*count = numGamepads;
}
return ids;
}
SDL_JoystickID* SDL_GetJoysticks(int *count)
{
int numJoysticks = SDL_NumJoysticks();
SDL_JoystickID* ids = SDL_calloc(numJoysticks + 1, sizeof(SDL_JoystickID));
for (int i = 0; i < numJoysticks; i++) {
ids[i] = i;
}
if (count != NULL) {
*count = numJoysticks;
}
return ids;
}
SDL_DisplayID * SDL_GetDisplays(int *count)
{
int numDisplays = SDL_GetNumVideoDisplays();
SDL_DisplayID* ids = SDL_calloc(numDisplays + 1, sizeof(SDL_DisplayID));
for (int i = 0; i < numDisplays; i++) {
ids[i] = i;
}
if (count != NULL) {
*count = numDisplays;
}
return ids;
}
const SDL_DisplayMode * SDL_GetWindowFullscreenMode(SDL_Window *window)
{
static SDL_DisplayMode mode;
if (SDL_GetWindowDisplayMode(window, &mode) == 0) {
return &mode;
}
else {
return NULL;
}
}
-303
View File
@@ -1,303 +0,0 @@
//
// Compatibility header for older versions of SDL.
// Include this instead of SDL.h directly.
//
#pragma once
#include <SDL.h>
#include <stdbool.h>
// This is a pure C header for compatibility with SDL.h
#ifdef __cplusplus
extern "C" {
#endif
// These SLDC_* functions and constants are special SDL-like things
// used to abstract certain SDL2 vs SDL3 differences.
void* SDLC_Win32_GetHwnd(SDL_Window* window);
void* SDLC_MacOS_GetWindow(SDL_Window* window);
void* SDLC_X11_GetDisplay(SDL_Window* window);
unsigned long SDLC_X11_GetWindow(SDL_Window* window);
void* SDLC_Wayland_GetDisplay(SDL_Window* window);
void* SDLC_Wayland_GetSurface(SDL_Window* window);
int SDLC_KMSDRM_GetFd(SDL_Window* window);
int SDLC_KMSDRM_GetDevIndex(SDL_Window* window);
typedef enum {
SDLC_VIDEO_UNKNOWN,
SDLC_VIDEO_WIN32,
SDLC_VIDEO_MACOS,
SDLC_VIDEO_X11,
SDLC_VIDEO_WAYLAND,
SDLC_VIDEO_KMSDRM,
} SDLC_VideoDriver;
SDLC_VideoDriver SDLC_GetVideoDriver();
bool SDLC_IsFullscreen(SDL_Window* window);
bool SDLC_IsFullscreenExclusive(SDL_Window* window);
bool SDLC_IsFullscreenDesktop(SDL_Window* window);
void SDLC_EnterFullscreen(SDL_Window* window, bool exclusive);
void SDLC_LeaveFullscreen(SDL_Window* window);
SDL_Window* SDLC_CreateWindowWithFallback(const char *title,
int x, int y, int w, int h,
Uint32 requiredFlags,
Uint32 optionalFlags);
void SDLC_FlushWindowEvents();
#define SDLC_SUCCESS(x) ((x) == 0)
#define SDLC_FAILURE(x) ((x) != 0)
// SDL_FRect wasn't added until 2.0.10
#if !SDL_VERSION_ATLEAST(2, 0, 10)
typedef struct SDL_FRect
{
float x;
float y;
float w;
float h;
} SDL_FRect;
#endif
#ifndef SDL_THREAD_PRIORITY_TIME_CRITICAL
#define SDL_THREAD_PRIORITY_TIME_CRITICAL SDL_THREAD_PRIORITY_HIGH
#endif
#ifndef SDL_HINT_VIDEO_X11_FORCE_EGL
#define SDL_HINT_VIDEO_X11_FORCE_EGL "SDL_VIDEO_X11_FORCE_EGL"
#endif
#ifndef SDL_HINT_KMSDRM_REQUIRE_DRM_MASTER
#define SDL_HINT_KMSDRM_REQUIRE_DRM_MASTER "SDL_KMSDRM_REQUIRE_DRM_MASTER"
#endif
#ifndef SDL_HINT_ALLOW_ALT_TAB_WHILE_GRABBED
#define SDL_HINT_ALLOW_ALT_TAB_WHILE_GRABBED "SDL_ALLOW_ALT_TAB_WHILE_GRABBED"
#endif
#ifndef SDL_HINT_JOYSTICK_HIDAPI_PS4_RUMBLE
#define SDL_HINT_JOYSTICK_HIDAPI_PS4_RUMBLE "SDL_JOYSTICK_HIDAPI_PS4_RUMBLE"
#endif
#ifndef SDL_HINT_JOYSTICK_HIDAPI_PS5_RUMBLE
#define SDL_HINT_JOYSTICK_HIDAPI_PS5_RUMBLE "SDL_JOYSTICK_HIDAPI_PS5_RUMBLE"
#endif
#ifndef SDL_HINT_WINDOWS_USE_D3D9EX
#define SDL_HINT_WINDOWS_USE_D3D9EX "SDL_WINDOWS_USE_D3D9EX"
#endif
#ifndef SDL_HINT_GAMECONTROLLER_USE_BUTTON_LABELS
#define SDL_HINT_GAMECONTROLLER_USE_BUTTON_LABELS "SDL_GAMECONTROLLER_USE_BUTTON_LABELS"
#endif
#ifndef SDL_HINT_MOUSE_RELATIVE_SCALING
#define SDL_HINT_MOUSE_RELATIVE_SCALING "SDL_MOUSE_RELATIVE_SCALING"
#endif
#ifndef SDL_HINT_AUDIO_DEVICE_APP_NAME
#define SDL_HINT_AUDIO_DEVICE_APP_NAME "SDL_AUDIO_DEVICE_APP_NAME"
#endif
#ifndef SDL_HINT_APP_NAME
#define SDL_HINT_APP_NAME "SDL_APP_NAME"
#endif
#ifndef SDL_HINT_MOUSE_AUTO_CAPTURE
#define SDL_HINT_MOUSE_AUTO_CAPTURE "SDL_MOUSE_AUTO_CAPTURE"
#endif
#ifndef SDL_HINT_VIDEO_WAYLAND_EMULATE_MOUSE_WARP
#define SDL_HINT_VIDEO_WAYLAND_EMULATE_MOUSE_WARP "SDL_VIDEO_WAYLAND_EMULATE_MOUSE_WARP"
#endif
// SDL3 renamed hints
#define SDL_HINT_VIDEO_FORCE_EGL SDL_HINT_VIDEO_X11_FORCE_EGL
// Events
#define SDL_EVENT_QUIT SDL_QUIT
#define SDL_EVENT_CLIPBOARD_UPDATE SDL_CLIPBOARDUPDATE
#define SDL_EVENT_GAMEPAD_ADDED SDL_CONTROLLERDEVICEADDED
#define SDL_EVENT_GAMEPAD_REMAPPED SDL_CONTROLLERDEVICEREMAPPED
#define SDL_EVENT_GAMEPAD_REMOVED SDL_CONTROLLERDEVICEREMOVED
#define SDL_EVENT_GAMEPAD_SENSOR_UPDATE SDL_CONTROLLERSENSORUPDATE
#define SDL_EVENT_GAMEPAD_BUTTON_DOWN SDL_CONTROLLERBUTTONDOWN
#define SDL_EVENT_GAMEPAD_BUTTON_UP SDL_CONTROLLERBUTTONUP
#define SDL_EVENT_GAMEPAD_AXIS_MOTION SDL_CONTROLLERAXISMOTION
#define SDL_EVENT_GAMEPAD_TOUCHPAD_DOWN SDL_CONTROLLERTOUCHPADDOWN
#define SDL_EVENT_GAMEPAD_TOUCHPAD_UP SDL_CONTROLLERTOUCHPADUP
#define SDL_EVENT_GAMEPAD_TOUCHPAD_MOTION SDL_CONTROLLERTOUCHPADMOTION
#define SDL_EVENT_JOYSTICK_ADDED SDL_JOYDEVICEADDED
#define SDL_EVENT_JOYSTICK_REMOVED SDL_JOYDEVICEREMOVED
#define SDL_EVENT_JOYSTICK_BATTERY_UPDATED SDL_JOYBATTERYUPDATED
#define SDL_EVENT_FINGER_DOWN SDL_FINGERDOWN
#define SDL_EVENT_FINGER_MOTION SDL_FINGERMOTION
#define SDL_EVENT_FINGER_UP SDL_FINGERUP
#define SDL_EVENT_KEY_DOWN SDL_KEYDOWN
#define SDL_EVENT_KEY_UP SDL_KEYUP
#define SDL_EVENT_MOUSE_BUTTON_DOWN SDL_MOUSEBUTTONDOWN
#define SDL_EVENT_MOUSE_BUTTON_UP SDL_MOUSEBUTTONUP
#define SDL_EVENT_MOUSE_MOTION SDL_MOUSEMOTION
#define SDL_EVENT_MOUSE_WHEEL SDL_MOUSEWHEEL
#define SDL_EVENT_RENDER_DEVICE_RESET SDL_RENDER_DEVICE_RESET
#define SDL_EVENT_RENDER_TARGETS_RESET SDL_RENDER_TARGETS_RESET
#define SDL_EVENT_USER SDL_USEREVENT
#define SDL_EVENT_WINDOW_FOCUS_GAINED SDL_WINDOWEVENT_FOCUS_GAINED
#define SDL_EVENT_WINDOW_FOCUS_LOST SDL_WINDOWEVENT_FOCUS_LOST
#define SDL_EVENT_WINDOW_MOUSE_ENTER SDL_WINDOWEVENT_ENTER
#define SDL_EVENT_WINDOW_MOUSE_LEAVE SDL_WINDOWEVENT_LEAVE
#define SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED SDL_WINDOWEVENT_SIZE_CHANGED
#define SDL_EVENT_WINDOW_SHOWN SDL_WINDOWEVENT_SHOWN
#define SDL_EVENT_WINDOW_DISPLAY_CHANGED SDL_WINDOWEVENT_DISPLAY_CHANGED
#define SDL_BUTTON_MASK(x) SDL_BUTTON(x)
#define gbutton cbutton
#define gaxis caxis
#define gdevice cdevice
#define gsensor csensor
#define gtouchpad ctouchpad
#define fingerID fingerId
#define touchID touchId
#define KEY_DOWN(x) ((x)->state == SDL_PRESSED)
#define KEY_KEY(x) ((x)->keysym.sym)
#define KEY_MOD(x) ((x)->keysym.mod)
#define KEY_SCANCODE(x) ((x)->keysym.scancode)
// Gamepad
#define SDL_INIT_GAMEPAD SDL_INIT_GAMECONTROLLER
#define SDL_GAMEPAD_BUTTON_SOUTH SDL_CONTROLLER_BUTTON_A
#define SDL_GAMEPAD_BUTTON_EAST SDL_CONTROLLER_BUTTON_B
#define SDL_GAMEPAD_BUTTON_WEST SDL_CONTROLLER_BUTTON_X
#define SDL_GAMEPAD_BUTTON_NORTH SDL_CONTROLLER_BUTTON_Y
#define SDL_GAMEPAD_BUTTON_LEFT_SHOULDER SDL_CONTROLLER_BUTTON_LEFTSHOULDER
#define SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER SDL_CONTROLLER_BUTTON_RIGHTSHOULDER
#define SDL_GAMEPAD_BUTTON_DPAD_UP SDL_CONTROLLER_BUTTON_DPAD_UP
#define SDL_GAMEPAD_BUTTON_DPAD_DOWN SDL_CONTROLLER_BUTTON_DPAD_DOWN
#define SDL_GAMEPAD_BUTTON_DPAD_LEFT SDL_CONTROLLER_BUTTON_DPAD_LEFT
#define SDL_GAMEPAD_BUTTON_DPAD_RIGHT SDL_CONTROLLER_BUTTON_DPAD_RIGHT
#define SDL_GAMEPAD_BUTTON_START SDL_CONTROLLER_BUTTON_START
#define SDL_GAMEPAD_BUTTON_TOUCHPAD SDL_CONTROLLER_BUTTON_TOUCHPAD
#define SDL_GAMEPAD_BINDTYPE_AXIS SDL_CONTROLLER_BINDTYPE_AXIS
#define SDL_GAMEPAD_BINDTYPE_BUTTON SDL_CONTROLLER_BINDTYPE_BUTTON
#define SDL_GAMEPAD_BINDTYPE_HAT SDL_CONTROLLER_BINDTYPE_HAT
#define SDL_GAMEPAD_BINDTYPE_NONE SDL_CONTROLLER_BINDTYPE_NONE
#define SDL_GAMEPAD_AXIS_LEFTX SDL_CONTROLLER_AXIS_LEFTX
#define SDL_GAMEPAD_AXIS_LEFTY SDL_CONTROLLER_AXIS_LEFTY
#define SDL_GAMEPAD_AXIS_RIGHTX SDL_CONTROLLER_AXIS_RIGHTX
#define SDL_GAMEPAD_AXIS_RIGHTY SDL_CONTROLLER_AXIS_RIGHTY
#define SDL_GAMEPAD_AXIS_LEFT_TRIGGER SDL_CONTROLLER_AXIS_TRIGGERLEFT
#define SDL_GAMEPAD_AXIS_RIGHT_TRIGGER SDL_CONTROLLER_AXIS_TRIGGERRIGHT
// SDL_OpenGamepad() not defined due to differing semantics
SDL_JoystickID* SDL_GetGamepads(int *count);
#define SDL_OpenGamepad(x) SDL_GameControllerOpen(x)
#define SDL_CloseGamepad(x) SDL_GameControllerClose(x)
#define SDL_GetGamepadMapping(x) SDL_GameControllerMapping(x)
#define SDL_GetGamepadName(x) SDL_GameControllerName(x)
#define SDL_GetGamepadVendor(x) SDL_GameControllerGetVendor(x)
#define SDL_GetGamepadProduct(x) SDL_GameControllerGetProduct(x)
#define SDL_GetGamepadJoystick(x) SDL_GameControllerGetJoystick(x)
#define SDL_GamepadHasButton(x, y) SDL_GameControllerHasButton(x, y)
#define SDL_GamepadHasSensor(x, y) SDL_GameControllerHasSensor(x, y)
#define SDL_SetGamepadPlayerIndex(x, y) SDL_GameControllerSetPlayerIndex(x, y)
#define SDL_GamepadSensorEnabled(x, y) SDL_GameControllerIsSensorEnabled(x, y)
#define SDL_SetGamepadSensorEnabled(x, y, z) SDL_GameControllerSetSensorEnabled(x, y, z)
#define SDL_GetNumGamepadTouchpads(x) SDL_GameControllerGetNumTouchpads(x)
#define SDL_SetGamepadLED(x, r, g, b) SDL_GameControllerSetLED(x, r, g, b)
#define SDL_RumbleGamepad(x, y, z, w) SDL_GameControllerRumble(x, y, z, w)
#define SDL_RumbleGamepadTriggers(x, y, z, w) SDL_GameControllerRumbleTriggers(x, y, z, w)
#define SDL_GetGamepadAxis(x, y) SDL_GameControllerGetAxis(x, y)
#define SDL_GetGamepadType(x) SDL_GameControllerGetType(x)
#define SDL_IsGamepad(x) SDL_IsGameController(x)
#define SDL_GAMEPAD_TYPE_STANDARD SDL_CONTROLLER_TYPE_UNKNOWN
#define SDL_GAMEPAD_TYPE_VIRTUAL SDL_CONTROLLER_TYPE_VIRTUAL
#define SDL_GAMEPAD_TYPE_XBOX360 SDL_CONTROLLER_TYPE_XBOX360
#define SDL_GAMEPAD_TYPE_XBOXONE SDL_CONTROLLER_TYPE_XBOXONE
#define SDL_GAMEPAD_TYPE_PS3 SDL_CONTROLLER_TYPE_PS3
#define SDL_GAMEPAD_TYPE_PS4 SDL_CONTROLLER_TYPE_PS4
#define SDL_GAMEPAD_TYPE_PS5 SDL_CONTROLLER_TYPE_PS5
#define SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_JOYCON_LEFT SDL_CONTROLLER_TYPE_NINTENDO_SWITCH_JOYCON_LEFT
#define SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_JOYCON_PAIR SDL_CONTROLLER_TYPE_NINTENDO_SWITCH_JOYCON_PAIR
#define SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_JOYCON_RIGHT SDL_CONTROLLER_TYPE_NINTENDO_SWITCH_JOYCON_RIGHT
#define SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_PRO SDL_CONTROLLER_TYPE_NINTENDO_SWITCH_PRO
// SDL_OpenGamepad() not defined due to differing semantics
SDL_JoystickID* SDL_GetJoysticks(int *count);
#define SDL_OpenJoystick(x) SDL_JoystickOpen(x)
#define SDL_CloseJoystick(x) SDL_JoystickClose(x)
#define SDL_GetJoystickID(x) SDL_JoystickInstanceID(x)
#define SDL_GetJoystickGUID(x) SDL_JoystickGetGUID(x)
#define SDL_GetJoystickPowerLevel(x) SDL_JoystickCurrentPowerLevel(x)
#define SDL_GetNumJoystickAxes(x) SDL_JoystickNumAxes(x)
#define SDL_GetNumJoystickBalls(x) SDL_JoystickNumBalls(x)
#define SDL_GetNumJoystickButtons(x) SDL_JoystickNumButtons(x)
#define SDL_GetNumJoystickHats(x) SDL_JoystickNumHats(x)
#define SDL_GetJoystickGUIDForID(x) SDL_JoystickGetDeviceGUID(x)
typedef SDL_ControllerAxisEvent SDL_GamepadAxisEvent;
typedef SDL_ControllerButtonEvent SDL_GamepadButtonEvent;
typedef SDL_ControllerSensorEvent SDL_GamepadSensorEvent;
typedef SDL_ControllerTouchpadEvent SDL_GamepadTouchpadEvent;
typedef SDL_ControllerDeviceEvent SDL_GamepadDeviceEvent;
typedef SDL_GameController SDL_Gamepad;
typedef SDL_GameControllerButton SDL_GamepadButton;
// Audio
#define SDL_AUDIO_F32 AUDIO_F32SYS
#define SDL_ResumeAudioDevice(x) SDL_PauseAudioDevice(x, 0)
// Atomics
#define SDL_GetAtomicInt(x) SDL_AtomicGet(x)
#define SDL_SetAtomicInt(x, y) SDL_AtomicSet(x, y)
#define SDL_GetAtomicPointer(x) SDL_AtomicGetPtr(x)
#define SDL_SetAtomicPointer(x, y) SDL_AtomicSetPtr(x, y)
#define SDL_LockSpinlock(x) SDL_AtomicLock(x)
#define SDL_TryLockSpinlock(x) SDL_AtomicTryLock(x)
#define SDL_UnlockSpinlock(x) SDL_AtomicUnlock(x)
typedef SDL_atomic_t SDL_AtomicInt;
// Video
#define SDL_KMOD_CTRL KMOD_CTRL
#define SDL_KMOD_ALT KMOD_ALT
#define SDL_KMOD_SHIFT KMOD_SHIFT
#define SDL_KMOD_GUI KMOD_GUI
#define SDL_WINDOW_HIGH_PIXEL_DENSITY SDL_WINDOW_ALLOW_HIGHDPI
#define SDL_SetWindowFullscreenMode(x, y) SDL_SetWindowDisplayMode(x, y)
#define SDL_GetDisplayForWindow(x) SDL_GetWindowDisplayIndex(x)
#define SDL_GetRenderViewport(x, y) SDL_RenderGetViewport(x, y)
#define SDL_DestroySurface(x) SDL_FreeSurface(x)
#define SDL_RenderTexture(x, y, z, w) SDL_RenderCopy(x, y, z, w)
#define SDL_GetPrimaryDisplay() (0)
#define SDL_CreateSurfaceFrom(w, h, fmt, pixels, pitch) SDL_CreateRGBSurfaceWithFormatFrom(pixels, w, h, SDL_BITSPERPIXEL(fmt), pitch, fmt)
#define SDLC_DEFAULT_RENDER_DRIVER -1
typedef int SDL_DisplayID;
SDL_DisplayID * SDL_GetDisplays(int *count);
const SDL_DisplayMode * SDL_GetWindowFullscreenMode(SDL_Window *window);
// Misc
#define SDL_GetNumLogicalCPUCores() SDL_GetCPUCount()
#define SDL_IOFromConstMem(x, y) SDL_RWFromConstMem(x, y)
#define SDL_SetCurrentThreadPriority(x) SDL_SetThreadPriority(x)
#define SDL_GUIDToString(x, y, z) SDL_JoystickGetGUIDString(x, y, z)
#ifdef __cplusplus
}
#endif
+71 -197
View File
@@ -12,13 +12,10 @@ include(../globaldefs.pri)
# 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
# do this for Windows and Mac (when disable-prebuilts is not defined), # do this for Windows and Mac, since they ship with the Qt runtime.
# since they always ship with the matching build of the Qt runtime. win32|macx {
!disable-prebuilts { CONFIG(release, debug|release) {
win32|macx { CONFIG += qtquickcompiler
CONFIG(release, debug|release) {
CONFIG += qtquickcompiler
}
} }
} }
@@ -51,133 +48,85 @@ win32 {
INCLUDEPATH += $$PWD/../libs/windows/include 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
# Work around a conflict with math.h inclusion between SDL and Qt 6
DEFINES += _USE_MATH_DEFINES
} }
macx:!disable-prebuilts { macx {
INCLUDEPATH += $$PWD/../libs/mac/include INCLUDEPATH += $$PWD/../libs/mac/include
INCLUDEPATH += $$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 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
# QMake doesn't handle framework-style includes correctly on its own
QMAKE_CFLAGS += -F$$PWD/../libs/mac/Frameworks
QMAKE_CXXFLAGS += -F$$PWD/../libs/mac/Frameworks
QMAKE_OBJECTIVE_CFLAGS += -F$$PWD/../libs/mac/Frameworks
} }
unix:if(!macx|disable-prebuilts) { unix:!macx {
CONFIG += link_pkgconfig CONFIG += link_pkgconfig
PKGCONFIG += openssl sdl2 SDL2_ttf PKGCONFIG += openssl sdl2 SDL2_ttf opus
# We have our own optimized libopus.a for Steam Link packagesExist(libavcodec) {
if(!config_SL|disable-prebuilts) { PKGCONFIG += libavcodec libavutil
PKGCONFIG += opus CONFIG += ffmpeg
packagesExist(libva) {
packagesExist(libva-x11) {
CONFIG += libva-x11
}
packagesExist(libva-wayland) {
CONFIG += libva-wayland
}
packagesExist(libva-drm) {
CONFIG += libva-drm
}
CONFIG += libva
}
packagesExist(vdpau) {
CONFIG += libvdpau
}
packagesExist(mmal) {
PKGCONFIG += mmal
CONFIG += mmal
}
packagesExist(libdrm) {
PKGCONFIG += libdrm
CONFIG += libdrm
}
packagesExist(ffnvcodec) {
PKGCONFIG += ffnvcodec
CONFIG += cuda
}
} }
!disable-ffmpeg { packagesExist(wayland-client) {
packagesExist(libavcodec) { DEFINES += HAS_WAYLAND
PKGCONFIG += libavcodec libavutil libswscale PKGCONFIG += wayland-client
CONFIG += ffmpeg }
!disable-libva { packagesExist(x11) {
packagesExist(libva) { DEFINES += HAS_X11
!disable-x11 { PKGCONFIG += x11
packagesExist(libva-x11) {
CONFIG += libva-x11
}
}
!disable-wayland {
packagesExist(libva-wayland) {
CONFIG += libva-wayland
}
}
!disable-libdrm {
packagesExist(libva-drm) {
CONFIG += libva-drm
}
}
CONFIG += libva
}
}
!disable-libvdpau {
packagesExist(vdpau) {
CONFIG += libvdpau
}
}
!disable-mmal {
packagesExist(mmal) {
PKGCONFIG += mmal
CONFIG += mmal
}
}
!disable-libdrm {
packagesExist(libdrm) {
PKGCONFIG += libdrm
CONFIG += libdrm
}
}
!disable-cuda {
packagesExist(ffnvcodec) {
PKGCONFIG += ffnvcodec
CONFIG += cuda
}
}
!disable-libplacebo {
packagesExist(libplacebo) {
PKGCONFIG += libplacebo
CONFIG += libplacebo
}
}
}
!disable-wayland {
packagesExist(wayland-client) {
CONFIG += wayland
PKGCONFIG += wayland-client
}
}
!disable-x11 {
packagesExist(x11) {
DEFINES += HAS_X11
PKGCONFIG += x11
}
}
} }
} }
win32 { win32 {
LIBS += -llibssl -llibcrypto -lSDL2 -lSDL2_ttf -lavcodec -lavutil -lswscale -lopus -ldxgi -ld3d11 -llibplacebo LIBS += -llibssl -llibcrypto -lSDL2 -lSDL2_ttf -lavcodec -lavutil -lopus -ldxgi -ld3d11
CONFIG += ffmpeg libplacebo CONFIG += ffmpeg
} }
win32:!winrt { win32:!winrt {
CONFIG += soundio discord-rpc CONFIG += soundio discord-rpc
} }
macx { macx {
!disable-prebuilts { LIBS += -lssl -lcrypto -lavcodec.59 -lavutil.57 -lopus -framework SDL2 -framework SDL2_ttf
LIBS += -lssl.3 -lcrypto.3 -lavcodec.61 -lavutil.59 -lswscale.8 -lopus -framework SDL2 -framework SDL2_ttf LIBS += -lobjc -framework VideoToolbox -framework AVFoundation -framework CoreVideo -framework CoreGraphics -framework CoreMedia -framework AppKit -framework Metal
CONFIG += discord-rpc
}
LIBS += -lobjc -framework VideoToolbox -framework AVFoundation -framework CoreVideo -framework CoreGraphics -framework CoreMedia -framework AppKit -framework Metal -framework QuartzCore
# For libsoundio # For libsoundio
LIBS += -framework CoreAudio -framework AudioUnit LIBS += -framework CoreAudio -framework AudioUnit
CONFIG += ffmpeg soundio CONFIG += ffmpeg soundio discord-rpc
} }
SOURCES += \ SOURCES += \
SDL_compat.c \
backend/nvaddress.cpp \ backend/nvaddress.cpp \
backend/nvapp.cpp \ backend/nvapp.cpp \
cli/pair.cpp \
main.cpp \ main.cpp \
backend/computerseeker.cpp \ backend/computerseeker.cpp \
backend/identitymanager.cpp \ backend/identitymanager.cpp \
@@ -188,7 +137,6 @@ SOURCES += \
backend/boxartmanager.cpp \ backend/boxartmanager.cpp \
backend/richpresencemanager.cpp \ backend/richpresencemanager.cpp \
cli/commandlineparser.cpp \ cli/commandlineparser.cpp \
cli/listapps.cpp \
cli/quitstream.cpp \ cli/quitstream.cpp \
cli/startstream.cpp \ cli/startstream.cpp \
settings/compatfetcher.cpp \ settings/compatfetcher.cpp \
@@ -215,10 +163,8 @@ SOURCES += \
wm.cpp wm.cpp
HEADERS += \ HEADERS += \
SDL_compat.h \
backend/nvaddress.h \ backend/nvaddress.h \
backend/nvapp.h \ backend/nvapp.h \
cli/pair.h \
settings/compatfetcher.h \ settings/compatfetcher.h \
settings/mappingfetcher.h \ settings/mappingfetcher.h \
utils.h \ utils.h \
@@ -231,7 +177,6 @@ HEADERS += \
backend/boxartmanager.h \ backend/boxartmanager.h \
backend/richpresencemanager.h \ backend/richpresencemanager.h \
cli/commandlineparser.h \ cli/commandlineparser.h \
cli/listapps.h \
cli/quitstream.h \ cli/quitstream.h \
cli/startstream.h \ cli/startstream.h \
settings/streamingpreferences.h \ settings/streamingpreferences.h \
@@ -257,18 +202,16 @@ ffmpeg {
DEFINES += HAVE_FFMPEG DEFINES += HAVE_FFMPEG
SOURCES += \ SOURCES += \
streaming/video/ffmpeg.cpp \ streaming/video/ffmpeg.cpp \
streaming/video/ffmpeg-renderers/genhwaccel.cpp \
streaming/video/ffmpeg-renderers/sdlvid.cpp \ streaming/video/ffmpeg-renderers/sdlvid.cpp \
streaming/video/ffmpeg-renderers/swframemapper.cpp \ streaming/video/ffmpeg-renderers/pacer/pacer.cpp \
streaming/video/ffmpeg-renderers/pacer/pacer.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/genhwaccel.h \
streaming/video/ffmpeg-renderers/sdlvid.h \ streaming/video/ffmpeg-renderers/sdlvid.h \
streaming/video/ffmpeg-renderers/swframemapper.h \ streaming/video/ffmpeg-renderers/pacer/pacer.h \
streaming/video/ffmpeg-renderers/pacer/pacer.h streaming/video/ffmpeg-renderers/pacer/nullthreadedvsyncsource.h
} }
libva { libva {
message(VAAPI renderer selected) message(VAAPI renderer selected)
@@ -290,7 +233,7 @@ libva-wayland {
PKGCONFIG += libva-wayland PKGCONFIG += libva-wayland
DEFINES += HAVE_LIBVA_WAYLAND DEFINES += HAVE_LIBVA_WAYLAND
} }
libva-drm { libva-wayland {
message(VAAPI DRM support enabled) message(VAAPI DRM support enabled)
PKGCONFIG += libva-drm PKGCONFIG += libva-drm
@@ -309,18 +252,6 @@ mmal {
DEFINES += HAVE_MMAL DEFINES += HAVE_MMAL
SOURCES += streaming/video/ffmpeg-renderers/mmal.cpp SOURCES += streaming/video/ffmpeg-renderers/mmal.cpp
HEADERS += streaming/video/ffmpeg-renderers/mmal.h HEADERS += streaming/video/ffmpeg-renderers/mmal.h
# We suppress EGL usage when MMAL is available because MMAL has
# significantly better performance than EGL on the Pi. Setting
# this option allows EGL usage even if built with MMAL support.
#
# It is highly recommended to also build with 'gpuslow' to avoid
# EGL being preferred if direct DRM rendering is available.
allow-egl-with-mmal {
message(Allowing EGL usage with MMAL enabled)
DEFINES += ALLOW_EGL_WITH_MMAL
}
} }
libdrm { libdrm {
message(DRM renderer selected) message(DRM renderer selected)
@@ -331,7 +262,7 @@ libdrm {
linux { linux {
message(Master hooks enabled) message(Master hooks enabled)
SOURCES += masterhook.c masterhook_internal.c SOURCES += masterhook.c
LIBS += -ldl LIBS += -ldl
} }
} }
@@ -341,19 +272,6 @@ cuda {
DEFINES += HAVE_CUDA DEFINES += HAVE_CUDA
SOURCES += streaming/video/ffmpeg-renderers/cuda.cpp SOURCES += streaming/video/ffmpeg-renderers/cuda.cpp
HEADERS += streaming/video/ffmpeg-renderers/cuda.h HEADERS += streaming/video/ffmpeg-renderers/cuda.h
# ffnvcodec uses libdl in cuda_load_functions()/cuda_free_functions()
LIBS += -ldl
}
libplacebo {
message(Vulkan support enabled via libplacebo)
DEFINES += HAVE_LIBPLACEBO_VULKAN
SOURCES += \
streaming/video/ffmpeg-renderers/plvk.cpp \
streaming/video/ffmpeg-renderers/plvk_c.c
HEADERS += \
streaming/video/ffmpeg-renderers/plvk.h
} }
config_EGL { config_EGL {
message(EGL renderer selected) message(EGL renderer selected)
@@ -362,22 +280,12 @@ config_EGL {
DEFINES += HAVE_EGL DEFINES += HAVE_EGL
SOURCES += \ SOURCES += \
streaming/video/ffmpeg-renderers/eglvid.cpp \ streaming/video/ffmpeg-renderers/eglvid.cpp \
streaming/video/ffmpeg-renderers/egl_extensions.cpp \ streaming/video/ffmpeg-renderers/egl_extensions.cpp
streaming/video/ffmpeg-renderers/eglimagefactory.cpp HEADERS += streaming/video/ffmpeg-renderers/eglvid.h
HEADERS += \
streaming/video/ffmpeg-renderers/eglvid.h \
streaming/video/ffmpeg-renderers/eglimagefactory.h
} }
config_SL { config_SL {
message(Steam Link build configuration selected) message(Steam Link build configuration selected)
!disable-prebuilts {
# Link against our NEON-optimized libopus build
LIBS += -L$$PWD/../libs/steamlink/lib
INCLUDEPATH += $$PWD/../libs/steamlink/include
LIBS += -lopus -larmasm -lNE10
}
DEFINES += EMBEDDED_BUILD STEAM_LINK HAVE_SLVIDEO HAVE_SLAUDIO DEFINES += EMBEDDED_BUILD STEAM_LINK HAVE_SLVIDEO HAVE_SLAUDIO
LIBS += -lSLVideo -lSLAudio LIBS += -lSLVideo -lSLAudio
@@ -408,9 +316,7 @@ macx {
message(VideoToolbox renderer selected) message(VideoToolbox renderer selected)
SOURCES += \ SOURCES += \
streaming/video/ffmpeg-renderers/vt_base.mm \ streaming/video/ffmpeg-renderers/vt.mm
streaming/video/ffmpeg-renderers/vt_avsamplelayer.mm \
streaming/video/ffmpeg-renderers/vt_metal.mm
HEADERS += \ HEADERS += \
streaming/video/ffmpeg-renderers/vt.h streaming/video/ffmpeg-renderers/vt.h
@@ -433,35 +339,13 @@ embedded {
DEFINES += EMBEDDED_BUILD DEFINES += EMBEDDED_BUILD
} }
glslow {
message(GL slow build)
DEFINES += GL_IS_SLOW
}
vkslow {
message(Vulkan slow build)
DEFINES += VULKAN_IS_SLOW
}
gpuslow {
message(GPU slow build)
DEFINES += GL_IS_SLOW VULKAN_IS_SLOW
}
wayland {
message(Wayland extensions enabled)
DEFINES += HAS_WAYLAND
SOURCES += streaming/video/ffmpeg-renderers/pacer/waylandvsyncsource.cpp
HEADERS += streaming/video/ffmpeg-renderers/pacer/waylandvsyncsource.h
}
RESOURCES += \ RESOURCES += \
resources.qrc \ resources.qrc \
qml.qrc qml.qrc
TRANSLATIONS += \ TRANSLATIONS += \
languages/qml_zh_CN.ts \ languages/qml_zh_cn.ts \
languages/qml_de.ts \ languages/qml_de.ts \
languages/qml_fr.ts \ languages/qml_fr.ts \
languages/qml_nb_NO.ts \ languages/qml_nb_NO.ts \
@@ -476,18 +360,12 @@ TRANSLATIONS += \
languages/qml_sv.ts \ languages/qml_sv.ts \
languages/qml_tr.ts \ languages/qml_tr.ts \
languages/qml_uk.ts \ languages/qml_uk.ts \
languages/qml_zh_TW.ts \ languages/qml_zh_Hant.ts \
languages/qml_el.ts \ languages/qml_el.ts \
languages/qml_hi.ts \ languages/qml_hi.ts \
languages/qml_it.ts \ languages/qml_it.ts \
languages/qml_pt.ts \ languages/qml_pt.ts \
languages/qml_pt_BR.ts \ languages/qml_pt_BR.ts
languages/qml_pl.ts \
languages/qml_cs.ts \
languages/qml_he.ts \
languages/qml_ckb.ts \
languages/qml_lt.ts \
languages/qml_et.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 =
@@ -576,19 +454,15 @@ macx {
APP_BUNDLE_RESOURCES.files = moonlight.icns APP_BUNDLE_RESOURCES.files = moonlight.icns
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.path = Contents/Frameworks
APP_BUNDLE_PLIST.files = $$OUT_PWD/Info.plist APP_BUNDLE_PLIST.files = $$OUT_PWD/Info.plist
APP_BUNDLE_PLIST.path = Contents APP_BUNDLE_PLIST.path = Contents
QMAKE_BUNDLE_DATA += APP_BUNDLE_RESOURCES APP_BUNDLE_PLIST QMAKE_BUNDLE_DATA += APP_BUNDLE_RESOURCES APP_BUNDLE_FRAMEWORKS APP_BUNDLE_PLIST
!disable-prebuilts { QMAKE_RPATHDIR += @executable_path/../Frameworks
APP_BUNDLE_FRAMEWORKS.files = $$files(../libs/mac/Frameworks/*.framework, true) $$files(../libs/mac/lib/*.dylib, true)
APP_BUNDLE_FRAMEWORKS.path = Contents/Frameworks
QMAKE_BUNDLE_DATA += APP_BUNDLE_FRAMEWORKS
QMAKE_RPATHDIR += @executable_path/../Frameworks
}
} }
VERSION = "$$cat(version.txt)" VERSION = "$$cat(version.txt)"
+8 -19
View File
@@ -6,17 +6,16 @@
#include <QJsonObject> #include <QJsonObject>
AutoUpdateChecker::AutoUpdateChecker(QObject *parent) : AutoUpdateChecker::AutoUpdateChecker(QObject *parent) :
QObject(parent) QObject(parent),
m_Nam(this)
{ {
m_Nam = new QNetworkAccessManager(this);
// Never communicate over HTTP // Never communicate over HTTP
m_Nam->setStrictTransportSecurityEnabled(true); m_Nam.setStrictTransportSecurityEnabled(true);
// Allow HTTP redirects // Allow HTTP redirects
m_Nam->setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy); m_Nam.setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy);
connect(m_Nam, &QNetworkAccessManager::finished, connect(&m_Nam, &QNetworkAccessManager::finished,
this, &AutoUpdateChecker::handleUpdateCheckRequestFinished); this, &AutoUpdateChecker::handleUpdateCheckRequestFinished);
QString currentVersion(VERSION_STR); QString currentVersion(VERSION_STR);
@@ -29,17 +28,12 @@ AutoUpdateChecker::AutoUpdateChecker(QObject *parent) :
void AutoUpdateChecker::start() void AutoUpdateChecker::start()
{ {
if (!m_Nam) {
Q_ASSERT(m_Nam);
return;
}
#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) || defined(STEAM_LINK) || defined(APP_IMAGE) // Only run update checker on platforms without auto-update
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) && QT_VERSION < QT_VERSION_CHECK(5, 15, 1) && !defined(QT_NO_BEARERMANAGEMENT) #if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) && QT_VERSION < QT_VERSION_CHECK(5, 15, 1) && !defined(QT_NO_BEARERMANAGEMENT)
// HACK: Set network accessibility to work around QTBUG-80947 (introduced in Qt 5.14.0 and fixed in Qt 5.15.1) // HACK: Set network accessibility to work around QTBUG-80947 (introduced in Qt 5.14.0 and fixed in Qt 5.15.1)
QT_WARNING_PUSH QT_WARNING_PUSH
QT_WARNING_DISABLE_DEPRECATED QT_WARNING_DISABLE_DEPRECATED
m_Nam->setNetworkAccessible(QNetworkAccessManager::Accessible); m_Nam.setNetworkAccessible(QNetworkAccessManager::Accessible);
QT_WARNING_POP QT_WARNING_POP
#endif #endif
@@ -51,7 +45,7 @@ void AutoUpdateChecker::start()
#else #else
request.setAttribute(QNetworkRequest::HTTP2AllowedAttribute, true); request.setAttribute(QNetworkRequest::HTTP2AllowedAttribute, true);
#endif #endif
m_Nam->get(request); m_Nam.get(request);
#endif #endif
} }
@@ -108,11 +102,6 @@ void AutoUpdateChecker::handleUpdateCheckRequestFinished(QNetworkReply* reply)
{ {
Q_ASSERT(reply->isFinished()); Q_ASSERT(reply->isFinished());
// Delete the QNetworkAccessManager to free resources and
// prevent the bearer plugin from polling in the background.
m_Nam->deleteLater();
m_Nam = nullptr;
if (reply->error() == QNetworkReply::NoError) { if (reply->error() == QNetworkReply::NoError) {
QTextStream stream(reply); QTextStream stream(reply);
@@ -212,7 +201,7 @@ void AutoUpdateChecker::handleUpdateCheckRequestFinished(QNetworkReply* reply)
<< QSysInfo::buildCpuArchitecture() << getPlatform() << QSysInfo::kernelVersion(); << QSysInfo::buildCpuArchitecture() << getPlatform() << QSysInfo::kernelVersion();
} }
else { else {
qWarning() << "Update checking failed with error:" << reply->error(); qWarning() << "Update checking failed with error: " << reply->error();
reply->deleteLater(); reply->deleteLater();
} }
} }
+1 -1
View File
@@ -25,5 +25,5 @@ private:
QString getPlatform(); QString getPlatform();
QVector<int> m_CurrentVersionQuad; QVector<int> m_CurrentVersionQuad;
QNetworkAccessManager* m_Nam; QNetworkAccessManager m_Nam;
}; };
+50 -205
View File
@@ -1,7 +1,7 @@
#include "computermanager.h" #include "computermanager.h"
#include "boxartmanager.h" #include "boxartmanager.h"
#include "nvhttp.h" #include "nvhttp.h"
#include "nvpairingmanager.h" #include "settings/streamingpreferences.h"
#include <Limelight.h> #include <Limelight.h>
#include <QtEndian> #include <QtEndian>
@@ -10,10 +10,7 @@
#include <QThreadPool> #include <QThreadPool>
#include <QCoreApplication> #include <QCoreApplication>
#include <random>
#define SER_HOSTS "hosts" #define SER_HOSTS "hosts"
#define SER_HOSTS_BACKUP "hostsbackup"
class PcMonitorThread : public QThread class PcMonitorThread : public QThread
{ {
@@ -45,7 +42,7 @@ private:
// 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) {
qInfo() << "Found unexpected PC" << newState.name << "looking for" << m_Computer->name; qInfo() << "Found unexpected PC " << newState.name << " looking for " << m_Computer->name;
return false; return false;
} }
@@ -144,43 +141,29 @@ private:
NvComputer* m_Computer; NvComputer* m_Computer;
}; };
ComputerManager::ComputerManager(StreamingPreferences* prefs) ComputerManager::ComputerManager(QObject *parent)
: m_Prefs(prefs), : QObject(parent),
m_PollingRef(0), m_PollingRef(0),
m_MdnsBrowser(nullptr), m_MdnsBrowser(nullptr),
m_CompatFetcher(nullptr), m_CompatFetcher(nullptr)
m_NeedsDelayedFlush(false)
{ {
QSettings settings; QSettings settings;
// If there's a hosts backup copy, we must have failed to commit
// a previous update before exiting. Restore the backup now.
int hosts = settings.beginReadArray(SER_HOSTS_BACKUP);
if (hosts == 0) {
// If there's no host backup, read from the primary location.
settings.endArray();
hosts = settings.beginReadArray(SER_HOSTS);
}
// Inflate our hosts from QSettings // Inflate our hosts from QSettings
int hosts = settings.beginReadArray(SER_HOSTS);
for (int i = 0; i < hosts; i++) { for (int i = 0; i < hosts; i++) {
settings.setArrayIndex(i); settings.setArrayIndex(i);
NvComputer* computer = new NvComputer(settings); NvComputer* computer = new NvComputer(settings);
m_KnownHosts[computer->uuid] = computer; m_KnownHosts[computer->uuid] = computer;
m_LastSerializedHosts[computer->uuid] = *computer;
} }
settings.endArray(); settings.endArray();
// Fetch latest compatibility data asynchronously // Fetch latest compatibility data asynchronously
m_CompatFetcher.start(); m_CompatFetcher.start();
// Start the delayed flush thread to handle saveHosts() calls
m_DelayedFlushThread = new DelayedFlushThread(this);
m_DelayedFlushThread->start();
// To quit in a timely manner, we must block additional requests // To quit in a timely manner, we must block additional requests
// after we receive the aboutToQuit() signal. This is necessary // after we receive the aboutToQuit() signal. This is neccessary
// because NvHTTP uses aboutToQuit() to abort requests in progress // because NvHTTP uses aboutToQuit() to abort requests in progres
// while quitting, however this is a one time signal - additional // while quitting, however this is a one time signal - additional
// requests would not be aborted and block termination. // requests would not be aborted and block termination.
connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this, &ComputerManager::handleAboutToQuit); connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this, &ComputerManager::handleAboutToQuit);
@@ -188,21 +171,6 @@ ComputerManager::ComputerManager(StreamingPreferences* prefs)
ComputerManager::~ComputerManager() ComputerManager::~ComputerManager()
{ {
// Stop the delayed flush thread before acquiring the lock in write mode
// to avoid deadlocking with a flush that needs the lock in read mode.
{
// Wake the delayed flush thread
m_DelayedFlushThread->requestInterruption();
m_DelayedFlushCondition.wakeOne();
// Wait for it to terminate (and finish any pending flush)
m_DelayedFlushThread->wait();
delete m_DelayedFlushThread;
// Delayed flushes should have completed by now
Q_ASSERT(!m_NeedsDelayedFlush);
}
QWriteLocker lock(&m_Lock); QWriteLocker lock(&m_Lock);
// Delete machines that haven't been resolved yet // Delete machines that haven't been resolved yet
@@ -232,80 +200,19 @@ ComputerManager::~ComputerManager()
} }
} }
void DelayedFlushThread::run() {
for (;;) {
// Wait for a delayed flush request or an interruption
{
QMutexLocker locker(&m_ComputerManager->m_DelayedFlushMutex);
while (!QThread::currentThread()->isInterruptionRequested() && !m_ComputerManager->m_NeedsDelayedFlush) {
m_ComputerManager->m_DelayedFlushCondition.wait(&m_ComputerManager->m_DelayedFlushMutex);
}
// Bail without flushing if we woke up for an interruption alone.
// If we have both an interruption and a flush request, do the flush.
if (!m_ComputerManager->m_NeedsDelayedFlush) {
Q_ASSERT(QThread::currentThread()->isInterruptionRequested());
break;
}
// Reset the delayed flush flag to ensure any racing saveHosts() call will set it again
m_ComputerManager->m_NeedsDelayedFlush = false;
// Update the last serialized hosts map under the delayed flush mutex
m_ComputerManager->m_LastSerializedHosts.clear();
for (const NvComputer* computer : m_ComputerManager->m_KnownHosts) {
// Copy the current state of the NvComputer to allow us to check later if we need
// to serialize it again when attribute updates occur.
QReadLocker computerLock(&computer->lock);
m_ComputerManager->m_LastSerializedHosts[computer->uuid] = *computer;
}
}
// Perform the flush
{
QSettings settings;
// First, write to the backup location
settings.beginWriteArray(SER_HOSTS_BACKUP);
{
QReadLocker lock(&m_ComputerManager->m_Lock);
int i = 0;
for (const NvComputer* computer : m_ComputerManager->m_KnownHosts) {
settings.setArrayIndex(i++);
computer->serialize(settings, false);
}
}
settings.endArray();
// Next, write to the primary location
settings.remove(SER_HOSTS);
settings.beginWriteArray(SER_HOSTS);
{
QReadLocker lock(&m_ComputerManager->m_Lock);
int i = 0;
for (const NvComputer* computer : m_ComputerManager->m_KnownHosts) {
settings.setArrayIndex(i++);
computer->serialize(settings, true);
}
}
settings.endArray();
// Finally, delete the backup copy
settings.remove(SER_HOSTS_BACKUP);
}
}
}
void ComputerManager::saveHosts() void ComputerManager::saveHosts()
{ {
Q_ASSERT(m_DelayedFlushThread != nullptr && m_DelayedFlushThread->isRunning()); QSettings settings;
QReadLocker lock(&m_Lock);
// Punt to a worker thread because QSettings on macOS can take ages (> 500 ms) settings.remove(SER_HOSTS);
// to persist our host list to disk (especially when a host has a bunch of apps). settings.beginWriteArray(SER_HOSTS);
QMutexLocker locker(&m_DelayedFlushMutex); int i = 0;
m_NeedsDelayedFlush = true; for (const NvComputer* computer : m_KnownHosts) {
m_DelayedFlushCondition.wakeOne(); settings.setArrayIndex(i++);
computer->serialize(settings);
}
settings.endArray();
} }
QHostAddress ComputerManager::getBestGlobalAddressV6(QVector<QHostAddress> &addresses) QHostAddress ComputerManager::getBestGlobalAddressV6(QVector<QHostAddress> &addresses)
@@ -352,15 +259,16 @@ void ComputerManager::startPolling()
return; return;
} }
if (m_Prefs->enableMdns) { StreamingPreferences prefs;
if (prefs.enableMdns) {
// Start an MDNS query for GameStream hosts // Start an MDNS query for GameStream hosts
m_MdnsServer.reset(new QMdnsEngine::Server()); m_MdnsBrowser = new QMdnsEngine::Browser(&m_MdnsServer, "_nvstream._tcp.local.", &m_MdnsCache);
m_MdnsBrowser = new QMdnsEngine::Browser(m_MdnsServer.data(), "_nvstream._tcp.local.");
connect(m_MdnsBrowser, &QMdnsEngine::Browser::serviceAdded, connect(m_MdnsBrowser, &QMdnsEngine::Browser::serviceAdded,
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, service);
connect(pendingComputer, &MdnsPendingComputer::resolvedHost, connect(pendingComputer, &MdnsPendingComputer::resolvedHost,
this, &ComputerManager::handleMdnsServiceResolved); this, &ComputerManager::handleMdnsServiceResolved);
m_PendingResolution.append(pendingComputer); m_PendingResolution.append(pendingComputer);
@@ -441,19 +349,6 @@ void ComputerManager::handleMdnsServiceResolved(MdnsPendingComputer* computer,
computer->deleteLater(); computer->deleteLater();
} }
void ComputerManager::saveHost(NvComputer *computer)
{
// If no serializable properties changed, don't bother saving hosts
QMutexLocker lock(&m_DelayedFlushMutex);
QReadLocker computerLock(&computer->lock);
if (!m_LastSerializedHosts.value(computer->uuid).isEqualSerialized(*computer)) {
// Queue a request for a delayed flush to QSettings outside of the lock
computerLock.unlock();
lock.unlock();
saveHosts();
}
}
void ComputerManager::handleComputerStateChanged(NvComputer* computer) void ComputerManager::handleComputerStateChanged(NvComputer* computer)
{ {
emit computerStateChanged(computer); emit computerStateChanged(computer);
@@ -463,20 +358,15 @@ void ComputerManager::handleComputerStateChanged(NvComputer* computer)
emit quitAppCompleted(QVariant()); emit quitAppCompleted(QVariant());
} }
// Save updates to this host // Save updated hosts to QSettings
saveHost(computer); saveHosts();
} }
QVector<NvComputer*> ComputerManager::getComputers() QVector<NvComputer*> ComputerManager::getComputers()
{ {
QReadLocker lock(&m_Lock); QReadLocker lock(&m_Lock);
// Return a sorted host list return QVector<NvComputer*>::fromList(m_KnownHosts.values());
auto hosts = QVector<NvComputer*>::fromList(m_KnownHosts.values());
std::stable_sort(hosts.begin(), hosts.end(), [](const NvComputer* host1, const NvComputer* host2) {
return host1->name.toLower() < host2->name.toLower();
});
return hosts;
} }
class DeferredHostDeletionTask : public QRunnable class DeferredHostDeletionTask : public QRunnable
@@ -500,7 +390,7 @@ public:
m_ComputerManager->m_KnownHosts.remove(m_Computer->uuid); m_ComputerManager->m_KnownHosts.remove(m_Computer->uuid);
} }
// Persist the new host list with this computer deleted // 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 entry first. This will stop all polling threads too.
@@ -541,13 +431,16 @@ void ComputerManager::renameHost(NvComputer* computer, QString name)
void ComputerManager::clientSideAttributeUpdated(NvComputer* computer) void ComputerManager::clientSideAttributeUpdated(NvComputer* computer)
{ {
// Persist the change
saveHosts();
// Notify the UI of the state change // Notify the UI of the state change
handleComputerStateChanged(computer); handleComputerStateChanged(computer);
} }
void ComputerManager::handleAboutToQuit() void ComputerManager::handleAboutToQuit()
{ {
QReadLocker lock(&m_Lock); QWriteLocker lock(&m_Lock);
// Interrupt polling threads immediately, so they // Interrupt polling threads immediately, so they
// avoid making additional requests while quitting // avoid making additional requests while quitting
@@ -562,8 +455,7 @@ class PendingPairingTask : public QObject, public QRunnable
public: public:
PendingPairingTask(ComputerManager* computerManager, NvComputer* computer, QString pin) PendingPairingTask(ComputerManager* computerManager, NvComputer* computer, QString pin)
: m_ComputerManager(computerManager), : m_Computer(computer),
m_Computer(computer),
m_Pin(pin) m_Pin(pin)
{ {
connect(this, &PendingPairingTask::pairingCompleted, connect(this, &PendingPairingTask::pairingCompleted,
@@ -583,34 +475,25 @@ private:
switch (result) switch (result)
{ {
case NvPairingManager::PairState::PIN_WRONG: case NvPairingManager::PairState::PIN_WRONG:
emit pairingCompleted(m_Computer, tr("The PIN from the PC didn't match. Please try again.")); emit pairingCompleted(m_Computer, "The PIN from the PC didn't match. Please try again.");
break; break;
case NvPairingManager::PairState::FAILED: case NvPairingManager::PairState::FAILED:
if (m_Computer->currentGameId != 0) { emit pairingCompleted(m_Computer, "Pairing failed. Please try again.");
emit pairingCompleted(m_Computer, tr("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."));
}
else {
emit pairingCompleted(m_Computer, tr("Pairing failed. Please try again."));
}
break; break;
case NvPairingManager::PairState::ALREADY_IN_PROGRESS: case NvPairingManager::PairState::ALREADY_IN_PROGRESS:
emit pairingCompleted(m_Computer, tr("Another pairing attempt is already in progress.")); emit pairingCompleted(m_Computer, "Another pairing attempt is already in progress.");
break; break;
case NvPairingManager::PairState::PAIRED: case NvPairingManager::PairState::PAIRED:
// Persist the newly pinned server certificate for this host
m_ComputerManager->saveHost(m_Computer);
emit pairingCompleted(m_Computer, nullptr); emit pairingCompleted(m_Computer, nullptr);
break; break;
} }
} catch (const GfeHttpResponseException& e) { } catch (const GfeHttpResponseException& e) {
emit pairingCompleted(m_Computer, tr("GeForce Experience returned error: %1").arg(e.toQString())); emit pairingCompleted(m_Computer, "GeForce Experience returned error: " + e.toQString());
} catch (const QtNetworkReplyException& e) { } catch (const QtNetworkReplyException& e) {
emit pairingCompleted(m_Computer, e.toQString()); emit pairingCompleted(m_Computer, e.toQString());
} }
} }
ComputerManager* m_ComputerManager;
NvComputer* m_Computer; NvComputer* m_Computer;
QString m_Pin; QString m_Pin;
}; };
@@ -697,10 +580,9 @@ void ComputerManager::stopPollingAsync()
m_PendingResolution.removeFirst(); m_PendingResolution.removeFirst();
} }
// Delete the browser and server to stop discovery and refresh polling // Delete the browser to stop discovery
delete m_MdnsBrowser; delete m_MdnsBrowser;
m_MdnsBrowser = nullptr; m_MdnsBrowser = nullptr;
m_MdnsServer.reset();
// 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) { for (ComputerPollingEntry* entry : m_PollEntries) {
@@ -710,8 +592,8 @@ void ComputerManager::stopPollingAsync()
void ComputerManager::addNewHostManually(QString address) void ComputerManager::addNewHostManually(QString address)
{ {
QUrl url = QUrl::fromUserInput("moonlight://" + address); QUrl url = QUrl::fromUserInput(address);
if (url.isValid() && !url.host().isEmpty() && url.scheme() == "moonlight") { if (url.isValid() && !url.host().isEmpty()) {
// If there wasn't a port specified, use the default // If there wasn't a port specified, use the default
addNewHost(NvAddress(url.host(), url.port(DEFAULT_HTTP_PORT)), false); addNewHost(NvAddress(url.host(), url.port(DEFAULT_HTTP_PORT)), false);
} }
@@ -729,15 +611,12 @@ public:
: m_ComputerManager(computerManager), : m_ComputerManager(computerManager),
m_Address(address), m_Address(address),
m_MdnsIpv6Address(mdnsIpv6Address), m_MdnsIpv6Address(mdnsIpv6Address),
m_Mdns(mdns), m_Mdns(mdns)
m_AboutToQuit(false)
{ {
connect(this, &PendingAddTask::computerAddCompleted, connect(this, &PendingAddTask::computerAddCompleted,
computerManager, &ComputerManager::computerAddCompleted); computerManager, &ComputerManager::computerAddCompleted);
connect(this, &PendingAddTask::computerStateChanged, connect(this, &PendingAddTask::computerStateChanged,
computerManager, &ComputerManager::handleComputerStateChanged); computerManager, &ComputerManager::handleComputerStateChanged);
connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit,
this, &PendingAddTask::handleAboutToQuit);
} }
signals: signals:
@@ -746,20 +625,9 @@ signals:
void computerStateChanged(NvComputer* computer); void computerStateChanged(NvComputer* computer);
private: private:
void handleAboutToQuit()
{
m_AboutToQuit = true;
}
QString fetchServerInfo(NvHTTP& http) QString fetchServerInfo(NvHTTP& http)
{ {
QString serverInfo; QString serverInfo;
// Do nothing if we're quitting
if (m_AboutToQuit) {
return QString();
}
try { try {
// There's a race condition between GameStream servers reporting presence over // There's a race condition between GameStream servers reporting presence over
// mDNS and the HTTPS server being ready to respond to our queries. To work // mDNS and the HTTPS server being ready to respond to our queries. To work
@@ -782,9 +650,10 @@ private:
return serverInfo; return serverInfo;
} catch (...) { } catch (...) {
if (!m_Mdns) { if (!m_Mdns) {
unsigned int portTestResult; StreamingPreferences prefs;
int portTestResult;
if (m_ComputerManager->m_Prefs->detectNetworkBlocking) { if (prefs.detectNetworkBlocking) {
// We failed to connect to the specified PC. Let's test to make sure this network // 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. // isn't blocking Moonlight, so we can tell the user about it.
portTestResult = LiTestClientConnectivity("qt.conntest.moonlight-stream.org", 443, portTestResult = LiTestClientConnectivity("qt.conntest.moonlight-stream.org", 443,
@@ -880,31 +749,16 @@ private:
hostAddress.isInSubnet(QHostAddress("192.168.0.0"), 16); hostAddress.isInSubnet(QHostAddress("192.168.0.0"), 16);
{ {
// Check if this PC already exists using opportunistic read lock // Check if this PC already exists
m_ComputerManager->m_Lock.lockForRead(); QWriteLocker lock(&m_ComputerManager->m_Lock);
NvComputer* existingComputer = m_ComputerManager->m_KnownHosts.value(newComputer->uuid); NvComputer* existingComputer = m_ComputerManager->m_KnownHosts.value(newComputer->uuid);
// If it doesn't already exist, convert to a write lock in preparation for updating.
//
// NB: ComputerManager's lock protects the host map itself, not the elements inside.
// Those are protected by their individual locks. Since we only mutate the map itself
// when the PC doesn't exist, we need the lock in write-mode for that case only.
if (existingComputer == nullptr) {
m_ComputerManager->m_Lock.unlock();
m_ComputerManager->m_Lock.lockForWrite();
// Since we had to unlock to lock for write, someone could have raced and added
// this PC before us. We have to check again whether it already exists.
existingComputer = m_ComputerManager->m_KnownHosts.value(newComputer->uuid);
}
if (existingComputer != nullptr) { if (existingComputer != nullptr) {
// Fold it into the existing PC // Fold it into the existing PC
bool changed = existingComputer->update(*newComputer); bool changed = existingComputer->update(*newComputer);
delete newComputer; delete newComputer;
// Drop the lock before notifying // Drop the lock before notifying
m_ComputerManager->m_Lock.unlock(); lock.unlock();
// For non-mDNS clients, let them know it succeeded // For non-mDNS clients, let them know it succeeded
if (!m_Mdns) { if (!m_Mdns) {
@@ -925,15 +779,17 @@ private:
m_ComputerManager->startPollingComputer(newComputer); m_ComputerManager->startPollingComputer(newComputer);
// Drop the lock before notifying // Drop the lock before notifying
m_ComputerManager->m_Lock.unlock(); lock.unlock();
// If this wasn't added via mDNS but it is a RFC 1918 IPv4 address and not a VPN, // 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. // go ahead and do the STUN request now to populate an external address.
if (!m_Mdns && addressIsSiteLocalV4 && newComputer->getActiveAddressReachability() != NvComputer::RI_VPN) { if (!m_Mdns && addressIsSiteLocalV4 && !newComputer->isReachableOverVpn()) {
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) {
lock.relock();
newComputer->setRemoteAddress(QHostAddress(qFromBigEndian(addr))); newComputer->setRemoteAddress(QHostAddress(qFromBigEndian(addr)));
lock.unlock();
} }
else { else {
qWarning() << "STUN failed to get WAN address:" << err; qWarning() << "STUN failed to get WAN address:" << err;
@@ -955,7 +811,6 @@ private:
NvAddress m_Address; NvAddress m_Address;
NvAddress m_MdnsIpv6Address; NvAddress m_MdnsIpv6Address;
bool m_Mdns; bool m_Mdns;
bool m_AboutToQuit;
}; };
void ComputerManager::addNewHost(NvAddress address, bool mdns, NvAddress mdnsIpv6Address) void ComputerManager::addNewHost(NvAddress address, bool mdns, NvAddress mdnsIpv6Address)
@@ -966,14 +821,4 @@ void ComputerManager::addNewHost(NvAddress address, bool mdns, NvAddress mdnsIpv
QThreadPool::globalInstance()->start(addTask); QThreadPool::globalInstance()->start(addTask);
} }
// TODO: Use QRandomGenerator when we drop Qt 5.9 support
QString ComputerManager::generatePinString()
{
std::uniform_int_distribution<int> dist(0, 9999);
std::random_device rd;
std::mt19937 engine(rd());
return QString::asprintf("%04u", dist(engine));
}
#include "computermanager.moc" #include "computermanager.moc"
+12 -71
View File
@@ -1,7 +1,7 @@
#pragma once #pragma once
#include "nvcomputer.h" #include "nvcomputer.h"
#include "settings/streamingpreferences.h" #include "nvpairingmanager.h"
#include "settings/compatfetcher.h" #include "settings/compatfetcher.h"
#include <qmdnsengine/server.h> #include <qmdnsengine/server.h>
@@ -15,38 +15,17 @@
#include <QSettings> #include <QSettings>
#include <QRunnable> #include <QRunnable>
#include <QTimer> #include <QTimer>
#include <QMutex>
#include <QWaitCondition>
class ComputerManager;
class DelayedFlushThread : public QThread
{
Q_OBJECT
public:
DelayedFlushThread(ComputerManager* cm)
: m_ComputerManager(cm)
{
setObjectName("CM Delayed Flush Thread");
}
void run();
private:
ComputerManager* m_ComputerManager;
};
class MdnsPendingComputer : public QObject class MdnsPendingComputer : public QObject
{ {
Q_OBJECT Q_OBJECT
public: public:
explicit MdnsPendingComputer(const QSharedPointer<QMdnsEngine::Server> server, explicit MdnsPendingComputer(QMdnsEngine::Server* server,
const QMdnsEngine::Service& service) const QMdnsEngine::Service& service)
: m_Hostname(service.hostname()), : m_Hostname(service.hostname()),
m_Port(service.port()), m_Port(service.port()),
m_ServerWeak(server), m_Server(server),
m_Resolver(nullptr) m_Resolver(nullptr)
{ {
// Start resolving // Start resolving
@@ -72,15 +51,9 @@ private slots:
void handleResolvedTimeout() void handleResolvedTimeout()
{ {
if (m_Addresses.isEmpty()) { if (m_Addresses.isEmpty()) {
if (m_Retries-- > 0) { // Try again
// Try again qInfo() << "Resolving" << hostname() << "timed out. Retrying...";
qInfo() << "Resolving" << hostname() << "timed out. Retrying..."; resolve();
resolve();
}
else {
qWarning() << "Giving up on resolving" << hostname() << "after repeated failures";
cleanup();
}
} }
else { else {
Q_ASSERT(!m_Addresses.isEmpty()); Q_ASSERT(!m_Addresses.isEmpty());
@@ -98,29 +71,10 @@ signals:
void resolvedHost(MdnsPendingComputer*,QVector<QHostAddress>&); void resolvedHost(MdnsPendingComputer*,QVector<QHostAddress>&);
private: private:
void cleanup()
{
// Delete our resolver, so we're guaranteed that nothing is referencing m_Server.
delete m_Resolver;
m_Resolver = nullptr;
// Now delete our strong reference that we held on behalf of m_Resolver.
// The server may be destroyed after we make this call.
m_Server.reset();
}
void resolve() void resolve()
{ {
// Clean up any existing resolver object and server references delete m_Resolver;
cleanup(); m_Resolver = new QMdnsEngine::Resolver(m_Server, m_Hostname);
// Re-acquire a strong reference if the server still exists.
m_Server = m_ServerWeak.toStrongRef();
if (!m_Server) {
return;
}
m_Resolver = new QMdnsEngine::Resolver(m_Server.data(), m_Hostname);
connect(m_Resolver, &QMdnsEngine::Resolver::resolved, connect(m_Resolver, &QMdnsEngine::Resolver::resolved,
this, &MdnsPendingComputer::handleResolvedAddress); this, &MdnsPendingComputer::handleResolvedAddress);
QTimer::singleShot(2000, this, &MdnsPendingComputer::handleResolvedTimeout); QTimer::singleShot(2000, this, &MdnsPendingComputer::handleResolvedTimeout);
@@ -128,11 +82,9 @@ private:
QByteArray m_Hostname; QByteArray m_Hostname;
uint16_t m_Port; uint16_t m_Port;
QWeakPointer<QMdnsEngine::Server> m_ServerWeak; QMdnsEngine::Server* m_Server;
QSharedPointer<QMdnsEngine::Server> m_Server;
QMdnsEngine::Resolver* m_Resolver; QMdnsEngine::Resolver* m_Resolver;
QVector<QHostAddress> m_Addresses; QVector<QHostAddress> m_Addresses;
int m_Retries = 10;
}; };
class ComputerPollingEntry class ComputerPollingEntry
@@ -214,11 +166,9 @@ class ComputerManager : public QObject
friend class DeferredHostDeletionTask; friend class DeferredHostDeletionTask;
friend class PendingAddTask; friend class PendingAddTask;
friend class PendingPairingTask;
friend class DelayedFlushThread;
public: public:
explicit ComputerManager(StreamingPreferences* prefs); explicit ComputerManager(QObject *parent = nullptr);
virtual ~ComputerManager(); virtual ~ComputerManager();
@@ -230,8 +180,6 @@ public:
void addNewHost(NvAddress address, bool mdns, NvAddress mdnsIpv6Address = NvAddress()); void addNewHost(NvAddress address, bool mdns, NvAddress mdnsIpv6Address = NvAddress());
QString generatePinString();
void pairHost(NvComputer* computer, QString pin); void pairHost(NvComputer* computer, QString pin);
void quitRunningApp(NvComputer* computer); void quitRunningApp(NvComputer* computer);
@@ -264,24 +212,17 @@ private slots:
private: private:
void saveHosts(); void saveHosts();
void saveHost(NvComputer* computer);
QHostAddress getBestGlobalAddressV6(QVector<QHostAddress>& addresses); QHostAddress getBestGlobalAddressV6(QVector<QHostAddress>& addresses);
void startPollingComputer(NvComputer* computer); void startPollingComputer(NvComputer* computer);
StreamingPreferences* m_Prefs;
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, ComputerPollingEntry*> m_PollEntries;
QHash<QString, NvComputer> m_LastSerializedHosts; // Protected by m_DelayedFlushMutex QMdnsEngine::Server m_MdnsServer;
QSharedPointer<QMdnsEngine::Server> m_MdnsServer;
QMdnsEngine::Browser* m_MdnsBrowser; QMdnsEngine::Browser* m_MdnsBrowser;
QMdnsEngine::Cache m_MdnsCache;
QVector<MdnsPendingComputer*> m_PendingResolution; QVector<MdnsPendingComputer*> m_PendingResolution;
CompatFetcher m_CompatFetcher; CompatFetcher m_CompatFetcher;
DelayedFlushThread* m_DelayedFlushThread;
QMutex m_DelayedFlushMutex; // Lock ordering: Must never be acquired while holding NvComputer lock
QWaitCondition m_DelayedFlushCondition;
bool m_NeedsDelayedFlush;
}; };
-7
View File
@@ -6,13 +6,6 @@ ComputerSeeker::ComputerSeeker(ComputerManager *manager, QString computerName, Q
: QObject(parent), m_ComputerManager(manager), m_ComputerName(computerName), : QObject(parent), m_ComputerManager(manager), m_ComputerName(computerName),
m_TimeoutTimer(new QTimer(this)) m_TimeoutTimer(new QTimer(this))
{ {
// If we know this computer, send a WOL packet to wake it up in case it is asleep.
for (NvComputer * computer: m_ComputerManager->getComputers()) {
if (this->matchComputer(computer)) {
computer->wake();
}
}
m_TimeoutTimer->setSingleShot(true); m_TimeoutTimer->setSingleShot(true);
connect(m_TimeoutTimer, &QTimer::timeout, connect(m_TimeoutTimer, &QTimer::timeout,
this, &ComputerSeeker::onTimeout); this, &ComputerSeeker::onTimeout);
+2 -10
View File
@@ -95,14 +95,6 @@ void IdentityManager::createCredentials(QSettings& settings)
BIO_free(biokey); BIO_free(biokey);
BIO_free(biocert); BIO_free(biocert);
// Check that the new keypair is valid before persisting it
if (getSslCertificate().isNull()) {
qFatal("Newly generated certificate is unreadable");
}
if (getSslKey().isNull()) {
qFatal("Newly generated private key is unreadable");
}
settings.setValue(SER_CERT, m_CachedPemCert); settings.setValue(SER_CERT, m_CachedPemCert);
settings.setValue(SER_KEY, m_CachedPrivateKey); settings.setValue(SER_KEY, m_CachedPrivateKey);
@@ -131,10 +123,10 @@ IdentityManager::IdentityManager()
// We should have valid credentials now. If not, we're screwed // We should have valid credentials now. If not, we're screwed
if (getSslCertificate().isNull()) { if (getSslCertificate().isNull()) {
qFatal("Certificate is unreadable"); qFatal("Newly generated certificate is unreadable");
} }
if (getSslKey().isNull()) { if (getSslKey().isNull()) {
qFatal("Private key is unreadable"); qFatal("Newly generated private key is unreadable");
} }
} }
+54 -160
View File
@@ -21,7 +21,6 @@
#define SER_APPLIST "apps" #define SER_APPLIST "apps"
#define SER_SRVCERT "srvcert" #define SER_SRVCERT "srvcert"
#define SER_CUSTOMNAME "customname" #define SER_CUSTOMNAME "customname"
#define SER_NVIDIASOFTWARE "nvidiasw"
NvComputer::NvComputer(QSettings& settings) NvComputer::NvComputer(QSettings& settings)
{ {
@@ -38,7 +37,6 @@ NvComputer::NvComputer(QSettings& settings)
this->manualAddress = NvAddress(settings.value(SER_MANUALADDR).toString(), this->manualAddress = NvAddress(settings.value(SER_MANUALADDR).toString(),
settings.value(SER_MANUALPORT, QVariant(DEFAULT_HTTP_PORT)).toUInt()); 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());
this->isNvidiaServerSoftware = settings.value(SER_NVIDIASOFTWARE).toBool();
int appCount = settings.beginReadArray(SER_APPLIST); int appCount = settings.beginReadArray(SER_APPLIST);
this->appList.reserve(appCount); this->appList.reserve(appCount);
@@ -67,14 +65,12 @@ NvComputer::NvComputer(QSettings& settings)
void NvComputer::setRemoteAddress(QHostAddress address) void NvComputer::setRemoteAddress(QHostAddress address)
{ {
QWriteLocker lock(&this->lock);
Q_ASSERT(this->externalPort != 0); Q_ASSERT(this->externalPort != 0);
this->remoteAddress = NvAddress(address, this->externalPort); this->remoteAddress = NvAddress(address, this->externalPort);
} }
void NvComputer::serialize(QSettings& settings, bool serializeApps) const void NvComputer::serialize(QSettings& settings) const
{ {
QReadLocker lock(&this->lock); QReadLocker lock(&this->lock);
@@ -91,10 +87,9 @@ void NvComputer::serialize(QSettings& settings, bool serializeApps) const
settings.setValue(SER_MANUALADDR, manualAddress.address()); settings.setValue(SER_MANUALADDR, manualAddress.address());
settings.setValue(SER_MANUALPORT, manualAddress.port()); settings.setValue(SER_MANUALPORT, manualAddress.port());
settings.setValue(SER_SRVCERT, serverCert.toPem()); settings.setValue(SER_SRVCERT, serverCert.toPem());
settings.setValue(SER_NVIDIASOFTWARE, isNvidiaServerSoftware);
// Avoid deleting an existing applist if we couldn't get one // Avoid deleting an existing applist if we couldn't get one
if (!appList.isEmpty() && serializeApps) { if (!appList.isEmpty()) {
settings.remove(SER_APPLIST); settings.remove(SER_APPLIST);
settings.beginWriteArray(SER_APPLIST); settings.beginWriteArray(SER_APPLIST);
for (int i = 0; i < appList.count(); i++) { for (int i = 0; i < appList.count(); i++) {
@@ -105,21 +100,6 @@ void NvComputer::serialize(QSettings& settings, bool serializeApps) const
} }
} }
bool NvComputer::isEqualSerialized(const NvComputer &that) const
{
return this->name == that.name &&
this->hasCustomName == that.hasCustomName &&
this->uuid == that.uuid &&
this->macAddress == that.macAddress &&
this->localAddress == that.localAddress &&
this->remoteAddress == that.remoteAddress &&
this->ipv6Address == that.ipv6Address &&
this->manualAddress == that.manualAddress &&
this->serverCert == that.serverCert &&
this->isNvidiaServerSoftware == that.isNvidiaServerSoftware &&
this->appList == that.appList;
}
void NvComputer::sortAppList() void NvComputer::sortAppList()
{ {
std::stable_sort(appList.begin(), appList.end(), [](const NvApp& app1, const NvApp& app2) { std::stable_sort(appList.begin(), appList.end(), [](const NvApp& app1, const NvApp& app2) {
@@ -151,8 +131,7 @@ NvComputer::NvComputer(NvHTTP& http, QString serverInfo)
this->serverCodecModeSupport = codecSupport.toInt(); this->serverCodecModeSupport = codecSupport.toInt();
} }
else { else {
// Assume H.264 is always supported this->serverCodecModeSupport = 0;
this->serverCodecModeSupport = SCM_H264;
} }
QString maxLumaPixelsHEVC = NvHTTP::getXmlString(serverInfo, "MaxLumaPixelsHEVC"); QString maxLumaPixelsHEVC = NvHTTP::getXmlString(serverInfo, "MaxLumaPixelsHEVC");
@@ -166,8 +145,8 @@ NvComputer::NvComputer(NvHTTP& http, QString serverInfo)
this->displayModes = NvHTTP::getDisplayModeList(serverInfo); this->displayModes = NvHTTP::getDisplayModeList(serverInfo);
std::stable_sort(this->displayModes.begin(), this->displayModes.end(), std::stable_sort(this->displayModes.begin(), this->displayModes.end(),
[](const NvDisplayMode& mode1, const NvDisplayMode& mode2) { [](const NvDisplayMode& mode1, const NvDisplayMode& mode2) {
return (uint64_t)mode1.width * mode1.height * mode1.refreshRate < return mode1.width * mode1.height * mode1.refreshRate <
(uint64_t)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 // We can get an IPv4 loopback address if we're using the GS IPv6 Forwarder
@@ -185,7 +164,7 @@ NvComputer::NvComputer(NvHTTP& http, QString serverInfo)
// to support dynamic HTTP WAN ports without requiring the user to manually enter the port. // to support dynamic HTTP WAN ports without requiring the user to manually enter the port.
QString remotePortStr = NvHTTP::getXmlString(serverInfo, "ExternalPort"); QString remotePortStr = NvHTTP::getXmlString(serverInfo, "ExternalPort");
if (remotePortStr.isEmpty() || (this->externalPort = remotePortStr.toUShort()) == 0) { if (remotePortStr.isEmpty() || (this->externalPort = remotePortStr.toUShort()) == 0) {
this->externalPort = http.httpPort(); this->externalPort = DEFAULT_HTTP_PORT;
} }
QString remoteAddress = NvHTTP::getXmlString(serverInfo, "ExternalIP"); QString remoteAddress = NvHTTP::getXmlString(serverInfo, "ExternalIP");
@@ -196,11 +175,6 @@ NvComputer::NvComputer(NvHTTP& http, QString serverInfo)
this->remoteAddress = NvAddress(); this->remoteAddress = NvAddress();
} }
// Real Nvidia host software (GeForce Experience and RTX Experience) both use the 'Mjolnir'
// codename in the state field and no version of Sunshine does. We can use this to bypass
// some assumptions about Nvidia hardware that don't apply to Sunshine hosts.
this->isNvidiaServerSoftware = NvHTTP::getXmlString(serverInfo, "state").contains("MJOLNIR");
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);
@@ -213,52 +187,40 @@ NvComputer::NvComputer(NvHTTP& http, QString serverInfo)
this->isSupportedServerVersion = CompatFetcher::isGfeVersionSupported(this->gfeVersion); this->isSupportedServerVersion = CompatFetcher::isGfeVersionSupported(this->gfeVersion);
} }
bool NvComputer::wake() const bool NvComputer::wake()
{ {
QByteArray wolPayload; if (state == NvComputer::CS_ONLINE) {
qWarning() << name << "is already online";
{ return true;
QReadLocker readLocker(&lock);
if (state == NvComputer::CS_ONLINE) {
qWarning() << name << "is already online";
return true;
}
if (macAddress.isEmpty()) {
qWarning() << name << "has no MAC address stored";
return false;
}
// Create the WoL payload
wolPayload.append(QByteArray::fromHex("FFFFFFFFFFFF"));
for (int i = 0; i < 16; i++) {
wolPayload.append(macAddress);
}
Q_ASSERT(wolPayload.size() == 102);
} }
// Ports used as-is if (macAddress.isEmpty()) {
const quint16 STATIC_WOL_PORTS[] = { qWarning() << name << "has no MAC address stored";
return false;
}
const quint16 WOL_PORTS[] = {
9, // Standard WOL port (privileged port) 9, // Standard WOL port (privileged port)
47998, 47999, 48000, 48002, 48010, // Ports opened by GFE
47009, // Port opened by Moonlight Internet Hosting Tool for WoL (non-privileged port) 47009, // Port opened by Moonlight Internet Hosting Tool for WoL (non-privileged port)
}; };
// Ports offset by the HTTP base port for hosts using alternate ports // Create the WoL payload
const quint16 DYNAMIC_WOL_PORTS[] = { QByteArray wolPayload;
47998, 47999, 48000, 48002, 48010, // Ports opened by GFE wolPayload.append(QByteArray::fromHex("FFFFFFFFFFFF"));
}; for (int i = 0; i < 16; i++) {
wolPayload.append(macAddress);
}
Q_ASSERT(wolPayload.count() == 102);
// 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.
QMap<QString, quint16> addressMap; QVector<QString> addressList;
QSet<quint16> basePortSet;
for (const NvAddress& addr : uniqueAddresses()) { for (const NvAddress& addr : uniqueAddresses()) {
addressMap.insert(addr.address(), addr.port()); addressList.append(addr.address());
basePortSet.insert(addr.port());
} }
addressMap.insert("255.255.255.255", 0); addressList.append("255.255.255.255");
// Try to broadcast on all available NICs // Try to broadcast on all available NICs
for (const QNetworkInterface& nic : QNetworkInterface::allInterfaces()) { for (const QNetworkInterface& nic : QNetworkInterface::allInterfaces()) {
@@ -277,42 +239,31 @@ bool NvComputer::wake() const
// Skip IPv6 which doesn't support broadcast // Skip IPv6 which doesn't support broadcast
if (!addr.broadcast().isNull()) { if (!addr.broadcast().isNull()) {
addressMap.insert(addr.broadcast().toString(), 0); addressList.append(addr.broadcast().toString());
} }
} }
if (!allNodesMulticast.scopeId().isEmpty()) { if (!allNodesMulticast.scopeId().isEmpty()) {
addressMap.insert(allNodesMulticast.toString(), 0); addressList.append(allNodesMulticast.toString());
} }
} }
// Try all unique address strings or host names // Try all unique address strings or host names
bool success = false; bool success = false;
for (auto i = addressMap.constBegin(); i != addressMap.constEnd(); i++) { for (QString& addressString : addressList) {
QHostAddress literalAddress; QHostInfo hostInfo = QHostInfo::fromName(addressString);
QList<QHostAddress> addressList;
// If this is an IPv4/IPv6 literal, don't use QHostInfo::fromName() because that will if (hostInfo.error() != QHostInfo::NoError) {
// try to perform a reverse DNS lookup that leads to delays sending WoL packets. qWarning() << "Error resolving" << addressString << ":" << hostInfo.errorString();
if (literalAddress.setAddress(i.key())) { continue;
addressList.append(literalAddress);
}
else {
QHostInfo hostInfo = QHostInfo::fromName(i.key());
if (hostInfo.error() != QHostInfo::NoError) {
qWarning() << "Error resolving" << i.key() << ":" << hostInfo.errorString();
continue;
}
addressList.append(hostInfo.addresses());
} }
// Try all IP addresses that this string resolves to // Try all IP addresses that this string resolves to
for (QHostAddress& address : addressList) { for (QHostAddress& address : hostInfo.addresses()) {
QUdpSocket sock; QUdpSocket sock;
// Send to all static ports // Send to all ports
for (quint16 port : STATIC_WOL_PORTS) { for (quint16 port : WOL_PORTS) {
if (sock.writeDatagram(wolPayload, address, port)) { if (sock.writeDatagram(wolPayload, address, port)) {
qInfo().nospace().noquote() << "Sent WoL packet to " << name << " via " << address.toString() << ":" << port; qInfo().nospace().noquote() << "Sent WoL packet to " << name << " via " << address.toString() << ":" << port;
success = true; success = true;
@@ -321,59 +272,24 @@ bool NvComputer::wake() const
qWarning() << "Send failed:" << sock.error(); qWarning() << "Send failed:" << sock.error();
} }
} }
QList<quint16> basePorts;
if (i.value() != 0) {
// If we have a known base port for this address, use only that port
basePorts.append(i.value());
}
else {
// If this is a broadcast address without a known HTTP port, try all of them
basePorts.append(basePortSet.values());
}
// Send to all dynamic ports using the HTTP port offset(s) for this address
for (quint16 basePort : basePorts) {
for (quint16 port : DYNAMIC_WOL_PORTS) {
port = (port - 47989) + basePort;
if (sock.writeDatagram(wolPayload, address, port)) {
qInfo().nospace().noquote() << "Sent WoL packet to " << name << " via " << address.toString() << ":" << port;
success = true;
}
else {
qWarning() << "Send failed:" << sock.error();
}
}
}
} }
} }
return success; return success;
} }
NvComputer::ReachabilityType NvComputer::getActiveAddressReachability() const bool NvComputer::isReachableOverVpn()
{ {
NvAddress copyOfActiveAddress; if (activeAddress.isNull()) {
return false;
{
QReadLocker readLocker(&lock);
if (activeAddress.isNull()) {
return ReachabilityType::RI_UNKNOWN;
}
// Grab a copy of the active address to avoid having to hold
// the computer lock while doing socket operations
copyOfActiveAddress = activeAddress;
} }
QTcpSocket s; QTcpSocket s;
s.setProxy(QNetworkProxy::NoProxy); s.setProxy(QNetworkProxy::NoProxy);
s.connectToHost(copyOfActiveAddress.address(), copyOfActiveAddress.port()); s.connectToHost(activeAddress.address(), activeAddress.port());
if (s.waitForConnected(3000)) { if (s.waitForConnected(3000)) {
Q_ASSERT(!s.localAddress().isNull()); Q_ASSERT(!s.localAddress().isNull());
Q_ASSERT(!s.peerAddress().isNull());
for (const QNetworkInterface& nic : QNetworkInterface::allInterfaces()) { for (const QNetworkInterface& nic : QNetworkInterface::allInterfaces()) {
// Ensure the interface is up // Ensure the interface is up
@@ -392,68 +308,48 @@ NvComputer::ReachabilityType NvComputer::getActiveAddressReachability() const
if (nic.type() == QNetworkInterface::Virtual || if (nic.type() == QNetworkInterface::Virtual ||
nic.type() == QNetworkInterface::Ppp) { nic.type() == QNetworkInterface::Ppp) {
// Treat PPP and virtual interfaces as likely VPNs // Treat PPP and virtual interfaces as likely VPNs
return ReachabilityType::RI_VPN; return true;
} }
if (nic.maximumTransmissionUnit() != 0 && nic.maximumTransmissionUnit() < 1500) { if (nic.maximumTransmissionUnit() != 0 && nic.maximumTransmissionUnit() < 1500) {
// Treat MTUs under 1500 as likely VPNs // Treat MTUs under 1500 as likely VPNs
return ReachabilityType::RI_VPN; return true;
} }
#endif #endif
if (nic.flags() & QNetworkInterface::IsPointToPoint) { if (nic.flags() & QNetworkInterface::IsPointToPoint) {
// Treat point-to-point links as likely VPNs. // Treat point-to-point links as likely VPNs.
// This check detects OpenVPN on Unix-like OSes. // This check detects OpenVPN on Unix-like OSes.
return ReachabilityType::RI_VPN; return true;
} }
#ifdef Q_OS_WINDOWS
if (nic.name().startsWith("iftype53_") || nic.name().startsWith("iftype131_")) {
// Match by NDIS interface type. These values are Microsoft's recommended values for VPN connections:
// https://learn.microsoft.com/en-US/troubleshoot/windows-client/networking/windows-connection-manager-disconnects-wlan#more-information
//
// The following VPNs use IF_TYPE_PROP_VIRTUAL under Windows:
// - WireguardNT VPNs
// - All WinTun-based VPNs (such as Slack Nebula)
// - OpenVPN with tap-windows6
return ReachabilityType::RI_VPN;
}
#endif
if (nic.hardwareAddress().startsWith("00:FF", Qt::CaseInsensitive)) { if (nic.hardwareAddress().startsWith("00:FF", Qt::CaseInsensitive)) {
// OpenVPN TAP interfaces have a MAC address starting with 00:FF on Windows // OpenVPN TAP interfaces have a MAC address starting with 00:FF on Windows
return ReachabilityType::RI_VPN; return true;
} }
if (nic.humanReadableName().startsWith("ZeroTier")) { if (nic.humanReadableName().startsWith("ZeroTier")) {
// ZeroTier interfaces always start with "ZeroTier" // ZeroTier interfaces always start with "ZeroTier"
return ReachabilityType::RI_VPN; return true;
} }
if (nic.humanReadableName().contains("VPN")) { if (nic.humanReadableName().contains("VPN")) {
// This one is just a final VPN heuristic if all else fails // This one is just a final heuristic if all else fails
return ReachabilityType::RI_VPN; return true;
} }
// Didn't meet any of our VPN heuristics. Let's see if the peer address is on-link. // Didn't meet any of our VPN heuristics
Q_ASSERT(addr.prefixLength() >= 0); return false;
if (addr.prefixLength() >= 0 && s.localAddress().isInSubnet(s.peerAddress(), addr.prefixLength())) {
return ReachabilityType::RI_LAN;
}
// Default to unknown if nothing else matched
return ReachabilityType::RI_UNKNOWN;
} }
} }
} }
qWarning() << "No match found for address:" << s.localAddress(); qWarning() << "No match found for address:" << s.localAddress();
return ReachabilityType::RI_UNKNOWN; return false;
} }
else { else {
// If we fail to connect, just pretend that it's not a VPN // If we fail to connect, just pretend that it's not a VPN
qWarning() << "Unable to check for reachability within 3 seconds"; return false;
return ReachabilityType::RI_UNKNOWN;
} }
} }
@@ -479,7 +375,6 @@ bool NvComputer::updateAppList(QVector<NvApp> newAppList) {
QVector<NvAddress> NvComputer::uniqueAddresses() const QVector<NvAddress> NvComputer::uniqueAddresses() const
{ {
QReadLocker readLocker(&lock);
QVector<NvAddress> uniqueAddressList; QVector<NvAddress> uniqueAddressList;
// Start with addresses correctly ordered // Start with addresses correctly ordered
@@ -511,7 +406,7 @@ QVector<NvAddress> NvComputer::uniqueAddresses() const
return uniqueAddressList; return uniqueAddressList;
} }
bool NvComputer::update(const NvComputer& that) bool NvComputer::update(NvComputer& that)
{ {
bool changed = false; bool changed = false;
@@ -561,7 +456,6 @@ bool NvComputer::update(const NvComputer& that)
ASSIGN_IF_CHANGED(gfeVersion); ASSIGN_IF_CHANGED(gfeVersion);
ASSIGN_IF_CHANGED(appVersion); ASSIGN_IF_CHANGED(appVersion);
ASSIGN_IF_CHANGED(isSupportedServerVersion); ASSIGN_IF_CHANGED(isSupportedServerVersion);
ASSIGN_IF_CHANGED(isNvidiaServerSoftware);
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);
+6 -37
View File
@@ -8,16 +8,6 @@
#include <QSettings> #include <QSettings>
#include <QRunnable> #include <QRunnable>
class CopySafeReadWriteLock : public QReadWriteLock
{
public:
CopySafeReadWriteLock() = default;
// Don't actually copy the QReadWriteLock
CopySafeReadWriteLock(const CopySafeReadWriteLock&) : QReadWriteLock() {}
CopySafeReadWriteLock& operator=(const CopySafeReadWriteLock &) { return *this; }
};
class NvComputer class NvComputer
{ {
friend class PcMonitorThread; friend class PcMonitorThread;
@@ -32,14 +22,6 @@ private:
bool pendingQuit; bool pendingQuit;
public: public:
NvComputer() = default;
// Caller is responsible for synchronizing read access to the other host
NvComputer(const NvComputer&) = default;
// Caller is responsible for synchronizing read access to the other host
NvComputer& operator=(const NvComputer &) = default;
explicit NvComputer(NvHTTP& http, QString serverInfo); explicit NvComputer(NvHTTP& http, QString serverInfo);
explicit NvComputer(QSettings& settings); explicit NvComputer(QSettings& settings);
@@ -48,30 +30,19 @@ public:
setRemoteAddress(QHostAddress); setRemoteAddress(QHostAddress);
bool bool
update(const NvComputer& that); update(NvComputer& that);
bool bool
wake() const; wake();
enum ReachabilityType bool
{ isReachableOverVpn();
RI_UNKNOWN,
RI_LAN,
RI_VPN,
};
ReachabilityType
getActiveAddressReachability() const;
QVector<NvAddress> QVector<NvAddress>
uniqueAddresses() const; uniqueAddresses() const;
void void
serialize(QSettings& settings, bool serializeApps) const; serialize(QSettings& settings) const;
// Caller is responsible for synchronizing read access to both hosts
bool
isEqualSerialized(const NvComputer& that) const;
enum PairState enum PairState
{ {
@@ -112,11 +83,9 @@ public:
QString uuid; QString uuid;
QSslCertificate serverCert; QSslCertificate serverCert;
QVector<NvApp> appList; QVector<NvApp> appList;
bool isNvidiaServerSoftware;
// Remember to update isEqualSerialized() when adding fields here!
// Synchronization // Synchronization
mutable CopySafeReadWriteLock lock; mutable QReadWriteLock lock;
private: private:
uint16_t externalPort; uint16_t externalPort;
+38 -19
View File
@@ -187,15 +187,12 @@ NvHTTP::getServerInfo(NvLogLevel logLevel, bool fastFail)
} }
void void
NvHTTP::startApp(QString verb, NvHTTP::launchApp(int appId,
bool isGfe, PSTREAM_CONFIGURATION streamConfig,
int appId, bool sops,
PSTREAM_CONFIGURATION streamConfig, bool localAudio,
bool sops, int gamepadMask,
bool localAudio, QString& rtspSessionUrl)
int gamepadMask,
bool persistGameControllersOnDisconnect,
QString& rtspSessionUrl)
{ {
int riKeyId; int riKeyId;
@@ -204,27 +201,25 @@ NvHTTP::startApp(QString verb,
QString response = QString response =
openConnectionToString(m_BaseUrlHttps, openConnectionToString(m_BaseUrlHttps,
verb, "launch",
"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, // Using an FPS value over 60 causes SOPS to default to 720p60,
// so force it to 0 to ensure the correct resolution is set. We // 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 // used to use 60 here but that locked the frame rate to 60 FPS
// on GFE 3.20.3. We don't need this hack for Sunshine. // on GFE 3.20.3.
QString::number((streamConfig->fps > 60 && isGfe) ? 0 : streamConfig->fps)+ 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)+
((streamConfig->supportedVideoFormats & VIDEO_FORMAT_MASK_10BIT) ? (streamConfig->enableHdr ?
"&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="+QString::number(SURROUNDAUDIOINFO_FROM_AUDIO_CONFIGURATION(streamConfig->audioConfiguration))+
"&remoteControllersBitmap="+QString::number(gamepadMask)+ "&remoteControllersBitmap="+QString::number(gamepadMask)+
"&gcmap="+QString::number(gamepadMask)+ "&gcmap="+QString::number(gamepadMask),
"&gcpersist="+QString::number(persistGameControllersOnDisconnect ? 1 : 0)+
LiGetLaunchUrlQueryParameters(),
LAUNCH_TIMEOUT_MS); LAUNCH_TIMEOUT_MS);
qInfo() << "Launch response:" << response; qInfo() << "Launch response:" << response;
@@ -235,6 +230,30 @@ NvHTTP::startApp(QString verb,
rtspSessionUrl = getXmlString(response, "sessionUrl0"); rtspSessionUrl = getXmlString(response, "sessionUrl0");
} }
void
NvHTTP::resumeApp(PSTREAM_CONFIGURATION streamConfig, QString& rtspSessionUrl)
{
int riKeyId;
memcpy(&riKeyId, streamConfig->remoteInputAesIv, sizeof(riKeyId));
riKeyId = qFromBigEndian(riKeyId);
QString response =
openConnectionToString(m_BaseUrlHttps,
"resume",
"rikey="+QString(QByteArray(streamConfig->remoteInputAesKey, sizeof(streamConfig->remoteInputAesKey)).toHex())+
"&rikeyid="+QString::number(riKeyId)+
"&surroundAudioInfo="+QString::number(SURROUNDAUDIOINFO_FROM_AUDIO_CONFIGURATION(streamConfig->audioConfiguration)),
RESUME_TIMEOUT_MS);
qInfo() << "Resume response:" << response;
// Throws if the request failed
verifyResponseStatus(response);
rtspSessionUrl = getXmlString(response, "sessionUrl0");
}
void void
NvHTTP::quitApp() NvHTTP::quitApp()
{ {
@@ -250,7 +269,7 @@ NvHTTP::quitApp()
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 requester. // if we're not the original requestor.
if (getCurrentGame(getServerInfo(NvHTTP::NVLL_ERROR)) != 0) { if (getCurrentGame(getServerInfo(NvHTTP::NVLL_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.
@@ -363,7 +382,7 @@ NvHTTP::verifyResponseStatus(QString xml)
} }
} }
throw GfeHttpResponseException(-1, "Malformed XML (missing root element)"); throw GfeHttpResponseException(-1, "Malformed GFE XML (missing root element)");
} }
QImage QImage
@@ -532,7 +551,7 @@ NvHTTP::openConnection(QUrl baseUrl,
if (reply->error() != QNetworkReply::NoError) if (reply->error() != QNetworkReply::NoError)
{ {
if (logLevel >= NvLogLevel::NVLL_ERROR) { if (logLevel >= NvLogLevel::NVLL_ERROR) {
qWarning() << command << "request failed with error:" << reply->error(); qWarning() << command << " request failed with error " << reply->error();
} }
if (reply->error() == QNetworkReply::SslHandshakeFailedError) { if (reply->error() == QNetworkReply::SslHandshakeFailedError) {
+10 -9
View File
@@ -162,15 +162,16 @@ public:
quitApp(); quitApp();
void void
startApp(QString verb, resumeApp(PSTREAM_CONFIGURATION streamConfig,
bool isGfe, QString& rtspSessionUrl);
int appId,
PSTREAM_CONFIGURATION streamConfig, void
bool sops, launchApp(int appId,
bool localAudio, PSTREAM_CONFIGURATION streamConfig,
int gamepadMask, bool sops,
bool persistGameControllersOnDisconnect, bool localAudio,
QString& rtspSessionUrl); int gamepadMask,
QString& rtspSessionUrl);
QVector<NvApp> QVector<NvApp>
getAppList(); getAppList();
+2 -2
View File
@@ -298,8 +298,8 @@ NvPairingManager::pair(QString appVersion, QString pin, QSslCertificate& serverC
} }
QByteArray pairingSecret = NvHTTP::getXmlStringFromHex(respXml, "pairingsecret"); QByteArray pairingSecret = NvHTTP::getXmlStringFromHex(respXml, "pairingsecret");
QByteArray serverSecret = pairingSecret.left(16); QByteArray serverSecret = QByteArray(pairingSecret.data(), 16);
QByteArray serverSignature = pairingSecret.mid(16); QByteArray serverSignature = QByteArray(&pairingSecret.data()[16], 256);
if (!verifySignature(serverSecret, if (!verifySignature(serverSecret,
serverSignature, serverSignature,
+51 -48
View File
@@ -2,7 +2,6 @@
#include "utils.h" #include "utils.h"
#include <QGuiApplication> #include <QGuiApplication>
#include <QLibraryInfo>
#include "streaming/session.h" #include "streaming/session.h"
#include "streaming/streamutils.h" #include "streaming/streamutils.h"
@@ -18,7 +17,6 @@ SystemProperties::SystemProperties()
hasDesktopEnvironment = WMUtils::isRunningDesktopEnvironment(); hasDesktopEnvironment = WMUtils::isRunningDesktopEnvironment();
isRunningWayland = WMUtils::isRunningWayland(); isRunningWayland = WMUtils::isRunningWayland();
isRunningXWayland = isRunningWayland && QGuiApplication::platformName() == "xcb"; isRunningXWayland = isRunningWayland && QGuiApplication::platformName() == "xcb";
usesMaterial3Theme = QLibraryInfo::version() >= QVersionNumber(6, 5, 0);
QString nativeArch = QSysInfo::currentCpuArchitecture(); QString nativeArch = QSysInfo::currentCpuArchitecture();
#ifdef Q_OS_WIN32 #ifdef Q_OS_WIN32
@@ -72,9 +70,15 @@ SystemProperties::SystemProperties()
// and cache the results to speed up future queries on this data. // and cache the results to speed up future queries on this data.
querySdlVideoInfo(); querySdlVideoInfo();
Q_ASSERT(!monitorRefreshRates.isEmpty()); Q_ASSERT(maximumStreamingFrameRate >= 60);
Q_ASSERT(!monitorDesktopResolutions.isEmpty());
Q_ASSERT(!monitorNativeResolutions.isEmpty()); Q_ASSERT(!monitorNativeResolutions.isEmpty());
Q_ASSERT(!monitorSafeAreaResolutions.isEmpty()); }
QRect SystemProperties::getDesktopResolution(int displayIndex)
{
// Returns default constructed QRect if out of bounds
return monitorDesktopResolutions.value(displayIndex);
} }
QRect SystemProperties::getNativeResolution(int displayIndex) QRect SystemProperties::getNativeResolution(int displayIndex)
@@ -83,18 +87,6 @@ QRect SystemProperties::getNativeResolution(int displayIndex)
return monitorNativeResolutions.value(displayIndex); return monitorNativeResolutions.value(displayIndex);
} }
QRect SystemProperties::getSafeAreaResolution(int displayIndex)
{
// Returns default constructed QRect if out of bounds
return monitorSafeAreaResolutions.value(displayIndex);
}
int SystemProperties::getRefreshRate(int displayIndex)
{
// Returns 0 if out of bounds
return monitorRefreshRates.value(displayIndex);
}
class QuerySdlVideoThread : public QThread class QuerySdlVideoThread : public QThread
{ {
public: public:
@@ -128,7 +120,7 @@ void SystemProperties::querySdlVideoInfoInternal()
{ {
hasHardwareAcceleration = false; hasHardwareAcceleration = false;
if (SDLC_FAILURE(SDL_InitSubSystem(SDL_INIT_VIDEO))) { if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s", "SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s",
SDL_GetError()); SDL_GetError());
@@ -139,15 +131,21 @@ void SystemProperties::querySdlVideoInfoInternal()
// We call the internal variant because we're already in a safe thread context. // We call the internal variant because we're already in a safe thread context.
refreshDisplaysInternal(); refreshDisplaysInternal();
SDL_Window* testWindow = SDLC_CreateWindowWithFallback("", 0, 0, 1280, 720, SDL_Window* testWindow = SDL_CreateWindow("", 0, 0, 1280, 720,
SDL_WINDOW_HIDDEN, SDL_WINDOW_HIDDEN | StreamUtils::getPlatformWindowFlags());
StreamUtils::getPlatformWindowFlags());
if (!testWindow) { if (!testWindow) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Failed to create window for hardware decode test: %s", "Failed to create test window with platform flags: %s",
SDL_GetError()); SDL_GetError());
SDL_QuitSubSystem(SDL_INIT_VIDEO);
return; 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); Session::getDecoderInfo(testWindow, hasHardwareAcceleration, rendererAlwaysFullScreen, supportsHdr, maximumResolution);
@@ -188,27 +186,44 @@ void SystemProperties::refreshDisplays()
void SystemProperties::refreshDisplaysInternal() void SystemProperties::refreshDisplaysInternal()
{ {
if (SDLC_FAILURE(SDL_InitSubSystem(SDL_INIT_VIDEO))) { if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s", "SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s",
SDL_GetError()); SDL_GetError());
return; return;
} }
monitorDesktopResolutions.clear();
monitorNativeResolutions.clear(); monitorNativeResolutions.clear();
int numDisplays = 0; // Never let the maximum drop below 60 FPS
SDL_DisplayID *displays = SDL_GetDisplays(&numDisplays); maximumStreamingFrameRate = 60;
SDL_DisplayMode bestMode; SDL_DisplayMode bestMode;
for (int i = 0; i < numDisplays; i++) { for (int displayIndex = 0; displayIndex < SDL_GetNumVideoDisplays(); displayIndex++) {
SDL_DisplayMode desktopMode; SDL_DisplayMode desktopMode;
SDL_Rect safeArea; int err;
if (StreamUtils::getNativeDesktopMode(displays[i], &desktopMode, &safeArea)) { err = SDL_GetDesktopDisplayMode(displayIndex, &desktopMode);
if (err == 0) {
if (desktopMode.w <= 8192 && desktopMode.h <= 8192) { if (desktopMode.w <= 8192 && desktopMode.h <= 8192) {
monitorNativeResolutions.insert(i, QRect(0, 0, desktopMode.w, desktopMode.h)); monitorDesktopResolutions.insert(displayIndex, QRect(0, 0, desktopMode.w, desktopMode.h));
monitorSafeAreaResolutions.insert(i, QRect(0, 0, safeArea.w, safeArea.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 { else {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
@@ -218,10 +233,9 @@ void SystemProperties::refreshDisplaysInternal()
// Start at desktop mode and work our way up // Start at desktop mode and work our way up
bestMode = desktopMode; bestMode = desktopMode;
int numDisplayModes = SDL_GetNumDisplayModes(displays[i]); for (int i = 0; i < SDL_GetNumDisplayModes(displayIndex); i++) {
for (int i = 0; i < numDisplayModes; i++) {
SDL_DisplayMode mode; SDL_DisplayMode mode;
if (SDL_GetDisplayMode(displays[i], i, &mode) == 0) { if (SDL_GetDisplayMode(displayIndex, i, &mode) == 0) {
if (mode.w == desktopMode.w && mode.h == desktopMode.h) { if (mode.w == desktopMode.w && mode.h == desktopMode.h) {
if (mode.refresh_rate > bestMode.refresh_rate) { if (mode.refresh_rate > bestMode.refresh_rate) {
bestMode = mode; bestMode = mode;
@@ -230,20 +244,9 @@ void SystemProperties::refreshDisplaysInternal()
} }
} }
// Try to normalize values around our our standard refresh rates. maximumStreamingFrameRate = qMax(maximumStreamingFrameRate, bestMode.refresh_rate);
// Some displays/OSes report values that are slightly off.
if (bestMode.refresh_rate >= 58 && bestMode.refresh_rate <= 62) {
monitorRefreshRates.append(60);
}
else if (bestMode.refresh_rate >= 28 && bestMode.refresh_rate <= 32) {
monitorRefreshRates.append(30);
}
else {
monitorRefreshRates.append(bestMode.refresh_rate);
}
} }
} }
SDL_free(displays);
SDL_QuitSubSystem(SDL_INIT_VIDEO); SDL_QuitSubSystem(SDL_INIT_VIDEO);
} }
+4 -6
View File
@@ -23,15 +23,14 @@ public:
Q_PROPERTY(bool hasBrowser MEMBER hasBrowser CONSTANT) Q_PROPERTY(bool hasBrowser MEMBER hasBrowser CONSTANT)
Q_PROPERTY(bool hasDiscordIntegration MEMBER hasDiscordIntegration CONSTANT) Q_PROPERTY(bool hasDiscordIntegration MEMBER hasDiscordIntegration CONSTANT)
Q_PROPERTY(QString unmappedGamepads MEMBER unmappedGamepads NOTIFY unmappedGamepadsChanged) Q_PROPERTY(QString unmappedGamepads MEMBER unmappedGamepads NOTIFY unmappedGamepadsChanged)
Q_PROPERTY(int maximumStreamingFrameRate MEMBER maximumStreamingFrameRate CONSTANT)
Q_PROPERTY(QSize maximumResolution MEMBER maximumResolution CONSTANT) Q_PROPERTY(QSize maximumResolution MEMBER maximumResolution CONSTANT)
Q_PROPERTY(QString versionString MEMBER versionString CONSTANT) Q_PROPERTY(QString versionString MEMBER versionString CONSTANT)
Q_PROPERTY(bool supportsHdr MEMBER supportsHdr CONSTANT) Q_PROPERTY(bool supportsHdr MEMBER supportsHdr CONSTANT)
Q_PROPERTY(bool usesMaterial3Theme MEMBER usesMaterial3Theme CONSTANT)
Q_INVOKABLE void refreshDisplays(); Q_INVOKABLE void refreshDisplays();
Q_INVOKABLE QRect getDesktopResolution(int displayIndex);
Q_INVOKABLE QRect getNativeResolution(int displayIndex); Q_INVOKABLE QRect getNativeResolution(int displayIndex);
Q_INVOKABLE QRect getSafeAreaResolution(int displayIndex);
Q_INVOKABLE int getRefreshRate(int displayIndex);
signals: signals:
void unmappedGamepadsChanged(); void unmappedGamepadsChanged();
@@ -51,12 +50,11 @@ private:
bool hasBrowser; bool hasBrowser;
bool hasDiscordIntegration; bool hasDiscordIntegration;
QString unmappedGamepads; QString unmappedGamepads;
int maximumStreamingFrameRate;
QSize maximumResolution; QSize maximumResolution;
QList<QRect> monitorDesktopResolutions;
QList<QRect> monitorNativeResolutions; QList<QRect> monitorNativeResolutions;
QList<QRect> monitorSafeAreaResolutions;
QList<int> monitorRefreshRates;
QString versionString; QString versionString;
bool supportsHdr; bool supportsHdr;
bool usesMaterial3Theme;
}; };
+5 -135
View File
@@ -164,10 +164,8 @@ GlobalCommandLineParser::ParseResult GlobalCommandLineParser::parse(const QStrin
"Starts Moonlight normally if no arguments are given.\n" "Starts Moonlight normally if no arguments are given.\n"
"\n" "\n"
"Available actions:\n" "Available actions:\n"
" list List the available apps on a host\n"
" quit Quit the currently running app\n" " quit Quit the currently running app\n"
" stream Start streaming an app\n" " stream Start streaming an app\n"
" pair Pair a new host\n"
"\n" "\n"
"See 'moonlight <action> --help' for help of specific action." "See 'moonlight <action> --help' for help of specific action."
); );
@@ -195,10 +193,6 @@ GlobalCommandLineParser::ParseResult GlobalCommandLineParser::parse(const QStrin
return QuitRequested; return QuitRequested;
} else if (action == "stream") { } else if (action == "stream") {
return StreamRequested; return StreamRequested;
} else if (action == "pair") {
return PairRequested;
} else if (action == "list") {
return ListRequested;
} }
} }
@@ -248,58 +242,6 @@ QString QuitCommandLineParser::getHost() const
return m_Host; return m_Host;
} }
PairCommandLineParser::PairCommandLineParser()
{
}
PairCommandLineParser::~PairCommandLineParser()
{
}
void PairCommandLineParser::parse(const QStringList &args)
{
CommandLineParser parser;
parser.setupCommonOptions();
parser.setApplicationDescription(
"\n"
"Pair with the specified host."
);
parser.addPositionalArgument("pair", "pair host");
parser.addPositionalArgument("host", "Host computer name, UUID, or IP address", "<host>");
parser.addValueOption("pin", "4 digit pairing PIN");
if (!parser.parse(args)) {
parser.showError(parser.errorText());
}
parser.handleUnknownOptions();
// This method will not return and terminates the process if --version or
// --help is specified
parser.handleHelpAndVersionOptions();
// Verify that host has been provided
auto posArgs = parser.positionalArguments();
if (posArgs.length() < 2) {
parser.showError("Host not provided");
}
m_Host = parser.positionalArguments().at(1);
m_PredefinedPin = parser.value("pin");
if (!m_PredefinedPin.isEmpty() && m_PredefinedPin.length() != 4) {
parser.showError("PIN must be 4 digits");
}
}
QString PairCommandLineParser::getHost() const
{
return m_Host;
}
QString PairCommandLineParser::getPredefinedPin() const
{
return m_PredefinedPin;
}
StreamCommandLineParser::StreamCommandLineParser() StreamCommandLineParser::StreamCommandLineParser()
{ {
m_WindowModeMap = { m_WindowModeMap = {
@@ -316,7 +258,6 @@ StreamCommandLineParser::StreamCommandLineParser()
{"auto", StreamingPreferences::VCC_AUTO}, {"auto", StreamingPreferences::VCC_AUTO},
{"H.264", StreamingPreferences::VCC_FORCE_H264}, {"H.264", StreamingPreferences::VCC_FORCE_H264},
{"HEVC", StreamingPreferences::VCC_FORCE_HEVC}, {"HEVC", StreamingPreferences::VCC_FORCE_HEVC},
{"AV1", StreamingPreferences::VCC_FORCE_AV1},
}; };
m_VideoDecoderMap = { m_VideoDecoderMap = {
{"auto", StreamingPreferences::VDS_AUTO}, {"auto", StreamingPreferences::VDS_AUTO},
@@ -372,9 +313,6 @@ void StreamCommandLineParser::parse(const QStringList &args, StreamingPreference
parser.addToggleOption("reverse-scroll-direction", "inverted scroll direction"); parser.addToggleOption("reverse-scroll-direction", "inverted scroll direction");
parser.addToggleOption("swap-gamepad-buttons", "swap A/B and X/Y gamepad buttons (Nintendo-style)"); parser.addToggleOption("swap-gamepad-buttons", "swap A/B and X/Y gamepad buttons (Nintendo-style)");
parser.addToggleOption("keep-awake", "prevent display sleep while streaming"); parser.addToggleOption("keep-awake", "prevent display sleep while streaming");
parser.addToggleOption("performance-overlay", "show performance overlay");
parser.addToggleOption("hdr", "HDR streaming");
parser.addToggleOption("yuv444", "YUV 4:4:4 sampling, if supported");
parser.addChoiceOption("capture-system-keys", "capture system key combos", m_CaptureSysKeysModeMap.keys()); 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());
@@ -413,20 +351,20 @@ 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, 10, 480)) { if (!inRange(preferences->fps, 30, 240)) {
fprintf(stderr, "Warning: FPS is out of the supported range (10 - 480 FPS). Performance may suffer!\n"); parser.showError("FPS must be in range: 30 - 240");
} }
} }
// Resolve --bitrate option // Resolve --bitrate option
if (parser.isSet("bitrate")) { if (parser.isSet("bitrate")) {
preferences->bitrateKbps = parser.getIntOption("bitrate"); preferences->bitrateKbps = parser.getIntOption("bitrate");
if (!inRange(preferences->bitrateKbps, 500, 500000)) { if (!inRange(preferences->bitrateKbps, 500, 150000)) {
fprintf(stderr, "Warning: Bitrate is out of the supported range (500 - 500000 Kbps). Performance may suffer!\n"); parser.showError("Bitrate must be in range: 500 - 150000");
} }
} else if (displaySet || parser.isSet("fps")) { } else if (displaySet || parser.isSet("fps")) {
preferences->bitrateKbps = preferences->getDefaultBitrate( preferences->bitrateKbps = preferences->getDefaultBitrate(
preferences->width, preferences->height, preferences->fps, preferences->enableYUV444); preferences->width, preferences->height, preferences->fps);
} }
// Resolve --packet-size option // Resolve --packet-size option
@@ -489,15 +427,6 @@ void StreamCommandLineParser::parse(const QStringList &args, StreamingPreference
// Resolve --keep-awake and --no-keep-awake options // Resolve --keep-awake and --no-keep-awake options
preferences->keepAwake = parser.getToggleOptionValue("keep-awake", preferences->keepAwake); preferences->keepAwake = parser.getToggleOptionValue("keep-awake", preferences->keepAwake);
// Resolve --performance-overlay and --no-performance-overlay options
preferences->showPerformanceOverlay = parser.getToggleOptionValue("performance-overlay", preferences->showPerformanceOverlay);
// Resolve --hdr and --no-hdr options
preferences->enableHdr = parser.getToggleOptionValue("hdr", preferences->enableHdr);
// Resolve --yuv444 and --no-yuv444 options
preferences->enableYUV444 = parser.getToggleOptionValue("yuv444", preferences->enableYUV444);
// Resolve --capture-system-keys option // Resolve --capture-system-keys option
if (parser.isSet("capture-system-keys")) { if (parser.isSet("capture-system-keys")) {
preferences->captureSysKeysMode = mapValue(m_CaptureSysKeysModeMap, parser.getChoiceOptionValue("capture-system-keys")); preferences->captureSysKeysMode = mapValue(m_CaptureSysKeysModeMap, parser.getChoiceOptionValue("capture-system-keys"));
@@ -539,62 +468,3 @@ QString StreamCommandLineParser::getAppName() const
{ {
return m_AppName; return m_AppName;
} }
ListCommandLineParser::ListCommandLineParser()
{
}
ListCommandLineParser::~ListCommandLineParser()
{
}
void ListCommandLineParser::parse(const QStringList &args)
{
CommandLineParser parser;
parser.setupCommonOptions();
parser.setApplicationDescription(
"\n"
"List the available apps on the given host."
);
parser.addPositionalArgument("list", "list available apps");
parser.addPositionalArgument("host", "Host computer name, UUID, or IP address", "<host>");
parser.addFlagOption("csv", "Print as CSV with additional information");
parser.addFlagOption("verbose", "Displays additional information");
if (!parser.parse(args)) {
parser.showError(parser.errorText());
}
parser.handleUnknownOptions();
m_PrintCSV = parser.isSet("csv");
m_Verbose = parser.isSet("verbose");
// This method will not return and terminates the process if --version or
// --help is specified
parser.handleHelpAndVersionOptions();
// Verify that host has been provided
auto posArgs = parser.positionalArguments();
if (posArgs.length() < 2) {
parser.showError("Host not provided");
}
m_Host = parser.positionalArguments().at(1);
}
QString ListCommandLineParser::getHost() const
{
return m_Host;
}
bool ListCommandLineParser::isPrintCSV() const
{
return m_PrintCSV;
}
bool ListCommandLineParser::isVerbose() const
{
return m_Verbose;
}
-36
View File
@@ -12,8 +12,6 @@ public:
NormalStartRequested, NormalStartRequested,
StreamRequested, StreamRequested,
QuitRequested, QuitRequested,
PairRequested,
ListRequested,
}; };
GlobalCommandLineParser(); GlobalCommandLineParser();
@@ -37,22 +35,6 @@ private:
QString m_Host; QString m_Host;
}; };
class PairCommandLineParser
{
public:
PairCommandLineParser();
virtual ~PairCommandLineParser();
void parse(const QStringList &args);
QString getHost() const;
QString getPredefinedPin() const;
private:
QString m_Host;
QString m_PredefinedPin;
};
class StreamCommandLineParser class StreamCommandLineParser
{ {
public: public:
@@ -73,21 +55,3 @@ private:
QMap<QString, StreamingPreferences::VideoDecoderSelection> m_VideoDecoderMap; QMap<QString, StreamingPreferences::VideoDecoderSelection> m_VideoDecoderMap;
QMap<QString, StreamingPreferences::CaptureSysKeysMode> m_CaptureSysKeysModeMap; QMap<QString, StreamingPreferences::CaptureSysKeysMode> m_CaptureSysKeysModeMap;
}; };
class ListCommandLineParser
{
public:
ListCommandLineParser();
virtual ~ListCommandLineParser();
void parse(const QStringList &args);
QString getHost() const;
bool isPrintCSV() const;
bool isVerbose() const;
private:
QString m_Host;
bool m_PrintCSV;
bool m_Verbose;
};
-208
View File
@@ -1,208 +0,0 @@
#include "listapps.h"
#include "backend/boxartmanager.h"
#include "backend/computermanager.h"
#include "backend/computerseeker.h"
#include <QCoreApplication>
#include <QTimer>
#define COMPUTER_SEEK_TIMEOUT 30000
#define APP_SEEK_TIMEOUT 10000
namespace CliListApps
{
enum State {
StateInit,
StateSeekComputer,
StateListApp,
StateSeekApp,
StateFailure,
};
class Event
{
public:
enum Type {
ComputerFound,
ComputerUpdated,
ComputerSeekTimedout,
Executed,
};
Event(Type type)
: type(type), computerManager(nullptr), computer(nullptr) {}
Type type;
ComputerManager *computerManager;
NvComputer *computer;
QString errorMessage;
};
class LauncherPrivate
{
Q_DECLARE_PUBLIC(Launcher)
public:
LauncherPrivate(Launcher *q) : q_ptr(q) {}
void handleEvent(Event event)
{
Q_Q(Launcher);
NvApp app;
switch (event.type) {
// Occurs when CLI main calls execute
case Event::Executed:
if (m_State == StateInit) {
m_State = StateSeekComputer;
m_ComputerManager = event.computerManager;
m_ComputerSeeker = new ComputerSeeker(m_ComputerManager, m_ComputerName, q);
q->connect(m_ComputerSeeker, &ComputerSeeker::computerFound,
q, &Launcher::onComputerFound);
q->connect(m_ComputerSeeker, &ComputerSeeker::errorTimeout,
q, &Launcher::onComputerSeekTimeout);
m_ComputerSeeker->start(COMPUTER_SEEK_TIMEOUT);
q->connect(m_ComputerManager, &ComputerManager::computerStateChanged,
q, &Launcher::onComputerUpdated);
m_BoxArtManager = new BoxArtManager(q);
if (m_Arguments.isVerbose()) {
fprintf(stdout, "Establishing connection to PC...\n");
}
}
break;
// Occurs when computer search timed out
case Event::ComputerSeekTimedout:
if (m_State == StateSeekComputer) {
fprintf(stderr, "%s\n", qPrintable(QString("Failed to connect to %1").arg(m_ComputerName)));
QCoreApplication::exit(-1);
}
break;
// Occurs when searched computer is found
case Event::ComputerFound:
if (m_State == StateSeekComputer) {
if (event.computer->pairState == NvComputer::PS_PAIRED) {
m_State = StateSeekApp;
m_Computer = event.computer;
m_TimeoutTimer->start(APP_SEEK_TIMEOUT);
if (m_Arguments.isVerbose()) {
fprintf(stdout, "Loading app list...\n");
}
} else {
m_State = StateFailure;
fprintf(stderr, "%s\n", qPrintable(QObject::tr("Computer %1 has not been paired. "
"Please open Moonlight to pair before retrieving games list.")
.arg(event.computer->name)));
QCoreApplication::exit(-1);
}
}
break;
// Occurs when a computer is updated
case Event::ComputerUpdated:
if (m_State == StateSeekApp) {
m_Arguments.isPrintCSV() ? printAppsCSV(m_Computer->appList) : printApps(m_Computer->appList);
QCoreApplication::exit(0);
}
break;
}
}
void printApps(QVector<NvApp> apps) {
for (int i = 0; i < apps.length(); i++) {
fprintf(stdout, "%s\n", qPrintable(apps[i].name));
}
}
void printAppsCSV(QVector<NvApp> apps) {
fprintf(stdout, "Name, ID, HDR Support, App Collection Game, Hidden, Direct Launch, Boxart URL\n");
for (int i = 0; i < apps.length(); i++) {
printAppCSV(apps[i]);
}
}
void printAppCSV(NvApp app) const
{
fprintf(stdout, "\"%s\",%d,%s,%s,%s,%s,\"%s\"\n", qPrintable(app.name),
app.id,
app.hdrSupported ? "true" : "false",
app.isAppCollectorGame ? "true" : "false",
app.hidden ? "true" : "false",
app.directLaunch ? "true" : "false",
qPrintable(m_BoxArtManager->loadBoxArt(m_Computer, app).toDisplayString()));
}
Launcher *q_ptr;
ComputerManager *m_ComputerManager;
QString m_ComputerName;
ComputerSeeker *m_ComputerSeeker;
BoxArtManager *m_BoxArtManager;
NvComputer *m_Computer;
State m_State;
QTimer *m_TimeoutTimer;
ListCommandLineParser m_Arguments;
};
Launcher::Launcher(QString computer, ListCommandLineParser arguments, QObject *parent)
: QObject(parent),
m_DPtr(new LauncherPrivate(this))
{
Q_D(Launcher);
d->m_ComputerName = computer;
d->m_State = StateInit;
d->m_TimeoutTimer = new QTimer(this);
d->m_TimeoutTimer->setSingleShot(true);
d->m_Arguments = arguments;
connect(d->m_TimeoutTimer, &QTimer::timeout,
this, &Launcher::onComputerSeekTimeout);
}
Launcher::~Launcher()
{
}
void Launcher::execute(ComputerManager *manager)
{
Q_D(Launcher);
Event event(Event::Executed);
event.computerManager = manager;
d->handleEvent(event);
}
bool Launcher::isExecuted() const
{
Q_D(const Launcher);
return d->m_State != StateInit;
}
void Launcher::onComputerFound(NvComputer *computer)
{
Q_D(Launcher);
Event event(Event::ComputerFound);
event.computer = computer;
d->handleEvent(event);
}
void Launcher::onComputerSeekTimeout()
{
Q_D(Launcher);
Event event(Event::ComputerSeekTimedout);
d->handleEvent(event);
}
void Launcher::onComputerUpdated(NvComputer *computer)
{
Q_D(Launcher);
Event event(Event::ComputerUpdated);
event.computer = computer;
d->handleEvent(event);
}
}
-38
View File
@@ -1,38 +0,0 @@
#pragma once
#include "commandlineparser.h"
#include <QObject>
#include <QVariant>
class ComputerManager;
class NvComputer;
namespace CliListApps
{
class LauncherPrivate;
class Launcher : public QObject
{
Q_OBJECT
Q_DECLARE_PRIVATE_D(m_DPtr, Launcher)
public:
explicit Launcher(QString computer, ListCommandLineParser arguments, QObject *parent = nullptr);
~Launcher();
Q_INVOKABLE void execute(ComputerManager *manager);
Q_INVOKABLE bool isExecuted() const;
private slots:
void onComputerFound(NvComputer *computer);
void onComputerUpdated(NvComputer *computer);
void onComputerSeekTimeout();
private:
QScopedPointer<LauncherPrivate> m_DPtr;
ListCommandLineParser m_Arguments;
};
}
-181
View File
@@ -1,181 +0,0 @@
#include "pair.h"
#include "backend/computermanager.h"
#include "backend/computerseeker.h"
#include <QCoreApplication>
#include <QTimer>
#define COMPUTER_SEEK_TIMEOUT 10000
namespace CliPair
{
enum State {
StateInit,
StateSeekComputer,
StatePairing,
StateFailure,
StateComplete,
};
class Event
{
public:
enum Type {
ComputerFound,
Executed,
PairingCompleted,
Timedout,
};
Event(Type type)
: type(type), computerManager(nullptr), computer(nullptr) {}
Type type;
ComputerManager *computerManager;
NvComputer *computer;
QString errorMessage;
};
class LauncherPrivate
{
Q_DECLARE_PUBLIC(Launcher)
public:
LauncherPrivate(Launcher *q) : q_ptr(q) {}
void handleEvent(Event event)
{
Q_Q(Launcher);
switch (event.type) {
// Occurs when CliPair becomes visible and the UI calls launcher's execute()
case Event::Executed:
if (m_State == StateInit) {
m_State = StateSeekComputer;
m_ComputerManager = event.computerManager;
q->connect(m_ComputerManager, &ComputerManager::pairingCompleted,
q, &Launcher::onPairingCompleted);
// If we weren't provided a predefined PIN, generate one now
if (m_PredefinedPin.isEmpty()) {
m_PredefinedPin = m_ComputerManager->generatePinString();
}
m_ComputerSeeker = new ComputerSeeker(m_ComputerManager, m_ComputerName, q);
q->connect(m_ComputerSeeker, &ComputerSeeker::computerFound,
q, &Launcher::onComputerFound);
q->connect(m_ComputerSeeker, &ComputerSeeker::errorTimeout,
q, &Launcher::onTimeout);
m_ComputerSeeker->start(COMPUTER_SEEK_TIMEOUT);
emit q->searchingComputer();
}
break;
// Occurs when searched computer is found
case Event::ComputerFound:
if (m_State == StateSeekComputer) {
if (event.computer->pairState == NvComputer::PS_PAIRED) {
m_State = StateFailure;
QString msg = QObject::tr("%1 is already paired").arg(event.computer->name);
emit q->failed(msg);
}
else {
Q_ASSERT(!m_PredefinedPin.isEmpty());
m_State = StatePairing;
m_ComputerManager->pairHost(event.computer, m_PredefinedPin);
emit q->pairing(event.computer->name, m_PredefinedPin);
}
}
break;
// Occurs when pairing operation completes
case Event::PairingCompleted:
if (m_State == StatePairing) {
if (event.errorMessage.isEmpty()) {
m_State = StateComplete;
emit q->success();
}
else {
m_State = StateFailure;
emit q->failed(event.errorMessage);
}
}
break;
// Occurs when computer search timed out
case Event::Timedout:
if (m_State == StateSeekComputer) {
m_State = StateFailure;
emit q->failed(QObject::tr("Failed to connect to %1").arg(m_ComputerName));
}
break;
}
}
Launcher *q_ptr;
QString m_ComputerName;
QString m_PredefinedPin;
ComputerManager *m_ComputerManager;
ComputerSeeker *m_ComputerSeeker;
NvComputer *m_Computer;
State m_State;
QTimer *m_TimeoutTimer;
};
Launcher::Launcher(QString computer, QString predefinedPin, QObject *parent)
: QObject(parent),
m_DPtr(new LauncherPrivate(this))
{
Q_D(Launcher);
d->m_ComputerName = computer;
d->m_PredefinedPin = predefinedPin;
d->m_State = StateInit;
d->m_TimeoutTimer = new QTimer(this);
d->m_TimeoutTimer->setSingleShot(true);
connect(d->m_TimeoutTimer, &QTimer::timeout,
this, &Launcher::onTimeout);
}
Launcher::~Launcher()
{
}
void Launcher::execute(ComputerManager *manager)
{
Q_D(Launcher);
Event event(Event::Executed);
event.computerManager = manager;
d->handleEvent(event);
}
bool Launcher::isExecuted() const
{
Q_D(const Launcher);
return d->m_State != StateInit;
}
void Launcher::onComputerFound(NvComputer *computer)
{
Q_D(Launcher);
Event event(Event::ComputerFound);
event.computer = computer;
d->handleEvent(event);
}
void Launcher::onTimeout()
{
Q_D(Launcher);
Event event(Event::Timedout);
d->handleEvent(event);
}
void Launcher::onPairingCompleted(NvComputer* computer, QString error)
{
Q_D(Launcher);
Event event(Event::PairingCompleted);
event.computer = computer;
event.errorMessage = error;
d->handleEvent(event);
}
}
-44
View File
@@ -1,44 +0,0 @@
#pragma once
#include <QObject>
#include <QVariant>
class ComputerManager;
class NvComputer;
class Session;
class StreamingPreferences;
namespace CliPair
{
class Event;
class LauncherPrivate;
class Launcher : public QObject
{
Q_OBJECT
Q_DECLARE_PRIVATE_D(m_DPtr, Launcher)
public:
explicit Launcher(QString computer, QString predefinedPin,
QObject *parent = nullptr);
~Launcher();
Q_INVOKABLE void execute(ComputerManager *manager);
Q_INVOKABLE bool isExecuted() const;
signals:
void searchingComputer();
void pairing(QString pcName, QString pin);
void failed(QString text);
void success();
private slots:
void onComputerFound(NvComputer *computer);
void onPairingCompleted(NvComputer *computer, QString error);
void onTimeout();
private:
QScopedPointer<LauncherPrivate> m_DPtr;
};
}
+3 -3
View File
@@ -6,7 +6,7 @@
#include <QCoreApplication> #include <QCoreApplication>
#include <QTimer> #include <QTimer>
#define COMPUTER_SEEK_TIMEOUT 30000 #define COMPUTER_SEEK_TIMEOUT 10000
#define APP_SEEK_TIMEOUT 10000 #define APP_SEEK_TIMEOUT 10000
namespace CliStartStream namespace CliStartStream
@@ -118,7 +118,7 @@ public:
} }
break; break;
// Occurs when the previous app quit has been completed, handles quitting errors if any // Occurs when the previous app quit has been completed, handles quitting errors if any
// happened. ComputerUpdated event's handler handles session start when previous app has // happended. ComputerUpdated event's handler handles session start when previous app has
// quit. // quit.
case Event::AppQuitCompleted: case Event::AppQuitCompleted:
if (m_State == StateSeekApp && !event.errorMessage.isEmpty()) { if (m_State == StateSeekApp && !event.errorMessage.isEmpty()) {
@@ -162,7 +162,7 @@ public:
QString getCurrentAppName() const QString getCurrentAppName() const
{ {
for (const NvApp& app : m_Computer->appList) { for (NvApp app : m_Computer->appList) {
if (m_Computer->currentGameId == app.id) { if (m_Computer->currentGameId == app.id) {
return app.name; return app.name;
} }
@@ -1,40 +0,0 @@
From 78261eb6e2fe728fa5d3843b5b8962b1ca559b7e Mon Sep 17 00:00:00 2001
From: David Rosca <nowrep@gmail.com>
Date: Sat, 16 Dec 2023 14:52:01 +0100
Subject: [PATCH] Vulkan: Don't try to reuse old swapchain
---
src/vulkan/swapchain.c | 12 ++++++++++--
1 file changed, 10 insertions(+), 2 deletions(-)
diff --git a/src/vulkan/swapchain.c b/src/vulkan/swapchain.c
index 0646e57c..c0b07ce4 100644
--- a/src/vulkan/swapchain.c
+++ b/src/vulkan/swapchain.c
@@ -612,13 +612,21 @@ static bool vk_sw_recreate(pl_swapchain sw, int w, int h)
}
#endif
+ {
+ pl_gpu_flush(gpu);
+ vk_wait_idle(vk);
+ for (int i = 0; i < vk->pool_graphics->num_queues; i++)
+ vk->QueueWaitIdle(vk->pool_graphics->queues[i]);
+ vk->DestroySwapchainKHR(vk->dev, p->swapchain, PL_VK_ALLOC);
+ }
+
// Calling `vkCreateSwapchainKHR` puts sinfo.oldSwapchain into a retired
// state whether the call succeeds or not, so we always need to garbage
// collect it afterwards - asynchronously as it may still be in use
- sinfo.oldSwapchain = p->swapchain;
+ /* sinfo.oldSwapchain = p->swapchain; */
p->swapchain = VK_NULL_HANDLE;
VkResult res = vk->CreateSwapchainKHR(vk->dev, &sinfo, PL_VK_ALLOC, &p->swapchain);
- vk_dev_callback(vk, VK_CB_FUNC(destroy_swapchain), vk, vk_wrap_handle(sinfo.oldSwapchain));
+ /* vk_dev_callback(vk, VK_CB_FUNC(destroy_swapchain), vk, vk_wrap_handle(sinfo.oldSwapchain)); */
PL_VK_ASSERT(res, "vk->CreateSwapchainKHR(...)");
// Get the new swapchain images
--
2.43.0
@@ -4,19 +4,21 @@
<metadata_license>CC0-1.0</metadata_license> <metadata_license>CC0-1.0</metadata_license>
<project_license>GPL-3.0+</project_license> <project_license>GPL-3.0+</project_license>
<name>Moonlight</name> <name>Moonlight</name>
<summary>Stream games and other applications from another PC running Sunshine or GeForce Experience</summary> <summary>Play games remotely from your NVIDIA GameStream-enabled PC</summary>
<description> <description>
<p>Moonlight can stream games and other applications from another PC with Sunshine or GeForce Experience software installed.</p> <p>Moonlight can stream games and other applications from a PC with an NVIDIA GeForce GTX/RTX or NVIDIA Quadro GPU and GeForce/Quadro Experience installed.</p>
<p>Features include:</p> <p>Features include:</p>
<ul> <ul>
<li>Streaming at up to 4K resolution</li>
<li>Support for up to 120 FPS streaming (high refresh rate monitor recommended)</li>
<li>Hardware accelerated video decoding with VAAPI, VDPAU, and NVDEC support</li> <li>Hardware accelerated video decoding with VAAPI, VDPAU, and NVDEC support</li>
<li>H.264, HEVC, and AV1 codec support</li>
<li>7.1 surround sound audio support</li> <li>7.1 surround sound audio support</li>
<li>10-point multitouch support</li>
<li>Gamepad support with force feedback for up to 16 players</li>
<li>Support for both pointer capture (for games) and direct mouse control (for remote desktop)</li> <li>Support for both pointer capture (for games) and direct mouse control (for remote desktop)</li>
<li>Support for passing system-wide keyboard shortcuts like Alt+Tab to the host</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>Gamepad support with force feedback for up to 4 players</li>
</ul> </ul>
</description> </description>
@@ -34,222 +36,6 @@
</screenshots> </screenshots>
<releases> <releases>
<release version="6.1.0" date="2024-09-16">
<description>
<p>New features:</p>
<ul>
<li>Experimental YUV 4:4:4 support for improved text clarity during remote desktop usage</li>
<li>HDR is now supported with software decoding (requires GPU with Vulkan support)</li>
<li>Bitrate limit can now be increased to 500 Mbps</li>
<li>Audio decoding now uses 32-bit floating point format</li>
</ul>
<p>Bugfixes:</p>
<ul>
<li>Fixed incorrect color range when streaming with AV1 from a host with an AMD GPU</li>
<li>Updated community-contributed translations from Weblate</li>
</ul>
</description>
</release>
<release version="6.0.1" date="2024-06-30">
<description>
<p>New features:</p>
<ul>
<li>HDR to SDR tonemapping is now supported by the Vulkan renderer on Linux</li>
</ul>
<p>Behavior Changes:</p>
<ul>
<li>More time is now provided for PCs to wake up before displaying an error when starting a stream from the CLI</li>
<li>Moonlight will no longer try to guess VA driver names when running libva 2.20 or later</li>
</ul>
<p>Bugfixes:</p>
<ul>
<li>Fixed crash when the Vulkan renderer executed on a system with no Vulkan devices</li>
<li>Updated community-contributed translations from Weblate</li>
</ul>
</description>
</release>
<release version="6.0.0" date="2024-06-06">
<description>
<p>New features:</p>
<ul>
<li>HDR is now supported on Steam Deck and other HDR-capable Linux systems using a new Vulkan-based renderer</li>
<li>Vulkan Video decoding of H.264, HEVC, and AV1 is now supported using the new Vulkan renderer</li>
<li>Full E2E stream encryption is now supported when streaming from Sunshine v0.22 or later</li>
<li>Initiating a stream via the command-line will now automatically wake the target machine if it is sleeping</li>
<li>Error codes are now displayed in the connection termination dialog</li>
<li>Detailed PC state information is now available via the new "View Details" context menu option (similar to the Android client)</li>
<li>The performance overlay can now be configured to be shown by default when starting a stream</li>
</ul>
<p>Behavior Changes:</p>
<ul>
<li>The selected bitrate is no longer silently adjusted by Moonlight based upon the video codec or HDR mode</li>
<li>The selected mouse mode when starting a stream no longer influences the acceleration curve when the mouse mode is toggled using the Ctrl+Alt+Shift+M key combo while streaming</li>
<li>Hardcoded fallback paths for VAAPI and VDPAU drivers are no longer used</li>
<li>Per-session keys (rikey and rikeyid values) are now automatically stripped from logs</li>
<li>Log messages will now be logged to stderr rather than stdout</li>
<li>EGL is now always used instead of GLX for loading OpenGL in X11 environments on non-x86 platforms</li>
</ul>
<p>Bugfixes:</p>
<ul>
<li>Improved reliability of streaming during momentary drops of WiFi connections</li>
<li>Improved performance with high polling rate mice</li>
<li>Improved input latency when a gamepad is connected</li>
<li>Added missing command-line options to control HDR and AV1 codec usage</li>
<li>Fixed instant connection termination error when streaming over the Internet behind certain Carrier-Grade NAT devices</li>
<li>Fixed incorrect MTU detection when streaming over the Internet without a VPN</li>
<li>Fixed recovery of audio when no audio output device is present when the stream starts</li>
<li>Fixed merging of gamepad state when multi-controller mode is disabled</li>
<li>Fixed indefinitely resending mDNS queries when a host could not be resolved</li>
<li>Fixed inability to pair when the host is not using a RSA-2048 key</li>
<li>Updated included gamepad mappings</li>
<li>Updated community-contributed translations from Weblate</li>
</ul>
</description>
</release>
<release version="5.0.1" date="2023-11-02">
<description>
<p>Bugfixes:</p>
<ul>
<li>Fixed connection issue streaming from some hosts running GeForce Experience or Sunshine v0.20 and earlier</li>
<li>Updated community-contributed translations from Weblate</li>
</ul>
</description>
</release>
<release version="5.0.0" date="2023-10-18">
<description>
<p>New features:</p>
<ul>
<li>Custom FPS values</li>
<li>Streaming using the AV1 codec from Sunshine hosts</li>
<li>Horizontal scrolling with Sunshine hosts</li>
<li>Native multi-touch input with Sunshine hosts</li>
<li>Controller type information is passed to Sunshine for controller type autoselection</li>
<li>Extended features of PS4/PS5 controllers (touchpad, battery state, gyro/accelerometer motion, and RGB LED) are natively passed to Sunshine hosts</li>
<li>Support for up to 16 gamepads with Sunshine hosts</li>
<li>Frame pacing support on Wayland</li>
<li>Host-side processing latency is now displayed in the performance overlay with Sunshine hosts</li>
<li>Window title now includes the PC name when streaming</li>
<li>Alphabetical sorting of the PC list</li>
<li>Updated Play and Stop button design to improve contrast with app box art</li>
<li>The settings page automatically scrolls to the selected element when using keyboard or gamepad navigation</li>
<li>Pressing Share+PS buttons on PS4/PS5 controllers can be used to emulate a clickpad press</li>
<li>Improved audio quality when streaming with a high bitrate from remote hosts</li>
<li>Increased mouse polling rate</li>
</ul>
<p>Behavior Changes:</p>
<ul>
<li>IPv6 addresses must be provided in URL-escaped form. Example: [2001:db8::1234:5678] or [2001:db8::1234:5678]:47989</li>
<li>Default bitrate logic has been adjusted to behave better with custom resolutions and frame rates</li>
</ul>
<p>Bugfixes:</p>
<ul>
<li>Reduced the frequency of audio underruns (clicks and pops) by increasing audio buffer size</li>
<li>Fixed "Quitting App" message never dismissing in some cases</li>
<li>Fixed parsing IPv6 literals with port numbers</li>
<li>Fixed unnecessary WiFi activity causing stream disruptions</li>
<li>Fixed paired hosts being forgotten if Moonlight terminates while writing hosts to disk</li>
<li>Fixed freeze when closing the stream on Windows with some software that uses DX11 hooks</li>
<li>Fixed delayed PC wake when activating Wake-On-LAN feature</li>
<li>Fixed Wake-on-LAN when Sunshine is running on alternate ports</li>
<li>Fixed leak of /dev/dri fds when trying to initialize VAAPI decoder</li>
<li>Fixed support for the Applications/Menu key</li>
<li>Fixed automatic language detection for Simplified Chinese and Traditional Chinese</li>
<li>Fixed streaming from Sunshine when the client PC has multiple network interfaces or IPv6 privacy extensions enabled</li>
<li>Fixed small receive buffer causing video packet loss on some systems</li>
<li>Fixed theme overrides causing the UI to be unusable on some systems</li>
<li>Fixed tearing with V-Sync enabled on macOS and Linux when using the SDL renderer (software decoding and some hardware decoders)</li>
<li>Fixed stateful V4L2 hardware decoding using h264_v4l2m2m and hevc_v4l2m2m decoders</li>
<li>Fixed CVE-2023-42801</li>
<li>Updated included gamepad mappings</li>
<li>Updated community-contributed translations from Weblate</li>
</ul>
</description>
</release>
<release version="4.3.1" date="2022-11-03">
<description>
<p>Changes:</p>
<ul>
<li>Fixed crash during specific patterns of audio packet loss</li>
<li>Fixed detection of local streaming when using IPv6 addresses</li>
<li>Windowed mode streaming now starts maximized if the GUI window was maximized</li>
<li>Updated community-contributed translations from Weblate</li>
</ul>
</description>
</release>
<release version="4.3.0" date="2022-10-28">
<description>
<p>New features:</p>
<ul>
<li>Improved support for GeForce Experience 3.26</li>
<li>Improved image quality and performance when recovering from video packet loss using HEVC</li>
<li>Improved error concealment after audio packet loss</li>
<li>Improved UI performance when paired with many hosts or hosts with large numbers of apps</li>
<li>Pairing with Sunshine hosts is now supported even if the host is in a streaming session</li>
</ul>
<p>Bugfixes:</p>
<ul>
<li>Fixed streaming at resolutions below 720x480 with GeForce Experience 3.26</li>
<li>Fixed window position and size being lost when toggling between full-screen and windowed mode</li>
<li>Fixed initial window size not always respecting the stream aspect ratio</li>
<li>Fixed unexpected error when streaming at resolutions above 4096x4096 using Sunshine</li>
<li>Updated included gamepad mappings</li>
<li>Updated community-contributed translations from Weblate</li>
</ul>
</description>
</release>
<release version="4.2.1" date="2022-09-12">
<description>
<p>Bugfixes:</p>
<ul>
<li>Fixed streaming from hosts running Sunshine</li>
<li>Updated community-contributed translations from Weblate</li>
</ul>
</description>
</release>
<release version="4.2.0" date="2022-09-10">
<description>
<p>New features:</p>
<ul>
<li>Added support for pairing via the terminal</li>
<li>Added support for listing installed apps on a host via the terminal</li>
<li>Improved handling of V-Sync on Wayland systems to reduce latency</li>
<li>Improved performance using the legacy non-EGL VAAPI rendering code</li>
</ul>
<p>Bugfixes:</p>
<ul>
<li>Fixed native resolution detection on high DPI Wayland systems</li>
<li>Fixed audio crackling with SDL 2.24.0 on systems using PulseAudio</li>
<li>Updated included gamepad mappings</li>
<li>Updated community-contributed translations from Weblate</li>
</ul>
</description>
</release>
<release version="4.1.0" date="2022-05-23">
<description>
<p>New features:</p>
<ul>
<li>Overlays are now supported on the non-EGL X11 VAAPI renderer (requires RGBA subpicture support in the VAAPI driver)</li>
<li>Cursor lock can now be disabled when in full-screen mode</li>
</ul>
<p>Behavior Changes:</p>
<ul>
<li>Moonlight can now reduce the client display resolution in full-screen mode if the current display mode doesn't have a high enough refresh rate for the selected stream frame rate</li>
<li>When choosing a new client display resolution in full-screen mode, Moonlight will pick a display mode with the closest aspect ratio to the selected stream resolution</li>
<li>Moonlight now defaults to borderless windowed mode on Wayland systems</li>
</ul>
<p>Bugfixes:</p>
<ul>
<li>Fixed missing toolbar buttons in certain languages</li>
<li>Fixed some text on the settings page not updating when the language is changed</li>
<li>Fixed a frame pacing bug that could cause excessive frame drops</li>
<li>Fixed the "keep display awake" setting not being saved when Moonlight restarts</li>
<li>Fixed black screen after unmaximizing the Moonlight window on some Linux systems</li>
<li>Updated included gamepad mappings</li>
<li>Updated community-contributed translations from Weblate</li>
</ul>
</description>
</release>
<release version="4.0.0" date="2022-04-02"> <release version="4.0.0" date="2022-04-02">
<description> <description>
<p>New features:</p> <p>New features:</p>
@@ -1,9 +1,9 @@
[Desktop Entry] [Desktop Entry]
Name=Moonlight Name=Moonlight
Comment=Stream games and other applications from another PC running Sunshine or GeForce Experience Comment=Stream games from your NVIDIA GameStream-enabled PC
Exec=moonlight Exec=moonlight
Icon=moonlight Icon=moonlight
Terminal=false Terminal=false
Type=Application Type=Application
Categories=Qt;Game; Categories=Qt;Game;
Keywords=nvidia;gamestream;stream;sunshine;remote play; Keywords=nvidia;gamestream;stream;
+1 -5
View File
@@ -5,13 +5,9 @@
# pairing data will be lost each reboot. # pairing data will be lost each reboot.
HOME=/usr/local/moonlight HOME=/usr/local/moonlight
# Enable Steam Controller detection in SDL
# https://github.com/moonlight-stream/moonlight-qt/issues/697
export SDL_ENABLE_STEAM_CONTROLLERS=1
# Renice PE_Single_CPU which seems to host A/V stuff # Renice PE_Single_CPU which seems to host A/V stuff
renice -10 -p $(pidof PE_Single_CPU) renice -10 -p $(pidof PE_Single_CPU)
# Renice Moonlight itself to avoid preemption by background tasks # Renice Moonlight itself to avoid preemption by background tasks
# Write output to a logfile in /tmp # Write output to a logfile in /tmp
exec nice -n -10 ./bin/moonlight > /tmp/moonlight.log 2>&1 exec nice -n -10 ./bin/moonlight > /tmp/moonlight.log
+14 -16
View File
@@ -1,6 +1,5 @@
import QtQuick 2.9 import QtQuick 2.9
import QtQuick.Controls 2.2 import QtQuick.Controls 2.2
import QtQuick.Controls.Material 2.2
import AppModel 1.0 import AppModel 1.0
import ComputerManager 1.0 import ComputerManager 1.0
@@ -90,7 +89,7 @@ CenteredGridView {
onSourceSizeChanged: { onSourceSizeChanged: {
// Nearly all of Nvidia's official box art does not match the dimensions of placeholder // Nearly all of Nvidia's official box art does not match the dimensions of placeholder
// images, however the one known exception is Overcooked. Therefore, we only execute // 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 // 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. // supported games all have box art, so this check is not required.
if (!model.isAppCollectorGame && if (!model.isAppCollectorGame &&
@@ -122,15 +121,15 @@ CenteredGridView {
anchors.fill: appIcon anchors.fill: appIcon
sourceComponent: Item { sourceComponent: Item {
RoundButton { ToolButton {
anchors.horizontalCenterOffset: appIcon.isPlaceholder ? -47 : 0 anchors.horizontalCenterOffset: appIcon.isPlaceholder ? -47 : 0
anchors.verticalCenterOffset: appIcon.isPlaceholder ? -75 : -60 anchors.verticalCenterOffset: appIcon.isPlaceholder ? -75 : -60
anchors.centerIn: parent anchors.centerIn: parent
implicitWidth: 85 implicitWidth: 125
implicitHeight: 85 implicitHeight: 125
Image { Image {
source: "qrc:/res/play_arrow_FILL1_wght700_GRAD200_opsz48.svg" source: "qrc:/res/baseline-play_circle_filled_white-48px.svg"
anchors.centerIn: parent anchors.centerIn: parent
sourceSize { sourceSize {
width: 75 width: 75
@@ -146,19 +145,17 @@ CenteredGridView {
ToolTip.delay: 1000 ToolTip.delay: 1000
ToolTip.timeout: 3000 ToolTip.timeout: 3000
ToolTip.visible: hovered ToolTip.visible: hovered
Material.background: "#D0808080"
} }
RoundButton { ToolButton {
anchors.horizontalCenterOffset: appIcon.isPlaceholder ? 47 : 0 anchors.horizontalCenterOffset: appIcon.isPlaceholder ? 47 : 0
anchors.verticalCenterOffset: appIcon.isPlaceholder ? -75 : 60 anchors.verticalCenterOffset: appIcon.isPlaceholder ? -75 : 60
anchors.centerIn: parent anchors.centerIn: parent
implicitWidth: 85 implicitWidth: 125
implicitHeight: 85 implicitHeight: 125
Image { Image {
source: "qrc:/res/stop_FILL1_wght700_GRAD200_opsz48.svg" source: "qrc:/res/baseline-cancel-24px.svg"
anchors.centerIn: parent anchors.centerIn: parent
sourceSize { sourceSize {
width: 75 width: 75
@@ -174,8 +171,6 @@ CenteredGridView {
ToolTip.delay: 1000 ToolTip.delay: 1000
ToolTip.timeout: 3000 ToolTip.timeout: 3000
ToolTip.visible: hovered ToolTip.visible: hovered
Material.background: "#D0808080"
} }
} }
} }
@@ -256,7 +251,7 @@ CenteredGridView {
anchors.fill: parent anchors.fill: parent
acceptedButtons: Qt.RightButton; acceptedButtons: Qt.RightButton;
onClicked: { onClicked: {
parent.pressAndHold() parent.onPressAndHold()
} }
} }
@@ -350,7 +345,7 @@ CenteredGridView {
function quitApp() { function quitApp() {
var component = Qt.createComponent("QuitSegue.qml") var component = Qt.createComponent("QuitSegue.qml")
var params = {"appName": appName, "quitRunningAppFn": function() { appModel.quitRunningApp() }} var params = {"appName": appName}
if (segueToStream) { if (segueToStream) {
// Store the session and app name if we're going to stream after // Store the session and app name if we're going to stream after
// successfully quitting the old app. // successfully quitting the old app.
@@ -363,6 +358,9 @@ CenteredGridView {
} }
stackView.push(component.createObject(stackView, params)) stackView.push(component.createObject(stackView, params))
// Trigger the quit after pushing the quit segue on screen
appModel.quitRunningApp()
} }
onAccepted: quitApp() onAccepted: quitApp()
-7
View File
@@ -2,7 +2,6 @@ import QtQuick 2.9
import QtQuick.Controls 2.2 import QtQuick.Controls 2.2
import SdlGamepadKeyNavigation 1.0 import SdlGamepadKeyNavigation 1.0
import SystemProperties 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 {
@@ -39,12 +38,6 @@ ComboBox {
popup.onAboutToShow: { popup.onAboutToShow: {
// Switch to normal navigation for combo boxes // Switch to normal navigation for combo boxes
SdlGamepadKeyNavigation.setUiNavMode(false) SdlGamepadKeyNavigation.setUiNavMode(false)
// Override the popup color to improve contrast with the overridden
// Material 2 background color set in main.qml.
if (SystemProperties.usesMaterial3Theme) {
popup.background.color = "#424242"
}
} }
popup.onAboutToHide: { popup.onAboutToHide: {
-86
View File
@@ -1,86 +0,0 @@
import QtQuick 2.0
import QtQuick.Controls 2.2
import ComputerManager 1.0
Item {
function onSearchingComputer() {
stageLabel.text = qsTr("Establishing connection to PC...")
}
function onPairing(pcName, pin) {
stageLabel.text = qsTr("Pairing... Please enter '%1' on %2.").arg(pin).arg(pcName)
}
function onFailed(message) {
stageIndicator.visible = false
errorDialog.text = message
errorDialog.open()
}
function onSuccess(appName) {
stageIndicator.visible = false
pairCompleteDialog.open()
}
// Allow user to back out of pairing
Keys.onEscapePressed: {
Qt.quit()
}
Keys.onBackPressed: {
Qt.quit()
}
Keys.onCancelPressed: {
Qt.quit()
}
StackView.onActivated: {
if (!launcher.isExecuted()) {
toolBar.visible = false
launcher.searchingComputer.connect(onSearchingComputer)
launcher.pairing.connect(onPairing)
launcher.failed.connect(onFailed)
launcher.success.connect(onSuccess)
launcher.execute(ComputerManager)
}
}
Row {
anchors.centerIn: parent
spacing: 5
id: stageIndicator
BusyIndicator {
id: stageSpinner
}
Label {
id: stageLabel
height: stageSpinner.height
font.pointSize: 20
verticalAlignment: Text.AlignVCenter
wrapMode: Text.Wrap
}
}
ErrorMessageDialog {
id: errorDialog
onClosed: {
Qt.quit();
}
}
NavigableMessageDialog {
id: pairCompleteDialog
closePolicy: Popup.CloseOnEscape
text:qsTr("Pairing completed successfully")
standardButtons: Dialog.Ok
onClosed: {
Qt.quit()
}
}
}
+8 -2
View File
@@ -2,6 +2,7 @@ import QtQuick 2.0
import QtQuick.Controls 2.2 import QtQuick.Controls 2.2
import ComputerManager 1.0 import ComputerManager 1.0
import SdlGamepadKeyNavigation 1.0
Item { Item {
function onSearchingComputer() { function onSearchingComputer() {
@@ -25,7 +26,6 @@ Item {
function onLaunchFailed(message) { function onLaunchFailed(message) {
errorDialog.text = message errorDialog.text = message
errorDialog.open() errorDialog.open()
console.error(message)
} }
function onAppQuitRequired(appName) { function onAppQuitRequired(appName) {
@@ -37,6 +37,10 @@ Item {
if (!launcher.isExecuted()) { if (!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)
@@ -80,8 +84,10 @@ Item {
function quitApp() { function quitApp() {
var component = Qt.createComponent("QuitSegue.qml") var component = Qt.createComponent("QuitSegue.qml")
var params = {"appName": appName, "quitRunningAppFn": function() { launcher.quitRunningApp() }} var params = {"appName": appName}
stackView.push(component.createObject(stackView, params)) stackView.push(component.createObject(stackView, params))
// Trigger the quit after pushing the quit segue on screen
launcher.quitRunningApp()
} }
onAccepted: quitApp() onAccepted: quitApp()
+35 -44
View File
@@ -6,7 +6,6 @@ import ComputerModel 1.0
import ComputerManager 1.0 import ComputerManager 1.0
import StreamingPreferences 1.0 import StreamingPreferences 1.0
import SystemProperties 1.0
import SdlGamepadKeyNavigation 1.0 import SdlGamepadKeyNavigation 1.0
CenteredGridView { CenteredGridView {
@@ -34,6 +33,11 @@ CenteredGridView {
// Setup signals on CM // Setup signals on CM
ComputerManager.computerAddCompleted.connect(addComplete) ComputerManager.computerAddCompleted.connect(addComplete)
// This is a bit of a hack to do this here as opposed to main.qml, but
// we need it enabled before calling getConnectedGamepads() and PcView
// is never destroyed, so it should be okay.
SdlGamepadKeyNavigation.enable()
// Highlight the first item if a gamepad is connected // Highlight the first item if a gamepad is connected
if (currentIndex == -1 && SdlGamepadKeyNavigation.getConnectedGamepads() > 0) { if (currentIndex == -1 && SdlGamepadKeyNavigation.getConnectedGamepads() > 0) {
currentIndex = 0 currentIndex = 0
@@ -95,7 +99,7 @@ CenteredGridView {
Label { Label {
height: searchSpinner.height height: searchSpinner.height
elide: Label.ElideRight elide: Label.ElideRight
text: StreamingPreferences.enableMdns ? qsTr("Searching for compatible hosts on your local network...") 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.") : qsTr("Automatic PC discovery is disabled. Add your PC manually.")
font.pointSize: 20 font.pointSize: 20
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
@@ -126,12 +130,12 @@ CenteredGridView {
id: stateIcon id: stateIcon
anchors.horizontalCenter: pcIcon.horizontalCenter anchors.horizontalCenter: pcIcon.horizontalCenter
anchors.verticalCenter: pcIcon.verticalCenter anchors.verticalCenter: pcIcon.verticalCenter
anchors.verticalCenterOffset: !model.online ? -18 : -16 anchors.verticalCenterOffset: -15
visible: !model.statusUnknown && (!model.online || !model.paired) visible: !model.statusUnknown && (!model.online || !model.paired)
source: !model.online ? "qrc:/res/warning_FILL1_wght300_GRAD200_opsz24.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: !model.online ? 75 : 70 width: 75
height: !model.online ? 75 : 70 height: 75
} }
} }
@@ -207,18 +211,10 @@ CenteredGridView {
text: qsTr("Delete PC") text: qsTr("Delete PC")
onTriggered: { onTriggered: {
deletePcDialog.pcIndex = index deletePcDialog.pcIndex = index
deletePcDialog.pcName = model.name // get confirmation first, actual closing is called from the dialog
deletePcDialog.open() deletePcDialog.open()
} }
} }
NavigableMenuItem {
parentMenu: pcContextMenu
text: qsTr("View Details")
onTriggered: {
showPcDetailsDialog.pcDetails = model.details
showPcDetailsDialog.open()
}
}
} }
} }
@@ -236,14 +232,22 @@ CenteredGridView {
stackView.push(appView) stackView.push(appView)
} }
else { else {
var pin = computerModel.generatePinString() if (!model.busy) {
var pin = computerModel.generatePinString()
// Kick off pairing in the background // Kick off pairing in the background
computerModel.pairComputer(index, pin) computerModel.pairComputer(index, pin)
// Display the pairing dialog // Display the pairing dialog
pairDialog.pin = pin pairDialog.pin = pin
pairDialog.open() pairDialog.open()
}
else {
// cannot pair while something is streaming or attempting to pair
errorDialog.text = qsTr("You cannot pair while a previous session is still running on the host PC. Quit any running games or reboot the host PC, then try pairing again.")
errorDialog.helpText = ""
errorDialog.open()
}
} }
} else if (!model.online) { } else if (!model.online) {
// Using open() here because it may be activated by keyboard // Using open() here because it may be activated by keyboard
@@ -266,7 +270,7 @@ CenteredGridView {
anchors.fill: parent anchors.fill: parent
acceptedButtons: Qt.RightButton; acceptedButtons: Qt.RightButton;
onClicked: { onClicked: {
parent.pressAndHold() parent.onPressAndHold()
} }
} }
@@ -275,12 +279,6 @@ CenteredGridView {
// the ItemDelegate and not where the mouse cursor is // the ItemDelegate and not where the mouse cursor is
pcContextMenu.open() pcContextMenu.open()
} }
Keys.onDeletePressed: {
deletePcDialog.pcIndex = index
deletePcDialog.pcName = model.name
deletePcDialog.open()
}
} }
ErrorMessageDialog { ErrorMessageDialog {
@@ -301,8 +299,7 @@ CenteredGridView {
// don't allow edits to the rest of the window while open // don't allow edits to the rest of the window while open
property string pin : "0000" property string pin : "0000"
text:qsTr("Please enter %1 on your host PC. This dialog will close when pairing is completed.").arg(pin)+"\n\n"+ text:qsTr("Please enter %1 on your GameStream PC. This dialog will close when pairing is completed.").arg(pin)
qsTr("If your host PC is running Sunshine, navigate to the Sunshine web UI to enter the PIN.")
standardButtons: Dialog.Cancel standardButtons: Dialog.Cancel
onRejected: { onRejected: {
// FIXME: We should interrupt pairing here // FIXME: We should interrupt pairing here
@@ -312,14 +309,16 @@ CenteredGridView {
NavigableMessageDialog { NavigableMessageDialog {
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
property int pcIndex : -1 property int pcIndex : -1;
property string pcName : "" text:qsTr("Are you sure you want to remove this PC?")
text: qsTr("Are you sure you want to remove '%1'?").arg(pcName)
standardButtons: Dialog.Yes | Dialog.No standardButtons: Dialog.Yes | Dialog.No
onAccepted: { function deletePc() {
computerModel.deleteComputer(pcIndex) console.log("deleting PC pairing for PC at index: " + pcIndex)
computerModel.deleteComputer(pcIndex);
} }
onAccepted: deletePc()
} }
NavigableMessageDialog { NavigableMessageDialog {
@@ -328,7 +327,7 @@ CenteredGridView {
standardButtons: Dialog.Ok standardButtons: Dialog.Ok
onAboutToShow: { onAboutToShow: {
testConnectionDialog.text = qsTr("Moonlight is testing your network connection to determine if any required ports are blocked.") + "\n\n" + qsTr("This may take a few seconds…") testConnectionDialog.text = qsTr("Moonlight is testing your network connection to determine if NVIDIA GameStream is blocked.") + "\n\n" + qsTr("This may take a few seconds…")
showSpinner = true showSpinner = true
} }
@@ -399,13 +398,5 @@ CenteredGridView {
} }
} }
NavigableMessageDialog {
id: showPcDetailsDialog
property string pcDetails : "";
text: showPcDetailsDialog.pcDetails
imageSrc: "qrc:/res/baseline-help_outline-24px.svg"
standardButtons: Dialog.Ok
}
ScrollBar.vertical: ScrollBar {} ScrollBar.vertical: ScrollBar {}
} }
-7
View File
@@ -6,7 +6,6 @@ import Session 1.0
Item { Item {
property string appName property string appName
property var quitRunningAppFn
property Session nextSession : null property Session nextSession : null
property string nextAppName : "" property string nextAppName : ""
@@ -18,7 +17,6 @@ Item {
if (error !== undefined) { if (error !== undefined) {
errorDialog.text = error errorDialog.text = error
errorDialog.open() errorDialog.open()
console.error(error)
} }
// 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
@@ -39,11 +37,6 @@ Item {
// Connect the quit completion signal // Connect the quit completion signal
ComputerManager.quitAppCompleted.connect(quitAppCompleted) ComputerManager.quitAppCompleted.connect(quitAppCompleted)
// Start the quit operation if requested
if (quitRunningAppFn) {
quitRunningAppFn()
}
} }
StackView.onDeactivating: { StackView.onDeactivating: {
+235 -459
View File
@@ -1,7 +1,6 @@
import QtQuick 2.9 import QtQuick 2.9
import QtQuick.Controls 2.2 import QtQuick.Controls 2.2
import QtQuick.Layouts 1.2 import QtQuick.Layouts 1.2
import QtQuick.Window 2.2
import StreamingPreferences 1.0 import StreamingPreferences 1.0
import ComputerManager 1.0 import ComputerManager 1.0
@@ -12,8 +11,6 @@ Flickable {
id: settingsPage id: settingsPage
objectName: qsTr("Settings") objectName: qsTr("Settings")
signal languageChanged()
boundsBehavior: Flickable.OvershootBounds boundsBehavior: Flickable.OvershootBounds
contentWidth: settingsColumn1.width > settingsColumn2.width ? settingsColumn1.width : settingsColumn2.width contentWidth: settingsColumn1.width > settingsColumn2.width ? settingsColumn1.width : settingsColumn2.width
@@ -26,49 +23,6 @@ Flickable {
} }
} }
function isChildOfFlickable(item) {
while (item) {
if (item.parent === contentItem) {
return true
}
item = item.parent
}
return false
}
NumberAnimation on contentY {
id: autoScrollAnimation
duration: 100
}
Window.onActiveFocusItemChanged: {
var item = Window.activeFocusItem
if (item) {
// Ignore non-child elements like the toolbar buttons
if (!isChildOfFlickable(item)) {
return
}
// Map the focus item's position into our content item's coordinate space
var pos = item.mapToItem(contentItem, 0, 0)
// Ensure some extra space is visible around the element we're scrolling to
var scrollMargin = height > 100 ? 50 : 0
if (pos.y - scrollMargin < contentY) {
autoScrollAnimation.from = contentY
autoScrollAnimation.to = Math.max(pos.y - scrollMargin, 0)
autoScrollAnimation.start()
}
else if (pos.y + item.height + scrollMargin > contentY + height) {
autoScrollAnimation.from = contentY
autoScrollAnimation.to = Math.min(pos.y + item.height + scrollMargin - height, contentHeight - height)
autoScrollAnimation.start()
}
}
}
StackView.onActivated: { StackView.onActivated: {
// This enables Tab and BackTab based navigation rather than arrow keys. // This enables Tab and BackTab based navigation rather than arrow keys.
// It is required to shift focus between controls on the settings page. // It is required to shift focus between controls on the settings page.
@@ -133,54 +87,59 @@ Flickable {
AutoResizingComboBox { AutoResizingComboBox {
property int lastIndexValue property int lastIndexValue
function addDetectedResolution(friendlyNamePrefix, rect) {
var indexToAdd = 0
for (var j = 0; j < resolutionComboBox.count; j++) {
var existing_width = parseInt(resolutionListModel.get(j).video_width);
var existing_height = parseInt(resolutionListModel.get(j).video_height);
if (rect.width === existing_width && rect.height === existing_height) {
// Duplicate entry, skip
indexToAdd = -1
break
}
else if (rect.width * rect.height > existing_width * existing_height) {
// Candidate entrypoint after this entry
indexToAdd = j + 1
}
}
// Insert this display's resolution if it's not a duplicate
if (indexToAdd >= 0) {
resolutionListModel.insert(indexToAdd,
{
"text": friendlyNamePrefix+" ("+rect.width+"x"+rect.height+")",
"video_width": ""+rect.width,
"video_height": ""+rect.height,
"is_custom": false
})
}
}
// ignore setting the index at first, and actually set it when the component is loaded // ignore setting the index at first, and actually set it when the component is loaded
Component.onCompleted: { Component.onCompleted: {
// Refresh display data before using it to build the list // Refresh display data before using it to build the list
SystemProperties.refreshDisplays() SystemProperties.refreshDisplays()
// Add native and safe area resolutions for all attached displays // Add native resolutions for all attached displays
var done = false var done = false
for (var displayIndex = 0; !done; displayIndex++) { for (var displayIndex = 0; !done; displayIndex++) {
var screenRect = SystemProperties.getNativeResolution(displayIndex); for (var displayResIndex = 0; displayResIndex < 2; displayResIndex++) {
var safeAreaRect = SystemProperties.getSafeAreaResolution(displayIndex); var screenRect;
if (screenRect.width === 0) { // Some platforms have different desktop resolutions
// Exceeded max count of displays // and native resolutions (like macOS with Retina displays)
done = true if (displayResIndex === 0) {
break screenRect = SystemProperties.getDesktopResolution(displayIndex)
}
else {
screenRect = SystemProperties.getNativeResolution(displayIndex)
}
if (screenRect.width === 0) {
// Exceeded max count of displays
done = true
break
}
var indexToAdd = 0
for (var j = 0; j < resolutionComboBox.count; j++) {
var existing_width = parseInt(resolutionListModel.get(j).video_width);
var existing_height = parseInt(resolutionListModel.get(j).video_height);
if (screenRect.width === existing_width && screenRect.height === existing_height) {
// Duplicate entry, skip
indexToAdd = -1
break
}
else if (screenRect.width * screenRect.height > existing_width * existing_height) {
// Candidate entrypoint after this entry
indexToAdd = j + 1
}
}
// Insert this display's resolution if it's not a duplicate
if (indexToAdd >= 0) {
resolutionListModel.insert(indexToAdd,
{
"text": "Native ("+screenRect.width+"x"+screenRect.height+")",
"video_width": ""+screenRect.width,
"video_height": ""+screenRect.height,
"is_custom": false
})
}
} }
addDetectedResolution(qsTr("Native"), screenRect)
addDetectedResolution(qsTr("Native (Excluding Notch)"), safeAreaRect)
} }
// Prune resolutions that are over the decoder's maximum // Prune resolutions that are over the decoder's maximum
@@ -216,7 +175,7 @@ Flickable {
if (!index_set) { if (!index_set) {
// We did not find a match. This must be a custom resolution. // We did not find a match. This must be a custom resolution.
resolutionListModel.append({ resolutionListModel.append({
"text": qsTr("Custom")+" ("+StreamingPreferences.width+"x"+StreamingPreferences.height+")", "text": "Custom ("+StreamingPreferences.width+"x"+StreamingPreferences.height+")",
"video_width": ""+StreamingPreferences.width, "video_width": ""+StreamingPreferences.width,
"video_height": ""+StreamingPreferences.height, "video_height": ""+StreamingPreferences.height,
"is_custom": true "is_custom": true
@@ -225,7 +184,7 @@ Flickable {
} }
else { else {
resolutionListModel.append({ resolutionListModel.append({
"text": qsTr("Custom"), "text": "Custom",
"video_width": "", "video_width": "",
"video_height": "", "video_height": "",
"is_custom": true "is_custom": true
@@ -283,8 +242,7 @@ Flickable {
StreamingPreferences.bitrateKbps = StreamingPreferences.getDefaultBitrate(StreamingPreferences.width, StreamingPreferences.bitrateKbps = StreamingPreferences.getDefaultBitrate(StreamingPreferences.width,
StreamingPreferences.height, StreamingPreferences.height,
StreamingPreferences.fps, StreamingPreferences.fps);
StreamingPreferences.enableYUV444);
slider.value = StreamingPreferences.bitrateKbps slider.value = StreamingPreferences.bitrateKbps
} }
@@ -439,212 +397,74 @@ Flickable {
} }
AutoResizingComboBox { AutoResizingComboBox {
property int lastIndexValue function createModel() {
var fpsListModel = Qt.createQmlObject('import QtQuick 2.0; ListModel {}', parent, '')
function updateBitrateForSelection() { var max_fps = SystemProperties.maximumStreamingFrameRate
// Only modify the bitrate if the values actually changed
var selectedFps = parseInt(model.get(fpsComboBox.currentIndex).video_fps)
if (StreamingPreferences.fps !== selectedFps) {
StreamingPreferences.fps = selectedFps
StreamingPreferences.bitrateKbps = StreamingPreferences.getDefaultBitrate(StreamingPreferences.width, // Default entries
StreamingPreferences.height, fpsListModel.append({"text": qsTr("%1 FPS").arg("30"), "video_fps": "30"})
StreamingPreferences.fps, fpsListModel.append({"text": qsTr("%1 FPS").arg("60"), "video_fps": "60"})
StreamingPreferences.enableYUV444);
slider.value = StreamingPreferences.bitrateKbps
}
lastIndexValue = currentIndex // Add unsupported FPS values that come before the display max FPS
} if (StreamingPreferences.unsupportedFps) {
if (max_fps > 90) {
NavigableDialog { fpsListModel.append({"text": qsTr("%1 FPS (Unsupported)").arg("90"), "video_fps": "90"})
function isInputValid() {
// If we have text that isn't valid, reject the input.
if (!fpsField.acceptableInput && fpsField.text) {
return false
} }
if (max_fps > 120) {
// The textbox needs to have text or placeholder text fpsListModel.append({"text": qsTr("%1 FPS (Unsupported)").arg("120"), "video_fps": "120"})
if (!fpsField.text && !fpsField.placeholderText) {
return false
}
return true
}
id: customFpsDialog
standardButtons: Dialog.Ok | Dialog.Cancel
onOpened: {
// Force keyboard focus on the textbox so keyboard navigation works
fpsField.forceActiveFocus()
// standardButton() was added in Qt 5.10, so we must check for it first
if (customFpsDialog.standardButton) {
customFpsDialog.standardButton(Dialog.Ok).enabled = customFpsDialog.isInputValid()
} }
} }
onClosed: { // Use 64 as the cutoff for adding a separate option to
fpsField.clear() // handle wonky displays that report just over 60 Hz.
} if (max_fps > 64) {
// Mark any FPS value greater than 120 as unsupported
onRejected: { if (StreamingPreferences.unsupportedFps && max_fps > 120) {
fpsComboBox.currentIndex = fpsComboBox.lastIndexValue fpsListModel.append({"text": qsTr("%1 FPS (Unsupported)").arg(max_fps), "video_fps": ""+max_fps})
}
onAccepted: {
// Reject if there's invalid input
if (!isInputValid()) {
reject()
return
} }
else if (max_fps > 120) {
var fps = fpsField.text ? fpsField.text : fpsField.placeholderText fpsListModel.append({"text": qsTr("%1 FPS").arg("120"), "video_fps": "120"})
}
// Find and update the custom entry else {
for (var i = 0; i < fpsListModel.count; i++) { fpsListModel.append({"text": qsTr("%1 FPS").arg(max_fps), "video_fps": ""+max_fps})
if (fpsListModel.get(i).is_custom) {
fpsListModel.setProperty(i, "video_fps", fps)
fpsListModel.setProperty(i, "text", qsTr("Custom (%1 FPS)").arg(fps))
// Now update the bitrate using the custom resolution
fpsComboBox.currentIndex = i
fpsComboBox.updateBitrateForSelection()
// Update the combobox width too
fpsComboBox.recalculateWidth()
break
}
} }
} }
ColumnLayout { // Add unsupported FPS values that come after the display max FPS
Label { if (StreamingPreferences.unsupportedFps) {
text: qsTr("Enter a custom frame rate:") if (max_fps < 90) {
font.bold: true fpsListModel.append({"text":qsTr("%1 FPS (Unsupported)").arg("90"), "video_fps": "90"})
} }
if (max_fps < 120) {
RowLayout { fpsListModel.append({"text":qsTr("%1 FPS (Unsupported)").arg("120"), "video_fps": "120"})
TextField {
id: fpsField
maximumLength: 4
inputMethodHints: Qt.ImhDigitsOnly
placeholderText: fpsListModel.get(fpsComboBox.currentIndex).video_fps
validator: IntValidator{bottom:10; top:9999}
focus: true
onTextChanged: {
// standardButton() was added in Qt 5.10, so we must check for it first
if (customFpsDialog.standardButton) {
customFpsDialog.standardButton(Dialog.Ok).enabled = customFpsDialog.isInputValid()
}
}
Keys.onReturnPressed: {
customFpsDialog.accept()
}
Keys.onEnterPressed: {
customFpsDialog.accept()
}
}
}
}
}
function addRefreshRateOrdered(fpsListModel, refreshRate, description, custom) {
var indexToAdd = 0
for (var j = 0; j < fpsListModel.count; j++) {
var existing_fps = parseInt(fpsListModel.get(j).video_fps);
if (refreshRate === existing_fps || (custom && fpsListModel.get(j).is_custom)) {
// Duplicate entry, skip
indexToAdd = -1
break
}
else if (refreshRate > existing_fps) {
// Candidate entrypoint after this entry
indexToAdd = j + 1
} }
} }
// Insert this frame rate if it's not a duplicate return fpsListModel
if (indexToAdd >= 0) {
// Custom values always go at the end of the list
if (custom) {
indexToAdd = fpsListModel.count
}
fpsListModel.insert(indexToAdd,
{
"text": description,
"video_fps": ""+refreshRate,
"is_custom": custom
})
}
return indexToAdd
} }
function reinitialize() { function reinitialize() {
// Add native refresh rate for all attached displays model = createModel()
var done = false
for (var displayIndex = 0; !done; displayIndex++) {
var refreshRate = SystemProperties.getRefreshRate(displayIndex);
if (refreshRate === 0) {
// Exceeded max count of displays
done = true
break
}
addRefreshRateOrdered(fpsListModel, refreshRate, qsTr("%1 FPS").arg(refreshRate), false)
}
var saved_fps = StreamingPreferences.fps var saved_fps = StreamingPreferences.fps
var found = false currentIndex = 0
for (var i = 0; i < model.count; i++) { for (var i = 0; i < model.count; i++) {
var el_fps = parseInt(model.get(i).video_fps); var el_fps = parseInt(model.get(i).video_fps);
// Look for a matching frame rate // Pick the highest value lesser or equal to the saved FPS
if (saved_fps === el_fps) { if (saved_fps >= el_fps) {
currentIndex = i currentIndex = i
found = true
break
} }
} }
// If we didn't find one, add a custom frame rate for the current value // Persist the selected value
if (!found) { activated(currentIndex)
currentIndex = addRefreshRateOrdered(model, saved_fps, qsTr("Custom (%1 FPS)").arg(saved_fps), true)
}
else {
addRefreshRateOrdered(model, "", qsTr("Custom"), true)
}
recalculateWidth()
lastIndexValue = currentIndex
} }
// ignore setting the index at first, and actually set it when the component is loaded // ignore setting the index at first, and actually set it when the component is loaded
Component.onCompleted: { Component.onCompleted: {
reinitialize() reinitialize()
languageChanged.connect(reinitialize)
}
model: ListModel {
id: fpsListModel
// Other elements may be added at runtime
ListElement {
text: qsTr("30 FPS")
video_fps: "30"
is_custom: false
}
ListElement {
text: qsTr("60 FPS")
video_fps: "60"
is_custom: false
}
} }
id: fpsComboBox id: fpsComboBox
@@ -652,11 +472,16 @@ Flickable {
textRole: "text" textRole: "text"
// ::onActivated must be used, as it only listens for when the index is changed by a human // ::onActivated must be used, as it only listens for when the index is changed by a human
onActivated : { onActivated : {
if (model.get(currentIndex).is_custom) { var selectedFps = parseInt(model.get(currentIndex).video_fps)
customFpsDialog.open()
} // Only modify the bitrate if the values actually changed
else { if (StreamingPreferences.fps !== selectedFps) {
updateBitrateForSelection() StreamingPreferences.fps = selectedFps
StreamingPreferences.bitrateKbps = StreamingPreferences.getDefaultBitrate(StreamingPreferences.width,
StreamingPreferences.height,
StreamingPreferences.fps);
slider.value = StreamingPreferences.bitrateKbps
} }
} }
} }
@@ -685,7 +510,7 @@ Flickable {
stepSize: 500 stepSize: 500
from : 500 from : 500
to: StreamingPreferences.unlockBitrate ? 500000 : 150000 to: 150000
snapMode: "SnapOnRelease" snapMode: "SnapOnRelease"
width: Math.min(bitrateDesc.implicitWidth, parent.width) width: Math.min(bitrateDesc.implicitWidth, parent.width)
@@ -694,11 +519,6 @@ Flickable {
bitrateTitle.text = qsTr("Video bitrate: %1 Mbps").arg(value / 1000.0) bitrateTitle.text = qsTr("Video bitrate: %1 Mbps").arg(value / 1000.0)
StreamingPreferences.bitrateKbps = value StreamingPreferences.bitrateKbps = value
} }
Component.onCompleted: {
// Refresh the text after translations change
languageChanged.connect(valueChanged)
}
} }
Label { Label {
@@ -711,53 +531,28 @@ Flickable {
} }
AutoResizingComboBox { AutoResizingComboBox {
function createModel() { // ignore setting the index at first, and actually set it when the component is loaded
var model = Qt.createQmlObject('import QtQuick 2.0; ListModel {}', parent, '') Component.onCompleted: {
model.append({
text: qsTr("Fullscreen"),
val: StreamingPreferences.WM_FULLSCREEN
})
model.append({
text: qsTr("Borderless windowed"),
val: StreamingPreferences.WM_FULLSCREEN_DESKTOP
})
model.append({
text: qsTr("Windowed"),
val: StreamingPreferences.WM_WINDOWED
})
// Set the recommended option based on the OS
for (var i = 0; i < model.count; i++) {
var thisWm = model.get(i).val;
if (thisWm === StreamingPreferences.recommendedFullScreenMode) {
model.get(i).text += " " + qsTr("(Recommended)")
model.move(i, 0, 1)
break
}
}
return model
}
// This is used on initialization and upon retranslation
function reinitialize() {
if (!visible) { if (!visible) {
// Do nothing if the control won't even be visible // Do nothing if the control won't even be visible
return return
} }
model = createModel() // Set the recommended option based on the OS
for (var i = 0; i < windowModeListModel.count; i++) {
var thisWm = windowModeListModel.get(i).val;
if (thisWm === StreamingPreferences.recommendedFullScreenMode) {
windowModeListModel.get(i).text += qsTr(" (Recommended)")
windowModeListModel.move(i, 0, 1);
break
}
}
currentIndex = 0 currentIndex = 0
// Set the current value based on the saved preferences
var savedWm = StreamingPreferences.windowMode var savedWm = StreamingPreferences.windowMode
for (var i = 0; i < model.count; i++) { for (var i = 0; i < windowModeListModel.count; i++) {
var thisWm = model.get(i).val; var thisWm = windowModeListModel.get(i).val;
if (savedWm === thisWm) { if (savedWm === thisWm) {
currentIndex = i currentIndex = i
break break
@@ -767,18 +562,28 @@ Flickable {
activated(currentIndex) activated(currentIndex)
} }
Component.onCompleted: {
reinitialize()
languageChanged.connect(reinitialize)
}
id: windowModeComboBox id: windowModeComboBox
visible: SystemProperties.hasDesktopEnvironment visible: SystemProperties.hasDesktopEnvironment
enabled: !SystemProperties.rendererAlwaysFullScreen enabled: !SystemProperties.rendererAlwaysFullScreen
hoverEnabled: true hoverEnabled: true
textRole: "text" textRole: "text"
model: ListModel {
id: windowModeListModel
ListElement {
text: qsTr("Fullscreen")
val: StreamingPreferences.WM_FULLSCREEN
}
ListElement {
text: qsTr("Borderless windowed")
val: StreamingPreferences.WM_FULLSCREEN_DESKTOP
}
ListElement {
text: qsTr("Windowed")
val: StreamingPreferences.WM_WINDOWED
}
}
onActivated: { onActivated: {
StreamingPreferences.windowMode = model.get(currentIndex).val StreamingPreferences.windowMode = windowModeListModel.get(currentIndex).val
} }
ToolTip.delay: 1000 ToolTip.delay: 1000
@@ -917,46 +722,6 @@ Flickable {
} }
} }
GroupBox {
id: hostSettingsGroupBox
width: (parent.width - (parent.leftPadding + parent.rightPadding))
padding: 12
title: "<font color=\"skyblue\">" + qsTr("Host Settings") + "</font>"
font.pointSize: 12
Column {
anchors.fill: parent
spacing: 5
CheckBox {
id: optimizeGameSettingsCheck
width: parent.width
text: qsTr("Optimize game settings for streaming")
font.pointSize: 12
checked: StreamingPreferences.gameOptimizations
onCheckedChanged: {
StreamingPreferences.gameOptimizations = checked
}
}
CheckBox {
id: quitAppAfter
width: parent.width
text: qsTr("Quit app on host PC after ending stream")
font.pointSize: 12
checked: StreamingPreferences.quitAppAfter
onCheckedChanged: {
StreamingPreferences.quitAppAfter = checked
}
ToolTip.delay: 1000
ToolTip.timeout: 5000
ToolTip.visible: hovered
ToolTip.text: qsTr("This will close the app or game you are streaming when you end your stream. You will lose any unsaved progress!")
}
}
}
GroupBox { GroupBox {
id: uiSettingsGroupBox id: uiSettingsGroupBox
width: (parent.width - (parent.leftPadding + parent.rightPadding)) width: (parent.width - (parent.leftPadding + parent.rightPadding))
@@ -1044,10 +809,10 @@ Flickable {
text: "한국어" // Korean text: "한국어" // Korean
val: StreamingPreferences.LANG_KO val: StreamingPreferences.LANG_KO
} }
ListElement { /* ListElement {
text: "Magyar" // Hungarian text: "Magyar" // Hungarian
val: StreamingPreferences.LANG_HU val: StreamingPreferences.LANG_HU
} } */
ListElement { ListElement {
text: "Nederlands" // Dutch text: "Nederlands" // Dutch
val: StreamingPreferences.LANG_NL val: StreamingPreferences.LANG_NL
@@ -1056,17 +821,17 @@ Flickable {
text: "Svenska" // Swedish text: "Svenska" // Swedish
val: StreamingPreferences.LANG_SV val: StreamingPreferences.LANG_SV
} }
ListElement { /* ListElement {
text: "Türkçe" // Turkish text: "Türkçe" // Turkish
val: StreamingPreferences.LANG_TR val: StreamingPreferences.LANG_TR
} } */
/* ListElement { /* ListElement {
text: "Українська" // Ukrainian text: "Українська" // Ukrainian
val: StreamingPreferences.LANG_UK val: StreamingPreferences.LANG_UK
} */ } */
ListElement { ListElement {
text: "繁體中文" // Traditional Chinese text: "繁体字" // Traditional Chinese
val: StreamingPreferences.LANG_ZH_TW val: StreamingPreferences.LANG_ZH_HANT
} }
ListElement { ListElement {
text: "Português" // Portuguese text: "Português" // Portuguese
@@ -1088,30 +853,6 @@ Flickable {
text: "हिन्दी, हिंदी" // Hindi text: "हिन्दी, हिंदी" // Hindi
val: StreamingPreferences.LANG_HI val: StreamingPreferences.LANG_HI
} */ } */
ListElement {
text: "Język polski" // Polish
val: StreamingPreferences.LANG_PL
}
ListElement {
text: "Čeština" // Czech
val: StreamingPreferences.LANG_CS
}
/* ListElement {
text: "עִבְרִית" // Hebrew
val: StreamingPreferences.LANG_HE
} */
/* ListElement {
text: "کرمانجیی خواروو" // Central Kurdish
val: StreamingPreferences.LANG_CKB
} */
/* ListElement {
text: "Lietuvių kalba" // Lithuanian
val: StreamingPreferences.LANG_LT
} */
/* ListElement {
text: "Eesti" // Estonian
val: StreamingPreferences.LANG_ET
} */
} }
// ::onActivated must be used, as it only listens for when the index is changed by a human // ::onActivated must be used, as it only listens for when the index is changed by a human
onActivated : { onActivated : {
@@ -1126,9 +867,6 @@ Flickable {
// Force the back operation to pop any AppView pages that exist. // Force the back operation to pop any AppView pages that exist.
// The AppView stops working after retranslate() for some reason. // The AppView stops working after retranslate() for some reason.
window.clearOnBack = true window.clearOnBack = true
// Signal other controls to adjust their text
languageChanged()
} }
} }
} }
@@ -1409,7 +1147,15 @@ Flickable {
font.pointSize: 12 font.pointSize: 12
checked: StreamingPreferences.swapFaceButtons checked: StreamingPreferences.swapFaceButtons
onCheckedChanged: { onCheckedChanged: {
StreamingPreferences.swapFaceButtons = checked // Check if the value changed (this is called on init too)
if (StreamingPreferences.swapFaceButtons !== checked) {
StreamingPreferences.swapFaceButtons = checked
// Save and restart SdlGamepadKeyNavigation so it can pull the new value
StreamingPreferences.save()
SdlGamepadKeyNavigation.disable()
SdlGamepadKeyNavigation.enable()
}
} }
ToolTip.delay: 1000 ToolTip.delay: 1000
@@ -1466,6 +1212,46 @@ Flickable {
} }
} }
GroupBox {
id: hostSettingsGroupBox
width: (parent.width - (parent.leftPadding + parent.rightPadding))
padding: 12
title: "<font color=\"skyblue\">" + qsTr("Host Settings") + "</font>"
font.pointSize: 12
Column {
anchors.fill: parent
spacing: 5
CheckBox {
id: optimizeGameSettingsCheck
width: parent.width
text: qsTr("Optimize game settings for streaming")
font.pointSize: 12
checked: StreamingPreferences.gameOptimizations
onCheckedChanged: {
StreamingPreferences.gameOptimizations = checked
}
}
CheckBox {
id: quitAppAfter
width: parent.width
text: qsTr("Quit app on host PC after ending stream")
font.pointSize: 12
checked: StreamingPreferences.quitAppAfter
onCheckedChanged: {
StreamingPreferences.quitAppAfter = checked
}
ToolTip.delay: 1000
ToolTip.timeout: 5000
ToolTip.visible: hovered
ToolTip.text: qsTr("This will close the app or game you are streaming when you end your stream. You will lose any unsaved progress!")
}
}
}
GroupBox { GroupBox {
id: advancedSettingsGroupBox id: advancedSettingsGroupBox
width: (parent.width - (parent.leftPadding + parent.rightPadding)) width: (parent.width - (parent.leftPadding + parent.rightPadding))
@@ -1502,6 +1288,7 @@ Flickable {
id: decoderComboBox id: decoderComboBox
textRole: "text" textRole: "text"
enabled: !enableHdr.checked
model: ListModel { model: ListModel {
id: decoderListModel id: decoderListModel
ListElement { ListElement {
@@ -1523,6 +1310,21 @@ Flickable {
StreamingPreferences.videoDecoderSelection = decoderListModel.get(currentIndex).val StreamingPreferences.videoDecoderSelection = decoderListModel.get(currentIndex).val
} }
} }
// This handles the state of the enableHdr checkbox changing
onEnabledChanged: {
if (enabled) {
StreamingPreferences.videoDecoderSelection = decoderListModel.get(currentIndex).val
}
else {
StreamingPreferences.videoDecoderSelection = StreamingPreferences.VDS_AUTO
}
}
ToolTip.delay: 1000
ToolTip.timeout: 5000
ToolTip.visible: hovered && !enabled
ToolTip.text: qsTr("Enabling HDR overrides manual decoder selections.")
} }
Label { Label {
@@ -1555,6 +1357,7 @@ Flickable {
id: codecComboBox id: codecComboBox
textRole: "text" textRole: "text"
enabled: !enableHdr.checked
model: ListModel { model: ListModel {
id: codecListModel id: codecListModel
ListElement { ListElement {
@@ -1569,10 +1372,6 @@ Flickable {
text: qsTr("HEVC (H.265)") text: qsTr("HEVC (H.265)")
val: StreamingPreferences.VCC_FORCE_HEVC val: StreamingPreferences.VCC_FORCE_HEVC
} }
ListElement {
text: qsTr("AV1 (Experimental)")
val: StreamingPreferences.VCC_FORCE_AV1
}
} }
// ::onActivated must be used, as it only listens for when the index is changed by a human // ::onActivated must be used, as it only listens for when the index is changed by a human
onActivated : { onActivated : {
@@ -1580,6 +1379,21 @@ Flickable {
StreamingPreferences.videoCodecConfig = codecListModel.get(currentIndex).val StreamingPreferences.videoCodecConfig = codecListModel.get(currentIndex).val
} }
} }
// This handles the state of the enableHdr checkbox changing
onEnabledChanged: {
if (enabled) {
StreamingPreferences.videoCodecConfig = codecListModel.get(currentIndex).val
}
else {
StreamingPreferences.videoCodecConfig = StreamingPreferences.VCC_FORCE_HEVC_HDR
}
}
ToolTip.delay: 1000
ToolTip.timeout: 5000
ToolTip.visible: hovered && !enabled
ToolTip.text: qsTr("Enabling HDR overrides manual codec selections.")
} }
CheckBox { CheckBox {
@@ -1587,12 +1401,8 @@ Flickable {
width: parent.width width: parent.width
text: qsTr("Enable HDR (Experimental)") text: qsTr("Enable HDR (Experimental)")
font.pointSize: 12 font.pointSize: 12
enabled: SystemProperties.supportsHdr enabled: SystemProperties.supportsHdr
checked: enabled && StreamingPreferences.enableHdr checked: enabled && StreamingPreferences.videoCodecConfig == StreamingPreferences.VCC_FORCE_HEVC_HDR
onCheckedChanged: {
StreamingPreferences.enableHdr = checked
}
// Updating StreamingPreferences.videoCodecConfig is handled above // Updating StreamingPreferences.videoCodecConfig is handled above
@@ -1606,50 +1416,22 @@ Flickable {
} }
CheckBox { CheckBox {
id: enableYUV444 id: unlockUnsupportedFps
width: parent.width width: parent.width
text: qsTr("Enable YUV 4:4:4 (Experimental)") text: qsTr("Unlock unsupported FPS options")
font.pointSize: 12 font.pointSize: 12
checked: StreamingPreferences.unsupportedFps
checked: StreamingPreferences.enableYUV444
onCheckedChanged: { onCheckedChanged: {
// This is called on init, so only reset to default bitrate when checked state changes. // This is called on init, so only do the work if we've
if (StreamingPreferences.enableYUV444 != checked) { // actually changed the value.
StreamingPreferences.enableYUV444 = checked if (StreamingPreferences.unsupportedFps != checked) {
StreamingPreferences.bitrateKbps = StreamingPreferences.getDefaultBitrate(StreamingPreferences.width, StreamingPreferences.unsupportedFps = checked
StreamingPreferences.height,
StreamingPreferences.fps, // The selectable FPS values depend on whether
StreamingPreferences.enableYUV444); // this option is enabled or not
slider.value = StreamingPreferences.bitrateKbps fpsComboBox.reinitialize()
} }
} }
ToolTip.delay: 1000
ToolTip.timeout: 5000
ToolTip.visible: hovered
ToolTip.text: enabled ?
qsTr("Good for streaming desktop and text-heavy games, but not recommended for fast-paced games.")
:
qsTr("YUV 4:4:4 is not supported on this PC.")
}
CheckBox {
id: unlockBitrate
width: parent.width
text: qsTr("Unlock bitrate limit (Experimental)")
font.pointSize: 12
checked: StreamingPreferences.unlockBitrate
onCheckedChanged: {
StreamingPreferences.unlockBitrate = checked
StreamingPreferences.bitrateKbps = Math.min(StreamingPreferences.bitrateKbps, slider.to)
slider.value = StreamingPreferences.bitrateKbps
}
ToolTip.delay: 1000
ToolTip.timeout: 5000
ToolTip.visible: hovered
ToolTip.text: qsTr("This unlocks extremely high video bitrates for use with Sunshine hosts. It should only be used when streaming over an Ethernet LAN connection.")
} }
CheckBox { CheckBox {
@@ -1664,6 +1446,10 @@ Flickable {
if (StreamingPreferences.enableMdns != checked) { if (StreamingPreferences.enableMdns != checked) {
StreamingPreferences.enableMdns = checked StreamingPreferences.enableMdns = checked
// We must save the updated preference to ensure
// ComputerManager can observe the change internally.
StreamingPreferences.save()
// Restart polling so the mDNS change takes effect // Restart polling so the mDNS change takes effect
if (window.pollingActive) { if (window.pollingActive) {
ComputerManager.stopPollingAsync() ComputerManager.stopPollingAsync()
@@ -1680,26 +1466,16 @@ Flickable {
font.pointSize: 12 font.pointSize: 12
checked: StreamingPreferences.detectNetworkBlocking checked: StreamingPreferences.detectNetworkBlocking
onCheckedChanged: { onCheckedChanged: {
StreamingPreferences.detectNetworkBlocking = checked // This is called on init, so only do the work if we've
} // actually changed the value.
} if (StreamingPreferences.detectNetworkBlocking != checked) {
StreamingPreferences.detectNetworkBlocking = checked
CheckBox { // We must save the updated preference to ensure
id: showPerformanceOverlay // ComputerManager can observe the change internally.
width: parent.width StreamingPreferences.save()
text: qsTr("Show performance stats while streaming") }
font.pointSize: 12
checked: StreamingPreferences.showPerformanceOverlay
onCheckedChanged: {
StreamingPreferences.showPerformanceOverlay = checked
} }
ToolTip.delay: 1000
ToolTip.timeout: 5000
ToolTip.visible: hovered
ToolTip.text: qsTr("Display real-time stream performance information while streaming.") + "\n\n" +
qsTr("You can toggle it at any time while streaming using Ctrl+Alt+Shift+S or Select+L1+R1+X.") + "\n\n" +
qsTr("The performance overlay is not supported on Steam Link or Raspberry Pi.")
} }
} }
} }
+3 -5
View File
@@ -45,7 +45,6 @@ Item {
{ {
// Display the error dialog after Session::exec() returns // Display the error dialog after Session::exec() returns
streamSegueErrorDialog.text = text streamSegueErrorDialog.text = text
console.error(text)
} }
function displayLaunchWarning(text) function displayLaunchWarning(text)
@@ -57,7 +56,6 @@ Item {
toast.text = text toast.text = text
toast.timeout = 3000 toast.timeout = 3000
toast.visible = true toast.visible = true
console.warn(text)
} }
function quitStarting() function quitStarting()
@@ -76,7 +74,7 @@ Item {
streamSegueErrorDialog.text += "\n\n" + qsTr("This PC's Internet connection is blocking Moonlight. Streaming over the Internet may not work while connected to this network.") 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.")
} }
// Re-enable GUI gamepad usage now // Enable GUI gamepad usage now
SdlGamepadKeyNavigation.enable() SdlGamepadKeyNavigation.enable()
if (quitAfter) { if (quitAfter) {
@@ -119,7 +117,7 @@ Item {
// Show the toolbar again when popped off the stack // Show the toolbar again when popped off the stack
toolBar.visible = true toolBar.visible = true
// Re-enable GUI gamepad usage now // Enable GUI gamepad usage now
SdlGamepadKeyNavigation.enable() SdlGamepadKeyNavigation.enable()
} }
@@ -176,7 +174,7 @@ Item {
gc() gc()
// Run the streaming session to completion // Run the streaming session to completion
session.exec(Window.window) session.exec(Screen.virtualX, Screen.virtualY)
} }
sourceComponent: Item {} sourceComponent: Item {}
+19 -52
View File
@@ -2,6 +2,8 @@
#include <QThreadPool> #include <QThreadPool>
#include <random>
ComputerModel::ComputerModel(QObject* object) ComputerModel::ComputerModel(QObject* object)
: QAbstractListModel(object) {} : QAbstractListModel(object) {}
@@ -42,46 +44,6 @@ QVariant ComputerModel::data(const QModelIndex& index, int role) const
return computer->state == NvComputer::CS_UNKNOWN; return computer->state == NvComputer::CS_UNKNOWN;
case ServerSupportedRole: case ServerSupportedRole:
return computer->isSupportedServerVersion; return computer->isSupportedServerVersion;
case DetailsRole: {
QString state, pairState;
switch (computer->state) {
case NvComputer::CS_ONLINE:
state = tr("Online");
break;
case NvComputer::CS_OFFLINE:
state = tr("Offline");
break;
default:
state = tr("Unknown");
break;
}
switch (computer->pairState) {
case NvComputer::PS_PAIRED:
pairState = tr("Paired");
break;
case NvComputer::PS_NOT_PAIRED:
pairState = tr("Unpaired");
break;
default:
pairState = tr("Unknown");
break;
}
return tr("Name: %1").arg(computer->name) + '\n' +
tr("Status: %1").arg(state) + '\n' +
tr("Active Address: %1").arg(computer->activeAddress.toString()) + '\n' +
tr("UUID: %1").arg(computer->uuid) + '\n' +
tr("Local Address: %1").arg(computer->localAddress.toString()) + '\n' +
tr("Remote Address: %1").arg(computer->remoteAddress.toString()) + '\n' +
tr("IPv6 Address: %1").arg(computer->ipv6Address.toString()) + '\n' +
tr("Manual Address: %1").arg(computer->manualAddress.toString()) + '\n' +
tr("MAC Address: %1").arg(computer->macAddress.isEmpty() ? tr("Unknown") : QString(computer->macAddress.toHex(':'))) + '\n' +
tr("Pair State: %1").arg(pairState) + '\n' +
tr("Running Game ID: %1").arg(computer->state == NvComputer::CS_ONLINE ? QString::number(computer->currentGameId) : tr("Unknown")) + '\n' +
tr("HTTPS Port: %1").arg(computer->state == NvComputer::CS_ONLINE ? QString::number(computer->activeHttpsPort) : tr("Unknown"));
}
default: default:
return QVariant(); return QVariant();
} }
@@ -109,7 +71,6 @@ QHash<int, QByteArray> ComputerModel::roleNames() const
names[WakeableRole] = "wakeable"; names[WakeableRole] = "wakeable";
names[StatusUnknownRole] = "statusUnknown"; names[StatusUnknownRole] = "statusUnknown";
names[ServerSupportedRole] = "serverSupported"; names[ServerSupportedRole] = "serverSupported";
names[DetailsRole] = "details";
return names; return names;
} }
@@ -179,9 +140,14 @@ void ComputerModel::renameComputer(int computerIndex, QString name)
m_ComputerManager->renameHost(m_Computers[computerIndex], name); m_ComputerManager->renameHost(m_Computers[computerIndex], name);
} }
// TODO: Use QRandomGenerator when we drop Qt 5.9 support
QString ComputerModel::generatePinString() QString ComputerModel::generatePinString()
{ {
return m_ComputerManager->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 class DeferredTestConnectionTask : public QObject, public QRunnable
@@ -227,18 +193,19 @@ void ComputerModel::handlePairingCompleted(NvComputer*, QString error)
void ComputerModel::handleComputerStateChanged(NvComputer* computer) void ComputerModel::handleComputerStateChanged(NvComputer* computer)
{ {
QVector<NvComputer*> newComputerList = m_ComputerManager->getComputers(); // If this is an existing computer, we can report the data changed
int index = m_Computers.indexOf(computer);
// Reset the model if the structural layout of the list has changed if (index >= 0) {
if (m_Computers != newComputerList) { // Let the view know that this specific computer changed
beginResetModel(); emit dataChanged(createIndex(index, 0), createIndex(index, 0));
m_Computers = newComputerList;
endResetModel();
} }
else { else {
// Let the view know that this specific computer changed // This is a new PC which may be inserted at an arbitrary point
int index = m_Computers.indexOf(computer); // in our computer list (since it comes from CM's QMap). Reload
emit dataChanged(createIndex(index, 0), createIndex(index, 0)); // the whole model state to ensure it stays consistent.
beginResetModel();
m_Computers = m_ComputerManager->getComputers();
endResetModel();
} }
} }
+1 -2
View File
@@ -15,8 +15,7 @@ class ComputerModel : public QAbstractListModel
BusyRole, BusyRole,
WakeableRole, WakeableRole,
StatusUnknownRole, StatusUnknownRole,
ServerSupportedRole, ServerSupportedRole
DetailsRole
}; };
public: public:
+49 -73
View File
@@ -2,7 +2,6 @@ import QtQuick 2.9
import QtQuick.Controls 2.2 import QtQuick.Controls 2.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.2
import ComputerManager 1.0 import ComputerManager 1.0
import AutoUpdateChecker 1.0 import AutoUpdateChecker 1.0
@@ -19,53 +18,17 @@ ApplicationWindow {
property bool clearOnBack: false property bool clearOnBack: false
id: window id: window
visible: true
width: 1280 width: 1280
height: 600 height: 600
// This function runs prior to creation of the initial StackView item visibility: {
function doEarlyInit() {
// Override the background color to Material 2 colors for Qt 6.5+
// in order to improve contrast between GFE's placeholder box art
// and the background of the app grid.
if (SystemProperties.usesMaterial3Theme) {
Material.background = "#303030"
}
SdlGamepadKeyNavigation.enable()
}
Component.onCompleted: {
// Show the window according to the user's preferences
if (SystemProperties.hasDesktopEnvironment) { if (SystemProperties.hasDesktopEnvironment) {
if (StreamingPreferences.uiDisplayMode == StreamingPreferences.UI_MAXIMIZED) { if (StreamingPreferences.uiDisplayMode == StreamingPreferences.UI_WINDOWED) return "Windowed"
window.showMaximized() else if (StreamingPreferences.uiDisplayMode == StreamingPreferences.UI_MAXIMIZED) return "Maximized"
} else if (StreamingPreferences.uiDisplayMode == StreamingPreferences.UI_FULLSCREEN) return "FullScreen"
else if (StreamingPreferences.uiDisplayMode == StreamingPreferences.UI_FULLSCREEN) {
window.showFullScreen()
}
else {
window.show()
}
} else { } else {
window.showFullScreen() return "FullScreen"
}
// Display any modal dialogs for configuration warnings
if (SystemProperties.isWow64) {
wow64Dialog.open()
}
else if (!SystemProperties.hasHardwareAcceleration) {
if (SystemProperties.isRunningXWayland) {
xWaylandDialog.open()
}
else {
noHwDecoderDialog.open()
}
}
if (SystemProperties.unmappedGamepads) {
unmappedGamepadDialog.unmappedGamepads = SystemProperties.unmappedGamepads
unmappedGamepadDialog.open()
} }
} }
@@ -86,16 +49,10 @@ ApplicationWindow {
StackView { StackView {
id: stackView id: stackView
initialItem: initialView
anchors.fill: parent anchors.fill: parent
focus: true focus: true
Component.onCompleted: {
// Perform our early initialization before constructing
// the initial view and pushing it to the StackView
doEarlyInit()
push(initialView)
}
onCurrentItemChanged: { onCurrentItemChanged: {
// Ensure focus travels to the next view when going back // Ensure focus travels to the next view when going back
if (currentItem) { if (currentItem) {
@@ -169,9 +126,6 @@ ApplicationWindow {
pollingActive = true pollingActive = true
} }
} }
// Poll for gamepad input only when the window is in focus
SdlGamepadKeyNavigation.notifyWindowFocus(visible && active)
} }
onActiveChanged: { onActiveChanged: {
@@ -190,25 +144,47 @@ ApplicationWindow {
// if focus does not return within a few minutes. // if focus does not return within a few minutes.
inactivityTimer.restart() inactivityTimer.restart()
} }
// Poll for gamepad input only when the window is in focus
SdlGamepadKeyNavigation.notifyWindowFocus(visible && active)
} }
// Workaround for lack of instanceof in Qt 5.9. property bool initialized: false
//
// Based on https://stackoverflow.com/questions/13923794/how-to-do-a-is-a-typeof-or-instanceof-in-qml // BUG: Using onAfterSynchronizing: here causes very strange
function qmltypeof(obj, className) { // QtObject, string -> bool // failures on Linux. Many shaders fail to compile and we
// className plus "(" is the class instance without modification // eventually segfault deep inside the Qt OpenGL code.
// className plus "_QML" is the class instance with user-defined properties onAfterRendering: {
var str = obj.toString(); // We use this callback to trigger dialog display because
return str.startsWith(className + "(") || str.startsWith(className + "_QML"); // it only happens once the window is fully constructed.
// Doing it earlier can lead to the dialog appearing behind
// the window or otherwise without input focus.
if (!initialized) {
// Set initialized before calling anything else, because
// pumping the event loop can cause us to get another
// onAfterRendering call and potentially reenter this code.
initialized = true;
if (SystemProperties.isWow64) {
wow64Dialog.open()
}
else if (!SystemProperties.hasHardwareAcceleration) {
if (SystemProperties.isRunningXWayland) {
xWaylandDialog.open()
}
else {
noHwDecoderDialog.open()
}
}
if (SystemProperties.unmappedGamepads) {
unmappedGamepadDialog.unmappedGamepads = SystemProperties.unmappedGamepads
unmappedGamepadDialog.open()
}
}
} }
function navigateTo(url, objectType) function navigateTo(url, objectName)
{ {
var existingItem = stackView.find(function(item, index) { var existingItem = stackView.find(function(item, index) {
return qmltypeof(item, objectType) return item.objectName === objectName
}) })
if (existingItem !== null) { if (existingItem !== null) {
@@ -275,7 +251,7 @@ ApplicationWindow {
Label { Label {
id: versionLabel id: versionLabel
visible: qmltypeof(stackView.currentItem, "SettingsView") visible: stackView.currentItem.objectName === qsTr("Settings")
text: qsTr("Version %1").arg(SystemProperties.versionString) text: qsTr("Version %1").arg(SystemProperties.versionString)
font.pointSize: 12 font.pointSize: 12
horizontalAlignment: Qt.AlignRight horizontalAlignment: Qt.AlignRight
@@ -285,9 +261,9 @@ ApplicationWindow {
NavigableToolButton { NavigableToolButton {
id: discordButton id: discordButton
visible: SystemProperties.hasBrowser && visible: SystemProperties.hasBrowser &&
qmltypeof(stackView.currentItem, "SettingsView") stackView.currentItem.objectName === qsTr("Settings")
iconSource: "qrc:/res/discord.svg" iconSource: "qrc:/res/Discord-Logo-White.svg"
ToolTip.delay: 1000 ToolTip.delay: 1000
ToolTip.timeout: 3000 ToolTip.timeout: 3000
@@ -304,7 +280,7 @@ ApplicationWindow {
NavigableToolButton { NavigableToolButton {
id: addPcButton id: addPcButton
visible: qmltypeof(stackView.currentItem, "PcView") visible: stackView.currentItem.objectName === qsTr("Computers")
iconSource: "qrc:/res/ic_add_to_queue_white_48px.svg" iconSource: "qrc:/res/ic_add_to_queue_white_48px.svg"
@@ -402,7 +378,7 @@ ApplicationWindow {
iconSource: "qrc:/res/ic_videogame_asset_white_48px.svg" iconSource: "qrc:/res/ic_videogame_asset_white_48px.svg"
onClicked: navigateTo("qrc:/gui/GamepadMapper.qml", "GamepadMapper") onClicked: navigateTo("qrc:/gui/GamepadMapper.qml", qsTr("Gamepad Mapping"))
Keys.onDownPressed: { Keys.onDownPressed: {
stackView.currentItem.forceActiveFocus(Qt.TabFocus) stackView.currentItem.forceActiveFocus(Qt.TabFocus)
@@ -414,7 +390,7 @@ ApplicationWindow {
iconSource: "qrc:/res/settings.svg" iconSource: "qrc:/res/settings.svg"
onClicked: navigateTo("qrc:/gui/SettingsView.qml", "SettingsView") onClicked: navigateTo("qrc:/gui/SettingsView.qml", qsTr("Settings"))
Keys.onDownPressed: { Keys.onDownPressed: {
stackView.currentItem.forceActiveFocus(Qt.TabFocus) stackView.currentItem.forceActiveFocus(Qt.TabFocus)
@@ -502,7 +478,7 @@ ApplicationWindow {
NavigableDialog { NavigableDialog {
id: addPcDialog id: addPcDialog
property string label: qsTr("Enter the IP address of your host PC:") property string label: qsTr("Enter the IP address of your GameStream PC:")
standardButtons: Dialog.Ok | Dialog.Cancel standardButtons: Dialog.Ok | Dialog.Cancel
+95 -85
View File
@@ -4,16 +4,15 @@
#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(StreamingPreferences* prefs) SdlGamepadKeyNavigation::SdlGamepadKeyNavigation()
: m_Prefs(prefs), : m_Enabled(false),
m_Enabled(false),
m_UiNavMode(false), m_UiNavMode(false),
m_FirstPoll(false), m_FirstPoll(false),
m_HasFocus(false),
m_LastAxisNavigationEventTime(0) m_LastAxisNavigationEventTime(0)
{ {
m_PollingTimer = new QTimer(this); m_PollingTimer = new QTimer(this);
@@ -37,7 +36,7 @@ void SdlGamepadKeyNavigation::enable()
// arrival events. Additionally, there's a race condition between // arrival events. Additionally, there's a race condition between
// our QML objects being destroyed and SDL being deinitialized that // our QML objects being destroyed and SDL being deinitialized that
// this solves too. // this solves too.
if (SDLC_FAILURE(SDL_InitSubSystem(SDL_INIT_GAMEPAD))) { if (SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) failed: %s", "SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) failed: %s",
SDL_GetError()); SDL_GetError());
@@ -52,23 +51,25 @@ void SdlGamepadKeyNavigation::enable()
// overlapping lifetimes of SdlGamepadKeyNavigation instances, so we // overlapping lifetimes of SdlGamepadKeyNavigation instances, so we
// will attach ourselves. // will attach ourselves.
SDL_PumpEvents(); SDL_PumpEvents();
SDL_FlushEvent(SDL_EVENT_GAMEPAD_ADDED); SDL_FlushEvent(SDL_CONTROLLERDEVICEADDED);
// Open all currently attached game controllers // Open all currently attached game controllers
int numGamepads = 0; for (int i = 0; i < SDL_NumJoysticks(); i++) {
SDL_JoystickID* gamepads = SDL_GetGamepads(&numGamepads); if (SDL_IsGameController(i)) {
for (int i = 0; i < numGamepads; i++) { SDL_GameController* gc = SDL_GameControllerOpen(i);
SDL_Gamepad * gc = SDL_OpenGamepad(i); if (gc != nullptr) {
if (gc != nullptr) { m_Gamepads.append(gc);
m_Gamepads.append(gc); }
} }
} }
SDL_free(gamepads);
// Flush events on the first poll
m_FirstPoll = true;
// Poll every 50 ms for a new joystick event
m_PollingTimer->start(50);
m_Enabled = true; m_Enabled = true;
// Start the polling timer if the window is focused
updateTimerState();
} }
void SdlGamepadKeyNavigation::disable() void SdlGamepadKeyNavigation::disable()
@@ -77,73 +78,68 @@ void SdlGamepadKeyNavigation::disable()
return; return;
} }
m_Enabled = false; m_PollingTimer->stop();
updateTimerState();
Q_ASSERT(!m_PollingTimer->isActive());
while (!m_Gamepads.isEmpty()) { while (!m_Gamepads.isEmpty()) {
SDL_CloseGamepad(m_Gamepads[0]); SDL_GameControllerClose(m_Gamepads[0]);
m_Gamepads.removeAt(0); m_Gamepads.removeAt(0);
} }
SDL_QuitSubSystem(SDL_INIT_GAMEPAD); SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER);
}
void SdlGamepadKeyNavigation::notifyWindowFocus(bool hasFocus) m_Enabled = false;
{
m_HasFocus = hasFocus;
updateTimerState();
} }
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 // Discard any pending button events on the first poll to avoid picking up
// stale input data from the stream session (like the quit combo). // stale input data from the stream session (like the quit combo).
if (m_FirstPoll) { if (m_FirstPoll) {
SDL_PumpEvents(); SDL_PumpEvents();
SDL_FlushEvent(SDL_EVENT_GAMEPAD_BUTTON_DOWN); SDL_FlushEvent(SDL_CONTROLLERBUTTONDOWN);
SDL_FlushEvent(SDL_EVENT_GAMEPAD_BUTTON_UP); SDL_FlushEvent(SDL_CONTROLLERBUTTONUP);
m_FirstPoll = false; m_FirstPoll = false;
} }
while (SDL_PollEvent(&event)) { while (SDL_PollEvent(&event)) {
switch (event.type) { switch (event.type) {
case SDL_EVENT_QUIT : case SDL_QUIT:
// SDL may send us a quit event since we initialize // SDL may send us a quit event since we initialize
// the video subsystem on startup. If we get one, // the video subsystem on startup. If we get one,
// forward it on for Qt to take care of. // forward it on for Qt to take care of.
QCoreApplication::instance()->quit(); QCoreApplication::instance()->quit();
break; break;
case SDL_EVENT_GAMEPAD_BUTTON_DOWN : case SDL_CONTROLLERBUTTONDOWN:
case SDL_EVENT_GAMEPAD_BUTTON_UP : case SDL_CONTROLLERBUTTONUP:
{ {
QEvent::Type type = QEvent::Type type =
event.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN ? event.type == SDL_CONTROLLERBUTTONDOWN ?
QEvent::Type::KeyPress : QEvent::Type::KeyRelease; QEvent::Type::KeyPress : QEvent::Type::KeyRelease;
// Swap face buttons if needed // Swap face buttons if needed
if (m_Prefs->swapFaceButtons) { if (prefs.swapFaceButtons) {
switch (event.gbutton.button) { switch (event.cbutton.button) {
case SDL_GAMEPAD_BUTTON_SOUTH : case SDL_CONTROLLER_BUTTON_A:
event.gbutton.button = SDL_GAMEPAD_BUTTON_EAST; event.cbutton.button = SDL_CONTROLLER_BUTTON_B;
break; break;
case SDL_GAMEPAD_BUTTON_EAST : case SDL_CONTROLLER_BUTTON_B:
event.gbutton.button = SDL_GAMEPAD_BUTTON_SOUTH; event.cbutton.button = SDL_CONTROLLER_BUTTON_A;
break; break;
case SDL_GAMEPAD_BUTTON_WEST : case SDL_CONTROLLER_BUTTON_X:
event.gbutton.button = SDL_GAMEPAD_BUTTON_NORTH; event.cbutton.button = SDL_CONTROLLER_BUTTON_Y;
break; break;
case SDL_GAMEPAD_BUTTON_NORTH : case SDL_CONTROLLER_BUTTON_Y:
event.gbutton.button = SDL_GAMEPAD_BUTTON_WEST; event.cbutton.button = SDL_CONTROLLER_BUTTON_X;
break; break;
} }
} }
switch (event.gbutton.button) { switch (event.cbutton.button) {
case SDL_GAMEPAD_BUTTON_DPAD_UP : case SDL_CONTROLLER_BUTTON_DPAD_UP:
if (m_UiNavMode) { if (m_UiNavMode) {
// Back-tab // Back-tab
sendKey(type, Qt::Key_Tab, Qt::ShiftModifier); sendKey(type, Qt::Key_Tab, Qt::ShiftModifier);
@@ -152,7 +148,7 @@ void SdlGamepadKeyNavigation::onPollingTimerFired()
sendKey(type, Qt::Key_Up); sendKey(type, Qt::Key_Up);
} }
break; break;
case SDL_GAMEPAD_BUTTON_DPAD_DOWN : case SDL_CONTROLLER_BUTTON_DPAD_DOWN:
if (m_UiNavMode) { if (m_UiNavMode) {
sendKey(type, Qt::Key_Tab); sendKey(type, Qt::Key_Tab);
} }
@@ -160,13 +156,13 @@ void SdlGamepadKeyNavigation::onPollingTimerFired()
sendKey(type, Qt::Key_Down); sendKey(type, Qt::Key_Down);
} }
break; break;
case SDL_GAMEPAD_BUTTON_DPAD_LEFT : case SDL_CONTROLLER_BUTTON_DPAD_LEFT:
sendKey(type, Qt::Key_Left); sendKey(type, Qt::Key_Left);
break; break;
case SDL_GAMEPAD_BUTTON_DPAD_RIGHT : case SDL_CONTROLLER_BUTTON_DPAD_RIGHT:
sendKey(type, Qt::Key_Right); sendKey(type, Qt::Key_Right);
break; break;
case SDL_GAMEPAD_BUTTON_SOUTH : case SDL_CONTROLLER_BUTTON_A:
if (m_UiNavMode) { if (m_UiNavMode) {
sendKey(type, Qt::Key_Space); sendKey(type, Qt::Key_Space);
} }
@@ -174,14 +170,14 @@ void SdlGamepadKeyNavigation::onPollingTimerFired()
sendKey(type, Qt::Key_Return); sendKey(type, Qt::Key_Return);
} }
break; break;
case SDL_GAMEPAD_BUTTON_EAST : case SDL_CONTROLLER_BUTTON_B:
sendKey(type, Qt::Key_Escape); sendKey(type, Qt::Key_Escape);
break; break;
case SDL_GAMEPAD_BUTTON_WEST : case SDL_CONTROLLER_BUTTON_X:
sendKey(type, Qt::Key_Menu); sendKey(type, Qt::Key_Menu);
break; break;
case SDL_GAMEPAD_BUTTON_NORTH : case SDL_CONTROLLER_BUTTON_Y:
case SDL_GAMEPAD_BUTTON_START : case SDL_CONTROLLER_BUTTON_START:
// HACK: We use this keycode to inform main.qml // HACK: We use this keycode to inform main.qml
// to show the settings when Key_Menu is handled // to show the settings when Key_Menu is handled
// by the control in focus. // by the control in focus.
@@ -192,37 +188,19 @@ void SdlGamepadKeyNavigation::onPollingTimerFired()
} }
break; break;
} }
case SDL_EVENT_GAMEPAD_ADDED : case SDL_CONTROLLERDEVICEADDED:
#if SDL_VERSION_ATLEAST(3, 0, 0) SDL_GameController* gc = SDL_GameControllerOpen(event.cdevice.which);
SDL_Gamepad* gc = SDL_OpenGamepad(event.gdevice.which);
if (gc != nullptr) { if (gc != nullptr) {
SDL_assert(!m_Gamepads.contains(gc));
m_Gamepads.append(gc); m_Gamepads.append(gc);
} }
#else
SDL_Gamepad* gc = SDL_GameControllerOpen(event.cdevice.which);
if (gc != nullptr) {
// SDL_CONTROLLERDEVICEADDED can be reported multiple times for the same
// gamepad in rare cases, because SDL2 doesn't fixup the device index in
// the SDL_CONTROLLERDEVICEADDED event if an unopened gamepad disappears
// before we've processed the add event.
if (!m_Gamepads.contains(gc)) {
m_Gamepads.append(gc);
}
else {
// We already have this game controller open
SDL_CloseGamepad(gc);
}
}
#endif
break; break;
} }
} }
// Handle analog sticks by polling // Handle analog sticks by polling
for (auto gc : m_Gamepads) { for (auto gc : m_Gamepads) {
short leftX = SDL_GetGamepadAxis(gc, SDL_GAMEPAD_AXIS_LEFTX); short leftX = SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_LEFTX);
short leftY = SDL_GetGamepadAxis(gc, SDL_GAMEPAD_AXIS_LEFTY); short leftY = SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_LEFTY);
if (SDL_GetTicks() - m_LastAxisNavigationEventTime < AXIS_NAVIGATION_REPEAT_DELAY) { if (SDL_GetTicks() - m_LastAxisNavigationEventTime < AXIS_NAVIGATION_REPEAT_DELAY) {
// Do nothing // Do nothing
} }
@@ -261,6 +239,31 @@ void SdlGamepadKeyNavigation::onPollingTimerFired()
sendKey(QEvent::Type::KeyRelease, Qt::Key_Right); sendKey(QEvent::Type::KeyRelease, Qt::Key_Right);
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);
}
}
} }
} }
@@ -274,17 +277,19 @@ void SdlGamepadKeyNavigation::sendKey(QEvent::Type type, Qt::Key key, Qt::Keyboa
} }
} }
void SdlGamepadKeyNavigation::updateTimerState() void SdlGamepadKeyNavigation::sendWheel(QPoint& angleDelta)
{ {
if (m_PollingTimer->isActive() && (!m_HasFocus || !m_Enabled)) { QGuiApplication* app = static_cast<QGuiApplication*>(QGuiApplication::instance());
m_PollingTimer->stop(); QWindow* focusWindow = app->focusWindow();
} if (focusWindow != nullptr) {
else if (!m_PollingTimer->isActive() && m_HasFocus && m_Enabled) { QPoint mousePos(focusWindow->width() / 2, focusWindow->height() / 2);
// Flush events on the first poll QPoint globalPos(focusWindow->mapToGlobal(mousePos));
m_FirstPoll = true; #if QT_VERSION >= QT_VERSION_CHECK(5, 12, 0)
QWheelEvent wheelEvent(mousePos, globalPos, QPoint(), angleDelta, Qt::NoButton, Qt::NoModifier, Qt::NoScrollPhase, false, Qt::MouseEventSynthesizedByApplication);
// Poll every 50 ms for a new joystick event #else
m_PollingTimer->start(50); QWheelEvent wheelEvent(mousePos, globalPos, QPoint(), angleDelta, angleDelta.y(), Qt::Vertical, Qt::NoButton, Qt::NoModifier, Qt::NoScrollPhase, Qt::MouseEventSynthesizedByApplication, false);
#endif
app->sendEvent(focusWindow, &wheelEvent);
} }
} }
@@ -298,6 +303,11 @@ int SdlGamepadKeyNavigation::getConnectedGamepads()
Q_ASSERT(m_Enabled); Q_ASSERT(m_Enabled);
int count = 0; int count = 0;
SDL_free(SDL_GetGamepads(&count)); for (int i = 0; i < SDL_NumJoysticks(); i++) {
if (SDL_IsGameController(i)) {
count++;
}
}
return count; return count;
} }
+4 -11
View File
@@ -3,16 +3,14 @@
#include <QTimer> #include <QTimer>
#include <QEvent> #include <QEvent>
#include "SDL_compat.h" #include <SDL.h>
#include "settings/streamingpreferences.h"
class SdlGamepadKeyNavigation : public QObject class SdlGamepadKeyNavigation : public QObject
{ {
Q_OBJECT Q_OBJECT
public: public:
SdlGamepadKeyNavigation(StreamingPreferences* prefs); SdlGamepadKeyNavigation();
~SdlGamepadKeyNavigation(); ~SdlGamepadKeyNavigation();
@@ -20,27 +18,22 @@ public:
Q_INVOKABLE void disable(); Q_INVOKABLE void disable();
Q_INVOKABLE void notifyWindowFocus(bool hasFocus);
Q_INVOKABLE void setUiNavMode(bool settingsMode); Q_INVOKABLE void setUiNavMode(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);
void updateTimerState();
private slots: private slots:
void onPollingTimerFired(); void onPollingTimerFired();
private: private:
StreamingPreferences* m_Prefs;
QTimer* m_PollingTimer; QTimer* m_PollingTimer;
QList<SDL_Gamepad*> m_Gamepads; QList<SDL_GameController*> m_Gamepads;
bool m_Enabled; bool m_Enabled;
bool m_UiNavMode; bool m_UiNavMode;
bool m_FirstPoll; bool m_FirstPoll;
bool m_HasFocus;
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.
+384 -637
View File
File diff suppressed because it is too large. Load diff
Binary file not shown.
+288 -533
View File
File diff suppressed because it is too large. Load diff
Binary file not shown.
+293 -542
View File
File diff suppressed because it is too large. Load diff
Binary file not shown.
File diff suppressed because it is too large. Load diff
Binary file not shown.
+384 -598
View File
File diff suppressed because it is too large. Load diff
Binary file not shown.
File diff suppressed because it is too large. Load diff
Binary file not shown.
+309 -558
View File
File diff suppressed because it is too large. Load diff
Binary file not shown.
+547 -812
View File
File diff suppressed because it is too large. Load diff
Binary file not shown.
+294 -543
View File
File diff suppressed because it is too large. Load diff
Binary file not shown.
+303 -556
View File
File diff suppressed because it is too large. Load diff
Binary file not shown.
+294 -547
View File
File diff suppressed because it is too large. Load diff
Binary file not shown.
File diff suppressed because it is too large. Load diff
Binary file not shown.
+288 -533
View File
File diff suppressed because it is too large. Load diff
Binary file not shown.
+292 -541
View File
File diff suppressed because it is too large. Load diff
Binary file not shown.
File diff suppressed because it is too large. Load diff
Binary file not shown.
+300 -549
View File
File diff suppressed because it is too large. Load diff
Binary file not shown.
+332 -581
View File
File diff suppressed because it is too large. Load diff
Binary file not shown.
+325 -574
View File
File diff suppressed because it is too large. Load diff
Binary file not shown.
+457 -718
View File
File diff suppressed because it is too large. Load diff
Binary file not shown.
+374 -627
View File
File diff suppressed because it is too large. Load diff
Binary file not shown.
+523 -788
View File
File diff suppressed because it is too large. Load diff
Binary file not shown.
+434 -695
View File
File diff suppressed because it is too large. Load diff
Binary file not shown.
+289 -538
View File
File diff suppressed because it is too large. Load diff
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large. Load diff
Binary file not shown.
Binary file not shown.
Loaded 100 of 230 files, more files were not shown because too many files have changed in this diff. Show more