Compare commits

..
Author SHA1 Message Date
Cameron Gutman 60fadcbf36 Add Qt EGLFS plugin to AppImage 2023-11-07 00:55:21 -06:00
244 changed files with 13095 additions and 33978 deletions
+3 -4
View File
@@ -37,13 +37,12 @@ List the games you've tried that exhibit the issue. To see if the issue is game-
- OS: [e.g. Windows 10 1809] - OS: [e.g. Windows 10 1809]
- Moonlight Version: [e.g. v0.9.0] - Moonlight Version: [e.g. v0.9.0]
- GPU: [e.g. Intel HD Graphics 520] - GPU: [e.g. Intel HD Graphics 520]
- Linux package type (if applicable): [e.g. Flatpak]
**Server PC details (please complete the following information)** **Server PC details (please complete the following information)**
- OS: [e.g. Windows 10 1809] - OS: [e.g. Windows 10 1809]
- Sunshine or GeForce Experience version: [e.g. Sunshine v0.21.0] - GeForce Experience version: [e.g. 3.16.0.140]
- GPU: [e.g. AMD Radeon RX 7900 XT] - Nvidia GPU driver: [e.g. 417.35]
- GPU driver: [e.g. 24.1.1] - Antivirus and firewall software: [e.g. Windows Defender and Windows Firewall]
**Moonlight Logs (please attach)** **Moonlight Logs (please attach)**
- On Windows, `Moonlight-###.log` files can be found in `%TEMP%`. Simply type that into the File Explorer path field to navigate there. - On Windows, `Moonlight-###.log` files can be found in `%TEMP%`. Simply type that into the File Explorer path field to navigate there.
-9
View File
@@ -1,9 +0,0 @@
# Please see the documentation for all configuration options:
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
version: 2
updates:
- package-ecosystem: "gitsubmodule"
directory: "/"
schedule:
interval: "daily"
-165
View File
@@ -1,165 +0,0 @@
---
name: Build - Linux AppImage
permissions:
contents: read
on:
workflow_call:
inputs:
ci_version:
required: true
type: string
jobs:
build:
env:
CI_VERSION: ${{ inputs.ci_version }}
runs-on: ubuntu-22.04
steps:
- name: Checkout Repository
uses: actions/checkout@v5
with:
submodules: 'recursive'
fetch-depth: 1
- name: Setup environment
run: |
wget -qO - https://packages.lunarg.com/lunarg-signing-key-pub.asc | sudo apt-key add -
sudo wget -qO /etc/apt/sources.list.d/lunarg-vulkan-1.4.313-jammy.list https://packages.lunarg.com/vulkan/1.4.313/lunarg-vulkan-1.4.313-jammy.list
sudo apt update
sudo apt install -y qt6-base-dev qt6-declarative-dev libqt6svg6-dev qml6-module-qtquick-controls qml6-module-qtquick-templates qml6-module-qtquick-layouts \
qml6-module-qtqml-workerscript qml6-module-qtquick-window qml6-module-qtquick python3-pip nasm libgbm-dev libdrm-dev libfreetype-dev libasound2-dev \
libdbus-1-dev libegl1-mesa-dev libgl1-mesa-dev libgles2-mesa-dev libglu1-mesa-dev libibus-1.0-dev libpulse-dev libudev-dev libx11-dev libxcursor-dev \
libxext-dev libxi-dev libxinerama-dev libxkbcommon-dev libxrandr-dev libxss-dev libxt-dev libxv-dev libxxf86vm-dev libxcb-dri3-dev libx11-xcb-dev \
libxfixes-dev libxtst-dev wayland-protocols libopus-dev libvdpau-dev libgl-dev libpipewire-0.3-dev liburing-dev vulkan-sdk cmake
sudo pip3 install meson
mkdir -p dep_root/{bin,include,lib}
echo "DEP_ROOT=$PWD/dep_root" >> "${GITHUB_ENV}"
echo "$PWD/dep_root/bin" >> "${GITHUB_PATH}"
- name: Checkout SDL3
uses: actions/checkout@v5
with:
repository: libsdl-org/SDL
ref: release-3.4.12
path: deps/SDL
- name: Build SDL3
working-directory: deps/SDL
run: |
cmake -DSDL_KMSDRM=OFF -DSDL_TEST_LIBRARY=OFF -DSDL_INSTALL_DOCS=OFF -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=ON -S . -B build
cmake --build build -j
sudo cmake --install build
- name: Checkout sdl2-compat
uses: actions/checkout@v5
with:
repository: libsdl-org/sdl2-compat
ref: release-2.32.70
path: deps/sdl2-compat
- name: Build sdl2-compat
working-directory: deps/sdl2-compat
run: |
cmake -DSDL2COMPAT_TESTS=OFF -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=ON -S . -B build
cmake --build build -j
sudo cmake --install build
- name: Checkout SDL_ttf
uses: actions/checkout@v5
with:
repository: libsdl-org/SDL_ttf
ref: a883e490e30fb44a5336ea3dcb990c6982c5216f
path: deps/SDL_ttf
submodules: 'recursive'
- name: Build SDL_ttf
working-directory: deps/SDL_ttf
run: |
./autogen.sh
./configure
make -j$(nproc)
sudo make install
- name: Checkout libva
uses: actions/checkout@v5
with:
repository: intel/libva
ref: 2.24.1
path: deps/libva
- name: Build libva
working-directory: deps/libva
run: |
./autogen.sh
./configure --enable-x11
make -j$(nproc)
sudo make install
- name: Checkout libplacebo
uses: actions/checkout@v5
with:
repository: haasn/libplacebo
ref: 2d0979fb54e025e904c7372666fffbf5dae40f66
path: deps/libplacebo
submodules: 'recursive'
- name: Build libplacebo
working-directory: deps/libplacebo
run: |
meson setup build -Dvulkan=enabled -Dopengl=disabled -Ddemos=false
ninja -C build
sudo ninja install -C build
- name: Build dav1d
working-directory: deps
env:
DAV1D_VER: 1.5.3
run: |
git clone --branch $DAV1D_VER --depth 1 https://code.videolan.org/videolan/dav1d.git
pushd dav1d
meson setup build -Ddefault_library=static -Dbuildtype=release -Denable_tools=false -Denable_tests=false
ninja -C build
sudo ninja install -C build
popd
- name: Checkout FFmpeg
uses: actions/checkout@v5
with:
repository: FFmpeg/FFmpeg
ref: n8.1.1
path: deps/FFmpeg
- name: Build FFmpeg
working-directory: deps/FFmpeg
run: |
./configure --enable-pic --enable-lto --disable-static --enable-shared --disable-all --disable-autodetect \
--enable-avcodec --enable-avformat --enable-swscale \
--enable-decoder=h264 --enable-decoder=hevc --enable-decoder=av1 \
--enable-vaapi --enable-hwaccel=h264_vaapi --enable-hwaccel=hevc_vaapi --enable-hwaccel=av1_vaapi \
--enable-vdpau --enable-hwaccel=h264_vdpau --enable-hwaccel=hevc_vdpau --enable-hwaccel=av1_vdpau \
--enable-libdrm --enable-vulkan --enable-hwaccel=h264_vulkan --enable-hwaccel=hevc_vulkan --enable-hwaccel=av1_vulkan \
--enable-libdav1d --enable-decoder=libdav1d
make -j$(nproc)
sudo make install
- name: Install linuxdeploy
working-directory: dep_root/bin
run: |
wget https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage
wget https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-plugin-qt-x86_64.AppImage
chmod +x linuxdeploy*.AppImage
- name: Build Binaries
run: |
sudo ldconfig
scripts/build-appimage.sh
- name: Upload Binaries
uses: actions/upload-artifact@v6
with:
name: Moonlight-LinuxAppImage-${{ env.CI_VERSION }}
path: build/installer-release/Moonlight-${{ env.CI_VERSION }}-x86_64.AppImage
compression-level: 0
if-no-files-found: error
-41
View File
@@ -1,41 +0,0 @@
---
name: Build - Steam Link
permissions:
contents: read
on:
workflow_call:
inputs:
ci_version:
required: true
type: string
jobs:
build:
env:
CI_VERSION: ${{ inputs.ci_version }}
runs-on: ubuntu-22.04
steps:
- name: Checkout Repository
uses: actions/checkout@v5
with:
submodules: 'recursive'
fetch-depth: 1
- name: Checkout Steam Link SDK
uses: actions/checkout@v5
with:
repository: ValveSoftware/steamlink-sdk
path: steamlink-sdk
fetch-depth: 1
- name: Build binaries
run: STEAMLINK_SDK_PATH=$PWD/steamlink-sdk scripts/build-steamlink-app.sh
- name: Upload Binaries
uses: actions/upload-artifact@v6
with:
name: Moonlight-SteamLink-${{ env.CI_VERSION }}
path: build/deploy-release/*
if-no-files-found: error
-125
View File
@@ -1,125 +0,0 @@
---
name: Build - Windows and macOS
permissions:
contents: read
on:
workflow_call:
inputs:
ci_version:
required: true
type: string
jobs:
build:
env:
CI_VERSION: ${{ inputs.ci_version }}
strategy:
fail-fast: false
matrix:
include:
# Windows
- os: windows-2025
qt_version: 6.11.1
# macOS
- os: macos-26
qt_version: 6.11.1
runs-on: ${{ matrix.os }}
steps:
- name: Checkout Repository
uses: actions/checkout@v5
with:
submodules: 'recursive'
fetch-depth: 1
- name: Download dependencies (Windows)
if: runner.os == 'Windows'
run: powershell ./setup-deps.ps1
- name: Install create-dmg (macOS)
if: runner.os == 'macOS'
run: |
brew install node
npm install --global create-dmg
- name: Install Qt
uses: jurplel/install-qt-action@v4
with:
cache: true
set-env: ${{ runner.os != 'Windows' }}
add-tools-to-path: ${{ runner.os != 'Windows' }}
version: ${{ matrix.qt_version }}
aqtsource: 'git+https://github.com/miurahr/aqtinstall.git@073e34d7c2ab4ae6961ed7cca690b3abd5ba5a7e'
- name: Install Qt ARM64 (Windows)
if: runner.os == 'Windows'
uses: jurplel/install-qt-action@v4
with:
cache: true
set-env: false
add-tools-to-path: false
arch: win64_msvc2022_arm64_cross_compiled
version: ${{ matrix.qt_version }}
- name: Build x64 binaries (Windows)
if: runner.os == 'Windows'
shell: cmd
run: |
set "PATH=%QT_PATH%;%PATH%"
scripts\build-arch.bat Release x64
env:
QT_PATH: ${{ runner.workspace }}\Qt\${{ matrix.qt_version }}\msvc2022_64\bin
- name: Build ARM64 binaries (Windows)
if: runner.os == 'Windows'
shell: cmd
run: |
set "PATH=%QT_PATH%;%PATH%"
scripts\build-arch.bat Release arm64
env:
QT_PATH: ${{ runner.workspace }}\Qt\${{ matrix.qt_version }}\msvc2022_arm64\bin
- name: Build Package
shell: pwsh
run: |
if ($Env:RUNNER_OS -eq "Windows") {
cmd /c scripts\generate-bundle.bat Release
} else {
bash scripts/generate-dmg.sh Release
}
- name: Upload Binaries (Windows x64)
uses: actions/upload-artifact@v6
if: runner.os == 'Windows'
with:
name: Moonlight-${{ runner.os }}-x64-${{ env.CI_VERSION }}
path: build/deploy-x64-release/*
if-no-files-found: error
- name: Upload Binaries (Windows arm64)
uses: actions/upload-artifact@v6
if: runner.os == 'Windows'
with:
name: Moonlight-${{ runner.os }}-arm64-${{ env.CI_VERSION }}
path: build/deploy-arm64-release/*
if-no-files-found: error
- name: Upload Binaries (macOS)
uses: actions/upload-artifact@v6
if: runner.os == 'macOS'
with:
name: Moonlight-${{ runner.os }}-${{ env.CI_VERSION }}
path: build/installer-Release/*.dmg
compression-level: 0
if-no-files-found: error
- name: Upload Symbols
uses: actions/upload-artifact@v6
with:
name: zDevDbgSyms-${{ runner.os }}-${{ env.CI_VERSION }}
path: |
build/installer-*/*.dsym/*
build/symbols-*/*.pdb
if-no-files-found: error
-45
View File
@@ -1,45 +0,0 @@
---
name: Build
permissions:
contents: read
on:
push:
pull_request:
branches:
- master
concurrency:
group: build-${{ github.ref }}
cancel-in-progress: true
jobs:
setup:
runs-on: ubuntu-latest
outputs:
ci_version: ${{ steps.set-ci-version.outputs.ci_version }}
steps:
- name: Set CI version
id: set-ci-version
shell: bash
run: |
CI_VERSION=`echo ${GITHUB_SHA} | cut -c1-6`
echo "ci_version=${CI_VERSION}" >> "${GITHUB_OUTPUT}"
build-appimage:
needs: setup
uses: ./.github/workflows/build-appimage.yml
with:
ci_version: ${{ needs.setup.outputs.ci_version }}
build-steamlink:
needs: setup
uses: ./.github/workflows/build-steamlink.yml
with:
ci_version: ${{ needs.setup.outputs.ci_version }}
build-windows-macos:
needs: setup
uses: ./.github/workflows/build-win-mac.yml
with:
ci_version: ${{ needs.setup.outputs.ci_version }}
-2
View File
@@ -2,8 +2,6 @@
**/.vs/ **/.vs/
.vscode/
build/ build/
libs/
config.tests/*/.qmake.stash config.tests/*/.qmake.stash
config.tests/*/Makefile config.tests/*/Makefile
+7
View File
@@ -7,6 +7,13 @@
[submodule "app/SDL_GameControllerDB"] [submodule "app/SDL_GameControllerDB"]
path = app/SDL_GameControllerDB path = app/SDL_GameControllerDB
url = https://github.com/gabomdq/SDL_GameControllerDB.git url = https://github.com/gabomdq/SDL_GameControllerDB.git
[submodule "soundio/libsoundio"]
path = soundio/libsoundio
url = https://github.com/cgutman/libsoundio.git
[submodule "h264bitstream/h264bitstream"] [submodule "h264bitstream/h264bitstream"]
path = h264bitstream/h264bitstream path = h264bitstream/h264bitstream
url = https://github.com/aizvorski/h264bitstream.git url = https://github.com/aizvorski/h264bitstream.git
[submodule "libs"]
path = libs
url = https://github.com/cgutman/moonlight-qt-prebuilts.git
shallow = true
+11 -22
View File
@@ -158,6 +158,17 @@ private:
L"EZFRD32.dll", L"EZFRD32.dll",
L"EZFRD64.dll", L"EZFRD64.dll",
// These are the newer dList DLLs for Optimus hybrid graphics DDI.
// https://docs.microsoft.com/en-us/windows-hardware/drivers/display/hybrid-system-ddi
//
// We forcefully block them from loading because Optimus has a bug that
// deadlocks DXVA2 when we present with D3DPRESENT_DONOTWAIT. This will prevent
// Optimus from ever using the dGPU even if the user has requested it.
// https://github.com/moonlight-stream/moonlight-qt/issues/240
// https://github.com/moonlight-stream/moonlight-qt/issues/235
L"nvdlist.dll",
L"nvdlistx.dll",
// These are the older dList/AppInit DLLs for Optimus hybrid graphics DDI. // These are the older dList/AppInit DLLs for Optimus hybrid graphics DDI.
// https://docs.microsoft.com/en-us/windows-hardware/drivers/display/hybrid-system-ddi // https://docs.microsoft.com/en-us/windows-hardware/drivers/display/hybrid-system-ddi
// //
@@ -207,28 +218,6 @@ private:
// Since the RTSS OSD doesn't even work with DXVA2, we'll just block the hooks entirely. // Since the RTSS OSD doesn't even work with DXVA2, we'll just block the hooks entirely.
L"RTSSHooks.dll", L"RTSSHooks.dll",
L"RTSSHooks64.dll", L"RTSSHooks64.dll",
// Bandicam's Vulkan layer DLL crashes during destruction of the Vulkan swapchain
// bdcamvk64+0x10b73:
// 00007ffa`be3b0b73 498b5630 mov rdx,qword ptr [r14+30h] ds:00000000`00000030=????????????????
//
// bdcamvk64+0x10b73
// libplacebo_357!vk_sw_destroy+0x14f [D:\a\moonlight-deps\moonlight-deps\libplacebo\src\vulkan\swapchain.c @ 460]
// libplacebo_357!pl_swapchain_destroy+0x1a [D:\a\moonlight-deps\moonlight-deps\libplacebo\src\swapchain.c @ 30]
// Moonlight!PlVkRenderer::~PlVkRenderer+0x159 [D:\a\moonlight-qt\app\streaming\video\ffmpeg-renderers\plvk.cpp @ 140]
// Moonlight!PlVkRenderer::`scalar deleting destructor'+0x17
// Moonlight!FFmpegVideoDecoder::reset+0x203 [D:\a\moonlight-qt\app\streaming\video\ffmpeg.cpp @ 301]
// Moonlight!FFmpegVideoDecoder::~FFmpegVideoDecoder+0x22 [D:\a\moonlight-qt\app\streaming\video\ffmpeg.cpp @ 257]
// Moonlight!FFmpegVideoDecoder::`scalar deleting destructor'+0x17
// Moonlight!Session::getDecoderInfo+0x19a [D:\a\moonlight-qt\app\streaming\session.cpp @ 415]
// Moonlight!SystemProperties::querySdlVideoInfoInternal+0x134 [D:\a\moonlight-qt\app\backend\systemproperties.cpp @ 161]
// Moonlight!SystemProperties::querySdlVideoInfo+0x97 [D:\a\moonlight-qt\app\backend\systemproperties.cpp @ 123]
// Moonlight!SystemProperties::SystemProperties+0x4bf [D:\a\moonlight-qt\app\backend\systemproperties.cpp @ 75]
//
// https://github.com/moonlight-stream/moonlight-qt/issues/1425
// https://github.com/moonlight-stream/moonlight-qt/issues/1579
L"bdcamvk32.dll",
L"bdcamvk64.dll",
}; };
}; };
+16 -45
View File
@@ -6,14 +6,13 @@ Moonlight also has mobile versions for [Android](https://github.com/moonlight-st
You can follow development on our [Discord server](https://moonlight-stream.org/discord) and help translate Moonlight into your language on [Weblate](https://hosted.weblate.org/projects/moonlight/moonlight-qt/). You can follow development on our [Discord server](https://moonlight-stream.org/discord) and help translate Moonlight into your language on [Weblate](https://hosted.weblate.org/projects/moonlight/moonlight-qt/).
[![Build](https://img.shields.io/github/actions/workflow/status/moonlight-stream/moonlight-qt/build.yml?branch=master)](https://github.com/moonlight-stream/moonlight-qt/actions/workflows/build.yml?query=branch%3Amaster) [![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) [![Downloads](https://img.shields.io/github/downloads/moonlight-stream/moonlight-qt/total)](https://github.com/moonlight-stream/moonlight-qt/releases)
[![Translation Status](https://hosted.weblate.org/widgets/moonlight/-/moonlight-qt/svg-badge.svg)](https://hosted.weblate.org/projects/moonlight/moonlight-qt/) [![Translation Status](https://hosted.weblate.org/widgets/moonlight/-/moonlight-qt/svg-badge.svg)](https://hosted.weblate.org/projects/moonlight/moonlight-qt/)
## Features ## Features
- Hardware accelerated video decoding on Windows, Mac, and Linux - Hardware accelerated video decoding on Windows, Mac, and Linux
- H.264, HEVC, and AV1 codec support (AV1 requires Sunshine and a supported host GPU) - H.264, HEVC, and AV1 codec support (AV1 requires Sunshine and a supported host GPU)
- YUV 4:4:4 support (Sunshine only)
- HDR streaming support - HDR streaming support
- 7.1 surround sound audio support - 7.1 surround sound audio support
- 10-point multitouch support (Sunshine only) - 10-point multitouch support (Sunshine only)
@@ -26,14 +25,11 @@ You can follow development on our [Discord server](https://moonlight-stream.org/
- [Snap (for Ubuntu-based Linux distros)](https://snapcraft.io/moonlight) - [Snap (for Ubuntu-based Linux distros)](https://snapcraft.io/moonlight)
- [Flatpak (for other Linux distros)](https://flathub.org/apps/details/com.moonlight_stream.Moonlight) - [Flatpak (for other Linux distros)](https://flathub.org/apps/details/com.moonlight_stream.Moonlight)
- [AppImage](https://github.com/moonlight-stream/moonlight-qt/releases) - [AppImage](https://github.com/moonlight-stream/moonlight-qt/releases)
- [Raspberry Pi 4 and 5](https://github.com/moonlight-stream/moonlight-docs/wiki/Installing-Moonlight-Qt-on-Raspberry-Pi-4) - [Raspberry Pi 4](https://github.com/moonlight-stream/moonlight-docs/wiki/Installing-Moonlight-Qt-on-Raspberry-Pi-4)
- [Generic ARM 32-bit and 64-bit Debian packages](https://github.com/moonlight-stream/moonlight-docs/wiki/Installing-Moonlight-Qt-on-ARM%E2%80%90based-Single-Board-Computers) (not for Raspberry Pi) - [Generic ARM SBC packages for Debian](https://github.com/moonlight-stream/moonlight-docs/wiki/Installing-Moonlight-Qt-on-ARM%E2%80%90based-Single-Board-Computers) (not for Raspberry Pi)
- [Experimental RISC-V Debian packages](https://github.com/moonlight-stream/moonlight-docs/wiki/Installing-Moonlight-Qt-on-RISC%E2%80%90V-Single-Board-Computers) - [Experimental RISC-V SBC packages for Debian](https://github.com/moonlight-stream/moonlight-docs/wiki/Installing-Moonlight-Qt-on-RISC%E2%80%90V-Single-Board-Computers)
- [NVIDIA Jetson and Nintendo Switch (Ubuntu L4T)](https://github.com/moonlight-stream/moonlight-docs/wiki/Installing-Moonlight-Qt-on-Linux4Tegra-(L4T)-Ubuntu) - [NVIDIA Jetson and Nintendo Switch (Ubuntu L4T)](https://github.com/moonlight-stream/moonlight-docs/wiki/Installing-Moonlight-Qt-on-Linux4Tegra-(L4T)-Ubuntu)
### Nightly Builds
- [Downloads](https://nightly.link/moonlight-stream/moonlight-qt/workflows/build/master)
#### Special Thanks #### Special Thanks
[![Hosted By: Cloudsmith](https://img.shields.io/badge/OSS%20hosting%20by-cloudsmith-blue?logo=cloudsmith&style=flat-square)](https://cloudsmith.com) [![Hosted By: Cloudsmith](https://img.shields.io/badge/OSS%20hosting%20by-cloudsmith-blue?logo=cloudsmith&style=flat-square)](https://cloudsmith.com)
@@ -43,68 +39,43 @@ Hosting for Moonlight's Debian and L4T package repositories is graciously provid
## Building ## Building
### Windows Build Requirements ### Windows Build Requirements
* Qt 6.7 SDK or later (earlier versions may work but are not officially supported) * Qt 5.15 SDK or later. Qt 6 is also supported for x64 and ARM64 builds.
* [Visual Studio 2022](https://visualstudio.microsoft.com/downloads/) (Community edition is fine) * [Visual Studio 2022](https://visualstudio.microsoft.com/downloads/) (Community edition is fine)
* Select **MSVC** option during Qt installation. MinGW is not supported. * Select **MSVC** option during Qt installation. MinGW is not supported.
* [7-Zip](https://www.7-zip.org/) (only if building installers for non-development PCs) * [7-Zip](https://www.7-zip.org/) (only if building installers for non-development PCs)
* Graphics Tools (only if running debug builds)
* Install "Graphics Tools" in the Optional Features page of the Windows Settings app.
* Alternatively, run `dism /online /add-capability /capabilityname:Tools.Graphics.DirectX~~~~0.0.1.0` and reboot.
### macOS Build Requirements ### macOS Build Requirements
* Qt 6.7 SDK or later (earlier versions may work but are not officially supported) * Qt 6.4 SDK or later
* Xcode 14 or later (earlier versions may work but are not officially supported) * Xcode 13 or later
* [create-dmg](https://github.com/sindresorhus/create-dmg) (only if building DMGs for use on non-development Macs) * [create-dmg](https://github.com/sindresorhus/create-dmg) (only if building DMGs for use on non-development Macs)
### Linux/Unix Build Requirements ### Linux/Unix Build Requirements
* Qt 6 is recommended, but Qt 5.12 or later is also supported (replace `qmake6` with `qmake` when using Qt 5). * Qt 5.9 SDK or later. Qt 6 is also supported.
* GCC or Clang * GCC or Clang
* FFmpeg 4.0 or later
* Install the required packages: * Install the required packages:
* Debian/Ubuntu: * Debian/Ubuntu: `libegl1-mesa-dev libgl1-mesa-dev libopus-dev libqt5svg5-dev libsdl2-dev libsdl2-ttf-dev libssl-dev libavcodec-dev libva-dev libvdpau-dev libxkbcommon-dev qtwayland5 qt5-qmake qtbase5-dev qtdeclarative5-dev qtquickcontrols2-5-dev wayland-protocols qml-module-qtquick-controls2 qml-module-qtquick-layouts qml-module-qtquick-window2 qml-module-qtquick2`
* Base Requirements: `libegl1-mesa-dev libgl1-mesa-dev libopus-dev libsdl2-dev libsdl2-ttf-dev libssl-dev libavcodec-dev libavformat-dev libswscale-dev libva-dev libvdpau-dev libxkbcommon-dev wayland-protocols libdrm-dev` * RedHat/Fedora: `openssl-devel SDL2-devel SDL2_ttf-devel ffmpeg-devel qt5-qtsvg-devel qt5-qtquickcontrols2-devel libva-devel libvdpau-devel opus-devel pulseaudio-libs-devel alsa-lib-devel`
* Qt 6 (Recommended): `qt6-base-dev qt6-declarative-dev libqt6svg6-dev qt6-wayland qml6-module-qtquick-controls qml6-module-qtquick-templates qml6-module-qtquick-layouts qml6-module-qtqml-workerscript qml6-module-qtquick-window qml6-module-qtquick` * FFmpeg 4.0 or later is required to build. If your distro doesn't package FFmpeg 4.0 or later, you can build and install it from source on https://ffmpeg.org/
* Qt 5: `qtbase5-dev qt5-qmake qtdeclarative5-dev qtquickcontrols2-5-dev qml-module-qtquick-controls2 qml-module-qtquick-layouts qml-module-qtquick-window2 qml-module-qtquick2 qtwayland5`
* RedHat/Fedora (RPM Fusion repo required):
* Base Requirements: `openssl-devel SDL2-devel SDL2_ttf-devel ffmpeg-devel libva-devel libvdpau-devel opus-devel pulseaudio-libs-devel alsa-lib-devel libdrm-devel`
* Qt 6 (Recommended): `qt6-qtsvg-devel qt6-qtdeclarative-devel`
* Qt 5: `qt5-qtsvg-devel qt5-qtquickcontrols2-devel`
* Building the Vulkan renderer requires a `libplacebo-dev`/`libplacebo-devel` version of at least v7.349.0 and FFmpeg 6.1 or later.
### Steam Link Build Requirements ### Steam Link Build Requirements
* [Steam Link SDK](https://github.com/ValveSoftware/steamlink-sdk) cloned on your build system * [Steam Link SDK](https://github.com/ValveSoftware/steamlink-sdk) cloned on your build system
* STEAMLINK_SDK_PATH environment variable set to the Steam Link SDK path * STEAMLINK_SDK_PATH environment variable set to the Steam Link SDK path
**Steam Link Hardware Limitations**
Moonlight builds for Steam Link are subject to hardware limitations of the Steam Link device:
* Maximum resolution: **1080p (1920x1080)**
* Maximum framerate: **60 FPS**
* Maximum video bitrate: **40 Mbps**
* **HDR streaming is not supported** on the original hardware
### Docker containers
If you want to use Docker for building, look at [this repo](https://github.com/cgutman/moonlight-packaging) containing canonical containers
for different architectures, which handle building deps and extra linking for you.
### Build Setup Steps ### Build Setup Steps
1. Install the latest Qt SDK (and optionally, the Qt Creator IDE) from https://www.qt.io/download 1. Install the latest Qt SDK (and optionally, the Qt Creator IDE) from https://www.qt.io/download
* You can install Qt via Homebrew on macOS, but you will need to use `brew install qt --with-debug` to be able to create debug builds of Moonlight. * You can install Qt via Homebrew on macOS, but you will need to use `brew install qt --with-debug` to be able to create debug builds of Moonlight.
* You may also use your Linux distro's package manager for the Qt SDK as long as the packages are Qt 5.12 or later. * You may also use your Linux distro's package manager for the Qt SDK as long as the packages are Qt 5.9 or later.
* This step is not required for building on Steam Link, because the Steam Link SDK includes Qt 5.14. * This step is not required for building on Steam Link, because the Steam Link SDK includes Qt 5.14.
2. Download submodules and dependencies 2. Run `git submodule update --init --recursive` from within `moonlight-qt/`
* Run `git submodule update --init --recursive` from within `moonlight-qt/`.
* On Windows and macOS, you must also run `setup-deps.ps1` (Windows) or `setup-deps.py` (macOS).
* Perform these steps each time you pull new changes from the Git repository.
3. Open the project in Qt Creator or build from qmake on the command line. 3. Open the project in Qt Creator or build from qmake on the command line.
* To build a binary for use on non-development machines, use the scripts in the `scripts` folder. * To build a binary for use on non-development machines, use the scripts in the `scripts` folder.
* For Windows builds, use `scripts\build-arch.bat` and `scripts\generate-bundle.bat`. Execute these scripts from the root of the repository within a Qt command prompt. Ensure 7-Zip binary directory is on your `%PATH%`. * For Windows builds, use `scripts\build-arch.bat` and `scripts\generate-bundle.bat`. Execute these scripts from the root of the repository within a Qt command prompt. Ensure 7-Zip binary directory is on your `%PATH%`.
* For macOS builds, use `scripts/generate-dmg.sh`. Execute this script from the root of the repository and ensure Qt's `bin` folder is in your `$PATH`. * For macOS builds, use `scripts/generate-dmg.sh`. Execute this script from the root of the repository and ensure Qt's `bin` folder is in your `$PATH`.
* For Steam Link builds, run `scripts/build-steamlink-app.sh` from the root of the repository. * For Steam Link builds, run `scripts/build-steamlink-app.sh` from the root of the repository.
* To build from the command line for development use on macOS or Linux, run `qmake6 moonlight-qt.pro` then `make debug` or `make release`. * To build from the command line for development use on macOS or Linux, run `qmake moonlight-qt.pro` then `make debug` or `make release`
* The final binary will be placed in `app/moonlight`. * To create an embedded build for a single-purpose device, use `qmake "CONFIG+=embedded" moonlight-qt.pro` and build normally.
* To create an embedded build for a single-purpose device, use `qmake6 "CONFIG+=embedded" moonlight-qt.pro` and build normally.
* This build will lack windowed mode, Discord/Help links, and other features that don't make sense on an embedded device. * This build will lack windowed mode, Discord/Help links, and other features that don't make sense on an embedded device.
* For platforms with poor GPU performance, add `"CONFIG+=gpuslow"` to prefer direct KMSDRM rendering over GL/Vulkan renderers. Direct KMSDRM rendering can use dedicated YUV/RGB conversion and scaling hardware rather than slower GPU shaders for these operations. * For platforms with poor GL performance, add `"CONFIG+=glslow"` to prefer direct KMSDRM rendering over EGL/GLES renderers. Direct KMSDRM rendering can use dedicated YUV/RGB conversion and scaling hardware rather than slower GPU shaders for these operations.
## Contribute ## Contribute
1. Fork us 1. Fork us
+3 -9
View File
@@ -2,12 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>GCSupportsGameMode</key>
<true/>
<key>CFBundleExecutable</key> <key>CFBundleExecutable</key>
<string>Moonlight</string> <string>Moonlight</string>
<key>LSSupportsGameMode</key>
<true/>
<key>CFBundleGetInfoString</key> <key>CFBundleGetInfoString</key>
<string>Stream games and other applications from another PC</string> <string>Stream games and other applications from another PC</string>
<key>CFBundleIconFile</key> <key>CFBundleIconFile</key>
@@ -21,7 +17,7 @@
<key>CFBundleSignature</key> <key>CFBundleSignature</key>
<string>????</string> <string>????</string>
<key>LSMinimumSystemVersion</key> <key>LSMinimumSystemVersion</key>
<string>11.0.0</string> <string>10.14</string>
<key>NSPrincipalClass</key> <key>NSPrincipalClass</key>
<string>NSApplication</string> <string>NSApplication</string>
<key>NSSupportsAutomaticGraphicsSwitching</key> <key>NSSupportsAutomaticGraphicsSwitching</key>
@@ -35,20 +31,18 @@
</array> </array>
<key>GCSupportsControllerUserInteraction</key> <key>GCSupportsControllerUserInteraction</key>
<true/> <true/>
<key>LSApplicationCategoryType</key>
<string>public.app-category.games</string>
<key>NSAppTransportSecurity</key> <key>NSAppTransportSecurity</key>
<dict> <dict>
<key>NSAllowsArbitraryLoads</key> <key>NSAllowsArbitraryLoads</key>
<true/> <true/>
</dict> </dict>
<key>NSLocalNetworkUsageDescription</key>
<string>Moonlight uses the local network to connect to your gaming PC for streaming.</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>VERSION</string> <string>VERSION</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>VERSION</string> <string>VERSION</string>
<key>CFBundleDisplayName</key> <key>CFBundleDisplayName</key>
<string>Moonlight</string> <string>Moonlight</string>
<key>LSApplicationCategoryType</key>
<string>public.app-category.games</string>
</dict> </dict>
</plist> </plist>
+1 -2
View File
@@ -11,8 +11,7 @@
<asmv3:application> <asmv3:application>
<asmv3:windowsSettings> <asmv3:windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware> <dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">permonitorv2, permonitor</dpiAwareness> <dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">permonitor</dpiAwareness>
<heapType xmlns="http://schemas.microsoft.com/SMI/2020/WindowsSettings">SegmentHeap</heapType>
</asmv3:windowsSettings> </asmv3:windowsSettings>
</asmv3:application> </asmv3:application>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1"> <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
-67
View File
@@ -1,67 +0,0 @@
//
// Compatibility header for older version of SDL.
// Include this instead of SDL.h directly.
//
#pragma once
#include <SDL.h>
// SDL_FRect wasn't added until 2.0.10
#if !SDL_VERSION_ATLEAST(2, 0, 10)
typedef struct SDL_FRect
{
float x;
float y;
float w;
float h;
} SDL_FRect;
#endif
#ifndef SDL_HINT_VIDEO_X11_FORCE_EGL
#define SDL_HINT_VIDEO_X11_FORCE_EGL "SDL_VIDEO_X11_FORCE_EGL"
#endif
#ifndef SDL_HINT_KMSDRM_REQUIRE_DRM_MASTER
#define SDL_HINT_KMSDRM_REQUIRE_DRM_MASTER "SDL_KMSDRM_REQUIRE_DRM_MASTER"
#endif
#ifndef SDL_HINT_ALLOW_ALT_TAB_WHILE_GRABBED
#define SDL_HINT_ALLOW_ALT_TAB_WHILE_GRABBED "SDL_ALLOW_ALT_TAB_WHILE_GRABBED"
#endif
#ifndef SDL_HINT_JOYSTICK_HIDAPI_PS4_RUMBLE
#define SDL_HINT_JOYSTICK_HIDAPI_PS4_RUMBLE "SDL_JOYSTICK_HIDAPI_PS4_RUMBLE"
#endif
#ifndef SDL_HINT_JOYSTICK_HIDAPI_PS5_RUMBLE
#define SDL_HINT_JOYSTICK_HIDAPI_PS5_RUMBLE "SDL_JOYSTICK_HIDAPI_PS5_RUMBLE"
#endif
#ifndef SDL_HINT_WINDOWS_USE_D3D9EX
#define SDL_HINT_WINDOWS_USE_D3D9EX "SDL_WINDOWS_USE_D3D9EX"
#endif
#ifndef SDL_HINT_GAMECONTROLLER_USE_BUTTON_LABELS
#define SDL_HINT_GAMECONTROLLER_USE_BUTTON_LABELS "SDL_GAMECONTROLLER_USE_BUTTON_LABELS"
#endif
#ifndef SDL_HINT_MOUSE_RELATIVE_SCALING
#define SDL_HINT_MOUSE_RELATIVE_SCALING "SDL_MOUSE_RELATIVE_SCALING"
#endif
#ifndef SDL_HINT_AUDIO_DEVICE_APP_NAME
#define SDL_HINT_AUDIO_DEVICE_APP_NAME "SDL_AUDIO_DEVICE_APP_NAME"
#endif
#ifndef SDL_HINT_APP_NAME
#define SDL_HINT_APP_NAME "SDL_APP_NAME"
#endif
#ifndef SDL_HINT_MOUSE_AUTO_CAPTURE
#define SDL_HINT_MOUSE_AUTO_CAPTURE "SDL_MOUSE_AUTO_CAPTURE"
#endif
#ifndef SDL_HINT_VIDEO_WAYLAND_EMULATE_MOUSE_WARP
#define SDL_HINT_VIDEO_WAYLAND_EMULATE_MOUSE_WARP "SDL_VIDEO_WAYLAND_EMULATE_MOUSE_WARP"
#endif
+63 -109
View File
@@ -1,5 +1,5 @@
QT += core quick network quickcontrols2 svg QT += core quick network quickcontrols2 svg
CONFIG += c++17 CONFIG += c++11
unix:!macx { unix:!macx {
TARGET = moonlight TARGET = moonlight
@@ -12,13 +12,10 @@ include(../globaldefs.pri)
# Precompile QML files to avoid writing qmlcache on portable versions. # Precompile QML files to avoid writing qmlcache on portable versions.
# Since this binds the app against the Qt runtime version, we will only # Since this binds the app against the Qt runtime version, we will only
# do this for Windows and Mac (when disable-prebuilts is not defined), # do this for Windows and Mac, since they ship with the Qt runtime.
# since they always ship with the matching build of the Qt runtime. win32|macx {
!disable-prebuilts { CONFIG(release, debug|release) {
win32|macx { CONFIG += qtquickcompiler
CONFIG(release, debug|release) {
CONFIG += qtquickcompiler
}
} }
} }
@@ -36,43 +33,44 @@ DEFINES += QT_DEPRECATED_WARNINGS
DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
win32 { win32 {
!exists($$PWD/../libs/windows) { contains(QT_ARCH, i386) {
error("Missing dependencies. Please run 'powershell .\setup-deps.ps1' to fetch prebuilt libraries.") LIBS += -L$$PWD/../libs/windows/lib/x86
INCLUDEPATH += $$PWD/../libs/windows/include/x86
} }
contains(QT_ARCH, x86_64) { contains(QT_ARCH, x86_64) {
LIBS += -L$$PWD/../libs/windows/lib/x64 LIBS += -L$$PWD/../libs/windows/lib/x64
INCLUDEPATH += $$PWD/../libs/windows/include/x64 $$PWD/../libs/windows/include/x64/SDL2 INCLUDEPATH += $$PWD/../libs/windows/include/x64
} }
contains(QT_ARCH, arm64) { contains(QT_ARCH, arm64) {
LIBS += -L$$PWD/../libs/windows/lib/arm64 LIBS += -L$$PWD/../libs/windows/lib/arm64
INCLUDEPATH += $$PWD/../libs/windows/include/arm64 $$PWD/../libs/windows/include/arm64/SDL2 INCLUDEPATH += $$PWD/../libs/windows/include/arm64
} }
INCLUDEPATH += $$PWD/../libs/windows/include INCLUDEPATH += $$PWD/../libs/windows/include
LIBS += ws2_32.lib winmm.lib dxva2.lib ole32.lib gdi32.lib user32.lib d3d9.lib dwmapi.lib dbghelp.lib LIBS += ws2_32.lib winmm.lib dxva2.lib ole32.lib gdi32.lib user32.lib d3d9.lib dwmapi.lib dbghelp.lib
}
macx:!disable-prebuilts {
!exists($$PWD/../libs/mac) {
error("Missing dependencies. Please run 'python3 setup-deps.py' to fetch prebuilt libraries.")
}
INCLUDEPATH += $$PWD/../libs/mac/include $$PWD/../libs/mac/include/SDL2 # Work around a conflict with math.h inclusion between SDL and Qt 6
LIBS += -L$$PWD/../libs/mac/lib DEFINES += _USE_MATH_DEFINES
}
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
LIBS += -L$$PWD/../libs/mac/lib -F$$PWD/../libs/mac/Frameworks
# QMake doesn't handle framework-style includes correctly on its own
QMAKE_CFLAGS += -F$$PWD/../libs/mac/Frameworks
QMAKE_CXXFLAGS += -F$$PWD/../libs/mac/Frameworks
QMAKE_OBJECTIVE_CFLAGS += -F$$PWD/../libs/mac/Frameworks
} }
unix:if(!macx|disable-prebuilts) { unix:!macx {
CONFIG += link_pkgconfig CONFIG += link_pkgconfig
PKGCONFIG += openssl sdl2 SDL2_ttf PKGCONFIG += openssl sdl2 SDL2_ttf opus
# We have our own optimized libopus.a for Steam Link
if(!config_SL|disable-prebuilts) {
PKGCONFIG += opus
}
!disable-ffmpeg { !disable-ffmpeg {
packagesExist(libavcodec) { packagesExist(libavcodec) {
PKGCONFIG += libavcodec libavutil libswscale PKGCONFIG += libavcodec libavutil
CONFIG += ffmpeg CONFIG += ffmpeg
!disable-libva { !disable-libva {
@@ -116,21 +114,12 @@ unix:if(!macx|disable-prebuilts) {
} }
} }
# Disabled by default due to reliability issues. See #1314. !disable-cuda {
# CUDA interop is superseded by VDPAU and Vulkan Video.
enable-cuda {
packagesExist(ffnvcodec) { packagesExist(ffnvcodec) {
PKGCONFIG += ffnvcodec PKGCONFIG += ffnvcodec
CONFIG += cuda CONFIG += cuda
} }
} }
!disable-libplacebo {
packagesExist(libplacebo) {
PKGCONFIG += libplacebo
CONFIG += libplacebo
}
}
} }
!disable-wayland { !disable-wayland {
@@ -149,20 +138,20 @@ unix:if(!macx|disable-prebuilts) {
} }
} }
win32 { win32 {
LIBS += -llibssl -llibcrypto -lSDL2 -lSDL2_ttf -lavcodec -lavutil -lswscale -lopus -ldxgi -ld3d11 -llibplacebo LIBS += -llibssl -llibcrypto -lSDL2 -lSDL2_ttf -lavcodec -lavutil -lopus -ldxgi -ld3d11
CONFIG += ffmpeg libplacebo CONFIG += ffmpeg
} }
win32:!winrt { win32:!winrt {
CONFIG += discord-rpc CONFIG += soundio discord-rpc
} }
macx { macx {
!disable-prebuilts { LIBS += -lssl -lcrypto -lavcodec.60 -lavutil.58 -lopus -framework SDL2 -framework SDL2_ttf
LIBS += -lssl.3 -lcrypto.3 -lavcodec.62 -lavutil.60 -lswscale.9 -lopus.0 -lSDL2 -lSDL2_ttf -lplacebo LIBS += -lobjc -framework VideoToolbox -framework AVFoundation -framework CoreVideo -framework CoreGraphics -framework CoreMedia -framework AppKit -framework Metal
CONFIG += discord-rpc libplacebo
}
LIBS += -lobjc -framework VideoToolbox -framework AVFoundation -framework CoreVideo -framework CoreGraphics -framework CoreMedia -framework AppKit -framework Metal -framework QuartzCore # For libsoundio
CONFIG += ffmpeg LIBS += -framework CoreAudio -framework AudioUnit
CONFIG += ffmpeg soundio discord-rpc
} }
SOURCES += \ SOURCES += \
@@ -196,7 +185,6 @@ SOURCES += \
streaming/audio/renderers/sdlaud.cpp \ streaming/audio/renderers/sdlaud.cpp \
gui/computermodel.cpp \ gui/computermodel.cpp \
gui/appmodel.cpp \ gui/appmodel.cpp \
streaming/bandwidth.cpp \
streaming/streamutils.cpp \ streaming/streamutils.cpp \
backend/autoupdatechecker.cpp \ backend/autoupdatechecker.cpp \
path.cpp \ path.cpp \
@@ -207,7 +195,6 @@ SOURCES += \
wm.cpp wm.cpp
HEADERS += \ HEADERS += \
SDL_compat.h \
backend/nvaddress.h \ backend/nvaddress.h \
backend/nvapp.h \ backend/nvapp.h \
cli/pair.h \ cli/pair.h \
@@ -234,7 +221,6 @@ HEADERS += \
gui/computermodel.h \ gui/computermodel.h \
gui/appmodel.h \ gui/appmodel.h \
streaming/video/decoder.h \ streaming/video/decoder.h \
streaming/bandwidth.h \
streaming/streamutils.h \ streaming/streamutils.h \
backend/autoupdatechecker.h \ backend/autoupdatechecker.h \
path.h \ path.h \
@@ -250,7 +236,6 @@ ffmpeg {
DEFINES += HAVE_FFMPEG DEFINES += HAVE_FFMPEG
SOURCES += \ SOURCES += \
streaming/video/ffmpeg.cpp \ streaming/video/ffmpeg.cpp \
streaming/video/ffmpeg-renderers/genhwaccel.cpp \
streaming/video/ffmpeg-renderers/sdlvid.cpp \ streaming/video/ffmpeg-renderers/sdlvid.cpp \
streaming/video/ffmpeg-renderers/swframemapper.cpp \ streaming/video/ffmpeg-renderers/swframemapper.cpp \
streaming/video/ffmpeg-renderers/pacer/pacer.cpp streaming/video/ffmpeg-renderers/pacer/pacer.cpp
@@ -258,7 +243,6 @@ ffmpeg {
HEADERS += \ HEADERS += \
streaming/video/ffmpeg.h \ streaming/video/ffmpeg.h \
streaming/video/ffmpeg-renderers/renderer.h \ streaming/video/ffmpeg-renderers/renderer.h \
streaming/video/ffmpeg-renderers/genhwaccel.h \
streaming/video/ffmpeg-renderers/sdlvid.h \ streaming/video/ffmpeg-renderers/sdlvid.h \
streaming/video/ffmpeg-renderers/swframemapper.h \ streaming/video/ffmpeg-renderers/swframemapper.h \
streaming/video/ffmpeg-renderers/pacer/pacer.h streaming/video/ffmpeg-renderers/pacer/pacer.h
@@ -307,7 +291,7 @@ mmal {
# significantly better performance than EGL on the Pi. Setting # significantly better performance than EGL on the Pi. Setting
# this option allows EGL usage even if built with MMAL support. # this option allows EGL usage even if built with MMAL support.
# #
# It is highly recommended to also build with 'gpuslow' to avoid # It is highly recommended to also build with 'glslow' to avoid
# EGL being preferred if direct DRM rendering is available. # EGL being preferred if direct DRM rendering is available.
allow-egl-with-mmal { allow-egl-with-mmal {
message(Allowing EGL usage with MMAL enabled) message(Allowing EGL usage with MMAL enabled)
@@ -323,12 +307,9 @@ libdrm {
HEADERS += streaming/video/ffmpeg-renderers/drm.h HEADERS += streaming/video/ffmpeg-renderers/drm.h
linux { linux {
!disable-masterhooks { message(Master hooks enabled)
message(Master hooks enabled) SOURCES += masterhook.c masterhook_internal.c
DEFINES += HAVE_DRM_MASTER_HOOKS LIBS += -ldl
SOURCES += masterhook.c masterhook_internal.c
LIBS += -ldl -pthread
}
} }
} }
cuda { cuda {
@@ -341,20 +322,6 @@ cuda {
# ffnvcodec uses libdl in cuda_load_functions()/cuda_free_functions() # ffnvcodec uses libdl in cuda_load_functions()/cuda_free_functions()
LIBS += -ldl LIBS += -ldl
} }
libplacebo {
message(Vulkan support enabled via libplacebo)
DEFINES += HAVE_LIBPLACEBO_VULKAN
SOURCES += \
streaming/video/ffmpeg-renderers/plvk.cpp \
streaming/video/ffmpeg-renderers/plvk_c.c
HEADERS += \
streaming/video/ffmpeg-renderers/plvk.h
macx {
SOURCES += streaming/video/ffmpeg-renderers/plvk_objc.mm
}
}
config_EGL { config_EGL {
message(EGL renderer selected) message(EGL renderer selected)
@@ -371,13 +338,6 @@ config_EGL {
config_SL { config_SL {
message(Steam Link build configuration selected) message(Steam Link build configuration selected)
!disable-prebuilts {
# Link against our NEON-optimized libopus build
LIBS += -L$$PWD/../libs/steamlink/lib
INCLUDEPATH += $$PWD/../libs/steamlink/include
LIBS += -lopus -larmasm -lNE10
}
DEFINES += EMBEDDED_BUILD STEAM_LINK HAVE_SLVIDEO HAVE_SLAUDIO DEFINES += EMBEDDED_BUILD STEAM_LINK HAVE_SLVIDEO HAVE_SLAUDIO
LIBS += -lSLVideo -lSLAudio LIBS += -lSLVideo -lSLAudio
@@ -408,13 +368,18 @@ macx {
message(VideoToolbox renderer selected) message(VideoToolbox renderer selected)
SOURCES += \ SOURCES += \
streaming/video/ffmpeg-renderers/vt_base.mm \ streaming/video/ffmpeg-renderers/vt.mm
streaming/video/ffmpeg-renderers/vt_avsamplelayer.mm \
streaming/video/ffmpeg-renderers/vt_metal.mm
HEADERS += \ HEADERS += \
streaming/video/ffmpeg-renderers/vt.h streaming/video/ffmpeg-renderers/vt.h
} }
soundio {
message(libsoundio audio renderer selected)
DEFINES += HAVE_SOUNDIO SOUNDIO_STATIC_LIBRARY
SOURCES += streaming/audio/renderers/soundioaudiorenderer.cpp
HEADERS += streaming/audio/renderers/soundioaudiorenderer.h
}
discord-rpc { discord-rpc {
message(Discord integration enabled) message(Discord integration enabled)
@@ -431,16 +396,6 @@ glslow {
DEFINES += GL_IS_SLOW DEFINES += GL_IS_SLOW
} }
vkslow {
message(Vulkan slow build)
DEFINES += VULKAN_IS_SLOW
}
gpuslow {
message(GPU slow build)
DEFINES += GL_IS_SLOW VULKAN_IS_SLOW
}
wayland { wayland {
message(Wayland extensions enabled) message(Wayland extensions enabled)
@@ -477,13 +432,7 @@ TRANSLATIONS += \
languages/qml_pt_BR.ts \ languages/qml_pt_BR.ts \
languages/qml_pl.ts \ languages/qml_pl.ts \
languages/qml_cs.ts \ languages/qml_cs.ts \
languages/qml_he.ts \ languages/qml_he.ts
languages/qml_ckb.ts \
languages/qml_lt.ts \
languages/qml_et.ts \
languages/qml_bg.ts \
languages/qml_eo.ts \
languages/qml_ta.ts
# Additional import path used to resolve QML modules in Qt Creator's code model # Additional import path used to resolve QML modules in Qt Creator's code model
QML_IMPORT_PATH = QML_IMPORT_PATH =
@@ -505,6 +454,15 @@ else:unix: LIBS += -L$$OUT_PWD/../qmdnsengine/ -lqmdnsengine
INCLUDEPATH += $$PWD/../qmdnsengine/qmdnsengine/src/include $$PWD/../qmdnsengine INCLUDEPATH += $$PWD/../qmdnsengine/qmdnsengine/src/include $$PWD/../qmdnsengine
DEPENDPATH += $$PWD/../qmdnsengine/qmdnsengine/src/include $$PWD/../qmdnsengine DEPENDPATH += $$PWD/../qmdnsengine/qmdnsengine/src/include $$PWD/../qmdnsengine
soundio {
win32:CONFIG(release, debug|release): LIBS += -L$$OUT_PWD/../soundio/release/ -lsoundio
else:win32:CONFIG(debug, debug|release): LIBS += -L$$OUT_PWD/../soundio/debug/ -lsoundio
else:unix: LIBS += -L$$OUT_PWD/../soundio/ -lsoundio
INCLUDEPATH += $$PWD/../soundio/libsoundio
DEPENDPATH += $$PWD/../soundio/libsoundio
}
win32:CONFIG(release, debug|release): LIBS += -L$$OUT_PWD/../h264bitstream/release/ -lh264bitstream win32:CONFIG(release, debug|release): LIBS += -L$$OUT_PWD/../h264bitstream/release/ -lh264bitstream
else:win32:CONFIG(debug, debug|release): LIBS += -L$$OUT_PWD/../h264bitstream/debug/ -lh264bitstream else:win32:CONFIG(debug, debug|release): LIBS += -L$$OUT_PWD/../h264bitstream/debug/ -lh264bitstream
else:unix: LIBS += -L$$OUT_PWD/../h264bitstream/ -lh264bitstream else:unix: LIBS += -L$$OUT_PWD/../h264bitstream/ -lh264bitstream
@@ -563,19 +521,15 @@ macx {
APP_BUNDLE_RESOURCES.files = moonlight.icns APP_BUNDLE_RESOURCES.files = moonlight.icns
APP_BUNDLE_RESOURCES.path = Contents/Resources APP_BUNDLE_RESOURCES.path = Contents/Resources
APP_BUNDLE_FRAMEWORKS.files = $$files(../libs/mac/Frameworks/*.framework, true) $$files(../libs/mac/lib/*.dylib, true)
APP_BUNDLE_FRAMEWORKS.path = Contents/Frameworks
APP_BUNDLE_PLIST.files = $$OUT_PWD/Info.plist APP_BUNDLE_PLIST.files = $$OUT_PWD/Info.plist
APP_BUNDLE_PLIST.path = Contents APP_BUNDLE_PLIST.path = Contents
QMAKE_BUNDLE_DATA += APP_BUNDLE_RESOURCES APP_BUNDLE_PLIST QMAKE_BUNDLE_DATA += APP_BUNDLE_RESOURCES APP_BUNDLE_FRAMEWORKS APP_BUNDLE_PLIST
!disable-prebuilts { QMAKE_RPATHDIR += @executable_path/../Frameworks
APP_BUNDLE_FRAMEWORKS.files = $$files(../libs/mac/Frameworks/*.framework, true) $$files(../libs/mac/lib/*.dylib, true)
APP_BUNDLE_FRAMEWORKS.path = Contents/Frameworks
QMAKE_BUNDLE_DATA += APP_BUNDLE_FRAMEWORKS
QMAKE_RPATHDIR += @executable_path/../Frameworks
}
} }
VERSION = "$$cat(version.txt)" VERSION = "$$cat(version.txt)"
+2 -2
View File
@@ -58,7 +58,7 @@ void AutoUpdateChecker::start()
void AutoUpdateChecker::parseStringToVersionQuad(QString& string, QVector<int>& version) void AutoUpdateChecker::parseStringToVersionQuad(QString& string, QVector<int>& version)
{ {
QStringList list = string.split('.'); QStringList list = string.split('.');
for (const QString& component : std::as_const(list)) { for (const QString& component : list) {
version.append(component.toInt()); version.append(component.toInt());
} }
} }
@@ -139,7 +139,7 @@ void AutoUpdateChecker::handleUpdateCheckRequestFinished(QNetworkReply* reply)
return; return;
} }
for (const auto& updateEntry : std::as_const(array)) { for (QJsonValueRef updateEntry : array) {
if (updateEntry.isObject()) { if (updateEntry.isObject()) {
QJsonObject updateObj = updateEntry.toObject(); QJsonObject updateObj = updateEntry.toObject();
if (!updateObj.contains("platform") || if (!updateObj.contains("platform") ||
+40 -69
View File
@@ -1,7 +1,7 @@
#include "computermanager.h" #include "computermanager.h"
#include "boxartmanager.h" #include "boxartmanager.h"
#include "nvhttp.h" #include "nvhttp.h"
#include "nvpairingmanager.h" #include "settings/streamingpreferences.h"
#include <Limelight.h> #include <Limelight.h>
#include <QtEndian> #include <QtEndian>
@@ -9,7 +9,8 @@
#include <QThread> #include <QThread>
#include <QThreadPool> #include <QThreadPool>
#include <QCoreApplication> #include <QCoreApplication>
#include <QRandomGenerator>
#include <random>
#define SER_HOSTS "hosts" #define SER_HOSTS "hosts"
#define SER_HOSTS_BACKUP "hostsbackup" #define SER_HOSTS_BACKUP "hostsbackup"
@@ -29,9 +30,9 @@ public:
} }
private: private:
bool tryPollComputer(QNetworkAccessManager* nam, NvAddress address, bool& changed) bool tryPollComputer(NvAddress address, bool& changed)
{ {
NvHTTP http(address, 0, m_Computer->serverCert, !m_Computer->isNvidiaServerSoftware, nam); NvHTTP http(address, 0, m_Computer->serverCert);
QString serverInfo; QString serverInfo;
try { try {
@@ -52,9 +53,9 @@ private:
return true; return true;
} }
bool updateAppList(QNetworkAccessManager* nam, bool& changed) bool updateAppList(bool& changed)
{ {
NvHTTP http(m_Computer, nam); NvHTTP http(m_Computer);
QVector<NvApp> appList; QVector<NvApp> appList;
@@ -74,21 +75,6 @@ private:
void run() override void run() override
{ {
// Reduce the power and performance impact of our
// computer status polling while it's running.
setPriority(QThread::LowPriority);
#if QT_VERSION >= QT_VERSION_CHECK(6, 9, 0)
setServiceLevel(QThread::QualityOfService::Eco);
#endif
// Share the QNetworkAccessManager to conserve resources when polling.
// Each instance creates a worker thread, so sharing them ensures that
// we are not spamming a new thread for every single polling attempt.
//
// Since QThread inherit the priority of the current thread, this also
// ensures that the NAM's worker thread will inherit our lower priority.
QNetworkAccessManager nam;
// Always fetch the applist the first time // Always fetch the applist the first time
int pollsSinceLastAppListFetch = POLLS_PER_APPLIST_FETCH; int pollsSinceLastAppListFetch = POLLS_PER_APPLIST_FETCH;
while (!isInterruptionRequested()) { while (!isInterruptionRequested()) {
@@ -101,7 +87,7 @@ private:
return; return;
} }
if (tryPollComputer(&nam, address, stateChanged)) { if (tryPollComputer(address, stateChanged)) {
if (!wasOnline) { if (!wasOnline) {
qInfo() << m_Computer->name << "is now online at" << m_Computer->activeAddress.toString(); qInfo() << m_Computer->name << "is now online at" << m_Computer->activeAddress.toString();
} }
@@ -132,7 +118,7 @@ private:
stateChanged = false; stateChanged = false;
} }
if (updateAppList(&nam, stateChanged)) { if (updateAppList(stateChanged)) {
pollsSinceLastAppListFetch = 0; pollsSinceLastAppListFetch = 0;
} }
} }
@@ -158,8 +144,8 @@ private:
NvComputer* m_Computer; NvComputer* m_Computer;
}; };
ComputerManager::ComputerManager(StreamingPreferences* prefs) ComputerManager::ComputerManager(QObject *parent)
: m_Prefs(prefs), : QObject(parent),
m_PollingRef(0), m_PollingRef(0),
m_MdnsBrowser(nullptr), m_MdnsBrowser(nullptr),
m_CompatFetcher(nullptr), m_CompatFetcher(nullptr),
@@ -231,17 +217,17 @@ ComputerManager::~ComputerManager()
m_MdnsBrowser = nullptr; m_MdnsBrowser = nullptr;
// Interrupt polling // Interrupt polling
for (ComputerPollingEntry* entry : std::as_const(m_PollEntries)) { for (ComputerPollingEntry* entry : m_PollEntries) {
entry->interrupt(); entry->interrupt();
} }
// Delete all polling entries (and associated threads) // Delete all polling entries (and associated threads)
for (ComputerPollingEntry* entry : std::as_const(m_PollEntries)) { for (ComputerPollingEntry* entry : m_PollEntries) {
delete entry; delete entry;
} }
// Destroy all NvComputer objects now that polling is halted // Destroy all NvComputer objects now that polling is halted
for (NvComputer* computer : std::as_const(m_KnownHosts)) { for (NvComputer* computer : m_KnownHosts) {
delete computer; delete computer;
} }
} }
@@ -268,7 +254,7 @@ void DelayedFlushThread::run() {
// Update the last serialized hosts map under the delayed flush mutex // Update the last serialized hosts map under the delayed flush mutex
m_ComputerManager->m_LastSerializedHosts.clear(); m_ComputerManager->m_LastSerializedHosts.clear();
for (const NvComputer* computer : std::as_const(m_ComputerManager->m_KnownHosts)) { for (const NvComputer* computer : m_ComputerManager->m_KnownHosts) {
// Copy the current state of the NvComputer to allow us to check later if we need // Copy the current state of the NvComputer to allow us to check later if we need
// to serialize it again when attribute updates occur. // to serialize it again when attribute updates occur.
QReadLocker computerLock(&computer->lock); QReadLocker computerLock(&computer->lock);
@@ -285,7 +271,7 @@ void DelayedFlushThread::run() {
{ {
QReadLocker lock(&m_ComputerManager->m_Lock); QReadLocker lock(&m_ComputerManager->m_Lock);
int i = 0; int i = 0;
for (const NvComputer* computer : std::as_const(m_ComputerManager->m_KnownHosts)) { for (const NvComputer* computer : m_ComputerManager->m_KnownHosts) {
settings.setArrayIndex(i++); settings.setArrayIndex(i++);
computer->serialize(settings, false); computer->serialize(settings, false);
} }
@@ -298,7 +284,7 @@ void DelayedFlushThread::run() {
{ {
QReadLocker lock(&m_ComputerManager->m_Lock); QReadLocker lock(&m_ComputerManager->m_Lock);
int i = 0; int i = 0;
for (const NvComputer* computer : std::as_const(m_ComputerManager->m_KnownHosts)) { for (const NvComputer* computer : m_ComputerManager->m_KnownHosts) {
settings.setArrayIndex(i++); settings.setArrayIndex(i++);
computer->serialize(settings, true); computer->serialize(settings, true);
} }
@@ -366,7 +352,9 @@ void ComputerManager::startPolling()
return; return;
} }
if (m_Prefs->enableMdns) { StreamingPreferences prefs;
if (prefs.enableMdns) {
// Start an MDNS query for GameStream hosts // Start an MDNS query for GameStream hosts
m_MdnsServer.reset(new QMdnsEngine::Server()); m_MdnsServer.reset(new QMdnsEngine::Server());
m_MdnsBrowser = new QMdnsEngine::Browser(m_MdnsServer.data(), "_nvstream._tcp.local."); m_MdnsBrowser = new QMdnsEngine::Browser(m_MdnsServer.data(), "_nvstream._tcp.local.");
@@ -424,15 +412,13 @@ void ComputerManager::handleMdnsServiceResolved(MdnsPendingComputer* computer,
bool added = false; bool added = false;
// Add the host using the IPv4 address // Add the host using the IPv4 address
for (const QHostAddress& address : std::as_const(addresses)) { for (const QHostAddress& address : addresses) {
if (address.protocol() == QAbstractSocket::IPv4Protocol) { if (address.protocol() == QAbstractSocket::IPv4Protocol) {
// NB: We don't just call addNewHost() here with v6Global because the IPv6 // 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 // 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 // or if this host lacks outbound IPv6 capability). We want to add IPv6 even if
// it's not currently reachable. // it's not currently reachable.
addNewHost(NvAddress(address, computer->port()), addNewHost(NvAddress(address, computer->port()), true, NvAddress(v6Global, computer->port()));
true, computer->hostname(),
NvAddress(v6Global, computer->port()));
added = true; added = true;
break; break;
} }
@@ -440,15 +426,13 @@ void ComputerManager::handleMdnsServiceResolved(MdnsPendingComputer* computer,
if (!added) { if (!added) {
// If we get here, there wasn't an IPv4 address so we'll do it v6-only // If we get here, there wasn't an IPv4 address so we'll do it v6-only
for (const QHostAddress& address : std::as_const(addresses)) { for (const QHostAddress& address : addresses) {
if (address.protocol() == QAbstractSocket::IPv6Protocol) { if (address.protocol() == QAbstractSocket::IPv6Protocol) {
// Use a link-local or site-local address for the "local address" // Use a link-local or site-local address for the "local address"
if (address.isInSubnet(QHostAddress("fe80::"), 10) || if (address.isInSubnet(QHostAddress("fe80::"), 10) ||
address.isInSubnet(QHostAddress("fec0::"), 10) || address.isInSubnet(QHostAddress("fec0::"), 10) ||
address.isInSubnet(QHostAddress("fc00::"), 7)) { address.isInSubnet(QHostAddress("fc00::"), 7)) {
addNewHost(NvAddress(address, computer->port()), addNewHost(NvAddress(address, computer->port()), true, NvAddress(v6Global, computer->port()));
true, computer->hostname(),
NvAddress(v6Global, computer->port()));
break; break;
} }
} }
@@ -569,7 +553,7 @@ void ComputerManager::handleAboutToQuit()
// Interrupt polling threads immediately, so they // Interrupt polling threads immediately, so they
// avoid making additional requests while quitting // avoid making additional requests while quitting
for (ComputerPollingEntry* entry : std::as_const(m_PollEntries)) { for (ComputerPollingEntry* entry : m_PollEntries) {
entry->interrupt(); entry->interrupt();
} }
} }
@@ -721,7 +705,7 @@ void ComputerManager::stopPollingAsync()
m_MdnsServer.reset(); m_MdnsServer.reset();
// Interrupt all threads, but don't wait for them to terminate // Interrupt all threads, but don't wait for them to terminate
for (ComputerPollingEntry* entry : std::as_const(m_PollEntries)) { for (ComputerPollingEntry* entry : m_PollEntries) {
entry->interrupt(); entry->interrupt();
} }
} }
@@ -733,10 +717,6 @@ void ComputerManager::addNewHostManually(QString address)
// If there wasn't a port specified, use the default // If there wasn't a port specified, use the default
addNewHost(NvAddress(url.host(), url.port(DEFAULT_HTTP_PORT)), false); addNewHost(NvAddress(url.host(), url.port(DEFAULT_HTTP_PORT)), false);
} }
else if (QHostAddress(address).protocol() == QAbstractSocket::IPv6Protocol) {
// The user specified an IPv6 literal without URL escaping, so use the default port
addNewHost(NvAddress(address, DEFAULT_HTTP_PORT), false);
}
else { else {
emit computerAddCompleted(false, false); emit computerAddCompleted(false, false);
} }
@@ -747,9 +727,8 @@ class PendingAddTask : public QObject, public QRunnable
Q_OBJECT Q_OBJECT
public: public:
PendingAddTask(ComputerManager* computerManager, QString name, NvAddress address, NvAddress mdnsIpv6Address, bool mdns) PendingAddTask(ComputerManager* computerManager, NvAddress address, NvAddress mdnsIpv6Address, bool mdns)
: m_ComputerManager(computerManager), : m_ComputerManager(computerManager),
m_Name(name),
m_Address(address), m_Address(address),
m_MdnsIpv6Address(mdnsIpv6Address), m_MdnsIpv6Address(mdnsIpv6Address),
m_Mdns(mdns), m_Mdns(mdns),
@@ -805,9 +784,10 @@ private:
return serverInfo; return serverInfo;
} catch (...) { } catch (...) {
if (!m_Mdns) { if (!m_Mdns) {
unsigned int portTestResult; StreamingPreferences prefs;
int portTestResult;
if (m_ComputerManager->m_Prefs->detectNetworkBlocking) { if (prefs.detectNetworkBlocking) {
// We failed to connect to the specified PC. Let's test to make sure this network // We failed to connect to the specified PC. Let's test to make sure this network
// isn't blocking Moonlight, so we can tell the user about it. // isn't blocking Moonlight, so we can tell the user about it.
portTestResult = LiTestClientConnectivity("qt.conntest.moonlight-stream.org", 443, portTestResult = LiTestClientConnectivity("qt.conntest.moonlight-stream.org", 443,
@@ -825,21 +805,9 @@ private:
void run() void run()
{ {
// Use the placeholder UID for the initial poll, then we'll switch to the real one if it's not GFE NvHTTP http(m_Address, 0, QSslCertificate());
NvHTTP http(m_Address, 0, QSslCertificate(), false);
if (m_Mdns) { qInfo() << "Processing new PC at" << m_Address.toString() << "from" << (m_Mdns ? "mDNS" : "user") << "with IPv6 address" << m_MdnsIpv6Address.toString();
if (m_MdnsIpv6Address.isNull()) {
qInfo() << "Processing new PC" << m_Name << "from mDNS with local address" << m_Address.toString();
}
else {
qInfo() << "Processing new PC" << m_Name << "from mDNS with local address" << m_Address.toString()
<< "and IPv6 address" << m_MdnsIpv6Address.toString();
}
}
else {
qInfo() << "Processing new PC at" << m_Address.toString() << "from user";
}
// Perform initial serverinfo fetch over HTTP since we don't know which cert to use // Perform initial serverinfo fetch over HTTP since we don't know which cert to use
QString serverInfo = fetchServerInfo(http); QString serverInfo = fetchServerInfo(http);
@@ -854,7 +822,6 @@ private:
// Create initial newComputer using HTTP serverinfo with no pinned cert // Create initial newComputer using HTTP serverinfo with no pinned cert
NvComputer* newComputer = new NvComputer(http, serverInfo); NvComputer* newComputer = new NvComputer(http, serverInfo);
http.setTrueUid(!newComputer->isNvidiaServerSoftware);
// Check if we have a record of this host UUID to pull the pinned cert // Check if we have a record of this host UUID to pull the pinned cert
NvComputer* existingComputer; NvComputer* existingComputer;
@@ -988,24 +955,28 @@ private:
} }
ComputerManager* m_ComputerManager; ComputerManager* m_ComputerManager;
QString m_Name;
NvAddress m_Address; NvAddress m_Address;
NvAddress m_MdnsIpv6Address; NvAddress m_MdnsIpv6Address;
bool m_Mdns; bool m_Mdns;
bool m_AboutToQuit; bool m_AboutToQuit;
}; };
void ComputerManager::addNewHost(NvAddress address, bool mdns, QString name, NvAddress mdnsIpv6Address) void ComputerManager::addNewHost(NvAddress address, bool mdns, NvAddress mdnsIpv6Address)
{ {
// Punt to a worker thread to avoid stalling the // Punt to a worker thread to avoid stalling the
// UI while waiting for serverinfo query to complete // UI while waiting for serverinfo query to complete
PendingAddTask* addTask = new PendingAddTask(this, name, address, mdnsIpv6Address, mdns); PendingAddTask* addTask = new PendingAddTask(this, address, mdnsIpv6Address, mdns);
QThreadPool::globalInstance()->start(addTask); QThreadPool::globalInstance()->start(addTask);
} }
// TODO: Use QRandomGenerator when we drop Qt 5.9 support
QString ComputerManager::generatePinString() QString ComputerManager::generatePinString()
{ {
return QString::asprintf("%04u", QRandomGenerator::system()->bounded(10000)); std::uniform_int_distribution<int> dist(0, 9999);
std::random_device rd;
std::mt19937 engine(rd());
return QString::asprintf("%04u", dist(engine));
} }
#include "computermanager.moc" #include "computermanager.moc"
+9 -22
View File
@@ -1,7 +1,7 @@
#pragma once #pragma once
#include "nvcomputer.h" #include "nvcomputer.h"
#include "settings/streamingpreferences.h" #include "nvpairingmanager.h"
#include "settings/compatfetcher.h" #include "settings/compatfetcher.h"
#include <qmdnsengine/server.h> #include <qmdnsengine/server.h>
@@ -72,15 +72,9 @@ private slots:
void handleResolvedTimeout() void handleResolvedTimeout()
{ {
if (m_Addresses.isEmpty()) { if (m_Addresses.isEmpty()) {
if (m_Retries-- > 0) { // Try again
// Try again qInfo() << "Resolving" << hostname() << "timed out. Retrying...";
qInfo() << "Resolving" << hostname() << "timed out. Retrying..."; resolve();
resolve();
}
else {
qWarning() << "Giving up on resolving" << hostname() << "after repeated failures";
cleanup();
}
} }
else { else {
Q_ASSERT(!m_Addresses.isEmpty()); Q_ASSERT(!m_Addresses.isEmpty());
@@ -90,6 +84,7 @@ private slots:
void handleResolvedAddress(const QHostAddress& address) void handleResolvedAddress(const QHostAddress& address)
{ {
qInfo() << "Resolved" << hostname() << "to" << address;
m_Addresses.push_back(address); m_Addresses.push_back(address);
} }
@@ -97,7 +92,7 @@ signals:
void resolvedHost(MdnsPendingComputer*,QVector<QHostAddress>&); void resolvedHost(MdnsPendingComputer*,QVector<QHostAddress>&);
private: private:
void cleanup() void resolve()
{ {
// Delete our resolver, so we're guaranteed that nothing is referencing m_Server. // Delete our resolver, so we're guaranteed that nothing is referencing m_Server.
delete m_Resolver; delete m_Resolver;
@@ -106,12 +101,6 @@ private:
// Now delete our strong reference that we held on behalf of m_Resolver. // Now delete our strong reference that we held on behalf of m_Resolver.
// The server may be destroyed after we make this call. // The server may be destroyed after we make this call.
m_Server.reset(); m_Server.reset();
}
void resolve()
{
// Clean up any existing resolver object and server references
cleanup();
// Re-acquire a strong reference if the server still exists. // Re-acquire a strong reference if the server still exists.
m_Server = m_ServerWeak.toStrongRef(); m_Server = m_ServerWeak.toStrongRef();
@@ -131,7 +120,6 @@ private:
QSharedPointer<QMdnsEngine::Server> m_Server; QSharedPointer<QMdnsEngine::Server> m_Server;
QMdnsEngine::Resolver* m_Resolver; QMdnsEngine::Resolver* m_Resolver;
QVector<QHostAddress> m_Addresses; QVector<QHostAddress> m_Addresses;
int m_Retries = 10;
}; };
class ComputerPollingEntry class ComputerPollingEntry
@@ -150,7 +138,7 @@ public:
// interrupt() should have taken care of this // interrupt() should have taken care of this
Q_ASSERT(m_ActiveThread == nullptr); Q_ASSERT(m_ActiveThread == nullptr);
for (QThread* thread : std::as_const(m_InactiveList)) { for (QThread* thread : m_InactiveList) {
thread->wait(); thread->wait();
delete thread; delete thread;
} }
@@ -217,7 +205,7 @@ class ComputerManager : public QObject
friend class DelayedFlushThread; friend class DelayedFlushThread;
public: public:
explicit ComputerManager(StreamingPreferences* prefs); explicit ComputerManager(QObject *parent = nullptr);
virtual ~ComputerManager(); virtual ~ComputerManager();
@@ -227,7 +215,7 @@ public:
Q_INVOKABLE void addNewHostManually(QString address); Q_INVOKABLE void addNewHostManually(QString address);
void addNewHost(NvAddress address, bool mdns, QString name = QString(), NvAddress mdnsIpv6Address = NvAddress()); void addNewHost(NvAddress address, bool mdns, NvAddress mdnsIpv6Address = NvAddress());
QString generatePinString(); QString generatePinString();
@@ -269,7 +257,6 @@ private:
void startPollingComputer(NvComputer* computer); void startPollingComputer(NvComputer* computer);
StreamingPreferences* m_Prefs;
int m_PollingRef; int m_PollingRef;
QReadWriteLock m_Lock; QReadWriteLock m_Lock;
QMap<QString, NvComputer*> m_KnownHosts; QMap<QString, NvComputer*> m_KnownHosts;
+6 -30
View File
@@ -6,12 +6,6 @@ ComputerSeeker::ComputerSeeker(ComputerManager *manager, QString computerName, Q
: QObject(parent), m_ComputerManager(manager), m_ComputerName(computerName), : QObject(parent), m_ComputerManager(manager), m_ComputerName(computerName),
m_TimeoutTimer(new QTimer(this)) m_TimeoutTimer(new QTimer(this))
{ {
// If we know this computer, send a WOL packet to wake it up in case it is asleep.
NvComputer* matchingComputer = findMatchingComputer();
if (matchingComputer) {
matchingComputer->wake();
}
m_TimeoutTimer->setSingleShot(true); m_TimeoutTimer->setSingleShot(true);
connect(m_TimeoutTimer, &QTimer::timeout, connect(m_TimeoutTimer, &QTimer::timeout,
this, &ComputerSeeker::onTimeout); this, &ComputerSeeker::onTimeout);
@@ -22,16 +16,11 @@ ComputerSeeker::ComputerSeeker(ComputerManager *manager, QString computerName, Q
void ComputerSeeker::start(int timeout) void ComputerSeeker::start(int timeout)
{ {
m_TimeoutTimer->start(timeout); m_TimeoutTimer->start(timeout);
// Seek desired computer by both connecting to it directly (this may fail
// If we don't know this computer by name, address, or UUID, try adding it // if m_ComputerName is UUID, or the name that doesn't resolve to an IP
// manually and see if we can find it by address, hostname, or mDNS. // address) and by polling it using mDNS, hopefully one of these methods
// // would find the host
// NB: We don't do this unconditionally because it will wipe out the user's m_ComputerManager->addNewHostManually(m_ComputerName);
// manual address if they pass another reachable hostname/address.
if (!findMatchingComputer()) {
m_ComputerManager->addNewHostManually(m_ComputerName);
}
m_ComputerManager->startPolling(); m_ComputerManager->startPolling();
} }
@@ -55,8 +44,7 @@ bool ComputerSeeker::matchComputer(NvComputer *computer) const
return true; return true;
} }
const auto uniqueAddresses = computer->uniqueAddresses(); for (const NvAddress& addr : computer->uniqueAddresses()) {
for (const NvAddress& addr : uniqueAddresses) {
if (addr.address().toLower() == value || addr.toString().toLower() == value) { if (addr.address().toLower() == value || addr.toString().toLower() == value) {
return true; return true;
} }
@@ -65,18 +53,6 @@ bool ComputerSeeker::matchComputer(NvComputer *computer) const
return false; return false;
} }
NvComputer* ComputerSeeker::findMatchingComputer() const
{
const auto computers = m_ComputerManager->getComputers();
for (NvComputer* computer : computers) {
if (this->matchComputer(computer)) {
return computer;
}
}
return nullptr;
}
bool ComputerSeeker::isOnline(NvComputer *computer) const bool ComputerSeeker::isOnline(NvComputer *computer) const
{ {
return computer->state == NvComputer::CS_ONLINE; return computer->state == NvComputer::CS_ONLINE;
-1
View File
@@ -24,7 +24,6 @@ private slots:
private: private:
bool matchComputer(NvComputer *computer) const; bool matchComputer(NvComputer *computer) const;
NvComputer* findMatchingComputer() const;
bool isOnline(NvComputer *computer) const; bool isOnline(NvComputer *computer) const;
private: private:
+36 -38
View File
@@ -31,10 +31,6 @@ void IdentityManager::createCredentials(QSettings& settings)
X509* cert = X509_new(); X509* cert = X509_new();
THROW_BAD_ALLOC_IF_NULL(cert); THROW_BAD_ALLOC_IF_NULL(cert);
#if OPENSSL_VERSION_NUMBER >= 0x30000000L
EVP_PKEY* pk = EVP_RSA_gen(2048);
THROW_BAD_ALLOC_IF_NULL(pk);
#else
EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, NULL); EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, NULL);
THROW_BAD_ALLOC_IF_NULL(ctx); THROW_BAD_ALLOC_IF_NULL(ctx);
@@ -47,7 +43,6 @@ void IdentityManager::createCredentials(QSettings& settings)
EVP_PKEY_CTX_free(ctx); EVP_PKEY_CTX_free(ctx);
THROW_BAD_ALLOC_IF_NULL(pk); THROW_BAD_ALLOC_IF_NULL(pk);
#endif
X509_set_version(cert, 2); X509_set_version(cert, 2);
ASN1_INTEGER_set(X509_get_serialNumber(cert), 0); ASN1_INTEGER_set(X509_get_serialNumber(cert), 0);
@@ -55,20 +50,28 @@ void IdentityManager::createCredentials(QSettings& settings)
X509_gmtime_adj(X509_get_notBefore(cert), 0); X509_gmtime_adj(X509_get_notBefore(cert), 0);
X509_gmtime_adj(X509_get_notAfter(cert), 60 * 60 * 24 * 365 * 20); // 20 yrs X509_gmtime_adj(X509_get_notAfter(cert), 60 * 60 * 24 * 365 * 20); // 20 yrs
#else #else
X509_gmtime_adj(X509_getm_notBefore(cert), 0); ASN1_TIME* before = ASN1_STRING_dup(X509_get0_notBefore(cert));
X509_gmtime_adj(X509_getm_notAfter(cert), 60 * 60 * 24 * 365 * 20); // 20 yrs THROW_BAD_ALLOC_IF_NULL(before);
ASN1_TIME* after = ASN1_STRING_dup(X509_get0_notAfter(cert));
THROW_BAD_ALLOC_IF_NULL(after);
X509_gmtime_adj(before, 0);
X509_gmtime_adj(after, 60 * 60 * 24 * 365 * 20); // 20 yrs
X509_set1_notBefore(cert, before);
X509_set1_notAfter(cert, after);
ASN1_STRING_free(before);
ASN1_STRING_free(after);
#endif #endif
X509_set_pubkey(cert, pk); X509_set_pubkey(cert, pk);
X509_NAME* name = X509_NAME_new(); X509_NAME* name = X509_get_subject_name(cert);
THROW_BAD_ALLOC_IF_NULL(name);
X509_NAME_add_entry_by_txt(name, "CN", MBSTRING_ASC, X509_NAME_add_entry_by_txt(name, "CN", MBSTRING_ASC,
reinterpret_cast<unsigned char *>(const_cast<char*>("NVIDIA GameStream Client")), reinterpret_cast<unsigned char *>(const_cast<char*>("NVIDIA GameStream Client")),
-1, -1, 0); -1, -1, 0);
X509_set_subject_name(cert, name);
X509_set_issuer_name(cert, name); X509_set_issuer_name(cert, name);
X509_NAME_free(name);
X509_sign(cert, pk, EVP_sha256()); X509_sign(cert, pk, EVP_sha256());
@@ -92,14 +95,6 @@ void IdentityManager::createCredentials(QSettings& settings)
BIO_free(biokey); BIO_free(biokey);
BIO_free(biocert); BIO_free(biocert);
// Check that the new keypair is valid before persisting it
if (getSslCertificate().isNull()) {
qFatal("Newly generated certificate is unreadable");
}
if (getSslKey().isNull()) {
qFatal("Newly generated private key is unreadable");
}
settings.setValue(SER_CERT, m_CachedPemCert); settings.setValue(SER_CERT, m_CachedPemCert);
settings.setValue(SER_KEY, m_CachedPrivateKey); settings.setValue(SER_KEY, m_CachedPrivateKey);
@@ -128,26 +123,10 @@ IdentityManager::IdentityManager()
// We should have valid credentials now. If not, we're screwed // We should have valid credentials now. If not, we're screwed
if (getSslCertificate().isNull()) { if (getSslCertificate().isNull()) {
qFatal("Certificate is unreadable"); qFatal("Newly generated certificate is unreadable");
} }
if (getSslKey().isNull()) { if (getSslKey().isNull()) {
qFatal("Private key is unreadable"); qFatal("Newly generated private key is unreadable");
}
// Load the unique ID from settings
m_CachedUniqueId = settings.value(SER_UNIQUEID).toString();
if (!m_CachedUniqueId.isEmpty()) {
qInfo() << "Loaded unique ID from settings:" << m_CachedUniqueId;
}
else {
// Generate a new unique ID in base 16
uint64_t uid;
RAND_bytes(reinterpret_cast<unsigned char*>(&uid), sizeof(uid));
m_CachedUniqueId = QString::number(uid, 16);
qInfo() << "Generated new unique ID:" << m_CachedUniqueId;
settings.setValue(SER_UNIQUEID, m_CachedUniqueId);
} }
} }
@@ -186,7 +165,7 @@ IdentityManager::getSslKey()
BUF_MEM* mem; BUF_MEM* mem;
BIO_get_mem_ptr(bio, &mem); BIO_get_mem_ptr(bio, &mem);
m_CachedSslKey = QSslKey(QByteArray(mem->data, (int)mem->length), QSsl::Rsa); m_CachedSslKey = QSslKey(QByteArray::fromRawData(mem->data, (int)mem->length), QSsl::Rsa);
BIO_free(bio); BIO_free(bio);
EVP_PKEY_free(pk); EVP_PKEY_free(pk);
@@ -206,6 +185,25 @@ IdentityManager::getSslConfig()
QString QString
IdentityManager::getUniqueId() IdentityManager::getUniqueId()
{ {
if (m_CachedUniqueId.isEmpty()) {
QSettings settings;
// Load the unique ID from settings
m_CachedUniqueId = settings.value(SER_UNIQUEID).toString();
if (!m_CachedUniqueId.isEmpty()) {
qInfo() << "Loaded unique ID from settings:" << m_CachedUniqueId;
}
else {
// Generate a new unique ID in base 16
uint64_t uid;
RAND_bytes(reinterpret_cast<unsigned char*>(&uid), sizeof(uid));
m_CachedUniqueId = QString::number(uid, 16);
qInfo() << "Generated new unique ID:" << m_CachedUniqueId;
settings.setValue(SER_UNIQUEID, m_CachedUniqueId);
}
}
return m_CachedUniqueId; return m_CachedUniqueId;
} }
+1 -3
View File
@@ -25,9 +25,7 @@ public:
bool isInitialized() bool isInitialized()
{ {
// We use isNull() instead of isEmpty() here because we want return id != 0 && !name.isEmpty();
// to detect cases where the name is unassigned, not empty.
return id != 0 && !name.isNull();
} }
void void
+10 -29
View File
@@ -99,7 +99,7 @@ void NvComputer::serialize(QSettings& settings, bool serializeApps) const
settings.beginWriteArray(SER_APPLIST); settings.beginWriteArray(SER_APPLIST);
for (int i = 0; i < appList.count(); i++) { for (int i = 0; i < appList.count(); i++) {
settings.setArrayIndex(i); settings.setArrayIndex(i);
appList.at(i).serialize(settings); appList[i].serialize(settings);
} }
settings.endArray(); settings.endArray();
} }
@@ -141,7 +141,7 @@ NvComputer::NvComputer(NvHTTP& http, QString serverInfo)
QString newMacString = NvHTTP::getXmlString(serverInfo, "mac"); QString newMacString = NvHTTP::getXmlString(serverInfo, "mac");
if (newMacString != "00:00:00:00:00:00") { if (newMacString != "00:00:00:00:00:00") {
QStringList macOctets = newMacString.split(':'); QStringList macOctets = newMacString.split(':');
for (const QString& macOctet : std::as_const(macOctets)) { for (const QString& macOctet : macOctets) {
this->macAddress.append((char) macOctet.toInt(nullptr, 16)); this->macAddress.append((char) macOctet.toInt(nullptr, 16));
} }
} }
@@ -254,16 +254,14 @@ bool NvComputer::wake() const
// case the host has timed out in ARP entries. // case the host has timed out in ARP entries.
QMap<QString, quint16> addressMap; QMap<QString, quint16> addressMap;
QSet<quint16> basePortSet; QSet<quint16> basePortSet;
const auto uniqueHostAddresses = uniqueAddresses(); for (const NvAddress& addr : uniqueAddresses()) {
for (const NvAddress& addr : uniqueHostAddresses) {
addressMap.insert(addr.address(), addr.port()); addressMap.insert(addr.address(), addr.port());
basePortSet.insert(addr.port()); basePortSet.insert(addr.port());
} }
addressMap.insert("255.255.255.255", 0); addressMap.insert("255.255.255.255", 0);
// Try to broadcast on all available NICs // Try to broadcast on all available NICs
const auto allInterfaces = QNetworkInterface::allInterfaces(); for (const QNetworkInterface& nic : QNetworkInterface::allInterfaces()) {
for (const QNetworkInterface& nic : allInterfaces) {
// Ensure the interface is up and skip the loopback adapter // Ensure the interface is up and skip the loopback adapter
if ((nic.flags() & QNetworkInterface::IsUp) == 0 || if ((nic.flags() & QNetworkInterface::IsUp) == 0 ||
(nic.flags() & QNetworkInterface::IsLoopBack) != 0) { (nic.flags() & QNetworkInterface::IsLoopBack) != 0) {
@@ -271,8 +269,7 @@ bool NvComputer::wake() const
} }
QHostAddress allNodesMulticast("FF02::1"); QHostAddress allNodesMulticast("FF02::1");
const auto allInterfaceAddresses = nic.addressEntries(); for (const QNetworkAddressEntry& addr : nic.addressEntries()) {
for (const QNetworkAddressEntry& addr : allInterfaceAddresses) {
// Store the scope ID for this NIC if IPv6 is enabled // Store the scope ID for this NIC if IPv6 is enabled
if (!addr.ip().scopeId().isEmpty()) { if (!addr.ip().scopeId().isEmpty()) {
allNodesMulticast.setScopeId(addr.ip().scopeId()); allNodesMulticast.setScopeId(addr.ip().scopeId());
@@ -376,17 +373,14 @@ NvComputer::ReachabilityType NvComputer::getActiveAddressReachability() const
s.connectToHost(copyOfActiveAddress.address(), copyOfActiveAddress.port()); s.connectToHost(copyOfActiveAddress.address(), copyOfActiveAddress.port());
if (s.waitForConnected(3000)) { if (s.waitForConnected(3000)) {
Q_ASSERT(!s.localAddress().isNull()); Q_ASSERT(!s.localAddress().isNull());
Q_ASSERT(!s.peerAddress().isNull());
const auto allInterfaces = QNetworkInterface::allInterfaces(); for (const QNetworkInterface& nic : QNetworkInterface::allInterfaces()) {
for (const QNetworkInterface& nic : allInterfaces) {
// Ensure the interface is up // Ensure the interface is up
if ((nic.flags() & QNetworkInterface::IsUp) == 0) { if ((nic.flags() & QNetworkInterface::IsUp) == 0) {
continue; continue;
} }
const auto allInterfaceAddresses = nic.addressEntries(); for (const QNetworkAddressEntry& addr : nic.addressEntries()) {
for (const QNetworkAddressEntry& addr : allInterfaceAddresses) {
if (addr.ip() == s.localAddress()) { if (addr.ip() == s.localAddress()) {
qInfo() << "Found matching interface:" << nic.humanReadableName() << nic.hardwareAddress() << nic.flags(); qInfo() << "Found matching interface:" << nic.humanReadableName() << nic.hardwareAddress() << nic.flags();
@@ -412,19 +406,6 @@ NvComputer::ReachabilityType NvComputer::getActiveAddressReachability() const
return ReachabilityType::RI_VPN; return ReachabilityType::RI_VPN;
} }
#ifdef Q_OS_WINDOWS
if (nic.name().startsWith("iftype53_") || nic.name().startsWith("iftype131_")) {
// Match by NDIS interface type. These values are Microsoft's recommended values for VPN connections:
// https://learn.microsoft.com/en-US/troubleshoot/windows-client/networking/windows-connection-manager-disconnects-wlan#more-information
//
// The following VPNs use IF_TYPE_PROP_VIRTUAL under Windows:
// - WireguardNT VPNs
// - All WinTun-based VPNs (such as Slack Nebula)
// - OpenVPN with tap-windows6
return ReachabilityType::RI_VPN;
}
#endif
if (nic.hardwareAddress().startsWith("00:FF", Qt::CaseInsensitive)) { if (nic.hardwareAddress().startsWith("00:FF", Qt::CaseInsensitive)) {
// OpenVPN TAP interfaces have a MAC address starting with 00:FF on Windows // OpenVPN TAP interfaces have a MAC address starting with 00:FF on Windows
return ReachabilityType::RI_VPN; return ReachabilityType::RI_VPN;
@@ -440,9 +421,9 @@ NvComputer::ReachabilityType NvComputer::getActiveAddressReachability() const
return ReachabilityType::RI_VPN; return ReachabilityType::RI_VPN;
} }
// Didn't meet any of our VPN heuristics. Let's see if the peer address is on-link. // Didn't meet any of our VPN heuristics. Let's see if it's on-link.
Q_ASSERT(addr.prefixLength() >= 0); Q_ASSERT(addr.prefixLength() >= 0);
if (addr.prefixLength() >= 0 && s.localAddress().isInSubnet(s.peerAddress(), addr.prefixLength())) { if (addr.prefixLength() >= 0 && s.localAddress().isInSubnet(addr.ip(), addr.prefixLength())) {
return ReachabilityType::RI_LAN; return ReachabilityType::RI_LAN;
} }
@@ -468,7 +449,7 @@ bool NvComputer::updateAppList(QVector<NvApp> newAppList) {
} }
// Propagate client-side attributes to the new app list // Propagate client-side attributes to the new app list
for (const NvApp& existingApp : std::as_const(appList)) { for (const NvApp& existingApp : appList) {
for (NvApp& newApp : newAppList) { for (NvApp& newApp : newAppList) {
if (existingApp.id == newApp.id) { if (existingApp.id == newApp.id) {
newApp.hidden = existingApp.hidden; newApp.hidden = existingApp.hidden;
+57 -74
View File
@@ -18,16 +18,8 @@
#define RESUME_TIMEOUT_MS 30000 #define RESUME_TIMEOUT_MS 30000
#define QUIT_TIMEOUT_MS 30000 #define QUIT_TIMEOUT_MS 30000
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) NvHTTP::NvHTTP(NvAddress address, uint16_t httpsPort, QSslCertificate serverCert) :
#define XML_NAME_EQUALS(x, y) ((x) == (y)) m_ServerCert(serverCert)
#else
#define XML_NAME_EQUALS(x, y) ((x) == (u##y))
#endif
NvHTTP::NvHTTP(NvAddress address, uint16_t httpsPort, QSslCertificate serverCert, bool useTrueUid, QNetworkAccessManager* nam) :
m_Nam(nam ? nam : new QNetworkAccessManager(this)),
m_ServerCert(serverCert),
m_UseTrueUid(useTrueUid)
{ {
m_BaseUrlHttp.setScheme("http"); m_BaseUrlHttp.setScheme("http");
m_BaseUrlHttps.setScheme("https"); m_BaseUrlHttps.setScheme("https");
@@ -37,12 +29,15 @@ NvHTTP::NvHTTP(NvAddress address, uint16_t httpsPort, QSslCertificate serverCert
// Never use a proxy server // Never use a proxy server
QNetworkProxy noProxy(QNetworkProxy::NoProxy); QNetworkProxy noProxy(QNetworkProxy::NoProxy);
m_Nam->setProxy(noProxy); m_Nam.setProxy(noProxy);
connect(&m_Nam, &QNetworkAccessManager::sslErrors, this, &NvHTTP::handleSslErrors);
} }
NvHTTP::NvHTTP(NvComputer* computer, QNetworkAccessManager* nam) : NvHTTP::NvHTTP(NvComputer* computer) :
NvHTTP(computer->activeAddress, computer->activeHttpsPort, computer->serverCert, !computer->isNvidiaServerSoftware, nam) NvHTTP(computer->activeAddress, computer->activeHttpsPort, computer->serverCert)
{ {
} }
void NvHTTP::setServerCert(QSslCertificate serverCert) void NvHTTP::setServerCert(QSslCertificate serverCert)
@@ -67,11 +62,6 @@ void NvHTTP::setHttpsPort(uint16_t port)
m_BaseUrlHttps.setPort(port); m_BaseUrlHttps.setPort(port);
} }
void NvHTTP::setTrueUid(bool useTrueUid)
{
m_UseTrueUid = useTrueUid;
}
NvAddress NvHTTP::address() NvAddress NvHTTP::address()
{ {
return m_Address; return m_Address;
@@ -120,7 +110,7 @@ NvHTTP::getCurrentGame(QString serverInfo)
// has the semantics that its name would indicate. To contain the effects of this change as much // has the semantics that its name would indicate. To contain the effects of this change as much
// as possible, we'll force the current game to zero if the server isn't in a streaming session. // as possible, we'll force the current game to zero if the server isn't in a streaming session.
QString serverState = getXmlString(serverInfo, "state"); QString serverState = getXmlString(serverInfo, "state");
if (serverState.endsWith("_SERVER_BUSY")) if (serverState != nullptr && serverState.endsWith("_SERVER_BUSY"))
{ {
return getXmlString(serverInfo, "currentgame").toInt(); return getXmlString(serverInfo, "currentgame").toInt();
} }
@@ -233,8 +223,7 @@ NvHTTP::startApp(QString verb,
"&surroundAudioInfo="+QString::number(SURROUNDAUDIOINFO_FROM_AUDIO_CONFIGURATION(streamConfig->audioConfiguration))+ "&surroundAudioInfo="+QString::number(SURROUNDAUDIOINFO_FROM_AUDIO_CONFIGURATION(streamConfig->audioConfiguration))+
"&remoteControllersBitmap="+QString::number(gamepadMask)+ "&remoteControllersBitmap="+QString::number(gamepadMask)+
"&gcmap="+QString::number(gamepadMask)+ "&gcmap="+QString::number(gamepadMask)+
"&gcpersist="+QString::number(persistGameControllersOnDisconnect ? 1 : 0)+ "&gcpersist="+QString::number(persistGameControllersOnDisconnect ? 1 : 0),
LiGetLaunchUrlQueryParameters(),
LAUNCH_TIMEOUT_MS); LAUNCH_TIMEOUT_MS);
qInfo() << "Launch response:" << response; qInfo() << "Launch response:" << response;
@@ -277,19 +266,17 @@ NvHTTP::getDisplayModeList(QString serverInfo)
while (!xmlReader.atEnd()) { while (!xmlReader.atEnd()) {
while (xmlReader.readNextStartElement()) { while (xmlReader.readNextStartElement()) {
auto name = xmlReader.name(); auto name = xmlReader.name();
if (XML_NAME_EQUALS(name, "DisplayMode")) { if (name == QString("DisplayMode")) {
modes.append(NvDisplayMode()); modes.append(NvDisplayMode());
} }
else if (!modes.isEmpty()) { else if (name == QString("Width")) {
if (XML_NAME_EQUALS(name, "Width")) { modes.last().width = xmlReader.readElementText().toInt();
modes.last().width = xmlReader.readElementText().toInt(); }
} else if (name == QString("Height")) {
else if (XML_NAME_EQUALS(name, "Height")) { modes.last().height = xmlReader.readElementText().toInt();
modes.last().height = xmlReader.readElementText().toInt(); }
} else if (name == QString("RefreshRate")) {
else if (XML_NAME_EQUALS(name, "RefreshRate")) { modes.last().refreshRate = xmlReader.readElementText().toInt();
modes.last().refreshRate = xmlReader.readElementText().toInt();
}
} }
} }
} }
@@ -312,36 +299,26 @@ NvHTTP::getAppList()
while (!xmlReader.atEnd()) { while (!xmlReader.atEnd()) {
while (xmlReader.readNextStartElement()) { while (xmlReader.readNextStartElement()) {
auto name = xmlReader.name(); auto name = xmlReader.name();
if (XML_NAME_EQUALS(name, "App")) { if (name == QString("App")) {
// We must have a valid app before advancing to the next one // We must have a valid app before advancing to the next one
if (!apps.isEmpty() && !apps.last().isInitialized()) { if (!apps.isEmpty() && !apps.last().isInitialized()) {
qWarning() << "Invalid applist XML"; qWarning() << "Invalid applist XML";
throw std::runtime_error("Invalid applist XML"); Q_ASSERT(false);
return QVector<NvApp>();
} }
apps.append(NvApp()); apps.append(NvApp());
} }
else if (!apps.isEmpty()) { else if (name == QString("AppTitle")) {
if (XML_NAME_EQUALS(name, "AppTitle")) { apps.last().name = xmlReader.readElementText();
// If an app has no name, Sunshine may send us <AppTitle/>, }
// which readElementText() returns as a null QString. else if (name == QString("ID")) {
// We want to treat this as an empty QString instead, so we apps.last().id = xmlReader.readElementText().toInt();
// will explicitly convert it. An empty string will satisfy }
// NvApp's isInitialized() check. else if (name == QString("IsHdrSupported")) {
QString name = xmlReader.readElementText(); apps.last().hdrSupported = xmlReader.readElementText() == "1";
if (name.isNull()) { }
name = ""; else if (name == QString("IsAppCollectorGame")) {
} apps.last().isAppCollectorGame = xmlReader.readElementText() == "1";
apps.last().name = name;
}
else if (XML_NAME_EQUALS(name, "ID")) {
apps.last().id = xmlReader.readElementText().toInt();
}
else if (XML_NAME_EQUALS(name, "IsHdrSupported")) {
apps.last().hdrSupported = xmlReader.readElementText() == "1";
}
else if (XML_NAME_EQUALS(name, "IsAppCollectorGame")) {
apps.last().isAppCollectorGame = xmlReader.readElementText() == "1";
}
} }
} }
} }
@@ -356,7 +333,7 @@ NvHTTP::verifyResponseStatus(QString xml)
while (xmlReader.readNextStartElement()) while (xmlReader.readNextStartElement())
{ {
if (XML_NAME_EQUALS(xmlReader.name(), "root")) if (xmlReader.name() == QString("root"))
{ {
// Status code can be 0xFFFFFFFF in some rare cases on GFE 3.20.3, and // 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() // QString::toInt() will fail in that case, so use QString::toUInt()
@@ -407,7 +384,13 @@ QByteArray
NvHTTP::getXmlStringFromHex(QString xml, NvHTTP::getXmlStringFromHex(QString xml,
QString tagName) QString tagName)
{ {
return QByteArray::fromHex(getXmlString(xml, tagName).toUtf8()); QString str = getXmlString(xml, tagName);
if (str == nullptr)
{
return nullptr;
}
return QByteArray::fromHex(str.toLatin1());
} }
QString QString
@@ -429,7 +412,7 @@ NvHTTP::getXmlString(QString xml,
} }
} }
return QString(); return nullptr;
} }
void NvHTTP::handleSslErrors(QNetworkReply* reply, const QList<QSslError>& errors) void NvHTTP::handleSslErrors(QNetworkReply* reply, const QList<QSslError>& errors)
@@ -492,9 +475,11 @@ NvHTTP::openConnection(QUrl baseUrl,
QUrl url(baseUrl); QUrl url(baseUrl);
url.setPath("/" + command); url.setPath("/" + command);
// Use a placeholder UID for GFE allow them to quit games for each other. // Use a common UID for Moonlight clients to allow them to quit
url.setQuery("uniqueid=" + (m_UseTrueUid ? IdentityManager::get()->getUniqueId() : "0123456789ABCDEF") + // games for each other (otherwise GFE gets screwed up and it requires
"&uuid=" + QUuid::createUuid().toRfc4122().toHex() + // manual intervention to solve).
url.setQuery("uniqueid=0123456789ABCDEF&uuid=" +
QUuid::createUuid().toRfc4122().toHex() +
((arguments != nullptr) ? ("&" + arguments) : "")); ((arguments != nullptr) ? ("&" + arguments) : ""));
QNetworkRequest request(url); QNetworkRequest request(url);
@@ -507,15 +492,15 @@ NvHTTP::openConnection(QUrl baseUrl,
request.setAttribute(QNetworkRequest::Http2AllowedAttribute, false); request.setAttribute(QNetworkRequest::Http2AllowedAttribute, false);
#endif #endif
#if QT_VERSION >= QT_VERSION_CHECK(6, 3, 0) #if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) && QT_VERSION < QT_VERSION_CHECK(5, 15, 1) && !defined(QT_NO_BEARERMANAGEMENT)
// Use fine-grained idle timeouts to avoid calling QNetworkAccessManager::clearAccessCache(), // HACK: Set network accessibility to work around QTBUG-80947 (introduced in Qt 5.14.0 and fixed in Qt 5.15.1)
// which tears down the NAM's global thread each time. We must not keep persistent connections QT_WARNING_PUSH
// or GFE will puke. QT_WARNING_DISABLE_DEPRECATED
request.setAttribute(QNetworkRequest::ConnectionCacheExpiryTimeoutSecondsAttribute, 0); m_Nam.setNetworkAccessible(QNetworkAccessManager::Accessible);
QT_WARNING_POP
#endif #endif
auto sslErrorsConnection = connect(m_Nam, &QNetworkAccessManager::sslErrors, this, &NvHTTP::handleSslErrors); QNetworkReply* reply = m_Nam.get(request);
QNetworkReply* reply = m_Nam->get(request);
// Run the request with a timeout if requested // Run the request with a timeout if requested
QEventLoop loop; QEventLoop loop;
@@ -538,11 +523,9 @@ NvHTTP::openConnection(QUrl baseUrl,
reply->abort(); reply->abort();
} }
#if QT_VERSION < QT_VERSION_CHECK(6, 3, 0) // We must clear out cached authentication and connections or
// If we couldn't use fine-grained connection idle timeouts, kill them all now // GFE will puke next time
m_Nam->clearAccessCache(); m_Nam.clearAccessCache();
#endif
disconnect(sslErrorsConnection);
// Handle error // Handle error
if (reply->error() != QNetworkReply::NoError) if (reply->error() != QNetworkReply::NoError)
+14 -15
View File
@@ -33,19 +33,19 @@ class GfeHttpResponseException : public std::exception
public: public:
GfeHttpResponseException(int statusCode, QString message) : GfeHttpResponseException(int statusCode, QString message) :
m_StatusCode(statusCode), m_StatusCode(statusCode),
m_StatusMessage(message.toUtf8()) m_StatusMessage(message)
{ {
} }
const char* what() const throw() const char* what() const throw()
{ {
return m_StatusMessage.constData(); return m_StatusMessage.toLatin1();
} }
const char* getStatusMessage() const const char* getStatusMessage() const
{ {
return m_StatusMessage.constData(); return m_StatusMessage.toLatin1();
} }
int getStatusCode() const int getStatusCode() const
@@ -55,12 +55,12 @@ public:
QString toQString() const QString toQString() const
{ {
return QString::fromUtf8(m_StatusMessage) + " (Error " + QString::number(m_StatusCode) + ")"; return m_StatusMessage + " (Error " + QString::number(m_StatusCode) + ")";
} }
private: private:
int m_StatusCode; int m_StatusCode;
QByteArray m_StatusMessage; QString m_StatusMessage;
}; };
class QtNetworkReplyException : public std::exception class QtNetworkReplyException : public std::exception
@@ -68,19 +68,19 @@ class QtNetworkReplyException : public std::exception
public: public:
QtNetworkReplyException(QNetworkReply::NetworkError error, QString errorText) : QtNetworkReplyException(QNetworkReply::NetworkError error, QString errorText) :
m_Error(error), m_Error(error),
m_ErrorText(errorText.toUtf8()) m_ErrorText(errorText)
{ {
} }
const char* what() const throw() const char* what() const throw()
{ {
return m_ErrorText.constData(); return m_ErrorText.toLatin1();
} }
const char* getErrorText() const const char* getErrorText() const
{ {
return m_ErrorText.constData(); return m_ErrorText.toLatin1();
} }
QNetworkReply::NetworkError getError() const QNetworkReply::NetworkError getError() const
@@ -90,12 +90,12 @@ public:
QString toQString() const QString toQString() const
{ {
return QString::fromUtf8(m_ErrorText) + " (Error " + QString::number(m_Error) + ")"; return m_ErrorText + " (Error " + QString::number(m_Error) + ")";
} }
private: private:
QNetworkReply::NetworkError m_Error; QNetworkReply::NetworkError m_Error;
QByteArray m_ErrorText; QString m_ErrorText;
}; };
class NvHTTP : public QObject class NvHTTP : public QObject
@@ -109,9 +109,9 @@ public:
NVLL_VERBOSE NVLL_VERBOSE
}; };
explicit NvHTTP(NvAddress address, uint16_t httpsPort, QSslCertificate serverCert, bool useTrueUid, QNetworkAccessManager* nam = nullptr); explicit NvHTTP(NvAddress address, uint16_t httpsPort, QSslCertificate serverCert);
explicit NvHTTP(NvComputer* computer, QNetworkAccessManager* nam = nullptr); explicit NvHTTP(NvComputer* computer);
static static
int int
@@ -142,9 +142,9 @@ public:
NvLogLevel logLevel = NvLogLevel::NVLL_VERBOSE); NvLogLevel logLevel = NvLogLevel::NVLL_VERBOSE);
void setServerCert(QSslCertificate serverCert); void setServerCert(QSslCertificate serverCert);
void setAddress(NvAddress address); void setAddress(NvAddress address);
void setHttpsPort(uint16_t port); void setHttpsPort(uint16_t port);
void setTrueUid(bool useTrueUid);
NvAddress address(); NvAddress address();
@@ -196,7 +196,6 @@ private:
NvLogLevel logLevel); NvLogLevel logLevel);
NvAddress m_Address; NvAddress m_Address;
QNetworkAccessManager* m_Nam; QNetworkAccessManager m_Nam;
QSslCertificate m_ServerCert; QSslCertificate m_ServerCert;
bool m_UseTrueUid;
}; };
+36 -53
View File
@@ -54,18 +54,16 @@ NvPairingManager::generateRandomBytes(int length)
QByteArray QByteArray
NvPairingManager::encrypt(const QByteArray& plaintext, const QByteArray& key) NvPairingManager::encrypt(const QByteArray& plaintext, const QByteArray& key)
{ {
if (plaintext.isEmpty()) { QByteArray ciphertext(plaintext.size(), 0);
return QByteArray(); EVP_CIPHER_CTX* cipher;
} int ciphertextLen;
EVP_CIPHER_CTX* cipher = EVP_CIPHER_CTX_new(); cipher = EVP_CIPHER_CTX_new();
THROW_BAD_ALLOC_IF_NULL(cipher); THROW_BAD_ALLOC_IF_NULL(cipher);
EVP_EncryptInit(cipher, EVP_aes_128_ecb(), reinterpret_cast<const unsigned char*>(key.data()), NULL); EVP_EncryptInit(cipher, EVP_aes_128_ecb(), reinterpret_cast<const unsigned char*>(key.data()), NULL);
EVP_CIPHER_CTX_set_padding(cipher, 0); EVP_CIPHER_CTX_set_padding(cipher, 0);
QByteArray ciphertext(plaintext.size(), 0);
int ciphertextLen;
EVP_EncryptUpdate(cipher, EVP_EncryptUpdate(cipher,
reinterpret_cast<unsigned char*>(ciphertext.data()), reinterpret_cast<unsigned char*>(ciphertext.data()),
&ciphertextLen, &ciphertextLen,
@@ -81,18 +79,16 @@ NvPairingManager::encrypt(const QByteArray& plaintext, const QByteArray& key)
QByteArray QByteArray
NvPairingManager::decrypt(const QByteArray& ciphertext, const QByteArray& key) NvPairingManager::decrypt(const QByteArray& ciphertext, const QByteArray& key)
{ {
if (ciphertext.isEmpty()) { QByteArray plaintext(ciphertext.size(), 0);
return QByteArray(); EVP_CIPHER_CTX* cipher;
} int plaintextLen;
EVP_CIPHER_CTX* cipher = EVP_CIPHER_CTX_new(); cipher = EVP_CIPHER_CTX_new();
THROW_BAD_ALLOC_IF_NULL(cipher); THROW_BAD_ALLOC_IF_NULL(cipher);
EVP_DecryptInit(cipher, EVP_aes_128_ecb(), reinterpret_cast<const unsigned char*>(key.data()), NULL); EVP_DecryptInit(cipher, EVP_aes_128_ecb(), reinterpret_cast<const unsigned char*>(key.data()), NULL);
EVP_CIPHER_CTX_set_padding(cipher, 0); EVP_CIPHER_CTX_set_padding(cipher, 0);
QByteArray plaintext(ciphertext.size(), 0);
int plaintextLen;
EVP_DecryptUpdate(cipher, EVP_DecryptUpdate(cipher,
reinterpret_cast<unsigned char*>(plaintext.data()), reinterpret_cast<unsigned char*>(plaintext.data()),
&plaintextLen, &plaintextLen,
@@ -105,29 +101,6 @@ NvPairingManager::decrypt(const QByteArray& ciphertext, const QByteArray& key)
return plaintext; return plaintext;
} }
QByteArray
NvPairingManager::getSignatureFromCert(X509* cert)
{
#if (OPENSSL_VERSION_NUMBER < 0x10002000L)
ASN1_BIT_STRING *asnSignature = cert->signature;
#elif (OPENSSL_VERSION_NUMBER < 0x10100000L)
ASN1_BIT_STRING *asnSignature;
X509_get0_signature(&asnSignature, NULL, cert);
#else
const ASN1_BIT_STRING *asnSignature;
X509_get0_signature(&asnSignature, NULL, cert);
#endif
return QByteArray(
#if (OPENSSL_VERSION_NUMBER < 0x10100000L)
reinterpret_cast<const char*>(ASN1_STRING_data(asnSignature)),
#else
reinterpret_cast<const char*>(ASN1_STRING_get0_data(asnSignature)),
#endif
ASN1_STRING_length(asnSignature)
);
}
QByteArray QByteArray
NvPairingManager::getSignatureFromPemCert(const QByteArray& certificate) NvPairingManager::getSignatureFromPemCert(const QByteArray& certificate)
{ {
@@ -141,7 +114,18 @@ NvPairingManager::getSignatureFromPemCert(const QByteArray& certificate)
X509* cert = PEM_read_bio_X509(bio, nullptr, nullptr, nullptr); X509* cert = PEM_read_bio_X509(bio, nullptr, nullptr, nullptr);
BIO_free_all(bio); BIO_free_all(bio);
QByteArray signature = getSignatureFromCert(cert); #if (OPENSSL_VERSION_NUMBER < 0x10002000L)
ASN1_BIT_STRING *asnSignature = cert->signature;
#elif (OPENSSL_VERSION_NUMBER < 0x10100000L)
ASN1_BIT_STRING *asnSignature;
X509_get0_signature(&asnSignature, NULL, cert);
#else
const ASN1_BIT_STRING *asnSignature;
X509_get0_signature(&asnSignature, NULL, cert);
#endif
QByteArray signature(reinterpret_cast<char*>(asnSignature->data), asnSignature->length);
X509_free(cert); X509_free(cert);
return signature; return signature;
@@ -200,7 +184,7 @@ NvPairingManager::signMessage(const QByteArray& message)
QByteArray QByteArray
NvPairingManager::saltPin(const QByteArray& salt, QString pin) NvPairingManager::saltPin(const QByteArray& salt, QString pin)
{ {
return QByteArray().append(salt).append(pin.toUtf8()); return QByteArray().append(salt).append(pin.toLatin1());
} }
NvPairingManager::PairState NvPairingManager::PairState
@@ -243,7 +227,8 @@ NvPairingManager::pair(QString appVersion, QString pin, QSslCertificate& serverC
} }
QByteArray serverCertStr = NvHTTP::getXmlStringFromHex(getCert, "plaincert"); QByteArray serverCertStr = NvHTTP::getXmlStringFromHex(getCert, "plaincert");
if (serverCertStr.isEmpty()) { if (serverCertStr == nullptr)
{
qCritical() << "Server likely already pairing"; qCritical() << "Server likely already pairing";
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, REQUEST_TIMEOUT_MS); m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, REQUEST_TIMEOUT_MS);
return PairState::ALREADY_IN_PROGRESS; return PairState::ALREADY_IN_PROGRESS;
@@ -278,18 +263,22 @@ NvPairingManager::pair(QString appVersion, QString pin, QSslCertificate& serverC
} }
QByteArray challengeResponseData = decrypt(m_Http.getXmlStringFromHex(challengeXml, "challengeresponse"), aesKey); QByteArray challengeResponseData = decrypt(m_Http.getXmlStringFromHex(challengeXml, "challengeresponse"), aesKey);
if (challengeResponseData.size() < hashLength) {
qCritical() << "Invalid challengeresponse at stage #2";
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, REQUEST_TIMEOUT_MS);
return PairState::FAILED;
}
QByteArray clientSecretData = generateRandomBytes(16); QByteArray clientSecretData = generateRandomBytes(16);
QByteArray challengeResponse; QByteArray challengeResponse;
QByteArray serverResponse(challengeResponseData.data(), hashLength); QByteArray serverResponse(challengeResponseData.data(), hashLength);
#if (OPENSSL_VERSION_NUMBER < 0x10002000L)
ASN1_BIT_STRING *asnSignature = m_Cert->signature;
#elif (OPENSSL_VERSION_NUMBER < 0x10100000L)
ASN1_BIT_STRING *asnSignature;
X509_get0_signature(&asnSignature, NULL, m_Cert);
#else
const ASN1_BIT_STRING *asnSignature;
X509_get0_signature(&asnSignature, NULL, m_Cert);
#endif
challengeResponse.append(challengeResponseData.data() + hashLength, 16); challengeResponse.append(challengeResponseData.data() + hashLength, 16);
challengeResponse.append(getSignatureFromCert(m_Cert)); challengeResponse.append(reinterpret_cast<char*>(asnSignature->data), asnSignature->length);
challengeResponse.append(clientSecretData); challengeResponse.append(clientSecretData);
QByteArray paddedHash = QCryptographicHash::hash(challengeResponse, hashAlgo); QByteArray paddedHash = QCryptographicHash::hash(challengeResponse, hashAlgo);
@@ -309,14 +298,8 @@ NvPairingManager::pair(QString appVersion, QString pin, QSslCertificate& serverC
} }
QByteArray pairingSecret = NvHTTP::getXmlStringFromHex(respXml, "pairingsecret"); QByteArray pairingSecret = NvHTTP::getXmlStringFromHex(respXml, "pairingsecret");
if (pairingSecret.size() <= 16) { QByteArray serverSecret = QByteArray(pairingSecret.data(), 16);
qCritical() << "Invalid pairingsecret at stage #3"; QByteArray serverSignature = QByteArray(&pairingSecret.data()[16], 256);
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, REQUEST_TIMEOUT_MS);
return PairState::FAILED;
}
QByteArray serverSecret = pairingSecret.left(16);
QByteArray serverSignature = pairingSecret.mid(16);
if (!verifySignature(serverSecret, if (!verifySignature(serverSecret,
serverSignature, serverSignature,
-3
View File
@@ -37,9 +37,6 @@ private:
QByteArray QByteArray
decrypt(const QByteArray& ciphertext, const QByteArray& key); decrypt(const QByteArray& ciphertext, const QByteArray& key);
QByteArray
getSignatureFromCert(X509* cert);
QByteArray QByteArray
getSignatureFromPemCert(const QByteArray& certificate); getSignatureFromPemCert(const QByteArray& certificate);
+90 -119
View File
@@ -12,38 +12,6 @@
#include <Windows.h> #include <Windows.h>
#endif #endif
class SystemPropertyQueryThread : public QThread
{
public:
SystemPropertyQueryThread(SystemProperties* properties)
: QThread(properties), m_Properties(properties)
{
setObjectName("System Properties Async Query Thread");
}
private:
void run() override
{
bool hasHardwareAcceleration;
bool rendererAlwaysFullScreen;
bool supportsHdr;
QSize maximumResolution;
Session::getDecoderInfo(m_Properties->testWindow, hasHardwareAcceleration, rendererAlwaysFullScreen, supportsHdr, maximumResolution);
// Propagate the decoder properties to the SystemProperties singleton and emit any change signals on the main thread
QMetaObject::invokeMethod(m_Properties, "updateDecoderProperties",
Qt::QueuedConnection,
Q_ARG(bool, hasHardwareAcceleration),
Q_ARG(bool, rendererAlwaysFullScreen),
Q_ARG(QSize, maximumResolution),
Q_ARG(bool, supportsHdr));
}
private:
SystemProperties* m_Properties;
};
SystemProperties::SystemProperties() SystemProperties::SystemProperties()
{ {
versionString = QString(VERSION_STR); versionString = QString(VERSION_STR);
@@ -57,8 +25,9 @@ SystemProperties::SystemProperties()
{ {
USHORT processArch, machineArch; USHORT processArch, machineArch;
// Use IsWow64Process2() because it doesn't lie on ARM64 // Use IsWow64Process2 on TH2 and later, because it supports ARM64
if (IsWow64Process2(GetCurrentProcess(), &processArch, &machineArch)) { auto fnIsWow64Process2 = (decltype(IsWow64Process2)*)GetProcAddress(GetModuleHandleA("kernel32.dll"), "IsWow64Process2");
if (fnIsWow64Process2 != nullptr && fnIsWow64Process2(GetCurrentProcess(), &processArch, &machineArch)) {
switch (machineArch) { switch (machineArch) {
case IMAGE_FILE_MACHINE_I386: case IMAGE_FILE_MACHINE_I386:
nativeArch = "i386"; nativeArch = "i386";
@@ -97,86 +66,61 @@ SystemProperties::SystemProperties()
hasDiscordIntegration = false; hasDiscordIntegration = false;
#endif #endif
// These will be queried asynchronously to avoid blocking the UI unmappedGamepads = SdlInputHandler::getUnmappedGamepads();
hasHardwareAcceleration = true;
rendererAlwaysFullScreen = false;
supportsHdr = true;
maximumResolution = QSize(0, 0);
}
SystemProperties::~SystemProperties() // 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.
waitForAsyncLoad(); querySdlVideoInfo();
}
void SystemProperties::updateDecoderProperties(bool hasHardwareAcceleration, bool rendererAlwaysFullScreen, QSize maximumResolution, bool supportsHdr) Q_ASSERT(!monitorRefreshRates.isEmpty());
{ Q_ASSERT(!monitorNativeResolutions.isEmpty());
SDL_assert(testWindow);
if (hasHardwareAcceleration != this->hasHardwareAcceleration) {
this->hasHardwareAcceleration = hasHardwareAcceleration;
emit hasHardwareAccelerationChanged();
}
if (rendererAlwaysFullScreen != this->rendererAlwaysFullScreen) {
this->rendererAlwaysFullScreen = rendererAlwaysFullScreen;
emit rendererAlwaysFullScreenChanged();
}
if (maximumResolution != this->maximumResolution) {
this->maximumResolution = maximumResolution;
emit maximumResolutionChanged();
}
if (supportsHdr != this->supportsHdr) {
this->supportsHdr = supportsHdr;
emit supportsHdrChanged();
}
SDL_DestroyWindow(testWindow);
testWindow = nullptr;
SDL_QuitSubSystem(SDL_INIT_VIDEO);
} }
QRect SystemProperties::getNativeResolution(int displayIndex) QRect SystemProperties::getNativeResolution(int displayIndex)
{ {
// Returns default constructed QRect if out of bounds // Returns default constructed QRect if out of bounds
Q_ASSERT(!monitorNativeResolutions.isEmpty());
return monitorNativeResolutions.value(displayIndex); return monitorNativeResolutions.value(displayIndex);
} }
QRect SystemProperties::getSafeAreaResolution(int displayIndex)
{
// Returns default constructed QRect if out of bounds
Q_ASSERT(!monitorSafeAreaResolutions.isEmpty());
return monitorSafeAreaResolutions.value(displayIndex);
}
int SystemProperties::getRefreshRate(int displayIndex) int SystemProperties::getRefreshRate(int displayIndex)
{ {
// Returns 0 if out of bounds // Returns 0 if out of bounds
Q_ASSERT(!monitorRefreshRates.isEmpty());
return monitorRefreshRates.value(displayIndex); return monitorRefreshRates.value(displayIndex);
} }
void SystemProperties::startAsyncLoad() class QuerySdlVideoThread : public QThread
{ {
if (systemPropertyQueryThread) { public:
// Already started/completed QuerySdlVideoThread(SystemProperties* me) :
return; QThread(nullptr),
m_Me(me) {}
void run() override
{
m_Me->querySdlVideoInfoInternal();
} }
// This isn't actually asynchronous (due to the need to synchronize with SystemProperties* m_Me;
// SdlGamepadKeyNavigation), but we don't query it in the constructor };
// because it's expensive.
unmappedGamepads = SdlInputHandler::getUnmappedGamepads(); void SystemProperties::querySdlVideoInfo()
if (!unmappedGamepads.isEmpty()) { {
emit unmappedGamepadsChanged(); 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;
// We initialize the video subsystem and test window on the main thread
// because some platforms (macOS) do not support window creation on
// non-main threads.
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) { if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s", "SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s",
@@ -184,34 +128,64 @@ void SystemProperties::startAsyncLoad()
return; return;
} }
testWindow = StreamUtils::createTestWindow();
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;
}
// Update display related attributes (max FPS, native resolution, etc). // Update display related attributes (max FPS, native resolution, etc).
// // We call the internal variant because we're already in a safe thread context.
// NB: SDL3 will forcefully refresh displays when a window is created, refreshDisplaysInternal();
// so we place this after the window creation to ensure we don't pay
// the penalty for mode enumeration twice.
refreshDisplays();
systemPropertyQueryThread = new SystemPropertyQueryThread(this); SDL_Window* testWindow = SDL_CreateWindow("", 0, 0, 1280, 720,
systemPropertyQueryThread->start(); SDL_WINDOW_HIDDEN | StreamUtils::getPlatformWindowFlags());
} if (!testWindow) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Failed to create test window with platform flags: %s",
SDL_GetError());
void SystemProperties::waitForAsyncLoad() testWindow = SDL_CreateWindow("", 0, 0, 1280, 720, SDL_WINDOW_HIDDEN);
{ if (!testWindow) {
if (systemPropertyQueryThread) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
systemPropertyQueryThread->wait(); "Failed to create window for hardware decode test: %s",
SDL_GetError());
SDL_QuitSubSystem(SDL_INIT_VIDEO);
return;
}
} }
Session::getDecoderInfo(testWindow, hasHardwareAcceleration, rendererAlwaysFullScreen, supportsHdr, maximumResolution);
SDL_DestroyWindow(testWindow);
SDL_QuitSubSystem(SDL_INIT_VIDEO);
} }
class RefreshDisplaysThread : public QThread
{
public:
RefreshDisplaysThread(SystemProperties* me) :
QThread(nullptr),
m_Me(me) {}
void run() override
{
m_Me->refreshDisplaysInternal();
}
SystemProperties* m_Me;
};
void SystemProperties::refreshDisplays() 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) { if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
@@ -225,12 +199,10 @@ void SystemProperties::refreshDisplays()
SDL_DisplayMode bestMode; SDL_DisplayMode bestMode;
for (int displayIndex = 0; displayIndex < SDL_GetNumVideoDisplays(); displayIndex++) { for (int displayIndex = 0; displayIndex < SDL_GetNumVideoDisplays(); displayIndex++) {
SDL_DisplayMode desktopMode; SDL_DisplayMode desktopMode;
SDL_Rect safeArea;
if (StreamUtils::getNativeDesktopMode(displayIndex, &desktopMode, &safeArea)) { if (StreamUtils::getNativeDesktopMode(displayIndex, &desktopMode)) {
if (desktopMode.w <= 8192 && desktopMode.h <= 8192) { if (desktopMode.w <= 8192 && desktopMode.h <= 8192) {
monitorNativeResolutions.insert(displayIndex, QRect(0, 0, desktopMode.w, desktopMode.h)); monitorNativeResolutions.insert(displayIndex, QRect(0, 0, desktopMode.w, desktopMode.h));
monitorSafeAreaResolutions.insert(displayIndex, QRect(0, 0, safeArea.w, safeArea.h));
} }
else { else {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
@@ -240,8 +212,7 @@ void SystemProperties::refreshDisplays()
// Start at desktop mode and work our way up // Start at desktop mode and work our way up
bestMode = desktopMode; bestMode = desktopMode;
int numDisplayModes = SDL_GetNumDisplayModes(displayIndex); for (int i = 0; i < SDL_GetNumDisplayModes(displayIndex); i++) {
for (int i = 0; i < numDisplayModes; i++) {
SDL_DisplayMode mode; SDL_DisplayMode mode;
if (SDL_GetDisplayMode(displayIndex, i, &mode) == 0) { if (SDL_GetDisplayMode(displayIndex, i, &mode) == 0) {
if (mode.w == desktopMode.w && mode.h == desktopMode.h) { if (mode.w == desktopMode.w && mode.h == desktopMode.h) {
+19 -41
View File
@@ -3,19 +3,18 @@
#include <QObject> #include <QObject>
#include <QRect> #include <QRect>
#include "SDL_compat.h"
class SystemProperties : public QObject class SystemProperties : public QObject
{ {
Q_OBJECT Q_OBJECT
friend class SystemPropertyQueryThread; friend class QuerySdlVideoThread;
friend class RefreshDisplaysThread;
public: public:
SystemProperties(); SystemProperties();
~SystemProperties();
// Static properties queried synchronously during the constructor Q_PROPERTY(bool hasHardwareAcceleration MEMBER hasHardwareAcceleration CONSTANT)
Q_PROPERTY(bool rendererAlwaysFullScreen MEMBER rendererAlwaysFullScreen CONSTANT)
Q_PROPERTY(bool isRunningWayland MEMBER isRunningWayland CONSTANT) Q_PROPERTY(bool isRunningWayland MEMBER isRunningWayland CONSTANT)
Q_PROPERTY(bool isRunningXWayland MEMBER isRunningXWayland CONSTANT) Q_PROPERTY(bool isRunningXWayland MEMBER isRunningXWayland CONSTANT)
Q_PROPERTY(bool isWow64 MEMBER isWow64 CONSTANT) Q_PROPERTY(bool isWow64 MEMBER isWow64 CONSTANT)
@@ -23,40 +22,26 @@ public:
Q_PROPERTY(bool hasDesktopEnvironment MEMBER hasDesktopEnvironment CONSTANT) Q_PROPERTY(bool hasDesktopEnvironment MEMBER hasDesktopEnvironment CONSTANT)
Q_PROPERTY(bool hasBrowser MEMBER hasBrowser CONSTANT) Q_PROPERTY(bool hasBrowser MEMBER hasBrowser CONSTANT)
Q_PROPERTY(bool hasDiscordIntegration MEMBER hasDiscordIntegration CONSTANT) Q_PROPERTY(bool hasDiscordIntegration MEMBER hasDiscordIntegration CONSTANT)
Q_PROPERTY(bool usesMaterial3Theme MEMBER usesMaterial3Theme CONSTANT)
Q_PROPERTY(QString versionString MEMBER versionString CONSTANT)
// Properties queried asynchronously (startAsyncLoad() must be called!)
Q_PROPERTY(bool hasHardwareAcceleration MEMBER hasHardwareAcceleration NOTIFY hasHardwareAccelerationChanged)
Q_PROPERTY(bool rendererAlwaysFullScreen MEMBER rendererAlwaysFullScreen NOTIFY rendererAlwaysFullScreenChanged)
Q_PROPERTY(QString unmappedGamepads MEMBER unmappedGamepads NOTIFY unmappedGamepadsChanged) Q_PROPERTY(QString unmappedGamepads MEMBER unmappedGamepads NOTIFY unmappedGamepadsChanged)
Q_PROPERTY(QSize maximumResolution MEMBER maximumResolution NOTIFY maximumResolutionChanged) Q_PROPERTY(QSize maximumResolution MEMBER maximumResolution CONSTANT)
Q_PROPERTY(bool supportsHdr MEMBER supportsHdr NOTIFY supportsHdrChanged) Q_PROPERTY(QString versionString MEMBER versionString CONSTANT)
Q_PROPERTY(bool supportsHdr MEMBER supportsHdr CONSTANT)
Q_PROPERTY(bool usesMaterial3Theme MEMBER usesMaterial3Theme CONSTANT)
// Either startAsyncLoad()+waitForAsyncLoad() or refreshDisplays() must be invoked first
Q_INVOKABLE QRect getNativeResolution(int displayIndex);
Q_INVOKABLE QRect getSafeAreaResolution(int displayIndex);
Q_INVOKABLE int getRefreshRate(int displayIndex);
Q_INVOKABLE void startAsyncLoad();
Q_INVOKABLE void waitForAsyncLoad();
Q_INVOKABLE void refreshDisplays(); Q_INVOKABLE void refreshDisplays();
Q_INVOKABLE QRect getNativeResolution(int displayIndex);
Q_INVOKABLE int getRefreshRate(int displayIndex);
signals: signals:
void unmappedGamepadsChanged(); void unmappedGamepadsChanged();
void hasHardwareAccelerationChanged();
void rendererAlwaysFullScreenChanged();
void maximumResolutionChanged();
void supportsHdrChanged();
private slots:
void updateDecoderProperties(bool hasHardwareAcceleration, bool rendererAlwaysFullScreen, QSize maximumResolution, bool supportsHdr);
private: private:
QThread* systemPropertyQueryThread = nullptr; void querySdlVideoInfo();
SDL_Window* testWindow = nullptr; void querySdlVideoInfoInternal();
void refreshDisplaysInternal();
// Properties set by the constructor bool hasHardwareAcceleration;
bool rendererAlwaysFullScreen;
bool isRunningWayland; bool isRunningWayland;
bool isRunningXWayland; bool isRunningXWayland;
bool isWow64; bool isWow64;
@@ -64,19 +49,12 @@ private:
bool hasDesktopEnvironment; bool hasDesktopEnvironment;
bool hasBrowser; bool hasBrowser;
bool hasDiscordIntegration; bool hasDiscordIntegration;
QString versionString;
bool usesMaterial3Theme;
// Properties only set if startAsyncLoad() is called
bool hasHardwareAcceleration;
bool rendererAlwaysFullScreen;
QSize maximumResolution;
bool supportsHdr;
QString unmappedGamepads; QString unmappedGamepads;
QSize maximumResolution;
// Properties set by refreshDisplays()
QList<QRect> monitorNativeResolutions; QList<QRect> monitorNativeResolutions;
QList<QRect> monitorSafeAreaResolutions;
QList<int> monitorRefreshRates; QList<int> monitorRefreshRates;
QString versionString;
bool supportsHdr;
bool usesMaterial3Theme;
}; };
+15 -21
View File
@@ -59,6 +59,13 @@ public:
void showMessage(QString message, MessageType type) const void showMessage(QString message, MessageType type) const
{ {
#if defined(Q_OS_WIN32)
UINT flags = MB_OK | MB_TOPMOST | MB_SETFOREGROUND;
flags |= (type == Info ? MB_ICONINFORMATION : MB_ICONERROR);
QString title = "Moonlight";
MessageBoxW(nullptr, reinterpret_cast<const wchar_t *>(message.utf16()),
reinterpret_cast<const wchar_t *>(title.utf16()), flags);
#endif
message = message.endsWith('\n') ? message : message + '\n'; message = message.endsWith('\n') ? message : message + '\n';
fputs(qPrintable(message), type == Info ? stdout : stderr); fputs(qPrintable(message), type == Info ? stdout : stderr);
} }
@@ -106,7 +113,7 @@ public:
QPair<int,int> getResolutionOptionValue(QString name) const QPair<int,int> getResolutionOptionValue(QString name) const
{ {
static QRegularExpression re("^(\\d+)x(\\d+)$", QRegularExpression::CaseInsensitiveOption); QRegularExpression re("^(\\d+)x(\\d+)$", QRegularExpression::CaseInsensitiveOption);
auto match = re.match(value(name)); auto match = re.match(value(name));
if (!match.hasMatch()) { if (!match.hasMatch()) {
showError(QString("Invalid %1 format: %2").arg(name, value(name))); showError(QString("Invalid %1 format: %2").arg(name, value(name)));
@@ -195,7 +202,7 @@ GlobalCommandLineParser::ParseResult GlobalCommandLineParser::parse(const QStrin
} }
} }
parser.showError("Invalid action"); parser.showError(QString("Invalid action"));
} }
} }
@@ -309,7 +316,6 @@ StreamCommandLineParser::StreamCommandLineParser()
{"auto", StreamingPreferences::VCC_AUTO}, {"auto", StreamingPreferences::VCC_AUTO},
{"H.264", StreamingPreferences::VCC_FORCE_H264}, {"H.264", StreamingPreferences::VCC_FORCE_H264},
{"HEVC", StreamingPreferences::VCC_FORCE_HEVC}, {"HEVC", StreamingPreferences::VCC_FORCE_HEVC},
{"AV1", StreamingPreferences::VCC_FORCE_AV1},
}; };
m_VideoDecoderMap = { m_VideoDecoderMap = {
{"auto", StreamingPreferences::VDS_AUTO}, {"auto", StreamingPreferences::VDS_AUTO},
@@ -365,9 +371,6 @@ void StreamCommandLineParser::parse(const QStringList &args, StreamingPreference
parser.addToggleOption("reverse-scroll-direction", "inverted scroll direction"); parser.addToggleOption("reverse-scroll-direction", "inverted scroll direction");
parser.addToggleOption("swap-gamepad-buttons", "swap A/B and X/Y gamepad buttons (Nintendo-style)"); parser.addToggleOption("swap-gamepad-buttons", "swap A/B and X/Y gamepad buttons (Nintendo-style)");
parser.addToggleOption("keep-awake", "prevent display sleep while streaming"); parser.addToggleOption("keep-awake", "prevent display sleep while streaming");
parser.addToggleOption("performance-overlay", "show performance overlay");
parser.addToggleOption("hdr", "HDR streaming");
parser.addToggleOption("yuv444", "YUV 4:4:4 sampling, if supported");
parser.addChoiceOption("capture-system-keys", "capture system key combos", m_CaptureSysKeysModeMap.keys()); parser.addChoiceOption("capture-system-keys", "capture system key combos", m_CaptureSysKeysModeMap.keys());
parser.addChoiceOption("video-codec", "video codec", m_VideoCodecMap.keys()); parser.addChoiceOption("video-codec", "video codec", m_VideoCodecMap.keys());
parser.addChoiceOption("video-decoder", "video decoder", m_VideoDecoderMap.keys()); parser.addChoiceOption("video-decoder", "video decoder", m_VideoDecoderMap.keys());
@@ -379,7 +382,7 @@ void StreamCommandLineParser::parse(const QStringList &args, StreamingPreference
parser.handleUnknownOptions(); parser.handleUnknownOptions();
// Resolve display's width and height // Resolve display's width and height
static QRegularExpression resolutionRexExp("^(720|1080|1440|4K|resolution)$"); QRegularExpression resolutionRexExp("^(720|1080|1440|4K|resolution)$");
QStringList resoOptions = parser.optionNames().filter(resolutionRexExp); QStringList resoOptions = parser.optionNames().filter(resolutionRexExp);
bool displaySet = !resoOptions.isEmpty(); bool displaySet = !resoOptions.isEmpty();
if (displaySet) { if (displaySet) {
@@ -406,20 +409,20 @@ void StreamCommandLineParser::parse(const QStringList &args, StreamingPreference
// Resolve --fps option // Resolve --fps option
if (parser.isSet("fps")) { if (parser.isSet("fps")) {
preferences->fps = parser.getIntOption("fps"); preferences->fps = parser.getIntOption("fps");
if (!inRange(preferences->fps, 10, 480)) { if (!inRange(preferences->fps, 30, 240)) {
fprintf(stderr, "Warning: FPS is out of the supported range (10 - 480 FPS). Performance may suffer!\n"); parser.showError("FPS must be in range: 30 - 240");
} }
} }
// Resolve --bitrate option // Resolve --bitrate option
if (parser.isSet("bitrate")) { if (parser.isSet("bitrate")) {
preferences->bitrateKbps = parser.getIntOption("bitrate"); preferences->bitrateKbps = parser.getIntOption("bitrate");
if (!inRange(preferences->bitrateKbps, 500, 500000)) { if (!inRange(preferences->bitrateKbps, 500, 150000)) {
fprintf(stderr, "Warning: Bitrate is out of the supported range (500 - 500000 Kbps). Performance may suffer!\n"); parser.showError("Bitrate must be in range: 500 - 150000");
} }
} else if (displaySet || parser.isSet("fps")) { } else if (displaySet || parser.isSet("fps")) {
preferences->bitrateKbps = preferences->getDefaultBitrate( preferences->bitrateKbps = preferences->getDefaultBitrate(
preferences->width, preferences->height, preferences->fps, preferences->enableYUV444); preferences->width, preferences->height, preferences->fps);
} }
// Resolve --packet-size option // Resolve --packet-size option
@@ -482,15 +485,6 @@ void StreamCommandLineParser::parse(const QStringList &args, StreamingPreference
// Resolve --keep-awake and --no-keep-awake options // Resolve --keep-awake and --no-keep-awake options
preferences->keepAwake = parser.getToggleOptionValue("keep-awake", preferences->keepAwake); preferences->keepAwake = parser.getToggleOptionValue("keep-awake", preferences->keepAwake);
// Resolve --performance-overlay and --no-performance-overlay options
preferences->showPerformanceOverlay = parser.getToggleOptionValue("performance-overlay", preferences->showPerformanceOverlay);
// Resolve --hdr and --no-hdr options
preferences->enableHdr = parser.getToggleOptionValue("hdr", preferences->enableHdr);
// Resolve --yuv444 and --no-yuv444 options
preferences->enableYUV444 = parser.getToggleOptionValue("yuv444", preferences->enableYUV444);
// Resolve --capture-system-keys option // Resolve --capture-system-keys option
if (parser.isSet("capture-system-keys")) { if (parser.isSet("capture-system-keys")) {
preferences->captureSysKeysMode = mapValue(m_CaptureSysKeysModeMap, parser.getChoiceOptionValue("capture-system-keys")); preferences->captureSysKeysMode = mapValue(m_CaptureSysKeysModeMap, parser.getChoiceOptionValue("capture-system-keys"));
+32 -20
View File
@@ -5,8 +5,10 @@
#include "backend/computerseeker.h" #include "backend/computerseeker.h"
#include <QCoreApplication> #include <QCoreApplication>
#include <QTimer>
#define COMPUTER_SEEK_TIMEOUT 30000 #define COMPUTER_SEEK_TIMEOUT 10000
#define APP_SEEK_TIMEOUT 10000
namespace CliListApps namespace CliListApps
{ {
@@ -14,7 +16,8 @@ namespace CliListApps
enum State { enum State {
StateInit, StateInit,
StateSeekComputer, StateSeekComputer,
StateListApps, StateListApp,
StateSeekApp,
StateFailure, StateFailure,
}; };
@@ -23,6 +26,7 @@ class Event
public: public:
enum Type { enum Type {
ComputerFound, ComputerFound,
ComputerUpdated,
ComputerSeekTimedout, ComputerSeekTimedout,
Executed, Executed,
}; };
@@ -62,6 +66,9 @@ public:
q, &Launcher::onComputerSeekTimeout); q, &Launcher::onComputerSeekTimeout);
m_ComputerSeeker->start(COMPUTER_SEEK_TIMEOUT); m_ComputerSeeker->start(COMPUTER_SEEK_TIMEOUT);
q->connect(m_ComputerManager, &ComputerManager::computerStateChanged,
q, &Launcher::onComputerUpdated);
m_BoxArtManager = new BoxArtManager(q); m_BoxArtManager = new BoxArtManager(q);
if (m_Arguments.isVerbose()) { if (m_Arguments.isVerbose()) {
@@ -81,28 +88,12 @@ public:
case Event::ComputerFound: case Event::ComputerFound:
if (m_State == StateSeekComputer) { if (m_State == StateSeekComputer) {
if (event.computer->pairState == NvComputer::PS_PAIRED) { if (event.computer->pairState == NvComputer::PS_PAIRED) {
m_State = StateListApps; m_State = StateSeekApp;
m_Computer = event.computer; m_Computer = event.computer;
m_TimeoutTimer->start(APP_SEEK_TIMEOUT);
if (m_Arguments.isVerbose()) { if (m_Arguments.isVerbose()) {
fprintf(stdout, "Loading app list...\n"); fprintf(stdout, "Loading app list...\n");
} }
// To avoid race conditions where ComputerSeeker stops async polling, but
// ComputerManager is yet to update the app list, we will explicitly fetch the latest app list.
// Otherwise, it becomes complicated as we would have to guess whether ComputerManager
// would emit 1 signal (the list did not change) or 2 signals (indicating that the list has changed)
try {
NvHTTP http{m_Computer};
const auto appList = http.getAppList();
m_Arguments.isPrintCSV() ? printAppsCSV(appList) : printApps(appList);
QCoreApplication::exit(0);
} catch (std::exception& exception) {
fprintf(stderr, "%s\n", exception.what());
QCoreApplication::exit(1);
}
} else { } else {
m_State = StateFailure; m_State = StateFailure;
fprintf(stderr, "%s\n", qPrintable(QObject::tr("Computer %1 has not been paired. " fprintf(stderr, "%s\n", qPrintable(QObject::tr("Computer %1 has not been paired. "
@@ -113,6 +104,14 @@ public:
} }
} }
break; break;
// Occurs when a computer is updated
case Event::ComputerUpdated:
if (m_State == StateSeekApp) {
m_Arguments.isPrintCSV() ? printAppsCSV(m_Computer->appList) : printApps(m_Computer->appList);
QCoreApplication::exit(0);
}
break;
} }
} }
@@ -147,6 +146,7 @@ public:
BoxArtManager *m_BoxArtManager; BoxArtManager *m_BoxArtManager;
NvComputer *m_Computer; NvComputer *m_Computer;
State m_State; State m_State;
QTimer *m_TimeoutTimer;
ListCommandLineParser m_Arguments; ListCommandLineParser m_Arguments;
}; };
@@ -157,7 +157,11 @@ Launcher::Launcher(QString computer, ListCommandLineParser arguments, QObject *p
Q_D(Launcher); Q_D(Launcher);
d->m_ComputerName = computer; d->m_ComputerName = computer;
d->m_State = StateInit; d->m_State = StateInit;
d->m_TimeoutTimer = new QTimer(this);
d->m_TimeoutTimer->setSingleShot(true);
d->m_Arguments = arguments; d->m_Arguments = arguments;
connect(d->m_TimeoutTimer, &QTimer::timeout,
this, &Launcher::onComputerSeekTimeout);
} }
Launcher::~Launcher() Launcher::~Launcher()
@@ -193,4 +197,12 @@ void Launcher::onComputerSeekTimeout()
d->handleEvent(event); d->handleEvent(event);
} }
void Launcher::onComputerUpdated(NvComputer *computer)
{
Q_D(Launcher);
Event event(Event::ComputerUpdated);
event.computer = computer;
d->handleEvent(event);
}
} }
+1
View File
@@ -27,6 +27,7 @@ public:
private slots: private slots:
void onComputerFound(NvComputer *computer); void onComputerFound(NvComputer *computer);
void onComputerUpdated(NvComputer *computer);
void onComputerSeekTimeout(); void onComputerSeekTimeout();
private: private:
+2 -2
View File
@@ -6,7 +6,7 @@
#include <QCoreApplication> #include <QCoreApplication>
#include <QTimer> #include <QTimer>
#define COMPUTER_SEEK_TIMEOUT 30000 #define COMPUTER_SEEK_TIMEOUT 10000
#define APP_SEEK_TIMEOUT 10000 #define APP_SEEK_TIMEOUT 10000
namespace CliStartStream namespace CliStartStream
@@ -162,7 +162,7 @@ public:
QString getCurrentAppName() const QString getCurrentAppName() const
{ {
for (const NvApp& app : std::as_const(m_Computer->appList)) { for (const NvApp& app : m_Computer->appList) {
if (m_Computer->currentGameId == app.id) { if (m_Computer->currentGameId == app.id) {
return app.name; return app.name;
} }
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<component type="desktop-application"> <component type="desktop">
<id>com.moonlight_stream.Moonlight</id> <id>com.moonlight_stream.Moonlight</id>
<metadata_license>CC0-1.0</metadata_license> <metadata_license>CC0-1.0</metadata_license>
<project_license>GPL-3.0+</project_license> <project_license>GPL-3.0+</project_license>
@@ -10,7 +10,7 @@
<p>Moonlight can stream games and other applications from another PC with Sunshine or GeForce Experience software installed.</p> <p>Moonlight can stream games and other applications from another PC with Sunshine or GeForce Experience software installed.</p>
<p>Features include:</p> <p>Features include:</p>
<ul> <ul>
<li>Hardware accelerated video decoding with VAAPI, VDPAU, and Vulkan Video support</li> <li>Hardware accelerated video decoding with VAAPI, VDPAU, and NVDEC support</li>
<li>H.264, HEVC, and AV1 codec support</li> <li>H.264, HEVC, and AV1 codec support</li>
<li>7.1 surround sound audio support</li> <li>7.1 surround sound audio support</li>
<li>10-point multitouch support</li> <li>10-point multitouch support</li>
@@ -23,106 +23,17 @@
<url type="homepage">https://moonlight-stream.org</url> <url type="homepage">https://moonlight-stream.org</url>
<url type="bugtracker">https://github.com/moonlight-stream/moonlight-qt/issues</url> <url type="bugtracker">https://github.com/moonlight-stream/moonlight-qt/issues</url>
<url type="help">https://github.com/moonlight-stream/moonlight-docs/wiki/Setup-Guide</url> <url type="help">https://github.com/moonlight-stream/moonlight-docs/wiki/Setup-Guide</url>
<url type="vcs-browser">https://github.com/moonlight-stream/moonlight-qt</url>
<developer id="org.moonlight-stream">
<name>Moonlight Game Streaming Team</name>
</developer>
<update_contact>cameron@moonlight-stream.org</update_contact>
<launchable type="desktop-id">com.moonlight_stream.Moonlight.desktop</launchable>
<provides>
<binary>moonlight</binary>
</provides>
<content_rating type="oars-1.1" />
<branding>
<color type="primary" scheme_preference="light">#710085</color>
<color type="primary" scheme_preference="dark">#55285d</color>
</branding>
<screenshots> <screenshots>
<screenshot type="default"> <screenshot type="default">
<image>https://moonlight-stream.org/appstream_resources/moonlight_screenshot_1.png</image> <image>https://moonlight-stream.org/appstream_resources/moonlight_screenshot_1.png</image>
<caption>Host selection page showing local PCs</caption>
</screenshot> </screenshot>
<screenshot> <screenshot>
<image>https://moonlight-stream.org/appstream_resources/moonlight_screenshot_2.png</image> <image>https://moonlight-stream.org/appstream_resources/moonlight_screenshot_2.png</image>
<caption>Settings page displaying streaming options</caption>
</screenshot> </screenshot>
</screenshots> </screenshots>
<releases> <releases>
<release version="6.1.0" date="2024-09-16">
<description>
<p>New features:</p>
<ul>
<li>Experimental YUV 4:4:4 support for improved text clarity during remote desktop usage</li>
<li>HDR is now supported with software decoding (requires GPU with Vulkan support)</li>
<li>Bitrate limit can now be increased to 500 Mbps</li>
<li>Audio decoding now uses 32-bit floating point format</li>
</ul>
<p>Bugfixes:</p>
<ul>
<li>Fixed incorrect color range when streaming with AV1 from a host with an AMD GPU</li>
<li>Updated community-contributed translations from Weblate</li>
</ul>
</description>
</release>
<release version="6.0.1" date="2024-06-30">
<description>
<p>New features:</p>
<ul>
<li>HDR to SDR tonemapping is now supported by the Vulkan renderer on Linux</li>
</ul>
<p>Behavior Changes:</p>
<ul>
<li>More time is now provided for PCs to wake up before displaying an error when starting a stream from the CLI</li>
<li>Moonlight will no longer try to guess VA driver names when running libva 2.20 or later</li>
</ul>
<p>Bugfixes:</p>
<ul>
<li>Fixed crash when the Vulkan renderer executed on a system with no Vulkan devices</li>
<li>Updated community-contributed translations from Weblate</li>
</ul>
</description>
</release>
<release version="6.0.0" date="2024-06-06">
<description>
<p>New features:</p>
<ul>
<li>HDR is now supported on Steam Deck and other HDR-capable Linux systems using a new Vulkan-based renderer</li>
<li>Vulkan Video decoding of H.264, HEVC, and AV1 is now supported using the new Vulkan renderer</li>
<li>Full E2E stream encryption is now supported when streaming from Sunshine v0.22 or later</li>
<li>Initiating a stream via the command-line will now automatically wake the target machine if it is sleeping</li>
<li>Error codes are now displayed in the connection termination dialog</li>
<li>Detailed PC state information is now available via the new "View Details" context menu option (similar to the Android client)</li>
<li>The performance overlay can now be configured to be shown by default when starting a stream</li>
</ul>
<p>Behavior Changes:</p>
<ul>
<li>The selected bitrate is no longer silently adjusted by Moonlight based upon the video codec or HDR mode</li>
<li>The selected mouse mode when starting a stream no longer influences the acceleration curve when the mouse mode is toggled using the Ctrl+Alt+Shift+M key combo while streaming</li>
<li>Hardcoded fallback paths for VAAPI and VDPAU drivers are no longer used</li>
<li>Per-session keys (rikey and rikeyid values) are now automatically stripped from logs</li>
<li>Log messages will now be logged to stderr rather than stdout</li>
<li>EGL is now always used instead of GLX for loading OpenGL in X11 environments on non-x86 platforms</li>
</ul>
<p>Bugfixes:</p>
<ul>
<li>Improved reliability of streaming during momentary drops of WiFi connections</li>
<li>Improved performance with high polling rate mice</li>
<li>Improved input latency when a gamepad is connected</li>
<li>Added missing command-line options to control HDR and AV1 codec usage</li>
<li>Fixed instant connection termination error when streaming over the Internet behind certain Carrier-Grade NAT devices</li>
<li>Fixed incorrect MTU detection when streaming over the Internet without a VPN</li>
<li>Fixed recovery of audio when no audio output device is present when the stream starts</li>
<li>Fixed merging of gamepad state when multi-controller mode is disabled</li>
<li>Fixed indefinitely resending mDNS queries when a host could not be resolved</li>
<li>Fixed inability to pair when the host is not using a RSA-2048 key</li>
<li>Updated included gamepad mappings</li>
<li>Updated community-contributed translations from Weblate</li>
</ul>
</description>
</release>
<release version="5.0.1" date="2023-11-02"> <release version="5.0.1" date="2023-11-02">
<description> <description>
<p>Bugfixes:</p> <p>Bugfixes:</p>
@@ -833,4 +744,19 @@
</release> </release>
</releases> </releases>
<categories>
<category>Game</category>
<category>Qt</category>
</categories>
<developer_name>Moonlight Game Streaming Team</developer_name>
<update_contact>cameron@moonlight-stream.org</update_contact>
<launchable type="desktop-id">com.moonlight_stream.Moonlight.desktop</launchable>
<provides>
<binary>moonlight</binary>
</provides>
<content_rating type="oars-1.0" />
</component> </component>
@@ -5,5 +5,5 @@ Exec=moonlight
Icon=moonlight Icon=moonlight
Terminal=false Terminal=false
Type=Application Type=Application
Categories=Qt;Game;Network;RemoteAccess; Categories=Qt;Game;
Keywords=nvidia;gamestream;stream;sunshine;remote play; Keywords=nvidia;gamestream;stream;sunshine;remote play;
+1 -5
View File
@@ -5,13 +5,9 @@
# pairing data will be lost each reboot. # pairing data will be lost each reboot.
HOME=/usr/local/moonlight HOME=/usr/local/moonlight
# Enable Steam Controller detection in SDL
# https://github.com/moonlight-stream/moonlight-qt/issues/697
export SDL_ENABLE_STEAM_CONTROLLERS=1
# Renice PE_Single_CPU which seems to host A/V stuff # Renice PE_Single_CPU which seems to host A/V stuff
renice -10 -p $(pidof PE_Single_CPU) renice -10 -p $(pidof PE_Single_CPU)
# Renice Moonlight itself to avoid preemption by background tasks # Renice Moonlight itself to avoid preemption by background tasks
# Write output to a logfile in /tmp # Write output to a logfile in /tmp
exec nice -n -10 ./bin/moonlight > /tmp/moonlight.log 2>&1 exec nice -n -10 ./bin/moonlight > /tmp/moonlight.log
+26 -32
View File
@@ -38,7 +38,7 @@ CenteredGridView {
activated = true activated = true
// Highlight the first item if a gamepad is connected // Highlight the first item if a gamepad is connected
if (currentIndex === -1 && SdlGamepadKeyNavigation.getConnectedGamepads() > 0) { if (currentIndex == -1 && SdlGamepadKeyNavigation.getConnectedGamepads() > 0) {
currentIndex = 0 currentIndex = 0
} }
@@ -94,9 +94,9 @@ CenteredGridView {
// the image size checks if this is not an app collector game. We know the officially // the image size checks if this is not an app collector game. We know the officially
// supported games all have box art, so this check is not required. // supported games all have box art, so this check is not required.
if (!model.isAppCollectorGame && if (!model.isAppCollectorGame &&
((sourceSize.width === 130 && sourceSize.height === 180) || // GFE 2.0 placeholder image ((sourceSize.width == 130 && sourceSize.height == 180) || // GFE 2.0 placeholder image
(sourceSize.width === 628 && sourceSize.height === 888) || // GFE 3.0 placeholder image (sourceSize.width == 628 && sourceSize.height == 888) || // GFE 3.0 placeholder image
(sourceSize.width === 200 && sourceSize.height === 266))) // Our no_app_image.png (sourceSize.width == 200 && sourceSize.height == 266))) // Our no_app_image.png
{ {
isPlaceholder = true isPlaceholder = true
} }
@@ -123,18 +123,20 @@ CenteredGridView {
sourceComponent: Item { sourceComponent: Item {
RoundButton { RoundButton {
// Don't steal focus from the toolbar buttons
focusPolicy: Qt.NoFocus
anchors.horizontalCenterOffset: appIcon.isPlaceholder ? -47 : 0 anchors.horizontalCenterOffset: appIcon.isPlaceholder ? -47 : 0
anchors.verticalCenterOffset: appIcon.isPlaceholder ? -75 : -60 anchors.verticalCenterOffset: appIcon.isPlaceholder ? -75 : -60
anchors.centerIn: parent anchors.centerIn: parent
implicitWidth: 85 implicitWidth: 85
implicitHeight: 85 implicitHeight: 85
icon.source: "qrc:/res/play_arrow_FILL1_wght700_GRAD200_opsz48.svg" Image {
icon.width: 75 source: "qrc:/res/play_arrow_FILL1_wght700_GRAD200_opsz48.svg"
icon.height: 75 anchors.centerIn: parent
sourceSize {
width: 75
height: 75
}
}
onClicked: { onClicked: {
launchOrResumeSelectedApp(true) launchOrResumeSelectedApp(true)
@@ -149,18 +151,20 @@ CenteredGridView {
} }
RoundButton { RoundButton {
// Don't steal focus from the toolbar buttons
focusPolicy: Qt.NoFocus
anchors.horizontalCenterOffset: appIcon.isPlaceholder ? 47 : 0 anchors.horizontalCenterOffset: appIcon.isPlaceholder ? 47 : 0
anchors.verticalCenterOffset: appIcon.isPlaceholder ? -75 : 60 anchors.verticalCenterOffset: appIcon.isPlaceholder ? -75 : 60
anchors.centerIn: parent anchors.centerIn: parent
implicitWidth: 85 implicitWidth: 85
implicitHeight: 85 implicitHeight: 85
icon.source: "qrc:/res/stop_FILL1_wght700_GRAD200_opsz48.svg" Image {
icon.width: 75 source: "qrc:/res/stop_FILL1_wght700_GRAD200_opsz48.svg"
icon.height: 75 anchors.centerIn: parent
sourceSize {
width: 75
height: 75
}
}
onClicked: { onClicked: {
doQuitGame() doQuitGame()
@@ -252,7 +256,7 @@ CenteredGridView {
anchors.fill: parent anchors.fill: parent
acceptedButtons: Qt.RightButton; acceptedButtons: Qt.RightButton;
onClicked: { onClicked: {
parent.pressAndHold() parent.onPressAndHold()
} }
} }
@@ -294,17 +298,19 @@ CenteredGridView {
asynchronous: true asynchronous: true
sourceComponent: NavigableMenu { sourceComponent: NavigableMenu {
id: appContextMenu id: appContextMenu
initiator: appContextMenuLoader.parent
NavigableMenuItem { NavigableMenuItem {
parentMenu: appContextMenu
text: model.running ? qsTr("Resume Game") : qsTr("Launch Game") text: model.running ? qsTr("Resume Game") : qsTr("Launch Game")
onTriggered: launchOrResumeSelectedApp(true) onTriggered: launchOrResumeSelectedApp(true)
} }
NavigableMenuItem { NavigableMenuItem {
parentMenu: appContextMenu
text: qsTr("Quit Game") text: qsTr("Quit Game")
onTriggered: doQuitGame() onTriggered: doQuitGame()
visible: model.running visible: model.running
} }
NavigableMenuItem { NavigableMenuItem {
parentMenu: appContextMenu
checkable: true checkable: true
checked: model.directLaunch checked: model.directLaunch
text: qsTr("Direct Launch") text: qsTr("Direct Launch")
@@ -317,6 +323,7 @@ CenteredGridView {
ToolTip.visible: hovered ToolTip.visible: hovered
} }
NavigableMenuItem { NavigableMenuItem {
parentMenu: appContextMenu
checkable: true checkable: true
checked: model.hidden checked: model.hidden
text: qsTr("Hide Game") text: qsTr("Hide Game")
@@ -332,19 +339,6 @@ CenteredGridView {
} }
} }
Row {
anchors.centerIn: parent
spacing: 5
visible: appGrid.count === 0
Label {
text: qsTr("This computer doesn't seem to have any applications or some applications are hidden")
font.pointSize: 20
verticalAlignment: Text.AlignVCenter
wrapMode: Text.Wrap
}
}
NavigableMessageDialog { NavigableMessageDialog {
id: quitAppDialog id: quitAppDialog
property string appName : "" property string appName : ""
@@ -356,7 +350,7 @@ CenteredGridView {
function quitApp() { function quitApp() {
var component = Qt.createComponent("QuitSegue.qml") var component = Qt.createComponent("QuitSegue.qml")
var params = {"appName": appName, "quitRunningAppFn": function() { appModel.quitRunningApp() }} var params = {"appName": appName, "quitRunningAppFn": appModel.quitRunningApp}
if (segueToStream) { if (segueToStream) {
// Store the session and app name if we're going to stream after // Store the session and app name if we're going to stream after
// successfully quitting the old app. // successfully quitting the old app.
-8
View File
@@ -50,12 +50,4 @@ ComboBox {
popup.onAboutToHide: { popup.onAboutToHide: {
SdlGamepadKeyNavigation.setUiNavMode(true) SdlGamepadKeyNavigation.setUiNavMode(true)
} }
Keys.onLeftPressed: {
decrementCurrentIndex()
}
Keys.onRightPressed: {
incrementCurrentIndex()
}
} }
+18
View File
@@ -2,6 +2,10 @@ import QtQuick 2.9
import QtQuick.Controls 2.2 import QtQuick.Controls 2.2
GridView { 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 int minMargin: 10
property real availableWidth: (parent.width - 2 * minMargin) property real availableWidth: (parent.width - 2 * minMargin)
property int itemsPerRow: availableWidth / cellWidth property int itemsPerRow: availableWidth / cellWidth
@@ -11,6 +15,11 @@ GridView {
function updateMargins() { function updateMargins() {
leftMargin = horizontalMargin leftMargin = horizontalMargin
rightMargin = horizontalMargin rightMargin = horizontalMargin
if (hasBrokenMargins) {
anchors.leftMargin = leftMargin
anchors.rightMargin = rightMargin
}
} }
onHorizontalMarginChanged: { onHorizontalMarginChanged: {
@@ -18,6 +27,15 @@ GridView {
} }
Component.onCompleted: { 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() updateMargins()
} }
+5 -1
View File
@@ -2,6 +2,7 @@ import QtQuick 2.0
import QtQuick.Controls 2.2 import QtQuick.Controls 2.2
import ComputerManager 1.0 import ComputerManager 1.0
import SdlGamepadKeyNavigation 1.0
Item { Item {
function onSearchingComputer() { function onSearchingComputer() {
@@ -38,6 +39,10 @@ Item {
if (!launcher.isExecuted()) { if (!launcher.isExecuted()) {
toolBar.visible = false toolBar.visible = false
// Normally this is enabled by PcView, but we will won't
// load PcView when streaming from the command-line.
SdlGamepadKeyNavigation.enable()
launcher.searchingComputer.connect(onSearchingComputer) launcher.searchingComputer.connect(onSearchingComputer)
launcher.pairing.connect(onPairing) launcher.pairing.connect(onPairing)
launcher.failed.connect(onFailed) launcher.failed.connect(onFailed)
@@ -53,7 +58,6 @@ Item {
BusyIndicator { BusyIndicator {
id: stageSpinner id: stageSpinner
running: visible
} }
Label { Label {
-1
View File
@@ -34,7 +34,6 @@ Item {
BusyIndicator { BusyIndicator {
id: stageSpinner id: stageSpinner
running: visible
} }
Label { Label {
+6 -2
View File
@@ -2,6 +2,7 @@ import QtQuick 2.0
import QtQuick.Controls 2.2 import QtQuick.Controls 2.2
import ComputerManager 1.0 import ComputerManager 1.0
import SdlGamepadKeyNavigation 1.0
Item { Item {
function onSearchingComputer() { function onSearchingComputer() {
@@ -37,6 +38,10 @@ Item {
if (!launcher.isExecuted()) { if (!launcher.isExecuted()) {
toolBar.visible = false toolBar.visible = false
// Normally this is enabled by PcView, but we will won't
// load PcView when streaming from the command-line.
SdlGamepadKeyNavigation.enable()
launcher.searchingComputer.connect(onSearchingComputer) launcher.searchingComputer.connect(onSearchingComputer)
launcher.searchingApp.connect(onSearchingApp) launcher.searchingApp.connect(onSearchingApp)
launcher.sessionCreated.connect(onSessionCreated) launcher.sessionCreated.connect(onSessionCreated)
@@ -52,7 +57,6 @@ Item {
BusyIndicator { BusyIndicator {
id: stageSpinner id: stageSpinner
running: visible
} }
Label { Label {
@@ -81,7 +85,7 @@ Item {
function quitApp() { function quitApp() {
var component = Qt.createComponent("QuitSegue.qml") var component = Qt.createComponent("QuitSegue.qml")
var params = {"appName": appName, "quitRunningAppFn": function() { launcher.quitRunningApp() }} var params = {"appName": appName, "quitRunningAppFn": launcher.quitRunningApp}
stackView.push(component.createObject(stackView, params)) stackView.push(component.createObject(stackView, params))
} }
+10 -6
View File
@@ -1,13 +1,17 @@
import QtQuick 2.0 import QtQuick 2.0
import QtQuick.Controls 2.5 import QtQuick.Controls 2.2
Dialog { Dialog {
modal: true // We should use Overlay.overlay here but that's not available in Qt 5.9 :(
anchors.centerIn: Overlay.overlay parent: ApplicationWindow.contentItem
onClosed: { x: Math.round((parent.width - width) / 2)
// We must force focus back to the last item. If we don't, y: Math.round((parent.height - height) / 2)
// gamepad and keyboard navigation will break after a
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. // dialog appears.
stackView.forceActiveFocus() stackView.forceActiveFocus()
} }
-5
View File
@@ -17,11 +17,6 @@ ItemDelegate {
} }
Keys.onUpPressed: { Keys.onUpPressed: {
grid.moveCurrentIndexUp() grid.moveCurrentIndexUp()
// If we've reached the top of the grid, move focus to the toolbar
if (grid.currentItem === this) {
nextItemInFocusChain(false).forceActiveFocus(Qt.TabFocus)
}
} }
Keys.onReturnPressed: { Keys.onReturnPressed: {
clicked() clicked()
+6 -11
View File
@@ -2,18 +2,13 @@ import QtQuick 2.0
import QtQuick.Controls 2.2 import QtQuick.Controls 2.2
Menu { Menu {
property var initiator
onOpened: { onOpened: {
// If the initiating object currently has keyboard focus, // Give focus to the first visible and enabled menu item
// give focus to the first visible and enabled menu item for (var i = 0; i < count; i++) {
if (initiator.focus) { var item = itemAt(i)
for (var i = 0; i < count; i++) { if (item.visible && item.enabled) {
var item = itemAt(i) item.forceActiveFocus(Qt.TabFocusReason)
if (item.visible && item.enabled) { break
item.forceActiveFocus(Qt.TabFocusReason)
break
}
} }
} }
} }
+6 -2
View File
@@ -2,6 +2,10 @@ import QtQuick 2.0
import QtQuick.Controls 2.2 import QtQuick.Controls 2.2
MenuItem { MenuItem {
// Qt 5.10 has a menu property, but we need to support 5.9
// so we must make our own.
property Menu parentMenu
// Ensure focus can't be given to an invisible item // Ensure focus can't be given to an invisible item
enabled: visible enabled: visible
height: visible ? implicitHeight : 0 height: visible ? implicitHeight : 0
@@ -11,7 +15,7 @@ MenuItem {
// We must close the context menu first or // We must close the context menu first or
// it can steal focus from any dialogs that // it can steal focus from any dialogs that
// onTriggered may spawn. // onTriggered may spawn.
menu.close() parentMenu.close()
} }
Keys.onReturnPressed: { Keys.onReturnPressed: {
@@ -23,6 +27,6 @@ MenuItem {
} }
Keys.onEscapePressed: { Keys.onEscapePressed: {
menu.close() parentMenu.close()
} }
} }
+13 -13
View File
@@ -15,9 +15,7 @@ NavigableDialog {
onOpened: { onOpened: {
// Force keyboard focus on the label so keyboard navigation works // Force keyboard focus on the label so keyboard navigation works
if (dialogButtonBox.count > 0) { dialogLabel.forceActiveFocus()
dialogButtonBox.itemAt(dialogButtonBox.count - 1).forceActiveFocus(Qt.TabFocus)
}
} }
RowLayout { RowLayout {
@@ -26,7 +24,6 @@ NavigableDialog {
BusyIndicator { BusyIndicator {
id: dialogSpinner id: dialogSpinner
visible: false visible: false
running: visible
} }
Image { Image {
@@ -54,6 +51,18 @@ NavigableDialog {
// will cause word wrap to kick in. // will cause word wrap to kick in.
Layout.maximumWidth: 400 Layout.maximumWidth: 400
Layout.maximumHeight: 400 Layout.maximumHeight: 400
Keys.onReturnPressed: {
accept()
}
Keys.onEnterPressed: {
accept()
}
Keys.onEscapePressed: {
reject()
}
} }
} }
@@ -61,15 +70,6 @@ NavigableDialog {
id: dialogButtonBox id: dialogButtonBox
standardButtons: dialog.standardButtons standardButtons: dialog.standardButtons
delegate: Button {
flat: true
Keys.onReturnPressed: clicked()
Keys.onEnterPressed: clicked()
Keys.onRightPressed: nextItemInFocusChain(true).forceActiveFocus(Qt.TabFocus)
Keys.onLeftPressed: nextItemInFocusChain(false).forceActiveFocus(Qt.TabFocus)
}
onHelpRequested: { onHelpRequested: {
Qt.openUrlExternally(helpUrl) Qt.openUrlExternally(helpUrl)
close() close()
+11 -3
View File
@@ -7,9 +7,17 @@ ToolButton {
activeFocusOnTab: true activeFocusOnTab: true
icon.source: iconSource // FIXME: We're using an Image here rather than icon.source because
icon.width: background.width // icons don't work on Qt 5.9 LTS.
icon.height: background.height 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 // This determines the size of the Material highlight. We increase it
// from the default because we use larger than normal icons for TV readability. // from the default because we use larger than normal icons for TV readability.
+16 -21
View File
@@ -6,7 +6,6 @@ import ComputerModel 1.0
import ComputerManager 1.0 import ComputerManager 1.0
import StreamingPreferences 1.0 import StreamingPreferences 1.0
import SystemProperties 1.0
import SdlGamepadKeyNavigation 1.0 import SdlGamepadKeyNavigation 1.0
CenteredGridView { CenteredGridView {
@@ -34,8 +33,13 @@ CenteredGridView {
// Setup signals on CM // Setup signals on CM
ComputerManager.computerAddCompleted.connect(addComplete) ComputerManager.computerAddCompleted.connect(addComplete)
// This is a bit of a hack to do this here as opposed to main.qml, but
// we need it enabled before calling getConnectedGamepads() and PcView
// is never destroyed, so it should be okay.
SdlGamepadKeyNavigation.enable()
// Highlight the first item if a gamepad is connected // Highlight the first item if a gamepad is connected
if (currentIndex === -1 && SdlGamepadKeyNavigation.getConnectedGamepads() > 0) { if (currentIndex == -1 && SdlGamepadKeyNavigation.getConnectedGamepads() > 0) {
currentIndex = 0 currentIndex = 0
} }
} }
@@ -90,7 +94,6 @@ CenteredGridView {
BusyIndicator { BusyIndicator {
id: searchSpinner id: searchSpinner
visible: StreamingPreferences.enableMdns visible: StreamingPreferences.enableMdns
running: visible
} }
Label { Label {
@@ -144,7 +147,6 @@ CenteredGridView {
width: 75 width: 75
height: 75 height: 75
visible: model.statusUnknown visible: model.statusUnknown
running: visible
} }
Label { Label {
@@ -165,13 +167,13 @@ CenteredGridView {
asynchronous: true asynchronous: true
sourceComponent: NavigableMenu { sourceComponent: NavigableMenu {
id: pcContextMenu id: pcContextMenu
initiator: pcContextMenuLoader.parent
MenuItem { MenuItem {
text: qsTr("PC Status: %1").arg(model.online ? qsTr("Online") : qsTr("Offline")) text: qsTr("PC Status: %1").arg(model.online ? qsTr("Online") : qsTr("Offline"))
font.bold: true font.bold: true
enabled: false enabled: false
} }
NavigableMenuItem { NavigableMenuItem {
parentMenu: pcContextMenu
text: qsTr("View All Apps") text: qsTr("View All Apps")
onTriggered: { onTriggered: {
var component = Qt.createComponent("AppView.qml") var component = Qt.createComponent("AppView.qml")
@@ -181,11 +183,13 @@ CenteredGridView {
visible: model.online && model.paired visible: model.online && model.paired
} }
NavigableMenuItem { NavigableMenuItem {
parentMenu: pcContextMenu
text: qsTr("Wake PC") text: qsTr("Wake PC")
onTriggered: computerModel.wakeComputer(index) onTriggered: computerModel.wakeComputer(index)
visible: !model.online && model.wakeable visible: !model.online && model.wakeable
} }
NavigableMenuItem { NavigableMenuItem {
parentMenu: pcContextMenu
text: qsTr("Test Network") text: qsTr("Test Network")
onTriggered: { onTriggered: {
computerModel.testConnectionForComputer(index) computerModel.testConnectionForComputer(index)
@@ -194,6 +198,7 @@ CenteredGridView {
} }
NavigableMenuItem { NavigableMenuItem {
parentMenu: pcContextMenu
text: qsTr("Rename PC") text: qsTr("Rename PC")
onTriggered: { onTriggered: {
renamePcDialog.pcIndex = index renamePcDialog.pcIndex = index
@@ -202,6 +207,7 @@ CenteredGridView {
} }
} }
NavigableMenuItem { NavigableMenuItem {
parentMenu: pcContextMenu
text: qsTr("Delete PC") text: qsTr("Delete PC")
onTriggered: { onTriggered: {
deletePcDialog.pcIndex = index deletePcDialog.pcIndex = index
@@ -209,13 +215,6 @@ CenteredGridView {
deletePcDialog.open() deletePcDialog.open()
} }
} }
NavigableMenuItem {
text: qsTr("View Details")
onTriggered: {
showPcDetailsDialog.pcDetails = model.details
showPcDetailsDialog.open()
}
}
} }
} }
@@ -263,7 +262,7 @@ CenteredGridView {
anchors.fill: parent anchors.fill: parent
acceptedButtons: Qt.RightButton; acceptedButtons: Qt.RightButton;
onClicked: { onClicked: {
parent.pressAndHold() parent.onPressAndHold()
} }
} }
@@ -290,6 +289,10 @@ CenteredGridView {
NavigableMessageDialog { NavigableMessageDialog {
id: pairDialog id: pairDialog
// Pairing dialog must be modal to prevent double-clicks from triggering
// pairing twice
modal: true
closePolicy: Popup.CloseOnEscape closePolicy: Popup.CloseOnEscape
// don't allow edits to the rest of the window while open // don't allow edits to the rest of the window while open
@@ -392,13 +395,5 @@ CenteredGridView {
} }
} }
NavigableMessageDialog {
id: showPcDetailsDialog
property string pcDetails : "";
text: showPcDetailsDialog.pcDetails
imageSrc: "qrc:/res/baseline-help_outline-24px.svg"
standardButtons: Dialog.Ok
}
ScrollBar.vertical: ScrollBar {} ScrollBar.vertical: ScrollBar {}
} }
-1
View File
@@ -60,7 +60,6 @@ Item {
BusyIndicator { BusyIndicator {
id: stageSpinner id: stageSpinner
running: visible
} }
Label { Label {
+168 -270
View File
@@ -133,45 +133,15 @@ Flickable {
AutoResizingComboBox { AutoResizingComboBox {
property int lastIndexValue property int lastIndexValue
function addDetectedResolution(friendlyNamePrefix, rect) {
var indexToAdd = 0
for (var j = 0; j < resolutionComboBox.count; j++) {
var existing_width = parseInt(resolutionListModel.get(j).video_width);
var existing_height = parseInt(resolutionListModel.get(j).video_height);
if (rect.width === existing_width && rect.height === existing_height) {
// Duplicate entry, skip
indexToAdd = -1
break
}
else if (rect.width * rect.height > existing_width * existing_height) {
// Candidate entrypoint after this entry
indexToAdd = j + 1
}
}
// Insert this display's resolution if it's not a duplicate
if (indexToAdd >= 0) {
resolutionListModel.insert(indexToAdd,
{
"text": friendlyNamePrefix+" ("+rect.width+"x"+rect.height+")",
"video_width": ""+rect.width,
"video_height": ""+rect.height,
"is_custom": false
})
}
}
// ignore setting the index at first, and actually set it when the component is loaded // ignore setting the index at first, and actually set it when the component is loaded
Component.onCompleted: { Component.onCompleted: {
// Refresh display data before using it to build the list // Refresh display data before using it to build the list
SystemProperties.refreshDisplays() SystemProperties.refreshDisplays()
// Add native and safe area resolutions for all attached displays // Add native resolutions for all attached displays
var done = false var done = false
for (var displayIndex = 0; !done; displayIndex++) { for (var displayIndex = 0; !done; displayIndex++) {
var screenRect = SystemProperties.getNativeResolution(displayIndex); var screenRect = SystemProperties.getNativeResolution(displayIndex);
var safeAreaRect = SystemProperties.getSafeAreaResolution(displayIndex);
if (screenRect.width === 0) { if (screenRect.width === 0) {
// Exceeded max count of displays // Exceeded max count of displays
@@ -179,8 +149,32 @@ Flickable {
break break
} }
addDetectedResolution(qsTr("Native"), screenRect) var indexToAdd = 0
addDetectedResolution(qsTr("Native (Excluding Notch)"), safeAreaRect) for (var j = 0; j < resolutionComboBox.count; j++) {
var existing_width = parseInt(resolutionListModel.get(j).video_width);
var existing_height = parseInt(resolutionListModel.get(j).video_height);
if (screenRect.width === existing_width && screenRect.height === existing_height) {
// Duplicate entry, skip
indexToAdd = -1
break
}
else if (screenRect.width * screenRect.height > existing_width * existing_height) {
// Candidate entrypoint after this entry
indexToAdd = j + 1
}
}
// Insert this display's resolution if it's not a duplicate
if (indexToAdd >= 0) {
resolutionListModel.insert(indexToAdd,
{
"text": "Native ("+screenRect.width+"x"+screenRect.height+")",
"video_width": ""+screenRect.width,
"video_height": ""+screenRect.height,
"is_custom": false
})
}
} }
// Prune resolutions that are over the decoder's maximum // Prune resolutions that are over the decoder's maximum
@@ -216,7 +210,7 @@ Flickable {
if (!index_set) { if (!index_set) {
// We did not find a match. This must be a custom resolution. // We did not find a match. This must be a custom resolution.
resolutionListModel.append({ resolutionListModel.append({
"text": qsTr("Custom")+" ("+StreamingPreferences.width+"x"+StreamingPreferences.height+")", "text": "Custom ("+StreamingPreferences.width+"x"+StreamingPreferences.height+")",
"video_width": ""+StreamingPreferences.width, "video_width": ""+StreamingPreferences.width,
"video_height": ""+StreamingPreferences.height, "video_height": ""+StreamingPreferences.height,
"is_custom": true "is_custom": true
@@ -225,7 +219,7 @@ Flickable {
} }
else { else {
resolutionListModel.append({ resolutionListModel.append({
"text": qsTr("Custom"), "text": "Custom",
"video_width": "", "video_width": "",
"video_height": "", "video_height": "",
"is_custom": true "is_custom": true
@@ -281,13 +275,10 @@ Flickable {
StreamingPreferences.width = selectedWidth StreamingPreferences.width = selectedWidth
StreamingPreferences.height = selectedHeight StreamingPreferences.height = selectedHeight
if (StreamingPreferences.autoAdjustBitrate) { StreamingPreferences.bitrateKbps = StreamingPreferences.getDefaultBitrate(StreamingPreferences.width,
StreamingPreferences.bitrateKbps = StreamingPreferences.getDefaultBitrate(StreamingPreferences.width, StreamingPreferences.height,
StreamingPreferences.height, StreamingPreferences.fps);
StreamingPreferences.fps, slider.value = StreamingPreferences.bitrateKbps
StreamingPreferences.enableYUV444);
slider.value = StreamingPreferences.bitrateKbps
}
} }
lastIndexValue = currentIndex lastIndexValue = currentIndex
@@ -449,13 +440,10 @@ Flickable {
if (StreamingPreferences.fps !== selectedFps) { if (StreamingPreferences.fps !== selectedFps) {
StreamingPreferences.fps = selectedFps StreamingPreferences.fps = selectedFps
if (StreamingPreferences.autoAdjustBitrate) { StreamingPreferences.bitrateKbps = StreamingPreferences.getDefaultBitrate(StreamingPreferences.width,
StreamingPreferences.bitrateKbps = StreamingPreferences.getDefaultBitrate(StreamingPreferences.width, StreamingPreferences.height,
StreamingPreferences.height, StreamingPreferences.fps);
StreamingPreferences.fps, slider.value = StreamingPreferences.bitrateKbps
StreamingPreferences.enableYUV444);
slider.value = StreamingPreferences.bitrateKbps
}
} }
lastIndexValue = currentIndex lastIndexValue = currentIndex
@@ -682,47 +670,26 @@ Flickable {
wrapMode: Text.Wrap wrapMode: Text.Wrap
} }
Row { Slider {
width: parent.width id: slider
spacing: 5
Slider { value: StreamingPreferences.bitrateKbps
id: slider
value: StreamingPreferences.bitrateKbps stepSize: 500
from : 500
to: 150000
stepSize: 500 snapMode: "SnapOnRelease"
from : 500 width: Math.min(bitrateDesc.implicitWidth, parent.width)
to: StreamingPreferences.unlockBitrate ? 500000 : 150000
snapMode: "SnapOnRelease" onValueChanged: {
width: Math.min(bitrateDesc.implicitWidth, parent.width - (resetBitrateButton.visible ? resetBitrateButton.width + parent.spacing : 0)) bitrateTitle.text = qsTr("Video bitrate: %1 Mbps").arg(value / 1000.0)
StreamingPreferences.bitrateKbps = value
onValueChanged: {
bitrateTitle.text = qsTr("Video bitrate: %1 Mbps").arg(value / 1000.0)
StreamingPreferences.bitrateKbps = value
}
onMoved: {
StreamingPreferences.autoAdjustBitrate = false
}
Component.onCompleted: {
// Refresh the text after translations change
languageChanged.connect(valueChanged)
}
} }
Button { Component.onCompleted: {
id: resetBitrateButton // Refresh the text after translations change
text: qsTr("Use Default (%1 Mbps)").arg(StreamingPreferences.getDefaultBitrate(StreamingPreferences.width, StreamingPreferences.height, StreamingPreferences.fps, StreamingPreferences.enableYUV444) / 1000.0) languageChanged.connect(onValueChanged)
visible: StreamingPreferences.bitrateKbps !== StreamingPreferences.getDefaultBitrate(StreamingPreferences.width, StreamingPreferences.height, StreamingPreferences.fps, StreamingPreferences.enableYUV444)
onClicked: {
var defaultBitrate = StreamingPreferences.getDefaultBitrate(StreamingPreferences.width, StreamingPreferences.height, StreamingPreferences.fps, StreamingPreferences.enableYUV444)
StreamingPreferences.bitrateKbps = defaultBitrate
StreamingPreferences.autoAdjustBitrate = true
slider.value = defaultBitrate
}
} }
} }
@@ -812,64 +779,38 @@ Flickable {
ToolTip.text: qsTr("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.") ToolTip.text: qsTr("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.")
} }
Row {
spacing: 5
width: parent.width
CheckBox {
id: vsyncCheck
hoverEnabled: true
text: qsTr("V-Sync")
font.pointSize: 12
checked: StreamingPreferences.enableVsync
onCheckedChanged: {
StreamingPreferences.enableVsync = checked
}
ToolTip.delay: 1000
ToolTip.timeout: 5000
ToolTip.visible: hovered
ToolTip.text: qsTr("Disabling V-Sync allows sub-frame rendering latency, but it can display visible tearing")
}
CheckBox {
id: framePacingCheck
hoverEnabled: true
text: qsTr("Frame pacing")
font.pointSize: 12
enabled: StreamingPreferences.enableVsync
checked: StreamingPreferences.enableVsync && StreamingPreferences.framePacing
onCheckedChanged: {
StreamingPreferences.framePacing = checked
}
ToolTip.delay: 1000
ToolTip.timeout: 5000
ToolTip.visible: hovered
ToolTip.text: qsTr("Frame pacing reduces micro-stutter by delaying frames that come in too early")
}
}
CheckBox { CheckBox {
id: enableHdr id: vsyncCheck
width: parent.width width: parent.width
text: qsTr("Enable HDR") hoverEnabled: true
font.pointSize: 12 text: qsTr("V-Sync")
font.pointSize: 12
enabled: SystemProperties.supportsHdr checked: StreamingPreferences.enableVsync
checked: enabled && StreamingPreferences.enableHdr
onCheckedChanged: { onCheckedChanged: {
StreamingPreferences.enableHdr = checked StreamingPreferences.enableVsync = checked
} }
// Updating StreamingPreferences.videoCodecConfig is handled above
ToolTip.delay: 1000 ToolTip.delay: 1000
ToolTip.timeout: 5000 ToolTip.timeout: 5000
ToolTip.visible: hovered ToolTip.visible: hovered
ToolTip.text: enabled ? ToolTip.text: qsTr("Disabling V-Sync allows sub-frame rendering latency, but it can display visible tearing")
qsTr("The stream will be HDR-capable, but some games may require an HDR monitor on your host PC to enable HDR mode.") }
:
qsTr("HDR streaming is not supported on this PC.") CheckBox {
id: framePacingCheck
width: parent.width
hoverEnabled: true
text: qsTr("Frame pacing")
font.pointSize: 12
enabled: StreamingPreferences.enableVsync
checked: StreamingPreferences.enableVsync && StreamingPreferences.framePacing
onCheckedChanged: {
StreamingPreferences.framePacing = checked
}
ToolTip.delay: 1000
ToolTip.timeout: 5000
ToolTip.visible: hovered
ToolTip.text: qsTr("Frame pacing reduces micro-stutter by delaying frames that come in too early")
} }
} }
} }
@@ -968,46 +909,6 @@ Flickable {
} }
} }
GroupBox {
id: hostSettingsGroupBox
width: (parent.width - (parent.leftPadding + parent.rightPadding))
padding: 12
title: "<font color=\"skyblue\">" + qsTr("Host Settings") + "</font>"
font.pointSize: 12
Column {
anchors.fill: parent
spacing: 5
CheckBox {
id: optimizeGameSettingsCheck
width: parent.width
text: qsTr("Optimize game settings for streaming")
font.pointSize: 12
checked: StreamingPreferences.gameOptimizations
onCheckedChanged: {
StreamingPreferences.gameOptimizations = checked
}
}
CheckBox {
id: quitAppAfter
width: parent.width
text: qsTr("Quit app on host PC after ending stream")
font.pointSize: 12
checked: StreamingPreferences.quitAppAfter
onCheckedChanged: {
StreamingPreferences.quitAppAfter = checked
}
ToolTip.delay: 1000
ToolTip.timeout: 5000
ToolTip.visible: hovered
ToolTip.text: qsTr("This will close the app or game you are streaming when you end your stream. You will lose any unsaved progress!")
}
}
}
GroupBox { GroupBox {
id: uiSettingsGroupBox id: uiSettingsGroupBox
width: (parent.width - (parent.leftPadding + parent.rightPadding)) width: (parent.width - (parent.leftPadding + parent.rightPadding))
@@ -1095,10 +996,10 @@ Flickable {
text: "한국어" // Korean text: "한국어" // Korean
val: StreamingPreferences.LANG_KO val: StreamingPreferences.LANG_KO
} }
ListElement { /* ListElement {
text: "Magyar" // Hungarian text: "Magyar" // Hungarian
val: StreamingPreferences.LANG_HU val: StreamingPreferences.LANG_HU
} } */
ListElement { ListElement {
text: "Nederlands" // Dutch text: "Nederlands" // Dutch
val: StreamingPreferences.LANG_NL val: StreamingPreferences.LANG_NL
@@ -1107,10 +1008,10 @@ Flickable {
text: "Svenska" // Swedish text: "Svenska" // Swedish
val: StreamingPreferences.LANG_SV val: StreamingPreferences.LANG_SV
} }
ListElement { /* ListElement {
text: "Türkçe" // Turkish text: "Türkçe" // Turkish
val: StreamingPreferences.LANG_TR val: StreamingPreferences.LANG_TR
} } */
/* ListElement { /* ListElement {
text: "Українська" // Ukrainian text: "Українська" // Ukrainian
val: StreamingPreferences.LANG_UK val: StreamingPreferences.LANG_UK
@@ -1123,10 +1024,10 @@ Flickable {
text: "Português" // Portuguese text: "Português" // Portuguese
val: StreamingPreferences.LANG_PT val: StreamingPreferences.LANG_PT
} }
ListElement { /* ListElement {
text: "Português do Brasil" // Brazilian Portuguese text: "Português do Brasil" // Brazilian Portuguese
val: StreamingPreferences.LANG_PT_BR val: StreamingPreferences.LANG_PT_BR
} } */
ListElement { ListElement {
text: "Ελληνικά" // Greek text: "Ελληνικά" // Greek
val: StreamingPreferences.LANG_EL val: StreamingPreferences.LANG_EL
@@ -1151,30 +1052,6 @@ Flickable {
text: "עִבְרִית" // Hebrew text: "עִבְרִית" // Hebrew
val: StreamingPreferences.LANG_HE val: StreamingPreferences.LANG_HE
} */ } */
/* ListElement {
text: "کرمانجیی خواروو" // Central Kurdish
val: StreamingPreferences.LANG_CKB
} */
/* ListElement {
text: "Lietuvių kalba" // Lithuanian
val: StreamingPreferences.LANG_LT
} */
/* ListElement {
text: "Eesti" // Estonian
val: StreamingPreferences.LANG_ET
} */
ListElement {
text: "Български" // Bulgarian
val: StreamingPreferences.LANG_BG
}
/* ListElement {
text: "Esperanto"
val: StreamingPreferences.LANG_EO
} */
ListElement {
text: "தமிழ்" // Tamil
val: StreamingPreferences.LANG_TA
}
} }
// ::onActivated must be used, as it only listens for when the index is changed by a human // ::onActivated must be used, as it only listens for when the index is changed by a human
onActivated : { onActivated : {
@@ -1262,17 +1139,6 @@ Flickable {
} }
} }
CheckBox {
id: configurationWarningsCheck
width: parent.width
text: qsTr("Show configuration warnings")
font.pointSize: 12
checked: StreamingPreferences.configurationWarnings
onCheckedChanged: {
StreamingPreferences.configurationWarnings = checked
}
}
CheckBox { CheckBox {
visible: SystemProperties.hasDiscordIntegration visible: SystemProperties.hasDiscordIntegration
id: discordPresenceCheck id: discordPresenceCheck
@@ -1483,7 +1349,15 @@ Flickable {
font.pointSize: 12 font.pointSize: 12
checked: StreamingPreferences.swapFaceButtons checked: StreamingPreferences.swapFaceButtons
onCheckedChanged: { onCheckedChanged: {
StreamingPreferences.swapFaceButtons = checked // Check if the value changed (this is called on init too)
if (StreamingPreferences.swapFaceButtons !== checked) {
StreamingPreferences.swapFaceButtons = checked
// Save and restart SdlGamepadKeyNavigation so it can pull the new value
StreamingPreferences.save()
SdlGamepadKeyNavigation.disable()
SdlGamepadKeyNavigation.enable()
}
} }
ToolTip.delay: 1000 ToolTip.delay: 1000
@@ -1540,6 +1414,46 @@ Flickable {
} }
} }
GroupBox {
id: hostSettingsGroupBox
width: (parent.width - (parent.leftPadding + parent.rightPadding))
padding: 12
title: "<font color=\"skyblue\">" + qsTr("Host Settings") + "</font>"
font.pointSize: 12
Column {
anchors.fill: parent
spacing: 5
CheckBox {
id: optimizeGameSettingsCheck
width: parent.width
text: qsTr("Optimize game settings for streaming")
font.pointSize: 12
checked: StreamingPreferences.gameOptimizations
onCheckedChanged: {
StreamingPreferences.gameOptimizations = checked
}
}
CheckBox {
id: quitAppAfter
width: parent.width
text: qsTr("Quit app on host PC after ending stream")
font.pointSize: 12
checked: StreamingPreferences.quitAppAfter
onCheckedChanged: {
StreamingPreferences.quitAppAfter = checked
}
ToolTip.delay: 1000
ToolTip.timeout: 5000
ToolTip.visible: hovered
ToolTip.text: qsTr("This will close the app or game you are streaming when you end your stream. You will lose any unsaved progress!")
}
}
}
GroupBox { GroupBox {
id: advancedSettingsGroupBox id: advancedSettingsGroupBox
width: (parent.width - (parent.leftPadding + parent.rightPadding)) width: (parent.width - (parent.leftPadding + parent.rightPadding))
@@ -1576,6 +1490,7 @@ Flickable {
id: decoderComboBox id: decoderComboBox
textRole: "text" textRole: "text"
enabled: !enableHdr.checked
model: ListModel { model: ListModel {
id: decoderListModel id: decoderListModel
ListElement { ListElement {
@@ -1597,6 +1512,21 @@ Flickable {
StreamingPreferences.videoDecoderSelection = decoderListModel.get(currentIndex).val StreamingPreferences.videoDecoderSelection = decoderListModel.get(currentIndex).val
} }
} }
// This handles the state of the enableHdr checkbox changing
onEnabledChanged: {
if (enabled) {
StreamingPreferences.videoDecoderSelection = decoderListModel.get(currentIndex).val
}
else {
StreamingPreferences.videoDecoderSelection = StreamingPreferences.VDS_AUTO
}
}
ToolTip.delay: 1000
ToolTip.timeout: 5000
ToolTip.visible: hovered && !enabled
ToolTip.text: qsTr("Enabling HDR overrides manual decoder selections.")
} }
Label { Label {
@@ -1644,7 +1574,7 @@ Flickable {
val: StreamingPreferences.VCC_FORCE_HEVC val: StreamingPreferences.VCC_FORCE_HEVC
} }
ListElement { ListElement {
text: qsTr("AV1") text: qsTr("AV1 (Experimental)")
val: StreamingPreferences.VCC_FORCE_AV1 val: StreamingPreferences.VCC_FORCE_AV1
} }
} }
@@ -1657,52 +1587,26 @@ Flickable {
} }
CheckBox { CheckBox {
id: enableYUV444 id: enableHdr
width: parent.width width: parent.width
text: qsTr("Enable YUV 4:4:4") text: qsTr("Enable HDR (Experimental)")
font.pointSize: 12 font.pointSize: 12
checked: StreamingPreferences.enableYUV444 enabled: SystemProperties.supportsHdr
checked: enabled && StreamingPreferences.enableHdr
onCheckedChanged: { onCheckedChanged: {
// This is called on init, so only reset to default bitrate when checked state changes. StreamingPreferences.enableHdr = checked
if (StreamingPreferences.enableYUV444 != checked) {
StreamingPreferences.enableYUV444 = checked
if (StreamingPreferences.autoAdjustBitrate) {
StreamingPreferences.bitrateKbps = StreamingPreferences.getDefaultBitrate(StreamingPreferences.width,
StreamingPreferences.height,
StreamingPreferences.fps,
StreamingPreferences.enableYUV444);
slider.value = StreamingPreferences.bitrateKbps
}
}
} }
// Updating StreamingPreferences.videoCodecConfig is handled above
ToolTip.delay: 1000 ToolTip.delay: 1000
ToolTip.timeout: 5000 ToolTip.timeout: 5000
ToolTip.visible: hovered ToolTip.visible: hovered
ToolTip.text: enabled ? ToolTip.text: enabled ?
qsTr("Good for streaming desktop and text-heavy games, but not recommended for fast-paced games.") qsTr("The stream will be HDR-capable, but some games may require an HDR monitor on your host PC to enable HDR mode.")
: :
qsTr("YUV 4:4:4 is not supported on this PC.") qsTr("HDR streaming is not supported on this PC.")
}
CheckBox {
id: unlockBitrate
width: parent.width
text: qsTr("Unlock bitrate limit (Experimental)")
font.pointSize: 12
checked: StreamingPreferences.unlockBitrate
onCheckedChanged: {
StreamingPreferences.unlockBitrate = checked
StreamingPreferences.bitrateKbps = Math.min(StreamingPreferences.bitrateKbps, slider.to)
slider.value = StreamingPreferences.bitrateKbps
}
ToolTip.delay: 1000
ToolTip.timeout: 5000
ToolTip.visible: hovered
ToolTip.text: qsTr("This unlocks extremely high video bitrates for use with Sunshine hosts. It should only be used when streaming over an Ethernet LAN connection.")
} }
CheckBox { CheckBox {
@@ -1717,6 +1621,10 @@ Flickable {
if (StreamingPreferences.enableMdns != checked) { if (StreamingPreferences.enableMdns != checked) {
StreamingPreferences.enableMdns = checked StreamingPreferences.enableMdns = checked
// We must save the updated preference to ensure
// ComputerManager can observe the change internally.
StreamingPreferences.save()
// Restart polling so the mDNS change takes effect // Restart polling so the mDNS change takes effect
if (window.pollingActive) { if (window.pollingActive) {
ComputerManager.stopPollingAsync() ComputerManager.stopPollingAsync()
@@ -1733,26 +1641,16 @@ Flickable {
font.pointSize: 12 font.pointSize: 12
checked: StreamingPreferences.detectNetworkBlocking checked: StreamingPreferences.detectNetworkBlocking
onCheckedChanged: { onCheckedChanged: {
StreamingPreferences.detectNetworkBlocking = checked // This is called on init, so only do the work if we've
} // actually changed the value.
} if (StreamingPreferences.detectNetworkBlocking != checked) {
StreamingPreferences.detectNetworkBlocking = checked
CheckBox { // We must save the updated preference to ensure
id: showPerformanceOverlay // ComputerManager can observe the change internally.
width: parent.width StreamingPreferences.save()
text: qsTr("Show performance stats while streaming") }
font.pointSize: 12
checked: StreamingPreferences.showPerformanceOverlay
onCheckedChanged: {
StreamingPreferences.showPerformanceOverlay = checked
} }
ToolTip.delay: 1000
ToolTip.timeout: 5000
ToolTip.visible: hovered
ToolTip.text: qsTr("Display real-time stream performance information while streaming.") + "\n\n" +
qsTr("You can toggle it at any time while streaming using Ctrl+Alt+Shift+S or Select+L1+R1+X.") + "\n\n" +
qsTr("The performance overlay is not supported on Steam Link or Raspberry Pi.")
} }
} }
} }
+35 -62
View File
@@ -4,7 +4,6 @@ import QtQuick.Window 2.2
import SdlGamepadKeyNavigation 1.0 import SdlGamepadKeyNavigation 1.0
import Session 1.0 import Session 1.0
import SystemProperties 1.0
Item { Item {
property Session session property Session session
@@ -49,6 +48,18 @@ Item {
console.error(text) console.error(text)
} }
function displayLaunchWarning(text)
{
// 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, '')
toast.text = text
toast.timeout = 3000
toast.visible = true
console.warn(text)
}
function quitStarting() function quitStarting()
{ {
// Avoid the push transition animation // Avoid the push transition animation
@@ -65,19 +76,23 @@ Item {
streamSegueErrorDialog.text += "\n\n" + qsTr("This PC's Internet connection is blocking Moonlight. Streaming over the Internet may not work while connected to this network.") streamSegueErrorDialog.text += "\n\n" + qsTr("This PC's Internet connection is blocking Moonlight. Streaming over the Internet may not work while connected to this network.")
} }
// Re-enable GUI gamepad usage now // Enable GUI gamepad usage now
SdlGamepadKeyNavigation.enable() SdlGamepadKeyNavigation.enable()
// Pop the StreamSegue off the stack if this is a GUI-based app launch if (quitAfter) {
if (!quitAfter) { if (streamSegueErrorDialog.text) {
// Quit when the error dialog is acknowledged
streamSegueErrorDialog.quitAfter = quitAfter
streamSegueErrorDialog.open()
}
else {
// Quit immediately
Qt.quit()
}
} else {
// Exit this view
stackView.pop() stackView.pop()
}
if (quitAfter && !streamSegueErrorDialog.text) {
// If this was a CLI launch without errors, exit now
Qt.quit()
}
else {
// Show the Qt window again after streaming // Show the Qt window again after streaming
window.visible = true window.visible = true
@@ -104,7 +119,7 @@ Item {
// Show the toolbar again when popped off the stack // Show the toolbar again when popped off the stack
toolBar.visible = true toolBar.visible = true
// Re-enable GUI gamepad usage now // Enable GUI gamepad usage now
SdlGamepadKeyNavigation.enable() SdlGamepadKeyNavigation.enable()
} }
@@ -117,14 +132,11 @@ Item {
session.stageFailed.connect(stageFailed) session.stageFailed.connect(stageFailed)
session.connectionStarted.connect(connectionStarted) session.connectionStarted.connect(connectionStarted)
session.displayLaunchError.connect(displayLaunchError) session.displayLaunchError.connect(displayLaunchError)
session.displayLaunchWarning.connect(displayLaunchWarning)
session.quitStarting.connect(quitStarting) session.quitStarting.connect(quitStarting)
session.sessionFinished.connect(sessionFinished) session.sessionFinished.connect(sessionFinished)
session.readyForDeletion.connect(sessionReadyForDeletion) session.readyForDeletion.connect(sessionReadyForDeletion)
// Ensure the SystemProperties async thread is finished,
// since it may currently be using the SDL video subsystem
SystemProperties.waitForAsyncLoad()
// Kick off the stream // Kick off the stream
spinnerTimer.start() spinnerTimer.start()
streamLoader.active = true streamLoader.active = true
@@ -139,20 +151,7 @@ Item {
// middle of the animation on Windows, which looks very // middle of the animation on Windows, which looks very
// obviously broken. // obviously broken.
interval: 100 interval: 100
onTriggered: stageSpinner.visible = true onTriggered: stageSpinner.running = true
}
Timer {
id: startSessionTimer
onTriggered: {
// 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()
// Run the streaming session to completion
session.start()
}
} }
Loader { Loader {
@@ -171,38 +170,13 @@ Item {
// Stop GUI gamepad usage now // Stop GUI gamepad usage now
SdlGamepadKeyNavigation.disable() SdlGamepadKeyNavigation.disable()
// Initialize the session and probe for host/client capabilities // Garbage collect QML stuff before we start streaming,
if (!session.initialize(window)) { // since we'll probably be streaming for a while and we
sessionFinished(0); // won't be able to GC during the stream.
sessionReadyForDeletion(); gc()
return;
}
// Don't wait unless we have toasts to display // Run the streaming session to completion
startSessionTimer.interval = 0 session.exec(Screen.virtualX, Screen.virtualY)
// Display the toasts together in a vertical centered arrangement
var yOffset = 0
for (var i = 0; i < session.launchWarnings.length; i++) {
var text = session.launchWarnings[i]
console.warn(text)
// Show the tooltip for 3 seconds
var toast = Qt.createQmlObject('import QtQuick.Controls 2.2; ToolTip {}', parent, '')
toast.timeout = 3000
toast.text = text
toast.y += yOffset
toast.visible = true
// Offset the next toast below the previous one
yOffset = toast.y + toast.padding + toast.height
// Allow an extra 500 ms for the tooltip's fade-out animation to finish
startSessionTimer.interval = toast.timeout + 500;
}
// Start the timer to wait for toasts (or start the session immediately)
startSessionTimer.start()
} }
sourceComponent: Item {} sourceComponent: Item {}
@@ -214,8 +188,7 @@ Item {
BusyIndicator { BusyIndicator {
id: stageSpinner id: stageSpinner
running: visible running: false
visible: false
} }
Label { Label {
+3 -3
View File
@@ -120,7 +120,7 @@ void AppModel::quitRunningApp()
bool AppModel::isAppCurrentlyVisible(const NvApp& app) bool AppModel::isAppCurrentlyVisible(const NvApp& app)
{ {
for (const NvApp& visibleApp : std::as_const(m_VisibleApps)) { for (const NvApp& visibleApp : m_VisibleApps) {
if (app.id == visibleApp.id) { if (app.id == visibleApp.id) {
return true; return true;
} }
@@ -156,7 +156,7 @@ void AppModel::updateAppList(QVector<NvApp> newList)
const NvApp& existingApp = m_VisibleApps.at(i); const NvApp& existingApp = m_VisibleApps.at(i);
bool found = false; bool found = false;
for (const NvApp& newApp : std::as_const(newVisibleList)) { for (const NvApp& newApp : newVisibleList) {
if (existingApp.id == newApp.id) { if (existingApp.id == newApp.id) {
// If the data changed, update it in our list // If the data changed, update it in our list
if (existingApp != newApp) { if (existingApp != newApp) {
@@ -178,7 +178,7 @@ void AppModel::updateAppList(QVector<NvApp> newList)
} }
// Process additions now // Process additions now
for (const NvApp& newApp : std::as_const(newVisibleList)) { for (const NvApp& newApp : newVisibleList) {
int insertionIndex = m_VisibleApps.size(); int insertionIndex = m_VisibleApps.size();
bool found = false; bool found = false;
-41
View File
@@ -42,46 +42,6 @@ QVariant ComputerModel::data(const QModelIndex& index, int role) const
return computer->state == NvComputer::CS_UNKNOWN; return computer->state == NvComputer::CS_UNKNOWN;
case ServerSupportedRole: case ServerSupportedRole:
return computer->isSupportedServerVersion; return computer->isSupportedServerVersion;
case DetailsRole: {
QString state, pairState;
switch (computer->state) {
case NvComputer::CS_ONLINE:
state = tr("Online");
break;
case NvComputer::CS_OFFLINE:
state = tr("Offline");
break;
default:
state = tr("Unknown");
break;
}
switch (computer->pairState) {
case NvComputer::PS_PAIRED:
pairState = tr("Paired");
break;
case NvComputer::PS_NOT_PAIRED:
pairState = tr("Unpaired");
break;
default:
pairState = tr("Unknown");
break;
}
return tr("Name: %1").arg(computer->name) + '\n' +
tr("Status: %1").arg(state) + '\n' +
tr("Active Address: %1").arg(computer->activeAddress.toString()) + '\n' +
tr("UUID: %1").arg(computer->uuid) + '\n' +
tr("Local Address: %1").arg(computer->localAddress.toString()) + '\n' +
tr("Remote Address: %1").arg(computer->remoteAddress.toString()) + '\n' +
tr("IPv6 Address: %1").arg(computer->ipv6Address.toString()) + '\n' +
tr("Manual Address: %1").arg(computer->manualAddress.toString()) + '\n' +
tr("MAC Address: %1").arg(computer->macAddress.isEmpty() ? tr("Unknown") : QString(computer->macAddress.toHex(':'))) + '\n' +
tr("Pair State: %1").arg(pairState) + '\n' +
tr("Running Game ID: %1").arg(computer->state == NvComputer::CS_ONLINE ? QString::number(computer->currentGameId) : tr("Unknown")) + '\n' +
tr("HTTPS Port: %1").arg(computer->state == NvComputer::CS_ONLINE ? QString::number(computer->activeHttpsPort) : tr("Unknown"));
}
default: default:
return QVariant(); return QVariant();
} }
@@ -109,7 +69,6 @@ QHash<int, QByteArray> ComputerModel::roleNames() const
names[WakeableRole] = "wakeable"; names[WakeableRole] = "wakeable";
names[StatusUnknownRole] = "statusUnknown"; names[StatusUnknownRole] = "statusUnknown";
names[ServerSupportedRole] = "serverSupported"; names[ServerSupportedRole] = "serverSupported";
names[DetailsRole] = "details";
return names; return names;
} }
+1 -2
View File
@@ -15,8 +15,7 @@ class ComputerModel : public QAbstractListModel
BusyRole, BusyRole,
WakeableRole, WakeableRole,
StatusUnknownRole, StatusUnknownRole,
ServerSupportedRole, ServerSupportedRole
DetailsRole
}; };
public: public:
+64 -80
View File
@@ -19,81 +19,32 @@ ApplicationWindow {
property bool clearOnBack: false property bool clearOnBack: false
id: window id: window
visible: true
width: 1280 width: 1280
height: 600 height: 600
// This function runs prior to creation of the initial StackView item // Override the background color to Material 2 colors for Qt 6.5+
function doEarlyInit() { // in order to improve contrast between GFE's placeholder box art
// Override the background color to Material 2 colors for Qt 6.5+ // and the background of the app grid.
// in order to improve contrast between GFE's placeholder box art Component.onCompleted: {
// and the background of the app grid.
if (SystemProperties.usesMaterial3Theme) { if (SystemProperties.usesMaterial3Theme) {
Material.background = "#303030" Material.background = "#303030"
} }
SdlGamepadKeyNavigation.enable()
} }
Component.onCompleted: { visibility: {
// Show the window according to the user's preferences
if (SystemProperties.hasDesktopEnvironment) { if (SystemProperties.hasDesktopEnvironment) {
if (StreamingPreferences.uiDisplayMode == StreamingPreferences.UI_MAXIMIZED) { if (StreamingPreferences.uiDisplayMode == StreamingPreferences.UI_WINDOWED) return "Windowed"
window.showMaximized() else if (StreamingPreferences.uiDisplayMode == StreamingPreferences.UI_MAXIMIZED) return "Maximized"
} else if (StreamingPreferences.uiDisplayMode == StreamingPreferences.UI_FULLSCREEN) return "FullScreen"
else if (StreamingPreferences.uiDisplayMode == StreamingPreferences.UI_FULLSCREEN) {
window.showFullScreen()
}
else {
window.show()
}
} else { } else {
window.showFullScreen() return "FullScreen"
}
// Display any modal dialogs for configuration warnings
if (runConfigChecks) {
if (SystemProperties.isWow64) {
wow64Dialog.open()
}
// Hardware acceleration and unmapped gamepads are checked asynchronously
SystemProperties.hasHardwareAccelerationChanged.connect(hasHardwareAccelerationChanged)
SystemProperties.unmappedGamepadsChanged.connect(hasUnmappedGamepadsChanged)
SystemProperties.startAsyncLoad()
} }
} }
function hasHardwareAccelerationChanged() {
if (!SystemProperties.hasHardwareAcceleration && StreamingPreferences.videoDecoderSelection !== StreamingPreferences.VDS_FORCE_SOFTWARE) {
if (SystemProperties.isRunningXWayland) {
xWaylandDialog.open()
}
else {
noHwDecoderDialog.open()
}
}
}
function hasUnmappedGamepadsChanged() {
if (SystemProperties.unmappedGamepads) {
unmappedGamepadDialog.unmappedGamepads = SystemProperties.unmappedGamepads
unmappedGamepadDialog.open()
}
}
// It would be better to use TextMetrics here, but it always lays out
// the text slightly more compactly than real Text does in ToolTip,
// causing unexpected line breaks to be inserted
Text {
id: tooltipTextLayoutHelper
visible: false
font: ToolTip.toolTip.font
text: ToolTip.toolTip.text
}
// This configures the maximum width of the singleton attached QML ToolTip. If left unconstrained, // 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. // it will never insert a line break and just extend on forever.
ToolTip.toolTip.contentWidth: Math.min(tooltipTextLayoutHelper.width, 400) ToolTip.toolTip.contentWidth: ToolTip.toolTip.implicitContentWidth < 400 ? ToolTip.toolTip.implicitContentWidth : 400
function goBack() { function goBack() {
if (clearOnBack) { if (clearOnBack) {
@@ -108,16 +59,10 @@ ApplicationWindow {
StackView { StackView {
id: stackView id: stackView
initialItem: initialView
anchors.fill: parent anchors.fill: parent
focus: true focus: true
Component.onCompleted: {
// Perform our early initialization before constructing
// the initial view and pushing it to the StackView
doEarlyInit()
push(initialView)
}
onCurrentItemChanged: { onCurrentItemChanged: {
// Ensure focus travels to the next view when going back // Ensure focus travels to the next view when going back
if (currentItem) { if (currentItem) {
@@ -191,9 +136,6 @@ ApplicationWindow {
pollingActive = true pollingActive = true
} }
} }
// Poll for gamepad input only when the window is in focus
SdlGamepadKeyNavigation.notifyWindowFocus(visible && active)
} }
onActiveChanged: { onActiveChanged: {
@@ -212,15 +154,57 @@ ApplicationWindow {
// if focus does not return within a few minutes. // if focus does not return within a few minutes.
inactivityTimer.restart() inactivityTimer.restart()
} }
}
// Poll for gamepad input only when the window is in focus property bool initialized: false
SdlGamepadKeyNavigation.notifyWindowFocus(visible && active)
// BUG: Using onAfterSynchronizing: here causes very strange
// failures on Linux. Many shaders fail to compile and we
// eventually segfault deep inside the Qt OpenGL code.
onAfterRendering: {
// We use this callback to trigger dialog display because
// it only happens once the window is fully constructed.
// Doing it earlier can lead to the dialog appearing behind
// the window or otherwise without input focus.
if (!initialized) {
// Set initialized before calling anything else, because
// pumping the event loop can cause us to get another
// onAfterRendering call and potentially reenter this code.
initialized = true;
if (SystemProperties.isWow64) {
wow64Dialog.open()
}
else if (!SystemProperties.hasHardwareAcceleration) {
if (SystemProperties.isRunningXWayland) {
xWaylandDialog.open()
}
else {
noHwDecoderDialog.open()
}
}
if (SystemProperties.unmappedGamepads) {
unmappedGamepadDialog.unmappedGamepads = SystemProperties.unmappedGamepads
unmappedGamepadDialog.open()
}
}
}
// Workaround for lack of instanceof in Qt 5.9.
//
// Based on https://stackoverflow.com/questions/13923794/how-to-do-a-is-a-typeof-or-instanceof-in-qml
function qmltypeof(obj, className) { // QtObject, string -> bool
// className plus "(" is the class instance without modification
// className plus "_QML" is the class instance with user-defined properties
var str = obj.toString();
return str.startsWith(className + "(") || str.startsWith(className + "_QML");
} }
function navigateTo(url, objectType) function navigateTo(url, objectType)
{ {
var existingItem = stackView.find(function(item, index) { var existingItem = stackView.find(function(item, index) {
return item instanceof objectType return qmltypeof(item, objectType)
}) })
if (existingItem !== null) { if (existingItem !== null) {
@@ -287,7 +271,7 @@ ApplicationWindow {
Label { Label {
id: versionLabel id: versionLabel
visible: stackView.currentItem instanceof SettingsView visible: qmltypeof(stackView.currentItem, "SettingsView")
text: qsTr("Version %1").arg(SystemProperties.versionString) text: qsTr("Version %1").arg(SystemProperties.versionString)
font.pointSize: 12 font.pointSize: 12
horizontalAlignment: Qt.AlignRight horizontalAlignment: Qt.AlignRight
@@ -297,9 +281,9 @@ ApplicationWindow {
NavigableToolButton { NavigableToolButton {
id: discordButton id: discordButton
visible: SystemProperties.hasBrowser && visible: SystemProperties.hasBrowser &&
stackView.currentItem instanceof SettingsView qmltypeof(stackView.currentItem, "SettingsView")
iconSource: "qrc:/res/discord.svg" iconSource: "qrc:/res/Discord-Logo-White.svg"
ToolTip.delay: 1000 ToolTip.delay: 1000
ToolTip.timeout: 3000 ToolTip.timeout: 3000
@@ -316,7 +300,7 @@ ApplicationWindow {
NavigableToolButton { NavigableToolButton {
id: addPcButton id: addPcButton
visible: stackView.currentItem instanceof PcView visible: qmltypeof(stackView.currentItem, "PcView")
iconSource: "qrc:/res/ic_add_to_queue_white_48px.svg" iconSource: "qrc:/res/ic_add_to_queue_white_48px.svg"
@@ -414,7 +398,7 @@ ApplicationWindow {
iconSource: "qrc:/res/ic_videogame_asset_white_48px.svg" iconSource: "qrc:/res/ic_videogame_asset_white_48px.svg"
onClicked: navigateTo("qrc:/gui/GamepadMapper.qml", GamepadMapper) onClicked: navigateTo("qrc:/gui/GamepadMapper.qml", "GamepadMapper")
Keys.onDownPressed: { Keys.onDownPressed: {
stackView.currentItem.forceActiveFocus(Qt.TabFocus) stackView.currentItem.forceActiveFocus(Qt.TabFocus)
@@ -426,7 +410,7 @@ ApplicationWindow {
iconSource: "qrc:/res/settings.svg" iconSource: "qrc:/res/settings.svg"
onClicked: navigateTo("qrc:/gui/SettingsView.qml", SettingsView) onClicked: navigateTo("qrc:/gui/SettingsView.qml", "SettingsView")
Keys.onDownPressed: { Keys.onDownPressed: {
stackView.currentItem.forceActiveFocus(Qt.TabFocus) stackView.currentItem.forceActiveFocus(Qt.TabFocus)
+20 -45
View File
@@ -4,16 +4,15 @@
#include <QGuiApplication> #include <QGuiApplication>
#include <QWindow> #include <QWindow>
#include "settings/streamingpreferences.h"
#include "settings/mappingmanager.h" #include "settings/mappingmanager.h"
#define AXIS_NAVIGATION_REPEAT_DELAY 150 #define AXIS_NAVIGATION_REPEAT_DELAY 150
SdlGamepadKeyNavigation::SdlGamepadKeyNavigation(StreamingPreferences* prefs) SdlGamepadKeyNavigation::SdlGamepadKeyNavigation()
: m_Prefs(prefs), : m_Enabled(false),
m_Enabled(false),
m_UiNavMode(false), m_UiNavMode(false),
m_FirstPoll(false), m_FirstPoll(false),
m_HasFocus(false),
m_LastAxisNavigationEventTime(0) m_LastAxisNavigationEventTime(0)
{ {
m_PollingTimer = new QTimer(this); m_PollingTimer = new QTimer(this);
@@ -51,16 +50,11 @@ void SdlGamepadKeyNavigation::enable()
// on first init of the GC subsystem. We can't depend on them due to // on first init of the GC subsystem. We can't depend on them due to
// overlapping lifetimes of SdlGamepadKeyNavigation instances, so we // overlapping lifetimes of SdlGamepadKeyNavigation instances, so we
// will attach ourselves. // will attach ourselves.
// SDL_PumpEvents();
// NB: We use SDL_JoystickUpdate() instead of SDL_PumpEvents() because
// the latter can do a bit more work that we want (like handling video
// events that we intentionally do not want to process yet).
SDL_JoystickUpdate();
SDL_FlushEvent(SDL_CONTROLLERDEVICEADDED); SDL_FlushEvent(SDL_CONTROLLERDEVICEADDED);
// Open all currently attached game controllers // Open all currently attached game controllers
int numJoysticks = SDL_NumJoysticks(); for (int i = 0; i < SDL_NumJoysticks(); i++) {
for (int i = 0; i < numJoysticks; i++) {
if (SDL_IsGameController(i)) { if (SDL_IsGameController(i)) {
SDL_GameController* gc = SDL_GameControllerOpen(i); SDL_GameController* gc = SDL_GameControllerOpen(i);
if (gc != nullptr) { if (gc != nullptr) {
@@ -69,10 +63,13 @@ void SdlGamepadKeyNavigation::enable()
} }
} }
m_Enabled = true; // Flush events on the first poll
m_FirstPoll = true;
// Start the polling timer if the window is focused // Poll every 50 ms for a new joystick event
updateTimerState(); m_PollingTimer->start(50);
m_Enabled = true;
} }
void SdlGamepadKeyNavigation::disable() void SdlGamepadKeyNavigation::disable()
@@ -81,9 +78,7 @@ void SdlGamepadKeyNavigation::disable()
return; return;
} }
m_Enabled = false; m_PollingTimer->stop();
updateTimerState();
Q_ASSERT(!m_PollingTimer->isActive());
while (!m_Gamepads.isEmpty()) { while (!m_Gamepads.isEmpty()) {
SDL_GameControllerClose(m_Gamepads[0]); SDL_GameControllerClose(m_Gamepads[0]);
@@ -91,31 +86,26 @@ void SdlGamepadKeyNavigation::disable()
} }
SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER); SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER);
}
void SdlGamepadKeyNavigation::notifyWindowFocus(bool hasFocus) m_Enabled = false;
{
m_HasFocus = hasFocus;
updateTimerState();
} }
void SdlGamepadKeyNavigation::onPollingTimerFired() void SdlGamepadKeyNavigation::onPollingTimerFired()
{ {
SDL_Event event; SDL_Event event;
StreamingPreferences prefs;
// Update joystick state without pumping other events (see enable() comment)
SDL_JoystickUpdate();
// Discard any pending button events on the first poll to avoid picking up // Discard any pending button events on the first poll to avoid picking up
// stale input data from the stream session (like the quit combo). // stale input data from the stream session (like the quit combo).
if (m_FirstPoll) { if (m_FirstPoll) {
SDL_PumpEvents();
SDL_FlushEvent(SDL_CONTROLLERBUTTONDOWN); SDL_FlushEvent(SDL_CONTROLLERBUTTONDOWN);
SDL_FlushEvent(SDL_CONTROLLERBUTTONUP); SDL_FlushEvent(SDL_CONTROLLERBUTTONUP);
m_FirstPoll = false; m_FirstPoll = false;
} }
// Peep events rather than polling to avoid calling SDL_PumpEvents() while (SDL_PollEvent(&event)) {
while (SDL_PeepEvents(&event, 1, SDL_GETEVENT, SDL_FIRSTEVENT, SDL_LASTEVENT) == 1) {
switch (event.type) { switch (event.type) {
case SDL_QUIT: case SDL_QUIT:
// SDL may send us a quit event since we initialize // SDL may send us a quit event since we initialize
@@ -131,7 +121,7 @@ void SdlGamepadKeyNavigation::onPollingTimerFired()
QEvent::Type::KeyPress : QEvent::Type::KeyRelease; QEvent::Type::KeyPress : QEvent::Type::KeyRelease;
// Swap face buttons if needed // Swap face buttons if needed
if (m_Prefs->swapFaceButtons) { if (prefs.swapFaceButtons) {
switch (event.cbutton.button) { switch (event.cbutton.button) {
case SDL_CONTROLLER_BUTTON_A: case SDL_CONTROLLER_BUTTON_A:
event.cbutton.button = SDL_CONTROLLER_BUTTON_B; event.cbutton.button = SDL_CONTROLLER_BUTTON_B;
@@ -218,7 +208,7 @@ void SdlGamepadKeyNavigation::onPollingTimerFired()
} }
// Handle analog sticks by polling // Handle analog sticks by polling
for (auto gc : std::as_const(m_Gamepads)) { for (auto gc : m_Gamepads) {
short leftX = SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_LEFTX); short leftX = SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_LEFTX);
short leftY = SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_LEFTY); short leftY = SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_LEFTY);
if (SDL_GetTicks() - m_LastAxisNavigationEventTime < AXIS_NAVIGATION_REPEAT_DELAY) { if (SDL_GetTicks() - m_LastAxisNavigationEventTime < AXIS_NAVIGATION_REPEAT_DELAY) {
@@ -272,20 +262,6 @@ void SdlGamepadKeyNavigation::sendKey(QEvent::Type type, Qt::Key key, Qt::Keyboa
} }
} }
void SdlGamepadKeyNavigation::updateTimerState()
{
if (m_PollingTimer->isActive() && (!m_HasFocus || !m_Enabled)) {
m_PollingTimer->stop();
}
else if (!m_PollingTimer->isActive() && m_HasFocus && m_Enabled) {
// Flush events on the first poll
m_FirstPoll = true;
// Poll every 50 ms for a new joystick event
m_PollingTimer->start(50);
}
}
void SdlGamepadKeyNavigation::setUiNavMode(bool uiNavMode) void SdlGamepadKeyNavigation::setUiNavMode(bool uiNavMode)
{ {
m_UiNavMode = uiNavMode; m_UiNavMode = uiNavMode;
@@ -296,8 +272,7 @@ int SdlGamepadKeyNavigation::getConnectedGamepads()
Q_ASSERT(m_Enabled); Q_ASSERT(m_Enabled);
int count = 0; int count = 0;
int numJoysticks = SDL_NumJoysticks(); for (int i = 0; i < SDL_NumJoysticks(); i++) {
for (int i = 0; i < numJoysticks; i++) {
if (SDL_IsGameController(i)) { if (SDL_IsGameController(i)) {
count++; count++;
} }
+2 -10
View File
@@ -3,16 +3,14 @@
#include <QTimer> #include <QTimer>
#include <QEvent> #include <QEvent>
#include "SDL_compat.h" #include <SDL.h>
#include "settings/streamingpreferences.h"
class SdlGamepadKeyNavigation : public QObject class SdlGamepadKeyNavigation : public QObject
{ {
Q_OBJECT Q_OBJECT
public: public:
SdlGamepadKeyNavigation(StreamingPreferences* prefs); SdlGamepadKeyNavigation();
~SdlGamepadKeyNavigation(); ~SdlGamepadKeyNavigation();
@@ -20,8 +18,6 @@ public:
Q_INVOKABLE void disable(); Q_INVOKABLE void disable();
Q_INVOKABLE void notifyWindowFocus(bool hasFocus);
Q_INVOKABLE void setUiNavMode(bool settingsMode); Q_INVOKABLE void setUiNavMode(bool settingsMode);
Q_INVOKABLE int getConnectedGamepads(); Q_INVOKABLE int getConnectedGamepads();
@@ -29,18 +25,14 @@ public:
private: private:
void sendKey(QEvent::Type type, Qt::Key key, Qt::KeyboardModifiers modifiers = Qt::NoModifier); void sendKey(QEvent::Type type, Qt::Key key, Qt::KeyboardModifiers modifiers = Qt::NoModifier);
void updateTimerState();
private slots: private slots:
void onPollingTimerFired(); void onPollingTimerFired();
private: private:
StreamingPreferences* m_Prefs;
QTimer* m_PollingTimer; QTimer* m_PollingTimer;
QList<SDL_GameController*> m_Gamepads; QList<SDL_GameController*> m_Gamepads;
bool m_Enabled; bool m_Enabled;
bool m_UiNavMode; bool m_UiNavMode;
bool m_FirstPoll; bool m_FirstPoll;
bool m_HasFocus;
Uint32 m_LastAxisNavigationEventTime; Uint32 m_LastAxisNavigationEventTime;
}; };
Binary file not shown.
File diff suppressed because it is too large Load Diff
Binary file not shown.
File diff suppressed because it is too large Load Diff
Binary file not shown.
+225 -415
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.
+309 -499
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.
+226 -416
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.
File diff suppressed because it is too large Load Diff
Binary file not shown.
+222 -412
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.
File diff suppressed because it is too large Load Diff
Binary file not shown.
+257 -447
View File
File diff suppressed because it is too large Load Diff
+216 -406
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.
+219 -409
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.
+403 -593
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.
+223 -413
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.
+251 -441
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.
+219 -409
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.
File diff suppressed because it is too large Load Diff
Binary file not shown.
+429 -619
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.
+328 -518
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.
+219 -409
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.
+254 -444
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.
+613 -803
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.

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