Compare commits

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

Before

Width:  |  Height:  |  Size: 2.9 KiB

-13
View File
@@ -1,13 +0,0 @@
#!/bin/sh
# The default HOME is not persistent, so override
# it to a path on the onboard flash. Otherwise our
# pairing data will be lost each reboot.
HOME=/usr/local/moonlight
# Renice PE_Single_CPU which seems to host A/V stuff
renice -10 -p $(pidof PE_Single_CPU)
# Renice Moonlight itself to avoid preemption by background tasks
# Write output to a logfile in /tmp
exec nice -n -10 ./bin/moonlight > /tmp/moonlight.log
-3
View File
@@ -1,3 +0,0 @@
name=Moonlight
icon=moonlight.png
run=moonlight.sh
+114 -259
View File
@@ -1,23 +1,29 @@
import QtQuick 2.9
import QtQuick.Dialogs 1.2
import QtQuick.Controls 2.2
import AppModel 1.0
import ComputerManager 1.0
import SdlGamepadKeyNavigation 1.0
CenteredGridView {
GridView {
property int computerIndex
property AppModel appModel : createModel()
property bool activated
property bool showHiddenGames
property bool showGames
id: appGrid
focus: true
activeFocusOnTab: true
topMargin: 20
bottomMargin: 5
cellWidth: 230; cellHeight: 297;
anchors.fill: parent
anchors.leftMargin: (parent.width % (cellWidth + anchors.rightMargin)) / 2
anchors.topMargin: 20
anchors.rightMargin: 5
anchors.bottomMargin: 5
cellWidth: 225; cellHeight: 385;
// The StackView will trigger a visibility change when
// we're pushed onto it, causing our onVisibleChanged
// routine to run, but only if we start as invisible
visible: false
function computerLost()
{
@@ -26,317 +32,154 @@ CenteredGridView {
}
Component.onCompleted: {
// Don't show any highlighted item until interacting with them.
// We do this here instead of onActivated to avoid losing the user's
// selection when backing out of a different page of the app.
// Don't show any highlighted item until interacting with them
currentIndex = -1
}
StackView.onActivated: {
appModel.computerLost.connect(computerLost)
activated = true
// Highlight the first item if a gamepad is connected
if (currentIndex == -1 && SdlGamepadKeyNavigation.getConnectedGamepads() > 0) {
currentIndex = 0
}
if (!showGames && !showHiddenGames) {
// Check if there's a direct launch app
var directLaunchAppIndex = model.getDirectLaunchAppIndex();
if (directLaunchAppIndex >= 0) {
// Start the direct launch app if nothing else is running
currentIndex = directLaunchAppIndex
currentItem.launchOrResumeSelectedApp(false)
// Set showGames so we will not loop when the stream ends
showGames = true
}
}
SdlGamepadKeyNavigation {
id: gamepadKeyNav
}
StackView.onDeactivating: {
appModel.computerLost.disconnect(computerLost)
activated = false
onVisibleChanged: {
if (visible) {
appModel.computerLost.connect(computerLost)
gamepadKeyNav.enable()
}
else {
appModel.computerLost.disconnect(computerLost)
gamepadKeyNav.disable()
}
}
function createModel()
{
var model = Qt.createQmlObject('import AppModel 1.0; AppModel {}', parent, '')
model.initialize(ComputerManager, computerIndex, showHiddenGames)
model.initialize(ComputerManager, computerIndex)
return model
}
model: appModel
delegate: NavigableItemDelegate {
width: 220; height: 287;
width: 200; height: 335;
grid: appGrid
property alias appContextMenu: appContextMenuLoader.item
property alias appNameText: appNameTextLoader.item
// Dim the app if it's hidden
opacity: model.hidden ? 0.4 : 1.0
Image {
id: appIcon
anchors.horizontalCenter: parent.horizontalCenter;
y: 20
source: model.boxart
sourceSize {
width: 150
height: 200
}
width: sourceSize.width
height: sourceSize.height
fillMode: Image.Pad
}
Image {
property bool isPlaceholder: false
id: appIcon
anchors.horizontalCenter: parent.horizontalCenter
y: 10
source: model.boxart
onSourceSizeChanged: {
if ((sourceSize.width == 130 && sourceSize.height == 180) || // GFE 2.0 placeholder image
(sourceSize.width == 628 && sourceSize.height == 888) || // GFE 3.0 placeholder image
(sourceSize.width == 200 && sourceSize.height == 266)) // Our no_app_image.png
{
isPlaceholder = true
}
else
{
isPlaceholder = false
}
width = 200
height = 267
id: runningIcon
anchors.centerIn: appIcon
visible: model.running
source: "qrc:/res/baseline-play_circle_filled_white-48px.svg"
sourceSize {
width: 75
height: 75
}
}
Label {
id: appNameText
text: model.name
width: parent.width
height: 125
anchors.top: appIcon.bottom
font.pointSize: 22
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.Wrap
elide: Text.ElideRight
// Display a tooltip with the full name if it's truncated
ToolTip.text: model.name
ToolTip.delay: 1000
ToolTip.timeout: 5000
ToolTip.visible: (parent.hovered || parent.highlighted) && (!appNameText || appNameText.truncated)
ToolTip.visible: (parent.hovered || parent.highlighted) && truncated
}
Loader {
active: model.running
asynchronous: true
anchors.fill: appIcon
sourceComponent: Item {
ToolButton {
anchors.horizontalCenterOffset: appIcon.isPlaceholder ? -47 : 0
anchors.verticalCenterOffset: appIcon.isPlaceholder ? -75 : -60
anchors.centerIn: parent
implicitWidth: 125
implicitHeight: 125
Image {
source: "qrc:/res/baseline-play_circle_filled_white-48px.svg"
anchors.centerIn: parent
sourceSize {
width: 75
height: 75
}
}
onClicked: {
launchOrResumeSelectedApp(true)
}
ToolTip.text: qsTr("Resume Game")
ToolTip.delay: 1000
ToolTip.timeout: 3000
ToolTip.visible: hovered
}
ToolButton {
anchors.horizontalCenterOffset: appIcon.isPlaceholder ? 47 : 0
anchors.verticalCenterOffset: appIcon.isPlaceholder ? -75 : 60
anchors.centerIn: parent
implicitWidth: 125
implicitHeight: 125
Image {
source: "qrc:/res/baseline-cancel-24px.svg"
anchors.centerIn: parent
sourceSize {
width: 75
height: 75
}
}
onClicked: {
doQuitGame()
}
ToolTip.text: qsTr("Quit Game")
ToolTip.delay: 1000
ToolTip.timeout: 3000
ToolTip.visible: hovered
}
}
}
Loader {
id: appNameTextLoader
active: appIcon.isPlaceholder
// This loader is not asynchronous to avoid noticeable differences
// in the time in which the text loads for each game.
width: appIcon.width
height: model.running ? 175 : appIcon.height
anchors.left: appIcon.left
anchors.right: appIcon.right
anchors.bottom: appIcon.bottom
sourceComponent: Label {
id: appNameText
text: model.name
font.pointSize: 22
leftPadding: 20
rightPadding: 20
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.Wrap
elide: Text.ElideRight
}
}
function launchOrResumeSelectedApp(quitExistingApp)
function launchOrResumeSelectedApp()
{
var runningId = appModel.getRunningAppId()
if (runningId !== 0 && runningId !== model.appid) {
if (quitExistingApp) {
quitAppDialog.appName = appModel.getRunningAppName()
quitAppDialog.segueToStream = true
quitAppDialog.nextAppName = model.name
quitAppDialog.nextAppIndex = index
quitAppDialog.open()
}
var runningIndex = appModel.getRunningAppIndex()
if (runningIndex >= 0 && runningIndex !== index) {
quitAppDialog.appName = appModel.getRunningAppName()
quitAppDialog.segueToStream = true
quitAppDialog.nextAppName = model.name
quitAppDialog.nextAppIndex = index
quitAppDialog.open()
return
}
var component = Qt.createComponent("StreamSegue.qml")
var segue = component.createObject(stackView, {
"appName": model.name,
"session": appModel.createSessionForApp(index),
"isResume": runningId === model.appid
})
var segue = component.createObject(stackView, {"appName": model.name, "session": appModel.createSessionForApp(index)})
stackView.push(segue)
}
onClicked: {
// Only allow clicking on the box art for non-running games.
// For running games, buttons will appear to resume or quit which
// will handle starting the game and clicks on the box art will
// be ignored.
if (!model.running) {
launchOrResumeSelectedApp(true)
}
}
onPressAndHold: {
// popup() ensures the menu appears under the mouse cursor
if (appContextMenu.popup) {
appContextMenu.popup()
}
else {
// Qt 5.9 doesn't have popup()
appContextMenu.open()
}
// Nothing is running or this app is running
launchOrResumeSelectedApp()
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.RightButton;
acceptedButtons: Qt.RightButton
onClicked: {
parent.onPressAndHold()
}
}
Keys.onReturnPressed: {
// Open the app context menu if activated via the gamepad or keyboard
// for running games. If the game isn't running, the above onClicked
// method will handle the launch.
if (model.running) {
// This will be keyboard/gamepad driven so use
// open() instead of popup()
appContextMenu.open()
}
}
Keys.onEnterPressed: {
// Open the app context menu if activated via the gamepad or keyboard
// for running games. If the game isn't running, the above onClicked
// method will handle the launch.
if (model.running) {
// This will be keyboard/gamepad driven so use
// open() instead of popup()
appContextMenu.open()
// popup() ensures the menu appears under the mouse cursor
if (appContextMenu.popup) {
appContextMenu.popup()
}
else {
// Qt 5.9 doesn't have popup()
appContextMenu.open()
}
}
}
Keys.onMenuPressed: {
// This will be keyboard/gamepad driven so use open() instead of popup()
// We must use open() here so the menu is positioned on
// the ItemDelegate and not where the mouse cursor is
appContextMenu.open()
}
function doQuitGame() {
quitAppDialog.appName = appModel.getRunningAppName()
quitAppDialog.segueToStream = false
quitAppDialog.open()
}
Loader {
id: appContextMenuLoader
asynchronous: true
sourceComponent: NavigableMenu {
id: appContextMenu
NavigableMenuItem {
parentMenu: appContextMenu
text: model.running ? qsTr("Resume Game") : qsTr("Launch Game")
onTriggered: launchOrResumeSelectedApp(true)
Menu {
id: appContextMenu
NavigableMenuItem {
text: model.running ? "Resume Game" : "Launch Game"
onTriggered: {
appContextMenu.close()
launchOrResumeSelectedApp()
}
NavigableMenuItem {
parentMenu: appContextMenu
text: qsTr("Quit Game")
onTriggered: doQuitGame()
visible: model.running
}
NavigableMenuItem {
parentMenu: appContextMenu
checkable: true
checked: model.directLaunch
text: qsTr("Direct Launch")
onTriggered: appModel.setAppDirectLaunch(model.index, !model.directLaunch)
enabled: !model.hidden
ToolTip.text: qsTr("Launch this app immediately when the host is selected, bypassing the app selection grid.")
ToolTip.delay: 1000
ToolTip.timeout: 3000
ToolTip.visible: hovered
}
NavigableMenuItem {
parentMenu: appContextMenu
checkable: true
checked: model.hidden
text: qsTr("Hide Game")
onTriggered: appModel.setAppHidden(model.index, !model.hidden)
enabled: model.hidden || (!model.running && !model.directLaunch)
ToolTip.text: qsTr("Hide this game from the app grid. To access hidden games, right-click on the host and choose %1.").arg(qsTr("View All Apps"))
ToolTip.delay: 1000
ToolTip.timeout: 5000
ToolTip.visible: hovered
}
NavigableMenuItem {
text: "Quit Game"
onTriggered: {
quitAppDialog.appName = appModel.getRunningAppName()
quitAppDialog.segueToStream = false
quitAppDialog.open()
}
visible: model.running
}
}
}
NavigableMessageDialog {
MessageDialog {
id: quitAppDialog
modality:Qt.WindowModal
property string appName : ""
property bool segueToStream : false
property string nextAppName: ""
property int nextAppIndex: 0
text:qsTr("Are you sure you want to quit %1? Any unsaved progress will be lost.").arg(appName)
standardButtons: Dialog.Yes | Dialog.No
text:"Are you sure you want to quit " + appName +"? Any unsaved progress will be lost."
standardButtons: StandardButton.Yes | StandardButton.No
function quitApp() {
var component = Qt.createComponent("QuitSegue.qml")
@@ -358,8 +201,20 @@ CenteredGridView {
appModel.quitRunningApp()
}
onYes: quitApp()
// For keyboard/gamepad navigation
onAccepted: quitApp()
}
ScrollBar.vertical: ScrollBar {}
ScrollBar.vertical: ScrollBar {
parent: appGrid.parent
anchors {
top: appGrid.top
left: appGrid.right
bottom: appGrid.bottom
leftMargin: -10
}
}
}
+4 -19
View File
@@ -1,15 +1,11 @@
import QtQuick 2.9
import QtQuick.Controls 2.2
import SdlGamepadKeyNavigation 1.0
// https://stackoverflow.com/questions/45029968/how-do-i-set-the-combobox-width-to-fit-the-largest-item
ComboBox {
property int textWidth
property int desiredWidth : leftPadding + textWidth + indicator.width + rightPadding
property int maximumWidth : parent.width
implicitWidth: desiredWidth < maximumWidth ? desiredWidth : maximumWidth
implicitWidth: leftPadding + textWidth + indicator.width + rightPadding
TextMetrics {
id: popupMetrics
@@ -19,7 +15,9 @@ ComboBox {
id: textMetrics
}
function recalculateWidth() {
// We call this every time the options change (and init)
// so we can adjust the combo box width here too
onActivated: {
textMetrics.font = font
popupMetrics.font = popup.font
textWidth = 0
@@ -30,17 +28,4 @@ ComboBox {
textWidth = Math.max(popupMetrics.width, textWidth)
}
}
// We call this every time the options change (and init)
// so we can adjust the combo box width here too
onActivated: recalculateWidth()
popup.onAboutToShow: {
// Switch to normal navigation for combo boxes
SdlGamepadKeyNavigation.setUiNavMode(false)
}
popup.onAboutToHide: {
SdlGamepadKeyNavigation.setUiNavMode(true)
}
}
-43
View File
@@ -1,43 +0,0 @@
import QtQuick 2.9
import QtQuick.Controls 2.2
GridView {
// Detect Qt 5.11 or earlier using presence of synchronousDrag.
// Prior to 5.12, the leftMargin and rightMargin values did not work.
property bool hasBrokenMargins: this.synchronousDrag === undefined
property int minMargin: 10
property real availableWidth: (parent.width - 2 * minMargin)
property int itemsPerRow: availableWidth / cellWidth
property real horizontalMargin: itemsPerRow < count && availableWidth >= cellWidth ?
(availableWidth % cellWidth) / 2 : minMargin
function updateMargins() {
leftMargin = horizontalMargin
rightMargin = horizontalMargin
if (hasBrokenMargins) {
anchors.leftMargin = leftMargin
anchors.rightMargin = rightMargin
}
}
onHorizontalMarginChanged: {
updateMargins()
}
Component.onCompleted: {
if (hasBrokenMargins) {
// This will cause an anchor conflict with the parent StackView
// which breaks animation, but otherwise the grid will not be
// centered in the window.
anchors.fill = parent
anchors.topMargin = topMargin
anchors.bottomMargin = bottomMargin
}
updateMargins()
}
boundsBehavior: Flickable.OvershootBounds
}
+20 -7
View File
@@ -1,16 +1,19 @@
import QtQuick 2.0
import QtQuick.Controls 2.2
import QtQuick.Dialogs 1.2
import ComputerManager 1.0
import Session 1.0
Item {
anchors.fill: parent
function onSearchingComputer() {
stageLabel.text = qsTr("Establishing connection to PC...")
stageLabel.text = "Establishing connection to PC..."
}
function onQuittingApp() {
stageLabel.text = qsTr("Quitting app...")
stageLabel.text = "Quitting app..."
}
function onFailure(message) {
@@ -18,8 +21,13 @@ Item {
errorDialog.open()
}
StackView.onActivated: {
if (!launcher.isExecuted()) {
// The StackView will trigger a visibility change when
// we're pushed onto it, causing our onVisibleChanged
// routine to run, but only if we start as invisible
visible: false
onVisibleChanged: {
if (visible && !launcher.isExecuted()) {
toolBar.visible = false
launcher.searchingComputer.connect(onSearchingComputer)
launcher.quittingApp.connect(onQuittingApp)
@@ -47,11 +55,16 @@ Item {
}
}
ErrorMessageDialog {
MessageDialog {
id: errorDialog
modality:Qt.WindowModal
icon: StandardIcon.Critical
standardButtons: StandardButton.Ok
onClosed: {
Qt.quit()
onVisibleChanged: {
if (!visible) {
Qt.quit()
}
}
}
}
+46 -18
View File
@@ -1,16 +1,19 @@
import QtQml 2.2
import QtQuick 2.0
import QtQuick.Controls 2.2
import QtQuick.Dialogs 1.2
import ComputerManager 1.0
import SdlGamepadKeyNavigation 1.0
Item {
visible: false
function onSearchingComputer() {
stageLabel.text = qsTr("Establishing connection to PC...")
stageLabel.text = "Establishing connection to PC..."
}
function onSearchingApp() {
stageLabel.text = qsTr("Loading app list...")
stageLabel.text = "Loading app list..."
}
function onSessionCreated(appName, session) {
@@ -33,14 +36,9 @@ Item {
quitAppDialog.open()
}
StackView.onActivated: {
if (!launcher.isExecuted()) {
onVisibleChanged: {
if (visible && !launcher.isExecuted()) {
toolBar.visible = false
// Normally this is enabled by PcView, but we will won't
// load PcView when streaming from the command-line.
SdlGamepadKeyNavigation.enable()
launcher.searchingComputer.connect(onSearchingComputer)
launcher.searchingApp.connect(onSearchingApp)
launcher.sessionCreated.connect(onSessionCreated)
@@ -68,18 +66,26 @@ Item {
}
}
ErrorMessageDialog {
MessageDialog {
id: errorDialog
onClosed: {
Qt.quit();
modality:Qt.WindowModal
icon: StandardIcon.Critical
standardButtons: StandardButton.Ok | StandardButton.Help
onHelp: {
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Troubleshooting");
}
onVisibleChanged: {
if (!visible) {
Qt.quit();
}
}
}
NavigableMessageDialog {
MessageDialog {
id: quitAppDialog
text:qsTr("Are you sure you want to quit %1? Any unsaved progress will be lost.").arg(appName)
standardButtons: Dialog.Yes | Dialog.No
modality:Qt.WindowModal
text:"Are you sure you want to quit " + appName +"? Any unsaved progress will be lost."
standardButtons: StandardButton.Yes | StandardButton.No
property string appName : ""
function quitApp() {
@@ -90,7 +96,29 @@ Item {
launcher.quitRunningApp()
}
onYes: quitApp()
// For keyboard/gamepad navigation
onAccepted: quitApp()
onRejected: Qt.quit()
// Exit process if app quit is rejected (reacts also to closing of the
// dialog from title bar's close button).
// Note: this depends on undocumented behavior of visibleChanged()
// signal being emitted before yes() or accepted() has been emitted.
onVisibleChanged: {
if (!visible) {
quitTimer.start()
}
}
Component.onCompleted: {
yes.connect(quitTimer.stop)
accepted.connect(quitTimer.stop)
}
}
Timer {
id: quitTimer
interval: 100
onTriggered: Qt.quit()
}
}
-8
View File
@@ -1,8 +0,0 @@
import QtQuick 2.0
import QtQuick.Controls 2.2
import SystemProperties 1.0
NavigableMessageDialog {
standardButtons: Dialog.Ok | (SystemProperties.hasBrowser ? Dialog.Help : 0)
}
+1 -1
View File
@@ -1,5 +1,5 @@
import QtQuick 2.0
Item {
objectName: qsTr("Gamepad Mapping")
objectName: "Gamepad Mapping"
}
-18
View File
@@ -1,18 +0,0 @@
import QtQuick 2.0
import QtQuick.Controls 2.2
Dialog {
// We should use Overlay.overlay here but that's not available in Qt 5.9 :(
parent: ApplicationWindow.contentItem
x: Math.round((parent.width - width) / 2)
y: Math.round((parent.height - height) / 2)
onAboutToHide: {
// We must force focus back to the last item for platforms without
// support for more than one active window like Steam Link. If
// we don't, gamepad and keyboard navigation will break after a
// dialog appears.
stackView.forceActiveFocus()
}
}
-3
View File
@@ -21,7 +21,4 @@ ItemDelegate {
Keys.onReturnPressed: {
clicked()
}
Keys.onEnterPressed: {
clicked()
}
}
-15
View File
@@ -1,15 +0,0 @@
import QtQuick 2.0
import QtQuick.Controls 2.2
Menu {
onOpened: {
// Give focus to the first visible and enabled menu item
for (var i = 0; i < count; i++) {
var item = itemAt(i)
if (item.visible && item.enabled) {
item.forceActiveFocus(Qt.TabFocusReason)
break
}
}
}
}
-20
View File
@@ -2,31 +2,11 @@ import QtQuick 2.0
import QtQuick.Controls 2.2
MenuItem {
// Qt 5.10 has a menu property, but we need to support 5.9
// so we must make our own.
property Menu parentMenu
// Ensure focus can't be given to an invisible item
enabled: visible
height: visible ? implicitHeight : 0
focusPolicy: visible ? Qt.TabFocus : Qt.NoFocus
onTriggered: {
// We must close the context menu first or
// it can steal focus from any dialogs that
// onTriggered may spawn.
parentMenu.close()
}
Keys.onReturnPressed: {
triggered()
}
Keys.onEnterPressed: {
triggered()
}
Keys.onEscapePressed: {
parentMenu.close()
}
}
-78
View File
@@ -1,78 +0,0 @@
import QtQuick 2.0
import QtQuick.Controls 2.2
import QtQuick.Layouts 1.2
NavigableDialog {
id: dialog
property alias text: dialogLabel.dialogText
property alias showSpinner: dialogSpinner.visible
property alias imageSrc: dialogImage.source
property string helpText
property string helpUrl : "https://github.com/moonlight-stream/moonlight-docs/wiki/Troubleshooting"
property string helpTextSeparator : " "
onOpened: {
// Force keyboard focus on the label so keyboard navigation works
dialogLabel.forceActiveFocus()
}
RowLayout {
spacing: 10
BusyIndicator {
id: dialogSpinner
visible: false
}
Image {
id: dialogImage
source: (standardButtons & Dialog.Yes) ?
"qrc:/res/baseline-help_outline-24px.svg" :
"qrc:/res/baseline-error_outline-24px.svg"
sourceSize {
// The icon should be square so use the height as the width too
width: 50
height: 50
}
visible: !showSpinner
}
Label {
property string dialogText
id: dialogLabel
text: dialogText + ((helpText && (standardButtons & Dialog.Help)) ? (helpTextSeparator + helpText) : "")
wrapMode: Text.Wrap
elide: Label.ElideRight
// Cap the width so the dialog doesn't grow horizontally forever. This
// will cause word wrap to kick in.
Layout.maximumWidth: 400
Layout.maximumHeight: 400
Keys.onReturnPressed: {
accept()
}
Keys.onEnterPressed: {
accept()
}
Keys.onEscapePressed: {
reject()
}
}
}
footer: DialogButtonBox {
id: dialogButtonBox
standardButtons: dialog.standardButtons
onHelpRequested: {
Qt.openUrlExternally(helpUrl)
close()
}
}
}
-23
View File
@@ -2,35 +2,12 @@ import QtQuick 2.0
import QtQuick.Controls 2.2
ToolButton {
property string iconSource
activeFocusOnTab: true
// FIXME: We're using an Image here rather than icon.source because
// icons don't work on Qt 5.9 LTS.
Image {
id: image
source: iconSource
anchors.centerIn: parent.background
sourceSize {
width: parent.background.width * 1.10
height: parent.background.height * 1.10
}
}
// This determines the size of the Material highlight. We increase it
// from the default because we use larger than normal icons for TV readability.
background.width: (parent.height - parent.anchors.bottomMargin - parent.anchors.topMargin) * 0.60
background.height: (parent.height - parent.anchors.bottomMargin - parent.anchors.topMargin) * 0.60
Keys.onReturnPressed: {
clicked()
}
Keys.onEnterPressed: {
clicked()
}
Keys.onRightPressed: {
nextItemInFocusChain(true).forceActiveFocus(Qt.TabFocus)
}
+127 -222
View File
@@ -1,6 +1,8 @@
import QtQuick 2.9
import QtQuick.Controls 2.2
import QtQuick.Dialogs 1.2
import QtQuick.Layouts 1.3
import QtQuick.Window 2.2
import ComputerModel 1.0
@@ -8,44 +10,48 @@ import ComputerManager 1.0
import StreamingPreferences 1.0
import SdlGamepadKeyNavigation 1.0
CenteredGridView {
GridView {
property ComputerModel computerModel : createModel()
id: pcGrid
focus: true
activeFocusOnTab: true
topMargin: 20
bottomMargin: 5
cellWidth: 310; cellHeight: 330;
objectName: qsTr("Computers")
anchors.fill: parent
anchors.leftMargin: (parent.width % (cellWidth + anchors.rightMargin)) / 2
anchors.topMargin: 20
anchors.rightMargin: 5
anchors.bottomMargin: 5
cellWidth: 350; cellHeight: 350;
objectName: "Computers"
Component.onCompleted: {
// Don't show any highlighted item until interacting with them.
// We do this here instead of onActivated to avoid losing the user's
// selection when backing out of a different page of the app.
currentIndex = -1
// The StackView will trigger a visibility change when
// we're pushed onto it, causing our onVisibleChanged
// routine to run, but only if we start as invisible
visible: false
StreamingPreferences {
id: prefs
}
// Note: Any initialization done here that is critical for streaming must
// also be done in CliStartStreamSegue.qml, since this code does not run
// for command-line initiated streams.
StackView.onActivated: {
// Setup signals on CM
ComputerManager.computerAddCompleted.connect(addComplete)
SdlGamepadKeyNavigation {
id: gamepadKeyNav
}
// This is a bit of a hack to do this here as opposed to main.qml, but
// we need it enabled before calling getConnectedGamepads() and PcView
// is never destroyed, so it should be okay.
SdlGamepadKeyNavigation.enable()
// Highlight the first item if a gamepad is connected
if (currentIndex == -1 && SdlGamepadKeyNavigation.getConnectedGamepads() > 0) {
currentIndex = 0
onVisibleChanged: {
if (visible) {
gamepadKeyNav.enable()
}
else {
gamepadKeyNav.disable()
}
}
StackView.onDeactivating: {
ComputerManager.computerAddCompleted.disconnect(addComplete)
Component.onCompleted: {
// Setup signals on CM
ComputerManager.computerAddCompleted.connect(addComplete)
// Don't show any highlighted item until interacting with them
currentIndex = -1
}
function pairingComplete(error)
@@ -56,23 +62,14 @@ CenteredGridView {
// Display a failed dialog if we got an error
if (error !== undefined) {
errorDialog.text = error
errorDialog.helpText = ""
errorDialog.open()
}
}
function addComplete(success, detectedPortBlocking)
function addComplete(success)
{
if (!success) {
errorDialog.text = qsTr("Unable to connect to the specified PC.")
if (detectedPortBlocking) {
errorDialog.text += "\n\n" + qsTr("This PC's Internet connection is blocking Moonlight. Streaming over the Internet may not work while connected to this network.")
}
else {
errorDialog.helpText = qsTr("Click the Help button for possible solutions.")
}
errorDialog.text = "Unable to connect to the specified PC. Click the Help button for possible solutions."
errorDialog.open()
}
}
@@ -82,43 +79,21 @@ CenteredGridView {
var model = Qt.createQmlObject('import ComputerModel 1.0; ComputerModel {}', parent, '')
model.initialize(ComputerManager)
model.pairingCompleted.connect(pairingComplete)
model.connectionTestCompleted.connect(testConnectionDialog.connectionTestComplete)
return model
}
Row {
anchors.centerIn: parent
spacing: 5
visible: pcGrid.count === 0
BusyIndicator {
id: searchSpinner
visible: StreamingPreferences.enableMdns
}
Label {
height: searchSpinner.height
elide: Label.ElideRight
text: StreamingPreferences.enableMdns ? qsTr("Searching for PCs on your local network with NVIDIA GameStream enabled...")
: qsTr("Automatic PC discovery is disabled. Add your PC manually.")
font.pointSize: 20
verticalAlignment: Text.AlignVCenter
wrapMode: Text.Wrap
}
}
model: computerModel
delegate: NavigableItemDelegate {
width: 300; height: 320;
width: 300; height: 300;
grid: pcGrid
property alias pcContextMenu : pcContextMenuLoader.item
Image {
id: pcIcon
anchors.horizontalCenter: parent.horizontalCenter
source: "qrc:/res/desktop_windows-48px.svg"
source: {
model.addPc ? "qrc:/res/ic_add_to_queue_white_48px.svg" : "qrc:/res/ic_tv_white_48px.svg"
}
sourceSize {
width: 200
height: 200
@@ -130,8 +105,8 @@ CenteredGridView {
id: stateIcon
anchors.horizontalCenter: pcIcon.horizontalCenter
anchors.verticalCenter: pcIcon.verticalCenter
anchors.verticalCenterOffset: -15
visible: !model.statusUnknown && (!model.online || !model.paired)
anchors.verticalCenterOffset: -10
visible: !model.addPc && !model.statusUnknown && (!model.online || !model.paired)
source: !model.online ? "qrc:/res/baseline-warning-24px.svg" : "qrc:/res/baseline-lock-24px.svg"
sourceSize {
width: 75
@@ -143,10 +118,10 @@ CenteredGridView {
id: statusUnknownSpinner
anchors.horizontalCenter: pcIcon.horizontalCenter
anchors.verticalCenter: pcIcon.verticalCenter
anchors.verticalCenterOffset: -15
anchors.verticalCenterOffset: -10
width: 75
height: 75
visible: model.statusUnknown
visible: !model.addPc && model.statusUnknown
}
Label {
@@ -155,71 +130,33 @@ CenteredGridView {
width: parent.width
anchors.top: pcIcon.bottom
anchors.bottom: parent.bottom
font.pointSize: 36
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.Wrap
elide: Text.ElideRight
}
Loader {
id: pcContextMenuLoader
asynchronous: true
sourceComponent: NavigableMenu {
id: pcContextMenu
MenuItem {
text: qsTr("PC Status: %1").arg(model.online ? qsTr("Online") : qsTr("Offline"))
font.bold: true
enabled: false
}
NavigableMenuItem {
parentMenu: pcContextMenu
text: qsTr("View All Apps")
onTriggered: {
var component = Qt.createComponent("AppView.qml")
var appView = component.createObject(stackView, {"computerIndex": index, "objectName": model.name, "showHiddenGames": true})
stackView.push(appView)
}
visible: model.online && model.paired
}
NavigableMenuItem {
parentMenu: pcContextMenu
text: qsTr("Wake PC")
onTriggered: computerModel.wakeComputer(index)
visible: !model.online && model.wakeable
}
NavigableMenuItem {
parentMenu: pcContextMenu
text: qsTr("Test Network")
onTriggered: {
computerModel.testConnectionForComputer(index)
testConnectionDialog.open()
}
}
NavigableMenuItem {
parentMenu: pcContextMenu
text: qsTr("Rename PC")
onTriggered: {
renamePcDialog.pcIndex = index
renamePcDialog.originalName = model.name
renamePcDialog.open()
}
}
NavigableMenuItem {
parentMenu: pcContextMenu
text: qsTr("Delete PC")
onTriggered: {
deletePcDialog.pcIndex = index
// get confirmation first, actual closing is called from the dialog
deletePcDialog.open()
}
Menu {
id: pcContextMenu
NavigableMenuItem {
text: "Wake PC"
onTriggered: computerModel.wakeComputer(index)
visible: !model.addPc && !model.online && model.wakeable
}
NavigableMenuItem {
text: "Delete PC"
onTriggered: {
deletePcDialog.pcIndex = index
// get confirmation first, actual closing is called from the dialog
deletePcDialog.open()
}
}
}
onClicked: {
if (model.online) {
if (model.addPc) {
addPcDialog.open()
}
else if (model.online) {
if (model.paired) {
// go to game view
var component = Qt.createComponent("AppView.qml")
@@ -228,7 +165,7 @@ CenteredGridView {
}
else {
if (!model.busy) {
var pin = computerModel.generatePinString()
var pin = ("0000" + Math.floor(Math.random() * 10000)).slice(-4)
// Kick off pairing in the background
computerModel.pairComputer(index, pin)
@@ -239,8 +176,7 @@ CenteredGridView {
}
else {
// cannot pair while something is streaming or attempting to pair
errorDialog.text = qsTr("You cannot pair while a previous session is still running on the host PC. Quit any running games or reboot the host PC, then try pairing again.")
errorDialog.helpText = ""
errorDialog.text = "This PC is currently busy. Make sure to quit any running games and try again."
errorDialog.open()
}
}
@@ -250,148 +186,117 @@ CenteredGridView {
}
}
onPressAndHold: {
// popup() ensures the menu appears under the mouse cursor
if (pcContextMenu.popup) {
pcContextMenu.popup()
}
else {
// Qt 5.9 doesn't have popup()
pcContextMenu.open()
}
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.RightButton;
onClicked: {
parent.onPressAndHold()
if (!model.addPc) {
// popup() ensures the menu appears under the mouse cursor
if (pcContextMenu.popup) {
pcContextMenu.popup()
}
else {
// Qt 5.9 doesn't have popup()
pcContextMenu.open()
}
}
}
}
Keys.onMenuPressed: {
// We must use open() here so the menu is positioned on
// the ItemDelegate and not where the mouse cursor is
pcContextMenu.open()
if (!model.addPc) {
// We must use open() here so the menu is positioned on
// the ItemDelegate and not where the mouse cursor is
pcContextMenu.open()
}
}
}
ErrorMessageDialog {
MessageDialog {
id: errorDialog
// Using Setup-Guide here instead of Troubleshooting because it's likely that users
// will arrive here by forgetting to enable GameStream or not forwarding ports.
helpUrl: "https://github.com/moonlight-stream/moonlight-docs/wiki/Setup-Guide"
// don't allow edits to the rest of the window while open
modality:Qt.WindowModal
icon: StandardIcon.Critical
standardButtons: StandardButton.Ok | StandardButton.Help
onHelp: {
// Using Setup-Guide here instead of Troubleshooting because it's likely that users
// will arrive here by forgetting to enable GameStream or not forwarding ports.
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Setup-Guide");
}
}
NavigableMessageDialog {
MessageDialog {
id: pairDialog
// Pairing dialog must be modal to prevent double-clicks from triggering
// pairing twice
modal: true
closePolicy: Popup.CloseOnEscape
// don't allow edits to the rest of the window while open
modality:Qt.WindowModal
property string pin : "0000"
text:qsTr("Please enter %1 on your GameStream PC. This dialog will close when pairing is completed.").arg(pin)
standardButtons: Dialog.Cancel
text:"Please enter " + pin + " on your GameStream PC. This dialog will close when pairing is completed."
standardButtons: StandardButton.Cancel
onRejected: {
// FIXME: We should interrupt pairing here
}
}
NavigableMessageDialog {
MessageDialog {
id: deletePcDialog
// don't allow edits to the rest of the window while open
modality:Qt.WindowModal
property int pcIndex : -1;
text:qsTr("Are you sure you want to remove this PC?")
standardButtons: Dialog.Yes | Dialog.No
text:"Are you sure you want to remove this PC?"
standardButtons: StandardButton.Yes | StandardButton.No
function deletePc() {
console.log("deleting PC pairing for PC at index: " + pcIndex)
computerModel.deleteComputer(pcIndex);
}
onYes: deletePc()
// For keyboard/gamepad activation
onAccepted: deletePc()
}
NavigableMessageDialog {
id: testConnectionDialog
closePolicy: Popup.CloseOnEscape
standardButtons: Dialog.Ok
Dialog {
id: addPcDialog
property string label: "Enter the IP address of your GameStream PC"
property string hint: "192.168.1.100"
property alias editText : editTextItem
onAboutToShow: {
testConnectionDialog.text = qsTr("Moonlight is testing your network connection to determine if NVIDIA GameStream is blocked.") + "\n\n" + qsTr("This may take a few seconds…")
showSpinner = true
standardButtons: StandardButton.Ok | StandardButton.Cancel
onVisibleChanged: {
editTextItem.focus = true
editTextItem.selectAll()
}
function connectionTestComplete(result, blockedPorts)
{
if (result === -1) {
text = qsTr("The network test could not be performed because none of Moonlight's connection testing servers were reachable from this PC. Check your Internet connection or try again later.")
imageSrc = "qrc:/res/baseline-warning-24px.svg"
}
else if (result === 0) {
text = qsTr("This network does not appear to be blocking Moonlight. If you still have trouble connecting, check your PC's firewall settings.") + "\n\n" + qsTr("If you are trying to stream over the Internet, install the Moonlight Internet Hosting Tool on your gaming PC and run the included Internet Streaming Tester to check your gaming PC's Internet connection.")
imageSrc = "qrc:/res/baseline-check_circle_outline-24px.svg"
}
else {
text = qsTr("Your PC's current network connection seems to be blocking Moonlight. Streaming over the Internet may not work while connected to this network.") + "\n\n" + qsTr("The following network ports were blocked:") + "\n"
text += blockedPorts
imageSrc = "qrc:/res/baseline-error_outline-24px.svg"
}
// Stop showing the spinner and show the image instead
showSpinner = false
}
}
NavigableDialog {
id: renamePcDialog
property string label: qsTr("Enter the new name for this PC:")
property string originalName
property int pcIndex : -1;
standardButtons: Dialog.Ok | Dialog.Cancel
onOpened: {
// Force keyboard focus on the textbox so keyboard navigation works
editText.forceActiveFocus()
}
onClosed: {
editText.clear()
}
onAccepted: {
if (editText.text) {
computerModel.renameComputer(pcIndex, editText.text)
}
ComputerManager.addNewHost(editText.text, false)
}
ColumnLayout {
Label {
text: renamePcDialog.label
font.bold: true
Text {
id: addPcDialogLabel
text: addPcDialog.label
}
Rectangle {
implicitWidth: parent.parent.width
height: editTextItem.height
TextField {
id: editText
placeholderText: renamePcDialog.originalName
Layout.fillWidth: true
focus: true
Keys.onReturnPressed: {
renamePcDialog.accept()
}
Keys.onEnterPressed: {
renamePcDialog.accept()
TextInput {
id: editTextItem
inputMethodHints: Qt.ImhPreferUppercase
text: addPcDialog.hint
}
}
}
}
ScrollBar.vertical: ScrollBar {}
ScrollBar.vertical: ScrollBar {
parent: pcGrid.parent
anchors {
top: pcGrid.top
left: pcGrid.right
bottom: pcGrid.bottom
leftMargin: -10
}
}
}
+26 -21
View File
@@ -1,5 +1,6 @@
import QtQuick 2.0
import QtQuick.Controls 2.2
import QtQuick.Dialogs 1.2
import ComputerManager 1.0
import Session 1.0
@@ -9,7 +10,14 @@ Item {
property Session nextSession : null
property string nextAppName : ""
property string stageText : qsTr("Quitting %1...").arg(appName)
property string stageText : "Quitting " + appName + "..."
anchors.fill: parent
// The StackView will trigger a visibility change when
// we're pushed onto it, causing our onVisibleChanged
// routine to run, but only if we start as invisible
visible: false
function quitAppCompleted(error)
{
@@ -19,34 +27,28 @@ Item {
errorDialog.open()
}
// Exit this view
stackView.pop()
// If we're supposed to launch another game after this, do so now
if (error === undefined && nextSession !== null) {
var component = Qt.createComponent("StreamSegue.qml")
var segue = component.createObject(stackView, {"appName": nextAppName, "session": nextSession})
stackView.replace(segue)
stackView.push(segue)
}
}
onVisibleChanged: {
if (visible) {
// Connect the quit completion signal
ComputerManager.quitAppCompleted.connect(quitAppCompleted)
}
else {
// Exit this view
stackView.pop()
// Disconnect the signal
ComputerManager.quitAppCompleted.disconnect(quitAppCompleted)
}
}
StackView.onActivated: {
// Hide the toolbar before we start loading
toolBar.visible = false
// Connect the quit completion signal
ComputerManager.quitAppCompleted.connect(quitAppCompleted)
}
StackView.onDeactivating: {
// Show the toolbar again
toolBar.visible = true
// Disconnect the signal
ComputerManager.quitAppCompleted.disconnect(quitAppCompleted)
}
Row {
anchors.centerIn: parent
spacing: 5
@@ -66,7 +68,10 @@ Item {
}
}
ErrorMessageDialog {
MessageDialog {
id: errorDialog
modality:Qt.WindowModal
icon: StandardIcon.Critical
standardButtons: StandardButton.Ok
}
}
+165 -772
View File
File diff suppressed because it is too large Load Diff
+66 -86
View File
@@ -1,32 +1,35 @@
import QtQuick 2.0
import QtQuick.Controls 2.2
import QtQuick.Dialogs 1.2
import QtQuick.Window 2.2
import ComputerManager 1.0
import SdlGamepadKeyNavigation 1.0
import Session 1.0
Item {
property Session session
property string appName
property string stageText : isResume ? qsTr("Resuming %1...").arg(appName) :
qsTr("Starting %1...").arg(appName)
property bool isResume : false
property string stageText : "Starting " + appName + "..."
property bool quitAfter : false
anchors.fill: parent
// The StackView will trigger a visibility change when
// we're pushed onto it, causing our onVisibleChanged
// routine to run, but only if we start as invisible
visible: false
function stageStarting(stage)
{
// Update the spinner text
stageText = qsTr("Starting %1...").arg(stage)
stageText = "Starting " + stage + "..."
}
function stageFailed(stage, errorCode, failingPorts)
function stageFailed(stage, errorCode)
{
// Display the error dialog after Session::exec() returns
streamSegueErrorDialog.text = qsTr("Starting %1 failed: Error %2").arg(stage).arg(errorCode)
if (failingPorts) {
streamSegueErrorDialog.text += "\n\n" + qsTr("Check your firewall and port forwarding rules for port(s): %1").arg(failingPorts)
}
errorDialog.text = "Starting " + stage + " failed: Error " + errorCode
errorDialog.open()
}
function connectionStarted()
@@ -35,7 +38,6 @@ Item {
// see them briefly when we pop off the StackView
stageSpinner.visible = false
stageLabel.visible = false
hintText.visible = false
// Hide the window now that streaming has begun
window.visible = false
@@ -44,7 +46,7 @@ Item {
function displayLaunchError(text)
{
// Display the error dialog after Session::exec() returns
streamSegueErrorDialog.text = text
errorDialog.text = text
}
function displayLaunchWarning(text)
@@ -52,7 +54,7 @@ Item {
// This toast appears for 3 seconds, just shorter than how long
// Session will wait for it to be displayed. This gives it time
// to transition to invisible before continuing.
var toast = Qt.createQmlObject('import QtQuick.Controls 2.2; ToolTip {}', parent, '')
var toast = Qt.createQmlObject('import QtQuick.Controls 2.3; ToolTip {}', parent, '')
toast.text = text
toast.timeout = 3000
toast.visible = true
@@ -68,20 +70,12 @@ Item {
window.visible = true
}
function sessionFinished(portTestResult)
function sessionFinished()
{
if (portTestResult !== 0 && portTestResult !== -1 && streamSegueErrorDialog.text) {
streamSegueErrorDialog.text += "\n\n" + qsTr("This PC's Internet connection is blocking Moonlight. Streaming over the Internet may not work while connected to this network.")
}
// Enable GUI gamepad usage now
SdlGamepadKeyNavigation.enable()
if (quitAfter) {
if (streamSegueErrorDialog.text) {
if (errorDialog.text) {
// Quit when the error dialog is acknowledged
streamSegueErrorDialog.quitAfter = quitAfter
streamSegueErrorDialog.open()
errorDialog.open()
}
else {
// Quit immediately
@@ -98,82 +92,48 @@ Item {
// the Qt UI is visible again to prevent losing
// focus on the dialog which would impact gamepad
// users.
if (streamSegueErrorDialog.text) {
streamSegueErrorDialog.quitAfter = quitAfter
streamSegueErrorDialog.open()
if (errorDialog.text) {
errorDialog.open()
}
}
// Garbage collect the Session object since it's pretty heavyweight
// and keeps other libraries (like SDL_TTF) around until it is deleted.
session = null
gc()
}
StackView.onDeactivating: {
// Show the toolbar again when popped off the stack
toolBar.visible = true
// Enable GUI gamepad usage now
SdlGamepadKeyNavigation.enable()
// It's important that we don't call enable() here
// or it may interfere with the Session instance
// getting notified of initial connected gamepads.
SdlGamepadKeyNavigation {
id: gamepadKeyNav
}
StackView.onActivated: {
// Hide the toolbar before we start loading
toolBar.visible = false
onVisibleChanged: {
if (visible) {
// Hide the toolbar before we start loading
toolBar.visible = false
// Hook up our signals
session.stageStarting.connect(stageStarting)
session.stageFailed.connect(stageFailed)
session.connectionStarted.connect(connectionStarted)
session.displayLaunchError.connect(displayLaunchError)
session.displayLaunchWarning.connect(displayLaunchWarning)
session.quitStarting.connect(quitStarting)
session.sessionFinished.connect(sessionFinished)
// Kick off the stream
spinnerTimer.start()
streamLoader.active = true
}
Timer {
id: spinnerTimer
// Display the spinner appearance a bit to allow us to reach
// the code in Session.exec() that pumps the event loop.
// If we display it immediately, it will briefly hang in the
// middle of the animation on Windows, which looks very
// obviously broken.
interval: 100
onTriggered: stageSpinner.running = true
}
Loader {
id: streamLoader
active: false
asynchronous: true
onLoaded: {
// Set the hint text. We do this here rather than
// in the hintText control itself to synchronize
// with Session.exec() which requires no concurrent
// gamepad usage.
hintText.text = qsTr("Tip:") + " " + qsTr("Press %1 to disconnect your session").arg(SdlGamepadKeyNavigation.getConnectedGamepads() > 0 ?
qsTr("Start+Select+L1+R1") : qsTr("Ctrl+Alt+Shift+Q"))
hintText.text = gamepadKeyNav.getConnectedGamepads() > 0 ?
"Tip: Press Start+Select+L1+R1 to disconnect your session" :
"Tip: Press Ctrl+Alt+Shift+Q to disconnect your session"
// Stop GUI gamepad usage now
SdlGamepadKeyNavigation.disable()
// Garbage collect QML stuff before we start streaming,
// since we'll probably be streaming for a while and we
// won't be able to GC during the stream.
gc()
// Hook up our signals
session.stageStarting.connect(stageStarting)
session.stageFailed.connect(stageFailed)
session.connectionStarted.connect(connectionStarted)
session.displayLaunchError.connect(displayLaunchError)
session.displayLaunchWarning.connect(displayLaunchWarning)
session.quitStarting.connect(quitStarting)
session.sessionFinished.connect(sessionFinished)
// Run the streaming session to completion
session.exec(Screen.virtualX, Screen.virtualY)
}
sourceComponent: Item {}
else if (!quitAfter) {
// Show the toolbar again when we become hidden
toolBar.visible = true
}
}
Row {
@@ -182,7 +142,6 @@ Item {
BusyIndicator {
id: stageSpinner
running: false
}
Label {
@@ -206,4 +165,25 @@ Item {
wrapMode: Text.Wrap
}
MessageDialog {
id: errorDialog
modality:Qt.WindowModal
icon: StandardIcon.Critical
standardButtons: StandardButton.Ok | StandardButton.Help
onAccepted: {
if (quitAfter) {
Qt.quit()
}
}
onHelp: {
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Troubleshooting");
if (quitAfter) {
Qt.quit()
}
}
}
}
+32 -173
View File
@@ -7,7 +7,7 @@ AppModel::AppModel(QObject *parent)
this, &AppModel::handleBoxArtLoaded);
}
void AppModel::initialize(ComputerManager* computerManager, int computerIndex, bool showHiddenGames)
void AppModel::initialize(ComputerManager* computerManager, int computerIndex)
{
m_ComputerManager = computerManager;
connect(m_ComputerManager, &ComputerManager::computerStateChanged,
@@ -15,23 +15,29 @@ void AppModel::initialize(ComputerManager* computerManager, int computerIndex, b
Q_ASSERT(computerIndex < m_ComputerManager->getComputers().count());
m_Computer = m_ComputerManager->getComputers().at(computerIndex);
m_Apps = m_Computer->appList;
m_CurrentGameId = m_Computer->currentGameId;
m_ShowHiddenGames = showHiddenGames;
updateAppList(m_Computer->appList);
}
int AppModel::getRunningAppId()
int AppModel::getRunningAppIndex()
{
return m_CurrentGameId;
if (m_CurrentGameId != 0) {
for (int i = 0; i < m_Apps.count(); i++) {
if (m_Apps[i].id == m_CurrentGameId) {
return i;
}
}
}
return -1;
}
QString AppModel::getRunningAppName()
{
if (m_CurrentGameId != 0) {
for (int i = 0; i < m_AllApps.count(); i++) {
if (m_AllApps[i].id == m_CurrentGameId) {
return m_AllApps[i].name;
for (int i = 0; i < m_Apps.count(); i++) {
if (m_Apps[i].id == m_CurrentGameId) {
return m_Apps[i].name;
}
}
}
@@ -41,23 +47,12 @@ QString AppModel::getRunningAppName()
Session* AppModel::createSessionForApp(int appIndex)
{
Q_ASSERT(appIndex < m_VisibleApps.count());
NvApp app = m_VisibleApps.at(appIndex);
Q_ASSERT(appIndex < m_Apps.count());
NvApp app = m_Apps.at(appIndex);
return new Session(m_Computer, app);
}
int AppModel::getDirectLaunchAppIndex()
{
for (int i = 0; i < m_VisibleApps.count(); i++) {
if (m_VisibleApps[i].directLaunch) {
return i;
}
}
return -1;
}
int AppModel::rowCount(const QModelIndex &parent) const
{
// For list models only the root node (an invalid parent) should return the list's size. For all
@@ -65,7 +60,7 @@ int AppModel::rowCount(const QModelIndex &parent) const
if (parent.isValid())
return 0;
return m_VisibleApps.count();
return m_Apps.count();
}
QVariant AppModel::data(const QModelIndex &index, int role) const
@@ -73,8 +68,8 @@ QVariant AppModel::data(const QModelIndex &index, int role) const
if (!index.isValid())
return QVariant();
Q_ASSERT(index.row() < m_VisibleApps.count());
NvApp app = m_VisibleApps.at(index.row());
Q_ASSERT(index.row() < m_Apps.count());
NvApp app = m_Apps.at(index.row());
switch (role)
{
@@ -85,12 +80,6 @@ QVariant AppModel::data(const QModelIndex &index, int role) const
case BoxArtRole:
// FIXME: const-correctness
return const_cast<BoxArtManager&>(m_BoxArtManager).loadBoxArt(m_Computer, app);
case HiddenRole:
return app.hidden;
case AppIdRole:
return app.id;
case DirectLaunchRole:
return app.directLaunch;
default:
return QVariant();
}
@@ -103,9 +92,6 @@ QHash<int, QByteArray> AppModel::roleNames() const
names[NameRole] = "name";
names[RunningRole] = "running";
names[BoxArtRole] = "boxart";
names[HiddenRole] = "hidden";
names[AppIdRole] = "appid";
names[DirectLaunchRole] = "directLaunch";
return names;
}
@@ -115,138 +101,6 @@ void AppModel::quitRunningApp()
m_ComputerManager->quitRunningApp(m_Computer);
}
bool AppModel::isAppCurrentlyVisible(const NvApp& app)
{
for (const NvApp& visibleApp : m_VisibleApps) {
if (app.id == visibleApp.id) {
return true;
}
}
return false;
}
QVector<NvApp> AppModel::getVisibleApps(const QVector<NvApp>& appList)
{
QVector<NvApp> visibleApps;
for (const NvApp& app : appList) {
// Don't immediately hide games that were previously visible. This
// allows users to easily uncheck the "Hide App" checkbox if they
// check it by mistake.
if (m_ShowHiddenGames || !app.hidden || isAppCurrentlyVisible(app)) {
visibleApps.append(app);
}
}
return visibleApps;
}
void AppModel::updateAppList(QVector<NvApp> newList)
{
m_AllApps = newList;
QVector<NvApp> newVisibleList = getVisibleApps(newList);
// Process removals and updates first
for (int i = 0; i < m_VisibleApps.count(); i++) {
const NvApp& existingApp = m_VisibleApps.at(i);
bool found = false;
for (const NvApp& newApp : newVisibleList) {
if (existingApp.id == newApp.id) {
// If the data changed, update it in our list
if (existingApp != newApp) {
m_VisibleApps.replace(i, newApp);
emit dataChanged(createIndex(i, 0), createIndex(i, 0));
}
found = true;
break;
}
}
if (!found) {
beginRemoveRows(QModelIndex(), i, i);
m_VisibleApps.removeAt(i);
endRemoveRows();
i--;
}
}
// Process additions now
for (const NvApp& newApp : newVisibleList) {
int insertionIndex = m_VisibleApps.size();
bool found = false;
for (int i = 0; i < m_VisibleApps.count(); i++) {
const NvApp& existingApp = m_VisibleApps.at(i);
if (existingApp.id == newApp.id) {
found = true;
break;
}
else if (existingApp.name.toLower() > newApp.name.toLower()) {
insertionIndex = i;
break;
}
}
if (!found) {
beginInsertRows(QModelIndex(), insertionIndex, insertionIndex);
m_VisibleApps.insert(insertionIndex, newApp);
endInsertRows();
}
}
Q_ASSERT(newVisibleList == m_VisibleApps);
}
void AppModel::setAppHidden(int appIndex, bool hidden)
{
Q_ASSERT(appIndex < m_VisibleApps.count());
int appId = m_VisibleApps.at(appIndex).id;
{
QWriteLocker lock(&m_Computer->lock);
for (NvApp& app : m_Computer->appList) {
if (app.id == appId) {
app.hidden = hidden;
break;
}
}
}
m_ComputerManager->clientSideAttributeUpdated(m_Computer);
}
void AppModel::setAppDirectLaunch(int appIndex, bool directLaunch)
{
Q_ASSERT(appIndex < m_VisibleApps.count());
int appId = m_VisibleApps.at(appIndex).id;
{
QWriteLocker lock(&m_Computer->lock);
for (NvApp& app : m_Computer->appList) {
if (directLaunch) {
// We must clear direct launch from all other apps
// to set it on the new app.
app.directLaunch = app.id == appId;
}
else if (app.id == appId) {
// If we're clearing direct launch, we're done once we
// find our matching app ID.
app.directLaunch = false;
break;
}
}
}
m_ComputerManager->clientSideAttributeUpdated(m_Computer);
}
void AppModel::handleComputerStateChanged(NvComputer* computer)
{
// Ignore updates for computers that aren't ours
@@ -265,15 +119,20 @@ void AppModel::handleComputerStateChanged(NvComputer* computer)
// First, process additions/removals from the app list. This
// is required because the new game may now be running, so
// we can't check that first.
if (computer->appList != m_AllApps) {
updateAppList(computer->appList);
if (computer->appList != m_Apps) {
// Just reset the whole thing if the list changes
beginResetModel();
m_Apps = computer->appList;
m_CurrentGameId = computer->currentGameId;
endResetModel();
return;
}
// Finally, process changes to the active app
if (computer->currentGameId != m_CurrentGameId) {
// First, invalidate the running state of newly running game
for (int i = 0; i < m_VisibleApps.count(); i++) {
if (m_VisibleApps[i].id == computer->currentGameId) {
for (int i = 0; i < m_Apps.count(); i++) {
if (m_Apps[i].id == computer->currentGameId) {
emit dataChanged(createIndex(i, 0),
createIndex(i, 0),
QVector<int>() << RunningRole);
@@ -283,8 +142,8 @@ void AppModel::handleComputerStateChanged(NvComputer* computer)
// Next, invalidate the running state of the old game (if it exists)
if (m_CurrentGameId != 0) {
for (int i = 0; i < m_VisibleApps.count(); i++) {
if (m_VisibleApps[i].id == m_CurrentGameId) {
for (int i = 0; i < m_Apps.count(); i++) {
if (m_Apps[i].id == m_CurrentGameId) {
emit dataChanged(createIndex(i, 0),
createIndex(i, 0),
QVector<int>() << RunningRole);
@@ -302,7 +161,7 @@ void AppModel::handleBoxArtLoaded(NvComputer* computer, NvApp app, QUrl /* image
{
Q_ASSERT(computer == m_Computer);
int index = m_VisibleApps.indexOf(app);
int index = m_Apps.indexOf(app);
// Make sure we're not delivering a callback to an app that's already been removed
if (index >= 0) {
+4 -20
View File
@@ -14,32 +14,23 @@ class AppModel : public QAbstractListModel
{
NameRole = Qt::UserRole,
RunningRole,
BoxArtRole,
HiddenRole,
AppIdRole,
DirectLaunchRole
BoxArtRole
};
public:
explicit AppModel(QObject *parent = nullptr);
// Must be called before any QAbstractListModel functions
Q_INVOKABLE void initialize(ComputerManager* computerManager, int computerIndex, bool showHiddenGames);
Q_INVOKABLE void initialize(ComputerManager* computerManager, int computerIndex);
Q_INVOKABLE Session* createSessionForApp(int appIndex);
Q_INVOKABLE int getDirectLaunchAppIndex();
Q_INVOKABLE int getRunningAppId();
Q_INVOKABLE int getRunningAppIndex();
Q_INVOKABLE QString getRunningAppName();
Q_INVOKABLE void quitRunningApp();
Q_INVOKABLE void setAppHidden(int appIndex, bool hidden);
Q_INVOKABLE void setAppDirectLaunch(int appIndex, bool directLaunch);
QVariant data(const QModelIndex &index, int role) const override;
int rowCount(const QModelIndex &parent) const override;
@@ -55,16 +46,9 @@ signals:
void computerLost();
private:
void updateAppList(QVector<NvApp> newList);
QVector<NvApp> getVisibleApps(const QVector<NvApp>& appList);
bool isAppCurrentlyVisible(const NvApp& app);
NvComputer* m_Computer;
BoxArtManager m_BoxArtManager;
ComputerManager* m_ComputerManager;
QVector<NvApp> m_VisibleApps, m_AllApps;
QVector<NvApp> m_Apps;
int m_CurrentGameId;
bool m_ShowHiddenGames;
};
+17 -50
View File
@@ -2,8 +2,6 @@
#include <QThreadPool>
#include <random>
ComputerModel::ComputerModel(QObject* object)
: QAbstractListModel(object) {}
@@ -24,6 +22,18 @@ QVariant ComputerModel::data(const QModelIndex& index, int role) const
return QVariant();
}
if (index.row() == m_Computers.count()) {
// We insert a synthetic item at the end for the Add PC option
switch (role) {
case NameRole:
return "Add PC";
case AddPcRole:
return true;
default:
return QVariant();
}
}
Q_ASSERT(index.row() < m_Computers.count());
NvComputer* computer = m_Computers[index.row()];
@@ -40,6 +50,8 @@ QVariant ComputerModel::data(const QModelIndex& index, int role) const
return computer->currentGameId != 0;
case WakeableRole:
return !computer->macAddress.isEmpty();
case AddPcRole:
return false;
case StatusUnknownRole:
return computer->state == NvComputer::CS_UNKNOWN;
default:
@@ -55,7 +67,8 @@ int ComputerModel::rowCount(const QModelIndex& parent) const
return 0;
}
return m_Computers.count();
// Add PC placeholder counts as 1
return m_Computers.count() + 1;
}
QHash<int, QByteArray> ComputerModel::roleNames() const
@@ -66,6 +79,7 @@ QHash<int, QByteArray> ComputerModel::roleNames() const
names[OnlineRole] = "online";
names[PairedRole] = "paired";
names[BusyRole] = "busy";
names[AddPcRole] = "addPc";
names[WakeableRole] = "wakeable";
names[StatusUnknownRole] = "statusUnknown";
@@ -130,52 +144,6 @@ void ComputerModel::wakeComputer(int computerIndex)
QThreadPool::globalInstance()->start(wakeTask);
}
void ComputerModel::renameComputer(int computerIndex, QString name)
{
Q_ASSERT(computerIndex < m_Computers.count());
m_ComputerManager->renameHost(m_Computers[computerIndex], name);
}
// TODO: Use QRandomGenerator when we drop Qt 5.9 support
QString ComputerModel::generatePinString()
{
std::uniform_int_distribution<int> dist(0, 9999);
std::random_device rd;
std::mt19937 engine(rd());
return QString::asprintf("%04u", dist(engine));
}
class DeferredTestConnectionTask : public QObject, public QRunnable
{
Q_OBJECT
public:
void run()
{
unsigned int portTestResult = LiTestClientConnectivity("qt.conntest.moonlight-stream.org", 443, ML_PORT_FLAG_ALL);
if (portTestResult == ML_TEST_RESULT_INCONCLUSIVE) {
emit connectionTestCompleted(-1, QString());
}
else {
char blockedPorts[512];
LiStringifyPortFlags(portTestResult, "\n", blockedPorts, sizeof(blockedPorts));
emit connectionTestCompleted(portTestResult, QString(blockedPorts));
}
}
signals:
void connectionTestCompleted(int result, QString blockedPorts);
};
void ComputerModel::testConnectionForComputer(int)
{
DeferredTestConnectionTask* testConnectionTask = new DeferredTestConnectionTask();
QObject::connect(testConnectionTask, &DeferredTestConnectionTask::connectionTestCompleted,
this, &ComputerModel::connectionTestCompleted);
QThreadPool::globalInstance()->start(testConnectionTask);
}
void ComputerModel::pairComputer(int computerIndex, QString pin)
{
Q_ASSERT(computerIndex < m_Computers.count());
@@ -206,4 +174,3 @@ void ComputerModel::handleComputerStateChanged(NvComputer* computer)
}
}
#include "computermodel.moc"
+2 -8
View File
@@ -14,7 +14,8 @@ class ComputerModel : public QAbstractListModel
PairedRole,
BusyRole,
WakeableRole,
StatusUnknownRole
StatusUnknownRole,
AddPcRole
};
public:
@@ -31,21 +32,14 @@ public:
Q_INVOKABLE void deleteComputer(int computerIndex);
Q_INVOKABLE QString generatePinString();
Q_INVOKABLE void pairComputer(int computerIndex, QString pin);
Q_INVOKABLE void testConnectionForComputer(int computerIndex);
Q_INVOKABLE void wakeComputer(int computerIndex);
Q_INVOKABLE void renameComputer(int computerIndex, QString name);
Q_INVOKABLE Session* createSessionForCurrentGame(int computerIndex);
signals:
void pairingCompleted(QVariant error);
void connectionTestCompleted(int result, QString blockedPorts);
private slots:
void handleComputerStateChanged(NvComputer* computer);
+118 -226
View File
@@ -1,50 +1,30 @@
import QtQuick 2.9
import QtQuick.Controls 2.2
import QtQuick.Dialogs 1.2
import QtQuick.Layouts 1.3
import QtQuick.Window 2.2
import QtQuick.Controls.Material 2.1
import ComputerManager 1.0
import AutoUpdateChecker 1.0
import StreamingPreferences 1.0
import SystemProperties 1.0
import SdlGamepadKeyNavigation 1.0
ApplicationWindow {
property bool pollingActive: false
// Set by SettingsView to force the back operation to pop all
// pages except the initial view. This is required when doing
// a retranslate() because AppView breaks for some reason.
property bool clearOnBack: false
id: window
visible: true
width: 1280
height: 600
visibility: {
if (SystemProperties.hasDesktopEnvironment) {
if (StreamingPreferences.uiDisplayMode == StreamingPreferences.UI_WINDOWED) return "Windowed"
else if (StreamingPreferences.uiDisplayMode == StreamingPreferences.UI_MAXIMIZED) return "Maximized"
else if (StreamingPreferences.uiDisplayMode == StreamingPreferences.UI_FULLSCREEN) return "FullScreen"
} else {
return "FullScreen"
}
}
// This configures the maximum width of the singleton attached QML ToolTip. If left unconstrained,
// it will never insert a line break and just extend on forever.
ToolTip.toolTip.contentWidth: ToolTip.toolTip.implicitContentWidth < 400 ? ToolTip.toolTip.implicitContentWidth : 400
visibility: prefs.startWindowed ? "Windowed" : "Maximized"
function goBack() {
if (clearOnBack) {
// Pop all items except the first one
stackView.pop(null)
clearOnBack = false
}
else {
stackView.pop()
}
Material.theme: Material.Dark
Material.accent: Material.Purple
StreamingPreferences {
id: prefs
}
StackView {
@@ -62,7 +42,7 @@ ApplicationWindow {
Keys.onEscapePressed: {
if (depth > 1) {
goBack()
stackView.pop()
}
else {
quitConfirmationDialog.open()
@@ -71,7 +51,7 @@ ApplicationWindow {
Keys.onBackPressed: {
if (depth > 1) {
goBack()
stackView.pop()
}
else {
quitConfirmationDialog.open()
@@ -162,20 +142,19 @@ ApplicationWindow {
// onAfterRendering call and potentially reenter this code.
initialized = true;
if (SystemProperties.isWow64) {
if (prefs.isRunningWayland()) {
waylandDialog.open()
}
else if (prefs.isWow64()) {
wow64Dialog.open()
}
else if (!SystemProperties.hasHardwareAcceleration) {
if (SystemProperties.isRunningXWayland) {
xWaylandDialog.open()
}
else {
noHwDecoderDialog.open()
}
else if (!prefs.hasAnyHardwareAcceleration()) {
noHwDecoderDialog.open()
}
if (SystemProperties.unmappedGamepads) {
unmappedGamepadDialog.unmappedGamepads = SystemProperties.unmappedGamepads
var unmappedGamepads = prefs.getUnmappedGamepads()
if (unmappedGamepads) {
unmappedGamepadDialog.unmappedGamepads = unmappedGamepads
unmappedGamepadDialog.open()
}
}
@@ -199,109 +178,44 @@ ApplicationWindow {
header: ToolBar {
id: toolBar
height: 60
anchors.topMargin: 5
anchors.bottomMargin: 5
Label {
id: titleLabel
visible: toolBar.width > 700
anchors.fill: parent
text: stackView.currentItem.objectName
font.pointSize: 20
elide: Label.ElideRight
horizontalAlignment: Qt.AlignHCenter
verticalAlignment: Qt.AlignVCenter
}
RowLayout {
spacing: 20
anchors.leftMargin: 10
anchors.rightMargin: 10
anchors.fill: parent
NavigableToolButton {
// Only make the button visible if the user has navigated somewhere.
visible: stackView.depth > 1
iconSource: "qrc:/res/arrow_left.svg"
// FIXME: We're using an Image here rather than icon.source because
// icons don't work on Qt 5.9 LTS.
Image {
source: "qrc:/res/arrow_left.svg"
anchors.centerIn: parent
sourceSize {
// The icon should be square so use the height as the width too
width: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
height: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
}
}
onClicked: goBack()
onClicked: stackView.pop()
Keys.onDownPressed: {
stackView.currentItem.forceActiveFocus(Qt.TabFocus)
}
}
// This label will appear when the window gets too small and
// we need to ensure the toolbar controls don't collide
Label {
id: titleRowLabel
font.pointSize: titleLabel.font.pointSize
id: titleLabel
text: stackView.currentItem.objectName
font.pointSize: 15
elide: Label.ElideRight
horizontalAlignment: Qt.AlignHCenter
verticalAlignment: Qt.AlignVCenter
Layout.fillWidth: true
// We need this label to always be visible so it can occupy
// the remaining space in the RowLayout. To "hide" it, we
// just set the text to empty string.
text: !titleLabel.visible ? stackView.currentItem.objectName : ""
}
Label {
id: versionLabel
visible: stackView.currentItem.objectName === qsTr("Settings")
text: qsTr("Version %1").arg(SystemProperties.versionString)
font.pointSize: 12
horizontalAlignment: Qt.AlignRight
verticalAlignment: Qt.AlignVCenter
}
NavigableToolButton {
id: discordButton
visible: SystemProperties.hasBrowser &&
stackView.currentItem.objectName === qsTr("Settings")
iconSource: "qrc:/res/Discord-Logo-White.svg"
ToolTip.delay: 1000
ToolTip.timeout: 3000
ToolTip.visible: hovered
ToolTip.text: qsTr("Join our community on Discord")
// TODO need to make sure browser is brought to foreground.
onClicked: Qt.openUrlExternally("https://moonlight-stream.org/discord");
Keys.onDownPressed: {
stackView.currentItem.forceActiveFocus(Qt.TabFocus)
}
}
NavigableToolButton {
id: addPcButton
visible: stackView.currentItem.objectName === qsTr("Computers")
iconSource: "qrc:/res/ic_add_to_queue_white_48px.svg"
ToolTip.delay: 1000
ToolTip.timeout: 3000
ToolTip.visible: hovered
ToolTip.text: qsTr("Add PC manually") + (newPcShortcut.nativeText ? (" ("+newPcShortcut.nativeText+")") : "")
Shortcut {
id: newPcShortcut
sequence: StandardKey.New
onActivated: addPcButton.clicked()
}
onClicked: {
addPcDialog.open()
}
Keys.onDownPressed: {
stackView.currentItem.forceActiveFocus(Qt.TabFocus)
}
}
NavigableToolButton {
@@ -309,25 +223,28 @@ ApplicationWindow {
id: updateButton
iconSource: "qrc:/res/update.svg"
Image {
source: "qrc:/res/update.svg"
anchors.centerIn: parent
sourceSize {
width: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
height: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
}
}
ToolTip.delay: 1000
ToolTip.timeout: 3000
ToolTip.visible: hovered || visible
ToolTip.visible: hovered
// Invisible until we get a callback notifying us that
// an update is available
visible: false
onClicked: {
if (SystemProperties.hasBrowser) {
Qt.openUrlExternally(browserUrl);
}
}
onClicked: Qt.openUrlExternally(browserUrl);
function updateAvailable(version, url)
{
ToolTip.text = qsTr("Update available for Moonlight: Version %1").arg(version)
ToolTip.text = "Update available for Moonlight: Version " + version
updateButton.browserUrl = url
updateButton.visible = true
}
@@ -344,14 +261,20 @@ ApplicationWindow {
NavigableToolButton {
id: helpButton
visible: SystemProperties.hasBrowser
iconSource: "qrc:/res/question_mark.svg"
Image {
source: "qrc:/res/question_mark.svg"
anchors.centerIn: parent
sourceSize {
width: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
height: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
}
}
ToolTip.delay: 1000
ToolTip.timeout: 3000
ToolTip.visible: hovered
ToolTip.text: qsTr("Help") + (helpShortcut.nativeText ? (" ("+helpShortcut.nativeText+")") : "")
ToolTip.text: "Help" + (helpShortcut.nativeText ? (" ("+helpShortcut.nativeText+")") : "")
Shortcut {
id: helpShortcut
@@ -374,11 +297,18 @@ ApplicationWindow {
ToolTip.delay: 1000
ToolTip.timeout: 3000
ToolTip.visible: hovered
ToolTip.text: qsTr("Gamepad Mapper")
ToolTip.text: "Gamepad Mapper"
iconSource: "qrc:/res/ic_videogame_asset_white_48px.svg"
Image {
source: "qrc:/res/ic_videogame_asset_white_48px.svg"
anchors.centerIn: parent
sourceSize {
width: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
height: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
}
}
onClicked: navigateTo("qrc:/gui/GamepadMapper.qml", qsTr("Gamepad Mapping"))
onClicked: navigateTo("qrc:/gui/GamepadMapper.qml", "Gamepad Mapping")
Keys.onDownPressed: {
stackView.currentItem.forceActiveFocus(Qt.TabFocus)
@@ -388,9 +318,16 @@ ApplicationWindow {
NavigableToolButton {
id: settingsButton
iconSource: "qrc:/res/settings.svg"
Image {
source: "qrc:/res/settings.svg"
anchors.centerIn: parent
sourceSize {
width: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
height: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
}
}
onClicked: navigateTo("qrc:/gui/SettingsView.qml", qsTr("Settings"))
onClicked: navigateTo("qrc:/gui/SettingsView.qml", "Settings")
Keys.onDownPressed: {
stackView.currentItem.forceActiveFocus(Qt.TabFocus)
@@ -405,117 +342,72 @@ ApplicationWindow {
ToolTip.delay: 1000
ToolTip.timeout: 3000
ToolTip.visible: hovered
ToolTip.text: qsTr("Settings") + (settingsShortcut.nativeText ? (" ("+settingsShortcut.nativeText+")") : "")
ToolTip.text: "Settings" + (settingsShortcut.nativeText ? (" ("+settingsShortcut.nativeText+")") : "")
}
}
}
ErrorMessageDialog {
MessageDialog {
id: noHwDecoderDialog
text: qsTr("No functioning hardware accelerated H.264 video decoder was detected by Moonlight. " +
"Your streaming performance may be severely degraded in this configuration.")
helpText: qsTr("Click the Help button for more information on solving this problem.")
helpUrl: "https://github.com/moonlight-stream/moonlight-docs/wiki/Fixing-Hardware-Decoding-Problems"
modality:Qt.WindowModal
icon: StandardIcon.Warning
standardButtons: StandardButton.Ok | StandardButton.Help
text: "No functioning hardware accelerated H.264 video decoder was detected by Moonlight. " +
"Your streaming performance may be severely degraded in this configuration. " +
"Click the Help button for more information on solving this problem."
onHelp: {
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Fixing-Hardware-Decoding-Problems");
}
}
ErrorMessageDialog {
id: xWaylandDialog
text: qsTr("Hardware acceleration doesn't work on XWayland. Continuing on XWayland may result in poor streaming performance. " +
"Try running with QT_QPA_PLATFORM=wayland or switch to X11.")
helpText: qsTr("Click the Help button for more information.")
helpUrl: "https://github.com/moonlight-stream/moonlight-docs/wiki/Fixing-Hardware-Decoding-Problems"
MessageDialog {
id: waylandDialog
modality:Qt.WindowModal
icon: StandardIcon.Warning
standardButtons: StandardButton.Ok | StandardButton.Help
text: "Moonlight does not support hardware acceleration on Wayland. Continuing on Wayland may result in poor streaming performance. " +
"Please switch to an X session for optimal performance."
onHelp: {
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Fixing-Hardware-Decoding-Problems");
}
}
NavigableMessageDialog {
MessageDialog {
id: wow64Dialog
standardButtons: Dialog.Ok | Dialog.Cancel
text: qsTr("This version of Moonlight isn't optimized for your PC. Please download the '%1' version of Moonlight for the best streaming performance.").arg(SystemProperties.friendlyNativeArchName)
modality:Qt.WindowModal
icon: StandardIcon.Warning
standardButtons: StandardButton.Ok | StandardButton.Cancel
text: "This PC is running a 64-bit version of Windows. Please download the x64 version of Moonlight for the best streaming performance."
onAccepted: {
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-qt/releases");
}
}
ErrorMessageDialog {
MessageDialog {
id: unmappedGamepadDialog
property string unmappedGamepads : ""
text: qsTr("Moonlight detected gamepads without a mapping:") + "\n" + unmappedGamepads
helpTextSeparator: "\n\n"
helpText: qsTr("Click the Help button for information on how to map your gamepads.")
helpUrl: "https://github.com/moonlight-stream/moonlight-docs/wiki/Gamepad-Mapping"
modality:Qt.WindowModal
icon: StandardIcon.Warning
standardButtons: StandardButton.Ok | StandardButton.Help
text: "Moonlight detected gamepads without a proper mapping. " +
"The following gamepads will not function until this is resolved: " + unmappedGamepads + "\n\n" +
"Click the Help button for information on how to map your gamepads."
onHelp: {
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Gamepad-Mapping");
}
}
// This dialog appears when quitting via keyboard or gamepad button
NavigableMessageDialog {
MessageDialog {
id: quitConfirmationDialog
standardButtons: Dialog.Yes | Dialog.No
text: qsTr("Are you sure you want to quit?")
modality:Qt.WindowModal
icon: StandardIcon.Warning
standardButtons: StandardButton.Yes | StandardButton.No
text: "Are you sure you want to quit?"
onYes: Qt.quit()
// For keyboard/gamepad navigation
onAccepted: Qt.quit()
}
// HACK: This belongs in StreamSegue but keeping a dialog around after the parent
// dies can trigger bugs in Qt 5.12 that cause the app to crash. For now, we will
// host this dialog in a QML component that is never destroyed.
//
// To repro: Start a stream, cut the network connection to trigger the "Connection
// terminated" dialog, wait until the app grid times out back to the PC grid, then
// try to dismiss the dialog.
ErrorMessageDialog {
id: streamSegueErrorDialog
property bool quitAfter: false
onClosed: {
if (quitAfter) {
Qt.quit()
}
// StreamSegue assumes its dialog will be re-created each time we
// start streaming, so fake it by wiping out the text each time.
text = ""
}
}
NavigableDialog {
id: addPcDialog
property string label: qsTr("Enter the IP address of your GameStream PC:")
standardButtons: Dialog.Ok | Dialog.Cancel
onOpened: {
// Force keyboard focus on the textbox so keyboard navigation works
editText.forceActiveFocus()
}
onClosed: {
editText.clear()
}
onAccepted: {
if (editText.text) {
ComputerManager.addNewHost(editText.text.trim(), false)
}
}
ColumnLayout {
Label {
text: addPcDialog.label
font.bold: true
}
TextField {
id: editText
Layout.fillWidth: true
focus: true
Keys.onReturnPressed: {
addPcDialog.accept()
}
Keys.onEnterPressed: {
addPcDialog.accept()
}
}
}
}
}
+44 -77
View File
@@ -4,18 +4,17 @@
#include <QGuiApplication>
#include <QWindow>
#include "settings/streamingpreferences.h"
#include "settings/mappingmanager.h"
#define AXIS_NAVIGATION_REPEAT_DELAY 150
SdlGamepadKeyNavigation::SdlGamepadKeyNavigation()
: m_Enabled(false),
m_UiNavMode(false),
m_SettingsMode(false),
m_LastAxisNavigationEventTime(0)
{
m_PollingTimer = new QTimer(this);
connect(m_PollingTimer, &QTimer::timeout, this, &SdlGamepadKeyNavigation::onPollingTimerFired);
connect(m_PollingTimer, SIGNAL(timeout()), this, SLOT(onPollingTimerFired()));
}
SdlGamepadKeyNavigation::~SdlGamepadKeyNavigation()
@@ -89,16 +88,9 @@ void SdlGamepadKeyNavigation::disable()
void SdlGamepadKeyNavigation::onPollingTimerFired()
{
SDL_Event event;
StreamingPreferences prefs;
while (SDL_PollEvent(&event)) {
switch (event.type) {
case SDL_QUIT:
// SDL may send us a quit event since we initialize
// the video subsystem on startup. If we get one,
// forward it on for Qt to take care of.
QCoreApplication::instance()->quit();
break;
case SDL_CONTROLLERBUTTONDOWN:
case SDL_CONTROLLERBUTTONUP:
{
@@ -106,27 +98,9 @@ void SdlGamepadKeyNavigation::onPollingTimerFired()
event.type == SDL_CONTROLLERBUTTONDOWN ?
QEvent::Type::KeyPress : QEvent::Type::KeyRelease;
// Swap face buttons if needed
if (prefs.swapFaceButtons) {
switch (event.cbutton.button) {
case SDL_CONTROLLER_BUTTON_A:
event.cbutton.button = SDL_CONTROLLER_BUTTON_B;
break;
case SDL_CONTROLLER_BUTTON_B:
event.cbutton.button = SDL_CONTROLLER_BUTTON_A;
break;
case SDL_CONTROLLER_BUTTON_X:
event.cbutton.button = SDL_CONTROLLER_BUTTON_Y;
break;
case SDL_CONTROLLER_BUTTON_Y:
event.cbutton.button = SDL_CONTROLLER_BUTTON_X;
break;
}
}
switch (event.cbutton.button) {
case SDL_CONTROLLER_BUTTON_DPAD_UP:
if (m_UiNavMode) {
if (m_SettingsMode) {
// Back-tab
sendKey(type, Qt::Key_Tab, Qt::ShiftModifier);
}
@@ -135,7 +109,7 @@ void SdlGamepadKeyNavigation::onPollingTimerFired()
}
break;
case SDL_CONTROLLER_BUTTON_DPAD_DOWN:
if (m_UiNavMode) {
if (m_SettingsMode) {
sendKey(type, Qt::Key_Tab);
}
else {
@@ -144,12 +118,24 @@ void SdlGamepadKeyNavigation::onPollingTimerFired()
break;
case SDL_CONTROLLER_BUTTON_DPAD_LEFT:
sendKey(type, Qt::Key_Left);
if (m_SettingsMode) {
// Some settings controls respond to left/right (like the slider)
// and others respond to up/down (like combo boxes). They seem to
// be mutually exclusive though so let's just send both.
sendKey(type, Qt::Key_Up);
}
break;
case SDL_CONTROLLER_BUTTON_DPAD_RIGHT:
sendKey(type, Qt::Key_Right);
if (m_SettingsMode) {
// Some settings controls respond to left/right (like the slider)
// and others respond to up/down (like combo boxes). They seem to
// be mutually exclusive though so let's just send both.
sendKey(type, Qt::Key_Down);
}
break;
case SDL_CONTROLLER_BUTTON_A:
if (m_UiNavMode) {
if (m_SettingsMode) {
sendKey(type, Qt::Key_Space);
}
else {
@@ -191,7 +177,7 @@ void SdlGamepadKeyNavigation::onPollingTimerFired()
// Do nothing
}
else if (leftY < -30000) {
if (m_UiNavMode) {
if (m_SettingsMode) {
// Back-tab
sendKey(QEvent::Type::KeyPress, Qt::Key_Tab, Qt::ShiftModifier);
sendKey(QEvent::Type::KeyRelease, Qt::Key_Tab, Qt::ShiftModifier);
@@ -204,7 +190,7 @@ void SdlGamepadKeyNavigation::onPollingTimerFired()
m_LastAxisNavigationEventTime = SDL_GetTicks();
}
else if (leftY > 30000) {
if (m_UiNavMode) {
if (m_SettingsMode) {
sendKey(QEvent::Type::KeyPress, Qt::Key_Tab);
sendKey(QEvent::Type::KeyRelease, Qt::Key_Tab);
}
@@ -218,38 +204,29 @@ void SdlGamepadKeyNavigation::onPollingTimerFired()
else if (leftX < -30000) {
sendKey(QEvent::Type::KeyPress, Qt::Key_Left);
sendKey(QEvent::Type::KeyRelease, Qt::Key_Left);
if (m_SettingsMode) {
// Some settings controls respond to left/right (like the slider)
// and others respond to up/down (like combo boxes). They seem to
// be mutually exclusive though so let's just send both.
sendKey(QEvent::Type::KeyPress, Qt::Key_Up);
sendKey(QEvent::Type::KeyRelease, Qt::Key_Up);
}
m_LastAxisNavigationEventTime = SDL_GetTicks();
}
else if (leftX > 30000) {
sendKey(QEvent::Type::KeyPress, Qt::Key_Right);
sendKey(QEvent::Type::KeyRelease, Qt::Key_Right);
if (m_SettingsMode) {
// Some settings controls respond to left/right (like the slider)
// and others respond to up/down (like combo boxes). They seem to
// be mutually exclusive though so let's just send both.
sendKey(QEvent::Type::KeyPress, Qt::Key_Down);
sendKey(QEvent::Type::KeyRelease, Qt::Key_Down);
}
m_LastAxisNavigationEventTime = SDL_GetTicks();
}
// In UI navigation mode (settings page), use the right stick to scroll
if (m_UiNavMode) {
short rightX = SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_RIGHTX);
short rightY = SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_RIGHTY);
QPoint wheelDelta;
if (rightX > 30000) {
wheelDelta.setX(30);
}
else if (rightX < -30000) {
wheelDelta.setX(-30);
}
if (rightY > 30000) {
wheelDelta.setY(-30);
}
else if (rightY < -30000) {
wheelDelta.setY(30);
}
if (!wheelDelta.isNull()) {
sendWheel(wheelDelta);
}
}
}
}
@@ -263,30 +240,19 @@ void SdlGamepadKeyNavigation::sendKey(QEvent::Type type, Qt::Key key, Qt::Keyboa
}
}
void SdlGamepadKeyNavigation::sendWheel(QPoint& angleDelta)
void SdlGamepadKeyNavigation::setSettingsMode(bool settingsMode)
{
QGuiApplication* app = static_cast<QGuiApplication*>(QGuiApplication::instance());
QWindow* focusWindow = app->focusWindow();
if (focusWindow != nullptr) {
QPoint mousePos(focusWindow->width() / 2, focusWindow->height() / 2);
QPoint globalPos(focusWindow->mapToGlobal(mousePos));
#if QT_VERSION >= QT_VERSION_CHECK(5, 12, 0)
QWheelEvent wheelEvent(mousePos, globalPos, QPoint(), angleDelta, Qt::NoButton, Qt::NoModifier, Qt::NoScrollPhase, false, Qt::MouseEventSynthesizedByApplication);
#else
QWheelEvent wheelEvent(mousePos, globalPos, QPoint(), angleDelta, angleDelta.y(), Qt::Vertical, Qt::NoButton, Qt::NoModifier, Qt::NoScrollPhase, Qt::MouseEventSynthesizedByApplication, false);
#endif
app->sendEvent(focusWindow, &wheelEvent);
}
}
void SdlGamepadKeyNavigation::setUiNavMode(bool uiNavMode)
{
m_UiNavMode = uiNavMode;
m_SettingsMode = settingsMode;
}
int SdlGamepadKeyNavigation::getConnectedGamepads()
{
Q_ASSERT(m_Enabled);
if (SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) failed: %s",
SDL_GetError());
return 0;
}
int count = 0;
for (int i = 0; i < SDL_NumJoysticks(); i++) {
@@ -295,5 +261,6 @@ int SdlGamepadKeyNavigation::getConnectedGamepads()
}
}
SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER);
return count;
}
+2 -3
View File
@@ -18,13 +18,12 @@ public:
Q_INVOKABLE void disable();
Q_INVOKABLE void setUiNavMode(bool settingsMode);
Q_INVOKABLE void setSettingsMode(bool settingsMode);
Q_INVOKABLE int getConnectedGamepads();
private:
void sendKey(QEvent::Type type, Qt::Key key, Qt::KeyboardModifiers modifiers = Qt::NoModifier);
void sendWheel(QPoint& angleDelta);
private slots:
void onPollingTimerFired();
@@ -33,6 +32,6 @@ private:
QTimer* m_PollingTimer;
QList<SDL_GameController*> m_Gamepads;
bool m_Enabled;
bool m_UiNavMode;
bool m_SettingsMode;
Uint32 m_LastAxisNavigationEventTime;
};
Binary file not shown.
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.
-990
View File
@@ -1,990 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1" language="zh_CN">
<context>
<name>AppView</name>
<message>
<location filename="../gui/AppView.qml" line="139"/>
<location filename="../gui/AppView.qml" line="293"/>
<source>Resume Game</source>
<translation>继续游戏</translation>
</message>
<message>
<location filename="../gui/AppView.qml" line="165"/>
<location filename="../gui/AppView.qml" line="298"/>
<source>Quit Game</source>
<translation>退出游戏</translation>
</message>
<message>
<location filename="../gui/AppView.qml" line="293"/>
<source>Launch Game</source>
<translation>启动游戏</translation>
</message>
<message>
<location filename="../gui/AppView.qml" line="306"/>
<source>Direct Launch</source>
<translation>直接启动</translation>
</message>
<message>
<location filename="../gui/AppView.qml" line="310"/>
<source>Launch this app immediately when the host is selected, bypassing the app selection grid.</source>
<translation>选择目标计算机后立即启动此应用程序,绕过应用程序选择网格。</translation>
</message>
<message>
<location filename="../gui/AppView.qml" line="319"/>
<source>Hide Game</source>
<translation>隐藏游戏</translation>
</message>
<message>
<location filename="../gui/AppView.qml" line="323"/>
<source>Hide this game from the app grid. To access hidden games, right-click on the host and choose %1.</source>
<translation>从应用程序网格中隐藏此游戏。要访问隐藏的游戏,请右键单击目标计算机并选择 %1。</translation>
</message>
<message>
<location filename="../gui/AppView.qml" line="323"/>
<source>View All Apps</source>
<translation>查看所有应用程序</translation>
</message>
<message>
<location filename="../gui/AppView.qml" line="338"/>
<source>Are you sure you want to quit %1? Any unsaved progress will be lost.</source>
<translation>你确定要退出 %1 吗?所有未保存的游戏进度都将丢失。</translation>
</message>
</context>
<context>
<name>CliQuitStreamSegue</name>
<message>
<location filename="../gui/CliQuitStreamSegue.qml" line="9"/>
<source>Establishing connection to PC...</source>
<translation>正在建立与目标计算机的连接...</translation>
</message>
<message>
<location filename="../gui/CliQuitStreamSegue.qml" line="13"/>
<source>Quitting app...</source>
<translation>应用程序退出中...</translation>
</message>
</context>
<context>
<name>CliStartStreamSegue</name>
<message>
<location filename="../gui/CliStartStreamSegue.qml" line="9"/>
<source>Establishing connection to PC...</source>
<translation>正在建立与目标计算机的连接...</translation>
</message>
<message>
<location filename="../gui/CliStartStreamSegue.qml" line="13"/>
<source>Loading app list...</source>
<translation>应用程序列表加载中...</translation>
</message>
<message>
<location filename="../gui/CliStartStreamSegue.qml" line="81"/>
<source>Are you sure you want to quit %1? Any unsaved progress will be lost.</source>
<translation>你确定要退出 %1 吗?任何未保存的进度都将丢失。</translation>
</message>
</context>
<context>
<name>GamepadMapper</name>
<message>
<location filename="../gui/GamepadMapper.qml" line="4"/>
<source>Gamepad Mapping</source>
<translation>手柄按键映射</translation>
</message>
</context>
<context>
<name>NvHTTP</name>
<message>
<location filename="../backend/nvhttp.cpp" line="327"/>
<source>Missing audio capture device. Reinstalling GeForce Experience should resolve this error.</source>
<translation>缺少音频捕获设备。重新安装 GeForce Experience 可以解决此错误。</translation>
</message>
</context>
<context>
<name>PcView</name>
<message>
<location filename="../gui/PcView.qml" line="20"/>
<source>Computers</source>
<translation>计算机列表</translation>
</message>
<message>
<location filename="../gui/PcView.qml" line="67"/>
<source>Unable to connect to the specified PC.</source>
<translation>无法连接到目标计算机。</translation>
</message>
<message>
<location filename="../gui/PcView.qml" line="70"/>
<source>This PC&apos;s Internet connection is blocking Moonlight. Streaming over the Internet may not work while connected to this network.</source>
<translation>此计算机的互联网连接被阻塞。当连接到此网络时,互联网上的流式传输可能无法工作。</translation>
</message>
<message>
<location filename="../gui/PcView.qml" line="73"/>
<source>Click the Help button for possible solutions.</source>
<translation>单击帮助按钮以获取可能的解决方案。</translation>
</message>
<message>
<location filename="../gui/PcView.qml" line="102"/>
<source>Searching for PCs on your local network with NVIDIA GameStream enabled...</source>
<translation>正在本地网络上搜索启用 NVIDIA GameStrea 的计算机...</translation>
</message>
<message>
<location filename="../gui/PcView.qml" line="103"/>
<source>Automatic PC discovery is disabled. Add your PC manually.</source>
<translation>已禁用计算机自动发现。需要手动添加计算机。</translation>
</message>
<message>
<location filename="../gui/PcView.qml" line="171"/>
<source>PC Status: %1</source>
<translation>计算机状态: %1</translation>
</message>
<message>
<location filename="../gui/PcView.qml" line="171"/>
<source>Online</source>
<translation>在线</translation>
</message>
<message>
<location filename="../gui/PcView.qml" line="171"/>
<source>Offline</source>
<translation>离线</translation>
</message>
<message>
<location filename="../gui/PcView.qml" line="177"/>
<source>View All Apps</source>
<translation>查看所有应用程序</translation>
</message>
<message>
<location filename="../gui/PcView.qml" line="187"/>
<source>Wake PC</source>
<translation>唤醒目标计算机</translation>
</message>
<message>
<location filename="../gui/PcView.qml" line="193"/>
<source>Test Network</source>
<translation>网络测试</translation>
</message>
<message>
<location filename="../gui/PcView.qml" line="202"/>
<source>Rename PC</source>
<translation>重命名计算机</translation>
</message>
<message>
<location filename="../gui/PcView.qml" line="211"/>
<source>Delete PC</source>
<translation>删除计算机</translation>
</message>
<message>
<location filename="../gui/PcView.qml" line="242"/>
<source>You cannot pair while a previous session is still running on the host PC. Quit any running games or reboot the host PC, then try pairing again.</source>
<translation>当上一个会话仍在运行时,无法进行配对。请退出所有正在运行的游戏或重新启动目标计算机,然后重试配对。</translation>
</message>
<message>
<location filename="../gui/PcView.qml" line="297"/>
<source>Please enter %1 on your GameStream PC. This dialog will close when pairing is completed.</source>
<translation>请在启用 GameStream 的计算机上输入 %1。配对完成后,此对话框将关闭。</translation>
</message>
<message>
<location filename="../gui/PcView.qml" line="308"/>
<source>Are you sure you want to remove this PC?</source>
<translation>确定要移除这个计算机吗?</translation>
</message>
<message>
<location filename="../gui/PcView.qml" line="325"/>
<source>Moonlight is testing your network connection to determine if NVIDIA GameStream is blocked.</source>
<translation>Moonlight 正在测试你的网络连接,以检测 NVIDIA GameStream 是否被阻止。</translation>
</message>
<message>
<location filename="../gui/PcView.qml" line="325"/>
<source>This may take a few seconds…</source>
<translation>这可能需要几秒钟...</translation>
</message>
<message>
<location filename="../gui/PcView.qml" line="332"/>
<source>The network test could not be performed because none of Moonlight&apos;s connection testing servers were reachable from this PC. Check your Internet connection or try again later.</source>
<translation>无法执行网络测试,因为此计算机无法访问 Moonlight 的连接测试服务器。请检查你的互联网连接或稍后再试。</translation>
</message>
<message>
<location filename="../gui/PcView.qml" line="336"/>
<source>This network does not appear to be blocking Moonlight. If you still have trouble connecting, check your PC&apos;s firewall settings.</source>
<translation>此网络似乎没有阻止 Moonlight。如果仍然无法连接,请检查计算机的防火墙设置。</translation>
</message>
<message>
<location filename="../gui/PcView.qml" line="336"/>
<source>If you are trying to stream over the Internet, install the Moonlight Internet Hosting Tool on your gaming PC and run the included Internet Streaming Tester to check your gaming PC&apos;s Internet connection.</source>
<translation>如果你想通过互联网进行流式传输,请在你的目标计算机上安装 Moonlight Internet Hosting 工具,并运行包含的互联网流式传输测试程序来检查你目标计算机的互联网连接。</translation>
</message>
<message>
<location filename="../gui/PcView.qml" line="340"/>
<source>Your PC&apos;s current network connection seems to be blocking Moonlight. Streaming over the Internet may not work while connected to this network.</source>
<translation>此计算机当前的网络连接似乎被阻塞了。当连接到此网络时,互联网上的流式传输可能无法工作。</translation>
</message>
<message>
<location filename="../gui/PcView.qml" line="340"/>
<source>The following network ports were blocked:</source>
<translation>下列网络端口被阻止:</translation>
</message>
<message>
<location filename="../gui/PcView.qml" line="352"/>
<source>Enter the new name for this PC:</source>
<translation>输入这台计算机的新名称:</translation>
</message>
</context>
<context>
<name>PendingQuitTask</name>
<message>
<location filename="../backend/computermanager.cpp" line="538"/>
<source>The running game wasn&apos;t started by this PC. You must quit the game on the host PC manually or use the device that originally started the game.</source>
<translation>正在运行的游戏不是由这台计算机启动的。你必须在目标计算机上手动退出游戏或使用最初启动游戏的设备。</translation>
</message>
</context>
<context>
<name>QObject</name>
<message>
<location filename="../cli/quitstream.cpp" line="76"/>
<location filename="../cli/startstream.cpp" line="133"/>
<source>Failed to connect to %1</source>
<translation>无法连接到 %1</translation>
</message>
<message>
<location filename="../cli/quitstream.cpp" line="88"/>
<location filename="../cli/startstream.cpp" line="89"/>
<source>Computer %1 has not been paired. Please open Moonlight to pair before streaming.</source>
<translation>计算机 %1 尚未配对。</translation>
</message>
<message>
<location filename="../cli/quitstream.cpp" line="102"/>
<location filename="../cli/startstream.cpp" line="126"/>
<source>Quitting app failed, reason: %1</source>
<translation>退出应用程序失败,原因: %1</translation>
</message>
<message>
<location filename="../cli/startstream.cpp" line="137"/>
<source>Failed to find application %1</source>
<translation>找不到应用程序 %1</translation>
</message>
</context>
<context>
<name>QuitSegue</name>
<message>
<location filename="../gui/QuitSegue.qml" line="12"/>
<source>Quitting %1...</source>
<translation>正在退出 %1...</translation>
</message>
</context>
<context>
<name>Session</name>
<message>
<location filename="../streaming/session.cpp" line="89"/>
<source>No video received from host.</source>
<translation>没有从目标计算机接收到视频画面。</translation>
</message>
<message>
<location filename="../streaming/session.cpp" line="90"/>
<source>Check your firewall and port forwarding rules for port(s): %1</source>
<translation>检查防火墙和端口转发规则: %1</translation>
</message>
<message>
<location filename="../streaming/session.cpp" line="95"/>
<source>Your network connection isn&apos;t performing well. Reduce your video bitrate setting or try a faster connection.</source>
<translation>网络连接性能不佳。请降低视频比特率或尝试更快的网络连接。</translation>
</message>
<message>
<location filename="../streaming/session.cpp" line="100"/>
<source>Something went wrong on your host PC when starting the stream.</source>
<translation>目标计算机启动流式传输时出现问题。</translation>
</message>
<message>
<location filename="../streaming/session.cpp" line="101"/>
<source>Make sure you don&apos;t have any DRM-protected content open on your host PC. You can also try restarting your host PC.</source>
<translation>确保目标计算机上没有打开任何受 DRM 保护的内容。你也可以尝试重新启动目标计算机。</translation>
</message>
<message>
<location filename="../streaming/session.cpp" line="102"/>
<source>If the issue persists, try reinstalling your GPU drivers and GeForce Experience.</source>
<translation>如果问题仍然存在,请尝试重新安装显卡驱动程序和 GeForce Experience。</translation>
</message>
<message>
<location filename="../streaming/session.cpp" line="107"/>
<source>Connection terminated</source>
<translation>会话已终止</translation>
</message>
<message>
<location filename="../streaming/session.cpp" line="578"/>
<source>Your selection to enable remote desktop mouse mode may cause problems in games.</source>
<translation>启用远程桌面鼠标模式可能会导致游戏出现问题。</translation>
</message>
<message>
<location filename="../streaming/session.cpp" line="583"/>
<source>HDR is not supported with software decoding.</source>
<translation>软件解码不支持 HDR。</translation>
</message>
<message>
<location filename="../streaming/session.cpp" line="587"/>
<source>Your settings selection to force software decoding may cause poor streaming performance.</source>
<translation>你选择的用于强制软件解码的设置可能会导致流式传输性能不佳。</translation>
</message>
<message>
<location filename="../streaming/session.cpp" line="592"/>
<source>Using unsupported FPS options may cause stuttering or lag.</source>
<translation>使用不受支持的 FPS 选项可能会导致卡顿或延迟。</translation>
</message>
<message>
<location filename="../streaming/session.cpp" line="595"/>
<source>V-sync will be disabled when streaming at a higher frame rate than the display.</source>
<translation>当流式传输的帧率高于显示时,垂直同步将被禁用。</translation>
</message>
<message>
<location filename="../streaming/session.cpp" line="612"/>
<source>Using software decoding due to your selection to force HEVC without GPU support. This may cause poor streaming performance.</source>
<translation>根据你的选择使用软件解码,以在没有显卡支持的情况下强制 HEVC。这可能会导致流式传输性能不佳。</translation>
</message>
<message>
<location filename="../streaming/session.cpp" line="615"/>
<source>This PC&apos;s GPU doesn&apos;t support HEVC decoding.</source>
<translation>此计算机的显卡不支持 HEVC 解码。</translation>
</message>
<message>
<location filename="../streaming/session.cpp" line="622"/>
<source>Your host PC GPU doesn&apos;t support HEVC. A GeForce GTX 900-series (Maxwell) or later GPU is required for HEVC streaming.</source>
<translation>目标计算机的显卡不支持 HEVC。</translation>
</message>
<message>
<location filename="../streaming/session.cpp" line="639"/>
<source>%1 doesn&apos;t support HDR10.</source>
<translation>%1 不支持 HDR10。</translation>
</message>
<message>
<location filename="../streaming/session.cpp" line="643"/>
<source>Your host PC GPU doesn&apos;t support HDR streaming. A GeForce GTX 1000-series (Pascal) or later GPU is required for HDR streaming.</source>
<translation>目标计算机的显卡不支持 HDR 流。</translation>
</message>
<message>
<location filename="../streaming/session.cpp" line="652"/>
<source>This PC&apos;s GPU doesn&apos;t support HEVC Main10 decoding for HDR streaming.</source>
<translation>此计算机的显卡不支持 HEVC Main10 解码 HDR 流。</translation>
</message>
<message>
<location filename="../streaming/session.cpp" line="665"/>
<source>GeForce Experience 3.0 or higher is required for 4K streaming.</source>
<translation>4K 流式传输需要 GeForce Experience 3.0 或更高版本。</translation>
</message>
<message>
<location filename="../streaming/session.cpp" line="680"/>
<source>Your selected surround sound setting is not supported by the current audio device.</source>
<translation>当前音频设备不支持你选择的环绕声设置。</translation>
</message>
<message>
<location filename="../streaming/session.cpp" line="687"/>
<source>Failed to open audio device. Audio will be unavailable during this session.</source>
<translation>无法打开音频设备。音频在此会话期间将不可用。</translation>
</message>
<message>
<location filename="../streaming/session.cpp" line="692"/>
<source>An attached gamepad has no mapping and won&apos;t be usable. Visit the Moonlight help to resolve this.</source>
<translation>附加的手柄没有映射,因此无法使用。请访问 Moonlight 帮助解决此问题。</translation>
</message>
<message>
<location filename="../streaming/session.cpp" line="700"/>
<source>Your host PC&apos;s GPU doesn&apos;t support streaming video resolutions over 4K.</source>
<translation>目标计算机的显卡不支持 4K 以上的流式传输视频分辨率。</translation>
</message>
<message>
<location filename="../streaming/session.cpp" line="704"/>
<source>Video resolutions over 4K are only supported by the HEVC codec.</source>
<translation>只有 HEVC 编解码器才支持 4K 以上的视频分辨率。</translation>
</message>
<message>
<location filename="../streaming/session.cpp" line="718"/>
<source>Your selection to force hardware decoding cannot be satisfied due to missing hardware decoding support on this PC&apos;s GPU.</source>
<translation>由于此计算机的显卡上缺少硬件解码支持,无法满足强制硬件解码的选择。</translation>
</message>
<message>
<location filename="../streaming/session.cpp" line="721"/>
<source>Your codec selection and force hardware decoding setting are not compatible. This PC&apos;s GPU lacks support for decoding your chosen codec.</source>
<translation>你选择的编解码器和强制硬件解码设置不兼容。此计算机的显卡不支持解码你选择的编解码器。</translation>
</message>
<message>
<location filename="../streaming/session.cpp" line="1022"/>
<source>GeForce Experience returned error: %1</source>
<translation>GeForce Experience 返回错误: %1</translation>
</message>
<message>
<location filename="../streaming/session.cpp" line="1445"/>
<source>Unable to initialize video decoder. Please check your streaming settings and try again.</source>
<translation>无法初始化视频解码器。请检查你的流式传输设置,然后重试。</translation>
</message>
</context>
<context>
<name>SettingsView</name>
<message>
<location filename="../gui/SettingsView.qml" line="12"/>
<source>Settings</source>
<translation>设置</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="60"/>
<source>Basic Settings</source>
<translation>基本设置</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="70"/>
<source>Resolution and FPS</source>
<translation>分辨率和 FPS</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="78"/>
<source>Setting values too high for your PC or network connection may cause lag, stuttering, or errors.</source>
<translation>如果你的计算机或网络连接设置的值过高,可能会导致延迟,卡顿或错误。</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="209"/>
<source>720p</source>
<translation></translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="215"/>
<source>1080p</source>
<translation></translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="221"/>
<source>1440p</source>
<translation></translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="227"/>
<source>4K</source>
<translation></translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="331"/>
<source>Custom resolutions are not officially supported by GeForce Experience, so it will not set your host display resolution. You will need to set it manually while in game.</source>
<translation>自定义分辨率不受 GeForce Experience 的正式支持,因此不会设置目标计算机的分辨率。你需要在游戏中手动设置。</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="332"/>
<source>Resolutions that are not supported by your client or host PC may cause streaming errors.</source>
<translation>使用当前计算机或目标计算机不支持的分辨率可能会导致流式传输错误。</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="338"/>
<source>Enter a custom resolution:</source>
<translation>输入自定义分辨率:</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="406"/>
<location filename="../gui/SettingsView.qml" line="407"/>
<location filename="../gui/SettingsView.qml" line="427"/>
<location filename="../gui/SettingsView.qml" line="430"/>
<source>%1 FPS</source>
<translation></translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="412"/>
<location filename="../gui/SettingsView.qml" line="415"/>
<location filename="../gui/SettingsView.qml" line="424"/>
<location filename="../gui/SettingsView.qml" line="437"/>
<location filename="../gui/SettingsView.qml" line="440"/>
<source>%1 FPS (Unsupported)</source>
<translation>%1 FPS (不支持)</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="493"/>
<source>Video bitrate:</source>
<translation>视频比特率:</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="501"/>
<source>Lower the bitrate on slower connections. Raise the bitrate to increase image quality.</source>
<translation>在较慢的连接上降低比特率。提高比特率以提高图像质量。</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="519"/>
<source>Video bitrate: %1 Mbps</source>
<translation>视频比特率: %1 Mbps</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="527"/>
<source>Display mode</source>
<translation>显示模式</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="545"/>
<source> (Recommended)</source>
<translation> (推荐)</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="573"/>
<location filename="../gui/SettingsView.qml" line="847"/>
<source>Fullscreen</source>
<translation>全屏</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="577"/>
<source>Borderless windowed</source>
<translation>无边框窗口</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="581"/>
<location filename="../gui/SettingsView.qml" line="839"/>
<source>Windowed</source>
<translation>窗口化</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="592"/>
<source>Fullscreen generally provides the best performance, but borderless windowed may work better with features like macOS Spaces, Alt+Tab, screenshot tools, on-screen overlays, etc.</source>
<translation>全屏通常提供最好的性能,但是无边框窗口可以更好地与 macOS Spaces、Alt+Tab、屏幕截图工具、屏幕覆盖等功能配合使用。</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="599"/>
<source>V-Sync</source>
<translation>垂直同步</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="609"/>
<source>Disabling V-Sync allows sub-frame rendering latency, but it can display visible tearing</source>
<translation>禁用垂直同步可允许子帧渲染延迟,但会显示可见的撕裂</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="616"/>
<source>Frame pacing</source>
<translation>帧速调节</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="626"/>
<source>Frame pacing reduces micro-stutter by delaying frames that come in too early</source>
<translation>通过延迟过早进入的帧来减少卡顿</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="636"/>
<source>Audio Settings</source>
<translation>音频设置</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="646"/>
<source>Audio configuration</source>
<translation>音频配置</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="671"/>
<source>Stereo</source>
<translation>立体声</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="675"/>
<source>5.1 surround sound</source>
<translation>5.1 环绕声</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="679"/>
<source>7.1 surround sound</source>
<translation>7.1 环绕声</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="693"/>
<source>Mute host PC speakers while streaming</source>
<translation>流式传输启动时将目标计算机的扬声器静音</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="703"/>
<source>You must restart any game currently in progress for this setting to take effect</source>
<translation>要使此设置生效,必须重新启动当前正在进行的所有游戏</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="709"/>
<source>Mute audio stream when Moonlight is not the active window</source>
<translation>当 Moonlight 失去焦点时静音</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="720"/>
<source>Mutes Moonlight&apos;s audio when you Alt+Tab out of the stream or click on a different window.</source>
<translation>当你用 Alt+Tab 键离开流式传输或者点击其他窗口时,Moonlight 将静音。</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="729"/>
<source>UI Settings</source>
<translation>界面设置</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="739"/>
<source>Language</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="765"/>
<source>Automatic</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="792"/>
<source>You must restart Moonlight for this change to take effect</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="806"/>
<source>GUI display mode</source>
<translation>窗口显示模式</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="843"/>
<source>Maximized</source>
<translation>最大化</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="860"/>
<source>Show connection quality warnings</source>
<translation>显示连接质量警告</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="872"/>
<source>Discord Rich Presence integration</source>
<translation>Discord Rich Presence 集成</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="882"/>
<source>Updates your Discord status to display the name of the game you&apos;re streaming.</source>
<translation>更新你的 Discord 状态,以显示正在运行的游戏名称。</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="900"/>
<source>Input Settings</source>
<translation>输入设置</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="911"/>
<source>Optimize mouse for remote desktop instead of games</source>
<translation>为远程桌面而不是游戏优化鼠标</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="922"/>
<source>This enables seamless mouse control without capturing the client&apos;s mouse cursor. It is ideal for remote desktop usage but will not work in most games.</source>
<translation>这可以实现无缝鼠标控制,而无需捕获客户端的鼠标光标。这是理想的远程桌面使用方案,但不会在大多数游戏中工作。</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="923"/>
<source>You can toggle this while streaming using Ctrl+Alt+Shift+M.</source>
<translation>你可以使用 Ctrl+Alt+Shift+M 切换鼠标控制模式。</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="924"/>
<source>NOTE: Due to a bug in GeForce Experience, this option may not work properly if your host PC has multiple monitors.</source>
<translation>注意:由于 GeForce Experience 的 Bug,如果你的目标计算机有多个显示器,则此选项可能无法正常工作。</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="934"/>
<source>Capture system keyboard shortcuts</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="942"/>
<source>This enables the capture of system-wide keyboard shortcuts like Alt+Tab that would normally be handled by the client OS while streaming.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Capture system keyboard shortcuts while streaming in fullscreen</source>
<translation type="vanished">在全屏流式传输时捕获系统键盘快捷键</translation>
</message>
<message>
<source>This enables the capture of system-wide keyboard shortcuts like Alt+Tab that would normally be handled by the client OS while streaming in fullscreen.</source>
<translation type="vanished">这允许捕获系统范围内的键盘快捷键(如Alt+Tab),这些快捷键通常在全屏流式传输时由客户端操作系统处理。</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="943"/>
<source>NOTE: Certain keyboard shortcuts like Ctrl+Alt+Del on Windows cannot be intercepted by any application, including Moonlight.</source>
<translation>注意:Windows 上的某些键盘快捷键(如 Ctrl+Alt+Del)不能被任何应用程序截获,包括 Moonlight。</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="972"/>
<source>in fullscreen</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="976"/>
<source>always</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="1006"/>
<source>Use touchscreen as a virtual trackpad</source>
<translation>将触摸屏用作虚拟触控板</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="1016"/>
<source>When checked, the touchscreen acts like a trackpad. When unchecked, the touchscreen will directly control the mouse pointer.</source>
<translation>选中时,触摸屏就像一个触控板。取消选中时,触摸屏将直接控制鼠标指针。</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="1023"/>
<source>Swap left and right mouse buttons</source>
<translation>交换鼠标左键和右键</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="1035"/>
<source>Reverse mouse scrolling direction</source>
<translation>反转鼠标滚动方向</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="1049"/>
<source>Gamepad Settings</source>
<translation>手柄设置</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="1059"/>
<source>Swap A/B and X/Y gamepad buttons</source>
<translation>交换手柄的 A/B 和 X/Y 按钮</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="1077"/>
<source>This switches gamepads into a Nintendo-style button layout</source>
<translation>这会将手柄切换为任天堂风格的按钮布局</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="1083"/>
<source>Force gamepad #1 always connected</source>
<translation>强制手柄 #1 始终连接</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="1093"/>
<source>Forces a single gamepad to always stay connected to the host, even if no gamepads are actually connected to this PC.</source>
<translation>强制单个手柄始终与目标计算机保持连接,即使没有任何手柄实际连接到此计算机。</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="1094"/>
<source>Only enable this option when streaming a game that doesn&apos;t support gamepads being connected after startup.</source>
<translation>仅在游戏启动后不支持手柄连接时才启用此选项。</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="1101"/>
<source>Enable mouse control with gamepads by holding the &apos;Start&apos; button</source>
<translation>按住手柄的“Start”按钮启用鼠标控制</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="1112"/>
<source>Process gamepad input when Moonlight is in the background</source>
<translation>当 Moonlight 在后台时处理手柄输入</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="1123"/>
<source>Allows Moonlight to capture gamepad inputs even if it&apos;s not the current window in focus</source>
<translation>即使不是焦点窗口,也允许 Moonlight 捕获手柄的输入</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="1132"/>
<source>Host Settings</source>
<translation>目标计算机设置</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="1142"/>
<source>Optimize game settings for streaming</source>
<translation>优化游戏设置以进行流式传输</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="1153"/>
<source>Quit app on host PC after ending stream</source>
<translation>流式传输结束后退出目标计算机上的应用程序</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="1163"/>
<source>This will close the app or game you are streaming when you end your stream. You will lose any unsaved progress!</source>
<translation>当你结束流式传输时,这将关闭正在运行的应用程序或游戏。这会导致你失去所有未保存的游戏进度!</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="1172"/>
<source>Advanced Settings</source>
<translation>高级设置</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="1182"/>
<source>Video decoder</source>
<translation>视频解码器</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="1207"/>
<location filename="../gui/SettingsView.qml" line="1253"/>
<source>Automatic (Recommended)</source>
<translation>自动 (推荐)</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="1211"/>
<source>Force software decoding</source>
<translation>强制软件解码</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="1215"/>
<source>Force hardware decoding</source>
<translation>强制硬件解码</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="1228"/>
<source>Video codec</source>
<translation>视频编解码器</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="1257"/>
<source>H.264</source>
<translation></translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="1261"/>
<source>HEVC (H.265)</source>
<translation></translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="1278"/>
<source>Unlock unsupported FPS options</source>
<translation>解锁不受支持的 FPS 选项</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="1297"/>
<source>Automatically find PCs on the local network (Recommended)</source>
<translation>自动在本地网络上查找计算机 (推荐)</translation>
</message>
<message>
<location filename="../gui/SettingsView.qml" line="1322"/>
<source>Automatically detect blocked connections (Recommended)</source>
<translation>自动检测被阻止的连接 (推荐)</translation>
</message>
</context>
<context>
<name>StreamSegue</name>
<message>
<location filename="../gui/StreamSegue.qml" line="11"/>
<source>Resuming %1...</source>
<translation>正在恢复 %1...</translation>
</message>
<message>
<location filename="../gui/StreamSegue.qml" line="12"/>
<location filename="../gui/StreamSegue.qml" line="19"/>
<source>Starting %1...</source>
<translation>正在启动 %1...</translation>
</message>
<message>
<location filename="../gui/StreamSegue.qml" line="25"/>
<source>Starting %1 failed: Error %2</source>
<translation>启动 %1 错误: 错误 %2</translation>
</message>
<message>
<location filename="../gui/StreamSegue.qml" line="28"/>
<source>Check your firewall and port forwarding rules for port(s): %1</source>
<translation>检查防火墙和端口转发规则: %1</translation>
</message>
<message>
<location filename="../gui/StreamSegue.qml" line="74"/>
<source>This PC&apos;s Internet connection is blocking Moonlight. Streaming over the Internet may not work while connected to this network.</source>
<translation>此计算机的互联网连接被阻塞。当连接到此网络时,互联网上的流式传输可能无法工作。</translation>
</message>
<message>
<location filename="../gui/StreamSegue.qml" line="161"/>
<source>Tip:</source>
<translation>提示:</translation>
</message>
<message>
<location filename="../gui/StreamSegue.qml" line="161"/>
<source>Press %1 to disconnect your session</source>
<translation>按 %1 来断开会话</translation>
</message>
<message>
<location filename="../gui/StreamSegue.qml" line="162"/>
<source>Start+Select+L1+R1</source>
<translation></translation>
</message>
<message>
<location filename="../gui/StreamSegue.qml" line="162"/>
<source>Ctrl+Alt+Shift+Q</source>
<translation></translation>
</message>
</context>
<context>
<name>main</name>
<message>
<location filename="../gui/main.qml" line="254"/>
<location filename="../gui/main.qml" line="264"/>
<location filename="../gui/main.qml" line="393"/>
<location filename="../gui/main.qml" line="408"/>
<source>Settings</source>
<translation>设置</translation>
</message>
<message>
<location filename="../gui/main.qml" line="255"/>
<source>Version %1</source>
<translation>版本 %1</translation>
</message>
<message>
<location filename="../gui/main.qml" line="271"/>
<source>Join our community on Discord</source>
<translation>加入我们的社区</translation>
</message>
<message>
<location filename="../gui/main.qml" line="283"/>
<source>Computers</source>
<translation>计算机列表</translation>
</message>
<message>
<location filename="../gui/main.qml" line="290"/>
<source>Add PC manually</source>
<translation>手动添加计算机</translation>
</message>
<message>
<location filename="../gui/main.qml" line="330"/>
<source>Update available for Moonlight: Version %1</source>
<translation>Moonlight 可用更新:版本 %1</translation>
</message>
<message>
<location filename="../gui/main.qml" line="354"/>
<source>Help</source>
<translation>帮助</translation>
</message>
<message>
<location filename="../gui/main.qml" line="377"/>
<source>Gamepad Mapper</source>
<translation>手柄按键映射</translation>
</message>
<message>
<location filename="../gui/main.qml" line="381"/>
<source>Gamepad Mapping</source>
<translation>手柄按键映射</translation>
</message>
<message>
<location filename="../gui/main.qml" line="415"/>
<source>No functioning hardware accelerated H.264 video decoder was detected by Moonlight. Your streaming performance may be severely degraded in this configuration.</source>
<translation>Moonlight 未检测到功能正常的硬件加速 H.264 视频解码器。</translation>
</message>
<message>
<location filename="../gui/main.qml" line="417"/>
<source>Click the Help button for more information on solving this problem.</source>
<translation>单击帮助按钮以获取有关解决此问题的更多信息。</translation>
</message>
<message>
<location filename="../gui/main.qml" line="423"/>
<source>Hardware acceleration doesn&apos;t work on XWayland. Continuing on XWayland may result in poor streaming performance. Try running with QT_QPA_PLATFORM=wayland or switch to X11.</source>
<translation>硬件加速在 XWayland 上不起作用。在 XWayland 上继续操作可能会导致流式传输性能下降。</translation>
</message>
<message>
<location filename="../gui/main.qml" line="425"/>
<source>Click the Help button for more information.</source>
<translation>单击帮助按钮以获取更多信息。</translation>
</message>
<message>
<location filename="../gui/main.qml" line="432"/>
<source>This version of Moonlight isn&apos;t optimized for your PC. Please download the &apos;%1&apos; version of Moonlight for the best streaming performance.</source>
<translation>此版本的 Moonlight 尚未针对你的计算机优化。请下载“%1”版本的 Moonlight,以获得最佳的流式传输性能。</translation>
</message>
<message>
<location filename="../gui/main.qml" line="441"/>
<source>Moonlight detected gamepads without a mapping:</source>
<translation>Moonlight 检测到没有映射的手柄:</translation>
</message>
<message>
<location filename="../gui/main.qml" line="443"/>
<source>Click the Help button for information on how to map your gamepads.</source>
<translation>单击帮助按钮以获取有关如何映射手柄的信息。</translation>
</message>
<message>
<location filename="../gui/main.qml" line="451"/>
<source>Are you sure you want to quit?</source>
<translation>确定要退出吗?</translation>
</message>
<message>
<location filename="../gui/main.qml" line="481"/>
<source>Enter the IP address of your GameStream PC:</source>
<translation>输入目标计算机的 IP 地址:</translation>
</message>
</context>
</TS>
+40 -254
View File
@@ -6,11 +6,6 @@
#include <QMutex>
#include <QtDebug>
#include <QNetworkProxyFactory>
#include <QPalette>
#include <QFont>
#include <QCursor>
#include <QElapsedTimer>
#include <QFile>
// Don't let SDL hook our main function, since Qt is already
// doing the same thing. This needs to be before any headers
@@ -22,21 +17,17 @@
#include "streaming/video/ffmpeg.h"
#endif
#if defined(Q_OS_WIN32) && defined(Q_PROCESSOR_X86)
#ifdef Q_OS_WIN32
#include "antihookingprotection.h"
#elif defined(Q_OS_LINUX)
#include <openssl/ssl.h>
#endif
#include "cli/quitstream.h"
#include "cli/startstream.h"
#include "cli/commandlineparser.h"
#include "path.h"
#include "utils.h"
#include "gui/computermodel.h"
#include "gui/appmodel.h"
#include "backend/autoupdatechecker.h"
#include "backend/systemproperties.h"
#include "streaming/session.h"
#include "settings/streamingpreferences.h"
#include "gui/sdlgamepadkeynavigation.h"
@@ -57,7 +48,7 @@
#endif
#ifdef USE_CUSTOM_LOGGER
static QElapsedTimer s_LoggerTime;
static QTime s_LoggerTime;
static QTextStream s_LoggerStream(stdout);
static QMutex s_LoggerLock;
#ifdef LOG_TO_FILE
@@ -76,12 +67,7 @@ void logToLoggerStream(QString& message)
return;
}
else if (s_LogLinesWritten == MAX_LOG_LINES) {
s_LoggerStream << "Log size limit reached!";
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
s_LoggerStream << Qt::endl;
#else
s_LoggerStream << endl;
#endif
s_LoggerStream << "Log size limit reached!" << endl;
s_LogLimitReached = true;
return;
}
@@ -150,6 +136,14 @@ void qtLogToDiskHandler(QtMsgType type, const QMessageLogContext&, const QString
break;
}
// HACK: Avoid printing this internal Qt warning which seems to print every time we
// start our event loop for the QNetworkReply. I have found this warning in many
// unrelated bug reports, but none actually tackle this warning itself. It seems to be
// new in Qt 5.12.
if (msg.startsWith("QNetworkReplyHttpImplPrivate::_q_startOperation was called more than once")) {
return;
}
QTime logTime = QTime::fromMSecsSinceStartOfDay(s_LoggerTime.elapsed());
QString txt = QString("%1 - Qt %2: %3\n").arg(logTime.toString()).arg(typeTxt).arg(msg);
@@ -196,7 +190,7 @@ LONG WINAPI UnhandledExceptionHandler(struct _EXCEPTION_POINTERS *ExceptionInfo)
WCHAR dmpFileName[MAX_PATH];
swprintf_s(dmpFileName, L"%ls\\Moonlight-%I64u.dmp",
(PWCHAR)QDir::toNativeSeparators(Path::getLogDir()).utf16(), QDateTime::currentSecsSinceEpoch());
QString qDmpFileName = QString::fromUtf16((const char16_t*)dmpFileName);
QString qDmpFileName = QString::fromUtf16((unsigned short*)dmpFileName);
HANDLE dumpHandle = CreateFileW(dmpFileName, GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
if (dumpHandle != INVALID_HANDLE_VALUE) {
MINIDUMP_EXCEPTION_INFORMATION info;
@@ -237,8 +231,6 @@ LONG WINAPI UnhandledExceptionHandler(struct _EXCEPTION_POINTERS *ExceptionInfo)
int main(int argc, char *argv[])
{
SDL_SetMainReady();
// Set the app version for the QCommandLineParser's showVersion() command
QCoreApplication::setApplicationVersion(VERSION_STR);
@@ -288,60 +280,19 @@ int main(int argc, char *argv[])
SetUnhandledExceptionFilter(UnhandledExceptionHandler);
#endif
#if defined(Q_OS_WIN32) && defined(Q_PROCESSOR_X86)
#ifdef Q_OS_WIN32
// Force AntiHooking.dll to be statically imported and loaded
// by ntdll on x86/x64 platforms by calling a dummy function.
// AntiHooking.dll is not currently built for ARM64.
// by ntdll by calling a dummy function.
AntiHookingDummyImport();
#elif defined(Q_OS_LINUX)
// Force libssl.so to be directly linked to our binary, so
// linuxdeployqt can find it and include it in our AppImage.
// QtNetwork will pull it in via dlopen().
SSL_free(nullptr);
#endif
// Avoid using High DPI on EGLFS. It breaks font rendering.
// https://bugreports.qt.io/browse/QTBUG-64377
//
// NB: We can't use QGuiApplication::platformName() here because it is only
// set once the QGuiApplication is created, which is too late to enable High DPI :(
if (WMUtils::isRunningWindowManager()) {
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
// Enable High DPI support on Qt 5.x. It is always enabled on Qt 6.0
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
#endif
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
// Enable fractional High DPI scaling on Qt 5.14 and later
QGuiApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::PassThrough);
#endif
}
else {
#ifndef STEAM_LINK
if (!qEnvironmentVariableIsSet("QT_QPA_PLATFORM")) {
qInfo() << "Unable to detect Wayland or X11, so EGLFS will be used by default. Set QT_QPA_PLATFORM to override this.";
qputenv("QT_QPA_PLATFORM", "eglfs");
if (!qEnvironmentVariableIsSet("QT_QPA_EGLFS_ALWAYS_SET_MODE")) {
qInfo() << "Setting display mode by default. Set QT_QPA_EGLFS_ALWAYS_SET_MODE=0 to override this.";
// The UI doesn't appear on RetroPie without this option.
qputenv("QT_QPA_EGLFS_ALWAYS_SET_MODE", "1");
}
if (!QFile("/dev/dri").exists()) {
qWarning() << "Unable to find a KMSDRM display device!";
qWarning() << "On the Raspberry Pi, you must enable the 'fake KMS' driver in raspi-config to use Moonlight outside of the GUI environment.";
}
}
#endif
}
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
// This avoids using the default keychain for SSL, which may cause
// password prompts on macOS.
qputenv("QT_SSL_USE_TEMPORARY_KEYCHAIN", "1");
qputenv("QT_SSL_USE_TEMPORARY_KEYCHAIN", QByteArray("1"));
#if defined(Q_OS_WIN32) && QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
#ifdef Q_OS_WIN32
if (!qEnvironmentVariableIsSet("QT_OPENGL")) {
// On Windows, use ANGLE so we don't have to load OpenGL
// user-mode drivers into our app. OGL drivers (especially Intel)
@@ -350,18 +301,6 @@ int main(int argc, char *argv[])
}
#endif
#if !defined(Q_OS_WIN32) || QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
// Moonlight requires the non-threaded renderer because we depend
// on being able to control the render thread by blocking in the
// main thread (and pumping events from the main thread when needed).
// That doesn't work with the threaded renderer which causes all
// sorts of odd behavior depending on the platform.
//
// NB: Windows defaults to the "windows" non-threaded render loop on
// Qt 5 and the threaded render loop on Qt 6.
qputenv("QSG_RENDER_LOOP", "basic");
#endif
// We don't want system proxies to apply to us
QNetworkProxyFactory::setUseSystemConfiguration(false);
@@ -372,145 +311,15 @@ int main(int argc, char *argv[])
// Register custom metatypes for use in signals
qRegisterMetaType<NvApp>("NvApp");
SDL_version compileVersion;
SDL_VERSION(&compileVersion);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Compiled with SDL %d.%d.%d",
compileVersion.major, compileVersion.minor, compileVersion.patch);
SDL_version runtimeVersion;
SDL_GetVersion(&runtimeVersion);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Running with SDL %d.%d.%d",
runtimeVersion.major, runtimeVersion.minor, runtimeVersion.patch);
// Allow the display to sleep by default. We will manually use SDL_DisableScreenSaver()
// and SDL_EnableScreenSaver() when appropriate. This hint must be set before
// initializing the SDL video subsystem to have any effect.
SDL_SetHint(SDL_HINT_VIDEO_ALLOW_SCREENSAVER, "1");
// For SDL backends that support it, use double buffering instead of triple buffering
// to save a frame of latency. This doesn't matter for MMAL or DRM renderers since they
// are drawing directly to the screen without involving SDL, but it may matter for other
// future KMSDRM platforms that use SDL for rendering.
SDL_SetHint(SDL_HINT_VIDEO_DOUBLE_BUFFER, "1");
// We use MMAL to render on Raspberry Pi, so we do not require DRM master.
SDL_SetHint("SDL_KMSDRM_REQUIRE_DRM_MASTER", "0");
// Use Direct3D 9Ex to avoid a deadlock caused by the D3D device being reset when
// the user triggers a UAC prompt. This option controls the software/SDL renderer.
// The DXVA2 renderer uses Direct3D 9Ex itself directly.
SDL_SetHint("SDL_WINDOWS_USE_D3D9EX", "1");
if (SDL_InitSubSystem(SDL_INIT_TIMER) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_TIMER) failed: %s",
SDL_GetError());
return -1;
}
#ifdef STEAM_LINK
// Steam Link requires that we initialize video before creating our
// QGuiApplication in order to configure the framebuffer correctly.
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s",
SDL_GetError());
return -1;
}
#endif
// Use atexit() to ensure SDL_Quit() is called. This avoids
// racing with object destruction where SDL may be used.
atexit(SDL_Quit);
// Avoid the default behavior of changing the timer resolution to 1 ms.
// We don't want this all the time that Moonlight is open. We will set
// it manually when we start streaming.
SDL_SetHint(SDL_HINT_TIMER_RESOLUTION, "0");
// Disable minimize on focus loss by default. Users seem to want this off by default.
SDL_SetHint(SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS, "0");
// SDL 2.0.12 changes the default behavior to use the button label rather than the button
// position as most other software does. Set this back to 0 to stay consistent with prior
// releases of Moonlight.
SDL_SetHint("SDL_GAMECONTROLLER_USE_BUTTON_LABELS", "0");
// Disable relative mouse scaling to renderer size or logical DPI. We want to send
// the mouse motion exactly how it was given to us.
SDL_SetHint("SDL_MOUSE_RELATIVE_SCALING", "0");
// Set our app name for SDL to use with PulseAudio. This matches what we provide
// as our app name to libsoundio too.
SDL_SetHint("SDL_AUDIO_DEVICE_APP_NAME", "Moonlight");
#ifdef QT_DEBUG
// Allow thread naming using exceptions on debug builds. SDL doesn't use SEH
// when throwing the exceptions, so we don't enable it for release builds out
// of caution.
SDL_SetHint(SDL_HINT_WINDOWS_DISABLE_THREAD_NAMING, "0");
#endif
QGuiApplication app(argc, argv);
// Apply the initial translation based on user preference
StreamingPreferences prefs;
prefs.retranslate();
// After the QGuiApplication is created, the platform stuff will be initialized
// and we can set the SDL video driver to match Qt.
if (WMUtils::isRunningWayland() && QGuiApplication::platformName() == "xcb") {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Detected XWayland. This will probably break hardware decoding! Try running with QT_QPA_PLATFORM=wayland or switch to X11.");
}
else if (QGuiApplication::platformName().startsWith("wayland")) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Detected Wayland");
qputenv("SDL_VIDEODRIVER", "wayland");
}
#ifdef STEAM_LINK
// Qt 5.9 from the Steam Link SDK is not able to load any fonts
// since the Steam Link doesn't include any of the ones it looks
// for. We know it has NotoSans so we will explicitly ask for that.
if (app.font().family().isEmpty()) {
qWarning() << "SL HACK: No default font - using NotoSans";
QFont fon("NotoSans");
app.setFont(fon);
}
// Move the mouse to the bottom right so it's invisible when using
// gamepad-only navigation.
QCursor().setPos(0xFFFF, 0xFFFF);
#elif !SDL_VERSION_ATLEAST(2, 0, 11) && defined(Q_OS_LINUX) && (defined(__arm__) || defined(__aarch64__))
if (qgetenv("SDL_VIDEO_GL_DRIVER").isEmpty() && QGuiApplication::platformName() == "eglfs") {
// Look for Raspberry Pi GLES libraries. SDL 2.0.10 and earlier needs some help finding
// the correct libraries for the KMSDRM backend if not compiled with the RPI backend enabled.
if (SDL_LoadObject("libbrcmGLESv2.so") != nullptr) {
qputenv("SDL_VIDEO_GL_DRIVER", "libbrcmGLESv2.so");
}
else if (SDL_LoadObject("/opt/vc/lib/libbrcmGLESv2.so") != nullptr) {
qputenv("SDL_VIDEO_GL_DRIVER", "/opt/vc/lib/libbrcmGLESv2.so");
}
}
#endif
#ifndef Q_OS_DARWIN
// Set the window icon except on macOS where we want to keep the
// modified macOS 11 style rounded corner icon.
app.setWindowIcon(QIcon(":/res/moonlight.svg"));
#endif
// This is necessary to show our icon correctly on Wayland
app.setDesktopFileName("com.moonlight_stream.Moonlight.desktop");
qputenv("SDL_VIDEO_WAYLAND_WMCLASS", "com.moonlight_stream.Moonlight");
qputenv("SDL_VIDEO_X11_WMCLASS", "com.moonlight_stream.Moonlight");
// Register our C++ types for QML
qmlRegisterType<ComputerModel>("ComputerModel", 1, 0, "ComputerModel");
qmlRegisterType<AppModel>("AppModel", 1, 0, "AppModel");
qmlRegisterType<StreamingPreferences>("StreamingPreferences", 1, 0, "StreamingPreferences");
qmlRegisterType<SdlGamepadKeyNavigation>("SdlGamepadKeyNavigation", 1, 0, "SdlGamepadKeyNavigation");
qmlRegisterUncreatableType<Session>("Session", 1, 0, "Session", "Session cannot be created from QML");
qmlRegisterSingletonType<ComputerManager>("ComputerManager", 1, 0,
"ComputerManager",
@@ -522,48 +331,8 @@ int main(int argc, char *argv[])
[](QQmlEngine*, QJSEngine*) -> QObject* {
return new AutoUpdateChecker();
});
qmlRegisterSingletonType<SystemProperties>("SystemProperties", 1, 0,
"SystemProperties",
[](QQmlEngine*, QJSEngine*) -> QObject* {
return new SystemProperties();
});
qmlRegisterSingletonType<SdlGamepadKeyNavigation>("SdlGamepadKeyNavigation", 1, 0,
"SdlGamepadKeyNavigation",
[](QQmlEngine*, QJSEngine*) -> QObject* {
return new SdlGamepadKeyNavigation();
});
qmlRegisterSingletonType<StreamingPreferences>("StreamingPreferences", 1, 0,
"StreamingPreferences",
[](QQmlEngine* qmlEngine, QJSEngine*) -> QObject* {
return new StreamingPreferences(qmlEngine);
});
// Create the identity manager on the main thread
IdentityManager::get();
#ifndef Q_OS_WINRT
// Use the dense material dark theme by default
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_STYLE")) {
qputenv("QT_QUICK_CONTROLS_STYLE", "Material");
}
#else
// Use universal dark on WinRT
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_STYLE")) {
qputenv("QT_QUICK_CONTROLS_STYLE", "Universal");
}
#endif
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_MATERIAL_THEME")) {
qputenv("QT_QUICK_CONTROLS_MATERIAL_THEME", "Dark");
}
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_MATERIAL_ACCENT")) {
qputenv("QT_QUICK_CONTROLS_MATERIAL_ACCENT", "Purple");
}
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_MATERIAL_VARIANT")) {
qputenv("QT_QUICK_CONTROLS_MATERIAL_VARIANT", "Dense");
}
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_UNIVERSAL_THEME")) {
qputenv("QT_QUICK_CONTROLS_UNIVERSAL_THEME", "Dark");
}
QQuickStyle::setStyle("Material");
QQmlApplicationEngine engine;
QString initialView;
@@ -571,11 +340,11 @@ int main(int argc, char *argv[])
GlobalCommandLineParser parser;
switch (parser.parse(app.arguments())) {
case GlobalCommandLineParser::NormalStartRequested:
initialView = "qrc:/gui/PcView.qml";
initialView = "PcView.qml";
break;
case GlobalCommandLineParser::StreamRequested:
{
initialView = "qrc:/gui/CliStartStreamSegue.qml";
initialView = "CliStartStreamSegue.qml";
StreamingPreferences* preferences = new StreamingPreferences(&app);
StreamCommandLineParser streamParser;
streamParser.parse(app.arguments(), preferences);
@@ -587,7 +356,7 @@ int main(int argc, char *argv[])
}
case GlobalCommandLineParser::QuitRequested:
{
initialView = "qrc:/gui/CliQuitStreamSegue.qml";
initialView = "CliQuitStreamSegue.qml";
QuitCommandLineParser quitParser;
quitParser.parse(app.arguments());
auto launcher = new CliQuitStream::Launcher(quitParser.getHost(), &app);
@@ -602,6 +371,23 @@ int main(int argc, char *argv[])
engine.load(QUrl(QStringLiteral("qrc:/gui/main.qml")));
if (engine.rootObjects().isEmpty())
return -1;
SDL_SetMainReady();
if (SDL_Init(SDL_INIT_TIMER) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_Init() failed: %s",
SDL_GetError());
}
// Use atexit() to ensure SDL_Quit() is called. This avoids
// racing with object destruction where SDL may be used.
atexit(SDL_Quit);
// Avoid the default behavior of changing the timer resolution to 1 ms.
// We don't want this all the time that Moonlight is open. We will set
// it manually when we start streaming.
SDL_SetHint(SDL_HINT_TIMER_RESOLUTION, "0");
int err = app.exec();
// Give worker tasks time to properly exit. Fixes PendingQuitTask
-146
View File
@@ -1,146 +0,0 @@
// This file contains hooks for several functions that allow
// Qt and SDL to more or less share DRM master ownership.
//
// This technique requires Linux v5.8 or later, or for Moonlight
// to run as root (with CAP_SYS_ADMIN). Prior to Linux v5.8,
// DRM_IOCTL_DROP_MASTER required CAP_SYS_ADMIN, which prevents
// our trick from working (without root, that is).
//
// The specific kernel change required to run without root is:
// https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=45bc3d26c95a8fc63a7d8668ca9e57ef0883351c
#define _GNU_SOURCE
#include <SDL.h>
#include <dlfcn.h>
#include <unistd.h>
#include <errno.h>
#include <sys/stat.h>
#include <xf86drm.h>
#include <xf86drmMode.h>
// We require SDL 2.0.15+ to hook because it supports sharing
// the DRM FD with our code. This avoids having multiple DRM FDs
// in flight at the same time which would significantly complicate
// the logic here because we'd need to figure out exactly which FD
// should be the master at any given time. With the position of our
// hooks, that is definitely not trivial.
#if SDL_VERSION_ATLEAST(2, 0, 15)
// Qt's DRM master FD grabbed by our hook
int g_QtDrmMasterFd = -1;
struct stat64 g_DrmMasterStat;
// The DRM master FD created for SDL
int g_SdlDrmMasterFd = -1;
// This hook will handle legacy DRM rendering
int drmModeSetCrtc(int fd, uint32_t crtcId, uint32_t bufferId,
uint32_t x, uint32_t y, uint32_t *connectors, int count,
drmModeModeInfoPtr mode)
{
// Grab the first DRM FD that makes it in here. This will be the Qt
// EGLFS backend's DRM FD, which we will need later.
if (g_QtDrmMasterFd == -1) {
g_QtDrmMasterFd = fd;
fstat64(g_QtDrmMasterFd, &g_DrmMasterStat);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Captured Qt EGLFS DRM master fd (legacy): %d",
g_QtDrmMasterFd);
}
// Call into the real thing
return ((typeof(drmModeSetCrtc)*)dlsym(RTLD_NEXT, "drmModeSetCrtc"))(fd, crtcId, bufferId, x, y, connectors, count, mode);
}
// This hook will handle atomic DRM rendering
int drmModeAtomicCommit(int fd, drmModeAtomicReqPtr req,
uint32_t flags, void *user_data)
{
// Grab the first DRM FD that makes it in here. This will be the Qt
// EGLFS backend's DRM FD, which we will need later.
if (g_QtDrmMasterFd == -1) {
g_QtDrmMasterFd = fd;
fstat64(g_QtDrmMasterFd, &g_DrmMasterStat);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Captured Qt EGLFS DRM master fd (atomic): %d",
g_QtDrmMasterFd);
}
// Call into the real thing
return ((typeof(drmModeAtomicCommit)*)dlsym(RTLD_NEXT, "drmModeAtomicCommit"))(fd, req, flags, user_data);
}
// This hook will handle SDL's open() on the DRM device. We just need to
// hook this variant of open(), since that's what SDL uses. When we see
// the open a FD for the same card as the Qt DRM master FD, we'll drop
// master on the Qt FD to allow the new FD to have master.
int open(const char *pathname, int flags)
{
// Call the real thing to do the open operation
int fd = ((typeof(open)*)dlsym(RTLD_NEXT, "open"))(pathname, flags);
// If the file was successfully opened and we have a DRM master FD,
// check if the FD we just opened is a DRM device.
if (fd >= 0 && g_QtDrmMasterFd != -1) {
if (strncmp(pathname, "/dev/dri/card", 13) == 0) {
// It's a DRM device, but is it _our_ DRM device?
struct stat64 fdstat;
fstat64(fd, &fdstat);
if (g_DrmMasterStat.st_dev == fdstat.st_dev &&
g_DrmMasterStat.st_ino == fdstat.st_ino) {
// It is our device. Time to do the magic!
// This code assumes SDL only ever opens a single FD
// for a given DRM device.
SDL_assert(g_SdlDrmMasterFd == -1);
// Drop master on Qt's FD so we can pick it up for SDL.
if (drmDropMaster(g_QtDrmMasterFd) < 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to drop master on Qt DRM FD: %d",
errno);
// Hope for the best
return fd;
}
// We are not allowed to call drmSetMaster() without CAP_SYS_ADMIN,
// but since we just dropped the master, we can become master by
// simply creating a new FD. Let's do it.
close(fd);
fd = ((typeof(open)*)dlsym(RTLD_NEXT, "open"))(pathname, flags);
g_SdlDrmMasterFd = fd;
}
}
}
return fd;
}
// Our close() hook handles restoring DRM master to the Qt FD
// after SDL closes its DRM FD.
int close(int fd)
{
// Call the real thing
int ret = ((typeof(close)*)dlsym(RTLD_NEXT, "close"))(fd);
if (ret == 0) {
// If we just closed the SDL DRM master FD, restore master
// to the Qt DRM FD. This works because the Qt DRM master FD
// was master once before, so we can set it as master again
// using drmSetMaster() without CAP_SYS_ADMIN.
if (g_SdlDrmMasterFd != -1 && fd == g_SdlDrmMasterFd) {
if (drmSetMaster(g_QtDrmMasterFd) < 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to restore master to Qt DRM FD: %d",
errno);
}
g_SdlDrmMasterFd = -1;
}
}
return ret;
}
#endif
Binary file not shown.
+18 -57
View File
@@ -6,9 +6,9 @@
#include <QSettings>
#include <QCoreApplication>
QString Path::s_CacheDir;
QString Path::s_LogDir;
QString Path::s_BoxArtCacheDir;
const QString Path::k_GameControllerMappingFile("gamecontrollerdb.txt");
QString Path::getLogDir()
{
@@ -22,74 +22,40 @@ QString Path::getBoxArtCacheDir()
return s_BoxArtCacheDir;
}
QByteArray Path::readDataFile(QString fileName)
{
QFile dataFile(getDataFilePath(fileName));
dataFile.open(QIODevice::ReadOnly);
return dataFile.readAll();
}
void Path::writeCacheFile(QString fileName, QByteArray data)
{
QDir cacheDir(s_CacheDir);
// Create the cache path if it does not exist
if (!cacheDir.exists()) {
cacheDir.mkpath(".");
}
QFile dataFile(cacheDir.absoluteFilePath(fileName));
dataFile.open(QIODevice::WriteOnly);
dataFile.write(data);
}
void Path::deleteCacheFile(QString fileName)
{
QFile dataFile(QDir(s_CacheDir).absoluteFilePath(fileName));
dataFile.remove();
}
QFileInfo Path::getCacheFileInfo(QString fileName)
{
return QFileInfo(QDir(s_CacheDir), fileName);
}
QString Path::getDataFilePath(QString fileName)
QString Path::getGamepadMappingFile()
{
QString candidatePath;
// Check the cache location first (used by Path::writeDataFile())
candidatePath = QDir(s_CacheDir).absoluteFilePath(fileName);
// Check the current directory first
candidatePath = QDir(QDir::currentPath()).absoluteFilePath(k_GameControllerMappingFile);
if (QFile::exists(candidatePath)) {
qInfo() << "Found" << fileName << "at" << candidatePath;
return candidatePath;
}
// Check the current directory
candidatePath = QDir(QDir::currentPath()).absoluteFilePath(fileName);
if (QFile::exists(candidatePath)) {
qInfo() << "Found" << fileName << "at" << candidatePath;
return candidatePath;
}
// Now check the data directories (for Linux, in particular)
candidatePath = QStandardPaths::locate(QStandardPaths::AppDataLocation, fileName);
candidatePath = QStandardPaths::locate(QStandardPaths::DataLocation, k_GameControllerMappingFile);
if (!candidatePath.isEmpty() && QFile::exists(candidatePath)) {
qInfo() << "Found" << fileName << "at" << candidatePath;
return candidatePath;
}
// Now try the directory of our app installation (for Windows, if current dir doesn't find it)
candidatePath = QDir(QCoreApplication::applicationDirPath()).absoluteFilePath(fileName);
candidatePath = QDir(QCoreApplication::applicationDirPath()).absoluteFilePath(k_GameControllerMappingFile);
if (QFile::exists(candidatePath)) {
qInfo() << "Found" << fileName << "at" << candidatePath;
return candidatePath;
}
// Return the QRC embedded copy
candidatePath = ":/data/" + fileName;
qInfo() << "Found" << fileName << "at" << candidatePath;
return QString(candidatePath);
// Now try the Resources folder in our app bundle for macOS
QDir dir = QDir(QCoreApplication::applicationDirPath());
dir.cdUp();
dir.cd("Resources");
dir.filePath(k_GameControllerMappingFile);
candidatePath = dir.absoluteFilePath(k_GameControllerMappingFile);
if (QFile::exists(candidatePath)) {
return candidatePath;
}
// Couldn't find it
return QString();
}
void Path::initialize(bool portable)
@@ -97,10 +63,6 @@ void Path::initialize(bool portable)
if (portable) {
s_LogDir = QDir::currentPath();
s_BoxArtCacheDir = QDir::currentPath() + "/boxart";
// In order for the If-Modified-Since logic to work in MappingFetcher,
// the cache directory must be different than the current directory.
s_CacheDir = QDir::currentPath() + "/cache";
}
else {
#ifdef Q_OS_DARWIN
@@ -110,7 +72,6 @@ void Path::initialize(bool portable)
#else
s_LogDir = QDir::tempPath();
#endif
s_CacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation);
s_BoxArtCacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/boxart";
}
}
+2 -9
View File
@@ -1,7 +1,6 @@
#pragma once
#include <QString>
#include <QFileInfo>
class Path
{
@@ -10,18 +9,12 @@ public:
static QString getBoxArtCacheDir();
static QByteArray readDataFile(QString fileName);
static void writeCacheFile(QString fileName, QByteArray data);
static void deleteCacheFile(QString fileName);
static QFileInfo getCacheFileInfo(QString fileName);
// Only safe to use directly for Qt classes
static QString getDataFilePath(QString fileName);
static QString getGamepadMappingFile();
static void initialize(bool portable);
private:
static QString s_CacheDir;
static QString s_LogDir;
static QString s_BoxArtCacheDir;
static const QString k_GameControllerMappingFile;
};
-5
View File
@@ -13,10 +13,5 @@
<file>gui/CliQuitStreamSegue.qml</file>
<file>gui/CliStartStreamSegue.qml</file>
<file>gui/AutoResizingComboBox.qml</file>
<file>gui/NavigableMenu.qml</file>
<file>gui/ErrorMessageDialog.qml</file>
<file>gui/NavigableMessageDialog.qml</file>
<file>gui/NavigableDialog.qml</file>
<file>gui/CenteredGridView.qml</file>
</qresource>
</RCC>
-1
View File
@@ -1 +0,0 @@
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 245 240" fill="white"><path class="st0" d="M104.4 103.9c-5.7 0-10.2 5-10.2 11.1s4.6 11.1 10.2 11.1c5.7 0 10.2-5 10.2-11.1.1-6.1-4.5-11.1-10.2-11.1zM140.9 103.9c-5.7 0-10.2 5-10.2 11.1s4.6 11.1 10.2 11.1c5.7 0 10.2-5 10.2-11.1s-4.5-11.1-10.2-11.1z"/><path class="st0" d="M189.5 20h-134C44.2 20 35 29.2 35 40.6v135.2c0 11.4 9.2 20.6 20.5 20.6h113.4l-5.3-18.5 12.8 11.9 12.1 11.2 21.5 19V40.6c0-11.4-9.2-20.6-20.5-20.6zm-38.6 130.6s-3.6-4.3-6.6-8.1c13.1-3.7 18.1-11.9 18.1-11.9-4.1 2.7-8 4.6-11.5 5.9-5 2.1-9.8 3.5-14.5 4.3-9.6 1.8-18.4 1.3-25.9-.1-5.7-1.1-10.6-2.7-14.7-4.3-2.3-.9-4.8-2-7.3-3.4-.3-.2-.6-.3-.9-.5-.2-.1-.3-.2-.4-.3-1.8-1-2.8-1.7-2.8-1.7s4.8 8 17.5 11.8c-3 3.8-6.7 8.3-6.7 8.3-22.1-.7-30.5-15.2-30.5-15.2 0-32.2 14.4-58.3 14.4-58.3 14.4-10.8 28.1-10.5 28.1-10.5l1 1.2c-18 5.2-26.3 13.1-26.3 13.1s2.2-1.2 5.9-2.9c10.7-4.7 19.2-6 22.7-6.3.6-.1 1.1-.2 1.7-.2 6.1-.8 13-1 20.2-.2 9.5 1.1 19.7 3.9 30.1 9.6 0 0-7.9-7.5-24.9-12.7l1.4-1.6s13.7-.3 28.1 10.5c0 0 14.4 26.1 14.4 58.3 0 0-8.5 14.5-30.6 15.2z"/></svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

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

Before

Width:  |  Height:  |  Size: 325 B

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

Before

Width:  |  Height:  |  Size: 339 B

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

Before

Width:  |  Height:  |  Size: 323 B

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

Before

Width:  |  Height:  |  Size: 397 B

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

Before

Width:  |  Height:  |  Size: 276 B

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

After

Width:  |  Height:  |  Size: 303 B

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

After

Width:  |  Height:  |  Size: 467 B

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

After

Width:  |  Height:  |  Size: 273 B

+3 -20
View File
@@ -1,35 +1,18 @@
<RCC>
<qresource prefix="/">
<file>res/ic_remove_circle_outline_white_48px.svg</file>
<file>res/ic_remove_from_queue_white_48px.svg</file>
<file>res/ic_videogame_asset_white_48px.svg</file>
<file>res/desktop_windows-48px.svg</file>
<file>res/ic_tv_white_48px.svg</file>
<file>res/ic_add_to_queue_white_48px.svg</file>
<file>res/baseline-lock-24px.svg</file>
<file>res/baseline-play_circle_filled_white-48px.svg</file>
<file>res/baseline-warning-24px.svg</file>
<file>res/baseline-cancel-24px.svg</file>
<file>res/no_app_image.png</file>
<file>res/settings.svg</file>
<file>res/arrow_left.svg</file>
<file>res/question_mark.svg</file>
<file>res/moonlight.svg</file>
<file>res/update.svg</file>
<file>res/baseline-help_outline-24px.svg</file>
<file>res/baseline-error_outline-24px.svg</file>
<file>res/baseline-check_circle_outline-24px.svg</file>
<file>res/Discord-Logo-White.svg</file>
<file>languages/qml_de.ts</file>
<file>languages/qml_de.qm</file>
<file>languages/qml_fr.ts</file>
<file>languages/qml_fr.qm</file>
<file>languages/qml_zh_cn.ts</file>
<file>languages/qml_zh_cn.qm</file>
</qresource>
<qresource prefix="/data">
<file alias="gamecontrollerdb.txt">SDL_GameControllerDB/gamecontrollerdb.txt</file>
<file alias="ModeSeven.ttf">ModeSeven.ttf</file>
<file alias="egl.frag">shaders/egl.frag</file>
<file alias="egl.vert">shaders/egl.vert</file>
<file alias="egl_overlay.frag">shaders/egl_overlay.frag</file>
<file alias="egl_overlay.vert">shaders/egl_overlay.vert</file>
</qresource>
</RCC>
-91
View File
@@ -1,91 +0,0 @@
#include "mappingfetcher.h"
#include "path.h"
#include <QNetworkReply>
MappingFetcher::MappingFetcher(QObject *parent) :
QObject(parent),
m_Nam(this)
{
// Never communicate over HTTP
m_Nam.setStrictTransportSecurityEnabled(true);
// Allow HTTP redirects
m_Nam.setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy);
connect(&m_Nam, &QNetworkAccessManager::finished,
this, &MappingFetcher::handleMappingListFetched);
}
void MappingFetcher::start()
{
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) && QT_VERSION < QT_VERSION_CHECK(5, 15, 1) && !defined(QT_NO_BEARERMANAGEMENT)
// HACK: Set network accessibility to work around QTBUG-80947 (introduced in Qt 5.14.0 and fixed in Qt 5.15.1)
QT_WARNING_PUSH
QT_WARNING_DISABLE_DEPRECATED
m_Nam.setNetworkAccessible(QNetworkAccessManager::Accessible);
QT_WARNING_POP
#endif
QUrl url("https://moonlight-stream.org/SDL_GameControllerDB/gamecontrollerdb.txt");
QNetworkRequest request(url);
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
request.setAttribute(QNetworkRequest::Http2AllowedAttribute, true);
#else
request.setAttribute(QNetworkRequest::HTTP2AllowedAttribute, true);
#endif
// Qt 5.12 introduced QNetworkRequest::IfModifiedSinceHeader. We _could_ implement it
// by hand (including QDateTime conversion to the correct string form) for earlier Qt
// versions, but that's a pain and this is just an optimization anyway. We'll do a bit
// of extra work on those legacy Qt versions by fetching the GCDB each time we launch.
#if QT_VERSION >= QT_VERSION_CHECK(5, 12, 0)
// Only download the file if it's newer than what we have
QFileInfo existingFileInfo = Path::getCacheFileInfo("gamecontrollerdb.txt");
if (existingFileInfo.exists()) {
// Make sure the cached file looks reasonable. It should have some data and
// the last modified time should not be in the future.
QDateTime lastModifiedTime = existingFileInfo.lastModified().toUTC();
if (existingFileInfo.size() > 0 && lastModifiedTime <= QDateTime::currentDateTimeUtc()) {
request.setHeader(QNetworkRequest::IfModifiedSinceHeader, existingFileInfo.lastModified().toUTC());
}
else {
Path::deleteCacheFile("gamecontrollerdb.txt");
}
}
#endif
// We'll get a callback when this is finished
m_Nam.get(request);
}
void MappingFetcher::handleMappingListFetched(QNetworkReply* reply)
{
Q_ASSERT(reply->isFinished());
if (reply->error() == QNetworkReply::NoError) {
// Queue the reply for deletion
reply->deleteLater();
// If we get a 304 back, Qt will happily just tell us our request was
// successful and let us try to write our empty response to disk. Check
// the status code directly to prevent this.
if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute) == 304) {
qInfo() << "Gamepad mappings are up to date";
}
else {
// Update the cached data on disk for next call to applyMappings()
QByteArray data = reply->readAll();
if (!data.isEmpty()) {
Path::writeCacheFile("gamecontrollerdb.txt", data);
}
qInfo() << "Downloaded updated gamepad mappings";
}
}
else {
qWarning() << "Failed to download updated gamepad mappings:" << reply->error();
reply->deleteLater();
}
}
-20
View File
@@ -1,20 +0,0 @@
#pragma once
#include <QObject>
#include <QNetworkAccessManager>
class MappingFetcher : public QObject
{
Q_OBJECT
public:
explicit MappingFetcher(QObject *parent = nullptr);
void start();
private slots:
void handleMappingListFetched(QNetworkReply* reply);
private:
QNetworkAccessManager m_Nam;
};
+18 -37
View File
@@ -10,18 +10,10 @@
#define SER_GUID "guid"
#define SER_MAPPING "mapping"
MappingFetcher* MappingManager::s_MappingFetcher;
MappingManager::MappingManager()
{
QSettings settings;
// Load updated mappings from the Internet once per Moonlight launch
if (s_MappingFetcher == nullptr) {
s_MappingFetcher = new MappingFetcher();
s_MappingFetcher->start();
}
// First load existing saved mappings. This ensures the user's
// hints can always override the old data.
int mappingCount = settings.beginReadArray(SER_GAMEPADMAPPING);
@@ -34,14 +26,8 @@ MappingManager::MappingManager()
settings.endArray();
// Finally load mappings from SDL_HINT_GAMECONTROLLERCONFIG
QStringList sdlMappings =
QString::fromLocal8Bit(SDL_GetHint(SDL_HINT_GAMECONTROLLERCONFIG))
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
.split('\n', Qt::SkipEmptyParts);
#else
.split('\n', QString::SkipEmptyParts);
#endif
for (const QString& sdlMapping : sdlMappings) {
QStringList sdlMappings = QString::fromLocal8Bit(SDL_GetHint(SDL_HINT_GAMECONTROLLERCONFIG)).split('\n', QString::SkipEmptyParts);
for (QString sdlMapping : sdlMappings) {
SdlGamepadMapping mapping(sdlMapping);
addMapping(mapping);
}
@@ -68,34 +54,29 @@ void MappingManager::save()
void MappingManager::applyMappings()
{
QByteArray mappingData = Path::readDataFile("gamecontrollerdb.txt");
if (!mappingData.isEmpty()) {
int newMappings = SDL_GameControllerAddMappingsFromRW(
SDL_RWFromConstMem(mappingData.constData(), mappingData.size()), 1);
QString mappingFile = Path::getGamepadMappingFile();
if (!mappingFile.isEmpty()) {
std::string mappingFileNative = QDir::toNativeSeparators(mappingFile).toStdString();
if (newMappings > 0) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Loading gamepad mappings from: %s",
mappingFileNative.c_str());
int newMappings = SDL_GameControllerAddMappingsFromFile(mappingFileNative.c_str());
if (newMappings < 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Error loading gamepad mappings: %s",
SDL_GetError());
}
else {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Loaded %d new gamepad mappings",
newMappings);
}
else {
if (newMappings < 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Error loading gamepad mappings: %s",
SDL_GetError());
}
else if (newMappings == 0) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"0 new mappings found in gamecontrollerdb.txt. Is it corrupt?");
}
// Try deleting the cached mapping list just in case it's corrupt
Path::deleteCacheFile("gamecontrollerdb.txt");
}
}
else {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Unable to load gamepad mapping file");
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"No gamepad mapping file found");
}
QList<SdlGamepadMapping> mappings = m_Mappings.values();
-4
View File
@@ -1,7 +1,5 @@
#pragma once
#include "mappingfetcher.h"
#include <QSettings>
class SdlGamepadMapping
@@ -72,7 +70,5 @@ public:
private:
QMap<QString, SdlGamepadMapping> m_Mappings;
static MappingFetcher* s_MappingFetcher;
};
+131 -164
View File
@@ -1,11 +1,8 @@
#include "streamingpreferences.h"
#include "streaming/session.h"
#include "streaming/streamutils.h"
#include <QSettings>
#include <QTranslator>
#include <QCoreApplication>
#include <QLocale>
#include <QtDebug>
#define SER_STREAMSETTINGS "streamsettings"
#define SER_WIDTH "width"
@@ -24,37 +21,12 @@
#define SER_UNSUPPORTEDFPS "unsupportedfps"
#define SER_MDNS "mdns"
#define SER_QUITAPPAFTER "quitAppAfter"
#define SER_ABSMOUSEMODE "mouseacceleration"
#define SER_ABSTOUCHMODE "abstouchmode"
#define SER_MOUSEACCELERATION "mouseacceleration"
#define SER_STARTWINDOWED "startwindowed"
#define SER_FRAMEPACING "framepacing"
#define SER_CONNWARNINGS "connwarnings"
#define SER_UIDISPLAYMODE "uidisplaymode"
#define SER_RICHPRESENCE "richpresence"
#define SER_GAMEPADMOUSE "gamepadmouse"
#define SER_DEFAULTVER "defaultver"
#define SER_PACKETSIZE "packetsize"
#define SER_DETECTNETBLOCKING "detectnetblocking"
#define SER_SWAPMOUSEBUTTONS "swapmousebuttons"
#define SER_MUTEONFOCUSLOSS "muteonfocusloss"
#define SER_BACKGROUNDGAMEPAD "backgroundgamepad"
#define SER_REVERSESCROLL "reversescroll"
#define SER_SWAPFACEBUTTONS "swapfacebuttons"
#define SER_CAPTURESYSKEYS "capturesyskeys"
#define SER_LANGUAGE "language"
#define CURRENT_DEFAULT_VER 1
StreamingPreferences::StreamingPreferences(QObject *parent)
: QObject(parent),
m_QmlEngine(nullptr)
{
reload();
}
StreamingPreferences::StreamingPreferences(QQmlEngine *qmlEngine, QObject *parent)
: QObject(parent),
m_QmlEngine(qmlEngine)
: QObject(parent)
{
reload();
}
@@ -63,14 +35,6 @@ void StreamingPreferences::reload()
{
QSettings settings;
int defaultVer = settings.value(SER_DEFAULTVER, 0).toInt();
#ifdef Q_OS_DARWIN
recommendedFullScreenMode = WindowMode::WM_FULLSCREEN_DESKTOP;
#else
recommendedFullScreenMode = WindowMode::WM_FULLSCREEN;
#endif
width = settings.value(SER_WIDTH, 1280).toInt();
height = settings.value(SER_HEIGHT, 720).toInt();
fps = settings.value(SER_FPS, 60).toInt();
@@ -82,21 +46,9 @@ void StreamingPreferences::reload()
unsupportedFps = settings.value(SER_UNSUPPORTEDFPS, false).toBool();
enableMdns = settings.value(SER_MDNS, true).toBool();
quitAppAfter = settings.value(SER_QUITAPPAFTER, false).toBool();
absoluteMouseMode = settings.value(SER_ABSMOUSEMODE, false).toBool();
absoluteTouchMode = settings.value(SER_ABSTOUCHMODE, true).toBool();
mouseAcceleration = settings.value(SER_MOUSEACCELERATION, false).toBool();
startWindowed = settings.value(SER_STARTWINDOWED, false).toBool();
framePacing = settings.value(SER_FRAMEPACING, false).toBool();
connectionWarnings = settings.value(SER_CONNWARNINGS, true).toBool();
richPresence = settings.value(SER_RICHPRESENCE, true).toBool();
gamepadMouse = settings.value(SER_GAMEPADMOUSE, true).toBool();
detectNetworkBlocking = settings.value(SER_DETECTNETBLOCKING, true).toBool();
packetSize = settings.value(SER_PACKETSIZE, 0).toInt();
swapMouseButtons = settings.value(SER_SWAPMOUSEBUTTONS, false).toBool();
muteOnFocusLoss = settings.value(SER_MUTEONFOCUSLOSS, false).toBool();
backgroundGamepad = settings.value(SER_BACKGROUNDGAMEPAD, false).toBool();
reverseScrollDirection = settings.value(SER_REVERSESCROLL, false).toBool();
swapFaceButtons = settings.value(SER_SWAPFACEBUTTONS, false).toBool();
captureSysKeysMode = static_cast<CaptureSysKeysMode>(settings.value(SER_CAPTURESYSKEYS,
static_cast<int>(CaptureSysKeysMode::CSK_OFF)).toInt());
audioConfig = static_cast<AudioConfig>(settings.value(SER_AUDIOCFG,
static_cast<int>(AudioConfig::AC_STEREO)).toInt());
videoCodecConfig = static_cast<VideoCodecConfig>(settings.value(SER_VIDEOCFG,
@@ -106,93 +58,7 @@ void StreamingPreferences::reload()
windowMode = static_cast<WindowMode>(settings.value(SER_WINDOWMODE,
// Try to load from the old preference value too
static_cast<int>(settings.value(SER_FULLSCREEN, true).toBool() ?
recommendedFullScreenMode : WindowMode::WM_WINDOWED)).toInt());
uiDisplayMode = static_cast<UIDisplayMode>(settings.value(SER_UIDISPLAYMODE,
static_cast<int>(settings.value(SER_STARTWINDOWED, true).toBool() ? UIDisplayMode::UI_WINDOWED
: UIDisplayMode::UI_MAXIMIZED)).toInt());
language = static_cast<Language>(settings.value(SER_LANGUAGE,
static_cast<int>(Language::LANG_AUTO)).toInt());
// Perform default settings updates as required based on last default version
if (defaultVer == 0) {
#ifdef Q_OS_DARWIN
// Update window mode setting on macOS from full-screen (old default) to borderless windowed (new default)
if (windowMode == WindowMode::WM_FULLSCREEN) {
windowMode = WindowMode::WM_FULLSCREEN_DESKTOP;
}
#endif
}
}
bool StreamingPreferences::retranslate()
{
static QTranslator* translator = nullptr;
#if QT_VERSION < QT_VERSION_CHECK(5, 10, 0)
if (m_QmlEngine != nullptr) {
// Dynamic retranslation is not supported until Qt 5.10
return false;
}
#endif
QTranslator* newTranslator = new QTranslator();
QString languageSuffix = getSuffixFromLanguage(language);
// Remove the old translator, even if we can't load a new one.
// Otherwise we'll be stuck with the old translated values instead
// of defaulting to English.
if (translator != nullptr) {
QCoreApplication::removeTranslator(translator);
delete translator;
translator = nullptr;
}
if (newTranslator->load(QString(":/languages/qml_") + languageSuffix)) {
qInfo() << "Successfully loaded translation for " << languageSuffix;
translator = newTranslator;
QCoreApplication::installTranslator(translator);
}
else {
qInfo() << "No translation available for " << languageSuffix;
delete newTranslator;
}
if (m_QmlEngine != nullptr) {
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
// This is a dynamic retranslation from the settings page.
// We have to kick the QML engine into reloading our text.
m_QmlEngine->retranslate();
#else
// Unreachable below Qt 5.10 due to the check above
Q_ASSERT(false);
#endif
}
else {
// This is a translation from a non-QML context, which means
// it is probably app startup. There's nothing to refresh.
}
return true;
}
QString StreamingPreferences::getSuffixFromLanguage(StreamingPreferences::Language lang)
{
switch (lang)
{
case LANG_DE:
return "de";
case LANG_EN:
return "en";
case LANG_FR:
return "fr";
case LANG_ZH_CN:
return "zh_cn";
case LANG_AUTO:
default:
return QLocale::system().name();
}
WindowMode::WM_FULLSCREEN : WindowMode::WM_WINDOWED)).toInt());
}
void StreamingPreferences::save()
@@ -210,27 +76,134 @@ void StreamingPreferences::save()
settings.setValue(SER_UNSUPPORTEDFPS, unsupportedFps);
settings.setValue(SER_MDNS, enableMdns);
settings.setValue(SER_QUITAPPAFTER, quitAppAfter);
settings.setValue(SER_ABSMOUSEMODE, absoluteMouseMode);
settings.setValue(SER_ABSTOUCHMODE, absoluteTouchMode);
settings.setValue(SER_MOUSEACCELERATION, mouseAcceleration);
settings.setValue(SER_STARTWINDOWED, startWindowed);
settings.setValue(SER_FRAMEPACING, framePacing);
settings.setValue(SER_CONNWARNINGS, connectionWarnings);
settings.setValue(SER_RICHPRESENCE, richPresence);
settings.setValue(SER_GAMEPADMOUSE, gamepadMouse);
settings.setValue(SER_PACKETSIZE, packetSize);
settings.setValue(SER_DETECTNETBLOCKING, detectNetworkBlocking);
settings.setValue(SER_AUDIOCFG, static_cast<int>(audioConfig));
settings.setValue(SER_VIDEOCFG, static_cast<int>(videoCodecConfig));
settings.setValue(SER_VIDEODEC, static_cast<int>(videoDecoderSelection));
settings.setValue(SER_WINDOWMODE, static_cast<int>(windowMode));
settings.setValue(SER_UIDISPLAYMODE, static_cast<int>(uiDisplayMode));
settings.setValue(SER_LANGUAGE, static_cast<int>(language));
settings.setValue(SER_DEFAULTVER, CURRENT_DEFAULT_VER);
settings.setValue(SER_SWAPMOUSEBUTTONS, swapMouseButtons);
settings.setValue(SER_MUTEONFOCUSLOSS, muteOnFocusLoss);
settings.setValue(SER_BACKGROUNDGAMEPAD, backgroundGamepad);
settings.setValue(SER_REVERSESCROLL, reverseScrollDirection);
settings.setValue(SER_SWAPFACEBUTTONS, swapFaceButtons);
settings.setValue(SER_CAPTURESYSKEYS, captureSysKeysMode);
}
bool StreamingPreferences::hasAnyHardwareAcceleration()
{
// Always use VDS_AUTO to avoid spamming the user with warnings
// if they've forced software decoding.
return Session::isHardwareDecodeAvailable(VDS_AUTO,
VIDEO_FORMAT_H264,
1920, 1080, 60);
}
bool StreamingPreferences::isRunningWayland()
{
return qgetenv("XDG_SESSION_TYPE") == QByteArray("wayland");
}
bool StreamingPreferences::isWow64()
{
#ifdef Q_OS_WIN32
return QSysInfo::currentCpuArchitecture() != QSysInfo::buildCpuArchitecture();
#else
return false;
#endif
}
QString StreamingPreferences::getUnmappedGamepads()
{
return SdlInputHandler::getUnmappedGamepads();
}
int StreamingPreferences::getMaximumStreamingFrameRate()
{
// Never let the maximum drop below 60 FPS
int maxFrameRate = 60;
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s",
SDL_GetError());
return maxFrameRate;
}
SDL_DisplayMode mode, bestMode, desktopMode;
for (int displayIndex = 0; displayIndex < SDL_GetNumVideoDisplays(); displayIndex++) {
// Get the native desktop resolution
if (StreamUtils::getRealDesktopMode(displayIndex, &desktopMode)) {
// Start at desktop mode and work our way up
bestMode = desktopMode;
for (int i = 0; i < SDL_GetNumDisplayModes(displayIndex); i++) {
if (SDL_GetDisplayMode(displayIndex, i, &mode) == 0) {
if (mode.w == desktopMode.w && mode.h == desktopMode.h) {
if (mode.refresh_rate > bestMode.refresh_rate) {
bestMode = mode;
}
}
}
}
maxFrameRate = qMax(maxFrameRate, bestMode.refresh_rate);
}
}
SDL_QuitSubSystem(SDL_INIT_VIDEO);
return maxFrameRate;
}
QRect StreamingPreferences::getDesktopResolution(int displayIndex)
{
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s",
SDL_GetError());
return QRect();
}
if (displayIndex >= SDL_GetNumVideoDisplays()) {
SDL_QuitSubSystem(SDL_INIT_VIDEO);
return QRect();
}
SDL_DisplayMode mode;
int err = SDL_GetDesktopDisplayMode(displayIndex, &mode);
SDL_QuitSubSystem(SDL_INIT_VIDEO);
if (err == 0) {
return QRect(0, 0, mode.w, mode.h);
}
else {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_GetDesktopDisplayMode() failed: %s",
SDL_GetError());
return QRect();
}
}
QRect StreamingPreferences::getNativeResolution(int displayIndex)
{
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s",
SDL_GetError());
return QRect();
}
if (displayIndex >= SDL_GetNumVideoDisplays()) {
SDL_QuitSubSystem(SDL_INIT_VIDEO);
return QRect();
}
SDL_DisplayMode mode;
bool success = StreamUtils::getRealDesktopMode(displayIndex, &mode);
SDL_QuitSubSystem(SDL_INIT_VIDEO);
if (success) {
return QRect(0, 0, mode.w, mode.h);
}
else {
return QRect();
}
}
int StreamingPreferences::getDefaultBitrate(int width, int height, int fps)
@@ -240,14 +213,8 @@ int StreamingPreferences::getDefaultBitrate(int width, int height, int fps)
// we don't want to bump those 16:10 resolutions up to the
// next 16:9 slot.
if (width * height <= 640 * 360) {
return static_cast<int>(1000 * (fps / 30.0));
}
else if (width * height <= 854 * 480) {
return static_cast<int>(1500 * (fps / 30.0));
}
// This covers 1280x720 and 1280x800 too
else if (width * height <= 1366 * 768) {
if (width * height <= 1366 * 768) {
return static_cast<int>(5000 * (fps / 30.0));
}
else if (width * height <= 1920 * 1200) {
+21 -85
View File
@@ -2,7 +2,6 @@
#include <QObject>
#include <QRect>
#include <QQmlEngine>
class StreamingPreferences : public QObject
{
@@ -10,20 +9,32 @@ class StreamingPreferences : public QObject
public:
StreamingPreferences(QObject *parent = nullptr);
StreamingPreferences(QQmlEngine *qmlEngine, QObject *parent = nullptr);
Q_INVOKABLE static int
getDefaultBitrate(int width, int height, int fps);
Q_INVOKABLE void save();
Q_INVOKABLE static bool hasAnyHardwareAcceleration();
Q_INVOKABLE static bool isRunningWayland();
Q_INVOKABLE static bool isWow64();
Q_INVOKABLE static int getMaximumStreamingFrameRate();
Q_INVOKABLE QRect getDesktopResolution(int displayIndex);
Q_INVOKABLE QRect getNativeResolution(int displayIndex);
Q_INVOKABLE QString getUnmappedGamepads();
void reload();
enum AudioConfig
{
AC_STEREO,
AC_51_SURROUND,
AC_71_SURROUND
AC_51_SURROUND
};
Q_ENUM(AudioConfig)
@@ -52,35 +63,6 @@ public:
};
Q_ENUM(WindowMode)
enum UIDisplayMode
{
UI_WINDOWED,
UI_MAXIMIZED,
UI_FULLSCREEN
};
Q_ENUM(UIDisplayMode)
// New entries must go at the end of the enum
// to avoid renumbering existing entries (which
// would affect existing user preferences).
enum Language
{
LANG_AUTO,
LANG_EN,
LANG_FR,
LANG_ZH_CN,
LANG_DE
};
Q_ENUM(Language);
enum CaptureSysKeysMode
{
CSK_OFF,
CSK_FULLSCREEN,
CSK_ALWAYS,
};
Q_ENUM(CaptureSysKeysMode);
Q_PROPERTY(int width MEMBER width NOTIFY displayModeChanged)
Q_PROPERTY(int height MEMBER height NOTIFY displayModeChanged)
Q_PROPERTY(int fps MEMBER fps NOTIFY displayModeChanged)
@@ -92,28 +74,13 @@ public:
Q_PROPERTY(bool unsupportedFps MEMBER unsupportedFps NOTIFY unsupportedFpsChanged)
Q_PROPERTY(bool enableMdns MEMBER enableMdns NOTIFY enableMdnsChanged)
Q_PROPERTY(bool quitAppAfter MEMBER quitAppAfter NOTIFY quitAppAfterChanged)
Q_PROPERTY(bool absoluteMouseMode MEMBER absoluteMouseMode NOTIFY absoluteMouseModeChanged)
Q_PROPERTY(bool absoluteTouchMode MEMBER absoluteTouchMode NOTIFY absoluteTouchModeChanged)
Q_PROPERTY(bool mouseAcceleration MEMBER mouseAcceleration NOTIFY mouseAccelerationChanged)
Q_PROPERTY(bool startWindowed MEMBER startWindowed NOTIFY startWindowedChanged)
Q_PROPERTY(bool framePacing MEMBER framePacing NOTIFY framePacingChanged)
Q_PROPERTY(bool connectionWarnings MEMBER connectionWarnings NOTIFY connectionWarningsChanged)
Q_PROPERTY(bool richPresence MEMBER richPresence NOTIFY richPresenceChanged)
Q_PROPERTY(bool gamepadMouse MEMBER gamepadMouse NOTIFY gamepadMouseChanged)
Q_PROPERTY(bool detectNetworkBlocking MEMBER detectNetworkBlocking NOTIFY detectNetworkBlockingChanged);
Q_PROPERTY(AudioConfig audioConfig MEMBER audioConfig NOTIFY audioConfigChanged)
Q_PROPERTY(VideoCodecConfig videoCodecConfig MEMBER videoCodecConfig NOTIFY videoCodecConfigChanged)
Q_PROPERTY(VideoDecoderSelection videoDecoderSelection MEMBER videoDecoderSelection NOTIFY videoDecoderSelectionChanged)
Q_PROPERTY(WindowMode windowMode MEMBER windowMode NOTIFY windowModeChanged)
Q_PROPERTY(WindowMode recommendedFullScreenMode MEMBER recommendedFullScreenMode CONSTANT)
Q_PROPERTY(UIDisplayMode uiDisplayMode MEMBER uiDisplayMode NOTIFY uiDisplayModeChanged)
Q_PROPERTY(bool swapMouseButtons MEMBER swapMouseButtons NOTIFY mouseButtonsChanged)
Q_PROPERTY(bool muteOnFocusLoss MEMBER muteOnFocusLoss NOTIFY muteOnFocusLossChanged)
Q_PROPERTY(bool backgroundGamepad MEMBER backgroundGamepad NOTIFY backgroundGamepadChanged)
Q_PROPERTY(bool reverseScrollDirection MEMBER reverseScrollDirection NOTIFY reverseScrollDirectionChanged)
Q_PROPERTY(bool swapFaceButtons MEMBER swapFaceButtons NOTIFY swapFaceButtonsChanged)
Q_PROPERTY(CaptureSysKeysMode captureSysKeysMode MEMBER captureSysKeysMode NOTIFY captureSysKeysModeChanged)
Q_PROPERTY(Language language MEMBER language NOTIFY languageChanged);
Q_INVOKABLE bool retranslate();
// Directly accessible members for preferences
int width;
@@ -127,27 +94,13 @@ public:
bool unsupportedFps;
bool enableMdns;
bool quitAppAfter;
bool absoluteMouseMode;
bool absoluteTouchMode;
bool mouseAcceleration;
bool startWindowed;
bool framePacing;
bool connectionWarnings;
bool richPresence;
bool gamepadMouse;
bool detectNetworkBlocking;
bool swapMouseButtons;
bool muteOnFocusLoss;
bool backgroundGamepad;
bool reverseScrollDirection;
bool swapFaceButtons;
int packetSize;
AudioConfig audioConfig;
VideoCodecConfig videoCodecConfig;
VideoDecoderSelection videoDecoderSelection;
WindowMode windowMode;
WindowMode recommendedFullScreenMode;
UIDisplayMode uiDisplayMode;
Language language;
CaptureSysKeysMode captureSysKeysMode;
signals:
void displayModeChanged();
@@ -159,29 +112,12 @@ signals:
void unsupportedFpsChanged();
void enableMdnsChanged();
void quitAppAfterChanged();
void absoluteMouseModeChanged();
void absoluteTouchModeChanged();
void mouseAccelerationChanged();
void audioConfigChanged();
void videoCodecConfigChanged();
void videoDecoderSelectionChanged();
void uiDisplayModeChanged();
void windowModeChanged();
void startWindowedChanged();
void framePacingChanged();
void connectionWarningsChanged();
void richPresenceChanged();
void gamepadMouseChanged();
void detectNetworkBlockingChanged();
void mouseButtonsChanged();
void muteOnFocusLossChanged();
void backgroundGamepadChanged();
void reverseScrollDirectionChanged();
void swapFaceButtonsChanged();
void captureSysKeysModeChanged();
void languageChanged();
private:
QString getSuffixFromLanguage(Language lang);
QQmlEngine* m_QmlEngine;
};
-21
View File
@@ -1,21 +0,0 @@
#version 300 es
#extension GL_OES_EGL_image_external : require
precision mediump float;
out vec4 FragColor;
in vec2 vTextCoord;
uniform mat3 yuvmat;
uniform vec3 offset;
uniform samplerExternalOES plane1;
uniform samplerExternalOES plane2;
void main() {
vec3 YCbCr = vec3(
texture2D(plane1, vTextCoord)[0],
texture2D(plane2, vTextCoord).xy
);
YCbCr -= offset;
FragColor = vec4(clamp(yuvmat * YCbCr, 0.0, 1.0), 1.0f);
}
-10
View File
@@ -1,10 +0,0 @@
#version 300 es
layout (location = 0) in vec2 aPosition; // 2D: X,Y
layout (location = 1) in vec2 aTexCoord;
out vec2 vTextCoord;
void main() {
vTextCoord = aTexCoord;
gl_Position = vec4(aPosition, 0, 1);
}
-11
View File
@@ -1,11 +0,0 @@
precision mediump float;
uniform sampler2D uTexture;
varying vec2 vTexCoord;
void main() {
vec4 abgr = texture2D(uTexture, vTexCoord);
gl_FragColor = abgr;
gl_FragColor.r = abgr.b;
gl_FragColor.b = abgr.r;
}
-8
View File
@@ -1,8 +0,0 @@
attribute vec2 aPosition; // 2D: X,Y
attribute vec2 aTexCoord;
varying vec2 vTexCoord;
void main() {
vTexCoord = aTexCoord;
gl_Position = vec4(aPosition, 0, 1);
}
+60 -162
View File
@@ -5,112 +5,55 @@
#include "renderers/soundioaudiorenderer.h"
#endif
#ifdef HAVE_SLAUDIO
#include "renderers/slaud.h"
#endif
#include "renderers/sdl.h"
#include <Limelight.h>
#define TRY_INIT_RENDERER(renderer, opusConfig) \
{ \
IAudioRenderer* __renderer = new renderer(); \
if (__renderer->prepareForPlayback(opusConfig)) \
return __renderer; \
delete __renderer; \
}
IAudioRenderer* Session::createAudioRenderer(const POPUS_MULTISTREAM_CONFIGURATION opusConfig)
IAudioRenderer* Session::createAudioRenderer()
{
// Handle explicit ML_AUDIO setting and fail if the requested backend fails
QString mlAudio = qgetenv("ML_AUDIO").toLower();
if (mlAudio == "sdl") {
TRY_INIT_RENDERER(SdlAudioRenderer, opusConfig)
return nullptr;
}
#ifdef HAVE_SOUNDIO
else if (mlAudio == "libsoundio") {
TRY_INIT_RENDERER(SoundIoAudioRenderer, opusConfig)
return nullptr;
}
#endif
#if defined(HAVE_SLAUDIO)
else if (mlAudio == "slaudio") {
TRY_INIT_RENDERER(SLAudioRenderer, opusConfig)
return nullptr;
}
#endif
else if (!mlAudio.isEmpty()) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Unknown audio backend: %s",
SDL_getenv("ML_AUDIO"));
return nullptr;
if (qgetenv("ML_AUDIO") == "SDL") {
return new SdlAudioRenderer();
}
// -------------- Automatic backend selection below this line ---------------
#if defined(HAVE_SLAUDIO)
// Steam Link should always have SLAudio
TRY_INIT_RENDERER(SLAudioRenderer, opusConfig)
#endif
#if !defined(Q_OS_WIN32) && !defined(Q_OS_DARWIN)
// Linux defaults to SDL due to persistent glitching issues under libsoundio.
// Platforms that libsoundio doesn't support also default to SDL.
TRY_INIT_RENDERER(SdlAudioRenderer, opusConfig)
#ifdef HAVE_SOUNDIO
TRY_INIT_RENDERER(SoundIoAudioRenderer, opusConfig)
#endif
return new SoundIoAudioRenderer();
#else
// Windows and macOS default to libsoundio and fall back to SDL
#ifdef HAVE_SOUNDIO
TRY_INIT_RENDERER(SoundIoAudioRenderer, opusConfig)
return new SdlAudioRenderer();
#endif
TRY_INIT_RENDERER(SdlAudioRenderer, opusConfig)
#endif
return nullptr;
}
int Session::getAudioRendererCapabilities(int audioConfiguration)
{
// Build a fake OPUS_MULTISTREAM_CONFIGURATION to give
// the renderer the channel count and sample rate.
OPUS_MULTISTREAM_CONFIGURATION opusConfig = {};
opusConfig.sampleRate = 48000;
opusConfig.samplesPerFrame = 240;
opusConfig.channelCount = CHANNEL_COUNT_FROM_AUDIO_CONFIGURATION(audioConfiguration);
IAudioRenderer* audioRenderer = createAudioRenderer(&opusConfig);
if (audioRenderer == nullptr) {
return 0;
}
int caps = audioRenderer->getCapabilities();
delete audioRenderer;
return caps;
}
bool Session::testAudio(int audioConfiguration)
{
// Build a fake OPUS_MULTISTREAM_CONFIGURATION to give
// the renderer the channel count and sample rate.
OPUS_MULTISTREAM_CONFIGURATION opusConfig = {};
opusConfig.sampleRate = 48000;
opusConfig.samplesPerFrame = 240;
opusConfig.channelCount = CHANNEL_COUNT_FROM_AUDIO_CONFIGURATION(audioConfiguration);
IAudioRenderer* audioRenderer;
IAudioRenderer* audioRenderer = createAudioRenderer(&opusConfig);
audioRenderer = createAudioRenderer();
if (audioRenderer == nullptr) {
return false;
}
// Build a fake OPUS_MULTISTREAM_CONFIGURATION to give
// the renderer the channel count and sample rate.
OPUS_MULTISTREAM_CONFIGURATION opusConfig = {};
opusConfig.sampleRate = 48000;
switch (audioConfiguration)
{
case AUDIO_CONFIGURATION_STEREO:
opusConfig.channelCount = 2;
break;
case AUDIO_CONFIGURATION_51_SURROUND:
opusConfig.channelCount = 6;
break;
default:
SDL_assert(false);
return false;
}
bool ret = audioRenderer->prepareForPlayback(&opusConfig);
delete audioRenderer;
return true;
return ret;
}
int Session::arInit(int /* audioConfiguration */,
@@ -121,34 +64,30 @@ int Session::arInit(int /* audioConfiguration */,
SDL_memcpy(&s_ActiveSession->m_AudioConfig, opusConfig, sizeof(*opusConfig));
s_ActiveSession->m_AudioRenderer = s_ActiveSession->createAudioRenderer(&s_ActiveSession->m_AudioConfig);
if (s_ActiveSession->m_AudioRenderer == nullptr) {
return -2;
}
// Allow the chosen renderer to remap Opus channels as needed to ensure proper output
s_ActiveSession->m_AudioRenderer->remapChannels(&s_ActiveSession->m_AudioConfig);
// Create the Opus decoder with the renderer's preferred channel mapping
s_ActiveSession->m_OpusDecoder =
opus_multistream_decoder_create(s_ActiveSession->m_AudioConfig.sampleRate,
s_ActiveSession->m_AudioConfig.channelCount,
s_ActiveSession->m_AudioConfig.streams,
s_ActiveSession->m_AudioConfig.coupledStreams,
s_ActiveSession->m_AudioConfig.mapping,
opus_multistream_decoder_create(opusConfig->sampleRate,
opusConfig->channelCount,
opusConfig->streams,
opusConfig->coupledStreams,
opusConfig->mapping,
&error);
if (s_ActiveSession->m_OpusDecoder == NULL) {
delete s_ActiveSession->m_AudioRenderer;
s_ActiveSession->m_AudioRenderer = nullptr;
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to create decoder: %d",
error);
return -1;
}
s_ActiveSession->m_AudioRenderer = s_ActiveSession->createAudioRenderer();
if (!s_ActiveSession->m_AudioRenderer->prepareForPlayback(opusConfig)) {
delete s_ActiveSession->m_AudioRenderer;
opus_multistream_decoder_destroy(s_ActiveSession->m_OpusDecoder);
return -2;
}
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Audio stream has %d channels",
s_ActiveSession->m_AudioConfig.channelCount);
opusConfig->channelCount);
return 0;
}
@@ -166,11 +105,8 @@ void Session::arDecodeAndPlaySample(char* sampleData, int sampleLength)
{
int samplesDecoded;
#ifndef STEAM_LINK
// Set this thread to high priority to reduce the chance of missing
// our sample delivery time. On Steam Link, this causes starvation
// of other threads due to severely restricted CPU time available,
// so we will skip it on that platform.
// Set this thread to high priority to reduce
// the chance of missing our sample delivery time
if (s_ActiveSession->m_AudioSampleCount == 0) {
if (SDL_SetThreadPriority(SDL_THREAD_PRIORITY_HIGH) < 0) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
@@ -178,59 +114,29 @@ void Session::arDecodeAndPlaySample(char* sampleData, int sampleLength)
SDL_GetError());
}
}
#endif
// See if we need to drop this sample
if (s_ActiveSession->m_DropAudioEndTime != 0) {
if (SDL_TICKS_PASSED(SDL_GetTicks(), s_ActiveSession->m_DropAudioEndTime)) {
// Avoid calling SDL_GetTicks() now
s_ActiveSession->m_DropAudioEndTime = 0;
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Audio drop window has ended");
}
else {
// We're still in the drop window
return;
}
}
s_ActiveSession->m_AudioSampleCount++;
// If audio is muted, don't decode or play the audio
if (s_ActiveSession->m_AudioMuted) {
return;
}
if (s_ActiveSession->m_AudioRenderer != nullptr) {
int desiredSize = sizeof(short) * s_ActiveSession->m_AudioConfig.samplesPerFrame * s_ActiveSession->m_AudioConfig.channelCount;
void* buffer = s_ActiveSession->m_AudioRenderer->getAudioBuffer(&desiredSize);
if (buffer == nullptr) {
return;
}
samplesDecoded = opus_multistream_decode(s_ActiveSession->m_OpusDecoder,
(unsigned char*)sampleData,
sampleLength,
(short*)buffer,
desiredSize / sizeof(short) / s_ActiveSession->m_AudioConfig.channelCount,
s_ActiveSession->m_OpusDecodeBuffer,
SAMPLES_PER_FRAME,
0);
// Update desiredSize with the number of bytes actually populated by the decoding operation
if (samplesDecoded > 0) {
SDL_assert(desiredSize >= (int)(sizeof(short) * samplesDecoded * s_ActiveSession->m_AudioConfig.channelCount));
desiredSize = sizeof(short) * samplesDecoded * s_ActiveSession->m_AudioConfig.channelCount;
}
else {
desiredSize = 0;
}
if (!s_ActiveSession->m_AudioRenderer->submitAudio(s_ActiveSession->m_OpusDecodeBuffer,
static_cast<int>(
sizeof(short) *
samplesDecoded *
s_ActiveSession->m_AudioConfig.channelCount))) {
if (!s_ActiveSession->m_AudioRenderer->submitAudio(desiredSize)) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Reinitializing audio renderer after failure");
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Reinitializing audio renderer after failure");
delete s_ActiveSession->m_AudioRenderer;
s_ActiveSession->m_AudioRenderer = nullptr;
delete s_ActiveSession->m_AudioRenderer;
s_ActiveSession->m_AudioRenderer = nullptr;
}
}
}
@@ -239,18 +145,10 @@ void Session::arDecodeAndPlaySample(char* sampleData, int sampleLength)
// safe to reinitialize here because we can't be torn down while
// the audio decoder/playback thread is still alive.
if (s_ActiveSession->m_AudioRenderer == nullptr && (s_ActiveSession->m_AudioSampleCount % 200) == 0) {
// Since we're doing this inline and audio initialization takes time, we need
// to drop samples to account for the time we've spent blocking audio rendering
// so we return to real-time playback and don't accumulate latency.
Uint32 audioReinitStartTime = SDL_GetTicks();
s_ActiveSession->m_AudioRenderer = s_ActiveSession->createAudioRenderer(&s_ActiveSession->m_AudioConfig);
Uint32 audioReinitStopTime = SDL_GetTicks();
s_ActiveSession->m_DropAudioEndTime = audioReinitStopTime + (audioReinitStopTime - audioReinitStartTime);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Audio reinitialization took %d ms - starting drop window",
audioReinitStopTime - audioReinitStartTime);
s_ActiveSession->m_AudioRenderer = s_ActiveSession->createAudioRenderer();
if (!s_ActiveSession->m_AudioRenderer->prepareForPlayback(&s_ActiveSession->m_AudioConfig)) {
delete s_ActiveSession->m_AudioRenderer;
s_ActiveSession->m_AudioRenderer = nullptr;
}
}
}
+96
View File
@@ -0,0 +1,96 @@
#include "qtaud.h"
#include <QDebug>
QtAudioRenderer::QtAudioRenderer()
: m_AudioOutput(nullptr),
m_OutputDevice(nullptr)
{
}
QtAudioRenderer::~QtAudioRenderer()
{
delete m_AudioOutput;
}
bool QtAudioRenderer::prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig)
{
QAudioFormat format;
// testAudio() assumes this sample rate
Q_ASSERT(opusConfig->sampleRate == 48000);
format.setSampleRate(opusConfig->sampleRate);
format.setChannelCount(opusConfig->channelCount);
format.setSampleSize(16);
format.setSampleType(QAudioFormat::SignedInt);
format.setCodec("audio/pcm");
m_AudioOutput = new QAudioOutput(format);
// Buffer 40 ms of audio data
m_AudioOutput->setBufferSize(format.bytesForDuration(40000));
m_OutputDevice = m_AudioOutput->start();
if (m_OutputDevice == nullptr) {
qWarning() << "Audio start failed:" << m_AudioOutput->error();
return false;
}
// Check what buffer size was really used after starting
qInfo() << "Audio stream buffer:" << m_AudioOutput->bufferSize() << "bytes";
return true;
}
void QtAudioRenderer::submitAudio(short* audioBuffer, int audioSize)
{
m_OutputDevice->write((const char *)audioBuffer, audioSize);
}
bool QtAudioRenderer::testAudio(int audioConfiguration)
{
QAudioFormat format;
format.setSampleRate(48000);
format.setSampleSize(16);
format.setSampleType(QAudioFormat::SignedInt);
format.setCodec("audio/pcm");
switch (audioConfiguration) {
case AUDIO_CONFIGURATION_STEREO:
format.setChannelCount(2);
break;
case AUDIO_CONFIGURATION_51_SURROUND:
format.setChannelCount(6);
break;
default:
Q_ASSERT(false);
return false;
}
if (QAudioDeviceInfo::defaultOutputDevice().isFormatSupported(format)) {
qInfo() << "Audio test successful with" << format.channelCount() << "channels";
return true;
}
else {
qWarning() << "Audio test failed with" << format.channelCount() << "channels";
return false;
}
}
int QtAudioRenderer::detectAudioConfiguration()
{
int preferredChannelCount = QAudioDeviceInfo::defaultOutputDevice().preferredFormat().channelCount();
qInfo() << "Audio output device prefers" << preferredChannelCount << "channel configuration";
// We can better downmix 5.1 to quad than we can upmix stereo
if (preferredChannelCount > 2) {
return AUDIO_CONFIGURATION_51_SURROUND;
}
else {
return AUDIO_CONFIGURATION_STEREO;
}
}
+24
View File
@@ -0,0 +1,24 @@
#pragma once
#include "renderer.h"
#include <QAudioOutput>
class QtAudioRenderer : public IAudioRenderer
{
public:
QtAudioRenderer();
virtual ~QtAudioRenderer();
virtual bool prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig);
virtual void submitAudio(short* audioBuffer, int audioSize);
virtual bool testAudio(int audioConfiguration);
virtual int detectAudioConfiguration();
private:
QAudioOutput* m_AudioOutput;
QIODevice* m_OutputDevice;
};
+4 -15
View File
@@ -2,6 +2,9 @@
#include <Limelight.h>
#define MAX_CHANNELS 6
#define SAMPLES_PER_FRAME 240
class IAudioRenderer
{
public:
@@ -9,20 +12,6 @@ public:
virtual bool prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig) = 0;
virtual void* getAudioBuffer(int* size) = 0;
// Return false if an unrecoverable error has occurred and the renderer must be reinitialized
virtual bool submitAudio(int bytesWritten) = 0;
virtual int getCapabilities() = 0;
virtual void remapChannels(POPUS_MULTISTREAM_CONFIGURATION) {
// Use default channel mapping:
// 0 - Front Left
// 1 - Front Right
// 2 - Center
// 3 - LFE
// 4 - Surround Left
// 5 - Surround Right
}
virtual bool submitAudio(short* audioBuffer, int audioSize) = 0;
};

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