mirror of
https://github.com/moonlight-stream/moonlight-qt.git
synced 2026-10-22 16:17:20 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d696536486 | ||
|
|
b46a3cf759 | ||
|
|
8a5e3cdb76 | ||
|
|
4fb39226ef | ||
|
|
c40174034c | ||
|
|
5029c855f0 | ||
|
|
2eee839eaa | ||
|
|
f42c7fec1e | ||
|
|
0ab07303c9 | ||
|
|
ad47990a87 | ||
|
|
1968461ce9 | ||
|
|
8aa3b5d3da | ||
|
|
23e77eefb0 | ||
|
|
094443cf26 | ||
|
|
b97758ef46 | ||
|
|
d1fe752028 | ||
|
|
b26df787cd | ||
|
|
4c5b254730 | ||
|
|
9a153b7170 | ||
|
|
065a6caee5 | ||
|
|
9c7df02941 | ||
|
|
c63c5100e2 | ||
|
|
bf05088d70 | ||
|
|
e7bb146f92 | ||
|
|
688c4a90d9 | ||
|
|
165ad96be1 | ||
|
|
115ced7ab4 | ||
|
|
ef15b5641a | ||
|
|
be50464619 | ||
|
|
d847b71c86 | ||
|
|
dfcc1ff899 | ||
|
|
65562937f3 | ||
|
|
7b5dc33a21 | ||
|
|
f64c13b9ec | ||
|
|
25f70e570e | ||
|
|
b27b64c25c | ||
|
|
bfc64c926a | ||
|
|
88ad983aa4 | ||
|
|
a95d2e76f5 | ||
|
|
037886ba5b | ||
|
|
011edfe2a0 | ||
|
|
1a5a6773ce | ||
|
|
dda22fd387 | ||
|
|
7254224347 | ||
|
|
789e52af57 | ||
|
|
b2fcd6c084 | ||
|
|
76e5794203 | ||
|
|
a81332dd23 | ||
|
|
e1fab22d98 | ||
|
|
cb5e58d465 | ||
|
|
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 |
No files matched your search
@@ -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/andrewrk/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();
|
||||||
|
|
||||||
@@ -1,11 +1,9 @@
|
|||||||
# Moonlight PC
|
# Moonlight PC
|
||||||
|
|
||||||
[Moonlight PC](http://moonlight-stream.com) is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield, but built to run on Windows, Mac, and Linux. This client is the successor to [Moonlight Chrome](https://github.com/moonlight-stream/moonlight-chrome) for streaming on PC.
|
[Moonlight PC](https://moonlight-stream.org) is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield, but built to run on Windows, Mac, and Linux. This client is the successor to [Moonlight Chrome](https://github.com/moonlight-stream/moonlight-chrome) for streaming on PC.
|
||||||
|
|
||||||
Moonlight also has mobile versions for [Android](https://github.com/moonlight-stream/moonlight-android) and [iOS](https://github.com/moonlight-stream/moonlight-ios).
|
Moonlight also has mobile versions for [Android](https://github.com/moonlight-stream/moonlight-android) and [iOS](https://github.com/moonlight-stream/moonlight-ios).
|
||||||
|
|
||||||
See the [releases page](https://github.com/moonlight-stream/moonlight-qt/releases) for download links for Windows and Mac. For Linux, we're on [Flathub](https://flathub.org/apps/details/com.moonlight_stream.Moonlight).
|
|
||||||
|
|
||||||
You can follow development on our [Discord server](https://discord.gg/6ERtzFY).
|
You can follow development on our [Discord server](https://discord.gg/6ERtzFY).
|
||||||
|
|
||||||
[](https://ci.appveyor.com/project/cgutman/moonlight-qt/branch/master)
|
[](https://ci.appveyor.com/project/cgutman/moonlight-qt/branch/master)
|
||||||
@@ -13,24 +11,50 @@ 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
|
||||||
|
|
||||||
|
## Downloads
|
||||||
|
- [Windows and macOS](https://github.com/moonlight-stream/moonlight-qt/releases)
|
||||||
|
- [Linux](https://flathub.org/apps/details/com.moonlight_stream.Moonlight)
|
||||||
|
|
||||||
## 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. Write code
|
2. Write code
|
||||||
3. Send Pull Requests
|
3. Send Pull Requests
|
||||||
|
|
||||||
Check out our [website](http://moonlight-stream.com) for project links and information.
|
Check out our [website](https://moonlight-stream.org) for project links and information.
|
||||||
+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.7.0</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>0.3.0</string>
|
<string>0.7.0</string>
|
||||||
<key>CFBundleDisplayName</key>
|
<key>CFBundleDisplayName</key>
|
||||||
<string>Moonlight</string>
|
<string>Moonlight</string>
|
||||||
</dict>
|
</dict>
|
||||||
|
|||||||
Submodule
+1
Submodule app/SDL_GameControllerDB added at 04ebce52ee.
+105
-20
@@ -11,6 +11,18 @@ 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(release, debug|release) {
|
||||||
|
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 +46,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
|
||||||
@@ -42,9 +54,17 @@ macx {
|
|||||||
}
|
}
|
||||||
|
|
||||||
unix:!macx {
|
unix:!macx {
|
||||||
CONFIG += link_pkgconfig
|
CONFIG += link_pkgconfig soundio
|
||||||
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 +86,68 @@ 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/computerseeker.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/quitstream.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/computerseeker.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/quitstream.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 +156,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 +199,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 +219,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 +257,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 +284,48 @@ 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
|
|
||||||
|
APP_BUNDLE_FRAMEWORKS.files = $$files(../libs/mac/Frameworks/*.framework, true)
|
||||||
|
APP_BUNDLE_FRAMEWORKS.path = Contents/Frameworks
|
||||||
|
|
||||||
|
QMAKE_BUNDLE_DATA += APP_BUNDLE_RESOURCES APP_BUNDLE_FRAMEWORKS
|
||||||
|
|
||||||
|
QMAKE_RPATHDIR += @executable_path/../Frameworks
|
||||||
}
|
}
|
||||||
|
|
||||||
VERSION = 0.3.0
|
VERSION = 0.7.0
|
||||||
DEFINES += VERSION_STR=\\\"0.3.0\\\"
|
DEFINES += VERSION_STR=\\\"0.7.0\\\"
|
||||||
@@ -12,6 +12,9 @@ AutoUpdateChecker::AutoUpdateChecker(QObject *parent) :
|
|||||||
// Never communicate over HTTP
|
// Never communicate over HTTP
|
||||||
m_Nam.setStrictTransportSecurityEnabled(true);
|
m_Nam.setStrictTransportSecurityEnabled(true);
|
||||||
|
|
||||||
|
// Allow HTTP redirects
|
||||||
|
m_Nam.setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy);
|
||||||
|
|
||||||
connect(&m_Nam, SIGNAL(finished(QNetworkReply*)),
|
connect(&m_Nam, SIGNAL(finished(QNetworkReply*)),
|
||||||
this, SLOT(handleUpdateCheckRequestFinished(QNetworkReply*)));
|
this, SLOT(handleUpdateCheckRequestFinished(QNetworkReply*)));
|
||||||
|
|
||||||
@@ -27,7 +30,7 @@ void AutoUpdateChecker::start()
|
|||||||
{
|
{
|
||||||
#if defined(Q_OS_WIN32) || defined(Q_OS_DARWIN) // Only run update checker on platforms without auto-update
|
#if defined(Q_OS_WIN32) || defined(Q_OS_DARWIN) // Only run update checker on platforms without auto-update
|
||||||
// We'll get a callback when this is finished
|
// We'll get a callback when this is finished
|
||||||
QUrl url("https://moonlight-stream.com/updates/qt.json");
|
QUrl url("https://moonlight-stream.org/updates/qt.json");
|
||||||
m_Nam.get(QNetworkRequest(url));
|
m_Nam.get(QNetworkRequest(url));
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
#include "computerseeker.h"
|
||||||
|
#include "computermanager.h"
|
||||||
|
#include <QTimer>
|
||||||
|
|
||||||
|
ComputerSeeker::ComputerSeeker(ComputerManager *manager, QString computerName, QObject *parent)
|
||||||
|
: QObject(parent), m_ComputerManager(manager), m_ComputerName(computerName),
|
||||||
|
m_TimeoutTimer(new QTimer(this))
|
||||||
|
{
|
||||||
|
m_TimeoutTimer->setSingleShot(true);
|
||||||
|
connect(m_TimeoutTimer, &QTimer::timeout,
|
||||||
|
this, &ComputerSeeker::onTimeout);
|
||||||
|
connect(m_ComputerManager, &ComputerManager::computerStateChanged,
|
||||||
|
this, &ComputerSeeker::onComputerUpdated);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ComputerSeeker::start(int timeout)
|
||||||
|
{
|
||||||
|
m_TimeoutTimer->start(timeout);
|
||||||
|
// 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ComputerSeeker::onComputerUpdated(NvComputer *computer)
|
||||||
|
{
|
||||||
|
if (!m_TimeoutTimer->isActive()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (matchComputer(computer) && isOnline(computer)) {
|
||||||
|
m_ComputerManager->stopPollingAsync();
|
||||||
|
m_TimeoutTimer->stop();
|
||||||
|
emit computerFound(computer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ComputerSeeker::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 ComputerSeeker::isOnline(NvComputer *computer) const
|
||||||
|
{
|
||||||
|
return computer->state == NvComputer::CS_ONLINE;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ComputerSeeker::onTimeout()
|
||||||
|
{
|
||||||
|
m_TimeoutTimer->stop();
|
||||||
|
m_ComputerManager->stopPollingAsync();
|
||||||
|
emit errorTimeout();
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
|
||||||
|
class ComputerManager;
|
||||||
|
class NvComputer;
|
||||||
|
class QTimer;
|
||||||
|
|
||||||
|
class ComputerSeeker : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
explicit ComputerSeeker(ComputerManager *manager, QString computerName, QObject *parent = nullptr);
|
||||||
|
|
||||||
|
void start(int timeout);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void computerFound(NvComputer *computer);
|
||||||
|
void errorTimeout();
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void onComputerUpdated(NvComputer *computer);
|
||||||
|
void onTimeout();
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool matchComputer(NvComputer *computer) const;
|
||||||
|
bool isOnline(NvComputer *computer) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
ComputerManager *m_ComputerManager;
|
||||||
|
QString m_ComputerName;
|
||||||
|
QTimer *m_TimeoutTimer;
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
|
};
|
||||||
+26
-14
@@ -10,6 +10,7 @@
|
|||||||
#include <QSslKey>
|
#include <QSslKey>
|
||||||
#include <QImageReader>
|
#include <QImageReader>
|
||||||
#include <QtEndian>
|
#include <QtEndian>
|
||||||
|
#include <QNetworkProxy>
|
||||||
|
|
||||||
#define REQUEST_TIMEOUT_MS 5000
|
#define REQUEST_TIMEOUT_MS 5000
|
||||||
|
|
||||||
@@ -24,14 +25,24 @@ NvHTTP::NvHTTP(QString address) :
|
|||||||
m_BaseUrlHttps.setHost(address);
|
m_BaseUrlHttps.setHost(address);
|
||||||
m_BaseUrlHttp.setPort(47989);
|
m_BaseUrlHttp.setPort(47989);
|
||||||
m_BaseUrlHttps.setPort(47984);
|
m_BaseUrlHttps.setPort(47984);
|
||||||
|
|
||||||
|
// Never use a proxy server
|
||||||
|
QNetworkProxy noProxy(QNetworkProxy::NoProxy);
|
||||||
|
m_Nam.setProxy(noProxy);
|
||||||
}
|
}
|
||||||
|
|
||||||
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 +69,7 @@ NvHTTP::getCurrentGame(QString serverInfo)
|
|||||||
}
|
}
|
||||||
|
|
||||||
QString
|
QString
|
||||||
NvHTTP::getServerInfo()
|
NvHTTP::getServerInfo(NvLogLevel logLevel)
|
||||||
{
|
{
|
||||||
QString serverInfo;
|
QString serverInfo;
|
||||||
|
|
||||||
@@ -70,7 +81,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 +94,7 @@ NvHTTP::getServerInfo()
|
|||||||
"serverinfo",
|
"serverinfo",
|
||||||
nullptr,
|
nullptr,
|
||||||
true,
|
true,
|
||||||
true);
|
logLevel);
|
||||||
verifyResponseStatus(serverInfo);
|
verifyResponseStatus(serverInfo);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -189,7 +200,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 +241,7 @@ NvHTTP::getAppList()
|
|||||||
"applist",
|
"applist",
|
||||||
nullptr,
|
nullptr,
|
||||||
true,
|
true,
|
||||||
true);
|
NvLogLevel::ERROR);
|
||||||
verifyResponseStatus(appxml);
|
verifyResponseStatus(appxml);
|
||||||
|
|
||||||
QXmlStreamReader xmlReader(appxml);
|
QXmlStreamReader xmlReader(appxml);
|
||||||
@@ -297,7 +308,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 +356,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 +374,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 +400,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 +408,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 +421,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,410 @@
|
|||||||
|
#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"
|
||||||
|
" quit Quit the currently running app\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 == "quit") {
|
||||||
|
return QuitRequested;
|
||||||
|
} else if (action == "stream") {
|
||||||
|
return StreamRequested;
|
||||||
|
} else {
|
||||||
|
parser.showError(QString("Invalid action: %1").arg(action));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QuitCommandLineParser::QuitCommandLineParser()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
QuitCommandLineParser::~QuitCommandLineParser()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void QuitCommandLineParser::parse(const QStringList &args)
|
||||||
|
{
|
||||||
|
CommandLineParser parser;
|
||||||
|
parser.setupCommonOptions();
|
||||||
|
parser.setApplicationDescription(
|
||||||
|
"\n"
|
||||||
|
"Quit the currently running app on the given host."
|
||||||
|
);
|
||||||
|
parser.addPositionalArgument("quit", "quit running app");
|
||||||
|
parser.addPositionalArgument("host", "Host computer name, UUID, or IP address", "<host>");
|
||||||
|
|
||||||
|
if (!parser.parse(args)) {
|
||||||
|
parser.showError(parser.errorText());
|
||||||
|
}
|
||||||
|
|
||||||
|
parser.handleUnknownOptions();
|
||||||
|
|
||||||
|
// This method will not return and terminates the process if --version or
|
||||||
|
// --help is specified
|
||||||
|
parser.handleHelpAndVersionOptions();
|
||||||
|
|
||||||
|
// Verify that host has been provided
|
||||||
|
auto posArgs = parser.positionalArguments();
|
||||||
|
if (posArgs.length() < 2) {
|
||||||
|
parser.showError("Host not provided");
|
||||||
|
}
|
||||||
|
m_Host = parser.positionalArguments().at(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString QuitCommandLineParser::getHost() const
|
||||||
|
{
|
||||||
|
return m_Host;
|
||||||
|
}
|
||||||
|
|
||||||
|
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("quit-after", "quit app after session");
|
||||||
|
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 --quit-after and --no-quit-after options
|
||||||
|
preferences->quitAppAfter = parser.getToggleOptionValue("quit-after", preferences->quitAppAfter);
|
||||||
|
|
||||||
|
// 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,56 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "settings/streamingpreferences.h"
|
||||||
|
|
||||||
|
#include <QMap>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
class GlobalCommandLineParser
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
enum ParseResult {
|
||||||
|
NormalStartRequested,
|
||||||
|
StreamRequested,
|
||||||
|
QuitRequested,
|
||||||
|
};
|
||||||
|
|
||||||
|
GlobalCommandLineParser();
|
||||||
|
virtual ~GlobalCommandLineParser();
|
||||||
|
|
||||||
|
ParseResult parse(const QStringList &args);
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
class QuitCommandLineParser
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
QuitCommandLineParser();
|
||||||
|
virtual ~QuitCommandLineParser();
|
||||||
|
|
||||||
|
void parse(const QStringList &args);
|
||||||
|
|
||||||
|
QString getHost() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
QString m_Host;
|
||||||
|
};
|
||||||
|
|
||||||
|
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,171 @@
|
|||||||
|
#include "quitstream.h"
|
||||||
|
|
||||||
|
#include "backend/computermanager.h"
|
||||||
|
#include "backend/computerseeker.h"
|
||||||
|
#include "streaming/session.h"
|
||||||
|
|
||||||
|
#include <QCoreApplication>
|
||||||
|
#include <QTimer>
|
||||||
|
|
||||||
|
#define COMPUTER_SEEK_TIMEOUT 10000
|
||||||
|
|
||||||
|
namespace CliQuitStream
|
||||||
|
{
|
||||||
|
|
||||||
|
enum State {
|
||||||
|
StateInit,
|
||||||
|
StateSeekComputer,
|
||||||
|
StateQuitApp,
|
||||||
|
StateFailure,
|
||||||
|
};
|
||||||
|
|
||||||
|
class Event
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
enum Type {
|
||||||
|
AppQuitCompleted,
|
||||||
|
ComputerFound,
|
||||||
|
ComputerSeekTimedout,
|
||||||
|
Executed,
|
||||||
|
};
|
||||||
|
|
||||||
|
Event(Type type)
|
||||||
|
: type(type), computerManager(nullptr), computer(nullptr) {}
|
||||||
|
|
||||||
|
Type type;
|
||||||
|
ComputerManager *computerManager;
|
||||||
|
NvComputer *computer;
|
||||||
|
QString errorMessage;
|
||||||
|
};
|
||||||
|
|
||||||
|
class LauncherPrivate
|
||||||
|
{
|
||||||
|
Q_DECLARE_PUBLIC(Launcher)
|
||||||
|
|
||||||
|
public:
|
||||||
|
LauncherPrivate(Launcher *q) : q_ptr(q) {}
|
||||||
|
|
||||||
|
void handleEvent(Event event)
|
||||||
|
{
|
||||||
|
Q_Q(Launcher);
|
||||||
|
NvApp app;
|
||||||
|
|
||||||
|
switch (event.type) {
|
||||||
|
// Occurs when CliQuitStreamSegue becomes visible and the UI calls launcher's execute()
|
||||||
|
case Event::Executed:
|
||||||
|
if (m_State == StateInit) {
|
||||||
|
m_State = StateSeekComputer;
|
||||||
|
m_ComputerManager = event.computerManager;
|
||||||
|
q->connect(m_ComputerManager, &ComputerManager::quitAppCompleted,
|
||||||
|
q, &Launcher::onQuitAppCompleted);
|
||||||
|
|
||||||
|
m_ComputerSeeker = new ComputerSeeker(m_ComputerManager, m_ComputerName, q);
|
||||||
|
q->connect(m_ComputerSeeker, &ComputerSeeker::computerFound,
|
||||||
|
q, &Launcher::onComputerFound);
|
||||||
|
q->connect(m_ComputerSeeker, &ComputerSeeker::errorTimeout,
|
||||||
|
q, &Launcher::onComputerSeekTimeout);
|
||||||
|
m_ComputerSeeker->start(COMPUTER_SEEK_TIMEOUT);
|
||||||
|
|
||||||
|
emit q->searchingComputer();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
// Occurs when computer search timed out
|
||||||
|
case Event::ComputerSeekTimedout:
|
||||||
|
if (m_State == StateSeekComputer) {
|
||||||
|
m_State = StateFailure;
|
||||||
|
emit q->failed(QString("Failed to connect to %1").arg(m_ComputerName));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
// Occurs when searched computer is found
|
||||||
|
case Event::ComputerFound:
|
||||||
|
if (m_State == StateSeekComputer) {
|
||||||
|
if (event.computer->pairState == NvComputer::PS_PAIRED) {
|
||||||
|
m_State = StateQuitApp;
|
||||||
|
emit q->quittingApp();
|
||||||
|
m_ComputerManager->quitRunningApp(event.computer);
|
||||||
|
} 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
// Occurs when app quit completed (error message is set if failed)
|
||||||
|
case Event::AppQuitCompleted:
|
||||||
|
if (m_State == StateQuitApp) {
|
||||||
|
if (event.errorMessage.isEmpty()) {
|
||||||
|
QCoreApplication::exit(0);
|
||||||
|
} else {
|
||||||
|
m_State = StateFailure;
|
||||||
|
emit q->failed(QString("Quitting app failed, reason: %1").arg(event.errorMessage));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Launcher *q_ptr;
|
||||||
|
ComputerManager *m_ComputerManager;
|
||||||
|
QString m_ComputerName;
|
||||||
|
ComputerSeeker *m_ComputerSeeker;
|
||||||
|
State m_State;
|
||||||
|
QTimer *m_TimeoutTimer;
|
||||||
|
};
|
||||||
|
|
||||||
|
Launcher::Launcher(QString computer, QObject *parent)
|
||||||
|
: QObject(parent),
|
||||||
|
m_DPtr(new LauncherPrivate(this))
|
||||||
|
{
|
||||||
|
Q_D(Launcher);
|
||||||
|
d->m_ComputerName = computer;
|
||||||
|
d->m_State = StateInit;
|
||||||
|
d->m_TimeoutTimer = new QTimer(this);
|
||||||
|
d->m_TimeoutTimer->setSingleShot(true);
|
||||||
|
connect(d->m_TimeoutTimer, &QTimer::timeout,
|
||||||
|
this, &Launcher::onComputerSeekTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
Launcher::~Launcher()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void Launcher::execute(ComputerManager *manager)
|
||||||
|
{
|
||||||
|
Q_D(Launcher);
|
||||||
|
Event event(Event::Executed);
|
||||||
|
event.computerManager = manager;
|
||||||
|
d->handleEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Launcher::isExecuted() const
|
||||||
|
{
|
||||||
|
Q_D(const Launcher);
|
||||||
|
return d->m_State != StateInit;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Launcher::onComputerFound(NvComputer *computer)
|
||||||
|
{
|
||||||
|
Q_D(Launcher);
|
||||||
|
Event event(Event::ComputerFound);
|
||||||
|
event.computer = computer;
|
||||||
|
d->handleEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Launcher::onComputerSeekTimeout()
|
||||||
|
{
|
||||||
|
Q_D(Launcher);
|
||||||
|
Event event(Event::ComputerSeekTimedout);
|
||||||
|
d->handleEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Launcher::onQuitAppCompleted(QVariant error)
|
||||||
|
{
|
||||||
|
Q_D(Launcher);
|
||||||
|
Event event(Event::AppQuitCompleted);
|
||||||
|
event.errorMessage = error.toString();
|
||||||
|
d->handleEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QVariant>
|
||||||
|
|
||||||
|
class ComputerManager;
|
||||||
|
class NvComputer;
|
||||||
|
|
||||||
|
namespace CliQuitStream
|
||||||
|
{
|
||||||
|
|
||||||
|
class LauncherPrivate;
|
||||||
|
|
||||||
|
class Launcher : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
Q_DECLARE_PRIVATE_D(m_DPtr, Launcher)
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit Launcher(QString computer, QObject *parent = nullptr);
|
||||||
|
~Launcher();
|
||||||
|
|
||||||
|
Q_INVOKABLE void execute(ComputerManager *manager);
|
||||||
|
Q_INVOKABLE bool isExecuted() const;
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void searchingComputer();
|
||||||
|
void quittingApp();
|
||||||
|
void failed(QString text);
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void onComputerFound(NvComputer *computer);
|
||||||
|
void onComputerSeekTimeout();
|
||||||
|
void onQuitAppCompleted(QVariant error);
|
||||||
|
|
||||||
|
private:
|
||||||
|
QScopedPointer<LauncherPrivate> m_DPtr;
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,256 @@
|
|||||||
|
#include "startstream.h"
|
||||||
|
#include "backend/computermanager.h"
|
||||||
|
#include "backend/computerseeker.h"
|
||||||
|
#include "streaming/session.h"
|
||||||
|
|
||||||
|
#include <QCoreApplication>
|
||||||
|
#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 {
|
||||||
|
AppQuitCompleted,
|
||||||
|
AppQuitRequested,
|
||||||
|
ComputerFound,
|
||||||
|
ComputerUpdated,
|
||||||
|
Executed,
|
||||||
|
Timedout,
|
||||||
|
};
|
||||||
|
|
||||||
|
Event(Type type)
|
||||||
|
: type(type), computerManager(nullptr), computer(nullptr) {}
|
||||||
|
|
||||||
|
Type type;
|
||||||
|
ComputerManager *computerManager;
|
||||||
|
NvComputer *computer;
|
||||||
|
QString errorMessage;
|
||||||
|
};
|
||||||
|
|
||||||
|
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) {
|
||||||
|
// Occurs when CliStartStreamSegue becomes visible and the UI calls launcher's execute()
|
||||||
|
case Event::Executed:
|
||||||
|
if (m_State == StateInit) {
|
||||||
|
m_State = StateSeekComputer;
|
||||||
|
m_ComputerManager = event.computerManager;
|
||||||
|
|
||||||
|
m_ComputerSeeker = new ComputerSeeker(m_ComputerManager, m_ComputerName, q);
|
||||||
|
q->connect(m_ComputerSeeker, &ComputerSeeker::computerFound,
|
||||||
|
q, &Launcher::onComputerFound);
|
||||||
|
q->connect(m_ComputerSeeker, &ComputerSeeker::errorTimeout,
|
||||||
|
q, &Launcher::onTimeout);
|
||||||
|
m_ComputerSeeker->start(COMPUTER_SEEK_TIMEOUT);
|
||||||
|
|
||||||
|
q->connect(m_ComputerManager, &ComputerManager::computerStateChanged,
|
||||||
|
q, &Launcher::onComputerUpdated);
|
||||||
|
q->connect(m_ComputerManager, &ComputerManager::quitAppCompleted,
|
||||||
|
q, &Launcher::onQuitAppCompleted);
|
||||||
|
|
||||||
|
emit q->searchingComputer();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
// Occurs when searched computer is found
|
||||||
|
case Event::ComputerFound:
|
||||||
|
if (m_State == StateSeekComputer) {
|
||||||
|
if (event.computer->pairState == NvComputer::PS_PAIRED) {
|
||||||
|
m_State = StateSeekApp;
|
||||||
|
m_Computer = event.computer;
|
||||||
|
m_TimeoutTimer->start(APP_SEEK_TIMEOUT);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
// Occurs when a computer is updated
|
||||||
|
case Event::ComputerUpdated:
|
||||||
|
if (m_State == StateSeekApp) {
|
||||||
|
int index = getAppIndex();
|
||||||
|
if (-1 != index) {
|
||||||
|
app = m_Computer->appList[index];
|
||||||
|
m_TimeoutTimer->stop();
|
||||||
|
if (isNotStreaming() || isStreamingApp(app)) {
|
||||||
|
m_State = StateStartSession;
|
||||||
|
session = new Session(m_Computer, app, m_Preferences);
|
||||||
|
emit q->sessionCreated(app.name, session);
|
||||||
|
} else {
|
||||||
|
emit q->appQuitRequired(getCurrentAppName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
// Occurs when there was another app running on computer and user accepted quit
|
||||||
|
// confirmation dialog
|
||||||
|
case Event::AppQuitRequested:
|
||||||
|
if (m_State == StateSeekApp) {
|
||||||
|
m_ComputerManager->quitRunningApp(m_Computer);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
// Occurs when the previous app quit has been completed, handles quitting errors if any
|
||||||
|
// happended. ComputerUpdated event's handler handles session start when previous app has
|
||||||
|
// quit.
|
||||||
|
case Event::AppQuitCompleted:
|
||||||
|
if (m_State == StateSeekApp && !event.errorMessage.isEmpty()) {
|
||||||
|
m_State = StateFailure;
|
||||||
|
emit q->failed(QString("Quitting app failed, reason: %1").arg(event.errorMessage));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
// Occurs when computer or app search timed out
|
||||||
|
case Event::Timedout:
|
||||||
|
if (m_State == StateSeekComputer) {
|
||||||
|
m_State = StateFailure;
|
||||||
|
emit q->failed(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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
ComputerSeeker *m_ComputerSeeker;
|
||||||
|
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::quitRunningApp()
|
||||||
|
{
|
||||||
|
Q_D(Launcher);
|
||||||
|
Event event(Event::AppQuitRequested);
|
||||||
|
d->handleEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Launcher::isExecuted() const
|
||||||
|
{
|
||||||
|
Q_D(const Launcher);
|
||||||
|
return d->m_State != StateInit;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Launcher::onComputerFound(NvComputer *computer)
|
||||||
|
{
|
||||||
|
Q_D(Launcher);
|
||||||
|
Event event(Event::ComputerFound);
|
||||||
|
event.computer = computer;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Launcher::onQuitAppCompleted(QVariant error)
|
||||||
|
{
|
||||||
|
Q_D(Launcher);
|
||||||
|
Event event(Event::AppQuitCompleted);
|
||||||
|
event.errorMessage = error.toString();
|
||||||
|
d->handleEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QVariant>
|
||||||
|
|
||||||
|
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);
|
||||||
|
Q_INVOKABLE void quitRunningApp();
|
||||||
|
Q_INVOKABLE bool isExecuted() const;
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void searchingComputer();
|
||||||
|
void searchingApp();
|
||||||
|
void sessionCreated(QString appName, Session *session);
|
||||||
|
void failed(QString text);
|
||||||
|
void appQuitRequired(QString appName);
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void onComputerFound(NvComputer *computer);
|
||||||
|
void onComputerUpdated(NvComputer *computer);
|
||||||
|
void onTimeout();
|
||||||
|
void onQuitAppCompleted(QVariant error);
|
||||||
|
|
||||||
|
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>
|
||||||
@@ -19,20 +19,154 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</description>
|
</description>
|
||||||
|
|
||||||
<url type="homepage">https://moonlight-stream.com</url>
|
<url type="homepage">https://moonlight-stream.org</url>
|
||||||
<url type="bugtracker">https://github.com/moonlight-stream/moonlight-qt/issues</url>
|
<url type="bugtracker">https://github.com/moonlight-stream/moonlight-qt/issues</url>
|
||||||
<url type="help">https://github.com/moonlight-stream/moonlight-docs/wiki/Setup-Guide</url>
|
<url type="help">https://github.com/moonlight-stream/moonlight-docs/wiki/Setup-Guide</url>
|
||||||
|
|
||||||
<screenshots>
|
<screenshots>
|
||||||
<screenshot type="default">
|
<screenshot type="default">
|
||||||
<image>https://moonlight-stream.com/appstream_resources/moonlight_screenshot_1.png</image>
|
<image>https://moonlight-stream.org/appstream_resources/moonlight_screenshot_1.png</image>
|
||||||
</screenshot>
|
</screenshot>
|
||||||
<screenshot>
|
<screenshot>
|
||||||
<image>https://moonlight-stream.com/appstream_resources/moonlight_screenshot_2.png</image>
|
<image>https://moonlight-stream.org/appstream_resources/moonlight_screenshot_2.png</image>
|
||||||
</screenshot>
|
</screenshot>
|
||||||
</screenshots>
|
</screenshots>
|
||||||
|
|
||||||
<releases>
|
<releases>
|
||||||
|
<release version="0.7.0" date="2018-12-05">
|
||||||
|
<description>
|
||||||
|
<p>New features:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Added option to quit the app after stopping the stream</li>
|
||||||
|
<li>Added CLI option to quit the running app</li>
|
||||||
|
</ul>
|
||||||
|
<p>Bugfixes:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Switched to libsoundio backend to address audio glitches and reduce latency</li>
|
||||||
|
<li>Fixed CLI version output</li>
|
||||||
|
<li>Fixed streaming error messages not showing up when launching from CLI</li>
|
||||||
|
<li>Updated included SDL gamepad mappings</li>
|
||||||
|
<li>Various minor UI tweaks</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release version="0.6.4" date="2018-11-20">
|
||||||
|
<description>
|
||||||
|
<p>New features:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Added an option to match client display refresh rate when FPS is unlocked</li>
|
||||||
|
</ul>
|
||||||
|
<p>Bugfixes:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Fixed multiple gamepads not being detected on the host in some scenarios</li>
|
||||||
|
<li>Fixed an incorrect warning when trying to stream at 4K</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release version="0.6.3" date="2018-11-16">
|
||||||
|
<description>
|
||||||
|
<p>New features:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Added support for GeForce Experience 3.16</li>
|
||||||
|
<li>Added an option to force Moonlight to start in windowed mode</li>
|
||||||
|
</ul>
|
||||||
|
<p>Bugfixes:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Fixed scrolling not working on the settings page</li>
|
||||||
|
<li>Skipped using proxy servers when attempting to stream</li>
|
||||||
|
<li>Fixed a couple possible crashes</li>
|
||||||
|
<li>Renamed the mouse acceleration option to be more clear</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<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>
|
||||||
@@ -97,7 +231,7 @@
|
|||||||
</categories>
|
</categories>
|
||||||
|
|
||||||
<developer_name>Moonlight Game Streaming Team</developer_name>
|
<developer_name>Moonlight Game Streaming Team</developer_name>
|
||||||
<update_contact>cameron@moonlight-stream.com</update_contact>
|
<update_contact>cameron@moonlight-stream.org</update_contact>
|
||||||
|
|
||||||
<launchable type="desktop-id">com.moonlight_stream.Moonlight.desktop</launchable>
|
<launchable type="desktop-id">com.moonlight_stream.Moonlight.desktop</launchable>
|
||||||
|
|
||||||
|
|||||||
+82
-51
@@ -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 {
|
||||||
@@ -76,10 +89,9 @@ GridView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Label {
|
||||||
id: appNameText
|
id: appNameText
|
||||||
text: model.name
|
text: model.name
|
||||||
color: "white"
|
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: 125
|
height: 125
|
||||||
anchors.top: appIcon.bottom
|
anchors.top: appIcon.bottom
|
||||||
@@ -87,6 +99,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 +113,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 +124,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 +161,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,70 @@
|
|||||||
|
import QtQuick 2.0
|
||||||
|
import QtQuick.Controls 2.2
|
||||||
|
import QtQuick.Dialogs 1.2
|
||||||
|
|
||||||
|
import ComputerManager 1.0
|
||||||
|
import Session 1.0
|
||||||
|
|
||||||
|
Item {
|
||||||
|
anchors.fill: parent
|
||||||
|
|
||||||
|
function onSearchingComputer() {
|
||||||
|
stageLabel.text = "Establishing connection to PC..."
|
||||||
|
}
|
||||||
|
|
||||||
|
function onQuittingApp() {
|
||||||
|
stageLabel.text = "Quitting app..."
|
||||||
|
}
|
||||||
|
|
||||||
|
function onFailure(message) {
|
||||||
|
errorDialog.text = message
|
||||||
|
errorDialog.open()
|
||||||
|
}
|
||||||
|
|
||||||
|
// The StackView will trigger a visibility change when
|
||||||
|
// we're pushed onto it, causing our onVisibleChanged
|
||||||
|
// routine to run, but only if we start as invisible
|
||||||
|
visible: false
|
||||||
|
|
||||||
|
onVisibleChanged: {
|
||||||
|
if (visible && !launcher.isExecuted()) {
|
||||||
|
toolBar.visible = false
|
||||||
|
launcher.searchingComputer.connect(onSearchingComputer)
|
||||||
|
launcher.quittingApp.connect(onQuittingApp)
|
||||||
|
launcher.failed.connect(onFailure)
|
||||||
|
launcher.execute(ComputerManager)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: 5
|
||||||
|
|
||||||
|
BusyIndicator {
|
||||||
|
id: stageSpinner
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
id: stageLabel
|
||||||
|
height: stageSpinner.height
|
||||||
|
text: stageText
|
||||||
|
font.pointSize: 20
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MessageDialog {
|
||||||
|
id: errorDialog
|
||||||
|
modality:Qt.WindowModal
|
||||||
|
icon: StandardIcon.Critical
|
||||||
|
standardButtons: StandardButton.Ok
|
||||||
|
|
||||||
|
onVisibleChanged: {
|
||||||
|
if (!visible) {
|
||||||
|
Qt.quit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
import QtQml 2.2
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
function onAppQuitRequired(appName) {
|
||||||
|
quitAppDialog.appName = appName
|
||||||
|
quitAppDialog.open()
|
||||||
|
}
|
||||||
|
|
||||||
|
onVisibleChanged: {
|
||||||
|
if (visible && !launcher.isExecuted()) {
|
||||||
|
toolBar.visible = false
|
||||||
|
launcher.searchingComputer.connect(onSearchingComputer)
|
||||||
|
launcher.searchingApp.connect(onSearchingApp)
|
||||||
|
launcher.sessionCreated.connect(onSessionCreated)
|
||||||
|
launcher.failed.connect(onLaunchFailed)
|
||||||
|
launcher.appQuitRequired.connect(onAppQuitRequired)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MessageDialog {
|
||||||
|
id: quitAppDialog
|
||||||
|
modality:Qt.WindowModal
|
||||||
|
text:"Are you sure you want to quit " + appName +"? Any unsaved progress will be lost."
|
||||||
|
standardButtons: StandardButton.Yes | StandardButton.No
|
||||||
|
property string appName : ""
|
||||||
|
|
||||||
|
function quitApp() {
|
||||||
|
var component = Qt.createComponent("QuitSegue.qml")
|
||||||
|
var params = {"appName": appName}
|
||||||
|
stackView.push(component.createObject(stackView, params))
|
||||||
|
// Trigger the quit after pushing the quit segue on screen
|
||||||
|
launcher.quitRunningApp()
|
||||||
|
}
|
||||||
|
|
||||||
|
onYes: quitApp()
|
||||||
|
|
||||||
|
// For keyboard/gamepad navigation
|
||||||
|
onAccepted: quitApp()
|
||||||
|
|
||||||
|
// Exit process if app quit is rejected (reacts also to closing of the
|
||||||
|
// dialog from title bar's close button).
|
||||||
|
// Note: this depends on undocumented behavior of visibleChanged()
|
||||||
|
// signal being emitted before yes() or accepted() has been emitted.
|
||||||
|
onVisibleChanged: {
|
||||||
|
if (!visible) {
|
||||||
|
quitTimer.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Component.onCompleted: {
|
||||||
|
yes.connect(quitTimer.stop)
|
||||||
|
accepted.connect(quitTimer.stop)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: quitTimer
|
||||||
|
interval: 100
|
||||||
|
onTriggered: Qt.quit()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
+82
-78
@@ -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
|
||||||
@@ -112,10 +124,9 @@ GridView {
|
|||||||
visible: !model.addPc && model.statusUnknown
|
visible: !model.addPc && model.statusUnknown
|
||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Label {
|
||||||
id: pcNameText
|
id: pcNameText
|
||||||
text: model.name
|
text: model.name
|
||||||
color: "white"
|
|
||||||
|
|
||||||
width: parent.width
|
width: parent.width
|
||||||
anchors.top: pcIcon.bottom
|
anchors.top: pcIcon.bottom
|
||||||
@@ -126,13 +137,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 +152,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 +211,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 +238,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
|
||||||
@@ -65,7 +65,6 @@ Item {
|
|||||||
verticalAlignment: Text.AlignVCenter
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
|
||||||
wrapMode: Text.Wrap
|
wrapMode: Text.Wrap
|
||||||
color: "white"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+334
-112
@@ -2,15 +2,51 @@ 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 {
|
Flickable {
|
||||||
id: settingsPage
|
id: settingsPage
|
||||||
objectName: "Settings"
|
objectName: "Settings"
|
||||||
|
anchors.fill: parent
|
||||||
|
|
||||||
|
contentWidth: settingsColumn1.width > settingsColumn2.width ? settingsColumn1.width : settingsColumn2.width
|
||||||
|
contentHeight: settingsColumn1.height > settingsColumn2.height ? settingsColumn1.height : settingsColumn2.height
|
||||||
|
|
||||||
|
ScrollBar.vertical: ScrollBar {
|
||||||
|
parent: settingsPage.parent
|
||||||
|
anchors {
|
||||||
|
top: settingsPage.top
|
||||||
|
left: settingsPage.right
|
||||||
|
bottom: settingsPage.bottom
|
||||||
|
|
||||||
|
leftMargin: -10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
StreamingPreferences {
|
StreamingPreferences {
|
||||||
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()
|
||||||
}
|
}
|
||||||
@@ -36,57 +72,69 @@ ScrollView {
|
|||||||
id: resFPStitle
|
id: resFPStitle
|
||||||
text: qsTr("Resolution and FPS")
|
text: qsTr("Resolution and FPS")
|
||||||
font.pointSize: 12
|
font.pointSize: 12
|
||||||
wrapMode: Text.Wrap
|
wrapMode: Text.Wrap
|
||||||
color: "white"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
id: resFPSdesc
|
id: resFPSdesc
|
||||||
text: qsTr("Setting values too high for your PC may cause lag, stuttering, or errors.")
|
text: qsTr("Setting values too high for your PC or network connection may cause lag, stuttering, or errors.")
|
||||||
font.pointSize: 9
|
font.pointSize: 9
|
||||||
wrapMode: Text.Wrap
|
wrapMode: Text.Wrap
|
||||||
color: "white"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 +143,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 +152,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 +186,105 @@ 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})
|
// Mark any FPS value greater than 120 as unsupported
|
||||||
|
if (prefs.unsupportedFps && max_fps > 120) {
|
||||||
|
fpsListModel.append({"text": max_fps+" FPS (Unsupported)", "video_fps": ""+max_fps})
|
||||||
|
}
|
||||||
|
else if (max_fps > 120) {
|
||||||
|
fpsListModel.append({"text": "120 FPS", "video_fps": "120"})
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -199,7 +295,6 @@ ScrollView {
|
|||||||
text: qsTr("Video bitrate: ")
|
text: qsTr("Video bitrate: ")
|
||||||
font.pointSize: 12
|
font.pointSize: 12
|
||||||
wrapMode: Text.Wrap
|
wrapMode: Text.Wrap
|
||||||
color: "white"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
@@ -208,7 +303,6 @@ ScrollView {
|
|||||||
text: qsTr("Lower bitrate to reduce lag and stuttering. Raise bitrate to increase image quality.")
|
text: qsTr("Lower bitrate to reduce lag and stuttering. Raise bitrate to increase image quality.")
|
||||||
font.pointSize: 9
|
font.pointSize: 9
|
||||||
wrapMode: Text.Wrap
|
wrapMode: Text.Wrap
|
||||||
color: "white"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Slider {
|
Slider {
|
||||||
@@ -230,27 +324,71 @@ 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: {
|
}
|
||||||
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
|
hoverEnabled: true
|
||||||
text: "<font color=\"white\">Enable V-Sync</font>"
|
textRole: "text"
|
||||||
font.pointSize: 12
|
model: ListModel {
|
||||||
visible: fullScreenCheck.checked
|
id: windowModeListModel
|
||||||
checked: prefs.enableVsync
|
ListElement {
|
||||||
onCheckedChanged: {
|
text: "Full-screen (Recommended)"
|
||||||
prefs.enableVsync = checked
|
val: StreamingPreferences.WM_FULLSCREEN
|
||||||
|
}
|
||||||
|
ListElement {
|
||||||
|
text: "Borderless windowed"
|
||||||
|
val: StreamingPreferences.WM_FULLSCREEN_DESKTOP
|
||||||
|
}
|
||||||
|
ListElement {
|
||||||
|
text: "Windowed"
|
||||||
|
val: StreamingPreferences.WM_WINDOWED
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
onActivated: {
|
||||||
|
prefs.windowMode = windowModeListModel.get(currentIndex).val
|
||||||
|
}
|
||||||
|
|
||||||
|
ToolTip.delay: 1000
|
||||||
|
ToolTip.timeout: 5000
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
ToolTip.text: "Full-screen generally provides the best performance, but borderless windowed may work better with features like macOS Spaces, Alt+Tab, screenshot tools, on-screen overlays, etc."
|
||||||
|
}
|
||||||
|
|
||||||
|
CheckBox {
|
||||||
|
id: vsyncCheck
|
||||||
|
hoverEnabled: true
|
||||||
|
text: "Enable V-Sync"
|
||||||
|
font.pointSize: 12
|
||||||
|
checked: prefs.enableVsync
|
||||||
|
onCheckedChanged: {
|
||||||
|
prefs.enableVsync = checked
|
||||||
|
}
|
||||||
|
|
||||||
|
ToolTip.delay: 1000
|
||||||
|
ToolTip.timeout: 5000
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
ToolTip.text: "Disabling V-Sync allows sub-frame rendering latency, but it can display visible tearing"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -273,38 +411,34 @@ ScrollView {
|
|||||||
text: qsTr("Audio configuration")
|
text: qsTr("Audio configuration")
|
||||||
font.pointSize: 12
|
font.pointSize: 12
|
||||||
wrapMode: Text.Wrap
|
wrapMode: Text.Wrap
|
||||||
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
|
||||||
@@ -315,6 +449,29 @@ ScrollView {
|
|||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
GroupBox {
|
||||||
|
id: uiSettingsGroupBox
|
||||||
|
width: (parent.width - 2 * parent.padding)
|
||||||
|
padding: 12
|
||||||
|
title: "<font color=\"skyblue\">UI Settings</font>"
|
||||||
|
font.pointSize: 12
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.fill: parent
|
||||||
|
spacing: 5
|
||||||
|
|
||||||
|
CheckBox {
|
||||||
|
id: startMaximizedCheck
|
||||||
|
text: "Maximize Moonlight window on startup"
|
||||||
|
font.pointSize: 12
|
||||||
|
checked: !prefs.startWindowed
|
||||||
|
onCheckedChanged: {
|
||||||
|
prefs.startWindowed = !checked
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
@@ -327,7 +484,7 @@ 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 {
|
Column {
|
||||||
@@ -335,13 +492,35 @@ ScrollView {
|
|||||||
spacing: 5
|
spacing: 5
|
||||||
|
|
||||||
CheckBox {
|
CheckBox {
|
||||||
id: multiControllerCheck
|
id: singleControllerCheck
|
||||||
text: "<font color=\"white\">Multiple controller support</font>"
|
text: "Force gamepad #1 always present"
|
||||||
font.pointSize: 12
|
font.pointSize: 12
|
||||||
checked: prefs.multiController
|
checked: !prefs.multiController
|
||||||
onCheckedChanged: {
|
onCheckedChanged: {
|
||||||
prefs.multiController = checked
|
prefs.multiController = !checked
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ToolTip.delay: 1000
|
||||||
|
ToolTip.timeout: 5000
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
ToolTip.text: "Forces a single gamepad to always stay connected to the host, even if no gamepads are actually connected to this PC.\n" +
|
||||||
|
"Only enable this option when streaming a game that doesn't support gamepads being connected after startup."
|
||||||
|
}
|
||||||
|
|
||||||
|
CheckBox {
|
||||||
|
id: rawInputCheck
|
||||||
|
hoverEnabled: true
|
||||||
|
text: "Raw mouse input"
|
||||||
|
font.pointSize: 12
|
||||||
|
checked: !prefs.mouseAcceleration
|
||||||
|
onCheckedChanged: {
|
||||||
|
prefs.mouseAcceleration = !checked
|
||||||
|
}
|
||||||
|
|
||||||
|
ToolTip.delay: 1000
|
||||||
|
ToolTip.timeout: 3000
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
ToolTip.text: "When checked, mouse input is not accelerated or scaled by the OS before passing to Moonlight"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -359,7 +538,7 @@ ScrollView {
|
|||||||
|
|
||||||
CheckBox {
|
CheckBox {
|
||||||
id: optimizeGameSettingsCheck
|
id: optimizeGameSettingsCheck
|
||||||
text: "<font color=\"white\">Optimize game settings</font>"
|
text: "Optimize game settings"
|
||||||
font.pointSize: 12
|
font.pointSize: 12
|
||||||
checked: prefs.gameOptimizations
|
checked: prefs.gameOptimizations
|
||||||
onCheckedChanged: {
|
onCheckedChanged: {
|
||||||
@@ -369,7 +548,7 @@ ScrollView {
|
|||||||
|
|
||||||
CheckBox {
|
CheckBox {
|
||||||
id: audioPcCheck
|
id: audioPcCheck
|
||||||
text: "<font color=\"white\">Play audio on host PC</font>"
|
text: "Play audio on host PC"
|
||||||
font.pointSize: 12
|
font.pointSize: 12
|
||||||
checked: prefs.playAudioOnHost
|
checked: prefs.playAudioOnHost
|
||||||
onCheckedChanged: {
|
onCheckedChanged: {
|
||||||
@@ -396,24 +575,24 @@ ScrollView {
|
|||||||
text: qsTr("Video decoder")
|
text: qsTr("Video decoder")
|
||||||
font.pointSize: 12
|
font.pointSize: 12
|
||||||
wrapMode: Text.Wrap
|
wrapMode: Text.Wrap
|
||||||
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
|
||||||
@@ -442,24 +621,24 @@ ScrollView {
|
|||||||
text: qsTr("Video codec")
|
text: qsTr("Video codec")
|
||||||
font.pointSize: 12
|
font.pointSize: 12
|
||||||
wrapMode: Text.Wrap
|
wrapMode: Text.Wrap
|
||||||
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 +668,49 @@ ScrollView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CheckBox {
|
||||||
|
id: unlockUnsupportedFps
|
||||||
|
text: "Unlock unsupported FPS options"
|
||||||
|
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: "Automatically find PCs on the local network (Recommended)"
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CheckBox {
|
||||||
|
id: quitAppAfter
|
||||||
|
text: "Quit app after quitting session"
|
||||||
|
font.pointSize: 12
|
||||||
|
checked: prefs.quitAppAfter
|
||||||
|
onCheckedChanged: {
|
||||||
|
prefs.quitAppAfter = checked
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+89
-12
@@ -1,13 +1,17 @@
|
|||||||
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 ComputerManager 1.0
|
||||||
|
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 +45,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,28 +60,77 @@ Item {
|
|||||||
toast.visible = true
|
toast.visible = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function quitStarting()
|
||||||
|
{
|
||||||
|
// Avoid the push transition animation
|
||||||
|
var component = Qt.createComponent("QuitSegue.qml")
|
||||||
|
stackView.replace(stackView.currentItem, component.createObject(stackView, {"appName": appName}), StackView.Immediate)
|
||||||
|
|
||||||
|
// Show the Qt window again to show quit segue
|
||||||
|
window.visible = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function sessionFinished()
|
||||||
|
{
|
||||||
|
if (quitAfter) {
|
||||||
|
if (errorDialog.text) {
|
||||||
|
// Quit when the error dialog is acknowledged
|
||||||
|
errorDialog.open()
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Quit immediately
|
||||||
|
Qt.quit()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Exit this view
|
||||||
|
stackView.pop()
|
||||||
|
|
||||||
|
// Show the Qt window again after streaming
|
||||||
|
window.visible = true
|
||||||
|
|
||||||
|
// 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
session.connectionStarted.connect(connectionStarted)
|
session.connectionStarted.connect(connectionStarted)
|
||||||
session.displayLaunchError.connect(displayLaunchError)
|
session.displayLaunchError.connect(displayLaunchError)
|
||||||
session.displayLaunchWarning.connect(displayLaunchWarning)
|
session.displayLaunchWarning.connect(displayLaunchWarning)
|
||||||
|
session.quitStarting.connect(quitStarting)
|
||||||
|
session.sessionFinished.connect(sessionFinished)
|
||||||
|
|
||||||
// Run the streaming session to completion
|
// Run the streaming session to completion
|
||||||
session.exec()
|
session.exec(Screen.virtualX, Screen.virtualY)
|
||||||
|
|
||||||
// Show the Qt window again after streaming
|
|
||||||
window.visible = true
|
|
||||||
|
|
||||||
// Exit this view
|
|
||||||
stackView.pop()
|
|
||||||
}
|
}
|
||||||
else {
|
else if (!quitAfter) {
|
||||||
// Show the toolbar again when we become hidden
|
// Show the toolbar again when we become hidden
|
||||||
toolBar.visible = true
|
toolBar.visible = true
|
||||||
}
|
}
|
||||||
@@ -99,14 +152,38 @@ Item {
|
|||||||
verticalAlignment: Text.AlignVCenter
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
|
||||||
wrapMode: Text.Wrap
|
wrapMode: Text.Wrap
|
||||||
color: "white"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
id: hintText
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.bottomMargin: 50
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
font.pointSize: 18
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
}
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
|
onAccepted: {
|
||||||
|
if (quitAfter) {
|
||||||
|
Qt.quit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onHelp: {
|
||||||
|
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Troubleshooting");
|
||||||
|
|
||||||
|
if (quitAfter) {
|
||||||
|
Qt.quit()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+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);
|
||||||
|
|
||||||
|
|||||||
+284
-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,145 @@ ApplicationWindow {
|
|||||||
width: 1280
|
width: 1280
|
||||||
height: 600
|
height: 600
|
||||||
|
|
||||||
|
visibility: prefs.startWindowed ? "Windowed" : "Maximized"
|
||||||
|
|
||||||
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) {
|
||||||
|
// Set initialized before calling anything else, because
|
||||||
|
// pumping the event loop can cause us to get another
|
||||||
|
// onAfterRendering call and potentially reenter this code.
|
||||||
|
initialized = true;
|
||||||
|
|
||||||
|
if (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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,22 +178,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 +218,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 +243,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 +253,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 +299,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;
|
||||||
|
};
|
||||||
+179
-11
@@ -1,9 +1,11 @@
|
|||||||
#include <QGuiApplication>
|
#include <QGuiApplication>
|
||||||
#include <QQmlApplicationEngine>
|
#include <QQmlApplicationEngine>
|
||||||
|
#include <QQmlContext>
|
||||||
#include <QIcon>
|
#include <QIcon>
|
||||||
#include <QQuickStyle>
|
#include <QQuickStyle>
|
||||||
#include <QMutex>
|
#include <QMutex>
|
||||||
#include <QtDebug>
|
#include <QtDebug>
|
||||||
|
#include <QNetworkProxyFactory>
|
||||||
|
|
||||||
// Don't let SDL hook our main function, since Qt is already
|
// Don't let SDL hook our main function, since Qt is already
|
||||||
// doing the same thing. This needs to be before any headers
|
// doing the same thing. This needs to be before any headers
|
||||||
@@ -11,12 +13,24 @@
|
|||||||
#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/quitstream.h"
|
||||||
|
#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 +76,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 +109,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 +137,102 @@ 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 the app version for the QCommandLineParser's showVersion() command
|
||||||
|
QCoreApplication::setApplicationVersion(VERSION_STR);
|
||||||
|
|
||||||
|
// 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 +260,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 +284,21 @@ 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
|
||||||
|
|
||||||
|
// We don't want system proxies to apply to us
|
||||||
|
QNetworkProxyFactory::setUseSystemConfiguration(false);
|
||||||
|
|
||||||
|
// Clear any default application proxy
|
||||||
|
QNetworkProxy noProxy(QNetworkProxy::NoProxy);
|
||||||
|
QNetworkProxy::setApplicationProxy(noProxy);
|
||||||
|
|
||||||
// Register custom metatypes for use in signals
|
// Register custom metatypes for use in signals
|
||||||
qRegisterMetaType<NvApp>("NvApp");
|
qRegisterMetaType<NvApp>("NvApp");
|
||||||
@@ -182,6 +311,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,19 +326,55 @@ 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;
|
||||||
|
}
|
||||||
|
case GlobalCommandLineParser::QuitRequested:
|
||||||
|
{
|
||||||
|
initialView = "CliQuitStreamSegue.qml";
|
||||||
|
QuitCommandLineParser quitParser;
|
||||||
|
quitParser.parse(app.arguments());
|
||||||
|
auto launcher = new CliQuitStream::Launcher(quitParser.getHost(), &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;
|
||||||
|
|
||||||
SDL_SetMainReady();
|
SDL_SetMainReady();
|
||||||
if (SDL_Init(0) != 0) {
|
if (SDL_Init(SDL_INIT_TIMER) != 0) {
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"SDL_Init() failed: %s",
|
"SDL_Init() failed: %s",
|
||||||
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 +382,9 @@ int main(int argc, char *argv[])
|
|||||||
|
|
||||||
int err = app.exec();
|
int err = app.exec();
|
||||||
|
|
||||||
SDL_Quit();
|
// Give worker tasks time to properly exit. Fixes PendingQuitTask
|
||||||
|
// sometimes freezing and blocking process exit.
|
||||||
|
QThreadPool::globalInstance()->waitForDone(30000);
|
||||||
|
|
||||||
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,11 @@
|
|||||||
<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/CliQuitStreamSegue.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="#FF0000">
|
||||||
<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,15 @@
|
|||||||
#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_QUITAPPAFTER "quitAppAfter"
|
||||||
|
#define SER_MOUSEACCELERATION "mouseacceleration"
|
||||||
|
#define SER_STARTWINDOWED "startwindowed"
|
||||||
|
|
||||||
StreamingPreferences::StreamingPreferences()
|
StreamingPreferences::StreamingPreferences(QObject *parent)
|
||||||
|
: QObject(parent)
|
||||||
{
|
{
|
||||||
reload();
|
reload();
|
||||||
}
|
}
|
||||||
@@ -30,17 +38,25 @@ 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();
|
||||||
|
quitAppAfter = settings.value(SER_QUITAPPAFTER, false).toBool();
|
||||||
|
mouseAcceleration = settings.value(SER_MOUSEACCELERATION, false).toBool();
|
||||||
|
startWindowed = settings.value(SER_STARTWINDOWED, 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 +67,19 @@ 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_QUITAPPAFTER, quitAppAfter);
|
||||||
|
settings.setValue(SER_MOUSEACCELERATION, mouseAcceleration);
|
||||||
|
settings.setValue(SER_STARTWINDOWED, startWindowed);
|
||||||
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 +96,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 +122,24 @@ 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
|
||||||
// Cap the frame rate at 120 FPS. Past this, the encoders start
|
if (StreamUtils::getRealDesktopMode(displayIndex, &desktopMode)) {
|
||||||
// to max out and drop frames.
|
|
||||||
maxFrameRate = qMax(maxFrameRate, qMin(120, mode.refresh_rate));
|
// Start at desktop mode and work our way up
|
||||||
|
bestMode = desktopMode;
|
||||||
|
for (int i = 0; i < SDL_GetNumDisplayModes(displayIndex); i++) {
|
||||||
|
if (SDL_GetDisplayMode(displayIndex, i, &mode) == 0) {
|
||||||
|
if (mode.w == desktopMode.w && mode.h == desktopMode.h) {
|
||||||
|
if (mode.refresh_rate > bestMode.refresh_rate) {
|
||||||
|
bestMode = mode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
maxFrameRate = qMax(maxFrameRate, bestMode.refresh_rate);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,7 +148,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 +163,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 +171,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,66 @@ 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 quitAppAfter MEMBER quitAppAfter NOTIFY quitAppAfterChanged)
|
||||||
|
Q_PROPERTY(bool mouseAcceleration MEMBER mouseAcceleration NOTIFY mouseAccelerationChanged)
|
||||||
|
Q_PROPERTY(bool startWindowed MEMBER startWindowed NOTIFY startWindowedChanged)
|
||||||
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 quitAppAfter;
|
||||||
|
bool mouseAcceleration;
|
||||||
|
bool startWindowed;
|
||||||
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 quitAppAfterChanged();
|
||||||
|
void mouseAccelerationChanged();
|
||||||
void audioConfigChanged();
|
void audioConfigChanged();
|
||||||
void videoCodecConfigChanged();
|
void videoCodecConfigChanged();
|
||||||
void videoDecoderSelectionChanged();
|
void videoDecoderSelectionChanged();
|
||||||
|
void windowModeChanged();
|
||||||
|
void startWindowedChanged();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -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,445 @@
|
|||||||
|
#include "soundioaudiorenderer.h"
|
||||||
|
|
||||||
|
#include <SDL.h>
|
||||||
|
|
||||||
|
#include <QtGlobal>
|
||||||
|
|
||||||
|
// 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. It is also the minimum
|
||||||
|
// size that we will write when called to fill a buffer.
|
||||||
|
const double SoundIoAudioRenderer::k_RawSampleLengthSec = 0.005;
|
||||||
|
|
||||||
|
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->software_latency = k_RawSampleLengthSec;
|
||||||
|
m_OutputStream->name = "Moonlight";
|
||||||
|
m_OutputStream->userdata = this;
|
||||||
|
m_OutputStream->error_callback = sioErrorCallback;
|
||||||
|
m_OutputStream->write_callback = sioWriteCallback;
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
// Ensure we always write at least a buffer, even if it's silence, to avoid
|
||||||
|
// busy looping when no audio data is available while libsoundio tries to keep
|
||||||
|
// us from starving the output device.
|
||||||
|
frameCountMin = qMax(frameCountMin, (int)(stream->sample_rate * k_RawSampleLengthSec));
|
||||||
|
frameCountMin = qMin(frameCountMin, frameCountMax);
|
||||||
|
|
||||||
|
// Place an upper-bound on audio stream latency to
|
||||||
|
// avoid accumulating packets in queue-based backends
|
||||||
|
// like WASAPI, ALSA, and PulseAudio.
|
||||||
|
//
|
||||||
|
// 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 != SoundIoBackendCoreAudio && me->m_SoundIo->current_backend != SoundIoBackendJack) {
|
||||||
|
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,38 @@
|
|||||||
|
#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;
|
||||||
|
|
||||||
|
static const double k_RawSampleLengthSec;
|
||||||
|
};
|
||||||
+440
-48
@@ -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,17 @@
|
|||||||
#define VK_NUMPAD0 0x60
|
#define VK_NUMPAD0 0x60
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#define MOUSE_POLLING_INTERVAL 5
|
||||||
|
|
||||||
|
// How long the mouse button will be pressed for a tap to click gesture
|
||||||
|
#define TAP_BUTTON_RELEASE_DELAY 100
|
||||||
|
|
||||||
|
// How long the fingers must be stationary to start a drag
|
||||||
|
#define DRAG_ACTIVATION_DELAY 650
|
||||||
|
|
||||||
|
// How far the finger can move before it cancels a drag or tap
|
||||||
|
#define DEAD_ZONE_DELTA 0.1f
|
||||||
|
|
||||||
const int SdlInputHandler::k_ButtonMap[] = {
|
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 +36,27 @@ 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*, int streamWidth, int streamHeight)
|
||||||
: m_MultiController(multiController)
|
: m_MultiController(prefs.multiController),
|
||||||
|
m_MouseMoveTimer(0),
|
||||||
|
m_LeftButtonReleaseTimer(0),
|
||||||
|
m_RightButtonReleaseTimer(0),
|
||||||
|
m_DragTimer(0),
|
||||||
|
m_DragButton(0),
|
||||||
|
m_NumFingersDown(0),
|
||||||
|
m_StreamWidth(streamWidth),
|
||||||
|
m_StreamHeight(streamHeight)
|
||||||
{
|
{
|
||||||
// Allow gamepad input when the app doesn't have focus
|
// 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,16 +66,22 @@ SdlInputHandler::SdlInputHandler(bool multiController)
|
|||||||
SDL_GetError());
|
SDL_GetError());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!m_MultiController) {
|
MappingManager mappingManager;
|
||||||
// Player 1 is always present in non-MC mode
|
mappingManager.applyMappings();
|
||||||
m_GamepadMask = 0x1;
|
|
||||||
}
|
// Initialize the gamepad mask with currently attached gamepads to avoid
|
||||||
else {
|
// causing gamepads to unexpectedly disappear and reappear on the host
|
||||||
// Otherwise, detect gamepads on the fly
|
// during stream startup as we detect currently attached gamepads one at a time.
|
||||||
m_GamepadMask = 0;
|
m_GamepadMask = getAttachedGamepadMask();
|
||||||
}
|
|
||||||
|
|
||||||
SDL_zero(m_GamepadState);
|
SDL_zero(m_GamepadState);
|
||||||
|
SDL_zero(m_TouchDownEvent);
|
||||||
|
SDL_zero(m_CumulativeDelta);
|
||||||
|
|
||||||
|
SDL_AtomicSet(&m_MouseDeltaX, 0);
|
||||||
|
SDL_AtomicSet(&m_MouseDeltaY, 0);
|
||||||
|
|
||||||
|
m_MouseMoveTimer = SDL_AddTimer(MOUSE_POLLING_INTERVAL, SdlInputHandler::mouseMoveTimerCallback, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
SdlInputHandler::~SdlInputHandler()
|
SdlInputHandler::~SdlInputHandler()
|
||||||
@@ -57,6 +92,11 @@ SdlInputHandler::~SdlInputHandler()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SDL_RemoveTimer(m_MouseMoveTimer);
|
||||||
|
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 +111,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 +127,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 +141,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 +359,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 +370,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 +388,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 +403,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 +420,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,11 +444,15 @@ void SdlInputHandler::handleMouseMotionEvent(SDL_MouseMotionEvent* event)
|
|||||||
// Not capturing
|
// Not capturing
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
else if (event->which == SDL_TOUCH_MOUSEID) {
|
||||||
if (event->xrel != 0 || event->yrel != 0) {
|
// Ignore synthetic mouse events
|
||||||
LiSendMouseMoveEvent((short)event->xrel,
|
return;
|
||||||
(short)event->yrel);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Batch until the next mouse polling window or we'll get awful
|
||||||
|
// input lag everything except GFE 3.14 and 3.15.
|
||||||
|
SDL_AtomicAdd(&m_MouseDeltaX, event->xrel);
|
||||||
|
SDL_AtomicAdd(&m_MouseDeltaY, event->yrel);
|
||||||
}
|
}
|
||||||
|
|
||||||
void SdlInputHandler::handleMouseWheelEvent(SDL_MouseWheelEvent* event)
|
void SdlInputHandler::handleMouseWheelEvent(SDL_MouseWheelEvent* event)
|
||||||
@@ -401,6 +461,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 +502,50 @@ 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint32 SdlInputHandler::mouseMoveTimerCallback(Uint32 interval, void *param)
|
||||||
|
{
|
||||||
|
auto me = reinterpret_cast<SdlInputHandler*>(param);
|
||||||
|
|
||||||
|
short deltaX = (short)SDL_AtomicSet(&me->m_MouseDeltaX, 0);
|
||||||
|
short deltaY = (short)SDL_AtomicSet(&me->m_MouseDeltaY, 0);
|
||||||
|
|
||||||
|
if (deltaX != 0 || deltaY != 0) {
|
||||||
|
LiSendMouseMoveEvent(deltaX, deltaY);
|
||||||
|
}
|
||||||
|
|
||||||
|
return interval;
|
||||||
|
}
|
||||||
|
|
||||||
void SdlInputHandler::handleControllerAxisEvent(SDL_ControllerAxisEvent* event)
|
void SdlInputHandler::handleControllerAxisEvent(SDL_ControllerAxisEvent* event)
|
||||||
{
|
{
|
||||||
GamepadState* state = findStateForGamepad(event->which);
|
GamepadState* state = findStateForGamepad(event->which);
|
||||||
@@ -494,6 +602,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -504,8 +629,31 @@ void SdlInputHandler::handleControllerDeviceEvent(SDL_ControllerDeviceEvent* eve
|
|||||||
if (event->type == SDL_CONTROLLERDEVICEADDED) {
|
if (event->type == SDL_CONTROLLERDEVICEADDED) {
|
||||||
int i;
|
int i;
|
||||||
const char* name;
|
const char* name;
|
||||||
|
SDL_GameController* controller;
|
||||||
|
const char* mapping;
|
||||||
|
char guidStr[33];
|
||||||
|
|
||||||
for (i = 0; i < MAX_GAMEPADS; i++) {
|
controller = SDL_GameControllerOpen(event->which);
|
||||||
|
if (controller == NULL) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Failed to open gamepad: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
#if SDL_VERSION_ATLEAST(2, 0, 9)
|
||||||
|
// Determine this player's preferred index
|
||||||
|
i = SDL_GameControllerGetPlayerIndex(controller);
|
||||||
|
if (i < 0 || i >= MAX_GAMEPADS || m_GamepadState[i].controller != NULL) {
|
||||||
|
// If the player index is unavailable or invalid, start searching from 0
|
||||||
|
i = 0;
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
// SDL 2.0.8 and earlier has no player number hints
|
||||||
|
i = 0;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
for (; i < MAX_GAMEPADS; i++) {
|
||||||
if (m_GamepadState[i].controller == NULL) {
|
if (m_GamepadState[i].controller == NULL) {
|
||||||
// Found an empty slot
|
// Found an empty slot
|
||||||
break;
|
break;
|
||||||
@@ -515,6 +663,7 @@ void SdlInputHandler::handleControllerDeviceEvent(SDL_ControllerDeviceEvent* eve
|
|||||||
if (i == MAX_GAMEPADS) {
|
if (i == MAX_GAMEPADS) {
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"No open gamepad slots found!");
|
"No open gamepad slots found!");
|
||||||
|
SDL_GameControllerClose(controller);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -526,26 +675,30 @@ void SdlInputHandler::handleControllerDeviceEvent(SDL_ControllerDeviceEvent* eve
|
|||||||
// Always player 1 in single controller mode
|
// Always player 1 in single controller mode
|
||||||
state->index = 0;
|
state->index = 0;
|
||||||
}
|
}
|
||||||
state->controller = SDL_GameControllerOpen(event->which);
|
|
||||||
if (state->controller == NULL) {
|
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Failed to open gamepad: %s",
|
|
||||||
SDL_GetError());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
state->controller = controller;
|
||||||
state->jsId = SDL_JoystickInstanceID(SDL_GameControllerGetJoystick(state->controller));
|
state->jsId = SDL_JoystickInstanceID(SDL_GameControllerGetJoystick(state->controller));
|
||||||
|
|
||||||
|
SDL_JoystickGetGUIDString(SDL_JoystickGetGUID(SDL_GameControllerGetJoystick(state->controller)),
|
||||||
|
guidStr, sizeof(guidStr));
|
||||||
|
mapping = SDL_GameControllerMapping(state->controller);
|
||||||
name = SDL_GameControllerName(state->controller);
|
name = SDL_GameControllerName(state->controller);
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"Gamepad %d (player %d) is: %s",
|
"Gamepad %d (player %d) is: %s (%s -> %s)",
|
||||||
i,
|
i,
|
||||||
state->index,
|
state->index,
|
||||||
name != NULL ? name : "<null>");
|
name != nullptr ? name : "<null>",
|
||||||
|
guidStr,
|
||||||
|
mapping != nullptr ? mapping : "<null>");
|
||||||
|
if (mapping != nullptr) {
|
||||||
|
SDL_free((void*)mapping);
|
||||||
|
}
|
||||||
|
|
||||||
// Add this gamepad to the gamepad mask
|
// Add this gamepad to the gamepad mask
|
||||||
if (m_MultiController) {
|
if (m_MultiController) {
|
||||||
SDL_assert(!(m_GamepadMask & (1 << state->index)));
|
// NB: Don't assert that it's unset here because we will already
|
||||||
|
// have the mask set for initially attached gamepads to avoid confusing
|
||||||
|
// apps running on the host.
|
||||||
m_GamepadMask |= (1 << state->index);
|
m_GamepadMask |= (1 << state->index);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -583,9 +736,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;
|
||||||
|
|
||||||
@@ -594,8 +910,9 @@ int SdlInputHandler::getAttachedGamepadMask()
|
|||||||
return 0x1;
|
return 0x1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Use SDL_GameControllerGetPlayerIndex() for accurate indexes?
|
||||||
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 +920,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,92 @@
|
|||||||
|
#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);
|
||||||
|
|
||||||
|
static
|
||||||
|
Uint32 mouseMoveTimerCallback(Uint32 interval, void* param);
|
||||||
|
|
||||||
|
bool m_MultiController;
|
||||||
|
SDL_TimerID m_MouseMoveTimer;
|
||||||
|
SDL_atomic_t m_MouseDeltaX;
|
||||||
|
SDL_atomic_t m_MouseDeltaY;
|
||||||
|
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[];
|
|
||||||
};
|
|
||||||
+522
-225
File diff suppressed because it is too large.
Load diff
@@ -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,20 @@ 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();
|
virtual ~Session();
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
@@ -36,21 +43,32 @@ signals:
|
|||||||
|
|
||||||
void displayLaunchWarning(QString text);
|
void displayLaunchWarning(QString text);
|
||||||
|
|
||||||
|
void quitStarting();
|
||||||
|
|
||||||
|
void sessionFinished();
|
||||||
|
|
||||||
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 +86,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 +106,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 +115,23 @@ 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;
|
||||||
|
bool m_UnexpectedTermination;
|
||||||
|
|
||||||
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
|
||||||
@@ -51,6 +51,7 @@
|
|||||||
#include "SDL_power.h"
|
#include "SDL_power.h"
|
||||||
#include "SDL_render.h"
|
#include "SDL_render.h"
|
||||||
#include "SDL_rwops.h"
|
#include "SDL_rwops.h"
|
||||||
|
#include "SDL_sensor.h"
|
||||||
#include "SDL_shape.h"
|
#include "SDL_shape.h"
|
||||||
#include "SDL_system.h"
|
#include "SDL_system.h"
|
||||||
#include "SDL_thread.h"
|
#include "SDL_thread.h"
|
||||||
@@ -80,10 +81,11 @@ extern "C" {
|
|||||||
#define SDL_INIT_HAPTIC 0x00001000u
|
#define SDL_INIT_HAPTIC 0x00001000u
|
||||||
#define SDL_INIT_GAMECONTROLLER 0x00002000u /**< SDL_INIT_GAMECONTROLLER implies SDL_INIT_JOYSTICK */
|
#define SDL_INIT_GAMECONTROLLER 0x00002000u /**< SDL_INIT_GAMECONTROLLER implies SDL_INIT_JOYSTICK */
|
||||||
#define SDL_INIT_EVENTS 0x00004000u
|
#define SDL_INIT_EVENTS 0x00004000u
|
||||||
|
#define SDL_INIT_SENSOR 0x00008000u
|
||||||
#define SDL_INIT_NOPARACHUTE 0x00100000u /**< compatibility; this flag is ignored. */
|
#define SDL_INIT_NOPARACHUTE 0x00100000u /**< compatibility; this flag is ignored. */
|
||||||
#define SDL_INIT_EVERYTHING ( \
|
#define SDL_INIT_EVERYTHING ( \
|
||||||
SDL_INIT_TIMER | SDL_INIT_AUDIO | SDL_INIT_VIDEO | SDL_INIT_EVENTS | \
|
SDL_INIT_TIMER | SDL_INIT_AUDIO | SDL_INIT_VIDEO | SDL_INIT_EVENTS | \
|
||||||
SDL_INIT_JOYSTICK | SDL_INIT_HAPTIC | SDL_INIT_GAMECONTROLLER \
|
SDL_INIT_JOYSTICK | SDL_INIT_HAPTIC | SDL_INIT_GAMECONTROLLER | SDL_INIT_SENSOR \
|
||||||
)
|
)
|
||||||
/* @} */
|
/* @} */
|
||||||
|
|
||||||
|
|||||||
@@ -140,7 +140,8 @@ typedef Uint16 SDL_AudioFormat;
|
|||||||
#define SDL_AUDIO_ALLOW_FREQUENCY_CHANGE 0x00000001
|
#define SDL_AUDIO_ALLOW_FREQUENCY_CHANGE 0x00000001
|
||||||
#define SDL_AUDIO_ALLOW_FORMAT_CHANGE 0x00000002
|
#define SDL_AUDIO_ALLOW_FORMAT_CHANGE 0x00000002
|
||||||
#define SDL_AUDIO_ALLOW_CHANNELS_CHANGE 0x00000004
|
#define SDL_AUDIO_ALLOW_CHANNELS_CHANGE 0x00000004
|
||||||
#define SDL_AUDIO_ALLOW_ANY_CHANGE (SDL_AUDIO_ALLOW_FREQUENCY_CHANGE|SDL_AUDIO_ALLOW_FORMAT_CHANGE|SDL_AUDIO_ALLOW_CHANNELS_CHANGE)
|
#define SDL_AUDIO_ALLOW_SAMPLES_CHANGE 0x00000008
|
||||||
|
#define SDL_AUDIO_ALLOW_ANY_CHANGE (SDL_AUDIO_ALLOW_FREQUENCY_CHANGE|SDL_AUDIO_ALLOW_FORMAT_CHANGE|SDL_AUDIO_ALLOW_CHANNELS_CHANGE|SDL_AUDIO_ALLOW_SAMPLES_CHANGE)
|
||||||
/* @} */
|
/* @} */
|
||||||
|
|
||||||
/* @} *//* Audio flags */
|
/* @} *//* Audio flags */
|
||||||
|
|||||||
@@ -82,6 +82,9 @@ typedef unsigned int uintptr_t;
|
|||||||
#define HAVE_DSOUND_H 1
|
#define HAVE_DSOUND_H 1
|
||||||
#define HAVE_DXGI_H 1
|
#define HAVE_DXGI_H 1
|
||||||
#define HAVE_XINPUT_H 1
|
#define HAVE_XINPUT_H 1
|
||||||
|
#define HAVE_MMDEVICEAPI_H 1
|
||||||
|
#define HAVE_AUDIOCLIENT_H 1
|
||||||
|
#define HAVE_ENDPOINTVOLUME_H 1
|
||||||
|
|
||||||
/* This is disabled by default to avoid C runtime dependencies and manifest requirements */
|
/* This is disabled by default to avoid C runtime dependencies and manifest requirements */
|
||||||
#ifdef HAVE_LIBC
|
#ifdef HAVE_LIBC
|
||||||
@@ -139,6 +142,8 @@ typedef unsigned int uintptr_t;
|
|||||||
#define HAVE__COPYSIGN 1
|
#define HAVE__COPYSIGN 1
|
||||||
#define HAVE_COS 1
|
#define HAVE_COS 1
|
||||||
#define HAVE_COSF 1
|
#define HAVE_COSF 1
|
||||||
|
#define HAVE_EXP 1
|
||||||
|
#define HAVE_EXPF 1
|
||||||
#define HAVE_FABS 1
|
#define HAVE_FABS 1
|
||||||
#define HAVE_FABSF 1
|
#define HAVE_FABSF 1
|
||||||
#define HAVE_FLOOR 1
|
#define HAVE_FLOOR 1
|
||||||
@@ -188,9 +193,13 @@ typedef unsigned int uintptr_t;
|
|||||||
/* Enable various input drivers */
|
/* Enable various input drivers */
|
||||||
#define SDL_JOYSTICK_DINPUT 1
|
#define SDL_JOYSTICK_DINPUT 1
|
||||||
#define SDL_JOYSTICK_XINPUT 1
|
#define SDL_JOYSTICK_XINPUT 1
|
||||||
|
#define SDL_JOYSTICK_HIDAPI 1
|
||||||
#define SDL_HAPTIC_DINPUT 1
|
#define SDL_HAPTIC_DINPUT 1
|
||||||
#define SDL_HAPTIC_XINPUT 1
|
#define SDL_HAPTIC_XINPUT 1
|
||||||
|
|
||||||
|
/* Enable the dummy sensor driver */
|
||||||
|
#define SDL_SENSOR_DUMMY 1
|
||||||
|
|
||||||
/* Enable various shared object loading systems */
|
/* Enable various shared object loading systems */
|
||||||
#define SDL_LOADSO_WINDOWS 1
|
#define SDL_LOADSO_WINDOWS 1
|
||||||
|
|
||||||
|
|||||||
@@ -51,16 +51,19 @@
|
|||||||
#include <intrin.h>
|
#include <intrin.h>
|
||||||
#else
|
#else
|
||||||
#ifdef __ALTIVEC__
|
#ifdef __ALTIVEC__
|
||||||
#if HAVE_ALTIVEC_H && !defined(__APPLE_ALTIVEC__) && !defined(SDL_DISABLE_ALTIVEC_H)
|
#if defined(HAVE_ALTIVEC_H) && !defined(__APPLE_ALTIVEC__) && !defined(SDL_DISABLE_ALTIVEC_H)
|
||||||
#include <altivec.h>
|
#include <altivec.h>
|
||||||
#undef pixel
|
#undef pixel
|
||||||
#undef bool
|
#undef bool
|
||||||
#endif
|
#endif
|
||||||
#endif
|
#endif
|
||||||
|
#if defined(__ARM_NEON__) && !defined(SDL_DISABLE_ARM_NEON_H)
|
||||||
|
#include <arm_neon.h>
|
||||||
|
#endif
|
||||||
#if defined(__3dNOW__) && !defined(SDL_DISABLE_MM3DNOW_H)
|
#if defined(__3dNOW__) && !defined(SDL_DISABLE_MM3DNOW_H)
|
||||||
#include <mm3dnow.h>
|
#include <mm3dnow.h>
|
||||||
#endif
|
#endif
|
||||||
#if HAVE_IMMINTRIN_H && !defined(SDL_DISABLE_IMMINTRIN_H)
|
#if defined(HAVE_IMMINTRIN_H) && !defined(SDL_DISABLE_IMMINTRIN_H)
|
||||||
#include <immintrin.h>
|
#include <immintrin.h>
|
||||||
#else
|
#else
|
||||||
#if defined(__MMX__) && !defined(SDL_DISABLE_MMINTRIN_H)
|
#if defined(__MMX__) && !defined(SDL_DISABLE_MMINTRIN_H)
|
||||||
@@ -159,6 +162,11 @@ extern DECLSPEC SDL_bool SDLCALL SDL_HasAVX(void);
|
|||||||
*/
|
*/
|
||||||
extern DECLSPEC SDL_bool SDLCALL SDL_HasAVX2(void);
|
extern DECLSPEC SDL_bool SDLCALL SDL_HasAVX2(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function returns true if the CPU has AVX-512F (foundation) features.
|
||||||
|
*/
|
||||||
|
extern DECLSPEC SDL_bool SDLCALL SDL_HasAVX512F(void);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This function returns true if the CPU has NEON (ARM SIMD) features.
|
* This function returns true if the CPU has NEON (ARM SIMD) features.
|
||||||
*/
|
*/
|
||||||
@@ -169,7 +177,6 @@ extern DECLSPEC SDL_bool SDLCALL SDL_HasNEON(void);
|
|||||||
*/
|
*/
|
||||||
extern DECLSPEC int SDLCALL SDL_GetSystemRAM(void);
|
extern DECLSPEC int SDLCALL SDL_GetSystemRAM(void);
|
||||||
|
|
||||||
|
|
||||||
/* Ends C function definitions when using C++ */
|
/* Ends C function definitions when using C++ */
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,6 +85,9 @@ typedef enum
|
|||||||
Called on Android in onResume()
|
Called on Android in onResume()
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/* Display events */
|
||||||
|
SDL_DISPLAYEVENT = 0x150, /**< Display state change */
|
||||||
|
|
||||||
/* Window events */
|
/* Window events */
|
||||||
SDL_WINDOWEVENT = 0x200, /**< Window state change */
|
SDL_WINDOWEVENT = 0x200, /**< Window state change */
|
||||||
SDL_SYSWMEVENT, /**< System specific event */
|
SDL_SYSWMEVENT, /**< System specific event */
|
||||||
@@ -144,6 +147,9 @@ typedef enum
|
|||||||
SDL_AUDIODEVICEADDED = 0x1100, /**< A new audio device is available */
|
SDL_AUDIODEVICEADDED = 0x1100, /**< A new audio device is available */
|
||||||
SDL_AUDIODEVICEREMOVED, /**< An audio device has been removed. */
|
SDL_AUDIODEVICEREMOVED, /**< An audio device has been removed. */
|
||||||
|
|
||||||
|
/* Sensor events */
|
||||||
|
SDL_SENSORUPDATE = 0x1200, /**< A sensor was updated */
|
||||||
|
|
||||||
/* Render events */
|
/* Render events */
|
||||||
SDL_RENDER_TARGETS_RESET = 0x2000, /**< The render targets have been reset and their contents need to be updated */
|
SDL_RENDER_TARGETS_RESET = 0x2000, /**< The render targets have been reset and their contents need to be updated */
|
||||||
SDL_RENDER_DEVICE_RESET, /**< The device has been reset and all textures need to be recreated */
|
SDL_RENDER_DEVICE_RESET, /**< The device has been reset and all textures need to be recreated */
|
||||||
@@ -168,6 +174,21 @@ typedef struct SDL_CommonEvent
|
|||||||
Uint32 timestamp; /**< In milliseconds, populated using SDL_GetTicks() */
|
Uint32 timestamp; /**< In milliseconds, populated using SDL_GetTicks() */
|
||||||
} SDL_CommonEvent;
|
} SDL_CommonEvent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* \brief Display state change event data (event.display.*)
|
||||||
|
*/
|
||||||
|
typedef struct SDL_DisplayEvent
|
||||||
|
{
|
||||||
|
Uint32 type; /**< ::SDL_DISPLAYEVENT */
|
||||||
|
Uint32 timestamp; /**< In milliseconds, populated using SDL_GetTicks() */
|
||||||
|
Uint32 display; /**< The associated display index */
|
||||||
|
Uint8 event; /**< ::SDL_DisplayEventID */
|
||||||
|
Uint8 padding1;
|
||||||
|
Uint8 padding2;
|
||||||
|
Uint8 padding3;
|
||||||
|
Sint32 data1; /**< event dependent data */
|
||||||
|
} SDL_DisplayEvent;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* \brief Window state change event data (event.window.*)
|
* \brief Window state change event data (event.window.*)
|
||||||
*/
|
*/
|
||||||
@@ -471,6 +492,17 @@ typedef struct SDL_DropEvent
|
|||||||
} SDL_DropEvent;
|
} SDL_DropEvent;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* \brief Sensor event structure (event.sensor.*)
|
||||||
|
*/
|
||||||
|
typedef struct SDL_SensorEvent
|
||||||
|
{
|
||||||
|
Uint32 type; /**< ::SDL_SENSORUPDATE */
|
||||||
|
Uint32 timestamp; /**< In milliseconds, populated using SDL_GetTicks() */
|
||||||
|
Sint32 which; /**< The instance ID of the sensor */
|
||||||
|
float data[6]; /**< Up to 6 values from the sensor - additional values can be queried using SDL_SensorGetData() */
|
||||||
|
} SDL_SensorEvent;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* \brief The "quit requested" event
|
* \brief The "quit requested" event
|
||||||
*/
|
*/
|
||||||
@@ -526,6 +558,7 @@ typedef union SDL_Event
|
|||||||
{
|
{
|
||||||
Uint32 type; /**< Event type, shared with all events */
|
Uint32 type; /**< Event type, shared with all events */
|
||||||
SDL_CommonEvent common; /**< Common event data */
|
SDL_CommonEvent common; /**< Common event data */
|
||||||
|
SDL_DisplayEvent display; /**< Window event data */
|
||||||
SDL_WindowEvent window; /**< Window event data */
|
SDL_WindowEvent window; /**< Window event data */
|
||||||
SDL_KeyboardEvent key; /**< Keyboard event data */
|
SDL_KeyboardEvent key; /**< Keyboard event data */
|
||||||
SDL_TextEditingEvent edit; /**< Text editing event data */
|
SDL_TextEditingEvent edit; /**< Text editing event data */
|
||||||
@@ -542,6 +575,7 @@ typedef union SDL_Event
|
|||||||
SDL_ControllerButtonEvent cbutton; /**< Game Controller button event data */
|
SDL_ControllerButtonEvent cbutton; /**< Game Controller button event data */
|
||||||
SDL_ControllerDeviceEvent cdevice; /**< Game Controller device event data */
|
SDL_ControllerDeviceEvent cdevice; /**< Game Controller device event data */
|
||||||
SDL_AudioDeviceEvent adevice; /**< Audio device event data */
|
SDL_AudioDeviceEvent adevice; /**< Audio device event data */
|
||||||
|
SDL_SensorEvent sensor; /**< Sensor event data */
|
||||||
SDL_QuitEvent quit; /**< Quit request event data */
|
SDL_QuitEvent quit; /**< Quit request event data */
|
||||||
SDL_UserEvent user; /**< Custom event data */
|
SDL_UserEvent user; /**< Custom event data */
|
||||||
SDL_SysWMEvent syswm; /**< System dependent window event data */
|
SDL_SysWMEvent syswm; /**< System dependent window event data */
|
||||||
|
|||||||
@@ -175,6 +175,14 @@ extern DECLSPEC SDL_bool SDLCALL SDL_IsGameController(int joystick_index);
|
|||||||
*/
|
*/
|
||||||
extern DECLSPEC const char *SDLCALL SDL_GameControllerNameForIndex(int joystick_index);
|
extern DECLSPEC const char *SDLCALL SDL_GameControllerNameForIndex(int joystick_index);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the mapping of a game controller.
|
||||||
|
* This can be called before any controllers are opened.
|
||||||
|
*
|
||||||
|
* \return the mapping string. Must be freed with SDL_free(). Returns NULL if no mapping is available
|
||||||
|
*/
|
||||||
|
extern DECLSPEC char *SDLCALL SDL_GameControllerMappingForDeviceIndex(int joystick_index);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Open a game controller for use.
|
* Open a game controller for use.
|
||||||
* The index passed as an argument refers to the N'th game controller on the system.
|
* The index passed as an argument refers to the N'th game controller on the system.
|
||||||
@@ -196,6 +204,13 @@ extern DECLSPEC SDL_GameController *SDLCALL SDL_GameControllerFromInstanceID(SDL
|
|||||||
*/
|
*/
|
||||||
extern DECLSPEC const char *SDLCALL SDL_GameControllerName(SDL_GameController *gamecontroller);
|
extern DECLSPEC const char *SDLCALL SDL_GameControllerName(SDL_GameController *gamecontroller);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the player index of an opened game controller, or -1 if it's not available
|
||||||
|
*
|
||||||
|
* For XInput controllers this returns the XInput user index.
|
||||||
|
*/
|
||||||
|
extern DECLSPEC int SDLCALL SDL_GameControllerGetPlayerIndex(SDL_GameController *gamecontroller);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the USB vendor ID of an opened controller, if available.
|
* Get the USB vendor ID of an opened controller, if available.
|
||||||
* If the vendor ID isn't available this function returns 0.
|
* If the vendor ID isn't available this function returns 0.
|
||||||
@@ -345,6 +360,19 @@ SDL_GameControllerGetBindForButton(SDL_GameController *gamecontroller,
|
|||||||
extern DECLSPEC Uint8 SDLCALL SDL_GameControllerGetButton(SDL_GameController *gamecontroller,
|
extern DECLSPEC Uint8 SDLCALL SDL_GameControllerGetButton(SDL_GameController *gamecontroller,
|
||||||
SDL_GameControllerButton button);
|
SDL_GameControllerButton button);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trigger a rumble effect
|
||||||
|
* Each call to this function cancels any previous rumble effect, and calling it with 0 intensity stops any rumbling.
|
||||||
|
*
|
||||||
|
* \param gamecontroller The controller to vibrate
|
||||||
|
* \param low_frequency_rumble The intensity of the low frequency (left) rumble motor, from 0 to 0xFFFF
|
||||||
|
* \param high_frequency_rumble The intensity of the high frequency (right) rumble motor, from 0 to 0xFFFF
|
||||||
|
* \param duration_ms The duration of the rumble effect, in milliseconds
|
||||||
|
*
|
||||||
|
* \return 0, or -1 if rumble isn't supported on this joystick
|
||||||
|
*/
|
||||||
|
extern DECLSPEC int SDLCALL SDL_GameControllerRumble(SDL_GameController *gamecontroller, Uint16 low_frequency_rumble, Uint16 high_frequency_rumble, Uint32 duration_ms);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Close a controller previously opened with SDL_GameControllerOpen().
|
* Close a controller previously opened with SDL_GameControllerOpen().
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -117,6 +117,17 @@
|
|||||||
extern "C" {
|
extern "C" {
|
||||||
#endif /* __cplusplus */
|
#endif /* __cplusplus */
|
||||||
|
|
||||||
|
/* FIXME: For SDL 2.1, adjust all the magnitude variables to be Uint16 (0xFFFF).
|
||||||
|
*
|
||||||
|
* At the moment the magnitude variables are mixed between signed/unsigned, and
|
||||||
|
* it is also not made clear that ALL of those variables expect a max of 0x7FFF.
|
||||||
|
*
|
||||||
|
* Some platforms may have higher precision than that (Linux FF, Windows XInput)
|
||||||
|
* so we should fix the inconsistency in favor of higher possible precision,
|
||||||
|
* adjusting for platforms that use different scales.
|
||||||
|
* -flibit
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* \typedef SDL_Haptic
|
* \typedef SDL_Haptic
|
||||||
*
|
*
|
||||||
@@ -656,8 +667,8 @@ typedef struct SDL_HapticRamp
|
|||||||
* This struct is exclusively for the ::SDL_HAPTIC_LEFTRIGHT effect.
|
* This struct is exclusively for the ::SDL_HAPTIC_LEFTRIGHT effect.
|
||||||
*
|
*
|
||||||
* The Left/Right effect is used to explicitly control the large and small
|
* The Left/Right effect is used to explicitly control the large and small
|
||||||
* motors, commonly found in modern game controllers. One motor is high
|
* motors, commonly found in modern game controllers. The small (right) motor
|
||||||
* frequency, the other is low frequency.
|
* is high frequency, and the large (left) motor is low frequency.
|
||||||
*
|
*
|
||||||
* \sa SDL_HAPTIC_LEFTRIGHT
|
* \sa SDL_HAPTIC_LEFTRIGHT
|
||||||
* \sa SDL_HapticEffect
|
* \sa SDL_HapticEffect
|
||||||
@@ -668,7 +679,7 @@ typedef struct SDL_HapticLeftRight
|
|||||||
Uint16 type; /**< ::SDL_HAPTIC_LEFTRIGHT */
|
Uint16 type; /**< ::SDL_HAPTIC_LEFTRIGHT */
|
||||||
|
|
||||||
/* Replay */
|
/* Replay */
|
||||||
Uint32 length; /**< Duration of the effect. */
|
Uint32 length; /**< Duration of the effect in milliseconds. */
|
||||||
|
|
||||||
/* Rumble */
|
/* Rumble */
|
||||||
Uint16 large_magnitude; /**< Control of the large controller motor. */
|
Uint16 large_magnitude; /**< Control of the large controller motor. */
|
||||||
|
|||||||
@@ -262,6 +262,16 @@ extern "C" {
|
|||||||
*/
|
*/
|
||||||
#define SDL_HINT_GRAB_KEYBOARD "SDL_GRAB_KEYBOARD"
|
#define SDL_HINT_GRAB_KEYBOARD "SDL_GRAB_KEYBOARD"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* \brief A variable setting the double click time, in milliseconds.
|
||||||
|
*/
|
||||||
|
#define SDL_HINT_MOUSE_DOUBLE_CLICK_TIME "SDL_MOUSE_DOUBLE_CLICK_TIME"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* \brief A variable setting the double click radius, in pixels.
|
||||||
|
*/
|
||||||
|
#define SDL_HINT_MOUSE_DOUBLE_CLICK_RADIUS "SDL_MOUSE_DOUBLE_CLICK_RADIUS"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* \brief A variable setting the speed scale for mouse motion, in floating point, when the mouse is not in relative mode
|
* \brief A variable setting the speed scale for mouse motion, in floating point, when the mouse is not in relative mode
|
||||||
*/
|
*/
|
||||||
@@ -329,7 +339,7 @@ extern "C" {
|
|||||||
#define SDL_HINT_IDLE_TIMER_DISABLED "SDL_IOS_IDLE_TIMER_DISABLED"
|
#define SDL_HINT_IDLE_TIMER_DISABLED "SDL_IOS_IDLE_TIMER_DISABLED"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* \brief A variable controlling which orientations are allowed on iOS.
|
* \brief A variable controlling which orientations are allowed on iOS/Android.
|
||||||
*
|
*
|
||||||
* In some circumstances it is necessary to be able to explicitly control
|
* In some circumstances it is necessary to be able to explicitly control
|
||||||
* which UI orientations are allowed.
|
* which UI orientations are allowed.
|
||||||
@@ -465,6 +475,88 @@ extern "C" {
|
|||||||
*/
|
*/
|
||||||
#define SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS "SDL_JOYSTICK_ALLOW_BACKGROUND_EVENTS"
|
#define SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS "SDL_JOYSTICK_ALLOW_BACKGROUND_EVENTS"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* \brief A variable controlling whether the HIDAPI joystick drivers should be used.
|
||||||
|
*
|
||||||
|
* This variable can be set to the following values:
|
||||||
|
* "0" - HIDAPI drivers are not used
|
||||||
|
* "1" - HIDAPI drivers are used (the default)
|
||||||
|
*
|
||||||
|
* This variable is the default for all drivers, but can be overridden by the hints for specific drivers below.
|
||||||
|
*/
|
||||||
|
#define SDL_HINT_JOYSTICK_HIDAPI "SDL_JOYSTICK_HIDAPI"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* \brief A variable controlling whether the HIDAPI driver for PS4 controllers should be used.
|
||||||
|
*
|
||||||
|
* This variable can be set to the following values:
|
||||||
|
* "0" - HIDAPI driver is not used
|
||||||
|
* "1" - HIDAPI driver is used
|
||||||
|
*
|
||||||
|
* The default is the value of SDL_HINT_JOYSTICK_HIDAPI
|
||||||
|
*/
|
||||||
|
#define SDL_HINT_JOYSTICK_HIDAPI_PS4 "SDL_JOYSTICK_HIDAPI_PS4"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* \brief A variable controlling whether extended input reports should be used for PS4 controllers when using the HIDAPI driver.
|
||||||
|
*
|
||||||
|
* This variable can be set to the following values:
|
||||||
|
* "0" - extended reports are not enabled (the default)
|
||||||
|
* "1" - extended reports
|
||||||
|
*
|
||||||
|
* Extended input reports allow rumble on Bluetooth PS4 controllers, but
|
||||||
|
* break DirectInput handling for applications that don't use SDL.
|
||||||
|
*
|
||||||
|
* Once extended reports are enabled, they can not be disabled without
|
||||||
|
* power cycling the controller.
|
||||||
|
*/
|
||||||
|
#define SDL_HINT_JOYSTICK_HIDAPI_PS4_RUMBLE "SDL_JOYSTICK_HIDAPI_PS4_RUMBLE"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* \brief A variable controlling whether the HIDAPI driver for Steam Controllers should be used.
|
||||||
|
*
|
||||||
|
* This variable can be set to the following values:
|
||||||
|
* "0" - HIDAPI driver is not used
|
||||||
|
* "1" - HIDAPI driver is used
|
||||||
|
*
|
||||||
|
* The default is the value of SDL_HINT_JOYSTICK_HIDAPI
|
||||||
|
*/
|
||||||
|
#define SDL_HINT_JOYSTICK_HIDAPI_STEAM "SDL_JOYSTICK_HIDAPI_STEAM"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* \brief A variable controlling whether the HIDAPI driver for Nintendo Switch controllers should be used.
|
||||||
|
*
|
||||||
|
* This variable can be set to the following values:
|
||||||
|
* "0" - HIDAPI driver is not used
|
||||||
|
* "1" - HIDAPI driver is used
|
||||||
|
*
|
||||||
|
* The default is the value of SDL_HINT_JOYSTICK_HIDAPI
|
||||||
|
*/
|
||||||
|
#define SDL_HINT_JOYSTICK_HIDAPI_SWITCH "SDL_JOYSTICK_HIDAPI_SWITCH"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* \brief A variable controlling whether the HIDAPI driver for XBox controllers should be used.
|
||||||
|
*
|
||||||
|
* This variable can be set to the following values:
|
||||||
|
* "0" - HIDAPI driver is not used
|
||||||
|
* "1" - HIDAPI driver is used
|
||||||
|
*
|
||||||
|
* The default is the value of SDL_HINT_JOYSTICK_HIDAPI
|
||||||
|
*/
|
||||||
|
#define SDL_HINT_JOYSTICK_HIDAPI_XBOX "SDL_JOYSTICK_HIDAPI_XBOX"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* \brief A variable that controls whether Steam Controllers should be exposed using the SDL joystick and game controller APIs
|
||||||
|
*
|
||||||
|
* The variable can be set to the following values:
|
||||||
|
* "0" - Do not scan for Steam Controllers
|
||||||
|
* "1" - Scan for Steam Controllers (the default)
|
||||||
|
*
|
||||||
|
* The default value is "1". This hint must be set before initializing the joystick subsystem.
|
||||||
|
*/
|
||||||
|
#define SDL_HINT_ENABLE_STEAM_CONTROLLERS "SDL_ENABLE_STEAM_CONTROLLERS"
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* \brief If set to "0" then never set the top most bit on a SDL Window, even if the video mode expects it.
|
* \brief If set to "0" then never set the top most bit on a SDL Window, even if the video mode expects it.
|
||||||
* This is a debugging aid for developers and not expected to be used by end users. The default is "1"
|
* This is a debugging aid for developers and not expected to be used by end users. The default is "1"
|
||||||
@@ -527,6 +619,10 @@ extern "C" {
|
|||||||
* This is specially useful if you build SDL against a non glibc libc library (such as musl) which
|
* This is specially useful if you build SDL against a non glibc libc library (such as musl) which
|
||||||
* provides a relatively small default thread stack size (a few kilobytes versus the default 8MB glibc uses).
|
* provides a relatively small default thread stack size (a few kilobytes versus the default 8MB glibc uses).
|
||||||
* Support for this hint is currently available only in the pthread, Windows, and PSP backend.
|
* Support for this hint is currently available only in the pthread, Windows, and PSP backend.
|
||||||
|
*
|
||||||
|
* Instead of this hint, in 2.0.9 and later, you can use
|
||||||
|
* SDL_CreateThreadWithStackSize(). This hint only works with the classic
|
||||||
|
* SDL_CreateThread().
|
||||||
*/
|
*/
|
||||||
#define SDL_HINT_THREAD_STACK_SIZE "SDL_THREAD_STACK_SIZE"
|
#define SDL_HINT_THREAD_STACK_SIZE "SDL_THREAD_STACK_SIZE"
|
||||||
|
|
||||||
@@ -752,6 +848,23 @@ extern "C" {
|
|||||||
*/
|
*/
|
||||||
#define SDL_HINT_ANDROID_SEPARATE_MOUSE_AND_TOUCH "SDL_ANDROID_SEPARATE_MOUSE_AND_TOUCH"
|
#define SDL_HINT_ANDROID_SEPARATE_MOUSE_AND_TOUCH "SDL_ANDROID_SEPARATE_MOUSE_AND_TOUCH"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* \brief A variable to control whether we trap the Android back button to handle it manually.
|
||||||
|
* This is necessary for the right mouse button to work on some Android devices, or
|
||||||
|
* to be able to trap the back button for use in your code reliably. If set to true,
|
||||||
|
* the back button will show up as an SDL_KEYDOWN / SDL_KEYUP pair with a keycode of
|
||||||
|
* SDL_SCANCODE_AC_BACK.
|
||||||
|
*
|
||||||
|
* The variable can be set to the following values:
|
||||||
|
* "0" - Back button will be handled as usual for system. (default)
|
||||||
|
* "1" - Back button will be trapped, allowing you to handle the key press
|
||||||
|
* manually. (This will also let right mouse click work on systems
|
||||||
|
* where the right mouse button functions as back.)
|
||||||
|
*
|
||||||
|
* The value of this hint is used at runtime, so it can be changed at any time.
|
||||||
|
*/
|
||||||
|
#define SDL_HINT_ANDROID_TRAP_BACK_BUTTON "SDL_ANDROID_TRAP_BACK_BUTTON"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* \brief A variable to control whether the return key on the soft keyboard
|
* \brief A variable to control whether the return key on the soft keyboard
|
||||||
* should hide the soft keyboard on Android and iOS.
|
* should hide the soft keyboard on Android and iOS.
|
||||||
|
|||||||
Loaded 100 of 135 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user