Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eaeb9ec6f7 | ||
|
|
88bb17fca8 | ||
|
|
f672b8534f | ||
|
|
839c0a45a0 | ||
|
|
974e44ba4c | ||
|
|
9461ebec2d | ||
|
|
48d8a53cd6 | ||
|
|
2a8dfd63da | ||
|
|
49ac431792 | ||
|
|
8a7c463c52 | ||
|
|
eab77fba7a | ||
|
|
61d90a5a88 | ||
|
|
ef99f02bc7 | ||
|
|
9166a604d6 | ||
|
|
d5a1189053 | ||
|
|
cb52a53922 | ||
|
|
1aff2ff0c3 | ||
|
|
fd63aad3b8 | ||
|
|
9a807a0685 | ||
|
|
40fc9fa26f | ||
|
|
cf0d29d452 | ||
|
|
e411c207ae | ||
|
|
5df2704ccc | ||
|
|
36f468a599 | ||
|
|
dfae69834a | ||
|
|
e53b32fa57 | ||
|
|
969afac696 | ||
|
|
50d4f267ba | ||
|
|
33b9caaca9 | ||
|
|
4f84843b00 | ||
|
|
44f415f94d | ||
|
|
6e4dd161ac | ||
|
|
c724f01c98 | ||
|
|
1fc63a6529 | ||
|
|
5d84ab4a47 | ||
|
|
fc315feac4 | ||
|
|
82a87e96b3 | ||
|
|
7d1ecc8d82 | ||
|
|
f9daf2a45c | ||
|
|
113751a4bc | ||
|
|
e182445593 | ||
|
|
6661ca17c2 | ||
|
|
1b4e75f49e | ||
|
|
22ed7f4e49 | ||
|
|
1d691cf763 | ||
|
|
9075f309ad | ||
|
|
18d97e9f73 | ||
|
|
bccd118abb | ||
|
|
3dc049ee5f | ||
|
|
a614a693e5 | ||
|
|
8dddcd04d5 | ||
|
|
209b4a1b02 | ||
|
|
b14a0fa2ce | ||
|
|
5f05363996 | ||
|
|
d5770ad8cc | ||
|
|
c239d0814b | ||
|
|
f7789a1e4a | ||
|
|
6d382078d3 | ||
|
|
162a16ed3a | ||
|
|
d706e81cd4 | ||
|
|
9fb0bffd61 | ||
|
|
4a452a2467 | ||
|
|
396d65b003 | ||
|
|
fa52e7c1b7 | ||
|
|
6758d6c43e | ||
|
|
4ad27670ec | ||
|
|
1ad072236d | ||
|
|
76387aacad | ||
|
|
200b1c5095 | ||
|
|
d14cfb577b | ||
|
|
c99b4f1559 | ||
|
|
45fdbd16b4 | ||
|
|
c04113c9fa | ||
|
|
4620fa5001 | ||
|
|
ba7dd31035 | ||
|
|
3928155b9c | ||
|
|
a0249b4337 | ||
|
|
d89982237d | ||
|
|
7e414f0af3 | ||
|
|
8fb37ae229 | ||
|
|
2e3de32810 | ||
|
|
f2e40889b2 | ||
|
|
896b247bc0 | ||
|
|
605d52f160 | ||
|
|
afb2b465a8 | ||
|
|
b1de66d878 | ||
|
|
71b625081a | ||
|
|
2cd4851601 | ||
|
|
987d104a6c | ||
|
|
60e6398dfa | ||
|
|
14ec0259df | ||
|
|
9f0617f6ee | ||
|
|
98297e0aed | ||
|
|
926b192722 | ||
|
|
97e5acecd7 | ||
|
|
2c960059ee | ||
|
|
e20bf3fbd9 | ||
|
|
af9fb0e091 | ||
|
|
f96911e1ba | ||
|
|
fdbb1c8d72 | ||
|
|
8a0089eb81 | ||
|
|
821d4ac9dd | ||
|
|
f4408473e6 | ||
|
|
1f759577ce | ||
|
|
04376ca047 | ||
|
|
7ac108f495 | ||
|
|
a2eece0f5c | ||
|
|
a03279df3b | ||
|
|
bef6c2b665 | ||
|
|
8002c65693 | ||
|
|
5c7247b8b5 | ||
|
|
1a18e68441 | ||
|
|
72afa55897 | ||
|
|
eed9fca10c | ||
|
|
25d97e187a | ||
|
|
7ca2ca8045 | ||
|
|
54885c3e5f | ||
|
|
b8ffb6e53d | ||
|
|
7bc43c226d | ||
|
|
da4697794d | ||
|
|
879c524658 | ||
|
|
e3a8f010c3 | ||
|
|
f2f8f92172 | ||
|
|
265ec96c6f | ||
|
|
6e06174db6 | ||
|
|
9677569293 | ||
|
|
c8e00195ed | ||
|
|
64bc189010 | ||
|
|
12496e4432 | ||
|
|
9e2fd67487 | ||
|
|
af17d56cbd | ||
|
|
6e0a657b76 | ||
|
|
b0a9fc7085 | ||
|
|
5e47b8a835 | ||
|
|
0b94ad1fef | ||
|
|
9dcd856099 | ||
|
|
cfbcc4093c | ||
|
|
b8169ae08b | ||
|
|
103625fbdd | ||
|
|
4712080a1b | ||
|
|
93c683be3a | ||
|
|
c9a7c15f98 | ||
|
|
64a08f0533 | ||
|
|
52c6455ca5 | ||
|
|
238681d72f | ||
|
|
dbcc6feb3f | ||
|
|
300b53f28c | ||
|
|
3dabb1c891 | ||
|
|
4381f7a973 | ||
|
|
77b767ae5b | ||
|
|
2c4f78133f | ||
|
|
ec2c181f4e | ||
|
|
fae98eb13a | ||
|
|
f1d117d5d7 | ||
|
|
529ac8041a | ||
|
|
391e84e0f4 | ||
|
|
6a2b717ff1 | ||
|
|
475a39cb73 | ||
|
|
4dba74e2f9 | ||
|
|
587d783955 | ||
|
|
3bfe87d516 | ||
|
|
d3e43012fb | ||
|
|
5da156b433 | ||
|
|
b2eeb63b6f | ||
|
|
a7517dace3 | ||
|
|
bd9f2d2009 | ||
|
|
78730f7235 | ||
|
|
8a2491d955 | ||
|
|
0d68faf40c | ||
|
|
957a2b2169 | ||
|
|
856318f947 | ||
|
|
4df9d6b20a | ||
|
|
8519d09141 | ||
|
|
1b4fe83739 | ||
|
|
8fc825b2eb | ||
|
|
6d0fc22ab5 | ||
|
|
9a71b4c237 | ||
|
|
9aa487418f | ||
|
|
203b2fcaca | ||
|
|
dae21f2638 | ||
|
|
af6e99cbac | ||
|
|
6d6acf4308 | ||
|
|
7dd4815edf | ||
|
|
2c068a99a3 | ||
|
|
fa93364ddd | ||
|
|
9b1eef3f63 | ||
|
|
ed6c07fd4b | ||
|
|
5ad96f5287 | ||
|
|
fa8845c7e2 | ||
|
|
b96ace7f04 | ||
|
|
234a8dc3d5 | ||
|
|
0467c2f893 | ||
|
|
762fe92933 | ||
|
|
16d105d462 | ||
|
|
8a8b624f47 | ||
|
|
9470615c63 | ||
|
|
8f0ea700a5 | ||
|
|
b7cbec0b1c | ||
|
|
9be9934b8c | ||
|
|
55f0e1e1d5 | ||
|
|
bbd784fc85 | ||
|
|
408f93732b |
@@ -4,3 +4,12 @@
|
|||||||
[submodule "qmdnsengine/qmdnsengine"]
|
[submodule "qmdnsengine/qmdnsengine"]
|
||||||
path = qmdnsengine/qmdnsengine
|
path = qmdnsengine/qmdnsengine
|
||||||
url = https://github.com/cgutman/qmdnsengine.git
|
url = https://github.com/cgutman/qmdnsengine.git
|
||||||
|
[submodule "app/SDL_GameControllerDB"]
|
||||||
|
path = app/SDL_GameControllerDB
|
||||||
|
url = https://github.com/gabomdq/SDL_GameControllerDB.git
|
||||||
|
[submodule "soundio/libsoundio"]
|
||||||
|
path = soundio/libsoundio
|
||||||
|
url = https://github.com/cgutman/libsoundio.git
|
||||||
|
[submodule "h264bitstream/h264bitstream"]
|
||||||
|
path = h264bitstream/h264bitstream
|
||||||
|
url = https://github.com/aizvorski/h264bitstream.git
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ matrix:
|
|||||||
- qt59base
|
- qt59base
|
||||||
- qt59quickcontrols2
|
- qt59quickcontrols2
|
||||||
- qt59svg
|
- qt59svg
|
||||||
|
- qt59multimedia
|
||||||
- libesd0-dev
|
- libesd0-dev
|
||||||
- libgl-dev
|
- libgl-dev
|
||||||
- libegl1-mesa-dev
|
- libegl1-mesa-dev
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
QT -= core gui
|
||||||
|
|
||||||
|
TARGET = AntiHooking
|
||||||
|
TEMPLATE = lib
|
||||||
|
|
||||||
|
# Support debug and release builds from command line for CI
|
||||||
|
CONFIG += debug_and_release
|
||||||
|
|
||||||
|
# Ensure symbols are always generated
|
||||||
|
CONFIG += force_debug_info
|
||||||
|
|
||||||
|
INCLUDEPATH += $$PWD/../libs/windows/include
|
||||||
|
contains(QT_ARCH, i386) {
|
||||||
|
LIBS += -L$$PWD/../libs/windows/lib/x86
|
||||||
|
}
|
||||||
|
contains(QT_ARCH, x86_64) {
|
||||||
|
LIBS += -L$$PWD/../libs/windows/lib/x64
|
||||||
|
}
|
||||||
|
|
||||||
|
LIBS += -lNktHookLib
|
||||||
|
DEFINES += ANTIHOOKING_LIBRARY
|
||||||
|
SOURCES += antihookingprotection.cpp
|
||||||
|
HEADERS += antihookingprotection.h
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
#include "antihookingprotection.h"
|
||||||
|
|
||||||
|
#include <NktHookLib.h>
|
||||||
|
|
||||||
|
typedef HMODULE (WINAPI *LoadLibraryAFunc)(LPCSTR lpLibFileName);
|
||||||
|
typedef HMODULE (WINAPI *LoadLibraryWFunc)(LPCWSTR lpLibFileName);
|
||||||
|
typedef HMODULE (WINAPI *LoadLibraryExAFunc)(LPCSTR lpLibFileName, HANDLE hFile, DWORD dwFlags);
|
||||||
|
typedef HMODULE (WINAPI *LoadLibraryExWFunc)(LPCWSTR lpLibFileName, HANDLE hFile, DWORD dwFlags);
|
||||||
|
|
||||||
|
class AntiHookingProtection
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
static void enable()
|
||||||
|
{
|
||||||
|
#ifdef QT_DEBUG
|
||||||
|
s_HookManager.SetEnableDebugOutput(true);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
HINSTANCE kernel32Handle = NktHookLibHelpers::GetModuleBaseAddress(L"kernel32.dll");
|
||||||
|
SIZE_T hookId;
|
||||||
|
|
||||||
|
s_HookManager.Hook(&hookId, (LPVOID*)&s_RealLoadLibraryA,
|
||||||
|
NktHookLibHelpers::GetProcedureAddress(kernel32Handle, "LoadLibraryA"),
|
||||||
|
(LPVOID)AntiHookingProtection::LoadLibraryAHook);
|
||||||
|
s_HookManager.Hook(&hookId, (LPVOID*)&s_RealLoadLibraryW,
|
||||||
|
NktHookLibHelpers::GetProcedureAddress(kernel32Handle, "LoadLibraryW"),
|
||||||
|
(LPVOID)AntiHookingProtection::LoadLibraryWHook);
|
||||||
|
s_HookManager.Hook(&hookId, (LPVOID*)&s_RealLoadLibraryExA,
|
||||||
|
NktHookLibHelpers::GetProcedureAddress(kernel32Handle, "LoadLibraryExA"),
|
||||||
|
(LPVOID)AntiHookingProtection::LoadLibraryExAHook);
|
||||||
|
s_HookManager.Hook(&hookId, (LPVOID*)&s_RealLoadLibraryExW,
|
||||||
|
NktHookLibHelpers::GetProcedureAddress(kernel32Handle, "LoadLibraryExW"),
|
||||||
|
(LPVOID)AntiHookingProtection::LoadLibraryExWHook);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
static bool isImageBlacklistedW(LPCWSTR lpLibFileName)
|
||||||
|
{
|
||||||
|
LPCWSTR dllName;
|
||||||
|
|
||||||
|
// If the library has a path prefixed, remove it
|
||||||
|
dllName = wcsrchr(lpLibFileName, '\\');
|
||||||
|
if (!dllName) {
|
||||||
|
// No prefix, so use the full name
|
||||||
|
dllName = lpLibFileName;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Advance past the backslash
|
||||||
|
dllName++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: We don't currently handle LoadLibrary calls where the
|
||||||
|
// library name does not include a file extension and the loader
|
||||||
|
// automatically assumes .dll.
|
||||||
|
|
||||||
|
for (int i = 0; i < ARRAYSIZE(k_BlacklistedDlls); i++) {
|
||||||
|
if (_wcsicmp(dllName, k_BlacklistedDlls[i]) == 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool isImageBlacklistedA(LPCSTR lpLibFileName)
|
||||||
|
{
|
||||||
|
int uniChars = MultiByteToWideChar(CP_THREAD_ACP, 0, lpLibFileName, -1, nullptr, 0);
|
||||||
|
if (uniChars > 0) {
|
||||||
|
PWCHAR wideBuffer = new WCHAR[uniChars];
|
||||||
|
uniChars = MultiByteToWideChar(CP_THREAD_ACP, 0,
|
||||||
|
lpLibFileName, -1,
|
||||||
|
wideBuffer, uniChars * sizeof(WCHAR));
|
||||||
|
if (uniChars > 0) {
|
||||||
|
bool ret = isImageBlacklistedW(wideBuffer);
|
||||||
|
delete[] wideBuffer;
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
delete[] wideBuffer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error path
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static HMODULE WINAPI LoadLibraryAHook(LPCSTR lpLibFileName)
|
||||||
|
{
|
||||||
|
if (lpLibFileName && isImageBlacklistedA(lpLibFileName)) {
|
||||||
|
SetLastError(ERROR_ACCESS_DISABLED_BY_POLICY);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
return s_RealLoadLibraryA(lpLibFileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
static HMODULE WINAPI LoadLibraryWHook(LPCWSTR lpLibFileName)
|
||||||
|
{
|
||||||
|
if (lpLibFileName && isImageBlacklistedW(lpLibFileName)) {
|
||||||
|
SetLastError(ERROR_ACCESS_DISABLED_BY_POLICY);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
return s_RealLoadLibraryW(lpLibFileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
static HMODULE WINAPI LoadLibraryExAHook(LPCSTR lpLibFileName, HANDLE hFile, DWORD dwFlags)
|
||||||
|
{
|
||||||
|
if (lpLibFileName && isImageBlacklistedA(lpLibFileName)) {
|
||||||
|
SetLastError(ERROR_ACCESS_DISABLED_BY_POLICY);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
return s_RealLoadLibraryExA(lpLibFileName, hFile, dwFlags);
|
||||||
|
}
|
||||||
|
|
||||||
|
static HMODULE WINAPI LoadLibraryExWHook(LPCWSTR lpLibFileName, HANDLE hFile, DWORD dwFlags)
|
||||||
|
{
|
||||||
|
if (lpLibFileName && isImageBlacklistedW(lpLibFileName)) {
|
||||||
|
SetLastError(ERROR_ACCESS_DISABLED_BY_POLICY);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
return s_RealLoadLibraryExW(lpLibFileName, hFile, dwFlags);
|
||||||
|
}
|
||||||
|
|
||||||
|
static CNktHookLib s_HookManager;
|
||||||
|
static LoadLibraryAFunc s_RealLoadLibraryA;
|
||||||
|
static LoadLibraryWFunc s_RealLoadLibraryW;
|
||||||
|
static LoadLibraryExAFunc s_RealLoadLibraryExA;
|
||||||
|
static LoadLibraryExWFunc s_RealLoadLibraryExW;
|
||||||
|
|
||||||
|
static constexpr LPCWSTR k_BlacklistedDlls[] = {
|
||||||
|
// 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"
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
CNktHookLib AntiHookingProtection::s_HookManager;
|
||||||
|
LoadLibraryAFunc AntiHookingProtection::s_RealLoadLibraryA;
|
||||||
|
LoadLibraryWFunc AntiHookingProtection::s_RealLoadLibraryW;
|
||||||
|
LoadLibraryExAFunc AntiHookingProtection::s_RealLoadLibraryExA;
|
||||||
|
LoadLibraryExWFunc AntiHookingProtection::s_RealLoadLibraryExW;
|
||||||
|
|
||||||
|
AH_EXPORT void AntiHookingDummyImport() {}
|
||||||
|
|
||||||
|
extern "C"
|
||||||
|
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID)
|
||||||
|
{
|
||||||
|
switch (fdwReason)
|
||||||
|
{
|
||||||
|
case DLL_PROCESS_ATTACH:
|
||||||
|
AntiHookingProtection::enable();
|
||||||
|
DisableThreadLibraryCalls(hinstDLL);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return TRUE;
|
||||||
|
};
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#ifdef ANTIHOOKING_LIBRARY
|
||||||
|
#define AH_EXPORT extern "C" __declspec(dllexport)
|
||||||
|
#else
|
||||||
|
#define AH_EXPORT extern "C" __declspec(dllimport)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
AH_EXPORT void AntiHookingDummyImport();
|
||||||
|
|
||||||
@@ -13,20 +13,42 @@ You can follow development on our [Discord server](https://discord.gg/6ERtzFY).
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
- Hardware accelerated video decoding on Windows, Mac, and Linux
|
- Hardware accelerated video decoding on Windows, Mac, and Linux
|
||||||
- Supports streaming at up to 120 FPS on high refresh rate monitors
|
- Supports streaming at up to 120 FPS (high refresh rate monitor recommended)
|
||||||
- Supports streaming at 720p, 1080p, 1440p, or 4K
|
- Supports streaming at 720p, 1080p, 1440p, 4K, and the client PC's native screen resolution
|
||||||
- 5.1 surround sound audio
|
- 5.1 surround sound audio
|
||||||
- HEVC support for better image quality at reduced bandwidth
|
- HEVC support for better image quality at reduced bandwidth
|
||||||
- Keyboard and mouse support
|
- Keyboard and mouse support
|
||||||
- Gamepad support with SDL gamepad mappings
|
- Gamepad support with SDL gamepad mappings
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
|
### General Build Requirements
|
||||||
|
* Qt 5.9 SDK or later
|
||||||
|
|
||||||
|
### Windows-specific Requirements
|
||||||
|
* Windows 7 or later
|
||||||
|
* Visual Studio 2015 or later
|
||||||
|
* Select MSVC Desktop toolchain during Qt installation
|
||||||
|
* 7-Zip (only if building installers for non-development PCs)
|
||||||
|
* WiX Toolset v3.11 or later (only if building installers for non-development PCs)
|
||||||
|
|
||||||
|
### Mac-specific Requirements
|
||||||
|
* macOS El Capitan (10.11) or later
|
||||||
|
* Xcode with High Sierra (10.13) SDK (Mojave SDK not yet supported until Qt 5.12 later this year)
|
||||||
|
* [create-dmg](https://github.com/sindresorhus/create-dmg) (only if building DMGs for use on non-development Macs)
|
||||||
|
|
||||||
|
### Linux-specific 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`
|
||||||
|
* FFmpeg 4.0 is required to build. If your distro doesn't package FFmpeg 4.0, you can build and install it from source on http://ffmpeg.org/
|
||||||
|
|
||||||
|
### Build Setup Steps
|
||||||
1. Install the latest Qt SDK (and optionally, the Qt Creator IDE) from https://www.qt.io/download
|
1. Install the latest Qt SDK (and optionally, the Qt Creator IDE) from https://www.qt.io/download
|
||||||
- On Windows, install Visual Studio 2017 and select MSVC 2017 toolchain during Qt installation
|
* You may also use Homebrew on macOS or your Linux distro's package manager for the Qt SDK as long as the packages are Qt 5.9 or later.
|
||||||
- On Mac, install the latest version of XCode
|
|
||||||
- On Linux, install your distro equivalents of: `openssl-devel qt5-devel SDL2-devel ffmpeg-devel qt5-qtquickcontrols2-devel libva-devel libvdpau-devel`
|
|
||||||
2. Run `git submodule update --init --recursive` from within `moonlight-qt/`
|
2. Run `git submodule update --init --recursive` from within `moonlight-qt/`
|
||||||
3. Open the project in Qt Creator or build from qmake on the command line
|
3. Open the project in Qt Creator or build from qmake on the command line.
|
||||||
|
* To build a binary for use on non-development machines, use the scripts in the `scripts` folder.
|
||||||
|
* For Windows builds, use `scripts\generate-installers.bat`. Execute this script from the root of the repository within a Qt command prompt. Ensure WiX and 7-Zip binary directories are in your `%PATH%`.
|
||||||
|
* For macOS builds, use `scripts/generate-dmg.sh`. Execute this script from the root of the repository and ensure Qt's `bin` folder is in your `$PATH`.
|
||||||
|
|
||||||
## Contribute
|
## Contribute
|
||||||
1. Fork us
|
1. Fork us
|
||||||
|
|||||||
+2
-2
@@ -23,9 +23,9 @@
|
|||||||
<key>NSSupportsAutomaticGraphicsSwitching</key>
|
<key>NSSupportsAutomaticGraphicsSwitching</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>0.3.0</string>
|
<string>0.6.2</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>0.3.0</string>
|
<string>0.6.2</string>
|
||||||
<key>CFBundleDisplayName</key>
|
<key>CFBundleDisplayName</key>
|
||||||
<string>Moonlight</string>
|
<string>Moonlight</string>
|
||||||
</dict>
|
</dict>
|
||||||
|
|||||||
Submodule
+1
Submodule app/SDL_GameControllerDB added at 19944691bf
+91
-18
@@ -11,6 +11,16 @@ unix:!macx {
|
|||||||
# Support debug and release builds from command line for CI
|
# Support debug and release builds from command line for CI
|
||||||
CONFIG += debug_and_release
|
CONFIG += debug_and_release
|
||||||
|
|
||||||
|
# Ensure symbols are always generated
|
||||||
|
CONFIG += force_debug_info
|
||||||
|
|
||||||
|
# Precompile QML files to avoid writing qmlcache on portable versions.
|
||||||
|
# Since this binds the app against the Qt runtime version, we will only
|
||||||
|
# do this for Windows and Mac, since they ship with the Qt runtime.
|
||||||
|
win32|macx {
|
||||||
|
CONFIG += qtquickcompiler
|
||||||
|
}
|
||||||
|
|
||||||
TEMPLATE = app
|
TEMPLATE = app
|
||||||
|
|
||||||
# The following define makes your compiler emit warnings if you use
|
# The following define makes your compiler emit warnings if you use
|
||||||
@@ -34,7 +44,7 @@ win32 {
|
|||||||
LIBS += -L$$PWD/../libs/windows/lib/x64
|
LIBS += -L$$PWD/../libs/windows/lib/x64
|
||||||
}
|
}
|
||||||
|
|
||||||
LIBS += ws2_32.lib winmm.lib dxva2.lib ole32.lib gdi32.lib user32.lib d3d9.lib dwmapi.lib
|
LIBS += ws2_32.lib winmm.lib dxva2.lib ole32.lib gdi32.lib user32.lib d3d9.lib dwmapi.lib dbghelp.lib
|
||||||
}
|
}
|
||||||
macx {
|
macx {
|
||||||
INCLUDEPATH += $$PWD/../libs/mac/include $$PWD/../libs/mac/Frameworks/SDL2.framework/Versions/A/Headers
|
INCLUDEPATH += $$PWD/../libs/mac/include $$PWD/../libs/mac/Frameworks/SDL2.framework/Versions/A/Headers
|
||||||
@@ -45,6 +55,14 @@ unix:!macx {
|
|||||||
CONFIG += link_pkgconfig
|
CONFIG += link_pkgconfig
|
||||||
PKGCONFIG += openssl sdl2 opus
|
PKGCONFIG += openssl sdl2 opus
|
||||||
|
|
||||||
|
# For libsoundio
|
||||||
|
packagesExist(libpulse) {
|
||||||
|
PKGCONFIG += libpulse
|
||||||
|
}
|
||||||
|
packagesExist(alsa) {
|
||||||
|
PKGCONFIG += alsa
|
||||||
|
}
|
||||||
|
|
||||||
packagesExist(libavcodec) {
|
packagesExist(libavcodec) {
|
||||||
PKGCONFIG += libavcodec libavutil
|
PKGCONFIG += libavcodec libavutil
|
||||||
CONFIG += ffmpeg
|
CONFIG += ffmpeg
|
||||||
@@ -66,47 +84,64 @@ unix:!macx {
|
|||||||
}
|
}
|
||||||
win32 {
|
win32 {
|
||||||
LIBS += -llibssl -llibcrypto -lSDL2 -lavcodec -lavutil -lopus
|
LIBS += -llibssl -llibcrypto -lSDL2 -lavcodec -lavutil -lopus
|
||||||
CONFIG += ffmpeg
|
CONFIG += ffmpeg soundio
|
||||||
}
|
}
|
||||||
macx {
|
macx {
|
||||||
LIBS += -lssl -lcrypto -lavcodec.58 -lavutil.56 -lopus -framework SDL2
|
LIBS += -lssl -lcrypto -lavcodec.58 -lavutil.56 -lopus -framework SDL2
|
||||||
LIBS += -lobjc -framework VideoToolbox -framework AVFoundation -framework CoreVideo -framework CoreGraphics -framework CoreMedia -framework AppKit
|
LIBS += -lobjc -framework VideoToolbox -framework AVFoundation -framework CoreVideo -framework CoreGraphics -framework CoreMedia -framework AppKit
|
||||||
CONFIG += ffmpeg
|
|
||||||
|
# For libsoundio
|
||||||
|
LIBS += -framework CoreAudio -framework AudioUnit
|
||||||
|
|
||||||
|
CONFIG += ffmpeg soundio
|
||||||
}
|
}
|
||||||
|
|
||||||
SOURCES += \
|
SOURCES += \
|
||||||
main.cpp \
|
main.cpp \
|
||||||
backend/identitymanager.cpp \
|
backend/identitymanager.cpp \
|
||||||
|
backend/nvcomputer.cpp \
|
||||||
backend/nvhttp.cpp \
|
backend/nvhttp.cpp \
|
||||||
backend/nvpairingmanager.cpp \
|
backend/nvpairingmanager.cpp \
|
||||||
backend/computermanager.cpp \
|
backend/computermanager.cpp \
|
||||||
backend/boxartmanager.cpp \
|
backend/boxartmanager.cpp \
|
||||||
|
cli/commandlineparser.cpp \
|
||||||
|
cli/startstream.cpp \
|
||||||
settings/streamingpreferences.cpp \
|
settings/streamingpreferences.cpp \
|
||||||
streaming/input.cpp \
|
streaming/input.cpp \
|
||||||
streaming/session.cpp \
|
streaming/session.cpp \
|
||||||
streaming/audio.cpp \
|
streaming/audio/audio.cpp \
|
||||||
|
streaming/audio/renderers/sdlaud.cpp \
|
||||||
gui/computermodel.cpp \
|
gui/computermodel.cpp \
|
||||||
gui/appmodel.cpp \
|
gui/appmodel.cpp \
|
||||||
streaming/streamutils.cpp \
|
streaming/streamutils.cpp \
|
||||||
backend/autoupdatechecker.cpp \
|
backend/autoupdatechecker.cpp \
|
||||||
path.cpp
|
path.cpp \
|
||||||
|
settings/mappingmanager.cpp \
|
||||||
|
gui/sdlgamepadkeynavigation.cpp
|
||||||
|
|
||||||
HEADERS += \
|
HEADERS += \
|
||||||
utils.h \
|
utils.h \
|
||||||
backend/identitymanager.h \
|
backend/identitymanager.h \
|
||||||
|
backend/nvcomputer.h \
|
||||||
backend/nvhttp.h \
|
backend/nvhttp.h \
|
||||||
backend/nvpairingmanager.h \
|
backend/nvpairingmanager.h \
|
||||||
backend/computermanager.h \
|
backend/computermanager.h \
|
||||||
backend/boxartmanager.h \
|
backend/boxartmanager.h \
|
||||||
|
cli/commandlineparser.h \
|
||||||
|
cli/startstream.h \
|
||||||
settings/streamingpreferences.h \
|
settings/streamingpreferences.h \
|
||||||
streaming/input.hpp \
|
streaming/input.h \
|
||||||
streaming/session.hpp \
|
streaming/session.h \
|
||||||
|
streaming/audio/renderers/renderer.h \
|
||||||
|
streaming/audio/renderers/sdl.h \
|
||||||
gui/computermodel.h \
|
gui/computermodel.h \
|
||||||
gui/appmodel.h \
|
gui/appmodel.h \
|
||||||
streaming/video/decoder.h \
|
streaming/video/decoder.h \
|
||||||
streaming/streamutils.h \
|
streaming/streamutils.h \
|
||||||
backend/autoupdatechecker.h \
|
backend/autoupdatechecker.h \
|
||||||
path.h
|
path.h \
|
||||||
|
settings/mappingmanager.h \
|
||||||
|
gui/sdlgamepadkeynavigation.h
|
||||||
|
|
||||||
# Platform-specific renderers and decoders
|
# Platform-specific renderers and decoders
|
||||||
ffmpeg {
|
ffmpeg {
|
||||||
@@ -115,7 +150,7 @@ ffmpeg {
|
|||||||
DEFINES += HAVE_FFMPEG
|
DEFINES += HAVE_FFMPEG
|
||||||
SOURCES += \
|
SOURCES += \
|
||||||
streaming/video/ffmpeg.cpp \
|
streaming/video/ffmpeg.cpp \
|
||||||
streaming/video/ffmpeg-renderers/sdl.cpp \
|
streaming/video/ffmpeg-renderers/sdlvid.cpp \
|
||||||
streaming/video/ffmpeg-renderers/pacer/pacer.cpp \
|
streaming/video/ffmpeg-renderers/pacer/pacer.cpp \
|
||||||
streaming/video/ffmpeg-renderers/pacer/nullthreadedvsyncsource.cpp
|
streaming/video/ffmpeg-renderers/pacer/nullthreadedvsyncsource.cpp
|
||||||
|
|
||||||
@@ -158,6 +193,7 @@ config_SLVideo {
|
|||||||
|
|
||||||
DEFINES += HAVE_SLVIDEO
|
DEFINES += HAVE_SLVIDEO
|
||||||
LIBS += -lSLVideo
|
LIBS += -lSLVideo
|
||||||
|
|
||||||
SOURCES += streaming/video/sl.cpp
|
SOURCES += streaming/video/sl.cpp
|
||||||
HEADERS += streaming/video/sl.h
|
HEADERS += streaming/video/sl.h
|
||||||
}
|
}
|
||||||
@@ -177,12 +213,19 @@ macx {
|
|||||||
|
|
||||||
SOURCES += \
|
SOURCES += \
|
||||||
streaming/video/ffmpeg-renderers/vt.mm \
|
streaming/video/ffmpeg-renderers/vt.mm \
|
||||||
streaming/video/ffmpeg-renderers/pacer/displaylinkvsyncsource.cpp
|
streaming/video/ffmpeg-renderers/pacer/displaylinkvsyncsource.mm
|
||||||
|
|
||||||
HEADERS += \
|
HEADERS += \
|
||||||
streaming/video/ffmpeg-renderers/vt.h \
|
streaming/video/ffmpeg-renderers/vt.h \
|
||||||
streaming/video/ffmpeg-renderers/pacer/displaylinkvsyncsource.h
|
streaming/video/ffmpeg-renderers/pacer/displaylinkvsyncsource.h
|
||||||
}
|
}
|
||||||
|
soundio {
|
||||||
|
message(libsoundio audio renderer selected)
|
||||||
|
|
||||||
|
DEFINES += HAVE_SOUNDIO SOUNDIO_STATIC_LIBRARY
|
||||||
|
SOURCES += streaming/audio/renderers/soundioaudiorenderer.cpp
|
||||||
|
HEADERS += streaming/audio/renderers/soundioaudiorenderer.h
|
||||||
|
}
|
||||||
|
|
||||||
RESOURCES += \
|
RESOURCES += \
|
||||||
resources.qrc \
|
resources.qrc \
|
||||||
@@ -208,6 +251,26 @@ else:unix: LIBS += -L$$OUT_PWD/../qmdnsengine/ -lqmdnsengine
|
|||||||
INCLUDEPATH += $$PWD/../qmdnsengine/qmdnsengine/src/include $$PWD/../qmdnsengine
|
INCLUDEPATH += $$PWD/../qmdnsengine/qmdnsengine/src/include $$PWD/../qmdnsengine
|
||||||
DEPENDPATH += $$PWD/../qmdnsengine/qmdnsengine/src/include $$PWD/../qmdnsengine
|
DEPENDPATH += $$PWD/../qmdnsengine/qmdnsengine/src/include $$PWD/../qmdnsengine
|
||||||
|
|
||||||
|
win32:CONFIG(release, debug|release): LIBS += -L$$OUT_PWD/../soundio/release/ -lsoundio
|
||||||
|
else:win32:CONFIG(debug, debug|release): LIBS += -L$$OUT_PWD/../soundio/debug/ -lsoundio
|
||||||
|
else:unix: LIBS += -L$$OUT_PWD/../soundio/ -lsoundio
|
||||||
|
|
||||||
|
INCLUDEPATH += $$PWD/../soundio/libsoundio
|
||||||
|
DEPENDPATH += $$PWD/../soundio/libsoundio
|
||||||
|
|
||||||
|
win32:CONFIG(release, debug|release): LIBS += -L$$OUT_PWD/../h264bitstream/release/ -lh264bitstream
|
||||||
|
else:win32:CONFIG(debug, debug|release): LIBS += -L$$OUT_PWD/../h264bitstream/debug/ -lh264bitstream
|
||||||
|
else:unix: LIBS += -L$$OUT_PWD/../h264bitstream/ -lh264bitstream
|
||||||
|
|
||||||
|
INCLUDEPATH += $$PWD/../h264bitstream/h264bitstream
|
||||||
|
DEPENDPATH += $$PWD/../h264bitstream/h264bitstream
|
||||||
|
|
||||||
|
win32:CONFIG(release, debug|release): LIBS += -L$$OUT_PWD/../AntiHooking/release/ -lAntiHooking
|
||||||
|
else:win32:CONFIG(debug, debug|release): LIBS += -L$$OUT_PWD/../AntiHooking/debug/ -lAntiHooking
|
||||||
|
|
||||||
|
INCLUDEPATH += $$PWD/../AntiHooking
|
||||||
|
DEPENDPATH += $$PWD/../AntiHooking
|
||||||
|
|
||||||
unix:!macx: {
|
unix:!macx: {
|
||||||
isEmpty(PREFIX) {
|
isEmpty(PREFIX) {
|
||||||
PREFIX = /usr/local
|
PREFIX = /usr/local
|
||||||
@@ -215,32 +278,42 @@ unix:!macx: {
|
|||||||
isEmpty(BINDIR) {
|
isEmpty(BINDIR) {
|
||||||
BINDIR = bin
|
BINDIR = bin
|
||||||
}
|
}
|
||||||
|
isEmpty(DATADIR) {
|
||||||
|
DATADIR = share
|
||||||
|
}
|
||||||
|
|
||||||
target.path = $$PREFIX/$$BINDIR/
|
target.path = $$PREFIX/$$BINDIR/
|
||||||
|
|
||||||
desktop.files = deploy/linux/com.moonlight_stream.Moonlight.desktop
|
desktop.files = deploy/linux/com.moonlight_stream.Moonlight.desktop
|
||||||
desktop.path = $$PREFIX/share/applications/
|
desktop.path = $$PREFIX/$$DATADIR/applications/
|
||||||
|
|
||||||
icons.files = res/moonlight.svg
|
icons.files = res/moonlight.svg
|
||||||
icons.path = $$PREFIX/share/icons/hicolor/scalable/apps/
|
icons.path = $$PREFIX/$$DATADIR/icons/hicolor/scalable/apps/
|
||||||
|
|
||||||
appdata.files = deploy/linux/com.moonlight_stream.Moonlight.appdata.xml
|
appstream.files = deploy/linux/com.moonlight_stream.Moonlight.appdata.xml
|
||||||
appdata.path = $$PREFIX/share/metainfo/
|
appstream.path = $$PREFIX/$$DATADIR/metainfo/
|
||||||
|
|
||||||
INSTALLS += target desktop icons appdata
|
appdata.files = SDL_GameControllerDB/gamecontrollerdb.txt
|
||||||
|
appdata.path = "$$PREFIX/$$DATADIR/Moonlight Game Streaming Project/Moonlight/"
|
||||||
|
|
||||||
|
INSTALLS += target appdata desktop icons appstream
|
||||||
}
|
}
|
||||||
win32 {
|
win32 {
|
||||||
RC_ICONS = moonlight.ico
|
RC_ICONS = moonlight.ico
|
||||||
|
QMAKE_TARGET_COMPANY = Moonlight Game Streaming Project
|
||||||
|
QMAKE_TARGET_DESCRIPTION = Moonlight Game Streaming Client
|
||||||
|
QMAKE_TARGET_PRODUCT = Moonlight
|
||||||
|
|
||||||
CONFIG -= embed_manifest_exe
|
CONFIG -= embed_manifest_exe
|
||||||
QMAKE_LFLAGS += /MANIFEST:embed /MANIFESTINPUT:$${PWD}/Moonlight.exe.manifest
|
QMAKE_LFLAGS += /MANIFEST:embed /MANIFESTINPUT:$${PWD}/Moonlight.exe.manifest
|
||||||
}
|
}
|
||||||
macx {
|
macx {
|
||||||
QMAKE_INFO_PLIST = $$PWD/Info.plist
|
QMAKE_INFO_PLIST = $$PWD/Info.plist
|
||||||
|
|
||||||
APP_BUNDLE_RESOURCES.files = moonlight.icns
|
APP_BUNDLE_RESOURCES.files = moonlight.icns SDL_GameControllerDB/gamecontrollerdb.txt
|
||||||
APP_BUNDLE_RESOURCES.path = Contents/Resources
|
APP_BUNDLE_RESOURCES.path = Contents/Resources
|
||||||
QMAKE_BUNDLE_DATA += APP_BUNDLE_RESOURCES
|
QMAKE_BUNDLE_DATA += APP_BUNDLE_RESOURCES
|
||||||
}
|
}
|
||||||
|
|
||||||
VERSION = 0.3.0
|
VERSION = 0.6.2
|
||||||
DEFINES += VERSION_STR=\\\"0.3.0\\\"
|
DEFINES += VERSION_STR=\\\"0.6.2\\\"
|
||||||
|
|||||||
@@ -6,8 +6,14 @@
|
|||||||
|
|
||||||
BoxArtManager::BoxArtManager(QObject *parent) :
|
BoxArtManager::BoxArtManager(QObject *parent) :
|
||||||
QObject(parent),
|
QObject(parent),
|
||||||
m_BoxArtDir(Path::getBoxArtCacheDir())
|
m_BoxArtDir(Path::getBoxArtCacheDir()),
|
||||||
|
m_ThreadPool(this)
|
||||||
{
|
{
|
||||||
|
// 4 is a good balance between fast loading for large
|
||||||
|
// app grids and not crushing GFE with tons of requests
|
||||||
|
// and causing UI jank from constantly stalling to decode
|
||||||
|
// new images.
|
||||||
|
m_ThreadPool.setMaxThreadCount(4);
|
||||||
if (!m_BoxArtDir.exists()) {
|
if (!m_BoxArtDir.exists()) {
|
||||||
m_BoxArtDir.mkpath(".");
|
m_BoxArtDir.mkpath(".");
|
||||||
}
|
}
|
||||||
@@ -30,6 +36,39 @@ BoxArtManager::getFilePathForBoxArt(NvComputer* computer, int appId)
|
|||||||
return dir.filePath(QString::number(appId) + ".png");
|
return dir.filePath(QString::number(appId) + ".png");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class NetworkBoxArtLoadTask : public QObject, public QRunnable
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
NetworkBoxArtLoadTask(BoxArtManager* boxArtManager, NvComputer* computer, NvApp& app)
|
||||||
|
: m_Bam(boxArtManager),
|
||||||
|
m_Computer(computer),
|
||||||
|
m_App(app)
|
||||||
|
{
|
||||||
|
connect(this, SIGNAL(boxArtFetchCompleted(NvComputer*,NvApp,QUrl)),
|
||||||
|
boxArtManager, SLOT(handleBoxArtLoadComplete(NvComputer*,NvApp,QUrl)));
|
||||||
|
}
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void boxArtFetchCompleted(NvComputer* computer, NvApp app, QUrl image);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void run()
|
||||||
|
{
|
||||||
|
QUrl image = m_Bam->loadBoxArtFromNetwork(m_Computer, m_App.id);
|
||||||
|
if (image.isEmpty()) {
|
||||||
|
// Give it another shot if it fails once
|
||||||
|
image = m_Bam->loadBoxArtFromNetwork(m_Computer, m_App.id);
|
||||||
|
}
|
||||||
|
emit boxArtFetchCompleted(m_Computer, m_App, image);
|
||||||
|
}
|
||||||
|
|
||||||
|
BoxArtManager* m_Bam;
|
||||||
|
NvComputer* m_Computer;
|
||||||
|
NvApp m_App;
|
||||||
|
};
|
||||||
|
|
||||||
QUrl BoxArtManager::loadBoxArt(NvComputer* computer, NvApp& app)
|
QUrl BoxArtManager::loadBoxArt(NvComputer* computer, NvApp& app)
|
||||||
{
|
{
|
||||||
// Try to open the cached file
|
// Try to open the cached file
|
||||||
@@ -41,7 +80,7 @@ QUrl BoxArtManager::loadBoxArt(NvComputer* computer, NvApp& app)
|
|||||||
// If we get here, we need to fetch asynchronously.
|
// If we get here, we need to fetch asynchronously.
|
||||||
// Kick off a worker on our thread pool to do just that.
|
// Kick off a worker on our thread pool to do just that.
|
||||||
NetworkBoxArtLoadTask* netLoadTask = new NetworkBoxArtLoadTask(this, computer, app);
|
NetworkBoxArtLoadTask* netLoadTask = new NetworkBoxArtLoadTask(this, computer, app);
|
||||||
QThreadPool::globalInstance()->start(netLoadTask);
|
m_ThreadPool.start(netLoadTask);
|
||||||
|
|
||||||
// Return the placeholder then we can notify the caller
|
// Return the placeholder then we can notify the caller
|
||||||
// later when the real image is ready.
|
// later when the real image is ready.
|
||||||
@@ -75,3 +114,5 @@ QUrl BoxArtManager::loadBoxArtFromNetwork(NvComputer* computer, int appId)
|
|||||||
|
|
||||||
return QUrl();
|
return QUrl();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#include "boxartmanager.moc"
|
||||||
|
|||||||
@@ -36,37 +36,5 @@ private:
|
|||||||
getFilePathForBoxArt(NvComputer* computer, int appId);
|
getFilePathForBoxArt(NvComputer* computer, int appId);
|
||||||
|
|
||||||
QDir m_BoxArtDir;
|
QDir m_BoxArtDir;
|
||||||
};
|
QThreadPool m_ThreadPool;
|
||||||
|
|
||||||
class NetworkBoxArtLoadTask : public QObject, public QRunnable
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
NetworkBoxArtLoadTask(BoxArtManager* boxArtManager, NvComputer* computer, NvApp& app)
|
|
||||||
: m_Bam(boxArtManager),
|
|
||||||
m_Computer(computer),
|
|
||||||
m_App(app)
|
|
||||||
{
|
|
||||||
connect(this, SIGNAL(boxArtFetchCompleted(NvComputer*,NvApp,QUrl)),
|
|
||||||
boxArtManager, SLOT(handleBoxArtLoadComplete(NvComputer*,NvApp,QUrl)));
|
|
||||||
}
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void boxArtFetchCompleted(NvComputer* computer, NvApp app, QUrl image);
|
|
||||||
|
|
||||||
private:
|
|
||||||
void run()
|
|
||||||
{
|
|
||||||
QUrl image = m_Bam->loadBoxArtFromNetwork(m_Computer, m_App.id);
|
|
||||||
if (image.isEmpty()) {
|
|
||||||
// Give it another shot if it fails once
|
|
||||||
image = m_Bam->loadBoxArtFromNetwork(m_Computer, m_App.id);
|
|
||||||
}
|
|
||||||
emit boxArtFetchCompleted(m_Computer, m_App, image);
|
|
||||||
}
|
|
||||||
|
|
||||||
BoxArtManager* m_Bam;
|
|
||||||
NvComputer* m_Computer;
|
|
||||||
NvApp m_App;
|
|
||||||
};
|
};
|
||||||
|
|||||||
+397
-290
@@ -1,278 +1,146 @@
|
|||||||
#include "computermanager.h"
|
#include "computermanager.h"
|
||||||
#include "nvhttp.h"
|
#include "nvhttp.h"
|
||||||
|
#include "settings/streamingpreferences.h"
|
||||||
|
|
||||||
|
#include <Limelight.h>
|
||||||
|
#include <QtEndian>
|
||||||
|
|
||||||
#include <QThread>
|
#include <QThread>
|
||||||
#include <QUdpSocket>
|
|
||||||
#include <QHostInfo>
|
|
||||||
#include <QThreadPool>
|
#include <QThreadPool>
|
||||||
|
|
||||||
#define SER_HOSTS "hosts"
|
#define SER_HOSTS "hosts"
|
||||||
#define SER_NAME "hostname"
|
|
||||||
#define SER_UUID "uuid"
|
|
||||||
#define SER_MAC "mac"
|
|
||||||
#define SER_LOCALADDR "localaddress"
|
|
||||||
#define SER_REMOTEADDR "remoteaddress"
|
|
||||||
#define SER_MANUALADDR "manualaddress"
|
|
||||||
#define SER_APPLIST "apps"
|
|
||||||
|
|
||||||
#define SER_APPNAME "name"
|
class PcMonitorThread : public QThread
|
||||||
#define SER_APPID "id"
|
|
||||||
#define SER_APPHDR "hdr"
|
|
||||||
|
|
||||||
NvComputer::NvComputer(QSettings& settings)
|
|
||||||
{
|
{
|
||||||
this->name = settings.value(SER_NAME).toString();
|
Q_OBJECT
|
||||||
this->uuid = settings.value(SER_UUID).toString();
|
|
||||||
this->macAddress = settings.value(SER_MAC).toByteArray();
|
|
||||||
this->localAddress = settings.value(SER_LOCALADDR).toString();
|
|
||||||
this->remoteAddress = settings.value(SER_REMOTEADDR).toString();
|
|
||||||
this->manualAddress = settings.value(SER_MANUALADDR).toString();
|
|
||||||
|
|
||||||
int appCount = settings.beginReadArray(SER_APPLIST);
|
#define TRIES_BEFORE_OFFLINING 2
|
||||||
for (int i = 0; i < appCount; i++) {
|
#define POLLS_PER_APPLIST_FETCH 10
|
||||||
NvApp app;
|
|
||||||
|
|
||||||
settings.setArrayIndex(i);
|
public:
|
||||||
|
PcMonitorThread(NvComputer* computer)
|
||||||
app.name = settings.value(SER_APPNAME).toString();
|
: m_Computer(computer)
|
||||||
app.id = settings.value(SER_APPID).toInt();
|
{
|
||||||
app.hdrSupported = settings.value(SER_APPHDR).toBool();
|
setObjectName("Polling thread for " + computer->name);
|
||||||
|
|
||||||
this->appList.append(app);
|
|
||||||
}
|
}
|
||||||
settings.endArray();
|
|
||||||
sortAppList();
|
|
||||||
|
|
||||||
this->activeAddress = nullptr;
|
private:
|
||||||
this->currentGameId = 0;
|
bool tryPollComputer(QString address, bool& changed)
|
||||||
this->pairState = PS_UNKNOWN;
|
{
|
||||||
this->state = CS_UNKNOWN;
|
NvHTTP http(address);
|
||||||
this->gfeVersion = nullptr;
|
|
||||||
this->appVersion = nullptr;
|
|
||||||
this->maxLumaPixelsHEVC = 0;
|
|
||||||
this->serverCodecModeSupport = 0;
|
|
||||||
this->pendingQuit = false;
|
|
||||||
this->gpuModel = nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
QString serverInfo;
|
||||||
NvComputer::serialize(QSettings& settings)
|
try {
|
||||||
{
|
serverInfo = http.getServerInfo(NvHTTP::NvLogLevel::NONE);
|
||||||
QReadLocker lock(&this->lock);
|
} catch (...) {
|
||||||
|
return false;
|
||||||
settings.setValue(SER_NAME, name);
|
|
||||||
settings.setValue(SER_UUID, uuid);
|
|
||||||
settings.setValue(SER_MAC, macAddress);
|
|
||||||
settings.setValue(SER_LOCALADDR, localAddress);
|
|
||||||
settings.setValue(SER_REMOTEADDR, remoteAddress);
|
|
||||||
settings.setValue(SER_MANUALADDR, manualAddress);
|
|
||||||
|
|
||||||
// Avoid deleting an existing applist if we couldn't get one
|
|
||||||
if (!appList.isEmpty()) {
|
|
||||||
settings.remove(SER_APPLIST);
|
|
||||||
settings.beginWriteArray(SER_APPLIST);
|
|
||||||
for (int i = 0; i < appList.count(); i++) {
|
|
||||||
settings.setArrayIndex(i);
|
|
||||||
|
|
||||||
settings.setValue(SER_APPNAME, appList[i].name);
|
|
||||||
settings.setValue(SER_APPID, appList[i].id);
|
|
||||||
settings.setValue(SER_APPHDR, appList[i].hdrSupported);
|
|
||||||
}
|
}
|
||||||
settings.endArray();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void NvComputer::sortAppList()
|
NvComputer newState(address, serverInfo);
|
||||||
{
|
|
||||||
std::stable_sort(appList.begin(), appList.end(), [](const NvApp& app1, const NvApp& app2) {
|
|
||||||
return app1.name.toLower() < app2.name.toLower();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
NvComputer::NvComputer(QString address, QString serverInfo)
|
// Ensure the machine that responded is the one we intended to contact
|
||||||
{
|
if (m_Computer->uuid != newState.uuid) {
|
||||||
this->name = NvHTTP::getXmlString(serverInfo, "hostname");
|
qInfo() << "Found unexpected PC " << newState.name << " looking for " << m_Computer->name;
|
||||||
if (this->name.isNull()) {
|
return false;
|
||||||
this->name = "UNKNOWN";
|
|
||||||
}
|
|
||||||
|
|
||||||
this->uuid = NvHTTP::getXmlString(serverInfo, "uniqueid");
|
|
||||||
QString newMacString = NvHTTP::getXmlString(serverInfo, "mac");
|
|
||||||
if (newMacString != "00:00:00:00:00:00") {
|
|
||||||
QStringList macOctets = newMacString.split(':');
|
|
||||||
for (QString macOctet : macOctets) {
|
|
||||||
this->macAddress.append((char) macOctet.toInt(nullptr, 16));
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
QString codecSupport = NvHTTP::getXmlString(serverInfo, "ServerCodecModeSupport");
|
changed = m_Computer->update(newState);
|
||||||
if (!codecSupport.isNull()) {
|
|
||||||
this->serverCodecModeSupport = codecSupport.toInt();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this->serverCodecModeSupport = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString maxLumaPixelsHEVC = NvHTTP::getXmlString(serverInfo, "MaxLumaPixelsHEVC");
|
|
||||||
if (!maxLumaPixelsHEVC.isNull()) {
|
|
||||||
this->maxLumaPixelsHEVC = maxLumaPixelsHEVC.toInt();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this->maxLumaPixelsHEVC = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
this->displayModes = NvHTTP::getDisplayModeList(serverInfo);
|
|
||||||
std::stable_sort(this->displayModes.begin(), this->displayModes.end(),
|
|
||||||
[](const NvDisplayMode& mode1, const NvDisplayMode& mode2) {
|
|
||||||
return mode1.width * mode1.height * mode1.refreshRate <
|
|
||||||
mode2.width * mode2.height * mode2.refreshRate;
|
|
||||||
});
|
|
||||||
|
|
||||||
this->localAddress = NvHTTP::getXmlString(serverInfo, "LocalIP");
|
|
||||||
this->remoteAddress = NvHTTP::getXmlString(serverInfo, "ExternalIP");
|
|
||||||
this->pairState = NvHTTP::getXmlString(serverInfo, "PairStatus") == "1" ?
|
|
||||||
PS_PAIRED : PS_NOT_PAIRED;
|
|
||||||
this->currentGameId = NvHTTP::getCurrentGame(serverInfo);
|
|
||||||
this->appVersion = NvHTTP::getXmlString(serverInfo, "appversion");
|
|
||||||
this->gfeVersion = NvHTTP::getXmlString(serverInfo, "GfeVersion");
|
|
||||||
this->gpuModel = NvHTTP::getXmlString(serverInfo, "gputype");
|
|
||||||
this->activeAddress = address;
|
|
||||||
this->state = NvComputer::CS_ONLINE;
|
|
||||||
this->pendingQuit = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool NvComputer::wake()
|
|
||||||
{
|
|
||||||
if (state == NvComputer::CS_ONLINE) {
|
|
||||||
qWarning() << name << "is already online";
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (macAddress.isNull()) {
|
bool updateAppList(bool& changed)
|
||||||
qWarning() << name << "has no MAC address stored";
|
{
|
||||||
return false;
|
Q_ASSERT(m_Computer->activeAddress != nullptr);
|
||||||
|
|
||||||
|
NvHTTP http(m_Computer->activeAddress);
|
||||||
|
|
||||||
|
QVector<NvApp> appList;
|
||||||
|
|
||||||
|
try {
|
||||||
|
appList = http.getAppList();
|
||||||
|
if (appList.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (...) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QWriteLocker lock(&m_Computer->lock);
|
||||||
|
if (m_Computer->appList != appList) {
|
||||||
|
m_Computer->appList = appList;
|
||||||
|
m_Computer->sortAppList();
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const quint16 WOL_PORTS[] = {
|
void run() override
|
||||||
7, 9, // Standard WOL ports
|
{
|
||||||
47998, 47999, 48000, // Ports opened by GFE
|
// Always fetch the applist the first time
|
||||||
};
|
int pollsSinceLastAppListFetch = POLLS_PER_APPLIST_FETCH;
|
||||||
|
while (!isInterruptionRequested()) {
|
||||||
|
bool stateChanged = false;
|
||||||
|
bool online = false;
|
||||||
|
bool wasOnline = m_Computer->state == NvComputer::CS_ONLINE;
|
||||||
|
for (int i = 0; i < TRIES_BEFORE_OFFLINING && !online; i++) {
|
||||||
|
for (auto& address : m_Computer->uniqueAddresses()) {
|
||||||
|
if (isInterruptionRequested()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Create the WoL payload
|
if (tryPollComputer(address, stateChanged)) {
|
||||||
QByteArray wolPayload;
|
if (!wasOnline) {
|
||||||
wolPayload.append(QByteArray::fromHex("FFFFFFFFFFFF"));
|
qInfo() << m_Computer->name << "is now online at" << m_Computer->activeAddress;
|
||||||
for (int i = 0; i < 16; i++) {
|
}
|
||||||
wolPayload.append(macAddress);
|
online = true;
|
||||||
}
|
break;
|
||||||
Q_ASSERT(wolPayload.count() == 102);
|
|
||||||
|
|
||||||
// Add the addresses that we know this host to be
|
|
||||||
// and broadcast addresses for this link just in
|
|
||||||
// case the host has timed out in ARP entries.
|
|
||||||
QVector<QString> addressList = uniqueAddresses();
|
|
||||||
addressList.append("255.255.255.255");
|
|
||||||
|
|
||||||
// Try all unique address strings or host names
|
|
||||||
bool success = false;
|
|
||||||
for (QString& addressString : addressList) {
|
|
||||||
QHostInfo hostInfo = QHostInfo::fromName(addressString);
|
|
||||||
|
|
||||||
// Try all IP addresses that this string resolves to
|
|
||||||
for (QHostAddress& address : hostInfo.addresses()) {
|
|
||||||
QUdpSocket sock;
|
|
||||||
|
|
||||||
// Bind to any address on the correct protocol
|
|
||||||
if (sock.bind(address.protocol() == QUdpSocket::IPv4Protocol ?
|
|
||||||
QHostAddress::AnyIPv4 : QHostAddress::AnyIPv6)) {
|
|
||||||
|
|
||||||
// Send to all ports
|
|
||||||
for (quint16 port : WOL_PORTS) {
|
|
||||||
if (sock.writeDatagram(wolPayload, address, port)) {
|
|
||||||
qInfo().nospace().noquote() << "Send WoL packet to " << name << " via " << address.toString() << ":" << port;
|
|
||||||
success = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return success;
|
// Check if we failed after all retry attempts
|
||||||
}
|
// Note: we don't need to acquire the read lock here,
|
||||||
|
// because we're on the writing thread.
|
||||||
QVector<QString> NvComputer::uniqueAddresses()
|
if (!online && m_Computer->state != NvComputer::CS_OFFLINE) {
|
||||||
{
|
qInfo() << m_Computer->name << "is now offline";
|
||||||
QVector<QString> uniqueAddressList;
|
m_Computer->state = NvComputer::CS_OFFLINE;
|
||||||
|
stateChanged = true;
|
||||||
// Start with addresses correctly ordered
|
|
||||||
uniqueAddressList.append(activeAddress);
|
|
||||||
uniqueAddressList.append(localAddress);
|
|
||||||
uniqueAddressList.append(remoteAddress);
|
|
||||||
uniqueAddressList.append(manualAddress);
|
|
||||||
|
|
||||||
// Prune duplicates (always giving precedence to the first)
|
|
||||||
for (int i = 0; i < uniqueAddressList.count(); i++) {
|
|
||||||
if (uniqueAddressList[i].isEmpty() || uniqueAddressList[i].isNull()) {
|
|
||||||
uniqueAddressList.remove(i);
|
|
||||||
i--;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
for (int j = i + 1; j < uniqueAddressList.count(); j++) {
|
|
||||||
if (uniqueAddressList[i] == uniqueAddressList[j]) {
|
|
||||||
// Always remove the later occurrence
|
|
||||||
uniqueAddressList.remove(j);
|
|
||||||
j--;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Grab the applist if it's empty or it's been long enough that we need to refresh
|
||||||
|
pollsSinceLastAppListFetch++;
|
||||||
|
if (m_Computer->state == NvComputer::CS_ONLINE &&
|
||||||
|
m_Computer->pairState == NvComputer::PS_PAIRED &&
|
||||||
|
(m_Computer->appList.isEmpty() || pollsSinceLastAppListFetch >= POLLS_PER_APPLIST_FETCH)) {
|
||||||
|
// Notify prior to the app list poll since it may take a while, and we don't
|
||||||
|
// want to delay onlining of a machine, especially if we already have a cached list.
|
||||||
|
if (stateChanged) {
|
||||||
|
emit computerStateChanged(m_Computer);
|
||||||
|
stateChanged = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updateAppList(stateChanged)) {
|
||||||
|
pollsSinceLastAppListFetch = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stateChanged) {
|
||||||
|
// Tell anyone listening that we've changed state
|
||||||
|
emit computerStateChanged(m_Computer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait a bit to poll again
|
||||||
|
QThread::sleep(3);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// We must have at least 1 address
|
signals:
|
||||||
Q_ASSERT(!uniqueAddressList.isEmpty());
|
void computerStateChanged(NvComputer* computer);
|
||||||
|
|
||||||
return uniqueAddressList;
|
private:
|
||||||
}
|
NvComputer* m_Computer;
|
||||||
|
};
|
||||||
bool NvComputer::update(NvComputer& that)
|
|
||||||
{
|
|
||||||
bool changed = false;
|
|
||||||
|
|
||||||
// Lock us for write and them for read
|
|
||||||
QWriteLocker thisLock(&this->lock);
|
|
||||||
QReadLocker thatLock(&that.lock);
|
|
||||||
|
|
||||||
// UUID may not change or we're talking to a new PC
|
|
||||||
Q_ASSERT(this->uuid == that.uuid);
|
|
||||||
|
|
||||||
#define ASSIGN_IF_CHANGED(field) \
|
|
||||||
if (this->field != that.field) { \
|
|
||||||
this->field = that.field; \
|
|
||||||
changed = true; \
|
|
||||||
}
|
|
||||||
|
|
||||||
#define ASSIGN_IF_CHANGED_AND_NONEMPTY(field) \
|
|
||||||
if (!that.field.isEmpty() && \
|
|
||||||
this->field != that.field) { \
|
|
||||||
this->field = that.field; \
|
|
||||||
changed = true; \
|
|
||||||
}
|
|
||||||
|
|
||||||
ASSIGN_IF_CHANGED(name);
|
|
||||||
ASSIGN_IF_CHANGED_AND_NONEMPTY(macAddress);
|
|
||||||
ASSIGN_IF_CHANGED_AND_NONEMPTY(localAddress);
|
|
||||||
ASSIGN_IF_CHANGED_AND_NONEMPTY(remoteAddress);
|
|
||||||
ASSIGN_IF_CHANGED_AND_NONEMPTY(manualAddress);
|
|
||||||
ASSIGN_IF_CHANGED(pairState);
|
|
||||||
ASSIGN_IF_CHANGED(serverCodecModeSupport);
|
|
||||||
ASSIGN_IF_CHANGED(currentGameId);
|
|
||||||
ASSIGN_IF_CHANGED(activeAddress);
|
|
||||||
ASSIGN_IF_CHANGED(state);
|
|
||||||
ASSIGN_IF_CHANGED(gfeVersion);
|
|
||||||
ASSIGN_IF_CHANGED(appVersion);
|
|
||||||
ASSIGN_IF_CHANGED(maxLumaPixelsHEVC);
|
|
||||||
ASSIGN_IF_CHANGED(gpuModel);
|
|
||||||
ASSIGN_IF_CHANGED_AND_NONEMPTY(appList);
|
|
||||||
ASSIGN_IF_CHANGED_AND_NONEMPTY(displayModes);
|
|
||||||
return changed;
|
|
||||||
}
|
|
||||||
|
|
||||||
ComputerManager::ComputerManager(QObject *parent)
|
ComputerManager::ComputerManager(QObject *parent)
|
||||||
: QObject(parent),
|
: QObject(parent),
|
||||||
@@ -363,17 +231,24 @@ void ComputerManager::startPolling()
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start an MDNS query for GameStream hosts
|
StreamingPreferences prefs;
|
||||||
m_MdnsBrowser = new QMdnsEngine::Browser(&m_MdnsServer, "_nvstream._tcp.local.", &m_MdnsCache);
|
|
||||||
connect(m_MdnsBrowser, &QMdnsEngine::Browser::serviceAdded,
|
|
||||||
this, [this](const QMdnsEngine::Service& service) {
|
|
||||||
qInfo() << "Discovered mDNS host:" << service.hostname();
|
|
||||||
|
|
||||||
MdnsPendingComputer* pendingComputer = new MdnsPendingComputer(&m_MdnsServer, &m_MdnsCache, service);
|
if (prefs.enableMdns) {
|
||||||
connect(pendingComputer, SIGNAL(resolvedv4(MdnsPendingComputer*,QHostAddress)),
|
// Start an MDNS query for GameStream hosts
|
||||||
this, SLOT(handleMdnsServiceResolved(MdnsPendingComputer*,QHostAddress)));
|
m_MdnsBrowser = new QMdnsEngine::Browser(&m_MdnsServer, "_nvstream._tcp.local.", &m_MdnsCache);
|
||||||
m_PendingResolution.append(pendingComputer);
|
connect(m_MdnsBrowser, &QMdnsEngine::Browser::serviceAdded,
|
||||||
});
|
this, [this](const QMdnsEngine::Service& service) {
|
||||||
|
qInfo() << "Discovered mDNS host:" << service.hostname();
|
||||||
|
|
||||||
|
MdnsPendingComputer* pendingComputer = new MdnsPendingComputer(&m_MdnsServer, &m_MdnsCache, service);
|
||||||
|
connect(pendingComputer, SIGNAL(resolvedv4(MdnsPendingComputer*,QHostAddress)),
|
||||||
|
this, SLOT(handleMdnsServiceResolved(MdnsPendingComputer*,QHostAddress)));
|
||||||
|
m_PendingResolution.append(pendingComputer);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
qWarning() << "mDNS is disabled by user preference";
|
||||||
|
}
|
||||||
|
|
||||||
// Start polling threads for each known host
|
// Start polling threads for each known host
|
||||||
QMapIterator<QString, NvComputer*> i(m_KnownHosts);
|
QMapIterator<QString, NvComputer*> i(m_KnownHosts);
|
||||||
@@ -383,6 +258,25 @@ void ComputerManager::startPolling()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Must hold m_Lock for write
|
||||||
|
void ComputerManager::startPollingComputer(NvComputer* computer)
|
||||||
|
{
|
||||||
|
if (m_PollingRef == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_PollThreads.contains(computer->uuid)) {
|
||||||
|
Q_ASSERT(m_PollThreads[computer->uuid]->isRunning());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
PcMonitorThread* thread = new PcMonitorThread(computer);
|
||||||
|
connect(thread, SIGNAL(computerStateChanged(NvComputer*)),
|
||||||
|
this, SLOT(handleComputerStateChanged(NvComputer*)));
|
||||||
|
m_PollThreads[computer->uuid] = thread;
|
||||||
|
thread->start();
|
||||||
|
}
|
||||||
|
|
||||||
void ComputerManager::handleMdnsServiceResolved(MdnsPendingComputer* computer,
|
void ComputerManager::handleMdnsServiceResolved(MdnsPendingComputer* computer,
|
||||||
const QHostAddress& address)
|
const QHostAddress& address)
|
||||||
{
|
{
|
||||||
@@ -394,6 +288,19 @@ void ComputerManager::handleMdnsServiceResolved(MdnsPendingComputer* computer,
|
|||||||
computer->deleteLater();
|
computer->deleteLater();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ComputerManager::handleComputerStateChanged(NvComputer* computer)
|
||||||
|
{
|
||||||
|
emit computerStateChanged(computer);
|
||||||
|
|
||||||
|
if (computer->pendingQuit && computer->currentGameId == 0) {
|
||||||
|
computer->pendingQuit = false;
|
||||||
|
emit quitAppCompleted(QVariant());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save updated hosts to QSettings
|
||||||
|
saveHosts();
|
||||||
|
}
|
||||||
|
|
||||||
QVector<NvComputer*> ComputerManager::getComputers()
|
QVector<NvComputer*> ComputerManager::getComputers()
|
||||||
{
|
{
|
||||||
QReadLocker lock(&m_Lock);
|
QReadLocker lock(&m_Lock);
|
||||||
@@ -410,9 +317,23 @@ public:
|
|||||||
|
|
||||||
void run()
|
void run()
|
||||||
{
|
{
|
||||||
QWriteLocker lock(&m_ComputerManager->m_Lock);
|
QThread* pollingThread;
|
||||||
|
|
||||||
QThread* pollingThread = m_ComputerManager->m_PollThreads[m_Computer->uuid];
|
// Only do the minimum amount of work while holding the writer lock.
|
||||||
|
// We must release it before calling saveHosts().
|
||||||
|
{
|
||||||
|
QWriteLocker lock(&m_ComputerManager->m_Lock);
|
||||||
|
|
||||||
|
pollingThread = m_ComputerManager->m_PollThreads[m_Computer->uuid];
|
||||||
|
|
||||||
|
m_ComputerManager->m_PollThreads.remove(m_Computer->uuid);
|
||||||
|
m_ComputerManager->m_KnownHosts.remove(m_Computer->uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Persist the new host list
|
||||||
|
m_ComputerManager->saveHosts();
|
||||||
|
|
||||||
|
// Delete the polling thread
|
||||||
if (pollingThread != nullptr) {
|
if (pollingThread != nullptr) {
|
||||||
pollingThread->requestInterruption();
|
pollingThread->requestInterruption();
|
||||||
|
|
||||||
@@ -425,9 +346,8 @@ public:
|
|||||||
delete pollingThread;
|
delete pollingThread;
|
||||||
}
|
}
|
||||||
|
|
||||||
m_ComputerManager->m_PollThreads.remove(m_Computer->uuid);
|
// Finally, delete the computer itself. This must be done
|
||||||
m_ComputerManager->m_KnownHosts.remove(m_Computer->uuid);
|
// last because the polling thread might be using it.
|
||||||
|
|
||||||
delete m_Computer;
|
delete m_Computer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -443,6 +363,55 @@ void ComputerManager::deleteHost(NvComputer* computer)
|
|||||||
QThreadPool::globalInstance()->start(new DeferredHostDeletionTask(this, computer));
|
QThreadPool::globalInstance()->start(new DeferredHostDeletionTask(this, computer));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class PendingPairingTask : public QObject, public QRunnable
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
PendingPairingTask(ComputerManager* computerManager, NvComputer* computer, QString pin)
|
||||||
|
: m_Computer(computer),
|
||||||
|
m_Pin(pin)
|
||||||
|
{
|
||||||
|
connect(this, &PendingPairingTask::pairingCompleted,
|
||||||
|
computerManager, &ComputerManager::pairingCompleted);
|
||||||
|
}
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void pairingCompleted(NvComputer* computer, QString error);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void run()
|
||||||
|
{
|
||||||
|
NvPairingManager pairingManager(m_Computer->activeAddress);
|
||||||
|
|
||||||
|
try {
|
||||||
|
NvPairingManager::PairState result = pairingManager.pair(m_Computer->appVersion, m_Pin);
|
||||||
|
switch (result)
|
||||||
|
{
|
||||||
|
case NvPairingManager::PairState::PIN_WRONG:
|
||||||
|
emit pairingCompleted(m_Computer, "The PIN from the PC didn't match. Please try again.");
|
||||||
|
break;
|
||||||
|
case NvPairingManager::PairState::FAILED:
|
||||||
|
emit pairingCompleted(m_Computer, "Pairing failed. Please try again.");
|
||||||
|
break;
|
||||||
|
case NvPairingManager::PairState::ALREADY_IN_PROGRESS:
|
||||||
|
emit pairingCompleted(m_Computer, "Another pairing attempt is already in progress.");
|
||||||
|
break;
|
||||||
|
case NvPairingManager::PairState::PAIRED:
|
||||||
|
emit pairingCompleted(m_Computer, nullptr);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (const GfeHttpResponseException& e) {
|
||||||
|
emit pairingCompleted(m_Computer, e.toQString());
|
||||||
|
} catch (const QtNetworkReplyException& e) {
|
||||||
|
emit pairingCompleted(m_Computer, e.toQString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NvComputer* m_Computer;
|
||||||
|
QString m_Pin;
|
||||||
|
};
|
||||||
|
|
||||||
void ComputerManager::pairHost(NvComputer* computer, QString pin)
|
void ComputerManager::pairHost(NvComputer* computer, QString pin)
|
||||||
{
|
{
|
||||||
// Punt to a worker thread to avoid stalling the
|
// Punt to a worker thread to avoid stalling the
|
||||||
@@ -451,6 +420,55 @@ void ComputerManager::pairHost(NvComputer* computer, QString pin)
|
|||||||
QThreadPool::globalInstance()->start(pairing);
|
QThreadPool::globalInstance()->start(pairing);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class PendingQuitTask : public QObject, public QRunnable
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
PendingQuitTask(ComputerManager* computerManager, NvComputer* computer)
|
||||||
|
: m_Computer(computer)
|
||||||
|
{
|
||||||
|
connect(this, &PendingQuitTask::quitAppFailed,
|
||||||
|
computerManager, &ComputerManager::quitAppCompleted);
|
||||||
|
}
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void quitAppFailed(QString error);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void run()
|
||||||
|
{
|
||||||
|
NvHTTP http(m_Computer->activeAddress);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (m_Computer->currentGameId != 0) {
|
||||||
|
http.quitApp();
|
||||||
|
}
|
||||||
|
} catch (const GfeHttpResponseException& e) {
|
||||||
|
{
|
||||||
|
QWriteLocker lock(&m_Computer->lock);
|
||||||
|
m_Computer->pendingQuit = false;
|
||||||
|
}
|
||||||
|
if (e.getStatusCode() == 599) {
|
||||||
|
// 599 is a special code we make a custom message for
|
||||||
|
emit quitAppFailed("The running game wasn't started by this PC. "
|
||||||
|
"You must quit the game on the host PC manually or use the device that originally started the game.");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
emit quitAppFailed(e.toQString());
|
||||||
|
}
|
||||||
|
} catch (const QtNetworkReplyException& e) {
|
||||||
|
{
|
||||||
|
QWriteLocker lock(&m_Computer->lock);
|
||||||
|
m_Computer->pendingQuit = false;
|
||||||
|
}
|
||||||
|
emit quitAppFailed(e.toQString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NvComputer* m_Computer;
|
||||||
|
};
|
||||||
|
|
||||||
void ComputerManager::quitRunningApp(NvComputer* computer)
|
void ComputerManager::quitRunningApp(NvComputer* computer)
|
||||||
{
|
{
|
||||||
QWriteLocker lock(&computer->lock);
|
QWriteLocker lock(&computer->lock);
|
||||||
@@ -498,6 +516,128 @@ void ComputerManager::stopPollingAsync()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class PendingAddTask : public QObject, public QRunnable
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
PendingAddTask(ComputerManager* computerManager, QString address, bool mdns)
|
||||||
|
: m_ComputerManager(computerManager),
|
||||||
|
m_Address(address),
|
||||||
|
m_Mdns(mdns)
|
||||||
|
{
|
||||||
|
connect(this, &PendingAddTask::computerAddCompleted,
|
||||||
|
computerManager, &ComputerManager::computerAddCompleted);
|
||||||
|
connect(this, &PendingAddTask::computerStateChanged,
|
||||||
|
computerManager, &ComputerManager::handleComputerStateChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void computerAddCompleted(QVariant success);
|
||||||
|
|
||||||
|
void computerStateChanged(NvComputer* computer);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void run()
|
||||||
|
{
|
||||||
|
NvHTTP http(m_Address);
|
||||||
|
|
||||||
|
qInfo() << "Processing new PC at" << m_Address << "from" << (m_Mdns ? "mDNS" : "user");
|
||||||
|
|
||||||
|
QString serverInfo;
|
||||||
|
try {
|
||||||
|
// There's a race condition between GameStream servers reporting presence over
|
||||||
|
// mDNS and the HTTPS server being ready to respond to our queries. To work
|
||||||
|
// around this issue, we will issue the request again after a few seconds if
|
||||||
|
// we see a ServiceUnavailableError error.
|
||||||
|
try {
|
||||||
|
serverInfo = http.getServerInfo(NvHTTP::NvLogLevel::VERBOSE);
|
||||||
|
} catch (const QtNetworkReplyException& e) {
|
||||||
|
if (e.getError() == QNetworkReply::ServiceUnavailableError) {
|
||||||
|
qWarning() << "Retrying request in 5 seconds after ServiceUnavailableError";
|
||||||
|
QThread::sleep(5);
|
||||||
|
serverInfo = http.getServerInfo(NvHTTP::NvLogLevel::VERBOSE);
|
||||||
|
qInfo() << "Retry successful";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Rethrow other errors
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (...) {
|
||||||
|
if (!m_Mdns) {
|
||||||
|
emit computerAddCompleted(false);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
NvComputer* newComputer = new NvComputer(m_Address, serverInfo);
|
||||||
|
|
||||||
|
// Update addresses depending on the context
|
||||||
|
if (m_Mdns) {
|
||||||
|
newComputer->localAddress = m_Address;
|
||||||
|
|
||||||
|
// Get the WAN IP address using STUN if we're on mDNS
|
||||||
|
quint32 addr;
|
||||||
|
int err = LiFindExternalAddressIP4("stun.stunprotocol.org", 3478, &addr);
|
||||||
|
if (err == 0) {
|
||||||
|
newComputer->remoteAddress = QHostAddress(qFromBigEndian(addr)).toString();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
qWarning() << "STUN failed to get WAN address:" << err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
newComputer->manualAddress = m_Address;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this PC already exists
|
||||||
|
QWriteLocker lock(&m_ComputerManager->m_Lock);
|
||||||
|
NvComputer* existingComputer = m_ComputerManager->m_KnownHosts[newComputer->uuid];
|
||||||
|
if (existingComputer != nullptr) {
|
||||||
|
// Fold it into the existing PC
|
||||||
|
bool changed = existingComputer->update(*newComputer);
|
||||||
|
delete newComputer;
|
||||||
|
|
||||||
|
// Drop the lock before notifying
|
||||||
|
lock.unlock();
|
||||||
|
|
||||||
|
// For non-mDNS clients, let them know it succeeded
|
||||||
|
if (!m_Mdns) {
|
||||||
|
emit computerAddCompleted(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tell our client if something changed
|
||||||
|
if (changed) {
|
||||||
|
qInfo() << existingComputer->name << "is now at" << existingComputer->activeAddress;
|
||||||
|
emit computerStateChanged(existingComputer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Store this in our active sets
|
||||||
|
m_ComputerManager->m_KnownHosts[newComputer->uuid] = newComputer;
|
||||||
|
|
||||||
|
// Start polling if enabled (write lock required)
|
||||||
|
m_ComputerManager->startPollingComputer(newComputer);
|
||||||
|
|
||||||
|
// Drop the lock before notifying
|
||||||
|
lock.unlock();
|
||||||
|
|
||||||
|
// For non-mDNS clients, let them know it succeeded
|
||||||
|
if (!m_Mdns) {
|
||||||
|
emit computerAddCompleted(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tell our client about this new PC
|
||||||
|
emit computerStateChanged(newComputer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ComputerManager* m_ComputerManager;
|
||||||
|
QString m_Address;
|
||||||
|
bool m_Mdns;
|
||||||
|
};
|
||||||
|
|
||||||
void ComputerManager::addNewHost(QString address, bool mdns)
|
void ComputerManager::addNewHost(QString address, bool mdns)
|
||||||
{
|
{
|
||||||
// Punt to a worker thread to avoid stalling the
|
// Punt to a worker thread to avoid stalling the
|
||||||
@@ -506,37 +646,4 @@ void ComputerManager::addNewHost(QString address, bool mdns)
|
|||||||
QThreadPool::globalInstance()->start(addTask);
|
QThreadPool::globalInstance()->start(addTask);
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
#include "computermanager.moc"
|
||||||
ComputerManager::handleComputerStateChanged(NvComputer* computer)
|
|
||||||
{
|
|
||||||
emit computerStateChanged(computer);
|
|
||||||
|
|
||||||
if (computer->pendingQuit && computer->currentGameId == 0) {
|
|
||||||
computer->pendingQuit = false;
|
|
||||||
emit quitAppCompleted(QVariant());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save updated hosts to QSettings
|
|
||||||
saveHosts();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Must hold m_Lock for write
|
|
||||||
void
|
|
||||||
ComputerManager::startPollingComputer(NvComputer* computer)
|
|
||||||
{
|
|
||||||
if (m_PollingRef == 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_PollThreads.contains(computer->uuid)) {
|
|
||||||
Q_ASSERT(m_PollThreads[computer->uuid]->isRunning());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
PcMonitorThread* thread = new PcMonitorThread(computer);
|
|
||||||
connect(thread, SIGNAL(computerStateChanged(NvComputer*)),
|
|
||||||
this, SLOT(handleComputerStateChanged(NvComputer*)));
|
|
||||||
m_PollThreads[computer->uuid] = thread;
|
|
||||||
thread->start();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include "nvhttp.h"
|
|
||||||
|
#include "nvcomputer.h"
|
||||||
#include "nvpairingmanager.h"
|
#include "nvpairingmanager.h"
|
||||||
|
|
||||||
#include <qmdnsengine/server.h>
|
#include <qmdnsengine/server.h>
|
||||||
@@ -13,195 +14,6 @@
|
|||||||
#include <QSettings>
|
#include <QSettings>
|
||||||
#include <QRunnable>
|
#include <QRunnable>
|
||||||
|
|
||||||
class NvComputer
|
|
||||||
{
|
|
||||||
friend class PcMonitorThread;
|
|
||||||
friend class ComputerManager;
|
|
||||||
friend class PendingQuitTask;
|
|
||||||
|
|
||||||
private:
|
|
||||||
void sortAppList();
|
|
||||||
|
|
||||||
bool pendingQuit;
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit NvComputer(QString address, QString serverInfo);
|
|
||||||
|
|
||||||
explicit NvComputer(QSettings& settings);
|
|
||||||
|
|
||||||
bool
|
|
||||||
update(NvComputer& that);
|
|
||||||
|
|
||||||
bool
|
|
||||||
wake();
|
|
||||||
|
|
||||||
QVector<QString>
|
|
||||||
uniqueAddresses();
|
|
||||||
|
|
||||||
void
|
|
||||||
serialize(QSettings& settings);
|
|
||||||
|
|
||||||
enum PairState
|
|
||||||
{
|
|
||||||
PS_UNKNOWN,
|
|
||||||
PS_PAIRED,
|
|
||||||
PS_NOT_PAIRED
|
|
||||||
};
|
|
||||||
|
|
||||||
enum ComputerState
|
|
||||||
{
|
|
||||||
CS_UNKNOWN,
|
|
||||||
CS_ONLINE,
|
|
||||||
CS_OFFLINE
|
|
||||||
};
|
|
||||||
|
|
||||||
// Ephemeral traits
|
|
||||||
ComputerState state;
|
|
||||||
PairState pairState;
|
|
||||||
QString activeAddress;
|
|
||||||
int currentGameId;
|
|
||||||
QString gfeVersion;
|
|
||||||
QString appVersion;
|
|
||||||
QVector<NvDisplayMode> displayModes;
|
|
||||||
int maxLumaPixelsHEVC;
|
|
||||||
int serverCodecModeSupport;
|
|
||||||
QString gpuModel;
|
|
||||||
|
|
||||||
// Persisted traits
|
|
||||||
QString localAddress;
|
|
||||||
QString remoteAddress;
|
|
||||||
QString manualAddress;
|
|
||||||
QByteArray macAddress;
|
|
||||||
QString name;
|
|
||||||
QString uuid;
|
|
||||||
QVector<NvApp> appList;
|
|
||||||
|
|
||||||
// Synchronization
|
|
||||||
QReadWriteLock lock;
|
|
||||||
};
|
|
||||||
|
|
||||||
// FIXME: MOC isn't finding Q_OBJECT properly when this is confined
|
|
||||||
// to computermanager.cpp as it should be.
|
|
||||||
class PcMonitorThread : public QThread
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
#define TRIES_BEFORE_OFFLINING 2
|
|
||||||
#define POLLS_PER_APPLIST_FETCH 10
|
|
||||||
|
|
||||||
public:
|
|
||||||
PcMonitorThread(NvComputer* computer)
|
|
||||||
: m_Computer(computer)
|
|
||||||
{
|
|
||||||
setObjectName("Polling thread for " + computer->name);
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
bool tryPollComputer(QString address, bool& changed)
|
|
||||||
{
|
|
||||||
NvHTTP http(address);
|
|
||||||
|
|
||||||
QString serverInfo;
|
|
||||||
try {
|
|
||||||
serverInfo = http.getServerInfo();
|
|
||||||
} catch (...) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
NvComputer newState(address, serverInfo);
|
|
||||||
|
|
||||||
// Ensure the machine that responded is the one we intended to contact
|
|
||||||
if (m_Computer->uuid != newState.uuid) {
|
|
||||||
qInfo() << "Found unexpected PC " << newState.name << " looking for " << m_Computer->name;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
changed = m_Computer->update(newState);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool updateAppList(bool& changed)
|
|
||||||
{
|
|
||||||
Q_ASSERT(m_Computer->activeAddress != nullptr);
|
|
||||||
|
|
||||||
NvHTTP http(m_Computer->activeAddress);
|
|
||||||
|
|
||||||
QVector<NvApp> appList;
|
|
||||||
|
|
||||||
try {
|
|
||||||
appList = http.getAppList();
|
|
||||||
if (appList.isEmpty()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} catch (...) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
QWriteLocker lock(&m_Computer->lock);
|
|
||||||
if (m_Computer->appList != appList) {
|
|
||||||
m_Computer->appList = appList;
|
|
||||||
m_Computer->sortAppList();
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void run() override
|
|
||||||
{
|
|
||||||
// Always fetch the applist the first time
|
|
||||||
int pollsSinceLastAppListFetch = POLLS_PER_APPLIST_FETCH;
|
|
||||||
while (!isInterruptionRequested()) {
|
|
||||||
bool stateChanged = false;
|
|
||||||
bool online = false;
|
|
||||||
for (int i = 0; i < TRIES_BEFORE_OFFLINING && !online; i++) {
|
|
||||||
for (auto& address : m_Computer->uniqueAddresses()) {
|
|
||||||
if (isInterruptionRequested()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tryPollComputer(address, stateChanged)) {
|
|
||||||
online = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if we failed after all retry attempts
|
|
||||||
// Note: we don't need to acquire the read lock here,
|
|
||||||
// because we're on the writing thread.
|
|
||||||
if (!online && m_Computer->state != NvComputer::CS_OFFLINE) {
|
|
||||||
m_Computer->state = NvComputer::CS_OFFLINE;
|
|
||||||
stateChanged = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Grab the applist if it's empty or it's been long enough that we need to refresh
|
|
||||||
pollsSinceLastAppListFetch++;
|
|
||||||
if (m_Computer->state == NvComputer::CS_ONLINE &&
|
|
||||||
m_Computer->pairState == NvComputer::PS_PAIRED &&
|
|
||||||
(m_Computer->appList.isEmpty() || pollsSinceLastAppListFetch >= POLLS_PER_APPLIST_FETCH)) {
|
|
||||||
if (updateAppList(stateChanged)) {
|
|
||||||
pollsSinceLastAppListFetch = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stateChanged) {
|
|
||||||
// Tell anyone listening that we've changed state
|
|
||||||
emit computerStateChanged(m_Computer);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait a bit to poll again
|
|
||||||
QThread::sleep(3);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void computerStateChanged(NvComputer* computer);
|
|
||||||
|
|
||||||
private:
|
|
||||||
NvComputer* m_Computer;
|
|
||||||
};
|
|
||||||
|
|
||||||
class MdnsPendingComputer : public QObject
|
class MdnsPendingComputer : public QObject
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
@@ -294,185 +106,3 @@ private:
|
|||||||
QMdnsEngine::Cache m_MdnsCache;
|
QMdnsEngine::Cache m_MdnsCache;
|
||||||
QVector<MdnsPendingComputer*> m_PendingResolution;
|
QVector<MdnsPendingComputer*> m_PendingResolution;
|
||||||
};
|
};
|
||||||
|
|
||||||
class PendingPairingTask : public QObject, public QRunnable
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
PendingPairingTask(ComputerManager* computerManager, NvComputer* computer, QString pin)
|
|
||||||
: m_Computer(computer),
|
|
||||||
m_Pin(pin)
|
|
||||||
{
|
|
||||||
connect(this, &PendingPairingTask::pairingCompleted,
|
|
||||||
computerManager, &ComputerManager::pairingCompleted);
|
|
||||||
}
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void pairingCompleted(NvComputer* computer, QString error);
|
|
||||||
|
|
||||||
private:
|
|
||||||
void run()
|
|
||||||
{
|
|
||||||
NvPairingManager pairingManager(m_Computer->activeAddress);
|
|
||||||
|
|
||||||
try {
|
|
||||||
NvPairingManager::PairState result = pairingManager.pair(m_Computer->appVersion, m_Pin);
|
|
||||||
switch (result)
|
|
||||||
{
|
|
||||||
case NvPairingManager::PairState::PIN_WRONG:
|
|
||||||
emit pairingCompleted(m_Computer, "The PIN from the PC didn't match. Please try again.");
|
|
||||||
break;
|
|
||||||
case NvPairingManager::PairState::FAILED:
|
|
||||||
emit pairingCompleted(m_Computer, "Pairing failed. Please try again.");
|
|
||||||
break;
|
|
||||||
case NvPairingManager::PairState::ALREADY_IN_PROGRESS:
|
|
||||||
emit pairingCompleted(m_Computer, "Another pairing attempt is already in progress.");
|
|
||||||
break;
|
|
||||||
case NvPairingManager::PairState::PAIRED:
|
|
||||||
emit pairingCompleted(m_Computer, nullptr);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} catch (const GfeHttpResponseException& e) {
|
|
||||||
emit pairingCompleted(m_Computer, e.toQString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
NvComputer* m_Computer;
|
|
||||||
QString m_Pin;
|
|
||||||
};
|
|
||||||
|
|
||||||
class PendingQuitTask : public QObject, public QRunnable
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
PendingQuitTask(ComputerManager* computerManager, NvComputer* computer)
|
|
||||||
: m_Computer(computer)
|
|
||||||
{
|
|
||||||
connect(this, &PendingQuitTask::quitAppFailed,
|
|
||||||
computerManager, &ComputerManager::quitAppCompleted);
|
|
||||||
}
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void quitAppFailed(QString error);
|
|
||||||
|
|
||||||
private:
|
|
||||||
void run()
|
|
||||||
{
|
|
||||||
NvHTTP http(m_Computer->activeAddress);
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (m_Computer->currentGameId != 0) {
|
|
||||||
http.quitApp();
|
|
||||||
}
|
|
||||||
} catch (const GfeHttpResponseException& e) {
|
|
||||||
{
|
|
||||||
QWriteLocker lock(&m_Computer->lock);
|
|
||||||
m_Computer->pendingQuit = false;
|
|
||||||
}
|
|
||||||
if (e.getStatusCode() == 599) {
|
|
||||||
// 599 is a special code we make a custom message for
|
|
||||||
emit quitAppFailed("The running game wasn't started by this PC. "
|
|
||||||
"You must quit the game on the host PC manually or use the device that originally started the game.");
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
emit quitAppFailed(e.toQString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
NvComputer* m_Computer;
|
|
||||||
};
|
|
||||||
|
|
||||||
class PendingAddTask : public QObject, public QRunnable
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
PendingAddTask(ComputerManager* computerManager, QString address, bool mdns)
|
|
||||||
: m_ComputerManager(computerManager),
|
|
||||||
m_Address(address),
|
|
||||||
m_Mdns(mdns)
|
|
||||||
{
|
|
||||||
connect(this, &PendingAddTask::computerAddCompleted,
|
|
||||||
computerManager, &ComputerManager::computerAddCompleted);
|
|
||||||
connect(this, &PendingAddTask::computerStateChanged,
|
|
||||||
computerManager, &ComputerManager::handleComputerStateChanged);
|
|
||||||
}
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void computerAddCompleted(QVariant success);
|
|
||||||
|
|
||||||
void computerStateChanged(NvComputer* computer);
|
|
||||||
|
|
||||||
private:
|
|
||||||
void run()
|
|
||||||
{
|
|
||||||
NvHTTP http(m_Address);
|
|
||||||
|
|
||||||
QString serverInfo;
|
|
||||||
try {
|
|
||||||
serverInfo = http.getServerInfo();
|
|
||||||
} catch (...) {
|
|
||||||
if (!m_Mdns) {
|
|
||||||
emit computerAddCompleted(false);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
NvComputer* newComputer = new NvComputer(m_Address, serverInfo);
|
|
||||||
|
|
||||||
// Update addresses depending on the context
|
|
||||||
if (m_Mdns) {
|
|
||||||
newComputer->localAddress = m_Address;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
newComputer->manualAddress = m_Address;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if this PC already exists
|
|
||||||
QWriteLocker lock(&m_ComputerManager->m_Lock);
|
|
||||||
NvComputer* existingComputer = m_ComputerManager->m_KnownHosts[newComputer->uuid];
|
|
||||||
if (existingComputer != nullptr) {
|
|
||||||
// Fold it into the existing PC
|
|
||||||
bool changed = existingComputer->update(*newComputer);
|
|
||||||
delete newComputer;
|
|
||||||
|
|
||||||
// Drop the lock before notifying
|
|
||||||
lock.unlock();
|
|
||||||
|
|
||||||
// For non-mDNS clients, let them know it succeeded
|
|
||||||
if (!m_Mdns) {
|
|
||||||
emit computerAddCompleted(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tell our client if something changed
|
|
||||||
if (changed) {
|
|
||||||
emit computerStateChanged(existingComputer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Store this in our active sets
|
|
||||||
m_ComputerManager->m_KnownHosts[newComputer->uuid] = newComputer;
|
|
||||||
|
|
||||||
// Start polling if enabled (write lock required)
|
|
||||||
m_ComputerManager->startPollingComputer(newComputer);
|
|
||||||
|
|
||||||
// Drop the lock before notifying
|
|
||||||
lock.unlock();
|
|
||||||
|
|
||||||
// For non-mDNS clients, let them know it succeeded
|
|
||||||
if (!m_Mdns) {
|
|
||||||
emit computerAddCompleted(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tell our client about this new PC
|
|
||||||
emit computerStateChanged(newComputer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ComputerManager* m_ComputerManager;
|
|
||||||
QString m_Address;
|
|
||||||
bool m_Mdns;
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -185,12 +185,12 @@ IdentityManager::getSslConfig()
|
|||||||
QString
|
QString
|
||||||
IdentityManager::getUniqueId()
|
IdentityManager::getUniqueId()
|
||||||
{
|
{
|
||||||
if (m_CachedUniqueId.isNull()) {
|
if (m_CachedUniqueId.isEmpty()) {
|
||||||
QSettings settings;
|
QSettings settings;
|
||||||
|
|
||||||
// Load the unique ID from settings
|
// Load the unique ID from settings
|
||||||
m_CachedUniqueId = settings.value(SER_UNIQUEID).toString();
|
m_CachedUniqueId = settings.value(SER_UNIQUEID).toString();
|
||||||
if (!m_CachedUniqueId.isEmpty() && !m_CachedUniqueId.isNull()) {
|
if (!m_CachedUniqueId.isEmpty()) {
|
||||||
qInfo() << "Loaded unique ID from settings:" << m_CachedUniqueId;
|
qInfo() << "Loaded unique ID from settings:" << m_CachedUniqueId;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
|||||||
@@ -0,0 +1,275 @@
|
|||||||
|
#include "nvcomputer.h"
|
||||||
|
|
||||||
|
#include <QUdpSocket>
|
||||||
|
#include <QHostInfo>
|
||||||
|
|
||||||
|
#define SER_NAME "hostname"
|
||||||
|
#define SER_UUID "uuid"
|
||||||
|
#define SER_MAC "mac"
|
||||||
|
#define SER_LOCALADDR "localaddress"
|
||||||
|
#define SER_REMOTEADDR "remoteaddress"
|
||||||
|
#define SER_MANUALADDR "manualaddress"
|
||||||
|
#define SER_APPLIST "apps"
|
||||||
|
|
||||||
|
#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->macAddress = settings.value(SER_MAC).toByteArray();
|
||||||
|
this->localAddress = settings.value(SER_LOCALADDR).toString();
|
||||||
|
this->remoteAddress = settings.value(SER_REMOTEADDR).toString();
|
||||||
|
this->manualAddress = settings.value(SER_MANUALADDR).toString();
|
||||||
|
|
||||||
|
int appCount = settings.beginReadArray(SER_APPLIST);
|
||||||
|
for (int i = 0; i < appCount; i++) {
|
||||||
|
NvApp app;
|
||||||
|
|
||||||
|
settings.setArrayIndex(i);
|
||||||
|
|
||||||
|
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();
|
||||||
|
sortAppList();
|
||||||
|
|
||||||
|
this->activeAddress = nullptr;
|
||||||
|
this->currentGameId = 0;
|
||||||
|
this->pairState = PS_UNKNOWN;
|
||||||
|
this->state = CS_UNKNOWN;
|
||||||
|
this->gfeVersion = nullptr;
|
||||||
|
this->appVersion = nullptr;
|
||||||
|
this->maxLumaPixelsHEVC = 0;
|
||||||
|
this->serverCodecModeSupport = 0;
|
||||||
|
this->pendingQuit = false;
|
||||||
|
this->gpuModel = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void NvComputer::serialize(QSettings& settings)
|
||||||
|
{
|
||||||
|
QReadLocker lock(&this->lock);
|
||||||
|
|
||||||
|
settings.setValue(SER_NAME, name);
|
||||||
|
settings.setValue(SER_UUID, uuid);
|
||||||
|
settings.setValue(SER_MAC, macAddress);
|
||||||
|
settings.setValue(SER_LOCALADDR, localAddress);
|
||||||
|
settings.setValue(SER_REMOTEADDR, remoteAddress);
|
||||||
|
settings.setValue(SER_MANUALADDR, manualAddress);
|
||||||
|
|
||||||
|
// Avoid deleting an existing applist if we couldn't get one
|
||||||
|
if (!appList.isEmpty()) {
|
||||||
|
settings.remove(SER_APPLIST);
|
||||||
|
settings.beginWriteArray(SER_APPLIST);
|
||||||
|
for (int i = 0; i < appList.count(); i++) {
|
||||||
|
settings.setArrayIndex(i);
|
||||||
|
|
||||||
|
settings.setValue(SER_APPNAME, appList[i].name);
|
||||||
|
settings.setValue(SER_APPID, appList[i].id);
|
||||||
|
settings.setValue(SER_APPHDR, appList[i].hdrSupported);
|
||||||
|
}
|
||||||
|
settings.endArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void NvComputer::sortAppList()
|
||||||
|
{
|
||||||
|
std::stable_sort(appList.begin(), appList.end(), [](const NvApp& app1, const NvApp& app2) {
|
||||||
|
return app1.name.toLower() < app2.name.toLower();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
NvComputer::NvComputer(QString address, QString serverInfo)
|
||||||
|
{
|
||||||
|
this->name = NvHTTP::getXmlString(serverInfo, "hostname");
|
||||||
|
if (this->name.isEmpty()) {
|
||||||
|
this->name = "UNKNOWN";
|
||||||
|
}
|
||||||
|
|
||||||
|
this->uuid = NvHTTP::getXmlString(serverInfo, "uniqueid");
|
||||||
|
QString newMacString = NvHTTP::getXmlString(serverInfo, "mac");
|
||||||
|
if (newMacString != "00:00:00:00:00:00") {
|
||||||
|
QStringList macOctets = newMacString.split(':');
|
||||||
|
for (QString macOctet : macOctets) {
|
||||||
|
this->macAddress.append((char) macOctet.toInt(nullptr, 16));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QString codecSupport = NvHTTP::getXmlString(serverInfo, "ServerCodecModeSupport");
|
||||||
|
if (!codecSupport.isEmpty()) {
|
||||||
|
this->serverCodecModeSupport = codecSupport.toInt();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this->serverCodecModeSupport = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString maxLumaPixelsHEVC = NvHTTP::getXmlString(serverInfo, "MaxLumaPixelsHEVC");
|
||||||
|
if (!maxLumaPixelsHEVC.isEmpty()) {
|
||||||
|
this->maxLumaPixelsHEVC = maxLumaPixelsHEVC.toInt();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this->maxLumaPixelsHEVC = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->displayModes = NvHTTP::getDisplayModeList(serverInfo);
|
||||||
|
std::stable_sort(this->displayModes.begin(), this->displayModes.end(),
|
||||||
|
[](const NvDisplayMode& mode1, const NvDisplayMode& mode2) {
|
||||||
|
return mode1.width * mode1.height * mode1.refreshRate <
|
||||||
|
mode2.width * mode2.height * mode2.refreshRate;
|
||||||
|
});
|
||||||
|
|
||||||
|
this->localAddress = NvHTTP::getXmlString(serverInfo, "LocalIP");
|
||||||
|
this->remoteAddress = NvHTTP::getXmlString(serverInfo, "ExternalIP");
|
||||||
|
this->pairState = NvHTTP::getXmlString(serverInfo, "PairStatus") == "1" ?
|
||||||
|
PS_PAIRED : PS_NOT_PAIRED;
|
||||||
|
this->currentGameId = NvHTTP::getCurrentGame(serverInfo);
|
||||||
|
this->appVersion = NvHTTP::getXmlString(serverInfo, "appversion");
|
||||||
|
this->gfeVersion = NvHTTP::getXmlString(serverInfo, "GfeVersion");
|
||||||
|
this->gpuModel = NvHTTP::getXmlString(serverInfo, "gputype");
|
||||||
|
this->activeAddress = address;
|
||||||
|
this->state = NvComputer::CS_ONLINE;
|
||||||
|
this->pendingQuit = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool NvComputer::wake()
|
||||||
|
{
|
||||||
|
if (state == NvComputer::CS_ONLINE) {
|
||||||
|
qWarning() << name << "is already online";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (macAddress.isEmpty()) {
|
||||||
|
qWarning() << name << "has no MAC address stored";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const quint16 WOL_PORTS[] = {
|
||||||
|
7, 9, // Standard WOL ports
|
||||||
|
47998, 47999, 48000, // Ports opened by GFE
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create the WoL payload
|
||||||
|
QByteArray wolPayload;
|
||||||
|
wolPayload.append(QByteArray::fromHex("FFFFFFFFFFFF"));
|
||||||
|
for (int i = 0; i < 16; i++) {
|
||||||
|
wolPayload.append(macAddress);
|
||||||
|
}
|
||||||
|
Q_ASSERT(wolPayload.count() == 102);
|
||||||
|
|
||||||
|
// Add the addresses that we know this host to be
|
||||||
|
// and broadcast addresses for this link just in
|
||||||
|
// case the host has timed out in ARP entries.
|
||||||
|
QVector<QString> addressList = uniqueAddresses();
|
||||||
|
addressList.append("255.255.255.255");
|
||||||
|
|
||||||
|
// Try all unique address strings or host names
|
||||||
|
bool success = false;
|
||||||
|
for (QString& addressString : addressList) {
|
||||||
|
QHostInfo hostInfo = QHostInfo::fromName(addressString);
|
||||||
|
|
||||||
|
if (hostInfo.error() != QHostInfo::NoError) {
|
||||||
|
qWarning() << "Error resolving" << addressString << ":" << hostInfo.errorString();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try all IP addresses that this string resolves to
|
||||||
|
for (QHostAddress& address : hostInfo.addresses()) {
|
||||||
|
QUdpSocket sock;
|
||||||
|
|
||||||
|
// Bind to any address on the correct protocol
|
||||||
|
if (sock.bind(address.protocol() == QUdpSocket::IPv4Protocol ?
|
||||||
|
QHostAddress::AnyIPv4 : QHostAddress::AnyIPv6)) {
|
||||||
|
|
||||||
|
// Send to all ports
|
||||||
|
for (quint16 port : WOL_PORTS) {
|
||||||
|
if (sock.writeDatagram(wolPayload, address, port)) {
|
||||||
|
qInfo().nospace().noquote() << "Send WoL packet to " << name << " via " << address.toString() << ":" << port;
|
||||||
|
success = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
QVector<QString> NvComputer::uniqueAddresses()
|
||||||
|
{
|
||||||
|
QVector<QString> uniqueAddressList;
|
||||||
|
|
||||||
|
// Start with addresses correctly ordered
|
||||||
|
uniqueAddressList.append(activeAddress);
|
||||||
|
uniqueAddressList.append(localAddress);
|
||||||
|
uniqueAddressList.append(remoteAddress);
|
||||||
|
uniqueAddressList.append(manualAddress);
|
||||||
|
|
||||||
|
// Prune duplicates (always giving precedence to the first)
|
||||||
|
for (int i = 0; i < uniqueAddressList.count(); i++) {
|
||||||
|
if (uniqueAddressList[i].isEmpty()) {
|
||||||
|
uniqueAddressList.remove(i);
|
||||||
|
i--;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (int j = i + 1; j < uniqueAddressList.count(); j++) {
|
||||||
|
if (uniqueAddressList[i] == uniqueAddressList[j]) {
|
||||||
|
// Always remove the later occurrence
|
||||||
|
uniqueAddressList.remove(j);
|
||||||
|
j--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We must have at least 1 address
|
||||||
|
Q_ASSERT(!uniqueAddressList.isEmpty());
|
||||||
|
|
||||||
|
return uniqueAddressList;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool NvComputer::update(NvComputer& that)
|
||||||
|
{
|
||||||
|
bool changed = false;
|
||||||
|
|
||||||
|
// Lock us for write and them for read
|
||||||
|
QWriteLocker thisLock(&this->lock);
|
||||||
|
QReadLocker thatLock(&that.lock);
|
||||||
|
|
||||||
|
// UUID may not change or we're talking to a new PC
|
||||||
|
Q_ASSERT(this->uuid == that.uuid);
|
||||||
|
|
||||||
|
#define ASSIGN_IF_CHANGED(field) \
|
||||||
|
if (this->field != that.field) { \
|
||||||
|
this->field = that.field; \
|
||||||
|
changed = true; \
|
||||||
|
}
|
||||||
|
|
||||||
|
#define ASSIGN_IF_CHANGED_AND_NONEMPTY(field) \
|
||||||
|
if (!that.field.isEmpty() && \
|
||||||
|
this->field != that.field) { \
|
||||||
|
this->field = that.field; \
|
||||||
|
changed = true; \
|
||||||
|
}
|
||||||
|
|
||||||
|
ASSIGN_IF_CHANGED(name);
|
||||||
|
ASSIGN_IF_CHANGED_AND_NONEMPTY(macAddress);
|
||||||
|
ASSIGN_IF_CHANGED_AND_NONEMPTY(localAddress);
|
||||||
|
ASSIGN_IF_CHANGED_AND_NONEMPTY(remoteAddress);
|
||||||
|
ASSIGN_IF_CHANGED_AND_NONEMPTY(manualAddress);
|
||||||
|
ASSIGN_IF_CHANGED(pairState);
|
||||||
|
ASSIGN_IF_CHANGED(serverCodecModeSupport);
|
||||||
|
ASSIGN_IF_CHANGED(currentGameId);
|
||||||
|
ASSIGN_IF_CHANGED(activeAddress);
|
||||||
|
ASSIGN_IF_CHANGED(state);
|
||||||
|
ASSIGN_IF_CHANGED(gfeVersion);
|
||||||
|
ASSIGN_IF_CHANGED(appVersion);
|
||||||
|
ASSIGN_IF_CHANGED(maxLumaPixelsHEVC);
|
||||||
|
ASSIGN_IF_CHANGED(gpuModel);
|
||||||
|
ASSIGN_IF_CHANGED_AND_NONEMPTY(appList);
|
||||||
|
ASSIGN_IF_CHANGED_AND_NONEMPTY(displayModes);
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "nvhttp.h"
|
||||||
|
|
||||||
|
#include <QThread>
|
||||||
|
#include <QReadWriteLock>
|
||||||
|
#include <QSettings>
|
||||||
|
#include <QRunnable>
|
||||||
|
|
||||||
|
class NvComputer
|
||||||
|
{
|
||||||
|
friend class PcMonitorThread;
|
||||||
|
friend class ComputerManager;
|
||||||
|
friend class PendingQuitTask;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void sortAppList();
|
||||||
|
|
||||||
|
bool pendingQuit;
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit NvComputer(QString address, QString serverInfo);
|
||||||
|
|
||||||
|
explicit NvComputer(QSettings& settings);
|
||||||
|
|
||||||
|
bool
|
||||||
|
update(NvComputer& that);
|
||||||
|
|
||||||
|
bool
|
||||||
|
wake();
|
||||||
|
|
||||||
|
QVector<QString>
|
||||||
|
uniqueAddresses();
|
||||||
|
|
||||||
|
void
|
||||||
|
serialize(QSettings& settings);
|
||||||
|
|
||||||
|
enum PairState
|
||||||
|
{
|
||||||
|
PS_UNKNOWN,
|
||||||
|
PS_PAIRED,
|
||||||
|
PS_NOT_PAIRED
|
||||||
|
};
|
||||||
|
|
||||||
|
enum ComputerState
|
||||||
|
{
|
||||||
|
CS_UNKNOWN,
|
||||||
|
CS_ONLINE,
|
||||||
|
CS_OFFLINE
|
||||||
|
};
|
||||||
|
|
||||||
|
// Ephemeral traits
|
||||||
|
ComputerState state;
|
||||||
|
PairState pairState;
|
||||||
|
QString activeAddress;
|
||||||
|
int currentGameId;
|
||||||
|
QString gfeVersion;
|
||||||
|
QString appVersion;
|
||||||
|
QVector<NvDisplayMode> displayModes;
|
||||||
|
int maxLumaPixelsHEVC;
|
||||||
|
int serverCodecModeSupport;
|
||||||
|
QString gpuModel;
|
||||||
|
|
||||||
|
// Persisted traits
|
||||||
|
QString localAddress;
|
||||||
|
QString remoteAddress;
|
||||||
|
QString manualAddress;
|
||||||
|
QByteArray macAddress;
|
||||||
|
QString name;
|
||||||
|
QString uuid;
|
||||||
|
QVector<NvApp> appList;
|
||||||
|
|
||||||
|
// Synchronization
|
||||||
|
QReadWriteLock lock;
|
||||||
|
};
|
||||||
+21
-14
@@ -29,9 +29,15 @@ NvHTTP::NvHTTP(QString address) :
|
|||||||
QVector<int>
|
QVector<int>
|
||||||
NvHTTP::parseQuad(QString quad)
|
NvHTTP::parseQuad(QString quad)
|
||||||
{
|
{
|
||||||
QStringList parts = quad.split(".");
|
|
||||||
QVector<int> ret;
|
QVector<int> ret;
|
||||||
|
|
||||||
|
// Return an empty vector for old GFE versions
|
||||||
|
// that were missing GfeVersion.
|
||||||
|
if (quad.isEmpty()) {
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
QStringList parts = quad.split(".");
|
||||||
for (int i = 0; i < 4; i++)
|
for (int i = 0; i < 4; i++)
|
||||||
{
|
{
|
||||||
ret.append(parts.at(i).toInt());
|
ret.append(parts.at(i).toInt());
|
||||||
@@ -58,7 +64,7 @@ NvHTTP::getCurrentGame(QString serverInfo)
|
|||||||
}
|
}
|
||||||
|
|
||||||
QString
|
QString
|
||||||
NvHTTP::getServerInfo()
|
NvHTTP::getServerInfo(NvLogLevel logLevel)
|
||||||
{
|
{
|
||||||
QString serverInfo;
|
QString serverInfo;
|
||||||
|
|
||||||
@@ -70,7 +76,7 @@ NvHTTP::getServerInfo()
|
|||||||
"serverinfo",
|
"serverinfo",
|
||||||
nullptr,
|
nullptr,
|
||||||
true,
|
true,
|
||||||
true);
|
logLevel);
|
||||||
// Throws if the request failed
|
// Throws if the request failed
|
||||||
verifyResponseStatus(serverInfo);
|
verifyResponseStatus(serverInfo);
|
||||||
}
|
}
|
||||||
@@ -83,7 +89,7 @@ NvHTTP::getServerInfo()
|
|||||||
"serverinfo",
|
"serverinfo",
|
||||||
nullptr,
|
nullptr,
|
||||||
true,
|
true,
|
||||||
true);
|
logLevel);
|
||||||
verifyResponseStatus(serverInfo);
|
verifyResponseStatus(serverInfo);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -189,7 +195,7 @@ NvHTTP::quitApp()
|
|||||||
|
|
||||||
// Newer GFE versions will just return success even if quitting fails
|
// Newer GFE versions will just return success even if quitting fails
|
||||||
// if we're not the original requestor.
|
// if we're not the original requestor.
|
||||||
if (getCurrentGame(getServerInfo()) != 0) {
|
if (getCurrentGame(getServerInfo(NvHTTP::ERROR)) != 0) {
|
||||||
// Generate a synthetic GfeResponseException letting the caller know
|
// Generate a synthetic GfeResponseException letting the caller know
|
||||||
// that they can't kill someone else's stream.
|
// that they can't kill someone else's stream.
|
||||||
throw GfeHttpResponseException(599, "");
|
throw GfeHttpResponseException(599, "");
|
||||||
@@ -230,7 +236,7 @@ NvHTTP::getAppList()
|
|||||||
"applist",
|
"applist",
|
||||||
nullptr,
|
nullptr,
|
||||||
true,
|
true,
|
||||||
true);
|
NvLogLevel::ERROR);
|
||||||
verifyResponseStatus(appxml);
|
verifyResponseStatus(appxml);
|
||||||
|
|
||||||
QXmlStreamReader xmlReader(appxml);
|
QXmlStreamReader xmlReader(appxml);
|
||||||
@@ -297,7 +303,8 @@ NvHTTP::getBoxArt(int appId)
|
|||||||
"appasset",
|
"appasset",
|
||||||
"appid="+QString::number(appId)+
|
"appid="+QString::number(appId)+
|
||||||
"&AssetType=2&AssetIdx=0",
|
"&AssetType=2&AssetIdx=0",
|
||||||
true);
|
true,
|
||||||
|
NvLogLevel::VERBOSE);
|
||||||
QImage image = QImageReader(reply).read();
|
QImage image = QImageReader(reply).read();
|
||||||
delete reply;
|
delete reply;
|
||||||
|
|
||||||
@@ -344,9 +351,9 @@ NvHTTP::openConnectionToString(QUrl baseUrl,
|
|||||||
QString command,
|
QString command,
|
||||||
QString arguments,
|
QString arguments,
|
||||||
bool enableTimeout,
|
bool enableTimeout,
|
||||||
bool suppressLogging)
|
NvLogLevel logLevel)
|
||||||
{
|
{
|
||||||
QNetworkReply* reply = openConnection(baseUrl, command, arguments, enableTimeout, suppressLogging);
|
QNetworkReply* reply = openConnection(baseUrl, command, arguments, enableTimeout, logLevel);
|
||||||
QString ret;
|
QString ret;
|
||||||
|
|
||||||
QTextStream stream(reply);
|
QTextStream stream(reply);
|
||||||
@@ -362,7 +369,7 @@ NvHTTP::openConnection(QUrl baseUrl,
|
|||||||
QString command,
|
QString command,
|
||||||
QString arguments,
|
QString arguments,
|
||||||
bool enableTimeout,
|
bool enableTimeout,
|
||||||
bool suppressLogging)
|
NvLogLevel logLevel)
|
||||||
{
|
{
|
||||||
// Build a URL for the request
|
// Build a URL for the request
|
||||||
QUrl url(baseUrl);
|
QUrl url(baseUrl);
|
||||||
@@ -388,7 +395,7 @@ NvHTTP::openConnection(QUrl baseUrl,
|
|||||||
{
|
{
|
||||||
QTimer::singleShot(REQUEST_TIMEOUT_MS, &loop, SLOT(quit()));
|
QTimer::singleShot(REQUEST_TIMEOUT_MS, &loop, SLOT(quit()));
|
||||||
}
|
}
|
||||||
if (!suppressLogging) {
|
if (logLevel >= NvLogLevel::VERBOSE) {
|
||||||
qInfo() << "Executing request:" << url.toString();
|
qInfo() << "Executing request:" << url.toString();
|
||||||
}
|
}
|
||||||
loop.exec(QEventLoop::ExcludeUserInputEvents);
|
loop.exec(QEventLoop::ExcludeUserInputEvents);
|
||||||
@@ -396,7 +403,7 @@ NvHTTP::openConnection(QUrl baseUrl,
|
|||||||
// Abort the request if it timed out
|
// Abort the request if it timed out
|
||||||
if (!reply->isFinished())
|
if (!reply->isFinished())
|
||||||
{
|
{
|
||||||
if (!suppressLogging) {
|
if (logLevel >= NvLogLevel::ERROR) {
|
||||||
qWarning() << "Aborting timed out request for" << url.toString();
|
qWarning() << "Aborting timed out request for" << url.toString();
|
||||||
}
|
}
|
||||||
reply->abort();
|
reply->abort();
|
||||||
@@ -409,10 +416,10 @@ NvHTTP::openConnection(QUrl baseUrl,
|
|||||||
// Handle error
|
// Handle error
|
||||||
if (reply->error() != QNetworkReply::NoError)
|
if (reply->error() != QNetworkReply::NoError)
|
||||||
{
|
{
|
||||||
if (!suppressLogging) {
|
if (logLevel >= NvLogLevel::ERROR) {
|
||||||
qWarning() << command << " request failed with error " << reply->error();
|
qWarning() << command << " request failed with error " << reply->error();
|
||||||
}
|
}
|
||||||
GfeHttpResponseException exception(reply->error(), reply->errorString());
|
QtNetworkReplyException exception(reply->error(), reply->errorString());
|
||||||
delete reply;
|
delete reply;
|
||||||
throw exception;
|
throw exception;
|
||||||
}
|
}
|
||||||
|
|||||||
+47
-5
@@ -5,7 +5,8 @@
|
|||||||
#include <Limelight.h>
|
#include <Limelight.h>
|
||||||
|
|
||||||
#include <QUrl>
|
#include <QUrl>
|
||||||
#include <QtNetwork/QNetworkAccessManager>
|
#include <QNetworkAccessManager>
|
||||||
|
#include <QNetworkReply>
|
||||||
|
|
||||||
class NvApp
|
class NvApp
|
||||||
{
|
{
|
||||||
@@ -17,7 +18,7 @@ public:
|
|||||||
|
|
||||||
bool isInitialized()
|
bool isInitialized()
|
||||||
{
|
{
|
||||||
return id != 0 && !name.isNull();
|
return id != 0 && !name.isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
int id;
|
int id;
|
||||||
@@ -77,9 +78,50 @@ private:
|
|||||||
QString m_StatusMessage;
|
QString m_StatusMessage;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
class QtNetworkReplyException : public std::exception
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
QtNetworkReplyException(QNetworkReply::NetworkError error, QString errorText) :
|
||||||
|
m_Error(error),
|
||||||
|
m_ErrorText(errorText)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* what() const throw()
|
||||||
|
{
|
||||||
|
return m_ErrorText.toLatin1();
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* getErrorText() const
|
||||||
|
{
|
||||||
|
return m_ErrorText.toLatin1();
|
||||||
|
}
|
||||||
|
|
||||||
|
QNetworkReply::NetworkError getError() const
|
||||||
|
{
|
||||||
|
return m_Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString toQString() const
|
||||||
|
{
|
||||||
|
return m_ErrorText + " (Error " + QString::number(m_Error) + ")";
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
QNetworkReply::NetworkError m_Error;
|
||||||
|
QString m_ErrorText;
|
||||||
|
};
|
||||||
|
|
||||||
class NvHTTP
|
class NvHTTP
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
enum NvLogLevel {
|
||||||
|
NONE,
|
||||||
|
ERROR,
|
||||||
|
VERBOSE
|
||||||
|
};
|
||||||
|
|
||||||
explicit NvHTTP(QString address);
|
explicit NvHTTP(QString address);
|
||||||
|
|
||||||
static
|
static
|
||||||
@@ -87,7 +129,7 @@ public:
|
|||||||
getCurrentGame(QString serverInfo);
|
getCurrentGame(QString serverInfo);
|
||||||
|
|
||||||
QString
|
QString
|
||||||
getServerInfo();
|
getServerInfo(NvLogLevel logLevel);
|
||||||
|
|
||||||
static
|
static
|
||||||
void
|
void
|
||||||
@@ -108,7 +150,7 @@ public:
|
|||||||
QString command,
|
QString command,
|
||||||
QString arguments,
|
QString arguments,
|
||||||
bool enableTimeout,
|
bool enableTimeout,
|
||||||
bool suppressLogging = false);
|
NvLogLevel logLevel = NvLogLevel::VERBOSE);
|
||||||
|
|
||||||
static
|
static
|
||||||
QVector<int>
|
QVector<int>
|
||||||
@@ -145,7 +187,7 @@ private:
|
|||||||
QString command,
|
QString command,
|
||||||
QString arguments,
|
QString arguments,
|
||||||
bool enableTimeout,
|
bool enableTimeout,
|
||||||
bool suppressLogging = false);
|
NvLogLevel logLevel);
|
||||||
|
|
||||||
QString m_Address;
|
QString m_Address;
|
||||||
QNetworkAccessManager m_Nam;
|
QNetworkAccessManager m_Nam;
|
||||||
|
|||||||
@@ -0,0 +1,362 @@
|
|||||||
|
#include "commandlineparser.h"
|
||||||
|
|
||||||
|
#include <QCommandLineParser>
|
||||||
|
#include <QRegularExpression>
|
||||||
|
|
||||||
|
#if defined(Q_OS_WIN)
|
||||||
|
#include <qt_windows.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
static bool inRange(int value, int min, int max)
|
||||||
|
{
|
||||||
|
return value >= min && value <= max;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This method returns key's value from QMap where the key is a QString.
|
||||||
|
// Key matching is case insensitive.
|
||||||
|
template <typename T>
|
||||||
|
static T mapValue(QMap<QString, T> map, QString key)
|
||||||
|
{
|
||||||
|
for(auto& item : map.toStdMap()) {
|
||||||
|
if (QString::compare(item.first, key, Qt::CaseInsensitive) == 0) {
|
||||||
|
return item.second;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return T();
|
||||||
|
}
|
||||||
|
|
||||||
|
class CommandLineParser : public QCommandLineParser
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
enum MessageType {
|
||||||
|
Info,
|
||||||
|
Error
|
||||||
|
};
|
||||||
|
|
||||||
|
void setupCommonOptions()
|
||||||
|
{
|
||||||
|
setSingleDashWordOptionMode(QCommandLineParser::ParseAsLongOptions);
|
||||||
|
addHelpOption();
|
||||||
|
addVersionOption();
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleHelpAndVersionOptions()
|
||||||
|
{
|
||||||
|
if (isSet("help")) {
|
||||||
|
showInfo(helpText());
|
||||||
|
}
|
||||||
|
if (isSet("version")) {
|
||||||
|
showVersion();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleUnknownOptions()
|
||||||
|
{
|
||||||
|
if (unknownOptionNames().length()) {
|
||||||
|
showError(QString("Unknown options: %1").arg(unknownOptionNames().join(", ")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void showMessage(QString message, MessageType type) const
|
||||||
|
{
|
||||||
|
#if defined(Q_OS_WIN)
|
||||||
|
UINT flags = MB_OK | MB_TOPMOST | MB_SETFOREGROUND;
|
||||||
|
flags |= (type == Info ? MB_ICONINFORMATION : MB_ICONERROR);
|
||||||
|
QString title = "Moonlight";
|
||||||
|
MessageBoxW(nullptr, reinterpret_cast<const wchar_t *>(message.utf16()),
|
||||||
|
reinterpret_cast<const wchar_t *>(title.utf16()), flags);
|
||||||
|
#endif
|
||||||
|
message = message.endsWith('\n') ? message : message + '\n';
|
||||||
|
fputs(qPrintable(message), type == Info ? stdout : stderr);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[ noreturn ]] void showInfo(QString message) const
|
||||||
|
{
|
||||||
|
showMessage(message, Info);
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[ noreturn ]] void showError(QString message) const
|
||||||
|
{
|
||||||
|
showMessage(message + "\n\n" + helpText(), Error);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
int getIntOption(QString name) const
|
||||||
|
{
|
||||||
|
bool ok;
|
||||||
|
int intValue = value(name).toInt(&ok);
|
||||||
|
if (!ok) {
|
||||||
|
showError(QString("Invalid %1 value: %2").arg(name, value(name)));
|
||||||
|
}
|
||||||
|
return intValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool getToggleOptionValue(QString name, bool defaultValue) const
|
||||||
|
{
|
||||||
|
QRegularExpression re(QString("^(%1|no-%1)$").arg(name));
|
||||||
|
QStringList options = optionNames().filter(re);
|
||||||
|
if (options.isEmpty()) {
|
||||||
|
return defaultValue;
|
||||||
|
} else {
|
||||||
|
return options.last() == name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QString getChoiceOptionValue(QString name) const
|
||||||
|
{
|
||||||
|
if (!m_Choices[name].contains(value(name), Qt::CaseInsensitive)) {
|
||||||
|
showError(QString("Invalid %1 choice: %2").arg(name, value(name)));
|
||||||
|
}
|
||||||
|
return value(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
QPair<int,int> getResolutionOptionValue(QString name) const
|
||||||
|
{
|
||||||
|
QRegularExpression re("^(\\d+)x(\\d+)$", QRegularExpression::CaseInsensitiveOption);
|
||||||
|
auto match = re.match(value(name));
|
||||||
|
if (!match.hasMatch()) {
|
||||||
|
showError(QString("Invalid %1 format: %2").arg(name, value(name)));
|
||||||
|
}
|
||||||
|
return qMakePair(match.captured(1).toInt(), match.captured(2).toInt());
|
||||||
|
}
|
||||||
|
|
||||||
|
void addFlagOption(QString name, QString descriptiveName)
|
||||||
|
{
|
||||||
|
addOption(QCommandLineOption(name, QString("Use %1.").arg(descriptiveName)));
|
||||||
|
}
|
||||||
|
|
||||||
|
void addToggleOption(QString name, QString descriptiveName)
|
||||||
|
{
|
||||||
|
addOption(QCommandLineOption(name, QString("Use %1.").arg(descriptiveName)));
|
||||||
|
addOption(QCommandLineOption("no-" + name, QString("Do not use %1.").arg(descriptiveName)));
|
||||||
|
}
|
||||||
|
|
||||||
|
void addValueOption(QString name, QString descriptiveName)
|
||||||
|
{
|
||||||
|
addOption(QCommandLineOption(name, QString("Specify %1 to use.").arg(descriptiveName), name));
|
||||||
|
}
|
||||||
|
|
||||||
|
void addChoiceOption(QString name, QString descriptiveName, QStringList choices)
|
||||||
|
{
|
||||||
|
addOption(QCommandLineOption(name, QString("Select %1: %2.").arg(descriptiveName, choices.join('/')), name));
|
||||||
|
m_Choices[name] = choices;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
QMap<QString, QStringList> m_Choices;
|
||||||
|
};
|
||||||
|
|
||||||
|
GlobalCommandLineParser::GlobalCommandLineParser()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
GlobalCommandLineParser::~GlobalCommandLineParser()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
GlobalCommandLineParser::ParseResult GlobalCommandLineParser::parse(const QStringList &args)
|
||||||
|
{
|
||||||
|
CommandLineParser parser;
|
||||||
|
parser.setupCommonOptions();
|
||||||
|
parser.setApplicationDescription(
|
||||||
|
"\n"
|
||||||
|
"Starts Moonlight normally if no arguments are given.\n"
|
||||||
|
"\n"
|
||||||
|
"Available actions:\n"
|
||||||
|
" stream Start streaming an app\n"
|
||||||
|
"\n"
|
||||||
|
"See 'moonlight <action> --help' for help of specific action."
|
||||||
|
);
|
||||||
|
parser.addPositionalArgument("action", "Action to execute", "<action>");
|
||||||
|
parser.parse(args);
|
||||||
|
auto posArgs = parser.positionalArguments();
|
||||||
|
QString action = posArgs.isEmpty() ? QString() : posArgs.first().toLower();
|
||||||
|
|
||||||
|
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 (action == "stream") {
|
||||||
|
return StreamRequested;
|
||||||
|
} else {
|
||||||
|
parser.showError(QString("Invalid action: %1").arg(action));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StreamCommandLineParser::StreamCommandLineParser()
|
||||||
|
{
|
||||||
|
m_WindowModeMap = {
|
||||||
|
{"fullscreen", StreamingPreferences::WM_FULLSCREEN},
|
||||||
|
{"windowed", StreamingPreferences::WM_WINDOWED},
|
||||||
|
{"borderless", StreamingPreferences::WM_FULLSCREEN_DESKTOP},
|
||||||
|
};
|
||||||
|
m_AudioConfigMap = {
|
||||||
|
{"stereo", StreamingPreferences::AC_STEREO},
|
||||||
|
{"5.1-surround", StreamingPreferences::AC_51_SURROUND},
|
||||||
|
};
|
||||||
|
m_VideoCodecMap = {
|
||||||
|
{"auto", StreamingPreferences::VCC_AUTO},
|
||||||
|
{"H.264", StreamingPreferences::VCC_FORCE_H264},
|
||||||
|
{"HEVC", StreamingPreferences::VCC_FORCE_HEVC},
|
||||||
|
};
|
||||||
|
m_VideoDecoderMap = {
|
||||||
|
{"auto", StreamingPreferences::VDS_AUTO},
|
||||||
|
{"software", StreamingPreferences::VDS_FORCE_HARDWARE},
|
||||||
|
{"hardware", StreamingPreferences::VDS_FORCE_SOFTWARE},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
StreamCommandLineParser::~StreamCommandLineParser()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void StreamCommandLineParser::parse(const QStringList &args, StreamingPreferences *preferences)
|
||||||
|
{
|
||||||
|
CommandLineParser parser;
|
||||||
|
parser.setupCommonOptions();
|
||||||
|
parser.setApplicationDescription(
|
||||||
|
"\n"
|
||||||
|
"Starts directly streaming a given app."
|
||||||
|
);
|
||||||
|
parser.addPositionalArgument("stream", "Start stream");
|
||||||
|
|
||||||
|
// Add other arguments and options
|
||||||
|
parser.addPositionalArgument("host", "Host computer name, UUID, or IP address", "<host>");
|
||||||
|
parser.addPositionalArgument("app", "App to stream", "\"<app>\"");
|
||||||
|
|
||||||
|
parser.addFlagOption("720", "1280x720 resolution");
|
||||||
|
parser.addFlagOption("1080", "1920x1080 resolution");
|
||||||
|
parser.addFlagOption("1440", "2560x1440 resolution");
|
||||||
|
parser.addFlagOption("4K", "3840x2160 resolution");
|
||||||
|
parser.addValueOption("resolution", "custom <width>x<height> resolution");
|
||||||
|
parser.addToggleOption("vsync", "V-Sync");
|
||||||
|
parser.addValueOption("fps", "FPS");
|
||||||
|
parser.addValueOption("bitrate", "bitrate in Kbps");
|
||||||
|
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("mouse-acceleration", "mouse acceleration");
|
||||||
|
parser.addToggleOption("game-optimization", "game optimizations");
|
||||||
|
parser.addToggleOption("audio-on-host", "audio on host PC");
|
||||||
|
parser.addChoiceOption("video-codec", "video codec", m_VideoCodecMap.keys());
|
||||||
|
parser.addChoiceOption("video-decoder", "video decoder", m_VideoDecoderMap.keys());
|
||||||
|
|
||||||
|
if (!parser.parse(args)) {
|
||||||
|
parser.showError(parser.errorText());
|
||||||
|
}
|
||||||
|
|
||||||
|
parser.handleUnknownOptions();
|
||||||
|
|
||||||
|
// Resolve display's width and height
|
||||||
|
QRegularExpression resolutionRexExp("^(720|1080|1440|4K|resolution)$");
|
||||||
|
QStringList resoOptions = parser.optionNames().filter(resolutionRexExp);
|
||||||
|
bool displaySet = resoOptions.length();
|
||||||
|
if (displaySet) {
|
||||||
|
QString name = resoOptions.last();
|
||||||
|
if (name == "720") {
|
||||||
|
preferences->width = 1280;
|
||||||
|
preferences->height = 720;
|
||||||
|
displaySet = true;
|
||||||
|
} else if (name == "1080") {
|
||||||
|
preferences->width = 1920;
|
||||||
|
preferences->height = 1080;
|
||||||
|
displaySet = true;
|
||||||
|
} else if (name == "1440") {
|
||||||
|
preferences->width = 2560;
|
||||||
|
preferences->height = 1440;
|
||||||
|
displaySet = true;
|
||||||
|
} else if (name == "4K") {
|
||||||
|
preferences->width = 3840;
|
||||||
|
preferences->height = 2160;
|
||||||
|
displaySet = true;
|
||||||
|
} else if (name == "resolution") {
|
||||||
|
auto resolution = parser.getResolutionOptionValue(name);
|
||||||
|
preferences->width = resolution.first;
|
||||||
|
preferences->height = resolution.second;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve --fps option
|
||||||
|
if (parser.isSet("fps")) {
|
||||||
|
preferences->fps = parser.getIntOption("fps");
|
||||||
|
if (!inRange(preferences->fps, 30, 120)) {
|
||||||
|
parser.showError("FPS must be in range: 30 - 120");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve --bitrate option
|
||||||
|
if (parser.isSet("bitrate")) {
|
||||||
|
preferences->bitrateKbps = parser.getIntOption("bitrate");
|
||||||
|
if (!inRange(preferences->bitrateKbps, 500, 150000)) {
|
||||||
|
parser.showError("Bitrate must be in range: 500 - 150000");
|
||||||
|
}
|
||||||
|
} else if (displaySet || parser.isSet("fps")) {
|
||||||
|
preferences->bitrateKbps = preferences->getDefaultBitrate(
|
||||||
|
preferences->width, preferences->height, preferences->fps);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve --display option
|
||||||
|
if (parser.isSet("display-mode")) {
|
||||||
|
preferences->windowMode = mapValue(m_WindowModeMap, parser.getChoiceOptionValue("display-mode"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve --vsync and --no-vsync options
|
||||||
|
preferences->enableVsync = parser.getToggleOptionValue("vsync", preferences->enableVsync);
|
||||||
|
|
||||||
|
// Resolve --audio-config option
|
||||||
|
if (parser.isSet("audio-config")) {
|
||||||
|
preferences->audioConfig = mapValue(m_AudioConfigMap, parser.getChoiceOptionValue("audio-config"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve --multi-controller and --no-multi-controller options
|
||||||
|
preferences->multiController = parser.getToggleOptionValue("multi-controller", preferences->multiController);
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
// Resolve --audio-on-host and --no-audio-on-host options
|
||||||
|
preferences->playAudioOnHost = parser.getToggleOptionValue("audio-on-host", preferences->playAudioOnHost);
|
||||||
|
|
||||||
|
// Resolve --video-codec option
|
||||||
|
if (parser.isSet("video-codec")) {
|
||||||
|
preferences->videoCodecConfig = mapValue(m_VideoCodecMap, parser.getChoiceOptionValue("video-codec"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve --video-decoder option
|
||||||
|
if (parser.isSet("video-decoder")) {
|
||||||
|
preferences->videoDecoderSelection = mapValue(m_VideoDecoderMap, parser.getChoiceOptionValue("video-decoder"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// This method will not return and terminates the process if --version or
|
||||||
|
// --help is specified
|
||||||
|
parser.handleHelpAndVersionOptions();
|
||||||
|
|
||||||
|
// Verify that both host and app has been provided
|
||||||
|
auto posArgs = parser.positionalArguments();
|
||||||
|
if (posArgs.length() < 2) {
|
||||||
|
parser.showError("Host not provided");
|
||||||
|
}
|
||||||
|
m_Host = parser.positionalArguments().at(1);
|
||||||
|
|
||||||
|
if (posArgs.length() < 3) {
|
||||||
|
parser.showError("App not provided");
|
||||||
|
}
|
||||||
|
m_AppName = parser.positionalArguments().at(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString StreamCommandLineParser::getHost() const
|
||||||
|
{
|
||||||
|
return m_Host;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString StreamCommandLineParser::getAppName() const
|
||||||
|
{
|
||||||
|
return m_AppName;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "settings/streamingpreferences.h"
|
||||||
|
|
||||||
|
#include <QMap>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
class GlobalCommandLineParser
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
enum ParseResult {
|
||||||
|
NormalStartRequested,
|
||||||
|
StreamRequested,
|
||||||
|
};
|
||||||
|
|
||||||
|
GlobalCommandLineParser();
|
||||||
|
virtual ~GlobalCommandLineParser();
|
||||||
|
|
||||||
|
ParseResult parse(const QStringList &args);
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
class StreamCommandLineParser
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
StreamCommandLineParser();
|
||||||
|
virtual ~StreamCommandLineParser();
|
||||||
|
|
||||||
|
void parse(const QStringList &args, StreamingPreferences *preferences);
|
||||||
|
|
||||||
|
QString getHost() const;
|
||||||
|
QString getAppName() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
QString m_Host;
|
||||||
|
QString m_AppName;
|
||||||
|
QMap<QString, StreamingPreferences::WindowMode> m_WindowModeMap;
|
||||||
|
QMap<QString, StreamingPreferences::AudioConfig> m_AudioConfigMap;
|
||||||
|
QMap<QString, StreamingPreferences::VideoCodecConfig> m_VideoCodecMap;
|
||||||
|
QMap<QString, StreamingPreferences::VideoDecoderSelection> m_VideoDecoderMap;
|
||||||
|
};
|
||||||
@@ -0,0 +1,220 @@
|
|||||||
|
#include "startstream.h"
|
||||||
|
#include "backend/computermanager.h"
|
||||||
|
#include "streaming/session.h"
|
||||||
|
|
||||||
|
#include <QTimer>
|
||||||
|
|
||||||
|
#define COMPUTER_SEEK_TIMEOUT 10000
|
||||||
|
#define APP_SEEK_TIMEOUT 10000
|
||||||
|
|
||||||
|
namespace CliStartStream
|
||||||
|
{
|
||||||
|
|
||||||
|
enum State {
|
||||||
|
StateInit,
|
||||||
|
StateSeekComputer,
|
||||||
|
StateSeekApp,
|
||||||
|
StateStartSession,
|
||||||
|
StateFailure,
|
||||||
|
};
|
||||||
|
|
||||||
|
class Event
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
enum Type {
|
||||||
|
ComputerUpdated,
|
||||||
|
Executed,
|
||||||
|
Timedout,
|
||||||
|
};
|
||||||
|
|
||||||
|
Event(Type type)
|
||||||
|
: type(type), computerManager(nullptr), computer(nullptr) {}
|
||||||
|
|
||||||
|
Type type;
|
||||||
|
ComputerManager *computerManager;
|
||||||
|
NvComputer *computer;
|
||||||
|
};
|
||||||
|
|
||||||
|
class LauncherPrivate
|
||||||
|
{
|
||||||
|
Q_DECLARE_PUBLIC(Launcher)
|
||||||
|
|
||||||
|
public:
|
||||||
|
LauncherPrivate(Launcher *q) : q_ptr(q) {}
|
||||||
|
|
||||||
|
void handleEvent(Event event)
|
||||||
|
{
|
||||||
|
Q_Q(Launcher);
|
||||||
|
Session* session;
|
||||||
|
NvApp app;
|
||||||
|
|
||||||
|
switch (event.type) {
|
||||||
|
case Event::Executed:
|
||||||
|
if (m_State == StateInit) {
|
||||||
|
m_State = StateSeekComputer;
|
||||||
|
m_TimeoutTimer->start(COMPUTER_SEEK_TIMEOUT);
|
||||||
|
m_ComputerManager = event.computerManager;
|
||||||
|
q->connect(m_ComputerManager, &ComputerManager::computerStateChanged,
|
||||||
|
q, &Launcher::onComputerUpdated);
|
||||||
|
// Seek desired computer by both connecting to it directly (this may fail
|
||||||
|
// if m_ComputerName is UUID, or the name that doesn't resolve to an IP
|
||||||
|
// address) and by polling it using mDNS, hopefully one of these methods
|
||||||
|
// would find the host
|
||||||
|
m_ComputerManager->addNewHost(m_ComputerName, false);
|
||||||
|
m_ComputerManager->startPolling();
|
||||||
|
emit q->searchingComputer();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Event::ComputerUpdated:
|
||||||
|
if (m_State == StateSeekComputer) {
|
||||||
|
if (matchComputer(event.computer) && isOnline(event.computer)) {
|
||||||
|
if (isPaired(event.computer)) {
|
||||||
|
m_State = StateSeekApp;
|
||||||
|
m_TimeoutTimer->start(APP_SEEK_TIMEOUT);
|
||||||
|
m_Computer = event.computer;
|
||||||
|
m_ComputerManager->stopPollingAsync();
|
||||||
|
emit q->searchingApp();
|
||||||
|
} else {
|
||||||
|
m_State = StateFailure;
|
||||||
|
QString msg = QString("Computer %1 has not been paired. "
|
||||||
|
"Please open Moonlight to pair before streaming.")
|
||||||
|
.arg(event.computer->name);
|
||||||
|
emit q->failed(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (m_State == StateSeekApp) {
|
||||||
|
int index = getAppIndex();
|
||||||
|
if (-1 != index) {
|
||||||
|
app = m_Computer->appList[index];
|
||||||
|
if (isNotStreaming() || isStreamingApp(app)) {
|
||||||
|
m_State = StateStartSession;
|
||||||
|
m_TimeoutTimer->stop();
|
||||||
|
session = new Session(m_Computer, app, m_Preferences);
|
||||||
|
emit q->sessionCreated(app.name, session);
|
||||||
|
} else {
|
||||||
|
m_State = StateFailure;
|
||||||
|
QString msg = QString("%1 is already running. Please quit %1 to stream %2.")
|
||||||
|
.arg(getCurrentAppName(), app.name);
|
||||||
|
emit q->failed(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Event::Timedout:
|
||||||
|
if (m_State == StateSeekComputer) {
|
||||||
|
m_State = StateFailure;
|
||||||
|
emit q->failed(QString("Failed to connect to %1").arg(m_ComputerName));
|
||||||
|
}
|
||||||
|
if (m_State == StateSeekApp) {
|
||||||
|
m_State = StateFailure;
|
||||||
|
emit q->failed(QString("Failed to find application %1").arg(m_AppName));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool matchComputer(NvComputer *computer) const
|
||||||
|
{
|
||||||
|
QString value = m_ComputerName.toLower();
|
||||||
|
return computer->name.toLower() == value ||
|
||||||
|
computer->localAddress.toLower() == value ||
|
||||||
|
computer->remoteAddress.toLower() == value ||
|
||||||
|
computer->manualAddress.toLower() == value ||
|
||||||
|
computer->uuid.toLower() == value;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isOnline(NvComputer *computer) const
|
||||||
|
{
|
||||||
|
return computer->state == NvComputer::CS_ONLINE;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isPaired(NvComputer *computer) const
|
||||||
|
{
|
||||||
|
return computer->pairState == NvComputer::PS_PAIRED;
|
||||||
|
}
|
||||||
|
|
||||||
|
int getAppIndex() const
|
||||||
|
{
|
||||||
|
for (int i = 0; i < m_Computer->appList.length(); i++) {
|
||||||
|
if (m_Computer->appList[i].name.toLower() == m_AppName.toLower()) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isNotStreaming() const
|
||||||
|
{
|
||||||
|
return m_Computer->currentGameId == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isStreamingApp(NvApp app) const
|
||||||
|
{
|
||||||
|
return m_Computer->currentGameId == app.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString getCurrentAppName() const
|
||||||
|
{
|
||||||
|
for (NvApp app : m_Computer->appList) {
|
||||||
|
if (m_Computer->currentGameId == app.id) {
|
||||||
|
return app.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "<UNKNOWN>";
|
||||||
|
}
|
||||||
|
|
||||||
|
Launcher *q_ptr;
|
||||||
|
QString m_ComputerName;
|
||||||
|
QString m_AppName;
|
||||||
|
StreamingPreferences *m_Preferences;
|
||||||
|
ComputerManager *m_ComputerManager;
|
||||||
|
NvComputer *m_Computer;
|
||||||
|
State m_State;
|
||||||
|
QTimer *m_TimeoutTimer;
|
||||||
|
};
|
||||||
|
|
||||||
|
Launcher::Launcher(QString computer, QString app,
|
||||||
|
StreamingPreferences* preferences, QObject *parent)
|
||||||
|
: QObject(parent),
|
||||||
|
m_DPtr(new LauncherPrivate(this))
|
||||||
|
{
|
||||||
|
Q_D(Launcher);
|
||||||
|
d->m_ComputerName = computer;
|
||||||
|
d->m_AppName = app;
|
||||||
|
d->m_Preferences = preferences;
|
||||||
|
d->m_State = StateInit;
|
||||||
|
d->m_TimeoutTimer = new QTimer(this);
|
||||||
|
d->m_TimeoutTimer->setSingleShot(true);
|
||||||
|
connect(d->m_TimeoutTimer, &QTimer::timeout,
|
||||||
|
this, &Launcher::onTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
Launcher::~Launcher()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void Launcher::execute(ComputerManager *manager)
|
||||||
|
{
|
||||||
|
Q_D(Launcher);
|
||||||
|
Event event(Event::Executed);
|
||||||
|
event.computerManager = manager;
|
||||||
|
d->handleEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Launcher::onComputerUpdated(NvComputer *computer)
|
||||||
|
{
|
||||||
|
Q_D(Launcher);
|
||||||
|
Event event(Event::ComputerUpdated);
|
||||||
|
event.computer = computer;
|
||||||
|
d->handleEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Launcher::onTimeout()
|
||||||
|
{
|
||||||
|
Q_D(Launcher);
|
||||||
|
Event event(Event::Timedout);
|
||||||
|
d->handleEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
|
||||||
|
class ComputerManager;
|
||||||
|
class NvComputer;
|
||||||
|
class Session;
|
||||||
|
class StreamingPreferences;
|
||||||
|
|
||||||
|
namespace CliStartStream
|
||||||
|
{
|
||||||
|
|
||||||
|
class Event;
|
||||||
|
class LauncherPrivate;
|
||||||
|
|
||||||
|
class Launcher : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
Q_DECLARE_PRIVATE_D(m_DPtr, Launcher)
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit Launcher(QString computer, QString app,
|
||||||
|
StreamingPreferences* preferences,
|
||||||
|
QObject *parent = nullptr);
|
||||||
|
~Launcher();
|
||||||
|
Q_INVOKABLE void execute(ComputerManager *manager);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void searchingComputer();
|
||||||
|
void searchingApp();
|
||||||
|
void sessionCreated(QString appName, Session *session);
|
||||||
|
void failed(QString text);
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void onComputerUpdated(NvComputer *computer);
|
||||||
|
void onTimeout();
|
||||||
|
|
||||||
|
private:
|
||||||
|
QScopedPointer<LauncherPrivate> m_DPtr;
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
<p>Features include:</p>
|
<p>Features include:</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>Streaming at up to 4K resolution</li>
|
<li>Streaming at up to 4K resolution</li>
|
||||||
<li>Support for up to 120 FPS streaming on high refresh rate monitors</li>
|
<li>Support for up to 120 FPS streaming (high refresh rate monitor recommended)</li>
|
||||||
<li>Hardware accelerated video decoding with VAAPI and VDPAU support</li>
|
<li>Hardware accelerated video decoding with VAAPI and VDPAU support</li>
|
||||||
<li>5.1 surround sound audio support</li>
|
<li>5.1 surround sound audio support</li>
|
||||||
<li>HEVC support for better video compression efficiency</li>
|
<li>HEVC support for better video compression efficiency</li>
|
||||||
@@ -33,6 +33,94 @@
|
|||||||
</screenshots>
|
</screenshots>
|
||||||
|
|
||||||
<releases>
|
<releases>
|
||||||
|
<release version="0.6.2" date="2018-10-28">
|
||||||
|
<description>
|
||||||
|
<p>New features:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Added automatic IP address detection for Internet streaming</li>
|
||||||
|
<li>Added a quit shortcut tip for gamepad users</li>
|
||||||
|
</ul>
|
||||||
|
<p>Bugfixes:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Fixed server state polling not being stopped while streaming</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release version="0.6.1" date="2018-10-14">
|
||||||
|
<description>
|
||||||
|
<p>New features:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Added support for quitting Moonlight via gamepad</li>
|
||||||
|
<li>Added tooltips for games with very long names</li>
|
||||||
|
</ul>
|
||||||
|
<p>Bugfixes:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Added a workaround for a memory leak in the VAAPI driver for AMD GPUs</li>
|
||||||
|
<li>Fixed combo boxes on the settings page being too small for certain DPI scaling</li>
|
||||||
|
<li>Reduced power usage when Moonlight is idling in the background for a while</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release version="0.6.0" date="2018-10-06">
|
||||||
|
<description>
|
||||||
|
<p>New features:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Added keyboard and gamepad UI navigation</li>
|
||||||
|
<li>Added support for mouse X1 and X2 buttons (back and forward)</li>
|
||||||
|
<li>Added support for mouse movement with touchscreens</li>
|
||||||
|
<li>Improved HEVC performance at high bitrates</li>
|
||||||
|
<li>Added support for launching streaming sessions directly from a terminal or script</li>
|
||||||
|
<li>Added quit shortcut for gamepads: Select+Start+L1+R1</li>
|
||||||
|
</ul>
|
||||||
|
<p>Bugfixes:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Fixed not saving deletion of PCs</li>
|
||||||
|
<li>Fixed keys being stuck down on the host PC after Moonlight loses focus or quits</li>
|
||||||
|
<li>Fixed some warning dialogs being stuck behind the UI window</li>
|
||||||
|
<li>Updated included gamepad mapping list</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release version="0.5.0" date="2018-09-15">
|
||||||
|
<description>
|
||||||
|
<p>New features:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Added option to enable mouse acceleration for remote desktop usage</li>
|
||||||
|
<li>Added option to disable automatic PC discovery</li>
|
||||||
|
<li>Added gamepad mappings from gabomdq's SDL_GameControllerDB</li>
|
||||||
|
<li>Added help links to error dialogs</li>
|
||||||
|
<li>Added quit shortcut tip to stream loading page</li>
|
||||||
|
<li>Restored the autodetect audio configuration option</li>
|
||||||
|
</ul>
|
||||||
|
<p>Bugfixes:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Fixed quit app dialog starting the wrong app</li>
|
||||||
|
<li>Fixed very high input lag on GFE 3.14.0 and earlier</li>
|
||||||
|
<li>Fixed sending errant mouse click when capturing the mouse</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release version="0.4.0" date="2018-09-08">
|
||||||
|
<description>
|
||||||
|
<p>New features:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Added option to enable (unsupported) 90/120 FPS streaming on 60 Hz monitors</li>
|
||||||
|
<li>Added refresh rate matching for better frame pacing on high refresh rate monitors</li>
|
||||||
|
<li>The streaming window now appears on the same monitor the UI was displayed on</li>
|
||||||
|
<li>The streaming window is now centered on the target monitor</li>
|
||||||
|
<li>Display a dialog when the connection is terminated by the remote PC</li>
|
||||||
|
<li>Added option to select between full-screen exclusive and borderless windowed mode</li>
|
||||||
|
</ul>
|
||||||
|
<p>Bugfixes:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Fixed streaming with no audio device</li>
|
||||||
|
<li>Fixed mouse lag with some high polling rate mice</li>
|
||||||
|
<li>Improved UI performance on the app grid page</li>
|
||||||
|
<li>1440p and 4K 120 FPS performance is improved to avoid encoder bottlenecks</li>
|
||||||
|
<li>Autodetect audio configuration option removed because it was broken</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
<release version="0.3.0" date="2018-08-21">
|
<release version="0.3.0" date="2018-08-21">
|
||||||
<description>
|
<description>
|
||||||
<p>Bugfixes:</p>
|
<p>Bugfixes:</p>
|
||||||
|
|||||||
+81
-49
@@ -1,27 +1,24 @@
|
|||||||
import QtQuick 2.9
|
import QtQuick 2.9
|
||||||
import QtQuick.Dialogs 1.3
|
import QtQuick.Dialogs 1.2
|
||||||
import QtQuick.Controls 2.2
|
import QtQuick.Controls 2.2
|
||||||
|
|
||||||
import AppModel 1.0
|
import AppModel 1.0
|
||||||
|
|
||||||
import ComputerManager 1.0
|
import ComputerManager 1.0
|
||||||
|
import SdlGamepadKeyNavigation 1.0
|
||||||
|
|
||||||
GridView {
|
GridView {
|
||||||
property int computerIndex
|
property int computerIndex
|
||||||
property AppModel appModel : createModel()
|
property AppModel appModel : createModel()
|
||||||
|
|
||||||
id: appGrid
|
id: appGrid
|
||||||
|
focus: true
|
||||||
|
activeFocusOnTab: true
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.leftMargin: (parent.width % (cellWidth + anchors.rightMargin)) / 2
|
anchors.leftMargin: (parent.width % (cellWidth + anchors.rightMargin)) / 2
|
||||||
anchors.topMargin: 20
|
anchors.topMargin: 20
|
||||||
anchors.rightMargin: 5
|
anchors.rightMargin: 5
|
||||||
anchors.bottomMargin: 5
|
anchors.bottomMargin: 5
|
||||||
cellWidth: 225; cellHeight: 350;
|
cellWidth: 225; cellHeight: 385;
|
||||||
focus: true
|
|
||||||
|
|
||||||
// Cache delegates for 1000px in both directions to improve
|
|
||||||
// scrolling and resizing performance
|
|
||||||
cacheBuffer: 1000
|
|
||||||
|
|
||||||
// The StackView will trigger a visibility change when
|
// The StackView will trigger a visibility change when
|
||||||
// we're pushed onto it, causing our onVisibleChanged
|
// we're pushed onto it, causing our onVisibleChanged
|
||||||
@@ -34,12 +31,23 @@ GridView {
|
|||||||
stackView.pop()
|
stackView.pop()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
// Don't show any highlighted item until interacting with them
|
||||||
|
currentIndex = -1
|
||||||
|
}
|
||||||
|
|
||||||
|
SdlGamepadKeyNavigation {
|
||||||
|
id: gamepadKeyNav
|
||||||
|
}
|
||||||
|
|
||||||
onVisibleChanged: {
|
onVisibleChanged: {
|
||||||
if (visible) {
|
if (visible) {
|
||||||
appModel.computerLost.connect(computerLost)
|
appModel.computerLost.connect(computerLost)
|
||||||
|
gamepadKeyNav.enable()
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
appModel.computerLost.disconnect(computerLost)
|
appModel.computerLost.disconnect(computerLost)
|
||||||
|
gamepadKeyNav.disable()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,17 +60,22 @@ GridView {
|
|||||||
|
|
||||||
model: appModel
|
model: appModel
|
||||||
|
|
||||||
delegate: Item {
|
delegate: NavigableItemDelegate {
|
||||||
width: 200; height: 300;
|
width: 200; height: 335;
|
||||||
|
grid: appGrid
|
||||||
|
|
||||||
Image {
|
Image {
|
||||||
id: appIcon
|
id: appIcon
|
||||||
anchors.horizontalCenter: parent.horizontalCenter;
|
anchors.horizontalCenter: parent.horizontalCenter;
|
||||||
|
y: 20
|
||||||
source: model.boxart
|
source: model.boxart
|
||||||
sourceSize {
|
sourceSize {
|
||||||
width: 150
|
width: 150
|
||||||
height: 200
|
height: 200
|
||||||
}
|
}
|
||||||
|
width: sourceSize.width
|
||||||
|
height: sourceSize.height
|
||||||
|
fillMode: Image.Pad
|
||||||
}
|
}
|
||||||
|
|
||||||
Image {
|
Image {
|
||||||
@@ -87,6 +100,12 @@ GridView {
|
|||||||
horizontalAlignment: Text.AlignHCenter
|
horizontalAlignment: Text.AlignHCenter
|
||||||
wrapMode: Text.Wrap
|
wrapMode: Text.Wrap
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
|
|
||||||
|
// Display a tooltip with the full name if it's truncated
|
||||||
|
ToolTip.text: model.name
|
||||||
|
ToolTip.delay: 1000
|
||||||
|
ToolTip.timeout: 5000
|
||||||
|
ToolTip.visible: (parent.hovered || parent.highlighted) && truncated
|
||||||
}
|
}
|
||||||
|
|
||||||
function launchOrResumeSelectedApp()
|
function launchOrResumeSelectedApp()
|
||||||
@@ -95,6 +114,8 @@ GridView {
|
|||||||
if (runningIndex >= 0 && runningIndex !== index) {
|
if (runningIndex >= 0 && runningIndex !== index) {
|
||||||
quitAppDialog.appName = appModel.getRunningAppName()
|
quitAppDialog.appName = appModel.getRunningAppName()
|
||||||
quitAppDialog.segueToStream = true
|
quitAppDialog.segueToStream = true
|
||||||
|
quitAppDialog.nextAppName = model.name
|
||||||
|
quitAppDialog.nextAppIndex = index
|
||||||
quitAppDialog.open()
|
quitAppDialog.open()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -104,60 +125,36 @@ GridView {
|
|||||||
stackView.push(segue)
|
stackView.push(segue)
|
||||||
}
|
}
|
||||||
|
|
||||||
MessageDialog {
|
onClicked: {
|
||||||
id: quitAppDialog
|
// Nothing is running or this app is running
|
||||||
modality:Qt.WindowModal
|
launchOrResumeSelectedApp()
|
||||||
property string appName : "";
|
|
||||||
property bool segueToStream : false
|
|
||||||
text:"Are you sure you want to quit " + appName +"? Any unsaved progress will be lost."
|
|
||||||
standardButtons: StandardButton.Yes | StandardButton.No
|
|
||||||
onYes: {
|
|
||||||
var component = Qt.createComponent("QuitSegue.qml")
|
|
||||||
var params = {"appName": appName}
|
|
||||||
if (segueToStream) {
|
|
||||||
// Store the session and app name if we're going to stream after
|
|
||||||
// successfully quitting the old app.
|
|
||||||
params.nextAppName = model.name
|
|
||||||
params.nextSession = appModel.createSessionForApp(index)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
params.nextAppName = null
|
|
||||||
params.nextSession = null
|
|
||||||
}
|
|
||||||
|
|
||||||
stackView.push(component.createObject(stackView, params))
|
|
||||||
|
|
||||||
// Trigger the quit after pushing the quit segue on screen
|
|
||||||
appModel.quitRunningApp()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
acceptedButtons: Qt.RightButton
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if (mouse.button === Qt.LeftButton) {
|
// popup() ensures the menu appears under the mouse cursor
|
||||||
// Nothing is running or this app is running
|
appContextMenu.popup()
|
||||||
launchOrResumeSelectedApp()
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Right click
|
|
||||||
appContextMenu.open()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Keys.onMenuPressed: {
|
||||||
|
// We must use open() here so the menu is positioned on
|
||||||
|
// the ItemDelegate and not where the mouse cursor is
|
||||||
|
appContextMenu.open()
|
||||||
|
}
|
||||||
|
|
||||||
Menu {
|
Menu {
|
||||||
id: appContextMenu
|
id: appContextMenu
|
||||||
MenuItem {
|
NavigableMenuItem {
|
||||||
text: model.running ? "Resume Game" : "Launch Game"
|
text: model.running ? "Resume Game" : "Launch Game"
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
appContextMenu.close()
|
appContextMenu.close()
|
||||||
launchOrResumeSelectedApp()
|
launchOrResumeSelectedApp()
|
||||||
}
|
}
|
||||||
height: visible ? implicitHeight : 0
|
|
||||||
}
|
}
|
||||||
MenuItem {
|
NavigableMenuItem {
|
||||||
text: "Quit Game"
|
text: "Quit Game"
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
quitAppDialog.appName = appModel.getRunningAppName()
|
quitAppDialog.appName = appModel.getRunningAppName()
|
||||||
@@ -165,11 +162,46 @@ GridView {
|
|||||||
quitAppDialog.open()
|
quitAppDialog.open()
|
||||||
}
|
}
|
||||||
visible: model.running
|
visible: model.running
|
||||||
height: visible ? implicitHeight : 0
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MessageDialog {
|
||||||
|
id: quitAppDialog
|
||||||
|
modality:Qt.WindowModal
|
||||||
|
property string appName : ""
|
||||||
|
property bool segueToStream : false
|
||||||
|
property string nextAppName: ""
|
||||||
|
property int nextAppIndex: 0
|
||||||
|
text:"Are you sure you want to quit " + appName +"? Any unsaved progress will be lost."
|
||||||
|
standardButtons: StandardButton.Yes | StandardButton.No
|
||||||
|
|
||||||
|
function quitApp() {
|
||||||
|
var component = Qt.createComponent("QuitSegue.qml")
|
||||||
|
var params = {"appName": appName}
|
||||||
|
if (segueToStream) {
|
||||||
|
// Store the session and app name if we're going to stream after
|
||||||
|
// successfully quitting the old app.
|
||||||
|
params.nextAppName = nextAppName
|
||||||
|
params.nextSession = appModel.createSessionForApp(nextAppIndex)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
params.nextAppName = null
|
||||||
|
params.nextSession = null
|
||||||
|
}
|
||||||
|
|
||||||
|
stackView.push(component.createObject(stackView, params))
|
||||||
|
|
||||||
|
// Trigger the quit after pushing the quit segue on screen
|
||||||
|
appModel.quitRunningApp()
|
||||||
|
}
|
||||||
|
|
||||||
|
onYes: quitApp()
|
||||||
|
|
||||||
|
// For keyboard/gamepad navigation
|
||||||
|
onAccepted: quitApp()
|
||||||
|
}
|
||||||
|
|
||||||
ScrollBar.vertical: ScrollBar {
|
ScrollBar.vertical: ScrollBar {
|
||||||
parent: appGrid.parent
|
parent: appGrid.parent
|
||||||
anchors {
|
anchors {
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import QtQuick 2.9
|
||||||
|
import QtQuick.Controls 2.2
|
||||||
|
|
||||||
|
// https://stackoverflow.com/questions/45029968/how-do-i-set-the-combobox-width-to-fit-the-largest-item
|
||||||
|
ComboBox {
|
||||||
|
property int textWidth
|
||||||
|
|
||||||
|
implicitWidth: leftPadding + textWidth + indicator.width + rightPadding
|
||||||
|
|
||||||
|
TextMetrics {
|
||||||
|
id: popupMetrics
|
||||||
|
}
|
||||||
|
|
||||||
|
TextMetrics {
|
||||||
|
id: textMetrics
|
||||||
|
}
|
||||||
|
|
||||||
|
// We call this every time the options change (and init)
|
||||||
|
// so we can adjust the combo box width here too
|
||||||
|
onActivated: {
|
||||||
|
textMetrics.font = font
|
||||||
|
popupMetrics.font = popup.font
|
||||||
|
textWidth = 0
|
||||||
|
for (var i = 0; i < count; i++){
|
||||||
|
textMetrics.text = textAt(i)
|
||||||
|
popupMetrics.text = textAt(i)
|
||||||
|
textWidth = Math.max(textMetrics.width, textWidth)
|
||||||
|
textWidth = Math.max(popupMetrics.width, textWidth)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import QtQuick 2.0
|
||||||
|
import QtQuick.Controls 2.2
|
||||||
|
import QtQuick.Dialogs 1.2
|
||||||
|
|
||||||
|
import ComputerManager 1.0
|
||||||
|
|
||||||
|
Item {
|
||||||
|
visible: false
|
||||||
|
|
||||||
|
function onSearchingComputer() {
|
||||||
|
stageLabel.text = "Establishing connection to PC..."
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSearchingApp() {
|
||||||
|
stageLabel.text = "Loading app list..."
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSessionCreated(appName, session) {
|
||||||
|
var component = Qt.createComponent("StreamSegue.qml")
|
||||||
|
var segue = component.createObject(stackView, {
|
||||||
|
"appName": appName,
|
||||||
|
"session": session,
|
||||||
|
"quitAfter": true
|
||||||
|
})
|
||||||
|
stackView.push(segue)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onLaunchFailed(message) {
|
||||||
|
errorDialog.text = message
|
||||||
|
errorDialog.open()
|
||||||
|
}
|
||||||
|
|
||||||
|
onVisibleChanged: {
|
||||||
|
if (visible) {
|
||||||
|
toolBar.visible = false
|
||||||
|
launcher.searchingComputer.connect(onSearchingComputer)
|
||||||
|
launcher.searchingApp.connect(onSearchingApp)
|
||||||
|
launcher.sessionCreated.connect(onSessionCreated)
|
||||||
|
launcher.failed.connect(onLaunchFailed)
|
||||||
|
launcher.execute(ComputerManager)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: 5
|
||||||
|
|
||||||
|
BusyIndicator {
|
||||||
|
id: stageSpinner
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
id: stageLabel
|
||||||
|
height: stageSpinner.height
|
||||||
|
font.pointSize: 20
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
color: "white"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MessageDialog {
|
||||||
|
id: errorDialog
|
||||||
|
modality:Qt.WindowModal
|
||||||
|
icon: StandardIcon.Critical
|
||||||
|
standardButtons: StandardButton.Ok | StandardButton.Help
|
||||||
|
onHelp: {
|
||||||
|
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Troubleshooting");
|
||||||
|
}
|
||||||
|
onVisibleChanged: {
|
||||||
|
if (!visible) {
|
||||||
|
Qt.quit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import QtQuick 2.0
|
||||||
|
import QtQuick.Controls 2.2
|
||||||
|
|
||||||
|
ItemDelegate {
|
||||||
|
property GridView grid
|
||||||
|
|
||||||
|
highlighted: grid.activeFocus && grid.currentItem === this
|
||||||
|
|
||||||
|
Keys.onLeftPressed: {
|
||||||
|
grid.moveCurrentIndexLeft()
|
||||||
|
}
|
||||||
|
Keys.onRightPressed: {
|
||||||
|
grid.moveCurrentIndexRight()
|
||||||
|
}
|
||||||
|
Keys.onDownPressed: {
|
||||||
|
grid.moveCurrentIndexDown()
|
||||||
|
}
|
||||||
|
Keys.onUpPressed: {
|
||||||
|
grid.moveCurrentIndexUp()
|
||||||
|
}
|
||||||
|
Keys.onReturnPressed: {
|
||||||
|
clicked()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import QtQuick 2.0
|
||||||
|
import QtQuick.Controls 2.2
|
||||||
|
|
||||||
|
MenuItem {
|
||||||
|
// Ensure focus can't be given to an invisible item
|
||||||
|
enabled: visible
|
||||||
|
height: visible ? implicitHeight : 0
|
||||||
|
|
||||||
|
Keys.onReturnPressed: {
|
||||||
|
triggered()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import QtQuick 2.0
|
||||||
|
import QtQuick.Controls 2.2
|
||||||
|
|
||||||
|
ToolButton {
|
||||||
|
activeFocusOnTab: true
|
||||||
|
|
||||||
|
Keys.onReturnPressed: {
|
||||||
|
clicked()
|
||||||
|
}
|
||||||
|
|
||||||
|
Keys.onRightPressed: {
|
||||||
|
nextItemInFocusChain(true).forceActiveFocus(Qt.TabFocus)
|
||||||
|
}
|
||||||
|
|
||||||
|
Keys.onLeftPressed: {
|
||||||
|
nextItemInFocusChain(false).forceActiveFocus(Qt.TabFocus)
|
||||||
|
}
|
||||||
|
}
|
||||||
+81
-76
@@ -1,6 +1,6 @@
|
|||||||
import QtQuick 2.9
|
import QtQuick 2.9
|
||||||
import QtQuick.Controls 2.2
|
import QtQuick.Controls 2.2
|
||||||
import QtQuick.Dialogs 1.3
|
import QtQuick.Dialogs 1.2
|
||||||
import QtQuick.Layouts 1.3
|
import QtQuick.Layouts 1.3
|
||||||
import QtQuick.Window 2.2
|
import QtQuick.Window 2.2
|
||||||
|
|
||||||
@@ -8,18 +8,20 @@ import ComputerModel 1.0
|
|||||||
|
|
||||||
import ComputerManager 1.0
|
import ComputerManager 1.0
|
||||||
import StreamingPreferences 1.0
|
import StreamingPreferences 1.0
|
||||||
|
import SdlGamepadKeyNavigation 1.0
|
||||||
|
|
||||||
GridView {
|
GridView {
|
||||||
property ComputerModel computerModel : createModel()
|
property ComputerModel computerModel : createModel()
|
||||||
|
|
||||||
id: pcGrid
|
id: pcGrid
|
||||||
|
focus: true
|
||||||
|
activeFocusOnTab: true
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.leftMargin: (parent.width % (cellWidth + anchors.rightMargin)) / 2
|
anchors.leftMargin: (parent.width % (cellWidth + anchors.rightMargin)) / 2
|
||||||
anchors.topMargin: 20
|
anchors.topMargin: 20
|
||||||
anchors.rightMargin: 5
|
anchors.rightMargin: 5
|
||||||
anchors.bottomMargin: 5
|
anchors.bottomMargin: 5
|
||||||
cellWidth: 350; cellHeight: 350;
|
cellWidth: 350; cellHeight: 350;
|
||||||
focus: true
|
|
||||||
objectName: "Computers"
|
objectName: "Computers"
|
||||||
|
|
||||||
// The StackView will trigger a visibility change when
|
// The StackView will trigger a visibility change when
|
||||||
@@ -31,16 +33,25 @@ GridView {
|
|||||||
id: prefs
|
id: prefs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SdlGamepadKeyNavigation {
|
||||||
|
id: gamepadKeyNav
|
||||||
|
}
|
||||||
|
|
||||||
|
onVisibleChanged: {
|
||||||
|
if (visible) {
|
||||||
|
gamepadKeyNav.enable()
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
gamepadKeyNav.disable()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
// Setup signals on CM
|
// Setup signals on CM
|
||||||
ComputerManager.computerAddCompleted.connect(addComplete)
|
ComputerManager.computerAddCompleted.connect(addComplete)
|
||||||
|
|
||||||
if (prefs.isRunningWayland()) {
|
// Don't show any highlighted item until interacting with them
|
||||||
waylandDialog.open()
|
currentIndex = -1
|
||||||
}
|
|
||||||
else if (!prefs.hasAnyHardwareAcceleration()) {
|
|
||||||
noHwDecoderDialog.open()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function pairingComplete(error)
|
function pairingComplete(error)
|
||||||
@@ -49,7 +60,7 @@ GridView {
|
|||||||
pairDialog.close()
|
pairDialog.close()
|
||||||
|
|
||||||
// Display a failed dialog if we got an error
|
// Display a failed dialog if we got an error
|
||||||
if (error !== null) {
|
if (error !== undefined) {
|
||||||
errorDialog.text = error
|
errorDialog.text = error
|
||||||
errorDialog.open()
|
errorDialog.open()
|
||||||
}
|
}
|
||||||
@@ -58,7 +69,7 @@ GridView {
|
|||||||
function addComplete(success)
|
function addComplete(success)
|
||||||
{
|
{
|
||||||
if (!success) {
|
if (!success) {
|
||||||
errorDialog.text = "Unable to connect to the specified PC. Ensure GameStream is enabled in GeForce Experience."
|
errorDialog.text = "Unable to connect to the specified PC. Click the Help button for possible solutions."
|
||||||
errorDialog.open()
|
errorDialog.open()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -73,8 +84,9 @@ GridView {
|
|||||||
|
|
||||||
model: computerModel
|
model: computerModel
|
||||||
|
|
||||||
delegate: Item {
|
delegate: NavigableItemDelegate {
|
||||||
width: 300; height: 300;
|
width: 300; height: 300;
|
||||||
|
grid: pcGrid
|
||||||
|
|
||||||
Image {
|
Image {
|
||||||
id: pcIcon
|
id: pcIcon
|
||||||
@@ -126,13 +138,12 @@ GridView {
|
|||||||
|
|
||||||
Menu {
|
Menu {
|
||||||
id: pcContextMenu
|
id: pcContextMenu
|
||||||
MenuItem {
|
NavigableMenuItem {
|
||||||
text: "Wake PC"
|
text: "Wake PC"
|
||||||
onTriggered: computerModel.wakeComputer(index)
|
onTriggered: computerModel.wakeComputer(index)
|
||||||
visible: !model.addPc && !model.online && model.wakeable
|
visible: !model.addPc && !model.online && model.wakeable
|
||||||
height: visible ? implicitHeight : 0
|
|
||||||
}
|
}
|
||||||
MenuItem {
|
NavigableMenuItem {
|
||||||
text: "Delete PC"
|
text: "Delete PC"
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
deletePcDialog.pcIndex = index
|
deletePcDialog.pcIndex = index
|
||||||
@@ -142,50 +153,58 @@ GridView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
if (model.addPc) {
|
||||||
|
addPcDialog.open()
|
||||||
|
}
|
||||||
|
else if (model.online) {
|
||||||
|
if (model.paired) {
|
||||||
|
// go to game view
|
||||||
|
var component = Qt.createComponent("AppView.qml")
|
||||||
|
var appView = component.createObject(stackView, {"computerIndex": index, "objectName": model.name})
|
||||||
|
stackView.push(appView)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (!model.busy) {
|
||||||
|
var pin = ("0000" + Math.floor(Math.random() * 10000)).slice(-4)
|
||||||
|
|
||||||
|
// Kick off pairing in the background
|
||||||
|
computerModel.pairComputer(index, pin)
|
||||||
|
|
||||||
|
// Display the pairing dialog
|
||||||
|
pairDialog.pin = pin
|
||||||
|
pairDialog.open()
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// cannot pair while something is streaming or attempting to pair
|
||||||
|
errorDialog.text = "This PC is currently busy. Make sure to quit any running games and try again."
|
||||||
|
errorDialog.open()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (!model.online) {
|
||||||
|
// Using open() here because it may be activated by keyboard
|
||||||
|
pcContextMenu.open()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
acceptedButtons: Qt.LeftButton | Qt.RightButton;
|
acceptedButtons: Qt.RightButton;
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if(mouse.button === Qt.LeftButton) {
|
if (!model.addPc) {
|
||||||
if (model.addPc) {
|
// popup() ensures the menu appears under the mouse cursor
|
||||||
addPcDialog.open()
|
pcContextMenu.popup()
|
||||||
}
|
|
||||||
else if (model.online) {
|
|
||||||
if (model.paired) {
|
|
||||||
// go to game view
|
|
||||||
var component = Qt.createComponent("AppView.qml")
|
|
||||||
var appView = component.createObject(stackView, {"computerIndex": index, "objectName": model.name})
|
|
||||||
stackView.push(appView)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
if (!model.busy) {
|
|
||||||
var pin = ("0000" + Math.floor(Math.random() * 10000)).slice(-4)
|
|
||||||
|
|
||||||
// Kick off pairing in the background
|
|
||||||
computerModel.pairComputer(index, pin)
|
|
||||||
|
|
||||||
// Display the pairing dialog
|
|
||||||
pairDialog.pin = pin
|
|
||||||
pairDialog.open()
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// cannot pair while something is streaming or attempting to pair
|
|
||||||
errorDialog.text = "This PC is currently busy. Make sure to quit any running games and try again."
|
|
||||||
errorDialog.open()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if(!model.online) {
|
|
||||||
pcContextMenu.open()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
else { // right click
|
|
||||||
if(!model.addPc) { // but only for actual PCs, not the add-pc option
|
|
||||||
pcContextMenu.open()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Keys.onMenuPressed: {
|
||||||
|
if (!model.addPc) {
|
||||||
|
// We must use open() here so the menu is positioned on
|
||||||
|
// the ItemDelegate and not where the mouse cursor is
|
||||||
|
pcContextMenu.open()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MessageDialog {
|
MessageDialog {
|
||||||
@@ -193,31 +212,11 @@ GridView {
|
|||||||
// don't allow edits to the rest of the window while open
|
// don't allow edits to the rest of the window while open
|
||||||
modality:Qt.WindowModal
|
modality:Qt.WindowModal
|
||||||
icon: StandardIcon.Critical
|
icon: StandardIcon.Critical
|
||||||
standardButtons: StandardButton.Ok
|
|
||||||
}
|
|
||||||
|
|
||||||
MessageDialog {
|
|
||||||
id: noHwDecoderDialog
|
|
||||||
modality:Qt.WindowModal
|
|
||||||
icon: StandardIcon.Warning
|
|
||||||
standardButtons: StandardButton.Ok | StandardButton.Help
|
standardButtons: StandardButton.Ok | StandardButton.Help
|
||||||
text: "No functioning hardware accelerated H.264 video decoder was detected by Moonlight. " +
|
|
||||||
"Your streaming performance may be severely degraded in this configuration. " +
|
|
||||||
"Click the Help button for more information on solving this problem."
|
|
||||||
onHelp: {
|
onHelp: {
|
||||||
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Fixing-Hardware-Decoding-Problems");
|
// Using Setup-Guide here instead of Troubleshooting because it's likely that users
|
||||||
}
|
// will arrive here by forgetting to enable GameStream or not forwarding ports.
|
||||||
}
|
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Setup-Guide");
|
||||||
|
|
||||||
MessageDialog {
|
|
||||||
id: waylandDialog
|
|
||||||
modality:Qt.WindowModal
|
|
||||||
icon: StandardIcon.Warning
|
|
||||||
standardButtons: StandardButton.Ok | StandardButton.Help
|
|
||||||
text: "Moonlight does not support hardware acceleration on Wayland. Continuing on Wayland may result in poor streaming performance. " +
|
|
||||||
"Please switch to an X session for optimal performance."
|
|
||||||
onHelp: {
|
|
||||||
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Fixing-Hardware-Decoding-Problems");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -240,10 +239,16 @@ GridView {
|
|||||||
property int pcIndex : -1;
|
property int pcIndex : -1;
|
||||||
text:"Are you sure you want to remove this PC?"
|
text:"Are you sure you want to remove this PC?"
|
||||||
standardButtons: StandardButton.Yes | StandardButton.No
|
standardButtons: StandardButton.Yes | StandardButton.No
|
||||||
onYes: {
|
|
||||||
|
function deletePc() {
|
||||||
console.log("deleting PC pairing for PC at index: " + pcIndex)
|
console.log("deleting PC pairing for PC at index: " + pcIndex)
|
||||||
computerModel.deleteComputer(pcIndex);
|
computerModel.deleteComputer(pcIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onYes: deletePc()
|
||||||
|
|
||||||
|
// For keyboard/gamepad activation
|
||||||
|
onAccepted: deletePc()
|
||||||
}
|
}
|
||||||
|
|
||||||
Dialog {
|
Dialog {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import QtQuick 2.0
|
import QtQuick 2.0
|
||||||
import QtQuick.Controls 2.2
|
import QtQuick.Controls 2.2
|
||||||
import QtQuick.Dialogs 1.3
|
import QtQuick.Dialogs 1.2
|
||||||
|
|
||||||
import ComputerManager 1.0
|
import ComputerManager 1.0
|
||||||
import Session 1.0
|
import Session 1.0
|
||||||
|
|||||||
+248
-97
@@ -2,6 +2,8 @@ import QtQuick 2.9
|
|||||||
import QtQuick.Controls 2.2
|
import QtQuick.Controls 2.2
|
||||||
|
|
||||||
import StreamingPreferences 1.0
|
import StreamingPreferences 1.0
|
||||||
|
import ComputerManager 1.0
|
||||||
|
import SdlGamepadKeyNavigation 1.0
|
||||||
|
|
||||||
ScrollView {
|
ScrollView {
|
||||||
id: settingsPage
|
id: settingsPage
|
||||||
@@ -11,6 +13,25 @@ ScrollView {
|
|||||||
id: prefs
|
id: prefs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The StackView will trigger a visibility change when
|
||||||
|
// we're pushed onto it, causing our onVisibleChanged
|
||||||
|
// routine to run, but only if we start as invisible
|
||||||
|
visible: false
|
||||||
|
|
||||||
|
SdlGamepadKeyNavigation {
|
||||||
|
id: gamepadKeyNav
|
||||||
|
}
|
||||||
|
|
||||||
|
onVisibleChanged: {
|
||||||
|
if (visible) {
|
||||||
|
gamepadKeyNav.setSettingsMode(true)
|
||||||
|
gamepadKeyNav.enable()
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
gamepadKeyNav.disable()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Component.onDestruction: {
|
Component.onDestruction: {
|
||||||
prefs.save()
|
prefs.save()
|
||||||
}
|
}
|
||||||
@@ -52,41 +73,55 @@ ScrollView {
|
|||||||
Row {
|
Row {
|
||||||
spacing: 5
|
spacing: 5
|
||||||
|
|
||||||
ComboBox {
|
AutoResizingComboBox {
|
||||||
// ignore setting the index at first, and actually set it when the component is loaded
|
// ignore setting the index at first, and actually set it when the component is loaded
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
// Add native resolutions for all attached displays
|
// Add native resolutions for all attached displays
|
||||||
for (var displayIndex = 0;; displayIndex++) {
|
var done = false
|
||||||
var screenRect = prefs.getDisplayResolution(displayIndex)
|
for (var displayIndex = 0; !done; displayIndex++) {
|
||||||
if (screenRect.width === 0) {
|
for (var displayResIndex = 0; displayResIndex < 2; displayResIndex++) {
|
||||||
// Exceeded max count of displays
|
var screenRect;
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
var indexToAdd = 0
|
// Some platforms have different desktop resolutions
|
||||||
for (var j = 0; j < resolutionComboBox.count; j++) {
|
// and native resolutions (like macOS with Retina displays)
|
||||||
var existing_width = parseInt(resolutionListModel.get(j).video_width);
|
if (displayResIndex == 0) {
|
||||||
var existing_height = parseInt(resolutionListModel.get(j).video_height);
|
screenRect = prefs.getDesktopResolution(displayIndex)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
screenRect = prefs.getNativeResolution(displayIndex)
|
||||||
|
}
|
||||||
|
|
||||||
if (screenRect.width === existing_width && screenRect.height === existing_height) {
|
if (screenRect.width === 0) {
|
||||||
// Duplicate entry, skip
|
// Exceeded max count of displays
|
||||||
indexToAdd = -1
|
done = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
else if (screenRect.width * screenRect.height > existing_width * existing_height) {
|
|
||||||
// Candidate entrypoint after this entry
|
|
||||||
indexToAdd = j + 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert this display's resolution if it's not a duplicate
|
var indexToAdd = 0
|
||||||
if (indexToAdd >= 0) {
|
for (var j = 0; j < resolutionComboBox.count; j++) {
|
||||||
resolutionListModel.insert(indexToAdd,
|
var existing_width = parseInt(resolutionListModel.get(j).video_width);
|
||||||
{
|
var existing_height = parseInt(resolutionListModel.get(j).video_height);
|
||||||
"text": "Native ("+screenRect.width+"x"+screenRect.height+")",
|
|
||||||
"video_width": ""+screenRect.width,
|
if (screenRect.width === existing_width && screenRect.height === existing_height) {
|
||||||
"video_height": ""+screenRect.height
|
// Duplicate entry, skip
|
||||||
})
|
indexToAdd = -1
|
||||||
|
break
|
||||||
|
}
|
||||||
|
else if (screenRect.width * screenRect.height > existing_width * existing_height) {
|
||||||
|
// Candidate entrypoint after this entry
|
||||||
|
indexToAdd = j + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert this display's resolution if it's not a duplicate
|
||||||
|
if (indexToAdd >= 0) {
|
||||||
|
resolutionListModel.insert(indexToAdd,
|
||||||
|
{
|
||||||
|
"text": "Native ("+screenRect.width+"x"+screenRect.height+")",
|
||||||
|
"video_width": ""+screenRect.width,
|
||||||
|
"video_height": ""+screenRect.height
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,7 +130,7 @@ ScrollView {
|
|||||||
var saved_width = prefs.width
|
var saved_width = prefs.width
|
||||||
var saved_height = prefs.height
|
var saved_height = prefs.height
|
||||||
currentIndex = 0
|
currentIndex = 0
|
||||||
for (var i = 0; i < resolutionComboBox.count; i++) {
|
for (var i = 0; i < resolutionListModel.count; i++) {
|
||||||
var el_width = parseInt(resolutionListModel.get(i).video_width);
|
var el_width = parseInt(resolutionListModel.get(i).video_width);
|
||||||
var el_height = parseInt(resolutionListModel.get(i).video_height);
|
var el_height = parseInt(resolutionListModel.get(i).video_height);
|
||||||
|
|
||||||
@@ -104,10 +139,12 @@ ScrollView {
|
|||||||
currentIndex = i
|
currentIndex = i
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Persist the selected value
|
||||||
|
activated(currentIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
id: resolutionComboBox
|
id: resolutionComboBox
|
||||||
width: 200
|
|
||||||
textRole: "text"
|
textRole: "text"
|
||||||
model: ListModel {
|
model: ListModel {
|
||||||
id: resolutionListModel
|
id: resolutionListModel
|
||||||
@@ -136,59 +173,96 @@ ScrollView {
|
|||||||
}
|
}
|
||||||
// ::onActivated must be used, as it only listens for when the index is changed by a human
|
// ::onActivated must be used, as it only listens for when the index is changed by a human
|
||||||
onActivated : {
|
onActivated : {
|
||||||
prefs.width = parseInt(resolutionListModel.get(currentIndex).video_width)
|
var selectedWidth = parseInt(resolutionListModel.get(currentIndex).video_width)
|
||||||
prefs.height = parseInt(resolutionListModel.get(currentIndex).video_height)
|
var selectedHeight = parseInt(resolutionListModel.get(currentIndex).video_height)
|
||||||
|
|
||||||
prefs.bitrateKbps = prefs.getDefaultBitrate(prefs.width, prefs.height, prefs.fps);
|
// Only modify the bitrate if the values actually changed
|
||||||
slider.value = prefs.bitrateKbps
|
if (prefs.width !== selectedWidth || prefs.height !== selectedHeight) {
|
||||||
|
prefs.width = selectedWidth
|
||||||
|
prefs.height = selectedHeight
|
||||||
|
|
||||||
|
prefs.bitrateKbps = prefs.getDefaultBitrate(prefs.width, prefs.height, prefs.fps);
|
||||||
|
slider.value = prefs.bitrateKbps
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ComboBox {
|
AutoResizingComboBox {
|
||||||
// ignore setting the index at first, and actually set it when the component is loaded
|
function createModel() {
|
||||||
Component.onCompleted: {
|
var fpsListModel = Qt.createQmlObject('import QtQuick 2.0; ListModel {}', parent, '')
|
||||||
|
|
||||||
// Get the max supported FPS on this system
|
// Get the max supported FPS on this system
|
||||||
var max_fps = prefs.getMaximumStreamingFrameRate();
|
var max_fps = prefs.getMaximumStreamingFrameRate();
|
||||||
|
|
||||||
|
// Default entries
|
||||||
|
fpsListModel.append({"text": "30 FPS", "video_fps": "30"})
|
||||||
|
fpsListModel.append({"text": "60 FPS", "video_fps": "60"})
|
||||||
|
|
||||||
|
// Add unsupported FPS values that come before the display max FPS
|
||||||
|
if (prefs.unsupportedFps) {
|
||||||
|
if (max_fps > 90) {
|
||||||
|
fpsListModel.append({"text": "90 FPS (Unsupported)", "video_fps": "90"})
|
||||||
|
}
|
||||||
|
if (max_fps > 120) {
|
||||||
|
fpsListModel.append({"text": "120 FPS (Unsupported)", "video_fps": "120"})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Use 64 as the cutoff for adding a separate option to
|
// Use 64 as the cutoff for adding a separate option to
|
||||||
// handle wonky displays that report just over 60 Hz.
|
// handle wonky displays that report just over 60 Hz.
|
||||||
if (max_fps > 64) {
|
if (max_fps > 64) {
|
||||||
fpsListModel.append({"text": max_fps+" FPS", "video_fps": ""+max_fps})
|
fpsListModel.append({"text": max_fps+" FPS", "video_fps": ""+max_fps})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add unsupported FPS values that come after the display max FPS
|
||||||
|
if (prefs.unsupportedFps) {
|
||||||
|
if (max_fps < 90) {
|
||||||
|
fpsListModel.append({"text": "90 FPS (Unsupported)", "video_fps": "90"})
|
||||||
|
}
|
||||||
|
if (max_fps < 120) {
|
||||||
|
fpsListModel.append({"text": "120 FPS (Unsupported)", "video_fps": "120"})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fpsListModel
|
||||||
|
}
|
||||||
|
|
||||||
|
function reinitialize() {
|
||||||
|
model = createModel()
|
||||||
|
|
||||||
var saved_fps = prefs.fps
|
var saved_fps = prefs.fps
|
||||||
currentIndex = 0
|
currentIndex = 0
|
||||||
for (var i = 0; i < fpsComboBox.count; i++) {
|
for (var i = 0; i < model.count; i++) {
|
||||||
var el_fps = parseInt(fpsListModel.get(i).video_fps);
|
var el_fps = parseInt(model.get(i).video_fps);
|
||||||
|
|
||||||
// Pick the highest value lesser or equal to the saved FPS
|
// Pick the highest value lesser or equal to the saved FPS
|
||||||
if (saved_fps >= el_fps) {
|
if (saved_fps >= el_fps) {
|
||||||
currentIndex = i
|
currentIndex = i
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Persist the selected value
|
||||||
|
activated(currentIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ignore setting the index at first, and actually set it when the component is loaded
|
||||||
|
Component.onCompleted: {
|
||||||
|
reinitialize()
|
||||||
}
|
}
|
||||||
|
|
||||||
id: fpsComboBox
|
id: fpsComboBox
|
||||||
textRole: "text"
|
textRole: "text"
|
||||||
model: ListModel {
|
|
||||||
id: fpsListModel
|
|
||||||
ListElement {
|
|
||||||
text: "30 FPS"
|
|
||||||
video_fps: "30"
|
|
||||||
}
|
|
||||||
ListElement {
|
|
||||||
text: "60 FPS"
|
|
||||||
video_fps: "60"
|
|
||||||
}
|
|
||||||
// A higher value may be added at runtime
|
|
||||||
// based on the attached display refresh rate
|
|
||||||
}
|
|
||||||
// ::onActivated must be used, as it only listens for when the index is changed by a human
|
// ::onActivated must be used, as it only listens for when the index is changed by a human
|
||||||
onActivated : {
|
onActivated : {
|
||||||
prefs.fps = parseInt(fpsListModel.get(currentIndex).video_fps)
|
var selectedFps = parseInt(model.get(currentIndex).video_fps)
|
||||||
|
|
||||||
prefs.bitrateKbps = prefs.getDefaultBitrate(prefs.width, prefs.height, prefs.fps);
|
// Only modify the bitrate if the values actually changed
|
||||||
slider.value = prefs.bitrateKbps
|
if (prefs.fps !== selectedFps) {
|
||||||
|
prefs.fps = selectedFps
|
||||||
|
|
||||||
|
prefs.bitrateKbps = prefs.getDefaultBitrate(prefs.width, prefs.height, prefs.fps);
|
||||||
|
slider.value = prefs.bitrateKbps
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -230,26 +304,59 @@ ScrollView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Row {
|
Label {
|
||||||
CheckBox {
|
width: parent.width
|
||||||
id: fullScreenCheck
|
id: windowModeTitle
|
||||||
text: "<font color=\"white\">Full-screen</font>"
|
text: qsTr("Display mode")
|
||||||
font.pointSize: 12
|
font.pointSize: 12
|
||||||
checked: prefs.fullScreen
|
wrapMode: Text.Wrap
|
||||||
onCheckedChanged: {
|
color: "white"
|
||||||
prefs.fullScreen = checked
|
}
|
||||||
|
|
||||||
|
AutoResizingComboBox {
|
||||||
|
// ignore setting the index at first, and actually set it when the component is loaded
|
||||||
|
Component.onCompleted: {
|
||||||
|
var savedWm = prefs.windowMode
|
||||||
|
currentIndex = 0
|
||||||
|
for (var i = 0; i < windowModeListModel.count; i++) {
|
||||||
|
var thisWm = windowModeListModel.get(i).val;
|
||||||
|
if (savedWm === thisWm) {
|
||||||
|
currentIndex = i
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
activated(currentIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
CheckBox {
|
id: windowModeComboBox
|
||||||
id: vsyncCheck
|
textRole: "text"
|
||||||
text: "<font color=\"white\">Enable V-Sync</font>"
|
model: ListModel {
|
||||||
font.pointSize: 12
|
id: windowModeListModel
|
||||||
visible: fullScreenCheck.checked
|
ListElement {
|
||||||
checked: prefs.enableVsync
|
text: "Full-screen (Recommended)"
|
||||||
onCheckedChanged: {
|
val: StreamingPreferences.WM_FULLSCREEN
|
||||||
prefs.enableVsync = checked
|
|
||||||
}
|
}
|
||||||
|
ListElement {
|
||||||
|
text: "Borderless windowed"
|
||||||
|
val: StreamingPreferences.WM_FULLSCREEN_DESKTOP
|
||||||
|
}
|
||||||
|
ListElement {
|
||||||
|
text: "Windowed"
|
||||||
|
val: StreamingPreferences.WM_WINDOWED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onActivated: {
|
||||||
|
prefs.windowMode = windowModeListModel.get(currentIndex).val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CheckBox {
|
||||||
|
id: vsyncCheck
|
||||||
|
text: "<font color=\"white\">Enable V-Sync</font>"
|
||||||
|
font.pointSize: 12
|
||||||
|
checked: prefs.enableVsync
|
||||||
|
onCheckedChanged: {
|
||||||
|
prefs.enableVsync = checked
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -276,35 +383,32 @@ ScrollView {
|
|||||||
color: "white"
|
color: "white"
|
||||||
}
|
}
|
||||||
|
|
||||||
ComboBox {
|
AutoResizingComboBox {
|
||||||
// ignore setting the index at first, and actually set it when the component is loaded
|
// ignore setting the index at first, and actually set it when the component is loaded
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
var saved_audio = prefs.audioConfig
|
var saved_audio = prefs.audioConfig
|
||||||
currentIndex = 0
|
currentIndex = 0
|
||||||
for(var i = 0; i < audioListModel.count; i++) {
|
for (var i = 0; i < audioListModel.count; i++) {
|
||||||
var el_audio= audioListModel.get(i).val;
|
var el_audio = audioListModel.get(i).val;
|
||||||
if(saved_audio === el_audio) {
|
if (saved_audio === el_audio) {
|
||||||
currentIndex = i
|
currentIndex = i
|
||||||
}
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
activated(currentIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
id: audioComboBox
|
id: audioComboBox
|
||||||
width: Math.min(bitrateDesc.implicitWidth, parent.width)
|
|
||||||
textRole: "text"
|
textRole: "text"
|
||||||
model: ListModel {
|
model: ListModel {
|
||||||
id: audioListModel
|
id: audioListModel
|
||||||
ListElement {
|
|
||||||
text: "Autodetect"
|
|
||||||
val: StreamingPreferences.AC_AUTO
|
|
||||||
}
|
|
||||||
ListElement {
|
ListElement {
|
||||||
text: "Stereo"
|
text: "Stereo"
|
||||||
val: StreamingPreferences.AC_FORCE_STEREO
|
val: StreamingPreferences.AC_STEREO
|
||||||
}
|
}
|
||||||
ListElement {
|
ListElement {
|
||||||
text: "5.1 surround sound"
|
text: "5.1 surround sound"
|
||||||
val: StreamingPreferences.AC_FORCE_SURROUND
|
val: StreamingPreferences.AC_51_SURROUND
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// ::onActivated must be used, as it only listens for when the index is changed by a human
|
// ::onActivated must be used, as it only listens for when the index is changed by a human
|
||||||
@@ -327,10 +431,10 @@ ScrollView {
|
|||||||
id: gamepadSettingsGroupBox
|
id: gamepadSettingsGroupBox
|
||||||
width: (parent.width - parent.padding)
|
width: (parent.width - parent.padding)
|
||||||
padding: 12
|
padding: 12
|
||||||
title: "<font color=\"skyblue\">Gamepad Settings</font>"
|
title: "<font color=\"skyblue\">Input Settings</font>"
|
||||||
font.pointSize: 12
|
font.pointSize: 12
|
||||||
|
|
||||||
Column {
|
Row {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
spacing: 5
|
spacing: 5
|
||||||
|
|
||||||
@@ -343,6 +447,16 @@ ScrollView {
|
|||||||
prefs.multiController = checked
|
prefs.multiController = checked
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CheckBox {
|
||||||
|
id: mouseAccelerationCheck
|
||||||
|
text: "<font color=\"white\">Enable mouse acceleration</font>"
|
||||||
|
font.pointSize: 12
|
||||||
|
checked: prefs.mouseAcceleration
|
||||||
|
onCheckedChanged: {
|
||||||
|
prefs.mouseAcceleration = checked
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -353,13 +467,15 @@ ScrollView {
|
|||||||
title: "<font color=\"skyblue\">Host Settings</font>"
|
title: "<font color=\"skyblue\">Host Settings</font>"
|
||||||
font.pointSize: 12
|
font.pointSize: 12
|
||||||
|
|
||||||
Column {
|
Row {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
spacing: 5
|
spacing: 5
|
||||||
|
|
||||||
CheckBox {
|
CheckBox {
|
||||||
id: optimizeGameSettingsCheck
|
id: optimizeGameSettingsCheck
|
||||||
text: "<font color=\"white\">Optimize game settings</font>"
|
text: "<font color=\"white\">Optimize game settings</font>"
|
||||||
|
// HACK: Match width of the other checkbox to make the UI not look bad
|
||||||
|
width: multiControllerCheck.width
|
||||||
font.pointSize: 12
|
font.pointSize: 12
|
||||||
checked: prefs.gameOptimizations
|
checked: prefs.gameOptimizations
|
||||||
onCheckedChanged: {
|
onCheckedChanged: {
|
||||||
@@ -399,21 +515,22 @@ ScrollView {
|
|||||||
color: "white"
|
color: "white"
|
||||||
}
|
}
|
||||||
|
|
||||||
ComboBox {
|
AutoResizingComboBox {
|
||||||
// ignore setting the index at first, and actually set it when the component is loaded
|
// ignore setting the index at first, and actually set it when the component is loaded
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
var saved_vds = prefs.videoDecoderSelection
|
var saved_vds = prefs.videoDecoderSelection
|
||||||
currentIndex = 0
|
currentIndex = 0
|
||||||
for(var i = 0; i < decoderListModel.count; i++) {
|
for (var i = 0; i < decoderListModel.count; i++) {
|
||||||
var el_vds = decoderListModel.get(i).val;
|
var el_vds = decoderListModel.get(i).val;
|
||||||
if(saved_vds === el_vds) {
|
if (saved_vds === el_vds) {
|
||||||
currentIndex = i
|
currentIndex = i
|
||||||
}
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
activated(currentIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
id: decoderComboBox
|
id: decoderComboBox
|
||||||
width: Math.min(bitrateDesc.implicitWidth, parent.width)
|
|
||||||
textRole: "text"
|
textRole: "text"
|
||||||
model: ListModel {
|
model: ListModel {
|
||||||
id: decoderListModel
|
id: decoderListModel
|
||||||
@@ -445,21 +562,22 @@ ScrollView {
|
|||||||
color: "white"
|
color: "white"
|
||||||
}
|
}
|
||||||
|
|
||||||
ComboBox {
|
AutoResizingComboBox {
|
||||||
// ignore setting the index at first, and actually set it when the component is loaded
|
// ignore setting the index at first, and actually set it when the component is loaded
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
var saved_vcc = prefs.videoCodecConfig
|
var saved_vcc = prefs.videoCodecConfig
|
||||||
currentIndex = 0
|
currentIndex = 0
|
||||||
for(var i = 0; i < codecListModel.count; i++) {
|
for(var i = 0; i < codecListModel.count; i++) {
|
||||||
var el_vcc = codecListModel.get(i).val;
|
var el_vcc = codecListModel.get(i).val;
|
||||||
if(saved_vcc === el_vcc) {
|
if (saved_vcc === el_vcc) {
|
||||||
currentIndex = i
|
currentIndex = i
|
||||||
}
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
activated(currentIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
id: codecComboBox
|
id: codecComboBox
|
||||||
width: Math.min(bitrateDesc.implicitWidth, parent.width)
|
|
||||||
textRole: "text"
|
textRole: "text"
|
||||||
model: ListModel {
|
model: ListModel {
|
||||||
id: codecListModel
|
id: codecListModel
|
||||||
@@ -489,6 +607,39 @@ ScrollView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CheckBox {
|
||||||
|
id: unlockUnsupportedFps
|
||||||
|
text: "<font color=\"white\">Unlock unsupported FPS options</font>"
|
||||||
|
font.pointSize: 12
|
||||||
|
checked: prefs.unsupportedFps
|
||||||
|
onCheckedChanged: {
|
||||||
|
prefs.unsupportedFps = checked
|
||||||
|
|
||||||
|
// The selectable FPS values depend on whether
|
||||||
|
// this option is enabled or not
|
||||||
|
fpsComboBox.reinitialize()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CheckBox {
|
||||||
|
id: enableMdns
|
||||||
|
text: "<font color=\"white\">Automatically find PCs on the local network (Recommended)</font>"
|
||||||
|
font.pointSize: 12
|
||||||
|
checked: prefs.enableMdns
|
||||||
|
onCheckedChanged: {
|
||||||
|
prefs.enableMdns = checked
|
||||||
|
|
||||||
|
// We must save the updated preference to ensure
|
||||||
|
// ComputerManager can observe the change internally.
|
||||||
|
prefs.save()
|
||||||
|
|
||||||
|
// Restart polling so the mDNS change takes effect
|
||||||
|
if (window.pollingActive) {
|
||||||
|
ComputerManager.stopPollingAsync()
|
||||||
|
ComputerManager.startPolling()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+53
-8
@@ -1,13 +1,16 @@
|
|||||||
import QtQuick 2.0
|
import QtQuick 2.0
|
||||||
import QtQuick.Controls 2.2
|
import QtQuick.Controls 2.2
|
||||||
import QtQuick.Dialogs 1.3
|
import QtQuick.Dialogs 1.2
|
||||||
|
import QtQuick.Window 2.2
|
||||||
|
|
||||||
|
import SdlGamepadKeyNavigation 1.0
|
||||||
import Session 1.0
|
import Session 1.0
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
property Session session
|
property Session session
|
||||||
property string appName
|
property string appName
|
||||||
property string stageText : "Starting " + appName + "..."
|
property string stageText : "Starting " + appName + "..."
|
||||||
|
property bool quitAfter : false
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
|
||||||
@@ -41,8 +44,8 @@ Item {
|
|||||||
|
|
||||||
function displayLaunchError(text)
|
function displayLaunchError(text)
|
||||||
{
|
{
|
||||||
|
// Display the error dialog after Session::exec() returns
|
||||||
errorDialog.text = text
|
errorDialog.text = text
|
||||||
errorDialog.open()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function displayLaunchWarning(text)
|
function displayLaunchWarning(text)
|
||||||
@@ -56,11 +59,26 @@ Item {
|
|||||||
toast.visible = true
|
toast.visible = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// It's important that we don't call enable() here
|
||||||
|
// or it may interfere with the Session instance
|
||||||
|
// getting notified of initial connected gamepads.
|
||||||
|
SdlGamepadKeyNavigation {
|
||||||
|
id: gamepadKeyNav
|
||||||
|
}
|
||||||
|
|
||||||
onVisibleChanged: {
|
onVisibleChanged: {
|
||||||
if (visible) {
|
if (visible) {
|
||||||
// Hide the toolbar before we start loading
|
// Hide the toolbar before we start loading
|
||||||
toolBar.visible = false
|
toolBar.visible = false
|
||||||
|
|
||||||
|
// Set the hint text. We do this here rather than
|
||||||
|
// in the hintText control itself to synchronize
|
||||||
|
// with Session.exec() which requires no concurrent
|
||||||
|
// gamepad usage.
|
||||||
|
hintText.text = gamepadKeyNav.getConnectedGamepads() > 0 ?
|
||||||
|
"Tip: Press Start+Select+L1+R1 to disconnect your session" :
|
||||||
|
"Tip: Press Ctrl+Alt+Shift+Q to disconnect your session"
|
||||||
|
|
||||||
// Hook up our signals
|
// Hook up our signals
|
||||||
session.stageStarting.connect(stageStarting)
|
session.stageStarting.connect(stageStarting)
|
||||||
session.stageFailed.connect(stageFailed)
|
session.stageFailed.connect(stageFailed)
|
||||||
@@ -69,13 +87,25 @@ Item {
|
|||||||
session.displayLaunchWarning.connect(displayLaunchWarning)
|
session.displayLaunchWarning.connect(displayLaunchWarning)
|
||||||
|
|
||||||
// Run the streaming session to completion
|
// Run the streaming session to completion
|
||||||
session.exec()
|
session.exec(Screen.virtualX, Screen.virtualY)
|
||||||
|
|
||||||
// Show the Qt window again after streaming
|
if (quitAfter) {
|
||||||
window.visible = true
|
Qt.quit()
|
||||||
|
} else {
|
||||||
|
// Show the Qt window again after streaming
|
||||||
|
window.visible = true
|
||||||
|
|
||||||
// Exit this view
|
// Exit this view
|
||||||
stackView.pop()
|
stackView.pop()
|
||||||
|
|
||||||
|
// Display any launch errors. We do this after
|
||||||
|
// the Qt UI is visible again to prevent losing
|
||||||
|
// focus on the dialog which would impact gamepad
|
||||||
|
// users.
|
||||||
|
if (errorDialog.text) {
|
||||||
|
errorDialog.open()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// Show the toolbar again when we become hidden
|
// Show the toolbar again when we become hidden
|
||||||
@@ -103,10 +133,25 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
id: hintText
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.bottomMargin: 50
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
font.pointSize: 18
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
color: "white"
|
||||||
|
}
|
||||||
|
|
||||||
MessageDialog {
|
MessageDialog {
|
||||||
id: errorDialog
|
id: errorDialog
|
||||||
modality:Qt.WindowModal
|
modality:Qt.WindowModal
|
||||||
icon: StandardIcon.Critical
|
icon: StandardIcon.Critical
|
||||||
standardButtons: StandardButton.Ok
|
standardButtons: StandardButton.Ok | StandardButton.Help
|
||||||
|
onHelp: {
|
||||||
|
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Troubleshooting");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
#include "backend/boxartmanager.h"
|
#include "backend/boxartmanager.h"
|
||||||
#include "backend/computermanager.h"
|
#include "backend/computermanager.h"
|
||||||
#include "streaming/session.hpp"
|
#include "streaming/session.h"
|
||||||
|
|
||||||
#include <QAbstractListModel>
|
#include <QAbstractListModel>
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
#include "computermodel.h"
|
#include "computermodel.h"
|
||||||
|
|
||||||
|
#include <QThreadPool>
|
||||||
|
|
||||||
ComputerModel::ComputerModel(QObject* object)
|
ComputerModel::ComputerModel(QObject* object)
|
||||||
: QAbstractListModel(object) {}
|
: QAbstractListModel(object) {}
|
||||||
|
|
||||||
@@ -119,11 +121,27 @@ void ComputerModel::deleteComputer(int computerIndex)
|
|||||||
endRemoveRows();
|
endRemoveRows();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ComputerModel::wakeComputer(int computerIndex)
|
class DeferredWakeHostTask : public QRunnable
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
DeferredWakeHostTask(NvComputer* computer)
|
||||||
|
: m_Computer(computer) {}
|
||||||
|
|
||||||
|
void run()
|
||||||
|
{
|
||||||
|
m_Computer->wake();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
NvComputer* m_Computer;
|
||||||
|
};
|
||||||
|
|
||||||
|
void ComputerModel::wakeComputer(int computerIndex)
|
||||||
{
|
{
|
||||||
Q_ASSERT(computerIndex < m_Computers.count());
|
Q_ASSERT(computerIndex < m_Computers.count());
|
||||||
|
|
||||||
return m_Computers[computerIndex]->wake();
|
DeferredWakeHostTask* wakeTask = new DeferredWakeHostTask(m_Computers[computerIndex]);
|
||||||
|
QThreadPool::globalInstance()->start(wakeTask);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ComputerModel::pairComputer(int computerIndex, QString pin)
|
void ComputerModel::pairComputer(int computerIndex, QString pin)
|
||||||
@@ -135,7 +153,7 @@ void ComputerModel::pairComputer(int computerIndex, QString pin)
|
|||||||
|
|
||||||
void ComputerModel::handlePairingCompleted(NvComputer*, QString error)
|
void ComputerModel::handlePairingCompleted(NvComputer*, QString error)
|
||||||
{
|
{
|
||||||
emit pairingCompleted(error.isNull() ? QVariant() : error);
|
emit pairingCompleted(error.isEmpty() ? QVariant() : error);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ComputerModel::handleComputerStateChanged(NvComputer* computer)
|
void ComputerModel::handleComputerStateChanged(NvComputer* computer)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#include "backend/computermanager.h"
|
#include "backend/computermanager.h"
|
||||||
#include "streaming/session.hpp"
|
#include "streaming/session.h"
|
||||||
|
|
||||||
#include <QAbstractListModel>
|
#include <QAbstractListModel>
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ public:
|
|||||||
|
|
||||||
Q_INVOKABLE void pairComputer(int computerIndex, QString pin);
|
Q_INVOKABLE void pairComputer(int computerIndex, QString pin);
|
||||||
|
|
||||||
Q_INVOKABLE bool wakeComputer(int computerIndex);
|
Q_INVOKABLE void wakeComputer(int computerIndex);
|
||||||
|
|
||||||
Q_INVOKABLE Session* createSessionForCurrentGame(int computerIndex);
|
Q_INVOKABLE Session* createSessionForCurrentGame(int computerIndex);
|
||||||
|
|
||||||
|
|||||||
+285
-51
@@ -1,5 +1,6 @@
|
|||||||
import QtQuick 2.9
|
import QtQuick 2.9
|
||||||
import QtQuick.Controls 2.2
|
import QtQuick.Controls 2.2
|
||||||
|
import QtQuick.Dialogs 1.2
|
||||||
import QtQuick.Layouts 1.3
|
import QtQuick.Layouts 1.3
|
||||||
import QtQuick.Window 2.2
|
import QtQuick.Window 2.2
|
||||||
|
|
||||||
@@ -7,6 +8,7 @@ import QtQuick.Controls.Material 2.1
|
|||||||
|
|
||||||
import ComputerManager 1.0
|
import ComputerManager 1.0
|
||||||
import AutoUpdateChecker 1.0
|
import AutoUpdateChecker 1.0
|
||||||
|
import StreamingPreferences 1.0
|
||||||
|
|
||||||
ApplicationWindow {
|
ApplicationWindow {
|
||||||
property bool pollingActive: false
|
property bool pollingActive: false
|
||||||
@@ -16,30 +18,146 @@ ApplicationWindow {
|
|||||||
width: 1280
|
width: 1280
|
||||||
height: 600
|
height: 600
|
||||||
|
|
||||||
|
// Maximize the window by default when the stream is configured
|
||||||
|
// for full-screen or borderless windowed. This is ideal for TV
|
||||||
|
// setups where the user doesn't want a tiny window in the middle
|
||||||
|
// of their screen when starting Moonlight.
|
||||||
|
visibility: prefs.windowMode != StreamingPreferences.WM_WINDOWED ? "Maximized" : "Windowed"
|
||||||
|
|
||||||
Material.theme: Material.Dark
|
Material.theme: Material.Dark
|
||||||
Material.accent: Material.Purple
|
Material.accent: Material.Purple
|
||||||
|
|
||||||
StackView {
|
StreamingPreferences {
|
||||||
id: stackView
|
id: prefs
|
||||||
initialItem: "PcView.qml"
|
|
||||||
anchors.fill: parent
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onVisibilityChanged: {
|
StackView {
|
||||||
// We don't want to just use 'active' here because that will stop polling if
|
id: stackView
|
||||||
// we lose focus, which might be overzealous for users with multiple screens
|
initialItem: initialView
|
||||||
// where we may be clearly visible on the other display. Ideally we'll poll
|
anchors.fill: parent
|
||||||
// only if the window is visible to the user (not if obscured by other windows),
|
focus: true
|
||||||
// but it seems difficult to do this portably.
|
|
||||||
var shouldPoll = visibility !== Window.Minimized && visibility !== Window.Hidden
|
|
||||||
|
|
||||||
if (shouldPoll && !pollingActive) {
|
onCurrentItemChanged: {
|
||||||
ComputerManager.startPolling()
|
// Ensure focus travels to the next view when going back
|
||||||
pollingActive = true
|
if (currentItem) {
|
||||||
|
currentItem.forceActiveFocus()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else if (!shouldPoll && pollingActive) {
|
|
||||||
ComputerManager.stopPollingAsync()
|
Keys.onEscapePressed: {
|
||||||
pollingActive = false
|
if (depth > 1) {
|
||||||
|
stackView.pop()
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
quitConfirmationDialog.open()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Keys.onBackPressed: {
|
||||||
|
if (depth > 1) {
|
||||||
|
stackView.pop()
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
quitConfirmationDialog.open()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Keys.onMenuPressed: {
|
||||||
|
settingsButton.clicked()
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is a keypress we've reserved for letting the
|
||||||
|
// SdlGamepadKeyNavigation object tell us to show settings
|
||||||
|
// when Menu is consumed by a focused control.
|
||||||
|
Keys.onHangupPressed: {
|
||||||
|
settingsButton.clicked()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This timer keeps us polling for 5 minutes of inactivity
|
||||||
|
// to allow the user to work with Moonlight on a second display
|
||||||
|
// while dealing with configuration issues. This will ensure
|
||||||
|
// machines come online even if the input focus isn't on Moonlight.
|
||||||
|
Timer {
|
||||||
|
id: inactivityTimer
|
||||||
|
interval: 5 * 60000
|
||||||
|
onTriggered: {
|
||||||
|
if (!active && pollingActive) {
|
||||||
|
ComputerManager.stopPollingAsync()
|
||||||
|
pollingActive = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onVisibleChanged: {
|
||||||
|
// When we become invisible while streaming is going on,
|
||||||
|
// stop polling immediately.
|
||||||
|
if (!visible) {
|
||||||
|
inactivityTimer.stop()
|
||||||
|
|
||||||
|
if (pollingActive) {
|
||||||
|
ComputerManager.stopPollingAsync()
|
||||||
|
pollingActive = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (active) {
|
||||||
|
// When we become visible and active again, start polling
|
||||||
|
inactivityTimer.stop()
|
||||||
|
|
||||||
|
// Restart polling if it was stopped
|
||||||
|
if (!pollingActive) {
|
||||||
|
ComputerManager.startPolling()
|
||||||
|
pollingActive = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onActiveChanged: {
|
||||||
|
if (active) {
|
||||||
|
// Stop the inactivity timer
|
||||||
|
inactivityTimer.stop()
|
||||||
|
|
||||||
|
// Restart polling if it was stopped
|
||||||
|
if (!pollingActive) {
|
||||||
|
ComputerManager.startPolling()
|
||||||
|
pollingActive = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Start the inactivity timer to stop polling
|
||||||
|
// if focus does not return within a few minutes.
|
||||||
|
inactivityTimer.restart()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
property bool initialized: false
|
||||||
|
|
||||||
|
// BUG: Using onAfterSynchronizing: here causes very strange
|
||||||
|
// failures on Linux. Many shaders fail to compile and we
|
||||||
|
// eventually segfault deep inside the Qt OpenGL code.
|
||||||
|
onAfterRendering: {
|
||||||
|
// We use this callback to trigger dialog display because
|
||||||
|
// it only happens once the window is fully constructed.
|
||||||
|
// Doing it earlier can lead to the dialog appearing behind
|
||||||
|
// the window or otherwise without input focus.
|
||||||
|
if (!initialized) {
|
||||||
|
if (prefs.isRunningWayland()) {
|
||||||
|
waylandDialog.open()
|
||||||
|
}
|
||||||
|
else if (prefs.isWow64()) {
|
||||||
|
wow64Dialog.open()
|
||||||
|
}
|
||||||
|
else if (!prefs.hasAnyHardwareAcceleration()) {
|
||||||
|
noHwDecoderDialog.open()
|
||||||
|
}
|
||||||
|
|
||||||
|
var unmappedGamepads = prefs.getUnmappedGamepads()
|
||||||
|
if (unmappedGamepads) {
|
||||||
|
unmappedGamepadDialog.unmappedGamepads = unmappedGamepads
|
||||||
|
unmappedGamepadDialog.open()
|
||||||
|
}
|
||||||
|
|
||||||
|
initialized = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,22 +179,33 @@ ApplicationWindow {
|
|||||||
|
|
||||||
header: ToolBar {
|
header: ToolBar {
|
||||||
id: toolBar
|
id: toolBar
|
||||||
|
anchors.topMargin: 5
|
||||||
|
anchors.bottomMargin: 5
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
spacing: 20
|
spacing: 20
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
|
||||||
ToolButton {
|
NavigableToolButton {
|
||||||
// Only make the button visible if the user has navigated somewhere.
|
// Only make the button visible if the user has navigated somewhere.
|
||||||
visible: stackView.depth > 1
|
visible: stackView.depth > 1
|
||||||
|
|
||||||
icon.source: "qrc:/res/arrow_left.svg"
|
// FIXME: We're using an Image here rather than icon.source because
|
||||||
|
// icons don't work on Qt 5.9 LTS.
|
||||||
|
Image {
|
||||||
|
source: "qrc:/res/arrow_left.svg"
|
||||||
|
anchors.centerIn: parent
|
||||||
|
sourceSize {
|
||||||
|
// The icon should be square so use the height as the width too
|
||||||
|
width: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
|
||||||
|
height: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onClicked: stackView.pop()
|
onClicked: stackView.pop()
|
||||||
|
|
||||||
Menu {
|
Keys.onDownPressed: {
|
||||||
id: backButton
|
stackView.currentItem.forceActiveFocus(Qt.TabFocus)
|
||||||
x: parent.width - width
|
|
||||||
transformOrigin: Menu.TopRight
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,12 +219,19 @@ ApplicationWindow {
|
|||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
}
|
}
|
||||||
|
|
||||||
ToolButton {
|
NavigableToolButton {
|
||||||
property string browserUrl: ""
|
property string browserUrl: ""
|
||||||
|
|
||||||
id: updateButton
|
id: updateButton
|
||||||
icon.source: "qrc:/res/update.svg"
|
|
||||||
icon.color: "#0BF200"
|
Image {
|
||||||
|
source: "qrc:/res/update.svg"
|
||||||
|
anchors.centerIn: parent
|
||||||
|
sourceSize {
|
||||||
|
width: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
|
||||||
|
height: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ToolTip.delay: 1000
|
ToolTip.delay: 1000
|
||||||
ToolTip.timeout: 3000
|
ToolTip.timeout: 3000
|
||||||
@@ -108,12 +244,6 @@ ApplicationWindow {
|
|||||||
|
|
||||||
onClicked: Qt.openUrlExternally(browserUrl);
|
onClicked: Qt.openUrlExternally(browserUrl);
|
||||||
|
|
||||||
Menu {
|
|
||||||
x: parent.width
|
|
||||||
transformOrigin: Menu.TopRight
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function updateAvailable(url)
|
function updateAvailable(url)
|
||||||
{
|
{
|
||||||
updateButton.browserUrl = url
|
updateButton.browserUrl = url
|
||||||
@@ -124,26 +254,44 @@ ApplicationWindow {
|
|||||||
AutoUpdateChecker.onUpdateAvailable.connect(updateAvailable)
|
AutoUpdateChecker.onUpdateAvailable.connect(updateAvailable)
|
||||||
AutoUpdateChecker.start()
|
AutoUpdateChecker.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Keys.onDownPressed: {
|
||||||
|
stackView.currentItem.forceActiveFocus(Qt.TabFocus)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ToolButton {
|
NavigableToolButton {
|
||||||
icon.source: "qrc:/res/question_mark.svg"
|
id: helpButton
|
||||||
|
|
||||||
|
Image {
|
||||||
|
source: "qrc:/res/question_mark.svg"
|
||||||
|
anchors.centerIn: parent
|
||||||
|
sourceSize {
|
||||||
|
width: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
|
||||||
|
height: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ToolTip.delay: 1000
|
ToolTip.delay: 1000
|
||||||
ToolTip.timeout: 3000
|
ToolTip.timeout: 3000
|
||||||
ToolTip.visible: hovered
|
ToolTip.visible: hovered
|
||||||
ToolTip.text: "Help"
|
ToolTip.text: "Help" + (helpShortcut.nativeText ? (" ("+helpShortcut.nativeText+")") : "")
|
||||||
|
|
||||||
|
Shortcut {
|
||||||
|
id: helpShortcut
|
||||||
|
sequence: StandardKey.HelpContents
|
||||||
|
onActivated: helpButton.clicked()
|
||||||
|
}
|
||||||
|
|
||||||
// TODO need to make sure browser is brought to foreground.
|
// TODO need to make sure browser is brought to foreground.
|
||||||
onClicked: Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Setup-Guide");
|
onClicked: Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Setup-Guide");
|
||||||
|
|
||||||
Menu {
|
Keys.onDownPressed: {
|
||||||
x: parent.width
|
stackView.currentItem.forceActiveFocus(Qt.TabFocus)
|
||||||
transformOrigin: Menu.TopRight
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ToolButton {
|
NavigableToolButton {
|
||||||
// TODO: Implement gamepad mapping then unhide this button
|
// TODO: Implement gamepad mapping then unhide this button
|
||||||
visible: false
|
visible: false
|
||||||
|
|
||||||
@@ -152,29 +300,115 @@ ApplicationWindow {
|
|||||||
ToolTip.visible: hovered
|
ToolTip.visible: hovered
|
||||||
ToolTip.text: "Gamepad Mapper"
|
ToolTip.text: "Gamepad Mapper"
|
||||||
|
|
||||||
icon.source: "qrc:/res/ic_videogame_asset_white_48px.svg"
|
Image {
|
||||||
|
source: "qrc:/res/ic_videogame_asset_white_48px.svg"
|
||||||
|
anchors.centerIn: parent
|
||||||
|
sourceSize {
|
||||||
|
width: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
|
||||||
|
height: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onClicked: navigateTo("qrc:/gui/GamepadMapper.qml", "Gamepad Mapping")
|
onClicked: navigateTo("qrc:/gui/GamepadMapper.qml", "Gamepad Mapping")
|
||||||
|
|
||||||
Menu {
|
Keys.onDownPressed: {
|
||||||
x: parent.width
|
stackView.currentItem.forceActiveFocus(Qt.TabFocus)
|
||||||
transformOrigin: Menu.TopRight
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ToolButton {
|
NavigableToolButton {
|
||||||
icon.source: "qrc:/res/settings.svg"
|
id: settingsButton
|
||||||
|
|
||||||
|
Image {
|
||||||
|
source: "qrc:/res/settings.svg"
|
||||||
|
anchors.centerIn: parent
|
||||||
|
sourceSize {
|
||||||
|
width: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
|
||||||
|
height: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onClicked: navigateTo("qrc:/gui/SettingsView.qml", "Settings")
|
onClicked: navigateTo("qrc:/gui/SettingsView.qml", "Settings")
|
||||||
|
|
||||||
|
Keys.onDownPressed: {
|
||||||
|
stackView.currentItem.forceActiveFocus(Qt.TabFocus)
|
||||||
|
}
|
||||||
|
|
||||||
|
Shortcut {
|
||||||
|
id: settingsShortcut
|
||||||
|
sequence: StandardKey.Preferences
|
||||||
|
onActivated: settingsButton.clicked()
|
||||||
|
}
|
||||||
|
|
||||||
ToolTip.delay: 1000
|
ToolTip.delay: 1000
|
||||||
ToolTip.timeout: 3000
|
ToolTip.timeout: 3000
|
||||||
ToolTip.visible: hovered
|
ToolTip.visible: hovered
|
||||||
ToolTip.text: "Settings"
|
ToolTip.text: "Settings" + (settingsShortcut.nativeText ? (" ("+settingsShortcut.nativeText+")") : "")
|
||||||
|
|
||||||
Menu {
|
|
||||||
x: parent.width
|
|
||||||
transformOrigin: Menu.TopRight
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MessageDialog {
|
||||||
|
id: noHwDecoderDialog
|
||||||
|
modality:Qt.WindowModal
|
||||||
|
icon: StandardIcon.Warning
|
||||||
|
standardButtons: StandardButton.Ok | StandardButton.Help
|
||||||
|
text: "No functioning hardware accelerated H.264 video decoder was detected by Moonlight. " +
|
||||||
|
"Your streaming performance may be severely degraded in this configuration. " +
|
||||||
|
"Click the Help button for more information on solving this problem."
|
||||||
|
onHelp: {
|
||||||
|
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Fixing-Hardware-Decoding-Problems");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MessageDialog {
|
||||||
|
id: waylandDialog
|
||||||
|
modality:Qt.WindowModal
|
||||||
|
icon: StandardIcon.Warning
|
||||||
|
standardButtons: StandardButton.Ok | StandardButton.Help
|
||||||
|
text: "Moonlight does not support hardware acceleration on Wayland. Continuing on Wayland may result in poor streaming performance. " +
|
||||||
|
"Please switch to an X session for optimal performance."
|
||||||
|
onHelp: {
|
||||||
|
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Fixing-Hardware-Decoding-Problems");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MessageDialog {
|
||||||
|
id: wow64Dialog
|
||||||
|
modality:Qt.WindowModal
|
||||||
|
icon: StandardIcon.Warning
|
||||||
|
standardButtons: StandardButton.Ok | StandardButton.Cancel
|
||||||
|
text: "This PC is running a 64-bit version of Windows. Please download the x64 version of Moonlight for the best streaming performance."
|
||||||
|
onAccepted: {
|
||||||
|
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-qt/releases");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MessageDialog {
|
||||||
|
id: unmappedGamepadDialog
|
||||||
|
property string unmappedGamepads : ""
|
||||||
|
modality:Qt.WindowModal
|
||||||
|
icon: StandardIcon.Warning
|
||||||
|
standardButtons: StandardButton.Ok | StandardButton.Help
|
||||||
|
text: "Moonlight detected gamepads without a proper mapping. " +
|
||||||
|
"The following gamepads will not function until this is resolved: " + unmappedGamepads + "\n\n" +
|
||||||
|
"Click the Help button for information on how to map your gamepads."
|
||||||
|
onHelp: {
|
||||||
|
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Gamepad-Mapping");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This dialog appears when quitting via keyboard or gamepad button
|
||||||
|
MessageDialog {
|
||||||
|
id: quitConfirmationDialog
|
||||||
|
modality:Qt.WindowModal
|
||||||
|
icon: StandardIcon.Warning
|
||||||
|
standardButtons: StandardButton.Yes | StandardButton.No
|
||||||
|
text: "Are you sure you want to quit?"
|
||||||
|
|
||||||
|
onYes: Qt.quit()
|
||||||
|
|
||||||
|
// For keyboard/gamepad navigation
|
||||||
|
onAccepted: Qt.quit()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,266 @@
|
|||||||
|
#include "sdlgamepadkeynavigation.h"
|
||||||
|
|
||||||
|
#include <QKeyEvent>
|
||||||
|
#include <QGuiApplication>
|
||||||
|
#include <QWindow>
|
||||||
|
|
||||||
|
#include "settings/mappingmanager.h"
|
||||||
|
|
||||||
|
#define AXIS_NAVIGATION_REPEAT_DELAY 150
|
||||||
|
|
||||||
|
SdlGamepadKeyNavigation::SdlGamepadKeyNavigation()
|
||||||
|
: m_Enabled(false),
|
||||||
|
m_SettingsMode(false),
|
||||||
|
m_LastAxisNavigationEventTime(0)
|
||||||
|
{
|
||||||
|
m_PollingTimer = new QTimer(this);
|
||||||
|
connect(m_PollingTimer, SIGNAL(timeout()), this, SLOT(onPollingTimerFired()));
|
||||||
|
}
|
||||||
|
|
||||||
|
SdlGamepadKeyNavigation::~SdlGamepadKeyNavigation()
|
||||||
|
{
|
||||||
|
disable();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SdlGamepadKeyNavigation::enable()
|
||||||
|
{
|
||||||
|
if (m_Enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We have to initialize and uninitialize this in enable()/disable()
|
||||||
|
// because we need to get out of the way of the Session class. If it
|
||||||
|
// doesn't get to reinitialize the GC subsystem, it won't get initial
|
||||||
|
// arrival events. Additionally, there's a race condition between
|
||||||
|
// our QML objects being destroyed and SDL being deinitialized that
|
||||||
|
// this solves too.
|
||||||
|
if (SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) != 0) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) failed: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
MappingManager mappingManager;
|
||||||
|
mappingManager.applyMappings();
|
||||||
|
|
||||||
|
// Drop all pending gamepad add events. SDL will generate these for us
|
||||||
|
// on first init of the GC subsystem. We can't depend on them due to
|
||||||
|
// overlapping lifetimes of SdlGamepadKeyNavigation instances, so we
|
||||||
|
// will attach ourselves.
|
||||||
|
SDL_PumpEvents();
|
||||||
|
SDL_FlushEvent(SDL_CONTROLLERDEVICEADDED);
|
||||||
|
|
||||||
|
// Open all currently attached game controllers
|
||||||
|
for (int i = 0; i < SDL_NumJoysticks(); i++) {
|
||||||
|
if (SDL_IsGameController(i)) {
|
||||||
|
SDL_GameController* gc = SDL_GameControllerOpen(i);
|
||||||
|
if (gc != nullptr) {
|
||||||
|
m_Gamepads.append(gc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Poll every 50 ms for a new joystick event
|
||||||
|
m_PollingTimer->start(50);
|
||||||
|
|
||||||
|
m_Enabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SdlGamepadKeyNavigation::disable()
|
||||||
|
{
|
||||||
|
if (!m_Enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_PollingTimer->stop();
|
||||||
|
|
||||||
|
while (!m_Gamepads.isEmpty()) {
|
||||||
|
SDL_GameControllerClose(m_Gamepads[0]);
|
||||||
|
m_Gamepads.removeAt(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER);
|
||||||
|
|
||||||
|
m_Enabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SdlGamepadKeyNavigation::onPollingTimerFired()
|
||||||
|
{
|
||||||
|
SDL_Event event;
|
||||||
|
|
||||||
|
while (SDL_PollEvent(&event)) {
|
||||||
|
switch (event.type) {
|
||||||
|
case SDL_CONTROLLERBUTTONDOWN:
|
||||||
|
case SDL_CONTROLLERBUTTONUP:
|
||||||
|
{
|
||||||
|
QEvent::Type type =
|
||||||
|
event.type == SDL_CONTROLLERBUTTONDOWN ?
|
||||||
|
QEvent::Type::KeyPress : QEvent::Type::KeyRelease;
|
||||||
|
|
||||||
|
switch (event.cbutton.button) {
|
||||||
|
case SDL_CONTROLLER_BUTTON_DPAD_UP:
|
||||||
|
if (m_SettingsMode) {
|
||||||
|
// Back-tab
|
||||||
|
sendKey(type, Qt::Key_Tab, Qt::ShiftModifier);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
sendKey(type, Qt::Key_Up);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case SDL_CONTROLLER_BUTTON_DPAD_DOWN:
|
||||||
|
if (m_SettingsMode) {
|
||||||
|
sendKey(type, Qt::Key_Tab);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
sendKey(type, Qt::Key_Down);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case SDL_CONTROLLER_BUTTON_DPAD_LEFT:
|
||||||
|
sendKey(type, Qt::Key_Left);
|
||||||
|
if (m_SettingsMode) {
|
||||||
|
// Some settings controls respond to left/right (like the slider)
|
||||||
|
// and others respond to up/down (like combo boxes). They seem to
|
||||||
|
// be mutually exclusive though so let's just send both.
|
||||||
|
sendKey(type, Qt::Key_Up);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case SDL_CONTROLLER_BUTTON_DPAD_RIGHT:
|
||||||
|
sendKey(type, Qt::Key_Right);
|
||||||
|
if (m_SettingsMode) {
|
||||||
|
// Some settings controls respond to left/right (like the slider)
|
||||||
|
// and others respond to up/down (like combo boxes). They seem to
|
||||||
|
// be mutually exclusive though so let's just send both.
|
||||||
|
sendKey(type, Qt::Key_Down);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case SDL_CONTROLLER_BUTTON_A:
|
||||||
|
if (m_SettingsMode) {
|
||||||
|
sendKey(type, Qt::Key_Space);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
sendKey(type, Qt::Key_Return);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case SDL_CONTROLLER_BUTTON_B:
|
||||||
|
sendKey(type, Qt::Key_Escape);
|
||||||
|
break;
|
||||||
|
case SDL_CONTROLLER_BUTTON_X:
|
||||||
|
sendKey(type, Qt::Key_Menu);
|
||||||
|
break;
|
||||||
|
case SDL_CONTROLLER_BUTTON_Y:
|
||||||
|
case SDL_CONTROLLER_BUTTON_START:
|
||||||
|
// HACK: We use this keycode to inform main.qml
|
||||||
|
// to show the settings when Key_Menu is handled
|
||||||
|
// by the control in focus.
|
||||||
|
sendKey(type, Qt::Key_Hangup);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case SDL_CONTROLLERDEVICEADDED:
|
||||||
|
SDL_GameController* gc = SDL_GameControllerOpen(event.cdevice.which);
|
||||||
|
if (gc != nullptr) {
|
||||||
|
m_Gamepads.append(gc);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle analog sticks by polling
|
||||||
|
for (auto gc : m_Gamepads) {
|
||||||
|
short leftX = SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_LEFTX);
|
||||||
|
short leftY = SDL_GameControllerGetAxis(gc, SDL_CONTROLLER_AXIS_LEFTY);
|
||||||
|
if (SDL_GetTicks() - m_LastAxisNavigationEventTime < AXIS_NAVIGATION_REPEAT_DELAY) {
|
||||||
|
// Do nothing
|
||||||
|
}
|
||||||
|
else if (leftY < -30000) {
|
||||||
|
if (m_SettingsMode) {
|
||||||
|
// Back-tab
|
||||||
|
sendKey(QEvent::Type::KeyPress, Qt::Key_Tab, Qt::ShiftModifier);
|
||||||
|
sendKey(QEvent::Type::KeyRelease, Qt::Key_Tab, Qt::ShiftModifier);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
sendKey(QEvent::Type::KeyPress, Qt::Key_Up);
|
||||||
|
sendKey(QEvent::Type::KeyRelease, Qt::Key_Up);
|
||||||
|
}
|
||||||
|
|
||||||
|
m_LastAxisNavigationEventTime = SDL_GetTicks();
|
||||||
|
}
|
||||||
|
else if (leftY > 30000) {
|
||||||
|
if (m_SettingsMode) {
|
||||||
|
sendKey(QEvent::Type::KeyPress, Qt::Key_Tab);
|
||||||
|
sendKey(QEvent::Type::KeyRelease, Qt::Key_Tab);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
sendKey(QEvent::Type::KeyPress, Qt::Key_Down);
|
||||||
|
sendKey(QEvent::Type::KeyRelease, Qt::Key_Down);
|
||||||
|
}
|
||||||
|
|
||||||
|
m_LastAxisNavigationEventTime = SDL_GetTicks();
|
||||||
|
}
|
||||||
|
else if (leftX < -30000) {
|
||||||
|
sendKey(QEvent::Type::KeyPress, Qt::Key_Left);
|
||||||
|
sendKey(QEvent::Type::KeyRelease, Qt::Key_Left);
|
||||||
|
if (m_SettingsMode) {
|
||||||
|
// Some settings controls respond to left/right (like the slider)
|
||||||
|
// and others respond to up/down (like combo boxes). They seem to
|
||||||
|
// be mutually exclusive though so let's just send both.
|
||||||
|
sendKey(QEvent::Type::KeyPress, Qt::Key_Up);
|
||||||
|
sendKey(QEvent::Type::KeyRelease, Qt::Key_Up);
|
||||||
|
}
|
||||||
|
|
||||||
|
m_LastAxisNavigationEventTime = SDL_GetTicks();
|
||||||
|
}
|
||||||
|
else if (leftX > 30000) {
|
||||||
|
sendKey(QEvent::Type::KeyPress, Qt::Key_Right);
|
||||||
|
sendKey(QEvent::Type::KeyRelease, Qt::Key_Right);
|
||||||
|
if (m_SettingsMode) {
|
||||||
|
// Some settings controls respond to left/right (like the slider)
|
||||||
|
// and others respond to up/down (like combo boxes). They seem to
|
||||||
|
// be mutually exclusive though so let's just send both.
|
||||||
|
sendKey(QEvent::Type::KeyPress, Qt::Key_Down);
|
||||||
|
sendKey(QEvent::Type::KeyRelease, Qt::Key_Down);
|
||||||
|
}
|
||||||
|
|
||||||
|
m_LastAxisNavigationEventTime = SDL_GetTicks();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SdlGamepadKeyNavigation::sendKey(QEvent::Type type, Qt::Key key, Qt::KeyboardModifiers modifiers)
|
||||||
|
{
|
||||||
|
QGuiApplication* app = static_cast<QGuiApplication*>(QGuiApplication::instance());
|
||||||
|
QWindow* focusWindow = app->focusWindow();
|
||||||
|
if (focusWindow != nullptr) {
|
||||||
|
QKeyEvent keyPressEvent(type, key, modifiers);
|
||||||
|
app->sendEvent(focusWindow, &keyPressEvent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SdlGamepadKeyNavigation::setSettingsMode(bool settingsMode)
|
||||||
|
{
|
||||||
|
m_SettingsMode = settingsMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
int SdlGamepadKeyNavigation::getConnectedGamepads()
|
||||||
|
{
|
||||||
|
if (SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) != 0) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) failed: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int count = 0;
|
||||||
|
for (int i = 0; i < SDL_NumJoysticks(); i++) {
|
||||||
|
if (SDL_IsGameController(i)) {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER);
|
||||||
|
return count;
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QTimer>
|
||||||
|
#include <QEvent>
|
||||||
|
|
||||||
|
#include <SDL.h>
|
||||||
|
|
||||||
|
class SdlGamepadKeyNavigation : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
SdlGamepadKeyNavigation();
|
||||||
|
|
||||||
|
~SdlGamepadKeyNavigation();
|
||||||
|
|
||||||
|
Q_INVOKABLE void enable();
|
||||||
|
|
||||||
|
Q_INVOKABLE void disable();
|
||||||
|
|
||||||
|
Q_INVOKABLE void setSettingsMode(bool settingsMode);
|
||||||
|
|
||||||
|
Q_INVOKABLE int getConnectedGamepads();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void sendKey(QEvent::Type type, Qt::Key key, Qt::KeyboardModifiers modifiers = Qt::NoModifier);
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void onPollingTimerFired();
|
||||||
|
|
||||||
|
private:
|
||||||
|
QTimer* m_PollingTimer;
|
||||||
|
QList<SDL_GameController*> m_Gamepads;
|
||||||
|
bool m_Enabled;
|
||||||
|
bool m_SettingsMode;
|
||||||
|
Uint32 m_LastAxisNavigationEventTime;
|
||||||
|
};
|
||||||
+154
-11
@@ -1,5 +1,6 @@
|
|||||||
#include <QGuiApplication>
|
#include <QGuiApplication>
|
||||||
#include <QQmlApplicationEngine>
|
#include <QQmlApplicationEngine>
|
||||||
|
#include <QQmlContext>
|
||||||
#include <QIcon>
|
#include <QIcon>
|
||||||
#include <QQuickStyle>
|
#include <QQuickStyle>
|
||||||
#include <QMutex>
|
#include <QMutex>
|
||||||
@@ -11,12 +12,23 @@
|
|||||||
#define SDL_MAIN_HANDLED
|
#define SDL_MAIN_HANDLED
|
||||||
#include <SDL.h>
|
#include <SDL.h>
|
||||||
|
|
||||||
|
#ifdef HAVE_FFMPEG
|
||||||
|
#include "streaming/video/ffmpeg.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef Q_OS_WIN32
|
||||||
|
#include "antihookingprotection.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include "cli/startstream.h"
|
||||||
|
#include "cli/commandlineparser.h"
|
||||||
#include "path.h"
|
#include "path.h"
|
||||||
#include "gui/computermodel.h"
|
#include "gui/computermodel.h"
|
||||||
#include "gui/appmodel.h"
|
#include "gui/appmodel.h"
|
||||||
#include "backend/autoupdatechecker.h"
|
#include "backend/autoupdatechecker.h"
|
||||||
#include "streaming/session.hpp"
|
#include "streaming/session.h"
|
||||||
#include "settings/streamingpreferences.h"
|
#include "settings/streamingpreferences.h"
|
||||||
|
#include "gui/sdlgamepadkeynavigation.h"
|
||||||
|
|
||||||
#if !defined(QT_DEBUG) && defined(Q_OS_WIN32)
|
#if !defined(QT_DEBUG) && defined(Q_OS_WIN32)
|
||||||
// Log to file for release Windows builds
|
// Log to file for release Windows builds
|
||||||
@@ -62,7 +74,8 @@ void logToLoggerStream(QString& message)
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
s_LoggerStream << message << endl;
|
s_LoggerStream << message;
|
||||||
|
s_LoggerStream.flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
void sdlLogToDiskHandler(void*, int category, SDL_LogPriority priority, const char* message)
|
void sdlLogToDiskHandler(void*, int category, SDL_LogPriority priority, const char* message)
|
||||||
@@ -94,7 +107,7 @@ void sdlLogToDiskHandler(void*, int category, SDL_LogPriority priority, const ch
|
|||||||
}
|
}
|
||||||
|
|
||||||
QTime logTime = QTime::fromMSecsSinceStartOfDay(s_LoggerTime.elapsed());
|
QTime logTime = QTime::fromMSecsSinceStartOfDay(s_LoggerTime.elapsed());
|
||||||
QString txt = QString("%1 - SDL %2 (%3): %4").arg(logTime.toString()).arg(priorityTxt).arg(category).arg(message);
|
QString txt = QString("%1 - SDL %2 (%3): %4\n").arg(logTime.toString()).arg(priorityTxt).arg(category).arg(message);
|
||||||
|
|
||||||
logToLoggerStream(txt);
|
logToLoggerStream(txt);
|
||||||
}
|
}
|
||||||
@@ -122,15 +135,99 @@ void qtLogToDiskHandler(QtMsgType type, const QMessageLogContext&, const QString
|
|||||||
}
|
}
|
||||||
|
|
||||||
QTime logTime = QTime::fromMSecsSinceStartOfDay(s_LoggerTime.elapsed());
|
QTime logTime = QTime::fromMSecsSinceStartOfDay(s_LoggerTime.elapsed());
|
||||||
QString txt = QString("%1 - Qt %2: %3").arg(logTime.toString()).arg(typeTxt).arg(msg);
|
QString txt = QString("%1 - Qt %2: %3\n").arg(logTime.toString()).arg(typeTxt).arg(msg);
|
||||||
|
|
||||||
|
logToLoggerStream(txt);
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef HAVE_FFMPEG
|
||||||
|
|
||||||
|
void ffmpegLogToDiskHandler(void* ptr, int level, const char* fmt, va_list vl)
|
||||||
|
{
|
||||||
|
char lineBuffer[1024];
|
||||||
|
static int printPrefix = 1;
|
||||||
|
|
||||||
|
if ((level & 0xFF) > av_log_get_level()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
av_log_format_line(ptr, level, fmt, vl, lineBuffer, sizeof(lineBuffer), &printPrefix);
|
||||||
|
|
||||||
|
QTime logTime = QTime::fromMSecsSinceStartOfDay(s_LoggerTime.elapsed());
|
||||||
|
QString txt = QString("%1 - FFmpeg: %2").arg(logTime.toString()).arg(lineBuffer);
|
||||||
|
|
||||||
logToLoggerStream(txt);
|
logToLoggerStream(txt);
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef Q_OS_WIN32
|
||||||
|
#define WIN32_LEAN_AND_MEAN
|
||||||
|
#include <Windows.h>
|
||||||
|
#include <DbgHelp.h>
|
||||||
|
|
||||||
|
static UINT s_HitUnhandledException = 0;
|
||||||
|
|
||||||
|
LONG WINAPI UnhandledExceptionHandler(struct _EXCEPTION_POINTERS *ExceptionInfo)
|
||||||
|
{
|
||||||
|
// Only write a dump for the first unhandled exception
|
||||||
|
if (InterlockedCompareExchange(&s_HitUnhandledException, 1, 0) != 0) {
|
||||||
|
return EXCEPTION_CONTINUE_SEARCH;
|
||||||
|
}
|
||||||
|
|
||||||
|
WCHAR dmpFileName[MAX_PATH];
|
||||||
|
swprintf_s(dmpFileName, L"%ls\\Moonlight-%I64u.dmp",
|
||||||
|
(PWCHAR)QDir::toNativeSeparators(Path::getLogDir()).utf16(), QDateTime::currentSecsSinceEpoch());
|
||||||
|
QString qDmpFileName = QString::fromUtf16((unsigned short*)dmpFileName);
|
||||||
|
HANDLE dumpHandle = CreateFileW(dmpFileName, GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
|
||||||
|
if (dumpHandle != INVALID_HANDLE_VALUE) {
|
||||||
|
MINIDUMP_EXCEPTION_INFORMATION info;
|
||||||
|
|
||||||
|
info.ThreadId = GetCurrentThreadId();
|
||||||
|
info.ExceptionPointers = ExceptionInfo;
|
||||||
|
info.ClientPointers = FALSE;
|
||||||
|
|
||||||
|
DWORD typeFlags = MiniDumpWithIndirectlyReferencedMemory |
|
||||||
|
MiniDumpIgnoreInaccessibleMemory |
|
||||||
|
MiniDumpWithUnloadedModules |
|
||||||
|
MiniDumpWithThreadInfo;
|
||||||
|
|
||||||
|
if (MiniDumpWriteDump(GetCurrentProcess(),
|
||||||
|
GetCurrentProcessId(),
|
||||||
|
dumpHandle,
|
||||||
|
(MINIDUMP_TYPE)typeFlags,
|
||||||
|
&info,
|
||||||
|
nullptr,
|
||||||
|
nullptr)) {
|
||||||
|
qCritical() << "Unhandled exception! Minidump written to:" << qDmpFileName;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
qCritical() << "Unhandled exception! Failed to write dump:" << GetLastError();
|
||||||
|
}
|
||||||
|
|
||||||
|
CloseHandle(dumpHandle);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
qCritical() << "Unhandled exception! Failed to open dump file:" << qDmpFileName << "with error" << GetLastError();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Let the program crash and WER collect a dump
|
||||||
|
return EXCEPTION_CONTINUE_SEARCH;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
int main(int argc, char *argv[])
|
int main(int argc, char *argv[])
|
||||||
{
|
{
|
||||||
|
// Set these here to allow us to use the default QSettings constructor.
|
||||||
|
// These also ensure that our cache directory is named correctly. As such,
|
||||||
|
// it is critical that these be called before Path::initialize().
|
||||||
|
QCoreApplication::setOrganizationName("Moonlight Game Streaming Project");
|
||||||
|
QCoreApplication::setOrganizationDomain("moonlight-stream.com");
|
||||||
|
QCoreApplication::setApplicationName("Moonlight");
|
||||||
|
|
||||||
if (QFile(QDir::currentPath() + "/portable.dat").exists()) {
|
if (QFile(QDir::currentPath() + "/portable.dat").exists()) {
|
||||||
qInfo() << "Running in portable mode from:" << QDir::currentPath();
|
qInfo() << "Running in portable mode from:" << QDir::currentPath();
|
||||||
QSettings::setDefaultFormat(QSettings::IniFormat);
|
QSettings::setDefaultFormat(QSettings::IniFormat);
|
||||||
@@ -158,6 +255,22 @@ int main(int argc, char *argv[])
|
|||||||
s_LoggerTime.start();
|
s_LoggerTime.start();
|
||||||
qInstallMessageHandler(qtLogToDiskHandler);
|
qInstallMessageHandler(qtLogToDiskHandler);
|
||||||
SDL_LogSetOutputFunction(sdlLogToDiskHandler, nullptr);
|
SDL_LogSetOutputFunction(sdlLogToDiskHandler, nullptr);
|
||||||
|
|
||||||
|
#ifdef HAVE_FFMPEG
|
||||||
|
av_log_set_callback(ffmpegLogToDiskHandler);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef Q_OS_WIN32
|
||||||
|
// Create a crash dump when we crash on Windows
|
||||||
|
SetUnhandledExceptionFilter(UnhandledExceptionHandler);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef Q_OS_WIN32
|
||||||
|
// Force AntiHooking.dll to be statically imported and loaded
|
||||||
|
// by ntdll by calling a dummy function.
|
||||||
|
AntiHookingDummyImport();
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
|
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
|
||||||
@@ -166,10 +279,14 @@ int main(int argc, char *argv[])
|
|||||||
// password prompts on macOS.
|
// password prompts on macOS.
|
||||||
qputenv("QT_SSL_USE_TEMPORARY_KEYCHAIN", QByteArray("1"));
|
qputenv("QT_SSL_USE_TEMPORARY_KEYCHAIN", QByteArray("1"));
|
||||||
|
|
||||||
// Set these here to allow us to use the default QSettings constructor
|
#ifdef Q_OS_WIN32
|
||||||
QCoreApplication::setOrganizationName("Moonlight Game Streaming Project");
|
if (!qEnvironmentVariableIsSet("QT_OPENGL")) {
|
||||||
QCoreApplication::setOrganizationDomain("moonlight-stream.com");
|
// On Windows, use ANGLE so we don't have to load OpenGL
|
||||||
QCoreApplication::setApplicationName("Moonlight");
|
// user-mode drivers into our app. OGL drivers (especially Intel)
|
||||||
|
// seem to crash Moonlight far more often than DirectX.
|
||||||
|
qputenv("QT_OPENGL", "angle");
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
// Register custom metatypes for use in signals
|
// Register custom metatypes for use in signals
|
||||||
qRegisterMetaType<NvApp>("NvApp");
|
qRegisterMetaType<NvApp>("NvApp");
|
||||||
@@ -182,6 +299,7 @@ int main(int argc, char *argv[])
|
|||||||
qmlRegisterType<ComputerModel>("ComputerModel", 1, 0, "ComputerModel");
|
qmlRegisterType<ComputerModel>("ComputerModel", 1, 0, "ComputerModel");
|
||||||
qmlRegisterType<AppModel>("AppModel", 1, 0, "AppModel");
|
qmlRegisterType<AppModel>("AppModel", 1, 0, "AppModel");
|
||||||
qmlRegisterType<StreamingPreferences>("StreamingPreferences", 1, 0, "StreamingPreferences");
|
qmlRegisterType<StreamingPreferences>("StreamingPreferences", 1, 0, "StreamingPreferences");
|
||||||
|
qmlRegisterType<SdlGamepadKeyNavigation>("SdlGamepadKeyNavigation", 1, 0, "SdlGamepadKeyNavigation");
|
||||||
qmlRegisterUncreatableType<Session>("Session", 1, 0, "Session", "Session cannot be created from QML");
|
qmlRegisterUncreatableType<Session>("Session", 1, 0, "Session", "Session cannot be created from QML");
|
||||||
qmlRegisterSingletonType<ComputerManager>("ComputerManager", 1, 0,
|
qmlRegisterSingletonType<ComputerManager>("ComputerManager", 1, 0,
|
||||||
"ComputerManager",
|
"ComputerManager",
|
||||||
@@ -196,8 +314,31 @@ int main(int argc, char *argv[])
|
|||||||
|
|
||||||
QQuickStyle::setStyle("Material");
|
QQuickStyle::setStyle("Material");
|
||||||
|
|
||||||
// Load the main.qml file
|
|
||||||
QQmlApplicationEngine engine;
|
QQmlApplicationEngine engine;
|
||||||
|
QString initialView;
|
||||||
|
|
||||||
|
GlobalCommandLineParser parser;
|
||||||
|
switch (parser.parse(app.arguments())) {
|
||||||
|
case GlobalCommandLineParser::NormalStartRequested:
|
||||||
|
initialView = "PcView.qml";
|
||||||
|
break;
|
||||||
|
case GlobalCommandLineParser::StreamRequested:
|
||||||
|
{
|
||||||
|
initialView = "CliStartStreamSegue.qml";
|
||||||
|
StreamingPreferences* preferences = new StreamingPreferences(&app);
|
||||||
|
StreamCommandLineParser streamParser;
|
||||||
|
streamParser.parse(app.arguments(), preferences);
|
||||||
|
QString host = streamParser.getHost();
|
||||||
|
QString appName = streamParser.getAppName();
|
||||||
|
auto launcher = new CliStartStream::Launcher(host, appName, preferences, &app);
|
||||||
|
engine.rootContext()->setContextProperty("launcher", launcher);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
engine.rootContext()->setContextProperty("initialView", initialView);
|
||||||
|
|
||||||
|
// Load the main.qml file
|
||||||
engine.load(QUrl(QStringLiteral("qrc:/gui/main.qml")));
|
engine.load(QUrl(QStringLiteral("qrc:/gui/main.qml")));
|
||||||
if (engine.rootObjects().isEmpty())
|
if (engine.rootObjects().isEmpty())
|
||||||
return -1;
|
return -1;
|
||||||
@@ -209,6 +350,10 @@ int main(int argc, char *argv[])
|
|||||||
SDL_GetError());
|
SDL_GetError());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Use atexit() to ensure SDL_Quit() is called. This avoids
|
||||||
|
// racing with object destruction where SDL may be used.
|
||||||
|
atexit(SDL_Quit);
|
||||||
|
|
||||||
// Avoid the default behavior of changing the timer resolution to 1 ms.
|
// Avoid the default behavior of changing the timer resolution to 1 ms.
|
||||||
// We don't want this all the time that Moonlight is open. We will set
|
// We don't want this all the time that Moonlight is open. We will set
|
||||||
// it manually when we start streaming.
|
// it manually when we start streaming.
|
||||||
@@ -216,7 +361,5 @@ int main(int argc, char *argv[])
|
|||||||
|
|
||||||
int err = app.exec();
|
int err = app.exec();
|
||||||
|
|
||||||
SDL_Quit();
|
|
||||||
|
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,11 @@
|
|||||||
#include <QDir>
|
#include <QDir>
|
||||||
#include <QStandardPaths>
|
#include <QStandardPaths>
|
||||||
#include <QSettings>
|
#include <QSettings>
|
||||||
|
#include <QCoreApplication>
|
||||||
|
|
||||||
QString Path::s_LogDir;
|
QString Path::s_LogDir;
|
||||||
QString Path::s_BoxArtCacheDir;
|
QString Path::s_BoxArtCacheDir;
|
||||||
|
const QString Path::k_GameControllerMappingFile("gamecontrollerdb.txt");
|
||||||
|
|
||||||
QString Path::getLogDir()
|
QString Path::getLogDir()
|
||||||
{
|
{
|
||||||
@@ -20,6 +22,42 @@ QString Path::getBoxArtCacheDir()
|
|||||||
return s_BoxArtCacheDir;
|
return s_BoxArtCacheDir;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QString Path::getGamepadMappingFile()
|
||||||
|
{
|
||||||
|
QString candidatePath;
|
||||||
|
|
||||||
|
// Check the current directory first
|
||||||
|
candidatePath = QDir(QDir::currentPath()).absoluteFilePath(k_GameControllerMappingFile);
|
||||||
|
if (QFile::exists(candidatePath)) {
|
||||||
|
return candidatePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now check the data directories (for Linux, in particular)
|
||||||
|
candidatePath = QStandardPaths::locate(QStandardPaths::DataLocation, k_GameControllerMappingFile);
|
||||||
|
if (!candidatePath.isEmpty() && QFile::exists(candidatePath)) {
|
||||||
|
return candidatePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now try the directory of our app installation (for Windows, if current dir doesn't find it)
|
||||||
|
candidatePath = QDir(QCoreApplication::applicationDirPath()).absoluteFilePath(k_GameControllerMappingFile);
|
||||||
|
if (QFile::exists(candidatePath)) {
|
||||||
|
return candidatePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now try the Resources folder in our app bundle for macOS
|
||||||
|
QDir dir = QDir(QCoreApplication::applicationDirPath());
|
||||||
|
dir.cdUp();
|
||||||
|
dir.cd("Resources");
|
||||||
|
dir.filePath(k_GameControllerMappingFile);
|
||||||
|
candidatePath = dir.absoluteFilePath(k_GameControllerMappingFile);
|
||||||
|
if (QFile::exists(candidatePath)) {
|
||||||
|
return candidatePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Couldn't find it
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
|
||||||
void Path::initialize(bool portable)
|
void Path::initialize(bool portable)
|
||||||
{
|
{
|
||||||
if (portable) {
|
if (portable) {
|
||||||
|
|||||||
@@ -9,9 +9,12 @@ public:
|
|||||||
|
|
||||||
static QString getBoxArtCacheDir();
|
static QString getBoxArtCacheDir();
|
||||||
|
|
||||||
|
static QString getGamepadMappingFile();
|
||||||
|
|
||||||
static void initialize(bool portable);
|
static void initialize(bool portable);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static QString s_LogDir;
|
static QString s_LogDir;
|
||||||
static QString s_BoxArtCacheDir;
|
static QString s_BoxArtCacheDir;
|
||||||
|
static const QString k_GameControllerMappingFile;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,5 +7,10 @@
|
|||||||
<file>gui/StreamSegue.qml</file>
|
<file>gui/StreamSegue.qml</file>
|
||||||
<file>gui/GamepadMapper.qml</file>
|
<file>gui/GamepadMapper.qml</file>
|
||||||
<file>gui/QuitSegue.qml</file>
|
<file>gui/QuitSegue.qml</file>
|
||||||
|
<file>gui/NavigableToolButton.qml</file>
|
||||||
|
<file>gui/NavigableItemDelegate.qml</file>
|
||||||
|
<file>gui/NavigableMenuItem.qml</file>
|
||||||
|
<file>gui/CliStartStreamSegue.qml</file>
|
||||||
|
<file>gui/AutoResizingComboBox.qml</file>
|
||||||
</qresource>
|
</qresource>
|
||||||
</RCC>
|
</RCC>
|
||||||
|
|||||||
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="24" height="24" viewBox="0 0 24 24">
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="24" height="24" viewBox="0 0 24 24" fill="#0BF200">
|
||||||
<path d="M21,10.12H14.22L16.96,7.3C14.23,4.6 9.81,4.5 7.08,7.2C4.35,9.91 4.35,14.28 7.08,17C9.81,19.7 14.23,19.7 16.96,17C18.32,15.65 19,14.08 19,12.1H21C21,14.08 20.12,16.65 18.36,18.39C14.85,21.87 9.15,21.87 5.64,18.39C2.14,14.92 2.11,9.28 5.62,5.81C9.13,2.34 14.76,2.34 18.27,5.81L21,3V10.12M12.5,8V12.25L16,14.33L15.28,15.54L11,13V8H12.5Z" />
|
<path d="M21,10.12H14.22L16.96,7.3C14.23,4.6 9.81,4.5 7.08,7.2C4.35,9.91 4.35,14.28 7.08,17C9.81,19.7 14.23,19.7 16.96,17C18.32,15.65 19,14.08 19,12.1H21C21,14.08 20.12,16.65 18.36,18.39C14.85,21.87 9.15,21.87 5.64,18.39C2.14,14.92 2.11,9.28 5.62,5.81C9.13,2.34 14.76,2.34 18.27,5.81L21,3V10.12M12.5,8V12.25L16,14.33L15.28,15.54L11,13V8H12.5Z" />
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 484 B After Width: | Height: | Size: 499 B |
@@ -0,0 +1,108 @@
|
|||||||
|
#include "mappingmanager.h"
|
||||||
|
#include "path.h"
|
||||||
|
|
||||||
|
#include <QDir>
|
||||||
|
|
||||||
|
#include <SDL.h>
|
||||||
|
|
||||||
|
#define SER_GAMEPADMAPPING "gcmapping"
|
||||||
|
|
||||||
|
#define SER_GUID "guid"
|
||||||
|
#define SER_MAPPING "mapping"
|
||||||
|
|
||||||
|
MappingManager::MappingManager()
|
||||||
|
{
|
||||||
|
QSettings settings;
|
||||||
|
|
||||||
|
// First load existing saved mappings. This ensures the user's
|
||||||
|
// hints can always override the old data.
|
||||||
|
int mappingCount = settings.beginReadArray(SER_GAMEPADMAPPING);
|
||||||
|
for (int i = 0; i < mappingCount; i++) {
|
||||||
|
settings.setArrayIndex(i);
|
||||||
|
|
||||||
|
SdlGamepadMapping mapping(settings.value(SER_GUID).toString(), settings.value(SER_MAPPING).toString());
|
||||||
|
addMapping(mapping);
|
||||||
|
}
|
||||||
|
settings.endArray();
|
||||||
|
|
||||||
|
// Finally load mappings from SDL_HINT_GAMECONTROLLERCONFIG
|
||||||
|
QStringList sdlMappings = QString::fromLocal8Bit(SDL_GetHint(SDL_HINT_GAMECONTROLLERCONFIG)).split('\n', QString::SkipEmptyParts);
|
||||||
|
for (QString sdlMapping : sdlMappings) {
|
||||||
|
SdlGamepadMapping mapping(sdlMapping);
|
||||||
|
addMapping(mapping);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the updated mappings to settings
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
|
||||||
|
void MappingManager::save()
|
||||||
|
{
|
||||||
|
QSettings settings;
|
||||||
|
|
||||||
|
settings.remove(SER_GAMEPADMAPPING);
|
||||||
|
settings.beginWriteArray(SER_GAMEPADMAPPING);
|
||||||
|
QList<SdlGamepadMapping> mappings = m_Mappings.values();
|
||||||
|
for (int i = 0; i < mappings.count(); i++) {
|
||||||
|
settings.setArrayIndex(i);
|
||||||
|
|
||||||
|
settings.setValue(SER_GUID, mappings[i].getGuid());
|
||||||
|
settings.setValue(SER_MAPPING, mappings[i].getMapping());
|
||||||
|
}
|
||||||
|
settings.endArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
void MappingManager::applyMappings()
|
||||||
|
{
|
||||||
|
QString mappingFile = Path::getGamepadMappingFile();
|
||||||
|
if (!mappingFile.isEmpty()) {
|
||||||
|
std::string mappingFileNative = QDir::toNativeSeparators(mappingFile).toStdString();
|
||||||
|
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Loading gamepad mappings from: %s",
|
||||||
|
mappingFileNative.c_str());
|
||||||
|
|
||||||
|
int newMappings = SDL_GameControllerAddMappingsFromFile(mappingFileNative.c_str());
|
||||||
|
if (newMappings < 0) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Error loading gamepad mappings: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Loaded %d new gamepad mappings",
|
||||||
|
newMappings);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"No gamepad mapping file found");
|
||||||
|
}
|
||||||
|
|
||||||
|
QList<SdlGamepadMapping> mappings = m_Mappings.values();
|
||||||
|
for (const SdlGamepadMapping& mapping : mappings) {
|
||||||
|
QString sdlMappingString = mapping.getSdlMappingString();
|
||||||
|
int ret = SDL_GameControllerAddMapping(qPrintable(sdlMappingString));
|
||||||
|
if (ret < 0) {
|
||||||
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Unable to add mapping: %s",
|
||||||
|
qPrintable(sdlMappingString));
|
||||||
|
}
|
||||||
|
else if (ret == 1) {
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Loaded saved user mapping: %s",
|
||||||
|
qPrintable(sdlMappingString));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MappingManager::addMapping(QString mappingString)
|
||||||
|
{
|
||||||
|
SdlGamepadMapping mapping(mappingString);
|
||||||
|
addMapping(mapping);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MappingManager::addMapping(SdlGamepadMapping& mapping)
|
||||||
|
{
|
||||||
|
m_Mappings[mapping.getGuid()] = mapping;
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QSettings>
|
||||||
|
|
||||||
|
class SdlGamepadMapping
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
SdlGamepadMapping() {}
|
||||||
|
|
||||||
|
SdlGamepadMapping(QString string)
|
||||||
|
{
|
||||||
|
QStringList mapping = string.split(",");
|
||||||
|
if (!mapping.isEmpty()) {
|
||||||
|
m_Guid = mapping[0];
|
||||||
|
|
||||||
|
string.remove(0, m_Guid.length() + 1);
|
||||||
|
m_Mapping = string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SdlGamepadMapping(QString guid, QString mapping)
|
||||||
|
: m_Guid(guid),
|
||||||
|
m_Mapping(mapping)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
bool operator==(const SdlGamepadMapping& other) const
|
||||||
|
{
|
||||||
|
return m_Guid == other.m_Guid && m_Mapping == other.m_Mapping;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString getGuid() const
|
||||||
|
{
|
||||||
|
return m_Guid;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString getMapping() const
|
||||||
|
{
|
||||||
|
return m_Mapping;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString getSdlMappingString() const
|
||||||
|
{
|
||||||
|
if (m_Guid.isEmpty() || m_Mapping.isEmpty()) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return m_Guid + "," + m_Mapping;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
QString m_Guid;
|
||||||
|
QString m_Mapping;
|
||||||
|
};
|
||||||
|
|
||||||
|
class MappingManager
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
MappingManager();
|
||||||
|
|
||||||
|
void addMapping(QString gamepadString);
|
||||||
|
|
||||||
|
void addMapping(SdlGamepadMapping& gamepadMapping);
|
||||||
|
|
||||||
|
void applyMappings();
|
||||||
|
|
||||||
|
void save();
|
||||||
|
|
||||||
|
private:
|
||||||
|
QMap<QString, SdlGamepadMapping> m_Mappings;
|
||||||
|
};
|
||||||
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
#include "streamingpreferences.h"
|
#include "streamingpreferences.h"
|
||||||
#include "streaming/session.hpp"
|
#include "streaming/session.h"
|
||||||
|
#include "streaming/streamutils.h"
|
||||||
|
|
||||||
#include <QSettings>
|
#include <QSettings>
|
||||||
|
|
||||||
@@ -16,8 +17,13 @@
|
|||||||
#define SER_AUDIOCFG "audiocfg"
|
#define SER_AUDIOCFG "audiocfg"
|
||||||
#define SER_VIDEOCFG "videocfg"
|
#define SER_VIDEOCFG "videocfg"
|
||||||
#define SER_VIDEODEC "videodec"
|
#define SER_VIDEODEC "videodec"
|
||||||
|
#define SER_WINDOWMODE "windowmode"
|
||||||
|
#define SER_UNSUPPORTEDFPS "unsupportedfps"
|
||||||
|
#define SER_MDNS "mdns"
|
||||||
|
#define SER_MOUSEACCELERATION "mouseacceleration"
|
||||||
|
|
||||||
StreamingPreferences::StreamingPreferences()
|
StreamingPreferences::StreamingPreferences(QObject *parent)
|
||||||
|
: QObject(parent)
|
||||||
{
|
{
|
||||||
reload();
|
reload();
|
||||||
}
|
}
|
||||||
@@ -30,17 +36,23 @@ void StreamingPreferences::reload()
|
|||||||
height = settings.value(SER_HEIGHT, 720).toInt();
|
height = settings.value(SER_HEIGHT, 720).toInt();
|
||||||
fps = settings.value(SER_FPS, 60).toInt();
|
fps = settings.value(SER_FPS, 60).toInt();
|
||||||
bitrateKbps = settings.value(SER_BITRATE, getDefaultBitrate(width, height, fps)).toInt();
|
bitrateKbps = settings.value(SER_BITRATE, getDefaultBitrate(width, height, fps)).toInt();
|
||||||
fullScreen = settings.value(SER_FULLSCREEN, true).toBool();
|
|
||||||
enableVsync = settings.value(SER_VSYNC, true).toBool();
|
enableVsync = settings.value(SER_VSYNC, true).toBool();
|
||||||
gameOptimizations = settings.value(SER_GAMEOPTS, true).toBool();
|
gameOptimizations = settings.value(SER_GAMEOPTS, true).toBool();
|
||||||
playAudioOnHost = settings.value(SER_HOSTAUDIO, false).toBool();
|
playAudioOnHost = settings.value(SER_HOSTAUDIO, false).toBool();
|
||||||
multiController = settings.value(SER_MULTICONT, true).toBool();
|
multiController = settings.value(SER_MULTICONT, true).toBool();
|
||||||
|
unsupportedFps = settings.value(SER_UNSUPPORTEDFPS, false).toBool();
|
||||||
|
enableMdns = settings.value(SER_MDNS, true).toBool();
|
||||||
|
mouseAcceleration = settings.value(SER_MOUSEACCELERATION, false).toBool();
|
||||||
audioConfig = static_cast<AudioConfig>(settings.value(SER_AUDIOCFG,
|
audioConfig = static_cast<AudioConfig>(settings.value(SER_AUDIOCFG,
|
||||||
static_cast<int>(AudioConfig::AC_AUTO)).toInt());
|
static_cast<int>(AudioConfig::AC_STEREO)).toInt());
|
||||||
videoCodecConfig = static_cast<VideoCodecConfig>(settings.value(SER_VIDEOCFG,
|
videoCodecConfig = static_cast<VideoCodecConfig>(settings.value(SER_VIDEOCFG,
|
||||||
static_cast<int>(VideoCodecConfig::VCC_AUTO)).toInt());
|
static_cast<int>(VideoCodecConfig::VCC_AUTO)).toInt());
|
||||||
videoDecoderSelection = static_cast<VideoDecoderSelection>(settings.value(SER_VIDEODEC,
|
videoDecoderSelection = static_cast<VideoDecoderSelection>(settings.value(SER_VIDEODEC,
|
||||||
static_cast<int>(VideoDecoderSelection::VDS_AUTO)).toInt());
|
static_cast<int>(VideoDecoderSelection::VDS_AUTO)).toInt());
|
||||||
|
windowMode = static_cast<WindowMode>(settings.value(SER_WINDOWMODE,
|
||||||
|
// Try to load from the old preference value too
|
||||||
|
static_cast<int>(settings.value(SER_FULLSCREEN, true).toBool() ?
|
||||||
|
WindowMode::WM_FULLSCREEN : WindowMode::WM_WINDOWED)).toInt());
|
||||||
}
|
}
|
||||||
|
|
||||||
void StreamingPreferences::save()
|
void StreamingPreferences::save()
|
||||||
@@ -51,14 +63,17 @@ void StreamingPreferences::save()
|
|||||||
settings.setValue(SER_HEIGHT, height);
|
settings.setValue(SER_HEIGHT, height);
|
||||||
settings.setValue(SER_FPS, fps);
|
settings.setValue(SER_FPS, fps);
|
||||||
settings.setValue(SER_BITRATE, bitrateKbps);
|
settings.setValue(SER_BITRATE, bitrateKbps);
|
||||||
settings.setValue(SER_FULLSCREEN, fullScreen);
|
|
||||||
settings.setValue(SER_VSYNC, enableVsync);
|
settings.setValue(SER_VSYNC, enableVsync);
|
||||||
settings.setValue(SER_GAMEOPTS, gameOptimizations);
|
settings.setValue(SER_GAMEOPTS, gameOptimizations);
|
||||||
settings.setValue(SER_HOSTAUDIO, playAudioOnHost);
|
settings.setValue(SER_HOSTAUDIO, playAudioOnHost);
|
||||||
settings.setValue(SER_MULTICONT, multiController);
|
settings.setValue(SER_MULTICONT, multiController);
|
||||||
|
settings.setValue(SER_UNSUPPORTEDFPS, unsupportedFps);
|
||||||
|
settings.setValue(SER_MDNS, enableMdns);
|
||||||
|
settings.setValue(SER_MOUSEACCELERATION, mouseAcceleration);
|
||||||
settings.setValue(SER_AUDIOCFG, static_cast<int>(audioConfig));
|
settings.setValue(SER_AUDIOCFG, static_cast<int>(audioConfig));
|
||||||
settings.setValue(SER_VIDEOCFG, static_cast<int>(videoCodecConfig));
|
settings.setValue(SER_VIDEOCFG, static_cast<int>(videoCodecConfig));
|
||||||
settings.setValue(SER_VIDEODEC, static_cast<int>(videoDecoderSelection));
|
settings.setValue(SER_VIDEODEC, static_cast<int>(videoDecoderSelection));
|
||||||
|
settings.setValue(SER_WINDOWMODE, static_cast<int>(windowMode));
|
||||||
}
|
}
|
||||||
|
|
||||||
bool StreamingPreferences::hasAnyHardwareAcceleration()
|
bool StreamingPreferences::hasAnyHardwareAcceleration()
|
||||||
@@ -75,6 +90,20 @@ bool StreamingPreferences::isRunningWayland()
|
|||||||
return qgetenv("XDG_SESSION_TYPE") == QByteArray("wayland");
|
return qgetenv("XDG_SESSION_TYPE") == QByteArray("wayland");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool StreamingPreferences::isWow64()
|
||||||
|
{
|
||||||
|
#ifdef Q_OS_WIN32
|
||||||
|
return QSysInfo::currentCpuArchitecture() != QSysInfo::buildCpuArchitecture();
|
||||||
|
#else
|
||||||
|
return false;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
QString StreamingPreferences::getUnmappedGamepads()
|
||||||
|
{
|
||||||
|
return SdlInputHandler::getUnmappedGamepads();
|
||||||
|
}
|
||||||
|
|
||||||
int StreamingPreferences::getMaximumStreamingFrameRate()
|
int StreamingPreferences::getMaximumStreamingFrameRate()
|
||||||
{
|
{
|
||||||
// Never let the maximum drop below 60 FPS
|
// Never let the maximum drop below 60 FPS
|
||||||
@@ -87,12 +116,26 @@ int StreamingPreferences::getMaximumStreamingFrameRate()
|
|||||||
return maxFrameRate;
|
return maxFrameRate;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int i = 0; i < SDL_GetNumVideoDisplays(); i++) {
|
SDL_DisplayMode mode, bestMode, desktopMode;
|
||||||
SDL_DisplayMode mode;
|
for (int displayIndex = 0; displayIndex < SDL_GetNumVideoDisplays(); displayIndex++) {
|
||||||
if (SDL_GetCurrentDisplayMode(i, &mode) == 0) {
|
// Get the native desktop resolution
|
||||||
|
if (StreamUtils::getRealDesktopMode(displayIndex, &desktopMode)) {
|
||||||
|
|
||||||
|
// Start at desktop mode and work our way up
|
||||||
|
bestMode = desktopMode;
|
||||||
|
for (int i = 0; i < SDL_GetNumDisplayModes(displayIndex); i++) {
|
||||||
|
if (SDL_GetDisplayMode(displayIndex, i, &mode) == 0) {
|
||||||
|
if (mode.w == desktopMode.w && mode.h == desktopMode.h) {
|
||||||
|
if (mode.refresh_rate > bestMode.refresh_rate) {
|
||||||
|
bestMode = mode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Cap the frame rate at 120 FPS. Past this, the encoders start
|
// Cap the frame rate at 120 FPS. Past this, the encoders start
|
||||||
// to max out and drop frames.
|
// to max out and drop frames.
|
||||||
maxFrameRate = qMax(maxFrameRate, qMin(120, mode.refresh_rate));
|
maxFrameRate = qMax(maxFrameRate, qMin(120, bestMode.refresh_rate));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,7 +144,7 @@ int StreamingPreferences::getMaximumStreamingFrameRate()
|
|||||||
return maxFrameRate;
|
return maxFrameRate;
|
||||||
}
|
}
|
||||||
|
|
||||||
QRect StreamingPreferences::getDisplayResolution(int displayIndex)
|
QRect StreamingPreferences::getDesktopResolution(int displayIndex)
|
||||||
{
|
{
|
||||||
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
|
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
@@ -116,7 +159,7 @@ QRect StreamingPreferences::getDisplayResolution(int displayIndex)
|
|||||||
}
|
}
|
||||||
|
|
||||||
SDL_DisplayMode mode;
|
SDL_DisplayMode mode;
|
||||||
int err = SDL_GetCurrentDisplayMode(displayIndex, &mode);
|
int err = SDL_GetDesktopDisplayMode(displayIndex, &mode);
|
||||||
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||||
|
|
||||||
if (err == 0) {
|
if (err == 0) {
|
||||||
@@ -124,12 +167,38 @@ QRect StreamingPreferences::getDisplayResolution(int displayIndex)
|
|||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"SDL_GetCurrentDisplayMode() failed: %s",
|
"SDL_GetDesktopDisplayMode() failed: %s",
|
||||||
SDL_GetError());
|
SDL_GetError());
|
||||||
return QRect();
|
return QRect();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QRect StreamingPreferences::getNativeResolution(int displayIndex)
|
||||||
|
{
|
||||||
|
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
return QRect();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (displayIndex >= SDL_GetNumVideoDisplays()) {
|
||||||
|
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||||
|
return QRect();
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_DisplayMode mode;
|
||||||
|
bool success = StreamUtils::getRealDesktopMode(displayIndex, &mode);
|
||||||
|
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
return QRect(0, 0, mode.w, mode.h);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return QRect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
int StreamingPreferences::getDefaultBitrate(int width, int height, int fps)
|
int StreamingPreferences::getDefaultBitrate(int width, int height, int fps)
|
||||||
{
|
{
|
||||||
// This table prefers 16:10 resolutions because they are
|
// This table prefers 16:10 resolutions because they are
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ class StreamingPreferences : public QObject
|
|||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
StreamingPreferences();
|
StreamingPreferences(QObject *parent = nullptr);
|
||||||
|
|
||||||
Q_INVOKABLE static int
|
Q_INVOKABLE static int
|
||||||
getDefaultBitrate(int width, int height, int fps);
|
getDefaultBitrate(int width, int height, int fps);
|
||||||
@@ -19,17 +19,22 @@ public:
|
|||||||
|
|
||||||
Q_INVOKABLE static bool isRunningWayland();
|
Q_INVOKABLE static bool isRunningWayland();
|
||||||
|
|
||||||
|
Q_INVOKABLE static bool isWow64();
|
||||||
|
|
||||||
Q_INVOKABLE static int getMaximumStreamingFrameRate();
|
Q_INVOKABLE static int getMaximumStreamingFrameRate();
|
||||||
|
|
||||||
Q_INVOKABLE QRect getDisplayResolution(int displayIndex);
|
Q_INVOKABLE QRect getDesktopResolution(int displayIndex);
|
||||||
|
|
||||||
|
Q_INVOKABLE QRect getNativeResolution(int displayIndex);
|
||||||
|
|
||||||
|
Q_INVOKABLE QString getUnmappedGamepads();
|
||||||
|
|
||||||
void reload();
|
void reload();
|
||||||
|
|
||||||
enum AudioConfig
|
enum AudioConfig
|
||||||
{
|
{
|
||||||
AC_AUTO,
|
AC_STEREO,
|
||||||
AC_FORCE_STEREO,
|
AC_51_SURROUND
|
||||||
AC_FORCE_SURROUND
|
|
||||||
};
|
};
|
||||||
Q_ENUM(AudioConfig)
|
Q_ENUM(AudioConfig)
|
||||||
|
|
||||||
@@ -50,43 +55,60 @@ public:
|
|||||||
};
|
};
|
||||||
Q_ENUM(VideoDecoderSelection)
|
Q_ENUM(VideoDecoderSelection)
|
||||||
|
|
||||||
|
enum WindowMode
|
||||||
|
{
|
||||||
|
WM_FULLSCREEN,
|
||||||
|
WM_FULLSCREEN_DESKTOP,
|
||||||
|
WM_WINDOWED
|
||||||
|
};
|
||||||
|
Q_ENUM(WindowMode)
|
||||||
|
|
||||||
Q_PROPERTY(int width MEMBER width NOTIFY displayModeChanged)
|
Q_PROPERTY(int width MEMBER width NOTIFY displayModeChanged)
|
||||||
Q_PROPERTY(int height MEMBER height NOTIFY displayModeChanged)
|
Q_PROPERTY(int height MEMBER height NOTIFY displayModeChanged)
|
||||||
Q_PROPERTY(int fps MEMBER fps NOTIFY displayModeChanged)
|
Q_PROPERTY(int fps MEMBER fps NOTIFY displayModeChanged)
|
||||||
Q_PROPERTY(int bitrateKbps MEMBER bitrateKbps NOTIFY bitrateChanged)
|
Q_PROPERTY(int bitrateKbps MEMBER bitrateKbps NOTIFY bitrateChanged)
|
||||||
Q_PROPERTY(bool fullScreen MEMBER fullScreen NOTIFY fullScreenChanged)
|
|
||||||
Q_PROPERTY(bool enableVsync MEMBER enableVsync NOTIFY enableVsyncChanged)
|
Q_PROPERTY(bool enableVsync MEMBER enableVsync NOTIFY enableVsyncChanged)
|
||||||
Q_PROPERTY(bool gameOptimizations MEMBER gameOptimizations NOTIFY gameOptimizationsChanged)
|
Q_PROPERTY(bool gameOptimizations MEMBER gameOptimizations NOTIFY gameOptimizationsChanged)
|
||||||
Q_PROPERTY(bool playAudioOnHost MEMBER playAudioOnHost NOTIFY playAudioOnHostChanged)
|
Q_PROPERTY(bool playAudioOnHost MEMBER playAudioOnHost NOTIFY playAudioOnHostChanged)
|
||||||
Q_PROPERTY(bool multiController MEMBER multiController NOTIFY multiControllerChanged)
|
Q_PROPERTY(bool multiController MEMBER multiController NOTIFY multiControllerChanged)
|
||||||
|
Q_PROPERTY(bool unsupportedFps MEMBER unsupportedFps NOTIFY unsupportedFpsChanged)
|
||||||
|
Q_PROPERTY(bool enableMdns MEMBER enableMdns NOTIFY enableMdnsChanged)
|
||||||
|
Q_PROPERTY(bool mouseAcceleration MEMBER mouseAcceleration NOTIFY mouseAccelerationChanged)
|
||||||
Q_PROPERTY(AudioConfig audioConfig MEMBER audioConfig NOTIFY audioConfigChanged)
|
Q_PROPERTY(AudioConfig audioConfig MEMBER audioConfig NOTIFY audioConfigChanged)
|
||||||
Q_PROPERTY(VideoCodecConfig videoCodecConfig MEMBER videoCodecConfig NOTIFY videoCodecConfigChanged)
|
Q_PROPERTY(VideoCodecConfig videoCodecConfig MEMBER videoCodecConfig NOTIFY videoCodecConfigChanged)
|
||||||
Q_PROPERTY(VideoDecoderSelection videoDecoderSelection MEMBER videoDecoderSelection NOTIFY videoDecoderSelectionChanged)
|
Q_PROPERTY(VideoDecoderSelection videoDecoderSelection MEMBER videoDecoderSelection NOTIFY videoDecoderSelectionChanged)
|
||||||
|
Q_PROPERTY(WindowMode windowMode MEMBER windowMode NOTIFY windowModeChanged)
|
||||||
|
|
||||||
// Directly accessible members for preferences
|
// Directly accessible members for preferences
|
||||||
int width;
|
int width;
|
||||||
int height;
|
int height;
|
||||||
int fps;
|
int fps;
|
||||||
int bitrateKbps;
|
int bitrateKbps;
|
||||||
bool fullScreen;
|
|
||||||
bool enableVsync;
|
bool enableVsync;
|
||||||
bool gameOptimizations;
|
bool gameOptimizations;
|
||||||
bool playAudioOnHost;
|
bool playAudioOnHost;
|
||||||
bool multiController;
|
bool multiController;
|
||||||
|
bool unsupportedFps;
|
||||||
|
bool enableMdns;
|
||||||
|
bool mouseAcceleration;
|
||||||
AudioConfig audioConfig;
|
AudioConfig audioConfig;
|
||||||
VideoCodecConfig videoCodecConfig;
|
VideoCodecConfig videoCodecConfig;
|
||||||
VideoDecoderSelection videoDecoderSelection;
|
VideoDecoderSelection videoDecoderSelection;
|
||||||
|
WindowMode windowMode;
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void displayModeChanged();
|
void displayModeChanged();
|
||||||
void bitrateChanged();
|
void bitrateChanged();
|
||||||
void fullScreenChanged();
|
|
||||||
void enableVsyncChanged();
|
void enableVsyncChanged();
|
||||||
void gameOptimizationsChanged();
|
void gameOptimizationsChanged();
|
||||||
void playAudioOnHostChanged();
|
void playAudioOnHostChanged();
|
||||||
void multiControllerChanged();
|
void multiControllerChanged();
|
||||||
|
void unsupportedFpsChanged();
|
||||||
|
void enableMdnsChanged();
|
||||||
|
void mouseAccelerationChanged();
|
||||||
void audioConfigChanged();
|
void audioConfigChanged();
|
||||||
void videoCodecConfigChanged();
|
void videoCodecConfigChanged();
|
||||||
void videoDecoderSelectionChanged();
|
void videoDecoderSelectionChanged();
|
||||||
|
void windowModeChanged();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,230 +0,0 @@
|
|||||||
#include "session.hpp"
|
|
||||||
|
|
||||||
#include <Limelight.h>
|
|
||||||
#include <SDL.h>
|
|
||||||
|
|
||||||
#define MAX_CHANNELS 6
|
|
||||||
#define SAMPLES_PER_FRAME 240
|
|
||||||
#define MIN_QUEUED_FRAMES 2
|
|
||||||
#define MAX_QUEUED_FRAMES 4
|
|
||||||
#define STOP_THE_WORLD_LIMIT 20
|
|
||||||
#define DROP_RATIO_DENOM 32
|
|
||||||
|
|
||||||
SDL_AudioDeviceID Session::s_AudioDevice;
|
|
||||||
OpusMSDecoder* Session::s_OpusDecoder;
|
|
||||||
short Session::s_OpusDecodeBuffer[MAX_CHANNELS * SAMPLES_PER_FRAME];
|
|
||||||
int Session::s_ChannelCount;
|
|
||||||
int Session::s_PendingDrops;
|
|
||||||
int Session::s_PendingHardDrops;
|
|
||||||
unsigned int Session::s_SampleIndex;
|
|
||||||
Uint32 Session::s_BaselinePendingData;
|
|
||||||
|
|
||||||
int Session::sdlDetermineAudioConfiguration()
|
|
||||||
{
|
|
||||||
SDL_AudioSpec want, have;
|
|
||||||
SDL_AudioDeviceID dev;
|
|
||||||
int ret;
|
|
||||||
|
|
||||||
if (SDL_InitSubSystem(SDL_INIT_AUDIO) != 0) {
|
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"SDL_InitSubSystem(SDL_INIT_AUDIO) failed: %s",
|
|
||||||
SDL_GetError());
|
|
||||||
return AUDIO_CONFIGURATION_STEREO;
|
|
||||||
}
|
|
||||||
|
|
||||||
SDL_zero(want);
|
|
||||||
want.freq = 48000;
|
|
||||||
want.format = AUDIO_S16;
|
|
||||||
want.channels = 6;
|
|
||||||
want.samples = 1024;
|
|
||||||
|
|
||||||
// Try to open for 5.1 surround sound, but allow SDL to tell us that's
|
|
||||||
// not available.
|
|
||||||
dev = SDL_OpenAudioDevice(NULL, 0, &want, &have, SDL_AUDIO_ALLOW_CHANNELS_CHANGE);
|
|
||||||
if (dev == 0) {
|
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Failed to open audio device");
|
|
||||||
// We'll probably have issues during audio stream init, but we'll
|
|
||||||
// try anyway
|
|
||||||
ret = AUDIO_CONFIGURATION_STEREO;
|
|
||||||
goto Exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
SDL_CloseAudioDevice(dev);
|
|
||||||
|
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Audio device has %d channels", have.channels);
|
|
||||||
|
|
||||||
if (have.channels > 2) {
|
|
||||||
// We don't support quadraphonic or 7.1 surround, but SDL
|
|
||||||
// should be able to downmix or upmix better from 5.1 than
|
|
||||||
// from stereo, so use 5.1 in non-stereo cases.
|
|
||||||
ret = AUDIO_CONFIGURATION_51_SURROUND;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
ret = AUDIO_CONFIGURATION_STEREO;
|
|
||||||
}
|
|
||||||
|
|
||||||
Exit:
|
|
||||||
SDL_QuitSubSystem(SDL_INIT_AUDIO);
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
int Session::sdlAudioInit(int /* audioConfiguration */,
|
|
||||||
POPUS_MULTISTREAM_CONFIGURATION opusConfig,
|
|
||||||
void* /* arContext */, int /* arFlags */)
|
|
||||||
{
|
|
||||||
SDL_AudioSpec want, have;
|
|
||||||
int error;
|
|
||||||
|
|
||||||
SDL_assert(!SDL_WasInit(SDL_INIT_AUDIO));
|
|
||||||
if (SDL_InitSubSystem(SDL_INIT_AUDIO) != 0) {
|
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"SDL_InitSubSystem(SDL_INIT_AUDIO) failed: %s",
|
|
||||||
SDL_GetError());
|
|
||||||
return AUDIO_CONFIGURATION_STEREO;
|
|
||||||
}
|
|
||||||
|
|
||||||
SDL_zero(want);
|
|
||||||
want.freq = opusConfig->sampleRate;
|
|
||||||
want.format = AUDIO_S16;
|
|
||||||
want.channels = opusConfig->channelCount;
|
|
||||||
|
|
||||||
// This is supposed to be a power of 2, but our
|
|
||||||
// frames contain a non-power of 2 number of samples,
|
|
||||||
// so the slop would require buffering another full frame.
|
|
||||||
// Specifying non-Po2 seems to work for our supported platforms.
|
|
||||||
want.samples = SAMPLES_PER_FRAME;
|
|
||||||
|
|
||||||
s_AudioDevice = SDL_OpenAudioDevice(NULL, 0, &want, &have, 0);
|
|
||||||
if (s_AudioDevice == 0) {
|
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Failed to open audio device: %s",
|
|
||||||
SDL_GetError());
|
|
||||||
SDL_QuitSubSystem(SDL_INIT_AUDIO);
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
s_OpusDecoder = opus_multistream_decoder_create(opusConfig->sampleRate,
|
|
||||||
opusConfig->channelCount,
|
|
||||||
opusConfig->streams,
|
|
||||||
opusConfig->coupledStreams,
|
|
||||||
opusConfig->mapping,
|
|
||||||
&error);
|
|
||||||
if (s_OpusDecoder == NULL) {
|
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Failed to create decoder: %d",
|
|
||||||
error);
|
|
||||||
SDL_CloseAudioDevice(s_AudioDevice);
|
|
||||||
s_AudioDevice = 0;
|
|
||||||
SDL_QuitSubSystem(SDL_INIT_AUDIO);
|
|
||||||
return -2;
|
|
||||||
}
|
|
||||||
|
|
||||||
// SDL counts pending samples in the queued
|
|
||||||
// audio size using the WASAPI backend. This
|
|
||||||
// includes silence, which can throw off our
|
|
||||||
// pending data count. Get a baseline so we
|
|
||||||
// can exclude that data.
|
|
||||||
s_BaselinePendingData = 0;
|
|
||||||
#ifdef Q_OS_WIN32
|
|
||||||
for (int i = 0; i < 100; i++) {
|
|
||||||
s_BaselinePendingData = qMax(s_BaselinePendingData, SDL_GetQueuedAudioSize(s_AudioDevice));
|
|
||||||
SDL_Delay(10);
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
s_BaselinePendingData *= 2;
|
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Baseline pending audio data: %d bytes",
|
|
||||||
s_BaselinePendingData);
|
|
||||||
|
|
||||||
s_ChannelCount = opusConfig->channelCount;
|
|
||||||
s_SampleIndex = 0;
|
|
||||||
s_PendingDrops = s_PendingHardDrops = 0;
|
|
||||||
|
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Audio stream has %d channels",
|
|
||||||
opusConfig->channelCount);
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Session::sdlAudioStart()
|
|
||||||
{
|
|
||||||
// Unpause the audio device
|
|
||||||
SDL_PauseAudioDevice(s_AudioDevice, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
void Session::sdlAudioStop()
|
|
||||||
{
|
|
||||||
// Pause the audio device
|
|
||||||
SDL_PauseAudioDevice(s_AudioDevice, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
void Session::sdlAudioCleanup()
|
|
||||||
{
|
|
||||||
SDL_CloseAudioDevice(s_AudioDevice);
|
|
||||||
s_AudioDevice = 0;
|
|
||||||
|
|
||||||
opus_multistream_decoder_destroy(s_OpusDecoder);
|
|
||||||
s_OpusDecoder = nullptr;
|
|
||||||
|
|
||||||
SDL_QuitSubSystem(SDL_INIT_AUDIO);
|
|
||||||
SDL_assert(!SDL_WasInit(SDL_INIT_AUDIO));
|
|
||||||
}
|
|
||||||
|
|
||||||
void Session::sdlAudioDecodeAndPlaySample(char* sampleData, int sampleLength)
|
|
||||||
{
|
|
||||||
int samplesDecoded;
|
|
||||||
|
|
||||||
s_SampleIndex++;
|
|
||||||
|
|
||||||
Uint32 queuedAudio = qMax((int)SDL_GetQueuedAudioSize(s_AudioDevice) - (int)s_BaselinePendingData, 0);
|
|
||||||
Uint32 framesQueued = queuedAudio / (SAMPLES_PER_FRAME * s_ChannelCount * sizeof(short));
|
|
||||||
|
|
||||||
// We must check this prior to the below checks to ensure we don't
|
|
||||||
// underflow if framesQueued - s_PendingHardDrops < 0.
|
|
||||||
if (framesQueued <= MIN_QUEUED_FRAMES) {
|
|
||||||
s_PendingDrops = s_PendingHardDrops = 0;
|
|
||||||
}
|
|
||||||
// Pend enough drops to get us back to MIN_QUEUED_FRAMES
|
|
||||||
else if (framesQueued - s_PendingHardDrops > STOP_THE_WORLD_LIMIT) {
|
|
||||||
s_PendingHardDrops = framesQueued - MIN_QUEUED_FRAMES;
|
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Pending hard drop of %u audio frames",
|
|
||||||
s_PendingHardDrops);
|
|
||||||
}
|
|
||||||
else if (framesQueued - s_PendingHardDrops - s_PendingDrops > MAX_QUEUED_FRAMES) {
|
|
||||||
s_PendingDrops = framesQueued - MIN_QUEUED_FRAMES;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine if this frame should be dropped
|
|
||||||
if (s_PendingHardDrops != 0) {
|
|
||||||
// Hard drops happen all at once to forcefully
|
|
||||||
// resync with the source.
|
|
||||||
s_PendingHardDrops--;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
else if (s_PendingDrops != 0 && s_SampleIndex % DROP_RATIO_DENOM == 0) {
|
|
||||||
// Normal drops are interspersed with the audio data
|
|
||||||
// to hide the glitches.
|
|
||||||
s_PendingDrops--;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
samplesDecoded = opus_multistream_decode(s_OpusDecoder,
|
|
||||||
(unsigned char*)sampleData,
|
|
||||||
sampleLength,
|
|
||||||
s_OpusDecodeBuffer,
|
|
||||||
SAMPLES_PER_FRAME,
|
|
||||||
0);
|
|
||||||
if (samplesDecoded > 0) {
|
|
||||||
if (SDL_QueueAudio(s_AudioDevice,
|
|
||||||
s_OpusDecodeBuffer,
|
|
||||||
sizeof(short) * samplesDecoded * s_ChannelCount) < 0) {
|
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Failed to queue audio sample: %s",
|
|
||||||
SDL_GetError());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
#include "../session.h"
|
||||||
|
#include "renderers/renderer.h"
|
||||||
|
|
||||||
|
#ifdef HAVE_SOUNDIO
|
||||||
|
#include "renderers/soundioaudiorenderer.h"
|
||||||
|
#else
|
||||||
|
#include "renderers/sdl.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include <Limelight.h>
|
||||||
|
|
||||||
|
IAudioRenderer* Session::createAudioRenderer()
|
||||||
|
{
|
||||||
|
#ifdef HAVE_SOUNDIO
|
||||||
|
return new SoundIoAudioRenderer();
|
||||||
|
#else
|
||||||
|
return new SdlAudioRenderer();
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Session::testAudio(int audioConfiguration)
|
||||||
|
{
|
||||||
|
IAudioRenderer* audioRenderer;
|
||||||
|
|
||||||
|
audioRenderer = createAudioRenderer();
|
||||||
|
if (audioRenderer == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build a fake OPUS_MULTISTREAM_CONFIGURATION to give
|
||||||
|
// the renderer the channel count and sample rate.
|
||||||
|
OPUS_MULTISTREAM_CONFIGURATION opusConfig = {};
|
||||||
|
opusConfig.sampleRate = 48000;
|
||||||
|
|
||||||
|
switch (audioConfiguration)
|
||||||
|
{
|
||||||
|
case AUDIO_CONFIGURATION_STEREO:
|
||||||
|
opusConfig.channelCount = 2;
|
||||||
|
break;
|
||||||
|
case AUDIO_CONFIGURATION_51_SURROUND:
|
||||||
|
opusConfig.channelCount = 6;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
SDL_assert(false);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ret = audioRenderer->prepareForPlayback(&opusConfig);
|
||||||
|
|
||||||
|
delete audioRenderer;
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
int Session::arInit(int /* audioConfiguration */,
|
||||||
|
const POPUS_MULTISTREAM_CONFIGURATION opusConfig,
|
||||||
|
void* /* arContext */, int /* arFlags */)
|
||||||
|
{
|
||||||
|
int error;
|
||||||
|
|
||||||
|
SDL_memcpy(&s_ActiveSession->m_AudioConfig, opusConfig, sizeof(*opusConfig));
|
||||||
|
|
||||||
|
s_ActiveSession->m_OpusDecoder =
|
||||||
|
opus_multistream_decoder_create(opusConfig->sampleRate,
|
||||||
|
opusConfig->channelCount,
|
||||||
|
opusConfig->streams,
|
||||||
|
opusConfig->coupledStreams,
|
||||||
|
opusConfig->mapping,
|
||||||
|
&error);
|
||||||
|
if (s_ActiveSession->m_OpusDecoder == NULL) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Failed to create decoder: %d",
|
||||||
|
error);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
s_ActiveSession->m_AudioRenderer = s_ActiveSession->createAudioRenderer();
|
||||||
|
if (!s_ActiveSession->m_AudioRenderer->prepareForPlayback(opusConfig)) {
|
||||||
|
delete s_ActiveSession->m_AudioRenderer;
|
||||||
|
opus_multistream_decoder_destroy(s_ActiveSession->m_OpusDecoder);
|
||||||
|
return -2;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Audio stream has %d channels",
|
||||||
|
opusConfig->channelCount);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Session::arCleanup()
|
||||||
|
{
|
||||||
|
delete s_ActiveSession->m_AudioRenderer;
|
||||||
|
s_ActiveSession->m_AudioRenderer = nullptr;
|
||||||
|
|
||||||
|
opus_multistream_decoder_destroy(s_ActiveSession->m_OpusDecoder);
|
||||||
|
s_ActiveSession->m_OpusDecoder = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Session::arDecodeAndPlaySample(char* sampleData, int sampleLength)
|
||||||
|
{
|
||||||
|
int samplesDecoded;
|
||||||
|
|
||||||
|
// Set this thread to high priority to reduce
|
||||||
|
// the chance of missing our sample delivery time
|
||||||
|
if (s_ActiveSession->m_AudioSampleCount == 0) {
|
||||||
|
if (SDL_SetThreadPriority(SDL_THREAD_PRIORITY_HIGH) < 0) {
|
||||||
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Unable to set audio thread to high priority: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
s_ActiveSession->m_AudioSampleCount++;
|
||||||
|
|
||||||
|
if (s_ActiveSession->m_AudioRenderer != nullptr) {
|
||||||
|
samplesDecoded = opus_multistream_decode(s_ActiveSession->m_OpusDecoder,
|
||||||
|
(unsigned char*)sampleData,
|
||||||
|
sampleLength,
|
||||||
|
s_ActiveSession->m_OpusDecodeBuffer,
|
||||||
|
SAMPLES_PER_FRAME,
|
||||||
|
0);
|
||||||
|
if (samplesDecoded > 0) {
|
||||||
|
if (!s_ActiveSession->m_AudioRenderer->submitAudio(s_ActiveSession->m_OpusDecodeBuffer,
|
||||||
|
static_cast<int>(
|
||||||
|
sizeof(short) *
|
||||||
|
samplesDecoded *
|
||||||
|
s_ActiveSession->m_AudioConfig.channelCount))) {
|
||||||
|
|
||||||
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Reinitializing audio renderer after failure");
|
||||||
|
|
||||||
|
delete s_ActiveSession->m_AudioRenderer;
|
||||||
|
s_ActiveSession->m_AudioRenderer = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only try to recreate the audio renderer every 200 samples (1 second)
|
||||||
|
// to avoid thrashing if the audio device is unavailable. It is
|
||||||
|
// safe to reinitialize here because we can't be torn down while
|
||||||
|
// the audio decoder/playback thread is still alive.
|
||||||
|
if (s_ActiveSession->m_AudioRenderer == nullptr && (s_ActiveSession->m_AudioSampleCount % 200) == 0) {
|
||||||
|
s_ActiveSession->m_AudioRenderer = s_ActiveSession->createAudioRenderer();
|
||||||
|
if (!s_ActiveSession->m_AudioRenderer->prepareForPlayback(&s_ActiveSession->m_AudioConfig)) {
|
||||||
|
delete s_ActiveSession->m_AudioRenderer;
|
||||||
|
s_ActiveSession->m_AudioRenderer = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
#include "qtaud.h"
|
||||||
|
|
||||||
|
#include <QDebug>
|
||||||
|
|
||||||
|
QtAudioRenderer::QtAudioRenderer()
|
||||||
|
: m_AudioOutput(nullptr),
|
||||||
|
m_OutputDevice(nullptr)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
QtAudioRenderer::~QtAudioRenderer()
|
||||||
|
{
|
||||||
|
delete m_AudioOutput;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool QtAudioRenderer::prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig)
|
||||||
|
{
|
||||||
|
QAudioFormat format;
|
||||||
|
|
||||||
|
// testAudio() assumes this sample rate
|
||||||
|
Q_ASSERT(opusConfig->sampleRate == 48000);
|
||||||
|
|
||||||
|
format.setSampleRate(opusConfig->sampleRate);
|
||||||
|
format.setChannelCount(opusConfig->channelCount);
|
||||||
|
format.setSampleSize(16);
|
||||||
|
format.setSampleType(QAudioFormat::SignedInt);
|
||||||
|
format.setCodec("audio/pcm");
|
||||||
|
|
||||||
|
m_AudioOutput = new QAudioOutput(format);
|
||||||
|
|
||||||
|
// Buffer 40 ms of audio data
|
||||||
|
m_AudioOutput->setBufferSize(format.bytesForDuration(40000));
|
||||||
|
|
||||||
|
m_OutputDevice = m_AudioOutput->start();
|
||||||
|
if (m_OutputDevice == nullptr) {
|
||||||
|
qWarning() << "Audio start failed:" << m_AudioOutput->error();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check what buffer size was really used after starting
|
||||||
|
qInfo() << "Audio stream buffer:" << m_AudioOutput->bufferSize() << "bytes";
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void QtAudioRenderer::submitAudio(short* audioBuffer, int audioSize)
|
||||||
|
{
|
||||||
|
m_OutputDevice->write((const char *)audioBuffer, audioSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool QtAudioRenderer::testAudio(int audioConfiguration)
|
||||||
|
{
|
||||||
|
QAudioFormat format;
|
||||||
|
|
||||||
|
format.setSampleRate(48000);
|
||||||
|
format.setSampleSize(16);
|
||||||
|
format.setSampleType(QAudioFormat::SignedInt);
|
||||||
|
format.setCodec("audio/pcm");
|
||||||
|
|
||||||
|
switch (audioConfiguration) {
|
||||||
|
case AUDIO_CONFIGURATION_STEREO:
|
||||||
|
format.setChannelCount(2);
|
||||||
|
break;
|
||||||
|
case AUDIO_CONFIGURATION_51_SURROUND:
|
||||||
|
format.setChannelCount(6);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
Q_ASSERT(false);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (QAudioDeviceInfo::defaultOutputDevice().isFormatSupported(format)) {
|
||||||
|
qInfo() << "Audio test successful with" << format.channelCount() << "channels";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
qWarning() << "Audio test failed with" << format.channelCount() << "channels";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int QtAudioRenderer::detectAudioConfiguration()
|
||||||
|
{
|
||||||
|
int preferredChannelCount = QAudioDeviceInfo::defaultOutputDevice().preferredFormat().channelCount();
|
||||||
|
|
||||||
|
qInfo() << "Audio output device prefers" << preferredChannelCount << "channel configuration";
|
||||||
|
|
||||||
|
// We can better downmix 5.1 to quad than we can upmix stereo
|
||||||
|
if (preferredChannelCount > 2) {
|
||||||
|
return AUDIO_CONFIGURATION_51_SURROUND;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return AUDIO_CONFIGURATION_STEREO;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "renderer.h"
|
||||||
|
#include <QAudioOutput>
|
||||||
|
|
||||||
|
class QtAudioRenderer : public IAudioRenderer
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
QtAudioRenderer();
|
||||||
|
|
||||||
|
virtual ~QtAudioRenderer();
|
||||||
|
|
||||||
|
virtual bool prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig);
|
||||||
|
|
||||||
|
virtual void submitAudio(short* audioBuffer, int audioSize);
|
||||||
|
|
||||||
|
virtual bool testAudio(int audioConfiguration);
|
||||||
|
|
||||||
|
virtual int detectAudioConfiguration();
|
||||||
|
|
||||||
|
private:
|
||||||
|
QAudioOutput* m_AudioOutput;
|
||||||
|
QIODevice* m_OutputDevice;
|
||||||
|
};
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Limelight.h>
|
||||||
|
|
||||||
|
#define MAX_CHANNELS 6
|
||||||
|
#define SAMPLES_PER_FRAME 240
|
||||||
|
|
||||||
|
class IAudioRenderer
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual ~IAudioRenderer() {}
|
||||||
|
|
||||||
|
virtual bool prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig) = 0;
|
||||||
|
|
||||||
|
// Return false if an unrecoverable error has occurred and the renderer must be reinitialized
|
||||||
|
virtual bool submitAudio(short* audioBuffer, int audioSize) = 0;
|
||||||
|
};
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "renderer.h"
|
||||||
|
#include <SDL.h>
|
||||||
|
|
||||||
|
class SdlAudioRenderer : public IAudioRenderer
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
SdlAudioRenderer();
|
||||||
|
|
||||||
|
virtual ~SdlAudioRenderer();
|
||||||
|
|
||||||
|
virtual bool prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig);
|
||||||
|
|
||||||
|
virtual bool submitAudio(short* audioBuffer, int audioSize);
|
||||||
|
|
||||||
|
private:
|
||||||
|
SDL_AudioDeviceID m_AudioDevice;
|
||||||
|
Uint32 m_ChannelCount;
|
||||||
|
Uint32 m_PendingDrops;
|
||||||
|
Uint32 m_PendingHardDrops;
|
||||||
|
Uint32 m_SampleIndex;
|
||||||
|
Uint32 m_BaselinePendingData;
|
||||||
|
};
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
#include "sdl.h"
|
||||||
|
|
||||||
|
#include <Limelight.h>
|
||||||
|
#include <SDL.h>
|
||||||
|
|
||||||
|
#include <QtGlobal>
|
||||||
|
|
||||||
|
#define MIN_QUEUED_FRAMES 2
|
||||||
|
#define MAX_QUEUED_FRAMES 4
|
||||||
|
#define STOP_THE_WORLD_LIMIT 20
|
||||||
|
#define DROP_RATIO_DENOM 32
|
||||||
|
|
||||||
|
SdlAudioRenderer::SdlAudioRenderer()
|
||||||
|
: m_AudioDevice(0),
|
||||||
|
m_ChannelCount(0),
|
||||||
|
m_PendingDrops(0),
|
||||||
|
m_PendingHardDrops(0),
|
||||||
|
m_SampleIndex(0),
|
||||||
|
m_BaselinePendingData(0)
|
||||||
|
{
|
||||||
|
SDL_assert(!SDL_WasInit(SDL_INIT_AUDIO));
|
||||||
|
if (SDL_InitSubSystem(SDL_INIT_AUDIO) != 0) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"SDL_InitSubSystem(SDL_INIT_AUDIO) failed: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
SDL_assert(SDL_WasInit(SDL_INIT_AUDIO));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SdlAudioRenderer::prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig)
|
||||||
|
{
|
||||||
|
SDL_AudioSpec want, have;
|
||||||
|
|
||||||
|
SDL_zero(want);
|
||||||
|
want.freq = opusConfig->sampleRate;
|
||||||
|
want.format = AUDIO_S16;
|
||||||
|
want.channels = opusConfig->channelCount;
|
||||||
|
|
||||||
|
// This is supposed to be a power of 2, but our
|
||||||
|
// frames contain a non-power of 2 number of samples,
|
||||||
|
// so the slop would require buffering another full frame.
|
||||||
|
// Specifying non-Po2 seems to work for our supported platforms.
|
||||||
|
want.samples = SAMPLES_PER_FRAME;
|
||||||
|
|
||||||
|
m_AudioDevice = SDL_OpenAudioDevice(NULL, 0, &want, &have, 0);
|
||||||
|
if (m_AudioDevice == 0) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Failed to open audio device: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
SDL_QuitSubSystem(SDL_INIT_AUDIO);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Desired audio buffer: %u samples (%lu bytes)",
|
||||||
|
want.samples,
|
||||||
|
want.samples * sizeof(short) * want.channels);
|
||||||
|
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Obtained audio buffer: %u samples (%u bytes)",
|
||||||
|
have.samples,
|
||||||
|
have.size);
|
||||||
|
|
||||||
|
// SDL counts pending samples in the queued
|
||||||
|
// audio size using the WASAPI backend. This
|
||||||
|
// includes silence, which can throw off our
|
||||||
|
// pending data count. Get a baseline so we
|
||||||
|
// can exclude that data.
|
||||||
|
m_BaselinePendingData = 0;
|
||||||
|
#ifdef Q_OS_WIN32
|
||||||
|
for (int i = 0; i < 100; i++) {
|
||||||
|
m_BaselinePendingData = qMax(m_BaselinePendingData, SDL_GetQueuedAudioSize(m_AudioDevice));
|
||||||
|
SDL_Delay(10);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
m_BaselinePendingData *= 2;
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Baseline pending audio data: %d bytes",
|
||||||
|
m_BaselinePendingData);
|
||||||
|
|
||||||
|
m_ChannelCount = opusConfig->channelCount;
|
||||||
|
m_SampleIndex = 0;
|
||||||
|
m_PendingDrops = m_PendingHardDrops = 0;
|
||||||
|
|
||||||
|
// Start playback
|
||||||
|
SDL_PauseAudioDevice(m_AudioDevice, 0);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
SdlAudioRenderer::~SdlAudioRenderer()
|
||||||
|
{
|
||||||
|
if (m_AudioDevice != 0) {
|
||||||
|
// Stop playback
|
||||||
|
SDL_PauseAudioDevice(m_AudioDevice, 1);
|
||||||
|
SDL_CloseAudioDevice(m_AudioDevice);
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_QuitSubSystem(SDL_INIT_AUDIO);
|
||||||
|
SDL_assert(!SDL_WasInit(SDL_INIT_AUDIO));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SdlAudioRenderer::submitAudio(short* audioBuffer, int audioSize)
|
||||||
|
{
|
||||||
|
m_SampleIndex++;
|
||||||
|
|
||||||
|
Uint32 queuedAudio = SDL_GetQueuedAudioSize(m_AudioDevice);
|
||||||
|
if (queuedAudio > m_BaselinePendingData) {
|
||||||
|
queuedAudio -= m_BaselinePendingData;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
queuedAudio = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint32 framesQueued = queuedAudio / (SAMPLES_PER_FRAME * m_ChannelCount * sizeof(short));
|
||||||
|
|
||||||
|
// We must check this prior to the below checks to ensure we don't
|
||||||
|
// underflow if framesQueued - m_PendingHardDrops < 0.
|
||||||
|
if (framesQueued <= MIN_QUEUED_FRAMES) {
|
||||||
|
m_PendingDrops = m_PendingHardDrops = 0;
|
||||||
|
}
|
||||||
|
// Pend enough drops to get us back to MIN_QUEUED_FRAMES, checking first
|
||||||
|
// to ensure we don't underflow.
|
||||||
|
else if (framesQueued > m_PendingHardDrops &&
|
||||||
|
framesQueued - m_PendingHardDrops > STOP_THE_WORLD_LIMIT) {
|
||||||
|
m_PendingHardDrops = framesQueued - MIN_QUEUED_FRAMES;
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Pending hard drop of %u audio frames",
|
||||||
|
m_PendingHardDrops);
|
||||||
|
}
|
||||||
|
// If we're under the stop the world limit, we can drop samples
|
||||||
|
// gracefully over the next little while.
|
||||||
|
else if (framesQueued > m_PendingHardDrops + m_PendingDrops &&
|
||||||
|
framesQueued - m_PendingHardDrops - m_PendingDrops > MAX_QUEUED_FRAMES) {
|
||||||
|
m_PendingDrops = framesQueued - MIN_QUEUED_FRAMES;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine if this frame should be dropped
|
||||||
|
if (m_PendingHardDrops != 0) {
|
||||||
|
// Hard drops happen all at once to forcefully
|
||||||
|
// resync with the source.
|
||||||
|
m_PendingHardDrops--;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else if (m_PendingDrops != 0 && m_SampleIndex % DROP_RATIO_DENOM == 0) {
|
||||||
|
// Normal drops are interspersed with the audio data
|
||||||
|
// to hide the glitches.
|
||||||
|
m_PendingDrops--;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SDL_QueueAudio(m_AudioDevice, audioBuffer, audioSize) < 0) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Failed to queue audio sample: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@@ -0,0 +1,436 @@
|
|||||||
|
#include "soundioaudiorenderer.h"
|
||||||
|
|
||||||
|
#include <SDL.h>
|
||||||
|
|
||||||
|
#include <QtGlobal>
|
||||||
|
|
||||||
|
SoundIoAudioRenderer::SoundIoAudioRenderer()
|
||||||
|
: m_OpusChannelCount(0),
|
||||||
|
m_SoundIo(nullptr),
|
||||||
|
m_Device(nullptr),
|
||||||
|
m_OutputStream(nullptr),
|
||||||
|
m_RingBuffer(nullptr),
|
||||||
|
m_Errored(false)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
SoundIoAudioRenderer::~SoundIoAudioRenderer()
|
||||||
|
{
|
||||||
|
if (m_OutputStream != nullptr) {
|
||||||
|
soundio_outstream_destroy(m_OutputStream);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Must be destroyed after the stream is stopped
|
||||||
|
// or we could still get sioWriteCallback() calls.
|
||||||
|
if (m_RingBuffer != nullptr) {
|
||||||
|
soundio_ring_buffer_destroy(m_RingBuffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_Device != nullptr) {
|
||||||
|
soundio_device_unref(m_Device);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_SoundIo != nullptr) {
|
||||||
|
soundio_destroy(m_SoundIo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int SoundIoAudioRenderer::scoreChannelLayout(const struct SoundIoChannelLayout* layout, const OPUS_MULTISTREAM_CONFIGURATION* opusConfig)
|
||||||
|
{
|
||||||
|
int score = 50;
|
||||||
|
|
||||||
|
// Compute a score for this layout based on how many matching channels
|
||||||
|
// we find (or acceptable alternatives).
|
||||||
|
for (int i = 0; i < layout->channel_count; i++) {
|
||||||
|
if (opusConfig->channelCount >= 2) {
|
||||||
|
switch (layout->channels[i]) {
|
||||||
|
case SoundIoChannelIdFrontLeft:
|
||||||
|
case SoundIoChannelIdFrontRight:
|
||||||
|
score += 2;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opusConfig->channelCount >= 6) {
|
||||||
|
switch (layout->channels[i]) {
|
||||||
|
case SoundIoChannelIdFrontCenter:
|
||||||
|
case SoundIoChannelIdLfe:
|
||||||
|
score += 2;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SoundIoChannelIdSideLeft:
|
||||||
|
case SoundIoChannelIdSideRight:
|
||||||
|
score++;
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Score layouts using the back L/R as higher
|
||||||
|
// value than those using side L/R.
|
||||||
|
case SoundIoChannelIdBackLeft:
|
||||||
|
case SoundIoChannelIdBackRight:
|
||||||
|
score += 2;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now subtract the difference between the desired and actual channel count
|
||||||
|
// to punish layouts that have extra unused speakers.
|
||||||
|
if (opusConfig->channelCount < layout->channel_count) {
|
||||||
|
score -= layout->channel_count - opusConfig->channelCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
return score;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SoundIoAudioRenderer::prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig)
|
||||||
|
{
|
||||||
|
m_SoundIo = soundio_create();
|
||||||
|
if (m_SoundIo == nullptr) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"soundio_create() failed");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_SoundIo->app_name = "Moonlight";
|
||||||
|
m_SoundIo->userdata = this;
|
||||||
|
m_SoundIo->on_backend_disconnect = sioBackendDisconnect;
|
||||||
|
m_SoundIo->on_devices_change = sioDevicesChanged;
|
||||||
|
|
||||||
|
int err = soundio_connect(m_SoundIo);
|
||||||
|
if (err != SoundIoErrorNone) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"soundio_connect() failed: %s",
|
||||||
|
soundio_strerror(err));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Audio backend: %s",
|
||||||
|
soundio_backend_name(m_SoundIo->current_backend));
|
||||||
|
|
||||||
|
// Don't continue if we could only open the dummy backend
|
||||||
|
if (m_SoundIo->current_backend == SoundIoBackendDummy) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flush events to update with new device arrivals
|
||||||
|
soundio_flush_events(m_SoundIo);
|
||||||
|
|
||||||
|
// Remember the actual channel count for later
|
||||||
|
m_OpusChannelCount = opusConfig->channelCount;
|
||||||
|
|
||||||
|
int outputDeviceIndex = soundio_default_output_device_index(m_SoundIo);
|
||||||
|
if (outputDeviceIndex < 0) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"No output device found");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_Device = soundio_get_output_device(m_SoundIo, outputDeviceIndex);
|
||||||
|
if (m_Device == nullptr) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"soundio_get_output_device() failed");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Selected audio device: %s",
|
||||||
|
m_Device->name);
|
||||||
|
|
||||||
|
m_OutputStream = soundio_outstream_create(m_Device);
|
||||||
|
if (m_OutputStream == nullptr) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"soundio_outstream_create() failed");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_OutputStream->format = SoundIoFormatS16NE;
|
||||||
|
m_OutputStream->sample_rate = opusConfig->sampleRate;
|
||||||
|
m_OutputStream->name = "Moonlight";
|
||||||
|
m_OutputStream->userdata = this;
|
||||||
|
m_OutputStream->error_callback = sioErrorCallback;
|
||||||
|
m_OutputStream->write_callback = sioWriteCallback;
|
||||||
|
|
||||||
|
// This determines the size of the buffers we'll
|
||||||
|
// get from CoreAudio. Since GFE sends us packets
|
||||||
|
// in 5 ms chunks, we'll give them to the OS in
|
||||||
|
// buffers of the same size.
|
||||||
|
m_OutputStream->software_latency = 0.005;
|
||||||
|
|
||||||
|
SoundIoChannelLayout bestLayout = m_Device->current_layout;
|
||||||
|
for (int i = 0; i < m_Device->layout_count; i++) {
|
||||||
|
if (scoreChannelLayout(&bestLayout, opusConfig) <
|
||||||
|
scoreChannelLayout(&m_Device->layouts[i], opusConfig)) {
|
||||||
|
bestLayout = m_Device->layouts[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bestLayout.channel_count < opusConfig->channelCount) {
|
||||||
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"No compatible channel layouts found. Some channels may not be played!");
|
||||||
|
}
|
||||||
|
|
||||||
|
m_OutputStream->layout = bestLayout;
|
||||||
|
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Native layout: %s (%d channels)",
|
||||||
|
m_OutputStream->layout.name ?
|
||||||
|
m_OutputStream->layout.name : "<UNKNOWN>",
|
||||||
|
m_OutputStream->layout.channel_count);
|
||||||
|
|
||||||
|
err = soundio_outstream_open(m_OutputStream);
|
||||||
|
if (err != SoundIoErrorNone) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"soundio_outstream_open() failed: %s",
|
||||||
|
soundio_strerror(err));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_OutputStream->layout_error) {
|
||||||
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Channel layout failed: %s",
|
||||||
|
soundio_strerror(m_OutputStream->layout_error));
|
||||||
|
|
||||||
|
// ALSA through PulseAudio appears to fail snd_pcm_set_chmap()
|
||||||
|
// even after claiming the layout is supported (and even on totally
|
||||||
|
// standard layouts like Stereo). We'll just ignore this for ALSA
|
||||||
|
// and only bail if we get an actual failure out of one of these APIs.
|
||||||
|
if (m_SoundIo->current_backend != SoundIoBackendAlsa) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m_EffectiveLayout = m_OutputStream->layout;
|
||||||
|
for (int i = 0; i < m_EffectiveLayout.channel_count; i++) {
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
if (m_EffectiveLayout.channels[i] == SoundIoChannelIdSideRight) {
|
||||||
|
m_EffectiveLayout.channels[i] = SoundIoChannelIdBackRight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buffer up to 6 packets of audio (30 ms) to smooth
|
||||||
|
// out network packet delivery jitter
|
||||||
|
m_RingBuffer = soundio_ring_buffer_create(m_SoundIo,
|
||||||
|
m_OutputStream->bytes_per_sample *
|
||||||
|
m_OpusChannelCount *
|
||||||
|
SAMPLES_PER_FRAME * 6);
|
||||||
|
if (m_RingBuffer == nullptr) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"soundio_ring_buffer_create() failed");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
err = soundio_outstream_start(m_OutputStream);
|
||||||
|
if (err != SoundIoErrorNone) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"soundio_outstream_start() failed: %s",
|
||||||
|
soundio_strerror(err));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SoundIoAudioRenderer::submitAudio(short* audioBuffer, int audioSize)
|
||||||
|
{
|
||||||
|
if (m_Errored) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flush events to update with new device arrivals
|
||||||
|
soundio_flush_events(m_SoundIo);
|
||||||
|
|
||||||
|
// We must always write a full frame of audio. If we don't,
|
||||||
|
// the reader will get out of sync with the writer and our
|
||||||
|
// channels will get all mixed up. To ensure this is always
|
||||||
|
// the case, round our bytes free down to the next multiple
|
||||||
|
// of our frame size.
|
||||||
|
int bytesFree = soundio_ring_buffer_free_count(m_RingBuffer);
|
||||||
|
int bytesPerFrame = m_OpusChannelCount * m_OutputStream->bytes_per_sample;
|
||||||
|
int bytesToWrite = qMin(audioSize, (bytesFree / bytesPerFrame) * bytesPerFrame);
|
||||||
|
memcpy(soundio_ring_buffer_write_ptr(m_RingBuffer), audioBuffer, bytesToWrite);
|
||||||
|
soundio_ring_buffer_advance_write_ptr(m_RingBuffer, bytesToWrite);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SoundIoAudioRenderer::sioErrorCallback(SoundIoOutStream* stream, int err)
|
||||||
|
{
|
||||||
|
auto me = reinterpret_cast<SoundIoAudioRenderer*>(stream->userdata);
|
||||||
|
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Audio rendering error: %s",
|
||||||
|
soundio_strerror(err));
|
||||||
|
|
||||||
|
// Trigger reinitialization
|
||||||
|
me->m_Errored = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SoundIoAudioRenderer::sioBackendDisconnect(SoundIo* soundio, int err)
|
||||||
|
{
|
||||||
|
auto me = reinterpret_cast<SoundIoAudioRenderer*>(soundio->userdata);
|
||||||
|
|
||||||
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Audio backend disconnected: %s",
|
||||||
|
soundio_strerror(err));
|
||||||
|
|
||||||
|
// Trigger reinitialization
|
||||||
|
me->m_Errored = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SoundIoAudioRenderer::sioDevicesChanged(SoundIo* soundio)
|
||||||
|
{
|
||||||
|
auto me = reinterpret_cast<SoundIoAudioRenderer*>(soundio->userdata);
|
||||||
|
|
||||||
|
if (me->m_Device == nullptr) {
|
||||||
|
// Ignore calls that take place during initialization
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int outputDeviceIndex = soundio_default_output_device_index(soundio);
|
||||||
|
if (outputDeviceIndex >= 0) {
|
||||||
|
struct SoundIoDevice* outputDevice = soundio_get_output_device(soundio, outputDeviceIndex);
|
||||||
|
if (outputDevice == nullptr) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"soundio_get_output_device() failed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!soundio_device_equal(outputDevice, me->m_Device)) {
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Default audio output device changed");
|
||||||
|
|
||||||
|
// Trigger reinitialization
|
||||||
|
me->m_Errored = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
soundio_device_unref(outputDevice);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// bytes_per_frame should never be used on the ring buffer! It's not always
|
||||||
|
// the same number of bytes per frames as the output stream!
|
||||||
|
void SoundIoAudioRenderer::sioWriteCallback(SoundIoOutStream* stream, int frameCountMin, int frameCountMax)
|
||||||
|
{
|
||||||
|
auto me = reinterpret_cast<SoundIoAudioRenderer*>(stream->userdata);
|
||||||
|
char* readPtr = soundio_ring_buffer_read_ptr(me->m_RingBuffer);
|
||||||
|
int framesLeft = soundio_ring_buffer_fill_count(me->m_RingBuffer) /
|
||||||
|
(me->m_OpusChannelCount * stream->bytes_per_sample);
|
||||||
|
int bytesRead = 0;
|
||||||
|
|
||||||
|
// Clamp framesLeft to frameCountMax
|
||||||
|
framesLeft = qMin(framesLeft, frameCountMax);
|
||||||
|
|
||||||
|
// Place an upper-bound on audio stream latency to
|
||||||
|
// avoid accumulating packets in queue-based backends
|
||||||
|
// like WASAPI. This bound was set by testing on several
|
||||||
|
// Windows machines. The highest latency was found on
|
||||||
|
// a XPS 9343 running Windows 7 in Steam Big Picture
|
||||||
|
// and the 5.1 audio test clip from Fraunhofer.
|
||||||
|
if (me->m_SoundIo->current_backend == SoundIoBackendWasapi) {
|
||||||
|
double latency;
|
||||||
|
if (soundio_outstream_get_latency(stream, &latency) == SoundIoErrorNone) {
|
||||||
|
if (latency > 0.050) {
|
||||||
|
// If our latency is higher than desired, drop these samples to gracefully lower
|
||||||
|
// the latency without glitching too much. Dropping the whole buffer causes
|
||||||
|
// a much more noticeable glitch. This approach also ensures that we don't
|
||||||
|
// accidentally underflow if the driver/kernel side is delayed and isn't
|
||||||
|
// consuming data fast enough. Dropping a frame at a time and re-evaluating
|
||||||
|
// each time ensures that we'll stop dropping if latency returns to normal.
|
||||||
|
readPtr += framesLeft * stream->bytes_per_sample * me->m_OpusChannelCount;
|
||||||
|
bytesRead += framesLeft * stream->bytes_per_sample * me->m_OpusChannelCount;
|
||||||
|
framesLeft = 0;
|
||||||
|
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Latency exceeded drop cap: %f",
|
||||||
|
latency);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (;;) {
|
||||||
|
int frameCount;
|
||||||
|
int err;
|
||||||
|
struct SoundIoChannelArea* areas;
|
||||||
|
|
||||||
|
// Always meet the minimum but don't write more than that
|
||||||
|
// if we'll have to insert silence
|
||||||
|
frameCount = qMax(framesLeft, frameCountMin);
|
||||||
|
|
||||||
|
if (frameCount == 0) {
|
||||||
|
// Nothing more to write
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
err = soundio_outstream_begin_write(stream, &areas, &frameCount);
|
||||||
|
if (err != SoundIoErrorNone) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"soundio_outstream_begin_write() failed: %s",
|
||||||
|
soundio_strerror(err));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
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.
|
||||||
|
// 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) {
|
||||||
|
// Write silence if we have no buffered frames left or
|
||||||
|
// nothing in the audio stream for this channel
|
||||||
|
memset(areas[ch].ptr, 0, stream->bytes_per_sample);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Write audio data from our ring buffer
|
||||||
|
memcpy(areas[ch].ptr,
|
||||||
|
&readPtr[readPtrChannel * stream->bytes_per_sample],
|
||||||
|
stream->bytes_per_sample);
|
||||||
|
}
|
||||||
|
|
||||||
|
areas[ch].ptr += areas[ch].step;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move on to the next frame if we aren't inserting silence
|
||||||
|
if (frame < framesLeft) {
|
||||||
|
readPtr += stream->bytes_per_sample * me->m_OpusChannelCount;
|
||||||
|
bytesRead += stream->bytes_per_sample * me->m_OpusChannelCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = soundio_outstream_end_write(stream);
|
||||||
|
if (err != SoundIoErrorNone && err != SoundIoErrorUnderflow) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"soundio_outstream_end_write() failed: %s",
|
||||||
|
soundio_strerror(err));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (framesLeft >= frameCount) {
|
||||||
|
framesLeft -= frameCount;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
framesLeft = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (frameCountMin >= frameCount) {
|
||||||
|
frameCountMin -= frameCount;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
frameCountMin = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
soundio_ring_buffer_advance_read_ptr(me->m_RingBuffer, bytesRead);
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "renderer.h"
|
||||||
|
|
||||||
|
#include <soundio/soundio.h>
|
||||||
|
|
||||||
|
class SoundIoAudioRenderer : public IAudioRenderer
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
SoundIoAudioRenderer();
|
||||||
|
|
||||||
|
~SoundIoAudioRenderer();
|
||||||
|
|
||||||
|
virtual bool prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig);
|
||||||
|
|
||||||
|
virtual bool submitAudio(short* audioBuffer, int audioSize);
|
||||||
|
|
||||||
|
private:
|
||||||
|
int scoreChannelLayout(const struct SoundIoChannelLayout* layout, const OPUS_MULTISTREAM_CONFIGURATION* opusConfig);
|
||||||
|
|
||||||
|
static void sioErrorCallback(struct SoundIoOutStream* stream, int err);
|
||||||
|
|
||||||
|
static void sioWriteCallback(struct SoundIoOutStream* stream, int frameCountMin, int frameCountMax);
|
||||||
|
|
||||||
|
static void sioBackendDisconnect(struct SoundIo* soundio, int err);
|
||||||
|
|
||||||
|
static void sioDevicesChanged(SoundIo* soundio);
|
||||||
|
|
||||||
|
int m_OpusChannelCount;
|
||||||
|
struct SoundIo* m_SoundIo;
|
||||||
|
struct SoundIoDevice* m_Device;
|
||||||
|
struct SoundIoOutStream* m_OutputStream;
|
||||||
|
struct SoundIoRingBuffer* m_RingBuffer;
|
||||||
|
struct SoundIoChannelLayout m_EffectiveLayout;
|
||||||
|
bool m_Errored;
|
||||||
|
};
|
||||||
+417
-28
@@ -1,8 +1,11 @@
|
|||||||
#include <Limelight.h>
|
#include <Limelight.h>
|
||||||
#include <SDL.h>
|
#include <SDL.h>
|
||||||
#include "streaming/session.hpp"
|
#include "streaming/session.h"
|
||||||
|
#include "settings/mappingmanager.h"
|
||||||
|
#include "path.h"
|
||||||
|
|
||||||
#include <QtGlobal>
|
#include <QtGlobal>
|
||||||
|
#include <QDir>
|
||||||
|
|
||||||
#define VK_0 0x30
|
#define VK_0 0x30
|
||||||
#define VK_A 0x41
|
#define VK_A 0x41
|
||||||
@@ -14,6 +17,15 @@
|
|||||||
#define VK_NUMPAD0 0x60
|
#define VK_NUMPAD0 0x60
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
// How long the mouse button will be pressed for a tap to click gesture
|
||||||
|
#define TAP_BUTTON_RELEASE_DELAY 100
|
||||||
|
|
||||||
|
// How long the fingers must be stationary to start a drag
|
||||||
|
#define DRAG_ACTIVATION_DELAY 650
|
||||||
|
|
||||||
|
// How far the finger can move before it cancels a drag or tap
|
||||||
|
#define DEAD_ZONE_DELTA 0.1f
|
||||||
|
|
||||||
const int SdlInputHandler::k_ButtonMap[] = {
|
const int SdlInputHandler::k_ButtonMap[] = {
|
||||||
A_FLAG, B_FLAG, X_FLAG, Y_FLAG,
|
A_FLAG, B_FLAG, X_FLAG, Y_FLAG,
|
||||||
BACK_FLAG, SPECIAL_FLAG, PLAY_FLAG,
|
BACK_FLAG, SPECIAL_FLAG, PLAY_FLAG,
|
||||||
@@ -22,12 +34,28 @@ const int SdlInputHandler::k_ButtonMap[] = {
|
|||||||
UP_FLAG, DOWN_FLAG, LEFT_FLAG, RIGHT_FLAG
|
UP_FLAG, DOWN_FLAG, LEFT_FLAG, RIGHT_FLAG
|
||||||
};
|
};
|
||||||
|
|
||||||
SdlInputHandler::SdlInputHandler(bool multiController)
|
SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, NvComputer* computer, int streamWidth, int streamHeight)
|
||||||
: m_MultiController(multiController)
|
: m_LastMouseMotionTime(0),
|
||||||
|
m_MultiController(prefs.multiController),
|
||||||
|
m_NeedsInputDelay(false),
|
||||||
|
m_LeftButtonReleaseTimer(0),
|
||||||
|
m_RightButtonReleaseTimer(0),
|
||||||
|
m_DragTimer(0),
|
||||||
|
m_DragButton(0),
|
||||||
|
m_NumFingersDown(0),
|
||||||
|
m_StreamWidth(streamWidth),
|
||||||
|
m_StreamHeight(streamHeight)
|
||||||
{
|
{
|
||||||
// Allow gamepad input when the app doesn't have focus
|
// Allow gamepad input when the app doesn't have focus
|
||||||
SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1");
|
SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1");
|
||||||
|
|
||||||
|
// If mouse acceleration is enabled, use relative mode warp (which
|
||||||
|
// is via normal motion events that are influenced by mouse acceleration).
|
||||||
|
// Otherwise, we'll use raw input capture which is straight from the device
|
||||||
|
// without modification by the OS.
|
||||||
|
SDL_SetHint(SDL_HINT_MOUSE_RELATIVE_MODE_WARP,
|
||||||
|
prefs.mouseAcceleration ? "1" : "0");
|
||||||
|
|
||||||
// We need to reinit this each time, since you only get
|
// We need to reinit this each time, since you only get
|
||||||
// an initial set of gamepad arrival events once per init.
|
// an initial set of gamepad arrival events once per init.
|
||||||
SDL_assert(!SDL_WasInit(SDL_INIT_GAMECONTROLLER));
|
SDL_assert(!SDL_WasInit(SDL_INIT_GAMECONTROLLER));
|
||||||
@@ -37,6 +65,9 @@ SdlInputHandler::SdlInputHandler(bool multiController)
|
|||||||
SDL_GetError());
|
SDL_GetError());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MappingManager mappingManager;
|
||||||
|
mappingManager.applyMappings();
|
||||||
|
|
||||||
if (!m_MultiController) {
|
if (!m_MultiController) {
|
||||||
// Player 1 is always present in non-MC mode
|
// Player 1 is always present in non-MC mode
|
||||||
m_GamepadMask = 0x1;
|
m_GamepadMask = 0x1;
|
||||||
@@ -46,7 +77,22 @@ SdlInputHandler::SdlInputHandler(bool multiController)
|
|||||||
m_GamepadMask = 0;
|
m_GamepadMask = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Prior to GFE 3.14.1, sending too many mouse motion events can cause
|
||||||
|
// GFE to choke and input latency to increase significantly. We will
|
||||||
|
// artificially throttle them to avoid this situation.
|
||||||
|
QVector<int> gfeVersion = NvHTTP::parseQuad(computer->gfeVersion);
|
||||||
|
if (gfeVersion.isEmpty() || // Very old versions don't have GfeVersion at all
|
||||||
|
gfeVersion[0] < 3 ||
|
||||||
|
(gfeVersion[0] == 3 && gfeVersion[1] < 14) ||
|
||||||
|
(gfeVersion[0] == 3 && gfeVersion[1] == 14 && gfeVersion[2] < 1)) {
|
||||||
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"This older version of GFE requires input delay hack");
|
||||||
|
m_NeedsInputDelay = true;
|
||||||
|
}
|
||||||
|
|
||||||
SDL_zero(m_GamepadState);
|
SDL_zero(m_GamepadState);
|
||||||
|
SDL_zero(m_TouchDownEvent);
|
||||||
|
SDL_zero(m_CumulativeDelta);
|
||||||
}
|
}
|
||||||
|
|
||||||
SdlInputHandler::~SdlInputHandler()
|
SdlInputHandler::~SdlInputHandler()
|
||||||
@@ -57,6 +103,10 @@ SdlInputHandler::~SdlInputHandler()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SDL_RemoveTimer(m_LeftButtonReleaseTimer);
|
||||||
|
SDL_RemoveTimer(m_RightButtonReleaseTimer);
|
||||||
|
SDL_RemoveTimer(m_DragTimer);
|
||||||
|
|
||||||
SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER);
|
SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER);
|
||||||
SDL_assert(!SDL_WasInit(SDL_INIT_GAMECONTROLLER));
|
SDL_assert(!SDL_WasInit(SDL_INIT_GAMECONTROLLER));
|
||||||
}
|
}
|
||||||
@@ -71,28 +121,13 @@ void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
|
|||||||
(event->keysym.mod & KMOD_CTRL) &&
|
(event->keysym.mod & KMOD_CTRL) &&
|
||||||
(event->keysym.mod & KMOD_ALT) &&
|
(event->keysym.mod & KMOD_ALT) &&
|
||||||
(event->keysym.mod & KMOD_SHIFT)) {
|
(event->keysym.mod & KMOD_SHIFT)) {
|
||||||
|
|
||||||
// Force raise all keys in the combo to avoid
|
|
||||||
// leaving them down after disconnecting
|
|
||||||
LiSendKeyboardEvent(0xA0, KEY_ACTION_UP, 0);
|
|
||||||
LiSendKeyboardEvent(0xA1, KEY_ACTION_UP, 0);
|
|
||||||
LiSendKeyboardEvent(0xA2, KEY_ACTION_UP, 0);
|
|
||||||
LiSendKeyboardEvent(0xA3, KEY_ACTION_UP, 0);
|
|
||||||
LiSendKeyboardEvent(0xA4, KEY_ACTION_UP, 0);
|
|
||||||
LiSendKeyboardEvent(0xA5, KEY_ACTION_UP, 0);
|
|
||||||
|
|
||||||
// Check for quit combo (Ctrl+Alt+Shift+Q)
|
// Check for quit combo (Ctrl+Alt+Shift+Q)
|
||||||
if (event->keysym.sym == SDLK_q) {
|
if (event->keysym.sym == SDLK_q) {
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"Detected quit key combo");
|
"Detected quit key combo");
|
||||||
|
|
||||||
// Uncapture the mouse to avoid processing any
|
|
||||||
// further keyboard input
|
|
||||||
SDL_SetRelativeMouseMode(SDL_FALSE);
|
|
||||||
|
|
||||||
SDL_Event event;
|
|
||||||
|
|
||||||
// Push a quit event to the main loop
|
// Push a quit event to the main loop
|
||||||
|
SDL_Event event;
|
||||||
event.type = SDL_QUIT;
|
event.type = SDL_QUIT;
|
||||||
event.quit.timestamp = SDL_GetTicks();
|
event.quit.timestamp = SDL_GetTicks();
|
||||||
SDL_PushEvent(&event);
|
SDL_PushEvent(&event);
|
||||||
@@ -102,7 +137,13 @@ void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
|
|||||||
else if (event->keysym.sym == SDLK_z) {
|
else if (event->keysym.sym == SDLK_z) {
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"Detected mouse capture toggle combo");
|
"Detected mouse capture toggle combo");
|
||||||
|
|
||||||
|
// Stop handling future input
|
||||||
SDL_SetRelativeMouseMode((SDL_bool)!SDL_GetRelativeMouseMode());
|
SDL_SetRelativeMouseMode((SDL_bool)!SDL_GetRelativeMouseMode());
|
||||||
|
|
||||||
|
// Force raise all keys to ensure they aren't stuck,
|
||||||
|
// since we won't get their key up events.
|
||||||
|
raiseAllKeys();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Check for the full-screen combo (Ctrl+Alt+Shift+X)
|
// Check for the full-screen combo (Ctrl+Alt+Shift+X)
|
||||||
@@ -110,6 +151,10 @@ void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
|
|||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"Detected full-screen toggle combo");
|
"Detected full-screen toggle combo");
|
||||||
Session::s_ActiveSession->toggleFullscreen();
|
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;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -324,6 +369,9 @@ void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
|
|||||||
case SDL_SCANCODE_APOSTROPHE:
|
case SDL_SCANCODE_APOSTROPHE:
|
||||||
keyCode = 0xDE;
|
keyCode = 0xDE;
|
||||||
break;
|
break;
|
||||||
|
case SDL_SCANCODE_NONUSBACKSLASH:
|
||||||
|
keyCode = 0xE2;
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"Unhandled button event: %d",
|
"Unhandled button event: %d",
|
||||||
@@ -332,6 +380,14 @@ void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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,
|
LiSendKeyboardEvent(keyCode,
|
||||||
event->state == SDL_PRESSED ?
|
event->state == SDL_PRESSED ?
|
||||||
KEY_ACTION_DOWN : KEY_ACTION_UP,
|
KEY_ACTION_DOWN : KEY_ACTION_UP,
|
||||||
@@ -342,9 +398,13 @@ void SdlInputHandler::handleMouseButtonEvent(SDL_MouseButtonEvent* event)
|
|||||||
{
|
{
|
||||||
int button;
|
int button;
|
||||||
|
|
||||||
// Capture the mouse again if clicked when unbound
|
// Capture the mouse again if clicked when unbound.
|
||||||
|
// We start capture on left button released instead of
|
||||||
|
// pressed to avoid sending an errant mouse button released
|
||||||
|
// event to the host when clicking into our window (since
|
||||||
|
// the pressed event was consumed by this code).
|
||||||
if (event->button == SDL_BUTTON_LEFT &&
|
if (event->button == SDL_BUTTON_LEFT &&
|
||||||
event->state == SDL_PRESSED &&
|
event->state == SDL_RELEASED &&
|
||||||
!SDL_GetRelativeMouseMode()) {
|
!SDL_GetRelativeMouseMode()) {
|
||||||
SDL_SetRelativeMouseMode(SDL_TRUE);
|
SDL_SetRelativeMouseMode(SDL_TRUE);
|
||||||
return;
|
return;
|
||||||
@@ -353,6 +413,10 @@ void SdlInputHandler::handleMouseButtonEvent(SDL_MouseButtonEvent* event)
|
|||||||
// Not capturing
|
// Not capturing
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
else if (event->which == SDL_TOUCH_MOUSEID) {
|
||||||
|
// Ignore synthetic mouse events
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
switch (event->button)
|
switch (event->button)
|
||||||
{
|
{
|
||||||
@@ -366,9 +430,11 @@ void SdlInputHandler::handleMouseButtonEvent(SDL_MouseButtonEvent* event)
|
|||||||
button = BUTTON_RIGHT;
|
button = BUTTON_RIGHT;
|
||||||
break;
|
break;
|
||||||
case SDL_BUTTON_X1:
|
case SDL_BUTTON_X1:
|
||||||
|
button = BUTTON_X1;
|
||||||
|
break;
|
||||||
case SDL_BUTTON_X2:
|
case SDL_BUTTON_X2:
|
||||||
// Unsupported by GameStream
|
button = BUTTON_X2;
|
||||||
return;
|
break;
|
||||||
default:
|
default:
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"Unhandled button event: %d",
|
"Unhandled button event: %d",
|
||||||
@@ -388,10 +454,44 @@ void SdlInputHandler::handleMouseMotionEvent(SDL_MouseMotionEvent* event)
|
|||||||
// Not capturing
|
// Not capturing
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
else if (event->which == SDL_TOUCH_MOUSEID) {
|
||||||
|
// Ignore synthetic mouse events
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (event->xrel != 0 || event->yrel != 0) {
|
short xdelta = (short)event->xrel;
|
||||||
LiSendMouseMoveEvent((short)event->xrel,
|
short ydelta = (short)event->yrel;
|
||||||
(short)event->yrel);
|
|
||||||
|
// If we're sending more than one motion event per millisecond,
|
||||||
|
// delay for 1 ms to allow batching of mouse move events. On older
|
||||||
|
// versions of GFE, we will unconditionally wait this 1 ms to
|
||||||
|
// work around an input processing issue that causes massive mouse latency.
|
||||||
|
Uint32 currentTime = SDL_GetTicks();
|
||||||
|
if (m_NeedsInputDelay || !SDL_TICKS_PASSED(currentTime, m_LastMouseMotionTime + 1)) {
|
||||||
|
SDL_Delay(1);
|
||||||
|
currentTime = SDL_GetTicks();
|
||||||
|
}
|
||||||
|
m_LastMouseMotionTime = currentTime;
|
||||||
|
|
||||||
|
// Pump even if we didn't delay since we might get some extra events
|
||||||
|
SDL_PumpEvents();
|
||||||
|
|
||||||
|
// Batch all of the pending mouse motion events
|
||||||
|
SDL_Event nextEvent;
|
||||||
|
while (SDL_PeepEvents(&nextEvent,
|
||||||
|
1,
|
||||||
|
SDL_GETEVENT,
|
||||||
|
SDL_MOUSEMOTION,
|
||||||
|
SDL_MOUSEMOTION) == 1) {
|
||||||
|
// In theory, these can overflow but in practice
|
||||||
|
// it should be highly unlikely since it would require
|
||||||
|
// moving 64K pixels in 1 ms.
|
||||||
|
xdelta += nextEvent.motion.xrel;
|
||||||
|
ydelta += nextEvent.motion.yrel;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (xdelta != 0 || ydelta != 0) {
|
||||||
|
LiSendMouseMoveEvent(xdelta, ydelta);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -401,6 +501,10 @@ void SdlInputHandler::handleMouseWheelEvent(SDL_MouseWheelEvent* event)
|
|||||||
// Not capturing
|
// Not capturing
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
else if (event->which == SDL_TOUCH_MOUSEID) {
|
||||||
|
// Ignore synthetic mouse events
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (event->y != 0) {
|
if (event->y != 0) {
|
||||||
LiSendScrollEvent((signed char)event->y);
|
LiSendScrollEvent((signed char)event->y);
|
||||||
@@ -438,6 +542,36 @@ void SdlInputHandler::sendGamepadState(GamepadState* state)
|
|||||||
state->rsY);
|
state->rsY);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Uint32 SdlInputHandler::releaseLeftButtonTimerCallback(Uint32, void*)
|
||||||
|
{
|
||||||
|
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint32 SdlInputHandler::releaseRightButtonTimerCallback(Uint32, void*)
|
||||||
|
{
|
||||||
|
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_RIGHT);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint32 SdlInputHandler::dragTimerCallback(Uint32, void *param)
|
||||||
|
{
|
||||||
|
auto me = reinterpret_cast<SdlInputHandler*>(param);
|
||||||
|
|
||||||
|
// Check how many fingers are down now to decide
|
||||||
|
// which button to hold down
|
||||||
|
if (me->m_NumFingersDown == 2) {
|
||||||
|
me->m_DragButton = BUTTON_RIGHT;
|
||||||
|
}
|
||||||
|
else if (me->m_NumFingersDown == 1) {
|
||||||
|
me->m_DragButton = BUTTON_LEFT;
|
||||||
|
}
|
||||||
|
|
||||||
|
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, me->m_DragButton);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
void SdlInputHandler::handleControllerAxisEvent(SDL_ControllerAxisEvent* event)
|
void SdlInputHandler::handleControllerAxisEvent(SDL_ControllerAxisEvent* event)
|
||||||
{
|
{
|
||||||
GamepadState* state = findStateForGamepad(event->which);
|
GamepadState* state = findStateForGamepad(event->which);
|
||||||
@@ -494,6 +628,23 @@ void SdlInputHandler::handleControllerButtonEvent(SDL_ControllerButtonEvent* eve
|
|||||||
state->buttons &= ~k_ButtonMap[event->button];
|
state->buttons &= ~k_ButtonMap[event->button];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle Start+Select+L1+R1 as a gamepad quit combo
|
||||||
|
if (state->buttons == (PLAY_FLAG | BACK_FLAG | LB_FLAG | RB_FLAG)) {
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Detected quit gamepad button combo");
|
||||||
|
|
||||||
|
// Push a quit event to the main loop
|
||||||
|
SDL_Event event;
|
||||||
|
event.type = SDL_QUIT;
|
||||||
|
event.quit.timestamp = SDL_GetTicks();
|
||||||
|
SDL_PushEvent(&event);
|
||||||
|
|
||||||
|
// Clear buttons down on this gameapd
|
||||||
|
LiSendMultiControllerEvent(state->index, m_GamepadMask,
|
||||||
|
0, 0, 0, 0, 0, 0, 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
sendGamepadState(state);
|
sendGamepadState(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -583,9 +734,172 @@ void SdlInputHandler::handleControllerDeviceEvent(SDL_ControllerDeviceEvent* eve
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SdlInputHandler::handleJoystickArrivalEvent(SDL_JoyDeviceEvent* event)
|
||||||
|
{
|
||||||
|
SDL_assert(event->type == SDL_JOYDEVICEADDED);
|
||||||
|
|
||||||
|
if (!SDL_IsGameController(event->which)) {
|
||||||
|
char guidStr[33];
|
||||||
|
SDL_JoystickGetGUIDString(SDL_JoystickGetDeviceGUID(event->which),
|
||||||
|
guidStr, sizeof(guidStr));
|
||||||
|
const char* name = SDL_JoystickNameForIndex(event->which);
|
||||||
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Joystick discovered with no mapping: %s %s",
|
||||||
|
name ? name : "<UNKNOWN>",
|
||||||
|
guidStr);
|
||||||
|
SDL_Joystick* joy = SDL_JoystickOpen(event->which);
|
||||||
|
if (joy != nullptr) {
|
||||||
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Number of axes: %d | Number of buttons: %d | Number of hats: %d",
|
||||||
|
SDL_JoystickNumAxes(joy), SDL_JoystickNumButtons(joy),
|
||||||
|
SDL_JoystickNumHats(joy));
|
||||||
|
SDL_JoystickClose(joy);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Unable to open joystick for query: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SdlInputHandler::handleTouchFingerEvent(SDL_TouchFingerEvent* event)
|
||||||
|
{
|
||||||
|
int fingerIndex = -1;
|
||||||
|
|
||||||
|
// Observations on Windows 10: x and y appear to be relative to 0,0 of the window client area.
|
||||||
|
// Although SDL documentation states they are 0.0 - 1.0 float values, they can actually be higher
|
||||||
|
// or lower than those values as touch events continue for touches started within the client area that
|
||||||
|
// leave the client area during a drag motion.
|
||||||
|
// dx and dy are deltas from the last touch event, not the first touch down.
|
||||||
|
|
||||||
|
// Determine the index of this finger using our list
|
||||||
|
// of fingers that are currently active on screen.
|
||||||
|
// This is also required to handle finger up which
|
||||||
|
// where the finger will not be in SDL_GetTouchFinger()
|
||||||
|
// anymore.
|
||||||
|
if (event->type != SDL_FINGERDOWN) {
|
||||||
|
for (int i = 0; i < MAX_FINGERS; i++) {
|
||||||
|
if (event->fingerId == m_TouchDownEvent[i].fingerId) {
|
||||||
|
fingerIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Resolve the new finger by determining the ID of each
|
||||||
|
// finger on the display.
|
||||||
|
int numTouchFingers = SDL_GetNumTouchFingers(event->touchId);
|
||||||
|
for (int i = 0; i < numTouchFingers; i++) {
|
||||||
|
SDL_Finger* finger = SDL_GetTouchFinger(event->touchId, i);
|
||||||
|
SDL_assert(finger != nullptr);
|
||||||
|
if (finger != nullptr) {
|
||||||
|
if (finger->id == event->fingerId) {
|
||||||
|
fingerIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fingerIndex < 0 || fingerIndex >= MAX_FINGERS) {
|
||||||
|
// Too many fingers
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle cursor motion based on the position of the
|
||||||
|
// primary finger on screen
|
||||||
|
if (fingerIndex == 0) {
|
||||||
|
// The event x and y values are relative to our window width
|
||||||
|
// and height. However, we want to scale them to be relative
|
||||||
|
// to the host resolution. Fortunately this is easy since we
|
||||||
|
// already have normalized values. We'll just multiply them
|
||||||
|
// by the stream dimensions to get real X and Y values rather
|
||||||
|
// than the client window dimensions.
|
||||||
|
short deltaX = static_cast<short>(event->dx * m_StreamWidth);
|
||||||
|
short deltaY = static_cast<short>(event->dy * m_StreamHeight);
|
||||||
|
if (deltaX != 0 || deltaY != 0) {
|
||||||
|
LiSendMouseMoveEvent(deltaX, deltaY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start a drag timer when primary or secondary
|
||||||
|
// fingers go down
|
||||||
|
if (event->type == SDL_FINGERDOWN &&
|
||||||
|
(fingerIndex == 0 || fingerIndex == 1)) {
|
||||||
|
SDL_RemoveTimer(m_DragTimer);
|
||||||
|
m_DragTimer = SDL_AddTimer(DRAG_ACTIVATION_DELAY,
|
||||||
|
dragTimerCallback,
|
||||||
|
this);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event->type == SDL_FINGERMOTION) {
|
||||||
|
// Count the total cumulative dx/dy that the finger
|
||||||
|
// has moved.
|
||||||
|
m_CumulativeDelta[fingerIndex] += qAbs(event->x);
|
||||||
|
m_CumulativeDelta[fingerIndex] += qAbs(event->y);
|
||||||
|
|
||||||
|
// If it's outside the deadzone delta, cancel drags and taps
|
||||||
|
if (m_CumulativeDelta[fingerIndex] > DEAD_ZONE_DELTA) {
|
||||||
|
SDL_RemoveTimer(m_DragTimer);
|
||||||
|
m_DragTimer = 0;
|
||||||
|
|
||||||
|
// This effectively cancels the tap logic below
|
||||||
|
m_TouchDownEvent[fingerIndex].timestamp = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event->type == SDL_FINGERUP) {
|
||||||
|
// Cancel the drag timer on finger up
|
||||||
|
SDL_RemoveTimer(m_DragTimer);
|
||||||
|
m_DragTimer = 0;
|
||||||
|
|
||||||
|
// Release any drag
|
||||||
|
if (m_DragButton != 0) {
|
||||||
|
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, m_DragButton);
|
||||||
|
m_DragButton = 0;
|
||||||
|
}
|
||||||
|
// 2 finger tap
|
||||||
|
else if (event->timestamp - m_TouchDownEvent[1].timestamp < 250) {
|
||||||
|
// Zero timestamp of the primary finger to ensure we won't
|
||||||
|
// generate a left click if the primary finger comes up soon.
|
||||||
|
m_TouchDownEvent[0].timestamp = 0;
|
||||||
|
|
||||||
|
// Press down the right mouse button
|
||||||
|
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_RIGHT);
|
||||||
|
|
||||||
|
// Queue a timer to release it in 100 ms
|
||||||
|
SDL_RemoveTimer(m_RightButtonReleaseTimer);
|
||||||
|
m_RightButtonReleaseTimer = SDL_AddTimer(TAP_BUTTON_RELEASE_DELAY,
|
||||||
|
releaseRightButtonTimerCallback,
|
||||||
|
nullptr);
|
||||||
|
}
|
||||||
|
// 1 finger tap
|
||||||
|
else if (event->timestamp - m_TouchDownEvent[0].timestamp < 250) {
|
||||||
|
// Press down the left mouse button
|
||||||
|
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT);
|
||||||
|
|
||||||
|
// Queue a timer to release it in 100 ms
|
||||||
|
SDL_RemoveTimer(m_LeftButtonReleaseTimer);
|
||||||
|
m_LeftButtonReleaseTimer = SDL_AddTimer(TAP_BUTTON_RELEASE_DELAY,
|
||||||
|
releaseLeftButtonTimerCallback,
|
||||||
|
nullptr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m_NumFingersDown = SDL_GetNumTouchFingers(event->touchId);
|
||||||
|
|
||||||
|
if (event->type == SDL_FINGERDOWN) {
|
||||||
|
m_TouchDownEvent[fingerIndex] = *event;
|
||||||
|
m_CumulativeDelta[fingerIndex] = 0;
|
||||||
|
}
|
||||||
|
else if (event->type == SDL_FINGERUP) {
|
||||||
|
m_TouchDownEvent[fingerIndex] = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
int SdlInputHandler::getAttachedGamepadMask()
|
int SdlInputHandler::getAttachedGamepadMask()
|
||||||
{
|
{
|
||||||
int i;
|
|
||||||
int count;
|
int count;
|
||||||
int mask;
|
int mask;
|
||||||
|
|
||||||
@@ -595,7 +909,7 @@ int SdlInputHandler::getAttachedGamepadMask()
|
|||||||
}
|
}
|
||||||
|
|
||||||
count = mask = 0;
|
count = mask = 0;
|
||||||
for (i = 0; i < SDL_NumJoysticks(); i++) {
|
for (int i = 0; i < SDL_NumJoysticks(); i++) {
|
||||||
if (SDL_IsGameController(i)) {
|
if (SDL_IsGameController(i)) {
|
||||||
mask |= (1 << count++);
|
mask |= (1 << count++);
|
||||||
}
|
}
|
||||||
@@ -603,3 +917,78 @@ int SdlInputHandler::getAttachedGamepadMask()
|
|||||||
|
|
||||||
return mask;
|
return mask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SdlInputHandler::raiseAllKeys()
|
||||||
|
{
|
||||||
|
if (m_KeysDown.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Raising %d keys",
|
||||||
|
m_KeysDown.count());
|
||||||
|
|
||||||
|
for (auto keyDown : m_KeysDown) {
|
||||||
|
LiSendKeyboardEvent(keyDown, KEY_ACTION_UP, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
m_KeysDown.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString SdlInputHandler::getUnmappedGamepads()
|
||||||
|
{
|
||||||
|
QString ret;
|
||||||
|
|
||||||
|
if (SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) != 0) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) failed: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
}
|
||||||
|
|
||||||
|
MappingManager mappingManager;
|
||||||
|
mappingManager.applyMappings();
|
||||||
|
|
||||||
|
for (int i = 0; i < SDL_NumJoysticks(); i++) {
|
||||||
|
if (!SDL_IsGameController(i)) {
|
||||||
|
char guidStr[33];
|
||||||
|
SDL_JoystickGetGUIDString(SDL_JoystickGetDeviceGUID(i),
|
||||||
|
guidStr, sizeof(guidStr));
|
||||||
|
const char* name = SDL_JoystickNameForIndex(i);
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Unmapped joystick: %s %s",
|
||||||
|
name ? name : "<UNKNOWN>",
|
||||||
|
guidStr);
|
||||||
|
SDL_Joystick* joy = SDL_JoystickOpen(i);
|
||||||
|
if (joy != nullptr) {
|
||||||
|
int numButtons = SDL_JoystickNumButtons(joy);
|
||||||
|
int numHats = SDL_JoystickNumHats(joy);
|
||||||
|
int numAxes = SDL_JoystickNumAxes(joy);
|
||||||
|
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Number of axes: %d | Number of buttons: %d | Number of hats: %d",
|
||||||
|
numAxes, numButtons, numHats);
|
||||||
|
|
||||||
|
if (numAxes >= 4 && 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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "settings/streamingpreferences.h"
|
||||||
|
#include "backend/computermanager.h"
|
||||||
|
|
||||||
|
#include <SDL.h>
|
||||||
|
|
||||||
|
struct GamepadState {
|
||||||
|
SDL_GameController* controller;
|
||||||
|
SDL_JoystickID jsId;
|
||||||
|
short index;
|
||||||
|
|
||||||
|
short buttons;
|
||||||
|
short lsX, lsY;
|
||||||
|
short rsX, rsY;
|
||||||
|
unsigned char lt, rt;
|
||||||
|
};
|
||||||
|
|
||||||
|
#define MAX_GAMEPADS 4
|
||||||
|
#define MAX_FINGERS 2
|
||||||
|
|
||||||
|
class SdlInputHandler
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit SdlInputHandler(StreamingPreferences& prefs, NvComputer* computer,
|
||||||
|
int streamWidth, int streamHeight);
|
||||||
|
|
||||||
|
~SdlInputHandler();
|
||||||
|
|
||||||
|
void handleKeyEvent(SDL_KeyboardEvent* event);
|
||||||
|
|
||||||
|
void handleMouseButtonEvent(SDL_MouseButtonEvent* event);
|
||||||
|
|
||||||
|
void handleMouseMotionEvent(SDL_MouseMotionEvent* event);
|
||||||
|
|
||||||
|
void handleMouseWheelEvent(SDL_MouseWheelEvent* event);
|
||||||
|
|
||||||
|
void handleControllerAxisEvent(SDL_ControllerAxisEvent* event);
|
||||||
|
|
||||||
|
void handleControllerButtonEvent(SDL_ControllerButtonEvent* event);
|
||||||
|
|
||||||
|
void handleControllerDeviceEvent(SDL_ControllerDeviceEvent* event);
|
||||||
|
|
||||||
|
void handleJoystickArrivalEvent(SDL_JoyDeviceEvent* event);
|
||||||
|
|
||||||
|
void handleTouchFingerEvent(SDL_TouchFingerEvent* event);
|
||||||
|
|
||||||
|
int getAttachedGamepadMask();
|
||||||
|
|
||||||
|
void raiseAllKeys();
|
||||||
|
|
||||||
|
static
|
||||||
|
QString getUnmappedGamepads();
|
||||||
|
|
||||||
|
private:
|
||||||
|
GamepadState*
|
||||||
|
findStateForGamepad(SDL_JoystickID id);
|
||||||
|
|
||||||
|
void sendGamepadState(GamepadState* state);
|
||||||
|
|
||||||
|
static
|
||||||
|
Uint32 releaseLeftButtonTimerCallback(Uint32 interval, void* param);
|
||||||
|
|
||||||
|
static
|
||||||
|
Uint32 releaseRightButtonTimerCallback(Uint32 interval, void* param);
|
||||||
|
|
||||||
|
static
|
||||||
|
Uint32 dragTimerCallback(Uint32 interval, void* param);
|
||||||
|
|
||||||
|
Uint32 m_LastMouseMotionTime;
|
||||||
|
bool m_MultiController;
|
||||||
|
bool m_NeedsInputDelay;
|
||||||
|
int m_GamepadMask;
|
||||||
|
GamepadState m_GamepadState[MAX_GAMEPADS];
|
||||||
|
QSet<short> m_KeysDown;
|
||||||
|
|
||||||
|
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[];
|
||||||
|
};
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <SDL.h>
|
|
||||||
|
|
||||||
struct GamepadState {
|
|
||||||
SDL_GameController* controller;
|
|
||||||
SDL_JoystickID jsId;
|
|
||||||
short index;
|
|
||||||
|
|
||||||
short buttons;
|
|
||||||
short lsX, lsY;
|
|
||||||
short rsX, rsY;
|
|
||||||
unsigned char lt, rt;
|
|
||||||
};
|
|
||||||
|
|
||||||
#define MAX_GAMEPADS 4
|
|
||||||
|
|
||||||
class SdlInputHandler
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
explicit SdlInputHandler(bool multiController);
|
|
||||||
|
|
||||||
~SdlInputHandler();
|
|
||||||
|
|
||||||
void handleKeyEvent(SDL_KeyboardEvent* event);
|
|
||||||
|
|
||||||
void handleMouseButtonEvent(SDL_MouseButtonEvent* event);
|
|
||||||
|
|
||||||
void handleMouseMotionEvent(SDL_MouseMotionEvent* event);
|
|
||||||
|
|
||||||
void handleMouseWheelEvent(SDL_MouseWheelEvent* event);
|
|
||||||
|
|
||||||
void handleControllerAxisEvent(SDL_ControllerAxisEvent* event);
|
|
||||||
|
|
||||||
void handleControllerButtonEvent(SDL_ControllerButtonEvent* event);
|
|
||||||
|
|
||||||
void handleControllerDeviceEvent(SDL_ControllerDeviceEvent* event);
|
|
||||||
|
|
||||||
int getAttachedGamepadMask();
|
|
||||||
|
|
||||||
private:
|
|
||||||
GamepadState*
|
|
||||||
findStateForGamepad(SDL_JoystickID id);
|
|
||||||
|
|
||||||
void sendGamepadState(GamepadState* state);
|
|
||||||
|
|
||||||
bool m_MultiController;
|
|
||||||
int m_GamepadMask;
|
|
||||||
GamepadState m_GamepadState[MAX_GAMEPADS];
|
|
||||||
|
|
||||||
static const int k_ButtonMap[];
|
|
||||||
};
|
|
||||||
+440
-163
@@ -1,5 +1,6 @@
|
|||||||
#include "session.hpp"
|
#include "session.h"
|
||||||
#include "settings/streamingpreferences.h"
|
#include "settings/streamingpreferences.h"
|
||||||
|
#include "streaming/streamutils.h"
|
||||||
|
|
||||||
#include <Limelight.h>
|
#include <Limelight.h>
|
||||||
#include <SDL.h>
|
#include <SDL.h>
|
||||||
@@ -19,13 +20,6 @@
|
|||||||
#include "video/sl.h"
|
#include "video/sl.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if defined(Q_OS_DARWIN)
|
|
||||||
// Using full-screen desktop allows allows Spaces to work on macOS.
|
|
||||||
#define SDL_OS_FULLSCREEN_FLAG SDL_WINDOW_FULLSCREEN_DESKTOP
|
|
||||||
#else
|
|
||||||
#define SDL_OS_FULLSCREEN_FLAG SDL_WINDOW_FULLSCREEN
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef Q_OS_WIN32
|
#ifdef Q_OS_WIN32
|
||||||
// Scaling the icon down on Win32 looks dreadful, so render at lower res
|
// Scaling the icon down on Win32 looks dreadful, so render at lower res
|
||||||
#define ICON_SIZE 32
|
#define ICON_SIZE 32
|
||||||
@@ -54,11 +48,11 @@ CONNECTION_LISTENER_CALLBACKS Session::k_ConnCallbacks = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
AUDIO_RENDERER_CALLBACKS Session::k_AudioCallbacks = {
|
AUDIO_RENDERER_CALLBACKS Session::k_AudioCallbacks = {
|
||||||
Session::sdlAudioInit,
|
Session::arInit,
|
||||||
Session::sdlAudioStart,
|
nullptr,
|
||||||
Session::sdlAudioStop,
|
nullptr,
|
||||||
Session::sdlAudioCleanup,
|
Session::arCleanup,
|
||||||
Session::sdlAudioDecodeAndPlaySample,
|
Session::arDecodeAndPlaySample,
|
||||||
CAPABILITY_DIRECT_SUBMIT
|
CAPABILITY_DIRECT_SUBMIT
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -85,6 +79,8 @@ void Session::clStageFailed(int stage, long errorCode)
|
|||||||
|
|
||||||
void Session::clConnectionTerminated(long errorCode)
|
void Session::clConnectionTerminated(long errorCode)
|
||||||
{
|
{
|
||||||
|
emit s_ActiveSession->displayLaunchError("Connection terminated");
|
||||||
|
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"Connection terminated: %ld",
|
"Connection terminated: %ld",
|
||||||
errorCode);
|
errorCode);
|
||||||
@@ -108,13 +104,15 @@ void Session::clLogMessage(const char* format, ...)
|
|||||||
va_end(ap);
|
va_end(ap);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#define CALL_INITIALIZE(dec) (dec)->initialize(vds, window, videoFormat, width, height, frameRate, enableVsync)
|
||||||
|
|
||||||
bool Session::chooseDecoder(StreamingPreferences::VideoDecoderSelection vds,
|
bool Session::chooseDecoder(StreamingPreferences::VideoDecoderSelection vds,
|
||||||
SDL_Window* window, int videoFormat, int width, int height,
|
SDL_Window* window, int videoFormat, int width, int height,
|
||||||
int frameRate, bool enableVsync, IVideoDecoder*& chosenDecoder)
|
int frameRate, bool enableVsync, IVideoDecoder*& chosenDecoder)
|
||||||
{
|
{
|
||||||
#ifdef HAVE_SLVIDEO
|
#ifdef HAVE_SLVIDEO
|
||||||
chosenDecoder = new SLVideoDecoder();
|
chosenDecoder = new SLVideoDecoder();
|
||||||
if (chosenDecoder->initialize(vds, window, videoFormat, width, height, frameRate)) {
|
if (CALL_INITIALIZE(chosenDecoder)) {
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"SLVideo video decoder chosen");
|
"SLVideo video decoder chosen");
|
||||||
return true;
|
return true;
|
||||||
@@ -129,7 +127,7 @@ bool Session::chooseDecoder(StreamingPreferences::VideoDecoderSelection vds,
|
|||||||
|
|
||||||
#ifdef HAVE_FFMPEG
|
#ifdef HAVE_FFMPEG
|
||||||
chosenDecoder = new FFmpegVideoDecoder();
|
chosenDecoder = new FFmpegVideoDecoder();
|
||||||
if (chosenDecoder->initialize(vds, window, videoFormat, width, height, frameRate, enableVsync)) {
|
if (CALL_INITIALIZE(chosenDecoder)) {
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"FFmpeg-based video decoder chosen");
|
"FFmpeg-based video decoder chosen");
|
||||||
return true;
|
return true;
|
||||||
@@ -172,25 +170,33 @@ int Session::drSubmitDecodeUnit(PDECODE_UNIT du)
|
|||||||
// Use a lock since we'll be yanking this decoder out
|
// Use a lock since we'll be yanking this decoder out
|
||||||
// from underneath the session when we initiate destruction.
|
// from underneath the session when we initiate destruction.
|
||||||
// We need to destroy the decoder on the main thread to satisfy
|
// We need to destroy the decoder on the main thread to satisfy
|
||||||
// some API constraints (like DXVA2).
|
// some API constraints (like DXVA2). If we can't acquire it,
|
||||||
|
// that means the decoder is about to be destroyed, so we can
|
||||||
|
// safely return DR_OK and wait for m_NeedsIdr to be set by
|
||||||
|
// the decoder reinitialization code.
|
||||||
|
|
||||||
SDL_AtomicLock(&s_ActiveSession->m_DecoderLock);
|
if (SDL_AtomicTryLock(&s_ActiveSession->m_DecoderLock)) {
|
||||||
|
if (s_ActiveSession->m_NeedsIdr) {
|
||||||
|
// If we reset our decoder, we'll need to request an IDR frame
|
||||||
|
s_ActiveSession->m_NeedsIdr = false;
|
||||||
|
SDL_AtomicUnlock(&s_ActiveSession->m_DecoderLock);
|
||||||
|
return DR_NEED_IDR;
|
||||||
|
}
|
||||||
|
|
||||||
if (s_ActiveSession->m_NeedsIdr) {
|
IVideoDecoder* decoder = s_ActiveSession->m_VideoDecoder;
|
||||||
// If we reset our decoder, we'll need to request an IDR frame
|
if (decoder != nullptr) {
|
||||||
s_ActiveSession->m_NeedsIdr = false;
|
int ret = decoder->submitDecodeUnit(du);
|
||||||
SDL_AtomicUnlock(&s_ActiveSession->m_DecoderLock);
|
SDL_AtomicUnlock(&s_ActiveSession->m_DecoderLock);
|
||||||
return DR_NEED_IDR;
|
return ret;
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
IVideoDecoder* decoder = s_ActiveSession->m_VideoDecoder;
|
SDL_AtomicUnlock(&s_ActiveSession->m_DecoderLock);
|
||||||
if (decoder != nullptr) {
|
return DR_OK;
|
||||||
int ret = decoder->submitDecodeUnit(du);
|
}
|
||||||
SDL_AtomicUnlock(&s_ActiveSession->m_DecoderLock);
|
|
||||||
return ret;
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
SDL_AtomicUnlock(&s_ActiveSession->m_DecoderLock);
|
// Decoder is going away. Ignore anything coming in until
|
||||||
|
// the lock is released.
|
||||||
return DR_OK;
|
return DR_OK;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -232,23 +238,70 @@ bool Session::isHardwareDecodeAvailable(StreamingPreferences::VideoDecoderSelect
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
Session::Session(NvComputer* computer, NvApp& app)
|
int Session::getDecoderCapabilities(StreamingPreferences::VideoDecoderSelection vds,
|
||||||
: m_Computer(computer),
|
int videoFormat, int width, int height, int frameRate)
|
||||||
|
{
|
||||||
|
IVideoDecoder* decoder;
|
||||||
|
|
||||||
|
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_Window* window = SDL_CreateWindow("", 0, 0, width, height, SDL_WINDOW_HIDDEN);
|
||||||
|
if (!window) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Failed to create window for hardware decode test: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!chooseDecoder(vds, window, videoFormat, width, height, frameRate, true, decoder)) {
|
||||||
|
SDL_DestroyWindow(window);
|
||||||
|
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_DestroyWindow(window);
|
||||||
|
|
||||||
|
int caps = decoder->getDecoderCapabilities();
|
||||||
|
|
||||||
|
delete decoder;
|
||||||
|
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||||
|
|
||||||
|
return caps;
|
||||||
|
}
|
||||||
|
|
||||||
|
Session::Session(NvComputer* computer, NvApp& app, StreamingPreferences *preferences)
|
||||||
|
: m_Preferences(preferences ? preferences : new StreamingPreferences(this)),
|
||||||
|
m_Computer(computer),
|
||||||
m_App(app),
|
m_App(app),
|
||||||
m_Window(nullptr),
|
m_Window(nullptr),
|
||||||
m_VideoDecoder(nullptr),
|
m_VideoDecoder(nullptr),
|
||||||
m_DecoderLock(0),
|
m_DecoderLock(0),
|
||||||
m_NeedsIdr(false)
|
m_NeedsIdr(false),
|
||||||
|
m_AudioDisabled(false),
|
||||||
|
m_DisplayOriginX(0),
|
||||||
|
m_DisplayOriginY(0),
|
||||||
|
m_PendingWindowedTransition(false),
|
||||||
|
m_OpusDecoder(nullptr),
|
||||||
|
m_AudioRenderer(nullptr),
|
||||||
|
m_AudioSampleCount(0)
|
||||||
{
|
{
|
||||||
qDebug() << "Server GPU:" << m_Computer->gpuModel;
|
}
|
||||||
|
|
||||||
|
void Session::initialize()
|
||||||
|
{
|
||||||
|
qInfo() << "Server GPU:" << m_Computer->gpuModel;
|
||||||
|
qInfo() << "Server GFE version:" << m_Computer->gfeVersion;
|
||||||
|
|
||||||
LiInitializeVideoCallbacks(&m_VideoCallbacks);
|
LiInitializeVideoCallbacks(&m_VideoCallbacks);
|
||||||
m_VideoCallbacks.setup = drSetup;
|
m_VideoCallbacks.setup = drSetup;
|
||||||
m_VideoCallbacks.submitDecodeUnit = drSubmitDecodeUnit;
|
m_VideoCallbacks.submitDecodeUnit = drSubmitDecodeUnit;
|
||||||
|
|
||||||
// Submit for decode without using a separate thread
|
|
||||||
m_VideoCallbacks.capabilities |= CAPABILITY_DIRECT_SUBMIT;
|
|
||||||
|
|
||||||
// Slice up to 4 times for parallel decode, once slice per core
|
// Slice up to 4 times for parallel decode, once slice per core
|
||||||
int slices = qMin(MAX_SLICES, SDL_GetCPUCount());
|
int slices = qMin(MAX_SLICES, SDL_GetCPUCount());
|
||||||
m_VideoCallbacks.capabilities |= CAPABILITY_SLICES_PER_FRAME(slices);
|
m_VideoCallbacks.capabilities |= CAPABILITY_SLICES_PER_FRAME(slices);
|
||||||
@@ -257,10 +310,10 @@ Session::Session(NvComputer* computer, NvApp& app)
|
|||||||
slices);
|
slices);
|
||||||
|
|
||||||
LiInitializeStreamConfiguration(&m_StreamConfig);
|
LiInitializeStreamConfiguration(&m_StreamConfig);
|
||||||
m_StreamConfig.width = m_Preferences.width;
|
m_StreamConfig.width = m_Preferences->width;
|
||||||
m_StreamConfig.height = m_Preferences.height;
|
m_StreamConfig.height = m_Preferences->height;
|
||||||
m_StreamConfig.fps = m_Preferences.fps;
|
m_StreamConfig.fps = m_Preferences->fps;
|
||||||
m_StreamConfig.bitrate = m_Preferences.bitrateKbps;
|
m_StreamConfig.bitrate = m_Preferences->bitrateKbps;
|
||||||
m_StreamConfig.hevcBitratePercentageMultiplier = 75;
|
m_StreamConfig.hevcBitratePercentageMultiplier = 75;
|
||||||
|
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
@@ -273,16 +326,12 @@ Session::Session(NvComputer* computer, NvApp& app)
|
|||||||
// Only the first 4 bytes are populated in the RI key IV
|
// Only the first 4 bytes are populated in the RI key IV
|
||||||
RAND_bytes(reinterpret_cast<unsigned char*>(m_StreamConfig.remoteInputAesIv), 4);
|
RAND_bytes(reinterpret_cast<unsigned char*>(m_StreamConfig.remoteInputAesIv), 4);
|
||||||
|
|
||||||
switch (m_Preferences.audioConfig)
|
switch (m_Preferences->audioConfig)
|
||||||
{
|
{
|
||||||
case StreamingPreferences::AC_AUTO:
|
case StreamingPreferences::AC_STEREO:
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Autodetecting audio configuration");
|
|
||||||
m_StreamConfig.audioConfiguration = sdlDetermineAudioConfiguration();
|
|
||||||
break;
|
|
||||||
case StreamingPreferences::AC_FORCE_STEREO:
|
|
||||||
m_StreamConfig.audioConfiguration = AUDIO_CONFIGURATION_STEREO;
|
m_StreamConfig.audioConfiguration = AUDIO_CONFIGURATION_STEREO;
|
||||||
break;
|
break;
|
||||||
case StreamingPreferences::AC_FORCE_SURROUND:
|
case StreamingPreferences::AC_51_SURROUND:
|
||||||
m_StreamConfig.audioConfiguration = AUDIO_CONFIGURATION_51_SURROUND;
|
m_StreamConfig.audioConfiguration = AUDIO_CONFIGURATION_51_SURROUND;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -291,16 +340,32 @@ Session::Session(NvComputer* computer, NvApp& app)
|
|||||||
"Audio configuration: %d",
|
"Audio configuration: %d",
|
||||||
m_StreamConfig.audioConfiguration);
|
m_StreamConfig.audioConfiguration);
|
||||||
|
|
||||||
switch (m_Preferences.videoCodecConfig)
|
switch (m_Preferences->videoCodecConfig)
|
||||||
{
|
{
|
||||||
case StreamingPreferences::VCC_AUTO:
|
case StreamingPreferences::VCC_AUTO:
|
||||||
// TODO: Determine if HEVC is better depending on the decoder
|
// TODO: Determine if HEVC is better depending on the decoder
|
||||||
m_StreamConfig.supportsHevc =
|
m_StreamConfig.supportsHevc =
|
||||||
isHardwareDecodeAvailable(m_Preferences.videoDecoderSelection,
|
isHardwareDecodeAvailable(m_Preferences->videoDecoderSelection,
|
||||||
VIDEO_FORMAT_H265,
|
VIDEO_FORMAT_H265,
|
||||||
m_StreamConfig.width,
|
m_StreamConfig.width,
|
||||||
m_StreamConfig.height,
|
m_StreamConfig.height,
|
||||||
m_StreamConfig.fps);
|
m_StreamConfig.fps);
|
||||||
|
#ifdef Q_OS_DARWIN
|
||||||
|
{
|
||||||
|
// Prior to GFE 3.11, GFE did not allow us to constrain
|
||||||
|
// the number of reference frames, so we have to fixup the SPS
|
||||||
|
// to allow decoding via VideoToolbox on macOS. Since we don't
|
||||||
|
// have fixup code for HEVC, just avoid it if GFE is too old.
|
||||||
|
QVector<int> gfeVersion = NvHTTP::parseQuad(m_Computer->gfeVersion);
|
||||||
|
if (gfeVersion.isEmpty() || // Very old versions don't have GfeVersion at all
|
||||||
|
gfeVersion[0] < 3 ||
|
||||||
|
(gfeVersion[0] == 3 && gfeVersion[1] < 11)) {
|
||||||
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Disabling HEVC on macOS due to old GFE version");
|
||||||
|
m_StreamConfig.supportsHevc = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
m_StreamConfig.enableHdr = false;
|
m_StreamConfig.enableHdr = false;
|
||||||
break;
|
break;
|
||||||
case StreamingPreferences::VCC_FORCE_H264:
|
case StreamingPreferences::VCC_FORCE_H264:
|
||||||
@@ -317,19 +382,30 @@ Session::Session(NvComputer* computer, NvApp& app)
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (computer->activeAddress == computer->remoteAddress) {
|
if (m_Computer->activeAddress == m_Computer->remoteAddress) {
|
||||||
m_StreamConfig.streamingRemotely = 1;
|
m_StreamConfig.streamingRemotely = 1;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
m_StreamConfig.streamingRemotely = 0;
|
m_StreamConfig.streamingRemotely = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (computer->activeAddress == computer->localAddress) {
|
if (m_Computer->activeAddress == m_Computer->localAddress) {
|
||||||
m_StreamConfig.packetSize = 1392;
|
m_StreamConfig.packetSize = 1392;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
m_StreamConfig.packetSize = 1024;
|
m_StreamConfig.packetSize = 1024;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
switch (m_Preferences->windowMode)
|
||||||
|
{
|
||||||
|
case StreamingPreferences::WM_FULLSCREEN_DESKTOP:
|
||||||
|
m_FullScreenFlag = SDL_WINDOW_FULLSCREEN_DESKTOP;
|
||||||
|
break;
|
||||||
|
case StreamingPreferences::WM_FULLSCREEN:
|
||||||
|
default:
|
||||||
|
m_FullScreenFlag = SDL_WINDOW_FULLSCREEN;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Session::emitLaunchWarning(QString text)
|
void Session::emitLaunchWarning(QString text)
|
||||||
@@ -352,20 +428,28 @@ bool Session::validateLaunch()
|
|||||||
{
|
{
|
||||||
QStringList warningList;
|
QStringList warningList;
|
||||||
|
|
||||||
if (m_Preferences.videoDecoderSelection == StreamingPreferences::VDS_FORCE_SOFTWARE) {
|
if (m_Preferences->videoDecoderSelection == StreamingPreferences::VDS_FORCE_SOFTWARE) {
|
||||||
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_StreamConfig.supportsHevc) {
|
if (m_Preferences->unsupportedFps && m_StreamConfig.fps > 60) {
|
||||||
bool hevcForced = m_Preferences.videoCodecConfig == StreamingPreferences::VCC_FORCE_HEVC ||
|
emitLaunchWarning("Using unsupported FPS options may cause stuttering or lag.");
|
||||||
m_Preferences.videoCodecConfig == StreamingPreferences::VCC_FORCE_HEVC_HDR;
|
|
||||||
|
|
||||||
if (!isHardwareDecodeAvailable(m_Preferences.videoDecoderSelection,
|
if (m_Preferences->enableVsync) {
|
||||||
|
emitLaunchWarning("V-sync will be disabled when streaming at a higher frame rate than the display.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_StreamConfig.supportsHevc) {
|
||||||
|
bool hevcForced = m_Preferences->videoCodecConfig == StreamingPreferences::VCC_FORCE_HEVC ||
|
||||||
|
m_Preferences->videoCodecConfig == StreamingPreferences::VCC_FORCE_HEVC_HDR;
|
||||||
|
|
||||||
|
if (!isHardwareDecodeAvailable(m_Preferences->videoDecoderSelection,
|
||||||
VIDEO_FORMAT_H265,
|
VIDEO_FORMAT_H265,
|
||||||
m_StreamConfig.width,
|
m_StreamConfig.width,
|
||||||
m_StreamConfig.height,
|
m_StreamConfig.height,
|
||||||
m_StreamConfig.fps) &&
|
m_StreamConfig.fps) &&
|
||||||
m_Preferences.videoDecoderSelection == StreamingPreferences::VDS_AUTO) {
|
m_Preferences->videoDecoderSelection == StreamingPreferences::VDS_AUTO) {
|
||||||
if (hevcForced) {
|
if (hevcForced) {
|
||||||
emitLaunchWarning("Using software decoding due to your selection to force HEVC without GPU support. This may cause poor streaming performance.");
|
emitLaunchWarning("Using software decoding due to your selection to force HEVC without GPU support. This may cause poor streaming performance.");
|
||||||
}
|
}
|
||||||
@@ -401,7 +485,7 @@ bool Session::validateLaunch()
|
|||||||
emitLaunchWarning("Your host PC GPU doesn't support HDR streaming. "
|
emitLaunchWarning("Your host PC GPU doesn't support HDR streaming. "
|
||||||
"A GeForce GTX 1000-series (Pascal) or later GPU is required for HDR streaming.");
|
"A GeForce GTX 1000-series (Pascal) or later GPU is required for HDR streaming.");
|
||||||
}
|
}
|
||||||
else if (!isHardwareDecodeAvailable(m_Preferences.videoDecoderSelection,
|
else if (!isHardwareDecodeAvailable(m_Preferences->videoDecoderSelection,
|
||||||
VIDEO_FORMAT_H265_MAIN10,
|
VIDEO_FORMAT_H265_MAIN10,
|
||||||
m_StreamConfig.width,
|
m_StreamConfig.width,
|
||||||
m_StreamConfig.height,
|
m_StreamConfig.height,
|
||||||
@@ -418,7 +502,7 @@ bool Session::validateLaunch()
|
|||||||
|
|
||||||
if (m_StreamConfig.width >= 3840) {
|
if (m_StreamConfig.width >= 3840) {
|
||||||
// Only allow 4K on GFE 3.x+
|
// Only allow 4K on GFE 3.x+
|
||||||
if (m_Computer->gfeVersion.isNull() || m_Computer->gfeVersion.startsWith("2.")) {
|
if (m_Computer->gfeVersion.isEmpty() || m_Computer->gfeVersion.startsWith("2.")) {
|
||||||
emitLaunchWarning("GeForce Experience 3.0 or higher is required for 4K streaming.");
|
emitLaunchWarning("GeForce Experience 3.0 or higher is required for 4K streaming.");
|
||||||
|
|
||||||
m_StreamConfig.width = 1920;
|
m_StreamConfig.width = 1920;
|
||||||
@@ -435,13 +519,36 @@ bool Session::validateLaunch()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (m_Preferences.videoDecoderSelection == StreamingPreferences::VDS_FORCE_HARDWARE &&
|
// Test if audio works at the specified audio configuration
|
||||||
!isHardwareDecodeAvailable(m_Preferences.videoDecoderSelection,
|
bool audioTestPassed = testAudio(m_StreamConfig.audioConfiguration);
|
||||||
|
|
||||||
|
// 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("5.1 surround sound is not supported by the current audio device.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If nothing worked, warn the user that audio will not work
|
||||||
|
m_AudioDisabled = !audioTestPassed;
|
||||||
|
if (m_AudioDisabled) {
|
||||||
|
emitLaunchWarning("Failed to open audio device. Audio will be unavailable during this session.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for unmapped gamepads
|
||||||
|
if (!SdlInputHandler::getUnmappedGamepads().isEmpty()) {
|
||||||
|
emitLaunchWarning("An attached gamepad has no mapping and won't be usable. Visit the Moonlight help to resolve this.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_Preferences->videoDecoderSelection == StreamingPreferences::VDS_FORCE_HARDWARE &&
|
||||||
|
!isHardwareDecodeAvailable(m_Preferences->videoDecoderSelection,
|
||||||
m_StreamConfig.supportsHevc ? VIDEO_FORMAT_H265 : VIDEO_FORMAT_H264,
|
m_StreamConfig.supportsHevc ? VIDEO_FORMAT_H265 : VIDEO_FORMAT_H264,
|
||||||
m_StreamConfig.width,
|
m_StreamConfig.width,
|
||||||
m_StreamConfig.height,
|
m_StreamConfig.height,
|
||||||
m_StreamConfig.fps)) {
|
m_StreamConfig.fps)) {
|
||||||
if (m_Preferences.videoCodecConfig == StreamingPreferences::VCC_AUTO) {
|
if (m_Preferences->videoCodecConfig == StreamingPreferences::VCC_AUTO) {
|
||||||
emit displayLaunchError("Your selection to force hardware decoding cannot be satisfied due to missing hardware decoding support on this PC's GPU.");
|
emit displayLaunchError("Your selection to force hardware decoding cannot be satisfied due to missing hardware decoding support on this PC's GPU.");
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -452,6 +559,13 @@ bool Session::validateLaunch()
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add the capability flags from the chosen decoder/renderer
|
||||||
|
m_VideoCallbacks.capabilities |= getDecoderCapabilities(m_Preferences->videoDecoderSelection,
|
||||||
|
m_StreamConfig.supportsHevc ? VIDEO_FORMAT_H265 : VIDEO_FORMAT_H264,
|
||||||
|
m_StreamConfig.width,
|
||||||
|
m_StreamConfig.height,
|
||||||
|
m_StreamConfig.fps);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -469,8 +583,7 @@ class DeferredSessionCleanupTask : public QRunnable
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
void Session::getWindowDimensions(bool fullScreen,
|
void Session::getWindowDimensions(int& x, int& y,
|
||||||
int& x, int& y,
|
|
||||||
int& width, int& height)
|
int& width, int& height)
|
||||||
{
|
{
|
||||||
int displayIndex = 0;
|
int displayIndex = 0;
|
||||||
@@ -479,20 +592,29 @@ void Session::getWindowDimensions(bool fullScreen,
|
|||||||
displayIndex = SDL_GetWindowDisplayIndex(m_Window);
|
displayIndex = SDL_GetWindowDisplayIndex(m_Window);
|
||||||
SDL_assert(displayIndex >= 0);
|
SDL_assert(displayIndex >= 0);
|
||||||
}
|
}
|
||||||
// If there's a display matching this exact resolution, pick that
|
// Create our window on the same display that Qt's UI
|
||||||
// one (for native full-screen streaming). Otherwise, assume
|
// was being displayed on.
|
||||||
// display 0 for now. TODO: Default to the screen that the Qt window is on
|
else {
|
||||||
else if (fullScreen) {
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Qt UI screen is at (%d,%d)",
|
||||||
|
m_DisplayOriginX, m_DisplayOriginY);
|
||||||
for (int i = 0; i < SDL_GetNumVideoDisplays(); i++) {
|
for (int i = 0; i < SDL_GetNumVideoDisplays(); i++) {
|
||||||
SDL_DisplayMode mode;
|
SDL_Rect displayBounds;
|
||||||
if (SDL_GetCurrentDisplayMode(i, &mode) == 0 &&
|
|
||||||
m_ActiveVideoWidth == mode.w &&
|
if (SDL_GetDisplayBounds(i, &displayBounds) == 0) {
|
||||||
m_ActiveVideoHeight == mode.h) {
|
if (displayBounds.x == m_DisplayOriginX &&
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
displayBounds.y == m_DisplayOriginY) {
|
||||||
"Found exact resolution match on display: %d",
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
i);
|
"SDL found matching display %d",
|
||||||
displayIndex = i;
|
i);
|
||||||
break;
|
displayIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"SDL_GetDisplayBounds(%d) failed: %s",
|
||||||
|
i, SDL_GetError());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -501,23 +623,19 @@ void Session::getWindowDimensions(bool fullScreen,
|
|||||||
if (SDL_GetDisplayUsableBounds(displayIndex, &usableBounds) == 0) {
|
if (SDL_GetDisplayUsableBounds(displayIndex, &usableBounds) == 0) {
|
||||||
width = usableBounds.w;
|
width = usableBounds.w;
|
||||||
height = usableBounds.h;
|
height = usableBounds.h;
|
||||||
x = usableBounds.x;
|
|
||||||
y = usableBounds.y;
|
|
||||||
|
|
||||||
if (m_Window != nullptr) {
|
if (m_Window != nullptr) {
|
||||||
int top, left, bottom, right;
|
int top, left, bottom, right;
|
||||||
|
|
||||||
if (SDL_GetWindowBordersSize(m_Window, &top, &left, &bottom, &right) < 0) {
|
if (SDL_GetWindowBordersSize(m_Window, &top, &left, &bottom, &right) == 0) {
|
||||||
|
width -= left + right;
|
||||||
|
height -= top + bottom;
|
||||||
|
}
|
||||||
|
else {
|
||||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"Unable to get window border size");
|
"Unable to get window border size: %s",
|
||||||
return;
|
SDL_GetError());
|
||||||
}
|
}
|
||||||
|
|
||||||
x += left;
|
|
||||||
y += top;
|
|
||||||
|
|
||||||
width -= left + right;
|
|
||||||
height -= top + bottom;
|
|
||||||
|
|
||||||
// If the stream window can fit within the usable drawing area with 1:1
|
// If the stream window can fit within the usable drawing area with 1:1
|
||||||
// scaling, do that rather than filling the screen.
|
// scaling, do that rather than filling the screen.
|
||||||
@@ -534,44 +652,111 @@ void Session::getWindowDimensions(bool fullScreen,
|
|||||||
|
|
||||||
width = m_StreamConfig.width;
|
width = m_StreamConfig.width;
|
||||||
height = m_StreamConfig.height;
|
height = m_StreamConfig.height;
|
||||||
x = y = SDL_WINDOWPOS_UNDEFINED_DISPLAY(displayIndex);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
x = y = SDL_WINDOWPOS_CENTERED_DISPLAY(displayIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Session::updateOptimalWindowDisplayMode()
|
||||||
|
{
|
||||||
|
SDL_DisplayMode desktopMode, bestMode, mode;
|
||||||
|
int displayIndex = SDL_GetWindowDisplayIndex(m_Window);
|
||||||
|
|
||||||
|
// Try the current display mode first. On macOS, this will be the normal
|
||||||
|
// scaled desktop resolution setting.
|
||||||
|
if (SDL_GetDesktopDisplayMode(displayIndex, &desktopMode) == 0) {
|
||||||
|
// If this doesn't fit the selected resolution, use the native
|
||||||
|
// resolution of the panel (unscaled).
|
||||||
|
if (desktopMode.w < m_ActiveVideoWidth || desktopMode.h < m_ActiveVideoHeight) {
|
||||||
|
if (!StreamUtils::getRealDesktopMode(displayIndex, &desktopMode)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"SDL_GetDesktopDisplayMode() failed: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start with the native desktop resolution and try to find
|
||||||
|
// the highest refresh rate that our stream FPS evenly divides.
|
||||||
|
bestMode = desktopMode;
|
||||||
|
bestMode.refresh_rate = 0;
|
||||||
|
for (int i = 0; i < SDL_GetNumDisplayModes(displayIndex); i++) {
|
||||||
|
if (SDL_GetDisplayMode(displayIndex, i, &mode) == 0) {
|
||||||
|
if (mode.w == desktopMode.w && mode.h == desktopMode.h &&
|
||||||
|
mode.refresh_rate % m_StreamConfig.fps == 0) {
|
||||||
|
if (mode.refresh_rate > bestMode.refresh_rate) {
|
||||||
|
bestMode = mode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bestMode.refresh_rate == 0) {
|
||||||
|
// We may find no match if the user has moved a 120 FPS
|
||||||
|
// stream onto a 60 Hz monitor (since no refresh rate can
|
||||||
|
// divide our FPS setting). We'll stick to the default in
|
||||||
|
// this case.
|
||||||
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"No matching refresh rate found; using desktop mode");
|
||||||
|
bestMode = desktopMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((SDL_GetWindowFlags(m_Window) & SDL_WINDOW_FULLSCREEN_DESKTOP) == SDL_WINDOW_FULLSCREEN) {
|
||||||
|
// Only print when the window is actually in full-screen exclusive mode,
|
||||||
|
// otherwise we're not actually using the mode we've set here
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Chosen best display mode: %dx%dx%d",
|
||||||
|
bestMode.w, bestMode.h, bestMode.refresh_rate);
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_SetWindowDisplayMode(m_Window, &bestMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Session::toggleFullscreen()
|
void Session::toggleFullscreen()
|
||||||
{
|
{
|
||||||
bool fullScreen = !(SDL_GetWindowFlags(m_Window) & SDL_OS_FULLSCREEN_FLAG);
|
bool fullScreen = !(SDL_GetWindowFlags(m_Window) & m_FullScreenFlag);
|
||||||
|
|
||||||
int x, y, width, height;
|
|
||||||
|
|
||||||
// If we're leaving full screen, toggle out before setting our size and position
|
|
||||||
// that way we have accurate display size metrics (not the size our stream changed it to).
|
|
||||||
if (!fullScreen) {
|
|
||||||
SDL_SetWindowFullscreen(m_Window, 0);
|
|
||||||
SDL_SetWindowResizable(m_Window, SDL_TRUE);
|
|
||||||
}
|
|
||||||
|
|
||||||
getWindowDimensions(fullScreen, x, y, width, height);
|
|
||||||
|
|
||||||
SDL_SetWindowPosition(m_Window, x, y);
|
|
||||||
SDL_SetWindowSize(m_Window, width, height);
|
|
||||||
|
|
||||||
if (fullScreen) {
|
if (fullScreen) {
|
||||||
SDL_SetWindowResizable(m_Window, SDL_FALSE);
|
SDL_SetWindowResizable(m_Window, SDL_FALSE);
|
||||||
SDL_SetWindowFullscreen(m_Window, SDL_OS_FULLSCREEN_FLAG);
|
SDL_SetWindowFullscreen(m_Window, m_FullScreenFlag);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
SDL_SetWindowFullscreen(m_Window, 0);
|
||||||
|
SDL_SetWindowResizable(m_Window, SDL_TRUE);
|
||||||
|
|
||||||
|
// Reposition the window when the resize is complete
|
||||||
|
m_PendingWindowedTransition = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Session::exec()
|
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).
|
||||||
|
initialize();
|
||||||
|
|
||||||
// Check for validation errors/warnings and emit
|
// Check for validation errors/warnings and emit
|
||||||
// signals for them, if appropriate
|
// signals for them, if appropriate
|
||||||
if (!validateLaunch()) {
|
if (!validateLaunch()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Manually pump the UI thread for the view
|
// Wait 1.5 seconds before connecting to let the user
|
||||||
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
|
// have time to read any messages present on the segue
|
||||||
|
uint32_t start = SDL_GetTicks();
|
||||||
|
while (!SDL_TICKS_PASSED(SDL_GetTicks(), start + 1500)) {
|
||||||
|
// Pump the UI loop while we wait
|
||||||
|
SDL_Delay(5);
|
||||||
|
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
|
||||||
|
}
|
||||||
|
|
||||||
// Wait for any old session to finish cleanup
|
// Wait for any old session to finish cleanup
|
||||||
s_ActiveSessionSemaphore.acquire();
|
s_ActiveSessionSemaphore.acquire();
|
||||||
@@ -581,7 +766,9 @@ void Session::exec()
|
|||||||
|
|
||||||
// Initialize the gamepad code with our preferences
|
// Initialize the gamepad code with our preferences
|
||||||
StreamingPreferences prefs;
|
StreamingPreferences prefs;
|
||||||
SdlInputHandler inputHandler(prefs.multiController);
|
SdlInputHandler inputHandler(prefs, m_Computer,
|
||||||
|
m_StreamConfig.width,
|
||||||
|
m_StreamConfig.height);
|
||||||
|
|
||||||
// The UI should have ensured the old game was already quit
|
// The UI should have ensured the old game was already quit
|
||||||
// if we decide to stream a different game.
|
// if we decide to stream a different game.
|
||||||
@@ -615,6 +802,10 @@ void Session::exec()
|
|||||||
emit displayLaunchError(e.toQString());
|
emit displayLaunchError(e.toQString());
|
||||||
s_ActiveSessionSemaphore.release();
|
s_ActiveSessionSemaphore.release();
|
||||||
return;
|
return;
|
||||||
|
} catch (const QtNetworkReplyException& e) {
|
||||||
|
emit displayLaunchError(e.toQString());
|
||||||
|
s_ActiveSessionSemaphore.release();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
SDL_assert(!SDL_WasInit(SDL_INIT_VIDEO));
|
SDL_assert(!SDL_WasInit(SDL_INIT_VIDEO));
|
||||||
@@ -636,15 +827,16 @@ void Session::exec()
|
|||||||
|
|
||||||
// Older GFE versions didn't have this field
|
// Older GFE versions didn't have this field
|
||||||
QByteArray siGfeVersion;
|
QByteArray siGfeVersion;
|
||||||
if (!m_Computer->gfeVersion.isNull()) {
|
if (!m_Computer->gfeVersion.isEmpty()) {
|
||||||
siGfeVersion = m_Computer->gfeVersion.toLatin1();
|
siGfeVersion = m_Computer->gfeVersion.toLatin1();
|
||||||
}
|
}
|
||||||
if (!siGfeVersion.isNull()) {
|
if (!siGfeVersion.isEmpty()) {
|
||||||
hostInfo.serverInfoGfeVersion = siGfeVersion.data();
|
hostInfo.serverInfoGfeVersion = siGfeVersion.data();
|
||||||
}
|
}
|
||||||
|
|
||||||
int err = LiStartConnection(&hostInfo, &m_StreamConfig, &k_ConnCallbacks,
|
int err = LiStartConnection(&hostInfo, &m_StreamConfig, &k_ConnCallbacks,
|
||||||
&m_VideoCallbacks, &k_AudioCallbacks,
|
&m_VideoCallbacks,
|
||||||
|
m_AudioDisabled ? nullptr : &k_AudioCallbacks,
|
||||||
NULL, 0, NULL, 0);
|
NULL, 0, NULL, 0);
|
||||||
if (err != 0) {
|
if (err != 0) {
|
||||||
// We already displayed an error dialog in the stage failure
|
// We already displayed an error dialog in the stage failure
|
||||||
@@ -659,15 +851,14 @@ void Session::exec()
|
|||||||
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
|
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
|
||||||
|
|
||||||
int x, y, width, height;
|
int x, y, width, height;
|
||||||
getWindowDimensions(m_Preferences.fullScreen,
|
getWindowDimensions(x, y, width, height);
|
||||||
x, y, width, height);
|
|
||||||
|
|
||||||
m_Window = SDL_CreateWindow("Moonlight",
|
m_Window = SDL_CreateWindow("Moonlight",
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
0);
|
SDL_WINDOW_ALLOW_HIGHDPI);
|
||||||
if (!m_Window) {
|
if (!m_Window) {
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"SDL_CreateWindow() failed: %s",
|
"SDL_CreateWindow() failed: %s",
|
||||||
@@ -677,30 +868,6 @@ void Session::exec()
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// For non-full screen windows, call getWindowDimensions()
|
|
||||||
// again after creating a window to allow it to account
|
|
||||||
// for window chrome size.
|
|
||||||
if (!m_Preferences.fullScreen) {
|
|
||||||
getWindowDimensions(false, x, y, width, height);
|
|
||||||
|
|
||||||
SDL_SetWindowPosition(m_Window, x, y);
|
|
||||||
SDL_SetWindowSize(m_Window, width, height);
|
|
||||||
|
|
||||||
// Passing SDL_WINDOW_RESIZABLE to set this during window
|
|
||||||
// creation causes our window to be full screen for some reason
|
|
||||||
SDL_SetWindowResizable(m_Window, SDL_TRUE);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Update the window display mode based on our current monitor
|
|
||||||
SDL_DisplayMode mode;
|
|
||||||
if (SDL_GetDesktopDisplayMode(SDL_GetWindowDisplayIndex(m_Window), &mode) == 0) {
|
|
||||||
SDL_SetWindowDisplayMode(m_Window, &mode);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enter full screen
|
|
||||||
SDL_SetWindowFullscreen(m_Window, SDL_OS_FULLSCREEN_FLAG);
|
|
||||||
}
|
|
||||||
|
|
||||||
QSvgRenderer svgIconRenderer(QString(":/res/moonlight.svg"));
|
QSvgRenderer svgIconRenderer(QString(":/res/moonlight.svg"));
|
||||||
QImage svgImage(ICON_SIZE, ICON_SIZE, QImage::Format_RGBA8888);
|
QImage svgImage(ICON_SIZE, ICON_SIZE, QImage::Format_RGBA8888);
|
||||||
svgImage.fill(0);
|
svgImage.fill(0);
|
||||||
@@ -714,17 +881,42 @@ void Session::exec()
|
|||||||
4 * svgImage.width(),
|
4 * svgImage.width(),
|
||||||
SDL_PIXELFORMAT_RGBA32);
|
SDL_PIXELFORMAT_RGBA32);
|
||||||
#ifndef Q_OS_DARWIN
|
#ifndef Q_OS_DARWIN
|
||||||
// Other platforms seem to preserve our Qt icon when creating a new window
|
// Other platforms seem to preserve our Qt icon when creating a new window.
|
||||||
if (iconSurface != nullptr) {
|
if (iconSurface != nullptr) {
|
||||||
|
// This must be called before entering full-screen mode on Windows
|
||||||
|
// or our icon will not persist when toggling to windowed mode
|
||||||
SDL_SetWindowIcon(m_Window, iconSurface);
|
SDL_SetWindowIcon(m_Window, iconSurface);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
// For non-full screen windows, call getWindowDimensions()
|
||||||
|
// again after creating a window to allow it to account
|
||||||
|
// for window chrome size.
|
||||||
|
if (m_Preferences->windowMode == StreamingPreferences::WM_WINDOWED) {
|
||||||
|
getWindowDimensions(x, y, width, height);
|
||||||
|
|
||||||
|
// We must set the size before the position because centering
|
||||||
|
// won't work unless it knows the final size of the window.
|
||||||
|
SDL_SetWindowSize(m_Window, width, height);
|
||||||
|
SDL_SetWindowPosition(m_Window, x, y);
|
||||||
|
|
||||||
|
// Passing SDL_WINDOW_RESIZABLE to set this during window
|
||||||
|
// creation causes our window to be full screen for some reason
|
||||||
|
SDL_SetWindowResizable(m_Window, SDL_TRUE);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Update the window display mode based on our current monitor
|
||||||
|
updateOptimalWindowDisplayMode();
|
||||||
|
|
||||||
|
// Enter full screen
|
||||||
|
SDL_SetWindowFullscreen(m_Window, m_FullScreenFlag);
|
||||||
|
}
|
||||||
|
|
||||||
#ifndef QT_DEBUG
|
#ifndef QT_DEBUG
|
||||||
// Capture the mouse by default on release builds only.
|
// Capture the mouse by default on release builds only.
|
||||||
// This prevents the mouse from becoming trapped inside
|
// This prevents the mouse from becoming trapped inside
|
||||||
// Moonlight when it's halted at a debug break.
|
// Moonlight when it's halted at a debug break.
|
||||||
if (m_Preferences.fullScreen) {
|
if (m_Preferences->windowMode != StreamingPreferences::WM_WINDOWED) {
|
||||||
SDL_SetRelativeMouseMode(SDL_TRUE);
|
SDL_SetRelativeMouseMode(SDL_TRUE);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@@ -757,6 +949,8 @@ void Session::exec()
|
|||||||
SDL_GetError());
|
SDL_GetError());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int currentDisplayIndex = SDL_GetWindowDisplayIndex(m_Window);
|
||||||
|
|
||||||
// Hijack this thread to be the SDL main thread. We have to do this
|
// 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.
|
// because we want to suspend all Qt processing until the stream is over.
|
||||||
SDL_Event event;
|
SDL_Event event;
|
||||||
@@ -795,11 +989,70 @@ void Session::exec()
|
|||||||
}
|
}
|
||||||
|
|
||||||
case SDL_WINDOWEVENT:
|
case SDL_WINDOWEVENT:
|
||||||
|
// 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) {
|
||||||
|
SDL_SetRelativeMouseMode(SDL_TRUE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
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)) {
|
||||||
|
SDL_SetRelativeMouseMode(SDL_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.
|
||||||
|
inputHandler.raiseAllKeys();
|
||||||
|
}
|
||||||
|
|
||||||
// We want to recreate the decoder for resizes (full-screen toggles) and the initial shown event.
|
// We want to recreate the decoder for resizes (full-screen toggles) and the initial shown event.
|
||||||
// We use SDL_WINDOWEVENT_SIZE_CHANGED rather than SDL_WINDOWEVENT_RESIZED because the latter doesn't
|
// We use SDL_WINDOWEVENT_SIZE_CHANGED rather than SDL_WINDOWEVENT_RESIZED because the latter doesn't
|
||||||
// seem to fire when switching from windowed to full-screen on X11.
|
// seem to fire when switching from windowed to full-screen on X11.
|
||||||
if (event.window.event != SDL_WINDOWEVENT_SIZE_CHANGED && event.window.event != SDL_WINDOWEVENT_SHOWN) {
|
if (event.window.event != SDL_WINDOWEVENT_SIZE_CHANGED && event.window.event != SDL_WINDOWEVENT_SHOWN) {
|
||||||
break;
|
// Check that the window display hasn't changed. If it has, we want
|
||||||
|
// to recreate the decoder to allow it to adapt to the new display.
|
||||||
|
// This will allow Pacer to pull the new display refresh rate.
|
||||||
|
if (SDL_GetWindowDisplayIndex(m_Window) == currentDisplayIndex) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Complete any repositioning that was deferred until
|
||||||
|
// the resize from full-screen to windowed had completed.
|
||||||
|
// If we try to do this immediately, the resize won't take effect
|
||||||
|
// properly on Windows.
|
||||||
|
if (m_PendingWindowedTransition) {
|
||||||
|
m_PendingWindowedTransition = false;
|
||||||
|
|
||||||
|
int x, y, width, height;
|
||||||
|
getWindowDimensions(x, y, width, height);
|
||||||
|
|
||||||
|
SDL_SetWindowSize(m_Window, width, height);
|
||||||
|
SDL_SetWindowPosition(m_Window, x, y);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fall through
|
// Fall through
|
||||||
@@ -817,10 +1070,8 @@ void Session::exec()
|
|||||||
SDL_FlushEvent(SDL_WINDOWEVENT);
|
SDL_FlushEvent(SDL_WINDOWEVENT);
|
||||||
|
|
||||||
// Update the window display mode based on our current monitor
|
// Update the window display mode based on our current monitor
|
||||||
SDL_DisplayMode mode;
|
currentDisplayIndex = SDL_GetWindowDisplayIndex(m_Window);
|
||||||
if (SDL_GetDesktopDisplayMode(SDL_GetWindowDisplayIndex(m_Window), &mode) == 0) {
|
updateOptimalWindowDisplayMode();
|
||||||
SDL_SetWindowDisplayMode(m_Window, &mode);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now that the old decoder is dead, flush any events it may
|
// Now that the old decoder is dead, flush any events it may
|
||||||
// have queued to reset itself (if this reset was the result
|
// have queued to reset itself (if this reset was the result
|
||||||
@@ -829,18 +1080,31 @@ void Session::exec()
|
|||||||
SDL_FlushEvent(SDL_RENDER_DEVICE_RESET);
|
SDL_FlushEvent(SDL_RENDER_DEVICE_RESET);
|
||||||
SDL_FlushEvent(SDL_RENDER_TARGETS_RESET);
|
SDL_FlushEvent(SDL_RENDER_TARGETS_RESET);
|
||||||
|
|
||||||
// Choose a new decoder (hopefully the same one, but possibly
|
{
|
||||||
// not if a GPU was removed or something).
|
// If the stream exceeds the display refresh rate (plus some slack),
|
||||||
if (!chooseDecoder(m_Preferences.videoDecoderSelection,
|
// forcefully disable V-sync to allow the stream to render faster
|
||||||
m_Window, m_ActiveVideoFormat, m_ActiveVideoWidth,
|
// than the display.
|
||||||
m_ActiveVideoHeight, m_ActiveVideoFrameRate,
|
int displayHz = StreamUtils::getDisplayRefreshRate(m_Window);
|
||||||
m_Preferences.enableVsync,
|
bool enableVsync = m_Preferences->enableVsync;
|
||||||
s_ActiveSession->m_VideoDecoder)) {
|
if (displayHz + 5 < m_StreamConfig.fps) {
|
||||||
SDL_AtomicUnlock(&m_DecoderLock);
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
"Disabling V-sync because refresh rate limit exceeded");
|
||||||
"Failed to recreate decoder after reset");
|
enableVsync = false;
|
||||||
emit displayLaunchError("Unable to initialize video decoder. Please check your streaming settings and try again.");
|
}
|
||||||
goto DispatchDeferredCleanup;
|
|
||||||
|
// Choose a new decoder (hopefully the same one, but possibly
|
||||||
|
// not if a GPU was removed or something).
|
||||||
|
if (!chooseDecoder(m_Preferences->videoDecoderSelection,
|
||||||
|
m_Window, m_ActiveVideoFormat, m_ActiveVideoWidth,
|
||||||
|
m_ActiveVideoHeight, m_ActiveVideoFrameRate,
|
||||||
|
enableVsync,
|
||||||
|
s_ActiveSession->m_VideoDecoder)) {
|
||||||
|
SDL_AtomicUnlock(&m_DecoderLock);
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Failed to recreate decoder after reset");
|
||||||
|
emit displayLaunchError("Unable to initialize video decoder. Please check your streaming settings and try again.");
|
||||||
|
goto DispatchDeferredCleanup;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Request an IDR frame to complete the reset
|
// Request an IDR frame to complete the reset
|
||||||
@@ -874,13 +1138,23 @@ void Session::exec()
|
|||||||
case SDL_CONTROLLERDEVICEREMOVED:
|
case SDL_CONTROLLERDEVICEREMOVED:
|
||||||
inputHandler.handleControllerDeviceEvent(&event.cdevice);
|
inputHandler.handleControllerDeviceEvent(&event.cdevice);
|
||||||
break;
|
break;
|
||||||
|
case SDL_JOYDEVICEADDED:
|
||||||
|
inputHandler.handleJoystickArrivalEvent(&event.jdevice);
|
||||||
|
break;
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
#ifndef Q_OS_DARWIN
|
||||||
|
case SDL_FINGERDOWN:
|
||||||
|
case SDL_FINGERMOTION:
|
||||||
|
case SDL_FINGERUP:
|
||||||
|
inputHandler.handleTouchFingerEvent(&event.tfinger);
|
||||||
|
break;
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"SDL_WaitEvent() failed: %s",
|
|
||||||
SDL_GetError());
|
|
||||||
|
|
||||||
DispatchDeferredCleanup:
|
DispatchDeferredCleanup:
|
||||||
#ifdef Q_OS_WIN32
|
#ifdef Q_OS_WIN32
|
||||||
// HACK: See comment above
|
// HACK: See comment above
|
||||||
@@ -893,6 +1167,9 @@ DispatchDeferredCleanup:
|
|||||||
SDL_EnableScreenSaver();
|
SDL_EnableScreenSaver();
|
||||||
SDL_SetHint(SDL_HINT_TIMER_RESOLUTION, "0");
|
SDL_SetHint(SDL_HINT_TIMER_RESOLUTION, "0");
|
||||||
|
|
||||||
|
// Raise any keys that are still down
|
||||||
|
inputHandler.raiseAllKeys();
|
||||||
|
|
||||||
// Destroy the decoder, since this must be done on the main thread
|
// Destroy the decoder, since this must be done on the main thread
|
||||||
SDL_AtomicLock(&m_DecoderLock);
|
SDL_AtomicLock(&m_DecoderLock);
|
||||||
delete m_VideoDecoder;
|
delete m_VideoDecoder;
|
||||||
|
|||||||
@@ -6,8 +6,9 @@
|
|||||||
#include <opus_multistream.h>
|
#include <opus_multistream.h>
|
||||||
#include "backend/computermanager.h"
|
#include "backend/computermanager.h"
|
||||||
#include "settings/streamingpreferences.h"
|
#include "settings/streamingpreferences.h"
|
||||||
#include "input.hpp"
|
#include "input.h"
|
||||||
#include "video/decoder.h"
|
#include "video/decoder.h"
|
||||||
|
#include "audio/renderers/renderer.h"
|
||||||
|
|
||||||
class Session : public QObject
|
class Session : public QObject
|
||||||
{
|
{
|
||||||
@@ -17,14 +18,18 @@ class Session : public QObject
|
|||||||
friend class DeferredSessionCleanupTask;
|
friend class DeferredSessionCleanupTask;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit Session(NvComputer* computer, NvApp& app);
|
explicit Session(NvComputer* computer, NvApp& app, StreamingPreferences *preferences = nullptr);
|
||||||
|
|
||||||
Q_INVOKABLE void exec();
|
Q_INVOKABLE void exec(int displayOriginX, int displayOriginY);
|
||||||
|
|
||||||
static
|
static
|
||||||
bool isHardwareDecodeAvailable(StreamingPreferences::VideoDecoderSelection vds,
|
bool isHardwareDecodeAvailable(StreamingPreferences::VideoDecoderSelection vds,
|
||||||
int videoFormat, int width, int height, int frameRate);
|
int videoFormat, int width, int height, int frameRate);
|
||||||
|
|
||||||
|
static
|
||||||
|
int getDecoderCapabilities(StreamingPreferences::VideoDecoderSelection vds,
|
||||||
|
int videoFormat, int width, int height, int frameRate);
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void stageStarting(QString stage);
|
void stageStarting(QString stage);
|
||||||
|
|
||||||
@@ -37,20 +42,27 @@ signals:
|
|||||||
void displayLaunchWarning(QString text);
|
void displayLaunchWarning(QString text);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
void initialize();
|
||||||
|
|
||||||
bool validateLaunch();
|
bool validateLaunch();
|
||||||
|
|
||||||
void emitLaunchWarning(QString text);
|
void emitLaunchWarning(QString text);
|
||||||
|
|
||||||
int getDecoderCapabilities();
|
int getDecoderCapabilities();
|
||||||
|
|
||||||
int sdlDetermineAudioConfiguration();
|
IAudioRenderer* createAudioRenderer();
|
||||||
|
|
||||||
void getWindowDimensions(bool fullScreen,
|
int detectAudioConfiguration();
|
||||||
int& x, int& y,
|
|
||||||
|
bool testAudio(int audioConfiguration);
|
||||||
|
|
||||||
|
void getWindowDimensions(int& x, int& y,
|
||||||
int& width, int& height);
|
int& width, int& height);
|
||||||
|
|
||||||
void toggleFullscreen();
|
void toggleFullscreen();
|
||||||
|
|
||||||
|
void updateOptimalWindowDisplayMode();
|
||||||
|
|
||||||
static
|
static
|
||||||
bool chooseDecoder(StreamingPreferences::VideoDecoderSelection vds,
|
bool chooseDecoder(StreamingPreferences::VideoDecoderSelection vds,
|
||||||
SDL_Window* window, int videoFormat, int width, int height,
|
SDL_Window* window, int videoFormat, int width, int height,
|
||||||
@@ -68,6 +80,17 @@ private:
|
|||||||
static
|
static
|
||||||
void clLogMessage(const char* format, ...);
|
void clLogMessage(const char* format, ...);
|
||||||
|
|
||||||
|
static
|
||||||
|
int arInit(int audioConfiguration,
|
||||||
|
const POPUS_MULTISTREAM_CONFIGURATION opusConfig,
|
||||||
|
void* arContext, int arFlags);
|
||||||
|
|
||||||
|
static
|
||||||
|
void arCleanup();
|
||||||
|
|
||||||
|
static
|
||||||
|
void arDecodeAndPlaySample(char* sampleData, int sampleLength);
|
||||||
|
|
||||||
static
|
static
|
||||||
int drSetup(int videoFormat, int width, int height, int frameRate, void*, int);
|
int drSetup(int videoFormat, int width, int height, int frameRate, void*, int);
|
||||||
|
|
||||||
@@ -77,24 +100,7 @@ private:
|
|||||||
static
|
static
|
||||||
int drSubmitDecodeUnit(PDECODE_UNIT du);
|
int drSubmitDecodeUnit(PDECODE_UNIT du);
|
||||||
|
|
||||||
static
|
StreamingPreferences* m_Preferences;
|
||||||
int sdlAudioInit(int audioConfiguration,
|
|
||||||
POPUS_MULTISTREAM_CONFIGURATION opusConfig,
|
|
||||||
void* arContext, int arFlags);
|
|
||||||
|
|
||||||
static
|
|
||||||
void sdlAudioStart();
|
|
||||||
|
|
||||||
static
|
|
||||||
void sdlAudioStop();
|
|
||||||
|
|
||||||
static
|
|
||||||
void sdlAudioCleanup();
|
|
||||||
|
|
||||||
static
|
|
||||||
void sdlAudioDecodeAndPlaySample(char* sampleData, int sampleLength);
|
|
||||||
|
|
||||||
StreamingPreferences m_Preferences;
|
|
||||||
STREAM_CONFIGURATION m_StreamConfig;
|
STREAM_CONFIGURATION m_StreamConfig;
|
||||||
DECODER_RENDERER_CALLBACKS m_VideoCallbacks;
|
DECODER_RENDERER_CALLBACKS m_VideoCallbacks;
|
||||||
NvComputer* m_Computer;
|
NvComputer* m_Computer;
|
||||||
@@ -103,20 +109,22 @@ private:
|
|||||||
IVideoDecoder* m_VideoDecoder;
|
IVideoDecoder* m_VideoDecoder;
|
||||||
SDL_SpinLock m_DecoderLock;
|
SDL_SpinLock m_DecoderLock;
|
||||||
bool m_NeedsIdr;
|
bool m_NeedsIdr;
|
||||||
|
bool m_AudioDisabled;
|
||||||
|
Uint32 m_FullScreenFlag;
|
||||||
|
int m_DisplayOriginX;
|
||||||
|
int m_DisplayOriginY;
|
||||||
|
bool m_PendingWindowedTransition;
|
||||||
|
|
||||||
int m_ActiveVideoFormat;
|
int m_ActiveVideoFormat;
|
||||||
int m_ActiveVideoWidth;
|
int m_ActiveVideoWidth;
|
||||||
int m_ActiveVideoHeight;
|
int m_ActiveVideoHeight;
|
||||||
int m_ActiveVideoFrameRate;
|
int m_ActiveVideoFrameRate;
|
||||||
|
|
||||||
static SDL_AudioDeviceID s_AudioDevice;
|
OpusMSDecoder* m_OpusDecoder;
|
||||||
static OpusMSDecoder* s_OpusDecoder;
|
short m_OpusDecodeBuffer[MAX_CHANNELS * SAMPLES_PER_FRAME];
|
||||||
static short s_OpusDecodeBuffer[];
|
IAudioRenderer* m_AudioRenderer;
|
||||||
static int s_ChannelCount;
|
OPUS_MULTISTREAM_CONFIGURATION m_AudioConfig;
|
||||||
static int s_PendingDrops;
|
int m_AudioSampleCount;
|
||||||
static int s_PendingHardDrops;
|
|
||||||
static unsigned int s_SampleIndex;
|
|
||||||
static Uint32 s_BaselinePendingData;
|
|
||||||
|
|
||||||
static AUDIO_RENDERER_CALLBACKS k_AudioCallbacks;
|
static AUDIO_RENDERER_CALLBACKS k_AudioCallbacks;
|
||||||
static CONNECTION_LISTENER_CALLBACKS k_ConnCallbacks;
|
static CONNECTION_LISTENER_CALLBACKS k_ConnCallbacks;
|
||||||
@@ -1,5 +1,11 @@
|
|||||||
#include "streamutils.h"
|
#include "streamutils.h"
|
||||||
|
|
||||||
|
#include <Qt>
|
||||||
|
|
||||||
|
#ifdef Q_OS_DARWIN
|
||||||
|
#include <ApplicationServices/ApplicationServices.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
void StreamUtils::scaleSourceToDestinationSurface(SDL_Rect* src, SDL_Rect* dst)
|
void StreamUtils::scaleSourceToDestinationSurface(SDL_Rect* src, SDL_Rect* dst)
|
||||||
{
|
{
|
||||||
int dstH = dst->w * src->h / src->w;
|
int dstH = dst->w * src->h / src->w;
|
||||||
@@ -18,3 +24,104 @@ void StreamUtils::scaleSourceToDestinationSurface(SDL_Rect* src, SDL_Rect* dst)
|
|||||||
SDL_assert(dst->h * src->w / src->h <= dst->w);
|
SDL_assert(dst->h * src->w / src->h <= dst->w);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int StreamUtils::getDisplayRefreshRate(SDL_Window* window)
|
||||||
|
{
|
||||||
|
int displayIndex = SDL_GetWindowDisplayIndex(window);
|
||||||
|
if (displayIndex < 0) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Failed to get current display: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
|
||||||
|
// Assume display 0 if it fails
|
||||||
|
displayIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_DisplayMode mode;
|
||||||
|
if ((SDL_GetWindowFlags(window) & SDL_WINDOW_FULLSCREEN_DESKTOP) == SDL_WINDOW_FULLSCREEN) {
|
||||||
|
// Use the window display mode for full-screen exclusive mode
|
||||||
|
if (SDL_GetWindowDisplayMode(window, &mode) != 0) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"SDL_GetWindowDisplayMode() failed: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
|
||||||
|
// Assume 60 Hz
|
||||||
|
return 60;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Use the current display mode for windowed and borderless
|
||||||
|
if (SDL_GetCurrentDisplayMode(displayIndex, &mode) != 0) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"SDL_GetCurrentDisplayMode() failed: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
|
||||||
|
// Assume 60 Hz
|
||||||
|
return 60;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// May be zero if undefined
|
||||||
|
if (mode.refresh_rate == 0) {
|
||||||
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Refresh rate unknown; assuming 60 Hz");
|
||||||
|
mode.refresh_rate = 60;
|
||||||
|
}
|
||||||
|
|
||||||
|
return mode.refresh_rate;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool StreamUtils::getRealDesktopMode(int displayIndex, SDL_DisplayMode* mode)
|
||||||
|
{
|
||||||
|
#ifdef Q_OS_DARWIN
|
||||||
|
#define MAX_DISPLAYS 16
|
||||||
|
CGDirectDisplayID displayIds[MAX_DISPLAYS];
|
||||||
|
uint32_t displayCount = 0;
|
||||||
|
CGGetActiveDisplayList(MAX_DISPLAYS, displayIds, &displayCount);
|
||||||
|
if (displayIndex >= displayCount) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Too many displays: %d vs %d",
|
||||||
|
displayIndex, displayCount);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_zerop(mode);
|
||||||
|
|
||||||
|
// Retina displays have non-native resolutions both below and above (!) their
|
||||||
|
// native resolution, so it's impossible for us to figure out what's actually
|
||||||
|
// native on macOS using the SDL API alone. We'll talk to CoreGraphics to
|
||||||
|
// find the correct resolution and match it in our SDL list.
|
||||||
|
CFArrayRef modeList = CGDisplayCopyAllDisplayModes(displayIds[displayIndex], nullptr);
|
||||||
|
CFIndex count = CFArrayGetCount(modeList);
|
||||||
|
for (CFIndex i = 0; i < count; i++) {
|
||||||
|
auto cgMode = (CGDisplayModeRef)(CFArrayGetValueAtIndex(modeList, i));
|
||||||
|
if ((CGDisplayModeGetIOFlags(cgMode) & kDisplayModeNativeFlag) != 0) {
|
||||||
|
mode->w = static_cast<int>(CGDisplayModeGetWidth(cgMode));
|
||||||
|
mode->h = static_cast<int>(CGDisplayModeGetHeight(cgMode));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CFRelease(modeList);
|
||||||
|
|
||||||
|
// Now find the SDL mode that matches the CG native mode
|
||||||
|
for (int i = 0; i < SDL_GetNumDisplayModes(displayIndex); i++) {
|
||||||
|
SDL_DisplayMode thisMode;
|
||||||
|
if (SDL_GetDisplayMode(displayIndex, i, &thisMode) == 0) {
|
||||||
|
if (thisMode.w == mode->w && thisMode.h == mode->h &&
|
||||||
|
thisMode.refresh_rate >= mode->refresh_rate) {
|
||||||
|
*mode = thisMode;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
if (SDL_GetDesktopDisplayMode(displayIndex, mode) != 0) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"SDL_GetDesktopDisplayMode() failed: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,4 +7,10 @@ class StreamUtils
|
|||||||
public:
|
public:
|
||||||
static
|
static
|
||||||
void scaleSourceToDestinationSurface(SDL_Rect* src, SDL_Rect* dst);
|
void scaleSourceToDestinationSurface(SDL_Rect* src, SDL_Rect* dst);
|
||||||
|
|
||||||
|
static
|
||||||
|
bool getRealDesktopMode(int displayIndex, SDL_DisplayMode* mode);
|
||||||
|
|
||||||
|
static
|
||||||
|
int getDisplayRefreshRate(SDL_Window* window);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,6 +8,21 @@
|
|||||||
|
|
||||||
#define MAX_SLICES 4
|
#define MAX_SLICES 4
|
||||||
|
|
||||||
|
typedef struct _VIDEO_STATS {
|
||||||
|
uint32_t receivedFrames;
|
||||||
|
uint32_t decodedFrames;
|
||||||
|
uint32_t renderedFrames;
|
||||||
|
uint32_t networkDroppedFrames;
|
||||||
|
uint32_t pacerDroppedFrames;
|
||||||
|
uint32_t totalReassemblyTime;
|
||||||
|
uint32_t totalDecodeTime;
|
||||||
|
uint32_t totalRenderTime;
|
||||||
|
float receivedFps;
|
||||||
|
float decodedFps;
|
||||||
|
float renderedFps;
|
||||||
|
uint32_t measurementStartTimestamp;
|
||||||
|
} VIDEO_STATS, *PVIDEO_STATS;
|
||||||
|
|
||||||
class IVideoDecoder {
|
class IVideoDecoder {
|
||||||
public:
|
public:
|
||||||
virtual ~IVideoDecoder() {}
|
virtual ~IVideoDecoder() {}
|
||||||
@@ -19,6 +34,7 @@ public:
|
|||||||
int frameRate,
|
int frameRate,
|
||||||
bool enableVsync) = 0;
|
bool enableVsync) = 0;
|
||||||
virtual bool isHardwareAccelerated() = 0;
|
virtual bool isHardwareAccelerated() = 0;
|
||||||
|
virtual int getDecoderCapabilities() = 0;
|
||||||
virtual int submitDecodeUnit(PDECODE_UNIT du) = 0;
|
virtual int submitDecodeUnit(PDECODE_UNIT du) = 0;
|
||||||
virtual void renderFrame(SDL_UserEvent* event) = 0;
|
virtual void renderFrame(SDL_UserEvent* event) = 0;
|
||||||
virtual void dropFrame(SDL_UserEvent* event) = 0;
|
virtual void dropFrame(SDL_UserEvent* event) = 0;
|
||||||
|
|||||||
@@ -266,74 +266,52 @@ bool DXVA2Renderer::initializeRenderer()
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
UINT guidCount;
|
DXVA2_VideoProcessorCaps caps;
|
||||||
GUID* guids;
|
hr = m_ProcService->GetVideoProcessorCaps(DXVA2_VideoProcProgressiveDevice, &m_Desc, renderTargetDesc.Format, &caps);
|
||||||
hr = m_ProcService->GetVideoProcessorDeviceGuids(&m_Desc, &guidCount, &guids);
|
|
||||||
if (FAILED(hr)) {
|
if (FAILED(hr)) {
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"GetVideoProcessorDeviceGuids() failed: %x",
|
"GetVideoProcessorCaps() failed for DXVA2_VideoProcProgressiveDevice: %x",
|
||||||
hr);
|
hr);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
UINT i;
|
if (!(caps.DeviceCaps & DXVA2_VPDev_HardwareDevice)) {
|
||||||
for (i = 0; i < guidCount; i++) {
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
DXVA2_VideoProcessorCaps caps;
|
"DXVA2_VideoProcProgressiveDevice is not hardware: %x",
|
||||||
hr = m_ProcService->GetVideoProcessorCaps(guids[i], &m_Desc, renderTargetDesc.Format, &caps);
|
caps.DeviceCaps);
|
||||||
if (SUCCEEDED(hr)) {
|
return false;
|
||||||
if (!(caps.DeviceCaps & DXVA2_VPDev_HardwareDevice)) {
|
}
|
||||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
else if (!(caps.VideoProcessorOperations & DXVA2_VideoProcess_YUV2RGB) &&
|
||||||
"Device %d is not hardware: %x",
|
!(caps.VideoProcessorOperations & DXVA2_VideoProcess_YUV2RGBExtended)) {
|
||||||
i,
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
caps.DeviceCaps);
|
"DXVA2_VideoProcProgressiveDevice can't convert YUV2RGB: %x",
|
||||||
continue;
|
caps.VideoProcessorOperations);
|
||||||
}
|
return false;
|
||||||
else if (!(caps.VideoProcessorOperations & DXVA2_VideoProcess_YUV2RGB) &&
|
}
|
||||||
!(caps.VideoProcessorOperations & DXVA2_VideoProcess_YUV2RGBExtended)) {
|
else if (!(caps.VideoProcessorOperations & DXVA2_VideoProcess_StretchX) ||
|
||||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
!(caps.VideoProcessorOperations & DXVA2_VideoProcess_StretchY)) {
|
||||||
"Device %d can't convert YUV2RGB: %x",
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
i,
|
"DXVA2_VideoProcProgressiveDevice can't stretch video: %x",
|
||||||
caps.VideoProcessorOperations);
|
caps.VideoProcessorOperations);
|
||||||
continue;
|
return false;
|
||||||
}
|
|
||||||
else if (!(caps.VideoProcessorOperations & DXVA2_VideoProcess_StretchX) ||
|
|
||||||
!(caps.VideoProcessorOperations & DXVA2_VideoProcess_StretchY)) {
|
|
||||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Device %d can't stretch video: %x",
|
|
||||||
i,
|
|
||||||
caps.VideoProcessorOperations);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_ProcService->GetProcAmpRange(guids[i], &m_Desc, renderTargetDesc.Format, DXVA2_ProcAmp_Brightness, &m_BrightnessRange);
|
|
||||||
m_ProcService->GetProcAmpRange(guids[i], &m_Desc, renderTargetDesc.Format, DXVA2_ProcAmp_Contrast, &m_ContrastRange);
|
|
||||||
m_ProcService->GetProcAmpRange(guids[i], &m_Desc, renderTargetDesc.Format, DXVA2_ProcAmp_Hue, &m_HueRange);
|
|
||||||
m_ProcService->GetProcAmpRange(guids[i], &m_Desc, renderTargetDesc.Format, DXVA2_ProcAmp_Saturation, &m_SaturationRange);
|
|
||||||
|
|
||||||
hr = m_ProcService->CreateVideoProcessor(guids[i], &m_Desc, renderTargetDesc.Format, 0, &m_Processor);
|
|
||||||
if (FAILED(hr)) {
|
|
||||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"CreateVideoProcessor() failed for GUID %d: %x",
|
|
||||||
i,
|
|
||||||
hr);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"GetVideoProcessorCaps() failed for GUID %d: %x",
|
|
||||||
i,
|
|
||||||
hr);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
CoTaskMemFree(guids);
|
if (caps.DeviceCaps & DXVA2_VPDev_EmulatedDXVA1) {
|
||||||
|
// DXVA2 over DXVA1 may have bad performance
|
||||||
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"DXVA2_VideoProcProgressiveDevice is DXVA1");
|
||||||
|
}
|
||||||
|
|
||||||
if (i == guidCount) {
|
m_ProcService->GetProcAmpRange(DXVA2_VideoProcProgressiveDevice, &m_Desc, renderTargetDesc.Format, DXVA2_ProcAmp_Brightness, &m_BrightnessRange);
|
||||||
|
m_ProcService->GetProcAmpRange(DXVA2_VideoProcProgressiveDevice, &m_Desc, renderTargetDesc.Format, DXVA2_ProcAmp_Contrast, &m_ContrastRange);
|
||||||
|
m_ProcService->GetProcAmpRange(DXVA2_VideoProcProgressiveDevice, &m_Desc, renderTargetDesc.Format, DXVA2_ProcAmp_Hue, &m_HueRange);
|
||||||
|
m_ProcService->GetProcAmpRange(DXVA2_VideoProcProgressiveDevice, &m_Desc, renderTargetDesc.Format, DXVA2_ProcAmp_Saturation, &m_SaturationRange);
|
||||||
|
|
||||||
|
hr = m_ProcService->CreateVideoProcessor(DXVA2_VideoProcProgressiveDevice, &m_Desc, renderTargetDesc.Format, 0, &m_Processor);
|
||||||
|
if (FAILED(hr)) {
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"Unable to find a usable DXVA2 processor");
|
"CreateVideoProcessor() failed for DXVA2_VideoProcProgressiveDevice: %x",
|
||||||
|
hr);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -364,6 +342,13 @@ bool DXVA2Renderer::isDecoderBlacklisted()
|
|||||||
id.Description,
|
id.Description,
|
||||||
id.VendorId,
|
id.VendorId,
|
||||||
id.DeviceId);
|
id.DeviceId);
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"GPU driver: %s %d.%d.%d.%d",
|
||||||
|
id.Driver,
|
||||||
|
HIWORD(id.DriverVersion.HighPart),
|
||||||
|
LOWORD(id.DriverVersion.HighPart),
|
||||||
|
HIWORD(id.DriverVersion.LowPart),
|
||||||
|
LOWORD(id.DriverVersion.LowPart));
|
||||||
|
|
||||||
if (id.VendorId == 0x8086) {
|
if (id.VendorId == 0x8086) {
|
||||||
// Intel seems to encode the series in the high byte of
|
// Intel seems to encode the series in the high byte of
|
||||||
@@ -379,26 +364,50 @@ bool DXVA2Renderer::isDecoderBlacklisted()
|
|||||||
case 0x1600: // Broadwell
|
case 0x1600: // Broadwell
|
||||||
case 0x2200: // Cherry Trail and Braswell
|
case 0x2200: // Cherry Trail and Braswell
|
||||||
// Blacklist these for HEVC to avoid hybrid decode
|
// Blacklist these for HEVC to avoid hybrid decode
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"GPU blacklisted for HEVC due to hybrid decode");
|
||||||
result = (m_VideoFormat & VIDEO_FORMAT_MASK_H265) != 0;
|
result = (m_VideoFormat & VIDEO_FORMAT_MASK_H265) != 0;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
// Everything else is fine with whatever it says it supports
|
// Intel drivers from before late-2017 had a bug that caused some strange artifacts
|
||||||
result = false;
|
// when decoding HEVC. Avoid HEVC on drivers prior to build 4836 which I confirmed
|
||||||
|
// is not affected on my Intel HD 515.
|
||||||
|
// https://github.com/moonlight-stream/moonlight-qt/issues/32
|
||||||
|
// 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,
|
||||||
|
"Intel driver version blacklisted for HEVC");
|
||||||
|
result = (m_VideoFormat & VIDEO_FORMAT_MASK_H265) != 0;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Everything else is fine with whatever it says it supports
|
||||||
|
result = false;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (id.VendorId == 0x10DE) {
|
else if (id.VendorId == 0x10DE) {
|
||||||
// For NVIDIA, we wait to avoid those GPUs with Feature Set E
|
// For NVIDIA, we wait to avoid those GPUs with Feature Set E
|
||||||
// for HEVC decoding, since that's hybrid.
|
// for HEVC decoding, since that's hybrid. It appears that Kepler GPUs
|
||||||
|
// also had some hybrid decode support (per DXVA2 Checker) so we'll
|
||||||
|
// blacklist those too.
|
||||||
// https://en.wikipedia.org/wiki/Nvidia_PureVideo
|
// https://en.wikipedia.org/wiki/Nvidia_PureVideo
|
||||||
|
// https://bluesky23.yukishigure.com/en/dxvac/deviceInfo/decoder.html
|
||||||
// http://envytools.readthedocs.io/en/latest/hw/pciid.html (missing GM200)
|
// http://envytools.readthedocs.io/en/latest/hw/pciid.html (missing GM200)
|
||||||
if ((id.DeviceId >= 0x1340 && id.DeviceId <= 0x137F) || // GM108
|
if ((id.DeviceId >= 0x1180 && id.DeviceId <= 0x11BF) || // GK104
|
||||||
|
(id.DeviceId >= 0x11C0 && id.DeviceId <= 0x11FF) || // GK106
|
||||||
|
(id.DeviceId >= 0x0FC0 && id.DeviceId <= 0x0FFF) || // GK107
|
||||||
|
(id.DeviceId >= 0x1000 && id.DeviceId <= 0x103F) || // GK110/GK110B
|
||||||
|
(id.DeviceId >= 0x1280 && id.DeviceId <= 0x12BF) || // GK208
|
||||||
|
(id.DeviceId >= 0x1340 && id.DeviceId <= 0x137F) || // GM108
|
||||||
(id.DeviceId >= 0x1380 && id.DeviceId <= 0x13BF) || // GM107
|
(id.DeviceId >= 0x1380 && id.DeviceId <= 0x13BF) || // GM107
|
||||||
(id.DeviceId >= 0x13C0 && id.DeviceId <= 0x13FF) || // GM204
|
(id.DeviceId >= 0x13C0 && id.DeviceId <= 0x13FF) || // GM204
|
||||||
(id.DeviceId >= 0x1617 && id.DeviceId <= 0x161A) || // GM204
|
(id.DeviceId >= 0x1617 && id.DeviceId <= 0x161A) || // GM204
|
||||||
(id.DeviceId == 0x1667) || // GM204
|
(id.DeviceId == 0x1667) || // GM204
|
||||||
(id.DeviceId >= 0x17C0 && id.DeviceId <= 0x17FF)) { // GM200
|
(id.DeviceId >= 0x17C0 && id.DeviceId <= 0x17FF)) { // GM200
|
||||||
// Avoid HEVC on Feature Set E GPUs
|
// Avoid HEVC on Feature Set E GPUs
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"GPU blacklisted for HEVC due to hybrid decode");
|
||||||
result = (m_VideoFormat & VIDEO_FORMAT_MASK_H265) != 0;
|
result = (m_VideoFormat & VIDEO_FORMAT_MASK_H265) != 0;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -466,7 +475,6 @@ bool DXVA2Renderer::initializeDevice(SDL_Window* window, bool enableVsync)
|
|||||||
D3DPRESENT_PARAMETERS d3dpp = {};
|
D3DPRESENT_PARAMETERS d3dpp = {};
|
||||||
d3dpp.hDeviceWindow = info.info.win.window;
|
d3dpp.hDeviceWindow = info.info.win.window;
|
||||||
d3dpp.Flags = D3DPRESENTFLAG_VIDEO;
|
d3dpp.Flags = D3DPRESENTFLAG_VIDEO;
|
||||||
d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
|
|
||||||
|
|
||||||
if ((windowFlags & SDL_WINDOW_FULLSCREEN_DESKTOP) == SDL_WINDOW_FULLSCREEN) {
|
if ((windowFlags & SDL_WINDOW_FULLSCREEN_DESKTOP) == SDL_WINDOW_FULLSCREEN) {
|
||||||
d3dpp.Windowed = false;
|
d3dpp.Windowed = false;
|
||||||
@@ -489,10 +497,20 @@ bool DXVA2Renderer::initializeDevice(SDL_Window* window, bool enableVsync)
|
|||||||
// to reduce latency by avoiding double v-syncing.
|
// to reduce latency by avoiding double v-syncing.
|
||||||
d3dpp.PresentationInterval = D3DPRESENT_INTERVAL_IMMEDIATE;
|
d3dpp.PresentationInterval = D3DPRESENT_INTERVAL_IMMEDIATE;
|
||||||
|
|
||||||
// D3DSWAPEFFECT_FLIPEX requires at least 2 back buffers to allow us to
|
// If V-sync is enabled (not rendering faster than display),
|
||||||
// continue while DWM is waiting to render the surface to the display.
|
// we can use FlipEx for more efficient swapping.
|
||||||
d3dpp.SwapEffect = D3DSWAPEFFECT_FLIPEX;
|
if (enableVsync) {
|
||||||
d3dpp.BackBufferCount = 2;
|
// D3DSWAPEFFECT_FLIPEX requires at least 2 back buffers to allow us to
|
||||||
|
// continue while DWM is waiting to render the surface to the display.
|
||||||
|
d3dpp.SwapEffect = D3DSWAPEFFECT_FLIPEX;
|
||||||
|
d3dpp.BackBufferCount = 2;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// With V-sync off, we won't use FlipEx because that will block while
|
||||||
|
// DWM is waiting to render our surface (effectively behaving like V-Sync).
|
||||||
|
d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
|
||||||
|
d3dpp.BackBufferCount = 1;
|
||||||
|
}
|
||||||
|
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"Windowed mode with DWM running");
|
"Windowed mode with DWM running");
|
||||||
@@ -501,6 +519,7 @@ bool DXVA2Renderer::initializeDevice(SDL_Window* window, bool enableVsync)
|
|||||||
// Uncomposited desktop or full-screen exclusive mode with V-sync enabled
|
// Uncomposited desktop or full-screen exclusive mode with V-sync enabled
|
||||||
// We will enable V-sync in this scenario to avoid tearing.
|
// We will enable V-sync in this scenario to avoid tearing.
|
||||||
d3dpp.PresentationInterval = D3DPRESENT_INTERVAL_ONE;
|
d3dpp.PresentationInterval = D3DPRESENT_INTERVAL_ONE;
|
||||||
|
d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
|
||||||
d3dpp.BackBufferCount = 1;
|
d3dpp.BackBufferCount = 1;
|
||||||
|
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
@@ -510,6 +529,7 @@ bool DXVA2Renderer::initializeDevice(SDL_Window* window, bool enableVsync)
|
|||||||
// Uncomposited desktop or full-screen exclusive mode with V-sync disabled
|
// Uncomposited desktop or full-screen exclusive mode with V-sync disabled
|
||||||
// We will allowing tearing for lowest latency.
|
// We will allowing tearing for lowest latency.
|
||||||
d3dpp.PresentationInterval = D3DPRESENT_INTERVAL_IMMEDIATE;
|
d3dpp.PresentationInterval = D3DPRESENT_INTERVAL_IMMEDIATE;
|
||||||
|
d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
|
||||||
d3dpp.BackBufferCount = 1;
|
d3dpp.BackBufferCount = 1;
|
||||||
|
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
@@ -619,6 +639,16 @@ bool DXVA2Renderer::needsTestFrame()
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int DXVA2Renderer::getDecoderCapabilities()
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
IFFmpegRenderer::VSyncConstraint DXVA2Renderer::getVsyncConstraint()
|
||||||
|
{
|
||||||
|
return VSYNC_ANY;
|
||||||
|
}
|
||||||
|
|
||||||
void DXVA2Renderer::renderFrameAtVsync(AVFrame *frame)
|
void DXVA2Renderer::renderFrameAtVsync(AVFrame *frame)
|
||||||
{
|
{
|
||||||
IDirect3DSurface9* surface = reinterpret_cast<IDirect3DSurface9*>(frame->data[3]);
|
IDirect3DSurface9* surface = reinterpret_cast<IDirect3DSurface9*>(frame->data[3]);
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ public:
|
|||||||
virtual bool prepareDecoderContext(AVCodecContext* context);
|
virtual bool prepareDecoderContext(AVCodecContext* context);
|
||||||
virtual void renderFrameAtVsync(AVFrame* frame);
|
virtual void renderFrameAtVsync(AVFrame* frame);
|
||||||
virtual bool needsTestFrame();
|
virtual bool needsTestFrame();
|
||||||
|
virtual int getDecoderCapabilities();
|
||||||
|
virtual VSyncConstraint getVsyncConstraint();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
bool initializeDecoder();
|
bool initializeDecoder();
|
||||||
|
|||||||
@@ -1,50 +0,0 @@
|
|||||||
#include "displaylinkvsyncsource.h"
|
|
||||||
|
|
||||||
DisplayLinkVsyncSource::DisplayLinkVsyncSource(Pacer* pacer)
|
|
||||||
: m_Pacer(pacer),
|
|
||||||
m_DisplayLink(nullptr)
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
DisplayLinkVsyncSource::~DisplayLinkVsyncSource()
|
|
||||||
{
|
|
||||||
if (m_DisplayLink != nullptr) {
|
|
||||||
CVDisplayLinkStop(m_DisplayLink);
|
|
||||||
CVDisplayLinkRelease(m_DisplayLink);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CVReturn
|
|
||||||
DisplayLinkVsyncSource::displayLinkOutputCallback(
|
|
||||||
CVDisplayLinkRef,
|
|
||||||
const CVTimeStamp* /* now */,
|
|
||||||
const CVTimeStamp* /* vsyncTime */,
|
|
||||||
CVOptionFlags,
|
|
||||||
CVOptionFlags*,
|
|
||||||
void *displayLinkContext)
|
|
||||||
{
|
|
||||||
auto me = reinterpret_cast<DisplayLinkVsyncSource*>(displayLinkContext);
|
|
||||||
|
|
||||||
// In my testing on macOS 10.13, this callback is invoked about 24 ms
|
|
||||||
// prior to the specified v-sync time (now - vsyncTime). Since this is
|
|
||||||
// greater than the standard v-sync interval (16 ms = 60 FPS), we will
|
|
||||||
// draw using the current host time, rather than the actual v-sync target
|
|
||||||
// time. Because the CVDisplayLink is in sync with the actual v-sync
|
|
||||||
// interval, even if many ms prior, we can safely use the current host time
|
|
||||||
// and get a consistent callback for each v-sync. This reduces video latency
|
|
||||||
// by at least 1 frame vs. rendering with the actual vsyncTime.
|
|
||||||
me->m_Pacer->vsyncCallback(500 / me->m_DisplayFps);
|
|
||||||
|
|
||||||
return kCVReturnSuccess;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool DisplayLinkVsyncSource::initialize(SDL_Window*, int displayFps)
|
|
||||||
{
|
|
||||||
m_DisplayFps = displayFps;
|
|
||||||
|
|
||||||
CVDisplayLinkCreateWithActiveCGDisplays(&m_DisplayLink);
|
|
||||||
CVDisplayLinkSetOutputCallback(m_DisplayLink, displayLinkOutputCallback, this);
|
|
||||||
CVDisplayLinkStart(m_DisplayLink);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
@@ -1,31 +1,11 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <CoreVideo/CoreVideo.h>
|
|
||||||
|
|
||||||
#include "pacer.h"
|
#include "pacer.h"
|
||||||
|
|
||||||
class DisplayLinkVsyncSource : public IVsyncSource
|
class DisplayLinkVsyncSourceFactory
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
DisplayLinkVsyncSource(Pacer* pacer);
|
|
||||||
|
|
||||||
virtual ~DisplayLinkVsyncSource();
|
|
||||||
|
|
||||||
virtual bool initialize(SDL_Window* window, int displayFps);
|
|
||||||
|
|
||||||
private:
|
|
||||||
static
|
static
|
||||||
CVReturn
|
IVsyncSource* createVsyncSource(Pacer* pacer);
|
||||||
displayLinkOutputCallback(
|
|
||||||
CVDisplayLinkRef,
|
|
||||||
const CVTimeStamp* now,
|
|
||||||
const CVTimeStamp* /* vsyncTime */,
|
|
||||||
CVOptionFlags,
|
|
||||||
CVOptionFlags*,
|
|
||||||
void *displayLinkContext);
|
|
||||||
|
|
||||||
Pacer* m_Pacer;
|
|
||||||
CVDisplayLinkRef m_DisplayLink;
|
|
||||||
int m_DisplayFps;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
#include "displaylinkvsyncsource.h"
|
||||||
|
|
||||||
|
#include <SDL_syswm.h>
|
||||||
|
|
||||||
|
#include <CoreVideo/CoreVideo.h>
|
||||||
|
|
||||||
|
#import <Cocoa/Cocoa.h>
|
||||||
|
|
||||||
|
class DisplayLinkVsyncSource : public IVsyncSource
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
DisplayLinkVsyncSource(Pacer* pacer)
|
||||||
|
: m_Pacer(pacer),
|
||||||
|
m_DisplayLink(nullptr)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual ~DisplayLinkVsyncSource() override
|
||||||
|
{
|
||||||
|
if (m_DisplayLink != nullptr) {
|
||||||
|
CVDisplayLinkStop(m_DisplayLink);
|
||||||
|
CVDisplayLinkRelease(m_DisplayLink);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static
|
||||||
|
CGDirectDisplayID
|
||||||
|
getDisplayID(NSScreen* screen)
|
||||||
|
{
|
||||||
|
NSNumber* screenNumber = [screen deviceDescription][@"NSScreenNumber"];
|
||||||
|
return [screenNumber unsignedIntValue];
|
||||||
|
}
|
||||||
|
|
||||||
|
static
|
||||||
|
CVReturn
|
||||||
|
displayLinkOutputCallback(
|
||||||
|
CVDisplayLinkRef displayLink,
|
||||||
|
const CVTimeStamp* /* now */,
|
||||||
|
const CVTimeStamp* /* vsyncTime */,
|
||||||
|
CVOptionFlags,
|
||||||
|
CVOptionFlags*,
|
||||||
|
void *displayLinkContext)
|
||||||
|
{
|
||||||
|
auto me = reinterpret_cast<DisplayLinkVsyncSource*>(displayLinkContext);
|
||||||
|
|
||||||
|
SDL_assert(displayLink == me->m_DisplayLink);
|
||||||
|
|
||||||
|
// In my testing on macOS 10.13, this callback is invoked about 24 ms
|
||||||
|
// prior to the specified v-sync time (now - vsyncTime). Since this is
|
||||||
|
// greater than the standard v-sync interval (16 ms = 60 FPS), we will
|
||||||
|
// draw using the current host time, rather than the actual v-sync target
|
||||||
|
// time. Because the CVDisplayLink is in sync with the actual v-sync
|
||||||
|
// interval, even if many ms prior, we can safely use the current host time
|
||||||
|
// and get a consistent callback for each v-sync. This reduces video latency
|
||||||
|
// by at least 1 frame vs. rendering with the actual vsyncTime.
|
||||||
|
me->m_Pacer->vsyncCallback(500 / me->m_DisplayFps);
|
||||||
|
|
||||||
|
return kCVReturnSuccess;
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual bool initialize(SDL_Window* window, int displayFps) override
|
||||||
|
{
|
||||||
|
SDL_SysWMinfo info;
|
||||||
|
|
||||||
|
SDL_VERSION(&info.version);
|
||||||
|
|
||||||
|
if (!SDL_GetWindowWMInfo(window, &info)) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"SDL_GetWindowWMInfo() failed: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_assert(info.subsystem == SDL_SYSWM_COCOA);
|
||||||
|
|
||||||
|
m_DisplayFps = displayFps;
|
||||||
|
|
||||||
|
NSScreen* screen = [info.info.cocoa.window screen];
|
||||||
|
CVReturn status;
|
||||||
|
if (screen == nullptr) {
|
||||||
|
// Window not visible on any display, so use a
|
||||||
|
// CVDisplayLink that can work with all active displays.
|
||||||
|
// When we become visible, we'll recreate ourselves
|
||||||
|
// and associate with the new screen.
|
||||||
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"NSWindow is not visible on any display");
|
||||||
|
status = CVDisplayLinkCreateWithActiveCGDisplays(&m_DisplayLink);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
CGDirectDisplayID displayId;
|
||||||
|
displayId = getDisplayID(screen);
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"NSWindow on display: %x",
|
||||||
|
displayId);
|
||||||
|
status = CVDisplayLinkCreateWithCGDisplay(displayId, &m_DisplayLink);
|
||||||
|
}
|
||||||
|
if (status != kCVReturnSuccess) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Failed to create CVDisplayLink: %d",
|
||||||
|
status);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
status = CVDisplayLinkSetOutputCallback(m_DisplayLink, displayLinkOutputCallback, this);
|
||||||
|
if (status != kCVReturnSuccess) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"CVDisplayLinkSetOutputCallback() failed: %d",
|
||||||
|
status);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
status = CVDisplayLinkStart(m_DisplayLink);
|
||||||
|
if (status != kCVReturnSuccess) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"CVDisplayLinkStart() failed: %d",
|
||||||
|
status);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
Pacer* m_Pacer;
|
||||||
|
CVDisplayLinkRef m_DisplayLink;
|
||||||
|
int m_DisplayFps;
|
||||||
|
};
|
||||||
|
|
||||||
|
IVsyncSource* DisplayLinkVsyncSourceFactory::createVsyncSource(Pacer* pacer) {
|
||||||
|
return new DisplayLinkVsyncSource(pacer);
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
#include "pacer.h"
|
#include "pacer.h"
|
||||||
|
#include "streaming/streamutils.h"
|
||||||
|
|
||||||
#include "nullthreadedvsyncsource.h"
|
#include "nullthreadedvsyncsource.h"
|
||||||
|
|
||||||
@@ -19,12 +20,13 @@
|
|||||||
// V-sync happens.
|
// V-sync happens.
|
||||||
#define TIMER_SLACK_MS 3
|
#define TIMER_SLACK_MS 3
|
||||||
|
|
||||||
Pacer::Pacer(IFFmpegRenderer* renderer) :
|
Pacer::Pacer(IFFmpegRenderer* renderer, PVIDEO_STATS videoStats) :
|
||||||
m_FrameQueueLock(0),
|
m_FrameQueueLock(0),
|
||||||
m_VsyncSource(nullptr),
|
m_VsyncSource(nullptr),
|
||||||
m_VsyncRenderer(renderer),
|
m_VsyncRenderer(renderer),
|
||||||
m_MaxVideoFps(0),
|
m_MaxVideoFps(0),
|
||||||
m_DisplayFps(0)
|
m_DisplayFps(0),
|
||||||
|
m_VideoStats(videoStats)
|
||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -81,6 +83,7 @@ void Pacer::vsyncCallback(int timeUntilNextVsyncMillis)
|
|||||||
// Catch up if we're several frames ahead
|
// Catch up if we're several frames ahead
|
||||||
while (m_FrameQueue.count() > frameDropTarget) {
|
while (m_FrameQueue.count() > frameDropTarget) {
|
||||||
AVFrame* frame = m_FrameQueue.dequeue();
|
AVFrame* frame = m_FrameQueue.dequeue();
|
||||||
|
m_VideoStats->pacerDroppedFrames++;
|
||||||
av_frame_free(&frame);
|
av_frame_free(&frame);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,7 +113,10 @@ RenderNextFrame:
|
|||||||
SDL_AtomicUnlock(&m_FrameQueueLock);
|
SDL_AtomicUnlock(&m_FrameQueueLock);
|
||||||
|
|
||||||
// Render it
|
// Render it
|
||||||
|
Uint32 beforeRender = SDL_GetTicks();
|
||||||
m_VsyncRenderer->renderFrameAtVsync(frame);
|
m_VsyncRenderer->renderFrameAtVsync(frame);
|
||||||
|
m_VideoStats->totalRenderTime += SDL_GetTicks() - beforeRender;
|
||||||
|
m_VideoStats->renderedFrames++;
|
||||||
|
|
||||||
// Free the frame
|
// Free the frame
|
||||||
av_frame_free(&frame);
|
av_frame_free(&frame);
|
||||||
@@ -120,22 +126,7 @@ bool Pacer::initialize(SDL_Window* window, int maxVideoFps, bool enableVsync)
|
|||||||
{
|
{
|
||||||
m_MaxVideoFps = maxVideoFps;
|
m_MaxVideoFps = maxVideoFps;
|
||||||
m_EnableVsync = enableVsync;
|
m_EnableVsync = enableVsync;
|
||||||
|
m_DisplayFps = StreamUtils::getDisplayRefreshRate(window);
|
||||||
int displayIndex = SDL_GetWindowDisplayIndex(window);
|
|
||||||
if (displayIndex < 0) {
|
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Failed to get current display: %s",
|
|
||||||
SDL_GetError());
|
|
||||||
|
|
||||||
// Assume display 0 if it fails
|
|
||||||
displayIndex = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
SDL_DisplayMode mode;
|
|
||||||
if (SDL_GetCurrentDisplayMode(displayIndex, &mode) == 0) {
|
|
||||||
// May be zero if undefined
|
|
||||||
m_DisplayFps = mode.refresh_rate;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_EnableVsync) {
|
if (m_EnableVsync) {
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
@@ -143,7 +134,7 @@ bool Pacer::initialize(SDL_Window* window, int maxVideoFps, bool enableVsync)
|
|||||||
m_DisplayFps, m_MaxVideoFps);
|
m_DisplayFps, m_MaxVideoFps);
|
||||||
|
|
||||||
#if defined(Q_OS_DARWIN)
|
#if defined(Q_OS_DARWIN)
|
||||||
m_VsyncSource = new DisplayLinkVsyncSource(this);
|
m_VsyncSource = DisplayLinkVsyncSourceFactory::createVsyncSource(this);
|
||||||
#elif defined(Q_OS_WIN32)
|
#elif defined(Q_OS_WIN32)
|
||||||
m_VsyncSource = new DxVsyncSource(this);
|
m_VsyncSource = new DxVsyncSource(this);
|
||||||
#else
|
#else
|
||||||
@@ -178,7 +169,10 @@ void Pacer::submitFrame(AVFrame* frame)
|
|||||||
SDL_AtomicUnlock(&m_FrameQueueLock);
|
SDL_AtomicUnlock(&m_FrameQueueLock);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
Uint32 beforeRender = SDL_GetTicks();
|
||||||
m_VsyncRenderer->renderFrameAtVsync(frame);
|
m_VsyncRenderer->renderFrameAtVsync(frame);
|
||||||
|
m_VideoStats->totalRenderTime += SDL_GetTicks() - beforeRender;
|
||||||
|
m_VideoStats->renderedFrames++;
|
||||||
av_frame_free(&frame);
|
av_frame_free(&frame);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "../../decoder.h"
|
||||||
#include "../renderer.h"
|
#include "../renderer.h"
|
||||||
|
|
||||||
#include <QQueue>
|
#include <QQueue>
|
||||||
@@ -13,7 +14,7 @@ public:
|
|||||||
class Pacer
|
class Pacer
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
Pacer(IFFmpegRenderer* renderer);
|
Pacer(IFFmpegRenderer* renderer, PVIDEO_STATS videoStats);
|
||||||
|
|
||||||
~Pacer();
|
~Pacer();
|
||||||
|
|
||||||
@@ -35,4 +36,5 @@ private:
|
|||||||
int m_MaxVideoFps;
|
int m_MaxVideoFps;
|
||||||
int m_DisplayFps;
|
int m_DisplayFps;
|
||||||
bool m_EnableVsync;
|
bool m_EnableVsync;
|
||||||
|
PVIDEO_STATS m_VideoStats;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,6 +8,12 @@ extern "C" {
|
|||||||
|
|
||||||
class IFFmpegRenderer {
|
class IFFmpegRenderer {
|
||||||
public:
|
public:
|
||||||
|
enum VSyncConstraint {
|
||||||
|
VSYNC_FORCE_OFF,
|
||||||
|
VSYNC_FORCE_ON,
|
||||||
|
VSYNC_ANY
|
||||||
|
};
|
||||||
|
|
||||||
virtual ~IFFmpegRenderer() {}
|
virtual ~IFFmpegRenderer() {}
|
||||||
virtual bool initialize(SDL_Window* window,
|
virtual bool initialize(SDL_Window* window,
|
||||||
int videoFormat,
|
int videoFormat,
|
||||||
@@ -18,6 +24,8 @@ public:
|
|||||||
virtual bool prepareDecoderContext(AVCodecContext* context) = 0;
|
virtual bool prepareDecoderContext(AVCodecContext* context) = 0;
|
||||||
virtual void renderFrameAtVsync(AVFrame* frame) = 0;
|
virtual void renderFrameAtVsync(AVFrame* frame) = 0;
|
||||||
virtual bool needsTestFrame() = 0;
|
virtual bool needsTestFrame() = 0;
|
||||||
|
virtual int getDecoderCapabilities() = 0;
|
||||||
|
virtual VSyncConstraint getVsyncConstraint() = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
class SdlRenderer : public IFFmpegRenderer {
|
class SdlRenderer : public IFFmpegRenderer {
|
||||||
@@ -33,6 +41,8 @@ public:
|
|||||||
virtual bool prepareDecoderContext(AVCodecContext* context);
|
virtual bool prepareDecoderContext(AVCodecContext* context);
|
||||||
virtual void renderFrameAtVsync(AVFrame* frame);
|
virtual void renderFrameAtVsync(AVFrame* frame);
|
||||||
virtual bool needsTestFrame();
|
virtual bool needsTestFrame();
|
||||||
|
virtual int getDecoderCapabilities();
|
||||||
|
virtual VSyncConstraint getVsyncConstraint();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
SDL_Renderer* m_Renderer;
|
SDL_Renderer* m_Renderer;
|
||||||
|
|||||||
+12
@@ -36,6 +36,18 @@ bool SdlRenderer::needsTestFrame()
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int SdlRenderer::getDecoderCapabilities()
|
||||||
|
{
|
||||||
|
// The FFmpeg CPU decoder can handle reference frame invalidation,
|
||||||
|
// but only for H.264.
|
||||||
|
return CAPABILITY_REFERENCE_FRAME_INVALIDATION_AVC;
|
||||||
|
}
|
||||||
|
|
||||||
|
IFFmpegRenderer::VSyncConstraint SdlRenderer::getVsyncConstraint()
|
||||||
|
{
|
||||||
|
return VSYNC_ANY;
|
||||||
|
}
|
||||||
|
|
||||||
bool SdlRenderer::initialize(SDL_Window* window,
|
bool SdlRenderer::initialize(SDL_Window* window,
|
||||||
int,
|
int,
|
||||||
int width,
|
int width,
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
#include <QString>
|
||||||
|
|
||||||
#include "vaapi.h"
|
#include "vaapi.h"
|
||||||
#include <streaming/streamutils.h>
|
#include <streaming/streamutils.h>
|
||||||
|
|
||||||
@@ -109,6 +111,26 @@ VAAPIRenderer::initialize(SDL_Window* window, int, int width, int height, int, b
|
|||||||
"Initialized VAAPI %d.%d",
|
"Initialized VAAPI %d.%d",
|
||||||
major, minor);
|
major, minor);
|
||||||
|
|
||||||
|
const char* vendorString = vaQueryVendorString(vaDeviceContext->display);
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Driver: %s",
|
||||||
|
vendorString ? vendorString : "<unknown>");
|
||||||
|
|
||||||
|
// AMD's Gallium VAAPI driver has a nasty memory leak
|
||||||
|
// that causes memory 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 (vendorString && qgetenv("FORCE_VAAPI") != "1") {
|
||||||
|
QString vendorStr(vendorString);
|
||||||
|
if (vendorStr.contains("AMD", Qt::CaseInsensitive) ||
|
||||||
|
vendorStr.contains("Radeon", Qt::CaseInsensitive)) {
|
||||||
|
// Fail and let VDPAU pick this up
|
||||||
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Avoiding VAAPI on AMD driver");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// This will populate the driver_quirks
|
// This will populate the driver_quirks
|
||||||
err = av_hwdevice_ctx_init(m_HwContext);
|
err = av_hwdevice_ctx_init(m_HwContext);
|
||||||
if (err < 0) {
|
if (err < 0) {
|
||||||
@@ -149,6 +171,17 @@ VAAPIRenderer::needsTestFrame()
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int
|
||||||
|
VAAPIRenderer::getDecoderCapabilities()
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
IFFmpegRenderer::VSyncConstraint VAAPIRenderer::getVsyncConstraint()
|
||||||
|
{
|
||||||
|
return VSYNC_ANY;
|
||||||
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
VAAPIRenderer::renderFrameAtVsync(AVFrame* frame)
|
VAAPIRenderer::renderFrameAtVsync(AVFrame* frame)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ public:
|
|||||||
virtual bool prepareDecoderContext(AVCodecContext* context);
|
virtual bool prepareDecoderContext(AVCodecContext* context);
|
||||||
virtual void renderFrameAtVsync(AVFrame* frame);
|
virtual void renderFrameAtVsync(AVFrame* frame);
|
||||||
virtual bool needsTestFrame();
|
virtual bool needsTestFrame();
|
||||||
|
virtual int getDecoderCapabilities();
|
||||||
|
virtual VSyncConstraint getVsyncConstraint();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
int m_WindowSystem;
|
int m_WindowSystem;
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ bool VDPAURenderer::initialize(SDL_Window* window, int, int width, int height, i
|
|||||||
GET_PROC_ADDRESS(VDP_FUNC_ID_OUTPUT_SURFACE_DESTROY, &m_VdpOutputSurfaceDestroy);
|
GET_PROC_ADDRESS(VDP_FUNC_ID_OUTPUT_SURFACE_DESTROY, &m_VdpOutputSurfaceDestroy);
|
||||||
GET_PROC_ADDRESS(VDP_FUNC_ID_OUTPUT_SURFACE_QUERY_CAPABILITIES, &m_VdpOutputSurfaceQueryCapabilities);
|
GET_PROC_ADDRESS(VDP_FUNC_ID_OUTPUT_SURFACE_QUERY_CAPABILITIES, &m_VdpOutputSurfaceQueryCapabilities);
|
||||||
GET_PROC_ADDRESS(VDP_FUNC_ID_VIDEO_SURFACE_GET_PARAMETERS, &m_VdpVideoSurfaceGetParameters);
|
GET_PROC_ADDRESS(VDP_FUNC_ID_VIDEO_SURFACE_GET_PARAMETERS, &m_VdpVideoSurfaceGetParameters);
|
||||||
|
GET_PROC_ADDRESS(VDP_FUNC_ID_GET_INFORMATION_STRING, &m_VdpGetInformationString);
|
||||||
|
|
||||||
SDL_GetWindowSize(window, (int*)&m_DisplayWidth, (int*)&m_DisplayHeight);
|
SDL_GetWindowSize(window, (int*)&m_DisplayWidth, (int*)&m_DisplayHeight);
|
||||||
|
|
||||||
@@ -130,6 +131,13 @@ bool VDPAURenderer::initialize(SDL_Window* window, int, int width, int height, i
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const char* infoString;
|
||||||
|
if (m_VdpGetInformationString(&infoString) == VDP_STATUS_OK) {
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Driver: %s",
|
||||||
|
infoString);
|
||||||
|
}
|
||||||
|
|
||||||
// Try our available output formats to find something the GPU supports
|
// Try our available output formats to find something the GPU supports
|
||||||
bool foundFormat = false;
|
bool foundFormat = false;
|
||||||
for (int i = 0; i < OUTPUT_SURFACE_FORMAT_COUNT; i++) {
|
for (int i = 0; i < OUTPUT_SURFACE_FORMAT_COUNT; i++) {
|
||||||
@@ -217,6 +225,9 @@ bool VDPAURenderer::prepareDecoderContext(AVCodecContext* context)
|
|||||||
// https://github.com/FFmpeg/FFmpeg/commit/64ecb78b7179cab2dbdf835463104679dbb7c895
|
// https://github.com/FFmpeg/FFmpeg/commit/64ecb78b7179cab2dbdf835463104679dbb7c895
|
||||||
context->hwaccel_flags |= AV_HWACCEL_FLAG_ALLOW_PROFILE_MISMATCH;
|
context->hwaccel_flags |= AV_HWACCEL_FLAG_ALLOW_PROFILE_MISMATCH;
|
||||||
|
|
||||||
|
// This flag is recommended due to hardware underreporting supported levels
|
||||||
|
context->hwaccel_flags |= AV_HWACCEL_FLAG_IGNORE_LEVEL;
|
||||||
|
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"Using VDPAU accelerated renderer");
|
"Using VDPAU accelerated renderer");
|
||||||
|
|
||||||
@@ -230,6 +241,16 @@ bool VDPAURenderer::needsTestFrame()
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int VDPAURenderer::getDecoderCapabilities()
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
IFFmpegRenderer::VSyncConstraint VDPAURenderer::getVsyncConstraint()
|
||||||
|
{
|
||||||
|
return VSYNC_ANY;
|
||||||
|
}
|
||||||
|
|
||||||
void VDPAURenderer::renderFrameAtVsync(AVFrame* frame)
|
void VDPAURenderer::renderFrameAtVsync(AVFrame* frame)
|
||||||
{
|
{
|
||||||
VdpStatus status;
|
VdpStatus status;
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ public:
|
|||||||
virtual bool prepareDecoderContext(AVCodecContext* context);
|
virtual bool prepareDecoderContext(AVCodecContext* context);
|
||||||
virtual void renderFrameAtVsync(AVFrame* frame);
|
virtual void renderFrameAtVsync(AVFrame* frame);
|
||||||
virtual bool needsTestFrame();
|
virtual bool needsTestFrame();
|
||||||
|
virtual int getDecoderCapabilities();
|
||||||
|
virtual VSyncConstraint getVsyncConstraint();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
uint32_t m_VideoWidth, m_VideoHeight;
|
uint32_t m_VideoWidth, m_VideoHeight;
|
||||||
@@ -54,6 +56,7 @@ private:
|
|||||||
VdpOutputSurfaceDestroy* m_VdpOutputSurfaceDestroy;
|
VdpOutputSurfaceDestroy* m_VdpOutputSurfaceDestroy;
|
||||||
VdpOutputSurfaceQueryCapabilities* m_VdpOutputSurfaceQueryCapabilities;
|
VdpOutputSurfaceQueryCapabilities* m_VdpOutputSurfaceQueryCapabilities;
|
||||||
VdpVideoSurfaceGetParameters* m_VdpVideoSurfaceGetParameters;
|
VdpVideoSurfaceGetParameters* m_VdpVideoSurfaceGetParameters;
|
||||||
|
VdpGetInformationString* m_VdpGetInformationString;
|
||||||
|
|
||||||
// X11 stuff
|
// X11 stuff
|
||||||
VdpPresentationQueueTargetCreateX11* m_VdpPresentationQueueTargetCreateX11;
|
VdpPresentationQueueTargetCreateX11* m_VdpPresentationQueueTargetCreateX11;
|
||||||
|
|||||||
@@ -160,10 +160,20 @@ public:
|
|||||||
// We need to add a subview for our display layer.
|
// We need to add a subview for our display layer.
|
||||||
NSView* contentView = info.info.cocoa.window.contentView;
|
NSView* contentView = info.info.cocoa.window.contentView;
|
||||||
m_View = [[NSView alloc] initWithFrame:contentView.bounds];
|
m_View = [[NSView alloc] initWithFrame:contentView.bounds];
|
||||||
m_View.wantsLayer = YES;
|
|
||||||
[contentView addSubview: m_View];
|
|
||||||
|
|
||||||
setupDisplayLayer();
|
m_DisplayLayer = [[AVSampleBufferDisplayLayer alloc] init];
|
||||||
|
m_DisplayLayer.bounds = m_View.bounds;
|
||||||
|
m_DisplayLayer.position = CGPointMake(CGRectGetMidX(m_View.bounds), CGRectGetMidY(m_View.bounds));
|
||||||
|
m_DisplayLayer.videoGravity = AVLayerVideoGravityResizeAspect;
|
||||||
|
|
||||||
|
// Create a layer-hosted view by setting the layer before wantsLayer
|
||||||
|
// This avoids us having to add our AVSampleBufferDisplayLayer as a
|
||||||
|
// sublayer of a layer-backed view which leaves a useless layer in
|
||||||
|
// the middle.
|
||||||
|
m_View.layer = m_DisplayLayer;
|
||||||
|
m_View.wantsLayer = YES;
|
||||||
|
|
||||||
|
[contentView addSubview: m_View];
|
||||||
|
|
||||||
err = av_hwdevice_ctx_create(&m_HwContext,
|
err = av_hwdevice_ctx_create(&m_HwContext,
|
||||||
AV_HWDEVICE_TYPE_VIDEOTOOLBOX,
|
AV_HWDEVICE_TYPE_VIDEOTOOLBOX,
|
||||||
@@ -196,25 +206,20 @@ public:
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
virtual int getDecoderCapabilities() override
|
||||||
void setupDisplayLayer()
|
|
||||||
{
|
{
|
||||||
CALayer* oldLayer = m_DisplayLayer;
|
return 0;
|
||||||
|
|
||||||
m_DisplayLayer = [[AVSampleBufferDisplayLayer alloc] init];
|
|
||||||
m_DisplayLayer.bounds = m_View.bounds;
|
|
||||||
m_DisplayLayer.position = CGPointMake(CGRectGetMidX(m_View.bounds), CGRectGetMidY(m_View.bounds));
|
|
||||||
m_DisplayLayer.videoGravity = AVLayerVideoGravityResizeAspect;
|
|
||||||
|
|
||||||
CALayer* viewLayer = m_View.layer;
|
|
||||||
if (oldLayer != nil) {
|
|
||||||
[viewLayer replaceSublayer:oldLayer with:m_DisplayLayer];
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
[viewLayer addSublayer:m_DisplayLayer];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
virtual IFFmpegRenderer::VSyncConstraint getVsyncConstraint() override
|
||||||
|
{
|
||||||
|
// This renderer is inherently tied to V-sync due how we're
|
||||||
|
// rendering with AVSampleBufferDisplay layer. Running without
|
||||||
|
// the V-Sync source leads to massive stuttering.
|
||||||
|
return VSYNC_FORCE_ON;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
AVBufferRef* m_HwContext;
|
AVBufferRef* m_HwContext;
|
||||||
AVSampleBufferDisplayLayer* m_DisplayLayer;
|
AVSampleBufferDisplayLayer* m_DisplayLayer;
|
||||||
CMVideoFormatDescriptionRef m_FormatDesc;
|
CMVideoFormatDescriptionRef m_FormatDesc;
|
||||||
|
|||||||
+229
-12
@@ -1,5 +1,7 @@
|
|||||||
#include <Limelight.h>
|
#include <Limelight.h>
|
||||||
#include "ffmpeg.h"
|
#include "ffmpeg.h"
|
||||||
|
#include "streaming/streamutils.h"
|
||||||
|
#include <h264_stream.h>
|
||||||
|
|
||||||
#ifdef Q_OS_WIN32
|
#ifdef Q_OS_WIN32
|
||||||
#include "ffmpeg-renderers/dxva2.h"
|
#include "ffmpeg-renderers/dxva2.h"
|
||||||
@@ -20,6 +22,8 @@
|
|||||||
// This is gross but it allows us to use sizeof()
|
// This is gross but it allows us to use sizeof()
|
||||||
#include "ffmpeg_videosamples.cpp"
|
#include "ffmpeg_videosamples.cpp"
|
||||||
|
|
||||||
|
#define MAX_SPS_EXTRA_SIZE 16
|
||||||
|
|
||||||
#define FAILED_DECODES_RESET_THRESHOLD 20
|
#define FAILED_DECODES_RESET_THRESHOLD 20
|
||||||
|
|
||||||
bool FFmpegVideoDecoder::isHardwareAccelerated()
|
bool FFmpegVideoDecoder::isHardwareAccelerated()
|
||||||
@@ -27,6 +31,11 @@ bool FFmpegVideoDecoder::isHardwareAccelerated()
|
|||||||
return m_HwDecodeCfg != nullptr;
|
return m_HwDecodeCfg != nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int FFmpegVideoDecoder::getDecoderCapabilities()
|
||||||
|
{
|
||||||
|
return m_Renderer->getDecoderCapabilities();
|
||||||
|
}
|
||||||
|
|
||||||
enum AVPixelFormat FFmpegVideoDecoder::ffGetFormat(AVCodecContext* context,
|
enum AVPixelFormat FFmpegVideoDecoder::ffGetFormat(AVCodecContext* context,
|
||||||
const enum AVPixelFormat* pixFmts)
|
const enum AVPixelFormat* pixFmts)
|
||||||
{
|
{
|
||||||
@@ -54,11 +63,18 @@ FFmpegVideoDecoder::FFmpegVideoDecoder()
|
|||||||
m_HwDecodeCfg(nullptr),
|
m_HwDecodeCfg(nullptr),
|
||||||
m_Renderer(nullptr),
|
m_Renderer(nullptr),
|
||||||
m_ConsecutiveFailedDecodes(0),
|
m_ConsecutiveFailedDecodes(0),
|
||||||
m_Pacer(nullptr)
|
m_Pacer(nullptr),
|
||||||
|
m_LastFrameNumber(0),
|
||||||
|
m_StreamFps(0),
|
||||||
|
m_NeedsSpsFixup(false)
|
||||||
{
|
{
|
||||||
av_init_packet(&m_Pkt);
|
av_init_packet(&m_Pkt);
|
||||||
SDL_AtomicSet(&m_QueuedFrames, 0);
|
SDL_AtomicSet(&m_QueuedFrames, 0);
|
||||||
|
|
||||||
|
SDL_zero(m_ActiveWndVideoStats);
|
||||||
|
SDL_zero(m_LastWndVideoStats);
|
||||||
|
SDL_zero(m_GlobalVideoStats);
|
||||||
|
|
||||||
// Use linear filtering when renderer scaling is required
|
// Use linear filtering when renderer scaling is required
|
||||||
SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "1");
|
SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "1");
|
||||||
}
|
}
|
||||||
@@ -100,17 +116,41 @@ void FFmpegVideoDecoder::reset()
|
|||||||
delete m_Pacer;
|
delete m_Pacer;
|
||||||
m_Pacer = nullptr;
|
m_Pacer = nullptr;
|
||||||
|
|
||||||
|
// This must be called after deleting Pacer because it
|
||||||
|
// may be holding AVFrames to free in its destructor.
|
||||||
|
// However, it must be called before deleting the IFFmpegRenderer
|
||||||
|
// since the codec context may be referencing objects that we
|
||||||
|
// need to delete in the renderer destructor.
|
||||||
|
avcodec_free_context(&m_VideoDecoderCtx);
|
||||||
|
|
||||||
delete m_Renderer;
|
delete m_Renderer;
|
||||||
m_Renderer = nullptr;
|
m_Renderer = nullptr;
|
||||||
|
|
||||||
avcodec_free_context(&m_VideoDecoderCtx);
|
logVideoStats(m_GlobalVideoStats, "Global video stats");
|
||||||
}
|
}
|
||||||
|
|
||||||
bool FFmpegVideoDecoder::completeInitialization(AVCodec* decoder, SDL_Window* window,
|
bool FFmpegVideoDecoder::completeInitialization(AVCodec* decoder, SDL_Window* window,
|
||||||
int videoFormat, int width, int height,
|
int videoFormat, int width, int height,
|
||||||
int maxFps, bool enableVsync, bool testOnly)
|
int maxFps, bool enableVsync, bool testOnly)
|
||||||
{
|
{
|
||||||
m_Pacer = new Pacer(m_Renderer);
|
auto vsyncConstraint = m_Renderer->getVsyncConstraint();
|
||||||
|
if (vsyncConstraint == IFFmpegRenderer::VSYNC_FORCE_OFF && enableVsync) {
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"V-sync is forcefully disabled by the active renderer");
|
||||||
|
enableVsync = false;
|
||||||
|
}
|
||||||
|
else if (vsyncConstraint == IFFmpegRenderer::VSYNC_FORCE_ON && !enableVsync) {
|
||||||
|
// FIXME: This duplicates logic in Session.cpp
|
||||||
|
int displayHz = StreamUtils::getDisplayRefreshRate(window);
|
||||||
|
if (displayHz + 5 >= maxFps) {
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"V-sync is forcefully enabled by the active renderer");
|
||||||
|
enableVsync = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m_StreamFps = maxFps;
|
||||||
|
m_Pacer = new Pacer(m_Renderer, &m_ActiveWndVideoStats);
|
||||||
if (!m_Pacer->initialize(window, maxFps, enableVsync)) {
|
if (!m_Pacer->initialize(window, maxFps, enableVsync)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -125,6 +165,10 @@ bool FFmpegVideoDecoder::completeInitialization(AVCodec* decoder, SDL_Window* wi
|
|||||||
// Always request low delay decoding
|
// Always request low delay decoding
|
||||||
m_VideoDecoderCtx->flags |= AV_CODEC_FLAG_LOW_DELAY;
|
m_VideoDecoderCtx->flags |= AV_CODEC_FLAG_LOW_DELAY;
|
||||||
|
|
||||||
|
// Allow display of corrupt frames and frames missing references
|
||||||
|
m_VideoDecoderCtx->flags |= AV_CODEC_FLAG_OUTPUT_CORRUPT;
|
||||||
|
m_VideoDecoderCtx->flags2 |= AV_CODEC_FLAG2_SHOW_ALL;
|
||||||
|
|
||||||
// Enable slice multi-threading for software decoding
|
// Enable slice multi-threading for software decoding
|
||||||
if (!m_HwDecodeCfg) {
|
if (!m_HwDecodeCfg) {
|
||||||
m_VideoDecoderCtx->thread_type = FF_THREAD_SLICE;
|
m_VideoDecoderCtx->thread_type = FF_THREAD_SLICE;
|
||||||
@@ -185,6 +229,17 @@ bool FFmpegVideoDecoder::completeInitialization(AVCodec* decoder, SDL_Window* wi
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
if ((videoFormat & VIDEO_FORMAT_MASK_H264) &&
|
||||||
|
!(m_Renderer->getDecoderCapabilities() & CAPABILITY_REFERENCE_FRAME_INVALIDATION_AVC)) {
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Using H.264 SPS fixup");
|
||||||
|
m_NeedsSpsFixup = true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
m_NeedsSpsFixup = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#ifdef QT_DEBUG
|
#ifdef QT_DEBUG
|
||||||
// Restore default log level before streaming
|
// Restore default log level before streaming
|
||||||
@@ -194,6 +249,80 @@ bool FFmpegVideoDecoder::completeInitialization(AVCodec* decoder, SDL_Window* wi
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void FFmpegVideoDecoder::addVideoStats(VIDEO_STATS& src, VIDEO_STATS& dst)
|
||||||
|
{
|
||||||
|
dst.receivedFrames += src.receivedFrames;
|
||||||
|
dst.decodedFrames += src.decodedFrames;
|
||||||
|
dst.renderedFrames += src.renderedFrames;
|
||||||
|
dst.networkDroppedFrames += src.networkDroppedFrames;
|
||||||
|
dst.pacerDroppedFrames += src.pacerDroppedFrames;
|
||||||
|
dst.totalReassemblyTime += src.totalReassemblyTime;
|
||||||
|
dst.totalDecodeTime += src.totalDecodeTime;
|
||||||
|
dst.totalRenderTime += src.totalRenderTime;
|
||||||
|
|
||||||
|
Uint32 now = SDL_GetTicks();
|
||||||
|
|
||||||
|
// Initialize the measurement start point if this is the first video stat window
|
||||||
|
if (!dst.measurementStartTimestamp) {
|
||||||
|
dst.measurementStartTimestamp = src.measurementStartTimestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The following code assumes the global measure was already started first
|
||||||
|
SDL_assert(dst.measurementStartTimestamp <= src.measurementStartTimestamp);
|
||||||
|
|
||||||
|
dst.receivedFps = (float)dst.receivedFrames / ((float)(now - dst.measurementStartTimestamp) / 1000);
|
||||||
|
dst.decodedFps = (float)dst.decodedFrames / ((float)(now - dst.measurementStartTimestamp) / 1000);
|
||||||
|
dst.renderedFps = (float)dst.renderedFrames / ((float)(now - dst.measurementStartTimestamp) / 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
void FFmpegVideoDecoder::logVideoStats(VIDEO_STATS& stats, const char* title)
|
||||||
|
{
|
||||||
|
if (stats.renderedFps > 0 || stats.renderedFrames != 0) {
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"%s", title);
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"----------------------------------------------------------");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stats.renderedFps > 0) {
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Incoming frame rate from network: %.2f FPS",
|
||||||
|
stats.receivedFps);
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Decoding frame rate: %.2f FPS",
|
||||||
|
stats.decodedFps);
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Rendering frame rate: %.2f FPS",
|
||||||
|
stats.renderedFps);
|
||||||
|
}
|
||||||
|
if (stats.renderedFrames != 0) {
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Total frames received: %u",
|
||||||
|
stats.receivedFrames);
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Total frames decoded: %u",
|
||||||
|
stats.decodedFrames);
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Total frames rendered: %u",
|
||||||
|
stats.renderedFrames);
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Average reassembly time: %.2f ms",
|
||||||
|
(float)stats.totalReassemblyTime / stats.decodedFrames);
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Average decode time: %.2f ms",
|
||||||
|
(float)stats.totalDecodeTime / stats.decodedFrames);
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Average render time: %.2f ms",
|
||||||
|
(float)stats.totalRenderTime / stats.renderedFrames);
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Frames lost during network transmission: %.2f%%",
|
||||||
|
(float)stats.networkDroppedFrames / stats.decodedFrames);
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Frames dropped by frame pacing: %.2f%%",
|
||||||
|
(float)stats.pacerDroppedFrames / stats.decodedFrames);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
IFFmpegRenderer* FFmpegVideoDecoder::createAcceleratedRenderer(const AVCodecHWConfig* hwDecodeCfg)
|
IFFmpegRenderer* FFmpegVideoDecoder::createAcceleratedRenderer(const AVCodecHWConfig* hwDecodeCfg)
|
||||||
{
|
{
|
||||||
if (!(hwDecodeCfg->methods & AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX)) {
|
if (!(hwDecodeCfg->methods & AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX)) {
|
||||||
@@ -310,28 +439,107 @@ bool FFmpegVideoDecoder::initialize(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void FFmpegVideoDecoder::writeBuffer(PLENTRY entry, int& offset)
|
||||||
|
{
|
||||||
|
if (m_NeedsSpsFixup && entry->bufferType == BUFFER_TYPE_SPS) {
|
||||||
|
const char naluHeader[] = {0x00, 0x00, 0x00, 0x01};
|
||||||
|
h264_stream_t* stream = h264_new();
|
||||||
|
int nalStart, nalEnd;
|
||||||
|
|
||||||
|
// Read the old NALU
|
||||||
|
find_nal_unit((uint8_t*)entry->data, entry->length, &nalStart, &nalEnd);
|
||||||
|
read_nal_unit(stream,
|
||||||
|
(unsigned char *)&entry->data[nalStart],
|
||||||
|
nalEnd - nalStart);
|
||||||
|
|
||||||
|
SDL_assert(nalStart == sizeof(naluHeader));
|
||||||
|
SDL_assert(nalEnd == entry->length);
|
||||||
|
|
||||||
|
// Fixup the SPS to what OS X needs to use hardware acceleration
|
||||||
|
stream->sps->num_ref_frames = 1;
|
||||||
|
stream->sps->vui.max_dec_frame_buffering = 1;
|
||||||
|
|
||||||
|
int initialOffset = offset;
|
||||||
|
|
||||||
|
// Copy the modified NALU data. This assumes a 3 byte prefix and
|
||||||
|
// begins writing from the 2nd byte, so we must write the data
|
||||||
|
// first, then go back and write the Annex B prefix.
|
||||||
|
offset += write_nal_unit(stream, (uint8_t*)&m_DecodeBuffer.data()[initialOffset + 3],
|
||||||
|
MAX_SPS_EXTRA_SIZE + entry->length - sizeof(naluHeader));
|
||||||
|
|
||||||
|
// Copy the NALU prefix over from the original SPS
|
||||||
|
memcpy(&m_DecodeBuffer.data()[initialOffset], naluHeader, sizeof(naluHeader));
|
||||||
|
offset += sizeof(naluHeader);
|
||||||
|
|
||||||
|
h264_free(stream);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Write the buffer as-is
|
||||||
|
memcpy(&m_DecodeBuffer.data()[offset],
|
||||||
|
entry->data,
|
||||||
|
entry->length);
|
||||||
|
offset += entry->length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
int FFmpegVideoDecoder::submitDecodeUnit(PDECODE_UNIT du)
|
int FFmpegVideoDecoder::submitDecodeUnit(PDECODE_UNIT du)
|
||||||
{
|
{
|
||||||
PLENTRY entry = du->bufferList;
|
PLENTRY entry = du->bufferList;
|
||||||
int err;
|
int err;
|
||||||
|
|
||||||
if (du->fullLength + AV_INPUT_BUFFER_PADDING_SIZE > m_DecodeBuffer.length()) {
|
// Flip stats windows roughly every second
|
||||||
m_DecodeBuffer = QByteArray(du->fullLength + AV_INPUT_BUFFER_PADDING_SIZE, 0);
|
if (m_ActiveWndVideoStats.receivedFrames == m_StreamFps) {
|
||||||
|
#if 0
|
||||||
|
VIDEO_STATS lastTwoWndStats = {};
|
||||||
|
addVideoStats(m_LastWndVideoStats, lastTwoWndStats);
|
||||||
|
addVideoStats(m_ActiveWndVideoStats, lastTwoWndStats);
|
||||||
|
|
||||||
|
// Print stats from the last 2 windows
|
||||||
|
logVideoStats(lastTwoWndStats, "Periodic video stats");
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Accumulate these values into the global stats
|
||||||
|
addVideoStats(m_ActiveWndVideoStats, m_GlobalVideoStats);
|
||||||
|
|
||||||
|
// Move this window into the last window slot and clear it for next window
|
||||||
|
SDL_memcpy(&m_LastWndVideoStats, &m_ActiveWndVideoStats, sizeof(m_ActiveWndVideoStats));
|
||||||
|
SDL_zero(m_ActiveWndVideoStats);
|
||||||
|
m_ActiveWndVideoStats.measurementStartTimestamp = SDL_GetTicks();
|
||||||
|
}
|
||||||
|
|
||||||
|
m_ActiveWndVideoStats.receivedFrames++;
|
||||||
|
|
||||||
|
if (!m_LastFrameNumber) {
|
||||||
|
m_ActiveWndVideoStats.measurementStartTimestamp = SDL_GetTicks();
|
||||||
|
m_LastFrameNumber = du->frameNumber;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Any frame number greater than m_LastFrameNumber + 1 represents a dropped frame
|
||||||
|
m_ActiveWndVideoStats.networkDroppedFrames += du->frameNumber - (m_LastFrameNumber + 1);
|
||||||
|
m_LastFrameNumber = du->frameNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
int requiredBufferSize = du->fullLength;
|
||||||
|
if (du->frameType == FRAME_TYPE_IDR) {
|
||||||
|
// Add some extra space in case we need to do an SPS fixup
|
||||||
|
requiredBufferSize += MAX_SPS_EXTRA_SIZE;
|
||||||
|
}
|
||||||
|
if (requiredBufferSize + AV_INPUT_BUFFER_PADDING_SIZE > m_DecodeBuffer.length()) {
|
||||||
|
m_DecodeBuffer = QByteArray(requiredBufferSize + AV_INPUT_BUFFER_PADDING_SIZE, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
int offset = 0;
|
int offset = 0;
|
||||||
while (entry != nullptr) {
|
while (entry != nullptr) {
|
||||||
memcpy(&m_DecodeBuffer.data()[offset],
|
writeBuffer(entry, offset);
|
||||||
entry->data,
|
|
||||||
entry->length);
|
|
||||||
offset += entry->length;
|
|
||||||
entry = entry->next;
|
entry = entry->next;
|
||||||
}
|
}
|
||||||
|
|
||||||
SDL_assert(offset == du->fullLength);
|
|
||||||
|
|
||||||
m_Pkt.data = reinterpret_cast<uint8_t*>(m_DecodeBuffer.data());
|
m_Pkt.data = reinterpret_cast<uint8_t*>(m_DecodeBuffer.data());
|
||||||
m_Pkt.size = du->fullLength;
|
m_Pkt.size = offset;
|
||||||
|
|
||||||
|
m_ActiveWndVideoStats.totalReassemblyTime += LiGetMillis() - du->receiveTimeMs;
|
||||||
|
|
||||||
|
Uint32 beforeDecode = SDL_GetTicks();
|
||||||
|
|
||||||
err = avcodec_send_packet(m_VideoDecoderCtx, &m_Pkt);
|
err = avcodec_send_packet(m_VideoDecoderCtx, &m_Pkt);
|
||||||
if (err < 0) {
|
if (err < 0) {
|
||||||
@@ -372,6 +580,11 @@ int FFmpegVideoDecoder::submitDecodeUnit(PDECODE_UNIT du)
|
|||||||
// Queue the frame for rendering from the main thread
|
// Queue the frame for rendering from the main thread
|
||||||
SDL_AtomicIncRef(&m_QueuedFrames);
|
SDL_AtomicIncRef(&m_QueuedFrames);
|
||||||
queueFrame(frame);
|
queueFrame(frame);
|
||||||
|
|
||||||
|
// Count time in avcodec_send_packet() and avcodec_receive_frame()
|
||||||
|
// as time spent decoding
|
||||||
|
m_ActiveWndVideoStats.totalDecodeTime += SDL_GetTicks() - beforeDecode;
|
||||||
|
m_ActiveWndVideoStats.decodedFrames++;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
av_frame_free(&frame);
|
av_frame_free(&frame);
|
||||||
@@ -415,6 +628,10 @@ void FFmpegVideoDecoder::dropFrame(SDL_UserEvent* event)
|
|||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
av_frame_free(&frame);
|
av_frame_free(&frame);
|
||||||
|
|
||||||
|
// Since Pacer won't see this frame, we'll mark it as a drop
|
||||||
|
// on its behalf
|
||||||
|
m_ActiveWndVideoStats.pacerDroppedFrames++;
|
||||||
}
|
}
|
||||||
SDL_AtomicDecRef(&m_QueuedFrames);
|
SDL_AtomicDecRef(&m_QueuedFrames);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ public:
|
|||||||
int maxFps,
|
int maxFps,
|
||||||
bool enableVsync) override;
|
bool enableVsync) override;
|
||||||
virtual bool isHardwareAccelerated() override;
|
virtual bool isHardwareAccelerated() override;
|
||||||
|
virtual int getDecoderCapabilities() override;
|
||||||
virtual int submitDecodeUnit(PDECODE_UNIT du) override;
|
virtual int submitDecodeUnit(PDECODE_UNIT du) override;
|
||||||
virtual void renderFrame(SDL_UserEvent* event) override;
|
virtual void renderFrame(SDL_UserEvent* event) override;
|
||||||
virtual void dropFrame(SDL_UserEvent* event) override;
|
virtual void dropFrame(SDL_UserEvent* event) override;
|
||||||
@@ -31,10 +32,16 @@ private:
|
|||||||
int videoFormat, int width, int height,
|
int videoFormat, int width, int height,
|
||||||
int maxFps, bool enableVsync, bool testOnly);
|
int maxFps, bool enableVsync, bool testOnly);
|
||||||
|
|
||||||
|
void logVideoStats(VIDEO_STATS& stats, const char* title);
|
||||||
|
|
||||||
|
void addVideoStats(VIDEO_STATS& src, VIDEO_STATS& dst);
|
||||||
|
|
||||||
IFFmpegRenderer* createAcceleratedRenderer(const AVCodecHWConfig* hwDecodeCfg);
|
IFFmpegRenderer* createAcceleratedRenderer(const AVCodecHWConfig* hwDecodeCfg);
|
||||||
|
|
||||||
void reset();
|
void reset();
|
||||||
|
|
||||||
|
void writeBuffer(PLENTRY entry, int& offset);
|
||||||
|
|
||||||
static
|
static
|
||||||
enum AVPixelFormat ffGetFormat(AVCodecContext* context,
|
enum AVPixelFormat ffGetFormat(AVCodecContext* context,
|
||||||
const enum AVPixelFormat* pixFmts);
|
const enum AVPixelFormat* pixFmts);
|
||||||
@@ -47,6 +54,12 @@ private:
|
|||||||
SDL_atomic_t m_QueuedFrames;
|
SDL_atomic_t m_QueuedFrames;
|
||||||
int m_ConsecutiveFailedDecodes;
|
int m_ConsecutiveFailedDecodes;
|
||||||
Pacer* m_Pacer;
|
Pacer* m_Pacer;
|
||||||
|
VIDEO_STATS m_ActiveWndVideoStats;
|
||||||
|
VIDEO_STATS m_LastWndVideoStats;
|
||||||
|
VIDEO_STATS m_GlobalVideoStats;
|
||||||
|
int m_LastFrameNumber;
|
||||||
|
int m_StreamFps;
|
||||||
|
bool m_NeedsSpsFixup;
|
||||||
|
|
||||||
static const uint8_t k_H264TestFrame[];
|
static const uint8_t k_H264TestFrame[];
|
||||||
static const uint8_t k_HEVCTestFrame[];
|
static const uint8_t k_HEVCTestFrame[];
|
||||||
|
|||||||
@@ -25,6 +25,12 @@ SLVideoDecoder::isHardwareAccelerated()
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int
|
||||||
|
SLVideoDecoder::getDecoderCapabilities()
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
bool
|
bool
|
||||||
SLVideoDecoder::initialize(StreamingPreferences::VideoDecoderSelection vds,
|
SLVideoDecoder::initialize(StreamingPreferences::VideoDecoderSelection vds,
|
||||||
SDL_Window*,
|
SDL_Window*,
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ public:
|
|||||||
int frameRate,
|
int frameRate,
|
||||||
bool enableVsync);
|
bool enableVsync);
|
||||||
virtual bool isHardwareAccelerated();
|
virtual bool isHardwareAccelerated();
|
||||||
|
virtual int getDecoderCapabilities();
|
||||||
virtual int submitDecodeUnit(PDECODE_UNIT du);
|
virtual int submitDecodeUnit(PDECODE_UNIT du);
|
||||||
|
|
||||||
// Unused since rendering is done directly from the decode thread
|
// Unused since rendering is done directly from the decode thread
|
||||||
|
|||||||
Submodule
+1
Submodule h264bitstream/h264bitstream added at 34f3c58afa
@@ -0,0 +1,46 @@
|
|||||||
|
#-------------------------------------------------
|
||||||
|
#
|
||||||
|
# Project created by QtCreator 2018-10-12T15:50:59
|
||||||
|
#
|
||||||
|
#-------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
QT -= core gui
|
||||||
|
|
||||||
|
TARGET = h264bitstream
|
||||||
|
TEMPLATE = lib
|
||||||
|
|
||||||
|
# Support debug and release builds from command line for CI
|
||||||
|
CONFIG += debug_and_release
|
||||||
|
|
||||||
|
# Ensure symbols are always generated
|
||||||
|
CONFIG += force_debug_info
|
||||||
|
|
||||||
|
# Build a static library
|
||||||
|
CONFIG += staticlib
|
||||||
|
|
||||||
|
# Disable warnings
|
||||||
|
CONFIG += warn_off
|
||||||
|
|
||||||
|
# Older GCC versions defaulted to GNU89
|
||||||
|
*-g++ {
|
||||||
|
QMAKE_CFLAGS += -std=gnu99
|
||||||
|
}
|
||||||
|
|
||||||
|
SRC_DIR = $$PWD/h264bitstream
|
||||||
|
|
||||||
|
SOURCES += \
|
||||||
|
$$SRC_DIR/h264_avcc.c \
|
||||||
|
$$SRC_DIR/h264_nal.c \
|
||||||
|
$$SRC_DIR/h264_sei.c \
|
||||||
|
$$SRC_DIR/h264_slice_data.c \
|
||||||
|
$$SRC_DIR/h264_stream.c
|
||||||
|
|
||||||
|
HEADERS += \
|
||||||
|
$$SRC_DIR/bs.h \
|
||||||
|
$$SRC_DIR/h264_avcc.h \
|
||||||
|
$$SRC_DIR/h264_sei.h \
|
||||||
|
$$SRC_DIR/h264_slice_data.h \
|
||||||
|
$$SRC_DIR/h264_stream.h
|
||||||
|
|
||||||
|
INCLUDEPATH += $$INC_DIR
|
||||||
@@ -0,0 +1,271 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2010-2013 Nektra S.A., Buenos Aires, Argentina.
|
||||||
|
* All rights reserved. Contact: http://www.nektra.com
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* This file is part of Deviare In-Proc
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* Commercial License Usage
|
||||||
|
* ------------------------
|
||||||
|
* Licensees holding valid commercial Deviare In-Proc licenses may use this
|
||||||
|
* file in accordance with the commercial license agreement provided with the
|
||||||
|
* Software or, alternatively, in accordance with the terms contained in
|
||||||
|
* a written agreement between you and Nektra. For licensing terms and
|
||||||
|
* conditions see http://www.nektra.com/licensing/. For further information
|
||||||
|
* use the contact form at http://www.nektra.com/contact/.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* GNU General Public License Usage
|
||||||
|
* --------------------------------
|
||||||
|
* Alternatively, this file may be used under the terms of the GNU
|
||||||
|
* General Public License version 3.0 as published by the Free Software
|
||||||
|
* Foundation and appearing in the file LICENSE.GPL included in the
|
||||||
|
* packaging of this file. Please review the following information to
|
||||||
|
* ensure the GNU General Public License version 3.0 requirements will be
|
||||||
|
* met: http://www.gnu.org/copyleft/gpl.html.
|
||||||
|
*
|
||||||
|
**/
|
||||||
|
|
||||||
|
#ifndef _NKTHOOKLIB
|
||||||
|
#define _NKTHOOKLIB
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
//-----------------------------------------------------------
|
||||||
|
|
||||||
|
#define NKTHOOKLIB_DontSkipInitialJumps 0x0001
|
||||||
|
#define NKTHOOKLIB_DontRemoveOnUnhook 0x0002
|
||||||
|
#define NKTHOOKLIB_DontSkipAnyJumps 0x0004
|
||||||
|
#define NKTHOOKLIB_SkipNullProcsToHook 0x0008
|
||||||
|
#define NKTHOOKLIB_UseAbsoluteIndirectJumps 0x0010
|
||||||
|
#define NKTHOOKLIB_DisallowReentrancy 0x0020
|
||||||
|
#define NKTHOOKLIB_DontEnableHooks 0x0040
|
||||||
|
|
||||||
|
#define NKTHOOKLIB_ProcessPlatformX86 1
|
||||||
|
#define NKTHOOKLIB_ProcessPlatformX64 2
|
||||||
|
|
||||||
|
#define NKTHOOKLIB_CurrentProcess ((HANDLE)(LONG_PTR)-1)
|
||||||
|
#define NKTHOOKLIB_CurrentThread ((HANDLE)(LONG_PTR)-2)
|
||||||
|
|
||||||
|
//-----------------------------------------------------------
|
||||||
|
|
||||||
|
class CNktHookLib
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
typedef struct tagHOOK_INFO {
|
||||||
|
SIZE_T nHookId;
|
||||||
|
LPVOID lpProcToHook;
|
||||||
|
LPVOID lpNewProcAddr;
|
||||||
|
//----
|
||||||
|
LPVOID lpCallOriginal;
|
||||||
|
} HOOK_INFO, *LPHOOK_INFO;
|
||||||
|
|
||||||
|
CNktHookLib();
|
||||||
|
~CNktHookLib();
|
||||||
|
|
||||||
|
DWORD Hook(__out SIZE_T *lpnHookId, __out LPVOID *lplpCallOriginal, __in LPVOID lpProcToHook,
|
||||||
|
__in LPVOID lpNewProcAddr, __in DWORD dwFlags=0);
|
||||||
|
DWORD Hook(__inout HOOK_INFO aHookInfo[], __in SIZE_T nCount, __in DWORD dwFlags=0);
|
||||||
|
DWORD Hook(__inout LPHOOK_INFO aHookInfo[], __in SIZE_T nCount, __in DWORD dwFlags = 0);
|
||||||
|
|
||||||
|
DWORD RemoteHook(__out SIZE_T *lpnHookId, __out LPVOID *lplpCallOriginal, __in DWORD dwPid,
|
||||||
|
__in LPVOID lpProcToHook, __in LPVOID lpNewProcAddr, __in DWORD dwFlags);
|
||||||
|
DWORD RemoteHook(__inout HOOK_INFO aHookInfo[], __in SIZE_T nCount, __in DWORD dwPid, __in DWORD dwFlags);
|
||||||
|
DWORD RemoteHook(__inout LPHOOK_INFO aHookInfo[], __in SIZE_T nCount, __in DWORD dwPid, __in DWORD dwFlags);
|
||||||
|
|
||||||
|
DWORD RemoteHook(__out SIZE_T *lpnHookId, __out LPVOID *lplpCallOriginal, __in HANDLE hProcess,
|
||||||
|
__in LPVOID lpProcToHook, __in LPVOID lpNewProcAddr, __in DWORD dwFlags);
|
||||||
|
DWORD RemoteHook(__inout HOOK_INFO aHookInfo[], __in SIZE_T nCount, __in HANDLE hProcess, __in DWORD dwFlags);
|
||||||
|
DWORD RemoteHook(__inout LPHOOK_INFO aHookInfo[], __in SIZE_T nCount, __in HANDLE hProcess, __in DWORD dwFlags);
|
||||||
|
|
||||||
|
DWORD Unhook(__in SIZE_T nHookId);
|
||||||
|
DWORD Unhook(__in HOOK_INFO aHookInfo[], __in SIZE_T nCount);
|
||||||
|
DWORD Unhook(__in LPHOOK_INFO aHookInfo[], __in SIZE_T nCount);
|
||||||
|
VOID UnhookProcess(__in DWORD dwPid);
|
||||||
|
VOID UnhookAll();
|
||||||
|
|
||||||
|
//NOTE: The following 2 (two) methods will remove the hooks from the internal list of hooks but the original
|
||||||
|
// hook(s) will remain active.
|
||||||
|
DWORD RemoveHook(__in SIZE_T nHookId, BOOL bDisable);
|
||||||
|
DWORD RemoveHook(__in HOOK_INFO aHookInfo[], __in SIZE_T nCount, __in BOOL bDisable);
|
||||||
|
DWORD RemoveHook(__in LPHOOK_INFO aHookInfo[], __in SIZE_T nCount, __in BOOL bDisable);
|
||||||
|
|
||||||
|
DWORD EnableHook(__in SIZE_T nHookId, __in BOOL bEnable);
|
||||||
|
DWORD EnableHook(__in HOOK_INFO aHookInfo[], __in SIZE_T nCount, __in BOOL bEnable);
|
||||||
|
DWORD EnableHook(__in LPHOOK_INFO aHookInfo[], __in SIZE_T nCount, __in BOOL bEnable);
|
||||||
|
|
||||||
|
DWORD SetSuspendThreadsWhileHooking(__in BOOL bEnable);
|
||||||
|
BOOL GetSuspendThreadsWhileHooking();
|
||||||
|
|
||||||
|
DWORD SetEnableDebugOutput(__in BOOL bEnable);
|
||||||
|
BOOL GetEnableDebugOutput();
|
||||||
|
|
||||||
|
void* __cdecl operator new(__in size_t nSize);
|
||||||
|
void* __cdecl operator new[](__in size_t nSize);
|
||||||
|
void* __cdecl operator new(__in size_t nSize, __inout void* lpInPlace);
|
||||||
|
void __cdecl operator delete(__inout void* p);
|
||||||
|
void __cdecl operator delete[](__inout void* p);
|
||||||
|
#if _MSC_VER >= 1200
|
||||||
|
void __cdecl operator delete(__inout void* p, __inout void* lpPlace);
|
||||||
|
#endif //_MSC_VER >= 1200
|
||||||
|
|
||||||
|
private:
|
||||||
|
DWORD HookCommon(__in LPVOID lpInfo, __in SIZE_T nCount, __in DWORD dwPid, __in DWORD dwFlags);
|
||||||
|
DWORD UnhookCommon(__in LPVOID lpInfo, __in SIZE_T nCount, __in DWORD dwFlags);
|
||||||
|
DWORD RemoveHookCommon(__in LPVOID lpInfo, __in SIZE_T nCount, __in BOOL bDisable, __in DWORD dwFlags);
|
||||||
|
DWORD EnableHookCommon(__in LPVOID lpInfo, __in SIZE_T nCount, __in BOOL bEnable, __in DWORD dwFlags);
|
||||||
|
|
||||||
|
private:
|
||||||
|
LPVOID lpInternals;
|
||||||
|
};
|
||||||
|
|
||||||
|
//-----------------------------------------------------------
|
||||||
|
|
||||||
|
namespace NktHookLibHelpers {
|
||||||
|
|
||||||
|
//NOTE: See "BuildNtSysCalls" below
|
||||||
|
typedef struct tagSYSCALLDEF {
|
||||||
|
LPSTR szNtApiNameA;
|
||||||
|
SIZE_T nOffset;
|
||||||
|
} SYSCALLDEF, *LPSYSCALLDEF;
|
||||||
|
|
||||||
|
//--------------------------------
|
||||||
|
|
||||||
|
//NOTE: See "SetApiResolverCallback" below
|
||||||
|
typedef LPVOID (__stdcall *lpfnInternalApiResolver)(__in_z LPCSTR szApiNameA, __in LPVOID lpUserParam);
|
||||||
|
|
||||||
|
//--------------------------------
|
||||||
|
|
||||||
|
HINSTANCE GetModuleBaseAddress(__in_z LPCWSTR szDllNameW);
|
||||||
|
LPVOID GetProcedureAddress(__in HINSTANCE hDll, __in LPCSTR szProcNameA);
|
||||||
|
|
||||||
|
HINSTANCE GetRemoteModuleBaseAddress(__in HANDLE hProcess, __in_z LPCWSTR szDllNameW, __in BOOL bScanMappedImages);
|
||||||
|
LPVOID GetRemoteProcedureAddress(__in HANDLE hProcess, __in HINSTANCE hDll, __in_z LPCSTR szProcNameA);
|
||||||
|
|
||||||
|
//--------------------------------
|
||||||
|
|
||||||
|
int sprintf_s(__out_z char *lpDest, __in size_t nMaxCount, __in_z const char *szFormatA, ...);
|
||||||
|
int vsnprintf(__out_z char *lpDest, __in size_t nMaxCount, __in_z const char *szFormatA, __in va_list lpArgList);
|
||||||
|
|
||||||
|
//only on XP or later
|
||||||
|
int swprintf_s(__out_z wchar_t *lpDest, __in size_t nMaxCount, __in_z const wchar_t *szFormatW, ...);
|
||||||
|
int vsnwprintf(__out_z wchar_t *lpDest, __in size_t nMaxCount, __in_z const wchar_t *szFormatW, __in va_list lpArgList);
|
||||||
|
|
||||||
|
//--------------------------------
|
||||||
|
|
||||||
|
//Returns a PROCESSOR_ARCHITECTURE_xxx value or -1 on error.
|
||||||
|
LONG GetProcessorArchitecture();
|
||||||
|
|
||||||
|
HANDLE OpenProcess(__in DWORD dwDesiredAccess, __in BOOL bInheritHandle, __in DWORD dwProcessId);
|
||||||
|
HANDLE OpenThread(__in DWORD dwDesiredAccess, __in BOOL bInheritHandle, __in DWORD dwThreadId);
|
||||||
|
|
||||||
|
LONG GetProcessPlatform(__in HANDLE hProcess);
|
||||||
|
|
||||||
|
SIZE_T ReadMem(__in HANDLE hProcess, __out LPVOID lpDest, __in LPVOID lpSrc, __in SIZE_T nBytesCount);
|
||||||
|
BOOL WriteMem(__in HANDLE hProcess, __out LPVOID lpDest, __in LPVOID lpSrc, __in SIZE_T nBytesCount);
|
||||||
|
|
||||||
|
LONG GetThreadPriority(__in HANDLE hThread, __out int *lpnPriority);
|
||||||
|
LONG SetThreadPriority(__in HANDLE hThread, __in int nPriority);
|
||||||
|
|
||||||
|
DWORD GetCurrentThreadId();
|
||||||
|
DWORD GetCurrentProcessId();
|
||||||
|
|
||||||
|
HANDLE GetProcessHeap();
|
||||||
|
LPVOID MemAlloc(__in SIZE_T nSize);
|
||||||
|
VOID MemFree(__in LPVOID lpPtr);
|
||||||
|
|
||||||
|
VOID MemSet(__out void *lpDest, __in int nVal, __in SIZE_T nCount);
|
||||||
|
VOID MemCopy(__out void *lpDest, __in const void *lpSrc, __in SIZE_T nCount);
|
||||||
|
SIZE_T TryMemCopy(__out void *lpDest, __in const void *lpSrc, __in SIZE_T nCount);
|
||||||
|
VOID MemMove(__out void *lpDest, __in const void *lpSrc, __in SIZE_T nCount);
|
||||||
|
int MemCompare(__in const void *lpBuf1, __in const void *lpBuf2, __in SIZE_T nCount);
|
||||||
|
|
||||||
|
//--------------------------------
|
||||||
|
|
||||||
|
VOID DebugPrint(__in LPCSTR szFormatA, ...);
|
||||||
|
VOID DebugVPrint(__in LPCSTR szFormatA, __in va_list argptr);
|
||||||
|
|
||||||
|
//--------------------------------
|
||||||
|
|
||||||
|
SIZE_T GetInstructionLength(__in LPVOID lpAddr, __in SIZE_T nSize, __in BYTE nPlatformBits,
|
||||||
|
__out_opt BOOL *lpbIsMemOp=NULL, __out_z_opt LPSTR szBufA=NULL, __in SIZE_T nBufLen=0);
|
||||||
|
|
||||||
|
//NOTE: When NktHookLib is initialized, it tries to locate needed ntdll's apis by scanning process' modules.
|
||||||
|
// If you want to override an api call, use this method to set the resolver address. LPVOID returned by
|
||||||
|
// the callback must have the same definition and calling convention than the one in ntdll. Return NULL
|
||||||
|
// if you want NktHookLib to use the real ntdll api.
|
||||||
|
VOID SetInternalApiResolverCallback(__in lpfnInternalApiResolver fnInternalApiResolver, __in LPVOID lpUserParam);
|
||||||
|
|
||||||
|
//This function generates a relocatable byte code with a copy of the original SysCall routine for each passes
|
||||||
|
//ntdll api. The code is created based on the original ntdll.dll image file on disk located on System32 or SysWow64,
|
||||||
|
//depending on the target platform.
|
||||||
|
//
|
||||||
|
//Useful for doing direct calls to low level apis bypassing third party hooks (like Chrome navigator does). Also, you
|
||||||
|
//can pass the generated code to another process. You can generate the code with this method, pass it to another
|
||||||
|
//process and then use SetApiResolverCallback above.
|
||||||
|
//
|
||||||
|
//NOTE: Not all NtXXX apis are SysCalls. Trying to generate code for a non-syscall api may generate an unexpected
|
||||||
|
// behavior.
|
||||||
|
// If 'lpCode' is NULL, the needed space is returned. Although syscalls uses less than 32 bytes, a maximum of 256
|
||||||
|
// bytes are supported for each requested api. You can safety allocate a block of 256*nDefsCount bytes to hold
|
||||||
|
// the generated code.
|
||||||
|
DWORD BuildNtSysCalls(__in LPSYSCALLDEF lpDefs, __in SIZE_T nDefsCount, __in SIZE_T nPlatform,
|
||||||
|
__out_opt LPVOID lpCode, __out SIZE_T *lpnCodeSize);
|
||||||
|
|
||||||
|
//--------------------------------
|
||||||
|
|
||||||
|
//NOTE: Return 0xFFFFFFFF if remote thread is not accessible
|
||||||
|
DWORD GetWin32LastError(__in_opt HANDLE hThread=NULL);
|
||||||
|
BOOL SetWin32LastError(__in DWORD dwErrorCode, __in_opt HANDLE hThread=NULL);
|
||||||
|
|
||||||
|
//--------------------------------
|
||||||
|
|
||||||
|
BOOL GetOsVersion(__out_opt LPDWORD lpdwVerMajor=NULL, __out_opt LPDWORD lpdwVerMinor=NULL,
|
||||||
|
__out_opt LPDWORD lpdwBuildNumber=NULL);
|
||||||
|
|
||||||
|
//--------------------------------
|
||||||
|
|
||||||
|
//NOTE: CreateProcessWithDllW and related functions returns the Win32 error code directly. NOERROR => Success.
|
||||||
|
//
|
||||||
|
// If "szDllNameW" string ends with 'x86.dll', 'x64.dll', '32.dll', '64.dll', the dll name will be adjusted
|
||||||
|
// in order to match the process platform. I.e.: "mydll_x86.dll" will become "mydll_x64.dll" on 64-bit processes.
|
||||||
|
DWORD CreateProcessWithDllW(__in_z_opt LPCWSTR lpApplicationName, __inout_z_opt LPWSTR lpCommandLine,
|
||||||
|
__in_opt LPSECURITY_ATTRIBUTES lpProcessAttributes,
|
||||||
|
__in_opt LPSECURITY_ATTRIBUTES lpThreadAttributes, __in BOOL bInheritHandles,
|
||||||
|
__in DWORD dwCreationFlags, __in_z_opt LPCWSTR lpEnvironment,
|
||||||
|
__in_z_opt LPCWSTR lpCurrentDirectory, __in LPSTARTUPINFOW lpStartupInfo,
|
||||||
|
__out LPPROCESS_INFORMATION lpProcessInformation, __in_z LPCWSTR szDllNameW,
|
||||||
|
__in_opt HANDLE hSignalCompleted=NULL, __in_z_opt LPCSTR szInitFunctionA=NULL,
|
||||||
|
__in_opt LPVOID lpInitFuncParams=NULL, __in_opt ULONG nInitFuncParamsSize=0);
|
||||||
|
|
||||||
|
DWORD CreateProcessWithLogonAndDllW(__in_z LPCWSTR lpUsername, __in_z_opt LPCWSTR lpDomain, __in_z LPCWSTR lpPassword,
|
||||||
|
__in DWORD dwLogonFlags, __in_opt LPCWSTR lpApplicationName,
|
||||||
|
__inout_opt LPWSTR lpCommandLine, __in DWORD dwCreationFlags,
|
||||||
|
__in_z_opt LPCWSTR lpEnvironment, __in_z_opt LPCWSTR lpCurrentDirectory,
|
||||||
|
__in LPSTARTUPINFOW lpStartupInfo, __out LPPROCESS_INFORMATION lpProcessInformation,
|
||||||
|
__in_z LPCWSTR szDllNameW, __in_opt HANDLE hSignalCompleted=NULL,
|
||||||
|
__in_z_opt LPCSTR szInitFunctionA=NULL, __in_opt LPVOID lpInitFuncParams=NULL,
|
||||||
|
__in_opt ULONG nInitFuncParamsSize=0);
|
||||||
|
|
||||||
|
DWORD CreateProcessWithTokenAndDllW(__in HANDLE hToken, __in DWORD dwLogonFlags, __in_z_opt LPCWSTR lpApplicationName,
|
||||||
|
__inout_opt LPWSTR lpCommandLine, __in DWORD dwCreationFlags,
|
||||||
|
__in_z_opt LPCWSTR lpEnvironment, __in_z_opt LPCWSTR lpCurrentDirectory,
|
||||||
|
__in LPSTARTUPINFOW lpStartupInfo, __out LPPROCESS_INFORMATION lpProcessInformation,
|
||||||
|
__in_z LPCWSTR szDllNameW, __in_opt HANDLE hSignalCompleted=NULL,
|
||||||
|
__in_z_opt LPCSTR szInitFunctionA=NULL, __in_opt LPVOID lpInitFuncParams=NULL,
|
||||||
|
__in_opt ULONG nInitFuncParamsSize=0);
|
||||||
|
|
||||||
|
DWORD InjectDllByPidW(__in DWORD dwPid, __in_z LPCWSTR szDllNameW, __in_z_opt LPCSTR szInitFunctionA=NULL,
|
||||||
|
__in_opt DWORD dwProcessInitWaitTimeoutMs=5000, __out_opt LPHANDLE lphInjectorThread=NULL,
|
||||||
|
__in_opt LPVOID lpInitFuncParams=NULL, __in_opt ULONG nInitFuncParamsSize=0);
|
||||||
|
|
||||||
|
DWORD InjectDllByHandleW(__in HANDLE hProcess, __in_z LPCWSTR szDllNameW, __in_z_opt LPCSTR szInitFunctionA=NULL,
|
||||||
|
__in_opt DWORD dwProcessInitWaitTimeoutMs=5000, __out_opt LPHANDLE lphInjectorThread=NULL,
|
||||||
|
__in_opt LPVOID lpInitFuncParams=NULL, __in_opt ULONG nInitFuncParamsSize=0);
|
||||||
|
|
||||||
|
} //NktHookLibHelpers
|
||||||
|
|
||||||
|
//-----------------------------------------------------------
|
||||||
|
|
||||||
|
#endif //_NKTHOOKLIB
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Submodule moonlight-common-c/moonlight-common-c updated: feb46b978e...11ea2b1053
@@ -12,6 +12,9 @@ TEMPLATE = lib
|
|||||||
# Support debug and release builds from command line for CI
|
# Support debug and release builds from command line for CI
|
||||||
CONFIG += debug_and_release
|
CONFIG += debug_and_release
|
||||||
|
|
||||||
|
# Ensure symbols are always generated
|
||||||
|
CONFIG += force_debug_info
|
||||||
|
|
||||||
win32 {
|
win32 {
|
||||||
INCLUDEPATH += $$PWD/../libs/windows/include
|
INCLUDEPATH += $$PWD/../libs/windows/include
|
||||||
}
|
}
|
||||||
@@ -21,6 +24,7 @@ macx {
|
|||||||
unix:!macx {
|
unix:!macx {
|
||||||
CONFIG += link_pkgconfig
|
CONFIG += link_pkgconfig
|
||||||
PKGCONFIG += openssl
|
PKGCONFIG += openssl
|
||||||
|
DEFINES += HAVE_CLOCK_GETTIME=1
|
||||||
}
|
}
|
||||||
|
|
||||||
COMMON_C_DIR = $$PWD/moonlight-common-c
|
COMMON_C_DIR = $$PWD/moonlight-common-c
|
||||||
@@ -52,6 +56,7 @@ SOURCES += \
|
|||||||
$$COMMON_C_DIR/src/RtspConnection.c \
|
$$COMMON_C_DIR/src/RtspConnection.c \
|
||||||
$$COMMON_C_DIR/src/RtspParser.c \
|
$$COMMON_C_DIR/src/RtspParser.c \
|
||||||
$$COMMON_C_DIR/src/SdpGenerator.c \
|
$$COMMON_C_DIR/src/SdpGenerator.c \
|
||||||
|
$$COMMON_C_DIR/src/SimpleStun.c \
|
||||||
$$COMMON_C_DIR/src/VideoDepacketizer.c \
|
$$COMMON_C_DIR/src/VideoDepacketizer.c \
|
||||||
$$COMMON_C_DIR/src/VideoStream.c
|
$$COMMON_C_DIR/src/VideoStream.c
|
||||||
HEADERS += \
|
HEADERS += \
|
||||||
@@ -63,7 +68,7 @@ INCLUDEPATH += \
|
|||||||
CONFIG += warn_off staticlib
|
CONFIG += warn_off staticlib
|
||||||
DEFINES += HAS_SOCKLEN_T
|
DEFINES += HAS_SOCKLEN_T
|
||||||
|
|
||||||
debug {
|
CONFIG(debug, debug|release) {
|
||||||
# Enable asserts on debug builds
|
# Enable asserts on debug builds
|
||||||
DEFINES += LC_DEBUG
|
DEFINES += LC_DEBUG
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-2
@@ -2,10 +2,16 @@ TEMPLATE = subdirs
|
|||||||
SUBDIRS = \
|
SUBDIRS = \
|
||||||
moonlight-common-c \
|
moonlight-common-c \
|
||||||
qmdnsengine \
|
qmdnsengine \
|
||||||
app
|
app \
|
||||||
|
soundio \
|
||||||
|
h264bitstream
|
||||||
|
|
||||||
# Build the dependencies in parallel before the final app
|
# Build the dependencies in parallel before the final app
|
||||||
app.depends = qmdnsengine moonlight-common-c
|
app.depends = qmdnsengine moonlight-common-c soundio h264bitstream
|
||||||
|
win32 {
|
||||||
|
SUBDIRS += AntiHooking
|
||||||
|
app.depends += AntiHooking
|
||||||
|
}
|
||||||
|
|
||||||
# Support debug and release builds from command line for CI
|
# Support debug and release builds from command line for CI
|
||||||
CONFIG += debug_and_release
|
CONFIG += debug_and_release
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ TEMPLATE = lib
|
|||||||
# Support debug and release builds from command line for CI
|
# Support debug and release builds from command line for CI
|
||||||
CONFIG += debug_and_release
|
CONFIG += debug_and_release
|
||||||
|
|
||||||
|
# Ensure symbols are always generated
|
||||||
|
CONFIG += force_debug_info
|
||||||
|
|
||||||
QMDNSE_DIR = $$PWD/qmdnsengine/src
|
QMDNSE_DIR = $$PWD/qmdnsengine/src
|
||||||
DEFINES += \
|
DEFINES += \
|
||||||
QT_NO_SIGNALS_SLOTS_KEYWORDS
|
QT_NO_SIGNALS_SLOTS_KEYWORDS
|
||||||
|
|||||||
@@ -33,6 +33,12 @@ pushd $BUILD_FOLDER
|
|||||||
make $(echo "$BUILD_CONFIG" | tr '[:upper:]' '[:lower:]') || fail "Make failed!"
|
make $(echo "$BUILD_CONFIG" | tr '[:upper:]' '[:lower:]') || fail "Make failed!"
|
||||||
popd
|
popd
|
||||||
|
|
||||||
|
echo Saving dSYM file
|
||||||
|
pushd $BUILD_FOLDER
|
||||||
|
dsymutil app/Moonlight.app/Contents/MacOS/Moonlight -o Moonlight.dsym || fail "dSYM creation failed!"
|
||||||
|
cp -R Moonlight.dsym $INSTALLER_FOLDER || fail "dSYM copy failed!"
|
||||||
|
popd
|
||||||
|
|
||||||
echo Copying dylib dependencies
|
echo Copying dylib dependencies
|
||||||
mkdir $BUILD_FOLDER/app/Moonlight.app/Contents/lib
|
mkdir $BUILD_FOLDER/app/Moonlight.app/Contents/lib
|
||||||
cp $SOURCE_ROOT/libs/mac/lib/*.dylib $BUILD_FOLDER/app/Moonlight.app/Contents/lib/ || fail "Dylib copy failed!"
|
cp $SOURCE_ROOT/libs/mac/lib/*.dylib $BUILD_FOLDER/app/Moonlight.app/Contents/lib/ || fail "Dylib copy failed!"
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ if /I "%BUILD_CONFIG%"=="debug" (
|
|||||||
if /I "%BUILD_CONFIG%"=="signed-release" (
|
if /I "%BUILD_CONFIG%"=="signed-release" (
|
||||||
set BUILD_CONFIG=release
|
set BUILD_CONFIG=release
|
||||||
set SIGN=1
|
set SIGN=1
|
||||||
|
set MUST_DEPLOY_SYMBOLS=1
|
||||||
) else (
|
) else (
|
||||||
echo Invalid build configuration
|
echo Invalid build configuration
|
||||||
exit /b 1
|
exit /b 1
|
||||||
@@ -31,7 +32,6 @@ if /I "%ARCH%" NEQ "x86" (
|
|||||||
)
|
)
|
||||||
|
|
||||||
set VS_PATH=%ProgramFiles(x86)%\Microsoft Visual Studio\2017\Community
|
set VS_PATH=%ProgramFiles(x86)%\Microsoft Visual Studio\2017\Community
|
||||||
set VCREDIST_PATH=%VS_PATH%\VC\Redist\MSVC\14.14.26405
|
|
||||||
set SIGNTOOL_PARAMS=sign /tr http://timestamp.digicert.com /td sha256 /fd sha256 /sha1 1B3C676E831A94EC0327C3347EB32C68C26B3A67 /v
|
set SIGNTOOL_PARAMS=sign /tr http://timestamp.digicert.com /td sha256 /fd sha256 /sha1 1B3C676E831A94EC0327C3347EB32C68C26B3A67 /v
|
||||||
|
|
||||||
call "%VS_PATH%\VC\Auxiliary\Build\vcvarsall.bat" %ARCH%
|
call "%VS_PATH%\VC\Auxiliary\Build\vcvarsall.bat" %ARCH%
|
||||||
@@ -42,15 +42,18 @@ set SOURCE_ROOT=%cd%
|
|||||||
set BUILD_FOLDER=%BUILD_ROOT%\build-%ARCH%-%BUILD_CONFIG%
|
set BUILD_FOLDER=%BUILD_ROOT%\build-%ARCH%-%BUILD_CONFIG%
|
||||||
set DEPLOY_FOLDER=%BUILD_ROOT%\deploy-%ARCH%-%BUILD_CONFIG%
|
set DEPLOY_FOLDER=%BUILD_ROOT%\deploy-%ARCH%-%BUILD_CONFIG%
|
||||||
set INSTALLER_FOLDER=%BUILD_ROOT%\installer-%ARCH%-%BUILD_CONFIG%
|
set INSTALLER_FOLDER=%BUILD_ROOT%\installer-%ARCH%-%BUILD_CONFIG%
|
||||||
|
set SYMBOLS_FOLDER=%BUILD_ROOT%\symbols-%ARCH%-%BUILD_CONFIG%
|
||||||
|
|
||||||
echo Cleaning output directories
|
echo Cleaning output directories
|
||||||
rmdir /s /q %DEPLOY_FOLDER%
|
rmdir /s /q %DEPLOY_FOLDER%
|
||||||
rmdir /s /q %BUILD_FOLDER%
|
rmdir /s /q %BUILD_FOLDER%
|
||||||
rmdir /s /q %INSTALLER_FOLDER%
|
rmdir /s /q %INSTALLER_FOLDER%
|
||||||
|
rmdir /s /q %SYMBOLS_FOLDER%
|
||||||
mkdir %BUILD_ROOT%
|
mkdir %BUILD_ROOT%
|
||||||
mkdir %DEPLOY_FOLDER%
|
mkdir %DEPLOY_FOLDER%
|
||||||
mkdir %BUILD_FOLDER%
|
mkdir %BUILD_FOLDER%
|
||||||
mkdir %INSTALLER_FOLDER%
|
mkdir %INSTALLER_FOLDER%
|
||||||
|
mkdir %SYMBOLS_FOLDER%
|
||||||
|
|
||||||
echo Configuring the project
|
echo Configuring the project
|
||||||
pushd %BUILD_FOLDER%
|
pushd %BUILD_FOLDER%
|
||||||
@@ -64,10 +67,39 @@ nmake %BUILD_CONFIG%
|
|||||||
if !ERRORLEVEL! NEQ 0 goto Error
|
if !ERRORLEVEL! NEQ 0 goto Error
|
||||||
popd
|
popd
|
||||||
|
|
||||||
|
echo Saving PDBs
|
||||||
|
for /r "%BUILD_FOLDER%" %%f in (*.pdb) do (
|
||||||
|
copy "%%f" %SYMBOLS_FOLDER%
|
||||||
|
if !ERRORLEVEL! NEQ 0 goto Error
|
||||||
|
)
|
||||||
|
copy %SOURCE_ROOT%\libs\windows\lib\%ARCH%\*.pdb %SYMBOLS_FOLDER%
|
||||||
|
if !ERRORLEVEL! NEQ 0 goto Error
|
||||||
|
7z a %SYMBOLS_FOLDER%\MoonlightDebuggingSymbols.zip %SYMBOLS_FOLDER%\*.pdb
|
||||||
|
if !ERRORLEVEL! NEQ 0 goto Error
|
||||||
|
|
||||||
|
if "%ML_SYMBOL_STORE%" NEQ "" (
|
||||||
|
echo Publishing PDBs to symbol store: %ML_SYMBOL_STORE%
|
||||||
|
symstore add /f %SYMBOLS_FOLDER%\*.pdb /s %ML_SYMBOL_STORE% /t Moonlight
|
||||||
|
if !ERRORLEVEL! NEQ 0 goto Error
|
||||||
|
) else (
|
||||||
|
if "%MUST_DEPLOY_SYMBOLS%"=="1" (
|
||||||
|
echo "A symbol server must be specified in ML_SYMBOL_STORE for signed release builds"
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
echo Copying DLL dependencies
|
echo Copying DLL dependencies
|
||||||
copy %SOURCE_ROOT%\libs\windows\lib\%ARCH%\*.dll %DEPLOY_FOLDER%
|
copy %SOURCE_ROOT%\libs\windows\lib\%ARCH%\*.dll %DEPLOY_FOLDER%
|
||||||
if !ERRORLEVEL! NEQ 0 goto Error
|
if !ERRORLEVEL! NEQ 0 goto Error
|
||||||
|
|
||||||
|
echo Copying AntiHooking.dll
|
||||||
|
copy %BUILD_FOLDER%\AntiHooking\%BUILD_CONFIG%\AntiHooking.dll %DEPLOY_FOLDER%
|
||||||
|
if !ERRORLEVEL! NEQ 0 goto Error
|
||||||
|
|
||||||
|
echo Copying GC mapping list
|
||||||
|
copy %SOURCE_ROOT%\app\SDL_GameControllerDB\gamecontrollerdb.txt %DEPLOY_FOLDER%
|
||||||
|
if !ERRORLEVEL! NEQ 0 goto Error
|
||||||
|
|
||||||
echo Deploying Qt dependencies
|
echo Deploying Qt dependencies
|
||||||
windeployqt.exe --dir %DEPLOY_FOLDER% --%BUILD_CONFIG% --qmldir %SOURCE_ROOT%\app\gui --no-opengl-sw --no-compiler-runtime %BUILD_FOLDER%\app\%BUILD_CONFIG%\Moonlight.exe
|
windeployqt.exe --dir %DEPLOY_FOLDER% --%BUILD_CONFIG% --qmldir %SOURCE_ROOT%\app\gui --no-opengl-sw --no-compiler-runtime %BUILD_FOLDER%\app\%BUILD_CONFIG%\Moonlight.exe
|
||||||
if !ERRORLEVEL! NEQ 0 goto Error
|
if !ERRORLEVEL! NEQ 0 goto Error
|
||||||
@@ -82,10 +114,12 @@ if !ERRORLEVEL! NEQ 0 goto Error
|
|||||||
|
|
||||||
if "%SIGN%"=="1" (
|
if "%SIGN%"=="1" (
|
||||||
echo Signing deployed binaries
|
echo Signing deployed binaries
|
||||||
|
set FILES_TO_SIGN=
|
||||||
for /r "%DEPLOY_FOLDER%" %%f in (*.dll *.exe) do (
|
for /r "%DEPLOY_FOLDER%" %%f in (*.dll *.exe) do (
|
||||||
signtool %SIGNTOOL_PARAMS% %%f
|
set FILES_TO_SIGN=!FILES_TO_SIGN! %%f
|
||||||
if !ERRORLEVEL! NEQ 0 goto Error
|
|
||||||
)
|
)
|
||||||
|
signtool %SIGNTOOL_PARAMS% !FILES_TO_SIGN!
|
||||||
|
if !ERRORLEVEL! NEQ 0 goto Error
|
||||||
)
|
)
|
||||||
|
|
||||||
echo Building MSI
|
echo Building MSI
|
||||||
@@ -99,7 +133,7 @@ if "%SIGN%"=="1" (
|
|||||||
)
|
)
|
||||||
|
|
||||||
echo Building bundle
|
echo Building bundle
|
||||||
set VCREDIST_INSTALLER=%VCREDIST_PATH%\vcredist_%ARCH%.exe
|
set VCREDIST_INSTALLER=%VCToolsRedistDir%vcredist_%ARCH%.exe
|
||||||
rem Bundles are always x86 binaries
|
rem Bundles are always x86 binaries
|
||||||
msbuild %SOURCE_ROOT%\wix\MoonlightSetup\MoonlightSetup.wixproj /p:Configuration=%BUILD_CONFIG% /p:Platform=x86
|
msbuild %SOURCE_ROOT%\wix\MoonlightSetup\MoonlightSetup.wixproj /p:Configuration=%BUILD_CONFIG% /p:Platform=x86
|
||||||
if !ERRORLEVEL! NEQ 0 goto Error
|
if !ERRORLEVEL! NEQ 0 goto Error
|
||||||
@@ -119,7 +153,7 @@ if "%SIGN%"=="1" (
|
|||||||
echo Building portable package
|
echo Building portable package
|
||||||
rem This must be done after WiX harvesting and signing, since the VCRT dlls are MS signed
|
rem This must be done after WiX harvesting and signing, since the VCRT dlls are MS signed
|
||||||
rem and should not be harvested for inclusion in the full installer
|
rem and should not be harvested for inclusion in the full installer
|
||||||
copy "%VCREDIST_PATH%\%ARCH%\Microsoft.VC141.CRT\*.dll" %DEPLOY_FOLDER%
|
copy "%VCToolsRedistDir%%ARCH%\Microsoft.VC141.CRT\*.dll" %DEPLOY_FOLDER%
|
||||||
rem This file tells Moonlight that it's a portable installation
|
rem This file tells Moonlight that it's a portable installation
|
||||||
echo. > %DEPLOY_FOLDER%\portable.dat
|
echo. > %DEPLOY_FOLDER%\portable.dat
|
||||||
if !ERRORLEVEL! NEQ 0 goto Error
|
if !ERRORLEVEL! NEQ 0 goto Error
|
||||||
@@ -132,6 +166,7 @@ if /i "%APPVEYOR%"=="true" (
|
|||||||
if !ERRORLEVEL! NEQ 0 goto Error
|
if !ERRORLEVEL! NEQ 0 goto Error
|
||||||
appveyor PushArtifact %INSTALLER_FOLDER%\MoonlightPortable.zip -FileName MoonlightPortable-%ARCH%-%BUILD_CONFIG%.zip
|
appveyor PushArtifact %INSTALLER_FOLDER%\MoonlightPortable.zip -FileName MoonlightPortable-%ARCH%-%BUILD_CONFIG%.zip
|
||||||
if !ERRORLEVEL! NEQ 0 goto Error
|
if !ERRORLEVEL! NEQ 0 goto Error
|
||||||
|
appveyor PushArtifact %SYMBOLS_FOLDER%\MoonlightDebuggingSymbols.zip -FileName MoonlightDebuggingSymbols-%ARCH%-%BUILD_CONFIG%.zip
|
||||||
)
|
)
|
||||||
|
|
||||||
echo Build successful!
|
echo Build successful!
|
||||||
@@ -139,4 +174,4 @@ exit /b 0
|
|||||||
|
|
||||||
:Error
|
:Error
|
||||||
echo Build failed!
|
echo Build failed!
|
||||||
exit /b !ERRORLEVEL!
|
exit /b !ERRORLEVEL!
|
||||||
|
|||||||
Submodule
+1
Submodule soundio/libsoundio added at 5d6a7210fa
@@ -0,0 +1,111 @@
|
|||||||
|
#-------------------------------------------------
|
||||||
|
#
|
||||||
|
# Project created by QtCreator 2018-10-02T15:27:07
|
||||||
|
#
|
||||||
|
#-------------------------------------------------
|
||||||
|
|
||||||
|
QT -= core gui
|
||||||
|
|
||||||
|
TARGET = soundio
|
||||||
|
TEMPLATE = lib
|
||||||
|
|
||||||
|
# Support debug and release builds from command line for CI
|
||||||
|
CONFIG += debug_and_release
|
||||||
|
|
||||||
|
# Ensure symbols are always generated
|
||||||
|
CONFIG += force_debug_info
|
||||||
|
|
||||||
|
# Build a static library
|
||||||
|
CONFIG += staticlib
|
||||||
|
|
||||||
|
# Force MSVC to compile C as C++ for atomic support
|
||||||
|
*-msvc {
|
||||||
|
QMAKE_CFLAGS += /TP
|
||||||
|
}
|
||||||
|
|
||||||
|
# Older GCC versions defaulted to GNU89
|
||||||
|
*-g++ {
|
||||||
|
QMAKE_CFLAGS += -std=gnu99
|
||||||
|
}
|
||||||
|
|
||||||
|
# Disable warnings
|
||||||
|
CONFIG += warn_off
|
||||||
|
|
||||||
|
unix:!macx {
|
||||||
|
CONFIG += link_pkgconfig
|
||||||
|
|
||||||
|
packagesExist(libpulse) {
|
||||||
|
PKGCONFIG += libpulse
|
||||||
|
CONFIG += pulseaudio
|
||||||
|
}
|
||||||
|
packagesExist(alsa) {
|
||||||
|
PKGCONFIG += alsa
|
||||||
|
CONFIG += alsa
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CONFIG(release, debug|release) {
|
||||||
|
# Disable asserts on release builds
|
||||||
|
DEFINES += NDEBUG
|
||||||
|
}
|
||||||
|
|
||||||
|
DEFINES += \
|
||||||
|
SOUNDIO_STATIC_LIBRARY \
|
||||||
|
SOUNDIO_VERSION_MAJOR=1 \
|
||||||
|
SOUNDIO_VERSION_MINOR=1 \
|
||||||
|
SOUNDIO_VERSION_PATCH=0 \
|
||||||
|
SOUNDIO_VERSION_STRING=\\\"1.1.0\\\"
|
||||||
|
|
||||||
|
SRC_DIR = $$PWD/libsoundio/src
|
||||||
|
INC_DIR = $$PWD/libsoundio
|
||||||
|
|
||||||
|
SOURCES += \
|
||||||
|
$$SRC_DIR/channel_layout.c \
|
||||||
|
$$SRC_DIR/dummy.c \
|
||||||
|
$$SRC_DIR/os.c \
|
||||||
|
$$SRC_DIR/ring_buffer.c \
|
||||||
|
$$SRC_DIR/soundio.c \
|
||||||
|
$$SRC_DIR/util.c
|
||||||
|
|
||||||
|
HEADERS += \
|
||||||
|
$$SRC_DIR/atomics.h \
|
||||||
|
$$SRC_DIR/dummy.h \
|
||||||
|
$$SRC_DIR/list.h \
|
||||||
|
$$SRC_DIR/os.h \
|
||||||
|
$$SRC_DIR/ring_buffer.h \
|
||||||
|
$$SRC_DIR/soundio_internal.h \
|
||||||
|
$$SRC_DIR/soundio_private.h \
|
||||||
|
$$SRC_DIR/util.h \
|
||||||
|
$$INC_DIR/soundio/soundio.h \
|
||||||
|
$$INC_DIR/soundio/endian.h
|
||||||
|
|
||||||
|
INCLUDEPATH += $$INC_DIR
|
||||||
|
|
||||||
|
win32 {
|
||||||
|
message(WASAPI backend selected)
|
||||||
|
|
||||||
|
DEFINES += SOUNDIO_HAVE_WASAPI
|
||||||
|
SOURCES += $$SRC_DIR/wasapi.c
|
||||||
|
HEADERS += $$SRC_DIR/wasapi.h
|
||||||
|
}
|
||||||
|
macx {
|
||||||
|
message(CoreAudio backend selected)
|
||||||
|
|
||||||
|
DEFINES += SOUNDIO_HAVE_COREAUDIO
|
||||||
|
SOURCES += $$SRC_DIR/coreaudio.c
|
||||||
|
HEADERS += $$SRC_DIR/coreaudio.h
|
||||||
|
}
|
||||||
|
pulseaudio {
|
||||||
|
message(PulseAudio backend selected)
|
||||||
|
|
||||||
|
DEFINES += SOUNDIO_HAVE_PULSEAUDIO
|
||||||
|
SOURCES += $$SRC_DIR/pulseaudio.c
|
||||||
|
HEADERS += $$SRC_DIR/pulseaudio.h
|
||||||
|
}
|
||||||
|
alsa {
|
||||||
|
message(ALSA backend selected)
|
||||||
|
|
||||||
|
DEFINES += SOUNDIO_HAVE_ALSA
|
||||||
|
SOURCES += $$SRC_DIR/alsa.c
|
||||||
|
HEADERS += $$SRC_DIR/alsa.h
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
<?define FullName = "Moonlight Game Streaming Client" ?>
|
<?define FullName = "Moonlight Game Streaming Client" ?>
|
||||||
|
|
||||||
<?define ShortcutName = "$(var.ShortName)" ?>
|
<?define ShortcutName = "$(var.ShortName)" ?>
|
||||||
<?define ShortcutDesc = "Stream games from a NVIDIA GameStream-compatible PC" ?>
|
<?define ShortcutDesc = "Stream games from your NVIDIA GameStream-enabled PC" ?>
|
||||||
<?define InstallFolder = "Moonlight Game Streaming" ?>
|
<?define InstallFolder = "Moonlight Game Streaming" ?>
|
||||||
|
|
||||||
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi"
|
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi"
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
Name="$(var.FullName)"
|
Name="$(var.FullName)"
|
||||||
Language="1033"
|
Language="1033"
|
||||||
Version="!(bind.fileVersion.MoonlightExe)"
|
Version="!(bind.fileVersion.MoonlightExe)"
|
||||||
Manufacturer="Moonlight Game Streaming Team"
|
Manufacturer="Moonlight Game Streaming Project"
|
||||||
UpgradeCode="5c09f94e-f809-4c6a-9b7b-597c99f041fe">
|
UpgradeCode="5c09f94e-f809-4c6a-9b7b-597c99f041fe">
|
||||||
|
|
||||||
<Package InstallerVersion="500" Compressed="yes" InstallScope="perMachine" />
|
<Package InstallerVersion="500" Compressed="yes" InstallScope="perMachine" />
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
</Directory>
|
</Directory>
|
||||||
</Directory>
|
</Directory>
|
||||||
|
|
||||||
<Property Id="APPDATAFOLDER">%LOCALAPPDATA%\Moonlight Game Streaming</Property>
|
<Property Id="APPDATAFOLDER">%LOCALAPPDATA%\Moonlight Game Streaming Project</Property>
|
||||||
|
|
||||||
<!-- There's no way to delete a registry key on uninstall but not major upgrade, so
|
<!-- There's no way to delete a registry key on uninstall but not major upgrade, so
|
||||||
we have to roll our own deletion via custom action -->
|
we have to roll our own deletion via custom action -->
|
||||||
@@ -60,16 +60,38 @@
|
|||||||
Target="[#MoonlightExe]"
|
Target="[#MoonlightExe]"
|
||||||
Directory="ApplicationProgramsFolder"
|
Directory="ApplicationProgramsFolder"
|
||||||
WorkingDirectory="INSTALLFOLDER" />
|
WorkingDirectory="INSTALLFOLDER" />
|
||||||
|
<RemoveFolder Id="CleanupStartMenuShortcut" Directory="ApplicationProgramsFolder" On="uninstall" />
|
||||||
|
<util:RemoveFolderEx Id="CleanupAppDataFolder" On="uninstall" Property="APPDATAFOLDER" />
|
||||||
|
<RegistryValue Root="HKCU" Key="Software\Moonlight Game Streaming Project" Name="Installed" Type="integer" Value="1" KeyPath="yes" />
|
||||||
|
</Component>
|
||||||
|
|
||||||
|
<Component Id="MoonlightDesktopShortcut" Guid="*" Directory="INSTALLFOLDER">
|
||||||
<Shortcut Id="DesktopShortcut"
|
<Shortcut Id="DesktopShortcut"
|
||||||
Name="$(var.ShortcutName)"
|
Name="$(var.ShortcutName)"
|
||||||
Description="$(var.ShortcutDesc)"
|
Description="$(var.ShortcutDesc)"
|
||||||
Target="[#MoonlightExe]"
|
Target="[#MoonlightExe]"
|
||||||
Directory="DesktopFolder"
|
Directory="DesktopFolder"
|
||||||
WorkingDirectory="INSTALLFOLDER" />
|
WorkingDirectory="INSTALLFOLDER" />
|
||||||
<RemoveFolder Id="CleanupStartMenuShortcut" Directory="ApplicationProgramsFolder" On="uninstall" />
|
|
||||||
<RemoveFolder Id="CleanupDesktopShortcut" Directory="DesktopFolder" On="uninstall" />
|
<RemoveFolder Id="CleanupDesktopShortcut" Directory="DesktopFolder" On="uninstall" />
|
||||||
<util:RemoveFolderEx Id="CleanupAppDataFolder" On="uninstall" Property="APPDATAFOLDER" />
|
<RegistryValue Root="HKCU"
|
||||||
<RegistryValue Root="HKCU" Key="Software\Moonlight Game Streaming Project" Name="Installed" Type="integer" Value="1" KeyPath="yes" />
|
Key="Software\Moonlight Game Streaming Project"
|
||||||
|
Name="DesktopShortcutInstalled"
|
||||||
|
Type="integer"
|
||||||
|
Value="1"
|
||||||
|
KeyPath="yes" />
|
||||||
|
<Condition>ADDDESKTOPSHORTCUT=1</Condition>
|
||||||
|
</Component>
|
||||||
|
|
||||||
|
<!-- Persist desktop shortcut's installed state to let Bundle.wxs know if
|
||||||
|
the desktop shortcut should installed by default when upgrading the
|
||||||
|
product -->
|
||||||
|
<Component Id="MoonlightDesktopShortcutState" Guid="*" Directory="INSTALLFOLDER">
|
||||||
|
<RegistryValue Root="HKCU"
|
||||||
|
Key="Software\Moonlight Game Streaming Project"
|
||||||
|
Name="DesktopShortcutInstallState"
|
||||||
|
Type="integer"
|
||||||
|
Value="[ADDDESKTOPSHORTCUT]"
|
||||||
|
KeyPath="yes" />
|
||||||
</Component>
|
</Component>
|
||||||
|
|
||||||
<DirectoryRef Id="INSTALLFOLDER">
|
<DirectoryRef Id="INSTALLFOLDER">
|
||||||
@@ -85,6 +107,8 @@
|
|||||||
<Feature Id="ProductFeature" Title="Moonlight" Level="1" ConfigurableDirectory="INSTALLFOLDER">
|
<Feature Id="ProductFeature" Title="Moonlight" Level="1" ConfigurableDirectory="INSTALLFOLDER">
|
||||||
<ComponentRef Id="Moonlight" />
|
<ComponentRef Id="Moonlight" />
|
||||||
<ComponentRef Id="MoonlightShortcuts" />
|
<ComponentRef Id="MoonlightShortcuts" />
|
||||||
|
<ComponentRef Id="MoonlightDesktopShortcutState" />
|
||||||
|
<ComponentRef Id="MoonlightDesktopShortcut" />
|
||||||
<ComponentGroupRef Id="MoonlightDependencies" />
|
<ComponentGroupRef Id="MoonlightDependencies" />
|
||||||
</Feature>
|
</Feature>
|
||||||
</Product>
|
</Product>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
<Bundle Name="Moonlight Game Streaming Client"
|
<Bundle Name="Moonlight Game Streaming Client"
|
||||||
Version="!(bind.PackageVersion.Moonlight)"
|
Version="!(bind.PackageVersion.Moonlight)"
|
||||||
Manufacturer="Moonlight Game Streaming Team"
|
Manufacturer="Moonlight Game Streaming Project"
|
||||||
UpgradeCode="466fa35d-4be4-40ef-9ce5-afadc3b63bc5"
|
UpgradeCode="466fa35d-4be4-40ef-9ce5-afadc3b63bc5"
|
||||||
HelpUrl="https://github.com/moonlight-stream/moonlight-docs/wiki/Setup-Guide"
|
HelpUrl="https://github.com/moonlight-stream/moonlight-docs/wiki/Setup-Guide"
|
||||||
UpdateUrl="https://github.com/moonlight-stream/moonlight-qt/releases"
|
UpdateUrl="https://github.com/moonlight-stream/moonlight-qt/releases"
|
||||||
@@ -51,13 +51,32 @@
|
|||||||
<Variable Name="InstallFolder" Type="string" Value="[ProgramFilesFolder]Moonlight Game Streaming" />
|
<Variable Name="InstallFolder" Type="string" Value="[ProgramFilesFolder]Moonlight Game Streaming" />
|
||||||
<?endif ?>
|
<?endif ?>
|
||||||
|
|
||||||
|
<!-- Define "Add desktop shortcut" -checkbox's state by defining a variable
|
||||||
|
which has same name as the checkbox has. Value 1 means that checkbox
|
||||||
|
is checked, 0 means that is unchecked-->
|
||||||
|
<!-- Set checkbox's state as checked by default -->
|
||||||
|
<Variable Name="AddDesktopShortcutCheckbox" Type="numeric" Value="1" />
|
||||||
|
<!-- Get checkbox's state from registry if present. The registry value
|
||||||
|
"DesktopShortcutInstallState" is set in Product.wxs. -->
|
||||||
|
<util:RegistrySearch Variable="HasDesktopShortcutInstallStateRegKey"
|
||||||
|
Root="HKCU"
|
||||||
|
Key="Software\Moonlight Game Streaming Project"
|
||||||
|
Value="DesktopShortcutInstallState"
|
||||||
|
Result="exists" />
|
||||||
|
<util:RegistrySearch Variable="AddDesktopShortcutCheckbox"
|
||||||
|
Root="HKCU"
|
||||||
|
Key="Software\Moonlight Game Streaming Project"
|
||||||
|
Value="DesktopShortcutInstallState"
|
||||||
|
Condition="HasDesktopShortcutInstallStateRegKey" />
|
||||||
|
|
||||||
<BootstrapperApplicationRef Id="WixStandardBootstrapperApplication.RtfLicense">
|
<BootstrapperApplicationRef Id="WixStandardBootstrapperApplication.RtfLicense">
|
||||||
<bal:WixStandardBootstrapperApplication
|
<bal:WixStandardBootstrapperApplication
|
||||||
ShowVersion="yes"
|
ShowVersion="yes"
|
||||||
LicenseFile="license.rtf"
|
LicenseFile="license.rtf"
|
||||||
LogoFile="..\..\app\moonlight_wix.png"
|
LogoFile="..\..\app\moonlight_wix.png"
|
||||||
ShowFilesInUse="yes"
|
ShowFilesInUse="yes"
|
||||||
LaunchTarget="[InstallFolder]\Moonlight.exe" />
|
LaunchTarget="[InstallFolder]\Moonlight.exe"
|
||||||
|
ThemeFile="RtfTheme.xml" />
|
||||||
</BootstrapperApplicationRef>
|
</BootstrapperApplicationRef>
|
||||||
|
|
||||||
<Chain>
|
<Chain>
|
||||||
@@ -75,6 +94,7 @@
|
|||||||
<MsiPackage Id="Moonlight" SourceFile="$(var.Moonlight.TargetPath)" Vital="yes">
|
<MsiPackage Id="Moonlight" SourceFile="$(var.Moonlight.TargetPath)" Vital="yes">
|
||||||
<MsiProperty Name="INSTALLFOLDER" Value="[InstallFolder]" />
|
<MsiProperty Name="INSTALLFOLDER" Value="[InstallFolder]" />
|
||||||
<MsiProperty Name="REINSTALLMODE" Value="[REINSTALLMODE]" />
|
<MsiProperty Name="REINSTALLMODE" Value="[REINSTALLMODE]" />
|
||||||
|
<MsiProperty Name="ADDDESKTOPSHORTCUT" Value="[AddDesktopShortcutCheckbox]" />
|
||||||
</MsiPackage>
|
</MsiPackage>
|
||||||
</Chain>
|
</Chain>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. -->
|
||||||
|
<!-- This file has been copied from C:\Program Files (x86)\WiX Toolset v3.11\SDK\themes\RtfTheme.xml and then altered. -->
|
||||||
|
|
||||||
|
<Theme xmlns="http://wixtoolset.org/schemas/thmutil/2010">
|
||||||
|
<Window Width="485" Height="300" HexStyle="100a0000" FontId="0">#(loc.Caption)</Window>
|
||||||
|
<Font Id="0" Height="-12" Weight="500" Foreground="000000" Background="FFFFFF">Segoe UI</Font>
|
||||||
|
<Font Id="1" Height="-24" Weight="500" Foreground="000000">Segoe UI</Font>
|
||||||
|
<Font Id="2" Height="-22" Weight="500" Foreground="666666">Segoe UI</Font>
|
||||||
|
<Font Id="3" Height="-12" Weight="500" Foreground="000000" Background="FFFFFF">Segoe UI</Font>
|
||||||
|
<Font Id="4" Height="-12" Weight="500" Foreground="ff0000" Background="FFFFFF" Underline="yes">Segoe UI</Font>
|
||||||
|
|
||||||
|
<Image X="11" Y="11" Width="64" Height="64" ImageFile="logo.png" Visible="yes"/>
|
||||||
|
<Text X="80" Y="11" Width="-11" Height="64" FontId="1" Visible="yes" DisablePrefix="yes">#(loc.Title)</Text>
|
||||||
|
|
||||||
|
<Page Name="Help">
|
||||||
|
<Text X="11" Y="80" Width="-11" Height="30" FontId="2" DisablePrefix="yes">#(loc.HelpHeader)</Text>
|
||||||
|
<Text X="11" Y="112" Width="-11" Height="-35" FontId="3" DisablePrefix="yes">#(loc.HelpText)</Text>
|
||||||
|
<Button Name="HelpCancelButton" X="-11" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">#(loc.HelpCloseButton)</Button>
|
||||||
|
</Page>
|
||||||
|
<Page Name="Install">
|
||||||
|
<Richedit Name="EulaRichedit" X="11" Y="80" Width="-11" Height="-70" TabStop="yes" FontId="0" HexStyle="0x800000" />
|
||||||
|
<Checkbox Name="EulaAcceptCheckbox" X="-11" Y="-41" Width="260" Height="17" TabStop="yes" FontId="3" HideWhenDisabled="yes">#(loc.InstallAcceptCheckbox)</Checkbox>
|
||||||
|
<Button Name="OptionsButton" X="-171" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0" HideWhenDisabled="yes">#(loc.InstallOptionsButton)</Button>
|
||||||
|
<Button Name="InstallButton" X="-91" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">#(loc.InstallInstallButton)</Button>
|
||||||
|
<Button Name="WelcomeCancelButton" X="-11" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">#(loc.InstallCloseButton)</Button>
|
||||||
|
</Page>
|
||||||
|
<Page Name="Options">
|
||||||
|
<Text X="11" Y="80" Width="-11" Height="30" FontId="2" DisablePrefix="yes">#(loc.OptionsHeader)</Text>
|
||||||
|
<Text X="11" Y="121" Width="-11" Height="17" FontId="3" DisablePrefix="yes">#(loc.OptionsLocationLabel)</Text>
|
||||||
|
<Editbox Name="FolderEditbox" X="11" Y="143" Width="-91" Height="21" TabStop="yes" FontId="3" FileSystemAutoComplete="yes" />
|
||||||
|
<Button Name="BrowseButton" X="-11" Y="142" Width="75" Height="23" TabStop="yes" FontId="3">#(loc.OptionsBrowseButton)</Button>
|
||||||
|
|
||||||
|
<!-- Added a checkbox for user to choose if shortcut is placed on
|
||||||
|
desktop or not. Checkbox's current state (checked/unchecked) is
|
||||||
|
read from a variable which has same name as this checkbox. The
|
||||||
|
variable is defined in Bundle.wxs. -->
|
||||||
|
<Checkbox Name="AddDesktopShortcutCheckbox" X="11" Y="178" Width="250" Height="17" TabStop="yes" FontId="3" HideWhenDisabled="yes">Add desktop shortcut</Checkbox>
|
||||||
|
|
||||||
|
<Button Name="OptionsOkButton" X="-91" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">#(loc.OptionsOkButton)</Button>
|
||||||
|
<Button Name="OptionsCancelButton" X="-11" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">#(loc.OptionsCancelButton)</Button>
|
||||||
|
</Page>
|
||||||
|
<Page Name="FilesInUse">
|
||||||
|
<Text X="11" Y="80" Width="-11" Height="30" FontId="2" DisablePrefix="yes">#(loc.FilesInUseHeader)</Text>
|
||||||
|
<Text X="11" Y="121" Width="-11" Height="34" FontId="3" DisablePrefix="yes">#(loc.FilesInUseLabel)</Text>
|
||||||
|
<Text Name="FilesInUseText" X="11" Y="150" Width="-11" Height="-86" FontId="3" DisablePrefix="yes" HexStyle="0x0000C000"></Text>
|
||||||
|
|
||||||
|
<Button Name="FilesInUseCloseRadioButton" X="11" Y="-60" Width="-11" Height="23" TabStop="yes" FontId="0" HideWhenDisabled="yes" HexStyle="0x000009">#(loc.FilesInUseCloseRadioButton)</Button>
|
||||||
|
<Button Name="FilesInUseDontCloseRadioButton" X="11" Y="-40" Width="-11" Height="23" TabStop="yes" FontId="0" HideWhenDisabled="yes" HexStyle="0x000009">#(loc.FilesInUseDontCloseRadioButton)</Button>
|
||||||
|
|
||||||
|
<Button Name="FilesInUseOkButton" X="-91" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0" HideWhenDisabled="yes">#(loc.FilesInUseOkButton)</Button>
|
||||||
|
<Button Name="FilesInUseCancelButton" X="-11" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">#(loc.FilesInUseCancelButton)</Button>
|
||||||
|
</Page>
|
||||||
|
<Page Name="Progress">
|
||||||
|
<Text X="11" Y="80" Width="-11" Height="30" FontId="2" DisablePrefix="yes">#(loc.ProgressHeader)</Text>
|
||||||
|
<Text X="11" Y="121" Width="70" Height="17" FontId="3" DisablePrefix="yes">#(loc.ProgressLabel)</Text>
|
||||||
|
<Text Name="OverallProgressPackageText" X="85" Y="121" Width="-11" Height="17" FontId="3" DisablePrefix="yes">#(loc.OverallProgressPackageText)</Text>
|
||||||
|
<Progressbar Name="OverallCalculatedProgressbar" X="11" Y="143" Width="-11" Height="15" />
|
||||||
|
<Button Name="ProgressCancelButton" X="-11" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">#(loc.ProgressCancelButton)</Button>
|
||||||
|
</Page>
|
||||||
|
<Page Name="Modify">
|
||||||
|
<Text X="11" Y="80" Width="-11" Height="30" FontId="2" DisablePrefix="yes">#(loc.ModifyHeader)</Text>
|
||||||
|
<Button Name="RepairButton" X="-171" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0" HideWhenDisabled="yes">#(loc.ModifyRepairButton)</Button>
|
||||||
|
<Button Name="UninstallButton" X="-91" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">#(loc.ModifyUninstallButton)</Button>
|
||||||
|
<Button Name="ModifyCancelButton" X="-11" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">#(loc.ModifyCloseButton)</Button>
|
||||||
|
</Page>
|
||||||
|
<Page Name="Success">
|
||||||
|
<Text Name="SuccessHeader" X="11" Y="80" Width="-11" Height="30" FontId="2" HideWhenDisabled="yes" DisablePrefix="yes">#(loc.SuccessHeader)</Text>
|
||||||
|
<Text Name="SuccessInstallHeader" X="11" Y="80" Width="-11" Height="30" FontId="2" HideWhenDisabled="yes" DisablePrefix="yes">#(loc.SuccessInstallHeader)</Text>
|
||||||
|
<Text Name="SuccessRepairHeader" X="11" Y="80" Width="-11" Height="30" FontId="2" HideWhenDisabled="yes" DisablePrefix="yes">#(loc.SuccessRepairHeader)</Text>
|
||||||
|
<Text Name="SuccessUninstallHeader" X="11" Y="80" Width="-11" Height="30" FontId="2" HideWhenDisabled="yes" DisablePrefix="yes">#(loc.SuccessUninstallHeader)</Text>
|
||||||
|
<Button Name="LaunchButton" X="-91" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0" HideWhenDisabled="yes">#(loc.SuccessLaunchButton)</Button>
|
||||||
|
<Text Name="SuccessRestartText" X="-11" Y="-51" Width="400" Height="34" FontId="3" HideWhenDisabled="yes" DisablePrefix="yes">#(loc.SuccessRestartText)</Text>
|
||||||
|
<Button Name="SuccessRestartButton" X="-91" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0" HideWhenDisabled="yes">#(loc.SuccessRestartButton)</Button>
|
||||||
|
<Button Name="SuccessCancelButton" X="-11" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">#(loc.SuccessCloseButton)</Button>
|
||||||
|
</Page>
|
||||||
|
<Page Name="Failure">
|
||||||
|
<Text Name="FailureHeader" X="11" Y="80" Width="-11" Height="30" FontId="2" HideWhenDisabled="yes" DisablePrefix="yes">#(loc.FailureHeader)</Text>
|
||||||
|
<Text Name="FailureInstallHeader" X="11" Y="80" Width="-11" Height="30" FontId="2" HideWhenDisabled="yes" DisablePrefix="yes">#(loc.FailureInstallHeader)</Text>
|
||||||
|
<Text Name="FailureUninstallHeader" X="11" Y="80" Width="-11" Height="30" FontId="2" HideWhenDisabled="yes" DisablePrefix="yes">#(loc.FailureUninstallHeader)</Text>
|
||||||
|
<Text Name="FailureRepairHeader" X="11" Y="80" Width="-11" Height="30" FontId="2" HideWhenDisabled="yes" DisablePrefix="yes">#(loc.FailureRepairHeader)</Text>
|
||||||
|
<Hypertext Name="FailureLogFileLink" X="11" Y="121" Width="-11" Height="42" FontId="3" TabStop="yes" HideWhenDisabled="yes">#(loc.FailureHyperlinkLogText)</Hypertext>
|
||||||
|
<Hypertext Name="FailureMessageText" X="22" Y="163" Width="-11" Height="51" FontId="3" TabStop="yes" HideWhenDisabled="yes" />
|
||||||
|
<Text Name="FailureRestartText" X="-11" Y="-51" Width="400" Height="34" FontId="3" HideWhenDisabled="yes" DisablePrefix="yes">#(loc.FailureRestartText)</Text>
|
||||||
|
<Button Name="FailureRestartButton" X="-91" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0" HideWhenDisabled="yes">#(loc.FailureRestartButton)</Button>
|
||||||
|
<Button Name="FailureCloseButton" X="-11" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">#(loc.FailureCloseButton)</Button>
|
||||||
|
</Page>
|
||||||
|
</Theme>
|
||||||
Reference in New Issue
Block a user