Compare commits

..
Author SHA1 Message Date
Cameron Gutman 12b7f6b7a3 Fix appstream metadata release date for v0.8.1 2019-01-29 18:52:01 -08:00
532 changed files with 9320 additions and 49253 deletions

No files matched your search

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

Before

Width:  |  Height:  |  Size: 2.9 KiB

-23
View File
@@ -1,23 +0,0 @@
#!/bin/sh
# The default HOME is not persistent, so override
# it to a path on the onboard flash. Otherwise our
# pairing data will be lost each reboot.
HOME=/usr/local/moonlight
# For a seamless transition across the Qt 5.9 -> Qt 5.14 boundary,
# we bundle binaries built with both Qt 5.9 and Qt 5.14 and pick
# the correct one at runtime.
if [ -d "/usr/local/Qt-5.14.1" ]; then
BIN="moonlight"
else
BIN="moonlight59"
fi
# Renice PE_Single_CPU which seems to host A/V stuff
renice -10 -p $(pidof PE_Single_CPU)
# Renice Moonlight itself to avoid preemption by background tasks
# Write output to a logfile in /tmp
echo Launching $BIN
exec nice -n -10 ./bin/$BIN > /tmp/moonlight.log
-3
View File
@@ -1,3 +0,0 @@
name=Moonlight
icon=moonlight.png
run=moonlight.sh
+101 -148
View File
@@ -1,21 +1,29 @@
import QtQuick 2.9 import QtQuick 2.9
import QtQuick.Dialogs 1.2
import QtQuick.Controls 2.2 import QtQuick.Controls 2.2
import AppModel 1.0 import AppModel 1.0
import ComputerManager 1.0 import ComputerManager 1.0
import SdlGamepadKeyNavigation 1.0 import SdlGamepadKeyNavigation 1.0
CenteredGridView { GridView {
property int computerIndex property int computerIndex
property AppModel appModel : createModel() property AppModel appModel : createModel()
property bool activated
id: appGrid id: appGrid
focus: true focus: true
activeFocusOnTab: true activeFocusOnTab: true
topMargin: 20 anchors.fill: parent
bottomMargin: 5 anchors.leftMargin: (parent.width % (cellWidth + anchors.rightMargin)) / 2
cellWidth: 230; cellHeight: 297; anchors.topMargin: 20
anchors.rightMargin: 5
anchors.bottomMargin: 5
cellWidth: 225; cellHeight: 385;
// The StackView will trigger a visibility change when
// we're pushed onto it, causing our onVisibleChanged
// routine to run, but only if we start as invisible
visible: false
function computerLost() function computerLost()
{ {
@@ -24,25 +32,23 @@ CenteredGridView {
} }
Component.onCompleted: { Component.onCompleted: {
// Don't show any highlighted item until interacting with them. // Don't show any highlighted item until interacting with them
// We do this here instead of onActivated to avoid losing the user's
// selection when backing out of a different page of the app.
currentIndex = -1 currentIndex = -1
} }
StackView.onActivated: { SdlGamepadKeyNavigation {
appModel.computerLost.connect(computerLost) id: gamepadKeyNav
activated = true
// Highlight the first item if a gamepad is connected
if (currentIndex == -1 && SdlGamepadKeyNavigation.getConnectedGamepads() > 0) {
currentIndex = 0
}
} }
StackView.onDeactivating: { onVisibleChanged: {
appModel.computerLost.disconnect(computerLost) if (visible) {
activated = false appModel.computerLost.connect(computerLost)
gamepadKeyNav.enable()
}
else {
appModel.computerLost.disconnect(computerLost)
gamepadKeyNav.disable()
}
} }
function createModel() function createModel()
@@ -55,112 +61,50 @@ CenteredGridView {
model: appModel model: appModel
delegate: NavigableItemDelegate { delegate: NavigableItemDelegate {
width: 220; height: 287; width: 200; height: 335;
grid: appGrid grid: appGrid
Image { Image {
property bool isPlaceholder: false
id: appIcon id: appIcon
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter;
y: 10 y: 20
source: model.boxart source: model.boxart
sourceSize {
onSourceSizeChanged: { width: 150
if ((sourceSize.width == 130 && sourceSize.height == 180) || // GFE 2.0 placeholder image height: 200
(sourceSize.width == 628 && sourceSize.height == 888) || // GFE 3.0 placeholder image
(sourceSize.width == 200 && sourceSize.height == 266)) // Our no_app_image.png
{
isPlaceholder = true
}
else
{
isPlaceholder = false
}
width = 200
height = 267
} }
width: sourceSize.width
height: sourceSize.height
fillMode: Image.Pad
}
Image {
id: runningIcon
anchors.centerIn: appIcon
visible: model.running
source: "qrc:/res/baseline-play_circle_filled_white-48px.svg"
sourceSize {
width: 75
height: 75
}
}
Label {
id: appNameText
text: model.name
width: parent.width
height: 125
anchors.top: appIcon.bottom
font.pointSize: 22
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.Wrap
elide: Text.ElideRight
// Display a tooltip with the full name if it's truncated // Display a tooltip with the full name if it's truncated
ToolTip.text: model.name ToolTip.text: model.name
ToolTip.delay: 1000 ToolTip.delay: 1000
ToolTip.timeout: 5000 ToolTip.timeout: 5000
ToolTip.visible: (parent.hovered || parent.highlighted) && (!appNameText.visible || appNameText.truncated) ToolTip.visible: (parent.hovered || parent.highlighted) && truncated
}
ToolButton {
id: resumeButton
anchors.horizontalCenterOffset: appIcon.isPlaceholder ? -47 : 0
anchors.verticalCenterOffset: appIcon.isPlaceholder ? -75 : -60
anchors.centerIn: appIcon
visible: model.running
implicitWidth: 125
implicitHeight: 125
Image {
source: "qrc:/res/baseline-play_circle_filled_white-48px.svg"
anchors.centerIn: parent
sourceSize {
width: 75
height: 75
}
}
onClicked: {
launchOrResumeSelectedApp()
}
ToolTip.text: "Resume Game"
ToolTip.delay: 1000
ToolTip.timeout: 3000
ToolTip.visible: hovered
}
ToolButton {
id: quitButton
anchors.horizontalCenterOffset: appIcon.isPlaceholder ? 47 : 0
anchors.verticalCenterOffset: appIcon.isPlaceholder ? -75 : 60
anchors.centerIn: appIcon
visible: model.running
implicitWidth: 125
implicitHeight: 125
Image {
source: "qrc:/res/baseline-cancel-24px.svg"
anchors.centerIn: parent
sourceSize {
width: 75
height: 75
}
}
onClicked: {
doQuitGame()
}
ToolTip.text: "Quit Game"
ToolTip.delay: 1000
ToolTip.timeout: 3000
ToolTip.visible: hovered
}
Label {
id: appNameText
visible: appIcon.isPlaceholder
text: model.name
width: appIcon.width
height: model.running ? 175 : appIcon.height
leftPadding: 20
rightPadding: 20
anchors.left: appIcon.left
anchors.right: appIcon.right
anchors.bottom: appIcon.bottom
font.pointSize: 22
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.Wrap
elide: Text.ElideRight
} }
function launchOrResumeSelectedApp() function launchOrResumeSelectedApp()
@@ -181,64 +125,61 @@ CenteredGridView {
} }
onClicked: { onClicked: {
// Only allow clicking on the box art for non-running games. // Nothing is running or this app is running
// For running games, buttons will appear to resume or quit which launchOrResumeSelectedApp()
// will handle starting the game and clicks on the box art will
// be ignored.
if (!model.running) {
launchOrResumeSelectedApp()
}
} }
Keys.onReturnPressed: { MouseArea {
// Open the app context menu if activated via the gamepad or keyboard anchors.fill: parent
// for running games. If the game isn't running, the above onClicked acceptedButtons: Qt.RightButton
// method will handle the launch. onClicked: {
if (model.running) { // popup() ensures the menu appears under the mouse cursor
// This will be keyboard/gamepad driven so use if (appContextMenu.popup) {
// open() instead of popup() appContextMenu.popup()
appContextMenu.open() }
else {
// Qt 5.9 doesn't have popup()
appContextMenu.open()
}
} }
} }
Keys.onMenuPressed: { Keys.onMenuPressed: {
if (model.running) { // We must use open() here so the menu is positioned on
// This will be keyboard/gamepad driven so use // the ItemDelegate and not where the mouse cursor is
// open() instead of popup() appContextMenu.open()
appContextMenu.open()
}
} }
function doQuitGame() { Menu {
quitAppDialog.appName = appModel.getRunningAppName()
quitAppDialog.segueToStream = false
quitAppDialog.open()
}
NavigableMenu {
id: appContextMenu id: appContextMenu
NavigableMenuItem { NavigableMenuItem {
parentMenu: appContextMenu
text: model.running ? "Resume Game" : "Launch Game" text: model.running ? "Resume Game" : "Launch Game"
onTriggered: launchOrResumeSelectedApp() onTriggered: {
appContextMenu.close()
launchOrResumeSelectedApp()
}
} }
NavigableMenuItem { NavigableMenuItem {
parentMenu: appContextMenu
text: "Quit Game" text: "Quit Game"
onTriggered: doQuitGame() onTriggered: {
quitAppDialog.appName = appModel.getRunningAppName()
quitAppDialog.segueToStream = false
quitAppDialog.open()
}
visible: model.running visible: model.running
} }
} }
} }
NavigableMessageDialog { MessageDialog {
id: quitAppDialog id: quitAppDialog
modality:Qt.WindowModal
property string appName : "" property string appName : ""
property bool segueToStream : false property bool segueToStream : false
property string nextAppName: "" property string nextAppName: ""
property int nextAppIndex: 0 property int nextAppIndex: 0
text:"Are you sure you want to quit " + appName +"? Any unsaved progress will be lost." text:"Are you sure you want to quit " + appName +"? Any unsaved progress will be lost."
standardButtons: Dialog.Yes | Dialog.No standardButtons: StandardButton.Yes | StandardButton.No
function quitApp() { function quitApp() {
var component = Qt.createComponent("QuitSegue.qml") var component = Qt.createComponent("QuitSegue.qml")
@@ -260,8 +201,20 @@ CenteredGridView {
appModel.quitRunningApp() appModel.quitRunningApp()
} }
onYes: quitApp()
// For keyboard/gamepad navigation
onAccepted: quitApp() onAccepted: quitApp()
} }
ScrollBar.vertical: ScrollBar {} ScrollBar.vertical: ScrollBar {
parent: appGrid.parent
anchors {
top: appGrid.top
left: appGrid.right
bottom: appGrid.bottom
leftMargin: -10
}
}
} }
+1 -14
View File
@@ -1,15 +1,11 @@
import QtQuick 2.9 import QtQuick 2.9
import QtQuick.Controls 2.2 import QtQuick.Controls 2.2
import SdlGamepadKeyNavigation 1.0
// https://stackoverflow.com/questions/45029968/how-do-i-set-the-combobox-width-to-fit-the-largest-item // https://stackoverflow.com/questions/45029968/how-do-i-set-the-combobox-width-to-fit-the-largest-item
ComboBox { ComboBox {
property int textWidth property int textWidth
property int desiredWidth : leftPadding + textWidth + indicator.width + rightPadding
property int maximumWidth : parent.width
implicitWidth: desiredWidth < maximumWidth ? desiredWidth : maximumWidth implicitWidth: leftPadding + textWidth + indicator.width + rightPadding
TextMetrics { TextMetrics {
id: popupMetrics id: popupMetrics
@@ -32,13 +28,4 @@ ComboBox {
textWidth = Math.max(popupMetrics.width, textWidth) textWidth = Math.max(popupMetrics.width, textWidth)
} }
} }
popup.onAboutToShow: {
// Switch to normal navigation for combo boxes
SdlGamepadKeyNavigation.setUiNavMode(false)
}
popup.onAboutToHide: {
SdlGamepadKeyNavigation.setUiNavMode(true)
}
} }
-43
View File
@@ -1,43 +0,0 @@
import QtQuick 2.9
import QtQuick.Controls 2.2
GridView {
// Detect Qt 5.11 or earlier using presence of synchronousDrag.
// Prior to 5.12, the leftMargin and rightMargin values did not work.
property bool hasBrokenMargins: this.synchronousDrag === undefined
property int minMargin: 10
property real availableWidth: (parent.width - 2 * minMargin)
property int itemsPerRow: availableWidth / cellWidth
property real horizontalMargin: itemsPerRow < count && availableWidth >= cellWidth ?
(availableWidth % cellWidth) / 2 : minMargin
function updateMargins() {
leftMargin = horizontalMargin
rightMargin = horizontalMargin
if (hasBrokenMargins) {
anchors.leftMargin = leftMargin
anchors.rightMargin = rightMargin
}
}
onHorizontalMarginChanged: {
updateMargins()
}
Component.onCompleted: {
if (hasBrokenMargins) {
// This will cause an anchor conflict with the parent StackView
// which breaks animation, but otherwise the grid will not be
// centered in the window.
anchors.fill = parent
anchors.topMargin = topMargin
anchors.bottomMargin = bottomMargin
}
updateMargins()
}
boundsBehavior: Flickable.OvershootBounds
}
+18 -5
View File
@@ -1,10 +1,13 @@
import QtQuick 2.0 import QtQuick 2.0
import QtQuick.Controls 2.2 import QtQuick.Controls 2.2
import QtQuick.Dialogs 1.2
import ComputerManager 1.0 import ComputerManager 1.0
import Session 1.0 import Session 1.0
Item { Item {
anchors.fill: parent
function onSearchingComputer() { function onSearchingComputer() {
stageLabel.text = "Establishing connection to PC..." stageLabel.text = "Establishing connection to PC..."
} }
@@ -18,8 +21,13 @@ Item {
errorDialog.open() errorDialog.open()
} }
StackView.onActivated: { // The StackView will trigger a visibility change when
if (!launcher.isExecuted()) { // we're pushed onto it, causing our onVisibleChanged
// routine to run, but only if we start as invisible
visible: false
onVisibleChanged: {
if (visible && !launcher.isExecuted()) {
toolBar.visible = false toolBar.visible = false
launcher.searchingComputer.connect(onSearchingComputer) launcher.searchingComputer.connect(onSearchingComputer)
launcher.quittingApp.connect(onQuittingApp) launcher.quittingApp.connect(onQuittingApp)
@@ -47,11 +55,16 @@ Item {
} }
} }
ErrorMessageDialog { MessageDialog {
id: errorDialog id: errorDialog
modality:Qt.WindowModal
icon: StandardIcon.Critical
standardButtons: StandardButton.Ok
onClosed: { onVisibleChanged: {
Qt.quit() if (!visible) {
Qt.quit()
}
} }
} }
} }
+43 -9
View File
@@ -1,9 +1,13 @@
import QtQml 2.2
import QtQuick 2.0 import QtQuick 2.0
import QtQuick.Controls 2.2 import QtQuick.Controls 2.2
import QtQuick.Dialogs 1.2
import ComputerManager 1.0 import ComputerManager 1.0
Item { Item {
visible: false
function onSearchingComputer() { function onSearchingComputer() {
stageLabel.text = "Establishing connection to PC..." stageLabel.text = "Establishing connection to PC..."
} }
@@ -32,8 +36,8 @@ Item {
quitAppDialog.open() quitAppDialog.open()
} }
StackView.onActivated: { onVisibleChanged: {
if (!launcher.isExecuted()) { if (visible && !launcher.isExecuted()) {
toolBar.visible = false toolBar.visible = false
launcher.searchingComputer.connect(onSearchingComputer) launcher.searchingComputer.connect(onSearchingComputer)
launcher.searchingApp.connect(onSearchingApp) launcher.searchingApp.connect(onSearchingApp)
@@ -62,18 +66,26 @@ Item {
} }
} }
ErrorMessageDialog { MessageDialog {
id: errorDialog id: errorDialog
modality:Qt.WindowModal
onClosed: { icon: StandardIcon.Critical
Qt.quit(); standardButtons: StandardButton.Ok | StandardButton.Help
onHelp: {
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Troubleshooting");
}
onVisibleChanged: {
if (!visible) {
Qt.quit();
}
} }
} }
NavigableMessageDialog { MessageDialog {
id: quitAppDialog id: quitAppDialog
modality:Qt.WindowModal
text:"Are you sure you want to quit " + appName +"? Any unsaved progress will be lost." text:"Are you sure you want to quit " + appName +"? Any unsaved progress will be lost."
standardButtons: Dialog.Yes | Dialog.No standardButtons: StandardButton.Yes | StandardButton.No
property string appName : "" property string appName : ""
function quitApp() { function quitApp() {
@@ -84,7 +96,29 @@ Item {
launcher.quitRunningApp() launcher.quitRunningApp()
} }
onYes: quitApp()
// For keyboard/gamepad navigation
onAccepted: quitApp() onAccepted: quitApp()
onRejected: Qt.quit()
// Exit process if app quit is rejected (reacts also to closing of the
// dialog from title bar's close button).
// Note: this depends on undocumented behavior of visibleChanged()
// signal being emitted before yes() or accepted() has been emitted.
onVisibleChanged: {
if (!visible) {
quitTimer.start()
}
}
Component.onCompleted: {
yes.connect(quitTimer.stop)
accepted.connect(quitTimer.stop)
}
}
Timer {
id: quitTimer
interval: 100
onTriggered: Qt.quit()
} }
} }
-8
View File
@@ -1,8 +0,0 @@
import QtQuick 2.0
import QtQuick.Controls 2.2
import SystemProperties 1.0
NavigableMessageDialog {
standardButtons: Dialog.Ok | (SystemProperties.hasBrowser ? Dialog.Help : 0)
}
-17
View File
@@ -1,17 +0,0 @@
import QtQuick 2.0
import QtQuick.Controls 2.2
Dialog {
parent: overlay
x: Math.round((parent.width - width) / 2)
y: Math.round((parent.height - height) / 2)
onAboutToHide: {
// We must force focus back to the last item for platforms without
// support for more than one active window like Steam Link. If
// we don't, gamepad and keyboard navigation will break after a
// dialog appears.
stackView.forceActiveFocus()
}
}
-8
View File
@@ -1,8 +0,0 @@
import QtQuick 2.0
import QtQuick.Controls 2.2
Menu {
onOpened: {
itemAt(0).forceActiveFocus(Qt.TabFocusReason)
}
}
-16
View File
@@ -2,27 +2,11 @@ import QtQuick 2.0
import QtQuick.Controls 2.2 import QtQuick.Controls 2.2
MenuItem { MenuItem {
// Qt 5.10 has a menu property, but we need to support 5.9
// so we must make our own.
property Menu parentMenu
// Ensure focus can't be given to an invisible item // Ensure focus can't be given to an invisible item
enabled: visible enabled: visible
height: visible ? implicitHeight : 0 height: visible ? implicitHeight : 0
focusPolicy: visible ? Qt.TabFocus : Qt.NoFocus
onTriggered: {
// We must close the context menu first or
// it can steal focus from any dialogs that
// onTriggered may spawn.
parentMenu.close()
}
Keys.onReturnPressed: { Keys.onReturnPressed: {
triggered() triggered()
} }
Keys.onEscapePressed: {
parentMenu.close()
}
} }
-65
View File
@@ -1,65 +0,0 @@
import QtQuick 2.0
import QtQuick.Controls 2.2
import QtQuick.Layouts 1.2
NavigableDialog {
id: dialog
property alias text: dialogLabel.dialogText
property string helpText
property string helpUrl : "https://github.com/moonlight-stream/moonlight-docs/wiki/Troubleshooting"
property string helpTextSeparator : " "
onOpened: {
// Force keyboard focus on the label so keyboard navigation works
dialogLabel.forceActiveFocus()
}
RowLayout {
spacing: 10
Image {
source: (standardButtons & Dialog.Yes) ?
"qrc:/res/baseline-help_outline-24px.svg" :
"qrc:/res/baseline-error_outline-24px.svg"
sourceSize {
// The icon should be square so use the height as the width too
width: 50
height: 50
}
}
Label {
property string dialogText
id: dialogLabel
text: dialogText + ((helpText && (standardButtons & Dialog.Help)) ? (helpTextSeparator + helpText) : "")
wrapMode: Text.Wrap
elide: Label.ElideRight
// Cap the width so the dialog doesn't grow horizontally forever. This
// will cause word wrap to kick in.
Layout.maximumWidth: 400
Layout.maximumHeight: 400
Keys.onReturnPressed: {
accept()
}
Keys.onEscapePressed: {
reject()
}
}
}
footer: DialogButtonBox {
id: dialogButtonBox
standardButtons: dialog.standardButtons
onHelpRequested: {
Qt.openUrlExternally(helpUrl)
close()
}
}
}
-19
View File
@@ -2,27 +2,8 @@ import QtQuick 2.0
import QtQuick.Controls 2.2 import QtQuick.Controls 2.2
ToolButton { ToolButton {
property string iconSource
activeFocusOnTab: true activeFocusOnTab: true
// FIXME: We're using an Image here rather than icon.source because
// icons don't work on Qt 5.9 LTS.
Image {
id: image
source: iconSource
anchors.centerIn: parent.background
sourceSize {
width: parent.background.width * 1.10
height: parent.background.height * 1.10
}
}
// This determines the size of the Material highlight. We increase it
// from the default because we use larger than normal icons for TV readability.
background.width: (parent.height - parent.anchors.bottomMargin - parent.anchors.topMargin) * 0.60
background.height: (parent.height - parent.anchors.bottomMargin - parent.anchors.topMargin) * 0.60
Keys.onReturnPressed: { Keys.onReturnPressed: {
clicked() clicked()
} }
+115 -134
View File
@@ -1,6 +1,8 @@
import QtQuick 2.9 import QtQuick 2.9
import QtQuick.Controls 2.2 import QtQuick.Controls 2.2
import QtQuick.Dialogs 1.2
import QtQuick.Layouts 1.3 import QtQuick.Layouts 1.3
import QtQuick.Window 2.2
import ComputerModel 1.0 import ComputerModel 1.0
@@ -8,41 +10,48 @@ import ComputerManager 1.0
import StreamingPreferences 1.0 import StreamingPreferences 1.0
import SdlGamepadKeyNavigation 1.0 import SdlGamepadKeyNavigation 1.0
CenteredGridView { GridView {
property ComputerModel computerModel : createModel() property ComputerModel computerModel : createModel()
id: pcGrid id: pcGrid
focus: true focus: true
activeFocusOnTab: true activeFocusOnTab: true
topMargin: 20 anchors.fill: parent
bottomMargin: 5 anchors.leftMargin: (parent.width % (cellWidth + anchors.rightMargin)) / 2
cellWidth: 310; cellHeight: 330; anchors.topMargin: 20
anchors.rightMargin: 5
anchors.bottomMargin: 5
cellWidth: 350; cellHeight: 350;
objectName: "Computers" objectName: "Computers"
Component.onCompleted: { // The StackView will trigger a visibility change when
// Don't show any highlighted item until interacting with them. // we're pushed onto it, causing our onVisibleChanged
// We do this here instead of onActivated to avoid losing the user's // routine to run, but only if we start as invisible
// selection when backing out of a different page of the app. visible: false
currentIndex = -1
StreamingPreferences {
id: prefs
} }
StackView.onActivated: { SdlGamepadKeyNavigation {
// Setup signals on CM id: gamepadKeyNav
ComputerManager.computerAddCompleted.connect(addComplete) }
// This is a bit of a hack to do this here as opposed to main.qml, but onVisibleChanged: {
// we need it enabled before calling getConnectedGamepads() and PcView if (visible) {
// is never destroyed, so it should be okay. gamepadKeyNav.enable()
SdlGamepadKeyNavigation.enable() }
else {
// Highlight the first item if a gamepad is connected gamepadKeyNav.disable()
if (currentIndex == -1 && SdlGamepadKeyNavigation.getConnectedGamepads() > 0) {
currentIndex = 0
} }
} }
StackView.onDeactivating: { Component.onCompleted: {
ComputerManager.computerAddCompleted.disconnect(addComplete) // Setup signals on CM
ComputerManager.computerAddCompleted.connect(addComplete)
// Don't show any highlighted item until interacting with them
currentIndex = -1
} }
function pairingComplete(error) function pairingComplete(error)
@@ -53,7 +62,6 @@ CenteredGridView {
// Display a failed dialog if we got an error // Display a failed dialog if we got an error
if (error !== undefined) { if (error !== undefined) {
errorDialog.text = error errorDialog.text = error
errorDialog.helpText = ""
errorDialog.open() errorDialog.open()
} }
} }
@@ -61,8 +69,7 @@ CenteredGridView {
function addComplete(success) function addComplete(success)
{ {
if (!success) { if (!success) {
errorDialog.text = "Unable to connect to the specified PC." errorDialog.text = "Unable to connect to the specified PC. Click the Help button for possible solutions."
errorDialog.helpText = "Click the Help button for possible solutions."
errorDialog.open() errorDialog.open()
} }
} }
@@ -75,37 +82,18 @@ CenteredGridView {
return model return model
} }
Row {
anchors.centerIn: parent
spacing: 5
visible: pcGrid.count === 0
BusyIndicator {
id: searchSpinner
visible: StreamingPreferences.enableMdns
}
Label {
height: searchSpinner.height
elide: Label.ElideRight
text: StreamingPreferences.enableMdns ? "Searching for PCs with NVIDIA GameStream enabled..."
: "Automatic PC discovery is disabled. Add your PC manually."
font.pointSize: 20
verticalAlignment: Text.AlignVCenter
wrapMode: Text.Wrap
}
}
model: computerModel model: computerModel
delegate: NavigableItemDelegate { delegate: NavigableItemDelegate {
width: 300; height: 320; width: 300; height: 300;
grid: pcGrid grid: pcGrid
Image { Image {
id: pcIcon id: pcIcon
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
source: "qrc:/res/desktop_windows-48px.svg" source: {
model.addPc ? "qrc:/res/ic_add_to_queue_white_48px.svg" : "qrc:/res/ic_tv_white_48px.svg"
}
sourceSize { sourceSize {
width: 200 width: 200
height: 200 height: 200
@@ -117,8 +105,8 @@ CenteredGridView {
id: stateIcon id: stateIcon
anchors.horizontalCenter: pcIcon.horizontalCenter anchors.horizontalCenter: pcIcon.horizontalCenter
anchors.verticalCenter: pcIcon.verticalCenter anchors.verticalCenter: pcIcon.verticalCenter
anchors.verticalCenterOffset: -15 anchors.verticalCenterOffset: -10
visible: !model.statusUnknown && (!model.online || !model.paired) visible: !model.addPc && !model.statusUnknown && (!model.online || !model.paired)
source: !model.online ? "qrc:/res/baseline-warning-24px.svg" : "qrc:/res/baseline-lock-24px.svg" source: !model.online ? "qrc:/res/baseline-warning-24px.svg" : "qrc:/res/baseline-lock-24px.svg"
sourceSize { sourceSize {
width: 75 width: 75
@@ -130,10 +118,10 @@ CenteredGridView {
id: statusUnknownSpinner id: statusUnknownSpinner
anchors.horizontalCenter: pcIcon.horizontalCenter anchors.horizontalCenter: pcIcon.horizontalCenter
anchors.verticalCenter: pcIcon.verticalCenter anchors.verticalCenter: pcIcon.verticalCenter
anchors.verticalCenterOffset: -15 anchors.verticalCenterOffset: -10
width: 75 width: 75
height: 75 height: 75
visible: model.statusUnknown visible: !model.addPc && model.statusUnknown
} }
Label { Label {
@@ -142,17 +130,19 @@ CenteredGridView {
width: parent.width width: parent.width
anchors.top: pcIcon.bottom anchors.top: pcIcon.bottom
anchors.bottom: parent.bottom
font.pointSize: 36 font.pointSize: 36
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
wrapMode: Text.Wrap wrapMode: Text.Wrap
elide: Text.ElideRight
} }
NavigableMenu { Menu {
id: pcContextMenu id: pcContextMenu
NavigableMenuItem { NavigableMenuItem {
parentMenu: pcContextMenu text: "Wake PC"
onTriggered: computerModel.wakeComputer(index)
visible: !model.addPc && !model.online && model.wakeable
}
NavigableMenuItem {
text: "Delete PC" text: "Delete PC"
onTriggered: { onTriggered: {
deletePcDialog.pcIndex = index deletePcDialog.pcIndex = index
@@ -160,25 +150,13 @@ CenteredGridView {
deletePcDialog.open() deletePcDialog.open()
} }
} }
NavigableMenuItem {
parentMenu: pcContextMenu
text: "Rename PC"
onTriggered: {
renamePcDialog.pcIndex = index
renamePcDialog.originalName = model.name
renamePcDialog.open()
}
}
NavigableMenuItem {
parentMenu: pcContextMenu
text: "Wake PC"
onTriggered: computerModel.wakeComputer(index)
visible: !model.online && model.wakeable
}
} }
onClicked: { onClicked: {
if (model.online) { if (model.addPc) {
addPcDialog.open()
}
else if (model.online) {
if (model.paired) { if (model.paired) {
// go to game view // go to game view
var component = Qt.createComponent("AppView.qml") var component = Qt.createComponent("AppView.qml")
@@ -198,8 +176,7 @@ CenteredGridView {
} }
else { else {
// cannot pair while something is streaming or attempting to pair // cannot pair while something is streaming or attempting to pair
errorDialog.text = "You cannot pair while a previous session is still running on the host PC. Quit any running games or reboot the host PC, then try pairing again." errorDialog.text = "This PC is currently busy. Make sure to quit any running games and try again."
errorDialog.helpText = ""
errorDialog.open() errorDialog.open()
} }
} }
@@ -209,113 +186,117 @@ CenteredGridView {
} }
} }
onPressAndHold: {
// popup() ensures the menu appears under the mouse cursor
if (pcContextMenu.popup) {
pcContextMenu.popup()
}
else {
// Qt 5.9 doesn't have popup()
pcContextMenu.open()
}
}
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
acceptedButtons: Qt.RightButton; acceptedButtons: Qt.RightButton;
onClicked: { onClicked: {
parent.onPressAndHold() if (!model.addPc) {
// popup() ensures the menu appears under the mouse cursor
if (pcContextMenu.popup) {
pcContextMenu.popup()
}
else {
// Qt 5.9 doesn't have popup()
pcContextMenu.open()
}
}
} }
} }
Keys.onMenuPressed: { Keys.onMenuPressed: {
// We must use open() here so the menu is positioned on if (!model.addPc) {
// the ItemDelegate and not where the mouse cursor is // We must use open() here so the menu is positioned on
pcContextMenu.open() // the ItemDelegate and not where the mouse cursor is
pcContextMenu.open()
}
} }
} }
ErrorMessageDialog { MessageDialog {
id: errorDialog id: errorDialog
// don't allow edits to the rest of the window while open
// Using Setup-Guide here instead of Troubleshooting because it's likely that users modality:Qt.WindowModal
// will arrive here by forgetting to enable GameStream or not forwarding ports. icon: StandardIcon.Critical
helpUrl: "https://github.com/moonlight-stream/moonlight-docs/wiki/Setup-Guide" standardButtons: StandardButton.Ok | StandardButton.Help
onHelp: {
// Using Setup-Guide here instead of Troubleshooting because it's likely that users
// will arrive here by forgetting to enable GameStream or not forwarding ports.
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Setup-Guide");
}
} }
NavigableMessageDialog { MessageDialog {
id: pairDialog id: pairDialog
// Pairing dialog must be modal to prevent double-clicks from triggering
// pairing twice
modal: true
closePolicy: Popup.CloseOnEscape
// don't allow edits to the rest of the window while open // don't allow edits to the rest of the window while open
modality:Qt.WindowModal
property string pin : "0000" property string pin : "0000"
text:"Please enter " + pin + " on your GameStream PC. This dialog will close when pairing is completed." text:"Please enter " + pin + " on your GameStream PC. This dialog will close when pairing is completed."
standardButtons: Dialog.Cancel standardButtons: StandardButton.Cancel
onRejected: { onRejected: {
// FIXME: We should interrupt pairing here // FIXME: We should interrupt pairing here
} }
} }
NavigableMessageDialog { MessageDialog {
id: deletePcDialog id: deletePcDialog
// don't allow edits to the rest of the window while open // don't allow edits to the rest of the window while open
modality:Qt.WindowModal
property int pcIndex : -1; property int pcIndex : -1;
text:"Are you sure you want to remove this PC?" text:"Are you sure you want to remove this PC?"
standardButtons: Dialog.Yes | Dialog.No standardButtons: StandardButton.Yes | StandardButton.No
function deletePc() { function deletePc() {
console.log("deleting PC pairing for PC at index: " + pcIndex) console.log("deleting PC pairing for PC at index: " + pcIndex)
computerModel.deleteComputer(pcIndex); computerModel.deleteComputer(pcIndex);
} }
onYes: deletePc()
// For keyboard/gamepad activation
onAccepted: deletePc() onAccepted: deletePc()
} }
NavigableDialog { Dialog {
id: renamePcDialog id: addPcDialog
property string label: "Enter the new name for this PC:" property string label: "Enter the IP address of your GameStream PC"
property string originalName property string hint: "192.168.1.100"
property int pcIndex : -1; property alias editText : editTextItem
standardButtons: Dialog.Ok | Dialog.Cancel standardButtons: StandardButton.Ok | StandardButton.Cancel
onVisibleChanged: {
onOpened: { editTextItem.focus = true
// Force keyboard focus on the textbox so keyboard navigation works editTextItem.selectAll()
editText.forceActiveFocus()
} }
onClosed: {
editText.clear()
}
onAccepted: { onAccepted: {
if (editText.text) { ComputerManager.addNewHost(editText.text, false)
computerModel.renameComputer(pcIndex, editText.text)
}
} }
ColumnLayout { ColumnLayout {
Label { Text {
text: renamePcDialog.label id: addPcDialogLabel
font.bold: true text: addPcDialog.label
} }
Rectangle {
implicitWidth: parent.parent.width
height: editTextItem.height
TextField { TextInput {
id: editText id: editTextItem
placeholderText: renamePcDialog.originalName inputMethodHints: Qt.ImhPreferUppercase
Layout.fillWidth: true text: addPcDialog.hint
focus: true
Keys.onReturnPressed: {
renamePcDialog.accept()
} }
} }
} }
} }
ScrollBar.vertical: ScrollBar {} ScrollBar.vertical: ScrollBar {
parent: pcGrid.parent
anchors {
top: pcGrid.top
left: pcGrid.right
bottom: pcGrid.bottom
leftMargin: -10
}
}
} }
+25 -20
View File
@@ -1,5 +1,6 @@
import QtQuick 2.0 import QtQuick 2.0
import QtQuick.Controls 2.2 import QtQuick.Controls 2.2
import QtQuick.Dialogs 1.2
import ComputerManager 1.0 import ComputerManager 1.0
import Session 1.0 import Session 1.0
@@ -11,6 +12,13 @@ Item {
property string stageText : "Quitting " + appName + "..." property string stageText : "Quitting " + appName + "..."
anchors.fill: parent
// The StackView will trigger a visibility change when
// we're pushed onto it, causing our onVisibleChanged
// routine to run, but only if we start as invisible
visible: false
function quitAppCompleted(error) function quitAppCompleted(error)
{ {
// Display a failed dialog if we got an error // Display a failed dialog if we got an error
@@ -19,34 +27,28 @@ Item {
errorDialog.open() errorDialog.open()
} }
// Exit this view
stackView.pop()
// If we're supposed to launch another game after this, do so now // If we're supposed to launch another game after this, do so now
if (error === undefined && nextSession !== null) { if (error === undefined && nextSession !== null) {
var component = Qt.createComponent("StreamSegue.qml") var component = Qt.createComponent("StreamSegue.qml")
var segue = component.createObject(stackView, {"appName": nextAppName, "session": nextSession}) var segue = component.createObject(stackView, {"appName": nextAppName, "session": nextSession})
stackView.replace(segue) stackView.push(segue)
}
}
onVisibleChanged: {
if (visible) {
// Connect the quit completion signal
ComputerManager.quitAppCompleted.connect(quitAppCompleted)
} }
else { else {
// Exit this view // Disconnect the signal
stackView.pop() ComputerManager.quitAppCompleted.disconnect(quitAppCompleted)
} }
} }
StackView.onActivated: {
// Hide the toolbar before we start loading
toolBar.visible = false
// Connect the quit completion signal
ComputerManager.quitAppCompleted.connect(quitAppCompleted)
}
StackView.onDeactivating: {
// Show the toolbar again
toolBar.visible = true
// Disconnect the signal
ComputerManager.quitAppCompleted.disconnect(quitAppCompleted)
}
Row { Row {
anchors.centerIn: parent anchors.centerIn: parent
spacing: 5 spacing: 5
@@ -66,7 +68,10 @@ Item {
} }
} }
ErrorMessageDialog { MessageDialog {
id: errorDialog id: errorDialog
modality:Qt.WindowModal
icon: StandardIcon.Critical
standardButtons: StandardButton.Ok
} }
} }
+111 -222
View File
@@ -4,51 +4,61 @@ import QtQuick.Controls 2.2
import StreamingPreferences 1.0 import StreamingPreferences 1.0
import ComputerManager 1.0 import ComputerManager 1.0
import SdlGamepadKeyNavigation 1.0 import SdlGamepadKeyNavigation 1.0
import SystemProperties 1.0
Flickable { Flickable {
id: settingsPage id: settingsPage
objectName: "Settings" objectName: "Settings"
anchors.fill: parent
boundsBehavior: Flickable.OvershootBounds
contentWidth: settingsColumn1.width > settingsColumn2.width ? settingsColumn1.width : settingsColumn2.width contentWidth: settingsColumn1.width > settingsColumn2.width ? settingsColumn1.width : settingsColumn2.width
contentHeight: settingsColumn1.height > settingsColumn2.height ? settingsColumn1.height : settingsColumn2.height contentHeight: settingsColumn1.height > settingsColumn2.height ? settingsColumn1.height : settingsColumn2.height
ScrollBar.vertical: ScrollBar { ScrollBar.vertical: ScrollBar {
parent: settingsPage.parent
anchors { anchors {
left: parent.right top: settingsPage.top
left: settingsPage.right
bottom: settingsPage.bottom
leftMargin: -10 leftMargin: -10
} }
} }
StackView.onActivated: { StreamingPreferences {
// This enables Tab and BackTab based navigation rather than arrow keys. id: prefs
// It is required to shift focus between controls on the settings page. }
SdlGamepadKeyNavigation.setUiNavMode(true)
// Highlight the first item if a gamepad is connected // The StackView will trigger a visibility change when
if (SdlGamepadKeyNavigation.getConnectedGamepads() > 0) { // we're pushed onto it, causing our onVisibleChanged
resolutionComboBox.forceActiveFocus(Qt.TabFocus) // routine to run, but only if we start as invisible
visible: false
SdlGamepadKeyNavigation {
id: gamepadKeyNav
}
onVisibleChanged: {
if (visible) {
gamepadKeyNav.setSettingsMode(true)
gamepadKeyNav.enable()
}
else {
gamepadKeyNav.disable()
} }
} }
StackView.onDeactivating: { Component.onDestruction: {
SdlGamepadKeyNavigation.setUiNavMode(false) prefs.save()
// Save the prefs so the Session can observe the changes
StreamingPreferences.save()
} }
Column { Column {
padding: 10 padding: 10
id: settingsColumn1 id: settingsColumn1
width: settingsPage.width / 2 width: settingsPage.width / 2 - padding
spacing: 15
GroupBox { GroupBox {
id: basicSettingsGroupBox id: basicSettingsGroupBox
width: (parent.width - (parent.leftPadding + parent.rightPadding)) width: (parent.width - 2 * parent.padding)
padding: 12 padding: 12
title: "<font color=\"skyblue\">Basic Settings</font>" title: "<font color=\"skyblue\">Basic Settings</font>"
font.pointSize: 12 font.pointSize: 12
@@ -75,7 +85,6 @@ Flickable {
Row { Row {
spacing: 5 spacing: 5
width: parent.width
AutoResizingComboBox { AutoResizingComboBox {
// 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
@@ -88,11 +97,11 @@ Flickable {
// Some platforms have different desktop resolutions // Some platforms have different desktop resolutions
// and native resolutions (like macOS with Retina displays) // and native resolutions (like macOS with Retina displays)
if (displayResIndex === 0) { if (displayResIndex == 0) {
screenRect = SystemProperties.getDesktopResolution(displayIndex) screenRect = prefs.getDesktopResolution(displayIndex)
} }
else { else {
screenRect = SystemProperties.getNativeResolution(displayIndex) screenRect = prefs.getNativeResolution(displayIndex)
} }
if (screenRect.width === 0) { if (screenRect.width === 0) {
@@ -129,24 +138,10 @@ Flickable {
} }
} }
// Prune resolutions that are over the decoder's maximum
var max_pixels = SystemProperties.maximumResolution.width * SystemProperties.maximumResolution.height;
if (max_pixels > 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 (existing_width * existing_height > max_pixels) {
resolutionListModel.remove(j)
j--
}
}
}
// load the saved width/height, and iterate through the ComboBox until a match is found // load the saved width/height, and iterate through the ComboBox until a match is found
// and set it to that index. // and set it to that index.
var saved_width = StreamingPreferences.width var saved_width = prefs.width
var saved_height = StreamingPreferences.height var saved_height = prefs.height
currentIndex = 0 currentIndex = 0
for (var i = 0; i < resolutionListModel.count; i++) { for (var i = 0; i < resolutionListModel.count; i++) {
var el_width = parseInt(resolutionListModel.get(i).video_width); var el_width = parseInt(resolutionListModel.get(i).video_width);
@@ -163,7 +158,6 @@ Flickable {
} }
id: resolutionComboBox id: resolutionComboBox
maximumWidth: parent.width / 2
textRole: "text" textRole: "text"
model: ListModel { model: ListModel {
id: resolutionListModel id: resolutionListModel
@@ -196,14 +190,12 @@ Flickable {
var selectedHeight = parseInt(resolutionListModel.get(currentIndex).video_height) var selectedHeight = parseInt(resolutionListModel.get(currentIndex).video_height)
// Only modify the bitrate if the values actually changed // Only modify the bitrate if the values actually changed
if (StreamingPreferences.width !== selectedWidth || StreamingPreferences.height !== selectedHeight) { if (prefs.width !== selectedWidth || prefs.height !== selectedHeight) {
StreamingPreferences.width = selectedWidth prefs.width = selectedWidth
StreamingPreferences.height = selectedHeight prefs.height = selectedHeight
StreamingPreferences.bitrateKbps = StreamingPreferences.getDefaultBitrate(StreamingPreferences.width, prefs.bitrateKbps = prefs.getDefaultBitrate(prefs.width, prefs.height, prefs.fps);
StreamingPreferences.height, slider.value = prefs.bitrateKbps
StreamingPreferences.fps);
slider.value = StreamingPreferences.bitrateKbps
} }
} }
} }
@@ -212,14 +204,15 @@ Flickable {
function createModel() { function createModel() {
var fpsListModel = Qt.createQmlObject('import QtQuick 2.0; ListModel {}', parent, '') var fpsListModel = Qt.createQmlObject('import QtQuick 2.0; ListModel {}', parent, '')
var max_fps = SystemProperties.maximumStreamingFrameRate // Get the max supported FPS on this system
var max_fps = prefs.getMaximumStreamingFrameRate();
// Default entries // Default entries
fpsListModel.append({"text": "30 FPS", "video_fps": "30"}) fpsListModel.append({"text": "30 FPS", "video_fps": "30"})
fpsListModel.append({"text": "60 FPS", "video_fps": "60"}) fpsListModel.append({"text": "60 FPS", "video_fps": "60"})
// Add unsupported FPS values that come before the display max FPS // Add unsupported FPS values that come before the display max FPS
if (StreamingPreferences.unsupportedFps) { if (prefs.unsupportedFps) {
if (max_fps > 90) { if (max_fps > 90) {
fpsListModel.append({"text": "90 FPS (Unsupported)", "video_fps": "90"}) fpsListModel.append({"text": "90 FPS (Unsupported)", "video_fps": "90"})
} }
@@ -232,7 +225,7 @@ Flickable {
// handle wonky displays that report just over 60 Hz. // handle wonky displays that report just over 60 Hz.
if (max_fps > 64) { if (max_fps > 64) {
// Mark any FPS value greater than 120 as unsupported // Mark any FPS value greater than 120 as unsupported
if (StreamingPreferences.unsupportedFps && max_fps > 120) { if (prefs.unsupportedFps && max_fps > 120) {
fpsListModel.append({"text": max_fps+" FPS (Unsupported)", "video_fps": ""+max_fps}) fpsListModel.append({"text": max_fps+" FPS (Unsupported)", "video_fps": ""+max_fps})
} }
else if (max_fps > 120) { else if (max_fps > 120) {
@@ -244,7 +237,7 @@ Flickable {
} }
// Add unsupported FPS values that come after the display max FPS // Add unsupported FPS values that come after the display max FPS
if (StreamingPreferences.unsupportedFps) { if (prefs.unsupportedFps) {
if (max_fps < 90) { if (max_fps < 90) {
fpsListModel.append({"text": "90 FPS (Unsupported)", "video_fps": "90"}) fpsListModel.append({"text": "90 FPS (Unsupported)", "video_fps": "90"})
} }
@@ -259,7 +252,7 @@ Flickable {
function reinitialize() { function reinitialize() {
model = createModel() model = createModel()
var saved_fps = StreamingPreferences.fps var saved_fps = prefs.fps
currentIndex = 0 currentIndex = 0
for (var i = 0; i < model.count; i++) { for (var i = 0; i < model.count; i++) {
var el_fps = parseInt(model.get(i).video_fps); var el_fps = parseInt(model.get(i).video_fps);
@@ -280,20 +273,17 @@ Flickable {
} }
id: fpsComboBox id: fpsComboBox
maximumWidth: parent.width / 2
textRole: "text" textRole: "text"
// ::onActivated must be used, as it only listens for when the index is changed by a human // ::onActivated must be used, as it only listens for when the index is changed by a human
onActivated : { onActivated : {
var selectedFps = parseInt(model.get(currentIndex).video_fps) var selectedFps = parseInt(model.get(currentIndex).video_fps)
// Only modify the bitrate if the values actually changed // Only modify the bitrate if the values actually changed
if (StreamingPreferences.fps !== selectedFps) { if (prefs.fps !== selectedFps) {
StreamingPreferences.fps = selectedFps prefs.fps = selectedFps
StreamingPreferences.bitrateKbps = StreamingPreferences.getDefaultBitrate(StreamingPreferences.width, prefs.bitrateKbps = prefs.getDefaultBitrate(prefs.width, prefs.height, prefs.fps);
StreamingPreferences.height, slider.value = prefs.bitrateKbps
StreamingPreferences.fps);
slider.value = StreamingPreferences.bitrateKbps
} }
} }
} }
@@ -318,7 +308,7 @@ Flickable {
Slider { Slider {
id: slider id: slider
value: StreamingPreferences.bitrateKbps value: prefs.bitrateKbps
stepSize: 500 stepSize: 500
from : 500 from : 500
@@ -329,7 +319,7 @@ Flickable {
onValueChanged: { onValueChanged: {
bitrateTitle.text = "Video bitrate: " + (value / 1000.0) + " Mbps" bitrateTitle.text = "Video bitrate: " + (value / 1000.0) + " Mbps"
StreamingPreferences.bitrateKbps = value prefs.bitrateKbps = value
} }
} }
@@ -344,40 +334,25 @@ Flickable {
AutoResizingComboBox { AutoResizingComboBox {
// 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: {
// Set the recommended option based on the OS var savedWm = prefs.windowMode
currentIndex = 0
for (var i = 0; i < windowModeListModel.count; i++) { for (var i = 0; i < windowModeListModel.count; i++) {
var thisWm = windowModeListModel.get(i).val; var thisWm = windowModeListModel.get(i).val;
if (thisWm === StreamingPreferences.recommendedFullScreenMode) { if (savedWm === thisWm) {
windowModeListModel.get(i).text += " (Recommended)" currentIndex = i
windowModeListModel.move(i, 0, 1);
break break
} }
} }
currentIndex = 0
if (SystemProperties.hasWindowManager && !SystemProperties.rendererAlwaysFullScreen) {
var savedWm = StreamingPreferences.windowMode
for (var i = 0; i < windowModeListModel.count; i++) {
var thisWm = windowModeListModel.get(i).val;
if (savedWm === thisWm) {
currentIndex = i
break
}
}
}
activated(currentIndex) activated(currentIndex)
} }
id: windowModeComboBox id: windowModeComboBox
enabled: SystemProperties.hasWindowManager && !SystemProperties.rendererAlwaysFullScreen
hoverEnabled: true hoverEnabled: true
textRole: "text" textRole: "text"
model: ListModel { model: ListModel {
id: windowModeListModel id: windowModeListModel
ListElement { ListElement {
text: "Full-screen" text: "Full-screen (Recommended)"
val: StreamingPreferences.WM_FULLSCREEN val: StreamingPreferences.WM_FULLSCREEN
} }
ListElement { ListElement {
@@ -390,7 +365,7 @@ Flickable {
} }
} }
onActivated: { onActivated: {
StreamingPreferences.windowMode = windowModeListModel.get(currentIndex).val prefs.windowMode = windowModeListModel.get(currentIndex).val
} }
ToolTip.delay: 1000 ToolTip.delay: 1000
@@ -401,13 +376,12 @@ Flickable {
CheckBox { CheckBox {
id: vsyncCheck id: vsyncCheck
width: parent.width
hoverEnabled: true hoverEnabled: true
text: "V-Sync" text: "V-Sync"
font.pointSize: 12 font.pointSize: 12
checked: StreamingPreferences.enableVsync checked: prefs.enableVsync
onCheckedChanged: { onCheckedChanged: {
StreamingPreferences.enableVsync = checked prefs.enableVsync = checked
} }
ToolTip.delay: 1000 ToolTip.delay: 1000
@@ -418,14 +392,13 @@ Flickable {
CheckBox { CheckBox {
id: framePacingCheck id: framePacingCheck
width: parent.width
hoverEnabled: true hoverEnabled: true
text: "Frame pacing" text: "Frame pacing"
font.pointSize: 12 font.pointSize: 12
enabled: StreamingPreferences.enableVsync enabled: prefs.enableVsync
checked: StreamingPreferences.enableVsync && StreamingPreferences.framePacing checked: prefs.enableVsync && prefs.framePacing
onCheckedChanged: { onCheckedChanged: {
StreamingPreferences.framePacing = checked prefs.framePacing = checked
} }
ToolTip.delay: 1000 ToolTip.delay: 1000
ToolTip.timeout: 5000 ToolTip.timeout: 5000
@@ -438,7 +411,7 @@ Flickable {
GroupBox { GroupBox {
id: audioSettingsGroupBox id: audioSettingsGroupBox
width: (parent.width - (parent.leftPadding + parent.rightPadding)) width: (parent.width - 2 * parent.padding)
padding: 12 padding: 12
title: "<font color=\"skyblue\">Audio Settings</font>" title: "<font color=\"skyblue\">Audio Settings</font>"
font.pointSize: 12 font.pointSize: 12
@@ -458,7 +431,7 @@ Flickable {
AutoResizingComboBox { AutoResizingComboBox {
// 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: {
var saved_audio = StreamingPreferences.audioConfig var saved_audio = prefs.audioConfig
currentIndex = 0 currentIndex = 0
for (var i = 0; i < audioListModel.count; i++) { for (var i = 0; i < audioListModel.count; i++) {
var el_audio = audioListModel.get(i).val; var el_audio = audioListModel.get(i).val;
@@ -482,14 +455,10 @@ Flickable {
text: "5.1 surround sound" text: "5.1 surround sound"
val: StreamingPreferences.AC_51_SURROUND val: StreamingPreferences.AC_51_SURROUND
} }
ListElement {
text: "7.1 surround sound"
val: StreamingPreferences.AC_71_SURROUND
}
} }
// ::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 : {
StreamingPreferences.audioConfig = audioListModel.get(currentIndex).val prefs.audioConfig = audioListModel.get(currentIndex).val
} }
} }
@@ -498,7 +467,7 @@ Flickable {
GroupBox { GroupBox {
id: uiSettingsGroupBox id: uiSettingsGroupBox
width: (parent.width - (parent.leftPadding + parent.rightPadding)) width: (parent.width - 2 * parent.padding)
padding: 12 padding: 12
title: "<font color=\"skyblue\">UI Settings</font>" title: "<font color=\"skyblue\">UI Settings</font>"
font.pointSize: 12 font.pointSize: 12
@@ -509,58 +478,26 @@ Flickable {
CheckBox { CheckBox {
id: startMaximizedCheck id: startMaximizedCheck
width: parent.width
text: "Maximize Moonlight window on startup" text: "Maximize Moonlight window on startup"
font.pointSize: 12 font.pointSize: 12
enabled: SystemProperties.hasWindowManager checked: !prefs.startWindowed
checked: !StreamingPreferences.startWindowed || !SystemProperties.hasWindowManager
onCheckedChanged: { onCheckedChanged: {
StreamingPreferences.startWindowed = !checked prefs.startWindowed = !checked
} }
} }
CheckBox {
id: connectionWarningsCheck
width: parent.width
text: "Show connection quality warnings"
font.pointSize: 12
checked: StreamingPreferences.connectionWarnings
onCheckedChanged: {
StreamingPreferences.connectionWarnings = checked
}
}
CheckBox {
visible: SystemProperties.hasDiscordIntegration
id: discordPresenceCheck
width: parent.width
text: "Discord Rich Presence integration"
font.pointSize: 12
checked: StreamingPreferences.richPresence
onCheckedChanged: {
StreamingPreferences.richPresence = checked
}
ToolTip.delay: 1000
ToolTip.timeout: 5000
ToolTip.visible: hovered
ToolTip.text: "Updates your Discord status to display the name of the game you're streaming."
}
} }
} }
} }
Column { Column {
padding: 10 padding: 10
rightPadding: 20
anchors.left: settingsColumn1.right anchors.left: settingsColumn1.right
id: settingsColumn2 id: settingsColumn2
width: settingsPage.width / 2 width: settingsPage.width / 2 - padding
spacing: 15
GroupBox { GroupBox {
id: gamepadSettingsGroupBox id: gamepadSettingsGroupBox
width: (parent.width - (parent.leftPadding + parent.rightPadding)) width: (parent.width - parent.padding)
padding: 12 padding: 12
title: "<font color=\"skyblue\">Input Settings</font>" title: "<font color=\"skyblue\">Input Settings</font>"
font.pointSize: 12 font.pointSize: 12
@@ -571,12 +508,11 @@ Flickable {
CheckBox { CheckBox {
id: singleControllerCheck id: singleControllerCheck
width: parent.width
text: "Force gamepad #1 always present" text: "Force gamepad #1 always present"
font.pointSize: 12 font.pointSize: 12
checked: !StreamingPreferences.multiController checked: !prefs.multiController
onCheckedChanged: { onCheckedChanged: {
StreamingPreferences.multiController = !checked prefs.multiController = !checked
} }
ToolTip.delay: 1000 ToolTip.delay: 1000
@@ -587,63 +523,26 @@ Flickable {
} }
CheckBox { CheckBox {
id: absoluteMouseCheck id: rawInputCheck
hoverEnabled: true hoverEnabled: true
width: parent.width text: "Raw mouse input"
text: "Optimize mouse for remote desktop instead of games"
font.pointSize: 12 font.pointSize: 12
enabled: SystemProperties.hasWindowManager checked: !prefs.mouseAcceleration
checked: StreamingPreferences.absoluteMouseMode && SystemProperties.hasWindowManager
onCheckedChanged: { onCheckedChanged: {
StreamingPreferences.absoluteMouseMode = checked prefs.mouseAcceleration = !checked
}
ToolTip.delay: 1000
ToolTip.timeout: 5000
ToolTip.visible: hovered
ToolTip.text: "This enables mouse control without capturing the client's mouse cursor. It will not work in most games.\n
You can toggle this while streaming using Ctrl+Alt+Shift+M."
}
CheckBox {
id: absoluteTouchCheck
hoverEnabled: true
width: parent.width
text: "Use touchscreen as a trackpad"
font.pointSize: 12
checked: !StreamingPreferences.absoluteTouchMode
onCheckedChanged: {
StreamingPreferences.absoluteTouchMode = !checked
}
ToolTip.delay: 1000
ToolTip.timeout: 5000
ToolTip.visible: hovered
ToolTip.text: "When checked, the touchscreen acts like a trackpad. When unchecked, the touchscreen will directly control the mouse pointer."
}
CheckBox {
id: gamepadMouseCheck
hoverEnabled: true
width: parent.width
text: "Gamepad mouse mode support"
font.pointSize: 12
checked: StreamingPreferences.gamepadMouse
onCheckedChanged: {
StreamingPreferences.gamepadMouse = checked
} }
ToolTip.delay: 1000 ToolTip.delay: 1000
ToolTip.timeout: 3000 ToolTip.timeout: 3000
ToolTip.visible: hovered ToolTip.visible: hovered
ToolTip.text: "When enabled, holding the Start button will toggle mouse mode" ToolTip.text: "When checked, mouse input is not accelerated or scaled by the OS before passing to Moonlight"
} }
} }
} }
GroupBox { GroupBox {
id: hostSettingsGroupBox id: hostSettingsGroupBox
width: (parent.width - (parent.leftPadding + parent.rightPadding)) width: (parent.width - parent.padding)
padding: 12 padding: 12
title: "<font color=\"skyblue\">Host Settings</font>" title: "<font color=\"skyblue\">Host Settings</font>"
font.pointSize: 12 font.pointSize: 12
@@ -654,23 +553,21 @@ Flickable {
CheckBox { CheckBox {
id: optimizeGameSettingsCheck id: optimizeGameSettingsCheck
width: parent.width
text: "Optimize game settings" text: "Optimize game settings"
font.pointSize: 12 font.pointSize: 12
checked: StreamingPreferences.gameOptimizations checked: prefs.gameOptimizations
onCheckedChanged: { onCheckedChanged: {
StreamingPreferences.gameOptimizations = checked prefs.gameOptimizations = checked
} }
} }
CheckBox { CheckBox {
id: audioPcCheck id: audioPcCheck
width: parent.width
text: "Play audio on host PC" text: "Play audio on host PC"
font.pointSize: 12 font.pointSize: 12
checked: StreamingPreferences.playAudioOnHost checked: prefs.playAudioOnHost
onCheckedChanged: { onCheckedChanged: {
StreamingPreferences.playAudioOnHost = checked prefs.playAudioOnHost = checked
} }
} }
} }
@@ -678,7 +575,7 @@ Flickable {
GroupBox { GroupBox {
id: advancedSettingsGroupBox id: advancedSettingsGroupBox
width: (parent.width - (parent.leftPadding + parent.rightPadding)) width: (parent.width - parent.padding)
padding: 12 padding: 12
title: "<font color=\"skyblue\">Advanced Settings</font>" title: "<font color=\"skyblue\">Advanced Settings</font>"
font.pointSize: 12 font.pointSize: 12
@@ -698,7 +595,7 @@ Flickable {
AutoResizingComboBox { AutoResizingComboBox {
// 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: {
var saved_vds = StreamingPreferences.videoDecoderSelection var saved_vds = prefs.videoDecoderSelection
currentIndex = 0 currentIndex = 0
for (var i = 0; i < decoderListModel.count; i++) { for (var i = 0; i < decoderListModel.count; i++) {
var el_vds = decoderListModel.get(i).val; var el_vds = decoderListModel.get(i).val;
@@ -729,7 +626,7 @@ Flickable {
} }
// ::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 : {
StreamingPreferences.videoDecoderSelection = decoderListModel.get(currentIndex).val prefs.videoDecoderSelection = decoderListModel.get(currentIndex).val
} }
} }
@@ -744,7 +641,7 @@ Flickable {
AutoResizingComboBox { AutoResizingComboBox {
// 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: {
var saved_vcc = StreamingPreferences.videoCodecConfig var saved_vcc = prefs.videoCodecConfig
currentIndex = 0 currentIndex = 0
for(var i = 0; i < codecListModel.count; i++) { for(var i = 0; i < codecListModel.count; i++) {
var el_vcc = codecListModel.get(i).val; var el_vcc = codecListModel.get(i).val;
@@ -765,76 +662,68 @@ Flickable {
val: StreamingPreferences.VCC_AUTO val: StreamingPreferences.VCC_AUTO
} }
ListElement { ListElement {
text: "H.264" text: "Force H.264"
val: StreamingPreferences.VCC_FORCE_H264 val: StreamingPreferences.VCC_FORCE_H264
} }
ListElement { ListElement {
text: "HEVC (H.265)" text: "Force HEVC"
val: StreamingPreferences.VCC_FORCE_HEVC val: StreamingPreferences.VCC_FORCE_HEVC
} }
ListElement { // HDR seems to be broken in GFE 3.14.1, and even when that's fixed
text: "HEVC HDR (Experimental)" // we'll probably need to gate this feature on OS support in our
// renderers.
/* ListElement {
text: "Force HEVC HDR"
val: StreamingPreferences.VCC_FORCE_HEVC_HDR val: StreamingPreferences.VCC_FORCE_HEVC_HDR
} } */
} }
// ::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 : {
StreamingPreferences.videoCodecConfig = codecListModel.get(currentIndex).val prefs.videoCodecConfig = codecListModel.get(currentIndex).val
} }
} }
CheckBox { CheckBox {
id: unlockUnsupportedFps id: unlockUnsupportedFps
width: parent.width
text: "Unlock unsupported FPS options" text: "Unlock unsupported FPS options"
font.pointSize: 12 font.pointSize: 12
checked: StreamingPreferences.unsupportedFps checked: prefs.unsupportedFps
onCheckedChanged: { onCheckedChanged: {
// This is called on init, so only do the work if we've prefs.unsupportedFps = checked
// actually changed the value.
if (StreamingPreferences.unsupportedFps != checked) {
StreamingPreferences.unsupportedFps = checked
// The selectable FPS values depend on whether // The selectable FPS values depend on whether
// this option is enabled or not // this option is enabled or not
fpsComboBox.reinitialize() fpsComboBox.reinitialize()
}
} }
} }
CheckBox { CheckBox {
id: enableMdns id: enableMdns
width: parent.width
text: "Automatically find PCs on the local network (Recommended)" text: "Automatically find PCs on the local network (Recommended)"
font.pointSize: 12 font.pointSize: 12
checked: StreamingPreferences.enableMdns checked: prefs.enableMdns
onCheckedChanged: { onCheckedChanged: {
// This is called on init, so only do the work if we've prefs.enableMdns = checked
// actually changed the value.
if (StreamingPreferences.enableMdns != checked) {
StreamingPreferences.enableMdns = checked
// We must save the updated preference to ensure // We must save the updated preference to ensure
// ComputerManager can observe the change internally. // ComputerManager can observe the change internally.
StreamingPreferences.save() prefs.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()
ComputerManager.startPolling() ComputerManager.startPolling()
}
} }
} }
} }
CheckBox { CheckBox {
id: quitAppAfter id: quitAppAfter
width: parent.width
text: "Quit app after quitting session" text: "Quit app after quitting session"
font.pointSize: 12 font.pointSize: 12
checked: StreamingPreferences.quitAppAfter checked: prefs.quitAppAfter
onCheckedChanged: { onCheckedChanged: {
StreamingPreferences.quitAppAfter = checked prefs.quitAppAfter = checked
} }
} }
} }
+60 -61
View File
@@ -1,7 +1,9 @@
import QtQuick 2.0 import QtQuick 2.0
import QtQuick.Controls 2.2 import QtQuick.Controls 2.2
import QtQuick.Dialogs 1.2
import QtQuick.Window 2.2 import QtQuick.Window 2.2
import ComputerManager 1.0
import SdlGamepadKeyNavigation 1.0 import SdlGamepadKeyNavigation 1.0
import Session 1.0 import Session 1.0
@@ -11,6 +13,13 @@ Item {
property string stageText : "Starting " + appName + "..." property string stageText : "Starting " + appName + "..."
property bool quitAfter : false property bool quitAfter : false
anchors.fill: parent
// The StackView will trigger a visibility change when
// we're pushed onto it, causing our onVisibleChanged
// routine to run, but only if we start as invisible
visible: false
function stageStarting(stage) function stageStarting(stage)
{ {
// Update the spinner text // Update the spinner text
@@ -19,8 +28,8 @@ Item {
function stageFailed(stage, errorCode) function stageFailed(stage, errorCode)
{ {
// Display the error dialog after Session::exec() returns errorDialog.text = "Starting " + stage + " failed: Error " + errorCode
streamSegueErrorDialog.text = "Starting " + stage + " failed: Error " + errorCode errorDialog.open()
} }
function connectionStarted() function connectionStarted()
@@ -29,7 +38,6 @@ Item {
// see them briefly when we pop off the StackView // see them briefly when we pop off the StackView
stageSpinner.visible = false stageSpinner.visible = false
stageLabel.visible = false stageLabel.visible = false
hintText.visible = false
// Hide the window now that streaming has begun // Hide the window now that streaming has begun
window.visible = false window.visible = false
@@ -38,7 +46,7 @@ Item {
function displayLaunchError(text) function displayLaunchError(text)
{ {
// Display the error dialog after Session::exec() returns // Display the error dialog after Session::exec() returns
streamSegueErrorDialog.text = text errorDialog.text = text
} }
function displayLaunchWarning(text) function displayLaunchWarning(text)
@@ -46,7 +54,7 @@ Item {
// This toast appears for 3 seconds, just shorter than how long // This toast appears for 3 seconds, just shorter than how long
// Session will wait for it to be displayed. This gives it time // Session will wait for it to be displayed. This gives it time
// to transition to invisible before continuing. // to transition to invisible before continuing.
var toast = Qt.createQmlObject('import QtQuick.Controls 2.2; ToolTip {}', parent, '') var toast = Qt.createQmlObject('import QtQuick.Controls 2.3; ToolTip {}', parent, '')
toast.text = text toast.text = text
toast.timeout = 3000 toast.timeout = 3000
toast.visible = true toast.visible = true
@@ -64,14 +72,10 @@ Item {
function sessionFinished() function sessionFinished()
{ {
// Enable GUI gamepad usage now
SdlGamepadKeyNavigation.enable()
if (quitAfter) { if (quitAfter) {
if (streamSegueErrorDialog.text) { if (errorDialog.text) {
// Quit when the error dialog is acknowledged // Quit when the error dialog is acknowledged
streamSegueErrorDialog.quitAfter = quitAfter errorDialog.open()
streamSegueErrorDialog.open()
} }
else { else {
// Quit immediately // Quit immediately
@@ -88,73 +92,48 @@ Item {
// the Qt UI is visible again to prevent losing // the Qt UI is visible again to prevent losing
// focus on the dialog which would impact gamepad // focus on the dialog which would impact gamepad
// users. // users.
if (streamSegueErrorDialog.text) { if (errorDialog.text) {
streamSegueErrorDialog.quitAfter = quitAfter errorDialog.open()
streamSegueErrorDialog.open()
} }
} }
} }
StackView.onDeactivating: { // It's important that we don't call enable() here
// Show the toolbar again when popped off the stack // or it may interfere with the Session instance
toolBar.visible = true // getting notified of initial connected gamepads.
SdlGamepadKeyNavigation {
// Enable GUI gamepad usage now id: gamepadKeyNav
SdlGamepadKeyNavigation.enable()
} }
StackView.onActivated: { onVisibleChanged: {
// Hide the toolbar before we start loading if (visible) {
toolBar.visible = false // Hide the toolbar before we start loading
toolBar.visible = false
// Hook up our signals
session.stageStarting.connect(stageStarting)
session.stageFailed.connect(stageFailed)
session.connectionStarted.connect(connectionStarted)
session.displayLaunchError.connect(displayLaunchError)
session.displayLaunchWarning.connect(displayLaunchWarning)
session.quitStarting.connect(quitStarting)
session.sessionFinished.connect(sessionFinished)
// Kick off the stream
spinnerTimer.start()
streamLoader.active = true
}
Timer {
id: spinnerTimer
// Display the spinner appearance a bit to allow us to reach
// the code in Session.exec() that pumps the event loop.
// If we display it immediately, it will briefly hang in the
// middle of the animation on Windows, which looks very
// obviously broken.
interval: 100
onTriggered: stageSpinner.running = true
}
Loader {
id: streamLoader
active: false
asynchronous: true
onLoaded: {
// Set the hint text. We do this here rather than // Set the hint text. We do this here rather than
// in the hintText control itself to synchronize // in the hintText control itself to synchronize
// with Session.exec() which requires no concurrent // with Session.exec() which requires no concurrent
// gamepad usage. // gamepad usage.
hintText.text = SdlGamepadKeyNavigation.getConnectedGamepads() > 0 ? hintText.text = gamepadKeyNav.getConnectedGamepads() > 0 ?
"Tip: Press Start+Select+L1+R1 to disconnect your session" : "Tip: Press Start+Select+L1+R1 to disconnect your session" :
"Tip: Press Ctrl+Alt+Shift+Q to disconnect your session" "Tip: Press Ctrl+Alt+Shift+Q to disconnect your session"
// Stop GUI gamepad usage now // Hook up our signals
SdlGamepadKeyNavigation.disable() session.stageStarting.connect(stageStarting)
session.stageFailed.connect(stageFailed)
session.connectionStarted.connect(connectionStarted)
session.displayLaunchError.connect(displayLaunchError)
session.displayLaunchWarning.connect(displayLaunchWarning)
session.quitStarting.connect(quitStarting)
session.sessionFinished.connect(sessionFinished)
// Run the streaming session to completion // Run the streaming session to completion
session.exec(Screen.virtualX, Screen.virtualY) session.exec(Screen.virtualX, Screen.virtualY)
} }
else if (!quitAfter) {
sourceComponent: Item {} // Show the toolbar again when we become hidden
toolBar.visible = true
}
} }
Row { Row {
@@ -163,7 +142,6 @@ Item {
BusyIndicator { BusyIndicator {
id: stageSpinner id: stageSpinner
running: false
} }
Label { Label {
@@ -187,4 +165,25 @@ Item {
wrapMode: Text.Wrap wrapMode: Text.Wrap
} }
MessageDialog {
id: errorDialog
modality:Qt.WindowModal
icon: StandardIcon.Critical
standardButtons: StandardButton.Ok | StandardButton.Help
onAccepted: {
if (quitAfter) {
Qt.quit()
}
}
onHelp: {
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Troubleshooting");
if (quitAfter) {
Qt.quit()
}
}
}
} }
+17 -8
View File
@@ -22,6 +22,18 @@ QVariant ComputerModel::data(const QModelIndex& index, int role) const
return QVariant(); return QVariant();
} }
if (index.row() == m_Computers.count()) {
// We insert a synthetic item at the end for the Add PC option
switch (role) {
case NameRole:
return "Add PC";
case AddPcRole:
return true;
default:
return QVariant();
}
}
Q_ASSERT(index.row() < m_Computers.count()); Q_ASSERT(index.row() < m_Computers.count());
NvComputer* computer = m_Computers[index.row()]; NvComputer* computer = m_Computers[index.row()];
@@ -38,6 +50,8 @@ QVariant ComputerModel::data(const QModelIndex& index, int role) const
return computer->currentGameId != 0; return computer->currentGameId != 0;
case WakeableRole: case WakeableRole:
return !computer->macAddress.isEmpty(); return !computer->macAddress.isEmpty();
case AddPcRole:
return false;
case StatusUnknownRole: case StatusUnknownRole:
return computer->state == NvComputer::CS_UNKNOWN; return computer->state == NvComputer::CS_UNKNOWN;
default: default:
@@ -53,7 +67,8 @@ int ComputerModel::rowCount(const QModelIndex& parent) const
return 0; return 0;
} }
return m_Computers.count(); // Add PC placeholder counts as 1
return m_Computers.count() + 1;
} }
QHash<int, QByteArray> ComputerModel::roleNames() const QHash<int, QByteArray> ComputerModel::roleNames() const
@@ -64,6 +79,7 @@ QHash<int, QByteArray> ComputerModel::roleNames() const
names[OnlineRole] = "online"; names[OnlineRole] = "online";
names[PairedRole] = "paired"; names[PairedRole] = "paired";
names[BusyRole] = "busy"; names[BusyRole] = "busy";
names[AddPcRole] = "addPc";
names[WakeableRole] = "wakeable"; names[WakeableRole] = "wakeable";
names[StatusUnknownRole] = "statusUnknown"; names[StatusUnknownRole] = "statusUnknown";
@@ -128,13 +144,6 @@ void ComputerModel::wakeComputer(int computerIndex)
QThreadPool::globalInstance()->start(wakeTask); QThreadPool::globalInstance()->start(wakeTask);
} }
void ComputerModel::renameComputer(int computerIndex, QString name)
{
Q_ASSERT(computerIndex < m_Computers.count());
m_ComputerManager->renameHost(m_Computers[computerIndex], name);
}
void ComputerModel::pairComputer(int computerIndex, QString pin) void ComputerModel::pairComputer(int computerIndex, QString pin)
{ {
Q_ASSERT(computerIndex < m_Computers.count()); Q_ASSERT(computerIndex < m_Computers.count());
+2 -3
View File
@@ -14,7 +14,8 @@ class ComputerModel : public QAbstractListModel
PairedRole, PairedRole,
BusyRole, BusyRole,
WakeableRole, WakeableRole,
StatusUnknownRole StatusUnknownRole,
AddPcRole
}; };
public: public:
@@ -35,8 +36,6 @@ public:
Q_INVOKABLE void wakeComputer(int computerIndex); Q_INVOKABLE void wakeComputer(int computerIndex);
Q_INVOKABLE void renameComputer(int computerIndex, QString name);
Q_INVOKABLE Session* createSessionForCurrentGame(int computerIndex); Q_INVOKABLE Session* createSessionForCurrentGame(int computerIndex);
signals: signals:
+108 -164
View File
@@ -1,13 +1,14 @@
import QtQuick 2.9 import QtQuick 2.9
import QtQuick.Controls 2.2 import QtQuick.Controls 2.2
import QtQuick.Dialogs 1.2
import QtQuick.Layouts 1.3 import QtQuick.Layouts 1.3
import QtQuick.Window 2.2 import QtQuick.Window 2.2
import QtQuick.Controls.Material 2.1
import ComputerManager 1.0 import ComputerManager 1.0
import AutoUpdateChecker 1.0 import AutoUpdateChecker 1.0
import StreamingPreferences 1.0 import StreamingPreferences 1.0
import SystemProperties 1.0
import SdlGamepadKeyNavigation 1.0
ApplicationWindow { ApplicationWindow {
property bool pollingActive: false property bool pollingActive: false
@@ -17,7 +18,14 @@ ApplicationWindow {
width: 1280 width: 1280
height: 600 height: 600
visibility: (SystemProperties.hasWindowManager && StreamingPreferences.startWindowed) ? "Windowed" : "Maximized" visibility: prefs.startWindowed ? "Windowed" : "Maximized"
Material.theme: Material.Dark
Material.accent: Material.Purple
StreamingPreferences {
id: prefs
}
StackView { StackView {
id: stackView id: stackView
@@ -134,20 +142,19 @@ ApplicationWindow {
// onAfterRendering call and potentially reenter this code. // onAfterRendering call and potentially reenter this code.
initialized = true; initialized = true;
if (SystemProperties.isWow64) { if (prefs.isRunningWayland()) {
waylandDialog.open()
}
else if (prefs.isWow64()) {
wow64Dialog.open() wow64Dialog.open()
} }
else if (!SystemProperties.hasHardwareAcceleration) { else if (!prefs.hasAnyHardwareAcceleration()) {
if (SystemProperties.isRunningXWayland) { noHwDecoderDialog.open()
xWaylandDialog.open()
}
else {
noHwDecoderDialog.open()
}
} }
if (SystemProperties.unmappedGamepads) { var unmappedGamepads = prefs.getUnmappedGamepads()
unmappedGamepadDialog.unmappedGamepads = SystemProperties.unmappedGamepads if (unmappedGamepads) {
unmappedGamepadDialog.unmappedGamepads = unmappedGamepads
unmappedGamepadDialog.open() unmappedGamepadDialog.open()
} }
} }
@@ -171,32 +178,28 @@ ApplicationWindow {
header: ToolBar { header: ToolBar {
id: toolBar id: toolBar
height: 60
anchors.topMargin: 5 anchors.topMargin: 5
anchors.bottomMargin: 5 anchors.bottomMargin: 5
Label {
id: titleLabel
visible: toolBar.width > 700
anchors.fill: parent
text: stackView.currentItem.objectName
font.pointSize: 20
elide: Label.ElideRight
horizontalAlignment: Qt.AlignHCenter
verticalAlignment: Qt.AlignVCenter
}
RowLayout { RowLayout {
spacing: 20 spacing: 20
anchors.leftMargin: 10
anchors.rightMargin: 10
anchors.fill: parent anchors.fill: parent
NavigableToolButton { NavigableToolButton {
// Only make the button visible if the user has navigated somewhere. // Only make the button visible if the user has navigated somewhere.
visible: stackView.depth > 1 visible: stackView.depth > 1
iconSource: "qrc:/res/arrow_left.svg" // FIXME: We're using an Image here rather than icon.source because
// icons don't work on Qt 5.9 LTS.
Image {
source: "qrc:/res/arrow_left.svg"
anchors.centerIn: parent
sourceSize {
// The icon should be square so use the height as the width too
width: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
height: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
}
}
onClicked: stackView.pop() onClicked: stackView.pop()
@@ -205,55 +208,14 @@ ApplicationWindow {
} }
} }
// This label will appear when the window gets too small and
// we need to ensure the toolbar controls don't collide
Label { Label {
id: titleRowLabel id: titleLabel
font.pointSize: titleLabel.font.pointSize text: stackView.currentItem.objectName
font.pointSize: 15
elide: Label.ElideRight elide: Label.ElideRight
horizontalAlignment: Qt.AlignHCenter horizontalAlignment: Qt.AlignHCenter
verticalAlignment: Qt.AlignVCenter verticalAlignment: Qt.AlignVCenter
Layout.fillWidth: true Layout.fillWidth: true
// We need this label to always be visible so it can occupy
// the remaining space in the RowLayout. To "hide" it, we
// just set the text to empty string.
text: !titleLabel.visible ? stackView.currentItem.objectName : ""
}
Label {
id: versionLabel
visible: stackView.currentItem.objectName === "Settings"
text: "Version " + SystemProperties.versionString
font.pointSize: 12
horizontalAlignment: Qt.AlignRight
verticalAlignment: Qt.AlignVCenter
}
NavigableToolButton {
id: addPcButton
visible: stackView.currentItem.objectName === "Computers"
iconSource: "qrc:/res/ic_add_to_queue_white_48px.svg"
ToolTip.delay: 1000
ToolTip.timeout: 3000
ToolTip.visible: hovered
ToolTip.text: "Add PC manually" + (newPcShortcut.nativeText ? (" ("+newPcShortcut.nativeText+")") : "")
Shortcut {
id: newPcShortcut
sequence: StandardKey.New
onActivated: addPcButton.clicked()
}
onClicked: {
addPcDialog.open()
}
Keys.onDownPressed: {
stackView.currentItem.forceActiveFocus(Qt.TabFocus)
}
} }
NavigableToolButton { NavigableToolButton {
@@ -261,21 +223,24 @@ ApplicationWindow {
id: updateButton id: updateButton
iconSource: "qrc:/res/update.svg" Image {
source: "qrc:/res/update.svg"
anchors.centerIn: parent
sourceSize {
width: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
height: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
}
}
ToolTip.delay: 1000 ToolTip.delay: 1000
ToolTip.timeout: 3000 ToolTip.timeout: 3000
ToolTip.visible: hovered || visible ToolTip.visible: hovered
// Invisible until we get a callback notifying us that // Invisible until we get a callback notifying us that
// an update is available // an update is available
visible: false visible: false
onClicked: { onClicked: Qt.openUrlExternally(browserUrl);
if (SystemProperties.hasBrowser) {
Qt.openUrlExternally(browserUrl);
}
}
function updateAvailable(version, url) function updateAvailable(version, url)
{ {
@@ -296,9 +261,15 @@ ApplicationWindow {
NavigableToolButton { NavigableToolButton {
id: helpButton id: helpButton
visible: SystemProperties.hasBrowser
iconSource: "qrc:/res/question_mark.svg" Image {
source: "qrc:/res/question_mark.svg"
anchors.centerIn: parent
sourceSize {
width: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
height: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
}
}
ToolTip.delay: 1000 ToolTip.delay: 1000
ToolTip.timeout: 3000 ToolTip.timeout: 3000
@@ -328,7 +299,14 @@ ApplicationWindow {
ToolTip.visible: hovered ToolTip.visible: hovered
ToolTip.text: "Gamepad Mapper" ToolTip.text: "Gamepad Mapper"
iconSource: "qrc:/res/ic_videogame_asset_white_48px.svg" Image {
source: "qrc:/res/ic_videogame_asset_white_48px.svg"
anchors.centerIn: parent
sourceSize {
width: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
height: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
}
}
onClicked: navigateTo("qrc:/gui/GamepadMapper.qml", "Gamepad Mapping") onClicked: navigateTo("qrc:/gui/GamepadMapper.qml", "Gamepad Mapping")
@@ -340,7 +318,14 @@ ApplicationWindow {
NavigableToolButton { NavigableToolButton {
id: settingsButton id: settingsButton
iconSource: "qrc:/res/settings.svg" Image {
source: "qrc:/res/settings.svg"
anchors.centerIn: parent
sourceSize {
width: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
height: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
}
}
onClicked: navigateTo("qrc:/gui/SettingsView.qml", "Settings") onClicked: navigateTo("qrc:/gui/SettingsView.qml", "Settings")
@@ -362,108 +347,67 @@ ApplicationWindow {
} }
} }
ErrorMessageDialog { MessageDialog {
id: noHwDecoderDialog id: noHwDecoderDialog
modality:Qt.WindowModal
icon: StandardIcon.Warning
standardButtons: StandardButton.Ok | StandardButton.Help
text: "No functioning hardware accelerated H.264 video decoder was detected by Moonlight. " + text: "No functioning hardware accelerated H.264 video decoder was detected by Moonlight. " +
"Your streaming performance may be severely degraded in this configuration." "Your streaming performance may be severely degraded in this configuration. " +
helpText: "Click the Help button for more information on solving this problem." "Click the Help button for more information on solving this problem."
helpUrl: "https://github.com/moonlight-stream/moonlight-docs/wiki/Fixing-Hardware-Decoding-Problems" onHelp: {
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Fixing-Hardware-Decoding-Problems");
}
} }
ErrorMessageDialog { MessageDialog {
id: xWaylandDialog id: waylandDialog
text: "Hardware acceleration doesn't work on XWayland. Continuing on XWayland may result in poor streaming performance. " + modality:Qt.WindowModal
"Try running with QT_QPA_PLATFORM=wayland or switch to X11." icon: StandardIcon.Warning
helpText: "Click the Help button for more information." standardButtons: StandardButton.Ok | StandardButton.Help
helpUrl: "https://github.com/moonlight-stream/moonlight-docs/wiki/Fixing-Hardware-Decoding-Problems" text: "Moonlight does not support hardware acceleration on Wayland. Continuing on Wayland may result in poor streaming performance. " +
"Please switch to an X session for optimal performance."
onHelp: {
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Fixing-Hardware-Decoding-Problems");
}
} }
NavigableMessageDialog { MessageDialog {
id: wow64Dialog id: wow64Dialog
standardButtons: Dialog.Ok | Dialog.Cancel modality:Qt.WindowModal
icon: StandardIcon.Warning
standardButtons: StandardButton.Ok | StandardButton.Cancel
text: "This PC is running a 64-bit version of Windows. Please download the x64 version of Moonlight for the best streaming performance." text: "This PC is running a 64-bit version of Windows. Please download the x64 version of Moonlight for the best streaming performance."
onAccepted: { onAccepted: {
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-qt/releases"); Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-qt/releases");
} }
} }
ErrorMessageDialog { MessageDialog {
id: unmappedGamepadDialog id: unmappedGamepadDialog
property string unmappedGamepads : "" property string unmappedGamepads : ""
text: "Moonlight detected gamepads without a mapping:\n" + unmappedGamepads modality:Qt.WindowModal
helpTextSeparator: "\n\n" icon: StandardIcon.Warning
helpText: "Click the Help button for information on how to map your gamepads." standardButtons: StandardButton.Ok | StandardButton.Help
helpUrl: "https://github.com/moonlight-stream/moonlight-docs/wiki/Gamepad-Mapping" text: "Moonlight detected gamepads without a proper mapping. " +
"The following gamepads will not function until this is resolved: " + unmappedGamepads + "\n\n" +
"Click the Help button for information on how to map your gamepads."
onHelp: {
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Gamepad-Mapping");
}
} }
// This dialog appears when quitting via keyboard or gamepad button // This dialog appears when quitting via keyboard or gamepad button
NavigableMessageDialog { MessageDialog {
id: quitConfirmationDialog id: quitConfirmationDialog
standardButtons: Dialog.Yes | Dialog.No modality:Qt.WindowModal
icon: StandardIcon.Warning
standardButtons: StandardButton.Yes | StandardButton.No
text: "Are you sure you want to quit?" text: "Are you sure you want to quit?"
onYes: Qt.quit()
// For keyboard/gamepad navigation // For keyboard/gamepad navigation
onAccepted: Qt.quit() onAccepted: Qt.quit()
} }
// HACK: This belongs in StreamSegue but keeping a dialog around after the parent
// dies can trigger bugs in Qt 5.12 that cause the app to crash. For now, we will
// host this dialog in a QML component that is never destroyed.
//
// To repro: Start a stream, cut the network connection to trigger the "Connection
// terminated" dialog, wait until the app grid times out back to the PC grid, then
// try to dismiss the dialog.
ErrorMessageDialog {
id: streamSegueErrorDialog
property bool quitAfter: false
onClosed: {
if (quitAfter) {
Qt.quit()
}
// StreamSegue assumes its dialog will be re-created each time we
// start streaming, so fake it by wiping out the text each time.
text = ""
}
}
NavigableDialog {
id: addPcDialog
property string label: "Enter the IP address of your GameStream PC:"
standardButtons: Dialog.Ok | Dialog.Cancel
onOpened: {
// Force keyboard focus on the textbox so keyboard navigation works
editText.forceActiveFocus()
}
onClosed: {
editText.clear()
}
onAccepted: {
if (editText.text) {
ComputerManager.addNewHost(editText.text, false)
}
}
ColumnLayout {
Label {
text: addPcDialog.label
font.bold: true
}
TextField {
id: editText
Layout.fillWidth: true
focus: true
Keys.onReturnPressed: {
addPcDialog.accept()
}
}
}
}
} }
+43 -56
View File
@@ -10,7 +10,7 @@
SdlGamepadKeyNavigation::SdlGamepadKeyNavigation() SdlGamepadKeyNavigation::SdlGamepadKeyNavigation()
: m_Enabled(false), : m_Enabled(false),
m_UiNavMode(false), m_SettingsMode(false),
m_LastAxisNavigationEventTime(0) m_LastAxisNavigationEventTime(0)
{ {
m_PollingTimer = new QTimer(this); m_PollingTimer = new QTimer(this);
@@ -91,12 +91,6 @@ void SdlGamepadKeyNavigation::onPollingTimerFired()
while (SDL_PollEvent(&event)) { while (SDL_PollEvent(&event)) {
switch (event.type) { switch (event.type) {
case SDL_QUIT:
// SDL may send us a quit event since we initialize
// the video subsystem on startup. If we get one,
// forward it on for Qt to take care of.
QCoreApplication::instance()->quit();
break;
case SDL_CONTROLLERBUTTONDOWN: case SDL_CONTROLLERBUTTONDOWN:
case SDL_CONTROLLERBUTTONUP: case SDL_CONTROLLERBUTTONUP:
{ {
@@ -106,7 +100,7 @@ void SdlGamepadKeyNavigation::onPollingTimerFired()
switch (event.cbutton.button) { switch (event.cbutton.button) {
case SDL_CONTROLLER_BUTTON_DPAD_UP: case SDL_CONTROLLER_BUTTON_DPAD_UP:
if (m_UiNavMode) { if (m_SettingsMode) {
// Back-tab // Back-tab
sendKey(type, Qt::Key_Tab, Qt::ShiftModifier); sendKey(type, Qt::Key_Tab, Qt::ShiftModifier);
} }
@@ -115,7 +109,7 @@ void SdlGamepadKeyNavigation::onPollingTimerFired()
} }
break; break;
case SDL_CONTROLLER_BUTTON_DPAD_DOWN: case SDL_CONTROLLER_BUTTON_DPAD_DOWN:
if (m_UiNavMode) { if (m_SettingsMode) {
sendKey(type, Qt::Key_Tab); sendKey(type, Qt::Key_Tab);
} }
else { else {
@@ -124,12 +118,24 @@ void SdlGamepadKeyNavigation::onPollingTimerFired()
break; break;
case SDL_CONTROLLER_BUTTON_DPAD_LEFT: case SDL_CONTROLLER_BUTTON_DPAD_LEFT:
sendKey(type, Qt::Key_Left); sendKey(type, Qt::Key_Left);
if (m_SettingsMode) {
// Some settings controls respond to left/right (like the slider)
// and others respond to up/down (like combo boxes). They seem to
// be mutually exclusive though so let's just send both.
sendKey(type, Qt::Key_Up);
}
break; break;
case SDL_CONTROLLER_BUTTON_DPAD_RIGHT: case SDL_CONTROLLER_BUTTON_DPAD_RIGHT:
sendKey(type, Qt::Key_Right); sendKey(type, Qt::Key_Right);
if (m_SettingsMode) {
// Some settings controls respond to left/right (like the slider)
// and others respond to up/down (like combo boxes). They seem to
// be mutually exclusive though so let's just send both.
sendKey(type, Qt::Key_Down);
}
break; break;
case SDL_CONTROLLER_BUTTON_A: case SDL_CONTROLLER_BUTTON_A:
if (m_UiNavMode) { if (m_SettingsMode) {
sendKey(type, Qt::Key_Space); sendKey(type, Qt::Key_Space);
} }
else { else {
@@ -171,7 +177,7 @@ void SdlGamepadKeyNavigation::onPollingTimerFired()
// Do nothing // Do nothing
} }
else if (leftY < -30000) { else if (leftY < -30000) {
if (m_UiNavMode) { if (m_SettingsMode) {
// Back-tab // Back-tab
sendKey(QEvent::Type::KeyPress, Qt::Key_Tab, Qt::ShiftModifier); sendKey(QEvent::Type::KeyPress, Qt::Key_Tab, Qt::ShiftModifier);
sendKey(QEvent::Type::KeyRelease, Qt::Key_Tab, Qt::ShiftModifier); sendKey(QEvent::Type::KeyRelease, Qt::Key_Tab, Qt::ShiftModifier);
@@ -184,7 +190,7 @@ void SdlGamepadKeyNavigation::onPollingTimerFired()
m_LastAxisNavigationEventTime = SDL_GetTicks(); m_LastAxisNavigationEventTime = SDL_GetTicks();
} }
else if (leftY > 30000) { else if (leftY > 30000) {
if (m_UiNavMode) { if (m_SettingsMode) {
sendKey(QEvent::Type::KeyPress, Qt::Key_Tab); sendKey(QEvent::Type::KeyPress, Qt::Key_Tab);
sendKey(QEvent::Type::KeyRelease, Qt::Key_Tab); sendKey(QEvent::Type::KeyRelease, Qt::Key_Tab);
} }
@@ -198,38 +204,29 @@ void SdlGamepadKeyNavigation::onPollingTimerFired()
else if (leftX < -30000) { else if (leftX < -30000) {
sendKey(QEvent::Type::KeyPress, Qt::Key_Left); sendKey(QEvent::Type::KeyPress, Qt::Key_Left);
sendKey(QEvent::Type::KeyRelease, Qt::Key_Left); sendKey(QEvent::Type::KeyRelease, Qt::Key_Left);
if (m_SettingsMode) {
// Some settings controls respond to left/right (like the slider)
// and others respond to up/down (like combo boxes). They seem to
// be mutually exclusive though so let's just send both.
sendKey(QEvent::Type::KeyPress, Qt::Key_Up);
sendKey(QEvent::Type::KeyRelease, Qt::Key_Up);
}
m_LastAxisNavigationEventTime = SDL_GetTicks(); m_LastAxisNavigationEventTime = SDL_GetTicks();
} }
else if (leftX > 30000) { else if (leftX > 30000) {
sendKey(QEvent::Type::KeyPress, Qt::Key_Right); sendKey(QEvent::Type::KeyPress, Qt::Key_Right);
sendKey(QEvent::Type::KeyRelease, Qt::Key_Right); sendKey(QEvent::Type::KeyRelease, Qt::Key_Right);
if (m_SettingsMode) {
// Some settings controls respond to left/right (like the slider)
// and others respond to up/down (like combo boxes). They seem to
// be mutually exclusive though so let's just send both.
sendKey(QEvent::Type::KeyPress, Qt::Key_Down);
sendKey(QEvent::Type::KeyRelease, Qt::Key_Down);
}
m_LastAxisNavigationEventTime = SDL_GetTicks(); m_LastAxisNavigationEventTime = SDL_GetTicks();
} }
// In UI navigation mode (settings page), use the right stick to scroll
if (m_UiNavMode) {
short rightX = SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_RIGHTX);
short rightY = SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_RIGHTY);
QPoint wheelDelta;
if (rightX > 30000) {
wheelDelta.setX(30);
}
else if (rightX < -30000) {
wheelDelta.setX(-30);
}
if (rightY > 30000) {
wheelDelta.setY(-30);
}
else if (rightY < -30000) {
wheelDelta.setY(30);
}
if (!wheelDelta.isNull()) {
sendWheel(wheelDelta);
}
}
} }
} }
@@ -243,30 +240,19 @@ void SdlGamepadKeyNavigation::sendKey(QEvent::Type type, Qt::Key key, Qt::Keyboa
} }
} }
void SdlGamepadKeyNavigation::sendWheel(QPoint& angleDelta) void SdlGamepadKeyNavigation::setSettingsMode(bool settingsMode)
{ {
QGuiApplication* app = static_cast<QGuiApplication*>(QGuiApplication::instance()); m_SettingsMode = settingsMode;
QWindow* focusWindow = app->focusWindow();
if (focusWindow != nullptr) {
QPoint mousePos(focusWindow->width() / 2, focusWindow->height() / 2);
QPoint globalPos(focusWindow->mapToGlobal(mousePos));
#if QT_VERSION >= QT_VERSION_CHECK(5, 12, 0)
QWheelEvent wheelEvent(mousePos, globalPos, QPoint(), angleDelta, Qt::NoButton, Qt::NoModifier, Qt::NoScrollPhase, false, Qt::MouseEventSynthesizedByApplication);
#else
QWheelEvent wheelEvent(mousePos, globalPos, QPoint(), angleDelta, angleDelta.y(), Qt::Vertical, Qt::NoButton, Qt::NoModifier, Qt::NoScrollPhase, Qt::MouseEventSynthesizedByApplication, false);
#endif
app->sendEvent(focusWindow, &wheelEvent);
}
}
void SdlGamepadKeyNavigation::setUiNavMode(bool uiNavMode)
{
m_UiNavMode = uiNavMode;
} }
int SdlGamepadKeyNavigation::getConnectedGamepads() int SdlGamepadKeyNavigation::getConnectedGamepads()
{ {
Q_ASSERT(m_Enabled); if (SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) failed: %s",
SDL_GetError());
return 0;
}
int count = 0; int count = 0;
for (int i = 0; i < SDL_NumJoysticks(); i++) { for (int i = 0; i < SDL_NumJoysticks(); i++) {
@@ -275,5 +261,6 @@ int SdlGamepadKeyNavigation::getConnectedGamepads()
} }
} }
SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER);
return count; return count;
} }
+2 -3
View File
@@ -18,13 +18,12 @@ public:
Q_INVOKABLE void disable(); Q_INVOKABLE void disable();
Q_INVOKABLE void setUiNavMode(bool settingsMode); Q_INVOKABLE void setSettingsMode(bool settingsMode);
Q_INVOKABLE int getConnectedGamepads(); Q_INVOKABLE int getConnectedGamepads();
private: private:
void sendKey(QEvent::Type type, Qt::Key key, Qt::KeyboardModifiers modifiers = Qt::NoModifier); void sendKey(QEvent::Type type, Qt::Key key, Qt::KeyboardModifiers modifiers = Qt::NoModifier);
void sendWheel(QPoint& angleDelta);
private slots: private slots:
void onPollingTimerFired(); void onPollingTimerFired();
@@ -33,6 +32,6 @@ private:
QTimer* m_PollingTimer; QTimer* m_PollingTimer;
QList<SDL_GameController*> m_Gamepads; QList<SDL_GameController*> m_Gamepads;
bool m_Enabled; bool m_Enabled;
bool m_UiNavMode; bool m_SettingsMode;
Uint32 m_LastAxisNavigationEventTime; Uint32 m_LastAxisNavigationEventTime;
}; };
+36 -200
View File
@@ -6,11 +6,6 @@
#include <QMutex> #include <QMutex>
#include <QtDebug> #include <QtDebug>
#include <QNetworkProxyFactory> #include <QNetworkProxyFactory>
#include <QPalette>
#include <QFont>
#include <QCursor>
#include <QElapsedTimer>
#include <QFile>
// Don't let SDL hook our main function, since Qt is already // Don't let SDL hook our main function, since Qt is already
// doing the same thing. This needs to be before any headers // doing the same thing. This needs to be before any headers
@@ -22,21 +17,17 @@
#include "streaming/video/ffmpeg.h" #include "streaming/video/ffmpeg.h"
#endif #endif
#if defined(Q_OS_WIN32) #ifdef Q_OS_WIN32
#include "antihookingprotection.h" #include "antihookingprotection.h"
#elif defined(Q_OS_LINUX)
#include <openssl/ssl.h>
#endif #endif
#include "cli/quitstream.h" #include "cli/quitstream.h"
#include "cli/startstream.h" #include "cli/startstream.h"
#include "cli/commandlineparser.h" #include "cli/commandlineparser.h"
#include "path.h" #include "path.h"
#include "utils.h"
#include "gui/computermodel.h" #include "gui/computermodel.h"
#include "gui/appmodel.h" #include "gui/appmodel.h"
#include "backend/autoupdatechecker.h" #include "backend/autoupdatechecker.h"
#include "backend/systemproperties.h"
#include "streaming/session.h" #include "streaming/session.h"
#include "settings/streamingpreferences.h" #include "settings/streamingpreferences.h"
#include "gui/sdlgamepadkeynavigation.h" #include "gui/sdlgamepadkeynavigation.h"
@@ -57,7 +48,7 @@
#endif #endif
#ifdef USE_CUSTOM_LOGGER #ifdef USE_CUSTOM_LOGGER
static QElapsedTimer s_LoggerTime; static QTime s_LoggerTime;
static QTextStream s_LoggerStream(stdout); static QTextStream s_LoggerStream(stdout);
static QMutex s_LoggerLock; static QMutex s_LoggerLock;
#ifdef LOG_TO_FILE #ifdef LOG_TO_FILE
@@ -76,12 +67,7 @@ void logToLoggerStream(QString& message)
return; return;
} }
else if (s_LogLinesWritten == MAX_LOG_LINES) { else if (s_LogLinesWritten == MAX_LOG_LINES) {
s_LoggerStream << "Log size limit reached!"; s_LoggerStream << "Log size limit reached!" << endl;
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
s_LoggerStream << Qt::endl;
#else
s_LoggerStream << endl;
#endif
s_LogLimitReached = true; s_LogLimitReached = true;
return; return;
} }
@@ -150,6 +136,14 @@ void qtLogToDiskHandler(QtMsgType type, const QMessageLogContext&, const QString
break; break;
} }
// HACK: Avoid printing this internal Qt warning which seems to print every time we
// start our event loop for the QNetworkReply. I have found this warning in many
// unrelated bug reports, but none actually tackle this warning itself. It seems to be
// new in Qt 5.12.
if (msg.startsWith("QNetworkReplyHttpImplPrivate::_q_startOperation was called more than once")) {
return;
}
QTime logTime = QTime::fromMSecsSinceStartOfDay(s_LoggerTime.elapsed()); QTime logTime = QTime::fromMSecsSinceStartOfDay(s_LoggerTime.elapsed());
QString txt = QString("%1 - Qt %2: %3\n").arg(logTime.toString()).arg(typeTxt).arg(msg); QString txt = QString("%1 - Qt %2: %3\n").arg(logTime.toString()).arg(typeTxt).arg(msg);
@@ -237,8 +231,6 @@ LONG WINAPI UnhandledExceptionHandler(struct _EXCEPTION_POINTERS *ExceptionInfo)
int main(int argc, char *argv[]) int main(int argc, char *argv[])
{ {
SDL_SetMainReady();
// Set the app version for the QCommandLineParser's showVersion() command // Set the app version for the QCommandLineParser's showVersion() command
QCoreApplication::setApplicationVersion(VERSION_STR); QCoreApplication::setApplicationVersion(VERSION_STR);
@@ -288,48 +280,17 @@ int main(int argc, char *argv[])
SetUnhandledExceptionFilter(UnhandledExceptionHandler); SetUnhandledExceptionFilter(UnhandledExceptionHandler);
#endif #endif
#if defined(Q_OS_WIN32) #ifdef Q_OS_WIN32
// Force AntiHooking.dll to be statically imported and loaded // Force AntiHooking.dll to be statically imported and loaded
// by ntdll by calling a dummy function. // by ntdll by calling a dummy function.
AntiHookingDummyImport(); AntiHookingDummyImport();
#elif defined(Q_OS_LINUX)
// Force libssl.so to be directly linked to our binary, so
// linuxdeployqt can find it and include it in our AppImage.
// QtNetwork will pull it in via dlopen().
SSL_free(nullptr);
#endif #endif
// Avoid using High DPI on EGLFS. It breaks font rendering. QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
// https://bugreports.qt.io/browse/QTBUG-64377
//
// NB: We can't use QGuiApplication::platformName() here because it is only
// set once the QGuiApplication is created, which is too late to enable High DPI :(
if (WMUtils::isRunningWindowManager()) {
// Enable High DPI support
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
// Enable fractional High DPI scaling on Qt 5.14 and later
QGuiApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::PassThrough);
#endif
}
else {
#ifndef STEAM_LINK
if (!qEnvironmentVariableIsSet("QT_QPA_PLATFORM")) {
qInfo() << "Unable to detect Wayland or X11, so EGLFS will be used by default. Set QT_QPA_PLATFORM to override this.";
qputenv("QT_QPA_PLATFORM", "eglfs");
if (!QFile("/dev/dri").exists()) {
qWarning() << "Unable to find a KMSDRM display device!";
qWarning() << "On the Raspberry Pi, you must enable the 'fake KMS' driver in raspi-config to use Moonlight outside of the GUI environment.";
}
}
#endif
}
// This avoids using the default keychain for SSL, which may cause // This avoids using the default keychain for SSL, which may cause
// password prompts on macOS. // password prompts on macOS.
qputenv("QT_SSL_USE_TEMPORARY_KEYCHAIN", "1"); qputenv("QT_SSL_USE_TEMPORARY_KEYCHAIN", QByteArray("1"));
#ifdef Q_OS_WIN32 #ifdef Q_OS_WIN32
if (!qEnvironmentVariableIsSet("QT_OPENGL")) { if (!qEnvironmentVariableIsSet("QT_OPENGL")) {
@@ -350,116 +311,15 @@ int main(int argc, char *argv[])
// Register custom metatypes for use in signals // Register custom metatypes for use in signals
qRegisterMetaType<NvApp>("NvApp"); qRegisterMetaType<NvApp>("NvApp");
SDL_version compileVersion;
SDL_VERSION(&compileVersion);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Compiled with SDL %d.%d.%d",
compileVersion.major, compileVersion.minor, compileVersion.patch);
SDL_version runtimeVersion;
SDL_GetVersion(&runtimeVersion);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Running with SDL %d.%d.%d",
runtimeVersion.major, runtimeVersion.minor, runtimeVersion.patch);
// Allow the display to sleep by default. We will manually use SDL_DisableScreenSaver()
// and SDL_EnableScreenSaver() when appropriate. This hint must be set before
// initializing the SDL video subsystem to have any effect.
SDL_SetHint(SDL_HINT_VIDEO_ALLOW_SCREENSAVER, "1");
// For SDL backends that support it, use double buffering instead of triple buffering
// to save a frame of latency. This doesn't matter for MMAL or DRM renderers since they
// are drawing directly to the screen without involving SDL, but it may matter for other
// future KMSDRM platforms that use SDL for rendering.
SDL_SetHint(SDL_HINT_VIDEO_DOUBLE_BUFFER, "1");
if (SDL_InitSubSystem(SDL_INIT_TIMER) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_TIMER) failed: %s",
SDL_GetError());
return -1;
}
#ifdef STEAM_LINK
// Steam Link requires that we initialize video before creating our
// QGuiApplication in order to configure the framebuffer correctly.
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s",
SDL_GetError());
return -1;
}
#endif
// Use atexit() to ensure SDL_Quit() is called. This avoids
// racing with object destruction where SDL may be used.
atexit(SDL_Quit);
// Avoid the default behavior of changing the timer resolution to 1 ms.
// We don't want this all the time that Moonlight is open. We will set
// it manually when we start streaming.
SDL_SetHint(SDL_HINT_TIMER_RESOLUTION, "0");
// Disable minimize on focus loss by default. Users seem to want this off by default.
SDL_SetHint(SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS, "0");
// SDL 2.0.12 changes the default behavior to use the button label rather than the button
// position as most other software does. Set this back to 0 to stay consistent with prior
// releases of Moonlight.
SDL_SetHint("SDL_GAMECONTROLLER_USE_BUTTON_LABELS", "0");
#ifdef QT_DEBUG
// Allow thread naming using exceptions on debug builds. SDL doesn't use SEH
// when throwing the exceptions, so we don't enable it for release builds out
// of caution.
SDL_SetHint(SDL_HINT_WINDOWS_DISABLE_THREAD_NAMING, "0");
#endif
QGuiApplication app(argc, argv); QGuiApplication app(argc, argv);
// After the QGuiApplication is created, the platform stuff will be initialized
// and we can set the SDL video driver to match Qt.
if (WMUtils::isRunningWayland() && QGuiApplication::platformName() == "xcb") {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Detected XWayland. This will probably break hardware decoding! Try running with QT_QPA_PLATFORM=wayland or switch to X11.");
}
else if (QGuiApplication::platformName().startsWith("wayland")) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Detected Wayland");
qputenv("SDL_VIDEODRIVER", "wayland");
}
#ifdef STEAM_LINK
// Qt 5.9 from the Steam Link SDK is not able to load any fonts
// since the Steam Link doesn't include any of the ones it looks
// for. We know it has NotoSans so we will explicitly ask for that.
if (app.font().family().isEmpty()) {
qWarning() << "SL HACK: No default font - using NotoSans";
QFont fon("NotoSans");
app.setFont(fon);
}
// Move the mouse to the bottom right so it's invisible when using
// gamepad-only navigation.
QCursor().setPos(0xFFFF, 0xFFFF);
#elif !SDL_VERSION_ATLEAST(2, 0, 11) && defined(Q_OS_LINUX) && (defined(__arm__) || defined(__aarch64__))
if (qgetenv("SDL_VIDEO_GL_DRIVER").isEmpty() && QGuiApplication::platformName() == "eglfs") {
// Look for Raspberry Pi GLES libraries. SDL 2.0.10 and earlier needs some help finding
// the correct libraries for the KMSDRM backend if not compiled with the RPI backend enabled.
if (SDL_LoadObject("libbrcmGLESv2.so") != nullptr) {
qputenv("SDL_VIDEO_GL_DRIVER", "libbrcmGLESv2.so");
}
else if (SDL_LoadObject("/opt/vc/lib/libbrcmGLESv2.so") != nullptr) {
qputenv("SDL_VIDEO_GL_DRIVER", "/opt/vc/lib/libbrcmGLESv2.so");
}
}
#endif
app.setWindowIcon(QIcon(":/res/moonlight.svg")); app.setWindowIcon(QIcon(":/res/moonlight.svg"));
// Register our C++ types for QML // Register our C++ types for QML
qmlRegisterType<ComputerModel>("ComputerModel", 1, 0, "ComputerModel"); qmlRegisterType<ComputerModel>("ComputerModel", 1, 0, "ComputerModel");
qmlRegisterType<AppModel>("AppModel", 1, 0, "AppModel"); qmlRegisterType<AppModel>("AppModel", 1, 0, "AppModel");
qmlRegisterType<StreamingPreferences>("StreamingPreferences", 1, 0, "StreamingPreferences");
qmlRegisterType<SdlGamepadKeyNavigation>("SdlGamepadKeyNavigation", 1, 0, "SdlGamepadKeyNavigation");
qmlRegisterUncreatableType<Session>("Session", 1, 0, "Session", "Session cannot be created from QML"); qmlRegisterUncreatableType<Session>("Session", 1, 0, "Session", "Session cannot be created from QML");
qmlRegisterSingletonType<ComputerManager>("ComputerManager", 1, 0, qmlRegisterSingletonType<ComputerManager>("ComputerManager", 1, 0,
"ComputerManager", "ComputerManager",
@@ -471,48 +331,8 @@ int main(int argc, char *argv[])
[](QQmlEngine*, QJSEngine*) -> QObject* { [](QQmlEngine*, QJSEngine*) -> QObject* {
return new AutoUpdateChecker(); return new AutoUpdateChecker();
}); });
qmlRegisterSingletonType<SystemProperties>("SystemProperties", 1, 0,
"SystemProperties",
[](QQmlEngine*, QJSEngine*) -> QObject* {
return new SystemProperties();
});
qmlRegisterSingletonType<SdlGamepadKeyNavigation>("SdlGamepadKeyNavigation", 1, 0,
"SdlGamepadKeyNavigation",
[](QQmlEngine*, QJSEngine*) -> QObject* {
return new SdlGamepadKeyNavigation();
});
qmlRegisterSingletonType<StreamingPreferences>("StreamingPreferences", 1, 0,
"StreamingPreferences",
[](QQmlEngine*, QJSEngine*) -> QObject* {
return new StreamingPreferences();
});
// Create the identity manager on the main thread QQuickStyle::setStyle("Material");
IdentityManager::get();
#ifndef Q_OS_WINRT
// Use the dense material dark theme by default
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_STYLE")) {
qputenv("QT_QUICK_CONTROLS_STYLE", "Material");
}
#else
// Use universal dark on WinRT
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_STYLE")) {
qputenv("QT_QUICK_CONTROLS_STYLE", "Universal");
}
#endif
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_MATERIAL_THEME")) {
qputenv("QT_QUICK_CONTROLS_MATERIAL_THEME", "Dark");
}
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_MATERIAL_ACCENT")) {
qputenv("QT_QUICK_CONTROLS_MATERIAL_ACCENT", "Purple");
}
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_MATERIAL_VARIANT")) {
qputenv("QT_QUICK_CONTROLS_MATERIAL_VARIANT", "Dense");
}
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_UNIVERSAL_THEME")) {
qputenv("QT_QUICK_CONTROLS_UNIVERSAL_THEME", "Dark");
}
QQmlApplicationEngine engine; QQmlApplicationEngine engine;
QString initialView; QString initialView;
@@ -520,11 +340,11 @@ int main(int argc, char *argv[])
GlobalCommandLineParser parser; GlobalCommandLineParser parser;
switch (parser.parse(app.arguments())) { switch (parser.parse(app.arguments())) {
case GlobalCommandLineParser::NormalStartRequested: case GlobalCommandLineParser::NormalStartRequested:
initialView = "qrc:/gui/PcView.qml"; initialView = "PcView.qml";
break; break;
case GlobalCommandLineParser::StreamRequested: case GlobalCommandLineParser::StreamRequested:
{ {
initialView = "qrc:/gui/CliStartStreamSegue.qml"; initialView = "CliStartStreamSegue.qml";
StreamingPreferences* preferences = new StreamingPreferences(&app); StreamingPreferences* preferences = new StreamingPreferences(&app);
StreamCommandLineParser streamParser; StreamCommandLineParser streamParser;
streamParser.parse(app.arguments(), preferences); streamParser.parse(app.arguments(), preferences);
@@ -536,7 +356,7 @@ int main(int argc, char *argv[])
} }
case GlobalCommandLineParser::QuitRequested: case GlobalCommandLineParser::QuitRequested:
{ {
initialView = "qrc:/gui/CliQuitStreamSegue.qml"; initialView = "CliQuitStreamSegue.qml";
QuitCommandLineParser quitParser; QuitCommandLineParser quitParser;
quitParser.parse(app.arguments()); quitParser.parse(app.arguments());
auto launcher = new CliQuitStream::Launcher(quitParser.getHost(), &app); auto launcher = new CliQuitStream::Launcher(quitParser.getHost(), &app);
@@ -552,6 +372,22 @@ int main(int argc, char *argv[])
if (engine.rootObjects().isEmpty()) if (engine.rootObjects().isEmpty())
return -1; return -1;
SDL_SetMainReady();
if (SDL_Init(SDL_INIT_TIMER) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_Init() failed: %s",
SDL_GetError());
}
// Use atexit() to ensure SDL_Quit() is called. This avoids
// racing with object destruction where SDL may be used.
atexit(SDL_Quit);
// Avoid the default behavior of changing the timer resolution to 1 ms.
// We don't want this all the time that Moonlight is open. We will set
// it manually when we start streaming.
SDL_SetHint(SDL_HINT_TIMER_RESOLUTION, "0");
int err = app.exec(); int err = app.exec();
// Give worker tasks time to properly exit. Fixes PendingQuitTask // Give worker tasks time to properly exit. Fixes PendingQuitTask
+17 -18
View File
@@ -8,6 +8,7 @@
QString Path::s_LogDir; QString Path::s_LogDir;
QString Path::s_BoxArtCacheDir; QString Path::s_BoxArtCacheDir;
const QString Path::k_GameControllerMappingFile("gamecontrollerdb.txt");
QString Path::getLogDir() QString Path::getLogDir()
{ {
@@ -21,42 +22,40 @@ QString Path::getBoxArtCacheDir()
return s_BoxArtCacheDir; return s_BoxArtCacheDir;
} }
QByteArray Path::readDataFile(QString fileName) QString Path::getGamepadMappingFile()
{
QFile dataFile(getDataFilePath(fileName));
dataFile.open(QIODevice::ReadOnly);
return dataFile.readAll();
}
QString Path::getDataFilePath(QString fileName)
{ {
QString candidatePath; QString candidatePath;
// Check the current directory first // Check the current directory first
candidatePath = QDir(QDir::currentPath()).absoluteFilePath(fileName); candidatePath = QDir(QDir::currentPath()).absoluteFilePath(k_GameControllerMappingFile);
if (QFile::exists(candidatePath)) { if (QFile::exists(candidatePath)) {
qInfo() << "Found" << fileName << "at" << candidatePath;
return candidatePath; return candidatePath;
} }
// Now check the data directories (for Linux, in particular) // Now check the data directories (for Linux, in particular)
candidatePath = QStandardPaths::locate(QStandardPaths::DataLocation, fileName); candidatePath = QStandardPaths::locate(QStandardPaths::DataLocation, k_GameControllerMappingFile);
if (!candidatePath.isEmpty() && QFile::exists(candidatePath)) { if (!candidatePath.isEmpty() && QFile::exists(candidatePath)) {
qInfo() << "Found" << fileName << "at" << candidatePath;
return candidatePath; return candidatePath;
} }
// Now try the directory of our app installation (for Windows, if current dir doesn't find it) // Now try the directory of our app installation (for Windows, if current dir doesn't find it)
candidatePath = QDir(QCoreApplication::applicationDirPath()).absoluteFilePath(fileName); candidatePath = QDir(QCoreApplication::applicationDirPath()).absoluteFilePath(k_GameControllerMappingFile);
if (QFile::exists(candidatePath)) { if (QFile::exists(candidatePath)) {
qInfo() << "Found" << fileName << "at" << candidatePath;
return candidatePath; return candidatePath;
} }
// Return the QRC embedded copy // Now try the Resources folder in our app bundle for macOS
candidatePath = ":/data/" + fileName; QDir dir = QDir(QCoreApplication::applicationDirPath());
qInfo() << "Found" << fileName << "at" << candidatePath; dir.cdUp();
return QString(candidatePath); dir.cd("Resources");
dir.filePath(k_GameControllerMappingFile);
candidatePath = dir.absoluteFilePath(k_GameControllerMappingFile);
if (QFile::exists(candidatePath)) {
return candidatePath;
}
// Couldn't find it
return QString();
} }
void Path::initialize(bool portable) void Path::initialize(bool portable)
+2 -4
View File
@@ -9,14 +9,12 @@ public:
static QString getBoxArtCacheDir(); static QString getBoxArtCacheDir();
static QByteArray readDataFile(QString fileName); static QString getGamepadMappingFile();
// Only safe to use directly for Qt classes
static QString getDataFilePath(QString fileName);
static void initialize(bool portable); static void initialize(bool portable);
private: private:
static QString s_LogDir; static QString s_LogDir;
static QString s_BoxArtCacheDir; static QString s_BoxArtCacheDir;
static const QString k_GameControllerMappingFile;
}; };
-5
View File
@@ -13,10 +13,5 @@
<file>gui/CliQuitStreamSegue.qml</file> <file>gui/CliQuitStreamSegue.qml</file>
<file>gui/CliStartStreamSegue.qml</file> <file>gui/CliStartStreamSegue.qml</file>
<file>gui/AutoResizingComboBox.qml</file> <file>gui/AutoResizingComboBox.qml</file>
<file>gui/NavigableMenu.qml</file>
<file>gui/ErrorMessageDialog.qml</file>
<file>gui/NavigableMessageDialog.qml</file>
<file>gui/NavigableDialog.qml</file>
<file>gui/CenteredGridView.qml</file>
</qresource> </qresource>
</RCC> </RCC>
-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="#fff"><path d="M12 2C6.47 2 2 6.47 2 12s4.47 10 10 10 10-4.47 10-10S17.53 2 12 2zm5 13.59L15.59 17 12 13.41 8.41 17 7 15.59 10.59 12 7 8.41 8.41 7 12 10.59 15.59 7 17 8.41 13.41 12 17 15.59z"/><path d="M0 0h24v24H0z" fill="none"/></svg>

Before

Width:  |  Height:  |  Size: 325 B

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

Before

Width:  |  Height:  |  Size: 323 B

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

Before

Width:  |  Height:  |  Size: 397 B

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

Before

Width:  |  Height:  |  Size: 276 B

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

After

Width:  |  Height:  |  Size: 303 B

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

After

Width:  |  Height:  |  Size: 467 B

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

After

Width:  |  Height:  |  Size: 273 B

+3 -10
View File
@@ -1,25 +1,18 @@
<RCC> <RCC>
<qresource prefix="/"> <qresource prefix="/">
<file>res/ic_remove_circle_outline_white_48px.svg</file>
<file>res/ic_remove_from_queue_white_48px.svg</file>
<file>res/ic_videogame_asset_white_48px.svg</file> <file>res/ic_videogame_asset_white_48px.svg</file>
<file>res/desktop_windows-48px.svg</file> <file>res/ic_tv_white_48px.svg</file>
<file>res/ic_add_to_queue_white_48px.svg</file> <file>res/ic_add_to_queue_white_48px.svg</file>
<file>res/baseline-lock-24px.svg</file> <file>res/baseline-lock-24px.svg</file>
<file>res/baseline-play_circle_filled_white-48px.svg</file> <file>res/baseline-play_circle_filled_white-48px.svg</file>
<file>res/baseline-warning-24px.svg</file> <file>res/baseline-warning-24px.svg</file>
<file>res/baseline-cancel-24px.svg</file>
<file>res/no_app_image.png</file> <file>res/no_app_image.png</file>
<file>res/settings.svg</file> <file>res/settings.svg</file>
<file>res/arrow_left.svg</file> <file>res/arrow_left.svg</file>
<file>res/question_mark.svg</file> <file>res/question_mark.svg</file>
<file>res/moonlight.svg</file> <file>res/moonlight.svg</file>
<file>res/update.svg</file> <file>res/update.svg</file>
<file>res/baseline-help_outline-24px.svg</file>
<file>res/baseline-error_outline-24px.svg</file>
</qresource>
<qresource prefix="/data">
<file alias="gamecontrollerdb.txt">SDL_GameControllerDB/gamecontrollerdb.txt</file>
<file alias="ModeSeven.ttf">ModeSeven.ttf</file>
<file alias="egl.frag">shaders/egl.frag</file>
<file alias="egl.vert">shaders/egl.vert</file>
</qresource> </qresource>
</RCC> </RCC>
+12 -13
View File
@@ -26,13 +26,7 @@ MappingManager::MappingManager()
settings.endArray(); settings.endArray();
// Finally load mappings from SDL_HINT_GAMECONTROLLERCONFIG // Finally load mappings from SDL_HINT_GAMECONTROLLERCONFIG
QStringList sdlMappings = QStringList sdlMappings = QString::fromLocal8Bit(SDL_GetHint(SDL_HINT_GAMECONTROLLERCONFIG)).split('\n', QString::SkipEmptyParts);
QString::fromLocal8Bit(SDL_GetHint(SDL_HINT_GAMECONTROLLERCONFIG))
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
.split('\n', Qt::SkipEmptyParts);
#else
.split('\n', QString::SkipEmptyParts);
#endif
for (QString sdlMapping : sdlMappings) { for (QString sdlMapping : sdlMappings) {
SdlGamepadMapping mapping(sdlMapping); SdlGamepadMapping mapping(sdlMapping);
addMapping(mapping); addMapping(mapping);
@@ -60,10 +54,15 @@ void MappingManager::save()
void MappingManager::applyMappings() void MappingManager::applyMappings()
{ {
QByteArray mappingData = Path::readDataFile("gamecontrollerdb.txt"); QString mappingFile = Path::getGamepadMappingFile();
if (!mappingData.isEmpty()) { if (!mappingFile.isEmpty()) {
int newMappings = SDL_GameControllerAddMappingsFromRW( std::string mappingFileNative = QDir::toNativeSeparators(mappingFile).toStdString();
SDL_RWFromConstMem(mappingData.constData(), mappingData.size()), 1);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Loading gamepad mappings from: %s",
mappingFileNative.c_str());
int newMappings = SDL_GameControllerAddMappingsFromFile(mappingFileNative.c_str());
if (newMappings < 0) { if (newMappings < 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Error loading gamepad mappings: %s", "Error loading gamepad mappings: %s",
@@ -76,8 +75,8 @@ void MappingManager::applyMappings()
} }
} }
else { else {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Unable to load gamepad mapping file"); "No gamepad mapping file found");
} }
QList<SdlGamepadMapping> mappings = m_Mappings.values(); QList<SdlGamepadMapping> mappings = m_Mappings.values();
+128 -42
View File
@@ -1,4 +1,6 @@
#include "streamingpreferences.h" #include "streamingpreferences.h"
#include "streaming/session.h"
#include "streaming/streamutils.h"
#include <QSettings> #include <QSettings>
@@ -19,17 +21,9 @@
#define SER_UNSUPPORTEDFPS "unsupportedfps" #define SER_UNSUPPORTEDFPS "unsupportedfps"
#define SER_MDNS "mdns" #define SER_MDNS "mdns"
#define SER_QUITAPPAFTER "quitAppAfter" #define SER_QUITAPPAFTER "quitAppAfter"
#define SER_ABSMOUSEMODE "mouseacceleration" #define SER_MOUSEACCELERATION "mouseacceleration"
#define SER_ABSTOUCHMODE "abstouchmode"
#define SER_STARTWINDOWED "startwindowed" #define SER_STARTWINDOWED "startwindowed"
#define SER_FRAMEPACING "framepacing" #define SER_FRAMEPACING "framepacing"
#define SER_CONNWARNINGS "connwarnings"
#define SER_RICHPRESENCE "richpresence"
#define SER_GAMEPADMOUSE "gamepadmouse"
#define SER_DEFAULTVER "defaultver"
#define SER_PACKETSIZE "packetsize"
#define CURRENT_DEFAULT_VER 1
StreamingPreferences::StreamingPreferences(QObject *parent) StreamingPreferences::StreamingPreferences(QObject *parent)
: QObject(parent) : QObject(parent)
@@ -41,14 +35,6 @@ void StreamingPreferences::reload()
{ {
QSettings settings; QSettings settings;
int defaultVer = settings.value(SER_DEFAULTVER, 0).toInt();
#ifdef Q_OS_DARWIN
recommendedFullScreenMode = WindowMode::WM_FULLSCREEN_DESKTOP;
#else
recommendedFullScreenMode = WindowMode::WM_FULLSCREEN;
#endif
width = settings.value(SER_WIDTH, 1280).toInt(); width = settings.value(SER_WIDTH, 1280).toInt();
height = settings.value(SER_HEIGHT, 720).toInt(); height = settings.value(SER_HEIGHT, 720).toInt();
fps = settings.value(SER_FPS, 60).toInt(); fps = settings.value(SER_FPS, 60).toInt();
@@ -60,14 +46,9 @@ void StreamingPreferences::reload()
unsupportedFps = settings.value(SER_UNSUPPORTEDFPS, false).toBool(); unsupportedFps = settings.value(SER_UNSUPPORTEDFPS, false).toBool();
enableMdns = settings.value(SER_MDNS, true).toBool(); enableMdns = settings.value(SER_MDNS, true).toBool();
quitAppAfter = settings.value(SER_QUITAPPAFTER, false).toBool(); quitAppAfter = settings.value(SER_QUITAPPAFTER, false).toBool();
absoluteMouseMode = settings.value(SER_ABSMOUSEMODE, false).toBool(); mouseAcceleration = settings.value(SER_MOUSEACCELERATION, false).toBool();
absoluteTouchMode = settings.value(SER_ABSTOUCHMODE, true).toBool(); startWindowed = settings.value(SER_STARTWINDOWED, false).toBool();
startWindowed = settings.value(SER_STARTWINDOWED, true).toBool();
framePacing = settings.value(SER_FRAMEPACING, false).toBool(); framePacing = settings.value(SER_FRAMEPACING, false).toBool();
connectionWarnings = settings.value(SER_CONNWARNINGS, true).toBool();
richPresence = settings.value(SER_RICHPRESENCE, true).toBool();
gamepadMouse = settings.value(SER_GAMEPADMOUSE, true).toBool();
packetSize = settings.value(SER_PACKETSIZE, 0).toInt();
audioConfig = static_cast<AudioConfig>(settings.value(SER_AUDIOCFG, audioConfig = static_cast<AudioConfig>(settings.value(SER_AUDIOCFG,
static_cast<int>(AudioConfig::AC_STEREO)).toInt()); static_cast<int>(AudioConfig::AC_STEREO)).toInt());
videoCodecConfig = static_cast<VideoCodecConfig>(settings.value(SER_VIDEOCFG, videoCodecConfig = static_cast<VideoCodecConfig>(settings.value(SER_VIDEOCFG,
@@ -77,17 +58,7 @@ void StreamingPreferences::reload()
windowMode = static_cast<WindowMode>(settings.value(SER_WINDOWMODE, windowMode = static_cast<WindowMode>(settings.value(SER_WINDOWMODE,
// Try to load from the old preference value too // Try to load from the old preference value too
static_cast<int>(settings.value(SER_FULLSCREEN, true).toBool() ? static_cast<int>(settings.value(SER_FULLSCREEN, true).toBool() ?
recommendedFullScreenMode : WindowMode::WM_WINDOWED)).toInt()); WindowMode::WM_FULLSCREEN : WindowMode::WM_WINDOWED)).toInt());
// Perform default settings updates as required based on last default version
if (defaultVer == 0) {
#ifdef Q_OS_DARWIN
// Update window mode setting on macOS from full-screen (old default) to borderless windowed (new default)
if (windowMode == WindowMode::WM_FULLSCREEN) {
windowMode = WindowMode::WM_FULLSCREEN_DESKTOP;
}
#endif
}
} }
void StreamingPreferences::save() void StreamingPreferences::save()
@@ -105,19 +76,134 @@ void StreamingPreferences::save()
settings.setValue(SER_UNSUPPORTEDFPS, unsupportedFps); settings.setValue(SER_UNSUPPORTEDFPS, unsupportedFps);
settings.setValue(SER_MDNS, enableMdns); settings.setValue(SER_MDNS, enableMdns);
settings.setValue(SER_QUITAPPAFTER, quitAppAfter); settings.setValue(SER_QUITAPPAFTER, quitAppAfter);
settings.setValue(SER_ABSMOUSEMODE, absoluteMouseMode); settings.setValue(SER_MOUSEACCELERATION, mouseAcceleration);
settings.setValue(SER_ABSTOUCHMODE, absoluteTouchMode);
settings.setValue(SER_STARTWINDOWED, startWindowed); settings.setValue(SER_STARTWINDOWED, startWindowed);
settings.setValue(SER_FRAMEPACING, framePacing); settings.setValue(SER_FRAMEPACING, framePacing);
settings.setValue(SER_CONNWARNINGS, connectionWarnings);
settings.setValue(SER_RICHPRESENCE, richPresence);
settings.setValue(SER_GAMEPADMOUSE, gamepadMouse);
settings.setValue(SER_PACKETSIZE, packetSize);
settings.setValue(SER_AUDIOCFG, static_cast<int>(audioConfig)); settings.setValue(SER_AUDIOCFG, static_cast<int>(audioConfig));
settings.setValue(SER_VIDEOCFG, static_cast<int>(videoCodecConfig)); settings.setValue(SER_VIDEOCFG, static_cast<int>(videoCodecConfig));
settings.setValue(SER_VIDEODEC, static_cast<int>(videoDecoderSelection)); settings.setValue(SER_VIDEODEC, static_cast<int>(videoDecoderSelection));
settings.setValue(SER_WINDOWMODE, static_cast<int>(windowMode)); settings.setValue(SER_WINDOWMODE, static_cast<int>(windowMode));
settings.setValue(SER_DEFAULTVER, CURRENT_DEFAULT_VER); }
bool StreamingPreferences::hasAnyHardwareAcceleration()
{
// Always use VDS_AUTO to avoid spamming the user with warnings
// if they've forced software decoding.
return Session::isHardwareDecodeAvailable(VDS_AUTO,
VIDEO_FORMAT_H264,
1920, 1080, 60);
}
bool StreamingPreferences::isRunningWayland()
{
return qgetenv("XDG_SESSION_TYPE") == QByteArray("wayland");
}
bool StreamingPreferences::isWow64()
{
#ifdef Q_OS_WIN32
return QSysInfo::currentCpuArchitecture() != QSysInfo::buildCpuArchitecture();
#else
return false;
#endif
}
QString StreamingPreferences::getUnmappedGamepads()
{
return SdlInputHandler::getUnmappedGamepads();
}
int StreamingPreferences::getMaximumStreamingFrameRate()
{
// Never let the maximum drop below 60 FPS
int maxFrameRate = 60;
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s",
SDL_GetError());
return maxFrameRate;
}
SDL_DisplayMode mode, bestMode, desktopMode;
for (int displayIndex = 0; displayIndex < SDL_GetNumVideoDisplays(); displayIndex++) {
// Get the native desktop resolution
if (StreamUtils::getRealDesktopMode(displayIndex, &desktopMode)) {
// Start at desktop mode and work our way up
bestMode = desktopMode;
for (int i = 0; i < SDL_GetNumDisplayModes(displayIndex); i++) {
if (SDL_GetDisplayMode(displayIndex, i, &mode) == 0) {
if (mode.w == desktopMode.w && mode.h == desktopMode.h) {
if (mode.refresh_rate > bestMode.refresh_rate) {
bestMode = mode;
}
}
}
}
maxFrameRate = qMax(maxFrameRate, bestMode.refresh_rate);
}
}
SDL_QuitSubSystem(SDL_INIT_VIDEO);
return maxFrameRate;
}
QRect StreamingPreferences::getDesktopResolution(int displayIndex)
{
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s",
SDL_GetError());
return QRect();
}
if (displayIndex >= SDL_GetNumVideoDisplays()) {
SDL_QuitSubSystem(SDL_INIT_VIDEO);
return QRect();
}
SDL_DisplayMode mode;
int err = SDL_GetDesktopDisplayMode(displayIndex, &mode);
SDL_QuitSubSystem(SDL_INIT_VIDEO);
if (err == 0) {
return QRect(0, 0, mode.w, mode.h);
}
else {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_GetDesktopDisplayMode() failed: %s",
SDL_GetError());
return QRect();
}
}
QRect StreamingPreferences::getNativeResolution(int displayIndex)
{
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s",
SDL_GetError());
return QRect();
}
if (displayIndex >= SDL_GetNumVideoDisplays()) {
SDL_QuitSubSystem(SDL_INIT_VIDEO);
return QRect();
}
SDL_DisplayMode mode;
bool success = StreamUtils::getRealDesktopMode(displayIndex, &mode);
SDL_QuitSubSystem(SDL_INIT_VIDEO);
if (success) {
return QRect(0, 0, mode.w, mode.h);
}
else {
return QRect();
}
} }
int StreamingPreferences::getDefaultBitrate(int width, int height, int fps) int StreamingPreferences::getDefaultBitrate(int width, int height, int fps)
+18 -20
View File
@@ -15,13 +15,26 @@ public:
Q_INVOKABLE void save(); Q_INVOKABLE void save();
Q_INVOKABLE static bool hasAnyHardwareAcceleration();
Q_INVOKABLE static bool isRunningWayland();
Q_INVOKABLE static bool isWow64();
Q_INVOKABLE static int getMaximumStreamingFrameRate();
Q_INVOKABLE QRect getDesktopResolution(int displayIndex);
Q_INVOKABLE QRect getNativeResolution(int displayIndex);
Q_INVOKABLE QString getUnmappedGamepads();
void reload(); void reload();
enum AudioConfig enum AudioConfig
{ {
AC_STEREO, AC_STEREO,
AC_51_SURROUND, AC_51_SURROUND
AC_71_SURROUND
}; };
Q_ENUM(AudioConfig) Q_ENUM(AudioConfig)
@@ -61,18 +74,13 @@ public:
Q_PROPERTY(bool unsupportedFps MEMBER unsupportedFps NOTIFY unsupportedFpsChanged) Q_PROPERTY(bool unsupportedFps MEMBER unsupportedFps NOTIFY unsupportedFpsChanged)
Q_PROPERTY(bool enableMdns MEMBER enableMdns NOTIFY enableMdnsChanged) Q_PROPERTY(bool enableMdns MEMBER enableMdns NOTIFY enableMdnsChanged)
Q_PROPERTY(bool quitAppAfter MEMBER quitAppAfter NOTIFY quitAppAfterChanged) Q_PROPERTY(bool quitAppAfter MEMBER quitAppAfter NOTIFY quitAppAfterChanged)
Q_PROPERTY(bool absoluteMouseMode MEMBER absoluteMouseMode NOTIFY absoluteMouseModeChanged) Q_PROPERTY(bool mouseAcceleration MEMBER mouseAcceleration NOTIFY mouseAccelerationChanged)
Q_PROPERTY(bool absoluteTouchMode MEMBER absoluteTouchMode NOTIFY absoluteTouchModeChanged)
Q_PROPERTY(bool startWindowed MEMBER startWindowed NOTIFY startWindowedChanged) Q_PROPERTY(bool startWindowed MEMBER startWindowed NOTIFY startWindowedChanged)
Q_PROPERTY(bool framePacing MEMBER framePacing NOTIFY framePacingChanged) Q_PROPERTY(bool framePacing MEMBER framePacing NOTIFY framePacingChanged)
Q_PROPERTY(bool connectionWarnings MEMBER connectionWarnings NOTIFY connectionWarningsChanged)
Q_PROPERTY(bool richPresence MEMBER richPresence NOTIFY richPresenceChanged)
Q_PROPERTY(bool gamepadMouse MEMBER gamepadMouse NOTIFY gamepadMouseChanged)
Q_PROPERTY(AudioConfig audioConfig MEMBER audioConfig NOTIFY audioConfigChanged) Q_PROPERTY(AudioConfig audioConfig MEMBER audioConfig NOTIFY audioConfigChanged)
Q_PROPERTY(VideoCodecConfig videoCodecConfig MEMBER videoCodecConfig NOTIFY videoCodecConfigChanged) Q_PROPERTY(VideoCodecConfig videoCodecConfig MEMBER videoCodecConfig NOTIFY videoCodecConfigChanged)
Q_PROPERTY(VideoDecoderSelection videoDecoderSelection MEMBER videoDecoderSelection NOTIFY videoDecoderSelectionChanged) Q_PROPERTY(VideoDecoderSelection videoDecoderSelection MEMBER videoDecoderSelection NOTIFY videoDecoderSelectionChanged)
Q_PROPERTY(WindowMode windowMode MEMBER windowMode NOTIFY windowModeChanged) Q_PROPERTY(WindowMode windowMode MEMBER windowMode NOTIFY windowModeChanged)
Q_PROPERTY(WindowMode recommendedFullScreenMode MEMBER recommendedFullScreenMode CONSTANT)
// Directly accessible members for preferences // Directly accessible members for preferences
int width; int width;
@@ -86,19 +94,13 @@ public:
bool unsupportedFps; bool unsupportedFps;
bool enableMdns; bool enableMdns;
bool quitAppAfter; bool quitAppAfter;
bool absoluteMouseMode; bool mouseAcceleration;
bool absoluteTouchMode;
bool startWindowed; bool startWindowed;
bool framePacing; bool framePacing;
bool connectionWarnings;
bool richPresence;
bool gamepadMouse;
int packetSize;
AudioConfig audioConfig; AudioConfig audioConfig;
VideoCodecConfig videoCodecConfig; VideoCodecConfig videoCodecConfig;
VideoDecoderSelection videoDecoderSelection; VideoDecoderSelection videoDecoderSelection;
WindowMode windowMode; WindowMode windowMode;
WindowMode recommendedFullScreenMode;
signals: signals:
void displayModeChanged(); void displayModeChanged();
@@ -110,16 +112,12 @@ signals:
void unsupportedFpsChanged(); void unsupportedFpsChanged();
void enableMdnsChanged(); void enableMdnsChanged();
void quitAppAfterChanged(); void quitAppAfterChanged();
void absoluteMouseModeChanged(); void mouseAccelerationChanged();
void absoluteTouchModeChanged();
void audioConfigChanged(); void audioConfigChanged();
void videoCodecConfigChanged(); void videoCodecConfigChanged();
void videoDecoderSelectionChanged(); void videoDecoderSelectionChanged();
void windowModeChanged(); void windowModeChanged();
void startWindowedChanged(); void startWindowedChanged();
void framePacingChanged(); void framePacingChanged();
void connectionWarningsChanged();
void richPresenceChanged();
void gamepadMouseChanged();
}; };
-21
View File
@@ -1,21 +0,0 @@
#version 300 es
#extension GL_OES_EGL_image_external : require
precision mediump float;
out vec4 FragColor;
in vec2 vTextCoord;
uniform mat3 yuvmat;
uniform vec3 offset;
uniform samplerExternalOES plane1;
uniform samplerExternalOES plane2;
void main() {
vec3 YCbCr = vec3(
texture2D(plane1, vTextCoord)[0],
texture2D(plane2, vTextCoord).xy
);
YCbCr -= offset;
FragColor = vec4(clamp(yuvmat * YCbCr, 0.0, 1.0), 1.0f);
}
-10
View File
@@ -1,10 +0,0 @@
#version 300 es
layout (location = 0) in vec2 aPosition; // 2D: X,Y
layout (location = 1) in vec2 aTexCoord;
out vec2 vTextCoord;
void main() {
vTextCoord = aTexCoord;
gl_Position = vec4(aPosition, 0, 1);
}
+60 -157
View File
@@ -5,112 +5,55 @@
#include "renderers/soundioaudiorenderer.h" #include "renderers/soundioaudiorenderer.h"
#endif #endif
#ifdef HAVE_SLAUDIO
#include "renderers/slaud.h"
#endif
#include "renderers/sdl.h" #include "renderers/sdl.h"
#include <Limelight.h> #include <Limelight.h>
#define TRY_INIT_RENDERER(renderer, opusConfig) \ IAudioRenderer* Session::createAudioRenderer()
{ \
IAudioRenderer* __renderer = new renderer(); \
if (__renderer->prepareForPlayback(opusConfig)) \
return __renderer; \
delete __renderer; \
}
IAudioRenderer* Session::createAudioRenderer(const POPUS_MULTISTREAM_CONFIGURATION opusConfig)
{ {
// Handle explicit ML_AUDIO setting and fail if the requested backend fails
QString mlAudio = qgetenv("ML_AUDIO").toLower();
if (mlAudio == "sdl") {
TRY_INIT_RENDERER(SdlAudioRenderer, opusConfig)
return nullptr;
}
#ifdef HAVE_SOUNDIO #ifdef HAVE_SOUNDIO
else if (mlAudio == "libsoundio") { if (qgetenv("ML_AUDIO") == "SDL") {
TRY_INIT_RENDERER(SoundIoAudioRenderer, opusConfig) return new SdlAudioRenderer();
return nullptr;
}
#endif
#if defined(HAVE_SLAUDIO)
else if (mlAudio == "slaudio") {
TRY_INIT_RENDERER(SLAudioRenderer, opusConfig)
return nullptr;
}
#endif
else if (!mlAudio.isEmpty()) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Unknown audio backend: %s",
SDL_getenv("ML_AUDIO"));
return nullptr;
} }
// -------------- Automatic backend selection below this line --------------- return new SoundIoAudioRenderer();
#if defined(HAVE_SLAUDIO)
// Steam Link should always have SLAudio
TRY_INIT_RENDERER(SLAudioRenderer, opusConfig)
#endif
#if !defined(Q_OS_WIN32) && !defined(Q_OS_DARWIN)
// Linux defaults to SDL due to persistent glitching issues under libsoundio.
// Platforms that libsoundio doesn't support also default to SDL.
TRY_INIT_RENDERER(SdlAudioRenderer, opusConfig)
#ifdef HAVE_SOUNDIO
TRY_INIT_RENDERER(SoundIoAudioRenderer, opusConfig)
#endif
#else #else
// Windows and macOS default to libsoundio and fall back to SDL return new SdlAudioRenderer();
#ifdef HAVE_SOUNDIO
TRY_INIT_RENDERER(SoundIoAudioRenderer, opusConfig)
#endif #endif
TRY_INIT_RENDERER(SdlAudioRenderer, opusConfig)
#endif
return nullptr;
}
int Session::getAudioRendererCapabilities(int audioConfiguration)
{
// Build a fake OPUS_MULTISTREAM_CONFIGURATION to give
// the renderer the channel count and sample rate.
OPUS_MULTISTREAM_CONFIGURATION opusConfig = {};
opusConfig.sampleRate = 48000;
opusConfig.samplesPerFrame = 240;
opusConfig.channelCount = CHANNEL_COUNT_FROM_AUDIO_CONFIGURATION(audioConfiguration);
IAudioRenderer* audioRenderer = createAudioRenderer(&opusConfig);
if (audioRenderer == nullptr) {
return 0;
}
int caps = audioRenderer->getCapabilities();
delete audioRenderer;
return caps;
} }
bool Session::testAudio(int audioConfiguration) bool Session::testAudio(int audioConfiguration)
{ {
// Build a fake OPUS_MULTISTREAM_CONFIGURATION to give IAudioRenderer* audioRenderer;
// the renderer the channel count and sample rate.
OPUS_MULTISTREAM_CONFIGURATION opusConfig = {};
opusConfig.sampleRate = 48000;
opusConfig.samplesPerFrame = 240;
opusConfig.channelCount = CHANNEL_COUNT_FROM_AUDIO_CONFIGURATION(audioConfiguration);
IAudioRenderer* audioRenderer = createAudioRenderer(&opusConfig); audioRenderer = createAudioRenderer();
if (audioRenderer == nullptr) { if (audioRenderer == nullptr) {
return false; return false;
} }
// Build a fake OPUS_MULTISTREAM_CONFIGURATION to give
// the renderer the channel count and sample rate.
OPUS_MULTISTREAM_CONFIGURATION opusConfig = {};
opusConfig.sampleRate = 48000;
switch (audioConfiguration)
{
case AUDIO_CONFIGURATION_STEREO:
opusConfig.channelCount = 2;
break;
case AUDIO_CONFIGURATION_51_SURROUND:
opusConfig.channelCount = 6;
break;
default:
SDL_assert(false);
return false;
}
bool ret = audioRenderer->prepareForPlayback(&opusConfig);
delete audioRenderer; delete audioRenderer;
return true; return ret;
} }
int Session::arInit(int /* audioConfiguration */, int Session::arInit(int /* audioConfiguration */,
@@ -121,34 +64,30 @@ int Session::arInit(int /* audioConfiguration */,
SDL_memcpy(&s_ActiveSession->m_AudioConfig, opusConfig, sizeof(*opusConfig)); SDL_memcpy(&s_ActiveSession->m_AudioConfig, opusConfig, sizeof(*opusConfig));
s_ActiveSession->m_AudioRenderer = s_ActiveSession->createAudioRenderer(&s_ActiveSession->m_AudioConfig);
if (s_ActiveSession->m_AudioRenderer == nullptr) {
return -2;
}
// Allow the chosen renderer to remap Opus channels as needed to ensure proper output
s_ActiveSession->m_AudioRenderer->remapChannels(&s_ActiveSession->m_AudioConfig);
// Create the Opus decoder with the renderer's preferred channel mapping
s_ActiveSession->m_OpusDecoder = s_ActiveSession->m_OpusDecoder =
opus_multistream_decoder_create(s_ActiveSession->m_AudioConfig.sampleRate, opus_multistream_decoder_create(opusConfig->sampleRate,
s_ActiveSession->m_AudioConfig.channelCount, opusConfig->channelCount,
s_ActiveSession->m_AudioConfig.streams, opusConfig->streams,
s_ActiveSession->m_AudioConfig.coupledStreams, opusConfig->coupledStreams,
s_ActiveSession->m_AudioConfig.mapping, opusConfig->mapping,
&error); &error);
if (s_ActiveSession->m_OpusDecoder == NULL) { if (s_ActiveSession->m_OpusDecoder == NULL) {
delete s_ActiveSession->m_AudioRenderer;
s_ActiveSession->m_AudioRenderer = nullptr;
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to create decoder: %d", "Failed to create decoder: %d",
error); error);
return -1; return -1;
} }
s_ActiveSession->m_AudioRenderer = s_ActiveSession->createAudioRenderer();
if (!s_ActiveSession->m_AudioRenderer->prepareForPlayback(opusConfig)) {
delete s_ActiveSession->m_AudioRenderer;
opus_multistream_decoder_destroy(s_ActiveSession->m_OpusDecoder);
return -2;
}
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Audio stream has %d channels", "Audio stream has %d channels",
s_ActiveSession->m_AudioConfig.channelCount); opusConfig->channelCount);
return 0; return 0;
} }
@@ -166,11 +105,8 @@ void Session::arDecodeAndPlaySample(char* sampleData, int sampleLength)
{ {
int samplesDecoded; int samplesDecoded;
#ifndef STEAM_LINK // Set this thread to high priority to reduce
// Set this thread to high priority to reduce the chance of missing // the chance of missing our sample delivery time
// our sample delivery time. On Steam Link, this causes starvation
// of other threads due to severely restricted CPU time available,
// so we will skip it on that platform.
if (s_ActiveSession->m_AudioSampleCount == 0) { if (s_ActiveSession->m_AudioSampleCount == 0) {
if (SDL_SetThreadPriority(SDL_THREAD_PRIORITY_HIGH) < 0) { if (SDL_SetThreadPriority(SDL_THREAD_PRIORITY_HIGH) < 0) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
@@ -178,54 +114,29 @@ void Session::arDecodeAndPlaySample(char* sampleData, int sampleLength)
SDL_GetError()); SDL_GetError());
} }
} }
#endif
// See if we need to drop this sample
if (s_ActiveSession->m_DropAudioEndTime != 0) {
if (SDL_TICKS_PASSED(SDL_GetTicks(), s_ActiveSession->m_DropAudioEndTime)) {
// Avoid calling SDL_GetTicks() now
s_ActiveSession->m_DropAudioEndTime = 0;
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Audio drop window has ended");
}
else {
// We're still in the drop window
return;
}
}
s_ActiveSession->m_AudioSampleCount++; s_ActiveSession->m_AudioSampleCount++;
if (s_ActiveSession->m_AudioRenderer != nullptr) { if (s_ActiveSession->m_AudioRenderer != nullptr) {
int desiredSize = sizeof(short) * s_ActiveSession->m_AudioConfig.samplesPerFrame * s_ActiveSession->m_AudioConfig.channelCount;
void* buffer = s_ActiveSession->m_AudioRenderer->getAudioBuffer(&desiredSize);
if (buffer == nullptr) {
return;
}
samplesDecoded = opus_multistream_decode(s_ActiveSession->m_OpusDecoder, samplesDecoded = opus_multistream_decode(s_ActiveSession->m_OpusDecoder,
(unsigned char*)sampleData, (unsigned char*)sampleData,
sampleLength, sampleLength,
(short*)buffer, s_ActiveSession->m_OpusDecodeBuffer,
desiredSize / sizeof(short) / s_ActiveSession->m_AudioConfig.channelCount, SAMPLES_PER_FRAME,
0); 0);
// Update desiredSize with the number of bytes actually populated by the decoding operation
if (samplesDecoded > 0) { if (samplesDecoded > 0) {
SDL_assert(desiredSize >= (int)(sizeof(short) * samplesDecoded * s_ActiveSession->m_AudioConfig.channelCount)); if (!s_ActiveSession->m_AudioRenderer->submitAudio(s_ActiveSession->m_OpusDecodeBuffer,
desiredSize = sizeof(short) * samplesDecoded * s_ActiveSession->m_AudioConfig.channelCount; static_cast<int>(
} sizeof(short) *
else { samplesDecoded *
desiredSize = 0; s_ActiveSession->m_AudioConfig.channelCount))) {
}
if (!s_ActiveSession->m_AudioRenderer->submitAudio(desiredSize)) { SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Reinitializing audio renderer after failure");
"Reinitializing audio renderer after failure");
delete s_ActiveSession->m_AudioRenderer; delete s_ActiveSession->m_AudioRenderer;
s_ActiveSession->m_AudioRenderer = nullptr; s_ActiveSession->m_AudioRenderer = nullptr;
}
} }
} }
@@ -234,18 +145,10 @@ void Session::arDecodeAndPlaySample(char* sampleData, int sampleLength)
// safe to reinitialize here because we can't be torn down while // safe to reinitialize here because we can't be torn down while
// the audio decoder/playback thread is still alive. // the audio decoder/playback thread is still alive.
if (s_ActiveSession->m_AudioRenderer == nullptr && (s_ActiveSession->m_AudioSampleCount % 200) == 0) { if (s_ActiveSession->m_AudioRenderer == nullptr && (s_ActiveSession->m_AudioSampleCount % 200) == 0) {
// Since we're doing this inline and audio initialization takes time, we need s_ActiveSession->m_AudioRenderer = s_ActiveSession->createAudioRenderer();
// to drop samples to account for the time we've spent blocking audio rendering if (!s_ActiveSession->m_AudioRenderer->prepareForPlayback(&s_ActiveSession->m_AudioConfig)) {
// so we return to real-time playback and don't accumulate latency. delete s_ActiveSession->m_AudioRenderer;
Uint32 audioReinitStartTime = SDL_GetTicks(); s_ActiveSession->m_AudioRenderer = nullptr;
}
s_ActiveSession->m_AudioRenderer = s_ActiveSession->createAudioRenderer(&s_ActiveSession->m_AudioConfig);
Uint32 audioReinitStopTime = SDL_GetTicks();
s_ActiveSession->m_DropAudioEndTime = audioReinitStopTime + (audioReinitStopTime - audioReinitStartTime);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Audio reinitialization took %d ms - starting drop window",
audioReinitStopTime - audioReinitStartTime);
} }
} }
+4 -15
View File
@@ -2,6 +2,9 @@
#include <Limelight.h> #include <Limelight.h>
#define MAX_CHANNELS 6
#define SAMPLES_PER_FRAME 240
class IAudioRenderer class IAudioRenderer
{ {
public: public:
@@ -9,20 +12,6 @@ public:
virtual bool prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig) = 0; virtual bool prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig) = 0;
virtual void* getAudioBuffer(int* size) = 0;
// Return false if an unrecoverable error has occurred and the renderer must be reinitialized // Return false if an unrecoverable error has occurred and the renderer must be reinitialized
virtual bool submitAudio(int bytesWritten) = 0; virtual bool submitAudio(short* audioBuffer, int audioSize) = 0;
virtual int getCapabilities() = 0;
virtual void remapChannels(POPUS_MULTISTREAM_CONFIGURATION) {
// Use default channel mapping:
// 0 - Front Left
// 1 - Front Right
// 2 - Center
// 3 - LFE
// 4 - Surround Left
// 5 - Surround Right
}
}; };
+1 -7
View File
@@ -12,14 +12,8 @@ public:
virtual bool prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig); virtual bool prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig);
virtual void* getAudioBuffer(int* size); virtual bool submitAudio(short* audioBuffer, int audioSize);
virtual bool submitAudio(int bytesWritten);
virtual int getCapabilities();
private: private:
SDL_AudioDeviceID m_AudioDevice; SDL_AudioDeviceID m_AudioDevice;
void* m_AudioBuffer;
int m_FrameSize;
}; };
+7 -78
View File
@@ -4,42 +4,11 @@
#include <SDL.h> #include <SDL.h>
#include <QtGlobal> #include <QtGlobal>
#include <QFile>
#include <QTextStream>
SdlAudioRenderer::SdlAudioRenderer() SdlAudioRenderer::SdlAudioRenderer()
: m_AudioDevice(0), : m_AudioDevice(0)
m_AudioBuffer(nullptr)
{ {
SDL_assert(!SDL_WasInit(SDL_INIT_AUDIO)); SDL_assert(!SDL_WasInit(SDL_INIT_AUDIO));
#ifdef HAVE_MMAL
// HACK: PulseAudio on Raspberry Pi suffers from horrible underruns,
// so switch to ALSA if we detect that we're on a Pi. We need to
// actually set a bogus PULSE_SERVER so it doesn't try to get smart on us
// and find PA anyway. SDL_AUDIODRIVER=pulseaudio can override this logic.
if (qgetenv("SDL_AUDIODRIVER").toLower() != "pulseaudio") {
QFile file("/proc/cpuinfo");
if (file.open(QIODevice::ReadOnly | QFile::Text)) {
QTextStream textStream(&file);
if (textStream.readAll().contains("Raspberry Pi")) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Avoiding PulseAudio on Raspberry Pi");
qputenv("PULSE_SERVER", "");
if (SDL_InitSubSystem(SDL_INIT_AUDIO) != 0) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"ALSA failed; falling back to default");
qunsetenv("PULSE_SERVER");
}
else {
SDL_QuitSubSystem(SDL_INIT_AUDIO);
}
}
}
}
#endif
if (SDL_InitSubSystem(SDL_INIT_AUDIO) != 0) { if (SDL_InitSubSystem(SDL_INIT_AUDIO) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_AUDIO) failed: %s", "SDL_InitSubSystem(SDL_INIT_AUDIO) failed: %s",
@@ -61,29 +30,21 @@ bool SdlAudioRenderer::prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION*
// frames contain a non-power of 2 number of samples, // frames contain a non-power of 2 number of samples,
// so the slop would require buffering another full frame. // so the slop would require buffering another full frame.
// Specifying non-Po2 seems to work for our supported platforms. // Specifying non-Po2 seems to work for our supported platforms.
want.samples = opusConfig->samplesPerFrame; want.samples = SAMPLES_PER_FRAME;
m_FrameSize = opusConfig->samplesPerFrame * sizeof(short) * opusConfig->channelCount;
m_AudioDevice = SDL_OpenAudioDevice(NULL, 0, &want, &have, 0); m_AudioDevice = SDL_OpenAudioDevice(NULL, 0, &want, &have, 0);
if (m_AudioDevice == 0) { if (m_AudioDevice == 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to open audio device: %s", "Failed to open audio device: %s",
SDL_GetError()); SDL_GetError());
return false; SDL_QuitSubSystem(SDL_INIT_AUDIO);
}
m_AudioBuffer = malloc(m_FrameSize);
if (m_AudioBuffer == nullptr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to allocate audio buffer");
return false; return false;
} }
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Desired audio buffer: %u samples (%u bytes)", "Desired audio buffer: %u samples (%lu bytes)",
want.samples, want.samples,
want.samples * (Uint32)sizeof(short) * want.channels); want.samples * sizeof(short) * want.channels);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Obtained audio buffer: %u samples (%u bytes)", "Obtained audio buffer: %u samples (%u bytes)",
@@ -104,39 +65,13 @@ SdlAudioRenderer::~SdlAudioRenderer()
SDL_CloseAudioDevice(m_AudioDevice); SDL_CloseAudioDevice(m_AudioDevice);
} }
if (m_AudioBuffer != nullptr) {
free(m_AudioBuffer);
}
SDL_QuitSubSystem(SDL_INIT_AUDIO); SDL_QuitSubSystem(SDL_INIT_AUDIO);
SDL_assert(!SDL_WasInit(SDL_INIT_AUDIO)); SDL_assert(!SDL_WasInit(SDL_INIT_AUDIO));
} }
void* SdlAudioRenderer::getAudioBuffer(int*) bool SdlAudioRenderer::submitAudio(short* audioBuffer, int audioSize)
{ {
return m_AudioBuffer; if (SDL_QueueAudio(m_AudioDevice, audioBuffer, audioSize) < 0) {
}
bool SdlAudioRenderer::submitAudio(int bytesWritten)
{
if (bytesWritten == 0) {
// Nothing to do
return true;
}
// Don't queue if there's already more than 30 ms of audio data waiting
// in Moonlight's audio queue.
if (LiGetPendingAudioDuration() > 30) {
return true;
}
// Provide backpressure on the queue to ensure too many frames don't build up
// in SDL's audio queue.
while (SDL_GetQueuedAudioSize(m_AudioDevice) / m_FrameSize > 10) {
SDL_Delay(1);
}
if (SDL_QueueAudio(m_AudioDevice, m_AudioBuffer, bytesWritten) < 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to queue audio sample: %s", "Failed to queue audio sample: %s",
SDL_GetError()); SDL_GetError());
@@ -144,9 +79,3 @@ bool SdlAudioRenderer::submitAudio(int bytesWritten)
return true; return true;
} }
int SdlAudioRenderer::getCapabilities()
{
// Direct submit can't be used because we use LiGetPendingAudioDuration()
return CAPABILITY_SUPPORTS_ARBITRARY_AUDIO_DURATION;
}
-145
View File
@@ -1,145 +0,0 @@
#include "slaud.h"
#include <SDL.h>
SLAudioRenderer::SLAudioRenderer()
: m_AudioContext(nullptr),
m_AudioStream(nullptr),
m_AudioBuffer(nullptr)
{
SLAudio_SetLogFunction(SLAudioRenderer::slLogCallback, nullptr);
}
bool SLAudioRenderer::prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig)
{
m_AudioContext = SLAudio_CreateContext();
if (m_AudioContext == nullptr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SLAudio_CreateContext() failed");
return false;
}
// This number is pretty conservative (especially for surround), but
// it's hard to avoid since we get crushed by CPU limitations.
m_MaxQueuedAudioMs = 40 * opusConfig->channelCount / 2;
m_AudioBufferSize = opusConfig->samplesPerFrame * sizeof(short) * opusConfig->channelCount;
m_AudioStream = SLAudio_CreateStream(m_AudioContext,
opusConfig->sampleRate,
opusConfig->channelCount,
m_AudioBufferSize,
1);
if (m_AudioStream == nullptr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SLAudio_CreateStream() failed");
return false;
}
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Using SLAudio renderer with %d samples per frame",
opusConfig->samplesPerFrame);
return true;
}
void SLAudioRenderer::remapChannels(POPUS_MULTISTREAM_CONFIGURATION opusConfig) {
OPUS_MULTISTREAM_CONFIGURATION originalConfig = *opusConfig;
// The Moonlight's default channel order is FL,FR,C,LFE,RL,RR,SL,SR
// SLAudio expects FL,C,FR,RL,RR,(SL,SR),LFE for 5.1/7.1 so we swap the channels around to match
if (opusConfig->channelCount == 3 || opusConfig->channelCount >= 6) {
// Swap FR and C
opusConfig->mapping[1] = originalConfig.mapping[2];
opusConfig->mapping[2] = originalConfig.mapping[1];
}
if (opusConfig->channelCount >= 6) {
// SLAudio expects the LFE channel at the end
opusConfig->mapping[opusConfig->channelCount - 1] = originalConfig.mapping[3];
// Slide the other channels down
memcpy(&opusConfig->mapping[3],
&originalConfig.mapping[4],
opusConfig->channelCount - 4);
}
}
void* SLAudioRenderer::getAudioBuffer(int* size)
{
SDL_assert(*size == m_AudioBufferSize);
if (m_AudioBuffer == nullptr) {
m_AudioBuffer = SLAudio_BeginFrame(m_AudioStream);
}
return m_AudioBuffer;
}
SLAudioRenderer::~SLAudioRenderer()
{
if (m_AudioBuffer != nullptr) {
memset(m_AudioBuffer, 0, m_AudioBufferSize);
SLAudio_SubmitFrame(m_AudioStream);
}
if (m_AudioStream != nullptr) {
SLAudio_FreeStream(m_AudioStream);
}
if (m_AudioContext != nullptr) {
SLAudio_FreeContext(m_AudioContext);
}
}
bool SLAudioRenderer::submitAudio(int bytesWritten)
{
if (bytesWritten == 0) {
// This buffer will be reused next time
return true;
}
if (LiGetPendingAudioDuration() < m_MaxQueuedAudioMs) {
SLAudio_SubmitFrame(m_AudioStream);
m_AudioBuffer = nullptr;
}
else {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Too many queued audio frames: %d",
LiGetPendingAudioFrames());
}
return true;
}
int SLAudioRenderer::getCapabilities()
{
return CAPABILITY_SLOW_OPUS_DECODER | CAPABILITY_SUPPORTS_ARBITRARY_AUDIO_DURATION;
}
void SLAudioRenderer::slLogCallback(void*, ESLAudioLog logLevel, const char *message)
{
SDL_LogPriority priority;
switch (logLevel)
{
case k_ESLAudioLogError:
priority = SDL_LOG_PRIORITY_ERROR;
break;
case k_ESLAudioLogWarning:
priority = SDL_LOG_PRIORITY_WARN;
break;
case k_ESLAudioLogInfo:
priority = SDL_LOG_PRIORITY_INFO;
break;
default:
case k_ESLAudioLogDebug:
priority = SDL_LOG_PRIORITY_DEBUG;
break;
}
SDL_LogMessage(SDL_LOG_CATEGORY_APPLICATION,
priority,
"SLAudio: %s",
message);
}
-32
View File
@@ -1,32 +0,0 @@
#pragma once
#include "renderer.h"
#include <SLAudio.h>
class SLAudioRenderer : public IAudioRenderer
{
public:
SLAudioRenderer();
virtual ~SLAudioRenderer();
virtual bool prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig);
virtual void* getAudioBuffer(int* size);
virtual bool submitAudio(int bytesWritten);
virtual int getCapabilities();
virtual void remapChannels(POPUS_MULTISTREAM_CONFIGURATION opusConfig);
private:
static void slLogCallback(void* context, ESLAudioLog logLevel, const char* message);
CSLAudioContext* m_AudioContext;
CSLAudioStream* m_AudioStream;
void* m_AudioBuffer;
int m_AudioBufferSize;
int m_MaxQueuedAudioMs;
};
@@ -4,13 +4,29 @@
#include <QtGlobal> #include <QtGlobal>
// GFE sends us packets in 5 ms chunks
const double SoundIoAudioRenderer::k_RawSampleLengthSec = 0.005;
#ifdef Q_OS_LINUX
// PulseAudio and ALSA require more than just 5 ms samples
// for some reason, so write a minimum of 25 ms each time to
// prevent underruns on Bluetooth.
// https://github.com/moonlight-stream/moonlight-qt/issues/147
// https://github.com/moonlight-stream/moonlight-qt/issues/157
const double SoundIoAudioRenderer::k_MinSampleLengthSec = 0.025;
#else
// This determines the size of the buffers we'll
// get from CoreAudio. It is also the minimum
// size that we will write when called to fill a buffer.
const double SoundIoAudioRenderer::k_MinSampleLengthSec = k_RawSampleLengthSec;
#endif
SoundIoAudioRenderer::SoundIoAudioRenderer() SoundIoAudioRenderer::SoundIoAudioRenderer()
: m_OpusChannelCount(0), : m_OpusChannelCount(0),
m_SoundIo(nullptr), m_SoundIo(nullptr),
m_Device(nullptr), m_Device(nullptr),
m_OutputStream(nullptr), m_OutputStream(nullptr),
m_RingBuffer(nullptr), m_RingBuffer(nullptr),
m_AudioPacketDuration(0),
m_Latency(0), m_Latency(0),
m_Errored(false) m_Errored(false)
{ {
@@ -72,17 +88,11 @@ int SoundIoAudioRenderer::scoreChannelLayout(const struct SoundIoChannelLayout*
score++; score++;
break; break;
// Score layouts using the back L/R as higher
// value than those using side L/R.
case SoundIoChannelIdBackLeft: case SoundIoChannelIdBackLeft:
case SoundIoChannelIdBackRight: case SoundIoChannelIdBackRight:
if (opusConfig->channelCount == 6) { score += 2;
// Score back channels higher in 5.1 mode to
// discourage selection of side channel layouts.
score += 2;
}
else {
// Score back channels normally for 7.1 mode
score++;
}
break; break;
default: default:
@@ -162,11 +172,9 @@ bool SoundIoAudioRenderer::prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATI
return false; return false;
} }
m_AudioPacketDuration = (opusConfig->samplesPerFrame / (opusConfig->sampleRate / 1000)) / 1000.0;
m_OutputStream->format = SoundIoFormatS16NE; m_OutputStream->format = SoundIoFormatS16NE;
m_OutputStream->sample_rate = opusConfig->sampleRate; m_OutputStream->sample_rate = opusConfig->sampleRate;
m_OutputStream->software_latency = m_AudioPacketDuration; m_OutputStream->software_latency = k_MinSampleLengthSec;
m_OutputStream->name = "Moonlight"; m_OutputStream->name = "Moonlight";
m_OutputStream->userdata = this; m_OutputStream->userdata = this;
m_OutputStream->error_callback = sioErrorCallback; m_OutputStream->error_callback = sioErrorCallback;
@@ -217,50 +225,42 @@ bool SoundIoAudioRenderer::prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATI
m_EffectiveLayout = m_OutputStream->layout; m_EffectiveLayout = m_OutputStream->layout;
for (int i = 0; i < m_EffectiveLayout.channel_count; i++) { for (int i = 0; i < m_EffectiveLayout.channel_count; i++) {
if (opusConfig->channelCount == 6) { // Fixup the layout to use back L/R so our channel position
// For 5.1, replace side L/R with back L/R so our channel position // logic in sioWriteCallback() works.
// logic in sioWriteCallback() works. if (m_EffectiveLayout.channels[i] == SoundIoChannelIdSideLeft) {
if (m_EffectiveLayout.channels[i] == SoundIoChannelIdSideLeft) { m_EffectiveLayout.channels[i] = SoundIoChannelIdBackLeft;
m_EffectiveLayout.channels[i] = SoundIoChannelIdBackLeft;
}
if (m_EffectiveLayout.channels[i] == SoundIoChannelIdSideRight) {
m_EffectiveLayout.channels[i] = SoundIoChannelIdBackRight;
}
} }
else if (opusConfig->channelCount == 8) { if (m_EffectiveLayout.channels[i] == SoundIoChannelIdSideRight) {
// For 5.1, replace side L/R with LOC/ROC so our channel position m_EffectiveLayout.channels[i] = SoundIoChannelIdBackRight;
// logic in sioWriteCallback() works.
if (m_EffectiveLayout.channels[i] == SoundIoChannelIdSideLeft) {
m_EffectiveLayout.channels[i] = SoundIoChannelIdFrontLeftCenter;
}
if (m_EffectiveLayout.channels[i] == SoundIoChannelIdSideRight) {
m_EffectiveLayout.channels[i] = SoundIoChannelIdFrontRightCenter;
}
} }
} }
int packetsToBuffer; int packetsToBuffer;
#ifdef Q_OS_LINUX
// PulseAudio and ALSA need the large buffer (see comment on k_MinSampleLengthSec),
// so we need a buffer at least double that size to allow packets to arrive
// while we're writing to the sink.
packetsToBuffer = (int)(k_MinSampleLengthSec / k_RawSampleLengthSec) * 2;
#else
if (m_SoundIo->current_backend == SoundIoBackendWasapi) { if (m_SoundIo->current_backend == SoundIoBackendWasapi) {
// 15 ms buffer seems to be fine for WASAPI // 15 ms buffer seems to be fine for WASAPI
packetsToBuffer = (int)ceil(0.015 / m_AudioPacketDuration); packetsToBuffer = 3;
} }
else { else {
// 30 ms buffer on CoreAudio to avoid glitching on macOS // 30 ms buffer on CoreAudio to avoid glitching on macOS
packetsToBuffer = (int)ceil(0.030 / m_AudioPacketDuration); packetsToBuffer = 6;
} }
#endif
// Always buffer at least 2 packets
packetsToBuffer = qMax(2, packetsToBuffer);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Audio buffer size: %f seconds", "Audio buffer size: %d packets",
packetsToBuffer * m_AudioPacketDuration); packetsToBuffer);
m_RingBuffer = soundio_ring_buffer_create(m_SoundIo, m_RingBuffer = soundio_ring_buffer_create(m_SoundIo,
m_OutputStream->bytes_per_sample * m_OutputStream->bytes_per_sample *
m_OpusChannelCount * m_OpusChannelCount *
opusConfig->samplesPerFrame * SAMPLES_PER_FRAME *
packetsToBuffer); packetsToBuffer);
if (m_RingBuffer == nullptr) { if (m_RingBuffer == nullptr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
@@ -284,8 +284,15 @@ bool SoundIoAudioRenderer::prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATI
return true; return true;
} }
void* SoundIoAudioRenderer::getAudioBuffer(int* size) bool SoundIoAudioRenderer::submitAudio(short* audioBuffer, int audioSize)
{ {
if (m_Errored) {
return false;
}
// Flush events to update with new device arrivals
soundio_flush_events(m_SoundIo);
// We must always write a full frame of audio. If we don't, // We must always write a full frame of audio. If we don't,
// the reader will get out of sync with the writer and our // the reader will get out of sync with the writer and our
// channels will get all mixed up. To ensure this is always // channels will get all mixed up. To ensure this is always
@@ -293,36 +300,13 @@ void* SoundIoAudioRenderer::getAudioBuffer(int* size)
// of our frame size. // of our frame size.
int bytesFree = soundio_ring_buffer_free_count(m_RingBuffer); int bytesFree = soundio_ring_buffer_free_count(m_RingBuffer);
int bytesPerFrame = m_OpusChannelCount * m_OutputStream->bytes_per_sample; int bytesPerFrame = m_OpusChannelCount * m_OutputStream->bytes_per_sample;
*size = qMin(*size, (bytesFree / bytesPerFrame) * bytesPerFrame); int bytesToWrite = qMin(audioSize, (bytesFree / bytesPerFrame) * bytesPerFrame);
return soundio_ring_buffer_write_ptr(m_RingBuffer); memcpy(soundio_ring_buffer_write_ptr(m_RingBuffer), audioBuffer, bytesToWrite);
} soundio_ring_buffer_advance_write_ptr(m_RingBuffer, bytesToWrite);
bool SoundIoAudioRenderer::submitAudio(int bytesWritten)
{
if (m_Errored) {
return false;
}
if (bytesWritten == 0) {
// Nothing to do
return true;
}
// Flush events to update with new device arrivals
soundio_flush_events(m_SoundIo);
// Advance the write pointer
soundio_ring_buffer_advance_write_ptr(m_RingBuffer, bytesWritten);
return true; return true;
} }
int SoundIoAudioRenderer::getCapabilities()
{
// TODO: Tweak buffer sizes then re-enable arbitrary audio duration
return CAPABILITY_DIRECT_SUBMIT /* | CAPABILITY_SUPPORTS_ARBITRARY_AUDIO_DURATION */;
}
void SoundIoAudioRenderer::sioErrorCallback(SoundIoOutStream* stream, int err) void SoundIoAudioRenderer::sioErrorCallback(SoundIoOutStream* stream, int err)
{ {
auto me = reinterpret_cast<SoundIoAudioRenderer*>(stream->userdata); auto me = reinterpret_cast<SoundIoAudioRenderer*>(stream->userdata);
@@ -387,19 +371,15 @@ void SoundIoAudioRenderer::sioWriteCallback(SoundIoOutStream* stream, int frameC
(me->m_OpusChannelCount * stream->bytes_per_sample); (me->m_OpusChannelCount * stream->bytes_per_sample);
int bytesRead = 0; int bytesRead = 0;
// Clamp framesLeft to frameCountMax
framesLeft = qMin(framesLeft, frameCountMax);
// Ensure we always write at least a buffer, even if it's silence, to avoid // Ensure we always write at least a buffer, even if it's silence, to avoid
// busy looping when no audio data is available while libsoundio tries to keep // busy looping when no audio data is available while libsoundio tries to keep
// us from starving the output device. // us from starving the output device.
frameCountMin = qMax(frameCountMin, (int)(stream->sample_rate * me->m_AudioPacketDuration)); frameCountMin = qMax(frameCountMin, (int)(stream->sample_rate * k_MinSampleLengthSec));
// Clamp frameCountMax to at least 2 packets or 20 ms to stop our latency from growing if audio packets lag.
// This makes sure that we never increase our latency far beyond what the sink is consuming.
frameCountMax = qMin(frameCountMax, (int)(stream->sample_rate * qMax(me->m_AudioPacketDuration * 2, 0.020)));
frameCountMin = qMin(frameCountMin, frameCountMax); frameCountMin = qMin(frameCountMin, frameCountMax);
// Clamp framesLeft to frameCountMax
framesLeft = qMin(framesLeft, frameCountMax);
// Track latency on queueing-based backends // Track latency on queueing-based backends
if (me->m_SoundIo->current_backend != SoundIoBackendCoreAudio && me->m_SoundIo->current_backend != SoundIoBackendJack) { if (me->m_SoundIo->current_backend != SoundIoBackendCoreAudio && me->m_SoundIo->current_backend != SoundIoBackendJack) {
soundio_outstream_get_latency(stream, &me->m_Latency); soundio_outstream_get_latency(stream, &me->m_Latency);
@@ -429,8 +409,9 @@ void SoundIoAudioRenderer::sioWriteCallback(SoundIoOutStream* stream, int frameC
for (int frame = 0; frame < frameCount; frame++) { for (int frame = 0; frame < frameCount; frame++) {
for (int ch = 0; ch < me->m_EffectiveLayout.channel_count; ch++) { for (int ch = 0; ch < me->m_EffectiveLayout.channel_count; ch++) {
// SoundIoChannelId - 1 happens to match Moonlight's channel layout // SoundIoChannelId - 1 happens to match Moonlight's channel layout.
// after we've applied our fixups to m_EffectiveLayout for 5.1 and 7.1. // For side L/R, this logic depends on us fixing those up
// in m_EffectiveLayout to back L/R.
int readPtrChannel = me->m_EffectiveLayout.channels[ch] - 1; int readPtrChannel = me->m_EffectiveLayout.channels[ch] - 1;
if (frame >= framesLeft || readPtrChannel >= me->m_OpusChannelCount) { if (frame >= framesLeft || readPtrChannel >= me->m_OpusChannelCount) {
@@ -13,11 +13,7 @@ public:
virtual bool prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig); virtual bool prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig);
virtual void* getAudioBuffer(int* size); virtual bool submitAudio(short* audioBuffer, int audioSize);
virtual bool submitAudio(int bytesWritten);
virtual int getCapabilities();
private: private:
int scoreChannelLayout(const struct SoundIoChannelLayout* layout, const OPUS_MULTISTREAM_CONFIGURATION* opusConfig); int scoreChannelLayout(const struct SoundIoChannelLayout* layout, const OPUS_MULTISTREAM_CONFIGURATION* opusConfig);
@@ -36,7 +32,9 @@ private:
struct SoundIoOutStream* m_OutputStream; struct SoundIoOutStream* m_OutputStream;
struct SoundIoRingBuffer* m_RingBuffer; struct SoundIoRingBuffer* m_RingBuffer;
struct SoundIoChannelLayout m_EffectiveLayout; struct SoundIoChannelLayout m_EffectiveLayout;
double m_AudioPacketDuration;
double m_Latency; double m_Latency;
bool m_Errored; bool m_Errored;
static const double k_RawSampleLengthSec;
static const double k_MinSampleLengthSec;
}; };
+997
View File
@@ -0,0 +1,997 @@
#include <Limelight.h>
#include <SDL.h>
#include "streaming/session.h"
#include "settings/mappingmanager.h"
#include "path.h"
#include <QtGlobal>
#include <QDir>
#define VK_0 0x30
#define VK_A 0x41
// These are real Windows VK_* codes
#ifndef VK_F1
#define VK_F1 0x70
#define VK_F13 0x7C
#define VK_NUMPAD0 0x60
#endif
#define MOUSE_POLLING_INTERVAL 5
// How long the mouse button will be pressed for a tap to click gesture
#define TAP_BUTTON_RELEASE_DELAY 100
// How long the fingers must be stationary to start a drag
#define DRAG_ACTIVATION_DELAY 650
// How far the finger can move before it cancels a drag or tap
#define DEAD_ZONE_DELTA 0.1f
const int SdlInputHandler::k_ButtonMap[] = {
A_FLAG, B_FLAG, X_FLAG, Y_FLAG,
BACK_FLAG, SPECIAL_FLAG, PLAY_FLAG,
LS_CLK_FLAG, RS_CLK_FLAG,
LB_FLAG, RB_FLAG,
UP_FLAG, DOWN_FLAG, LEFT_FLAG, RIGHT_FLAG
};
SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, NvComputer*, int streamWidth, int streamHeight)
: m_MultiController(prefs.multiController),
m_MouseMoveTimer(0),
m_LeftButtonReleaseTimer(0),
m_RightButtonReleaseTimer(0),
m_DragTimer(0),
m_DragButton(0),
m_NumFingersDown(0),
m_StreamWidth(streamWidth),
m_StreamHeight(streamHeight)
{
// Allow gamepad input when the app doesn't have focus
SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1");
// If mouse acceleration is enabled, use relative mode warp (which
// is via normal motion events that are influenced by mouse acceleration).
// Otherwise, we'll use raw input capture which is straight from the device
// without modification by the OS.
SDL_SetHint(SDL_HINT_MOUSE_RELATIVE_MODE_WARP,
prefs.mouseAcceleration ? "1" : "0");
// We need to reinit this each time, since you only get
// an initial set of gamepad arrival events once per init.
SDL_assert(!SDL_WasInit(SDL_INIT_GAMECONTROLLER));
if (SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) failed: %s",
SDL_GetError());
}
MappingManager mappingManager;
mappingManager.applyMappings();
// Initialize the gamepad mask with currently attached gamepads to avoid
// causing gamepads to unexpectedly disappear and reappear on the host
// during stream startup as we detect currently attached gamepads one at a time.
m_GamepadMask = getAttachedGamepadMask();
SDL_zero(m_GamepadState);
SDL_zero(m_TouchDownEvent);
SDL_zero(m_CumulativeDelta);
SDL_AtomicSet(&m_MouseDeltaX, 0);
SDL_AtomicSet(&m_MouseDeltaY, 0);
m_MouseMoveTimer = SDL_AddTimer(MOUSE_POLLING_INTERVAL, SdlInputHandler::mouseMoveTimerCallback, this);
}
SdlInputHandler::~SdlInputHandler()
{
for (int i = 0; i < MAX_GAMEPADS; i++) {
if (m_GamepadState[i].controller != nullptr) {
SDL_GameControllerClose(m_GamepadState[i].controller);
}
}
SDL_RemoveTimer(m_MouseMoveTimer);
SDL_RemoveTimer(m_LeftButtonReleaseTimer);
SDL_RemoveTimer(m_RightButtonReleaseTimer);
SDL_RemoveTimer(m_DragTimer);
SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER);
SDL_assert(!SDL_WasInit(SDL_INIT_GAMECONTROLLER));
}
void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
{
short keyCode;
char modifiers;
// Check for our special key combos
if ((event->state == SDL_PRESSED) &&
(event->keysym.mod & KMOD_CTRL) &&
(event->keysym.mod & KMOD_ALT) &&
(event->keysym.mod & KMOD_SHIFT)) {
// Check for quit combo (Ctrl+Alt+Shift+Q)
if (event->keysym.sym == SDLK_q) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Detected quit key combo");
// Push a quit event to the main loop
SDL_Event event;
event.type = SDL_QUIT;
event.quit.timestamp = SDL_GetTicks();
SDL_PushEvent(&event);
return;
}
// Check for the unbind combo (Ctrl+Alt+Shift+Z)
else if (event->keysym.sym == SDLK_z) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Detected mouse capture toggle combo");
// Stop handling future input
SDL_SetRelativeMouseMode((SDL_bool)!SDL_GetRelativeMouseMode());
// Force raise all keys to ensure they aren't stuck,
// since we won't get their key up events.
raiseAllKeys();
return;
}
// Check for the full-screen combo (Ctrl+Alt+Shift+X)
else if (event->keysym.sym == SDLK_x) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Detected full-screen toggle combo");
Session::s_ActiveSession->toggleFullscreen();
// Force raise all keys just be safe across this full-screen/windowed
// transition just in case key events get lost.
raiseAllKeys();
return;
}
}
if (!SDL_GetRelativeMouseMode()) {
// Not capturing
return;
}
// Set modifier flags
modifiers = 0;
if (event->keysym.mod & KMOD_CTRL) {
modifiers |= MODIFIER_CTRL;
}
if (event->keysym.mod & KMOD_ALT) {
modifiers |= MODIFIER_ALT;
}
if (event->keysym.mod & KMOD_SHIFT) {
modifiers |= MODIFIER_SHIFT;
}
// Set keycode. We explicitly use scancode here because GFE will try to correct
// for AZERTY layouts on the host but it depends on receiving VK_ values matching
// a QWERTY layout to work.
if (event->keysym.scancode >= SDL_SCANCODE_1 && event->keysym.scancode <= SDL_SCANCODE_9) {
// SDL defines SDL_SCANCODE_0 > SDL_SCANCODE_9, so we need to handle that manually
keyCode = (event->keysym.scancode - SDL_SCANCODE_1) + VK_0 + 1;
}
else if (event->keysym.scancode >= SDL_SCANCODE_A && event->keysym.scancode <= SDL_SCANCODE_Z) {
keyCode = (event->keysym.scancode - SDL_SCANCODE_A) + VK_A;
}
else if (event->keysym.scancode >= SDL_SCANCODE_F1 && event->keysym.scancode <= SDL_SCANCODE_F12) {
keyCode = (event->keysym.scancode - SDL_SCANCODE_F1) + VK_F1;
}
else if (event->keysym.scancode >= SDL_SCANCODE_F13 && event->keysym.scancode <= SDL_SCANCODE_F24) {
keyCode = (event->keysym.scancode - SDL_SCANCODE_F13) + VK_F13;
}
else if (event->keysym.scancode >= SDL_SCANCODE_KP_1 && event->keysym.scancode <= SDL_SCANCODE_KP_9) {
// SDL defines SDL_SCANCODE_KP_0 > SDL_SCANCODE_KP_9, so we need to handle that manually
keyCode = (event->keysym.scancode - SDL_SCANCODE_KP_1) + VK_NUMPAD0 + 1;
}
else {
switch (event->keysym.scancode) {
case SDL_SCANCODE_BACKSPACE:
keyCode = 0x08;
break;
case SDL_SCANCODE_TAB:
keyCode = 0x09;
break;
case SDL_SCANCODE_CLEAR:
keyCode = 0x0C;
break;
case SDL_SCANCODE_KP_ENTER: // FIXME: Is this correct?
case SDL_SCANCODE_RETURN:
keyCode = 0x0D;
break;
case SDL_SCANCODE_PAUSE:
keyCode = 0x13;
break;
case SDL_SCANCODE_CAPSLOCK:
keyCode = 0x14;
break;
case SDL_SCANCODE_ESCAPE:
keyCode = 0x1B;
break;
case SDL_SCANCODE_SPACE:
keyCode = 0x20;
break;
case SDL_SCANCODE_PAGEUP:
keyCode = 0x21;
break;
case SDL_SCANCODE_PAGEDOWN:
keyCode = 0x22;
break;
case SDL_SCANCODE_END:
keyCode = 0x23;
break;
case SDL_SCANCODE_HOME:
keyCode = 0x24;
break;
case SDL_SCANCODE_LEFT:
keyCode = 0x25;
break;
case SDL_SCANCODE_UP:
keyCode = 0x26;
break;
case SDL_SCANCODE_RIGHT:
keyCode = 0x27;
break;
case SDL_SCANCODE_DOWN:
keyCode = 0x28;
break;
case SDL_SCANCODE_SELECT:
keyCode = 0x29;
break;
case SDL_SCANCODE_EXECUTE:
keyCode = 0x2B;
break;
case SDL_SCANCODE_PRINTSCREEN:
keyCode = 0x2C;
break;
case SDL_SCANCODE_INSERT:
keyCode = 0x2D;
break;
case SDL_SCANCODE_DELETE:
keyCode = 0x2E;
break;
case SDL_SCANCODE_HELP:
keyCode = 0x2F;
break;
case SDL_SCANCODE_KP_0:
// See comment above about why we only handle SDL_SCANCODE_KP_0 here
keyCode = VK_NUMPAD0;
break;
case SDL_SCANCODE_0:
// See comment above about why we only handle SDL_SCANCODE_0 here
keyCode = VK_0;
break;
case SDL_SCANCODE_KP_MULTIPLY:
keyCode = 0x6A;
break;
case SDL_SCANCODE_KP_PLUS:
keyCode = 0x6B;
break;
case SDL_SCANCODE_KP_COMMA:
keyCode = 0x6C;
break;
case SDL_SCANCODE_KP_MINUS:
keyCode = 0x6D;
break;
case SDL_SCANCODE_KP_PERIOD:
keyCode = 0x6E;
break;
case SDL_SCANCODE_KP_DIVIDE:
keyCode = 0x6F;
break;
case SDL_SCANCODE_NUMLOCKCLEAR:
keyCode = 0x90;
break;
case SDL_SCANCODE_SCROLLLOCK:
keyCode = 0x91;
break;
case SDL_SCANCODE_LSHIFT:
keyCode = 0xA0;
break;
case SDL_SCANCODE_RSHIFT:
keyCode = 0xA1;
break;
case SDL_SCANCODE_LCTRL:
keyCode = 0xA2;
break;
case SDL_SCANCODE_RCTRL:
keyCode = 0xA3;
break;
case SDL_SCANCODE_LALT:
keyCode = 0xA4;
break;
case SDL_SCANCODE_RALT:
keyCode = 0xA5;
break;
case SDL_SCANCODE_AC_BACK:
keyCode = 0xA6;
break;
case SDL_SCANCODE_AC_FORWARD:
keyCode = 0xA7;
break;
case SDL_SCANCODE_AC_REFRESH:
keyCode = 0xA8;
break;
case SDL_SCANCODE_AC_STOP:
keyCode = 0xA9;
break;
case SDL_SCANCODE_AC_SEARCH:
keyCode = 0xAA;
break;
case SDL_SCANCODE_AC_BOOKMARKS:
keyCode = 0xAB;
break;
case SDL_SCANCODE_AC_HOME:
keyCode = 0xAC;
break;
case SDL_SCANCODE_SEMICOLON:
keyCode = 0xBA;
break;
case SDL_SCANCODE_EQUALS:
keyCode = 0xBB;
break;
case SDL_SCANCODE_COMMA:
keyCode = 0xBC;
break;
case SDL_SCANCODE_MINUS:
keyCode = 0xBD;
break;
case SDL_SCANCODE_PERIOD:
keyCode = 0xBE;
break;
case SDL_SCANCODE_SLASH:
keyCode = 0xBF;
break;
case SDL_SCANCODE_GRAVE:
keyCode = 0xC0;
break;
case SDL_SCANCODE_LEFTBRACKET:
keyCode = 0xDB;
break;
case SDL_SCANCODE_BACKSLASH:
keyCode = 0xDC;
break;
case SDL_SCANCODE_RIGHTBRACKET:
keyCode = 0xDD;
break;
case SDL_SCANCODE_APOSTROPHE:
keyCode = 0xDE;
break;
case SDL_SCANCODE_NONUSBACKSLASH:
keyCode = 0xE2;
break;
default:
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Unhandled button event: %d",
event->keysym.scancode);
return;
}
}
// Track the key state so we always know which keys are down
if (event->state == SDL_PRESSED) {
m_KeysDown.insert(keyCode);
}
else {
m_KeysDown.remove(keyCode);
}
LiSendKeyboardEvent(keyCode,
event->state == SDL_PRESSED ?
KEY_ACTION_DOWN : KEY_ACTION_UP,
modifiers);
}
void SdlInputHandler::handleMouseButtonEvent(SDL_MouseButtonEvent* event)
{
int button;
// Capture the mouse again if clicked when unbound.
// We start capture on left button released instead of
// pressed to avoid sending an errant mouse button released
// event to the host when clicking into our window (since
// the pressed event was consumed by this code).
if (event->button == SDL_BUTTON_LEFT &&
event->state == SDL_RELEASED &&
!SDL_GetRelativeMouseMode()) {
SDL_SetRelativeMouseMode(SDL_TRUE);
return;
}
else if (!SDL_GetRelativeMouseMode()) {
// Not capturing
return;
}
else if (event->which == SDL_TOUCH_MOUSEID) {
// Ignore synthetic mouse events
return;
}
switch (event->button)
{
case SDL_BUTTON_LEFT:
button = BUTTON_LEFT;
break;
case SDL_BUTTON_MIDDLE:
button = BUTTON_MIDDLE;
break;
case SDL_BUTTON_RIGHT:
button = BUTTON_RIGHT;
break;
case SDL_BUTTON_X1:
button = BUTTON_X1;
break;
case SDL_BUTTON_X2:
button = BUTTON_X2;
break;
default:
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Unhandled button event: %d",
event->button);
return;
}
LiSendMouseButtonEvent(event->state == SDL_PRESSED ?
BUTTON_ACTION_PRESS :
BUTTON_ACTION_RELEASE,
button);
}
void SdlInputHandler::handleMouseMotionEvent(SDL_MouseMotionEvent* event)
{
if (!SDL_GetRelativeMouseMode()) {
// Not capturing
return;
}
else if (event->which == SDL_TOUCH_MOUSEID) {
// Ignore synthetic mouse events
return;
}
// Batch until the next mouse polling window or we'll get awful
// input lag everything except GFE 3.14 and 3.15.
SDL_AtomicAdd(&m_MouseDeltaX, event->xrel);
SDL_AtomicAdd(&m_MouseDeltaY, event->yrel);
}
void SdlInputHandler::handleMouseWheelEvent(SDL_MouseWheelEvent* event)
{
if (!SDL_GetRelativeMouseMode()) {
// Not capturing
return;
}
else if (event->which == SDL_TOUCH_MOUSEID) {
// Ignore synthetic mouse events
return;
}
if (event->y != 0) {
LiSendScrollEvent((signed char)event->y);
}
}
GamepadState*
SdlInputHandler::findStateForGamepad(SDL_JoystickID id)
{
int i;
for (i = 0; i < MAX_GAMEPADS; i++) {
if (m_GamepadState[i].jsId == id) {
SDL_assert(!m_MultiController || m_GamepadState[i].index == i);
return &m_GamepadState[i];
}
}
// This should only happen with > 4 gamepads
SDL_assert(SDL_NumJoysticks() > 4);
return nullptr;
}
void SdlInputHandler::sendGamepadState(GamepadState* state)
{
SDL_assert(m_GamepadMask == 0x1 || m_MultiController);
LiSendMultiControllerEvent(state->index,
m_GamepadMask,
state->buttons,
state->lt,
state->rt,
state->lsX,
state->lsY,
state->rsX,
state->rsY);
}
Uint32 SdlInputHandler::releaseLeftButtonTimerCallback(Uint32, void*)
{
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
return 0;
}
Uint32 SdlInputHandler::releaseRightButtonTimerCallback(Uint32, void*)
{
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_RIGHT);
return 0;
}
Uint32 SdlInputHandler::dragTimerCallback(Uint32, void *param)
{
auto me = reinterpret_cast<SdlInputHandler*>(param);
// Check how many fingers are down now to decide
// which button to hold down
if (me->m_NumFingersDown == 2) {
me->m_DragButton = BUTTON_RIGHT;
}
else if (me->m_NumFingersDown == 1) {
me->m_DragButton = BUTTON_LEFT;
}
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, me->m_DragButton);
return 0;
}
Uint32 SdlInputHandler::mouseMoveTimerCallback(Uint32 interval, void *param)
{
auto me = reinterpret_cast<SdlInputHandler*>(param);
short deltaX = (short)SDL_AtomicSet(&me->m_MouseDeltaX, 0);
short deltaY = (short)SDL_AtomicSet(&me->m_MouseDeltaY, 0);
if (deltaX != 0 || deltaY != 0) {
LiSendMouseMoveEvent(deltaX, deltaY);
}
return interval;
}
void SdlInputHandler::handleControllerAxisEvent(SDL_ControllerAxisEvent* event)
{
GamepadState* state = findStateForGamepad(event->which);
if (state == NULL) {
return;
}
switch (event->axis)
{
case SDL_CONTROLLER_AXIS_LEFTX:
state->lsX = event->value;
break;
case SDL_CONTROLLER_AXIS_LEFTY:
// Signed values have one more negative value than
// positive value, so inverting the sign on -32768
// could actually cause the value to overflow and
// wrap around to be negative again. Avoid that by
// capping the value at 32767.
state->lsY = -qMax(event->value, (short)-32767);
break;
case SDL_CONTROLLER_AXIS_RIGHTX:
state->rsX = event->value;
break;
case SDL_CONTROLLER_AXIS_RIGHTY:
state->rsY = -qMax(event->value, (short)-32767);
break;
case SDL_CONTROLLER_AXIS_TRIGGERLEFT:
state->lt = (unsigned char)(event->value * 255UL / 32767);
break;
case SDL_CONTROLLER_AXIS_TRIGGERRIGHT:
state->rt = (unsigned char)(event->value * 255UL / 32767);
break;
default:
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Unhandled controller axis: %d",
event->axis);
return;
}
sendGamepadState(state);
}
void SdlInputHandler::handleControllerButtonEvent(SDL_ControllerButtonEvent* event)
{
GamepadState* state = findStateForGamepad(event->which);
if (state == NULL) {
return;
}
if (event->state == SDL_PRESSED) {
state->buttons |= k_ButtonMap[event->button];
}
else {
state->buttons &= ~k_ButtonMap[event->button];
}
// Handle Start+Select+L1+R1 as a gamepad quit combo
if (state->buttons == (PLAY_FLAG | BACK_FLAG | LB_FLAG | RB_FLAG)) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Detected quit gamepad button combo");
// Push a quit event to the main loop
SDL_Event event;
event.type = SDL_QUIT;
event.quit.timestamp = SDL_GetTicks();
SDL_PushEvent(&event);
// Clear buttons down on this gameapd
LiSendMultiControllerEvent(state->index, m_GamepadMask,
0, 0, 0, 0, 0, 0, 0);
return;
}
sendGamepadState(state);
}
void SdlInputHandler::handleControllerDeviceEvent(SDL_ControllerDeviceEvent* event)
{
GamepadState* state;
if (event->type == SDL_CONTROLLERDEVICEADDED) {
int i;
const char* name;
SDL_GameController* controller;
const char* mapping;
char guidStr[33];
controller = SDL_GameControllerOpen(event->which);
if (controller == NULL) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to open gamepad: %s",
SDL_GetError());
return;
}
#if SDL_VERSION_ATLEAST(2, 0, 9)
// Determine this player's preferred index
i = SDL_GameControllerGetPlayerIndex(controller);
if (i < 0 || i >= MAX_GAMEPADS || m_GamepadState[i].controller != NULL) {
// If the player index is unavailable or invalid, start searching from 0
i = 0;
}
#else
// SDL 2.0.8 and earlier has no player number hints
i = 0;
#endif
for (; i < MAX_GAMEPADS; i++) {
if (m_GamepadState[i].controller == NULL) {
// Found an empty slot
break;
}
}
if (i == MAX_GAMEPADS) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"No open gamepad slots found!");
SDL_GameControllerClose(controller);
return;
}
state = &m_GamepadState[i];
if (m_MultiController) {
state->index = i;
}
else {
// Always player 1 in single controller mode
state->index = 0;
}
state->controller = controller;
state->jsId = SDL_JoystickInstanceID(SDL_GameControllerGetJoystick(state->controller));
SDL_JoystickGetGUIDString(SDL_JoystickGetGUID(SDL_GameControllerGetJoystick(state->controller)),
guidStr, sizeof(guidStr));
mapping = SDL_GameControllerMapping(state->controller);
name = SDL_GameControllerName(state->controller);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Gamepad %d (player %d) is: %s (%s -> %s)",
i,
state->index,
name != nullptr ? name : "<null>",
guidStr,
mapping != nullptr ? mapping : "<null>");
if (mapping != nullptr) {
SDL_free((void*)mapping);
}
// Add this gamepad to the gamepad mask
if (m_MultiController) {
// NB: Don't assert that it's unset here because we will already
// have the mask set for initially attached gamepads to avoid confusing
// apps running on the host.
m_GamepadMask |= (1 << state->index);
}
else {
SDL_assert(m_GamepadMask == 0x1);
}
// Send an empty event to tell the PC we've arrived
sendGamepadState(state);
}
else if (event->type == SDL_CONTROLLERDEVICEREMOVED) {
state = findStateForGamepad(event->which);
if (state != NULL) {
SDL_GameControllerClose(state->controller);
// Remove this from the gamepad mask in MC-mode
if (m_MultiController) {
SDL_assert(m_GamepadMask & (1 << state->index));
m_GamepadMask &= ~(1 << state->index);
}
else {
SDL_assert(m_GamepadMask == 0x1);
}
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Gamepad %d is gone",
state->index);
// Send a final event to let the PC know this gamepad is gone
LiSendMultiControllerEvent(state->index, m_GamepadMask,
0, 0, 0, 0, 0, 0, 0);
// Clear all remaining state from this slot
SDL_memset(state, 0, sizeof(*state));
}
}
}
void SdlInputHandler::handleJoystickArrivalEvent(SDL_JoyDeviceEvent* event)
{
SDL_assert(event->type == SDL_JOYDEVICEADDED);
if (!SDL_IsGameController(event->which)) {
char guidStr[33];
SDL_JoystickGetGUIDString(SDL_JoystickGetDeviceGUID(event->which),
guidStr, sizeof(guidStr));
const char* name = SDL_JoystickNameForIndex(event->which);
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Joystick discovered with no mapping: %s %s",
name ? name : "<UNKNOWN>",
guidStr);
SDL_Joystick* joy = SDL_JoystickOpen(event->which);
if (joy != nullptr) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Number of axes: %d | Number of buttons: %d | Number of hats: %d",
SDL_JoystickNumAxes(joy), SDL_JoystickNumButtons(joy),
SDL_JoystickNumHats(joy));
SDL_JoystickClose(joy);
}
else {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Unable to open joystick for query: %s",
SDL_GetError());
}
}
}
void SdlInputHandler::handleTouchFingerEvent(SDL_TouchFingerEvent* event)
{
int fingerIndex = -1;
// Observations on Windows 10: x and y appear to be relative to 0,0 of the window client area.
// Although SDL documentation states they are 0.0 - 1.0 float values, they can actually be higher
// or lower than those values as touch events continue for touches started within the client area that
// leave the client area during a drag motion.
// dx and dy are deltas from the last touch event, not the first touch down.
// Determine the index of this finger using our list
// of fingers that are currently active on screen.
// This is also required to handle finger up which
// where the finger will not be in SDL_GetTouchFinger()
// anymore.
if (event->type != SDL_FINGERDOWN) {
for (int i = 0; i < MAX_FINGERS; i++) {
if (event->fingerId == m_TouchDownEvent[i].fingerId) {
fingerIndex = i;
break;
}
}
}
else {
// Resolve the new finger by determining the ID of each
// finger on the display.
int numTouchFingers = SDL_GetNumTouchFingers(event->touchId);
for (int i = 0; i < numTouchFingers; i++) {
SDL_Finger* finger = SDL_GetTouchFinger(event->touchId, i);
SDL_assert(finger != nullptr);
if (finger != nullptr) {
if (finger->id == event->fingerId) {
fingerIndex = i;
break;
}
}
}
}
if (fingerIndex < 0 || fingerIndex >= MAX_FINGERS) {
// Too many fingers
return;
}
// Handle cursor motion based on the position of the
// primary finger on screen
if (fingerIndex == 0) {
// The event x and y values are relative to our window width
// and height. However, we want to scale them to be relative
// to the host resolution. Fortunately this is easy since we
// already have normalized values. We'll just multiply them
// by the stream dimensions to get real X and Y values rather
// than the client window dimensions.
short deltaX = static_cast<short>(event->dx * m_StreamWidth);
short deltaY = static_cast<short>(event->dy * m_StreamHeight);
if (deltaX != 0 || deltaY != 0) {
LiSendMouseMoveEvent(deltaX, deltaY);
}
}
// Start a drag timer when primary or secondary
// fingers go down
if (event->type == SDL_FINGERDOWN &&
(fingerIndex == 0 || fingerIndex == 1)) {
SDL_RemoveTimer(m_DragTimer);
m_DragTimer = SDL_AddTimer(DRAG_ACTIVATION_DELAY,
dragTimerCallback,
this);
}
if (event->type == SDL_FINGERMOTION) {
// Count the total cumulative dx/dy that the finger
// has moved.
m_CumulativeDelta[fingerIndex] += qAbs(event->x);
m_CumulativeDelta[fingerIndex] += qAbs(event->y);
// If it's outside the deadzone delta, cancel drags and taps
if (m_CumulativeDelta[fingerIndex] > DEAD_ZONE_DELTA) {
SDL_RemoveTimer(m_DragTimer);
m_DragTimer = 0;
// This effectively cancels the tap logic below
m_TouchDownEvent[fingerIndex].timestamp = 0;
}
}
if (event->type == SDL_FINGERUP) {
// Cancel the drag timer on finger up
SDL_RemoveTimer(m_DragTimer);
m_DragTimer = 0;
// Release any drag
if (m_DragButton != 0) {
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, m_DragButton);
m_DragButton = 0;
}
// 2 finger tap
else if (event->timestamp - m_TouchDownEvent[1].timestamp < 250) {
// Zero timestamp of the primary finger to ensure we won't
// generate a left click if the primary finger comes up soon.
m_TouchDownEvent[0].timestamp = 0;
// Press down the right mouse button
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_RIGHT);
// Queue a timer to release it in 100 ms
SDL_RemoveTimer(m_RightButtonReleaseTimer);
m_RightButtonReleaseTimer = SDL_AddTimer(TAP_BUTTON_RELEASE_DELAY,
releaseRightButtonTimerCallback,
nullptr);
}
// 1 finger tap
else if (event->timestamp - m_TouchDownEvent[0].timestamp < 250) {
// Press down the left mouse button
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT);
// Queue a timer to release it in 100 ms
SDL_RemoveTimer(m_LeftButtonReleaseTimer);
m_LeftButtonReleaseTimer = SDL_AddTimer(TAP_BUTTON_RELEASE_DELAY,
releaseLeftButtonTimerCallback,
nullptr);
}
}
m_NumFingersDown = SDL_GetNumTouchFingers(event->touchId);
if (event->type == SDL_FINGERDOWN) {
m_TouchDownEvent[fingerIndex] = *event;
m_CumulativeDelta[fingerIndex] = 0;
}
else if (event->type == SDL_FINGERUP) {
m_TouchDownEvent[fingerIndex] = {};
}
}
int SdlInputHandler::getAttachedGamepadMask()
{
int count;
int mask;
if (!m_MultiController) {
// Player 1 is always present in non-MC mode
return 0x1;
}
// TODO: Use SDL_GameControllerGetPlayerIndex() for accurate indexes?
count = mask = 0;
for (int i = 0; i < SDL_NumJoysticks(); i++) {
if (SDL_IsGameController(i)) {
mask |= (1 << count++);
}
}
return mask;
}
void SdlInputHandler::raiseAllKeys()
{
if (m_KeysDown.isEmpty()) {
return;
}
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Raising %d keys",
m_KeysDown.count());
for (auto keyDown : m_KeysDown) {
LiSendKeyboardEvent(keyDown, KEY_ACTION_UP, 0);
}
m_KeysDown.clear();
}
QString SdlInputHandler::getUnmappedGamepads()
{
QString ret;
if (SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) failed: %s",
SDL_GetError());
}
MappingManager mappingManager;
mappingManager.applyMappings();
for (int i = 0; i < SDL_NumJoysticks(); i++) {
if (!SDL_IsGameController(i)) {
char guidStr[33];
SDL_JoystickGetGUIDString(SDL_JoystickGetDeviceGUID(i),
guidStr, sizeof(guidStr));
const char* name = SDL_JoystickNameForIndex(i);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Unmapped joystick: %s %s",
name ? name : "<UNKNOWN>",
guidStr);
SDL_Joystick* joy = SDL_JoystickOpen(i);
if (joy != nullptr) {
int numButtons = SDL_JoystickNumButtons(joy);
int numHats = SDL_JoystickNumHats(joy);
int numAxes = SDL_JoystickNumAxes(joy);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Number of axes: %d | Number of buttons: %d | Number of hats: %d",
numAxes, numButtons, numHats);
if ((numAxes >= 4 && numAxes <= 8) && numButtons >= 8 && numHats <= 1) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Joystick likely to be an unmapped game controller");
if (!ret.isEmpty()) {
ret += ", ";
}
ret += name;
}
SDL_JoystickClose(joy);
}
else {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Unable to open joystick for query: %s",
SDL_GetError());
}
}
}
SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER);
return ret;
}
@@ -10,15 +10,6 @@ struct GamepadState {
SDL_JoystickID jsId; SDL_JoystickID jsId;
short index; short index;
#if !SDL_VERSION_ATLEAST(2, 0, 9)
SDL_Haptic* haptic;
int hapticMethod;
int hapticEffectId;
#endif
SDL_TimerID mouseEmulationTimer;
uint32_t lastStartDownTime;
short buttons; short buttons;
short lsX, lsY; short lsX, lsY;
short rsX, rsY; short rsX, rsY;
@@ -28,13 +19,6 @@ struct GamepadState {
#define MAX_GAMEPADS 4 #define MAX_GAMEPADS 4
#define MAX_FINGERS 2 #define MAX_FINGERS 2
#define GAMEPAD_HAPTIC_METHOD_NONE 0
#define GAMEPAD_HAPTIC_METHOD_LEFTRIGHT 1
#define GAMEPAD_HAPTIC_METHOD_SIMPLERUMBLE 2
#define GAMEPAD_HAPTIC_SIMPLE_HIFREQ_MOTOR_WEIGHT 0.33
#define GAMEPAD_HAPTIC_SIMPLE_LOWFREQ_MOTOR_WEIGHT 0.8
class SdlInputHandler class SdlInputHandler
{ {
public: public:
@@ -43,8 +27,6 @@ public:
~SdlInputHandler(); ~SdlInputHandler();
void setWindow(SDL_Window* window);
void handleKeyEvent(SDL_KeyboardEvent* event); void handleKeyEvent(SDL_KeyboardEvent* event);
void handleMouseButtonEvent(SDL_MouseButtonEvent* event); void handleMouseButtonEvent(SDL_MouseButtonEvent* event);
@@ -61,22 +43,12 @@ public:
void handleJoystickArrivalEvent(SDL_JoyDeviceEvent* event); void handleJoystickArrivalEvent(SDL_JoyDeviceEvent* event);
void rumble(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor);
void handleTouchFingerEvent(SDL_TouchFingerEvent* event); void handleTouchFingerEvent(SDL_TouchFingerEvent* event);
int getAttachedGamepadMask(); int getAttachedGamepadMask();
void raiseAllKeys(); void raiseAllKeys();
void notifyMouseLeave();
void notifyFocusLost();
bool isCaptureActive();
void setCaptureActive(bool active);
static static
QString getUnmappedGamepads(); QString getUnmappedGamepads();
@@ -86,19 +58,6 @@ private:
void sendGamepadState(GamepadState* state); void sendGamepadState(GamepadState* state);
void handleAbsoluteFingerEvent(SDL_TouchFingerEvent* event);
void handleRelativeFingerEvent(SDL_TouchFingerEvent* event);
static
Uint32 longPressTimerCallback(Uint32 interval, void* param);
static
Uint32 mouseMoveTimerCallback(Uint32 interval, void* param);
static
Uint32 mouseEmulationTimerCallback(Uint32 interval, void* param);
static static
Uint32 releaseLeftButtonTimerCallback(Uint32 interval, void* param); Uint32 releaseLeftButtonTimerCallback(Uint32 interval, void* param);
@@ -108,32 +67,26 @@ private:
static static
Uint32 dragTimerCallback(Uint32 interval, void* param); Uint32 dragTimerCallback(Uint32 interval, void* param);
SDL_Window* m_Window; static
Uint32 mouseMoveTimerCallback(Uint32 interval, void* param);
bool m_MultiController; bool m_MultiController;
bool m_GamepadMouse;
SDL_TimerID m_MouseMoveTimer; SDL_TimerID m_MouseMoveTimer;
SDL_atomic_t m_MouseDeltaX; SDL_atomic_t m_MouseDeltaX;
SDL_atomic_t m_MouseDeltaY; SDL_atomic_t m_MouseDeltaY;
int m_GamepadMask; int m_GamepadMask;
GamepadState m_GamepadState[MAX_GAMEPADS]; GamepadState m_GamepadState[MAX_GAMEPADS];
QSet<short> m_KeysDown; QSet<short> m_KeysDown;
bool m_FakeCaptureActive;
SDL_TouchFingerEvent m_LastTouchDownEvent;
SDL_TouchFingerEvent m_LastTouchUpEvent;
SDL_TimerID m_LongPressTimer;
int m_StreamWidth;
int m_StreamHeight;
bool m_AbsoluteMouseMode;
bool m_AbsoluteTouchMode;
Uint32 m_PendingMouseLeaveButtonUp;
SDL_TouchFingerEvent m_TouchDownEvent[MAX_FINGERS]; SDL_TouchFingerEvent m_TouchDownEvent[MAX_FINGERS];
float m_CumulativeDelta[MAX_FINGERS];
SDL_TimerID m_LeftButtonReleaseTimer; SDL_TimerID m_LeftButtonReleaseTimer;
SDL_TimerID m_RightButtonReleaseTimer; SDL_TimerID m_RightButtonReleaseTimer;
SDL_TimerID m_DragTimer; SDL_TimerID m_DragTimer;
char m_DragButton; char m_DragButton;
int m_NumFingersDown; int m_NumFingersDown;
int m_StreamWidth;
int m_StreamHeight;
static const int k_ButtonMap[]; static const int k_ButtonMap[];
}; };
-107
View File
@@ -1,107 +0,0 @@
#include "input.h"
#include <Limelight.h>
#include <SDL.h>
#include "streaming/streamutils.h"
#include <QtMath>
// How long the fingers must be stationary to start a right click
#define LONG_PRESS_ACTIVATION_DELAY 650
// How far the finger can move before it cancels a right click
#define LONG_PRESS_ACTIVATION_DELTA 0.01f
// How long the double tap deadzone stays in effect between touch up and touch down
#define DOUBLE_TAP_DEAD_ZONE_DELAY 250
// How far the finger can move before it can override the double tap deadzone
#define DOUBLE_TAP_DEAD_ZONE_DELTA 0.025f
Uint32 SdlInputHandler::longPressTimerCallback(Uint32, void*)
{
// Raise the left click and start a right click
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_RIGHT);
return 0;
}
void SdlInputHandler::handleAbsoluteFingerEvent(SDL_TouchFingerEvent* event)
{
// Observations on Windows 10: x and y appear to be relative to 0,0 of the window client area.
// Although SDL documentation states they are 0.0 - 1.0 float values, they can actually be higher
// or lower than those values as touch events continue for touches started within the client area that
// leave the client area during a drag motion.
// dx and dy are deltas from the last touch event, not the first touch down.
// Ignore touch down events with more than one finger
if (event->type == SDL_FINGERDOWN && SDL_GetNumTouchFingers(event->touchId) > 1) {
return;
}
// Ignore touch move and touch up events from the non-primary finger
if (event->type != SDL_FINGERDOWN && event->fingerId != m_LastTouchDownEvent.fingerId) {
return;
}
SDL_Rect src, dst;
int windowWidth, windowHeight;
SDL_GetWindowSize(m_Window, &windowWidth, &windowHeight);
src.x = src.y = 0;
src.w = m_StreamWidth;
src.h = m_StreamHeight;
dst.x = dst.y = 0;
dst.w = windowWidth;
dst.h = windowHeight;
// Use the stream and window sizes to determine the video region
StreamUtils::scaleSourceToDestinationSurface(&src, &dst);
if (qSqrt(qPow(event->x - m_LastTouchDownEvent.x, 2) + qPow(event->y - m_LastTouchDownEvent.y, 2)) > LONG_PRESS_ACTIVATION_DELTA) {
// Moved too far since touch down. Cancel the long press timer.
SDL_RemoveTimer(m_LongPressTimer);
m_LongPressTimer = 0;
}
// Don't reposition for finger down events within the deadzone. This makes double-clicking easier.
if (event->type != SDL_FINGERDOWN ||
event->timestamp - m_LastTouchUpEvent.timestamp > DOUBLE_TAP_DEAD_ZONE_DELAY ||
qSqrt(qPow(event->x - m_LastTouchUpEvent.x, 2) + qPow(event->y - m_LastTouchUpEvent.y, 2)) > DOUBLE_TAP_DEAD_ZONE_DELTA) {
// Scale window-relative events to be video-relative and clamp to video region
short x = qMin(qMax((int)(event->x * windowWidth), dst.x), dst.x + dst.w);
short y = qMin(qMax((int)(event->y * windowHeight), dst.y), dst.y + dst.h);
// Update the cursor position relative to the video region
LiSendMousePositionEvent(x - dst.x, y - dst.y, dst.w, dst.h);
}
if (event->type == SDL_FINGERDOWN) {
m_LastTouchDownEvent = *event;
// Start/restart the long press timer
SDL_RemoveTimer(m_LongPressTimer);
m_LongPressTimer = SDL_AddTimer(LONG_PRESS_ACTIVATION_DELAY,
longPressTimerCallback,
nullptr);
// Left button down on finger down
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT);
}
else if (event->type == SDL_FINGERUP) {
m_LastTouchUpEvent = *event;
// Cancel the long press timer
SDL_RemoveTimer(m_LongPressTimer);
m_LongPressTimer = 0;
// Left button up on finger up
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
// Raise right button too in case we triggered a long press gesture
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_RIGHT);
}
}
-595
View File
@@ -1,595 +0,0 @@
#include "streaming/session.h"
#include <Limelight.h>
#include <SDL.h>
#include "settings/mappingmanager.h"
#include <QtMath>
// How long the Start button must be pressed to toggle mouse emulation
#define MOUSE_EMULATION_LONG_PRESS_TIME 750
// How long between polling the gamepad to send virtual mouse input
#define MOUSE_EMULATION_POLLING_INTERVAL 50
// Determines how fast the mouse will move each interval
#define MOUSE_EMULATION_MOTION_MULTIPLIER 4
// Determines the maximum motion amount before allowing movement
#define MOUSE_EMULATION_DEADZONE 2
// Haptic capabilities (in addition to those from SDL_HapticQuery())
#define ML_HAPTIC_GC_RUMBLE (1U << 16)
#define ML_HAPTIC_SIMPLE_RUMBLE (1U << 17)
const int SdlInputHandler::k_ButtonMap[] = {
A_FLAG, B_FLAG, X_FLAG, Y_FLAG,
BACK_FLAG, SPECIAL_FLAG, PLAY_FLAG,
LS_CLK_FLAG, RS_CLK_FLAG,
LB_FLAG, RB_FLAG,
UP_FLAG, DOWN_FLAG, LEFT_FLAG, RIGHT_FLAG
};
GamepadState*
SdlInputHandler::findStateForGamepad(SDL_JoystickID id)
{
int i;
for (i = 0; i < MAX_GAMEPADS; i++) {
if (m_GamepadState[i].jsId == id) {
SDL_assert(!m_MultiController || m_GamepadState[i].index == i);
return &m_GamepadState[i];
}
}
// This should only happen with > 4 gamepads
SDL_assert(SDL_NumJoysticks() > 4);
return nullptr;
}
void SdlInputHandler::sendGamepadState(GamepadState* state)
{
SDL_assert(m_GamepadMask == 0x1 || m_MultiController);
LiSendMultiControllerEvent(state->index,
m_GamepadMask,
state->buttons,
state->lt,
state->rt,
state->lsX,
state->lsY,
state->rsX,
state->rsY);
}
Uint32 SdlInputHandler::mouseEmulationTimerCallback(Uint32 interval, void *param)
{
auto gamepad = reinterpret_cast<GamepadState*>(param);
short rawX;
short rawY;
// Determine which analog stick is currently receiving the strongest input
if ((uint32_t)qAbs(gamepad->lsX) + qAbs(gamepad->lsY) > (uint32_t)qAbs(gamepad->rsX) + qAbs(gamepad->rsY)) {
rawX = gamepad->lsX;
rawY = -gamepad->lsY;
}
else {
rawX = gamepad->rsX;
rawY = -gamepad->rsY;
}
float deltaX;
float deltaY;
// Produce a base vector for mouse movement with increased speed as we deviate further from center
deltaX = qPow(rawX / 32766.0f * MOUSE_EMULATION_MOTION_MULTIPLIER, 3);
deltaY = qPow(rawY / 32766.0f * MOUSE_EMULATION_MOTION_MULTIPLIER, 3);
// Enforce deadzones
deltaX = qAbs(deltaX) > MOUSE_EMULATION_DEADZONE ? deltaX - MOUSE_EMULATION_DEADZONE : 0;
deltaY = qAbs(deltaY) > MOUSE_EMULATION_DEADZONE ? deltaY - MOUSE_EMULATION_DEADZONE : 0;
if (deltaX != 0 || deltaY != 0) {
LiSendMouseMoveEvent((short)deltaX, (short)deltaY);
}
return interval;
}
void SdlInputHandler::handleControllerAxisEvent(SDL_ControllerAxisEvent* event)
{
SDL_JoystickID gameControllerId = event->which;
GamepadState* state = findStateForGamepad(gameControllerId);
if (state == NULL) {
return;
}
// Batch all pending axis motion events for this gamepad to save CPU time
SDL_Event nextEvent;
for (;;) {
switch (event->axis)
{
case SDL_CONTROLLER_AXIS_LEFTX:
state->lsX = event->value;
break;
case SDL_CONTROLLER_AXIS_LEFTY:
// Signed values have one more negative value than
// positive value, so inverting the sign on -32768
// could actually cause the value to overflow and
// wrap around to be negative again. Avoid that by
// capping the value at 32767.
state->lsY = -qMax(event->value, (short)-32767);
break;
case SDL_CONTROLLER_AXIS_RIGHTX:
state->rsX = event->value;
break;
case SDL_CONTROLLER_AXIS_RIGHTY:
state->rsY = -qMax(event->value, (short)-32767);
break;
case SDL_CONTROLLER_AXIS_TRIGGERLEFT:
state->lt = (unsigned char)(event->value * 255UL / 32767);
break;
case SDL_CONTROLLER_AXIS_TRIGGERRIGHT:
state->rt = (unsigned char)(event->value * 255UL / 32767);
break;
default:
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Unhandled controller axis: %d",
event->axis);
return;
}
// Check for another event to batch with
if (SDL_PeepEvents(&nextEvent, 1, SDL_PEEKEVENT, SDL_CONTROLLERAXISMOTION, SDL_CONTROLLERAXISMOTION) <= 0) {
break;
}
event = &nextEvent.caxis;
if (event->which != gameControllerId) {
// Stop batching if a different gamepad interrupts us
break;
}
// Remove the next event to batch
SDL_PeepEvents(&nextEvent, 1, SDL_GETEVENT, SDL_CONTROLLERAXISMOTION, SDL_CONTROLLERAXISMOTION);
}
// Only send the gamepad state to the host if it's not in mouse emulation mode
if (state->mouseEmulationTimer == 0) {
sendGamepadState(state);
}
}
void SdlInputHandler::handleControllerButtonEvent(SDL_ControllerButtonEvent* event)
{
GamepadState* state = findStateForGamepad(event->which);
if (state == NULL) {
return;
}
if (event->state == SDL_PRESSED) {
state->buttons |= k_ButtonMap[event->button];
if (event->button == SDL_CONTROLLER_BUTTON_START) {
state->lastStartDownTime = SDL_GetTicks();
}
else if (state->mouseEmulationTimer != 0) {
if (event->button == SDL_CONTROLLER_BUTTON_A) {
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT);
}
else if (event->button == SDL_CONTROLLER_BUTTON_B) {
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_RIGHT);
}
else if (event->button == SDL_CONTROLLER_BUTTON_X) {
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_MIDDLE);
}
else if (event->button == SDL_CONTROLLER_BUTTON_LEFTSHOULDER) {
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_X1);
}
else if (event->button == SDL_CONTROLLER_BUTTON_RIGHTSHOULDER) {
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_X2);
}
else if (event->button == SDL_CONTROLLER_BUTTON_DPAD_UP) {
LiSendScrollEvent(1);
}
else if (event->button == SDL_CONTROLLER_BUTTON_DPAD_DOWN) {
LiSendScrollEvent(-1);
}
}
}
else {
state->buttons &= ~k_ButtonMap[event->button];
if (event->button == SDL_CONTROLLER_BUTTON_START) {
if (SDL_GetTicks() - state->lastStartDownTime > MOUSE_EMULATION_LONG_PRESS_TIME) {
if (state->mouseEmulationTimer != 0) {
SDL_RemoveTimer(state->mouseEmulationTimer);
state->mouseEmulationTimer = 0;
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Mouse emulation deactivated");
Session::get()->notifyMouseEmulationMode(false);
}
else if (m_GamepadMouse) {
// Send the start button up event to the host, since we won't do it below
sendGamepadState(state);
state->mouseEmulationTimer = SDL_AddTimer(MOUSE_EMULATION_POLLING_INTERVAL, SdlInputHandler::mouseEmulationTimerCallback, state);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Mouse emulation active");
Session::get()->notifyMouseEmulationMode(true);
}
}
}
else if (state->mouseEmulationTimer != 0) {
if (event->button == SDL_CONTROLLER_BUTTON_A) {
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
}
else if (event->button == SDL_CONTROLLER_BUTTON_B) {
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_RIGHT);
}
else if (event->button == SDL_CONTROLLER_BUTTON_X) {
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_MIDDLE);
}
else if (event->button == SDL_CONTROLLER_BUTTON_LEFTSHOULDER) {
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_X1);
}
else if (event->button == SDL_CONTROLLER_BUTTON_RIGHTSHOULDER) {
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_X2);
}
}
}
// Handle Start+Select+L1+R1 as a gamepad quit combo
if (state->buttons == (PLAY_FLAG | BACK_FLAG | LB_FLAG | RB_FLAG)) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Detected quit gamepad button combo");
// Push a quit event to the main loop
SDL_Event event;
event.type = SDL_QUIT;
event.quit.timestamp = SDL_GetTicks();
SDL_PushEvent(&event);
// Clear buttons down on this gameapd
LiSendMultiControllerEvent(state->index, m_GamepadMask,
0, 0, 0, 0, 0, 0, 0);
return;
}
// Only send the gamepad state to the host if it's not in mouse emulation mode
if (state->mouseEmulationTimer == 0) {
sendGamepadState(state);
}
}
void SdlInputHandler::handleControllerDeviceEvent(SDL_ControllerDeviceEvent* event)
{
GamepadState* state;
if (event->type == SDL_CONTROLLERDEVICEADDED) {
int i;
const char* name;
SDL_GameController* controller;
const char* mapping;
char guidStr[33];
uint32_t hapticCaps;
controller = SDL_GameControllerOpen(event->which);
if (controller == NULL) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to open gamepad: %s",
SDL_GetError());
return;
}
// We used to use SDL_GameControllerGetPlayerIndex() here but that
// can lead to strange issues due to bugs in Windows where an Xbox
// controller will join as player 2, even though no player 1 controller
// is connected at all. This pretty much screws any attempt to use
// the gamepad in single player games, so just assign them in order from 0.
i = 0;
for (; i < MAX_GAMEPADS; i++) {
SDL_assert(m_GamepadState[i].controller != controller);
if (m_GamepadState[i].controller == NULL) {
// Found an empty slot
break;
}
}
if (i == MAX_GAMEPADS) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"No open gamepad slots found!");
SDL_GameControllerClose(controller);
return;
}
state = &m_GamepadState[i];
if (m_MultiController) {
state->index = i;
#if SDL_VERSION_ATLEAST(2, 0, 12)
// This will change indicators on the controller to show the assigned
// player index. For Xbox 360 controllers, that means updating the LED
// ring to light up the corresponding quadrant for this player.
SDL_GameControllerSetPlayerIndex(controller, state->index);
#endif
}
else {
// Always player 1 in single controller mode
state->index = 0;
}
state->controller = controller;
state->jsId = SDL_JoystickInstanceID(SDL_GameControllerGetJoystick(state->controller));
#if SDL_VERSION_ATLEAST(2, 0, 9)
// Perform a tiny rumble to see if haptics are supported.
// NB: We cannot use zeros for rumble intensity or SDL will not actually call the JS driver
// and we'll get a (potentially false) success value returned.
hapticCaps = SDL_GameControllerRumble(controller, 1, 1, 1) == 0 ?
ML_HAPTIC_GC_RUMBLE : 0;
#else
state->haptic = SDL_HapticOpenFromJoystick(SDL_GameControllerGetJoystick(state->controller));
state->hapticEffectId = -1;
state->hapticMethod = GAMEPAD_HAPTIC_METHOD_NONE;
if (state->haptic != nullptr) {
// Query for supported haptic effects
hapticCaps = SDL_HapticQuery(state->haptic);
hapticCaps |= SDL_HapticRumbleSupported(state->haptic) ?
ML_HAPTIC_SIMPLE_RUMBLE : 0;
if ((SDL_HapticQuery(state->haptic) & SDL_HAPTIC_LEFTRIGHT) == 0) {
if (SDL_HapticRumbleSupported(state->haptic)) {
if (SDL_HapticRumbleInit(state->haptic) == 0) {
state->hapticMethod = GAMEPAD_HAPTIC_METHOD_SIMPLERUMBLE;
}
}
if (state->hapticMethod == GAMEPAD_HAPTIC_METHOD_NONE) {
SDL_HapticClose(state->haptic);
state->haptic = nullptr;
}
} else {
state->hapticMethod = GAMEPAD_HAPTIC_METHOD_LEFTRIGHT;
}
}
else {
hapticCaps = 0;
}
#endif
SDL_JoystickGetGUIDString(SDL_JoystickGetGUID(SDL_GameControllerGetJoystick(state->controller)),
guidStr, sizeof(guidStr));
mapping = SDL_GameControllerMapping(state->controller);
name = SDL_GameControllerName(state->controller);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Gamepad %d (player %d) is: %s (haptic capabilities: 0x%x) (mapping: %s -> %s)",
i,
state->index,
name != nullptr ? name : "<null>",
hapticCaps,
guidStr,
mapping != nullptr ? mapping : "<null>");
if (mapping != nullptr) {
SDL_free((void*)mapping);
}
// Add this gamepad to the gamepad mask
if (m_MultiController) {
// NB: Don't assert that it's unset here because we will already
// have the mask set for initially attached gamepads to avoid confusing
// apps running on the host.
m_GamepadMask |= (1 << state->index);
}
else {
SDL_assert(m_GamepadMask == 0x1);
}
// Send an empty event to tell the PC we've arrived
sendGamepadState(state);
}
else if (event->type == SDL_CONTROLLERDEVICEREMOVED) {
state = findStateForGamepad(event->which);
if (state != NULL) {
if (state->mouseEmulationTimer != 0) {
Session::get()->notifyMouseEmulationMode(false);
SDL_RemoveTimer(state->mouseEmulationTimer);
}
SDL_GameControllerClose(state->controller);
#if !SDL_VERSION_ATLEAST(2, 0, 9)
if (state->haptic != nullptr) {
SDL_HapticClose(state->haptic);
}
#endif
// Remove this from the gamepad mask in MC-mode
if (m_MultiController) {
SDL_assert(m_GamepadMask & (1 << state->index));
m_GamepadMask &= ~(1 << state->index);
}
else {
SDL_assert(m_GamepadMask == 0x1);
}
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Gamepad %d is gone",
state->index);
// Send a final event to let the PC know this gamepad is gone
LiSendMultiControllerEvent(state->index, m_GamepadMask,
0, 0, 0, 0, 0, 0, 0);
// Clear all remaining state from this slot
SDL_memset(state, 0, sizeof(*state));
}
}
}
void SdlInputHandler::handleJoystickArrivalEvent(SDL_JoyDeviceEvent* event)
{
SDL_assert(event->type == SDL_JOYDEVICEADDED);
if (!SDL_IsGameController(event->which)) {
char guidStr[33];
SDL_JoystickGetGUIDString(SDL_JoystickGetDeviceGUID(event->which),
guidStr, sizeof(guidStr));
const char* name = SDL_JoystickNameForIndex(event->which);
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Joystick discovered with no mapping: %s %s",
name ? name : "<UNKNOWN>",
guidStr);
SDL_Joystick* joy = SDL_JoystickOpen(event->which);
if (joy != nullptr) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Number of axes: %d | Number of buttons: %d | Number of hats: %d",
SDL_JoystickNumAxes(joy), SDL_JoystickNumButtons(joy),
SDL_JoystickNumHats(joy));
SDL_JoystickClose(joy);
}
else {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Unable to open joystick for query: %s",
SDL_GetError());
}
}
}
void SdlInputHandler::rumble(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor)
{
// Make sure the controller number is within our supported count
if (controllerNumber >= MAX_GAMEPADS) {
return;
}
#if SDL_VERSION_ATLEAST(2, 0, 9)
if (m_GamepadState[controllerNumber].controller != nullptr) {
SDL_GameControllerRumble(m_GamepadState[controllerNumber].controller, lowFreqMotor, highFreqMotor, 30000);
}
#else
// Check if the controller supports haptics (and if the controller exists at all)
SDL_Haptic* haptic = m_GamepadState[controllerNumber].haptic;
if (haptic == nullptr) {
return;
}
// Stop the last effect we played
if (m_GamepadState[controllerNumber].hapticMethod == GAMEPAD_HAPTIC_METHOD_LEFTRIGHT) {
if (m_GamepadState[controllerNumber].hapticEffectId >= 0) {
SDL_HapticDestroyEffect(haptic, m_GamepadState[controllerNumber].hapticEffectId);
}
} else if (m_GamepadState[controllerNumber].hapticMethod == GAMEPAD_HAPTIC_METHOD_SIMPLERUMBLE) {
SDL_HapticRumbleStop(haptic);
}
// If this callback is telling us to stop both motors, don't bother queuing a new effect
if (lowFreqMotor == 0 && highFreqMotor == 0) {
return;
}
if (m_GamepadState[controllerNumber].hapticMethod == GAMEPAD_HAPTIC_METHOD_LEFTRIGHT) {
SDL_HapticEffect effect;
SDL_memset(&effect, 0, sizeof(effect));
effect.type = SDL_HAPTIC_LEFTRIGHT;
// The effect should last until we are instructed to stop or change it
effect.leftright.length = SDL_HAPTIC_INFINITY;
// SDL haptics range from 0-32767 but XInput uses 0-65535, so divide by 2 to correct for SDL's scaling
effect.leftright.large_magnitude = lowFreqMotor / 2;
effect.leftright.small_magnitude = highFreqMotor / 2;
// Play the new effect
m_GamepadState[controllerNumber].hapticEffectId = SDL_HapticNewEffect(haptic, &effect);
if (m_GamepadState[controllerNumber].hapticEffectId >= 0) {
SDL_HapticRunEffect(haptic, m_GamepadState[controllerNumber].hapticEffectId, 1);
}
} else if (m_GamepadState[controllerNumber].hapticMethod == GAMEPAD_HAPTIC_METHOD_SIMPLERUMBLE) {
SDL_HapticRumblePlay(haptic,
std::min(1.0, (GAMEPAD_HAPTIC_SIMPLE_HIFREQ_MOTOR_WEIGHT*highFreqMotor +
GAMEPAD_HAPTIC_SIMPLE_LOWFREQ_MOTOR_WEIGHT*lowFreqMotor) / 65535.0),
SDL_HAPTIC_INFINITY);
}
#endif
}
QString SdlInputHandler::getUnmappedGamepads()
{
QString ret;
if (SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) failed: %s",
SDL_GetError());
}
MappingManager mappingManager;
mappingManager.applyMappings();
for (int i = 0; i < SDL_NumJoysticks(); i++) {
if (!SDL_IsGameController(i)) {
char guidStr[33];
SDL_JoystickGetGUIDString(SDL_JoystickGetDeviceGUID(i),
guidStr, sizeof(guidStr));
const char* name = SDL_JoystickNameForIndex(i);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Unmapped joystick: %s %s",
name ? name : "<UNKNOWN>",
guidStr);
SDL_Joystick* joy = SDL_JoystickOpen(i);
if (joy != nullptr) {
int numButtons = SDL_JoystickNumButtons(joy);
int numHats = SDL_JoystickNumHats(joy);
int numAxes = SDL_JoystickNumAxes(joy);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Number of axes: %d | Number of buttons: %d | Number of hats: %d",
numAxes, numButtons, numHats);
if ((numAxes >= 4 && numAxes <= 8) && numButtons >= 8 && numHats <= 1) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Joystick likely to be an unmapped game controller");
if (!ret.isEmpty()) {
ret += ", ";
}
ret += name;
}
SDL_JoystickClose(joy);
}
else {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Unable to open joystick for query: %s",
SDL_GetError());
}
}
}
SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER);
return ret;
}
int SdlInputHandler::getAttachedGamepadMask()
{
int count;
int mask;
if (!m_MultiController) {
// Player 1 is always present in non-MC mode
return 0x1;
}
count = mask = 0;
for (int i = 0; i < SDL_NumJoysticks(); i++) {
if (SDL_IsGameController(i)) {
mask |= (1 << count++);
}
}
return mask;
}
-300
View File
@@ -1,300 +0,0 @@
#include <Limelight.h>
#include <SDL.h>
#include "streaming/session.h"
#include "settings/mappingmanager.h"
#include "path.h"
#include <QtGlobal>
#include <QDir>
#define MOUSE_POLLING_INTERVAL 5
SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, NvComputer*, int streamWidth, int streamHeight)
: m_MultiController(prefs.multiController),
m_GamepadMouse(prefs.gamepadMouse),
m_MouseMoveTimer(0),
m_FakeCaptureActive(false),
m_LongPressTimer(0),
m_StreamWidth(streamWidth),
m_StreamHeight(streamHeight),
m_AbsoluteMouseMode(prefs.absoluteMouseMode),
m_AbsoluteTouchMode(prefs.absoluteTouchMode),
m_PendingMouseLeaveButtonUp(0),
m_LeftButtonReleaseTimer(0),
m_RightButtonReleaseTimer(0),
m_DragTimer(0),
m_DragButton(0),
m_NumFingersDown(0)
{
// Allow gamepad input when the app doesn't have focus
SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1");
// If absolute mouse mode is enabled, use relative mode warp (which
// is via normal motion events that are influenced by mouse acceleration).
// Otherwise, we'll use raw input capture which is straight from the device
// without modification by the OS.
SDL_SetHintWithPriority(SDL_HINT_MOUSE_RELATIVE_MODE_WARP,
prefs.absoluteMouseMode ? "1" : "0",
SDL_HINT_OVERRIDE);
// Allow clicks to pass through to us when focusing the window. If we're in
// absolute mouse mode, this will avoid the user having to click twice to
// trigger a click on the host if the Moonlight window is not focused. In
// relative mode, the click event will trigger the mouse to be recaptured.
SDL_SetHint(SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH, "1");
#if defined(Q_OS_DARWIN) && !SDL_VERSION_ATLEAST(2, 0, 10)
// SDL 2.0.9 on macOS has a broken HIDAPI mapping for the older Xbox One S
// firmware, so we have to disable HIDAPI for Xbox gamepads on macOS until
// SDL 2.0.10 where the bug is fixed.
// https://github.com/moonlight-stream/moonlight-qt/issues/133
// https://bugzilla.libsdl.org/show_bug.cgi?id=4395
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_XBOX, "0");
#endif
#if SDL_VERSION_ATLEAST(2, 0, 9)
// Enabling extended input reports allows rumble to function on Bluetooth PS4
// controllers, but breaks DirectInput applications. We will enable it because
// it's likely that working rumble is what the user is expecting. If they don't
// want this behavior, they can override it with the environment variable.
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS4_RUMBLE, "1");
#endif
// We must initialize joystick explicitly before gamecontroller in order
// to ensure we receive gamecontroller attach events for gamepads where
// SDL doesn't have a built-in mapping. By starting joystick first, we
// can allow mapping manager to update the mappings before GC attach
// events are generated.
SDL_assert(!SDL_WasInit(SDL_INIT_JOYSTICK));
if (SDL_InitSubSystem(SDL_INIT_JOYSTICK) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_JOYSTICK) failed: %s",
SDL_GetError());
}
MappingManager mappingManager;
mappingManager.applyMappings();
// Flush gamepad arrival and departure events which may be queued before
// starting the gamecontroller subsystem again. This prevents us from
// receiving duplicate arrival and departure events for the same gamepad.
SDL_FlushEvent(SDL_CONTROLLERDEVICEADDED);
SDL_FlushEvent(SDL_CONTROLLERDEVICEREMOVED);
// We need to reinit this each time, since you only get
// an initial set of gamepad arrival events once per init.
SDL_assert(!SDL_WasInit(SDL_INIT_GAMECONTROLLER));
if (SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) failed: %s",
SDL_GetError());
}
#if !SDL_VERSION_ATLEAST(2, 0, 9)
SDL_assert(!SDL_WasInit(SDL_INIT_HAPTIC));
if (SDL_InitSubSystem(SDL_INIT_HAPTIC) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_HAPTIC) failed: %s",
SDL_GetError());
}
#endif
// Initialize the gamepad mask with currently attached gamepads to avoid
// causing gamepads to unexpectedly disappear and reappear on the host
// during stream startup as we detect currently attached gamepads one at a time.
m_GamepadMask = getAttachedGamepadMask();
SDL_zero(m_GamepadState);
SDL_zero(m_LastTouchDownEvent);
SDL_zero(m_LastTouchUpEvent);
SDL_zero(m_TouchDownEvent);
SDL_AtomicSet(&m_MouseDeltaX, 0);
SDL_AtomicSet(&m_MouseDeltaY, 0);
Uint32 pollingInterval = QString(qgetenv("MOUSE_POLLING_INTERVAL")).toUInt();
if (pollingInterval == 0) {
pollingInterval = MOUSE_POLLING_INTERVAL;
}
else {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Using custom mouse polling interval: %u ms",
pollingInterval);
}
m_MouseMoveTimer = SDL_AddTimer(pollingInterval, SdlInputHandler::mouseMoveTimerCallback, this);
}
SdlInputHandler::~SdlInputHandler()
{
for (int i = 0; i < MAX_GAMEPADS; i++) {
if (m_GamepadState[i].mouseEmulationTimer != 0) {
Session::get()->notifyMouseEmulationMode(false);
SDL_RemoveTimer(m_GamepadState[i].mouseEmulationTimer);
}
#if !SDL_VERSION_ATLEAST(2, 0, 9)
if (m_GamepadState[i].haptic != nullptr) {
SDL_HapticClose(m_GamepadState[i].haptic);
}
#endif
if (m_GamepadState[i].controller != nullptr) {
SDL_GameControllerClose(m_GamepadState[i].controller);
}
}
SDL_RemoveTimer(m_MouseMoveTimer);
SDL_RemoveTimer(m_LongPressTimer);
SDL_RemoveTimer(m_LeftButtonReleaseTimer);
SDL_RemoveTimer(m_RightButtonReleaseTimer);
SDL_RemoveTimer(m_DragTimer);
#if !SDL_VERSION_ATLEAST(2, 0, 9)
SDL_QuitSubSystem(SDL_INIT_HAPTIC);
SDL_assert(!SDL_WasInit(SDL_INIT_HAPTIC));
#endif
SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER);
SDL_assert(!SDL_WasInit(SDL_INIT_GAMECONTROLLER));
SDL_QuitSubSystem(SDL_INIT_JOYSTICK);
SDL_assert(!SDL_WasInit(SDL_INIT_JOYSTICK));
// Return background event handling to off
SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "0");
#ifdef STEAM_LINK
// Hide SDL's cursor on Steam Link after quitting the stream.
// FIXME: We should also do this for other situations where SDL
// and Qt will draw their own mouse cursors like KMSDRM or RPi
// video backends.
SDL_ShowCursor(SDL_DISABLE);
#endif
}
void SdlInputHandler::setWindow(SDL_Window *window)
{
m_Window = window;
}
void SdlInputHandler::raiseAllKeys()
{
if (m_KeysDown.isEmpty()) {
return;
}
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Raising %d keys",
m_KeysDown.count());
for (auto keyDown : m_KeysDown) {
LiSendKeyboardEvent(keyDown, KEY_ACTION_UP, 0);
}
m_KeysDown.clear();
}
void SdlInputHandler::notifyMouseLeave()
{
#ifdef Q_OS_WIN32
// SDL on Windows doesn't send the mouse button up until the mouse re-enters the window
// after leaving it. This breaks some of the Aero snap gestures, so we'll fake it here.
if (m_AbsoluteMouseMode && isCaptureActive()) {
// NB: Not using SDL_GetGlobalMouseState() because we want our state not the system's
Uint32 mouseState = SDL_GetMouseState(nullptr, nullptr);
for (Uint32 button = SDL_BUTTON_LEFT; button <= SDL_BUTTON_X2; button++) {
if (mouseState & SDL_BUTTON(button)) {
m_PendingMouseLeaveButtonUp = button;
break;
}
}
}
#endif
}
void SdlInputHandler::notifyFocusLost()
{
// Release mouse cursor when another window is activated (e.g. by using ALT+TAB).
// This lets user to interact with our window's title bar and with the buttons in it.
// Doing this while the window is full-screen breaks the transition out of FS
// (desktop and exclusive), so we must check for that before releasing mouse capture.
if (!(SDL_GetWindowFlags(m_Window) & SDL_WINDOW_FULLSCREEN) && !m_AbsoluteMouseMode) {
setCaptureActive(false);
}
// Raise all keys that are currently pressed. If we don't do this, certain keys
// used in shortcuts that cause focus loss (such as Alt+Tab) may get stuck down.
raiseAllKeys();
}
bool SdlInputHandler::isCaptureActive()
{
if (SDL_GetRelativeMouseMode()) {
return true;
}
// Some platforms don't support SDL_SetRelativeMouseMode
return m_FakeCaptureActive;
}
void SdlInputHandler::setCaptureActive(bool active)
{
if (active) {
// If we're in full-screen exclusive mode, grab the cursor so it can't accidentally leave our window.
if ((SDL_GetWindowFlags(m_Window) & SDL_WINDOW_FULLSCREEN_DESKTOP) == SDL_WINDOW_FULLSCREEN) {
SDL_SetWindowGrab(m_Window, SDL_TRUE);
}
if (!m_AbsoluteMouseMode) {
// If our window is occluded when mouse is captured, the mouse may
// get stuck on top of the occluding window and not be properly
// captured. We can avoid this by raising our window before we
// capture the mouse.
SDL_RaiseWindow(m_Window);
}
// If we're in relative mode, try to activate SDL's relative mouse mode
if (m_AbsoluteMouseMode || SDL_SetRelativeMouseMode(SDL_TRUE) < 0) {
// Relative mouse mode didn't work or was disabled, so we'll just hide the cursor
SDL_ShowCursor(SDL_DISABLE);
m_FakeCaptureActive = true;
}
}
else {
if (m_FakeCaptureActive) {
// Display the cursor again
SDL_ShowCursor(SDL_ENABLE);
m_FakeCaptureActive = false;
}
else {
SDL_SetRelativeMouseMode(SDL_FALSE);
}
// Allow the cursor to leave the bounds of our window again.
SDL_SetWindowGrab(m_Window, SDL_FALSE);
}
}
void SdlInputHandler::handleTouchFingerEvent(SDL_TouchFingerEvent* event)
{
#if SDL_VERSION_ATLEAST(2, 0, 10)
if (SDL_GetTouchDeviceType(event->touchId) != SDL_TOUCH_DEVICE_DIRECT) {
// Ignore anything that isn't a touchscreen. We may get callbacks
// for trackpads, but we want to handle those in the mouse path.
return;
}
#elif defined(Q_OS_DARWIN)
// SDL2 sends touch events from trackpads by default on
// macOS. This totally screws our actual mouse handling,
// so we must explicitly ignore touch events on macOS
// until SDL 2.0.10 where we have SDL_GetTouchDeviceType()
// to tell them apart.
return;
#endif
if (m_AbsoluteTouchMode) {
handleAbsoluteFingerEvent(event);
}
else {
handleRelativeFingerEvent(event);
}
}
-420
View File
@@ -1,420 +0,0 @@
#include "streaming/session.h"
#include <Limelight.h>
#include <SDL.h>
#include <QGuiApplication>
// Until we can fully capture these on all platforms (without conflicting with
// OS-provided shortcuts), we should avoid passing them through to the host.
//#define ENABLE_META
#define VK_0 0x30
#define VK_A 0x41
// These are real Windows VK_* codes
#ifndef VK_F1
#define VK_F1 0x70
#define VK_F13 0x7C
#define VK_NUMPAD0 0x60
#endif
void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
{
short keyCode;
char modifiers;
// Check for our special key combos
if ((event->state == SDL_PRESSED) &&
(event->keysym.mod & KMOD_CTRL) &&
(event->keysym.mod & KMOD_ALT) &&
(event->keysym.mod & KMOD_SHIFT)) {
// First we test the SDLK combos for matches,
// that way we ensure that latin keyboard users
// can match to the key they see on their keyboards.
// If nothing matches that, we'll then go on to
// checking scancodes so non-latin keyboard users
// can have working hotkeys (though possibly in
// odd positions). We must do all SDLK tests before
// any scancode tests to avoid issues in cases
// where the SDLK for one shortcut collides with
// the scancode of another.
// Check for quit combo (Ctrl+Alt+Shift+Q)
if (event->keysym.sym == SDLK_q) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Detected quit key combo (SDLK)");
// Push a quit event to the main loop
SDL_Event event;
event.type = SDL_QUIT;
event.quit.timestamp = SDL_GetTicks();
SDL_PushEvent(&event);
return;
}
// Check for the unbind combo (Ctrl+Alt+Shift+Z) unless on EGLFS which has no window manager
else if (event->keysym.sym == SDLK_z && QGuiApplication::platformName() != "eglfs") {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Detected mouse capture toggle combo (SDLK)");
// Stop handling future input
setCaptureActive(!isCaptureActive());
// Force raise all keys to ensure they aren't stuck,
// since we won't get their key up events.
raiseAllKeys();
return;
}
// Check for the mouse mode combo (Ctrl+Alt+Shift+M) unless on EGLFS which has no window manager
else if (event->keysym.sym == SDLK_m && QGuiApplication::platformName() != "eglfs") {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Detected mouse mode toggle combo (SDLK)");
// Uncapture input
setCaptureActive(false);
// Toggle mouse mode
m_AbsoluteMouseMode = !m_AbsoluteMouseMode;
// Recapture input
setCaptureActive(true);
return;
}
else if (event->keysym.sym == SDLK_x && QGuiApplication::platformName() != "eglfs") {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Detected full-screen toggle combo (SDLK)");
Session::s_ActiveSession->toggleFullscreen();
// Force raise all keys just be safe across this full-screen/windowed
// transition just in case key events get lost.
raiseAllKeys();
return;
}
// Check for overlay combo (Ctrl+Alt+Shift+S)
else if (event->keysym.sym == SDLK_s) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Detected stats toggle combo (SDLK)");
// Toggle the stats overlay
Session::get()->getOverlayManager().setOverlayState(Overlay::OverlayDebug,
!Session::get()->getOverlayManager().isOverlayEnabled(Overlay::OverlayDebug));
// Force raise all keys just be safe across this full-screen/windowed
// transition just in case key events get lost.
raiseAllKeys();
return;
}
// Check for quit combo (Ctrl+Alt+Shift+Q)
else if (event->keysym.scancode == SDL_SCANCODE_Q) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Detected quit key combo (scancode)");
// Push a quit event to the main loop
SDL_Event event;
event.type = SDL_QUIT;
event.quit.timestamp = SDL_GetTicks();
SDL_PushEvent(&event);
return;
}
// Check for the unbind combo (Ctrl+Alt+Shift+Z)
else if (event->keysym.scancode == SDL_SCANCODE_Z && QGuiApplication::platformName() != "eglfs") {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Detected mouse capture toggle combo (scancode)");
// Stop handling future input
setCaptureActive(!isCaptureActive());
// Force raise all keys to ensure they aren't stuck,
// since we won't get their key up events.
raiseAllKeys();
return;
}
// Check for the full-screen combo (Ctrl+Alt+Shift+X) unless on EGLFS which has no window manager
else if (event->keysym.scancode == SDL_SCANCODE_X && QGuiApplication::platformName() != "eglfs") {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Detected full-screen toggle combo (scancode)");
Session::s_ActiveSession->toggleFullscreen();
// Force raise all keys just be safe across this full-screen/windowed
// transition just in case key events get lost.
raiseAllKeys();
return;
}
// Check for the mouse mode toggle combo (Ctrl+Alt+Shift+M) unless on EGLFS which has no window manager
else if (event->keysym.scancode == SDL_SCANCODE_M && QGuiApplication::platformName() != "eglfs") {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Detected mouse mode toggle combo (scancode)");
// Uncapture input
setCaptureActive(false);
// Toggle mouse mode
m_AbsoluteMouseMode = !m_AbsoluteMouseMode;
// Recapture input
setCaptureActive(true);
return;
}
else if (event->keysym.scancode == SDL_SCANCODE_S) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Detected stats toggle combo (scancode)");
// Toggle the stats overlay
Session::get()->getOverlayManager().setOverlayState(Overlay::OverlayDebug,
!Session::get()->getOverlayManager().isOverlayEnabled(Overlay::OverlayDebug));
// Force raise all keys just be safe across this full-screen/windowed
// transition just in case key events get lost.
raiseAllKeys();
return;
}
}
if (event->repeat) {
// Ignore repeat key down events
SDL_assert(event->state == SDL_PRESSED);
return;
}
// Set modifier flags
modifiers = 0;
if (event->keysym.mod & KMOD_CTRL) {
modifiers |= MODIFIER_CTRL;
}
if (event->keysym.mod & KMOD_ALT) {
modifiers |= MODIFIER_ALT;
}
if (event->keysym.mod & KMOD_SHIFT) {
modifiers |= MODIFIER_SHIFT;
}
#ifdef ENABLE_META
if (event->keysym.mod & KMOD_GUI) {
modifiers |= MODIFIER_META;
}
#endif
// Set keycode. We explicitly use scancode here because GFE will try to correct
// for AZERTY layouts on the host but it depends on receiving VK_ values matching
// a QWERTY layout to work.
if (event->keysym.scancode >= SDL_SCANCODE_1 && event->keysym.scancode <= SDL_SCANCODE_9) {
// SDL defines SDL_SCANCODE_0 > SDL_SCANCODE_9, so we need to handle that manually
keyCode = (event->keysym.scancode - SDL_SCANCODE_1) + VK_0 + 1;
}
else if (event->keysym.scancode >= SDL_SCANCODE_A && event->keysym.scancode <= SDL_SCANCODE_Z) {
keyCode = (event->keysym.scancode - SDL_SCANCODE_A) + VK_A;
}
else if (event->keysym.scancode >= SDL_SCANCODE_F1 && event->keysym.scancode <= SDL_SCANCODE_F12) {
keyCode = (event->keysym.scancode - SDL_SCANCODE_F1) + VK_F1;
}
else if (event->keysym.scancode >= SDL_SCANCODE_F13 && event->keysym.scancode <= SDL_SCANCODE_F24) {
keyCode = (event->keysym.scancode - SDL_SCANCODE_F13) + VK_F13;
}
else if (event->keysym.scancode >= SDL_SCANCODE_KP_1 && event->keysym.scancode <= SDL_SCANCODE_KP_9) {
// SDL defines SDL_SCANCODE_KP_0 > SDL_SCANCODE_KP_9, so we need to handle that manually
keyCode = (event->keysym.scancode - SDL_SCANCODE_KP_1) + VK_NUMPAD0 + 1;
}
else {
switch (event->keysym.scancode) {
case SDL_SCANCODE_BACKSPACE:
keyCode = 0x08;
break;
case SDL_SCANCODE_TAB:
keyCode = 0x09;
break;
case SDL_SCANCODE_CLEAR:
keyCode = 0x0C;
break;
case SDL_SCANCODE_KP_ENTER: // FIXME: Is this correct?
case SDL_SCANCODE_RETURN:
keyCode = 0x0D;
break;
case SDL_SCANCODE_PAUSE:
keyCode = 0x13;
break;
case SDL_SCANCODE_CAPSLOCK:
keyCode = 0x14;
break;
case SDL_SCANCODE_ESCAPE:
keyCode = 0x1B;
break;
case SDL_SCANCODE_SPACE:
keyCode = 0x20;
break;
case SDL_SCANCODE_PAGEUP:
keyCode = 0x21;
break;
case SDL_SCANCODE_PAGEDOWN:
keyCode = 0x22;
break;
case SDL_SCANCODE_END:
keyCode = 0x23;
break;
case SDL_SCANCODE_HOME:
keyCode = 0x24;
break;
case SDL_SCANCODE_LEFT:
keyCode = 0x25;
break;
case SDL_SCANCODE_UP:
keyCode = 0x26;
break;
case SDL_SCANCODE_RIGHT:
keyCode = 0x27;
break;
case SDL_SCANCODE_DOWN:
keyCode = 0x28;
break;
case SDL_SCANCODE_SELECT:
keyCode = 0x29;
break;
case SDL_SCANCODE_EXECUTE:
keyCode = 0x2B;
break;
case SDL_SCANCODE_PRINTSCREEN:
keyCode = 0x2C;
break;
case SDL_SCANCODE_INSERT:
keyCode = 0x2D;
break;
case SDL_SCANCODE_DELETE:
keyCode = 0x2E;
break;
case SDL_SCANCODE_HELP:
keyCode = 0x2F;
break;
case SDL_SCANCODE_KP_0:
// See comment above about why we only handle SDL_SCANCODE_KP_0 here
keyCode = VK_NUMPAD0;
break;
case SDL_SCANCODE_0:
// See comment above about why we only handle SDL_SCANCODE_0 here
keyCode = VK_0;
break;
case SDL_SCANCODE_KP_MULTIPLY:
keyCode = 0x6A;
break;
case SDL_SCANCODE_KP_PLUS:
keyCode = 0x6B;
break;
case SDL_SCANCODE_KP_COMMA:
keyCode = 0x6C;
break;
case SDL_SCANCODE_KP_MINUS:
keyCode = 0x6D;
break;
case SDL_SCANCODE_KP_PERIOD:
keyCode = 0x6E;
break;
case SDL_SCANCODE_KP_DIVIDE:
keyCode = 0x6F;
break;
case SDL_SCANCODE_NUMLOCKCLEAR:
keyCode = 0x90;
break;
case SDL_SCANCODE_SCROLLLOCK:
keyCode = 0x91;
break;
case SDL_SCANCODE_LSHIFT:
keyCode = 0xA0;
break;
case SDL_SCANCODE_RSHIFT:
keyCode = 0xA1;
break;
case SDL_SCANCODE_LCTRL:
keyCode = 0xA2;
break;
case SDL_SCANCODE_RCTRL:
keyCode = 0xA3;
break;
case SDL_SCANCODE_LALT:
keyCode = 0xA4;
break;
case SDL_SCANCODE_RALT:
keyCode = 0xA5;
break;
#ifdef ENABLE_META
case SDL_SCANCODE_LGUI:
keyCode = 0x5B;
break;
case SDL_SCANCODE_RGUI:
keyCode = 0x5C;
break;
#endif
case SDL_SCANCODE_AC_BACK:
keyCode = 0xA6;
break;
case SDL_SCANCODE_AC_FORWARD:
keyCode = 0xA7;
break;
case SDL_SCANCODE_AC_REFRESH:
keyCode = 0xA8;
break;
case SDL_SCANCODE_AC_STOP:
keyCode = 0xA9;
break;
case SDL_SCANCODE_AC_SEARCH:
keyCode = 0xAA;
break;
case SDL_SCANCODE_AC_BOOKMARKS:
keyCode = 0xAB;
break;
case SDL_SCANCODE_AC_HOME:
keyCode = 0xAC;
break;
case SDL_SCANCODE_SEMICOLON:
keyCode = 0xBA;
break;
case SDL_SCANCODE_EQUALS:
keyCode = 0xBB;
break;
case SDL_SCANCODE_COMMA:
keyCode = 0xBC;
break;
case SDL_SCANCODE_MINUS:
keyCode = 0xBD;
break;
case SDL_SCANCODE_PERIOD:
keyCode = 0xBE;
break;
case SDL_SCANCODE_SLASH:
keyCode = 0xBF;
break;
case SDL_SCANCODE_GRAVE:
keyCode = 0xC0;
break;
case SDL_SCANCODE_LEFTBRACKET:
keyCode = 0xDB;
break;
case SDL_SCANCODE_BACKSLASH:
keyCode = 0xDC;
break;
case SDL_SCANCODE_RIGHTBRACKET:
keyCode = 0xDD;
break;
case SDL_SCANCODE_APOSTROPHE:
keyCode = 0xDE;
break;
case SDL_SCANCODE_NONUSBACKSLASH:
keyCode = 0xE2;
break;
default:
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Unhandled button event: %d",
event->keysym.scancode);
return;
}
}
// Track the key state so we always know which keys are down
if (event->state == SDL_PRESSED) {
m_KeysDown.insert(keyCode);
}
else {
m_KeysDown.remove(keyCode);
}
LiSendKeyboardEvent(keyCode,
event->state == SDL_PRESSED ?
KEY_ACTION_DOWN : KEY_ACTION_UP,
modifiers);
}
-154
View File
@@ -1,154 +0,0 @@
#include "input.h"
#include <Limelight.h>
#include <SDL.h>
#include "streaming/streamutils.h"
void SdlInputHandler::handleMouseButtonEvent(SDL_MouseButtonEvent* event)
{
int button;
if (event->which == SDL_TOUCH_MOUSEID) {
// Ignore synthetic mouse events
return;
}
else if (!isCaptureActive()) {
if (event->button == SDL_BUTTON_LEFT && event->state == SDL_RELEASED) {
// Capture the mouse again if clicked when unbound.
// We start capture on left button released instead of
// pressed to avoid sending an errant mouse button released
// event to the host when clicking into our window (since
// the pressed event was consumed by this code).
setCaptureActive(true);
}
// Not capturing
return;
}
switch (event->button)
{
case SDL_BUTTON_LEFT:
button = BUTTON_LEFT;
break;
case SDL_BUTTON_MIDDLE:
button = BUTTON_MIDDLE;
break;
case SDL_BUTTON_RIGHT:
button = BUTTON_RIGHT;
break;
case SDL_BUTTON_X1:
button = BUTTON_X1;
break;
case SDL_BUTTON_X2:
button = BUTTON_X2;
break;
default:
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Unhandled button event: %d",
event->button);
return;
}
LiSendMouseButtonEvent(event->state == SDL_PRESSED ?
BUTTON_ACTION_PRESS :
BUTTON_ACTION_RELEASE,
button);
}
void SdlInputHandler::handleMouseMotionEvent(SDL_MouseMotionEvent* event)
{
if (!isCaptureActive()) {
// Not capturing
return;
}
else if (event->which == SDL_TOUCH_MOUSEID) {
// Ignore synthetic mouse events
return;
}
if (m_AbsoluteMouseMode) {
SDL_Rect src, dst;
src.x = src.y = 0;
src.w = m_StreamWidth;
src.h = m_StreamHeight;
dst.x = dst.y = 0;
SDL_GetWindowSize(m_Window, &dst.w, &dst.h);
// Use the stream and window sizes to determine the video region
StreamUtils::scaleSourceToDestinationSurface(&src, &dst);
// Clamp motion to the video region
short x = qMin(qMax(event->x - dst.x, 0), dst.w);
short y = qMin(qMax(event->y - dst.y, 0), dst.h);
// Send the mouse position update
LiSendMousePositionEvent(x, y, dst.w, dst.h);
}
else {
// Batch until the next mouse polling window or we'll get awful
// input lag everything except GFE 3.14 and 3.15.
SDL_AtomicAdd(&m_MouseDeltaX, event->xrel);
SDL_AtomicAdd(&m_MouseDeltaY, event->yrel);
}
}
void SdlInputHandler::handleMouseWheelEvent(SDL_MouseWheelEvent* event)
{
if (!isCaptureActive()) {
// Not capturing
return;
}
else if (event->which == SDL_TOUCH_MOUSEID) {
// Ignore synthetic mouse events
return;
}
if (event->y != 0) {
LiSendScrollEvent((signed char)event->y);
}
}
Uint32 SdlInputHandler::mouseMoveTimerCallback(Uint32 interval, void *param)
{
auto me = reinterpret_cast<SdlInputHandler*>(param);
short deltaX = (short)SDL_AtomicSet(&me->m_MouseDeltaX, 0);
short deltaY = (short)SDL_AtomicSet(&me->m_MouseDeltaY, 0);
if (deltaX != 0 || deltaY != 0) {
LiSendMouseMoveEvent(deltaX, deltaY);
}
#ifdef Q_OS_WIN32
// See comment in SdlInputHandler::notifyMouseLeave()
if (me->m_AbsoluteMouseMode && me->m_PendingMouseLeaveButtonUp != 0 && me->isCaptureActive()) {
int mouseX, mouseY;
int windowX, windowY;
Uint32 mouseState = SDL_GetGlobalMouseState(&mouseX, &mouseY);
SDL_GetWindowPosition(me->m_Window, &windowX, &windowY);
// If the button is now up, send the synthetic mouse up event
if ((mouseState & SDL_BUTTON(me->m_PendingMouseLeaveButtonUp)) == 0) {
SDL_Event event;
event.button.type = SDL_MOUSEBUTTONUP;
event.button.timestamp = SDL_GetTicks();
event.button.windowID = SDL_GetWindowID(me->m_Window);
event.button.which = 0;
event.button.button = me->m_PendingMouseLeaveButtonUp;
event.button.state = SDL_RELEASED;
event.button.clicks = 1;
event.button.x = mouseX - windowX;
event.button.y = mouseY - windowY;
SDL_PushEvent(&event);
me->m_PendingMouseLeaveButtonUp = 0;
}
}
#endif
return interval;
}
-175
View File
@@ -1,175 +0,0 @@
#include "input.h"
#include <Limelight.h>
#include <SDL.h>
#include <QtMath>
// How long the mouse button will be pressed for a tap to click gesture
#define TAP_BUTTON_RELEASE_DELAY 100
// How long the fingers must be stationary to start a drag
#define DRAG_ACTIVATION_DELAY 650
// How far the finger can move before it cancels a drag or tap
#define DEAD_ZONE_DELTA 0.01f
Uint32 SdlInputHandler::releaseLeftButtonTimerCallback(Uint32, void*)
{
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
return 0;
}
Uint32 SdlInputHandler::releaseRightButtonTimerCallback(Uint32, void*)
{
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_RIGHT);
return 0;
}
Uint32 SdlInputHandler::dragTimerCallback(Uint32, void *param)
{
auto me = reinterpret_cast<SdlInputHandler*>(param);
// Check how many fingers are down now to decide
// which button to hold down
if (me->m_NumFingersDown == 2) {
me->m_DragButton = BUTTON_RIGHT;
}
else if (me->m_NumFingersDown == 1) {
me->m_DragButton = BUTTON_LEFT;
}
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, me->m_DragButton);
return 0;
}
void SdlInputHandler::handleRelativeFingerEvent(SDL_TouchFingerEvent* event)
{
int fingerIndex = -1;
// Observations on Windows 10: x and y appear to be relative to 0,0 of the window client area.
// Although SDL documentation states they are 0.0 - 1.0 float values, they can actually be higher
// or lower than those values as touch events continue for touches started within the client area that
// leave the client area during a drag motion.
// dx and dy are deltas from the last touch event, not the first touch down.
// Determine the index of this finger using our list
// of fingers that are currently active on screen.
// This is also required to handle finger up which
// where the finger will not be in SDL_GetTouchFinger()
// anymore.
if (event->type != SDL_FINGERDOWN) {
for (int i = 0; i < MAX_FINGERS; i++) {
if (event->fingerId == m_TouchDownEvent[i].fingerId) {
fingerIndex = i;
break;
}
}
}
else {
// Resolve the new finger by determining the ID of each
// finger on the display.
int numTouchFingers = SDL_GetNumTouchFingers(event->touchId);
for (int i = 0; i < numTouchFingers; i++) {
SDL_Finger* finger = SDL_GetTouchFinger(event->touchId, i);
SDL_assert(finger != nullptr);
if (finger != nullptr) {
if (finger->id == event->fingerId) {
fingerIndex = i;
break;
}
}
}
}
if (fingerIndex < 0 || fingerIndex >= MAX_FINGERS) {
// Too many fingers
return;
}
// Handle cursor motion based on the position of the
// primary finger on screen
if (fingerIndex == 0) {
// The event x and y values are relative to our window width
// and height. However, we want to scale them to be relative
// to the host resolution. Fortunately this is easy since we
// already have normalized values. We'll just multiply them
// by the stream dimensions to get real X and Y values rather
// than the client window dimensions.
short deltaX = static_cast<short>(event->dx * m_StreamWidth);
short deltaY = static_cast<short>(event->dy * m_StreamHeight);
if (deltaX != 0 || deltaY != 0) {
LiSendMouseMoveEvent(deltaX, deltaY);
}
}
// Start a drag timer when primary or secondary
// fingers go down
if (event->type == SDL_FINGERDOWN &&
(fingerIndex == 0 || fingerIndex == 1)) {
SDL_RemoveTimer(m_DragTimer);
m_DragTimer = SDL_AddTimer(DRAG_ACTIVATION_DELAY,
dragTimerCallback,
this);
}
if (event->type == SDL_FINGERMOTION) {
// If it's outside the deadzone delta, cancel drags and taps
if (qSqrt(qPow(event->x - m_TouchDownEvent[fingerIndex].x, 2) +
qPow(event->y - m_TouchDownEvent[fingerIndex].y, 2)) > DEAD_ZONE_DELTA) {
SDL_RemoveTimer(m_DragTimer);
m_DragTimer = 0;
// This effectively cancels the tap logic below
m_TouchDownEvent[fingerIndex].timestamp = 0;
}
}
if (event->type == SDL_FINGERUP) {
// Cancel the drag timer on finger up
SDL_RemoveTimer(m_DragTimer);
m_DragTimer = 0;
// Release any drag
if (m_DragButton != 0) {
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, m_DragButton);
m_DragButton = 0;
}
// 2 finger tap
else if (event->timestamp - m_TouchDownEvent[1].timestamp < 250) {
// Zero timestamp of the primary finger to ensure we won't
// generate a left click if the primary finger comes up soon.
m_TouchDownEvent[0].timestamp = 0;
// Press down the right mouse button
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_RIGHT);
// Queue a timer to release it in 100 ms
SDL_RemoveTimer(m_RightButtonReleaseTimer);
m_RightButtonReleaseTimer = SDL_AddTimer(TAP_BUTTON_RELEASE_DELAY,
releaseRightButtonTimerCallback,
nullptr);
}
// 1 finger tap
else if (event->timestamp - m_TouchDownEvent[0].timestamp < 250) {
// Press down the left mouse button
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT);
// Queue a timer to release it in 100 ms
SDL_RemoveTimer(m_LeftButtonReleaseTimer);
m_LeftButtonReleaseTimer = SDL_AddTimer(TAP_BUTTON_RELEASE_DELAY,
releaseLeftButtonTimerCallback,
nullptr);
}
}
m_NumFingersDown = SDL_GetNumTouchFingers(event->touchId);
if (event->type == SDL_FINGERDOWN) {
m_TouchDownEvent[fingerIndex] = *event;
}
else if (event->type == SDL_FINGERUP) {
m_TouchDownEvent[fingerIndex] = {};
}
}
+220 -411
View File
File diff suppressed because it is too large. Load diff
+18 -43
View File
@@ -4,11 +4,11 @@
#include <Limelight.h> #include <Limelight.h>
#include <opus_multistream.h> #include <opus_multistream.h>
#include "backend/computermanager.h"
#include "settings/streamingpreferences.h" #include "settings/streamingpreferences.h"
#include "input/input.h" #include "input.h"
#include "video/decoder.h" #include "video/decoder.h"
#include "audio/renderers/renderer.h" #include "audio/renderers/renderer.h"
#include "video/overlaymanager.h"
class Session : public QObject class Session : public QObject
{ {
@@ -25,23 +25,17 @@ public:
Q_INVOKABLE void exec(int displayOriginX, int displayOriginY); Q_INVOKABLE void exec(int displayOriginX, int displayOriginY);
static static
void getDecoderInfo(SDL_Window* window, bool isHardwareDecodeAvailable(StreamingPreferences::VideoDecoderSelection vds,
bool& isHardwareAccelerated, bool& isFullScreenOnly, QSize& maxResolution); int videoFormat, int width, int height, int frameRate);
static Session* get() static
{ int getDecoderCapabilities(StreamingPreferences::VideoDecoderSelection vds,
return s_ActiveSession; int videoFormat, int width, int height, int frameRate);
}
Overlay::OverlayManager& getOverlayManager()
{
return m_OverlayManager;
}
signals: signals:
void stageStarting(QString stage); void stageStarting(QString stage);
void stageFailed(QString stage, int errorCode); void stageFailed(QString stage, long errorCode);
void connectionStarted(); void connectionStarted();
@@ -54,59 +48,45 @@ signals:
void sessionFinished(); void sessionFinished();
private: private:
bool initialize(); void initialize();
bool validateLaunch(SDL_Window* testWindow); bool validateLaunch();
void emitLaunchWarning(QString text); void emitLaunchWarning(QString text);
bool populateDecoderProperties(SDL_Window* window); int getDecoderCapabilities();
IAudioRenderer* createAudioRenderer(const POPUS_MULTISTREAM_CONFIGURATION opusConfig); IAudioRenderer* createAudioRenderer();
int detectAudioConfiguration();
bool testAudio(int audioConfiguration); bool testAudio(int audioConfiguration);
int getAudioRendererCapabilities(int audioConfiguration);
void getWindowDimensions(int& x, int& y, void getWindowDimensions(int& x, int& y,
int& width, int& height); int& width, int& height);
void toggleFullscreen(); void toggleFullscreen();
void notifyMouseEmulationMode(bool enabled);
void updateOptimalWindowDisplayMode(); void updateOptimalWindowDisplayMode();
static
bool isHardwareDecodeAvailable(SDL_Window* window,
StreamingPreferences::VideoDecoderSelection vds,
int videoFormat, int width, int height, int frameRate);
static static
bool chooseDecoder(StreamingPreferences::VideoDecoderSelection vds, bool chooseDecoder(StreamingPreferences::VideoDecoderSelection vds,
SDL_Window* window, int videoFormat, int width, int height, SDL_Window* window, int videoFormat, int width, int height,
int frameRate, bool enableVsync, bool enableFramePacing, int frameRate, bool enableVsync, bool enableFramePacing,
bool testOnly,
IVideoDecoder*& chosenDecoder); IVideoDecoder*& chosenDecoder);
static static
void clStageStarting(int stage); void clStageStarting(int stage);
static static
void clStageFailed(int stage, int errorCode); void clStageFailed(int stage, long errorCode);
static static
void clConnectionTerminated(int errorCode); void clConnectionTerminated(long errorCode);
static static
void clLogMessage(const char* format, ...); void clLogMessage(const char* format, ...);
static
void clRumble(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor);
static
void clConnectionStatusUpdate(int connectionStatus);
static static
int arInit(int audioConfiguration, int arInit(int audioConfiguration,
const POPUS_MULTISTREAM_CONFIGURATION opusConfig, const POPUS_MULTISTREAM_CONFIGURATION opusConfig,
@@ -130,7 +110,6 @@ private:
StreamingPreferences* m_Preferences; StreamingPreferences* m_Preferences;
STREAM_CONFIGURATION m_StreamConfig; STREAM_CONFIGURATION m_StreamConfig;
DECODER_RENDERER_CALLBACKS m_VideoCallbacks; DECODER_RENDERER_CALLBACKS m_VideoCallbacks;
AUDIO_RENDERER_CALLBACKS m_AudioCallbacks;
NvComputer* m_Computer; NvComputer* m_Computer;
NvApp m_App; NvApp m_App;
SDL_Window* m_Window; SDL_Window* m_Window;
@@ -143,9 +122,6 @@ private:
int m_DisplayOriginY; int m_DisplayOriginY;
bool m_PendingWindowedTransition; bool m_PendingWindowedTransition;
bool m_UnexpectedTermination; bool m_UnexpectedTermination;
SdlInputHandler* m_InputHandler;
SDL_SpinLock m_InputHandlerLock;
int m_MouseEmulationRefCount;
int m_ActiveVideoFormat; int m_ActiveVideoFormat;
int m_ActiveVideoWidth; int m_ActiveVideoWidth;
@@ -153,13 +129,12 @@ private:
int m_ActiveVideoFrameRate; int m_ActiveVideoFrameRate;
OpusMSDecoder* m_OpusDecoder; OpusMSDecoder* m_OpusDecoder;
short m_OpusDecodeBuffer[MAX_CHANNELS * SAMPLES_PER_FRAME];
IAudioRenderer* m_AudioRenderer; IAudioRenderer* m_AudioRenderer;
OPUS_MULTISTREAM_CONFIGURATION m_AudioConfig; OPUS_MULTISTREAM_CONFIGURATION m_AudioConfig;
int m_AudioSampleCount; int m_AudioSampleCount;
Uint32 m_DropAudioEndTime;
Overlay::OverlayManager m_OverlayManager;
static AUDIO_RENDERER_CALLBACKS k_AudioCallbacks;
static CONNECTION_LISTENER_CALLBACKS k_ConnCallbacks; static CONNECTION_LISTENER_CALLBACKS k_ConnCallbacks;
static Session* s_ActiveSession; static Session* s_ActiveSession;
static QSemaphore s_ActiveSessionSemaphore; static QSemaphore s_ActiveSessionSemaphore;
+4 -2
View File
@@ -12,12 +12,14 @@ void StreamUtils::scaleSourceToDestinationSurface(SDL_Rect* src, SDL_Rect* dst)
int dstW = dst->h * src->w / src->h; int dstW = dst->h * src->w / src->h;
if (dstH > dst->h) { if (dstH > dst->h) {
dst->x += (dst->w - dstW) / 2; dst->y = 0;
dst->x = (dst->w - dstW) / 2;
dst->w = dstW; dst->w = dstW;
SDL_assert(dst->w * src->h / src->w <= dst->h); SDL_assert(dst->w * src->h / src->w <= dst->h);
} }
else { else {
dst->y += (dst->h - dstH) / 2; dst->x = 0;
dst->y = (dst->h - dstH) / 2;
dst->h = dstH; dst->h = dstH;
SDL_assert(dst->h * src->w / src->h <= dst->w); SDL_assert(dst->h * src->w / src->h <= dst->w);
} }
+23 -19
View File
@@ -12,41 +12,45 @@ typedef struct _VIDEO_STATS {
uint32_t receivedFrames; uint32_t receivedFrames;
uint32_t decodedFrames; uint32_t decodedFrames;
uint32_t renderedFrames; uint32_t renderedFrames;
uint32_t totalFrames;
uint32_t networkDroppedFrames; uint32_t networkDroppedFrames;
uint32_t pacerDroppedFrames; uint32_t pacerDroppedFrames;
uint32_t totalReassemblyTime; uint32_t totalReassemblyTime;
uint32_t totalDecodeTime; uint32_t totalDecodeTime;
uint32_t totalPacerTime; uint32_t totalPacerTime;
uint32_t totalRenderTime; uint32_t totalRenderTime;
float totalFps;
float receivedFps; float receivedFps;
float decodedFps; float decodedFps;
float renderedFps; float renderedFps;
uint32_t measurementStartTimestamp; uint32_t measurementStartTimestamp;
} VIDEO_STATS, *PVIDEO_STATS; } VIDEO_STATS, *PVIDEO_STATS;
typedef struct _DECODER_PARAMETERS {
SDL_Window* window;
StreamingPreferences::VideoDecoderSelection vds;
int videoFormat;
int width;
int height;
int frameRate;
bool enableVsync;
bool enableFramePacing;
} DECODER_PARAMETERS, *PDECODER_PARAMETERS;
class IVideoDecoder { class IVideoDecoder {
public: public:
virtual ~IVideoDecoder() {} virtual ~IVideoDecoder() {}
virtual bool initialize(PDECODER_PARAMETERS params) = 0; virtual bool initialize(StreamingPreferences::VideoDecoderSelection vds,
SDL_Window* window,
int videoFormat,
int width,
int height,
int frameRate,
bool enableVsync,
bool enableFramePacing) = 0;
virtual bool isHardwareAccelerated() = 0; virtual bool isHardwareAccelerated() = 0;
virtual bool isAlwaysFullScreen() = 0;
virtual int getDecoderCapabilities() = 0; virtual int getDecoderCapabilities() = 0;
virtual int getDecoderColorspace() = 0;
virtual QSize getDecoderMaxResolution() = 0;
virtual int submitDecodeUnit(PDECODE_UNIT du) = 0; virtual int submitDecodeUnit(PDECODE_UNIT du) = 0;
virtual void renderFrameOnMainThread() = 0; virtual void renderFrame(SDL_UserEvent* event) = 0;
virtual void dropFrame(SDL_UserEvent* event) = 0;
virtual void queueFrame(void* data1 = nullptr,
void* data2 = nullptr)
{
SDL_Event event;
event.type = SDL_USEREVENT;
event.user.code = SDL_CODE_FRAME_READY;
event.user.data1 = data1;
event.user.data2 = data2;
SDL_PushEvent(&event);
}
}; };
@@ -1,57 +0,0 @@
#include "cuda.h"
CUDARenderer::CUDARenderer()
: m_HwContext(nullptr)
{
}
CUDARenderer::~CUDARenderer()
{
if (m_HwContext != nullptr) {
av_buffer_unref(&m_HwContext);
}
}
bool CUDARenderer::initialize(PDECODER_PARAMETERS)
{
int err;
err = av_hwdevice_ctx_create(&m_HwContext, AV_HWDEVICE_TYPE_CUDA, nullptr, nullptr, 0);
if (err != 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"av_hwdevice_ctx_create(CUDA) failed: %d",
err);
return false;
}
return true;
}
bool CUDARenderer::prepareDecoderContext(AVCodecContext* context, AVDictionary**)
{
context->hw_device_ctx = av_buffer_ref(m_HwContext);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Using CUDA accelerated decoder");
return true;
}
void CUDARenderer::renderFrame(AVFrame*)
{
// We only support indirect rendering
SDL_assert(false);
}
bool CUDARenderer::needsTestFrame()
{
return true;
}
bool CUDARenderer::isDirectRenderingSupported()
{
// We only support rendering via SDL read-back
return false;
}
@@ -1,18 +0,0 @@
#pragma once
#include "renderer.h"
class CUDARenderer : public IFFmpegRenderer {
public:
CUDARenderer();
virtual ~CUDARenderer() override;
virtual bool initialize(PDECODER_PARAMETERS) override;
virtual bool prepareDecoderContext(AVCodecContext* context, AVDictionary** options) override;
virtual void renderFrame(AVFrame* frame) override;
virtual bool needsTestFrame() override;
virtual bool isDirectRenderingSupported() override;
private:
AVBufferRef* m_HwContext;
};
@@ -1,279 +0,0 @@
#include "drm.h"
extern "C" {
#include <libavutil/hwcontext_drm.h>
}
#include <libdrm/drm_fourcc.h>
#include <unistd.h>
#include <fcntl.h>
#include "streaming/streamutils.h"
#include "streaming/session.h"
#include <Limelight.h>
DrmRenderer::DrmRenderer()
: m_DrmFd(-1),
m_CrtcId(0),
m_PlaneId(0),
m_CurrentFbId(0)
{
}
DrmRenderer::~DrmRenderer()
{
if (m_CurrentFbId != 0) {
drmModeRmFB(m_DrmFd, m_CurrentFbId);
}
if (m_DrmFd != -1) {
close(m_DrmFd);
}
}
bool DrmRenderer::prepareDecoderContext(AVCodecContext*, AVDictionary**)
{
/* Nothing to do */
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Using DRM renderer");
return true;
}
bool DrmRenderer::initialize(PDECODER_PARAMETERS)
{
const char* device = SDL_getenv("DRM_DEV");
int i;
if (device == nullptr) {
device = "/dev/dri/card0";
}
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Opening DRM device: %s",
device);
m_DrmFd = open(device, O_RDWR | O_CLOEXEC);
if (m_DrmFd < 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to open DRM device: %d",
errno);
return false;
}
drmModeRes* resources = drmModeGetResources(m_DrmFd);
if (resources == nullptr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"drmModeGetResources() failed: %d",
errno);
return false;
}
// Look for a connected connector and get the associated encoder
uint32_t encoderId = 0;
for (i = 0; i < resources->count_connectors && encoderId == 0; i++) {
drmModeConnector* connector = drmModeGetConnector(m_DrmFd, resources->connectors[i]);
if (connector != nullptr) {
if (connector->connection == DRM_MODE_CONNECTED && connector->count_modes > 0) {
encoderId = connector->encoder_id;
}
drmModeFreeConnector(connector);
}
}
if (encoderId == 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"No connected displays found!");
drmModeFreeResources(resources);
return false;
}
// Now find the CRTC from the encoder
m_CrtcId = 0;
for (i = 0; i < resources->count_encoders && m_CrtcId == 0; i++) {
drmModeEncoder* encoder = drmModeGetEncoder(m_DrmFd, resources->encoders[i]);
if (encoder != nullptr) {
if (encoder->encoder_id == encoderId) {
m_CrtcId = encoder->crtc_id;
}
drmModeFreeEncoder(encoder);
}
}
if (m_CrtcId == 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"DRM encoder not found!");
drmModeFreeResources(resources);
return false;
}
int crtcIndex = -1;
for (int i = 0; i < resources->count_crtcs; i++) {
if (resources->crtcs[i] == m_CrtcId) {
drmModeCrtc* crtc = drmModeGetCrtc(m_DrmFd, resources->crtcs[i]);
crtcIndex = i;
m_OutputRect.x = m_OutputRect.y = 0;
m_OutputRect.w = crtc->width;
m_OutputRect.h = crtc->height;
drmModeFreeCrtc(crtc);
break;
}
}
drmModeFreeResources(resources);
if (crtcIndex == -1) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to get CRTC!");
return false;
}
drmSetClientCap(m_DrmFd, DRM_CLIENT_CAP_UNIVERSAL_PLANES, 1);
drmModePlaneRes* planeRes = drmModeGetPlaneResources(m_DrmFd);
if (planeRes == nullptr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"drmGetPlaneResources() failed: %d",
errno);
return false;
}
// Find an NV12 overlay plane to render on
m_PlaneId = 0;
for (uint32_t i = 0; i < planeRes->count_planes && m_PlaneId == 0; i++) {
drmModePlane* plane = drmModeGetPlane(m_DrmFd, planeRes->planes[i]);
if (plane != nullptr) {
bool matchingFormat = false;
for (uint32_t j = 0; j < plane->count_formats; j++) {
if (plane->formats[j] == DRM_FORMAT_NV12) {
matchingFormat = true;
break;
}
}
if (matchingFormat == false) {
drmModeFreePlane(plane);
continue;
}
if ((plane->possible_crtcs & (1 << crtcIndex)) && plane->crtc_id == 0) {
drmModeObjectPropertiesPtr props = drmModeObjectGetProperties(m_DrmFd, planeRes->planes[i], DRM_MODE_OBJECT_PLANE);
if (props != nullptr) {
for (uint32_t j = 0; j < props->count_props && m_PlaneId == 0; j++) {
drmModePropertyPtr prop = drmModeGetProperty(m_DrmFd, props->props[j]);
if (prop != nullptr) {
if (!strcmp(prop->name, "type") && props->prop_values[j] == DRM_PLANE_TYPE_OVERLAY) {
m_PlaneId = plane->plane_id;
}
drmModeFreeProperty(prop);
}
}
drmModeFreeObjectProperties(props);
}
}
drmModeFreePlane(plane);
}
}
drmModeFreePlaneResources(planeRes);
return true;
}
enum AVPixelFormat DrmRenderer::getPreferredPixelFormat(int)
{
// DRM PRIME buffers
return AV_PIX_FMT_DRM_PRIME;
}
int DrmRenderer::getRendererAttributes()
{
// This renderer can only draw in full-screen
return RENDERER_ATTRIBUTE_FULLSCREEN_ONLY;
}
void DrmRenderer::renderFrame(AVFrame* frame)
{
if (frame == nullptr) {
// End of stream - nothing to do for us
return;
}
AVDRMFrameDescriptor* drmFrame = (AVDRMFrameDescriptor*)frame->data[0];
int err;
uint32_t primeHandle;
uint32_t handles[4] = {};
uint32_t pitches[4] = {};
uint32_t offsets[4] = {};
SDL_Rect src, dst;
src.x = src.y = 0;
src.w = frame->width;
src.h = frame->height;
dst = m_OutputRect;
StreamUtils::scaleSourceToDestinationSurface(&src, &dst);
// Convert the FD in the AVDRMFrameDescriptor to a PRIME handle
// that can be used in drmModeAddFB2()
SDL_assert(drmFrame->nb_objects == 1);
err = drmPrimeFDToHandle(m_DrmFd, drmFrame->objects[0].fd, &primeHandle);
if (err < 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"drmPrimeFDToHandle() failed: %d",
errno);
return;
}
SDL_assert(drmFrame->nb_layers == 1);
SDL_assert(drmFrame->layers[0].nb_planes == 2);
for (int i = 0; i < drmFrame->layers[0].nb_planes; i++) {
handles[i] = primeHandle;
pitches[i] = drmFrame->layers[0].planes[i].pitch;
offsets[i] = drmFrame->layers[0].planes[i].offset;
}
// Remember the last FB object we created so we can free it
// when we are finished rendering this one (if successful).
uint32_t lastFbId = m_CurrentFbId;
// Create a frame buffer object from the PRIME buffer
err = drmModeAddFB2(m_DrmFd, frame->width, frame->height,
drmFrame->layers[0].format,
handles, pitches, offsets, &m_CurrentFbId, 0);
if (err < 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"drmModeAddFB2() failed: %d",
errno);
m_CurrentFbId = lastFbId;
return;
}
// Update the overlay
err = drmModeSetPlane(m_DrmFd, m_PlaneId, m_CrtcId, m_CurrentFbId, 0,
dst.x, dst.y,
dst.w, dst.h,
0, 0,
frame->width << 16,
frame->height << 16);
if (err < 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"drmModeSetPlane() failed: %d",
errno);
drmModeRmFB(m_DrmFd, m_CurrentFbId);
m_CurrentFbId = lastFbId;
return;
}
// Free the previous FB object which has now been superseded
drmModeRmFB(m_DrmFd, lastFbId);
}
@@ -1,25 +0,0 @@
#pragma once
#include "renderer.h"
#include <xf86drm.h>
#include <xf86drmMode.h>
class DrmRenderer : public IFFmpegRenderer {
public:
DrmRenderer();
virtual ~DrmRenderer() override;
virtual bool initialize(PDECODER_PARAMETERS params) override;
virtual bool prepareDecoderContext(AVCodecContext* context, AVDictionary** options) override;
virtual void renderFrame(AVFrame* frame) override;
virtual enum AVPixelFormat getPreferredPixelFormat(int videoFormat) override;
virtual int getRendererAttributes() override;
private:
int m_DrmFd;
uint32_t m_CrtcId;
uint32_t m_PlaneId;
uint32_t m_CurrentFbId;
SDL_Rect m_OutputRect;
};
+84 -326
View File
@@ -2,7 +2,6 @@
#include "dxva2.h" #include "dxva2.h"
#include "../ffmpeg.h" #include "../ffmpeg.h"
#include <streaming/streamutils.h> #include <streaming/streamutils.h>
#include <streaming/session.h>
#include <SDL_syswm.h> #include <SDL_syswm.h>
@@ -26,10 +25,7 @@ DXVA2Renderer::DXVA2Renderer() :
m_RenderTarget(nullptr), m_RenderTarget(nullptr),
m_ProcService(nullptr), m_ProcService(nullptr),
m_Processor(nullptr), m_Processor(nullptr),
m_FrameIndex(0), m_FrameIndex(0)
m_DebugOverlayFont(nullptr),
m_StatusOverlayFont(nullptr),
m_BlockingPresent(false)
{ {
RtlZeroMemory(m_DecSurfaces, sizeof(m_DecSurfaces)); RtlZeroMemory(m_DecSurfaces, sizeof(m_DecSurfaces));
RtlZeroMemory(&m_DXVAContext, sizeof(m_DXVAContext)); RtlZeroMemory(&m_DXVAContext, sizeof(m_DXVAContext));
@@ -48,8 +44,6 @@ DXVA2Renderer::~DXVA2Renderer()
SAFE_COM_RELEASE(m_RenderTarget); SAFE_COM_RELEASE(m_RenderTarget);
SAFE_COM_RELEASE(m_ProcService); SAFE_COM_RELEASE(m_ProcService);
SAFE_COM_RELEASE(m_Processor); SAFE_COM_RELEASE(m_Processor);
SAFE_COM_RELEASE(m_DebugOverlayFont);
SAFE_COM_RELEASE(m_StatusOverlayFont);
for (int i = 0; i < ARRAYSIZE(m_DecSurfaces); i++) { for (int i = 0; i < ARRAYSIZE(m_DecSurfaces); i++) {
SAFE_COM_RELEASE(m_DecSurfaces[i]); SAFE_COM_RELEASE(m_DecSurfaces[i]);
@@ -80,7 +74,7 @@ AVBufferRef* DXVA2Renderer::ffPoolAlloc(void* opaque, int)
return NULL; return NULL;
} }
bool DXVA2Renderer::prepareDecoderContext(AVCodecContext* context, AVDictionary**) bool DXVA2Renderer::prepareDecoderContext(AVCodecContext* context)
{ {
// m_DXVAContext.workaround and report_id already initialized elsewhere // m_DXVAContext.workaround and report_id already initialized elsewhere
m_DXVAContext.decoder = m_Decoder; m_DXVAContext.decoder = m_Decoder;
@@ -107,7 +101,7 @@ bool DXVA2Renderer::prepareDecoderContext(AVCodecContext* context, AVDictionary*
int DXVA2Renderer::ffGetBuffer2(AVCodecContext* context, AVFrame* frame, int) int DXVA2Renderer::ffGetBuffer2(AVCodecContext* context, AVFrame* frame, int)
{ {
DXVA2Renderer* me = (DXVA2Renderer*)((FFmpegVideoDecoder*)context->opaque)->getBackendRenderer(); DXVA2Renderer* me = (DXVA2Renderer*)((FFmpegVideoDecoder*)context->opaque)->getRenderer();
frame->buf[0] = av_buffer_pool_get(me->m_Pool); frame->buf[0] = av_buffer_pool_get(me->m_Pool);
if (!frame->buf[0]) { if (!frame->buf[0]) {
@@ -262,118 +256,66 @@ bool DXVA2Renderer::initializeRenderer()
m_DisplayWidth = renderTargetDesc.Width; m_DisplayWidth = renderTargetDesc.Width;
m_DisplayHeight = renderTargetDesc.Height; m_DisplayHeight = renderTargetDesc.Height;
if (!isDXVideoProcessorAPIBlacklisted()) { hr = DXVA2CreateVideoService(m_Device, IID_IDirectXVideoProcessorService,
hr = DXVA2CreateVideoService(m_Device, IID_IDirectXVideoProcessorService, reinterpret_cast<void**>(&m_ProcService));
reinterpret_cast<void**>(&m_ProcService));
if (FAILED(hr)) { if (FAILED(hr)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"DXVA2CreateVideoService(IID_IDirectXVideoProcessorService) failed: %x", "DXVA2CreateVideoService(IID_IDirectXVideoProcessorService) failed: %x",
hr); hr);
return false;
}
DXVA2_VideoProcessorCaps caps;
hr = m_ProcService->GetVideoProcessorCaps(DXVA2_VideoProcProgressiveDevice, &m_Desc, renderTargetDesc.Format, &caps);
if (FAILED(hr)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"GetVideoProcessorCaps() failed for DXVA2_VideoProcProgressiveDevice: %x",
hr);
return false;
}
if (!(caps.DeviceCaps & DXVA2_VPDev_HardwareDevice)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"DXVA2_VideoProcProgressiveDevice is not hardware: %x",
caps.DeviceCaps);
return false;
}
else if (!(caps.VideoProcessorOperations & DXVA2_VideoProcess_YUV2RGB) &&
!(caps.VideoProcessorOperations & DXVA2_VideoProcess_YUV2RGBExtended)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"DXVA2_VideoProcProgressiveDevice can't convert YUV2RGB: %x",
caps.VideoProcessorOperations);
return false;
}
else if (!(caps.VideoProcessorOperations & DXVA2_VideoProcess_StretchX) ||
!(caps.VideoProcessorOperations & DXVA2_VideoProcess_StretchY)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"DXVA2_VideoProcProgressiveDevice can't stretch video: %x",
caps.VideoProcessorOperations);
return false;
}
if (caps.DeviceCaps & DXVA2_VPDev_EmulatedDXVA1) {
// DXVA2 over DXVA1 may have bad performance
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"DXVA2_VideoProcProgressiveDevice is DXVA1");
}
m_ProcService->GetProcAmpRange(DXVA2_VideoProcProgressiveDevice, &m_Desc, renderTargetDesc.Format, DXVA2_ProcAmp_Brightness, &m_BrightnessRange);
m_ProcService->GetProcAmpRange(DXVA2_VideoProcProgressiveDevice, &m_Desc, renderTargetDesc.Format, DXVA2_ProcAmp_Contrast, &m_ContrastRange);
m_ProcService->GetProcAmpRange(DXVA2_VideoProcProgressiveDevice, &m_Desc, renderTargetDesc.Format, DXVA2_ProcAmp_Hue, &m_HueRange);
m_ProcService->GetProcAmpRange(DXVA2_VideoProcProgressiveDevice, &m_Desc, renderTargetDesc.Format, DXVA2_ProcAmp_Saturation, &m_SaturationRange);
hr = m_ProcService->CreateVideoProcessor(DXVA2_VideoProcProgressiveDevice, &m_Desc, renderTargetDesc.Format, 0, &m_Processor);
if (FAILED(hr)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"CreateVideoProcessor() failed for DXVA2_VideoProcProgressiveDevice: %x",
hr);
return false;
}
}
return true;
}
bool DXVA2Renderer::isDXVideoProcessorAPIBlacklisted()
{
IDirect3D9* d3d9;
HRESULT hr;
bool result = false;
if (qgetenv("DXVA2_DISABLE_VIDPROC_BLACKLIST") == "1") {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"IDirectXVideoProcessor blacklist is disabled");
return false; return false;
} }
hr = m_Device->GetDirect3D(&d3d9); DXVA2_VideoProcessorCaps caps;
if (SUCCEEDED(hr)) { hr = m_ProcService->GetVideoProcessorCaps(DXVA2_VideoProcProgressiveDevice, &m_Desc, renderTargetDesc.Format, &caps);
D3DCAPS9 caps; if (FAILED(hr)) {
hr = m_Device->GetDeviceCaps(&caps);
if (SUCCEEDED(hr)) {
D3DADAPTER_IDENTIFIER9 id;
hr = d3d9->GetAdapterIdentifier(caps.AdapterOrdinal, 0, &id);
if (SUCCEEDED(hr) && id.VendorId == 0x8086) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Avoiding IDirectXVideoProcessor API on Intel GPU");
// On Intel GPUs, we can get unwanted video "enhancements" due to post-processing
// effects that the GPU driver forces on us. In many cases, this makes the video
// actually look worse. We can avoid these by using StretchRect() instead on these
// platforms.
result = true;
}
else {
result = false;
}
}
else {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"GetDeviceCaps() failed: %x", hr);
}
d3d9->Release();
}
else {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"GetDirect3D() failed: %x", hr); "GetVideoProcessorCaps() failed for DXVA2_VideoProcProgressiveDevice: %x",
hr);
return false;
} }
return result; if (!(caps.DeviceCaps & DXVA2_VPDev_HardwareDevice)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"DXVA2_VideoProcProgressiveDevice is not hardware: %x",
caps.DeviceCaps);
return false;
}
else if (!(caps.VideoProcessorOperations & DXVA2_VideoProcess_YUV2RGB) &&
!(caps.VideoProcessorOperations & DXVA2_VideoProcess_YUV2RGBExtended)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"DXVA2_VideoProcProgressiveDevice can't convert YUV2RGB: %x",
caps.VideoProcessorOperations);
return false;
}
else if (!(caps.VideoProcessorOperations & DXVA2_VideoProcess_StretchX) ||
!(caps.VideoProcessorOperations & DXVA2_VideoProcess_StretchY)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"DXVA2_VideoProcProgressiveDevice can't stretch video: %x",
caps.VideoProcessorOperations);
return false;
}
if (caps.DeviceCaps & DXVA2_VPDev_EmulatedDXVA1) {
// DXVA2 over DXVA1 may have bad performance
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"DXVA2_VideoProcProgressiveDevice is DXVA1");
}
m_ProcService->GetProcAmpRange(DXVA2_VideoProcProgressiveDevice, &m_Desc, renderTargetDesc.Format, DXVA2_ProcAmp_Brightness, &m_BrightnessRange);
m_ProcService->GetProcAmpRange(DXVA2_VideoProcProgressiveDevice, &m_Desc, renderTargetDesc.Format, DXVA2_ProcAmp_Contrast, &m_ContrastRange);
m_ProcService->GetProcAmpRange(DXVA2_VideoProcProgressiveDevice, &m_Desc, renderTargetDesc.Format, DXVA2_ProcAmp_Hue, &m_HueRange);
m_ProcService->GetProcAmpRange(DXVA2_VideoProcProgressiveDevice, &m_Desc, renderTargetDesc.Format, DXVA2_ProcAmp_Saturation, &m_SaturationRange);
hr = m_ProcService->CreateVideoProcessor(DXVA2_VideoProcProgressiveDevice, &m_Desc, renderTargetDesc.Format, 0, &m_Processor);
if (FAILED(hr)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"CreateVideoProcessor() failed for DXVA2_VideoProcProgressiveDevice: %x",
hr);
return false;
}
return true;
} }
bool DXVA2Renderer::isDecoderBlacklisted() bool DXVA2Renderer::isDecoderBlacklisted()
@@ -382,11 +324,8 @@ bool DXVA2Renderer::isDecoderBlacklisted()
HRESULT hr; HRESULT hr;
bool result = false; bool result = false;
if (qgetenv("DXVA2_DISABLE_DECODER_BLACKLIST") == "1") { // TODO: Update for HEVC Main10
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, SDL_assert(m_VideoFormat != VIDEO_FORMAT_H265_MAIN10);
"DXVA2 decoder blacklist is disabled");
return false;
}
hr = m_Device->GetDirect3D(&d3d9); hr = m_Device->GetDirect3D(&d3d9);
if (SUCCEEDED(hr)) { if (SUCCEEDED(hr)) {
@@ -429,15 +368,6 @@ bool DXVA2Renderer::isDecoderBlacklisted()
"GPU blacklisted for HEVC due to hybrid decode"); "GPU blacklisted for HEVC due to hybrid decode");
result = (m_VideoFormat & VIDEO_FORMAT_MASK_H265) != 0; result = (m_VideoFormat & VIDEO_FORMAT_MASK_H265) != 0;
break; break;
case 0x1900: // Skylake
// Blacklist these for HEVC Main10 to avoid hybrid decode.
// Regular HEVC Main is fine though.
if (m_VideoFormat == VIDEO_FORMAT_H265_MAIN10) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"GPU blacklisted for HEVC Main10 due to hybrid decode");
result = false;
break;
}
default: default:
// Intel drivers from before late-2017 had a bug that caused some strange artifacts // Intel drivers from before late-2017 had a bug that caused some strange artifacts
// when decoding HEVC. Avoid HEVC on drivers prior to build 4836 which I confirmed // when decoding HEVC. Avoid HEVC on drivers prior to build 4836 which I confirmed
@@ -546,34 +476,12 @@ bool DXVA2Renderer::initializeDevice(SDL_Window* window, bool enableVsync)
d3dpp.hDeviceWindow = info.info.win.window; d3dpp.hDeviceWindow = info.info.win.window;
d3dpp.Flags = D3DPRESENTFLAG_VIDEO; d3dpp.Flags = D3DPRESENTFLAG_VIDEO;
if (m_VideoFormat == VIDEO_FORMAT_H265_MAIN10) {
// Verify 10-bit A2R10G10B10 color support. This is only available
// as a display format in full-screen exclusive mode on DX9.
hr = d3d9ex->CheckDeviceType(adapterIndex,
D3DDEVTYPE_HAL,
D3DFMT_A2R10G10B10,
D3DFMT_A2R10G10B10,
FALSE);
if (FAILED(hr)) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"GPU/driver doesn't support A2R10G10B10");
d3d9ex->Release();
return false;
}
}
if ((windowFlags & SDL_WINDOW_FULLSCREEN_DESKTOP) == SDL_WINDOW_FULLSCREEN) { if ((windowFlags & SDL_WINDOW_FULLSCREEN_DESKTOP) == SDL_WINDOW_FULLSCREEN) {
d3dpp.Windowed = false; d3dpp.Windowed = false;
d3dpp.BackBufferWidth = currentMode.Width; d3dpp.BackBufferWidth = currentMode.Width;
d3dpp.BackBufferHeight = currentMode.Height; d3dpp.BackBufferHeight = currentMode.Height;
d3dpp.FullScreen_RefreshRateInHz = currentMode.RefreshRate; d3dpp.FullScreen_RefreshRateInHz = currentMode.RefreshRate;
d3dpp.BackBufferFormat = currentMode.Format;
if (m_VideoFormat == VIDEO_FORMAT_H265_MAIN10) {
d3dpp.BackBufferFormat = currentMode.Format = D3DFMT_A2R10G10B10;
}
else {
d3dpp.BackBufferFormat = currentMode.Format;
}
} }
else { else {
d3dpp.Windowed = true; d3dpp.Windowed = true;
@@ -604,8 +512,6 @@ bool DXVA2Renderer::initializeDevice(SDL_Window* window, bool enableVsync)
d3dpp.BackBufferCount = 1; d3dpp.BackBufferCount = 1;
} }
m_BlockingPresent = false;
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Windowed mode with DWM running"); "Windowed mode with DWM running");
} }
@@ -615,7 +521,6 @@ bool DXVA2Renderer::initializeDevice(SDL_Window* window, bool enableVsync)
d3dpp.PresentationInterval = D3DPRESENT_INTERVAL_ONE; d3dpp.PresentationInterval = D3DPRESENT_INTERVAL_ONE;
d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD; d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
d3dpp.BackBufferCount = 1; d3dpp.BackBufferCount = 1;
m_BlockingPresent = true;
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"V-Sync enabled"); "V-Sync enabled");
@@ -626,7 +531,6 @@ bool DXVA2Renderer::initializeDevice(SDL_Window* window, bool enableVsync)
d3dpp.PresentationInterval = D3DPRESENT_INTERVAL_IMMEDIATE; d3dpp.PresentationInterval = D3DPRESENT_INTERVAL_IMMEDIATE;
d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD; d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
d3dpp.BackBufferCount = 1; d3dpp.BackBufferCount = 1;
m_BlockingPresent = false;
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"V-Sync disabled in tearing mode"); "V-Sync disabled in tearing mode");
@@ -677,23 +581,19 @@ bool DXVA2Renderer::initializeDevice(SDL_Window* window, bool enableVsync)
return true; return true;
} }
bool DXVA2Renderer::initialize(PDECODER_PARAMETERS params) bool DXVA2Renderer::initialize(SDL_Window* window, int videoFormat, int width, int height, int, bool enableVsync)
{ {
m_VideoFormat = params->videoFormat; m_VideoFormat = videoFormat;
m_VideoWidth = params->width; m_VideoWidth = width;
m_VideoHeight = params->height; m_VideoHeight = height;
RtlZeroMemory(&m_Desc, sizeof(m_Desc)); RtlZeroMemory(&m_Desc, sizeof(m_Desc));
int alignment; int alignment;
// HEVC using DXVA requires 128 pixel alignment, however Intel GPUs decoding HEVC // HEVC using DXVA requires 128B alignment
// using StretchRect() to render draw a translucent green line at the top of
// the screen in full-screen mode at 720p/1080p unless we use 32 pixel alignment.
// This appears to work without issues on AMD and Nvidia GPUs too, so we will
// do it unconditionally for now.
if (m_VideoFormat & VIDEO_FORMAT_MASK_H265) { if (m_VideoFormat & VIDEO_FORMAT_MASK_H265) {
alignment = 32; alignment = 128;
} }
else { else {
alignment = 16; alignment = 16;
@@ -708,15 +608,9 @@ bool DXVA2Renderer::initialize(PDECODER_PARAMETERS params)
m_Desc.SampleFormat.VideoPrimaries = DXVA2_VideoPrimaries_Unknown; m_Desc.SampleFormat.VideoPrimaries = DXVA2_VideoPrimaries_Unknown;
m_Desc.SampleFormat.VideoTransferFunction = DXVA2_VideoTransFunc_Unknown; m_Desc.SampleFormat.VideoTransferFunction = DXVA2_VideoTransFunc_Unknown;
m_Desc.SampleFormat.SampleFormat = DXVA2_SampleProgressiveFrame; m_Desc.SampleFormat.SampleFormat = DXVA2_SampleProgressiveFrame;
m_Desc.Format = (D3DFORMAT)MAKEFOURCC('N','V','1','2');
if (m_VideoFormat == VIDEO_FORMAT_H265_MAIN10) { if (!initializeDevice(window, enableVsync)) {
m_Desc.Format = (D3DFORMAT)MAKEFOURCC('P','0','1','0');
}
else {
m_Desc.Format = (D3DFORMAT)MAKEFOURCC('N','V','1','2');
}
if (!initializeDevice(params->window, params->enableVsync)) {
return false; return false;
} }
@@ -733,90 +627,30 @@ bool DXVA2Renderer::initialize(PDECODER_PARAMETERS params)
// input focus. This glitches out the renderer and a bunch of other stuff. // input focus. This glitches out the renderer and a bunch of other stuff.
// Direct3D9Ex itself seems to have this minimize on focus loss behavior on its // Direct3D9Ex itself seems to have this minimize on focus loss behavior on its
// own, so just disable SDL's handling of the focus loss event. // own, so just disable SDL's handling of the focus loss event.
SDL_SetHintWithPriority(SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS, "0", SDL_HINT_OVERRIDE); SDL_SetHint(SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS, "0");
return true; return true;
} }
void DXVA2Renderer::notifyOverlayUpdated(Overlay::OverlayType type) bool DXVA2Renderer::needsTestFrame()
{ {
HRESULT hr; // We validate the DXVA2 profiles are supported
// in initialize() so no test frame is required
switch (type) return false;
{
case Overlay::OverlayDebug:
if (m_DebugOverlayFont == nullptr) {
hr = D3DXCreateFontA(m_Device,
Session::get()->getOverlayManager().getOverlayFontSize(Overlay::OverlayDebug),
0,
FW_HEAVY,
1,
false,
ANSI_CHARSET,
OUT_DEFAULT_PRECIS,
DEFAULT_QUALITY,
DEFAULT_PITCH | FF_DONTCARE,
"",
&m_DebugOverlayFont);
if (FAILED(hr)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"D3DXCreateFontA() failed: %x",
hr);
m_DebugOverlayFont = nullptr;
}
}
break;
case Overlay::OverlayStatusUpdate:
if (m_StatusOverlayFont == nullptr) {
hr = D3DXCreateFontA(m_Device,
Session::get()->getOverlayManager().getOverlayFontSize(Overlay::OverlayStatusUpdate),
0,
FW_HEAVY,
1,
false,
ANSI_CHARSET,
OUT_DEFAULT_PRECIS,
DEFAULT_QUALITY,
DEFAULT_PITCH | FF_DONTCARE,
"",
&m_StatusOverlayFont);
if (FAILED(hr)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"D3DXCreateFontA() failed: %x",
hr);
m_StatusOverlayFont = nullptr;
}
}
break;
default:
SDL_assert(false);
break;
}
} }
int DXVA2Renderer::getDecoderColorspace() int DXVA2Renderer::getDecoderCapabilities()
{ {
if (isDXVideoProcessorAPIBlacklisted()) { return 0;
// StretchRect() assumes Rec 601 on Intel GPUs
return COLORSPACE_REC_601;
}
else {
// VideoProcessBlt() *should* properly handle whatever, since
// we provide colorspace information. However, AMD GPUs seem to
// always assume Rec 709, so we'll use that as our default.
return COLORSPACE_REC_709;
}
} }
void DXVA2Renderer::renderFrame(AVFrame *frame) IFFmpegRenderer::FramePacingConstraint DXVA2Renderer::getFramePacingConstraint()
{ {
if (frame == nullptr) { return PACING_ANY;
// End of stream - nothing to do for us }
return;
}
void DXVA2Renderer::renderFrameAtVsync(AVFrame *frame)
{
IDirect3DSurface9* surface = reinterpret_cast<IDirect3DSurface9*>(frame->data[3]); IDirect3DSurface9* surface = reinterpret_cast<IDirect3DSurface9*>(frame->data[3]);
HRESULT hr; HRESULT hr;
@@ -951,10 +785,12 @@ void DXVA2Renderer::renderFrame(AVFrame *frame)
bltParams.Alpha = DXVA2_Fixed32OpaqueAlpha(); bltParams.Alpha = DXVA2_Fixed32OpaqueAlpha();
hr = m_Device->Clear(0, nullptr, D3DCLEAR_TARGET, D3DCOLOR_ARGB(255, 0, 0, 0), 0.0f, 0); m_Device->Clear(0, nullptr, D3DCLEAR_TARGET, D3DCOLOR_ARGB(255, 0, 0, 0), 0.0f, 0);
hr = m_Processor->VideoProcessBlt(m_RenderTarget, &bltParams, &sample, 1, nullptr);
if (FAILED(hr)) { if (FAILED(hr)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Clear() failed: %x", "VideoProcessBlt() failed: %x",
hr); hr);
SDL_Event event; SDL_Event event;
event.type = SDL_RENDER_TARGETS_RESET; event.type = SDL_RENDER_TARGETS_RESET;
@@ -962,85 +798,7 @@ void DXVA2Renderer::renderFrame(AVFrame *frame)
return; return;
} }
hr = m_Device->BeginScene(); hr = m_Device->PresentEx(nullptr, nullptr, nullptr, nullptr, 0);
if (FAILED(hr)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"BeginScene() failed: %x",
hr);
SDL_Event event;
event.type = SDL_RENDER_TARGETS_RESET;
SDL_PushEvent(&event);
return;
}
if (m_Processor) {
hr = m_Processor->VideoProcessBlt(m_RenderTarget, &bltParams, &sample, 1, nullptr);
if (FAILED(hr)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"VideoProcessBlt() failed, falling back to StretchRect(): %x",
hr);
m_Processor->Release();
m_Processor = nullptr;
}
}
if (!m_Processor) {
// This function doesn't trigger any of Intel's garbage video "enhancements"
hr = m_Device->StretchRect(surface, &sample.SrcRect, m_RenderTarget, &sample.DstRect, D3DTEXF_NONE);
if (FAILED(hr)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"StretchRect() failed: %x",
hr);
SDL_Event event;
event.type = SDL_RENDER_TARGETS_RESET;
SDL_PushEvent(&event);
return;
}
}
if (m_DebugOverlayFont != nullptr) {
if (Session::get()->getOverlayManager().isOverlayEnabled(Overlay::OverlayDebug)) {
SDL_Color color = Session::get()->getOverlayManager().getOverlayColor(Overlay::OverlayDebug);
m_DebugOverlayFont->DrawTextA(nullptr,
Session::get()->getOverlayManager().getOverlayText(Overlay::OverlayDebug),
-1,
&sample.DstRect,
DT_LEFT | DT_NOCLIP,
D3DCOLOR_ARGB(color.a, color.r, color.g, color.b));
}
}
if (m_StatusOverlayFont != nullptr) {
if (Session::get()->getOverlayManager().isOverlayEnabled(Overlay::OverlayStatusUpdate)) {
SDL_Color color = Session::get()->getOverlayManager().getOverlayColor(Overlay::OverlayStatusUpdate);
m_StatusOverlayFont->DrawTextA(nullptr,
Session::get()->getOverlayManager().getOverlayText(Overlay::OverlayStatusUpdate),
-1,
&sample.DstRect,
DT_RIGHT | DT_NOCLIP,
D3DCOLOR_ARGB(color.a, color.r, color.g, color.b));
}
}
hr = m_Device->EndScene();
if (FAILED(hr)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"EndScene() failed: %x",
hr);
SDL_Event event;
event.type = SDL_RENDER_TARGETS_RESET;
SDL_PushEvent(&event);
return;
}
do {
// Use D3DPRESENT_DONOTWAIT if present may block in order to avoid holding the giant
// lock around this D3D device for excessive lengths of time (blocking concurrent decoding tasks).
hr = m_Device->PresentEx(nullptr, nullptr, nullptr, nullptr, m_BlockingPresent ? D3DPRESENT_DONOTWAIT : 0);
if (hr == D3DERR_WASSTILLDRAWING) {
SDL_Delay(1);
}
} while (hr == D3DERR_WASSTILLDRAWING);
if (FAILED(hr)) { if (FAILED(hr)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"PresentEx() failed: %x", "PresentEx() failed: %x",
+12 -11
View File
@@ -4,7 +4,6 @@
#include "pacer/pacer.h" #include "pacer/pacer.h"
#include <d3d9.h> #include <d3d9.h>
#include <d3dx9.h>
#include <dxva2api.h> #include <dxva2api.h>
extern "C" { extern "C" {
@@ -15,19 +14,24 @@ class DXVA2Renderer : public IFFmpegRenderer
{ {
public: public:
DXVA2Renderer(); DXVA2Renderer();
virtual ~DXVA2Renderer() override; virtual ~DXVA2Renderer();
virtual bool initialize(PDECODER_PARAMETERS params) override; virtual bool initialize(SDL_Window* window,
virtual bool prepareDecoderContext(AVCodecContext* context, AVDictionary** options) override; int videoFormat,
virtual void renderFrame(AVFrame* frame) override; int width,
virtual void notifyOverlayUpdated(Overlay::OverlayType) override; int height,
virtual int getDecoderColorspace() override; int maxFps,
bool enableVsync);
virtual bool prepareDecoderContext(AVCodecContext* context);
virtual void renderFrameAtVsync(AVFrame* frame);
virtual bool needsTestFrame();
virtual int getDecoderCapabilities();
virtual FramePacingConstraint getFramePacingConstraint();
private: private:
bool initializeDecoder(); bool initializeDecoder();
bool initializeRenderer(); bool initializeRenderer();
bool initializeDevice(SDL_Window* window, bool enableVsync); bool initializeDevice(SDL_Window* window, bool enableVsync);
bool isDecoderBlacklisted(); bool isDecoderBlacklisted();
bool isDXVideoProcessorAPIBlacklisted();
static static
AVBufferRef* ffPoolAlloc(void* opaque, int size); AVBufferRef* ffPoolAlloc(void* opaque, int size);
@@ -63,7 +67,4 @@ private:
DXVA2_ValueRange m_SaturationRange; DXVA2_ValueRange m_SaturationRange;
DXVA2_VideoDesc m_Desc; DXVA2_VideoDesc m_Desc;
REFERENCE_TIME m_FrameIndex; REFERENCE_TIME m_FrameIndex;
LPD3DXFONT m_DebugOverlayFont;
LPD3DXFONT m_StatusOverlayFont;
bool m_BlockingPresent;
}; };
@@ -1,21 +0,0 @@
// vim: noai:ts=4:sw=4:softtabstop=4:expandtab
#include "renderer.h"
#include <SDL_egl.h>
static QStringList egl_get_extensions(EGLDisplay dpy) {
const auto EGLExtensionsStr = eglQueryString(dpy, EGL_EXTENSIONS);
if (!EGLExtensionsStr) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Unable to get EGL extensions");
return QStringList();
}
return QString(EGLExtensionsStr).split(" ");
}
EGLExtensions::EGLExtensions(EGLDisplay dpy) :
m_Extensions(egl_get_extensions(dpy))
{}
bool EGLExtensions::isSupported(const QString &extension) const {
return m_Extensions.contains(extension);
}
@@ -1,627 +0,0 @@
// vim: noai:ts=4:sw=4:softtabstop=4:expandtab
#include "eglvid.h"
#include "path.h"
#include "streaming/session.h"
#include "streaming/streamutils.h"
#include <QDir>
#include <Limelight.h>
#include <unistd.h>
#include <SDL_egl.h>
#include <SDL_opengles2.h>
#include <SDL_render.h>
#include <SDL_syswm.h>
#ifndef EGL_VERSION_1_5
typedef intptr_t EGLAttrib;
#endif
#if !defined(EGL_VERSION_1_5) || !defined(EGL_EGL_PROTOTYPES)
typedef EGLDisplay (EGLAPIENTRYP PFNEGLGETPLATFORMDISPLAYPROC) (EGLenum platform, void *native_display, const EGLAttrib *attrib_list);
#endif
#ifndef EGL_EXT_platform_base
typedef EGLDisplay (EGLAPIENTRYP PFNEGLGETPLATFORMDISPLAYEXTPROC) (EGLenum platform, void *native_display, const EGLint *attrib_list);
#endif
// These are EGL extensions, so some platform headers may not provide them
#ifndef EGL_PLATFORM_WAYLAND_KHR
#define EGL_PLATFORM_WAYLAND_KHR 0x31D8
#endif
#ifndef EGL_PLATFORM_X11_KHR
#define EGL_PLATFORM_X11_KHR 0x31D5
#endif
/* TODO:
* - handle more pixel formats
* - handle software decoding
*/
/* DOC/misc:
* - https://kernel-recipes.org/en/2016/talks/video-and-colorspaces/
* - http://www.brucelindbloom.com/
* - https://learnopengl.com/Getting-started/Shaders
* - https://github.com/stunpix/yuvit
* - https://en.wikipedia.org/wiki/YCbCr#ITU-R_BT.601_conversion
* - https://www.renesas.com/eu/en/www/doc/application-note/an9717.pdf
* - https://www.xilinx.com/support/documentation/application_notes/xapp283.pdf
* - https://www.itu.int/dms_pubrec/itu-r/rec/bt/R-REC-BT.709-6-201506-I!!PDF-E.pdf
* - https://www.khronos.org/registry/OpenGL/extensions/OES/OES_EGL_image_external.txt
* - https://gist.github.com/rexguo/6696123
* - https://wiki.libsdl.org/CategoryVideo
*/
#define EGL_LOG(Category, ...) SDL_Log ## Category(\
SDL_LOG_CATEGORY_APPLICATION, \
"EGLRenderer: " __VA_ARGS__)
EGLRenderer::EGLRenderer(IFFmpegRenderer *backendRenderer)
:
m_SwPixelFormat(AV_PIX_FMT_NONE),
m_EGLDisplay(EGL_NO_DISPLAY),
m_Textures{0},
m_ShaderProgram(0),
m_Context(0),
m_Window(nullptr),
m_Backend(backendRenderer),
m_VAO(0),
m_ColorSpace(AVCOL_SPC_NB),
m_ColorFull(false),
m_BlockingSwapBuffers(false),
m_glEGLImageTargetTexture2DOES(nullptr),
m_glGenVertexArraysOES(nullptr),
m_glBindVertexArrayOES(nullptr),
m_glDeleteVertexArraysOES(nullptr),
m_DummyRenderer(nullptr)
{
SDL_assert(backendRenderer);
SDL_assert(backendRenderer->canExportEGL());
// Save these global parameters so we can restore them in our destructor
SDL_GL_GetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, &m_OldContextProfileMask);
SDL_GL_GetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, &m_OldContextMajorVersion);
SDL_GL_GetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, &m_OldContextMinorVersion);
}
EGLRenderer::~EGLRenderer()
{
if (m_Context) {
// Reattach the GL context to the main thread for destruction
SDL_GL_MakeCurrent(m_Window, m_Context);
if (m_ShaderProgram) {
glDeleteProgram(m_ShaderProgram);
}
if (m_VAO) {
SDL_assert(m_glDeleteVertexArraysOES != nullptr);
m_glDeleteVertexArraysOES(1, &m_VAO);
}
if (m_DummyRenderer) {
SDL_DestroyRenderer(m_DummyRenderer);
}
SDL_GL_DeleteContext(m_Context);
}
// Reset the global properties back to what they were before
SDL_SetHint(SDL_HINT_OPENGL_ES_DRIVER, "0");
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, m_OldContextProfileMask);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, m_OldContextMajorVersion);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, m_OldContextMinorVersion);
}
bool EGLRenderer::prepareDecoderContext(AVCodecContext*, AVDictionary**)
{
/* Nothing to do */
EGL_LOG(Info, "Using EGL renderer");
return true;
}
void EGLRenderer::notifyOverlayUpdated(Overlay::OverlayType)
{
// TODO: FIXME
}
bool EGLRenderer::isPixelFormatSupported(int, AVPixelFormat pixelFormat)
{
// Remember to keep this in sync with EGLRenderer::renderFrame()!
switch (pixelFormat)
{
case AV_PIX_FMT_NV12:
return true;
default:
return false;
}
}
int EGLRenderer::loadAndBuildShader(int shaderType,
const char *file) {
GLuint shader = glCreateShader(shaderType);
if (!shader || shader == GL_INVALID_ENUM) {
EGL_LOG(Error, "Can't create shader: %d", glGetError());
return 0;
}
auto sourceData = Path::readDataFile(file);
GLint len = sourceData.size();
const char *buf = sourceData.data();
glShaderSource(shader, 1, &buf, &len);
glCompileShader(shader);
GLint status;
glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
if (!status) {
char shaderLog[512];
glGetShaderInfoLog(shader, sizeof (shaderLog), nullptr, shaderLog);
EGL_LOG(Error, "Cannot load shader \"%s\": %s", file, shaderLog);
return 0;
}
return shader;
}
bool EGLRenderer::openDisplay(unsigned int platform, void* nativeDisplay)
{
PFNEGLGETPLATFORMDISPLAYPROC eglGetPlatformDisplayProc;
PFNEGLGETPLATFORMDISPLAYEXTPROC eglGetPlatformDisplayEXTProc;
m_EGLDisplay = EGL_NO_DISPLAY;
// NB: eglGetPlatformDisplay() and eglGetPlatformDisplayEXT() have slightly different definitions
eglGetPlatformDisplayProc = (typeof(eglGetPlatformDisplayProc))eglGetProcAddress("eglGetPlatformDisplay");
eglGetPlatformDisplayEXTProc = (typeof(eglGetPlatformDisplayEXTProc))eglGetProcAddress("eglGetPlatformDisplayEXT");
if (m_EGLDisplay == EGL_NO_DISPLAY) {
// eglGetPlatformDisplay() is part of the EGL 1.5 core specification
if (eglGetPlatformDisplayProc != nullptr) {
m_EGLDisplay = eglGetPlatformDisplayProc(platform, nativeDisplay, nullptr);
if (m_EGLDisplay == EGL_NO_DISPLAY) {
EGL_LOG(Warn, "eglGetPlatformDisplay() failed: %d", eglGetError());
}
}
}
if (m_EGLDisplay == EGL_NO_DISPLAY) {
// eglGetPlatformDisplayEXT() is an extension for EGL 1.4
const EGLExtensions eglExtensions(EGL_NO_DISPLAY);
if (eglExtensions.isSupported("EGL_EXT_platform_base")) {
if (eglGetPlatformDisplayEXTProc != nullptr) {
m_EGLDisplay = eglGetPlatformDisplayEXTProc(platform, nativeDisplay, nullptr);
if (m_EGLDisplay == EGL_NO_DISPLAY) {
EGL_LOG(Warn, "eglGetPlatformDisplayEXT() failed: %d", eglGetError());
}
}
else {
EGL_LOG(Warn, "EGL_EXT_platform_base supported but no eglGetPlatformDisplayEXT() export!");
}
}
}
if (m_EGLDisplay == EGL_NO_DISPLAY) {
// Finally, if all else fails, use eglGetDisplay()
m_EGLDisplay = eglGetDisplay((EGLNativeDisplayType)nativeDisplay);
if (m_EGLDisplay == EGL_NO_DISPLAY) {
EGL_LOG(Error, "eglGetDisplay() failed: %d", eglGetError());
}
}
return m_EGLDisplay != EGL_NO_DISPLAY;
}
bool EGLRenderer::compileShader() {
SDL_assert(!m_ShaderProgram);
SDL_assert(m_SwPixelFormat != AV_PIX_FMT_NONE);
// XXX: TODO: other formats
SDL_assert(m_SwPixelFormat == AV_PIX_FMT_NV12);
bool ret = false;
GLuint vertexShader = loadAndBuildShader(GL_VERTEX_SHADER, "egl.vert");
if (!vertexShader)
return false;
GLuint fragmentShader = loadAndBuildShader(GL_FRAGMENT_SHADER, "egl.frag");
if (!fragmentShader)
goto fragError;
m_ShaderProgram = glCreateProgram();
if (!m_ShaderProgram) {
EGL_LOG(Error, "Cannot create shader program");
goto progFailCreate;
}
glAttachShader(m_ShaderProgram, vertexShader);
glAttachShader(m_ShaderProgram, fragmentShader);
glLinkProgram(m_ShaderProgram);
int status;
glGetProgramiv(m_ShaderProgram, GL_LINK_STATUS, &status);
if (status) {
ret = true;
} else {
char shader_log[512];
glGetProgramInfoLog(m_ShaderProgram, sizeof (shader_log), nullptr, shader_log);
EGL_LOG(Error, "Cannot link shader program: %s", shader_log);
glDeleteProgram(m_ShaderProgram);
m_ShaderProgram = 0;
}
progFailCreate:
glDeleteShader(fragmentShader);
fragError:
glDeleteShader(vertexShader);
return ret;
}
bool EGLRenderer::initialize(PDECODER_PARAMETERS params)
{
m_Window = params->window;
if (params->videoFormat == VIDEO_FORMAT_H265_MAIN10) {
// EGL doesn't support rendering YUV 10-bit textures yet
return false;
}
// It's not safe to attempt to opportunistically create a GLES2
// renderer prior to 2.0.10. If GLES2 isn't available, SDL will
// attempt to dereference a null pointer and crash Moonlight.
// https://bugzilla.libsdl.org/show_bug.cgi?id=4350
// https://hg.libsdl.org/SDL/rev/84618d571795
if (!SDL_VERSION_ATLEAST(2, 0, 10)) {
EGL_LOG(Error, "Not supported until SDL 2.0.10");
return false;
}
/*
* Create a dummy renderer in order to craft an accelerated SDL Window.
* Request opengl ES 3.0 context, otherwise it will SIGSEGV
* https://gitlab.freedesktop.org/mesa/mesa/issues/1011
*/
SDL_SetHint(SDL_HINT_OPENGL_ES_DRIVER, "1");
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0);
int renderIndex;
int maxRenderers = SDL_GetNumRenderDrivers();
SDL_assert(maxRenderers >= 0);
SDL_RendererInfo renderInfo;
for (renderIndex = 0; renderIndex < maxRenderers; ++renderIndex) {
if (SDL_GetRenderDriverInfo(renderIndex, &renderInfo))
continue;
if (!strcmp(renderInfo.name, "opengles2")) {
SDL_assert(renderInfo.flags & SDL_RENDERER_ACCELERATED);
break;
}
}
if (renderIndex == maxRenderers) {
EGL_LOG(Error, "Could not find a suitable SDL_Renderer");
return false;
}
if (!(m_DummyRenderer = SDL_CreateRenderer(m_Window, renderIndex, SDL_RENDERER_ACCELERATED))) {
EGL_LOG(Error, "SDL_CreateRenderer() failed: %s", SDL_GetError());
return false;
}
SDL_SysWMinfo info;
SDL_VERSION(&info.version);
if (!SDL_GetWindowWMInfo(params->window, &info)) {
EGL_LOG(Error, "SDL_GetWindowWMInfo() failed: %s", SDL_GetError());
return false;
}
switch (info.subsystem) {
#ifdef SDL_VIDEO_DRIVER_WAYLAND
case SDL_SYSWM_WAYLAND:
if (!openDisplay(EGL_PLATFORM_WAYLAND_KHR, info.info.wl.display)) {
return false;
}
break;
#endif
#ifdef SDL_VIDEO_DRIVER_X11
case SDL_SYSWM_X11:
if (!openDisplay(EGL_PLATFORM_X11_KHR, info.info.x11.display)) {
return false;
}
break;
#endif
default:
EGL_LOG(Error, "not compatible with SYSWM");
return false;
}
if (m_EGLDisplay == EGL_NO_DISPLAY) {
EGL_LOG(Error, "Cannot get EGL display: %d", eglGetError());
return false;
}
if (!(m_Context = SDL_GL_CreateContext(params->window))) {
EGL_LOG(Error, "Cannot create OpenGL context: %s", SDL_GetError());
return false;
}
if (SDL_GL_MakeCurrent(params->window, m_Context)) {
EGL_LOG(Error, "Cannot use created EGL context: %s", SDL_GetError());
return false;
}
const EGLExtensions eglExtensions(m_EGLDisplay);
if (!eglExtensions.isSupported("EGL_KHR_image_base") &&
!eglExtensions.isSupported("EGL_KHR_image")) {
EGL_LOG(Error, "EGL_KHR_image unsupported");
return false;
}
else if (!SDL_GL_ExtensionSupported("GL_OES_EGL_image")) {
EGL_LOG(Error, "GL_OES_EGL_image unsupported");
return false;
}
if (!m_Backend->initializeEGL(m_EGLDisplay, eglExtensions))
return false;
if (!(m_glEGLImageTargetTexture2DOES = (typeof(m_glEGLImageTargetTexture2DOES))eglGetProcAddress("glEGLImageTargetTexture2DOES"))) {
EGL_LOG(Error,
"EGL: cannot retrieve `glEGLImageTargetTexture2DOES` address");
return false;
}
// Vertex arrays are an extension on OpenGL ES 2.0
if (SDL_GL_ExtensionSupported("GL_OES_vertex_array_object")) {
m_glGenVertexArraysOES = (typeof(m_glGenVertexArraysOES))eglGetProcAddress("glGenVertexArraysOES");
m_glBindVertexArrayOES = (typeof(m_glBindVertexArrayOES))eglGetProcAddress("glBindVertexArrayOES");
m_glDeleteVertexArraysOES = (typeof(m_glDeleteVertexArraysOES))eglGetProcAddress("glDeleteVertexArraysOES");
}
else {
// They are included in OpenGL ES 3.0 as part of the standard
m_glGenVertexArraysOES = (typeof(m_glGenVertexArraysOES))eglGetProcAddress("glGenVertexArrays");
m_glBindVertexArrayOES = (typeof(m_glBindVertexArrayOES))eglGetProcAddress("glBindVertexArray");
m_glDeleteVertexArraysOES = (typeof(m_glDeleteVertexArraysOES))eglGetProcAddress("glDeleteVertexArrays");
}
if (!m_glGenVertexArraysOES || !m_glBindVertexArrayOES || !m_glDeleteVertexArraysOES) {
EGL_LOG(Error, "Failed to find VAO functions");
return false;
}
/* Compute the video region size in order to keep the aspect ratio of the
* video stream.
*/
SDL_Rect src, dst;
src.x = src.y = dst.x = dst.y = 0;
src.w = params->width;
src.h = params->height;
SDL_GetWindowSize(m_Window, &dst.w, &dst.h);
StreamUtils::scaleSourceToDestinationSurface(&src, &dst);
glViewport(dst.x, dst.y, dst.w, dst.h);
glClearColor(0, 0, 0, 1);
glClear(GL_COLOR_BUFFER_BIT);
if (params->enableVsync) {
// Try to use adaptive VSYNC unless we're using frame pacing.
// Frame pacing relies on us blocking in renderFrame() to
// match the display refresh rate.
if (params->enableFramePacing || SDL_GL_SetSwapInterval(-1)) {
SDL_GL_SetSwapInterval(1);
m_BlockingSwapBuffers = true;
}
} else {
SDL_GL_SetSwapInterval(0);
}
SDL_GL_SwapWindow(params->window);
glGenTextures(EGL_MAX_PLANES, m_Textures);
for (size_t i = 0; i < EGL_MAX_PLANES; ++i) {
glBindTexture(GL_TEXTURE_EXTERNAL_OES, m_Textures[i]);
glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
}
GLenum err = glGetError();
if (err != GL_NO_ERROR)
EGL_LOG(Error, "OpenGL error: %d", err);
// Detach the context from this thread, so the render thread can attach it
SDL_GL_MakeCurrent(m_Window, nullptr);
return err == GL_NO_ERROR;
}
const float *EGLRenderer::getColorMatrix() {
/* The conversion matrices are shamelessly stolen from linux:
* drivers/media/platform/imx-pxp.c:pxp_setup_csc
*/
static const float bt601Lim[] = {
1.1644f, 1.1644f, 1.1644f,
0.0f, -0.3917f, 2.0172f,
1.5960f, -0.8129f, 0.0f
};
static const float bt601Full[] = {
1.0f, 1.0f, 1.0f,
0.0f, -0.3441f, 1.7720f,
1.4020f, -0.7141f, 0.0f
};
static const float bt709Lim[] = {
1.1644f, 1.1644f, 1.1644f,
0.0f, -0.2132f, 2.1124f,
1.7927f, -0.5329f, 0.0f
};
static const float bt709Full[] = {
1.0f, 1.0f, 1.0f,
0.0f, -0.1873f, 1.8556f,
1.5748f, -0.4681f, 0.0f
};
static const float bt2020Lim[] = {
1.1644f, 1.1644f, 1.1644f,
0.0f, -0.1874f, 2.1418f,
1.6781f, -0.6505f, 0.0f
};
static const float bt2020Full[] = {
1.0f, 1.0f, 1.0f,
0.0f, -0.1646f, 1.8814f,
1.4746f, -0.5714f, 0.0f
};
switch (m_ColorSpace) {
case AVCOL_SPC_SMPTE170M:
case AVCOL_SPC_BT470BG:
EGL_LOG(Info, "BT-601 pixels");
return m_ColorFull ? bt601Full : bt601Lim;
case AVCOL_SPC_BT709:
EGL_LOG(Info, "BT-709 pixels");
return m_ColorFull ? bt709Full : bt709Lim;
case AVCOL_SPC_BT2020_NCL:
case AVCOL_SPC_BT2020_CL:
EGL_LOG(Info, "BT-2020 pixels");
return m_ColorFull ? bt2020Full : bt2020Lim;
};
EGL_LOG(Warn, "unknown color space: %d, falling back to BT-601",
m_ColorSpace);
return bt601Lim;
}
bool EGLRenderer::specialize() {
SDL_assert(!m_VAO);
// Attach our GL context to the render thread
SDL_GL_MakeCurrent(m_Window, m_Context);
if (!compileShader())
return false;
// The viewport should have the aspect ratio of the video stream
static const float vertices[] = {
// pos .... // tex coords
1.0f, 1.0f, 1.0f, 0.0f,
1.0f, -1.0f, 1.0f, 1.0f,
-1.0f, -1.0f, 0.0f, 1.0f,
-1.0f, 1.0f, 0.0f, 0.0f,
};
static const unsigned int indices[] = {
0, 1, 3,
1, 2, 3,
};
glUseProgram(m_ShaderProgram);
unsigned int VBO, EBO;
m_glGenVertexArraysOES(1, &m_VAO);
glGenBuffers(1, &VBO);
glGenBuffers(1, &EBO);
m_glBindVertexArrayOES(m_VAO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof (vertices), vertices, GL_STATIC_DRAW);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof (indices), indices, GL_STATIC_DRAW);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)(2 * sizeof (float)));
glEnableVertexAttribArray(1);
glBindBuffer(GL_ARRAY_BUFFER, 0);
m_glBindVertexArrayOES(0);
int yuvmatLocation = glGetUniformLocation(m_ShaderProgram, "yuvmat");
glUniformMatrix3fv(yuvmatLocation, 1, GL_FALSE, getColorMatrix());
static const float limitedOffsets[] = { 16.0f / 255.0f, 128.0f / 255.0f, 128.0f / 255.0f };
static const float fullOffsets[] = { 0.0f, 128.0f / 255.0f, 128.0f / 255.0f };
int offLocation = glGetUniformLocation(m_ShaderProgram, "offset");
glUniform3fv(offLocation, 1, m_ColorFull ? fullOffsets : limitedOffsets);
int colorPlane = glGetUniformLocation(m_ShaderProgram, "plane1");
glUniform1i(colorPlane, 0);
colorPlane = glGetUniformLocation(m_ShaderProgram, "plane2");
glUniform1i(colorPlane, 1);
glDeleteBuffers(1, &VBO);
glDeleteBuffers(1, &EBO);
GLenum err = glGetError();
if (err != GL_NO_ERROR) {
EGL_LOG(Error, "OpenGL error: %d", err);
}
return err == GL_NO_ERROR;
}
void EGLRenderer::renderFrame(AVFrame* frame)
{
EGLImage imgs[EGL_MAX_PLANES];
if (frame == nullptr) {
// End of stream - unbind the GL context
SDL_GL_MakeCurrent(m_Window, nullptr);
return;
}
if (frame->hw_frames_ctx != nullptr) {
// Find the native read-back format and load the shader
if (m_SwPixelFormat == AV_PIX_FMT_NONE) {
auto hwFrameCtx = (AVHWFramesContext*)frame->hw_frames_ctx->data;
m_SwPixelFormat = hwFrameCtx->sw_format;
SDL_assert(m_SwPixelFormat != AV_PIX_FMT_NONE);
EGL_LOG(Info, "Selected read-back format: %d", m_SwPixelFormat);
// XXX: TODO: Handle other pixel formats
SDL_assert(m_SwPixelFormat == AV_PIX_FMT_NV12);
m_ColorSpace = frame->colorspace;
m_ColorFull = frame->color_range == AVCOL_RANGE_JPEG;
if (!specialize()) {
m_SwPixelFormat = AV_PIX_FMT_NONE;
return;
}
}
ssize_t plane_count = m_Backend->exportEGLImages(frame, m_EGLDisplay, imgs);
if (plane_count < 0)
return;
for (ssize_t i = 0; i < plane_count; ++i) {
glActiveTexture(GL_TEXTURE0 + i);
glBindTexture(GL_TEXTURE_EXTERNAL_OES, m_Textures[i]);
m_glEGLImageTargetTexture2DOES(GL_TEXTURE_EXTERNAL_OES, imgs[i]);
}
} else {
// TODO: load texture for SW decoding ?
EGL_LOG(Error, "EGL rendering only supports hw frames");
return;
}
glClear(GL_COLOR_BUFFER_BIT);
glUseProgram(m_ShaderProgram);
m_glBindVertexArrayOES(m_VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
SDL_GL_SwapWindow(m_Window);
if (m_BlockingSwapBuffers) {
// This glClear() forces us to block until the buffer swap is
// complete to continue rendering. Mesa won't actually wait
// for the swap with just glFinish() alone. Waiting here keeps us
// in lock step with the display refresh rate. If we don't wait
// here, we'll stall on the first GL call next frame. Doing the
// wait here instead allows more time for a newer frame to arrive
// for next renderFrame() call.
glClear(GL_COLOR_BUFFER_BIT);
glFinish();
}
if (frame->hw_frames_ctx != nullptr)
m_Backend->freeEGLImages(m_EGLDisplay, imgs);
}
@@ -1,47 +0,0 @@
#pragma once
#include "renderer.h"
#include <SDL_opengles2.h>
#include <SDL_opengles2_gl2ext.h>
class EGLRenderer : public IFFmpegRenderer {
public:
EGLRenderer(IFFmpegRenderer *backendRenderer);
virtual ~EGLRenderer() override;
virtual bool initialize(PDECODER_PARAMETERS params) override;
virtual bool prepareDecoderContext(AVCodecContext* context, AVDictionary** options) override;
virtual void renderFrame(AVFrame* frame) override;
virtual void notifyOverlayUpdated(Overlay::OverlayType) override;
virtual bool isPixelFormatSupported(int videoFormat, enum AVPixelFormat pixelFormat) override;
private:
bool compileShader();
bool specialize();
const float *getColorMatrix();
static int loadAndBuildShader(int shaderType, const char *filename);
bool openDisplay(unsigned int platform, void* nativeDisplay);
int m_SwPixelFormat;
void *m_EGLDisplay;
unsigned m_Textures[EGL_MAX_PLANES];
unsigned m_ShaderProgram;
SDL_GLContext m_Context;
SDL_Window *m_Window;
IFFmpegRenderer *m_Backend;
unsigned int m_VAO;
int m_ColorSpace;
bool m_ColorFull;
bool m_BlockingSwapBuffers;
PFNGLEGLIMAGETARGETTEXTURE2DOESPROC m_glEGLImageTargetTexture2DOES;
PFNGLGENVERTEXARRAYSOESPROC m_glGenVertexArraysOES;
PFNGLBINDVERTEXARRAYOESPROC m_glBindVertexArrayOES;
PFNGLDELETEVERTEXARRAYSOESPROC m_glDeleteVertexArraysOES;
int m_OldContextProfileMask;
int m_OldContextMajorVersion;
int m_OldContextMinorVersion;
SDL_Renderer *m_DummyRenderer;
};
@@ -1,170 +0,0 @@
#include "mmal.h"
#include "streaming/streamutils.h"
#include "streaming/session.h"
#include <Limelight.h>
MmalRenderer::MmalRenderer()
: m_Renderer(nullptr),
m_InputPort(nullptr)
{
}
MmalRenderer::~MmalRenderer()
{
if (m_InputPort != nullptr) {
mmal_port_disable(m_InputPort);
}
if (m_Renderer != nullptr) {
mmal_component_destroy(m_Renderer);
}
}
bool MmalRenderer::prepareDecoderContext(AVCodecContext* context, AVDictionary** options)
{
// FFmpeg defaults this to 10 which is too large to fit in the default 64 MB VRAM split.
// Reducing to 2 seems to work fine for our bitstreams (max of 1 buffered frame needed).
av_dict_set_int(options, "extra_buffers", 2, 0);
// MMAL seems to dislike certain initial width and height values, but it seems okay
// with getting zero for the width and height. We'll zero them all the time to be safe.
context->width = 0;
context->height = 0;
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Using MMAL renderer");
return true;
}
bool MmalRenderer::initialize(PDECODER_PARAMETERS params)
{
MMAL_STATUS_T status;
status = mmal_component_create(MMAL_COMPONENT_DEFAULT_VIDEO_RENDERER, &m_Renderer);
if (status != MMAL_SUCCESS) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"mmal_component_create() failed: %x (%s)",
status, mmal_status_to_string(status));
return false;
}
m_InputPort = m_Renderer->input[0];
m_InputPort->format->encoding = MMAL_ENCODING_OPAQUE;
m_InputPort->format->es->video.width = params->width;
m_InputPort->format->es->video.height = params->height;
m_InputPort->format->es->video.crop.x = 0;
m_InputPort->format->es->video.crop.y = 0;
m_InputPort->format->es->video.crop.width = params->width;
m_InputPort->format->es->video.crop.height = params->height;
status = mmal_port_format_commit(m_InputPort);
if (status != MMAL_SUCCESS) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"mmal_port_format_commit() failed: %x (%s)",
status, mmal_status_to_string(status));
return false;
}
status = mmal_component_enable(m_Renderer);
if (status != MMAL_SUCCESS) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"mmal_component_enable() failed: %x (%s)",
status, mmal_status_to_string(status));
return false;
}
{
MMAL_DISPLAYREGION_T dr;
dr.hdr.id = MMAL_PARAMETER_DISPLAYREGION;
dr.hdr.size = sizeof(MMAL_DISPLAYREGION_T);
dr.set = MMAL_DISPLAY_SET_LAYER;
dr.layer = 128;
dr.set |= MMAL_DISPLAY_SET_ALPHA;
dr.alpha = 255;
dr.set |= MMAL_DISPLAY_SET_FULLSCREEN;
dr.fullscreen = true;
{
SDL_Rect src, dst;
src.x = src.y = 0;
src.w = params->width;
src.h = params->height;
dst.x = dst.y = 0;
SDL_GetWindowSize(params->window, &dst.w, &dst.h);
StreamUtils::scaleSourceToDestinationSurface(&src, &dst);
dr.set |= MMAL_DISPLAY_SET_DEST_RECT;
dr.dest_rect.x = dst.x;
dr.dest_rect.y = dst.y;
dr.dest_rect.width = dst.w;
dr.dest_rect.height = dst.h;
}
}
status = mmal_port_enable(m_InputPort, InputPortCallback);
if (status != MMAL_SUCCESS) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"mmal_port_enable() failed: %x (%s)",
status, mmal_status_to_string(status));
return false;
}
return true;
}
void MmalRenderer::InputPortCallback(MMAL_PORT_T*, MMAL_BUFFER_HEADER_T* buffer)
{
mmal_buffer_header_release(buffer);
}
enum AVPixelFormat MmalRenderer::getPreferredPixelFormat(int videoFormat)
{
// Opaque MMAL buffers
SDL_assert(videoFormat == VIDEO_FORMAT_H264);
return AV_PIX_FMT_MMAL;
}
int MmalRenderer::getRendererAttributes()
{
// This renderer can only draw in full-screen and maxes out at 1080p
return RENDERER_ATTRIBUTE_FULLSCREEN_ONLY | RENDERER_ATTRIBUTE_1080P_MAX;
}
bool MmalRenderer::needsTestFrame()
{
// We won't be able to decode if the GPU memory is 64 MB or lower,
// so we must test before allowing the decoder to be used.
return true;
}
void MmalRenderer::renderFrame(AVFrame* frame)
{
if (frame == nullptr) {
// End of stream - nothing to do for us
return;
}
MMAL_BUFFER_HEADER_T* buffer = (MMAL_BUFFER_HEADER_T*)frame->data[3];
MMAL_STATUS_T status;
status = mmal_port_send_buffer(m_InputPort, buffer);
if (status != MMAL_SUCCESS) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"mmal_port_send_buffer() failed: %x (%s)",
status, mmal_status_to_string(status));
}
else {
// Prevent the buffer from being freed during av_frame_free()
// until rendering is complete. The reference is dropped in
// InputPortCallback().
mmal_buffer_header_acquire(buffer);
}
}
Loaded 100 of 532 files, more files were not shown because too many files have changed in this diff. Show more