Compare commits

..
1 Commits
313 changed files with 3419 additions and 10378 deletions
+2 -2
View File
@@ -1,10 +1,10 @@
---
name: Bug report
about: Ask for help on Discord before reporting a bug
about: Create a report to help us address issues in Moonlight
---
**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
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://discord.gg/MySTSdq
**Describe the bug**
A clear and concise description of what the bug is.
-1
View File
@@ -1 +0,0 @@
blank_issues_enabled: false
+1 -1
View File
@@ -1,4 +1,4 @@
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 />
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://discord.gg/MySTSdq) instead of GitHub issues. There are many more people available on Discord to help you and answer your questions.<br /><br />
This issue tracker should only be used for specific bugs or feature requests.<br /><br />
Thank you, and happy streaming!
+1 -1
View File
@@ -6,7 +6,7 @@
url = https://github.com/cgutman/qmdnsengine.git
[submodule "app/SDL_GameControllerDB"]
path = app/SDL_GameControllerDB
url = https://github.com/gabomdq/SDL_GameControllerDB.git
url = https://github.com/gabomdq/gamecontrollerdb.git
[submodule "soundio/libsoundio"]
path = soundio/libsoundio
url = https://github.com/andrewrk/libsoundio.git
+72
View File
@@ -0,0 +1,72 @@
language: cpp
git:
depth: 1
matrix:
include:
- os: osx
osx_image: xcode10.2
compiler: clang
env: TRAVIS_TARGET=osx
addons:
homebrew:
packages:
- qt
- os: linux
sudo: required
dist: xenial
compiler: gcc
env: TRAVIS_TARGET=steamlink
addons:
apt:
packages:
- libc6:i386
- libstdc++6:i386
- os: linux
sudo: required
dist: bionic
compiler: gcc
env: TRAVIS_TARGET=linux
addons:
apt:
sources:
- sourceline: 'ppa:savoury1/multimedia'
- sourceline: 'ppa:savoury1/ffmpeg4'
packages:
- qt5-default
- qtbase5-dev
- qtquickcontrols2-5-dev
- libqt5svg5-dev
- libgl1-mesa-dev
- libegl1-mesa-dev
- libxkbcommon-dev
- wayland-protocols
- libopus-dev
- libva-dev
- libvdpau-dev
- libsdl2-dev
- qtdeclarative5-dev
- libssl-dev
- libsdl2-ttf-dev
- libavcodec-dev
- libavutil-dev
install:
- '[ "$TRAVIS_TARGET" != osx ] || brew link --force qt'
- if [[ "$TRAVIS_TARGET" = "osx" ]]; then nvm use node && npm install --global create-dmg; fi
- if [[ "$TRAVIS_TARGET" = "steamlink" ]]; then git clone --depth=1 https://github.com/ValveSoftware/steamlink-sdk.git $HOME/steamlink-sdk; fi
before_script:
- '[ "$TRAVIS_TARGET" = "steamlink" ] || qmake -v'
- '[[ "$TRAVIS_TARGET" != linux || "$CXX" != clang++ ]] || export QMAKESPEC=linux-clang'
- '[[ "$TRAVIS_TARGET" != linux || "$CXX" != g++ ]] || export QMAKESPEC=linux-g++'
- '[ "$TRAVIS_TARGET" != osx ] || export QMAKESPEC=macx-clang'
- '[ "$TRAVIS_TARGET" != "steamlink" ] || export STEAMLINK_SDK_PATH=$HOME/steamlink-sdk'
script:
- '[ "$TRAVIS_TARGET" != "linux" ] || qmake moonlight-qt.pro -spec $QMAKESPEC'
- '[ "$TRAVIS_TARGET" != "linux" ] || make -j$(nproc) debug'
- '[ "$TRAVIS_TARGET" != "linux" ] || make -j$(nproc) release'
- '[ "$TRAVIS_TARGET" != "osx" ] || scripts/generate-dmg.sh Release'
- '[ "$TRAVIS_TARGET" != "steamlink" ] || scripts/build-steamlink-app.sh'
+5 -37
View File
@@ -131,22 +131,17 @@ private:
static LoadLibraryExWFunc s_RealLoadLibraryExW;
static constexpr LPCWSTR k_BlacklistedDlls[] = {
// These A-Volute DLLs shipped with various audio driver packages improperly handle
// D3D9 exclusive fullscreen in a way that causes CreateDeviceEx() to deadlock.
// https://github.com/moonlight-stream/moonlight-qt/issues/102
L"NahimicOSD.dll", // ASUS Sonic Radar 3
L"SSAudioOSD.dll", // SteelSeries headsets
L"SS2OSD.dll", // ASUS Sonic Studio 2
L"Nahimic2OSD.dll",
L"NahimicMSIOSD.dll",
L"nhAsusPhoebusOSD.dll" // ASUS Phoebus
// This DLL shipped with ASUS Sonic Radar 3 improperly handles
// D3D9 exclusive fullscreen in a way that causes CreateDeviceEx()
// to deadlock. https://github.com/moonlight-stream/moonlight-qt/issues/102
L"NahimicOSD.dll",
// 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.
// These are the 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
@@ -157,33 +152,6 @@ private:
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:
//
+13 -17
View File
@@ -4,33 +4,31 @@
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)
[![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)
[![Downloads](https://img.shields.io/github/downloads/moonlight-stream/moonlight-qt/total)](https://github.com/moonlight-stream/moonlight-qt/releases)
## Features
- Hardware accelerated video decoding on Windows, Mac, and Linux
- Supports streaming at up to 120 FPS (high refresh rate monitor recommended)
- Supports streaming at 720p, 1080p, 1440p, 4K, and the client PC's native screen resolution
- 7.1 surround sound audio support
- Support for both pointer capture (for games) and direct mouse control (for remote desktop)
- Direct touchscreen mouse control
- 5.1 surround sound audio
- HEVC support for better image quality at reduced bandwidth
- Keyboard and mouse support
- Gamepad support with force feedback for up to 4 players
## Downloads
- [Windows, macOS, and Steam Link](https://github.com/moonlight-stream/moonlight-qt/releases)
- [Snap (for Ubuntu-based Linux distros)](https://snapcraft.io/moonlight)
- [Flatpak (for other Linux distros)](https://flathub.org/apps/details/com.moonlight_stream.Moonlight)
- [AppImage](https://github.com/moonlight-stream/moonlight-qt/releases)
- [Raspberry Pi 4](https://github.com/moonlight-stream/moonlight-docs/wiki/Installing-Moonlight-Qt-on-Raspberry-Pi-4)
- [NVIDIA Jetson and Nintendo Switch (Ubuntu L4T)](https://github.com/moonlight-stream/moonlight-docs/wiki/Installing-Moonlight-Qt-on-Linux4Tegra-(L4T)-Ubuntu)
## Building
### General Build Requirements
* Qt 5.9 SDK or later
### Windows Build Requirements
* Qt 5.12.4 SDK or later
### Windows-specific Build Requirements
* Windows 7 or later
* [Visual Studio](https://visualstudio.microsoft.com/downloads/) 2017 (Community edition is fine)
* Select MSVC Desktop toolchain during Qt installation
@@ -38,19 +36,17 @@ You can follow development on our [Discord server](https://moonlight-stream.org/
* [7-Zip](https://www.7-zip.org/) (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
* Qt 5.14 SDK or later
* macOS High Sierra (10.13) or later
* Xcode 11
### Mac-specific Build Requirements
* macOS Sierra (10.12) or later
* Xcode 10
* [create-dmg](https://github.com/sindresorhus/create-dmg) (only if building DMGs for use on non-development Macs)
### Linux/Unix Build Requirements
* Qt 5.9 SDK or later
### Linux-specific Build Requirements
* 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`
* 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-specific 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
+19 -36
View File
@@ -46,7 +46,7 @@ win32 {
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 {
INCLUDEPATH += $$PWD/../libs/mac/include
@@ -59,6 +59,20 @@ unix:!macx {
CONFIG += link_pkgconfig
PKGCONFIG += openssl sdl2 SDL2_ttf opus
# SLAudio is used on Steam Link
!config_SL {
CONFIG += soundio
}
soundio {
packagesExist(libpulse) {
PKGCONFIG += libpulse
}
packagesExist(alsa) {
PKGCONFIG += alsa
}
}
packagesExist(libavcodec) {
PKGCONFIG += libavcodec libavutil
CONFIG += ffmpeg
@@ -90,16 +104,6 @@ unix:!macx {
CONFIG += libdrm
}
}
packagesExist(wayland-client) {
DEFINES += HAS_WAYLAND
PKGCONFIG += wayland-client
}
packagesExist(x11) {
DEFINES += HAS_X11
PKGCONFIG += x11
}
}
win32 {
LIBS += -llibssl -llibcrypto -lSDL2 -lSDL2_ttf -lavcodec -lavutil -lopus -ld3dx9
@@ -119,7 +123,6 @@ macx {
}
SOURCES += \
backend/nvapp.cpp \
main.cpp \
backend/computerseeker.cpp \
backend/identitymanager.cpp \
@@ -133,12 +136,7 @@ SOURCES += \
cli/quitstream.cpp \
cli/startstream.cpp \
settings/streamingpreferences.cpp \
streaming/input/abstouch.cpp \
streaming/input/gamepad.cpp \
streaming/input/input.cpp \
streaming/input/keyboard.cpp \
streaming/input/mouse.cpp \
streaming/input/reltouch.cpp \
streaming/input.cpp \
streaming/session.cpp \
streaming/audio/audio.cpp \
streaming/audio/renderers/sdlaud.cpp \
@@ -150,11 +148,9 @@ SOURCES += \
settings/mappingmanager.cpp \
gui/sdlgamepadkeynavigation.cpp \
streaming/video/overlaymanager.cpp \
backend/systemproperties.cpp \
wm.cpp
backend/systemproperties.cpp
HEADERS += \
backend/nvapp.h \
utils.h \
backend/computerseeker.h \
backend/identitymanager.h \
@@ -168,7 +164,7 @@ HEADERS += \
cli/quitstream.h \
cli/startstream.h \
settings/streamingpreferences.h \
streaming/input/input.h \
streaming/input.h \
streaming/session.h \
streaming/audio/renderers/renderer.h \
streaming/audio/renderers/sdl.h \
@@ -250,16 +246,6 @@ libdrm {
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)
@@ -401,10 +387,7 @@ macx {
APP_BUNDLE_FRAMEWORKS.files = $$files(../libs/mac/Frameworks/*.framework, true) $$files(../libs/mac/lib/*.dylib, true)
APP_BUNDLE_FRAMEWORKS.path = Contents/Frameworks
APP_BUNDLE_PLIST.files = $$OUT_PWD/Info.plist
APP_BUNDLE_PLIST.path = Contents
QMAKE_BUNDLE_DATA += APP_BUNDLE_RESOURCES APP_BUNDLE_FRAMEWORKS APP_BUNDLE_PLIST
QMAKE_BUNDLE_DATA += APP_BUNDLE_RESOURCES APP_BUNDLE_FRAMEWORKS
QMAKE_RPATHDIR += @executable_path/../Frameworks
}
+3 -10
View File
@@ -28,14 +28,9 @@ AutoUpdateChecker::AutoUpdateChecker(QObject *parent) :
void AutoUpdateChecker::start()
{
#if defined(Q_OS_WIN32) || defined(Q_OS_DARWIN) || defined(STEAM_LINK) || defined(APP_IMAGE) // Only run update checker on platforms without auto-update
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) && QT_VERSION < QT_VERSION_CHECK(5, 15, 1) && !defined(QT_NO_BEARERMANAGEMENT)
// HACK: Set network accessibility to work around QTBUG-80947 (introduced in Qt 5.14.0 and fixed in Qt 5.15.1)
QT_WARNING_PUSH
QT_WARNING_DISABLE_DEPRECATED
#if defined(Q_OS_WIN32) || defined(Q_OS_DARWIN) || defined(STEAM_LINK) // Only run update checker on platforms without auto-update
// HACK: Set network accessibility to work around QTBUG-80947
m_Nam.setNetworkAccessible(QNetworkAccessManager::Accessible);
QT_WARNING_POP
#endif
// We'll get a callback when this is finished
QUrl url("https://moonlight-stream.org/updates/qt.json");
@@ -53,10 +48,8 @@ void AutoUpdateChecker::parseStringToVersionQuad(QString& string, QVector<int>&
QString AutoUpdateChecker::getPlatform()
{
#if defined(STEAM_LINK)
#ifdef STEAM_LINK
return QStringLiteral("steamlink");
#elif defined(APP_IMAGE)
return QStringLiteral("appimage");
#else
return QSysInfo::productType();
#endif
+4 -18
View File
@@ -71,10 +71,10 @@ private:
QUrl BoxArtManager::loadBoxArt(NvComputer* computer, NvApp& app)
{
// Try to open the cached file if it exists and contains data
QFile cacheFile(getFilePathForBoxArt(computer, app.id));
if (cacheFile.exists() && cacheFile.size() > 0) {
return QUrl::fromLocalFile(cacheFile.fileName());
// Try to open the cached file
QString cacheFilePath = getFilePathForBoxArt(computer, app.id);
if (QFile::exists(cacheFilePath)) {
return QUrl::fromLocalFile(cacheFilePath);
}
// If we get here, we need to fetch asynchronously.
@@ -87,16 +87,6 @@ QUrl BoxArtManager::loadBoxArt(NvComputer* computer, NvApp& app)
return QUrl("qrc:/res/no_app_image.png");
}
void BoxArtManager::deleteBoxArt(NvComputer* computer)
{
QDir dir(Path::getBoxArtCacheDir());
// Delete everything in this computer's box art directory
if (dir.cd(computer->uuid)) {
dir.removeRecursively();
}
}
void BoxArtManager::handleBoxArtLoadComplete(NvComputer* computer, NvApp app, QUrl image)
{
if (!image.isEmpty()) {
@@ -119,10 +109,6 @@ QUrl BoxArtManager::loadBoxArtFromNetwork(NvComputer* computer, int appId)
if (image.save(cachePath)) {
return QUrl::fromLocalFile(cachePath);
}
else {
// A failed save() may leave a zero byte file. Make sure that's removed.
QFile(cachePath).remove();
}
}
return QUrl();
-4
View File
@@ -18,10 +18,6 @@ public:
QUrl
loadBoxArt(NvComputer* computer, NvApp& app);
static
void
deleteBoxArt(NvComputer* computer);
signals:
void
boxArtLoadComplete(NvComputer* computer, NvApp app, QUrl image);
+15 -71
View File
@@ -1,5 +1,4 @@
#include "computermanager.h"
#include "boxartmanager.h"
#include "nvhttp.h"
#include "settings/streamingpreferences.h"
@@ -33,7 +32,7 @@ private:
QString serverInfo;
try {
serverInfo = http.getServerInfo(NvHTTP::NvLogLevel::NVLL_NONE, true);
serverInfo = http.getServerInfo(NvHTTP::NvLogLevel::NVLL_NONE);
} catch (...) {
return false;
}
@@ -68,7 +67,12 @@ private:
}
QWriteLocker lock(&m_Computer->lock);
changed = m_Computer->updateAppList(appList);
if (m_Computer->appList != appList) {
m_Computer->appList = appList;
m_Computer->sortAppList();
changed = true;
}
return true;
}
@@ -80,7 +84,7 @@ private:
bool stateChanged = false;
bool online = false;
bool wasOnline = m_Computer->state == NvComputer::CS_ONLINE;
for (int i = 0; i < (wasOnline ? TRIES_BEFORE_OFFLINING : 1) && !online; i++) {
for (int i = 0; i < TRIES_BEFORE_OFFLINING && !online; i++) {
for (auto& address : m_Computer->uniqueAddresses()) {
if (isInterruptionRequested()) {
return;
@@ -205,10 +209,9 @@ void ComputerManager::saveHosts()
settings.remove(SER_HOSTS);
settings.beginWriteArray(SER_HOSTS);
int i = 0;
for (const NvComputer* computer : m_KnownHosts) {
settings.setArrayIndex(i++);
computer->serialize(settings);
for (int i = 0; i < m_KnownHosts.count(); i++) {
settings.setArrayIndex(i);
m_KnownHosts.value(m_KnownHosts.keys()[i])->serialize(settings);
}
settings.endArray();
}
@@ -395,9 +398,6 @@ public:
// Delete the polling entry first. This will stop all polling threads too.
delete pollingEntry;
// Delete cached box art
BoxArtManager::deleteBoxArt(m_Computer);
// Finally, delete the computer itself. This must be done
// last because the polling thread might be using it.
delete m_Computer;
@@ -415,28 +415,6 @@ void ComputerManager::deleteHost(NvComputer* 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::clientSideAttributeUpdated(NvComputer* computer)
{
// Persist the change
saveHosts();
// Notify the UI of the state change
handleComputerStateChanged(computer);
}
void ComputerManager::handleAboutToQuit()
{
QWriteLocker lock(&m_Lock);
@@ -607,7 +585,7 @@ public:
}
signals:
void computerAddCompleted(QVariant success, QVariant detectedPortBlocking);
void computerAddCompleted(QVariant success);
void computerStateChanged(NvComputer* computer);
@@ -637,20 +615,7 @@ private:
return serverInfo;
} catch (...) {
if (!m_Mdns) {
StreamingPreferences prefs;
int portTestResult;
if (prefs.detectNetworkBlocking) {
// We failed to connect to the specified PC. Let's test to make sure this network
// isn't blocking Moonlight, so we can tell the user about it.
portTestResult = LiTestClientConnectivity("qt.conntest.moonlight-stream.org", 443,
ML_PORT_FLAG_TCP_47984 | ML_PORT_FLAG_TCP_47989);
}
else {
portTestResult = 0;
}
emit computerAddCompleted(false, portTestResult != 0 && portTestResult != ML_TEST_RESULT_INCONCLUSIVE);
emit computerAddCompleted(false);
}
return QString();
}
@@ -728,12 +693,6 @@ private:
newComputer->manualAddress = m_Address;
}
QHostAddress hostAddress(m_Address);
bool addressIsSiteLocalV4 =
hostAddress.isInSubnet(QHostAddress("10.0.0.0"), 8) ||
hostAddress.isInSubnet(QHostAddress("172.16.0.0"), 12) ||
hostAddress.isInSubnet(QHostAddress("192.168.0.0"), 16);
{
// Check if this PC already exists
QWriteLocker lock(&m_ComputerManager->m_Lock);
@@ -748,7 +707,7 @@ private:
// For non-mDNS clients, let them know it succeeded
if (!m_Mdns) {
emit computerAddCompleted(true, false);
emit computerAddCompleted(true);
}
// Tell our client if something changed
@@ -767,24 +726,9 @@ private:
// Drop the lock before notifying
lock.unlock();
// If this wasn't added via mDNS but it is a RFC 1918 IPv4 address and not a VPN,
// go ahead and do the STUN request now to populate an external address.
if (!m_Mdns && addressIsSiteLocalV4 && !newComputer->isReachableOverVpn()) {
quint32 addr;
int err = LiFindExternalAddressIP4("stun.moonlight-stream.org", 3478, &addr);
if (err == 0) {
lock.relock();
newComputer->remoteAddress = QHostAddress(qFromBigEndian(addr)).toString();
lock.unlock();
}
else {
qWarning() << "STUN failed to get WAN address:" << err;
}
}
// For non-mDNS clients, let them know it succeeded
if (!m_Mdns) {
emit computerAddCompleted(true, false);
emit computerAddCompleted(true);
}
// Tell our client about this new PC
+1 -5
View File
@@ -179,16 +179,12 @@ public:
// computer is deleted inside this call
void deleteHost(NvComputer* computer);
void renameHost(NvComputer* computer, QString name);
void clientSideAttributeUpdated(NvComputer* computer);
signals:
void computerStateChanged(NvComputer* computer);
void pairingCompleted(NvComputer* computer, QString error);
void computerAddCompleted(QVariant success, QVariant detectedPortBlocking);
void computerAddCompleted(QVariant success);
void quitAppCompleted(QVariant error);
+14 -11
View File
@@ -5,6 +5,7 @@
#include <openssl/pem.h>
#include <openssl/rsa.h>
#include <openssl/bn.h>
#include <openssl/x509.h>
#include <openssl/rand.h>
@@ -31,19 +32,20 @@ void IdentityManager::createCredentials(QSettings& settings)
X509* cert = X509_new();
THROW_BAD_ALLOC_IF_NULL(cert);
EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, NULL);
THROW_BAD_ALLOC_IF_NULL(ctx);
EVP_PKEY_keygen_init(ctx);
EVP_PKEY_CTX_set_rsa_keygen_bits(ctx, 2048);
// pk must be initialized on input
EVP_PKEY* pk = NULL;
EVP_PKEY_keygen(ctx, &pk);
EVP_PKEY_CTX_free(ctx);
EVP_PKEY* pk = EVP_PKEY_new();
THROW_BAD_ALLOC_IF_NULL(pk);
BIGNUM* bne = BN_new();
THROW_BAD_ALLOC_IF_NULL(bne);
RSA* rsa = RSA_new();
THROW_BAD_ALLOC_IF_NULL(rsa);
BN_set_word(bne, RSA_F4);
RSA_generate_key_ex(rsa, 2048, bne, nullptr);
EVP_PKEY_assign_RSA(pk, rsa);
X509_set_version(cert, 2);
ASN1_INTEGER_set(X509_get_serialNumber(cert), 0);
#if OPENSSL_VERSION_NUMBER < 0x10100000L
@@ -92,6 +94,7 @@ void IdentityManager::createCredentials(QSettings& settings)
X509_free(cert);
EVP_PKEY_free(pk);
BN_free(bne);
BIO_free(biokey);
BIO_free(biocert);
-25
View File
@@ -1,25 +0,0 @@
#include "nvapp.h"
#define SER_APPNAME "name"
#define SER_APPID "id"
#define SER_APPHDR "hdr"
#define SER_APPCOLLECTOR "appcollector"
#define SER_HIDDEN "hidden"
NvApp::NvApp(QSettings& settings)
{
name = settings.value(SER_APPNAME).toString();
id = settings.value(SER_APPID).toInt();
hdrSupported = settings.value(SER_APPHDR).toBool();
isAppCollectorGame = settings.value(SER_APPCOLLECTOR).toBool();
hidden = settings.value(SER_HIDDEN).toBool();
}
void NvApp::serialize(QSettings& settings) const
{
settings.setValue(SER_APPNAME, name);
settings.setValue(SER_APPID, id);
settings.setValue(SER_APPHDR, hdrSupported);
settings.setValue(SER_APPCOLLECTOR, isAppCollectorGame);
settings.setValue(SER_HIDDEN, hidden);
}
-40
View File
@@ -1,40 +0,0 @@
#pragma once
#include <QSettings>
class NvApp
{
public:
NvApp() {}
explicit NvApp(QSettings& settings);
bool operator==(const NvApp& other) const
{
return id == other.id &&
name == other.name &&
hdrSupported == other.hdrSupported &&
isAppCollectorGame == other.isAppCollectorGame &&
hidden == other.hidden;
}
bool operator!=(const NvApp& other) const
{
return !operator==(other);
}
bool isInitialized()
{
return id != 0 && !name.isEmpty();
}
void
serialize(QSettings& settings) const;
int id = 0;
QString name;
bool hdrSupported = false;
bool isAppCollectorGame = false;
bool hidden = false;
};
Q_DECLARE_METATYPE(NvApp)
+19 -40
View File
@@ -1,5 +1,4 @@
#include "nvcomputer.h"
#include "nvapp.h"
#include <QUdpSocket>
#include <QHostInfo>
@@ -15,13 +14,15 @@
#define SER_IPV6ADDR "ipv6address"
#define SER_APPLIST "apps"
#define SER_SRVCERT "srvcert"
#define SER_CUSTOMNAME "customname"
#define SER_APPNAME "name"
#define SER_APPID "id"
#define SER_APPHDR "hdr"
NvComputer::NvComputer(QSettings& settings)
{
this->name = settings.value(SER_NAME).toString();
this->uuid = settings.value(SER_UUID).toString();
this->hasCustomName = settings.value(SER_CUSTOMNAME).toBool();
this->macAddress = settings.value(SER_MAC).toByteArray();
this->localAddress = settings.value(SER_LOCALADDR).toString();
this->remoteAddress = settings.value(SER_REMOTEADDR).toString();
@@ -31,9 +32,14 @@ NvComputer::NvComputer(QSettings& settings)
int appCount = settings.beginReadArray(SER_APPLIST);
for (int i = 0; i < appCount; i++) {
NvApp app;
settings.setArrayIndex(i);
NvApp app(settings);
app.name = settings.value(SER_APPNAME).toString();
app.id = settings.value(SER_APPID).toInt();
app.hdrSupported = settings.value(SER_APPHDR).toBool();
this->appList.append(app);
}
settings.endArray();
@@ -51,12 +57,11 @@ NvComputer::NvComputer(QSettings& settings)
this->gpuModel = nullptr;
}
void NvComputer::serialize(QSettings& settings) const
void NvComputer::serialize(QSettings& settings)
{
QReadLocker lock(&this->lock);
settings.setValue(SER_NAME, name);
settings.setValue(SER_CUSTOMNAME, hasCustomName);
settings.setValue(SER_UUID, uuid);
settings.setValue(SER_MAC, macAddress);
settings.setValue(SER_LOCALADDR, localAddress);
@@ -71,7 +76,10 @@ void NvComputer::serialize(QSettings& settings) const
settings.beginWriteArray(SER_APPLIST);
for (int i = 0; i < appList.count(); i++) {
settings.setArrayIndex(i);
appList[i].serialize(settings);
settings.setValue(SER_APPNAME, appList[i].name);
settings.setValue(SER_APPID, appList[i].id);
settings.setValue(SER_APPHDR, appList[i].hdrSupported);
}
settings.endArray();
}
@@ -88,7 +96,6 @@ NvComputer::NvComputer(QString address, QString serverInfo, QSslCertificate serv
{
this->serverCert = serverCert;
this->hasCustomName = false;
this->name = NvHTTP::getXmlString(serverInfo, "hostname");
if (this->name.isEmpty()) {
this->name = "UNKNOWN";
@@ -157,9 +164,8 @@ bool NvComputer::wake()
}
const quint16 WOL_PORTS[] = {
9, // Standard WOL port (privileged port)
7, 9, // Standard WOL ports
47998, 47999, 48000, 48002, 48010, // Ports opened by GFE
47009, // Port opened by Moonlight Internet Hosting Tool for WoL (non-privileged port)
};
// Create the WoL payload
@@ -307,26 +313,7 @@ bool NvComputer::isReachableOverVpn()
}
}
bool NvComputer::updateAppList(QVector<NvApp> newAppList) {
if (appList == newAppList) {
return false;
}
// Propagate client-side attributes to the new app list
for (const NvApp& existingApp : appList) {
for (NvApp& newApp : newAppList) {
if (existingApp.id == newApp.id) {
newApp.hidden = existingApp.hidden;
}
}
}
appList = newAppList;
sortAppList();
return true;
}
QVector<QString> NvComputer::uniqueAddresses() const
QVector<QString> NvComputer::uniqueAddresses()
{
QVector<QString> uniqueAddressList;
@@ -390,10 +377,7 @@ bool NvComputer::update(NvComputer& that)
changed = true; \
}
if (!hasCustomName) {
// Only overwrite the name if it's not custom
ASSIGN_IF_CHANGED(name);
}
ASSIGN_IF_CHANGED(name);
ASSIGN_IF_CHANGED_AND_NONEMPTY(macAddress);
ASSIGN_IF_CHANGED_AND_NONEMPTY(localAddress);
ASSIGN_IF_CHANGED_AND_NONEMPTY(remoteAddress);
@@ -409,12 +393,7 @@ bool NvComputer::update(NvComputer& that)
ASSIGN_IF_CHANGED(maxLumaPixelsHEVC);
ASSIGN_IF_CHANGED(gpuModel);
ASSIGN_IF_CHANGED_AND_NONNULL(serverCert);
ASSIGN_IF_CHANGED_AND_NONEMPTY(appList);
ASSIGN_IF_CHANGED_AND_NONEMPTY(displayModes);
if (!that.appList.isEmpty()) {
// updateAppList() handles merging client-side attributes
updateAppList(that.appList);
}
return changed;
}
+3 -6
View File
@@ -16,8 +16,6 @@ class NvComputer
private:
void sortAppList();
bool updateAppList(QVector<NvApp> newAppList);
bool pendingQuit;
public:
@@ -35,10 +33,10 @@ public:
isReachableOverVpn();
QVector<QString>
uniqueAddresses() const;
uniqueAddresses();
void
serialize(QSettings& settings) const;
serialize(QSettings& settings);
enum PairState
{
@@ -73,11 +71,10 @@ public:
QString manualAddress;
QByteArray macAddress;
QString name;
bool hasCustomName;
QString uuid;
QSslCertificate serverCert;
QVector<NvApp> appList;
// Synchronization
mutable QReadWriteLock lock;
QReadWriteLock lock;
};
+43 -38
View File
@@ -12,7 +12,6 @@
#include <QtEndian>
#include <QNetworkProxy>
#define FAST_FAIL_TIMEOUT_MS 2000
#define REQUEST_TIMEOUT_MS 5000
#define LAUNCH_TIMEOUT_MS 120000
#define RESUME_TIMEOUT_MS 30000
@@ -93,7 +92,7 @@ NvHTTP::getCurrentGame(QString serverInfo)
}
QString
NvHTTP::getServerInfo(NvLogLevel logLevel, bool fastFail)
NvHTTP::getServerInfo(NvLogLevel logLevel)
{
QString serverInfo;
@@ -107,7 +106,7 @@ NvHTTP::getServerInfo(NvLogLevel logLevel, bool fastFail)
serverInfo = openConnectionToString(m_BaseUrlHttps,
"serverinfo",
nullptr,
fastFail ? FAST_FAIL_TIMEOUT_MS : REQUEST_TIMEOUT_MS,
REQUEST_TIMEOUT_MS,
logLevel);
// Throws if the request failed
verifyResponseStatus(serverInfo);
@@ -120,7 +119,7 @@ NvHTTP::getServerInfo(NvLogLevel logLevel, bool fastFail)
serverInfo = openConnectionToString(m_BaseUrlHttp,
"serverinfo",
nullptr,
fastFail ? FAST_FAIL_TIMEOUT_MS : REQUEST_TIMEOUT_MS,
REQUEST_TIMEOUT_MS,
logLevel);
verifyResponseStatus(serverInfo);
}
@@ -137,7 +136,7 @@ NvHTTP::getServerInfo(NvLogLevel logLevel, bool fastFail)
serverInfo = openConnectionToString(m_BaseUrlHttp,
"serverinfo",
nullptr,
fastFail ? FAST_FAIL_TIMEOUT_MS : REQUEST_TIMEOUT_MS,
REQUEST_TIMEOUT_MS,
logLevel);
verifyResponseStatus(serverInfo);
}
@@ -145,6 +144,30 @@ NvHTTP::getServerInfo(NvLogLevel logLevel, bool fastFail)
return serverInfo;
}
static QString
getSurroundAudioInfoString(int audioConfig)
{
int channelMask;
int channelCount;
switch (audioConfig)
{
case AUDIO_CONFIGURATION_STEREO:
channelCount = 2;
channelMask = 0x3;
break;
case AUDIO_CONFIGURATION_51_SURROUND:
channelCount = 6;
channelMask = 0xFC;
break;
default:
Q_ASSERT(false);
return 0;
}
return QString::number(channelMask << 16 | channelCount);
}
void
NvHTTP::launchApp(int appId,
PSTREAM_CONFIGURATION streamConfig,
@@ -164,10 +187,9 @@ NvHTTP::launchApp(int appId,
"&mode="+QString::number(streamConfig->width)+"x"+
QString::number(streamConfig->height)+"x"+
// Using an FPS value over 60 causes SOPS to default to 720p60,
// so force it to 0 to ensure the correct resolution is set. We
// used to use 60 here but that locked the frame rate to 60 FPS
// on GFE 3.20.3.
QString::number(streamConfig->fps > 60 ? 0 : streamConfig->fps)+
// so force it to 60 when starting. This won't impact our ability
// to get > 60 FPS while actually streaming though.
QString::number(streamConfig->fps > 60 ? 60 : streamConfig->fps)+
"&additionalStates=1&sops="+QString::number(sops ? 1 : 0)+
"&rikey="+QByteArray(streamConfig->remoteInputAesKey, sizeof(streamConfig->remoteInputAesKey)).toHex()+
"&rikeyid="+QString::number(riKeyId)+
@@ -175,7 +197,7 @@ NvHTTP::launchApp(int appId,
"&hdrMode=1&clientHdrCapVersion=0&clientHdrCapSupportedFlagsInUint32=0&clientHdrCapMetaDataId=NV_STATIC_METADATA_TYPE_1&clientHdrCapDisplayData=0x0x0x0x0x0x0x0x0x0x0" :
"")+
"&localAudioPlayMode="+QString::number(localAudio ? 1 : 0)+
"&surroundAudioInfo="+QString::number(SURROUNDAUDIOINFO_FROM_AUDIO_CONFIGURATION(streamConfig->audioConfiguration))+
"&surroundAudioInfo="+getSurroundAudioInfoString(streamConfig->audioConfiguration)+
"&remoteControllersBitmap="+QString::number(gamepadMask)+
"&gcmap="+QString::number(gamepadMask),
LAUNCH_TIMEOUT_MS);
@@ -197,7 +219,7 @@ NvHTTP::resumeApp(PSTREAM_CONFIGURATION streamConfig)
"resume",
"rikey="+QString(QByteArray(streamConfig->remoteInputAesKey, sizeof(streamConfig->remoteInputAesKey)).toHex())+
"&rikeyid="+QString::number(riKeyId)+
"&surroundAudioInfo="+QString::number(SURROUNDAUDIOINFO_FROM_AUDIO_CONFIGURATION(streamConfig->audioConfiguration)),
"&surroundAudioInfo="+getSurroundAudioInfoString(streamConfig->audioConfiguration),
RESUME_TIMEOUT_MS);
// Throws if the request failed
@@ -234,16 +256,16 @@ NvHTTP::getDisplayModeList(QString serverInfo)
while (!xmlReader.atEnd()) {
while (xmlReader.readNextStartElement()) {
QStringRef name = xmlReader.name();
if (name == "DisplayMode") {
if (xmlReader.name() == "DisplayMode") {
modes.append(NvDisplayMode());
}
else if (name == "Width") {
else if (xmlReader.name() == "Width") {
modes.last().width = xmlReader.readElementText().toInt();
}
else if (name == "Height") {
else if (xmlReader.name() == "Height") {
modes.last().height = xmlReader.readElementText().toInt();
}
else if (name == "RefreshRate") {
else if (xmlReader.name() == "RefreshRate") {
modes.last().refreshRate = xmlReader.readElementText().toInt();
}
}
@@ -267,7 +289,7 @@ NvHTTP::getAppList()
while (!xmlReader.atEnd()) {
while (xmlReader.readNextStartElement()) {
QStringRef name = xmlReader.name();
if (name == "App") {
if (xmlReader.name() == "App") {
// We must have a valid app before advancing to the next one
if (!apps.isEmpty() && !apps.last().isInitialized()) {
qWarning() << "Invalid applist XML";
@@ -276,18 +298,15 @@ NvHTTP::getAppList()
}
apps.append(NvApp());
}
else if (name == "AppTitle") {
else if (xmlReader.name() == "AppTitle") {
apps.last().name = xmlReader.readElementText();
}
else if (name == "ID") {
else if (xmlReader.name() == "ID") {
apps.last().id = xmlReader.readElementText().toInt();
}
else if (name == "IsHdrSupported") {
else if (xmlReader.name() == "IsHdrSupported") {
apps.last().hdrSupported = xmlReader.readElementText() == "1";
}
else if (name == "IsAppCollectorGame") {
apps.last().isAppCollectorGame = xmlReader.readElementText() == "1";
}
}
}
@@ -303,10 +322,7 @@ NvHTTP::verifyResponseStatus(QString xml)
{
if (xmlReader.name() == "root")
{
// Status code can be 0xFFFFFFFF in some rare cases on GFE 3.20.3, and
// QString::toInt() will fail in that case, so use QString::toUInt()
// and cast the result to an int instead.
int statusCode = (int)xmlReader.attributes().value("status_code").toUInt();
int statusCode = xmlReader.attributes().value("status_code").toInt();
if (statusCode == 200)
{
// Successful
@@ -319,12 +335,6 @@ NvHTTP::verifyResponseStatus(QString xml)
// 401 is expected for unpaired PCs when we fetch serverinfo over HTTPS
qWarning() << "Request failed:" << statusCode << statusMessage;
}
if (statusCode == -1 && statusMessage == "Invalid") {
// Special case handling an audio capture error which GFE doesn't
// provide any useful status message for.
statusCode = 418;
statusMessage = "Missing audio capture device. Reinstalling GeForce Experience should resolve this error.";
}
throw GfeHttpResponseException(statusCode, statusMessage);
}
}
@@ -444,13 +454,8 @@ NvHTTP::openConnection(QUrl baseUrl,
// Add our client certificate
request.setSslConfiguration(IdentityManager::get()->getSslConfig());
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) && QT_VERSION < QT_VERSION_CHECK(5, 15, 1) && !defined(QT_NO_BEARERMANAGEMENT)
// HACK: Set network accessibility to work around QTBUG-80947 (introduced in Qt 5.14.0 and fixed in Qt 5.15.1)
QT_WARNING_PUSH
QT_WARNING_DISABLE_DEPRECATED
// HACK: Set reachability to work around QTBUG-80947
m_Nam.setNetworkAccessible(QNetworkAccessManager::Accessible);
QT_WARNING_POP
#endif
QNetworkReply* reply = m_Nam.get(request);
+21 -2
View File
@@ -1,7 +1,6 @@
#pragma once
#include "identitymanager.h"
#include "nvapp.h"
#include <Limelight.h>
@@ -9,6 +8,26 @@
#include <QNetworkAccessManager>
#include <QNetworkReply>
class NvApp
{
public:
bool operator==(const NvApp& other) const
{
return id == other.id;
}
bool isInitialized()
{
return id != 0 && !name.isEmpty();
}
int id;
QString name;
bool hdrSupported;
};
Q_DECLARE_METATYPE(NvApp)
class NvDisplayMode
{
public:
@@ -112,7 +131,7 @@ public:
getCurrentGame(QString serverInfo);
QString
getServerInfo(NvLogLevel logLevel, bool fastFail = false);
getServerInfo(NvLogLevel logLevel);
static
void
+27 -55
View File
@@ -1,9 +1,8 @@
#include "nvpairingmanager.h"
#include "utils.h"
#include <stdexcept>
#include <openssl/bio.h>
#include <openssl/aes.h>
#include <openssl/rand.h>
#include <openssl/pem.h>
#include <openssl/x509.h>
@@ -52,63 +51,39 @@ NvPairingManager::generateRandomBytes(int length)
}
QByteArray
NvPairingManager::encrypt(const QByteArray& plaintext, const QByteArray& key)
NvPairingManager::encrypt(QByteArray plaintext, AES_KEY* key)
{
QByteArray ciphertext(plaintext.size(), 0);
EVP_CIPHER_CTX* cipher;
int ciphertextLen;
cipher = EVP_CIPHER_CTX_new();
THROW_BAD_ALLOC_IF_NULL(cipher);
EVP_EncryptInit(cipher, EVP_aes_128_ecb(), reinterpret_cast<const unsigned char*>(key.data()), NULL);
EVP_CIPHER_CTX_set_padding(cipher, 0);
EVP_EncryptUpdate(cipher,
reinterpret_cast<unsigned char*>(ciphertext.data()),
&ciphertextLen,
reinterpret_cast<const unsigned char*>(plaintext.data()),
plaintext.length());
Q_ASSERT(ciphertextLen == ciphertext.length());
EVP_CIPHER_CTX_free(cipher);
for (int i = 0; i < plaintext.size(); i += 16)
{
AES_encrypt(reinterpret_cast<unsigned char*>(&plaintext.data()[i]),
reinterpret_cast<unsigned char*>(&ciphertext.data()[i]),
key);
}
return ciphertext;
}
QByteArray
NvPairingManager::decrypt(const QByteArray& ciphertext, const QByteArray& key)
NvPairingManager::decrypt(QByteArray ciphertext, AES_KEY* key)
{
QByteArray plaintext(ciphertext.size(), 0);
EVP_CIPHER_CTX* cipher;
int plaintextLen;
cipher = EVP_CIPHER_CTX_new();
THROW_BAD_ALLOC_IF_NULL(cipher);
EVP_DecryptInit(cipher, EVP_aes_128_ecb(), reinterpret_cast<const unsigned char*>(key.data()), NULL);
EVP_CIPHER_CTX_set_padding(cipher, 0);
EVP_DecryptUpdate(cipher,
reinterpret_cast<unsigned char*>(plaintext.data()),
&plaintextLen,
reinterpret_cast<const unsigned char*>(ciphertext.data()),
ciphertext.length());
Q_ASSERT(plaintextLen == plaintext.length());
EVP_CIPHER_CTX_free(cipher);
for (int i = 0; i < plaintext.size(); i += 16)
{
AES_decrypt(reinterpret_cast<unsigned char*>(&ciphertext.data()[i]),
reinterpret_cast<unsigned char*>(&plaintext.data()[i]),
key);
}
return plaintext;
}
QByteArray
NvPairingManager::getSignatureFromPemCert(const QByteArray& certificate)
NvPairingManager::getSignatureFromPemCert(QByteArray certificate)
{
#if (OPENSSL_VERSION_NUMBER < 0x10100000L)
BIO* bio = BIO_new_mem_buf(const_cast<char*>(certificate.data()), -1);
#else
BIO* bio = BIO_new_mem_buf(certificate.data(), -1);
#endif
THROW_BAD_ALLOC_IF_NULL(bio);
X509* cert = PEM_read_bio_X509(bio, nullptr, nullptr, nullptr);
@@ -132,13 +107,9 @@ NvPairingManager::getSignatureFromPemCert(const QByteArray& certificate)
}
bool
NvPairingManager::verifySignature(const QByteArray& data, const QByteArray& signature, const QByteArray& serverCertificate)
NvPairingManager::verifySignature(QByteArray data, QByteArray signature, QByteArray serverCertificate)
{
#if (OPENSSL_VERSION_NUMBER < 0x10100000L)
BIO* bio = BIO_new_mem_buf(const_cast<char*>(serverCertificate.data()), -1);
#else
BIO* bio = BIO_new_mem_buf(serverCertificate.data(), -1);
#endif
THROW_BAD_ALLOC_IF_NULL(bio);
X509* cert = PEM_read_bio_X509(bio, nullptr, nullptr, nullptr);
@@ -152,7 +123,7 @@ NvPairingManager::verifySignature(const QByteArray& data, const QByteArray& sign
EVP_DigestVerifyInit(mdctx, nullptr, EVP_sha256(), nullptr, pubKey);
EVP_DigestVerifyUpdate(mdctx, data.data(), data.length());
int result = EVP_DigestVerifyFinal(mdctx, reinterpret_cast<unsigned char*>(const_cast<char*>(signature.data())), signature.length());
int result = EVP_DigestVerifyFinal(mdctx, reinterpret_cast<unsigned char*>(signature.data()), signature.length());
EVP_PKEY_free(pubKey);
EVP_MD_CTX_destroy(mdctx);
@@ -162,7 +133,7 @@ NvPairingManager::verifySignature(const QByteArray& data, const QByteArray& sign
}
QByteArray
NvPairingManager::signMessage(const QByteArray& message)
NvPairingManager::signMessage(QByteArray message)
{
EVP_MD_CTX *ctx = EVP_MD_CTX_create();
THROW_BAD_ALLOC_IF_NULL(ctx);
@@ -172,7 +143,7 @@ NvPairingManager::signMessage(const QByteArray& message)
EVP_DigestInit_ex(ctx, md, NULL);
EVP_DigestSignInit(ctx, NULL, md, NULL, m_PrivateKey);
EVP_DigestSignUpdate(ctx, reinterpret_cast<unsigned char*>(const_cast<char*>(message.data())), message.length());
EVP_DigestSignUpdate(ctx, reinterpret_cast<unsigned char*>(message.data()), message.length());
size_t signatureLength = 0;
EVP_DigestSignFinal(ctx, NULL, &signatureLength);
@@ -186,7 +157,7 @@ NvPairingManager::signMessage(const QByteArray& message)
}
QByteArray
NvPairingManager::saltPin(const QByteArray& salt, QString pin)
NvPairingManager::saltPin(QByteArray salt, QString pin)
{
return QByteArray().append(salt).append(pin.toLatin1());
}
@@ -215,8 +186,9 @@ NvPairingManager::pair(QString appVersion, QString pin, QSslCertificate& serverC
QByteArray salt = generateRandomBytes(16);
QByteArray saltedPin = saltPin(salt, pin);
QByteArray aesKey = QCryptographicHash::hash(saltedPin, hashAlgo).data();
aesKey.truncate(16);
AES_KEY encKey, decKey;
AES_set_decrypt_key(reinterpret_cast<const unsigned char*>(QCryptographicHash::hash(saltedPin, hashAlgo).data()), 128, &decKey);
AES_set_encrypt_key(reinterpret_cast<const unsigned char*>(QCryptographicHash::hash(saltedPin, hashAlgo).data()), 128, &encKey);
QString getCert = m_Http.openConnectionToString(m_Http.m_BaseUrlHttp,
"pair",
@@ -251,7 +223,7 @@ NvPairingManager::pair(QString appVersion, QString pin, QSslCertificate& serverC
m_Http.setServerCert(serverCert);
QByteArray randomChallenge = generateRandomBytes(16);
QByteArray encryptedChallenge = encrypt(randomChallenge, aesKey);
QByteArray encryptedChallenge = encrypt(randomChallenge, &encKey);
QString challengeXml = m_Http.openConnectionToString(m_Http.m_BaseUrlHttp,
"pair",
"devicename=roth&updateState=1&clientchallenge=" +
@@ -265,7 +237,7 @@ NvPairingManager::pair(QString appVersion, QString pin, QSslCertificate& serverC
return PairState::FAILED;
}
QByteArray challengeResponseData = decrypt(m_Http.getXmlStringFromHex(challengeXml, "challengeresponse"), aesKey);
QByteArray challengeResponseData = decrypt(m_Http.getXmlStringFromHex(challengeXml, "challengeresponse"), &decKey);
QByteArray clientSecretData = generateRandomBytes(16);
QByteArray challengeResponse;
QByteArray serverResponse(challengeResponseData.data(), hashLength);
@@ -286,7 +258,7 @@ NvPairingManager::pair(QString appVersion, QString pin, QSslCertificate& serverC
QByteArray paddedHash = QCryptographicHash::hash(challengeResponse, hashAlgo);
paddedHash.resize(32);
QByteArray encryptedChallengeResponseHash = encrypt(paddedHash, aesKey);
QByteArray encryptedChallengeResponseHash = encrypt(paddedHash, &encKey);
QString respXml = m_Http.openConnectionToString(m_Http.m_BaseUrlHttp,
"pair",
"devicename=roth&updateState=1&serverchallengeresp=" +
+7 -6
View File
@@ -3,6 +3,7 @@
#include "identitymanager.h"
#include "nvhttp.h"
#include <openssl/aes.h>
#include <openssl/x509.h>
#include <openssl/evp.h>
@@ -29,22 +30,22 @@ private:
generateRandomBytes(int length);
QByteArray
saltPin(const QByteArray& salt, QString pin);
saltPin(QByteArray salt, QString pin);
QByteArray
encrypt(const QByteArray& plaintext, const QByteArray& key);
encrypt(QByteArray plaintext, AES_KEY* key);
QByteArray
decrypt(const QByteArray& ciphertext, const QByteArray& key);
decrypt(QByteArray ciphertext, AES_KEY* key);
QByteArray
getSignatureFromPemCert(const QByteArray& certificate);
getSignatureFromPemCert(QByteArray certificate);
bool
verifySignature(const QByteArray& data, const QByteArray& signature, const QByteArray& serverCertificate);
verifySignature(QByteArray data, QByteArray signature, QByteArray serverCertificate);
QByteArray
signMessage(const QByteArray& message);
signMessage(QByteArray message);
NvHTTP m_Http;
X509* m_Cert;
-3
View File
@@ -24,9 +24,6 @@ RichPresenceManager::RichPresenceManager(StreamingPreferences& prefs, QString ga
discordPresence.largeImageKey = "icon";
Discord_UpdatePresence(&discordPresence);
}
#else
Q_UNUSED(prefs)
Q_UNUSED(gameName)
#endif
}
+16 -38
View File
@@ -1,5 +1,4 @@
#include "systemproperties.h"
#include "utils.h"
#include <QGuiApplication>
@@ -8,10 +7,8 @@
SystemProperties::SystemProperties()
{
versionString = QString(VERSION_STR);
hasWindowManager = WMUtils::isRunningWindowManager();
isRunningWayland = WMUtils::isRunningWayland();
isRunningXWayland = isRunningWayland && QGuiApplication::platformName() == "xcb";
isRunningWayland = qgetenv("XDG_SESSION_TYPE") == "wayland";
isRunningXWayland = qgetenv("XDG_SESSION_TYPE") == "wayland" && QGuiApplication::platformName() == "xcb";
#ifdef Q_OS_WIN32
isWow64 = QSysInfo::currentCpuArchitecture() != QSysInfo::buildCpuArchitecture();
@@ -20,8 +17,7 @@ SystemProperties::SystemProperties()
#endif
#ifndef STEAM_LINK
// Assume we can probably launch a browser if we're in a GUI environment
hasBrowser = hasWindowManager;
hasBrowser = true;
#else
hasBrowser = false;
#endif
@@ -78,14 +74,7 @@ void SystemProperties::querySdlVideoInfo()
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);
}
monitorDesktopResolutions.insert(displayIndex, QRect(0, 0, desktopMode.w, desktopMode.h));
}
else {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
@@ -94,14 +83,7 @@ void SystemProperties::querySdlVideoInfo()
}
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);
}
monitorNativeResolutions.insert(displayIndex, QRect(0, 0, desktopMode.w, desktopMode.h));
// Start at desktop mode and work our way up
bestMode = desktopMode;
@@ -120,24 +102,20 @@ void SystemProperties::querySdlVideoInfo()
}
}
SDL_Window* testWindow = SDL_CreateWindow("", 0, 0, 1280, 720,
SDL_WINDOW_HIDDEN | StreamUtils::getPlatformWindowFlags());
SDL_Window* testWindow = SDL_CreateWindow("", 0, 0, 1280, 720, SDL_WINDOW_HIDDEN);
if (!testWindow) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Failed to create test window with platform flags: %s",
SDL_GetError());
testWindow = SDL_CreateWindow("", 0, 0, 1280, 720, SDL_WINDOW_HIDDEN);
if (!testWindow) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to create window for hardware decode test: %s",
SDL_GetError());
SDL_QuitSubSystem(SDL_INIT_VIDEO);
return;
}
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);
hasHardwareAcceleration =
Session::isHardwareDecodeAvailable(testWindow,
StreamingPreferences::VDS_AUTO,
VIDEO_FORMAT_H264,
1920, 1080, 60);
SDL_DestroyWindow(testWindow);
-8
View File
@@ -11,17 +11,13 @@ 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);
@@ -33,18 +29,14 @@ 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;
};
+13 -42
View File
@@ -172,31 +172,20 @@ GlobalCommandLineParser::ParseResult GlobalCommandLineParser::parse(const QStrin
parser.addPositionalArgument("action", "Action to execute", "<action>");
parser.parse(args);
auto posArgs = parser.positionalArguments();
QString action = posArgs.isEmpty() ? QString() : posArgs.first().toLower();
if (posArgs.isEmpty()) {
if (action == "") {
// This method will not return and terminates the process if --version
// or --help is specified
parser.handleHelpAndVersionOptions();
parser.handleUnknownOptions();
return NormalStartRequested;
}
else {
// If users supply arguments that accept values prior to the "quit"
// or "stream" positional arguments, we will not be able to correctly
// parse the value out of the input because this QCommandLineParser
// doesn't know about all of the options that "quit" and "stream"
// commands can accept. To work around this issue, we just look
// for "quit" or "stream" positional arguments anywhere.
for (int i = 0; i < posArgs.size(); i++) {
QString action = posArgs.at(i).toLower();
if (action == "quit") {
return QuitRequested;
} else if (action == "stream") {
return StreamRequested;
}
}
parser.showError(QString("Invalid action"));
} else if (action == "quit") {
return QuitRequested;
} else if (action == "stream") {
return StreamRequested;
} else {
parser.showError(QString("Invalid action: %1").arg(action));
}
}
@@ -252,7 +241,6 @@ StreamCommandLineParser::StreamCommandLineParser()
m_AudioConfigMap = {
{"stereo", StreamingPreferences::AC_STEREO},
{"5.1-surround", StreamingPreferences::AC_51_SURROUND},
{"7.1-surround", StreamingPreferences::AC_71_SURROUND},
};
m_VideoCodecMap = {
{"auto", StreamingPreferences::VCC_AUTO},
@@ -261,8 +249,8 @@ StreamCommandLineParser::StreamCommandLineParser()
};
m_VideoDecoderMap = {
{"auto", StreamingPreferences::VDS_AUTO},
{"software", StreamingPreferences::VDS_FORCE_SOFTWARE},
{"hardware", StreamingPreferences::VDS_FORCE_HARDWARE},
{"software", StreamingPreferences::VDS_FORCE_HARDWARE},
{"hardware", StreamingPreferences::VDS_FORCE_SOFTWARE},
};
}
@@ -292,14 +280,11 @@ void StreamCommandLineParser::parse(const QStringList &args, StreamingPreference
parser.addToggleOption("vsync", "V-Sync");
parser.addValueOption("fps", "FPS");
parser.addValueOption("bitrate", "bitrate in Kbps");
parser.addValueOption("packet-size", "video packet size");
parser.addChoiceOption("display-mode", "display mode", m_WindowModeMap.keys());
parser.addChoiceOption("audio-config", "audio config", m_AudioConfigMap.keys());
parser.addToggleOption("multi-controller", "multiple controller support");
parser.addToggleOption("quit-after", "quit app after session");
parser.addToggleOption("absolute-mouse", "remote desktop optimized mouse control");
parser.addToggleOption("mouse-buttons-swap", "left and right mouse buttons swap");
parser.addToggleOption("touchscreen-trackpad", "touchscreen in trackpad mode");
parser.addToggleOption("mouse-acceleration", "mouse acceleration");
parser.addToggleOption("game-optimization", "game optimizations");
parser.addToggleOption("audio-on-host", "audio on host PC");
parser.addToggleOption("frame-pacing", "frame pacing");
@@ -360,14 +345,6 @@ void StreamCommandLineParser::parse(const QStringList &args, StreamingPreference
preferences->width, preferences->height, preferences->fps);
}
// Resolve --packet-size option
if (parser.isSet("packet-size")) {
preferences->packetSize = parser.getIntOption("packet-size");
if (preferences->packetSize < 1024) {
parser.showError("Packet size must be greater than 1024 bytes");
}
}
// Resolve --display option
if (parser.isSet("display-mode")) {
preferences->windowMode = mapValue(m_WindowModeMap, parser.getChoiceOptionValue("display-mode"));
@@ -387,14 +364,8 @@ void StreamCommandLineParser::parse(const QStringList &args, StreamingPreference
// Resolve --quit-after and --no-quit-after options
preferences->quitAppAfter = parser.getToggleOptionValue("quit-after", preferences->quitAppAfter);
// Resolve --absolute-mouse and --no-absolute-mouse options
preferences->absoluteMouseMode = parser.getToggleOptionValue("absolute-mouse", preferences->absoluteMouseMode);
// Resolve --mouse-buttons-swap and --no-mouse-buttons-swap options
preferences->swapMouseButtons = parser.getToggleOptionValue("mouse-buttons-swap", preferences->swapMouseButtons);
// Resolve --touchscreen-trackpad and --no-touchscreen-trackpad options
preferences->absoluteTouchMode = !parser.getToggleOptionValue("touchscreen-trackpad", !preferences->absoluteTouchMode);
// Resolve --mouse-acceleration and --no-mouse-acceleration options
preferences->mouseAcceleration = parser.getToggleOptionValue("mouse-acceleration", preferences->mouseAcceleration);
// Resolve --game-optimization and --no-game-optimization options
preferences->gameOptimizations = parser.getToggleOptionValue("game-optimization", preferences->gameOptimizations);
@@ -7,15 +7,13 @@
<summary>Play games remotely from your NVIDIA GameStream-enabled PC</summary>
<description>
<p>Moonlight can stream games and other applications from a PC with an NVIDIA GeForce GTX/RTX or NVIDIA Quadro GPU and GeForce/Quadro Experience installed.</p>
<p>Moonlight can stream games and other applications from a PC with an NVIDIA GeForce GTX 600-series or higher GPU and GeForce Experience installed.</p>
<p>Features include:</p>
<ul>
<li>Streaming at up to 4K resolution</li>
<li>Support for up to 120 FPS streaming (high refresh rate monitor recommended)</li>
<li>Hardware accelerated video decoding with VAAPI, VDPAU, and NVDEC support</li>
<li>7.1 surround sound audio support</li>
<li>Support for both pointer capture (for games) and direct mouse control (for remote desktop)</li>
<li>Direct touchscreen mouse control</li>
<li>5.1 surround sound audio support</li>
<li>HEVC support for better video compression efficiency</li>
<li>Gamepad support with force feedback for up to 4 players</li>
</ul>
@@ -35,76 +33,6 @@
</screenshots>
<releases>
<release version="2.2.0" date="2020-09-10">
<description>
<p>New features:</p>
<ul>
<li>Added the ability to hide unwanted apps and games</li>
<li>Added a network test to determine if your Internet connection is blocking Moonlight</li>
<li>Added the option to swap mouse buttons</li>
<li>Added codec and resolution to the on-screen performance stats</li>
<li>Added a warning if the connection is too unstable to receive any complete video frames</li>
<li>Added a Discord link to the toolbar in settings</li>
<li>Enabled the new EGL renderer by default on Linux</li>
</ul>
<p>Bugfixes:</p>
<ul>
<li>Fixed streaming over networks that block QoS-tagged packets</li>
<li>Selecting a frame rate higher than 60 FPS no longer sets the host desktop to 720p</li>
<li>Right-side modifier keys no longer press the corresponding left-side keys too</li>
<li>Fixed mouse position never reaching the edge of the screen with certain desktop resolutions</li>
<li>Improved synchronization between host and client mouse cursor in remote desktop mouse mode</li>
<li>Cached box art is now deleted when the PC is removed from Moonlight</li>
<li>Fixed several CLI options not properly overriding GUI options</li>
<li>Updated included gamepad mappings</li>
<li>Updated to Qt 5.15.1</li>
</ul>
</description>
</release>
<release version="2.1.0" date="2020-05-17">
<description>
<p>New features:</p>
<ul>
<li>Remote desktop mode now allows interaction with the host without having to click to focus the Moonlight window first</li>
<li>Added a new EGL renderer for improved performance on Wayland and XWayland</li>
<li>Added the option to toggle between direct touchscreen mode and the old trackpad mode</li>
<li>Added the option to rename host PCs</li>
<li>Added the ability to scroll the settings page with the right analog stick</li>
<li>Various UI improvements</li>
</ul>
<p>Bugfixes:</p>
<ul>
<li>Fixed unreliable mouse capture when the stream begins</li>
<li>Fixed cursor being trapped in full-screen mode even when the mouse wasn't captured</li>
<li>Fixed key presses not being sent to the host if the Super/Meta/Windows key is pressed</li>
<li>Updated included gamepad mappings</li>
</ul>
</description>
</release>
<release version="2.0.0" date="2020-04-28">
<description>
<p>New features:</p>
<ul>
<li>Remote desktop-optimized mouse mode which doesn't require capturing the mouse cursor</li>
<li>Touchscreen input now directly controls the mouse cursor</li>
<li>7.1 surround sound support</li>
</ul>
<p>Bugfixes:</p>
<ul>
<li>Improved performance on some lossy networks</li>
<li>Fixed disconnect delay when streaming from GeForce Experience 3.20.3.63</li>
<li>Fixed frame rate not exceeding 60 when streaming from GeForce Experience 3.20.3.63</li>
<li>Fixed parsing of command-line options when passing flags prior to the action</li>
<li>Fixed an issue that may cause excessive mDNS network traffic while Moonlight is running</li>
<li>Fixed hardware decoding on Intel GPUs running Mesa 20.0 and/or the Iris driver</li>
<li>Fixed hardware decoding on Comet Lake and later Intel GPUs using the iHD VA-API driver</li>
<li>Fixed hardware decoding on AMD Radeon GPUs with XWayland</li>
<li>Fixed a crash when using the iHD VA-API driver on Wayland</li>
<li>Fixed delay after exiting the stream on Wayland</li>
<li>Updated included gamepad mappings</li>
</ul>
</description>
</release>
<release version="1.2.0" date="2019-12-17">
<description>
<p>Bugfixes:</p>
+1 -11
View File
@@ -5,19 +5,9 @@
# 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
exec nice -n -10 ./bin/moonlight > /tmp/moonlight.log
+17 -35
View File
@@ -9,7 +9,6 @@ CenteredGridView {
property int computerIndex
property AppModel appModel : createModel()
property bool activated
property bool showHiddenGames
id: appGrid
focus: true
@@ -49,7 +48,7 @@ CenteredGridView {
function createModel()
{
var model = Qt.createQmlObject('import AppModel 1.0; AppModel {}', parent, '')
model.initialize(ComputerManager, computerIndex, showHiddenGames)
model.initialize(ComputerManager, computerIndex)
return model
}
@@ -59,9 +58,6 @@ CenteredGridView {
width: 220; height: 287;
grid: appGrid
// Dim the app if it's hidden
opacity: model.hidden ? 0.4 : 1.0
Image {
property bool isPlaceholder: false
@@ -169,8 +165,8 @@ CenteredGridView {
function launchOrResumeSelectedApp()
{
var runningId = appModel.getRunningAppId()
if (runningId !== 0 && runningId !== model.appid) {
var runningIndex = appModel.getRunningAppIndex()
if (runningIndex >= 0 && runningIndex !== index) {
quitAppDialog.appName = appModel.getRunningAppName()
quitAppDialog.segueToStream = true
quitAppDialog.nextAppName = model.name
@@ -194,25 +190,6 @@ CenteredGridView {
}
}
onPressAndHold: {
// popup() ensures the menu appears under the mouse cursor
if (appContextMenu.popup) {
appContextMenu.popup()
}
else {
// Qt 5.9 doesn't have popup()
appContextMenu.open()
}
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.RightButton;
onClicked: {
parent.onPressAndHold()
}
}
Keys.onReturnPressed: {
// Open the app context menu if activated via the gamepad or keyboard
// for running games. If the game isn't running, the above onClicked
@@ -251,14 +228,6 @@ CenteredGridView {
onTriggered: doQuitGame()
visible: model.running
}
NavigableMenuItem {
parentMenu: appContextMenu
checkable: true
checked: model.hidden
text: "Hide Game"
onTriggered: appModel.setAppHidden(model.index, !model.hidden)
visible: !model.running || model.hidden
}
}
}
@@ -294,5 +263,18 @@ CenteredGridView {
onAccepted: quitApp()
}
ScrollBar.vertical: ScrollBar {}
ScrollBar.vertical: ScrollBar {
// Manually hide the scrollbar to prevent it from being drawn on top
// of the StreamSegue during the transition. It can sometimes get stuck
// there since we're not consistently pumping the event loop while
// starting the stream.
visible: activated
parent: appGrid.parent
anchors {
top: parent.top
right: parent.right
bottom: parent.bottom
}
}
}
+1 -3
View File
@@ -6,10 +6,8 @@ import SdlGamepadKeyNavigation 1.0
// https://stackoverflow.com/questions/45029968/how-do-i-set-the-combobox-width-to-fit-the-largest-item
ComboBox {
property int textWidth
property int desiredWidth : leftPadding + textWidth + indicator.width + rightPadding
property int maximumWidth : parent.width
implicitWidth: desiredWidth < maximumWidth ? desiredWidth : maximumWidth
implicitWidth: leftPadding + textWidth + indicator.width + rightPadding
TextMetrics {
id: popupMetrics
-2
View File
@@ -38,6 +38,4 @@ GridView {
updateMargins()
}
boundsBehavior: Flickable.OvershootBounds
}
-6
View File
@@ -2,7 +2,6 @@ import QtQuick 2.0
import QtQuick.Controls 2.2
import ComputerManager 1.0
import SdlGamepadKeyNavigation 1.0
Item {
function onSearchingComputer() {
@@ -36,11 +35,6 @@ Item {
StackView.onActivated: {
if (!launcher.isExecuted()) {
toolBar.visible = false
// Normally this is enabled by PcView, but we will won't
// load PcView when streaming from the command-line.
SdlGamepadKeyNavigation.enable()
launcher.searchingComputer.connect(onSearchingComputer)
launcher.searchingApp.connect(onSearchingApp)
launcher.sessionCreated.connect(onSessionCreated)
-9
View File
@@ -6,8 +6,6 @@ NavigableDialog {
id: dialog
property alias text: dialogLabel.dialogText
property alias showSpinner: dialogSpinner.visible
property alias imageSrc: dialogImage.source
property string helpText
property string helpUrl : "https://github.com/moonlight-stream/moonlight-docs/wiki/Troubleshooting"
@@ -21,13 +19,7 @@ NavigableDialog {
RowLayout {
spacing: 10
BusyIndicator {
id: dialogSpinner
visible: false
}
Image {
id: dialogImage
source: (standardButtons & Dialog.Yes) ?
"qrc:/res/baseline-help_outline-24px.svg" :
"qrc:/res/baseline-error_outline-24px.svg"
@@ -36,7 +28,6 @@ NavigableDialog {
width: 50
height: 50
}
visible: !showSpinner
}
Label {
+15 -131
View File
@@ -1,6 +1,5 @@
import QtQuick 2.9
import QtQuick.Controls 2.2
import QtQuick.Layouts 1.3
import ComputerModel 1.0
@@ -26,9 +25,6 @@ CenteredGridView {
currentIndex = -1
}
// Note: Any initialization done here that is critical for streaming must
// also be done in CliStartStreamSegue.qml, since this code does not run
// for command-line initiated streams.
StackView.onActivated: {
// Setup signals on CM
ComputerManager.computerAddCompleted.connect(addComplete)
@@ -61,18 +57,11 @@ CenteredGridView {
}
}
function addComplete(success, detectedPortBlocking)
function addComplete(success)
{
if (!success) {
errorDialog.text = "Unable to connect to the specified PC."
if (detectedPortBlocking) {
errorDialog.text += "\n\nThis PC's Internet connection is blocking Moonlight. Streaming over the Internet may not work while connected to this network."
}
else {
errorDialog.helpText = "Click the Help button for possible solutions."
}
errorDialog.helpText = "Click the Help button for possible solutions."
errorDialog.open()
}
}
@@ -82,7 +71,6 @@ CenteredGridView {
var model = Qt.createQmlObject('import ComputerModel 1.0; ComputerModel {}', parent, '')
model.initialize(ComputerManager)
model.pairingCompleted.connect(pairingComplete)
model.connectionTestCompleted.connect(testConnectionDialog.connectionTestComplete)
return model
}
@@ -162,50 +150,6 @@ CenteredGridView {
NavigableMenu {
id: pcContextMenu
NavigableMenuItem {
parentMenu: pcContextMenu
text: "View Apps"
onTriggered: {
var component = Qt.createComponent("AppView.qml")
var appView = component.createObject(stackView, {"computerIndex": index, "objectName": model.name})
stackView.push(appView)
}
visible: model.online && model.paired
}
NavigableMenuItem {
parentMenu: pcContextMenu
text: "View Hidden Apps"
onTriggered: {
var component = Qt.createComponent("AppView.qml")
var appView = component.createObject(stackView, {"computerIndex": index, "objectName": model.name, "showHiddenGames": true})
stackView.push(appView)
}
visible: model.online && model.paired
}
NavigableMenuItem {
parentMenu: pcContextMenu
text: "Wake PC"
onTriggered: computerModel.wakeComputer(index)
visible: !model.online && model.wakeable
}
NavigableMenuItem {
parentMenu: pcContextMenu
text: "Test Network"
onTriggered: {
computerModel.testConnectionForComputer(index)
testConnectionDialog.open()
}
}
NavigableMenuItem {
parentMenu: pcContextMenu
text: "Rename PC"
onTriggered: {
renamePcDialog.pcIndex = index
renamePcDialog.originalName = model.name
renamePcDialog.open()
}
}
NavigableMenuItem {
parentMenu: pcContextMenu
text: "Delete PC"
@@ -215,6 +159,12 @@ CenteredGridView {
deletePcDialog.open()
}
}
NavigableMenuItem {
parentMenu: pcContextMenu
text: "Wake PC"
onTriggered: computerModel.wakeComputer(index)
visible: !model.online && model.wakeable
}
}
onClicked: {
@@ -227,7 +177,7 @@ CenteredGridView {
}
else {
if (!model.busy) {
var pin = computerModel.generatePinString()
var pin = ("0000" + Math.floor(Math.random() * 10000)).slice(-4)
// Kick off pairing in the background
computerModel.pairComputer(index, pin)
@@ -315,78 +265,12 @@ CenteredGridView {
onAccepted: deletePc()
}
NavigableMessageDialog {
id: testConnectionDialog
closePolicy: Popup.CloseOnEscape
standardButtons: Dialog.Ok
onAboutToShow: {
testConnectionDialog.text = "Moonlight is testing your network connection to determine if NVIDIA GameStream is blocked.\n\nThis may take a few seconds…"
showSpinner = true
}
function connectionTestComplete(result, blockedPorts)
{
if (result === -1) {
text = "The network test could not be performed because none of Moonlight's connection testing servers were reachable from this PC. Check your Internet connection or try again later."
imageSrc = "qrc:/res/baseline-warning-24px.svg"
}
else if (result === 0) {
text = "This network does not appear to be blocking Moonlight. If you still have trouble connecting, check your PC's firewall settings.\n\nIf you are trying to stream over the Internet, install the Moonlight Internet Hosting Tool on your gaming PC and run the included Internet Streaming Tester to check your gaming PC's Internet connection."
imageSrc = "qrc:/res/baseline-check_circle_outline-24px.svg"
}
else {
text = "Your PC's current network connection seems to be blocking Moonlight. Streaming over the Internet may not work while connected to this network.\n\nThe following network ports were blocked:\n"
text += blockedPorts
imageSrc = "qrc:/res/baseline-error_outline-24px.svg"
}
// Stop showing the spinner and show the image instead
showSpinner = false
ScrollBar.vertical: ScrollBar {
parent: pcGrid.parent
anchors {
top: parent.top
right: parent.right
bottom: parent.bottom
}
}
NavigableDialog {
id: renamePcDialog
property string label: "Enter the new name for this PC:"
property string originalName
property int pcIndex : -1;
standardButtons: Dialog.Ok | Dialog.Cancel
onOpened: {
// Force keyboard focus on the textbox so keyboard navigation works
editText.forceActiveFocus()
}
onClosed: {
editText.clear()
}
onAccepted: {
if (editText.text) {
computerModel.renameComputer(pcIndex, editText.text)
}
}
ColumnLayout {
Label {
text: renamePcDialog.label
font.bold: true
}
TextField {
id: editText
placeholderText: renamePcDialog.originalName
Layout.fillWidth: true
focus: true
Keys.onReturnPressed: {
renamePcDialog.accept()
}
}
}
}
ScrollBar.vertical: ScrollBar {}
}
+41 -139
View File
@@ -10,14 +10,16 @@ Flickable {
id: settingsPage
objectName: "Settings"
boundsBehavior: Flickable.OvershootBounds
contentWidth: settingsColumn1.width > settingsColumn2.width ? settingsColumn1.width : settingsColumn2.width
contentHeight: settingsColumn1.height > settingsColumn2.height ? settingsColumn1.height : settingsColumn2.height
ScrollBar.vertical: ScrollBar {
parent: settingsPage.parent
anchors {
left: parent.right
top: settingsPage.top
left: settingsPage.right
bottom: settingsPage.bottom
leftMargin: -10
}
}
@@ -43,12 +45,11 @@ Flickable {
Column {
padding: 10
id: settingsColumn1
width: settingsPage.width / 2
spacing: 15
width: settingsPage.width / 2 - padding
GroupBox {
id: basicSettingsGroupBox
width: (parent.width - (parent.leftPadding + parent.rightPadding))
width: (parent.width - 2 * parent.padding)
padding: 12
title: "<font color=\"skyblue\">Basic Settings</font>"
font.pointSize: 12
@@ -75,7 +76,6 @@ Flickable {
Row {
spacing: 5
width: parent.width
AutoResizingComboBox {
// ignore setting the index at first, and actually set it when the component is loaded
@@ -88,7 +88,7 @@ Flickable {
// Some platforms have different desktop resolutions
// and native resolutions (like macOS with Retina displays)
if (displayResIndex === 0) {
if (displayResIndex == 0) {
screenRect = SystemProperties.getDesktopResolution(displayIndex)
}
else {
@@ -129,20 +129,6 @@ 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
// and set it to that index.
var saved_width = StreamingPreferences.width
@@ -163,7 +149,6 @@ Flickable {
}
id: resolutionComboBox
maximumWidth: parent.width / 2
textRole: "text"
model: ListModel {
id: resolutionListModel
@@ -280,7 +265,6 @@ Flickable {
}
id: fpsComboBox
maximumWidth: parent.width / 2
textRole: "text"
// ::onActivated must be used, as it only listens for when the index is changed by a human
onActivated : {
@@ -354,24 +338,19 @@ Flickable {
}
}
var savedWm = StreamingPreferences.windowMode
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
}
}
for (var i = 0; i < windowModeListModel.count; i++) {
var thisWm = windowModeListModel.get(i).val;
if (savedWm === thisWm) {
currentIndex = i
break
}
}
activated(currentIndex)
}
id: windowModeComboBox
enabled: SystemProperties.hasWindowManager && !SystemProperties.rendererAlwaysFullScreen
hoverEnabled: true
textRole: "text"
model: ListModel {
@@ -401,7 +380,6 @@ Flickable {
CheckBox {
id: vsyncCheck
width: parent.width
hoverEnabled: true
text: "V-Sync"
font.pointSize: 12
@@ -418,7 +396,6 @@ Flickable {
CheckBox {
id: framePacingCheck
width: parent.width
hoverEnabled: true
text: "Frame pacing"
font.pointSize: 12
@@ -438,7 +415,7 @@ Flickable {
GroupBox {
id: audioSettingsGroupBox
width: (parent.width - (parent.leftPadding + parent.rightPadding))
width: (parent.width - 2 * parent.padding)
padding: 12
title: "<font color=\"skyblue\">Audio Settings</font>"
font.pointSize: 12
@@ -482,10 +459,6 @@ Flickable {
text: "5.1 surround sound"
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 : {
@@ -498,7 +471,7 @@ Flickable {
GroupBox {
id: uiSettingsGroupBox
width: (parent.width - (parent.leftPadding + parent.rightPadding))
width: (parent.width - 2 * parent.padding)
padding: 12
title: "<font color=\"skyblue\">UI Settings</font>"
font.pointSize: 12
@@ -509,11 +482,9 @@ Flickable {
CheckBox {
id: startMaximizedCheck
width: parent.width
text: "Maximize Moonlight window on startup"
font.pointSize: 12
enabled: SystemProperties.hasWindowManager
checked: !StreamingPreferences.startWindowed || !SystemProperties.hasWindowManager
checked: !StreamingPreferences.startWindowed
onCheckedChanged: {
StreamingPreferences.startWindowed = !checked
}
@@ -521,7 +492,6 @@ Flickable {
CheckBox {
id: connectionWarningsCheck
width: parent.width
text: "Show connection quality warnings"
font.pointSize: 12
checked: StreamingPreferences.connectionWarnings
@@ -533,7 +503,6 @@ Flickable {
CheckBox {
visible: SystemProperties.hasDiscordIntegration
id: discordPresenceCheck
width: parent.width
text: "Discord Rich Presence integration"
font.pointSize: 12
checked: StreamingPreferences.richPresence
@@ -552,15 +521,13 @@ Flickable {
Column {
padding: 10
rightPadding: 20
anchors.left: settingsColumn1.right
id: settingsColumn2
width: settingsPage.width / 2
spacing: 15
width: settingsPage.width / 2 - padding
GroupBox {
id: gamepadSettingsGroupBox
width: (parent.width - (parent.leftPadding + parent.rightPadding))
width: (parent.width - parent.padding)
padding: 12
title: "<font color=\"skyblue\">Input Settings</font>"
font.pointSize: 12
@@ -571,7 +538,6 @@ Flickable {
CheckBox {
id: singleControllerCheck
width: parent.width
text: "Force gamepad #1 always present"
font.pointSize: 12
checked: !StreamingPreferences.multiController
@@ -587,45 +553,24 @@ Flickable {
}
CheckBox {
id: absoluteMouseCheck
id: rawInputCheck
hoverEnabled: true
width: parent.width
text: "Optimize mouse for remote desktop instead of games"
text: "Raw mouse input"
font.pointSize: 12
enabled: SystemProperties.hasWindowManager
checked: StreamingPreferences.absoluteMouseMode && SystemProperties.hasWindowManager
checked: !StreamingPreferences.mouseAcceleration
onCheckedChanged: {
StreamingPreferences.absoluteMouseMode = checked
StreamingPreferences.mouseAcceleration = !checked
}
ToolTip.delay: 1000
ToolTip.timeout: 5000
ToolTip.timeout: 3000
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."
ToolTip.text: "When checked, mouse input is not accelerated or scaled by the OS before passing to Moonlight"
}
CheckBox {
id: gamepadMouseCheck
hoverEnabled: true
width: parent.width
text: "Gamepad mouse mode support"
font.pointSize: 12
checked: StreamingPreferences.gamepadMouse
@@ -638,29 +583,12 @@ Flickable {
ToolTip.visible: hovered
ToolTip.text: "When enabled, holding the Start button will toggle mouse mode"
}
CheckBox {
id: swapMouseButtonsCheck
hoverEnabled: true
width: parent.width
text: "Swap mouse buttons"
font.pointSize: 12
checked: StreamingPreferences.swapMouseButtons
onCheckedChanged: {
StreamingPreferences.swapMouseButtons = checked
}
ToolTip.delay: 1000
ToolTip.timeout: 3000
ToolTip.visible: hovered
ToolTip.text: "When checked, swap the left and right mouse buttons"
}
}
}
GroupBox {
id: hostSettingsGroupBox
width: (parent.width - (parent.leftPadding + parent.rightPadding))
width: (parent.width - parent.padding)
padding: 12
title: "<font color=\"skyblue\">Host Settings</font>"
font.pointSize: 12
@@ -671,8 +599,7 @@ Flickable {
CheckBox {
id: optimizeGameSettingsCheck
width: parent.width
text: "Optimize game settings for streaming"
text: "Optimize game settings"
font.pointSize: 12
checked: StreamingPreferences.gameOptimizations
onCheckedChanged: {
@@ -682,7 +609,6 @@ Flickable {
CheckBox {
id: audioPcCheck
width: parent.width
text: "Play audio on host PC"
font.pointSize: 12
checked: StreamingPreferences.playAudioOnHost
@@ -690,28 +616,12 @@ Flickable {
StreamingPreferences.playAudioOnHost = checked
}
}
CheckBox {
id: quitAppAfter
width: parent.width
text: "Quit app on host PC after ending stream"
font.pointSize: 12
checked: StreamingPreferences.quitAppAfter
onCheckedChanged: {
StreamingPreferences.quitAppAfter = checked
}
ToolTip.delay: 1000
ToolTip.timeout: 5000
ToolTip.visible: hovered
ToolTip.text: "This will close the app or game you are streaming when you end your stream. You will lose any unsaved progress!"
}
}
}
GroupBox {
id: advancedSettingsGroupBox
width: (parent.width - (parent.leftPadding + parent.rightPadding))
width: (parent.width - parent.padding)
padding: 12
title: "<font color=\"skyblue\">Advanced Settings</font>"
font.pointSize: 12
@@ -798,17 +708,20 @@ Flickable {
val: StreamingPreferences.VCC_AUTO
}
ListElement {
text: "H.264"
text: "Force H.264"
val: StreamingPreferences.VCC_FORCE_H264
}
ListElement {
text: "HEVC (H.265)"
text: "Force HEVC"
val: StreamingPreferences.VCC_FORCE_HEVC
}
/*ListElement {
text: "HEVC HDR (Experimental)"
// HDR seems to be broken in GFE 3.14.1, and even when that's fixed
// 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
}*/
} */
}
// ::onActivated must be used, as it only listens for when the index is changed by a human
onActivated : {
@@ -818,7 +731,6 @@ Flickable {
CheckBox {
id: unlockUnsupportedFps
width: parent.width
text: "Unlock unsupported FPS options"
font.pointSize: 12
checked: StreamingPreferences.unsupportedFps
@@ -837,7 +749,6 @@ Flickable {
CheckBox {
id: enableMdns
width: parent.width
text: "Automatically find PCs on the local network (Recommended)"
font.pointSize: 12
checked: StreamingPreferences.enableMdns
@@ -861,21 +772,12 @@ Flickable {
}
CheckBox {
id: detectNetworkBlocking
width: parent.width
text: "Automatically detect blocked connections (Recommended)"
id: quitAppAfter
text: "Quit app after quitting session"
font.pointSize: 12
checked: StreamingPreferences.detectNetworkBlocking
checked: StreamingPreferences.quitAppAfter
onCheckedChanged: {
// This is called on init, so only do the work if we've
// actually changed the value.
if (StreamingPreferences.detectNetworkBlocking != checked) {
StreamingPreferences.detectNetworkBlocking = checked
// We must save the updated preference to ensure
// ComputerManager can observe the change internally.
StreamingPreferences.save()
}
StreamingPreferences.quitAppAfter = checked
}
}
}
+1 -19
View File
@@ -62,12 +62,8 @@ Item {
window.visible = true
}
function sessionFinished(portTestResult)
function sessionFinished()
{
if (portTestResult !== 0 && portTestResult !== -1 && streamSegueErrorDialog.text) {
streamSegueErrorDialog.text += "\n\nThis PC's Internet connection is blocking Moonlight. Streaming over the Internet may not work while connected to this network."
}
// Enable GUI gamepad usage now
SdlGamepadKeyNavigation.enable()
@@ -121,22 +117,9 @@ Item {
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
@@ -167,7 +150,6 @@ Item {
BusyIndicator {
id: stageSpinner
running: false
}
Label {
+32 -133
View File
@@ -7,7 +7,7 @@ AppModel::AppModel(QObject *parent)
this, &AppModel::handleBoxArtLoaded);
}
void AppModel::initialize(ComputerManager* computerManager, int computerIndex, bool showHiddenGames)
void AppModel::initialize(ComputerManager* computerManager, int computerIndex)
{
m_ComputerManager = computerManager;
connect(m_ComputerManager, &ComputerManager::computerStateChanged,
@@ -15,23 +15,29 @@ void AppModel::initialize(ComputerManager* computerManager, int computerIndex, b
Q_ASSERT(computerIndex < m_ComputerManager->getComputers().count());
m_Computer = m_ComputerManager->getComputers().at(computerIndex);
m_Apps = m_Computer->appList;
m_CurrentGameId = m_Computer->currentGameId;
m_ShowHiddenGames = showHiddenGames;
updateAppList(m_Computer->appList);
}
int AppModel::getRunningAppId()
int AppModel::getRunningAppIndex()
{
return m_CurrentGameId;
if (m_CurrentGameId != 0) {
for (int i = 0; i < m_Apps.count(); i++) {
if (m_Apps[i].id == m_CurrentGameId) {
return i;
}
}
}
return -1;
}
QString AppModel::getRunningAppName()
{
if (m_CurrentGameId != 0) {
for (int i = 0; i < m_AllApps.count(); i++) {
if (m_AllApps[i].id == m_CurrentGameId) {
return m_AllApps[i].name;
for (int i = 0; i < m_Apps.count(); i++) {
if (m_Apps[i].id == m_CurrentGameId) {
return m_Apps[i].name;
}
}
}
@@ -41,8 +47,8 @@ QString AppModel::getRunningAppName()
Session* AppModel::createSessionForApp(int appIndex)
{
Q_ASSERT(appIndex < m_VisibleApps.count());
NvApp app = m_VisibleApps.at(appIndex);
Q_ASSERT(appIndex < m_Apps.count());
NvApp app = m_Apps.at(appIndex);
return new Session(m_Computer, app);
}
@@ -54,7 +60,7 @@ int AppModel::rowCount(const QModelIndex &parent) const
if (parent.isValid())
return 0;
return m_VisibleApps.count();
return m_Apps.count();
}
QVariant AppModel::data(const QModelIndex &index, int role) const
@@ -62,8 +68,8 @@ QVariant AppModel::data(const QModelIndex &index, int role) const
if (!index.isValid())
return QVariant();
Q_ASSERT(index.row() < m_VisibleApps.count());
NvApp app = m_VisibleApps.at(index.row());
Q_ASSERT(index.row() < m_Apps.count());
NvApp app = m_Apps.at(index.row());
switch (role)
{
@@ -74,10 +80,6 @@ QVariant AppModel::data(const QModelIndex &index, int role) const
case BoxArtRole:
// FIXME: const-correctness
return const_cast<BoxArtManager&>(m_BoxArtManager).loadBoxArt(m_Computer, app);
case HiddenRole:
return app.hidden;
case AppIdRole:
return app.id;
default:
return QVariant();
}
@@ -90,8 +92,6 @@ QHash<int, QByteArray> AppModel::roleNames() const
names[NameRole] = "name";
names[RunningRole] = "running";
names[BoxArtRole] = "boxart";
names[HiddenRole] = "hidden";
names[AppIdRole] = "appid";
return names;
}
@@ -101,112 +101,6 @@ void AppModel::quitRunningApp()
m_ComputerManager->quitRunningApp(m_Computer);
}
bool AppModel::isAppCurrentlyVisible(const NvApp& app)
{
for (const NvApp& visibleApp : m_VisibleApps) {
if (app.id == visibleApp.id) {
return true;
}
}
return false;
}
QVector<NvApp> AppModel::getVisibleApps(const QVector<NvApp>& appList)
{
QVector<NvApp> visibleApps;
for (const NvApp& app : appList) {
// Don't immediately hide games that were previously visible. This
// allows users to easily uncheck the "Hide App" checkbox if they
// check it by mistake.
if (m_ShowHiddenGames || !app.hidden || isAppCurrentlyVisible(app)) {
visibleApps.append(app);
}
}
return visibleApps;
}
void AppModel::updateAppList(QVector<NvApp> newList)
{
m_AllApps = newList;
QVector<NvApp> newVisibleList = getVisibleApps(newList);
// Process removals and updates first
for (int i = 0; i < m_VisibleApps.count(); i++) {
const NvApp& existingApp = m_VisibleApps.at(i);
bool found = false;
for (const NvApp& newApp : newVisibleList) {
if (existingApp.id == newApp.id) {
// If the data changed, update it in our list
if (existingApp != newApp) {
m_VisibleApps.replace(i, newApp);
emit dataChanged(createIndex(i, 0), createIndex(i, 0));
}
found = true;
break;
}
}
if (!found) {
beginRemoveRows(QModelIndex(), i, i);
m_VisibleApps.removeAt(i);
endRemoveRows();
i--;
}
}
// Process additions now
for (const NvApp& newApp : newVisibleList) {
int insertionIndex = m_VisibleApps.size();
bool found = false;
for (int i = 0; i < m_VisibleApps.count(); i++) {
const NvApp& existingApp = m_VisibleApps.at(i);
if (existingApp.id == newApp.id) {
found = true;
break;
}
else if (existingApp.name.toLower() > newApp.name.toLower()) {
insertionIndex = i;
break;
}
}
if (!found) {
beginInsertRows(QModelIndex(), insertionIndex, insertionIndex);
m_VisibleApps.insert(insertionIndex, newApp);
endInsertRows();
}
}
Q_ASSERT(newVisibleList == m_VisibleApps);
}
void AppModel::setAppHidden(int appIndex, bool hidden)
{
Q_ASSERT(appIndex < m_VisibleApps.count());
int appId = m_VisibleApps.at(appIndex).id;
{
QWriteLocker lock(&m_Computer->lock);
for (NvApp& app : m_Computer->appList) {
if (app.id == appId) {
app.hidden = hidden;
break;
}
}
}
m_ComputerManager->clientSideAttributeUpdated(m_Computer);
}
void AppModel::handleComputerStateChanged(NvComputer* computer)
{
// Ignore updates for computers that aren't ours
@@ -225,15 +119,20 @@ void AppModel::handleComputerStateChanged(NvComputer* computer)
// First, process additions/removals from the app list. This
// is required because the new game may now be running, so
// we can't check that first.
if (computer->appList != m_AllApps) {
updateAppList(computer->appList);
if (computer->appList != m_Apps) {
// Just reset the whole thing if the list changes
beginResetModel();
m_Apps = computer->appList;
m_CurrentGameId = computer->currentGameId;
endResetModel();
return;
}
// Finally, process changes to the active app
if (computer->currentGameId != m_CurrentGameId) {
// First, invalidate the running state of newly running game
for (int i = 0; i < m_VisibleApps.count(); i++) {
if (m_VisibleApps[i].id == computer->currentGameId) {
for (int i = 0; i < m_Apps.count(); i++) {
if (m_Apps[i].id == computer->currentGameId) {
emit dataChanged(createIndex(i, 0),
createIndex(i, 0),
QVector<int>() << RunningRole);
@@ -243,8 +142,8 @@ void AppModel::handleComputerStateChanged(NvComputer* computer)
// Next, invalidate the running state of the old game (if it exists)
if (m_CurrentGameId != 0) {
for (int i = 0; i < m_VisibleApps.count(); i++) {
if (m_VisibleApps[i].id == m_CurrentGameId) {
for (int i = 0; i < m_Apps.count(); i++) {
if (m_Apps[i].id == m_CurrentGameId) {
emit dataChanged(createIndex(i, 0),
createIndex(i, 0),
QVector<int>() << RunningRole);
@@ -262,7 +161,7 @@ void AppModel::handleBoxArtLoaded(NvComputer* computer, NvApp app, QUrl /* image
{
Q_ASSERT(computer == m_Computer);
int index = m_VisibleApps.indexOf(app);
int index = m_Apps.indexOf(app);
// Make sure we're not delivering a callback to an app that's already been removed
if (index >= 0) {
+4 -15
View File
@@ -14,27 +14,23 @@ class AppModel : public QAbstractListModel
{
NameRole = Qt::UserRole,
RunningRole,
BoxArtRole,
HiddenRole,
AppIdRole,
BoxArtRole
};
public:
explicit AppModel(QObject *parent = nullptr);
// Must be called before any QAbstractListModel functions
Q_INVOKABLE void initialize(ComputerManager* computerManager, int computerIndex, bool showHiddenGames);
Q_INVOKABLE void initialize(ComputerManager* computerManager, int computerIndex);
Q_INVOKABLE Session* createSessionForApp(int appIndex);
Q_INVOKABLE int getRunningAppId();
Q_INVOKABLE int getRunningAppIndex();
Q_INVOKABLE QString getRunningAppName();
Q_INVOKABLE void quitRunningApp();
Q_INVOKABLE void setAppHidden(int appIndex, bool hidden);
QVariant data(const QModelIndex &index, int role) const override;
int rowCount(const QModelIndex &parent) const override;
@@ -50,16 +46,9 @@ signals:
void computerLost();
private:
void updateAppList(QVector<NvApp> newList);
QVector<NvApp> getVisibleApps(const QVector<NvApp>& appList);
bool isAppCurrentlyVisible(const NvApp& app);
NvComputer* m_Computer;
BoxArtManager m_BoxArtManager;
ComputerManager* m_ComputerManager;
QVector<NvApp> m_VisibleApps, m_AllApps;
QVector<NvApp> m_Apps;
int m_CurrentGameId;
bool m_ShowHiddenGames;
};
-66
View File
@@ -2,8 +2,6 @@
#include <QThreadPool>
#include <random>
ComputerModel::ComputerModel(QObject* object)
: QAbstractListModel(object) {}
@@ -130,69 +128,6 @@ void ComputerModel::wakeComputer(int computerIndex)
QThreadPool::globalInstance()->start(wakeTask);
}
void ComputerModel::renameComputer(int computerIndex, QString name)
{
Q_ASSERT(computerIndex < m_Computers.count());
m_ComputerManager->renameHost(m_Computers[computerIndex], name);
}
// TODO: Use QRandomGenerator when we drop Qt 5.9 support
QString ComputerModel::generatePinString()
{
std::uniform_int_distribution<int> dist(0, 9999);
std::random_device rd;
std::mt19937 engine(rd());
return QString::asprintf("%04u", dist(engine));
}
class DeferredTestConnectionTask : public QObject, public QRunnable
{
Q_OBJECT
public:
void run()
{
unsigned int portTestResult = LiTestClientConnectivity("qt.conntest.moonlight-stream.org", 443, ML_PORT_FLAG_ALL);
if (portTestResult == ML_TEST_RESULT_INCONCLUSIVE) {
emit connectionTestCompleted(-1, QString());
}
else {
QString blockedPorts;
for (int i = 0; i < 32; i++) {
if (portTestResult & (1 << i)) {
if (!blockedPorts.isEmpty()) {
blockedPorts += "\n";
}
if (LiGetProtocolFromPortFlagIndex(i) == 17 /* IPPROTO_UDP */) {
blockedPorts += "UDP ";
}
else {
blockedPorts += "TCP ";
}
blockedPorts += QString::number(LiGetPortFromPortFlagIndex(i));
}
}
emit connectionTestCompleted(portTestResult, blockedPorts);
}
}
signals:
void connectionTestCompleted(int result, QString blockedPorts);
};
void ComputerModel::testConnectionForComputer(int)
{
DeferredTestConnectionTask* testConnectionTask = new DeferredTestConnectionTask();
QObject::connect(testConnectionTask, &DeferredTestConnectionTask::connectionTestCompleted,
this, &ComputerModel::connectionTestCompleted);
QThreadPool::globalInstance()->start(testConnectionTask);
}
void ComputerModel::pairComputer(int computerIndex, QString pin)
{
Q_ASSERT(computerIndex < m_Computers.count());
@@ -223,4 +158,3 @@ void ComputerModel::handleComputerStateChanged(NvComputer* computer)
}
}
#include "computermodel.moc"
-7
View File
@@ -31,21 +31,14 @@ public:
Q_INVOKABLE void deleteComputer(int computerIndex);
Q_INVOKABLE QString generatePinString();
Q_INVOKABLE void pairComputer(int computerIndex, QString pin);
Q_INVOKABLE void testConnectionForComputer(int computerIndex);
Q_INVOKABLE void wakeComputer(int computerIndex);
Q_INVOKABLE void renameComputer(int computerIndex, QString name);
Q_INVOKABLE Session* createSessionForCurrentGame(int computerIndex);
signals:
void pairingCompleted(QVariant error);
void connectionTestCompleted(int result, QString blockedPorts);
private slots:
void handleComputerStateChanged(NvComputer* computer);
+4 -54
View File
@@ -17,7 +17,7 @@ ApplicationWindow {
width: 1280
height: 600
visibility: (SystemProperties.hasWindowManager && StreamingPreferences.startWindowed) ? "Windowed" : "Maximized"
visibility: StreamingPreferences.startWindowed ? "Windowed" : "Maximized"
StackView {
id: stackView
@@ -175,17 +175,6 @@ ApplicationWindow {
anchors.topMargin: 5
anchors.bottomMargin: 5
Label {
id: titleLabel
visible: toolBar.width > 700
anchors.fill: parent
text: stackView.currentItem.objectName
font.pointSize: 20
elide: Label.ElideRight
horizontalAlignment: Qt.AlignHCenter
verticalAlignment: Qt.AlignVCenter
}
RowLayout {
spacing: 20
anchors.leftMargin: 10
@@ -205,49 +194,14 @@ ApplicationWindow {
}
}
// This label will appear when the window gets too small and
// we need to ensure the toolbar controls don't collide
Label {
id: titleRowLabel
font.pointSize: titleLabel.font.pointSize
id: titleLabel
text: stackView.currentItem.objectName
font.pointSize: 15
elide: Label.ElideRight
horizontalAlignment: Qt.AlignHCenter
verticalAlignment: Qt.AlignVCenter
Layout.fillWidth: true
// We need this label to always be visible so it can occupy
// the remaining space in the RowLayout. To "hide" it, we
// just set the text to empty string.
text: !titleLabel.visible ? stackView.currentItem.objectName : ""
}
Label {
id: versionLabel
visible: stackView.currentItem.objectName === "Settings"
text: "Version " + SystemProperties.versionString
font.pointSize: 12
horizontalAlignment: Qt.AlignRight
verticalAlignment: Qt.AlignVCenter
}
NavigableToolButton {
id: discordButton
visible: SystemProperties.hasBrowser &&
stackView.currentItem.objectName === "Settings"
iconSource: "qrc:/res/Discord-Logo-White.svg"
ToolTip.delay: 1000
ToolTip.timeout: 3000
ToolTip.visible: hovered
ToolTip.text: "Join our community on Discord"
// TODO need to make sure browser is brought to foreground.
onClicked: Qt.openUrlExternally("https://moonlight-stream.org/discord");
Keys.onDownPressed: {
stackView.currentItem.forceActiveFocus(Qt.TabFocus)
}
}
NavigableToolButton {
@@ -459,10 +413,6 @@ ApplicationWindow {
editText.forceActiveFocus()
}
onClosed: {
editText.clear()
}
onAccepted: {
if (editText.text) {
ComputerManager.addNewHost(editText.text, false)
-41
View File
@@ -205,31 +205,6 @@ void SdlGamepadKeyNavigation::onPollingTimerFired()
sendKey(QEvent::Type::KeyRelease, Qt::Key_Right);
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,22 +218,6 @@ void SdlGamepadKeyNavigation::sendKey(QEvent::Type type, Qt::Key key, Qt::Keyboa
}
}
void SdlGamepadKeyNavigation::sendWheel(QPoint& angleDelta)
{
QGuiApplication* app = static_cast<QGuiApplication*>(QGuiApplication::instance());
QWindow* focusWindow = app->focusWindow();
if (focusWindow != nullptr) {
QPoint mousePos(focusWindow->width() / 2, focusWindow->height() / 2);
QPoint globalPos(focusWindow->mapToGlobal(mousePos));
#if QT_VERSION >= QT_VERSION_CHECK(5, 12, 0)
QWheelEvent wheelEvent(mousePos, globalPos, QPoint(), angleDelta, Qt::NoButton, Qt::NoModifier, Qt::NoScrollPhase, false, Qt::MouseEventSynthesizedByApplication);
#else
QWheelEvent wheelEvent(mousePos, globalPos, QPoint(), angleDelta, angleDelta.y(), Qt::Vertical, Qt::NoButton, Qt::NoModifier, Qt::NoScrollPhase, Qt::MouseEventSynthesizedByApplication, false);
#endif
app->sendEvent(focusWindow, &wheelEvent);
}
}
void SdlGamepadKeyNavigation::setUiNavMode(bool uiNavMode)
{
m_UiNavMode = uiNavMode;
-1
View File
@@ -24,7 +24,6 @@ public:
private:
void sendKey(QEvent::Type type, Qt::Key key, Qt::KeyboardModifiers modifiers = Qt::NoModifier);
void sendWheel(QPoint& angleDelta);
private slots:
void onPollingTimerFired();
+9 -68
View File
@@ -10,7 +10,6 @@
#include <QFont>
#include <QCursor>
#include <QElapsedTimer>
#include <QFile>
// Don't let SDL hook our main function, since Qt is already
// doing the same thing. This needs to be before any headers
@@ -22,17 +21,14 @@
#include "streaming/video/ffmpeg.h"
#endif
#if defined(Q_OS_WIN32)
#ifdef Q_OS_WIN32
#include "antihookingprotection.h"
#elif defined(Q_OS_LINUX)
#include <openssl/ssl.h>
#endif
#include "cli/quitstream.h"
#include "cli/startstream.h"
#include "cli/commandlineparser.h"
#include "path.h"
#include "utils.h"
#include "gui/computermodel.h"
#include "gui/appmodel.h"
#include "backend/autoupdatechecker.h"
@@ -76,12 +72,7 @@ void logToLoggerStream(QString& message)
return;
}
else if (s_LogLinesWritten == MAX_LOG_LINES) {
s_LoggerStream << "Log size limit reached!";
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
s_LoggerStream << Qt::endl;
#else
s_LoggerStream << endl;
#endif
s_LoggerStream << "Log size limit reached!" << endl;
s_LogLimitReached = true;
return;
}
@@ -288,44 +279,19 @@ int main(int argc, char *argv[])
SetUnhandledExceptionFilter(UnhandledExceptionHandler);
#endif
#if defined(Q_OS_WIN32)
#ifdef Q_OS_WIN32
// Force AntiHooking.dll to be statically imported and loaded
// by ntdll by calling a dummy function.
AntiHookingDummyImport();
#elif defined(Q_OS_LINUX)
// Force libssl.so to be directly linked to our binary, so
// linuxdeployqt can find it and include it in our AppImage.
// QtNetwork will pull it in via dlopen().
SSL_free(nullptr);
#endif
// Avoid using High DPI on EGLFS. It breaks font rendering.
// https://bugreports.qt.io/browse/QTBUG-64377
//
// NB: We can't use QGuiApplication::platformName() here because it is only
// set once the QGuiApplication is created, which is too late to enable High DPI :(
if (WMUtils::isRunningWindowManager()) {
// Enable High DPI support
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
// 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);
// 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
// password prompts on macOS.
@@ -367,12 +333,6 @@ int main(int argc, char *argv[])
// 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",
@@ -403,11 +363,6 @@ int main(int argc, char *argv[])
// 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
@@ -419,12 +374,12 @@ int main(int argc, char *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") {
if (qgetenv("XDG_SESSION_TYPE") == "wayland" && 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");
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Detected Wayland. Performance may be worse than X11.");
qputenv("SDL_VIDEODRIVER", "wayland");
}
@@ -442,17 +397,6 @@ int main(int argc, char *argv[])
// 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"));
@@ -487,9 +431,6 @@ int main(int argc, char *argv[])
return new StreamingPreferences();
});
// Create the identity manager on the main thread
IdentityManager::get();
#ifndef Q_OS_WINRT
// Use the dense material dark theme by default
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_STYLE")) {
-1
View File
@@ -1 +0,0 @@
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 245 240"><style>.st0{fill:#FFFFFF;}</style><path class="st0" d="M104.4 103.9c-5.7 0-10.2 5-10.2 11.1s4.6 11.1 10.2 11.1c5.7 0 10.2-5 10.2-11.1.1-6.1-4.5-11.1-10.2-11.1zM140.9 103.9c-5.7 0-10.2 5-10.2 11.1s4.6 11.1 10.2 11.1c5.7 0 10.2-5 10.2-11.1s-4.5-11.1-10.2-11.1z"/><path class="st0" d="M189.5 20h-134C44.2 20 35 29.2 35 40.6v135.2c0 11.4 9.2 20.6 20.5 20.6h113.4l-5.3-18.5 12.8 11.9 12.1 11.2 21.5 19V40.6c0-11.4-9.2-20.6-20.5-20.6zm-38.6 130.6s-3.6-4.3-6.6-8.1c13.1-3.7 18.1-11.9 18.1-11.9-4.1 2.7-8 4.6-11.5 5.9-5 2.1-9.8 3.5-14.5 4.3-9.6 1.8-18.4 1.3-25.9-.1-5.7-1.1-10.6-2.7-14.7-4.3-2.3-.9-4.8-2-7.3-3.4-.3-.2-.6-.3-.9-.5-.2-.1-.3-.2-.4-.3-1.8-1-2.8-1.7-2.8-1.7s4.8 8 17.5 11.8c-3 3.8-6.7 8.3-6.7 8.3-22.1-.7-30.5-15.2-30.5-15.2 0-32.2 14.4-58.3 14.4-58.3 14.4-10.8 28.1-10.5 28.1-10.5l1 1.2c-18 5.2-26.3 13.1-26.3 13.1s2.2-1.2 5.9-2.9c10.7-4.7 19.2-6 22.7-6.3.6-.1 1.1-.2 1.7-.2 6.1-.8 13-1 20.2-.2 9.5 1.1 19.7 3.9 30.1 9.6 0 0-7.9-7.5-24.9-12.7l1.4-1.6s13.7-.3 28.1 10.5c0 0 14.4 26.1 14.4 58.3 0 0-8.5 14.5-30.6 15.2z"/></svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

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

Before

Width:  |  Height:  |  Size: 339 B

-4
View File
@@ -15,13 +15,9 @@
<file>res/update.svg</file>
<file>res/baseline-help_outline-24px.svg</file>
<file>res/baseline-error_outline-24px.svg</file>
<file>res/baseline-check_circle_outline-24px.svg</file>
<file>res/Discord-Logo-White.svg</file>
</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>
</RCC>
+1 -7
View File
@@ -26,13 +26,7 @@ MappingManager::MappingManager()
settings.endArray();
// Finally load mappings from SDL_HINT_GAMECONTROLLERCONFIG
QStringList sdlMappings =
QString::fromLocal8Bit(SDL_GetHint(SDL_HINT_GAMECONTROLLERCONFIG))
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
.split('\n', Qt::SkipEmptyParts);
#else
.split('\n', QString::SkipEmptyParts);
#endif
QStringList sdlMappings = QString::fromLocal8Bit(SDL_GetHint(SDL_HINT_GAMECONTROLLERCONFIG)).split('\n', QString::SkipEmptyParts);
for (QString sdlMapping : sdlMappings) {
SdlGamepadMapping mapping(sdlMapping);
addMapping(mapping);
+4 -16
View File
@@ -19,17 +19,13 @@
#define SER_UNSUPPORTEDFPS "unsupportedfps"
#define SER_MDNS "mdns"
#define SER_QUITAPPAFTER "quitAppAfter"
#define SER_ABSMOUSEMODE "mouseacceleration"
#define SER_ABSTOUCHMODE "abstouchmode"
#define SER_MOUSEACCELERATION "mouseacceleration"
#define SER_STARTWINDOWED "startwindowed"
#define SER_FRAMEPACING "framepacing"
#define SER_CONNWARNINGS "connwarnings"
#define SER_RICHPRESENCE "richpresence"
#define SER_GAMEPADMOUSE "gamepadmouse"
#define SER_DEFAULTVER "defaultver"
#define SER_PACKETSIZE "packetsize"
#define SER_DETECTNETBLOCKING "detectnetblocking"
#define SER_SWAPMOUSEBUTTONS "swapmousebuttons"
#define CURRENT_DEFAULT_VER 1
@@ -62,16 +58,12 @@ void StreamingPreferences::reload()
unsupportedFps = settings.value(SER_UNSUPPORTEDFPS, false).toBool();
enableMdns = settings.value(SER_MDNS, true).toBool();
quitAppAfter = settings.value(SER_QUITAPPAFTER, false).toBool();
absoluteMouseMode = settings.value(SER_ABSMOUSEMODE, false).toBool();
absoluteTouchMode = settings.value(SER_ABSTOUCHMODE, true).toBool();
startWindowed = settings.value(SER_STARTWINDOWED, true).toBool();
mouseAcceleration = settings.value(SER_MOUSEACCELERATION, false).toBool();
startWindowed = settings.value(SER_STARTWINDOWED, false).toBool();
framePacing = settings.value(SER_FRAMEPACING, false).toBool();
connectionWarnings = settings.value(SER_CONNWARNINGS, true).toBool();
richPresence = settings.value(SER_RICHPRESENCE, true).toBool();
gamepadMouse = settings.value(SER_GAMEPADMOUSE, true).toBool();
detectNetworkBlocking = settings.value(SER_DETECTNETBLOCKING, true).toBool();
packetSize = settings.value(SER_PACKETSIZE, 0).toInt();
swapMouseButtons = settings.value(SER_SWAPMOUSEBUTTONS, false).toBool();
audioConfig = static_cast<AudioConfig>(settings.value(SER_AUDIOCFG,
static_cast<int>(AudioConfig::AC_STEREO)).toInt());
videoCodecConfig = static_cast<VideoCodecConfig>(settings.value(SER_VIDEOCFG,
@@ -109,21 +101,17 @@ void StreamingPreferences::save()
settings.setValue(SER_UNSUPPORTEDFPS, unsupportedFps);
settings.setValue(SER_MDNS, enableMdns);
settings.setValue(SER_QUITAPPAFTER, quitAppAfter);
settings.setValue(SER_ABSMOUSEMODE, absoluteMouseMode);
settings.setValue(SER_ABSTOUCHMODE, absoluteTouchMode);
settings.setValue(SER_MOUSEACCELERATION, mouseAcceleration);
settings.setValue(SER_STARTWINDOWED, startWindowed);
settings.setValue(SER_FRAMEPACING, framePacing);
settings.setValue(SER_CONNWARNINGS, connectionWarnings);
settings.setValue(SER_RICHPRESENCE, richPresence);
settings.setValue(SER_GAMEPADMOUSE, gamepadMouse);
settings.setValue(SER_PACKETSIZE, packetSize);
settings.setValue(SER_DETECTNETBLOCKING, detectNetworkBlocking);
settings.setValue(SER_AUDIOCFG, static_cast<int>(audioConfig));
settings.setValue(SER_VIDEOCFG, static_cast<int>(videoCodecConfig));
settings.setValue(SER_VIDEODEC, static_cast<int>(videoDecoderSelection));
settings.setValue(SER_WINDOWMODE, static_cast<int>(windowMode));
settings.setValue(SER_DEFAULTVER, CURRENT_DEFAULT_VER);
settings.setValue(SER_SWAPMOUSEBUTTONS, swapMouseButtons);
}
int StreamingPreferences::getDefaultBitrate(int width, int height, int fps)
+4 -15
View File
@@ -20,8 +20,7 @@ public:
enum AudioConfig
{
AC_STEREO,
AC_51_SURROUND,
AC_71_SURROUND
AC_51_SURROUND
};
Q_ENUM(AudioConfig)
@@ -61,20 +60,17 @@ public:
Q_PROPERTY(bool unsupportedFps MEMBER unsupportedFps NOTIFY unsupportedFpsChanged)
Q_PROPERTY(bool enableMdns MEMBER enableMdns NOTIFY enableMdnsChanged)
Q_PROPERTY(bool quitAppAfter MEMBER quitAppAfter NOTIFY quitAppAfterChanged)
Q_PROPERTY(bool absoluteMouseMode MEMBER absoluteMouseMode NOTIFY absoluteMouseModeChanged)
Q_PROPERTY(bool absoluteTouchMode MEMBER absoluteTouchMode NOTIFY absoluteTouchModeChanged)
Q_PROPERTY(bool mouseAcceleration MEMBER mouseAcceleration NOTIFY mouseAccelerationChanged)
Q_PROPERTY(bool startWindowed MEMBER startWindowed NOTIFY startWindowedChanged)
Q_PROPERTY(bool framePacing MEMBER framePacing NOTIFY framePacingChanged)
Q_PROPERTY(bool connectionWarnings MEMBER connectionWarnings NOTIFY connectionWarningsChanged)
Q_PROPERTY(bool richPresence MEMBER richPresence NOTIFY richPresenceChanged)
Q_PROPERTY(bool gamepadMouse MEMBER gamepadMouse NOTIFY gamepadMouseChanged)
Q_PROPERTY(bool detectNetworkBlocking MEMBER detectNetworkBlocking NOTIFY detectNetworkBlockingChanged);
Q_PROPERTY(AudioConfig audioConfig MEMBER audioConfig NOTIFY audioConfigChanged)
Q_PROPERTY(VideoCodecConfig videoCodecConfig MEMBER videoCodecConfig NOTIFY videoCodecConfigChanged)
Q_PROPERTY(VideoDecoderSelection videoDecoderSelection MEMBER videoDecoderSelection NOTIFY videoDecoderSelectionChanged)
Q_PROPERTY(WindowMode windowMode MEMBER windowMode NOTIFY windowModeChanged)
Q_PROPERTY(WindowMode recommendedFullScreenMode MEMBER recommendedFullScreenMode CONSTANT)
Q_PROPERTY(bool swapMouseButtons MEMBER swapMouseButtons NOTIFY mouseButtonsChanged)
// Directly accessible members for preferences
int width;
@@ -88,16 +84,12 @@ public:
bool unsupportedFps;
bool enableMdns;
bool quitAppAfter;
bool absoluteMouseMode;
bool absoluteTouchMode;
bool mouseAcceleration;
bool startWindowed;
bool framePacing;
bool connectionWarnings;
bool richPresence;
bool gamepadMouse;
bool detectNetworkBlocking;
bool swapMouseButtons;
int packetSize;
AudioConfig audioConfig;
VideoCodecConfig videoCodecConfig;
VideoDecoderSelection videoDecoderSelection;
@@ -114,8 +106,7 @@ signals:
void unsupportedFpsChanged();
void enableMdnsChanged();
void quitAppAfterChanged();
void absoluteMouseModeChanged();
void absoluteTouchModeChanged();
void mouseAccelerationChanged();
void audioConfigChanged();
void videoCodecConfigChanged();
void videoDecoderSelectionChanged();
@@ -125,7 +116,5 @@ signals:
void connectionWarningsChanged();
void richPresenceChanged();
void gamepadMouseChanged();
void detectNetworkBlockingChanged();
void mouseButtonsChanged();
};
-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);
}
+27 -3
View File
@@ -80,7 +80,19 @@ int Session::getAudioRendererCapabilities(int audioConfiguration)
OPUS_MULTISTREAM_CONFIGURATION opusConfig = {};
opusConfig.sampleRate = 48000;
opusConfig.samplesPerFrame = 240;
opusConfig.channelCount = CHANNEL_COUNT_FROM_AUDIO_CONFIGURATION(audioConfiguration);
switch (audioConfiguration)
{
case AUDIO_CONFIGURATION_STEREO:
opusConfig.channelCount = 2;
break;
case AUDIO_CONFIGURATION_51_SURROUND:
opusConfig.channelCount = 6;
break;
default:
SDL_assert(false);
return 0;
}
IAudioRenderer* audioRenderer = createAudioRenderer(&opusConfig);
if (audioRenderer == nullptr) {
@@ -101,7 +113,19 @@ bool Session::testAudio(int audioConfiguration)
OPUS_MULTISTREAM_CONFIGURATION opusConfig = {};
opusConfig.sampleRate = 48000;
opusConfig.samplesPerFrame = 240;
opusConfig.channelCount = CHANNEL_COUNT_FROM_AUDIO_CONFIGURATION(audioConfiguration);
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;
}
IAudioRenderer* audioRenderer = createAudioRenderer(&opusConfig);
if (audioRenderer == nullptr) {
@@ -213,7 +237,7 @@ void Session::arDecodeAndPlaySample(char* sampleData, int sampleLength)
// Update desiredSize with the number of bytes actually populated by the decoding operation
if (samplesDecoded > 0) {
SDL_assert(desiredSize >= (int)(sizeof(short) * samplesDecoded * s_ActiveSession->m_AudioConfig.channelCount));
SDL_assert(desiredSize >= sizeof(short) * samplesDecoded * s_ActiveSession->m_AudioConfig.channelCount);
desiredSize = sizeof(short) * samplesDecoded * s_ActiveSession->m_AudioConfig.channelCount;
}
else {
+2 -32
View File
@@ -4,42 +4,12 @@
#include <SDL.h>
#include <QtGlobal>
#include <QFile>
#include <QTextStream>
SdlAudioRenderer::SdlAudioRenderer()
: m_AudioDevice(0),
m_AudioBuffer(nullptr)
{
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) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_InitSubSystem(SDL_INIT_AUDIO) failed: %s",
@@ -81,9 +51,9 @@ bool SdlAudioRenderer::prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION*
}
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Desired audio buffer: %u samples (%u bytes)",
"Desired audio buffer: %u samples (%lu bytes)",
want.samples,
want.samples * (Uint32)sizeof(short) * want.channels);
want.samples * sizeof(short) * want.channels);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Obtained audio buffer: %u samples (%u bytes)",
+15 -15
View File
@@ -42,26 +42,26 @@ bool SLAudioRenderer::prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* o
return true;
}
#define SWAP_CHANNEL(i, j) \
tmp = opusConfig->mapping[i]; \
opusConfig->mapping[i] = opusConfig->mapping[j]; \
opusConfig->mapping[j] = tmp
void SLAudioRenderer::remapChannels(POPUS_MULTISTREAM_CONFIGURATION opusConfig) {
OPUS_MULTISTREAM_CONFIGURATION originalConfig = *opusConfig;
unsigned char tmp;
// 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 == 6) {
// The Moonlight's default channel order is FL,FR,C,LFE,RL,RR
// SLAudio expects FL,C,FR,RL,RR,LFE 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];
}
// Swap FR and C - now FL,C,FR,LFE,RL,RR
SWAP_CHANNEL(1, 2);
if (opusConfig->channelCount >= 6) {
// SLAudio expects the LFE channel at the end
opusConfig->mapping[opusConfig->channelCount - 1] = originalConfig.mapping[3];
// Swap LFE and RR - now FL,C,FR,RR,RL,LFE
SWAP_CHANNEL(3, 5);
// Slide the other channels down
memcpy(&opusConfig->mapping[3],
&originalConfig.mapping[4],
opusConfig->channelCount - 4);
// Swap RR and RL - now FL,C,FR,RL,RR,LFE
SWAP_CHANNEL(3, 4);
}
}
@@ -72,17 +72,11 @@ int SoundIoAudioRenderer::scoreChannelLayout(const struct SoundIoChannelLayout*
score++;
break;
// Score layouts using the back L/R as higher
// value than those using side L/R.
case SoundIoChannelIdBackLeft:
case SoundIoChannelIdBackRight:
if (opusConfig->channelCount == 6) {
// 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++;
}
score += 2;
break;
default:
@@ -217,25 +211,13 @@ bool SoundIoAudioRenderer::prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATI
m_EffectiveLayout = m_OutputStream->layout;
for (int i = 0; i < m_EffectiveLayout.channel_count; i++) {
if (opusConfig->channelCount == 6) {
// For 5.1, replace side L/R with back L/R so our channel position
// logic in sioWriteCallback() works.
if (m_EffectiveLayout.channels[i] == SoundIoChannelIdSideLeft) {
m_EffectiveLayout.channels[i] = SoundIoChannelIdBackLeft;
}
if (m_EffectiveLayout.channels[i] == SoundIoChannelIdSideRight) {
m_EffectiveLayout.channels[i] = SoundIoChannelIdBackRight;
}
// Fixup the layout to use back L/R so our channel position
// logic in sioWriteCallback() works.
if (m_EffectiveLayout.channels[i] == SoundIoChannelIdSideLeft) {
m_EffectiveLayout.channels[i] = SoundIoChannelIdBackLeft;
}
else if (opusConfig->channelCount == 8) {
// For 5.1, replace side L/R with LOC/ROC so our channel position
// 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;
}
if (m_EffectiveLayout.channels[i] == SoundIoChannelIdSideRight) {
m_EffectiveLayout.channels[i] = SoundIoChannelIdBackRight;
}
}
@@ -429,8 +411,9 @@ void SoundIoAudioRenderer::sioWriteCallback(SoundIoOutStream* stream, int frameC
for (int frame = 0; frame < frameCount; frame++) {
for (int ch = 0; ch < me->m_EffectiveLayout.channel_count; ch++) {
// SoundIoChannelId - 1 happens to match Moonlight's channel layout
// after we've applied our fixups to m_EffectiveLayout for 5.1 and 7.1.
// SoundIoChannelId - 1 happens to match Moonlight's channel layout.
// 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;
if (frame >= framesLeft || readPtrChannel >= me->m_OpusChannelCount) {
File diff suppressed because it is too large Load Diff
@@ -5,9 +5,6 @@
#include <SDL.h>
#define SDL_CODE_HIDE_CURSOR 1
#define SDL_CODE_SHOW_CURSOR 2
struct GamepadState {
SDL_GameController* controller;
SDL_JoystickID jsId;
@@ -46,8 +43,6 @@ public:
~SdlInputHandler();
void setWindow(SDL_Window* window);
void handleKeyEvent(SDL_KeyboardEvent* event);
void handleMouseButtonEvent(SDL_MouseButtonEvent* event);
@@ -72,20 +67,10 @@ public:
void raiseAllKeys();
void notifyMouseLeave();
void notifyFocusLost();
bool isCaptureActive();
void setCaptureActive(bool active);
bool isMouseInVideoRegion(int mouseX, int mouseY, int windowWidth = -1, int windowHeight = -1);
void updateMousePositionReport(int mouseX, int mouseY);
void flushMousePositionUpdate();
static
QString getUnmappedGamepads();
@@ -95,19 +80,6 @@ private:
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
Uint32 releaseLeftButtonTimerCallback(Uint32 interval, void* param);
@@ -117,45 +89,31 @@ private:
static
Uint32 dragTimerCallback(Uint32 interval, void* param);
SDL_Window* m_Window;
static
Uint32 mouseMoveTimerCallback(Uint32 interval, void* param);
static
Uint32 mouseEmulationTimerCallback(Uint32 interval, void* param);
bool m_MultiController;
bool m_GamepadMouse;
bool m_SwapMouseButtons;
SDL_TimerID m_MouseMoveTimer;
SDL_atomic_t m_MouseDeltaX;
SDL_atomic_t m_MouseDeltaY;
SDL_SpinLock m_MousePositionLock;
struct {
int x, y;
int windowWidth, windowHeight;
} m_MousePositionReport;
SDL_atomic_t m_MousePositionUpdated;
bool m_MouseWasInVideoRegion;
bool m_PendingMouseButtonsAllUpOnVideoRegionLeave;
int m_GamepadMask;
GamepadState m_GamepadState[MAX_GAMEPADS];
QSet<short> m_KeysDown;
bool m_FakeCaptureActive;
QString m_OldIgnoreDevices;
QString m_OldIgnoreDevicesExcept;
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];
float m_CumulativeDelta[MAX_FINGERS];
SDL_TimerID m_LeftButtonReleaseTimer;
SDL_TimerID m_RightButtonReleaseTimer;
SDL_TimerID m_DragTimer;
char m_DragButton;
int m_NumFingersDown;
int m_StreamWidth;
int m_StreamHeight;
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;
}
-346
View File
@@ -1,346 +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_SwapMouseButtons(prefs.swapMouseButtons),
m_MouseMoveTimer(0),
m_MousePositionLock(0),
m_MouseWasInVideoRegion(false),
m_PendingMouseButtonsAllUpOnVideoRegionLeave(false),
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
m_OldIgnoreDevices = SDL_GetHint(SDL_HINT_GAMECONTROLLER_IGNORE_DEVICES);
m_OldIgnoreDevicesExcept = SDL_GetHint(SDL_HINT_GAMECONTROLLER_IGNORE_DEVICES_EXCEPT);
QString streamIgnoreDevices = qgetenv("STREAM_GAMECONTROLLER_IGNORE_DEVICES");
QString streamIgnoreDevicesExcept = qgetenv("STREAM_GAMECONTROLLER_IGNORE_DEVICES_EXCEPT");
if (!streamIgnoreDevices.isEmpty() && !streamIgnoreDevices.endsWith(',')) {
streamIgnoreDevices += ',';
}
streamIgnoreDevices += m_OldIgnoreDevices;
// For SDL_HINT_GAMECONTROLLER_IGNORE_DEVICES, we use the union of SDL_GAMECONTROLLER_IGNORE_DEVICES
// and STREAM_GAMECONTROLLER_IGNORE_DEVICES while streaming. STREAM_GAMECONTROLLER_IGNORE_DEVICES_EXCEPT
// overrides SDL_GAMECONTROLLER_IGNORE_DEVICES_EXCEPT while streaming.
SDL_SetHint(SDL_HINT_GAMECONTROLLER_IGNORE_DEVICES, streamIgnoreDevices.toUtf8());
SDL_SetHint(SDL_HINT_GAMECONTROLLER_IGNORE_DEVICES_EXCEPT, streamIgnoreDevicesExcept.toUtf8());
// 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_zero(m_MousePositionReport);
SDL_AtomicSet(&m_MouseDeltaX, 0);
SDL_AtomicSet(&m_MouseDeltaY, 0);
SDL_AtomicSet(&m_MousePositionUpdated, 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");
// Restore the ignored devices
SDL_SetHint(SDL_HINT_GAMECONTROLLER_IGNORE_DEVICES, m_OldIgnoreDevices.toUtf8());
SDL_SetHint(SDL_HINT_GAMECONTROLLER_IGNORE_DEVICES_EXCEPT, m_OldIgnoreDevicesExcept.toUtf8());
#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;
}
// Synchronize the client and host cursor when activating absolute capture
if (m_AbsoluteMouseMode) {
int mouseX, mouseY;
int windowX, windowY;
// We have to use SDL_GetGlobalMouseState() because macOS may not reflect
// the new position of the mouse when outside the window.
SDL_GetGlobalMouseState(&mouseX, &mouseY);
// Convert global mouse state to window-relative
SDL_GetWindowPosition(m_Window, &windowX, &windowY);
mouseX -= windowX;
mouseY -= windowY;
if (isMouseInVideoRegion(mouseX, mouseY)) {
updateMousePositionReport(mouseX, mouseY);
}
}
}
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);
}
-277
View File
@@ -1,277 +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 &&
isMouseInVideoRegion(event->x, event->y)) {
// 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;
}
else if (m_AbsoluteMouseMode && !isMouseInVideoRegion(event->x, event->y) && event->state == SDL_PRESSED) {
// Ignore button presses outside the video region, but allow button releases
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;
}
if (m_SwapMouseButtons) {
if (button == BUTTON_RIGHT)
button = BUTTON_LEFT;
else if (button == BUTTON_LEFT)
button = BUTTON_RIGHT;
}
// Ensure any pending mouse position update has been sent before we send
// our button event. This ensures that the cursor is in the correct location
// when the button event is issued.
//
// On platforms like macOS, the mouse doesn't track when the window isn't
// focused. When we gain focus via mouse click, we immediately get a mouse
// move event and a mouse button event. If we don't flush here, the button
// will probably arrive before the mouse timer issues the position update.
flushMousePositionUpdate();
LiSendMouseButtonEvent(event->state == SDL_PRESSED ?
BUTTON_ACTION_PRESS :
BUTTON_ACTION_RELEASE,
button);
}
void SdlInputHandler::updateMousePositionReport(int mouseX, int mouseY)
{
int windowWidth, windowHeight;
// Call SDL_GetWindowSize() before entering the spinlock
SDL_GetWindowSize(m_Window, &windowWidth, &windowHeight);
SDL_AtomicLock(&m_MousePositionLock);
m_MousePositionReport.x = mouseX;
m_MousePositionReport.y = mouseY;
m_MousePositionReport.windowWidth = windowWidth;
m_MousePositionReport.windowHeight = windowHeight;
SDL_AtomicUnlock(&m_MousePositionLock);
SDL_AtomicSet(&m_MousePositionUpdated, 1);
}
void SdlInputHandler::flushMousePositionUpdate()
{
bool hasNewPosition = SDL_AtomicSet(&m_MousePositionUpdated, 0) != 0;
if (hasNewPosition) {
// If the lock is held now, the main thread is trying to update
// the mouse position. We'll pick up the new position next time.
if (SDL_AtomicTryLock(&m_MousePositionLock)) {
SDL_Rect src, dst;
bool mouseInVideoRegion;
src.x = src.y = 0;
src.w = m_StreamWidth;
src.h = m_StreamHeight;
dst.x = dst.y = 0;
dst.w = m_MousePositionReport.windowWidth;
dst.h = m_MousePositionReport.windowHeight;
// Use the stream and window sizes to determine the video region
StreamUtils::scaleSourceToDestinationSurface(&src, &dst);
mouseInVideoRegion = isMouseInVideoRegion(m_MousePositionReport.x,
m_MousePositionReport.y,
m_MousePositionReport.windowWidth,
m_MousePositionReport.windowHeight);
// Clamp motion to the video region
short x = qMin(qMax(m_MousePositionReport.x - dst.x, 0), dst.w);
short y = qMin(qMax(m_MousePositionReport.y - dst.y, 0), dst.h);
// Release the spinlock to unblock the main thread
SDL_AtomicUnlock(&m_MousePositionLock);
// Send the mouse position update if one of the following is true:
// a) it is in the video region now
// b) it just left the video region (to ensure the mouse is clamped to the video boundary)
// c) a mouse button is still down from before the cursor left the video region (to allow smooth dragging)
Uint32 buttonState = SDL_GetMouseState(nullptr, nullptr);
if (buttonState == 0) {
m_PendingMouseButtonsAllUpOnVideoRegionLeave = false;
}
if (mouseInVideoRegion || m_MouseWasInVideoRegion || m_PendingMouseButtonsAllUpOnVideoRegionLeave) {
LiSendMousePositionEvent(x, y, dst.w, dst.h);
}
// Adjust the cursor visibility if applicable
if (mouseInVideoRegion ^ m_MouseWasInVideoRegion) {
// We must push an event for the main thread to process, because it's not safe
// to directly can SDL_ShowCursor() on the arbitrary thread on which this timer
// executes.
SDL_Event event;
event.type = SDL_USEREVENT;
event.user.code = mouseInVideoRegion ? SDL_CODE_HIDE_CURSOR : SDL_CODE_SHOW_CURSOR;
SDL_PushEvent(&event);
if (!mouseInVideoRegion && buttonState != 0) {
// If we still have a button pressed on leave, wait for that to come up
// before we stop sending mouse position events.
m_PendingMouseButtonsAllUpOnVideoRegionLeave = true;
}
}
m_MouseWasInVideoRegion = mouseInVideoRegion;
}
}
}
void SdlInputHandler::handleMouseMotionEvent(SDL_MouseMotionEvent* event)
{
if (!isCaptureActive()) {
// 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.
if (m_AbsoluteMouseMode) {
updateMousePositionReport(event->x, event->y);
}
else {
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 (m_AbsoluteMouseMode) {
int mouseX, mouseY;
SDL_GetMouseState(&mouseX, &mouseY);
if (!isMouseInVideoRegion(mouseX, mouseY)) {
// Ignore scroll events outside the video region
return;
}
}
if (event->y != 0) {
LiSendScrollEvent((signed char)event->y);
}
}
bool SdlInputHandler::isMouseInVideoRegion(int mouseX, int mouseY, int windowWidth, int windowHeight)
{
SDL_Rect src, dst;
if (windowWidth < 0 || windowHeight < 0) {
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);
return (mouseX >= dst.x && mouseX <= dst.x + dst.w) &&
(mouseY >= dst.y && mouseY <= dst.y + dst.h);
}
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);
}
// Send mouse position updates if applicable
me->flushMousePositionUpdate();
#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] = {};
}
}
+147 -323
View File
@@ -30,18 +30,6 @@
#include <QSvgRenderer>
#include <QPainter>
#include <QImage>
#include <QGuiApplication>
#include <QCursor>
#define CONN_TEST_SERVER "qt.conntest.moonlight-stream.org"
// Running the connection process asynchronously seems to reliably
// cause a crash in QSGRenderThread on Wayland and strange crashes
// elsewhere. Until these are figured out, avoid the async connect
// thread on Linux and BSDs.
#if !defined(Q_OS_UNIX) || defined(Q_OS_DARWIN)
#define USE_ASYNC_CONNECT_THREAD 1
#endif
CONNECTION_LISTENER_CALLBACKS Session::k_ConnCallbacks = {
Session::clStageStarting,
@@ -49,6 +37,8 @@ CONNECTION_LISTENER_CALLBACKS Session::k_ConnCallbacks = {
Session::clStageFailed,
nullptr,
Session::clConnectionTerminated,
nullptr,
nullptr,
Session::clLogMessage,
Session::clRumble,
Session::clConnectionStatusUpdate
@@ -63,53 +53,28 @@ void Session::clStageStarting(int stage)
// which happens to be the main thread, so it's cool to interact
// with the GUI in these callbacks.
emit s_ActiveSession->stageStarting(QString::fromLocal8Bit(LiGetStageName(stage)));
#ifndef USE_ASYNC_CONNECT_THREAD
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
QCoreApplication::sendPostedEvents();
#endif
}
void Session::clStageFailed(int stage, int errorCode)
void Session::clStageFailed(int stage, long errorCode)
{
// Perform the port test now, while we're on the async connection thread and not blocking the UI.
s_ActiveSession->m_PortTestResults = LiTestClientConnectivity(CONN_TEST_SERVER, 443, LiGetPortFlagsFromStage(stage));
// We know this is called on the same thread as LiStartConnection()
// which happens to be the main thread, so it's cool to interact
// with the GUI in these callbacks.
emit s_ActiveSession->stageFailed(QString::fromLocal8Bit(LiGetStageName(stage)), errorCode);
#ifndef USE_ASYNC_CONNECT_THREAD
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
QCoreApplication::sendPostedEvents();
#endif
}
void Session::clConnectionTerminated(int errorCode)
void Session::clConnectionTerminated(long errorCode)
{
s_ActiveSession->m_PortTestResults = LiTestClientConnectivity(CONN_TEST_SERVER, 443, LiGetPortFlagsFromTerminationErrorCode(errorCode));
// Display the termination dialog if this was not intended
switch (errorCode) {
case ML_ERROR_GRACEFUL_TERMINATION:
break;
case ML_ERROR_NO_VIDEO_TRAFFIC:
s_ActiveSession->m_UnexpectedTermination = true;
emit s_ActiveSession->displayLaunchError("No video received from host. Check the host PC's firewall and port forwarding rules.");
break;
case ML_ERROR_NO_VIDEO_FRAME:
s_ActiveSession->m_UnexpectedTermination = true;
emit s_ActiveSession->displayLaunchError("Your network connection isn't performing well. Reduce your video bitrate setting or try a faster connection.");
break;
default:
if (errorCode != 0) {
s_ActiveSession->m_UnexpectedTermination = true;
emit s_ActiveSession->displayLaunchError("Connection terminated");
break;
}
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Connection terminated: %d",
"Connection terminated: %ld",
errorCode);
// Push a quit event to the main loop
@@ -285,25 +250,6 @@ int Session::drSubmitDecodeUnit(PDECODE_UNIT du)
}
}
void Session::getDecoderInfo(SDL_Window* window,
bool& isHardwareAccelerated, bool& isFullScreenOnly, QSize& maxResolution)
{
IVideoDecoder* decoder;
if (!chooseDecoder(StreamingPreferences::VDS_AUTO,
window, VIDEO_FORMAT_H264, 1920, 1080, 60,
true, false, true, decoder)) {
isHardwareAccelerated = isFullScreenOnly = false;
return;
}
isHardwareAccelerated = decoder->isHardwareAccelerated();
isFullScreenOnly = decoder->isAlwaysFullScreen();
maxResolution = decoder->getDecoderMaxResolution();
delete decoder;
}
bool Session::isHardwareDecodeAvailable(SDL_Window* window,
StreamingPreferences::VideoDecoderSelection vds,
int videoFormat, int width, int height, int frameRate)
@@ -361,8 +307,6 @@ Session::Session(NvComputer* computer, NvApp& app, StreamingPreferences *prefere
m_InputHandler(nullptr),
m_InputHandlerLock(0),
m_MouseEmulationRefCount(0),
m_AsyncConnectionSuccess(false),
m_PortTestResults(0),
m_OpusDecoder(nullptr),
m_AudioRenderer(nullptr),
m_AudioSampleCount(0),
@@ -390,21 +334,13 @@ bool Session::initialize()
}
// Create a hidden window to use for decoder initialization tests
SDL_Window* testWindow = SDL_CreateWindow("", 0, 0, 1280, 720,
SDL_WINDOW_HIDDEN | StreamUtils::getPlatformWindowFlags());
SDL_Window* testWindow = SDL_CreateWindow("", 0, 0, 1280, 720, SDL_WINDOW_HIDDEN);
if (!testWindow) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Failed to create test window with platform flags: %s",
SDL_GetError());
testWindow = SDL_CreateWindow("", 0, 0, 1280, 720, SDL_WINDOW_HIDDEN);
if (!testWindow) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to create window for hardware decode test: %s",
SDL_GetError());
SDL_QuitSubSystem(SDL_INIT_VIDEO);
return false;
}
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to create window for hardware decode test: %s",
SDL_GetError());
SDL_QuitSubSystem(SDL_INIT_VIDEO);
return false;
}
qInfo() << "Server GPU:" << m_Computer->gpuModel;
@@ -439,9 +375,6 @@ bool Session::initialize()
case StreamingPreferences::AC_51_SURROUND:
m_StreamConfig.audioConfiguration = AUDIO_CONFIGURATION_51_SURROUND;
break;
case StreamingPreferences::AC_71_SURROUND:
m_StreamConfig.audioConfiguration = AUDIO_CONFIGURATION_71_SURROUND;
break;
}
LiInitializeAudioCallbacks(&m_AudioCallbacks);
@@ -451,11 +384,8 @@ bool Session::initialize()
m_AudioCallbacks.capabilities = getAudioRendererCapabilities(m_StreamConfig.audioConfiguration);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Audio channel count: %d",
CHANNEL_COUNT_FROM_AUDIO_CONFIGURATION(m_StreamConfig.audioConfiguration));
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Audio channel mask: %X",
CHANNEL_MASK_FROM_AUDIO_CONFIGURATION(m_StreamConfig.audioConfiguration));
"Audio configuration: %d",
m_StreamConfig.audioConfiguration);
switch (m_Preferences->videoCodecConfig)
{
@@ -511,17 +441,6 @@ bool Session::initialize()
break;
}
#if !SDL_VERSION_ATLEAST(2, 0, 11)
// HACK: Using a full-screen window breaks mouse capture on the Pi's LXDE
// GUI environment. Force the session to use windowed mode (which won't
// really matter anyway because the MMAL renderer always draws full-screen).
if (qgetenv("DESKTOP_SESSION") == "LXDE-pi") {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Forcing windowed mode on LXDE-Pi");
m_FullScreenFlag = 0;
}
#endif
// Check for validation errors/warnings and emit
// signals for them, if appropriate
bool ret = validateLaunch(testWindow);
@@ -555,7 +474,6 @@ void Session::emitLaunchWarning(QString text)
// Pump the UI loop while we wait
SDL_Delay(5);
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
QCoreApplication::sendPostedEvents();
}
}
@@ -563,18 +481,8 @@ bool Session::validateLaunch(SDL_Window* testWindow)
{
QStringList warningList;
if (m_Preferences->absoluteMouseMode && !m_App.isAppCollectorGame) {
emitLaunchWarning("Your selection to enable remote desktop mouse mode may cause problems in games.");
}
if (m_Preferences->videoDecoderSelection == StreamingPreferences::VDS_FORCE_SOFTWARE) {
if (m_Preferences->videoCodecConfig == StreamingPreferences::VCC_FORCE_HEVC_HDR) {
emitLaunchWarning("HDR is not supported with software decoding.");
m_StreamConfig.enableHdr = false;
}
else {
emitLaunchWarning("Your settings selection to force software decoding may cause poor streaming performance.");
}
emitLaunchWarning("Your settings selection to force software decoding may cause poor streaming performance.");
}
if (m_Preferences->unsupportedFps && m_StreamConfig.fps > 60) {
@@ -661,12 +569,12 @@ bool Session::validateLaunch(SDL_Window* testWindow)
// Test if audio works at the specified audio configuration
bool audioTestPassed = testAudio(m_StreamConfig.audioConfiguration);
// Gracefully degrade to stereo if surround sound doesn't work
if (!audioTestPassed && CHANNEL_COUNT_FROM_AUDIO_CONFIGURATION(m_StreamConfig.audioConfiguration) > 2) {
// Gracefully degrade to stereo if 5.1 doesn't work
if (!audioTestPassed && m_StreamConfig.audioConfiguration == AUDIO_CONFIGURATION_51_SURROUND) {
audioTestPassed = testAudio(AUDIO_CONFIGURATION_STEREO);
if (audioTestPassed) {
m_StreamConfig.audioConfiguration = AUDIO_CONFIGURATION_STEREO;
emitLaunchWarning("Your selected surround sound setting is not supported by the current audio device.");
emitLaunchWarning("5.1 surround sound is not supported by the current audio device.");
}
}
@@ -681,18 +589,6 @@ bool Session::validateLaunch(SDL_Window* testWindow)
emitLaunchWarning("An attached gamepad has no mapping and won't be usable. Visit the Moonlight help to resolve this.");
}
// NVENC will fail to initialize when using dimensions over 4096 and H.264.
if (m_StreamConfig.width > 4096 || m_StreamConfig.height > 4096) {
if (m_Computer->maxLumaPixelsHEVC == 0) {
emit displayLaunchError("Your host PC's GPU doesn't support streaming video resolutions over 4K.");
return false;
}
else if (!m_StreamConfig.supportsHevc) {
emit displayLaunchError("Video resolutions over 4K are only supported by the HEVC codec.");
return false;
}
}
if (m_Preferences->videoDecoderSelection == StreamingPreferences::VDS_FORCE_HARDWARE &&
!m_StreamConfig.enableHdr && // HEVC Main10 was already checked for hardware decode support above
!isHardwareDecodeAvailable(testWindow,
@@ -742,7 +638,7 @@ private:
emit m_Session->quitStarting();
}
else {
emit m_Session->sessionFinished(m_Session->m_PortTestResults);
emit m_Session->sessionFinished();
}
// Finish cleanup of the connection state
@@ -760,7 +656,7 @@ private:
}
// Session is finished now
emit m_Session->sessionFinished(m_Session->m_PortTestResults);
emit m_Session->sessionFinished();
}
}
@@ -913,18 +809,10 @@ void Session::toggleFullscreen()
bool fullScreen = !(SDL_GetWindowFlags(m_Window) & m_FullScreenFlag);
if (fullScreen) {
if (m_FullScreenFlag == SDL_WINDOW_FULLSCREEN && m_InputHandler->isCaptureActive()) {
// Confine the cursor to the window if we're capturing input
SDL_SetWindowGrab(m_Window, SDL_TRUE);
}
SDL_SetWindowResizable(m_Window, SDL_FALSE);
SDL_SetWindowFullscreen(m_Window, m_FullScreenFlag);
}
else {
// Unconfine the cursor
SDL_SetWindowGrab(m_Window, SDL_FALSE);
SDL_SetWindowFullscreen(m_Window, 0);
SDL_SetWindowResizable(m_Window, SDL_TRUE);
@@ -949,37 +837,39 @@ void Session::notifyMouseEmulationMode(bool enabled)
}
}
class AsyncConnectionStartThread : public QThread
void Session::exec(int displayOriginX, int displayOriginY)
{
public:
AsyncConnectionStartThread(Session* session) :
QThread(nullptr),
m_Session(session) {}
m_DisplayOriginX = displayOriginX;
m_DisplayOriginY = displayOriginY;
void run() override
{
m_Session->m_AsyncConnectionSuccess = m_Session->startConnectionAsync();
// Complete initialization in this deferred context to avoid
// calling expensive functions in the constructor (during the
// process of loading the StreamSegue).
if (!initialize()) {
emit sessionFinished();
return;
}
Session* m_Session;
};
// Called in a non-main thread
bool Session::startConnectionAsync()
{
// Wait 1.5 seconds before connecting to let the user
// have time to read any messages present on the segue
#ifdef USE_ASYNC_CONNECT_THREAD
SDL_Delay(1500);
#else
uint32_t start = SDL_GetTicks();
while (!SDL_TICKS_PASSED(SDL_GetTicks(), start + 1500)) {
// Pump the UI loop while we wait since we're not async
// Pump the UI loop while we wait
SDL_Delay(5);
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
QCoreApplication::sendPostedEvents();
}
#endif
// Wait for any old session to finish cleanup
s_ActiveSessionSemaphore.acquire();
// We're now active
s_ActiveSession = this;
// Initialize the gamepad code with our preferences
StreamingPreferences prefs;
m_InputHandler = new SdlInputHandler(prefs, m_Computer,
m_StreamConfig.width,
m_StreamConfig.height);
// The UI should have ensured the old game was already quit
// if we decide to stream a different game.
@@ -996,7 +886,7 @@ bool Session::startConnectionAsync()
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Found host supported resolution: %dx%d",
mode.width, mode.height);
enableGameOptimizations = m_Preferences->gameOptimizations;
enableGameOptimizations = prefs.gameOptimizations;
break;
}
}
@@ -1009,15 +899,23 @@ bool Session::startConnectionAsync()
else {
http.launchApp(m_App.id, &m_StreamConfig,
enableGameOptimizations,
m_Preferences->playAudioOnHost,
prefs.playAudioOnHost,
m_InputHandler->getAttachedGamepadMask());
}
} catch (const GfeHttpResponseException& e) {
delete m_InputHandler;
m_InputHandler = nullptr;
SDL_QuitSubSystem(SDL_INIT_VIDEO);
emit displayLaunchError("GeForce Experience returned error: " + e.toQString());
return false;
QThreadPool::globalInstance()->start(new DeferredSessionCleanupTask(this));
return;
} catch (const QtNetworkReplyException& e) {
delete m_InputHandler;
m_InputHandler = nullptr;
SDL_QuitSubSystem(SDL_INIT_VIDEO);
emit displayLaunchError(e.toQString());
return false;
QThreadPool::globalInstance()->start(new DeferredSessionCleanupTask(this));
return;
}
QByteArray hostnameStr = m_Computer->activeAddress.toLatin1();
@@ -1036,28 +934,17 @@ bool Session::startConnectionAsync()
hostInfo.serverInfoGfeVersion = siGfeVersion.data();
}
if (m_Preferences->packetSize != 0) {
// Override default packet size and remote streaming detection
// NB: Using STREAM_CFG_AUTO will cap our packet size at 1024 for remote hosts.
m_StreamConfig.streamingRemotely = STREAM_CFG_LOCAL;
m_StreamConfig.packetSize = m_Preferences->packetSize;
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Using custom packet size: %d bytes",
m_Preferences->packetSize);
// isReachableOverVpn() does network I/O, so we only attempt to check
// VPN reachability if we've already contacted the PC successfully
if (m_Computer->isReachableOverVpn()) {
// It looks like our route to this PC is over a VPN.
// Treat it as remote even if the target address is in RFC 1918 address space.
m_StreamConfig.streamingRemotely = STREAM_CFG_REMOTE;
m_StreamConfig.packetSize = 1024;
}
else {
// isReachableOverVpn() does network I/O, so we only attempt to check
// VPN reachability if we've already contacted the PC successfully
if (m_Computer->isReachableOverVpn()) {
// It looks like our route to this PC is over a VPN.
// Treat it as remote even if the target address is in RFC 1918 address space.
m_StreamConfig.streamingRemotely = STREAM_CFG_REMOTE;
m_StreamConfig.packetSize = 1024;
}
else {
m_StreamConfig.streamingRemotely = STREAM_CFG_AUTO;
m_StreamConfig.packetSize = 1392;
}
m_StreamConfig.streamingRemotely = STREAM_CFG_AUTO;
m_StreamConfig.packetSize = 1392;
}
int err = LiStartConnection(&hostInfo, &m_StreamConfig, &k_ConnCallbacks,
@@ -1067,60 +954,6 @@ bool Session::startConnectionAsync()
if (err != 0) {
// We already displayed an error dialog in the stage failure
// listener.
return false;
}
emit connectionStarted();
return true;
}
void Session::exec(int displayOriginX, int displayOriginY)
{
m_DisplayOriginX = displayOriginX;
m_DisplayOriginY = displayOriginY;
// Complete initialization in this deferred context to avoid
// calling expensive functions in the constructor (during the
// process of loading the StreamSegue).
//
// NB: This initializes the SDL video subsystem, so it must be
// called on the main thread.
if (!initialize()) {
emit sessionFinished(0);
return;
}
// Wait for any old session to finish cleanup
s_ActiveSessionSemaphore.acquire();
// We're now active
s_ActiveSession = this;
// Initialize the gamepad code with our preferences
// NB: m_InputHandler must be initialize before starting the connection.
m_InputHandler = new SdlInputHandler(*m_Preferences, m_Computer,
m_StreamConfig.width,
m_StreamConfig.height);
// Kick off the async connection thread while we sit here and pump the event loop
AsyncConnectionStartThread asyncConnThread(this);
#ifdef USE_ASYNC_CONNECT_THREAD
asyncConnThread.start();
while (!asyncConnThread.wait(10)) {
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
QCoreApplication::sendPostedEvents();
}
#else
asyncConnThread.run();
#endif
// Pump the event loop one last time to ensure we pick up any events from
// the thread that happened while it was in the final successful QThread::wait().
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
QCoreApplication::sendPostedEvents();
// If the connection failed, clean up and abort the connection.
if (!m_AsyncConnectionSuccess) {
delete m_InputHandler;
m_InputHandler = nullptr;
SDL_QuitSubSystem(SDL_INIT_VIDEO);
@@ -1128,48 +961,30 @@ void Session::exec(int displayOriginX, int displayOriginY)
return;
}
// Pump the message loop to update the UI
emit connectionStarted();
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
int x, y, width, height;
getWindowDimensions(x, y, width, height);
#ifdef STEAM_LINK
// We need a little delay before creating the window or we will trigger some kind
// of graphics driver bug on Steam Link that causes a jagged overlay to appear in
// the top right corner randomly.
SDL_Delay(500);
#endif
m_Window = SDL_CreateWindow("Moonlight",
x,
y,
width,
height,
SDL_WINDOW_ALLOW_HIGHDPI | StreamUtils::getPlatformWindowFlags());
SDL_WINDOW_ALLOW_HIGHDPI);
if (!m_Window) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"SDL_CreateWindow() failed with platform flags: %s",
SDL_GetError());
m_Window = SDL_CreateWindow("Moonlight",
x,
y,
width,
height,
SDL_WINDOW_ALLOW_HIGHDPI);
if (!m_Window) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_CreateWindow() failed: %s",
SDL_GetError());
delete m_InputHandler;
m_InputHandler = nullptr;
SDL_QuitSubSystem(SDL_INIT_VIDEO);
QThreadPool::globalInstance()->start(new DeferredSessionCleanupTask(this));
return;
}
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_CreateWindow() failed: %s",
SDL_GetError());
delete m_InputHandler;
m_InputHandler = nullptr;
SDL_QuitSubSystem(SDL_INIT_VIDEO);
QThreadPool::globalInstance()->start(new DeferredSessionCleanupTask(this));
return;
}
m_InputHandler->setWindow(m_Window);
QSvgRenderer svgIconRenderer(QString(":/res/moonlight.svg"));
QImage svgImage(ICON_SIZE, ICON_SIZE, QImage::Format_RGBA8888);
svgImage.fill(0);
@@ -1214,21 +1029,18 @@ void Session::exec(int displayOriginX, int displayOriginY)
SDL_SetWindowFullscreen(m_Window, m_FullScreenFlag);
}
bool needsFirstEnterCapture = false;
bool needsPostDecoderCreationCapture = false;
// HACK: For Wayland, we wait until we get the first SDL_WINDOWEVENT_ENTER
// event where it seems to work consistently on GNOME. For other platforms,
// especially where SDL may call SDL_RecreateWindow(), we must only capture
// after the decoder is created.
if (strcmp(SDL_GetCurrentVideoDriver(), "wayland") == 0) {
// Native Wayland: Capture on SDL_WINDOWEVENT_ENTER
needsFirstEnterCapture = true;
}
else {
// X11/XWayland: Capture after decoder creation
needsPostDecoderCreationCapture = true;
#ifndef QT_DEBUG
// Capture the mouse by default on release builds only.
// This prevents the mouse from becoming trapped inside
// Moonlight when it's halted at a debug break.
if (m_Preferences->windowMode != StreamingPreferences::WM_WINDOWED) {
// HACK: This doesn't work on Wayland until we render a frame, so
// just don't do it for now.
if (strcmp(SDL_GetCurrentVideoDriver(), "wayland") != 0) {
m_InputHandler->setCaptureActive(true);
}
}
#endif
// Stop text input. SDL enables it by default
// when we initialize the video subsystem, but this
@@ -1239,11 +1051,6 @@ void Session::exec(int displayOriginX, int displayOriginY)
// Disable the screen saver
SDL_DisableScreenSaver();
// Hide Qt's fake mouse cursor on EGLFS systems
if (QGuiApplication::platformName() == "eglfs") {
QGuiApplication::setOverrideCursor(QCursor(Qt::BlankCursor));
}
// Set timer resolution to 1 ms on Windows for greater
// sleep precision and more accurate callback timing.
SDL_SetHint(SDL_HINT_TIMER_RESOLUTION, "1");
@@ -1256,7 +1063,7 @@ void Session::exec(int displayOriginX, int displayOriginY)
m_UnexpectedTermination = false;
// Start rich presence to indicate we're in game
RichPresenceManager presence(*m_Preferences, m_App.name);
RichPresenceManager presence(prefs, m_App.name);
// Hijack this thread to be the SDL main thread. We have to do this
// because we want to suspend all Qt processing until the stream is over.
@@ -1284,33 +1091,48 @@ void Session::exec(int displayOriginX, int displayOriginY)
goto DispatchDeferredCleanup;
case SDL_USEREVENT:
switch (event.user.code) {
case SDL_CODE_FRAME_READY:
m_VideoDecoder->renderFrameOnMainThread();
break;
case SDL_CODE_HIDE_CURSOR:
SDL_ShowCursor(SDL_DISABLE);
break;
case SDL_CODE_SHOW_CURSOR:
SDL_ShowCursor(SDL_ENABLE);
break;
default:
SDL_assert(false);
}
SDL_assert(event.user.code == SDL_CODE_FRAME_READY);
m_VideoDecoder->renderFrameOnMainThread();
break;
case SDL_WINDOWEVENT:
if (event.window.event == SDL_WINDOWEVENT_FOCUS_LOST) {
m_InputHandler->notifyFocusLost();
}
else if (event.window.event == SDL_WINDOWEVENT_LEAVE) {
m_InputHandler->notifyMouseLeave();
// Capture mouse cursor when user actives the window by clicking on
// window's client area (borders and title bar excluded).
// Without this you would have to click the window twice (once to
// activate it, second time to enable capture). With this you need to
// click it only once.
// On Linux, the button press event is delivered after the focus gain
// so this is not neccessary (and leads to a click sent to the host
// when focusing the window by clicking).
// By excluding window's borders and title bar out, lets user still
// interact with them without mouse capture kicking in.
#if defined(Q_OS_WIN32) || defined(Q_OS_DARWIN)
if (event.window.event == SDL_WINDOWEVENT_FOCUS_GAINED) {
int mouseX, mouseY;
Uint32 mouseState = SDL_GetGlobalMouseState(&mouseX, &mouseY);
if (mouseState & SDL_BUTTON(SDL_BUTTON_LEFT)) {
int x, y, width, height;
SDL_GetWindowPosition(m_Window, &x, &y);
SDL_GetWindowSize(m_Window, &width, &height);
if (mouseX > x && mouseX < x+width && mouseY > y && mouseY < y+height) {
m_InputHandler->setCaptureActive(true);
}
}
}
#endif
// Capture the mouse on SDL_WINDOWEVENT_ENTER if needed
if (needsFirstEnterCapture && event.window.event == SDL_WINDOWEVENT_ENTER) {
m_InputHandler->setCaptureActive(true);
needsFirstEnterCapture = false;
if (event.window.event == SDL_WINDOWEVENT_FOCUS_LOST) {
// 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_InputHandler->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.
m_InputHandler->raiseAllKeys();
}
// We want to recreate the decoder for resizes (full-screen toggles) and the initial shown event.
@@ -1339,12 +1161,6 @@ void Session::exec(int displayOriginX, int displayOriginY)
SDL_SetWindowPosition(m_Window, x, y);
}
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Recreating renderer for window event: %d (%d %d)",
event.window.event,
event.window.data1,
event.window.data2);
// Fall through
case SDL_RENDER_DEVICE_RESET:
case SDL_RENDER_TARGETS_RESET:
@@ -1397,14 +1213,6 @@ void Session::exec(int displayOriginX, int displayOriginY)
emit displayLaunchError("Unable to initialize video decoder. Please check your streaming settings and try again.");
goto DispatchDeferredCleanup;
}
// As of SDL 2.0.12, SDL_RecreateWindow() doesn't carry over mouse capture
// or mouse hiding state to the new window. By capturing after the decoder
// is set up, this ensures the window re-creation is already done.
if (needsPostDecoderCreationCapture) {
m_InputHandler->setCaptureActive(true);
needsFirstEnterCapture = false;
}
}
// Request an IDR frame to complete the reset
@@ -1455,9 +1263,6 @@ DispatchDeferredCleanup:
m_InputHandler->setCaptureActive(false);
SDL_EnableScreenSaver();
SDL_SetHint(SDL_HINT_TIMER_RESOLUTION, "0");
if (QGuiApplication::platformName() == "eglfs") {
QGuiApplication::restoreOverrideCursor();
}
// Raise any keys that are still down
m_InputHandler->raiseAllKeys();
@@ -1477,6 +1282,25 @@ DispatchDeferredCleanup:
m_VideoDecoder = nullptr;
SDL_AtomicUnlock(&m_DecoderLock);
if (strcmp(SDL_GetCurrentVideoDriver(), "wayland") == 0) {
// HACK: SDL (as of 2.0.10) has a bug that causes Mutter not to destroy the window
// surface when in full-screen unless we render more frames after we request
// to exit full-screen. The amount of frames required is variable but 500 ms
// of frames seems sufficient in my testing.
SDL_SetWindowFullscreen(m_Window, 0);
SDL_Renderer* renderer = SDL_CreateRenderer(m_Window, -1, SDL_RENDERER_PRESENTVSYNC);
if (renderer != nullptr) {
SDL_SetRenderDrawColor(renderer, 0, 0, 0, SDL_ALPHA_OPAQUE);
for (int i = 0; i < 10; i++)
{
SDL_RenderClear(renderer);
SDL_RenderPresent(renderer);
SDL_Delay(50);
}
SDL_DestroyRenderer(renderer);
}
}
// This must be called after the decoder is deleted, because
// the renderer may want to interact with the window
SDL_DestroyWindow(m_Window);
+8 -18
View File
@@ -5,7 +5,7 @@
#include <Limelight.h>
#include <opus_multistream.h>
#include "settings/streamingpreferences.h"
#include "input/input.h"
#include "input.h"
#include "video/decoder.h"
#include "audio/renderers/renderer.h"
#include "video/overlaymanager.h"
@@ -16,7 +16,6 @@ class Session : public QObject
friend class SdlInputHandler;
friend class DeferredSessionCleanupTask;
friend class AsyncConnectionStartThread;
public:
explicit Session(NvComputer* computer, NvApp& app, StreamingPreferences *preferences = nullptr);
@@ -26,8 +25,9 @@ public:
Q_INVOKABLE void exec(int displayOriginX, int displayOriginY);
static
void getDecoderInfo(SDL_Window* window,
bool& isHardwareAccelerated, bool& isFullScreenOnly, QSize& maxResolution);
bool isHardwareDecodeAvailable(SDL_Window* window,
StreamingPreferences::VideoDecoderSelection vds,
int videoFormat, int width, int height, int frameRate);
static Session* get()
{
@@ -42,7 +42,7 @@ public:
signals:
void stageStarting(QString stage);
void stageFailed(QString stage, int errorCode);
void stageFailed(QString stage, long errorCode);
void connectionStarted();
@@ -52,13 +52,11 @@ signals:
void quitStarting();
void sessionFinished(int portTestResult);
void sessionFinished();
private:
bool initialize();
bool startConnectionAsync();
bool validateLaunch(SDL_Window* testWindow);
void emitLaunchWarning(QString text);
@@ -80,11 +78,6 @@ private:
void updateOptimalWindowDisplayMode();
static
bool isHardwareDecodeAvailable(SDL_Window* window,
StreamingPreferences::VideoDecoderSelection vds,
int videoFormat, int width, int height, int frameRate);
static
bool chooseDecoder(StreamingPreferences::VideoDecoderSelection vds,
SDL_Window* window, int videoFormat, int width, int height,
@@ -96,10 +89,10 @@ private:
void clStageStarting(int stage);
static
void clStageFailed(int stage, int errorCode);
void clStageFailed(int stage, long errorCode);
static
void clConnectionTerminated(int errorCode);
void clConnectionTerminated(long errorCode);
static
void clLogMessage(const char* format, ...);
@@ -150,9 +143,6 @@ private:
SDL_SpinLock m_InputHandlerLock;
int m_MouseEmulationRefCount;
bool m_AsyncConnectionSuccess;
int m_PortTestResults;
int m_ActiveVideoFormat;
int m_ActiveVideoWidth;
int m_ActiveVideoHeight;
+4 -11
View File
@@ -6,27 +6,20 @@
#include <ApplicationServices/ApplicationServices.h>
#endif
Uint32 StreamUtils::getPlatformWindowFlags()
{
#ifdef Q_OS_DARWIN
return SDL_WINDOW_METAL;
#else
return 0;
#endif
}
void StreamUtils::scaleSourceToDestinationSurface(SDL_Rect* src, SDL_Rect* dst)
{
int dstH = dst->w * src->h / src->w;
int dstW = dst->h * src->w / src->h;
if (dstH > dst->h) {
dst->x += (dst->w - dstW) / 2;
dst->y = 0;
dst->x = (dst->w - dstW) / 2;
dst->w = dstW;
SDL_assert(dst->w * src->h / src->w <= dst->h);
}
else {
dst->y += (dst->h - dstH) / 2;
dst->x = 0;
dst->y = (dst->h - dstH) / 2;
dst->h = dstH;
SDL_assert(dst->h * src->w / src->h <= dst->w);
}
-3
View File
@@ -5,9 +5,6 @@
class StreamUtils
{
public:
static
Uint32 getPlatformWindowFlags();
static
void scaleSourceToDestinationSurface(SDL_Rect* src, SDL_Rect* dst);
-2
View File
@@ -43,10 +43,8 @@ public:
virtual ~IVideoDecoder() {}
virtual bool initialize(PDECODER_PARAMETERS params) = 0;
virtual bool isHardwareAccelerated() = 0;
virtual bool isAlwaysFullScreen() = 0;
virtual int getDecoderCapabilities() = 0;
virtual int getDecoderColorspace() = 0;
virtual QSize getDecoderMaxResolution() = 0;
virtual int submitDecodeUnit(PDECODE_UNIT du) = 0;
virtual void renderFrameOnMainThread() = 0;
};
@@ -28,7 +28,7 @@ bool CUDARenderer::initialize(PDECODER_PARAMETERS)
return true;
}
bool CUDARenderer::prepareDecoderContext(AVCodecContext* context, AVDictionary**)
bool CUDARenderer::prepareDecoderContext(AVCodecContext* context)
{
context->hw_device_ctx = av_buffer_ref(m_HwContext);
+1 -1
View File
@@ -7,7 +7,7 @@ public:
CUDARenderer();
virtual ~CUDARenderer() override;
virtual bool initialize(PDECODER_PARAMETERS) override;
virtual bool prepareDecoderContext(AVCodecContext* context, AVDictionary** options) override;
virtual bool prepareDecoderContext(AVCodecContext* context) override;
virtual void renderFrame(AVFrame* frame) override;
virtual bool needsTestFrame() override;
virtual bool isDirectRenderingSupported() override;
+1 -12
View File
@@ -33,7 +33,7 @@ DrmRenderer::~DrmRenderer()
}
}
bool DrmRenderer::prepareDecoderContext(AVCodecContext*, AVDictionary**)
bool DrmRenderer::prepareDecoderContext(AVCodecContext*)
{
/* Nothing to do */
@@ -194,19 +194,8 @@ enum AVPixelFormat DrmRenderer::getPreferredPixelFormat(int)
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;
+1 -2
View File
@@ -10,10 +10,9 @@ public:
DrmRenderer();
virtual ~DrmRenderer() override;
virtual bool initialize(PDECODER_PARAMETERS params) override;
virtual bool prepareDecoderContext(AVCodecContext* context, AVDictionary** options) override;
virtual bool prepareDecoderContext(AVCodecContext* context) override;
virtual void renderFrame(AVFrame* frame) override;
virtual enum AVPixelFormat getPreferredPixelFormat(int videoFormat) override;
virtual int getRendererAttributes() override;
private:
int m_DrmFd;
+4 -29
View File
@@ -80,7 +80,7 @@ AVBufferRef* DXVA2Renderer::ffPoolAlloc(void* opaque, int)
return NULL;
}
bool DXVA2Renderer::prepareDecoderContext(AVCodecContext* context, AVDictionary**)
bool DXVA2Renderer::prepareDecoderContext(AVCodecContext* context)
{
// m_DXVAContext.workaround and report_id already initialized elsewhere
m_DXVAContext.decoder = m_Decoder;
@@ -446,7 +446,7 @@ bool DXVA2Renderer::isDecoderBlacklisted()
// https://www.intel.com/content/www/us/en/support/articles/000005654/graphics-drivers.html
if (LOWORD(id.DriverVersion.LowPart) < 4836) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Detected buggy Intel GPU driver installed. Update your Intel GPU driver to enable HEVC!");
"Intel driver version blacklisted for HEVC");
result = (m_VideoFormat & VIDEO_FORMAT_MASK_H265) != 0;
}
else {
@@ -546,34 +546,14 @@ bool DXVA2Renderer::initializeDevice(SDL_Window* window, bool enableVsync)
d3dpp.hDeviceWindow = info.info.win.window;
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) {
d3dpp.Windowed = false;
d3dpp.BackBufferWidth = currentMode.Width;
d3dpp.BackBufferHeight = currentMode.Height;
d3dpp.FullScreen_RefreshRateInHz = currentMode.RefreshRate;
if (m_VideoFormat == VIDEO_FORMAT_H265_MAIN10) {
d3dpp.BackBufferFormat = currentMode.Format = D3DFMT_A2R10G10B10;
}
else {
d3dpp.BackBufferFormat = currentMode.Format;
}
// TODO: Need to select 10-bit color format for HDR
d3dpp.BackBufferFormat = currentMode.Format;
}
else {
d3dpp.Windowed = true;
@@ -812,11 +792,6 @@ int DXVA2Renderer::getDecoderColorspace()
void DXVA2Renderer::renderFrame(AVFrame *frame)
{
if (frame == nullptr) {
// End of stream - nothing to do for us
return;
}
IDirect3DSurface9* surface = reinterpret_cast<IDirect3DSurface9*>(frame->data[3]);
HRESULT hr;
+1 -1
View File
@@ -17,7 +17,7 @@ public:
DXVA2Renderer();
virtual ~DXVA2Renderer() override;
virtual bool initialize(PDECODER_PARAMETERS params) override;
virtual bool prepareDecoderContext(AVCodecContext* context, AVDictionary** options) override;
virtual bool prepareDecoderContext(AVCodecContext* context) override;
virtual void renderFrame(AVFrame* frame) override;
virtual void notifyOverlayUpdated(Overlay::OverlayType) override;
virtual int getDecoderColorspace() override;
@@ -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,633 +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__)
SDL_Window* EGLRenderer::s_LastFailedWindow = nullptr;
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);
}
SDL_GL_DeleteContext(m_Context);
}
if (m_DummyRenderer) {
SDL_DestroyRenderer(m_DummyRenderer);
}
// 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;
}
// HACK: Work around bug where renderer will repeatedly fail with:
// SDL_CreateRenderer() failed: Could not create GLES window surface
if (m_Window == s_LastFailedWindow) {
EGL_LOG(Error, "SDL_CreateRenderer() already failed on this window!");
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());
s_LastFailedWindow = m_Window;
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) {
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,51 +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;
// HACK: Work around bug where renderer will repeatedly fail with:
// SDL_CreateRenderer() failed: Could not create GLES window surface
static SDL_Window* s_LastFailedWindow;
};
+3 -21
View File
@@ -22,16 +22,9 @@ MmalRenderer::~MmalRenderer()
}
}
bool MmalRenderer::prepareDecoderContext(AVCodecContext* context, AVDictionary** options)
bool MmalRenderer::prepareDecoderContext(AVCodecContext*)
{
// 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;
/* Nothing to do */
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Using MMAL renderer");
@@ -89,7 +82,7 @@ bool MmalRenderer::initialize(PDECODER_PARAMETERS params)
dr.alpha = 255;
dr.set |= MMAL_DISPLAY_SET_FULLSCREEN;
dr.fullscreen = true;
dr.fullscreen = (SDL_GetWindowFlags(params->window) & SDL_WINDOW_FULLSCREEN) != 0;
{
SDL_Rect src, dst;
@@ -132,12 +125,6 @@ enum AVPixelFormat MmalRenderer::getPreferredPixelFormat(int videoFormat)
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,
@@ -147,11 +134,6 @@ bool MmalRenderer::needsTestFrame()
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;
+1 -2
View File
@@ -12,11 +12,10 @@ public:
MmalRenderer();
virtual ~MmalRenderer() override;
virtual bool initialize(PDECODER_PARAMETERS params) override;
virtual bool prepareDecoderContext(AVCodecContext* context, AVDictionary** options) override;
virtual bool prepareDecoderContext(AVCodecContext* context) override;
virtual void renderFrame(AVFrame* frame) override;
virtual enum AVPixelFormat getPreferredPixelFormat(int videoFormat) override;
virtual bool needsTestFrame() override;
virtual int getRendererAttributes() override;
private:
static void InputPortCallback(MMAL_PORT_T* port, MMAL_BUFFER_HEADER_T* buffer);
@@ -45,10 +45,6 @@ Pacer::~Pacer()
m_RenderQueueNotEmpty.wakeAll();
SDL_WaitThread(m_RenderThread, nullptr);
}
else {
// Send a null AVFrame to indicate end of stream on the main thread
m_VsyncRenderer->renderFrame(nullptr);
}
// Delete any remaining unconsumed frames
while (!m_RenderQueue.isEmpty()) {
@@ -110,9 +106,6 @@ int Pacer::renderThread(void* context)
me->renderLastFrameAndUnlock();
}
// Send a null AVFrame to indicate end of stream on the render thread
me->m_VsyncRenderer->renderFrame(nullptr);
return 0;
}
+12 -52
View File
@@ -9,30 +9,16 @@ extern "C" {
#include <libavcodec/avcodec.h>
}
#ifdef HAVE_EGL
// SDL_egl.h have too many conflicts, we will do without
typedef void *EGLDisplay;
typedef void *EGLImage;
#define EGL_MAX_PLANES 4
class EGLExtensions {
public:
EGLExtensions(EGLDisplay dpy);
~EGLExtensions() {}
bool isSupported(const QString &extension) const;
private:
const QStringList m_Extensions;
};
#endif
#define RENDERER_ATTRIBUTE_FULLSCREEN_ONLY 0x01
#define RENDERER_ATTRIBUTE_1080P_MAX 0x02
class IFFmpegRenderer : public Overlay::IOverlayRenderer {
public:
enum FramePacingConstraint {
PACING_FORCE_OFF,
PACING_FORCE_ON,
PACING_ANY
};
virtual bool initialize(PDECODER_PARAMETERS params) = 0;
virtual bool prepareDecoderContext(AVCodecContext* context, AVDictionary** options) = 0;
virtual bool prepareDecoderContext(AVCodecContext* context) = 0;
virtual void renderFrame(AVFrame* frame) = 0;
virtual bool needsTestFrame() {
@@ -45,16 +31,16 @@ public:
return 0;
}
virtual int getRendererAttributes() {
// No special attributes by default
return 0;
}
virtual int getDecoderColorspace() {
// Rec 601 is default
return COLORSPACE_REC_601;
}
virtual FramePacingConstraint getFramePacingConstraint() {
// No pacing preference
return PACING_ANY;
}
virtual bool isRenderThreadSupported() {
// Render thread is supported by default
return true;
@@ -76,34 +62,8 @@ public:
}
}
virtual bool isPixelFormatSupported(int videoFormat, enum AVPixelFormat pixelFormat) {
// By default, we only support the preferred pixel format
return getPreferredPixelFormat(videoFormat) == pixelFormat;
}
// IOverlayRenderer
virtual void notifyOverlayUpdated(Overlay::OverlayType) override {
// Nothing
}
#ifdef HAVE_EGL
// By default we can't do EGL
virtual bool canExportEGL() {
return false;
}
virtual bool initializeEGL(EGLDisplay,
const EGLExtensions &) {
return false;
}
virtual ssize_t exportEGLImages(AVFrame *,
EGLDisplay,
EGLImage[EGL_MAX_PLANES]) {
return -1;
}
// Free the ressources allocated during the last `exportEGLImages` call
virtual void freeEGLImages(EGLDisplay, EGLImage[EGL_MAX_PLANES]) {}
#endif
};
+14 -43
View File
@@ -1,7 +1,6 @@
#include "sdlvid.h"
#include "streaming/session.h"
#include "streaming/streamutils.h"
#include "path.h"
#include <QDir>
@@ -59,7 +58,7 @@ SdlRenderer::~SdlRenderer()
}
}
bool SdlRenderer::prepareDecoderContext(AVCodecContext*, AVDictionary**)
bool SdlRenderer::prepareDecoderContext(AVCodecContext*)
{
/* Nothing to do */
@@ -128,19 +127,14 @@ bool SdlRenderer::isRenderThreadSupported()
return true;
}
bool SdlRenderer::isPixelFormatSupported(int, AVPixelFormat pixelFormat)
int SdlRenderer::getDecoderCapabilities()
{
// Remember to keep this in sync with SdlRenderer::renderFrame()!
switch (pixelFormat)
{
case AV_PIX_FMT_YUV420P:
case AV_PIX_FMT_NV12:
case AV_PIX_FMT_NV21:
return true;
default:
return false;
}
// Slice up to 4 times for parallel decode, once slice per core
int slices = qMin(MAX_SLICES, SDL_GetCPUCount());
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Encoder configured for %d slices per frame",
slices);
return CAPABILITY_SLICES_PER_FRAME(slices);
}
bool SdlRenderer::initialize(PDECODER_PARAMETERS params)
@@ -177,26 +171,9 @@ bool SdlRenderer::initialize(PDECODER_PARAMETERS params)
return false;
}
// SDL_CreateRenderer() can end up having to recreate our window (SDL_RecreateWindow())
// to ensure it's compatible with the renderer's OpenGL context. If that happens, we
// can get spurious SDL_WINDOWEVENT events that will cause us to (again) recreate our
// renderer. This can lead to an infinite to renderer recreation, so discard all
// SDL_WINDOWEVENT events after SDL_CreateRenderer().
SDL_PumpEvents();
SDL_FlushEvent(SDL_WINDOWEVENT);
// Calculate the video region size, scaling to fill the output size while
// preserving the aspect ratio of the video stream.
SDL_Rect src, dst;
src.x = src.y = 0;
src.w = params->width;
src.h = params->height;
dst.x = dst.y = 0;
SDL_GetRendererOutputSize(m_Renderer, &dst.w, &dst.h);
StreamUtils::scaleSourceToDestinationSurface(&src, &dst);
// Ensure the viewport is set to the desired video region
SDL_RenderSetViewport(m_Renderer, &dst);
// The window may be smaller than the stream size, so ensure our
// logical rendering surface size is equal to the stream size
SDL_RenderSetLogicalSize(m_Renderer, params->width, params->height);
// Draw a black frame until the video stream starts rendering
SDL_SetRenderDrawColor(m_Renderer, 0, 0, 0, SDL_ALPHA_OPAQUE);
@@ -229,10 +206,10 @@ void SdlRenderer::renderOverlay(Overlay::OverlayType type)
if (type == Overlay::OverlayStatusUpdate) {
// Bottom Left
SDL_Rect viewportRect;
SDL_RenderGetViewport(m_Renderer, &viewportRect);
int unused;
SDL_RenderGetLogicalSize(m_Renderer, &unused, &m_OverlayRects[type].y);
m_OverlayRects[type].x = 0;
m_OverlayRects[type].y = viewportRect.h - surface->h;
m_OverlayRects[type].y -= surface->h;
}
else if (type == Overlay::OverlayDebug) {
// Top left
@@ -259,11 +236,6 @@ void SdlRenderer::renderFrame(AVFrame* frame)
int err;
AVFrame* swFrame = nullptr;
if (frame == nullptr) {
// End of stream - nothing to do for us
return;
}
if (frame->hw_frames_ctx != nullptr) {
// If we are acting as the frontend for a hardware
// accelerated decoder, we'll need to read the frame
@@ -309,7 +281,6 @@ void SdlRenderer::renderFrame(AVFrame* frame)
if (m_Texture == nullptr) {
Uint32 sdlFormat;
// Remember to keep this in sync with SdlRenderer::isPixelFormatSupported()!
switch (frame->format)
{
case AV_PIX_FMT_YUV420P:
@@ -9,11 +9,11 @@ public:
SdlRenderer();
virtual ~SdlRenderer() override;
virtual bool initialize(PDECODER_PARAMETERS params) override;
virtual bool prepareDecoderContext(AVCodecContext* context, AVDictionary** options) override;
virtual bool prepareDecoderContext(AVCodecContext* context) override;
virtual void renderFrame(AVFrame* frame) override;
virtual void notifyOverlayUpdated(Overlay::OverlayType) override;
virtual bool isRenderThreadSupported() override;
virtual bool isPixelFormatSupported(int videoFormat, enum AVPixelFormat pixelFormat) override;
virtual int getDecoderCapabilities() override;
private:
void renderOverlay(Overlay::OverlayType type);
+87 -405
View File
@@ -1,27 +1,18 @@
#include <QString>
#include "vaapi.h"
#include "utils.h"
#include <streaming/streamutils.h>
#include <SDL_syswm.h>
#ifdef HAVE_EGL
#include <SDL_egl.h>
#endif
#include <unistd.h>
#include <fcntl.h>
VAAPIRenderer::VAAPIRenderer()
: m_HwContext(nullptr),
m_DrmFd(-1),
m_BlacklistedForDirectRendering(false)
m_DrmFd(-1)
{
#ifdef HAVE_EGL
m_PrimeDescriptor.num_layers = 0;
m_PrimeDescriptor.num_objects = 0;
m_EGLExtDmaBuf = false;
#endif
}
VAAPIRenderer::~VAAPIRenderer()
@@ -45,106 +36,26 @@ VAAPIRenderer::~VAAPIRenderer()
}
}
VADisplay
VAAPIRenderer::openDisplay(SDL_Window* window)
{
SDL_SysWMinfo info;
VADisplay display;
SDL_VERSION(&info.version);
if (!SDL_GetWindowWMInfo(window, &info)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_GetWindowWMInfo() failed: %s",
SDL_GetError());
return nullptr;
}
m_WindowSystem = info.subsystem;
if (info.subsystem == SDL_SYSWM_X11) {
m_XWindow = info.info.x11.window;
#ifdef HAVE_LIBVA_X11
display = vaGetDisplay(info.info.x11.display);
if (display == nullptr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Unable to open X11 display for VAAPI");
return nullptr;
}
#else
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Moonlight not compiled with VAAPI X11 support!");
return nullptr;
#endif
}
else if (info.subsystem == SDL_SYSWM_WAYLAND) {
#ifdef HAVE_LIBVA_WAYLAND
display = vaGetDisplayWl(info.info.wl.display);
if (display == nullptr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Unable to open Wayland display for VAAPI");
return nullptr;
}
#else
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Moonlight not compiled with VAAPI Wayland support!");
return nullptr;
#endif
}
// TODO: Upstream a better solution for SDL_GetWindowWMInfo on KMSDRM
else if (strcmp(SDL_GetCurrentVideoDriver(), "KMSDRM") == 0) {
#ifdef HAVE_LIBVA_DRM
if (m_DrmFd < 0) {
const char* device = SDL_getenv("DRM_DEV");
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 nullptr;
}
}
display = vaGetDisplayDRM(m_DrmFd);
if (display == nullptr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Unable to open DRM display for VAAPI");
return nullptr;
}
#else
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Moonlight not compiled with VAAPI DRM support!");
return nullptr;
#endif
}
else {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Unsupported VAAPI rendering subsystem: %d",
info.subsystem);
return nullptr;
}
return display;
}
bool
VAAPIRenderer::initialize(PDECODER_PARAMETERS params)
{
int err;
SDL_SysWMinfo info;
m_VideoWidth = params->width;
m_VideoHeight = params->height;
SDL_GetWindowSize(params->window, &m_DisplayWidth, &m_DisplayHeight);
SDL_VERSION(&info.version);
if (!SDL_GetWindowWMInfo(params->window, &info)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"SDL_GetWindowWMInfo() failed: %s",
SDL_GetError());
return false;
}
m_HwContext = av_hwdevice_ctx_alloc(AV_HWDEVICE_TYPE_VAAPI);
if (!m_HwContext) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
@@ -155,93 +66,79 @@ VAAPIRenderer::initialize(PDECODER_PARAMETERS params)
AVHWDeviceContext* deviceContext = (AVHWDeviceContext*)m_HwContext->data;
AVVAAPIDeviceContext* vaDeviceContext = (AVVAAPIDeviceContext*)deviceContext->hwctx;
vaDeviceContext->display = openDisplay(params->window);
if (vaDeviceContext->display == nullptr) {
// openDisplay() logs the error
m_WindowSystem = info.subsystem;
if (info.subsystem == SDL_SYSWM_X11) {
#ifdef HAVE_LIBVA_X11
m_XWindow = info.info.x11.window;
vaDeviceContext->display = vaGetDisplay(info.info.x11.display);
if (!vaDeviceContext->display) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Unable to open X11 display for VAAPI");
return false;
}
#else
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Moonlight not compiled with VAAPI X11 support!");
return false;
#endif
}
else if (info.subsystem == SDL_SYSWM_WAYLAND) {
#ifdef HAVE_LIBVA_WAYLAND
vaDeviceContext->display = vaGetDisplayWl(info.info.wl.display);
if (!vaDeviceContext->display) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Unable to open Wayland display for VAAPI");
return false;
}
#else
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Moonlight not compiled with VAAPI Wayland support!");
return false;
#endif
}
// TODO: Upstream a better solution for SDL_GetWindowWMInfo on KMSDRM
else if (strcmp(SDL_GetCurrentVideoDriver(), "KMSDRM") == 0) {
#ifdef HAVE_LIBVA_DRM
const char* device = SDL_getenv("DRM_DEV");
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;
}
vaDeviceContext->display = vaGetDisplayDRM(m_DrmFd);
if (!vaDeviceContext->display) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Unable to open DRM display for VAAPI");
return false;
}
#else
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Moonlight not compiled with VAAPI DRM support!");
return false;
#endif
}
else {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Unsupported VAAPI rendering subsystem: %d",
info.subsystem);
return false;
}
int major, minor;
VAStatus status;
bool setPathVar = false;
for (;;) {
status = vaInitialize(vaDeviceContext->display, &major, &minor);
if (status != VA_STATUS_SUCCESS && qEnvironmentVariableIsEmpty("LIBVA_DRIVER_NAME")) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Trying fallback VAAPI driver names");
// It would be nice to use vaSetDriverName() here, but there's no way to unset
// it and get back to the default driver selection logic once we've overridden
// the driver name using that API. As a result, we must use LIBVA_DRIVER_NAME.
if (status != VA_STATUS_SUCCESS) {
// The iHD driver supports newer hardware like Ice Lake and Comet Lake.
// It should be picked by default on those platforms, but that doesn't
// always seem to be the case for some reason.
qputenv("LIBVA_DRIVER_NAME", "iHD");
status = vaInitialize(vaDeviceContext->display, &major, &minor);
}
if (status != VA_STATUS_SUCCESS) {
// The Iris driver in Mesa 20.0 returns a bogus VA driver (iris_drv_video.so)
// even though the correct driver is still i965. If we hit this path, we'll
// explicitly try i965 to handle this case.
qputenv("LIBVA_DRIVER_NAME", "i965");
status = vaInitialize(vaDeviceContext->display, &major, &minor);
}
if (status != VA_STATUS_SUCCESS) {
// The RadeonSI driver is compatible with XWayland but can't be detected by libva
// so try it too if all else fails.
qputenv("LIBVA_DRIVER_NAME", "radeonsi");
status = vaInitialize(vaDeviceContext->display, &major, &minor);
}
if (status != VA_STATUS_SUCCESS) {
// Unset LIBVA_DRIVER_NAME if none of the drivers we tried worked. This ensures
// we will get a fresh start using the default driver selection behavior after
// setting LIBVA_DRIVERS_PATH in the code below.
qunsetenv("LIBVA_DRIVER_NAME");
}
}
if (status == VA_STATUS_SUCCESS) {
// Success!
break;
}
if (qEnvironmentVariableIsEmpty("LIBVA_DRIVERS_PATH")) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Trying fallback VAAPI driver paths");
qputenv("LIBVA_DRIVERS_PATH",
#if Q_PROCESSOR_WORDSIZE == 8
"/usr/lib64/dri:" // Fedora x86_64
"/usr/lib64/va/drivers:" // Gentoo x86_64
#endif
"/usr/lib/dri:" // Arch i386 & x86_64, Fedora i386
"/usr/lib/va/drivers:" // Gentoo i386
#if defined(Q_PROCESSOR_X86_64)
"/usr/lib/x86_64-linux-gnu/dri:" // Ubuntu/Debian x86_64
#elif defined(Q_PROCESSOR_X86_32)
"/usr/lib/i386-linux-gnu/dri:" // Ubuntu/Debian i386
#endif
);
setPathVar = true;
}
else {
if (setPathVar) {
// Unset LIBVA_DRIVERS_PATH if we set it ourselves
// and we didn't find any working VAAPI drivers.
qunsetenv("LIBVA_DRIVERS_PATH");
}
// Give up
break;
}
}
status = vaInitialize(vaDeviceContext->display, &major, &minor);
if (status != VA_STATUS_SUCCESS) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to initialize VAAPI: %d",
@@ -254,7 +151,6 @@ VAAPIRenderer::initialize(PDECODER_PARAMETERS params)
major, minor);
const char* vendorString = vaQueryVendorString(vaDeviceContext->display);
QString vendorStr(vendorString);
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Driver: %s",
vendorString ? vendorString : "<unknown>");
@@ -263,8 +159,9 @@ VAAPIRenderer::initialize(PDECODER_PARAMETERS params)
// to be leaked for each submitted frame. The Flatpak runtime has a VDPAU
// driver in place that works well, so use that instead on AMD systems. If
// we're using Wayland, we have no choice but to use VAAPI because VDPAU
// is only supported under X11 or XWayland.
if (qgetenv("FORCE_VAAPI") != "1" && !WMUtils::isRunningWayland()) {
// is only supported under X11.
if (vendorString && qgetenv("FORCE_VAAPI") != "1" && m_WindowSystem == SDL_SYSWM_X11) {
QString vendorStr(vendorString);
if (vendorStr.contains("AMD", Qt::CaseInsensitive) ||
vendorStr.contains("Radeon", Qt::CaseInsensitive)) {
// Fail and let VDPAU pick this up
@@ -274,12 +171,6 @@ VAAPIRenderer::initialize(PDECODER_PARAMETERS params)
}
}
if (WMUtils::isRunningWayland()) {
// The iHD VAAPI driver can initialize on XWayland but it crashes in
// vaPutSurface() so we must also not directly render on XWayland.
m_BlacklistedForDirectRendering = vendorStr.contains("iHD");
}
// This will populate the driver_quirks
err = av_hwdevice_ctx_init(m_HwContext);
if (err < 0) {
@@ -301,7 +192,7 @@ VAAPIRenderer::initialize(PDECODER_PARAMETERS params)
}
bool
VAAPIRenderer::prepareDecoderContext(AVCodecContext* context, AVDictionary**)
VAAPIRenderer::prepareDecoderContext(AVCodecContext* context)
{
context->hw_device_ctx = av_buffer_ref(m_HwContext);
@@ -323,43 +214,9 @@ VAAPIRenderer::needsTestFrame()
bool
VAAPIRenderer::isDirectRenderingSupported()
{
if (qgetenv("VAAPI_FORCE_DIRECT") == "1") {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Using direct rendering due to environment variable");
return true;
}
else if (qgetenv("VAAPI_FORCE_INDIRECT") == "1") {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Using indirect rendering due to environment variable");
return false;
}
// We only support direct rendering on X11 with VAEntrypointVideoProc support
if (m_WindowSystem != SDL_SYSWM_X11 || m_BlacklistedForDirectRendering) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Using indirect rendering due to WM or blacklist");
return false;
}
AVHWDeviceContext* deviceContext = (AVHWDeviceContext*)m_HwContext->data;
AVVAAPIDeviceContext* vaDeviceContext = (AVVAAPIDeviceContext*)deviceContext->hwctx;
VAEntrypoint entrypoints[vaMaxNumEntrypoints(vaDeviceContext->display)];
int entrypointCount;
VAStatus status = vaQueryConfigEntrypoints(vaDeviceContext->display, VAProfileNone, entrypoints, &entrypointCount);
if (status == VA_STATUS_SUCCESS) {
for (int i = 0; i < entrypointCount; i++) {
// Without VAEntrypointVideoProc support, the driver will crash inside vaPutSurface()
if (entrypoints[i] == VAEntrypointVideoProc) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Using direct rendering with VAEntrypointVideoProc");
return true;
}
}
}
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Using indirect rendering due to lack of VAEntrypointVideoProc");
return false;
// Many Wayland renderers don't support YUV surfaces, so use
// another frontend renderer to draw our frames.
return m_WindowSystem == SDL_SYSWM_X11;
}
int VAAPIRenderer::getDecoderColorspace()
@@ -372,11 +229,6 @@ int VAAPIRenderer::getDecoderColorspace()
void
VAAPIRenderer::renderFrame(AVFrame* frame)
{
if (frame == nullptr) {
// End of stream - nothing to do for us
return;
}
VASurfaceID surface = (VASurfaceID)(uintptr_t)frame->data[3];
AVHWDeviceContext* deviceContext = (AVHWDeviceContext*)m_HwContext->data;
AVVAAPIDeviceContext* vaDeviceContext = (AVVAAPIDeviceContext*)deviceContext->hwctx;
@@ -435,173 +287,3 @@ VAAPIRenderer::renderFrame(AVFrame* frame)
SDL_assert(false);
}
}
#ifdef HAVE_EGL
// Ensure that vaExportSurfaceHandle() is supported by the VA-API driver
bool
VAAPIRenderer::canExportEGL() {
AVHWDeviceContext* deviceContext = (AVHWDeviceContext*)m_HwContext->data;
AVVAAPIDeviceContext* vaDeviceContext = (AVVAAPIDeviceContext*)deviceContext->hwctx;
VASurfaceID surfaceId;
VAStatus st;
VADRMPRIMESurfaceDescriptor descriptor;
VASurfaceAttrib attrs[2];
int attributeCount = 0;
if (qgetenv("VAAPI_FORCE_DIRECT") == "1") {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Using direct rendering due to environment variable");
return false;
}
// FFmpeg handles setting these quirk flags for us
if (!(vaDeviceContext->driver_quirks & AV_VAAPI_DRIVER_QUIRK_ATTRIB_MEMTYPE)) {
attrs[attributeCount].type = VASurfaceAttribMemoryType;
attrs[attributeCount].flags = VA_SURFACE_ATTRIB_SETTABLE;
attrs[attributeCount].value.type = VAGenericValueTypeInteger;
attrs[attributeCount].value.value.i = VA_SURFACE_ATTRIB_MEM_TYPE_VA;
attributeCount++;
}
// These attributes are required for i965 to create a surface that can
// be successfully exported via vaExportSurfaceHandle(). iHD doesn't
// need these, but it doesn't seem to hurt either.
attrs[attributeCount].type = VASurfaceAttribPixelFormat;
attrs[attributeCount].flags = VA_SURFACE_ATTRIB_SETTABLE;
attrs[attributeCount].value.type = VAGenericValueTypeInteger;
attrs[attributeCount].value.value.i = VA_FOURCC_NV12;
attributeCount++;
st = vaCreateSurfaces(vaDeviceContext->display,
VA_RT_FORMAT_YUV420,
1280,
720,
&surfaceId,
1,
attrs,
attributeCount);
if (st != VA_STATUS_SUCCESS) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"vaCreateSurfaces() failed: %d", st);
return false;
}
st = vaExportSurfaceHandle(vaDeviceContext->display,
surfaceId,
VA_SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME_2,
VA_EXPORT_SURFACE_READ_ONLY | VA_EXPORT_SURFACE_SEPARATE_LAYERS,
&descriptor);
vaDestroySurfaces(vaDeviceContext->display, &surfaceId, 1);
if (st != VA_STATUS_SUCCESS) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"vaExportSurfaceHandle() failed: %d", st);
return false;
}
for (size_t i = 0; i < descriptor.num_objects; ++i) {
close(descriptor.objects[i].fd);
}
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"VAAPI driver supports exporting DRM PRIME surface handles");
return true;
}
bool
VAAPIRenderer::initializeEGL(EGLDisplay,
const EGLExtensions &ext) {
if (!ext.isSupported("EGL_EXT_image_dma_buf_import")) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"VAAPI-EGL: DMABUF unsupported");
return false;
}
m_EGLExtDmaBuf = ext.isSupported("EGL_EXT_image_dma_buf_import_modifiers");
return true;
}
ssize_t
VAAPIRenderer::exportEGLImages(AVFrame *frame, EGLDisplay dpy,
EGLImage images[EGL_MAX_PLANES]) {
ssize_t count = 0;
auto hwFrameCtx = (AVHWFramesContext*)frame->hw_frames_ctx->data;
AVVAAPIDeviceContext* vaDeviceContext = (AVVAAPIDeviceContext*)hwFrameCtx->device_ctx->hwctx;
VASurfaceID surface_id = (VASurfaceID)(uintptr_t)frame->data[3];
VAStatus st = vaExportSurfaceHandle(vaDeviceContext->display,
surface_id,
VA_SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME_2,
VA_EXPORT_SURFACE_READ_ONLY | VA_EXPORT_SURFACE_SEPARATE_LAYERS,
&m_PrimeDescriptor);
if (st != VA_STATUS_SUCCESS) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"vaExportSurfaceHandle failed: %d", st);
return -1;
}
SDL_assert(m_PrimeDescriptor.num_layers <= EGL_MAX_PLANES);
st = vaSyncSurface(vaDeviceContext->display, surface_id);
if (st != VA_STATUS_SUCCESS) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"vaSyncSurface failed: %d", st);
goto sync_fail;
}
for (size_t i = 0; i < m_PrimeDescriptor.num_layers; ++i) {
const auto &layer = m_PrimeDescriptor.layers[i];
const auto &object = m_PrimeDescriptor.objects[layer.object_index[0]];
EGLAttrib attribs[17] = {
EGL_LINUX_DRM_FOURCC_EXT, (EGLint)layer.drm_format,
EGL_WIDTH, i == 0 ? frame->width : frame->width / 2,
EGL_HEIGHT, i == 0 ? frame->height : frame->height / 2,
EGL_DMA_BUF_PLANE0_FD_EXT, object.fd,
EGL_DMA_BUF_PLANE0_OFFSET_EXT, (EGLint)layer.offset[0],
EGL_DMA_BUF_PLANE0_PITCH_EXT, (EGLint)layer.pitch[0],
EGL_NONE,
};
if (m_EGLExtDmaBuf) {
const EGLAttrib extra[] = {
EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT,
(EGLint)object.drm_format_modifier,
EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT,
(EGLint)(object.drm_format_modifier >> 32),
EGL_NONE,
};
memcpy((void *)(&attribs[12]), (void *)extra, sizeof (extra));
}
images[i] = eglCreateImage(dpy, EGL_NO_CONTEXT,
EGL_LINUX_DMA_BUF_EXT,
nullptr, attribs);
if (!images[i]) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"eglCreateImage() Failed: %d", eglGetError());
goto create_image_fail;
}
++count;
}
return count;
create_image_fail:
m_PrimeDescriptor.num_layers = count;
sync_fail:
freeEGLImages(dpy, images);
return -1;
}
void
VAAPIRenderer::freeEGLImages(EGLDisplay dpy, EGLImage images[EGL_MAX_PLANES]) {
for (size_t i = 0; i < m_PrimeDescriptor.num_layers; ++i) {
eglDestroyImage(dpy, images[i]);
}
for (size_t i = 0; i < m_PrimeDescriptor.num_objects; ++i) {
close(m_PrimeDescriptor.objects[i].fd);
}
m_PrimeDescriptor.num_layers = 0;
m_PrimeDescriptor.num_objects = 0;
}
#endif
+1 -18
View File
@@ -26,9 +26,6 @@ extern "C" {
#include <va/va_drm.h>
#endif
#include <libavutil/hwcontext_vaapi.h>
#ifdef HAVE_EGL
#include <va/va_drmcommon.h>
#endif
}
class VAAPIRenderer : public IFFmpegRenderer
@@ -37,25 +34,16 @@ public:
VAAPIRenderer();
virtual ~VAAPIRenderer() override;
virtual bool initialize(PDECODER_PARAMETERS params) override;
virtual bool prepareDecoderContext(AVCodecContext* context, AVDictionary** options) override;
virtual bool prepareDecoderContext(AVCodecContext* context) override;
virtual void renderFrame(AVFrame* frame) override;
virtual bool needsTestFrame() override;
virtual bool isDirectRenderingSupported() override;
virtual int getDecoderColorspace() override;
#ifdef HAVE_EGL
virtual bool canExportEGL() override;
virtual bool initializeEGL(EGLDisplay dpy, const EGLExtensions &ext) override;
virtual ssize_t exportEGLImages(AVFrame *frame, EGLDisplay dpy, EGLImage images[EGL_MAX_PLANES]) override;
virtual void freeEGLImages(EGLDisplay dpy, EGLImage[EGL_MAX_PLANES]) override;
#endif
private:
VADisplay openDisplay(SDL_Window* window);
int m_WindowSystem;
AVBufferRef* m_HwContext;
int m_DrmFd;
bool m_BlacklistedForDirectRendering;
#ifdef HAVE_LIBVA_X11
Window m_XWindow;
@@ -65,9 +53,4 @@ private:
int m_VideoHeight;
int m_DisplayWidth;
int m_DisplayHeight;
#ifdef HAVE_EGL
VADRMPRIMESurfaceDescriptor m_PrimeDescriptor;
bool m_EGLExtDmaBuf;
#endif
};
+1 -47
View File
@@ -64,21 +64,6 @@ bool VDPAURenderer::initialize(PDECODER_PARAMETERS params)
int err;
VdpStatus status;
SDL_SysWMinfo info;
static const char* driverPathsToTry[] = {
#if Q_PROCESSOR_WORDSIZE == 8
"/usr/lib64",
"/usr/lib64/vdpau", // Fedora x86_64
#endif
"/usr/lib",
"/usr/lib/vdpau", // Fedora i386
#if defined(Q_PROCESSOR_X86_64)
"/usr/lib/x86_64-linux-gnu",
"/usr/lib/x86_64-linux-gnu/vdpau", // Ubuntu/Debian x86_64
#elif defined(Q_PROCESSOR_X86_32)
"/usr/lib/i386-linux-gnu",
"/usr/lib/i386-linux-gnu/vdpau", // Ubuntu/Debian i386
#endif
};
m_VideoWidth = params->width;
m_VideoHeight = params->height;
@@ -86,32 +71,6 @@ bool VDPAURenderer::initialize(PDECODER_PARAMETERS params)
err = av_hwdevice_ctx_create(&m_HwContext,
AV_HWDEVICE_TYPE_VDPAU,
nullptr, nullptr, 0);
if (err < 0 && qEnvironmentVariableIsEmpty("VDPAU_DRIVER_PATH")) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Trying fallback VDPAU driver paths");
// Unlike libva, libvdpau doesn't support multiple paths in
// their VDPAU_DRIVER_PATH variable, so we must try them all
// one at a time.
for (auto& driverPath : driverPathsToTry) {
qputenv("VDPAU_DRIVER_PATH", driverPath);
err = av_hwdevice_ctx_create(&m_HwContext,
AV_HWDEVICE_TYPE_VDPAU,
nullptr, nullptr, 0);
if (err == 0) {
// Success!
break;
}
}
if (err < 0) {
// Unset VDPAU_DRIVER_PATH if we set it ourselves
// and we didn't find any working VDPAU drivers.
qunsetenv("VDPAU_DRIVER_PATH");
}
}
if (err < 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to create VDPAU context: %d",
@@ -266,7 +225,7 @@ bool VDPAURenderer::initialize(PDECODER_PARAMETERS params)
return true;
}
bool VDPAURenderer::prepareDecoderContext(AVCodecContext* context, AVDictionary**)
bool VDPAURenderer::prepareDecoderContext(AVCodecContext* context)
{
context->hw_device_ctx = av_buffer_ref(m_HwContext);
@@ -302,11 +261,6 @@ int VDPAURenderer::getDecoderColorspace()
void VDPAURenderer::renderFrame(AVFrame* frame)
{
if (frame == nullptr) {
// End of stream - nothing to do for us
return;
}
VdpStatus status;
VdpVideoSurface videoSurface = (VdpVideoSurface)(uintptr_t)frame->data[3];
+1 -1
View File
@@ -14,7 +14,7 @@ public:
VDPAURenderer();
virtual ~VDPAURenderer() override;
virtual bool initialize(PDECODER_PARAMETERS params) override;
virtual bool prepareDecoderContext(AVCodecContext* context, AVDictionary** options) override;
virtual bool prepareDecoderContext(AVCodecContext* context) override;
virtual void renderFrame(AVFrame* frame) override;
virtual bool needsTestFrame() override;
virtual int getDecoderColorspace() override;
+5 -120
View File
@@ -16,19 +16,6 @@
#import <dispatch/dispatch.h>
#import <Metal/Metal.h>
@interface VTView : NSView
- (NSView *)hitTest:(NSPoint)point;
@end
@implementation VTView
- (NSView *)hitTest:(NSPoint)point {
Q_UNUSED(point);
return nil;
}
@end
class VTRenderer : public IFFmpegRenderer
{
public:
@@ -38,7 +25,6 @@ public:
m_FormatDesc(nullptr),
m_StreamView(nullptr),
m_DisplayLink(nullptr),
m_ColorSpace(nullptr),
m_VsyncMutex(nullptr),
m_VsyncPassed(nullptr)
{
@@ -68,10 +54,6 @@ public:
CFRelease(m_FormatDesc);
}
if (m_ColorSpace != nullptr) {
CGColorSpaceRelease(m_ColorSpace);
}
for (int i = 0; i < Overlay::OverlayMax; i++) {
if (m_OverlayTextFields[i] != nullptr) {
[m_OverlayTextFields[i] removeFromSuperview];
@@ -158,11 +140,6 @@ public:
// Caller frees frame after we return
virtual void renderFrame(AVFrame* frame) override
{
if (frame == nullptr) {
// End of stream - nothing to do for us
return;
}
OSStatus status;
CVPixelBufferRef pixBuf = reinterpret_cast<CVPixelBufferRef>(frame->data[3]);
@@ -177,91 +154,6 @@ public:
return;
}
// Strip the attachments added by VT. They are likely wrong.
CVBufferRemoveAllAttachments(pixBuf);
CVBufferSetAttachment(pixBuf,
kCVImageBufferChromaSubsamplingKey,
kCVImageBufferChromaSubsampling_420,
kCVAttachmentMode_ShouldPropagate);
switch (frame->color_primaries) {
case AVCOL_PRI_BT709:
CVBufferSetAttachment(pixBuf,
kCVImageBufferColorPrimariesKey,
kCVImageBufferColorPrimaries_ITU_R_709_2,
kCVAttachmentMode_ShouldPropagate);
break;
case AVCOL_PRI_SMPTE170M:
CVBufferSetAttachment(pixBuf,
kCVImageBufferColorPrimariesKey,
kCVImageBufferColorPrimaries_SMPTE_C,
kCVAttachmentMode_ShouldPropagate);
break;
case AVCOL_PRI_BT2020:
CVBufferSetAttachment(pixBuf,
kCVImageBufferColorPrimariesKey,
kCVImageBufferColorPrimaries_ITU_R_2020,
kCVAttachmentMode_ShouldPropagate);
break;
default:
break;
}
switch (frame->color_trc) {
case AVCOL_TRC_BT709:
case AVCOL_TRC_SMPTE170M:
CVBufferSetAttachment(pixBuf,
kCVImageBufferTransferFunctionKey,
kCVImageBufferTransferFunction_ITU_R_709_2,
kCVAttachmentMode_ShouldPropagate);
break;
case AVCOL_TRC_BT2020_10:
CVBufferSetAttachment(pixBuf,
kCVImageBufferTransferFunctionKey,
kCVImageBufferTransferFunction_ITU_R_2020,
kCVAttachmentMode_ShouldPropagate);
break;
default:
break;
}
switch (frame->colorspace) {
case AVCOL_SPC_BT709:
if (m_ColorSpace == nullptr) {
m_ColorSpace = CGColorSpaceCreateWithName(kCGColorSpaceITUR_709);
}
CVBufferSetAttachment(pixBuf,
kCVImageBufferYCbCrMatrixKey,
kCVImageBufferYCbCrMatrix_ITU_R_709_2,
kCVAttachmentMode_ShouldPropagate);
break;
case AVCOL_SPC_BT2020_NCL:
if (m_ColorSpace == nullptr) {
m_ColorSpace = CGColorSpaceCreateWithName(kCGColorSpaceITUR_2020);
}
CVBufferSetAttachment(pixBuf,
kCVImageBufferYCbCrMatrixKey,
kCVImageBufferYCbCrMatrix_ITU_R_2020,
kCVAttachmentMode_ShouldPropagate);
break;
case AVCOL_SPC_SMPTE170M:
if (m_ColorSpace == nullptr) {
m_ColorSpace = CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
}
CVBufferSetAttachment(pixBuf,
kCVImageBufferYCbCrMatrixKey,
kCVImageBufferYCbCrMatrix_ITU_R_601_4,
kCVAttachmentMode_ShouldPropagate);
break;
default:
break;
}
if (m_ColorSpace != nullptr) {
CVBufferSetAttachment(pixBuf, kCVImageBufferCGColorSpaceKey, m_ColorSpace, kCVAttachmentMode_ShouldPropagate);
}
// If the format has changed or doesn't exist yet, construct it with the
// pixel buffer data
if (!m_FormatDesc || !CMVideoFormatDescriptionMatchesImageBuffer(m_FormatDesc, pixBuf)) {
@@ -282,7 +174,7 @@ public:
CMSampleTimingInfo timingInfo = {
.duration = kCMTimeInvalid,
.decodeTimeStamp = kCMTimeInvalid,
.presentationTimeStamp = CMClockMakeHostTimeFromSystemUnits(mach_absolute_time())
.presentationTimeStamp = CMTimeMake(mach_absolute_time(), 1000 * 1000 * 1000)
};
CMSampleBufferRef sampleBuffer;
@@ -416,7 +308,7 @@ public:
// SDL adds its own content view to listen for events.
// We need to add a subview for our display layer.
NSView* contentView = info.info.cocoa.window.contentView;
m_StreamView = [[VTView alloc] initWithFrame:contentView.bounds];
m_StreamView = [[NSView alloc] initWithFrame:contentView.bounds];
m_DisplayLayer = [[AVSampleBufferDisplayLayer alloc] init];
m_DisplayLayer.bounds = m_StreamView.bounds;
@@ -465,10 +357,10 @@ public:
switch (type) {
case Overlay::OverlayDebug:
[m_OverlayTextFields[type] setAlignment:NSTextAlignmentLeft];
[m_OverlayTextFields[type] setAlignment:NSLeftTextAlignment];
break;
case Overlay::OverlayStatusUpdate:
[m_OverlayTextFields[type] setAlignment:NSTextAlignmentRight];
[m_OverlayTextFields[type] setAlignment:NSRightTextAlignment];
break;
default:
break;
@@ -518,7 +410,7 @@ public:
}
}
virtual bool prepareDecoderContext(AVCodecContext* context, AVDictionary**) override
virtual bool prepareDecoderContext(AVCodecContext* context) override
{
context->hw_device_ctx = av_buffer_ref(m_HwContext);
@@ -537,12 +429,6 @@ public:
return true;
}
int getDecoderColorspace() override
{
// macOS seems to handle Rec 601 best
return COLORSPACE_REC_601;
}
private:
AVBufferRef* m_HwContext;
AVSampleBufferDisplayLayer* m_DisplayLayer;
@@ -550,7 +436,6 @@ private:
NSView* m_StreamView;
NSTextField* m_OverlayTextFields[Overlay::OverlayMax];
CVDisplayLinkRef m_DisplayLink;
CGColorSpaceRef m_ColorSpace;
SDL_mutex* m_VsyncMutex;
SDL_cond* m_VsyncPassed;
};
+36 -243
View File
@@ -32,10 +32,6 @@
#include "ffmpeg-renderers/drm.h"
#endif
#ifdef HAVE_EGL
#include "ffmpeg-renderers/eglvid.h"
#endif
// This is gross but it allows us to use sizeof()
#include "ffmpeg_videosamples.cpp"
@@ -49,25 +45,9 @@ bool FFmpegVideoDecoder::isHardwareAccelerated()
(m_VideoDecoderCtx->codec->capabilities & AV_CODEC_CAP_HARDWARE) != 0;
}
bool FFmpegVideoDecoder::isAlwaysFullScreen()
{
return m_BackendRenderer->getRendererAttributes() & RENDERER_ATTRIBUTE_FULLSCREEN_ONLY;
}
int FFmpegVideoDecoder::getDecoderCapabilities()
{
int capabilities = m_BackendRenderer->getDecoderCapabilities();
if (!isHardwareAccelerated()) {
// Slice up to 4 times for parallel CPU decoding, once slice per core
int slices = qMin(MAX_SLICES, SDL_GetCPUCount());
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Encoder configured for %d slices per frame",
slices);
capabilities |= CAPABILITY_SLICES_PER_FRAME(slices);
}
return capabilities;
return m_BackendRenderer->getDecoderCapabilities();
}
int FFmpegVideoDecoder::getDecoderColorspace()
@@ -75,17 +55,6 @@ int FFmpegVideoDecoder::getDecoderColorspace()
return m_FrontendRenderer->getDecoderColorspace();
}
QSize FFmpegVideoDecoder::getDecoderMaxResolution()
{
if (m_BackendRenderer->getRendererAttributes() & RENDERER_ATTRIBUTE_1080P_MAX) {
return QSize(1920, 1080);
}
else {
// No known maximum
return QSize(0, 0);
}
}
enum AVPixelFormat FFmpegVideoDecoder::ffGetFormat(AVCodecContext* context,
const enum AVPixelFormat* pixFmts)
{
@@ -93,7 +62,7 @@ enum AVPixelFormat FFmpegVideoDecoder::ffGetFormat(AVCodecContext* context,
const enum AVPixelFormat *p;
for (p = pixFmts; *p != -1; p++) {
// Only match our hardware decoding codec or preferred SW pixel
// Only match our hardware decoding codec or SW pixel
// format (if not using hardware decoding). It's crucial
// to override the default get_format() which will try
// to gracefully fall back to software decode and break us.
@@ -104,15 +73,6 @@ enum AVPixelFormat FFmpegVideoDecoder::ffGetFormat(AVCodecContext* context,
}
}
// Failed to match the preferred pixel formats. Try non-preferred options for non-hwaccel decoders.
if (decoder->m_HwDecodeCfg == nullptr) {
for (p = pixFmts; *p != -1; p++) {
if (decoder->m_FrontendRenderer->isPixelFormatSupported(decoder->m_VideoFormat, *p)) {
return *p;
}
}
}
return AV_PIX_FMT_NONE;
}
@@ -124,11 +84,8 @@ FFmpegVideoDecoder::FFmpegVideoDecoder(bool testOnly)
m_FrontendRenderer(nullptr),
m_ConsecutiveFailedDecodes(0),
m_Pacer(nullptr),
m_FramesIn(0),
m_FramesOut(0),
m_LastFrameNumber(0),
m_StreamFps(0),
m_VideoFormat(0),
m_NeedsSpsFixup(false),
m_TestOnly(testOnly)
{
@@ -194,16 +151,6 @@ void FFmpegVideoDecoder::reset()
bool FFmpegVideoDecoder::createFrontendRenderer(PDECODER_PARAMETERS params)
{
#ifdef HAVE_EGL
if (m_BackendRenderer->canExportEGL()) {
m_FrontendRenderer = new EGLRenderer(m_BackendRenderer);
if (m_FrontendRenderer->initialize(params)) {
return true;
}
delete m_FrontendRenderer;
}
#endif
if (m_BackendRenderer->isDirectRenderingSupported()) {
// The backend renderer can render to the display
m_FrontendRenderer = m_BackendRenderer;
@@ -217,6 +164,23 @@ bool FFmpegVideoDecoder::createFrontendRenderer(PDECODER_PARAMETERS params)
}
}
// Determine whether the frontend renderer prefers frame pacing
auto vsyncConstraint = m_FrontendRenderer->getFramePacingConstraint();
if (vsyncConstraint == IFFmpegRenderer::PACING_FORCE_OFF && params->enableFramePacing) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Frame pacing is forcefully disabled by the frontend renderer");
params->enableFramePacing = false;
}
else if (vsyncConstraint == IFFmpegRenderer::PACING_FORCE_ON && !params->enableFramePacing) {
// FIXME: This duplicates logic in Session.cpp
int displayHz = StreamUtils::getDisplayRefreshRate(params->window);
if (displayHz + 5 >= params->frameRate) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Frame pacing is forcefully enabled by the frontend renderer");
params->enableFramePacing = true;
}
}
return true;
}
@@ -231,7 +195,6 @@ bool FFmpegVideoDecoder::completeInitialization(AVCodec* decoder, PDECODER_PARAM
}
m_StreamFps = params->frameRate;
m_VideoFormat = params->videoFormat;
// Don't bother initializing Pacer if we're not actually going to render
if (!testFrame) {
@@ -280,10 +243,8 @@ bool FFmpegVideoDecoder::completeInitialization(AVCodec* decoder, PDECODER_PARAM
m_VideoDecoderCtx->pix_fmt = m_FrontendRenderer->getPreferredPixelFormat(params->videoFormat);
m_VideoDecoderCtx->get_format = ffGetFormat;
AVDictionary* options = nullptr;
// Allow the backend renderer to attach data to this decoder
if (!m_BackendRenderer->prepareDecoderContext(m_VideoDecoderCtx, &options)) {
if (!m_BackendRenderer->prepareDecoderContext(m_VideoDecoderCtx)) {
return false;
}
@@ -294,8 +255,7 @@ bool FFmpegVideoDecoder::completeInitialization(AVCodec* decoder, PDECODER_PARAM
SDL_assert(m_VideoDecoderCtx->opaque == nullptr);
m_VideoDecoderCtx->opaque = this;
int err = avcodec_open2(m_VideoDecoderCtx, decoder, &options);
av_dict_free(&options);
int err = avcodec_open2(m_VideoDecoderCtx, decoder, nullptr);
if (err < 0) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Unable to open decoder for format: %x",
@@ -329,42 +289,7 @@ bool FFmpegVideoDecoder::completeInitialization(AVCodec* decoder, PDECODER_PARAM
return false;
}
AVFrame* frame = av_frame_alloc();
if (!frame) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Failed to allocate frame");
return false;
}
// Some decoders won't output on the first frame, so we'll submit
// a few test frames if we get an EAGAIN error.
for (int retries = 0; retries < 5; retries++) {
// Most FFmpeg decoders process input using a "push" model.
// We'll see those fail here if the format is not supported.
err = avcodec_send_packet(m_VideoDecoderCtx, &m_Pkt);
if (err < 0) {
av_frame_free(&frame);
char errorstring[512];
av_strerror(err, errorstring, sizeof(errorstring));
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Test decode failed: %s", errorstring);
return false;
}
// A few FFmpeg decoders (h264_mmal) process here using a "pull" model.
// Those decoders will fail here if the format is not supported.
err = avcodec_receive_frame(m_VideoDecoderCtx, frame);
if (err == AVERROR(EAGAIN)) {
// Wait a little while to let the hardware work
SDL_Delay(100);
}
else {
// Done!
break;
}
}
av_frame_free(&frame);
err = avcodec_send_packet(m_VideoDecoderCtx, &m_Pkt);
if (err < 0) {
char errorstring[512];
av_strerror(err, errorstring, sizeof(errorstring));
@@ -423,45 +348,17 @@ void FFmpegVideoDecoder::addVideoStats(VIDEO_STATS& src, VIDEO_STATS& dst)
void FFmpegVideoDecoder::stringifyVideoStats(VIDEO_STATS& stats, char* output)
{
int offset = 0;
const char* codecString;
// Start with an empty string
output[offset] = 0;
switch (m_VideoFormat)
{
case VIDEO_FORMAT_H264:
codecString = "H.264";
break;
case VIDEO_FORMAT_H265:
codecString = "HEVC";
break;
case VIDEO_FORMAT_H265_MAIN10:
codecString = "HEVC Main 10";
break;
default:
SDL_assert(false);
codecString = "UNKNOWN";
break;
}
if (stats.receivedFps > 0) {
if (m_VideoDecoderCtx != nullptr) {
offset += sprintf(&output[offset],
"Video stream: %dx%d %.2f FPS (Codec: %s)\n",
m_VideoDecoderCtx->width,
m_VideoDecoderCtx->height,
stats.totalFps,
codecString);
}
offset += sprintf(&output[offset],
"Estimated host PC frame rate: %.2f FPS\n"
"Incoming frame rate from network: %.2f FPS\n"
"Decoding frame rate: %.2f FPS\n"
"Rendering frame rate: %.2f FPS\n",
stats.totalFps,
stats.receivedFps,
stats.decodedFps,
stats.renderedFps);
@@ -591,61 +488,11 @@ bool FFmpegVideoDecoder::tryInitializeRenderer(AVCodec* decoder,
bool FFmpegVideoDecoder::initialize(PDECODER_PARAMETERS params)
{
AVCodec* decoder;
// Increase log level until the first frame is decoded
av_log_set_level(AV_LOG_DEBUG);
// First try decoders that the user has manually specified via environment variables.
// These must output surfaces in one of the formats that the SDL renderer supports,
// which is currently:
// - AV_PIX_FMT_YUV420P (preferred)
// - AV_PIX_FMT_NV12
// - AV_PIX_FMT_NV21
// These formats should cover most/all decoders that output in a standard YUV format.
{
QString h264DecoderHint = qgetenv("H264_DECODER_HINT");
if (!h264DecoderHint.isEmpty() && (params->videoFormat & VIDEO_FORMAT_MASK_H264)) {
QByteArray decoderString = h264DecoderHint.toLocal8Bit();
AVCodec* customAvcDecoder = avcodec_find_decoder_by_name(decoderString.constData());
if (customAvcDecoder != nullptr &&
tryInitializeRenderer(customAvcDecoder, params, nullptr,
[]() -> IFFmpegRenderer* { return new SdlRenderer(); })) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Using custom H.264 decoder (H264_DECODER_HINT): %s",
decoderString.constData());
return true;
}
else {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Custom H.264 decoder (H264_DECODER_HINT) failed to load: %s",
decoderString.constData());
}
}
}
{
QString hevcDecoderHint = qgetenv("HEVC_DECODER_HINT");
if (!hevcDecoderHint.isEmpty() && (params->videoFormat & VIDEO_FORMAT_MASK_H265)) {
QByteArray decoderString = hevcDecoderHint.toLocal8Bit();
AVCodec* customHevcDecoder = avcodec_find_decoder_by_name(decoderString.constData());
if (customHevcDecoder != nullptr &&
tryInitializeRenderer(customHevcDecoder, params, nullptr,
[]() -> IFFmpegRenderer* { return new SdlRenderer(); })) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Using custom HEVC decoder (HEVC_DECODER_HINT): %s",
decoderString.constData());
return true;
}
else {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Custom HEVC decoder (HEVC_DECODER_HINT) failed to load: %s",
decoderString.constData());
}
}
}
AVCodec* decoder;
if (params->videoFormat & VIDEO_FORMAT_MASK_H264) {
decoder = avcodec_find_decoder(AV_CODEC_ID_H264);
}
@@ -696,58 +543,20 @@ bool FFmpegVideoDecoder::initialize(PDECODER_PARAMETERS params)
#endif
#ifdef HAVE_DRM
{
// RKMPP is a hardware accelerated decoder that outputs DRI PRIME buffers
AVCodec* rkmppDecoder;
// RKMPP is a hardware accelerated decoder that outputs DRI PRIME buffers
AVCodec* rkmppDecoder;
if (params->videoFormat & VIDEO_FORMAT_MASK_H264) {
rkmppDecoder = avcodec_find_decoder_by_name("h264_rkmpp");
}
else {
rkmppDecoder = avcodec_find_decoder_by_name("hevc_rkmpp");
}
if (rkmppDecoder != nullptr &&
tryInitializeRenderer(rkmppDecoder, params, nullptr,
[]() -> IFFmpegRenderer* { return new DrmRenderer(); })) {
return true;
}
if (params->videoFormat & VIDEO_FORMAT_MASK_H264) {
rkmppDecoder = avcodec_find_decoder_by_name("h264_rkmpp");
}
#endif
#ifdef Q_OS_LINUX
{
AVCodec* nvmpiDecoder;
if (params->videoFormat & VIDEO_FORMAT_MASK_H264) {
nvmpiDecoder = avcodec_find_decoder_by_name("h264_nvmpi");
}
else {
nvmpiDecoder = avcodec_find_decoder_by_name("hevc_nvmpi");
}
if (nvmpiDecoder != nullptr &&
tryInitializeRenderer(nvmpiDecoder, params, nullptr,
[]() -> IFFmpegRenderer* { return new SdlRenderer(); })) {
return true;
}
else {
rkmppDecoder = avcodec_find_decoder_by_name("hevc_rkmpp");
}
{
AVCodec* v4l2Decoder;
if (params->videoFormat & VIDEO_FORMAT_MASK_H264) {
v4l2Decoder = avcodec_find_decoder_by_name("h264_v4l2m2m");
}
else {
v4l2Decoder = avcodec_find_decoder_by_name("hevc_v4l2m2m");
}
if (v4l2Decoder != nullptr &&
tryInitializeRenderer(v4l2Decoder, params, nullptr,
[]() -> IFFmpegRenderer* { return new SdlRenderer(); })) {
return true;
}
if (rkmppDecoder != nullptr &&
tryInitializeRenderer(rkmppDecoder, params, nullptr,
[]() -> IFFmpegRenderer* { return new DrmRenderer(); })) {
return true;
}
#endif
@@ -884,13 +693,6 @@ int FFmpegVideoDecoder::submitDecodeUnit(PDECODE_UNIT du)
m_Pkt.data = reinterpret_cast<uint8_t*>(m_DecodeBuffer.data());
m_Pkt.size = offset;
if (du->frameType == FRAME_TYPE_IDR) {
m_Pkt.flags = AV_PKT_FLAG_KEY;
}
else {
m_Pkt.flags = 0;
}
m_ActiveWndVideoStats.totalReassemblyTime += LiGetMillis() - du->receiveTimeMs;
Uint32 beforeDecode = SDL_GetTicks();
@@ -917,8 +719,6 @@ int FFmpegVideoDecoder::submitDecodeUnit(PDECODE_UNIT du)
return DR_NEED_IDR;
}
m_FramesIn++;
AVFrame* frame = av_frame_alloc();
if (!frame) {
// Failed to allocate a frame but we did submit,
@@ -930,8 +730,6 @@ int FFmpegVideoDecoder::submitDecodeUnit(PDECODE_UNIT du)
err = avcodec_receive_frame(m_VideoDecoderCtx, frame);
if (err == 0) {
m_FramesOut++;
// Reset failed decodes count if we reached this far
m_ConsecutiveFailedDecodes = 0;
@@ -947,11 +745,6 @@ int FFmpegVideoDecoder::submitDecodeUnit(PDECODE_UNIT du)
// Count time in avcodec_send_packet() and avcodec_receive_frame()
// as time spent decoding
m_ActiveWndVideoStats.totalDecodeTime += SDL_GetTicks() - beforeDecode;
// Also count the frame-to-frame delay if the decoder is delaying frames
// until a subsequent frame is submitted.
m_ActiveWndVideoStats.totalDecodeTime += (m_FramesIn - m_FramesOut) * (1000 / m_StreamFps);
m_ActiveWndVideoStats.decodedFrames++;
// Queue the frame for rendering (or render now if pacer is disabled)
-6
View File
@@ -16,10 +16,8 @@ public:
virtual ~FFmpegVideoDecoder() override;
virtual bool initialize(PDECODER_PARAMETERS params) override;
virtual bool isHardwareAccelerated() override;
virtual bool isAlwaysFullScreen() override;
virtual int getDecoderCapabilities() override;
virtual int getDecoderColorspace() override;
virtual QSize getDecoderMaxResolution() override;
virtual int submitDecodeUnit(PDECODE_UNIT du) override;
virtual void renderFrameOnMainThread() override;
@@ -63,12 +61,8 @@ private:
VIDEO_STATS m_LastWndVideoStats;
VIDEO_STATS m_GlobalVideoStats;
int m_FramesIn;
int m_FramesOut;
int m_LastFrameNumber;
int m_StreamFps;
int m_VideoFormat;
bool m_NeedsSpsFixup;
bool m_TestOnly;
-10
View File
@@ -25,11 +25,6 @@ SLVideoDecoder::isHardwareAccelerated()
return true;
}
bool SLVideoDecoder::isAlwaysFullScreen()
{
return true;
}
int
SLVideoDecoder::getDecoderCapabilities()
{
@@ -42,11 +37,6 @@ SLVideoDecoder::getDecoderColorspace()
return COLORSPACE_REC_709;
}
QSize SLVideoDecoder::getDecoderMaxResolution()
{
return QSize(1920, 1080);
}
bool
SLVideoDecoder::initialize(PDECODER_PARAMETERS params)
{
-2
View File
@@ -11,10 +11,8 @@ public:
virtual ~SLVideoDecoder();
virtual bool initialize(PDECODER_PARAMETERS params);
virtual bool isHardwareAccelerated();
virtual bool isAlwaysFullScreen();
virtual int getDecoderCapabilities();
virtual int getDecoderColorspace();
virtual QSize getDecoderMaxResolution();
virtual int submitDecodeUnit(PDECODE_UNIT du);
// Unused since rendering is done directly from the decode thread
-5
View File
@@ -3,8 +3,3 @@
#define THROW_BAD_ALLOC_IF_NULL(x) \
if ((x) == nullptr) throw std::bad_alloc()
namespace WMUtils {
bool isRunningX11();
bool isRunningWayland();
bool isRunningWindowManager();
}
+1 -1
View File
@@ -1 +1 @@
2.2.0
1.2.1
-48
View File
@@ -1,48 +0,0 @@
#include <QtGlobal>
#include "utils.h"
#ifdef HAS_X11
#include <X11/Xlib.h>
#endif
#ifdef HAS_WAYLAND
#include <wayland-client.h>
#endif
bool WMUtils::isRunningX11()
{
#ifdef HAS_X11
Display* display = XOpenDisplay(nullptr);
if (display != nullptr) {
XCloseDisplay(display);
return true;
}
#endif
return false;
}
bool WMUtils::isRunningWayland()
{
#ifdef HAS_WAYLAND
struct wl_display* display = wl_display_connect(nullptr);
if (display != nullptr) {
wl_display_disconnect(display);
return true;
}
#endif
return false;
}
bool WMUtils::isRunningWindowManager()
{
#if defined(Q_OS_WIN) || defined(Q_OS_DARWIN)
// Windows and macOS are always running a window manager
return true;
#else
// On Unix OSes, look for Wayland or X
return WMUtils::isRunningWayland() || WMUtils::isRunningX11();
#endif
}
-59
View File
@@ -1,59 +0,0 @@
version: 0.0.0.{build}
clone_depth: 1
environment:
matrix:
- APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2019
QTDIR: C:\Qt\5.15\msvc2019_64
ARCH: x64
- APPVEYOR_BUILD_WORKER_IMAGE: macOS
BUILD_TARGET: macos
- APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2019
QTDIR: C:\Qt\5.15\msvc2019
ARCH: x86
- APPVEYOR_BUILD_WORKER_IMAGE: Ubuntu1604
BUILD_TARGET: steamlink
- APPVEYOR_BUILD_WORKER_IMAGE: Ubuntu1604
BUILD_TARGET: linux
FFMPEG_CONFIGURE_ARGS: --enable-pic --disable-static --enable-shared --disable-all --enable-avcodec --enable-decoder=h264 --enable-decoder=hevc --enable-nvdec --enable-hwaccel=h264_nvdec --enable-hwaccel=hevc_nvdec --enable-hwaccel=h264_vaapi --enable-hwaccel=hevc_vaapi --enable-hwaccel=h264_vdpau --enable-hwaccel=hevc_vdpau
install:
- sh: '[ "$BUILD_TARGET" != macos ] || nvm use node'
- sh: '[ "$BUILD_TARGET" != macos ] || npm install --global create-dmg'
- sh: '[ "$BUILD_TARGET" != macos ] || brew install qt'
- sh: '[ "$BUILD_TARGET" != macos ] || brew link --force qt'
- sh: '[ "$BUILD_TARGET" != steamlink ] || sudo apt install -y libc6:i386 libstdc++6:i386'
- sh: '[ "$BUILD_TARGET" != steamlink ] || git clone --depth=1 https://github.com/ValveSoftware/steamlink-sdk.git $HOME/steamlink-sdk'
- sh: '[ "$BUILD_TARGET" != linux ] || sudo add-apt-repository ppa:beineri/opt-qt-5.14.2-xenial'
- sh: '[ "$BUILD_TARGET" != linux ] || sudo apt update'
- sh: '[ "$BUILD_TARGET" != linux ] || sudo apt install -y qt514base qt514quickcontrols2 qt514svg qt514wayland nasm libgbm-dev libdrm-dev libfreetype6-dev libasound2-dev libdbus-1-dev libegl1-mesa-dev libgl1-mesa-dev libgles2-mesa-dev libglu1-mesa-dev libibus-1.0-dev libpulse-dev libudev-dev libx11-dev libxcursor-dev libxext-dev libxi-dev libxinerama-dev libxkbcommon-dev libxrandr-dev libxss-dev libxt-dev libxv-dev libxxf86vm-dev wayland-protocols libopus-dev libvdpau-dev'
- sh: 'if [[ "$BUILD_TARGET" = linux ]]; then export SDL2_VER=2.0.12 && wget https://www.libsdl.org/release/SDL2-$SDL2_VER.tar.gz && tar -xvf SDL2-$SDL2_VER.tar.gz && cd SDL2-$SDL2_VER && ./configure && make -j$(nproc) && sudo make install && cd ..; fi'
- sh: 'if [[ "$BUILD_TARGET" = linux ]]; then export SDL2_TTF_VER=2.0.15 && wget https://www.libsdl.org/projects/SDL_ttf/release/SDL2_ttf-$SDL2_TTF_VER.tar.gz && tar -xvf SDL2_ttf-$SDL2_TTF_VER.tar.gz && cd SDL2_ttf-$SDL2_TTF_VER && ./configure && make -j$(nproc) && sudo make install && cd ..; fi'
- sh: 'if [[ "$BUILD_TARGET" = linux ]]; then export LIBVA_VER=2.8.0 && wget https://github.com/intel/libva/releases/download/$LIBVA_VER/libva-$LIBVA_VER.tar.bz2 && tar -xvf libva-$LIBVA_VER.tar.bz2 && cd libva-$LIBVA_VER && ./configure && make -j$(nproc) && sudo make install && cd ..; fi'
- sh: 'if [[ "$BUILD_TARGET" = linux ]]; then export NVHDR_VER=8.1.24.11 && wget https://github.com/FFmpeg/nv-codec-headers/releases/download/n$NVHDR_VER/nv-codec-headers-$NVHDR_VER.tar.gz && tar -xvf nv-codec-headers-$NVHDR_VER.tar.gz && cd nv-codec-headers-$NVHDR_VER && sudo make install && cd ..; fi'
- sh: 'if [[ "$BUILD_TARGET" = linux ]]; then export FFMPEG_VER=4.3.1 && wget https://ffmpeg.org/releases/ffmpeg-$FFMPEG_VER.tar.bz2 && tar -xvf ffmpeg-$FFMPEG_VER.tar.bz2 && cd ffmpeg-$FFMPEG_VER && ./configure $FFMPEG_CONFIGURE_ARGS && make -j$(nproc) && sudo make install && sudo ldconfig && cd ..; fi'
- sh: 'if [[ "$BUILD_TARGET" = linux ]]; then mkdir $HOME/bin && wget -O $HOME/bin/linuxdeployqt https://github.com/probonopd/linuxdeployqt/releases/download/continuous/linuxdeployqt-continuous-x86_64.AppImage && chmod a+x $HOME/bin/linuxdeployqt; fi'
before_build:
- 'git submodule update --init --recursive'
- cmd: 'set PATH=%PATH%;%QTDIR%\bin;%ProgramFiles(x86)%\WiX Toolset v3.11\bin'
- cmd: 'set /p VERSION=<app\version.txt'
- sh: 'export VERSION=`cat app/version.txt`'
build_script:
- cmd: 'scripts\generate-installers.bat Debug %ARCH%'
- cmd: 'scripts\generate-installers.bat Release %ARCH%'
- sh: '[ "$BUILD_TARGET" != linux ] || source /opt/qt514/bin/qt514-env.sh'
- sh: '[ "$BUILD_TARGET" != linux ] || PATH=$PATH:$HOME/bin scripts/build-appimage.sh'
- sh: '[ "$BUILD_TARGET" != macos ] || scripts/generate-dmg.sh Release'
- sh: '[ "$BUILD_TARGET" != steamlink ] || STEAMLINK_SDK_PATH=$HOME/steamlink-sdk scripts/build-steamlink-app.sh'
after_build:
- cmd: 'appveyor PushArtifact build\installer-%ARCH%-Release\MoonlightPortable-%ARCH%-%VERSION%.zip -FileName MoonlightPortable-%ARCH%-r%APPVEYOR_BUILD_NUMBER%.zip'
- cmd: 'appveyor PushArtifact build\symbols-%ARCH%-Release\MoonlightDebuggingSymbols-%ARCH%-%VERSION%.zip -FileName MoonlightDebuggingSymbols-%ARCH%-r%APPVEYOR_BUILD_NUMBER%.zip'
- sh: '[ "$BUILD_TARGET" != steamlink ] || appveyor PushArtifact build/installer-release/Moonlight-SteamLink-$VERSION.zip -FileName Moonlight-SteamLink-r$APPVEYOR_BUILD_NUMBER.zip'
- sh: '[ "$BUILD_TARGET" != macos ] || appveyor PushArtifact build/installer-Release/Moonlight-$VERSION.dmg -FileName Moonlight-r$APPVEYOR_BUILD_NUMBER.dmg'
- sh: '[ "$BUILD_TARGET" != linux ] || appveyor PushArtifact build/installer-release/Moonlight-$VERSION-x86_64.AppImage -FileName Moonlight-r$APPVEYOR_BUILD_NUMBER-x86_64.AppImage'
deploy: off
-13
View File
@@ -1,13 +0,0 @@
SOURCES = main.cpp
CONFIG += link_pkgconfig
# This adds /opt/vc/include to the include path which
# pulls in the BRCM GLES and EGL libraries. If we don't
# add this, we'll get the system's headers which expose
# functionality the runtime GL implementation won't have.
packagesExist(mmal) {
PKGCONFIG += mmal
}
PKGCONFIG += sdl2 egl libavcodec libavutil
-14
View File
@@ -1,14 +0,0 @@
#include <SDL_egl.h>
#include <SDL_opengles2.h>
#ifndef EGL_VERSION_1_4
#error EGLRenderer requires EGL 1.4
#endif
#ifndef GL_ES_VERSION_2_0
#error EGLRenderer requires OpenGL ES 2.0
#endif
int main() {
return 0;
}

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