Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
12b7f6b7a3 | ||
|
|
32e7ddf272 | ||
|
|
7091e0324e | ||
|
|
8f8532921c | ||
|
|
e9936992ec | ||
|
|
6375055b3e | ||
|
|
88b1ea296c | ||
|
|
2a13a869f3 | ||
|
|
89c342bb6f | ||
|
|
067f5e33aa | ||
|
|
33695f1c6b | ||
|
|
52ef84873e | ||
|
|
c21ba5c808 | ||
|
|
6cf408f12b | ||
|
|
8bdc2d285d | ||
|
|
a66fbcd6be | ||
|
|
93402a62fc | ||
|
|
2a56fb6c70 | ||
|
|
8276c031e9 | ||
|
|
102e5f6de9 | ||
|
|
1f7bdbfbcb | ||
|
|
f2fdd048c7 | ||
|
|
5a2cfc15a6 | ||
|
|
11c7131875 | ||
|
|
34aacc90d7 | ||
|
|
e75dce2543 | ||
|
|
e2774309fb | ||
|
|
dfefdc93c5 | ||
|
|
d46ea28bba | ||
|
|
d35c5bcaba | ||
|
|
2b14856d7f | ||
|
|
3316fdb80e | ||
|
|
c5d70c48c9 | ||
|
|
82230ef2f5 | ||
|
|
dcfd3d3e48 | ||
|
|
cb1e2ac32e | ||
|
|
ec2c15946a | ||
|
|
8be73142f8 | ||
|
|
d1640e3bb8 | ||
|
|
8612e6726d | ||
|
|
3b4694e9f8 | ||
|
|
14cb2e1999 | ||
|
|
cfabaf334e | ||
|
|
c054536fab | ||
|
|
35aac18b4e | ||
|
|
de3928cb4d | ||
|
|
9bf612b800 | ||
|
|
d9a8d67e3d | ||
|
|
99b0a56a6a | ||
|
|
61d7aa0400 | ||
|
|
1e98594972 | ||
|
|
c6383f042c | ||
|
|
b4d8e0e551 | ||
|
|
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 |
@@ -13,6 +13,9 @@ Any special steps that are required for the bug to appear.
|
|||||||
**Screenshots**
|
**Screenshots**
|
||||||
If applicable, add screenshots to help explain your problem. If the issue is related to video glitching or poor quality, please include screenshots.
|
If applicable, add screenshots to help explain your problem. If the issue is related to video glitching or poor quality, please include screenshots.
|
||||||
|
|
||||||
|
**Affected games**
|
||||||
|
List the games you've tried that exhibit the issue. To see if the issue is game-specific, try streaming Steam Big Picture with Moonlight and see if the issue persists there.
|
||||||
|
|
||||||
**Moonlight settings (please complete the following information)**
|
**Moonlight settings (please complete the following information)**
|
||||||
- Have any settings been adjusted from defaults?
|
- Have any settings been adjusted from defaults?
|
||||||
- If so, which settings have been changed?
|
- If so, which settings have been changed?
|
||||||
@@ -22,10 +25,15 @@ If applicable, add screenshots to help explain your problem. If the issue is rel
|
|||||||
- OS: [e.g. Windows 10 1803]
|
- OS: [e.g. Windows 10 1803]
|
||||||
- Moonlight Version: [e.g. v0.2.0]
|
- Moonlight Version: [e.g. v0.2.0]
|
||||||
|
|
||||||
**Logs (please attach)**
|
**Server PC details (please complete the following information)**
|
||||||
|
- OS: [e.g. Windows 10 1803]
|
||||||
|
- GeForce Experience version: [e.g. 3.16.0.140]
|
||||||
|
- Nvidia GPU driver: [e.g. 417.35]
|
||||||
|
|
||||||
|
**Moonlight Logs (please attach)**
|
||||||
- On Windows, `Moonlight-###.log` files can be found in `%TEMP%`. Simply type that into the File Explorer path field to navigate there.
|
- On Windows, `Moonlight-###.log` files can be found in `%TEMP%`. Simply type that into the File Explorer path field to navigate there.
|
||||||
- On macOS, `Moonlight-###.log` files can be found in `/tmp`. In Finder, press Cmd+Shift+G, then type `/tmp` to navigate there.
|
- On macOS, `Moonlight-###.log` files can be found in `/tmp`. In Finder, press Cmd+Shift+G, then type `/tmp` to navigate there.
|
||||||
- On Linux, logs print to the terminal when running via `flatpak run com.moonlight_stream.Moonlight`
|
- On Linux, logs print to the terminal when running via `flatpak run com.moonlight_stream.Moonlight`
|
||||||
|
|
||||||
**Additional context**
|
**Additional context**
|
||||||
Add any other context about the problem here.
|
Anything else you think may be relevant to the issue
|
||||||
|
|||||||
@@ -3,3 +3,5 @@
|
|||||||
|
|
||||||
**/.vs/
|
**/.vs/
|
||||||
build/
|
build/
|
||||||
|
config.tests/*/.qmake.stash
|
||||||
|
config.tests/*/Makefile
|
||||||
|
|||||||
@@ -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,52 @@ 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 Sierra (10.12) or later
|
||||||
|
* Xcode 10
|
||||||
|
* [create-dmg](https://github.com/sindresorhus/create-dmg) (only if building DMGs for use on non-development Macs)
|
||||||
|
|
||||||
|
### Linux-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 pulseaudio-libs-devel alsa-lib-devel`
|
||||||
|
* FFmpeg 4.0 is required to build. If your distro doesn't package FFmpeg 4.0, you can build and install it from source on http://ffmpeg.org/
|
||||||
|
|
||||||
|
### 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 can install Qt via Homebrew on macOS, but you will need to use `brew install qt --with-debug` to be able to create debug builds of Moonlight.
|
||||||
- On Mac, install the latest version of XCode
|
* You may also use your Linux distro's package manager for the Qt SDK as long as the packages are Qt 5.9 or later.
|
||||||
- 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`.
|
||||||
|
* To build from the command line for development use, run `qmake moonlight-qt.pro` then `make debug` or `make release`
|
||||||
|
|
||||||
## 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.
|
||||||
|
|||||||
@@ -23,9 +23,9 @@
|
|||||||
<key>NSSupportsAutomaticGraphicsSwitching</key>
|
<key>NSSupportsAutomaticGraphicsSwitching</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>0.4.0</string>
|
<string>VERSION</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>0.4.0</string>
|
<string>VERSION</string>
|
||||||
<key>CFBundleDisplayName</key>
|
<key>CFBundleDisplayName</key>
|
||||||
<string>Moonlight</string>
|
<string>Moonlight</string>
|
||||||
</dict>
|
</dict>
|
||||||
|
|||||||
@@ -14,6 +14,15 @@ CONFIG += debug_and_release
|
|||||||
# Ensure symbols are always generated
|
# Ensure symbols are always generated
|
||||||
CONFIG += force_debug_info
|
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
|
||||||
@@ -45,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
|
||||||
@@ -69,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 {
|
||||||
@@ -118,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
|
||||||
|
|
||||||
@@ -161,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
|
||||||
}
|
}
|
||||||
@@ -186,6 +225,13 @@ macx {
|
|||||||
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 \
|
||||||
@@ -211,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
|
||||||
@@ -218,19 +284,25 @@ 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
|
||||||
@@ -242,12 +314,22 @@ win32 {
|
|||||||
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
|
# Create Info.plist in object dir with the correct version string
|
||||||
|
system(cp $$PWD/Info.plist $$OUT_PWD/Info.plist)
|
||||||
|
system(sed -i -e 's/VERSION/$$cat(version.txt)/g' $$OUT_PWD/Info.plist)
|
||||||
|
|
||||||
APP_BUNDLE_RESOURCES.files = moonlight.icns
|
QMAKE_INFO_PLIST = $$OUT_PWD/Info.plist
|
||||||
|
|
||||||
|
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.4.0
|
VERSION = "$$cat(version.txt)"
|
||||||
DEFINES += VERSION_STR=\\\"0.4.0\\\"
|
DEFINES += VERSION_STR=\\\"$$cat(version.txt)\\\"
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -113,7 +116,8 @@ void AutoUpdateChecker::handleUpdateCheckRequestFinished(QNetworkReply* reply)
|
|||||||
if (currentVer < latestVer) {
|
if (currentVer < latestVer) {
|
||||||
qDebug() << "Update available";
|
qDebug() << "Update available";
|
||||||
|
|
||||||
emit onUpdateAvailable(updateObj["browser_url"].toString());
|
emit onUpdateAvailable(updateObj["version"].toString(),
|
||||||
|
updateObj["browser_url"].toString());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
else if (currentVer > latestVer) {
|
else if (currentVer > latestVer) {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ public:
|
|||||||
Q_INVOKABLE void start();
|
Q_INVOKABLE void start();
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void onUpdateAvailable(QString url);
|
void onUpdateAvailable(QString newVersion, QString url);
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void handleUpdateCheckRequestFinished(QNetworkReply* reply);
|
void handleUpdateCheckRequestFinished(QNetworkReply* reply);
|
||||||
|
|||||||
@@ -36,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
|
||||||
@@ -64,7 +97,7 @@ void BoxArtManager::handleBoxArtLoadComplete(NvComputer* computer, NvApp app, QU
|
|||||||
|
|
||||||
QUrl BoxArtManager::loadBoxArtFromNetwork(NvComputer* computer, int appId)
|
QUrl BoxArtManager::loadBoxArtFromNetwork(NvComputer* computer, int appId)
|
||||||
{
|
{
|
||||||
NvHTTP http(computer->activeAddress);
|
NvHTTP http(computer->activeAddress, computer->serverCert);
|
||||||
|
|
||||||
QString cachePath = getFilePathForBoxArt(computer, appId);
|
QString cachePath = getFilePathForBoxArt(computer, appId);
|
||||||
QImage image;
|
QImage image;
|
||||||
@@ -81,3 +114,5 @@ QUrl BoxArtManager::loadBoxArtFromNetwork(NvComputer* computer, int appId)
|
|||||||
|
|
||||||
return QUrl();
|
return QUrl();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#include "boxartmanager.moc"
|
||||||
|
|||||||
@@ -38,36 +38,3 @@ private:
|
|||||||
QDir m_BoxArtDir;
|
QDir m_BoxArtDir;
|
||||||
QThreadPool m_ThreadPool;
|
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;
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,283 +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, m_Computer->serverCert);
|
||||||
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, QSslCertificate());
|
||||||
{
|
|
||||||
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);
|
||||||
}
|
|
||||||
|
|
||||||
const quint16 WOL_PORTS[] = {
|
NvHTTP http(m_Computer->activeAddress, m_Computer->serverCert);
|
||||||
7, 9, // Standard WOL ports
|
|
||||||
47998, 47999, 48000, // Ports opened by GFE
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create the WoL payload
|
QVector<NvApp> appList;
|
||||||
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
|
try {
|
||||||
// and broadcast addresses for this link just in
|
appList = http.getAppList();
|
||||||
// case the host has timed out in ARP entries.
|
if (appList.isEmpty()) {
|
||||||
QVector<QString> addressList = uniqueAddresses();
|
return false;
|
||||||
addressList.append("255.255.255.255");
|
}
|
||||||
|
} catch (...) {
|
||||||
// Try all unique address strings or host names
|
return false;
|
||||||
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
|
QWriteLocker lock(&m_Computer->lock);
|
||||||
for (QHostAddress& address : hostInfo.addresses()) {
|
if (m_Computer->appList != appList) {
|
||||||
QUdpSocket sock;
|
m_Computer->appList = appList;
|
||||||
|
m_Computer->sortAppList();
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
// Bind to any address on the correct protocol
|
return true;
|
||||||
if (sock.bind(address.protocol() == QUdpSocket::IPv4Protocol ?
|
}
|
||||||
QHostAddress::AnyIPv4 : QHostAddress::AnyIPv6)) {
|
|
||||||
|
|
||||||
// Send to all ports
|
void run() override
|
||||||
for (quint16 port : WOL_PORTS) {
|
{
|
||||||
if (sock.writeDatagram(wolPayload, address, port)) {
|
// Always fetch the applist the first time
|
||||||
qInfo().nospace().noquote() << "Send WoL packet to " << name << " via " << address.toString() << ":" << port;
|
int pollsSinceLastAppListFetch = POLLS_PER_APPLIST_FETCH;
|
||||||
success = true;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tryPollComputer(address, stateChanged)) {
|
||||||
|
if (!wasOnline) {
|
||||||
|
qInfo() << m_Computer->name << "is now online at" << m_Computer->activeAddress;
|
||||||
|
}
|
||||||
|
online = true;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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),
|
||||||
@@ -355,7 +218,7 @@ void ComputerManager::saveHosts()
|
|||||||
settings.beginWriteArray(SER_HOSTS);
|
settings.beginWriteArray(SER_HOSTS);
|
||||||
for (int i = 0; i < m_KnownHosts.count(); i++) {
|
for (int i = 0; i < m_KnownHosts.count(); i++) {
|
||||||
settings.setArrayIndex(i);
|
settings.setArrayIndex(i);
|
||||||
m_KnownHosts[m_KnownHosts.keys()[i]]->serialize(settings);
|
m_KnownHosts.value(m_KnownHosts.keys()[i])->serialize(settings);
|
||||||
}
|
}
|
||||||
settings.endArray();
|
settings.endArray();
|
||||||
}
|
}
|
||||||
@@ -368,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);
|
||||||
@@ -388,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.value(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)
|
||||||
{
|
{
|
||||||
@@ -399,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);
|
||||||
@@ -415,9 +317,22 @@ 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.take(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();
|
||||||
|
|
||||||
@@ -430,9 +345,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -448,6 +362,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, m_Computer->serverCert);
|
||||||
|
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
|
||||||
@@ -456,6 +419,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, m_Computer->serverCert);
|
||||||
|
|
||||||
|
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);
|
||||||
@@ -503,6 +515,163 @@ 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:
|
||||||
|
QString fetchServerInfo(NvHTTP& http)
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return serverInfo;
|
||||||
|
} catch (...) {
|
||||||
|
if (!m_Mdns) {
|
||||||
|
emit computerAddCompleted(false);
|
||||||
|
}
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void run()
|
||||||
|
{
|
||||||
|
NvHTTP http(m_Address, QSslCertificate());
|
||||||
|
|
||||||
|
qInfo() << "Processing new PC at" << m_Address << "from" << (m_Mdns ? "mDNS" : "user");
|
||||||
|
|
||||||
|
// Perform initial serverinfo fetch over HTTP since we don't know which cert to use
|
||||||
|
QString serverInfo = fetchServerInfo(http);
|
||||||
|
if (serverInfo.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create initial newComputer using HTTP serverinfo with no pinned cert
|
||||||
|
NvComputer* newComputer = new NvComputer(m_Address, serverInfo, QSslCertificate());
|
||||||
|
|
||||||
|
// Check if we have a record of this host UUID to pull the pinned cert
|
||||||
|
NvComputer* existingComputer;
|
||||||
|
{
|
||||||
|
QReadLocker lock(&m_ComputerManager->m_Lock);
|
||||||
|
existingComputer = m_ComputerManager->m_KnownHosts.value(newComputer->uuid);
|
||||||
|
if (existingComputer != nullptr) {
|
||||||
|
http.setServerCert(existingComputer->serverCert);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch serverinfo again over HTTPS with the pinned cert
|
||||||
|
if (existingComputer != nullptr) {
|
||||||
|
serverInfo = fetchServerInfo(http);
|
||||||
|
if (serverInfo.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the polled computer with the HTTPS information
|
||||||
|
NvComputer httpsComputer(m_Address, serverInfo, QSslCertificate());
|
||||||
|
newComputer->update(httpsComputer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.moonlight-stream.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.value(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
|
||||||
@@ -511,37 +680,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,200 +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(NvHTTP::NvLogLevel::NONE);
|
|
||||||
} 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;
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tryPollComputer(address, stateChanged)) {
|
|
||||||
if (!wasOnline) {
|
|
||||||
qInfo() << m_Computer->name << "is now online at" << m_Computer->activeAddress;
|
|
||||||
}
|
|
||||||
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) {
|
|
||||||
qInfo() << m_Computer->name << "is now 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
|
||||||
@@ -299,188 +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);
|
|
||||||
|
|
||||||
qInfo() << "Processing new PC at" << m_Address << "from" << (m_Mdns ? "mDNS" : "user");
|
|
||||||
|
|
||||||
QString serverInfo;
|
|
||||||
try {
|
|
||||||
serverInfo = http.getServerInfo(NvHTTP::NvLogLevel::VERBOSE);
|
|
||||||
} 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) {
|
|
||||||
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;
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -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,313 @@
|
|||||||
|
#include "nvcomputer.h"
|
||||||
|
|
||||||
|
#include <QUdpSocket>
|
||||||
|
#include <QHostInfo>
|
||||||
|
#include <QNetworkInterface>
|
||||||
|
|
||||||
|
#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_SRVCERT "srvcert"
|
||||||
|
|
||||||
|
#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();
|
||||||
|
this->serverCert = QSslCertificate(settings.value(SER_SRVCERT).toByteArray());
|
||||||
|
|
||||||
|
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);
|
||||||
|
settings.setValue(SER_SRVCERT, serverCert.toPem());
|
||||||
|
|
||||||
|
// 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, QSslCertificate serverCert)
|
||||||
|
{
|
||||||
|
this->serverCert = serverCert;
|
||||||
|
|
||||||
|
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 to broadcast on all available NICs
|
||||||
|
for (const QNetworkInterface& nic : QNetworkInterface::allInterfaces()) {
|
||||||
|
// Ensure the interface is up and skip the loopback adapter
|
||||||
|
if ((nic.flags() & QNetworkInterface::IsUp) == 0 ||
|
||||||
|
(nic.flags() & QNetworkInterface::IsLoopBack) != 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
QHostAddress allNodesMulticast("FF02::1");
|
||||||
|
for (const QNetworkAddressEntry& addr : nic.addressEntries()) {
|
||||||
|
// Store the scope ID for this NIC if IPv6 is enabled
|
||||||
|
if (!addr.ip().scopeId().isEmpty()) {
|
||||||
|
allNodesMulticast.setScopeId(addr.ip().scopeId());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip IPv6 which doesn't support broadcast
|
||||||
|
if (!addr.broadcast().isNull()) {
|
||||||
|
addressList.append(addr.broadcast().toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!allNodesMulticast.scopeId().isEmpty()) {
|
||||||
|
addressList.append(allNodesMulticast.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
// Send to all ports
|
||||||
|
for (quint16 port : WOL_PORTS) {
|
||||||
|
if (sock.writeDatagram(wolPayload, address, port)) {
|
||||||
|
qInfo().nospace().noquote() << "Sent WoL packet to " << name << " via " << address.toString() << ":" << port;
|
||||||
|
success = true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
qWarning() << "Send failed:" << sock.error();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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; \
|
||||||
|
}
|
||||||
|
|
||||||
|
#define ASSIGN_IF_CHANGED_AND_NONNULL(field) \
|
||||||
|
if (!that.field.isNull() && \
|
||||||
|
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_NONNULL(serverCert);
|
||||||
|
ASSIGN_IF_CHANGED_AND_NONEMPTY(appList);
|
||||||
|
ASSIGN_IF_CHANGED_AND_NONEMPTY(displayModes);
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
#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, QSslCertificate serverCert);
|
||||||
|
|
||||||
|
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;
|
||||||
|
QSslCertificate serverCert;
|
||||||
|
QVector<NvApp> appList;
|
||||||
|
|
||||||
|
// Synchronization
|
||||||
|
QReadWriteLock lock;
|
||||||
|
};
|
||||||
@@ -10,11 +10,16 @@
|
|||||||
#include <QSslKey>
|
#include <QSslKey>
|
||||||
#include <QImageReader>
|
#include <QImageReader>
|
||||||
#include <QtEndian>
|
#include <QtEndian>
|
||||||
|
#include <QNetworkProxy>
|
||||||
|
|
||||||
#define REQUEST_TIMEOUT_MS 5000
|
#define REQUEST_TIMEOUT_MS 5000
|
||||||
|
#define LAUNCH_TIMEOUT_MS 120000
|
||||||
|
#define RESUME_TIMEOUT_MS 30000
|
||||||
|
#define QUIT_TIMEOUT_MS 30000
|
||||||
|
|
||||||
NvHTTP::NvHTTP(QString address) :
|
NvHTTP::NvHTTP(QString address, QSslCertificate serverCert) :
|
||||||
m_Address(address)
|
m_Address(address),
|
||||||
|
m_ServerCert(serverCert)
|
||||||
{
|
{
|
||||||
Q_ASSERT(!address.isEmpty());
|
Q_ASSERT(!address.isEmpty());
|
||||||
|
|
||||||
@@ -24,14 +29,29 @@ 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
void NvHTTP::setServerCert(QSslCertificate serverCert)
|
||||||
|
{
|
||||||
|
m_ServerCert = serverCert;
|
||||||
}
|
}
|
||||||
|
|
||||||
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());
|
||||||
@@ -62,36 +82,50 @@ NvHTTP::getServerInfo(NvLogLevel logLevel)
|
|||||||
{
|
{
|
||||||
QString serverInfo;
|
QString serverInfo;
|
||||||
|
|
||||||
try
|
// Check if we have a pinned cert for this host yet
|
||||||
|
if (!m_ServerCert.isNull())
|
||||||
{
|
{
|
||||||
// Always try HTTPS first, since it properly reports
|
try
|
||||||
// pairing status (and a few other attributes).
|
|
||||||
serverInfo = openConnectionToString(m_BaseUrlHttps,
|
|
||||||
"serverinfo",
|
|
||||||
nullptr,
|
|
||||||
true,
|
|
||||||
logLevel);
|
|
||||||
// Throws if the request failed
|
|
||||||
verifyResponseStatus(serverInfo);
|
|
||||||
}
|
|
||||||
catch (const GfeHttpResponseException& e)
|
|
||||||
{
|
|
||||||
if (e.getStatusCode() == 401)
|
|
||||||
{
|
{
|
||||||
// Certificate validation error, fallback to HTTP
|
// Always try HTTPS first, since it properly reports
|
||||||
serverInfo = openConnectionToString(m_BaseUrlHttp,
|
// pairing status (and a few other attributes).
|
||||||
|
serverInfo = openConnectionToString(m_BaseUrlHttps,
|
||||||
"serverinfo",
|
"serverinfo",
|
||||||
nullptr,
|
nullptr,
|
||||||
true,
|
REQUEST_TIMEOUT_MS,
|
||||||
logLevel);
|
logLevel);
|
||||||
|
// Throws if the request failed
|
||||||
verifyResponseStatus(serverInfo);
|
verifyResponseStatus(serverInfo);
|
||||||
}
|
}
|
||||||
else
|
catch (const GfeHttpResponseException& e)
|
||||||
{
|
{
|
||||||
// Rethrow real errors
|
if (e.getStatusCode() == 401)
|
||||||
throw e;
|
{
|
||||||
|
// Certificate validation error, fallback to HTTP
|
||||||
|
serverInfo = openConnectionToString(m_BaseUrlHttp,
|
||||||
|
"serverinfo",
|
||||||
|
nullptr,
|
||||||
|
REQUEST_TIMEOUT_MS,
|
||||||
|
logLevel);
|
||||||
|
verifyResponseStatus(serverInfo);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Rethrow real errors
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Only use HTTP prior to pairing
|
||||||
|
serverInfo = openConnectionToString(m_BaseUrlHttp,
|
||||||
|
"serverinfo",
|
||||||
|
nullptr,
|
||||||
|
REQUEST_TIMEOUT_MS,
|
||||||
|
logLevel);
|
||||||
|
verifyResponseStatus(serverInfo);
|
||||||
|
}
|
||||||
|
|
||||||
return serverInfo;
|
return serverInfo;
|
||||||
}
|
}
|
||||||
@@ -149,7 +183,7 @@ NvHTTP::launchApp(int appId,
|
|||||||
"&surroundAudioInfo="+getSurroundAudioInfoString(streamConfig->audioConfiguration)+
|
"&surroundAudioInfo="+getSurroundAudioInfoString(streamConfig->audioConfiguration)+
|
||||||
"&remoteControllersBitmap="+QString::number(gamepadMask)+
|
"&remoteControllersBitmap="+QString::number(gamepadMask)+
|
||||||
"&gcmap="+QString::number(gamepadMask),
|
"&gcmap="+QString::number(gamepadMask),
|
||||||
false);
|
LAUNCH_TIMEOUT_MS);
|
||||||
|
|
||||||
// Throws if the request failed
|
// Throws if the request failed
|
||||||
verifyResponseStatus(response);
|
verifyResponseStatus(response);
|
||||||
@@ -169,7 +203,7 @@ NvHTTP::resumeApp(PSTREAM_CONFIGURATION streamConfig)
|
|||||||
"rikey="+QString(QByteArray(streamConfig->remoteInputAesKey, sizeof(streamConfig->remoteInputAesKey)).toHex())+
|
"rikey="+QString(QByteArray(streamConfig->remoteInputAesKey, sizeof(streamConfig->remoteInputAesKey)).toHex())+
|
||||||
"&rikeyid="+QString::number(riKeyId)+
|
"&rikeyid="+QString::number(riKeyId)+
|
||||||
"&surroundAudioInfo="+getSurroundAudioInfoString(streamConfig->audioConfiguration),
|
"&surroundAudioInfo="+getSurroundAudioInfoString(streamConfig->audioConfiguration),
|
||||||
false);
|
RESUME_TIMEOUT_MS);
|
||||||
|
|
||||||
// Throws if the request failed
|
// Throws if the request failed
|
||||||
verifyResponseStatus(response);
|
verifyResponseStatus(response);
|
||||||
@@ -182,7 +216,7 @@ NvHTTP::quitApp()
|
|||||||
openConnectionToString(m_BaseUrlHttps,
|
openConnectionToString(m_BaseUrlHttps,
|
||||||
"cancel",
|
"cancel",
|
||||||
nullptr,
|
nullptr,
|
||||||
false);
|
QUIT_TIMEOUT_MS);
|
||||||
|
|
||||||
// Throws if the request failed
|
// Throws if the request failed
|
||||||
verifyResponseStatus(response);
|
verifyResponseStatus(response);
|
||||||
@@ -229,7 +263,7 @@ NvHTTP::getAppList()
|
|||||||
QString appxml = openConnectionToString(m_BaseUrlHttps,
|
QString appxml = openConnectionToString(m_BaseUrlHttps,
|
||||||
"applist",
|
"applist",
|
||||||
nullptr,
|
nullptr,
|
||||||
true,
|
REQUEST_TIMEOUT_MS,
|
||||||
NvLogLevel::ERROR);
|
NvLogLevel::ERROR);
|
||||||
verifyResponseStatus(appxml);
|
verifyResponseStatus(appxml);
|
||||||
|
|
||||||
@@ -297,7 +331,7 @@ NvHTTP::getBoxArt(int appId)
|
|||||||
"appasset",
|
"appasset",
|
||||||
"appid="+QString::number(appId)+
|
"appid="+QString::number(appId)+
|
||||||
"&AssetType=2&AssetIdx=0",
|
"&AssetType=2&AssetIdx=0",
|
||||||
true,
|
REQUEST_TIMEOUT_MS,
|
||||||
NvLogLevel::VERBOSE);
|
NvLogLevel::VERBOSE);
|
||||||
QImage image = QImageReader(reply).read();
|
QImage image = QImageReader(reply).read();
|
||||||
delete reply;
|
delete reply;
|
||||||
@@ -344,10 +378,10 @@ QString
|
|||||||
NvHTTP::openConnectionToString(QUrl baseUrl,
|
NvHTTP::openConnectionToString(QUrl baseUrl,
|
||||||
QString command,
|
QString command,
|
||||||
QString arguments,
|
QString arguments,
|
||||||
bool enableTimeout,
|
int timeoutMs,
|
||||||
NvLogLevel logLevel)
|
NvLogLevel logLevel)
|
||||||
{
|
{
|
||||||
QNetworkReply* reply = openConnection(baseUrl, command, arguments, enableTimeout, logLevel);
|
QNetworkReply* reply = openConnection(baseUrl, command, arguments, timeoutMs, logLevel);
|
||||||
QString ret;
|
QString ret;
|
||||||
|
|
||||||
QTextStream stream(reply);
|
QTextStream stream(reply);
|
||||||
@@ -362,14 +396,18 @@ QNetworkReply*
|
|||||||
NvHTTP::openConnection(QUrl baseUrl,
|
NvHTTP::openConnection(QUrl baseUrl,
|
||||||
QString command,
|
QString command,
|
||||||
QString arguments,
|
QString arguments,
|
||||||
bool enableTimeout,
|
int timeoutMs,
|
||||||
NvLogLevel logLevel)
|
NvLogLevel logLevel)
|
||||||
{
|
{
|
||||||
// Build a URL for the request
|
// Build a URL for the request
|
||||||
QUrl url(baseUrl);
|
QUrl url(baseUrl);
|
||||||
url.setPath("/" + command);
|
url.setPath("/" + command);
|
||||||
url.setQuery("uniqueid=" + IdentityManager::get()->getUniqueId() +
|
|
||||||
"&uuid=" + QUuid::createUuid().toRfc4122().toHex() +
|
// Use a common UID for Moonlight clients to allow them to quit
|
||||||
|
// games for each other (otherwise GFE gets screwed up and it requires
|
||||||
|
// manual intervention to solve).
|
||||||
|
url.setQuery("uniqueid=0123456789ABCDEF&uuid=" +
|
||||||
|
QUuid::createUuid().toRfc4122().toHex() +
|
||||||
((arguments != nullptr) ? ("&" + arguments) : ""));
|
((arguments != nullptr) ? ("&" + arguments) : ""));
|
||||||
|
|
||||||
QNetworkRequest request = QNetworkRequest(url);
|
QNetworkRequest request = QNetworkRequest(url);
|
||||||
@@ -379,15 +417,28 @@ NvHTTP::openConnection(QUrl baseUrl,
|
|||||||
|
|
||||||
QNetworkReply* reply = m_Nam.get(request);
|
QNetworkReply* reply = m_Nam.get(request);
|
||||||
|
|
||||||
// Ignore self-signed certificate errors (since GFE uses them)
|
// Assert that we always have a server cert for HTTPS, since
|
||||||
reply->ignoreSslErrors();
|
// the request will fail anyway if we do not.
|
||||||
|
Q_ASSERT(!m_ServerCert.isNull() || baseUrl == m_BaseUrlHttp);
|
||||||
|
|
||||||
|
if (!m_ServerCert.isNull()) {
|
||||||
|
// Pin the server certificate received during pairing
|
||||||
|
QList<QSslError> expectedSslErrors;
|
||||||
|
expectedSslErrors.append(QSslError(QSslError::HostNameMismatch, m_ServerCert));
|
||||||
|
expectedSslErrors.append(QSslError(QSslError::SelfSignedCertificate, m_ServerCert));
|
||||||
|
|
||||||
|
// The SecureTransport backend for Qt TLS on macOS throws CertificateUntrusted
|
||||||
|
// instead of SelfSignedCertificate, so we will need to allow that error too.
|
||||||
|
expectedSslErrors.append(QSslError(QSslError::CertificateUntrusted, m_ServerCert));
|
||||||
|
|
||||||
|
reply->ignoreSslErrors(expectedSslErrors);
|
||||||
|
}
|
||||||
|
|
||||||
// Run the request with a timeout if requested
|
// Run the request with a timeout if requested
|
||||||
QEventLoop loop;
|
QEventLoop loop;
|
||||||
QObject::connect(reply, SIGNAL(finished()), &loop, SLOT(quit()));
|
QObject::connect(reply, SIGNAL(finished()), &loop, SLOT(quit()));
|
||||||
if (enableTimeout)
|
if (timeoutMs) {
|
||||||
{
|
QTimer::singleShot(timeoutMs, &loop, SLOT(quit()));
|
||||||
QTimer::singleShot(REQUEST_TIMEOUT_MS, &loop, SLOT(quit()));
|
|
||||||
}
|
}
|
||||||
if (logLevel >= NvLogLevel::VERBOSE) {
|
if (logLevel >= NvLogLevel::VERBOSE) {
|
||||||
qInfo() << "Executing request:" << url.toString();
|
qInfo() << "Executing request:" << url.toString();
|
||||||
@@ -413,9 +464,24 @@ NvHTTP::openConnection(QUrl baseUrl,
|
|||||||
if (logLevel >= NvLogLevel::ERROR) {
|
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());
|
|
||||||
delete reply;
|
if (reply->error() == QNetworkReply::SslHandshakeFailedError) {
|
||||||
throw exception;
|
// This will trigger falling back to HTTP for the serverinfo query
|
||||||
|
// then pairing again to get the updated certificate.
|
||||||
|
GfeHttpResponseException exception(401, "Server certificate mismatch");
|
||||||
|
delete reply;
|
||||||
|
throw exception;
|
||||||
|
}
|
||||||
|
else if (reply->error() == QNetworkReply::OperationCanceledError) {
|
||||||
|
QtNetworkReplyException exception(QNetworkReply::TimeoutError, "Request timed out");
|
||||||
|
delete reply;
|
||||||
|
throw exception;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
QtNetworkReplyException exception(reply->error(), reply->errorString());
|
||||||
|
delete reply;
|
||||||
|
throw exception;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return reply;
|
return reply;
|
||||||
|
|||||||
@@ -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,6 +78,41 @@ 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:
|
||||||
@@ -86,7 +122,7 @@ public:
|
|||||||
VERBOSE
|
VERBOSE
|
||||||
};
|
};
|
||||||
|
|
||||||
explicit NvHTTP(QString address);
|
explicit NvHTTP(QString address, QSslCertificate serverCert);
|
||||||
|
|
||||||
static
|
static
|
||||||
int
|
int
|
||||||
@@ -113,9 +149,11 @@ public:
|
|||||||
openConnectionToString(QUrl baseUrl,
|
openConnectionToString(QUrl baseUrl,
|
||||||
QString command,
|
QString command,
|
||||||
QString arguments,
|
QString arguments,
|
||||||
bool enableTimeout,
|
int timeoutMs,
|
||||||
NvLogLevel logLevel = NvLogLevel::VERBOSE);
|
NvLogLevel logLevel = NvLogLevel::VERBOSE);
|
||||||
|
|
||||||
|
void setServerCert(QSslCertificate serverCert);
|
||||||
|
|
||||||
static
|
static
|
||||||
QVector<int>
|
QVector<int>
|
||||||
parseQuad(QString quad);
|
parseQuad(QString quad);
|
||||||
@@ -150,9 +188,10 @@ private:
|
|||||||
openConnection(QUrl baseUrl,
|
openConnection(QUrl baseUrl,
|
||||||
QString command,
|
QString command,
|
||||||
QString arguments,
|
QString arguments,
|
||||||
bool enableTimeout,
|
int timeoutMs,
|
||||||
NvLogLevel logLevel);
|
NvLogLevel logLevel);
|
||||||
|
|
||||||
QString m_Address;
|
QString m_Address;
|
||||||
QNetworkAccessManager m_Nam;
|
QNetworkAccessManager m_Nam;
|
||||||
|
QSslCertificate m_ServerCert;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,8 +8,10 @@
|
|||||||
#include <openssl/x509.h>
|
#include <openssl/x509.h>
|
||||||
#include <openssl/evp.h>
|
#include <openssl/evp.h>
|
||||||
|
|
||||||
|
#define REQUEST_TIMEOUT_MS 5000
|
||||||
|
|
||||||
NvPairingManager::NvPairingManager(QString address) :
|
NvPairingManager::NvPairingManager(QString address) :
|
||||||
m_Http(address)
|
m_Http(address, QSslCertificate())
|
||||||
{
|
{
|
||||||
QByteArray cert = IdentityManager::get()->getCertificate();
|
QByteArray cert = IdentityManager::get()->getCertificate();
|
||||||
BIO *bio = BIO_new_mem_buf(cert.data(), -1);
|
BIO *bio = BIO_new_mem_buf(cert.data(), -1);
|
||||||
@@ -161,7 +163,7 @@ NvPairingManager::saltPin(QByteArray salt, QString pin)
|
|||||||
}
|
}
|
||||||
|
|
||||||
NvPairingManager::PairState
|
NvPairingManager::PairState
|
||||||
NvPairingManager::pair(QString appVersion, QString pin)
|
NvPairingManager::pair(QString appVersion, QString pin, QSslCertificate& serverCert)
|
||||||
{
|
{
|
||||||
int serverMajorVersion = NvHTTP::parseQuad(appVersion).at(0);
|
int serverMajorVersion = NvHTTP::parseQuad(appVersion).at(0);
|
||||||
qInfo() << "Pairing with server generation:" << serverMajorVersion;
|
qInfo() << "Pairing with server generation:" << serverMajorVersion;
|
||||||
@@ -192,7 +194,7 @@ NvPairingManager::pair(QString appVersion, QString pin)
|
|||||||
"pair",
|
"pair",
|
||||||
"devicename=roth&updateState=1&phrase=getservercert&salt=" +
|
"devicename=roth&updateState=1&phrase=getservercert&salt=" +
|
||||||
salt.toHex() + "&clientcert=" + IdentityManager::get()->getCertificate().toHex(),
|
salt.toHex() + "&clientcert=" + IdentityManager::get()->getCertificate().toHex(),
|
||||||
false);
|
0);
|
||||||
NvHTTP::verifyResponseStatus(getCert);
|
NvHTTP::verifyResponseStatus(getCert);
|
||||||
if (NvHTTP::getXmlString(getCert, "paired") != "1")
|
if (NvHTTP::getXmlString(getCert, "paired") != "1")
|
||||||
{
|
{
|
||||||
@@ -200,26 +202,38 @@ NvPairingManager::pair(QString appVersion, QString pin)
|
|||||||
return PairState::FAILED;
|
return PairState::FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
QByteArray serverCert = NvHTTP::getXmlStringFromHex(getCert, "plaincert");
|
QByteArray serverCertStr = NvHTTP::getXmlStringFromHex(getCert, "plaincert");
|
||||||
if (serverCert == nullptr)
|
if (serverCertStr == nullptr)
|
||||||
{
|
{
|
||||||
qCritical() << "Server likely already pairing";
|
qCritical() << "Server likely already pairing";
|
||||||
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
|
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, REQUEST_TIMEOUT_MS);
|
||||||
return PairState::ALREADY_IN_PROGRESS;
|
return PairState::ALREADY_IN_PROGRESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
serverCert = QSslCertificate(serverCertStr);
|
||||||
|
if (serverCert.isNull()) {
|
||||||
|
Q_ASSERT(!serverCert.isNull());
|
||||||
|
|
||||||
|
qCritical() << "Failed to parse plaincert";
|
||||||
|
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, REQUEST_TIMEOUT_MS);
|
||||||
|
return PairState::FAILED;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pin this cert for TLS
|
||||||
|
m_Http.setServerCert(serverCert);
|
||||||
|
|
||||||
QByteArray randomChallenge = generateRandomBytes(16);
|
QByteArray randomChallenge = generateRandomBytes(16);
|
||||||
QByteArray encryptedChallenge = encrypt(randomChallenge, &encKey);
|
QByteArray encryptedChallenge = encrypt(randomChallenge, &encKey);
|
||||||
QString challengeXml = m_Http.openConnectionToString(m_Http.m_BaseUrlHttp,
|
QString challengeXml = m_Http.openConnectionToString(m_Http.m_BaseUrlHttp,
|
||||||
"pair",
|
"pair",
|
||||||
"devicename=roth&updateState=1&clientchallenge=" +
|
"devicename=roth&updateState=1&clientchallenge=" +
|
||||||
encryptedChallenge.toHex(),
|
encryptedChallenge.toHex(),
|
||||||
true);
|
REQUEST_TIMEOUT_MS);
|
||||||
NvHTTP::verifyResponseStatus(challengeXml);
|
NvHTTP::verifyResponseStatus(challengeXml);
|
||||||
if (NvHTTP::getXmlString(challengeXml, "paired") != "1")
|
if (NvHTTP::getXmlString(challengeXml, "paired") != "1")
|
||||||
{
|
{
|
||||||
qCritical() << "Failed pairing at stage #2";
|
qCritical() << "Failed pairing at stage #2";
|
||||||
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
|
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, REQUEST_TIMEOUT_MS);
|
||||||
return PairState::FAILED;
|
return PairState::FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -247,12 +261,12 @@ NvPairingManager::pair(QString appVersion, QString pin)
|
|||||||
"pair",
|
"pair",
|
||||||
"devicename=roth&updateState=1&serverchallengeresp=" +
|
"devicename=roth&updateState=1&serverchallengeresp=" +
|
||||||
encryptedChallengeResponseHash.toHex(),
|
encryptedChallengeResponseHash.toHex(),
|
||||||
true);
|
REQUEST_TIMEOUT_MS);
|
||||||
NvHTTP::verifyResponseStatus(respXml);
|
NvHTTP::verifyResponseStatus(respXml);
|
||||||
if (NvHTTP::getXmlString(respXml, "paired") != "1")
|
if (NvHTTP::getXmlString(respXml, "paired") != "1")
|
||||||
{
|
{
|
||||||
qCritical() << "Failed pairing at stage #3";
|
qCritical() << "Failed pairing at stage #3";
|
||||||
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
|
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, REQUEST_TIMEOUT_MS);
|
||||||
return PairState::FAILED;
|
return PairState::FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -262,21 +276,21 @@ NvPairingManager::pair(QString appVersion, QString pin)
|
|||||||
|
|
||||||
if (!verifySignature(serverSecret,
|
if (!verifySignature(serverSecret,
|
||||||
serverSignature,
|
serverSignature,
|
||||||
serverCert))
|
serverCertStr))
|
||||||
{
|
{
|
||||||
qCritical() << "MITM detected";
|
qCritical() << "MITM detected";
|
||||||
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
|
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, REQUEST_TIMEOUT_MS);
|
||||||
return PairState::FAILED;
|
return PairState::FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
QByteArray expectedResponseData;
|
QByteArray expectedResponseData;
|
||||||
expectedResponseData.append(randomChallenge);
|
expectedResponseData.append(randomChallenge);
|
||||||
expectedResponseData.append(getSignatureFromPemCert(serverCert));
|
expectedResponseData.append(getSignatureFromPemCert(serverCertStr));
|
||||||
expectedResponseData.append(serverSecret);
|
expectedResponseData.append(serverSecret);
|
||||||
if (QCryptographicHash::hash(expectedResponseData, hashAlgo) != serverResponse)
|
if (QCryptographicHash::hash(expectedResponseData, hashAlgo) != serverResponse)
|
||||||
{
|
{
|
||||||
qCritical() << "Incorrect PIN";
|
qCritical() << "Incorrect PIN";
|
||||||
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
|
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, REQUEST_TIMEOUT_MS);
|
||||||
return PairState::PIN_WRONG;
|
return PairState::PIN_WRONG;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -288,24 +302,24 @@ NvPairingManager::pair(QString appVersion, QString pin)
|
|||||||
"pair",
|
"pair",
|
||||||
"devicename=roth&updateState=1&clientpairingsecret=" +
|
"devicename=roth&updateState=1&clientpairingsecret=" +
|
||||||
clientPairingSecret.toHex(),
|
clientPairingSecret.toHex(),
|
||||||
true);
|
REQUEST_TIMEOUT_MS);
|
||||||
NvHTTP::verifyResponseStatus(secretRespXml);
|
NvHTTP::verifyResponseStatus(secretRespXml);
|
||||||
if (NvHTTP::getXmlString(secretRespXml, "paired") != "1")
|
if (NvHTTP::getXmlString(secretRespXml, "paired") != "1")
|
||||||
{
|
{
|
||||||
qCritical() << "Failed pairing at stage #4";
|
qCritical() << "Failed pairing at stage #4";
|
||||||
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
|
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, REQUEST_TIMEOUT_MS);
|
||||||
return PairState::FAILED;
|
return PairState::FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
QString pairChallengeXml = m_Http.openConnectionToString(m_Http.m_BaseUrlHttps,
|
QString pairChallengeXml = m_Http.openConnectionToString(m_Http.m_BaseUrlHttps,
|
||||||
"pair",
|
"pair",
|
||||||
"devicename=roth&updateState=1&phrase=pairchallenge",
|
"devicename=roth&updateState=1&phrase=pairchallenge",
|
||||||
true);
|
REQUEST_TIMEOUT_MS);
|
||||||
NvHTTP::verifyResponseStatus(pairChallengeXml);
|
NvHTTP::verifyResponseStatus(pairChallengeXml);
|
||||||
if (NvHTTP::getXmlString(pairChallengeXml, "paired") != "1")
|
if (NvHTTP::getXmlString(pairChallengeXml, "paired") != "1")
|
||||||
{
|
{
|
||||||
qCritical() << "Failed pairing at stage #5";
|
qCritical() << "Failed pairing at stage #5";
|
||||||
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, true);
|
m_Http.openConnectionToString(m_Http.m_BaseUrlHttp, "unpair", nullptr, REQUEST_TIMEOUT_MS);
|
||||||
return PairState::FAILED;
|
return PairState::FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ public:
|
|||||||
~NvPairingManager();
|
~NvPairingManager();
|
||||||
|
|
||||||
PairState
|
PairState
|
||||||
pair(QString appVersion, QString pin);
|
pair(QString appVersion, QString pin, QSslCertificate& serverCert);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QByteArray
|
QByteArray
|
||||||
|
|||||||
@@ -0,0 +1,414 @@
|
|||||||
|
#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.addToggleOption("frame-pacing", "frame pacing");
|
||||||
|
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 --frame-pacing and --no-frame-pacing options
|
||||||
|
preferences->framePacing = parser.getToggleOptionValue("frame-pacing", preferences->framePacing);
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
@@ -4,14 +4,14 @@
|
|||||||
<metadata_license>CC0-1.0</metadata_license>
|
<metadata_license>CC0-1.0</metadata_license>
|
||||||
<project_license>GPL-3.0+</project_license>
|
<project_license>GPL-3.0+</project_license>
|
||||||
<name>Moonlight</name>
|
<name>Moonlight</name>
|
||||||
<summary>Stream games from your NVIDIA GameStream-enabled PC</summary>
|
<summary>Play games remotely from your NVIDIA GameStream-enabled PC</summary>
|
||||||
|
|
||||||
<description>
|
<description>
|
||||||
<p>Moonlight can stream games and other applications from a PC with an NVIDIA GeForce GTX 600-series or higher GPU and GeForce Experience installed.</p>
|
<p>Moonlight can stream games and other applications from a PC with an NVIDIA GeForce GTX 600-series or higher GPU and GeForce Experience installed.</p>
|
||||||
<p>Features include:</p>
|
<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,168 @@
|
|||||||
</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.8.1" date="2019-01-08">
|
||||||
|
<description>
|
||||||
|
<p>New features:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Stricter adherence to the specified bitrate when streaming over the Internet</li>
|
||||||
|
</ul>
|
||||||
|
<p>Bugfixes:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Fixed audio crackling that impacted certain systems</li>
|
||||||
|
<li>Fixed Wake-on-LAN packets failing to send on some systems</li>
|
||||||
|
<li>Reduced audio latency when resuming a session where audio is already playing</li>
|
||||||
|
<li>Added option to enable the old SDL audio renderer by setting ML_AUDIO=SDL</li>
|
||||||
|
<li>Updated to use the new Qt 5.12 Flatpak runtime</li>
|
||||||
|
<li>Updated to FFmpeg 4.1</li>
|
||||||
|
<li>Minor UI tweaks</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<release version="0.8.0" date="2018-12-25">
|
||||||
|
<description>
|
||||||
|
<p>New features:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Improved security of pairing - After this update, you must re-pair with your PC!</li>
|
||||||
|
<li>Enabled quitting games started by other Moonlight clients also running this update</li>
|
||||||
|
</ul>
|
||||||
|
<p>Bugfixes:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Fixed the video bitrate slider not working above certain values on GFE 3.16</li>
|
||||||
|
<li>Fixed broken Bluetooth audio with PulseAudio</li>
|
||||||
|
<li>Reduced audio latency</li>
|
||||||
|
<li>Fixed some non-gamepad devices triggering the unmapped gamepad dialog box</li>
|
||||||
|
<li>Minor UI tweaks</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
|
<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">
|
<release version="0.4.0" date="2018-09-08">
|
||||||
<description>
|
<description>
|
||||||
<p>New features:</p>
|
<p>New features:</p>
|
||||||
@@ -118,7 +266,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>
|
||||||
|
|
||||||
|
|||||||
@@ -3,14 +3,16 @@ 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
|
||||||
@@ -29,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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,8 +60,9 @@ GridView {
|
|||||||
|
|
||||||
model: appModel
|
model: appModel
|
||||||
|
|
||||||
delegate: ItemDelegate {
|
delegate: NavigableItemDelegate {
|
||||||
width: 200; height: 335;
|
width: 200; height: 335;
|
||||||
|
grid: appGrid
|
||||||
|
|
||||||
Image {
|
Image {
|
||||||
id: appIcon
|
id: appIcon
|
||||||
@@ -59,6 +73,9 @@ GridView {
|
|||||||
width: 150
|
width: 150
|
||||||
height: 200
|
height: 200
|
||||||
}
|
}
|
||||||
|
width: sourceSize.width
|
||||||
|
height: sourceSize.height
|
||||||
|
fillMode: Image.Pad
|
||||||
}
|
}
|
||||||
|
|
||||||
Image {
|
Image {
|
||||||
@@ -72,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
|
||||||
@@ -83,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()
|
||||||
@@ -91,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
|
||||||
}
|
}
|
||||||
@@ -109,22 +133,33 @@ GridView {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
acceptedButtons: Qt.RightButton
|
acceptedButtons: Qt.RightButton
|
||||||
onClicked: {
|
onClicked: {
|
||||||
// Right click
|
// popup() ensures the menu appears under the mouse cursor
|
||||||
appContextMenu.open()
|
if (appContextMenu.popup) {
|
||||||
|
appContextMenu.popup()
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Qt 5.9 doesn't have popup()
|
||||||
|
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()
|
||||||
@@ -132,7 +167,6 @@ GridView {
|
|||||||
quitAppDialog.open()
|
quitAppDialog.open()
|
||||||
}
|
}
|
||||||
visible: model.running
|
visible: model.running
|
||||||
height: visible ? implicitHeight : 0
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -140,18 +174,21 @@ GridView {
|
|||||||
MessageDialog {
|
MessageDialog {
|
||||||
id: quitAppDialog
|
id: quitAppDialog
|
||||||
modality:Qt.WindowModal
|
modality:Qt.WindowModal
|
||||||
property string appName : "";
|
property string appName : ""
|
||||||
property bool segueToStream : false
|
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."
|
text:"Are you sure you want to quit " + appName +"? Any unsaved progress will be lost."
|
||||||
standardButtons: StandardButton.Yes | StandardButton.No
|
standardButtons: StandardButton.Yes | StandardButton.No
|
||||||
onYes: {
|
|
||||||
|
function quitApp() {
|
||||||
var component = Qt.createComponent("QuitSegue.qml")
|
var component = Qt.createComponent("QuitSegue.qml")
|
||||||
var params = {"appName": appName}
|
var params = {"appName": appName}
|
||||||
if (segueToStream) {
|
if (segueToStream) {
|
||||||
// Store the session and app name if we're going to stream after
|
// Store the session and app name if we're going to stream after
|
||||||
// successfully quitting the old app.
|
// successfully quitting the old app.
|
||||||
params.nextAppName = model.name
|
params.nextAppName = nextAppName
|
||||||
params.nextSession = appModel.createSessionForApp(index)
|
params.nextSession = appModel.createSessionForApp(nextAppIndex)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
params.nextAppName = null
|
params.nextAppName = null
|
||||||
@@ -163,6 +200,11 @@ GridView {
|
|||||||
// Trigger the quit after pushing the quit segue on screen
|
// Trigger the quit after pushing the quit segue on screen
|
||||||
appModel.quitRunningApp()
|
appModel.quitRunningApp()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onYes: quitApp()
|
||||||
|
|
||||||
|
// For keyboard/gamepad navigation
|
||||||
|
onAccepted: quitApp()
|
||||||
}
|
}
|
||||||
|
|
||||||
ScrollBar.vertical: ScrollBar {
|
ScrollBar.vertical: ScrollBar {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,11 +8,14 @@ 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
|
||||||
@@ -30,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)
|
||||||
@@ -57,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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -72,8 +84,9 @@ GridView {
|
|||||||
|
|
||||||
model: computerModel
|
model: computerModel
|
||||||
|
|
||||||
delegate: ItemDelegate {
|
delegate: NavigableItemDelegate {
|
||||||
width: 300; height: 300;
|
width: 300; height: 300;
|
||||||
|
grid: pcGrid
|
||||||
|
|
||||||
Image {
|
Image {
|
||||||
id: pcIcon
|
id: pcIcon
|
||||||
@@ -111,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
|
||||||
@@ -125,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
|
||||||
@@ -170,6 +181,7 @@ GridView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (!model.online) {
|
} else if (!model.online) {
|
||||||
|
// Using open() here because it may be activated by keyboard
|
||||||
pcContextMenu.open()
|
pcContextMenu.open()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -178,12 +190,26 @@ GridView {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
acceptedButtons: Qt.RightButton;
|
acceptedButtons: Qt.RightButton;
|
||||||
onClicked: {
|
onClicked: {
|
||||||
// right click
|
if (!model.addPc) {
|
||||||
if (!model.addPc) { // but only for actual PCs, not the add-pc option
|
// popup() ensures the menu appears under the mouse cursor
|
||||||
pcContextMenu.open()
|
if (pcContextMenu.popup) {
|
||||||
|
pcContextMenu.popup()
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Qt 5.9 doesn't have popup()
|
||||||
|
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 {
|
||||||
@@ -191,31 +217,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");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,10 +244,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 {
|
||||||
|
|||||||
@@ -65,7 +65,6 @@ Item {
|
|||||||
verticalAlignment: Text.AlignVCenter
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
|
||||||
wrapMode: Text.Wrap
|
wrapMode: Text.Wrap
|
||||||
color: "white"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
@@ -37,22 +73,20 @@ ScrollView {
|
|||||||
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
|
||||||
@@ -124,7 +158,6 @@ ScrollView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
id: resolutionComboBox
|
id: resolutionComboBox
|
||||||
width: 200
|
|
||||||
textRole: "text"
|
textRole: "text"
|
||||||
model: ListModel {
|
model: ListModel {
|
||||||
id: resolutionListModel
|
id: resolutionListModel
|
||||||
@@ -167,7 +200,7 @@ ScrollView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ComboBox {
|
AutoResizingComboBox {
|
||||||
function createModel() {
|
function createModel() {
|
||||||
var fpsListModel = Qt.createQmlObject('import QtQuick 2.0; ListModel {}', parent, '')
|
var fpsListModel = Qt.createQmlObject('import QtQuick 2.0; ListModel {}', parent, '')
|
||||||
|
|
||||||
@@ -191,7 +224,16 @@ ScrollView {
|
|||||||
// 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
|
// Add unsupported FPS values that come after the display max FPS
|
||||||
@@ -232,7 +274,6 @@ ScrollView {
|
|||||||
|
|
||||||
id: fpsComboBox
|
id: fpsComboBox
|
||||||
textRole: "text"
|
textRole: "text"
|
||||||
width: 250
|
|
||||||
// ::onActivated must be used, as it only listens for when the index is changed by a human
|
// ::onActivated must be used, as it only listens for when the index is changed by a human
|
||||||
onActivated : {
|
onActivated : {
|
||||||
var selectedFps = parseInt(model.get(currentIndex).video_fps)
|
var selectedFps = parseInt(model.get(currentIndex).video_fps)
|
||||||
@@ -254,21 +295,18 @@ 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 {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
id: bitrateDesc
|
id: bitrateDesc
|
||||||
text: qsTr("Lower bitrate to reduce lag and stuttering. Raise bitrate to increase image quality.")
|
text: qsTr("Lower the bitrate on slower connections. Raise the bitrate to increase image quality.")
|
||||||
font.pointSize: 9
|
font.pointSize: 9
|
||||||
wrapMode: Text.Wrap
|
wrapMode: Text.Wrap
|
||||||
color: "white"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Slider {
|
Slider {
|
||||||
id: slider
|
id: slider
|
||||||
wheelEnabled: true
|
|
||||||
|
|
||||||
value: prefs.bitrateKbps
|
value: prefs.bitrateKbps
|
||||||
|
|
||||||
@@ -291,10 +329,9 @@ ScrollView {
|
|||||||
text: qsTr("Display mode")
|
text: qsTr("Display mode")
|
||||||
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 savedWm = prefs.windowMode
|
var savedWm = prefs.windowMode
|
||||||
@@ -310,7 +347,7 @@ ScrollView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
id: windowModeComboBox
|
id: windowModeComboBox
|
||||||
width: Math.min(bitrateDesc.implicitWidth, parent.width)
|
hoverEnabled: true
|
||||||
textRole: "text"
|
textRole: "text"
|
||||||
model: ListModel {
|
model: ListModel {
|
||||||
id: windowModeListModel
|
id: windowModeListModel
|
||||||
@@ -330,16 +367,43 @@ ScrollView {
|
|||||||
onActivated: {
|
onActivated: {
|
||||||
prefs.windowMode = windowModeListModel.get(currentIndex).val
|
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 {
|
CheckBox {
|
||||||
id: vsyncCheck
|
id: vsyncCheck
|
||||||
text: "<font color=\"white\">Enable V-Sync</font>"
|
hoverEnabled: true
|
||||||
|
text: "V-Sync"
|
||||||
font.pointSize: 12
|
font.pointSize: 12
|
||||||
checked: prefs.enableVsync
|
checked: prefs.enableVsync
|
||||||
onCheckedChanged: {
|
onCheckedChanged: {
|
||||||
prefs.enableVsync = checked
|
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"
|
||||||
|
}
|
||||||
|
|
||||||
|
CheckBox {
|
||||||
|
id: framePacingCheck
|
||||||
|
hoverEnabled: true
|
||||||
|
text: "Frame pacing"
|
||||||
|
font.pointSize: 12
|
||||||
|
enabled: prefs.enableVsync
|
||||||
|
checked: prefs.enableVsync && prefs.framePacing
|
||||||
|
onCheckedChanged: {
|
||||||
|
prefs.framePacing = checked
|
||||||
|
}
|
||||||
|
ToolTip.delay: 1000
|
||||||
|
ToolTip.timeout: 5000
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
ToolTip.text: "Frame pacing reduces micro-stutter by delaying frames that come in too early"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -362,10 +426,9 @@ 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
|
||||||
@@ -381,25 +444,16 @@ ScrollView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
id: audioComboBox
|
id: audioComboBox
|
||||||
width: Math.min(bitrateDesc.implicitWidth, parent.width)
|
|
||||||
textRole: "text"
|
textRole: "text"
|
||||||
model: ListModel {
|
model: ListModel {
|
||||||
id: audioListModel
|
id: audioListModel
|
||||||
// Detection of audio channels only works on Windows
|
|
||||||
// and even so, it's still unreliable because audio devices
|
|
||||||
// lie about how many channels they can support. Hide the
|
|
||||||
// option to autodetect until this is resolved.
|
|
||||||
/*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
|
||||||
@@ -410,6 +464,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 {
|
||||||
@@ -422,7 +499,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 {
|
||||||
@@ -430,13 +507,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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -454,7 +553,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: {
|
||||||
@@ -464,7 +563,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: {
|
||||||
@@ -491,10 +590,9 @@ 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
|
||||||
@@ -510,7 +608,6 @@ ScrollView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
id: decoderComboBox
|
id: decoderComboBox
|
||||||
width: Math.min(bitrateDesc.implicitWidth, parent.width)
|
|
||||||
textRole: "text"
|
textRole: "text"
|
||||||
model: ListModel {
|
model: ListModel {
|
||||||
id: decoderListModel
|
id: decoderListModel
|
||||||
@@ -539,10 +636,9 @@ 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
|
||||||
@@ -558,7 +654,6 @@ ScrollView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
id: codecComboBox
|
id: codecComboBox
|
||||||
width: Math.min(bitrateDesc.implicitWidth, parent.width)
|
|
||||||
textRole: "text"
|
textRole: "text"
|
||||||
model: ListModel {
|
model: ListModel {
|
||||||
id: codecListModel
|
id: codecListModel
|
||||||
@@ -590,7 +685,7 @@ ScrollView {
|
|||||||
|
|
||||||
CheckBox {
|
CheckBox {
|
||||||
id: unlockUnsupportedFps
|
id: unlockUnsupportedFps
|
||||||
text: "<font color=\"white\">Unlock unsupported FPS options</font>"
|
text: "Unlock unsupported FPS options"
|
||||||
font.pointSize: 12
|
font.pointSize: 12
|
||||||
checked: prefs.unsupportedFps
|
checked: prefs.unsupportedFps
|
||||||
onCheckedChanged: {
|
onCheckedChanged: {
|
||||||
@@ -601,6 +696,36 @@ ScrollView {
|
|||||||
fpsComboBox.reinitialize()
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,12 +3,15 @@ import QtQuick.Controls 2.2
|
|||||||
import QtQuick.Dialogs 1.2
|
import QtQuick.Dialogs 1.2
|
||||||
import QtQuick.Window 2.2
|
import QtQuick.Window 2.2
|
||||||
|
|
||||||
|
import ComputerManager 1.0
|
||||||
|
import SdlGamepadKeyNavigation 1.0
|
||||||
import 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
|
||||||
|
|
||||||
@@ -42,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)
|
||||||
@@ -57,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(Screen.virtualX, Screen.virtualY)
|
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
|
||||||
}
|
}
|
||||||
@@ -100,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()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -153,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>
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,7 +185,7 @@ ApplicationWindow {
|
|||||||
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
|
||||||
|
|
||||||
@@ -85,6 +202,10 @@ ApplicationWindow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onClicked: stackView.pop()
|
onClicked: stackView.pop()
|
||||||
|
|
||||||
|
Keys.onDownPressed: {
|
||||||
|
stackView.currentItem.forceActiveFocus(Qt.TabFocus)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
@@ -97,7 +218,7 @@ ApplicationWindow {
|
|||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
}
|
}
|
||||||
|
|
||||||
ToolButton {
|
NavigableToolButton {
|
||||||
property string browserUrl: ""
|
property string browserUrl: ""
|
||||||
|
|
||||||
id: updateButton
|
id: updateButton
|
||||||
@@ -114,7 +235,6 @@ ApplicationWindow {
|
|||||||
ToolTip.delay: 1000
|
ToolTip.delay: 1000
|
||||||
ToolTip.timeout: 3000
|
ToolTip.timeout: 3000
|
||||||
ToolTip.visible: hovered
|
ToolTip.visible: hovered
|
||||||
ToolTip.text: "Update available for Moonlight"
|
|
||||||
|
|
||||||
// Invisible until we get a callback notifying us that
|
// Invisible until we get a callback notifying us that
|
||||||
// an update is available
|
// an update is available
|
||||||
@@ -122,9 +242,9 @@ ApplicationWindow {
|
|||||||
|
|
||||||
onClicked: Qt.openUrlExternally(browserUrl);
|
onClicked: Qt.openUrlExternally(browserUrl);
|
||||||
|
|
||||||
|
function updateAvailable(version, url)
|
||||||
function updateAvailable(url)
|
|
||||||
{
|
{
|
||||||
|
ToolTip.text = "Update available for Moonlight: Version " + version
|
||||||
updateButton.browserUrl = url
|
updateButton.browserUrl = url
|
||||||
updateButton.visible = true
|
updateButton.visible = true
|
||||||
}
|
}
|
||||||
@@ -133,9 +253,15 @@ ApplicationWindow {
|
|||||||
AutoUpdateChecker.onUpdateAvailable.connect(updateAvailable)
|
AutoUpdateChecker.onUpdateAvailable.connect(updateAvailable)
|
||||||
AutoUpdateChecker.start()
|
AutoUpdateChecker.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Keys.onDownPressed: {
|
||||||
|
stackView.currentItem.forceActiveFocus(Qt.TabFocus)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ToolButton {
|
NavigableToolButton {
|
||||||
|
id: helpButton
|
||||||
|
|
||||||
Image {
|
Image {
|
||||||
source: "qrc:/res/question_mark.svg"
|
source: "qrc:/res/question_mark.svg"
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
@@ -148,13 +274,23 @@ ApplicationWindow {
|
|||||||
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");
|
||||||
|
|
||||||
|
Keys.onDownPressed: {
|
||||||
|
stackView.currentItem.forceActiveFocus(Qt.TabFocus)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ToolButton {
|
NavigableToolButton {
|
||||||
// TODO: Implement gamepad mapping then unhide this button
|
// TODO: Implement gamepad mapping then unhide this button
|
||||||
visible: false
|
visible: false
|
||||||
|
|
||||||
@@ -173,9 +309,15 @@ ApplicationWindow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onClicked: navigateTo("qrc:/gui/GamepadMapper.qml", "Gamepad Mapping")
|
onClicked: navigateTo("qrc:/gui/GamepadMapper.qml", "Gamepad Mapping")
|
||||||
|
|
||||||
|
Keys.onDownPressed: {
|
||||||
|
stackView.currentItem.forceActiveFocus(Qt.TabFocus)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ToolButton {
|
NavigableToolButton {
|
||||||
|
id: settingsButton
|
||||||
|
|
||||||
Image {
|
Image {
|
||||||
source: "qrc:/res/settings.svg"
|
source: "qrc:/res/settings.svg"
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
@@ -187,11 +329,85 @@ ApplicationWindow {
|
|||||||
|
|
||||||
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+")") : "")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
||||||
@@ -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
|
||||||
@@ -15,12 +17,20 @@
|
|||||||
#include "streaming/video/ffmpeg.h"
|
#include "streaming/video/ffmpeg.h"
|
||||||
#endif
|
#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
|
||||||
@@ -126,6 +136,14 @@ void qtLogToDiskHandler(QtMsgType type, const QMessageLogContext&, const QString
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HACK: Avoid printing this internal Qt warning which seems to print every time we
|
||||||
|
// start our event loop for the QNetworkReply. I have found this warning in many
|
||||||
|
// unrelated bug reports, but none actually tackle this warning itself. It seems to be
|
||||||
|
// new in Qt 5.12.
|
||||||
|
if (msg.startsWith("QNetworkReplyHttpImplPrivate::_q_startOperation was called more than once")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
QTime logTime = QTime::fromMSecsSinceStartOfDay(s_LoggerTime.elapsed());
|
QTime logTime = QTime::fromMSecsSinceStartOfDay(s_LoggerTime.elapsed());
|
||||||
QString txt = QString("%1 - Qt %2: %3\n").arg(logTime.toString()).arg(typeTxt).arg(msg);
|
QString txt = QString("%1 - Qt %2: %3\n").arg(logTime.toString()).arg(typeTxt).arg(msg);
|
||||||
|
|
||||||
@@ -213,6 +231,9 @@ LONG WINAPI UnhandledExceptionHandler(struct _EXCEPTION_POINTERS *ExceptionInfo)
|
|||||||
|
|
||||||
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.
|
// Set these here to allow us to use the default QSettings constructor.
|
||||||
// These also ensure that our cache directory is named correctly. As such,
|
// These also ensure that our cache directory is named correctly. As such,
|
||||||
// it is critical that these be called before Path::initialize().
|
// it is critical that these be called before Path::initialize().
|
||||||
@@ -259,6 +280,12 @@ int main(int argc, char *argv[])
|
|||||||
SetUnhandledExceptionFilter(UnhandledExceptionHandler);
|
SetUnhandledExceptionFilter(UnhandledExceptionHandler);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifdef Q_OS_WIN32
|
||||||
|
// Force AntiHooking.dll to be statically imported and loaded
|
||||||
|
// by ntdll by calling a dummy function.
|
||||||
|
AntiHookingDummyImport();
|
||||||
|
#endif
|
||||||
|
|
||||||
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
|
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
|
||||||
|
|
||||||
// This avoids using the default keychain for SSL, which may cause
|
// This avoids using the default keychain for SSL, which may cause
|
||||||
@@ -266,11 +293,21 @@ int main(int argc, char *argv[])
|
|||||||
qputenv("QT_SSL_USE_TEMPORARY_KEYCHAIN", QByteArray("1"));
|
qputenv("QT_SSL_USE_TEMPORARY_KEYCHAIN", QByteArray("1"));
|
||||||
|
|
||||||
#ifdef Q_OS_WIN32
|
#ifdef Q_OS_WIN32
|
||||||
// On Windows, use ANGLE so we don't have to load both DX and OGL
|
if (!qEnvironmentVariableIsSet("QT_OPENGL")) {
|
||||||
// user-mode drivers into our app.
|
// On Windows, use ANGLE so we don't have to load OpenGL
|
||||||
qputenv("QT_OPENGL", "angle");
|
// user-mode drivers into our app. OGL drivers (especially Intel)
|
||||||
|
// seem to crash Moonlight far more often than DirectX.
|
||||||
|
qputenv("QT_OPENGL", "angle");
|
||||||
|
}
|
||||||
#endif
|
#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");
|
||||||
|
|
||||||
@@ -282,6 +319,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",
|
||||||
@@ -296,19 +334,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.
|
||||||
@@ -316,7 +390,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;
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 345 KiB After Width: | Height: | Size: 328 KiB |
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 2.6 KiB |
@@ -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,69 +1 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
<?xml version="1.0" standalone="no"?><!-- Generator: Gravit.io --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="isolation:isolate" viewBox="0 0 256 256" width="256" height="256"><g><circle vector-effect="non-scaling-stroke" cx="128" cy="128" r="128" fill="rgb(86,92,100)"/><circle vector-effect="non-scaling-stroke" cx="128.00000000000003" cy="128.00000000000003" r="96" fill="rgb(255,255,255)"/><path d=" M 137.66 132 L 224 132 L 224 124 L 137.66 124 L 198.71 62.95 L 193.05 57.29 L 132 118.34 L 132 32 L 124 32 L 124 118.34 L 62.95 57.29 L 57.29 62.95 L 118.34 124 L 32 124 L 32 132 L 118.34 132 L 57.29 193.05 L 62.95 198.71 L 124 137.66 L 124 224 L 132 224 L 132 137.66 L 193.05 198.71 L 198.71 193.05 L 137.66 132 Z " fill-rule="evenodd" fill="rgb(86,92,100)"/></g></svg>
|
||||||
<svg
|
|
||||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
|
||||||
xmlns:cc="http://creativecommons.org/ns#"
|
|
||||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
id="svg3761"
|
|
||||||
viewBox="0 0 680 680"
|
|
||||||
height="680"
|
|
||||||
width="680"
|
|
||||||
version="1.0">
|
|
||||||
<metadata
|
|
||||||
id="metadata3767">
|
|
||||||
<rdf:RDF>
|
|
||||||
<cc:Work
|
|
||||||
rdf:about="">
|
|
||||||
<dc:format>image/svg+xml</dc:format>
|
|
||||||
<dc:type
|
|
||||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
|
||||||
<dc:title></dc:title>
|
|
||||||
</cc:Work>
|
|
||||||
</rdf:RDF>
|
|
||||||
</metadata>
|
|
||||||
<defs
|
|
||||||
id="defs3765" />
|
|
||||||
<g
|
|
||||||
id="g3759"
|
|
||||||
transform="translate(-135,491) scale(0.100000,-0.100000)">
|
|
||||||
<circle
|
|
||||||
id="circle3741"
|
|
||||||
fill="#565C64"
|
|
||||||
r="3400"
|
|
||||||
cy="1505"
|
|
||||||
cx="4755" />
|
|
||||||
<path
|
|
||||||
id="path3743"
|
|
||||||
style="fill:#ffffff"
|
|
||||||
d="M4550 4230 c-582 -48 -1123 -271 -1568 -646 l-84 -70 869 -869 c477 -477 871 -868 876 -868 4 0 6 552 5 1228 l-3 1227 -25 1 c-14 0 -45 -1 -70 -3z" />
|
|
||||||
<path
|
|
||||||
id="path3745"
|
|
||||||
style="fill:#ffffff"
|
|
||||||
d="M4867 4230 c-10 -9 -9 -2453 1 -2453 4 0 398 391 875 868 l869 869 -84 70 c-344 290 -749 492 -1178 587 -166 36 -469 74 -483 59z" />
|
|
||||||
<path
|
|
||||||
id="path3747"
|
|
||||||
style="fill:#ffffff"
|
|
||||||
d="M2673 3275 c-241 -285 -412 -595 -526 -953 -63 -196 -111 -458 -119 -640 l-3 -70 1228 -3 c675 -1 1227 1 1227 5 0 5 -391 399 -868 876 l-869 869 -70 -84z" />
|
|
||||||
<path
|
|
||||||
id="path3749"
|
|
||||||
style="fill:#ffffff"
|
|
||||||
d="M5898 2490 c-478 -477 -868 -871 -868 -876 0 -4 552 -6 1228 -5 l1227 3 -3 70 c-14 326 -124 729 -287 1055 -91 182 -259 430 -387 573 l-43 48 -867 -868z" />
|
|
||||||
<path
|
|
||||||
id="path3751"
|
|
||||||
style="fill:#ffffff"
|
|
||||||
d="M2026 1388 c-7 -11 10 -198 30 -321 76 -486 291 -952 617 -1338 l70 -84 869 869 c477 477 868 871 868 875 0 11 -2448 10 -2454 -1z" />
|
|
||||||
<path
|
|
||||||
id="path3753"
|
|
||||||
style="fill:#ffffff"
|
|
||||||
d="M5030 1390 c0 -5 391 -399 868 -876 l869 -869 70 84 c241 285 412 595 526 953 63 196 111 458 119 640 l3 70 -1227 3 c-676 1 -1228 -1 -1228 -5z" />
|
|
||||||
<path
|
|
||||||
id="path3755"
|
|
||||||
style="fill:#ffffff"
|
|
||||||
d="M3767 359 l-869 -869 84 -70 c285 -241 595 -412 953 -526 196 -63 458 -111 640 -119 l70 -3 3 1228 c1 675 -1 1227 -5 1227 -5 0 -399 -391 -876 -868z" />
|
|
||||||
<path
|
|
||||||
id="path3757"
|
|
||||||
style="fill:#ffffff"
|
|
||||||
d="M4862 0 l3 -1228 70 3 c182 8 444 56 640 119 358 114 668 285 953 526 l84 70 -869 869 c-477 477 -871 868 -876 868 -4 0 -6 -552 -5 -1227z" />
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 820 B |
@@ -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" fill="#0BF200">
|
<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: 499 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,5 @@
|
|||||||
#include "streamingpreferences.h"
|
#include "streamingpreferences.h"
|
||||||
#include "streaming/session.hpp"
|
#include "streaming/session.h"
|
||||||
#include "streaming/streamutils.h"
|
#include "streaming/streamutils.h"
|
||||||
|
|
||||||
#include <QSettings>
|
#include <QSettings>
|
||||||
@@ -19,8 +19,14 @@
|
|||||||
#define SER_VIDEODEC "videodec"
|
#define SER_VIDEODEC "videodec"
|
||||||
#define SER_WINDOWMODE "windowmode"
|
#define SER_WINDOWMODE "windowmode"
|
||||||
#define SER_UNSUPPORTEDFPS "unsupportedfps"
|
#define SER_UNSUPPORTEDFPS "unsupportedfps"
|
||||||
|
#define SER_MDNS "mdns"
|
||||||
|
#define SER_QUITAPPAFTER "quitAppAfter"
|
||||||
|
#define SER_MOUSEACCELERATION "mouseacceleration"
|
||||||
|
#define SER_STARTWINDOWED "startwindowed"
|
||||||
|
#define SER_FRAMEPACING "framepacing"
|
||||||
|
|
||||||
StreamingPreferences::StreamingPreferences()
|
StreamingPreferences::StreamingPreferences(QObject *parent)
|
||||||
|
: QObject(parent)
|
||||||
{
|
{
|
||||||
reload();
|
reload();
|
||||||
}
|
}
|
||||||
@@ -38,8 +44,13 @@ void StreamingPreferences::reload()
|
|||||||
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();
|
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();
|
||||||
|
framePacing = settings.value(SER_FRAMEPACING, false).toBool();
|
||||||
audioConfig = static_cast<AudioConfig>(settings.value(SER_AUDIOCFG,
|
audioConfig = static_cast<AudioConfig>(settings.value(SER_AUDIOCFG,
|
||||||
static_cast<int>(AudioConfig::AC_FORCE_STEREO)).toInt());
|
static_cast<int>(AudioConfig::AC_STEREO)).toInt());
|
||||||
videoCodecConfig = static_cast<VideoCodecConfig>(settings.value(SER_VIDEOCFG,
|
videoCodecConfig = static_cast<VideoCodecConfig>(settings.value(SER_VIDEOCFG,
|
||||||
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,
|
||||||
@@ -63,6 +74,11 @@ void StreamingPreferences::save()
|
|||||||
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_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_FRAMEPACING, framePacing);
|
||||||
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));
|
||||||
@@ -83,6 +99,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
|
||||||
@@ -112,9 +142,7 @@ int StreamingPreferences::getMaximumStreamingFrameRate()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cap the frame rate at 120 FPS. Past this, the encoders start
|
maxFrameRate = qMax(maxFrameRate, bestMode.refresh_rate);
|
||||||
// to max out and drop frames.
|
|
||||||
maxFrameRate = qMax(maxFrameRate, qMin(120, bestMode.refresh_rate));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,19 +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 getDesktopResolution(int displayIndex);
|
Q_INVOKABLE QRect getDesktopResolution(int displayIndex);
|
||||||
|
|
||||||
Q_INVOKABLE QRect getNativeResolution(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)
|
||||||
|
|
||||||
@@ -69,6 +72,11 @@ public:
|
|||||||
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 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(bool framePacing MEMBER framePacing NOTIFY framePacingChanged)
|
||||||
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)
|
||||||
@@ -84,6 +92,11 @@ public:
|
|||||||
bool playAudioOnHost;
|
bool playAudioOnHost;
|
||||||
bool multiController;
|
bool multiController;
|
||||||
bool unsupportedFps;
|
bool unsupportedFps;
|
||||||
|
bool enableMdns;
|
||||||
|
bool quitAppAfter;
|
||||||
|
bool mouseAcceleration;
|
||||||
|
bool startWindowed;
|
||||||
|
bool framePacing;
|
||||||
AudioConfig audioConfig;
|
AudioConfig audioConfig;
|
||||||
VideoCodecConfig videoCodecConfig;
|
VideoCodecConfig videoCodecConfig;
|
||||||
VideoDecoderSelection videoDecoderSelection;
|
VideoDecoderSelection videoDecoderSelection;
|
||||||
@@ -97,9 +110,14 @@ signals:
|
|||||||
void playAudioOnHostChanged();
|
void playAudioOnHostChanged();
|
||||||
void multiControllerChanged();
|
void multiControllerChanged();
|
||||||
void unsupportedFpsChanged();
|
void unsupportedFpsChanged();
|
||||||
|
void enableMdnsChanged();
|
||||||
|
void quitAppAfterChanged();
|
||||||
|
void mouseAccelerationChanged();
|
||||||
void audioConfigChanged();
|
void audioConfigChanged();
|
||||||
void videoCodecConfigChanged();
|
void videoCodecConfigChanged();
|
||||||
void videoDecoderSelectionChanged();
|
void videoDecoderSelectionChanged();
|
||||||
void windowModeChanged();
|
void windowModeChanged();
|
||||||
|
void startWindowedChanged();
|
||||||
|
void framePacingChanged();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,284 +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;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool Session::testAudio(int audioConfiguration)
|
|
||||||
{
|
|
||||||
SDL_AudioSpec want, have;
|
|
||||||
SDL_AudioDeviceID dev;
|
|
||||||
bool ret;
|
|
||||||
|
|
||||||
if (SDL_InitSubSystem(SDL_INIT_AUDIO) != 0) {
|
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Audio test - SDL_InitSubSystem(SDL_INIT_AUDIO) failed: %s",
|
|
||||||
SDL_GetError());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
SDL_zero(want);
|
|
||||||
want.freq = 48000;
|
|
||||||
want.format = AUDIO_S16;
|
|
||||||
want.samples = SAMPLES_PER_FRAME;
|
|
||||||
|
|
||||||
switch (audioConfiguration) {
|
|
||||||
case AUDIO_CONFIGURATION_STEREO:
|
|
||||||
want.channels = 2;
|
|
||||||
break;
|
|
||||||
case AUDIO_CONFIGURATION_51_SURROUND:
|
|
||||||
want.channels = 6;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
SDL_assert(false);
|
|
||||||
ret = false;
|
|
||||||
goto Exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test audio device for functionality
|
|
||||||
dev = SDL_OpenAudioDevice(NULL, 0, &want, &have, 0);
|
|
||||||
if (dev == 0) {
|
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Audio test - Failed to open audio device: %s",
|
|
||||||
SDL_GetError());
|
|
||||||
ret = false;
|
|
||||||
goto Exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
SDL_CloseAudioDevice(dev);
|
|
||||||
|
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Audio test - Successful with %d channels",
|
|
||||||
want.channels);
|
|
||||||
|
|
||||||
ret = true;
|
|
||||||
|
|
||||||
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 -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
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,154 @@
|
|||||||
|
#include "../session.h"
|
||||||
|
#include "renderers/renderer.h"
|
||||||
|
|
||||||
|
#ifdef HAVE_SOUNDIO
|
||||||
|
#include "renderers/soundioaudiorenderer.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include "renderers/sdl.h"
|
||||||
|
|
||||||
|
#include <Limelight.h>
|
||||||
|
|
||||||
|
IAudioRenderer* Session::createAudioRenderer()
|
||||||
|
{
|
||||||
|
#ifdef HAVE_SOUNDIO
|
||||||
|
if (qgetenv("ML_AUDIO") == "SDL") {
|
||||||
|
return new SdlAudioRenderer();
|
||||||
|
}
|
||||||
|
|
||||||
|
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,19 @@
|
|||||||
|
#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;
|
||||||
|
};
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
#include "sdl.h"
|
||||||
|
|
||||||
|
#include <Limelight.h>
|
||||||
|
#include <SDL.h>
|
||||||
|
|
||||||
|
#include <QtGlobal>
|
||||||
|
|
||||||
|
SdlAudioRenderer::SdlAudioRenderer()
|
||||||
|
: m_AudioDevice(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);
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
{
|
||||||
|
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,463 @@
|
|||||||
|
#include "soundioaudiorenderer.h"
|
||||||
|
|
||||||
|
#include <SDL.h>
|
||||||
|
|
||||||
|
#include <QtGlobal>
|
||||||
|
|
||||||
|
// GFE sends us packets in 5 ms chunks
|
||||||
|
const double SoundIoAudioRenderer::k_RawSampleLengthSec = 0.005;
|
||||||
|
|
||||||
|
#ifdef Q_OS_LINUX
|
||||||
|
// PulseAudio and ALSA require more than just 5 ms samples
|
||||||
|
// for some reason, so write a minimum of 25 ms each time to
|
||||||
|
// prevent underruns on Bluetooth.
|
||||||
|
// https://github.com/moonlight-stream/moonlight-qt/issues/147
|
||||||
|
// https://github.com/moonlight-stream/moonlight-qt/issues/157
|
||||||
|
const double SoundIoAudioRenderer::k_MinSampleLengthSec = 0.025;
|
||||||
|
#else
|
||||||
|
// This determines the size of the buffers we'll
|
||||||
|
// get from CoreAudio. It is also the minimum
|
||||||
|
// size that we will write when called to fill a buffer.
|
||||||
|
const double SoundIoAudioRenderer::k_MinSampleLengthSec = k_RawSampleLengthSec;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
SoundIoAudioRenderer::SoundIoAudioRenderer()
|
||||||
|
: m_OpusChannelCount(0),
|
||||||
|
m_SoundIo(nullptr),
|
||||||
|
m_Device(nullptr),
|
||||||
|
m_OutputStream(nullptr),
|
||||||
|
m_RingBuffer(nullptr),
|
||||||
|
m_Latency(0),
|
||||||
|
m_Errored(false)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
SoundIoAudioRenderer::~SoundIoAudioRenderer()
|
||||||
|
{
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Audio latency: %f",
|
||||||
|
m_Latency);
|
||||||
|
|
||||||
|
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_MinSampleLengthSec;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int packetsToBuffer;
|
||||||
|
|
||||||
|
#ifdef Q_OS_LINUX
|
||||||
|
// PulseAudio and ALSA need the large buffer (see comment on k_MinSampleLengthSec),
|
||||||
|
// so we need a buffer at least double that size to allow packets to arrive
|
||||||
|
// while we're writing to the sink.
|
||||||
|
packetsToBuffer = (int)(k_MinSampleLengthSec / k_RawSampleLengthSec) * 2;
|
||||||
|
#else
|
||||||
|
if (m_SoundIo->current_backend == SoundIoBackendWasapi) {
|
||||||
|
// 15 ms buffer seems to be fine for WASAPI
|
||||||
|
packetsToBuffer = 3;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// 30 ms buffer on CoreAudio to avoid glitching on macOS
|
||||||
|
packetsToBuffer = 6;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Audio buffer size: %d packets",
|
||||||
|
packetsToBuffer);
|
||||||
|
|
||||||
|
m_RingBuffer = soundio_ring_buffer_create(m_SoundIo,
|
||||||
|
m_OutputStream->bytes_per_sample *
|
||||||
|
m_OpusChannelCount *
|
||||||
|
SAMPLES_PER_FRAME *
|
||||||
|
packetsToBuffer);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// HACK: For some reason, a constant latency hangs around in the audio pipeline
|
||||||
|
// unless we wait for the audio stream to drain before actually submitting any samples.
|
||||||
|
// This is a gross hack, but it works remarkably well.
|
||||||
|
SDL_Delay(500);
|
||||||
|
|
||||||
|
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_MinSampleLengthSec));
|
||||||
|
frameCountMin = qMin(frameCountMin, frameCountMax);
|
||||||
|
|
||||||
|
// Track latency on queueing-based backends
|
||||||
|
if (me->m_SoundIo->current_backend != SoundIoBackendCoreAudio && me->m_SoundIo->current_backend != SoundIoBackendJack) {
|
||||||
|
soundio_outstream_get_latency(stream, &me->m_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,40 @@
|
|||||||
|
#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;
|
||||||
|
double m_Latency;
|
||||||
|
bool m_Errored;
|
||||||
|
|
||||||
|
static const double k_RawSampleLengthSec;
|
||||||
|
static const double k_MinSampleLengthSec;
|
||||||
|
};
|
||||||
@@ -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,13 +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_LastMouseMotionTime(0),
|
: m_MultiController(prefs.multiController),
|
||||||
m_MultiController(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));
|
||||||
@@ -38,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()
|
||||||
@@ -58,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));
|
||||||
}
|
}
|
||||||
@@ -72,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);
|
||||||
@@ -103,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)
|
||||||
@@ -111,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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -336,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,
|
||||||
@@ -346,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;
|
||||||
@@ -357,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)
|
||||||
{
|
{
|
||||||
@@ -370,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",
|
||||||
@@ -392,38 +444,15 @@ void SdlInputHandler::handleMouseMotionEvent(SDL_MouseMotionEvent* event)
|
|||||||
// Not capturing
|
// Not capturing
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
short xdelta = (short)event->xrel;
|
else if (event->which == SDL_TOUCH_MOUSEID) {
|
||||||
short ydelta = (short)event->yrel;
|
// Ignore synthetic mouse events
|
||||||
|
return;
|
||||||
// If we're sending more than one motion event per millisecond,
|
|
||||||
// delay for 1 ms to allow batching of mouse move events.
|
|
||||||
Uint32 currentTime = SDL_GetTicks();
|
|
||||||
if (!SDL_TICKS_PASSED(currentTime, m_LastMouseMotionTime + 1)) {
|
|
||||||
SDL_Delay(1);
|
|
||||||
currentTime = SDL_GetTicks();
|
|
||||||
}
|
|
||||||
m_LastMouseMotionTime = currentTime;
|
|
||||||
|
|
||||||
// Pump even if we didn't delay since we might get some extra events
|
|
||||||
SDL_PumpEvents();
|
|
||||||
|
|
||||||
// Batch all of the pending mouse motion events
|
|
||||||
SDL_Event nextEvent;
|
|
||||||
while (SDL_PeepEvents(&nextEvent,
|
|
||||||
1,
|
|
||||||
SDL_GETEVENT,
|
|
||||||
SDL_MOUSEMOTION,
|
|
||||||
SDL_MOUSEMOTION) == 1) {
|
|
||||||
// In theory, these can overflow but in practice
|
|
||||||
// it should be highly unlikely since it would require
|
|
||||||
// moving 64K pixels in 1 ms.
|
|
||||||
xdelta += nextEvent.motion.xrel;
|
|
||||||
ydelta += nextEvent.motion.yrel;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (xdelta != 0 || ydelta != 0) {
|
// Batch until the next mouse polling window or we'll get awful
|
||||||
LiSendMouseMoveEvent(xdelta, ydelta);
|
// 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)
|
||||||
@@ -432,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);
|
||||||
@@ -469,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);
|
||||||
@@ -525,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -535,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;
|
||||||
@@ -546,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -557,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 {
|
||||||
@@ -614,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;
|
||||||
|
|
||||||
@@ -625,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++);
|
||||||
}
|
}
|
||||||
@@ -634,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 && numAxes <= 8) && numButtons >= 8 && numHats <= 1) {
|
||||||
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Joystick likely to be an unmapped game controller");
|
||||||
|
if (!ret.isEmpty()) {
|
||||||
|
ret += ", ";
|
||||||
|
}
|
||||||
|
|
||||||
|
ret += name;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_JoystickClose(joy);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Unable to open joystick for query: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER);
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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,53 +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);
|
|
||||||
|
|
||||||
Uint32 m_LastMouseMotionTime;
|
|
||||||
bool m_MultiController;
|
|
||||||
int m_GamepadMask;
|
|
||||||
GamepadState m_GamepadState[MAX_GAMEPADS];
|
|
||||||
|
|
||||||
static const int k_ButtonMap[];
|
|
||||||
};
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
#include "session.hpp"
|
#include "session.h"
|
||||||
#include "settings/streamingpreferences.h"
|
#include "settings/streamingpreferences.h"
|
||||||
#include "streaming/streamutils.h"
|
#include "streaming/streamutils.h"
|
||||||
|
|
||||||
@@ -6,12 +6,6 @@
|
|||||||
#include <SDL.h>
|
#include <SDL.h>
|
||||||
#include "utils.h"
|
#include "utils.h"
|
||||||
|
|
||||||
// HACK: Need to call SetThreadExecutionState() because SDL doesn't
|
|
||||||
#ifdef Q_OS_WIN32
|
|
||||||
#define WIN32_LEAN_AND_MEAN
|
|
||||||
#include <Windows.h>
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef HAVE_FFMPEG
|
#ifdef HAVE_FFMPEG
|
||||||
#include "video/ffmpeg.h"
|
#include "video/ffmpeg.h"
|
||||||
#endif
|
#endif
|
||||||
@@ -48,11 +42,11 @@ CONNECTION_LISTENER_CALLBACKS Session::k_ConnCallbacks = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
AUDIO_RENDERER_CALLBACKS Session::k_AudioCallbacks = {
|
AUDIO_RENDERER_CALLBACKS Session::k_AudioCallbacks = {
|
||||||
Session::sdlAudioInit,
|
Session::arInit,
|
||||||
Session::sdlAudioStart,
|
nullptr,
|
||||||
Session::sdlAudioStop,
|
nullptr,
|
||||||
Session::sdlAudioCleanup,
|
Session::arCleanup,
|
||||||
Session::sdlAudioDecodeAndPlaySample,
|
Session::arDecodeAndPlaySample,
|
||||||
CAPABILITY_DIRECT_SUBMIT
|
CAPABILITY_DIRECT_SUBMIT
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -79,6 +73,7 @@ void Session::clStageFailed(int stage, long errorCode)
|
|||||||
|
|
||||||
void Session::clConnectionTerminated(long errorCode)
|
void Session::clConnectionTerminated(long errorCode)
|
||||||
{
|
{
|
||||||
|
s_ActiveSession->m_UnexpectedTermination = true;
|
||||||
emit s_ActiveSession->displayLaunchError("Connection terminated");
|
emit s_ActiveSession->displayLaunchError("Connection terminated");
|
||||||
|
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
@@ -104,12 +99,16 @@ void Session::clLogMessage(const char* format, ...)
|
|||||||
va_end(ap);
|
va_end(ap);
|
||||||
}
|
}
|
||||||
|
|
||||||
#define CALL_INITIALIZE(dec) (dec)->initialize(vds, window, videoFormat, width, height, frameRate, enableVsync)
|
#define CALL_INITIALIZE(dec) (dec)->initialize(vds, window, videoFormat, width, height, frameRate, enableVsync, enableFramePacing)
|
||||||
|
|
||||||
bool Session::chooseDecoder(StreamingPreferences::VideoDecoderSelection vds,
|
bool Session::chooseDecoder(StreamingPreferences::VideoDecoderSelection vds,
|
||||||
SDL_Window* window, int videoFormat, int width, int height,
|
SDL_Window* window, int videoFormat, int width, int height,
|
||||||
int frameRate, bool enableVsync, IVideoDecoder*& chosenDecoder)
|
int frameRate, bool enableVsync, bool enableFramePacing, IVideoDecoder*& chosenDecoder)
|
||||||
{
|
{
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"V-sync %s",
|
||||||
|
enableVsync ? "enabled" : "disabled");
|
||||||
|
|
||||||
#ifdef HAVE_SLVIDEO
|
#ifdef HAVE_SLVIDEO
|
||||||
chosenDecoder = new SLVideoDecoder();
|
chosenDecoder = new SLVideoDecoder();
|
||||||
if (CALL_INITIALIZE(chosenDecoder)) {
|
if (CALL_INITIALIZE(chosenDecoder)) {
|
||||||
@@ -170,25 +169,33 @@ int Session::drSubmitDecodeUnit(PDECODE_UNIT du)
|
|||||||
// Use a lock since we'll be yanking this decoder out
|
// Use a lock since we'll be yanking this decoder out
|
||||||
// from underneath the session when we initiate destruction.
|
// from underneath the session when we initiate destruction.
|
||||||
// We need to destroy the decoder on the main thread to satisfy
|
// We need to destroy the decoder on the main thread to satisfy
|
||||||
// some API constraints (like DXVA2).
|
// some API constraints (like DXVA2). If we can't acquire it,
|
||||||
|
// that means the decoder is about to be destroyed, so we can
|
||||||
|
// safely return DR_OK and wait for m_NeedsIdr to be set by
|
||||||
|
// the decoder reinitialization code.
|
||||||
|
|
||||||
SDL_AtomicLock(&s_ActiveSession->m_DecoderLock);
|
if (SDL_AtomicTryLock(&s_ActiveSession->m_DecoderLock)) {
|
||||||
|
if (s_ActiveSession->m_NeedsIdr) {
|
||||||
|
// If we reset our decoder, we'll need to request an IDR frame
|
||||||
|
s_ActiveSession->m_NeedsIdr = false;
|
||||||
|
SDL_AtomicUnlock(&s_ActiveSession->m_DecoderLock);
|
||||||
|
return DR_NEED_IDR;
|
||||||
|
}
|
||||||
|
|
||||||
if (s_ActiveSession->m_NeedsIdr) {
|
IVideoDecoder* decoder = s_ActiveSession->m_VideoDecoder;
|
||||||
// If we reset our decoder, we'll need to request an IDR frame
|
if (decoder != nullptr) {
|
||||||
s_ActiveSession->m_NeedsIdr = false;
|
int ret = decoder->submitDecodeUnit(du);
|
||||||
SDL_AtomicUnlock(&s_ActiveSession->m_DecoderLock);
|
SDL_AtomicUnlock(&s_ActiveSession->m_DecoderLock);
|
||||||
return DR_NEED_IDR;
|
return ret;
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
IVideoDecoder* decoder = s_ActiveSession->m_VideoDecoder;
|
SDL_AtomicUnlock(&s_ActiveSession->m_DecoderLock);
|
||||||
if (decoder != nullptr) {
|
return DR_OK;
|
||||||
int ret = decoder->submitDecodeUnit(du);
|
}
|
||||||
SDL_AtomicUnlock(&s_ActiveSession->m_DecoderLock);
|
|
||||||
return ret;
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
SDL_AtomicUnlock(&s_ActiveSession->m_DecoderLock);
|
// Decoder is going away. Ignore anything coming in until
|
||||||
|
// the lock is released.
|
||||||
return DR_OK;
|
return DR_OK;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -214,17 +221,20 @@ bool Session::isHardwareDecodeAvailable(StreamingPreferences::VideoDecoderSelect
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!chooseDecoder(vds, window, videoFormat, width, height, frameRate, true, decoder)) {
|
if (!chooseDecoder(vds, window, videoFormat, width, height, frameRate, true, false, decoder)) {
|
||||||
SDL_DestroyWindow(window);
|
SDL_DestroyWindow(window);
|
||||||
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
SDL_DestroyWindow(window);
|
|
||||||
|
|
||||||
bool ret = decoder->isHardwareAccelerated();
|
bool ret = decoder->isHardwareAccelerated();
|
||||||
|
|
||||||
delete decoder;
|
delete decoder;
|
||||||
|
|
||||||
|
// This must be called after the decoder is deleted, because
|
||||||
|
// the renderer may want to interact with the window
|
||||||
|
SDL_DestroyWindow(window);
|
||||||
|
|
||||||
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
@@ -251,24 +261,28 @@ int Session::getDecoderCapabilities(StreamingPreferences::VideoDecoderSelection
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!chooseDecoder(vds, window, videoFormat, width, height, frameRate, true, decoder)) {
|
if (!chooseDecoder(vds, window, videoFormat, width, height, frameRate, true, false, decoder)) {
|
||||||
SDL_DestroyWindow(window);
|
SDL_DestroyWindow(window);
|
||||||
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
SDL_DestroyWindow(window);
|
|
||||||
|
|
||||||
int caps = decoder->getDecoderCapabilities();
|
int caps = decoder->getDecoderCapabilities();
|
||||||
|
|
||||||
delete decoder;
|
delete decoder;
|
||||||
|
|
||||||
|
// This must be called after the decoder is deleted, because
|
||||||
|
// the renderer may want to interact with the window
|
||||||
|
SDL_DestroyWindow(window);
|
||||||
|
|
||||||
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||||
|
|
||||||
return caps;
|
return caps;
|
||||||
}
|
}
|
||||||
|
|
||||||
Session::Session(NvComputer* computer, NvApp& app)
|
Session::Session(NvComputer* computer, NvApp& app, StreamingPreferences *preferences)
|
||||||
: m_Computer(computer),
|
: m_Preferences(preferences ? preferences : new StreamingPreferences(this)),
|
||||||
|
m_Computer(computer),
|
||||||
m_App(app),
|
m_App(app),
|
||||||
m_Window(nullptr),
|
m_Window(nullptr),
|
||||||
m_VideoDecoder(nullptr),
|
m_VideoDecoder(nullptr),
|
||||||
@@ -277,17 +291,31 @@ Session::Session(NvComputer* computer, NvApp& app)
|
|||||||
m_AudioDisabled(false),
|
m_AudioDisabled(false),
|
||||||
m_DisplayOriginX(0),
|
m_DisplayOriginX(0),
|
||||||
m_DisplayOriginY(0),
|
m_DisplayOriginY(0),
|
||||||
m_PendingWindowedTransition(false)
|
m_PendingWindowedTransition(false),
|
||||||
|
m_UnexpectedTermination(true), // Failure prior to streaming is unexpected
|
||||||
|
m_OpusDecoder(nullptr),
|
||||||
|
m_AudioRenderer(nullptr),
|
||||||
|
m_AudioSampleCount(0)
|
||||||
{
|
{
|
||||||
qDebug() << "Server GPU:" << m_Computer->gpuModel;
|
}
|
||||||
|
|
||||||
|
Session::~Session()
|
||||||
|
{
|
||||||
|
// Acquire session semaphore to ensure all cleanup is done before the destructor returns
|
||||||
|
// and the object is deallocated.
|
||||||
|
s_ActiveSessionSemaphore.acquire();
|
||||||
|
s_ActiveSessionSemaphore.release();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Session::initialize()
|
||||||
|
{
|
||||||
|
qInfo() << "Server GPU:" << m_Computer->gpuModel;
|
||||||
|
qInfo() << "Server GFE version:" << m_Computer->gfeVersion;
|
||||||
|
|
||||||
LiInitializeVideoCallbacks(&m_VideoCallbacks);
|
LiInitializeVideoCallbacks(&m_VideoCallbacks);
|
||||||
m_VideoCallbacks.setup = drSetup;
|
m_VideoCallbacks.setup = drSetup;
|
||||||
m_VideoCallbacks.submitDecodeUnit = drSubmitDecodeUnit;
|
m_VideoCallbacks.submitDecodeUnit = drSubmitDecodeUnit;
|
||||||
|
|
||||||
// Submit for decode without using a separate thread
|
|
||||||
m_VideoCallbacks.capabilities |= CAPABILITY_DIRECT_SUBMIT;
|
|
||||||
|
|
||||||
// Slice up to 4 times for parallel decode, once slice per core
|
// Slice up to 4 times for parallel decode, once slice per core
|
||||||
int slices = qMin(MAX_SLICES, SDL_GetCPUCount());
|
int slices = qMin(MAX_SLICES, SDL_GetCPUCount());
|
||||||
m_VideoCallbacks.capabilities |= CAPABILITY_SLICES_PER_FRAME(slices);
|
m_VideoCallbacks.capabilities |= CAPABILITY_SLICES_PER_FRAME(slices);
|
||||||
@@ -296,11 +324,13 @@ Session::Session(NvComputer* computer, NvApp& app)
|
|||||||
slices);
|
slices);
|
||||||
|
|
||||||
LiInitializeStreamConfiguration(&m_StreamConfig);
|
LiInitializeStreamConfiguration(&m_StreamConfig);
|
||||||
m_StreamConfig.width = m_Preferences.width;
|
m_StreamConfig.width = m_Preferences->width;
|
||||||
m_StreamConfig.height = m_Preferences.height;
|
m_StreamConfig.height = m_Preferences->height;
|
||||||
m_StreamConfig.fps = m_Preferences.fps;
|
m_StreamConfig.fps = m_Preferences->fps;
|
||||||
m_StreamConfig.bitrate = m_Preferences.bitrateKbps;
|
m_StreamConfig.bitrate = m_Preferences->bitrateKbps;
|
||||||
m_StreamConfig.hevcBitratePercentageMultiplier = 75;
|
m_StreamConfig.hevcBitratePercentageMultiplier = 75;
|
||||||
|
m_StreamConfig.streamingRemotely = STREAM_CFG_AUTO;
|
||||||
|
m_StreamConfig.packetSize = 1392;
|
||||||
|
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"Video bitrate: %d kbps",
|
"Video bitrate: %d kbps",
|
||||||
@@ -312,16 +342,12 @@ Session::Session(NvComputer* computer, NvApp& app)
|
|||||||
// Only the first 4 bytes are populated in the RI key IV
|
// Only the first 4 bytes are populated in the RI key IV
|
||||||
RAND_bytes(reinterpret_cast<unsigned char*>(m_StreamConfig.remoteInputAesIv), 4);
|
RAND_bytes(reinterpret_cast<unsigned char*>(m_StreamConfig.remoteInputAesIv), 4);
|
||||||
|
|
||||||
switch (m_Preferences.audioConfig)
|
switch (m_Preferences->audioConfig)
|
||||||
{
|
{
|
||||||
case StreamingPreferences::AC_AUTO:
|
case StreamingPreferences::AC_STEREO:
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Autodetecting audio configuration");
|
|
||||||
m_StreamConfig.audioConfiguration = sdlDetermineAudioConfiguration();
|
|
||||||
break;
|
|
||||||
case StreamingPreferences::AC_FORCE_STEREO:
|
|
||||||
m_StreamConfig.audioConfiguration = AUDIO_CONFIGURATION_STEREO;
|
m_StreamConfig.audioConfiguration = AUDIO_CONFIGURATION_STEREO;
|
||||||
break;
|
break;
|
||||||
case StreamingPreferences::AC_FORCE_SURROUND:
|
case StreamingPreferences::AC_51_SURROUND:
|
||||||
m_StreamConfig.audioConfiguration = AUDIO_CONFIGURATION_51_SURROUND;
|
m_StreamConfig.audioConfiguration = AUDIO_CONFIGURATION_51_SURROUND;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -330,26 +356,32 @@ Session::Session(NvComputer* computer, NvApp& app)
|
|||||||
"Audio configuration: %d",
|
"Audio configuration: %d",
|
||||||
m_StreamConfig.audioConfiguration);
|
m_StreamConfig.audioConfiguration);
|
||||||
|
|
||||||
switch (m_Preferences.videoCodecConfig)
|
switch (m_Preferences->videoCodecConfig)
|
||||||
{
|
{
|
||||||
case StreamingPreferences::VCC_AUTO:
|
case StreamingPreferences::VCC_AUTO:
|
||||||
// TODO: Determine if HEVC is better depending on the decoder
|
// TODO: Determine if HEVC is better depending on the decoder
|
||||||
m_StreamConfig.supportsHevc =
|
m_StreamConfig.supportsHevc =
|
||||||
isHardwareDecodeAvailable(m_Preferences.videoDecoderSelection,
|
isHardwareDecodeAvailable(m_Preferences->videoDecoderSelection,
|
||||||
VIDEO_FORMAT_H265,
|
VIDEO_FORMAT_H265,
|
||||||
m_StreamConfig.width,
|
m_StreamConfig.width,
|
||||||
m_StreamConfig.height,
|
m_StreamConfig.height,
|
||||||
m_StreamConfig.fps);
|
m_StreamConfig.fps);
|
||||||
|
#ifdef Q_OS_DARWIN
|
||||||
// Oddly enough, the H.264 encoder on my GTX 1080 is actually more
|
{
|
||||||
// performant than the HEVC encoder (especially because split frame encode
|
// Prior to GFE 3.11, GFE did not allow us to constrain
|
||||||
// is disabled for HEVC by moonlight-common-c). Use H.264 for 1440p 100 FPS and above.
|
// the number of reference frames, so we have to fixup the SPS
|
||||||
if (m_StreamConfig.width * m_StreamConfig.height * m_StreamConfig.fps >= 2560 * 1440 * 100) {
|
// to allow decoding via VideoToolbox on macOS. Since we don't
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
// have fixup code for HEVC, just avoid it if GFE is too old.
|
||||||
"Preferring H.264 for higher performance");
|
QVector<int> gfeVersion = NvHTTP::parseQuad(m_Computer->gfeVersion);
|
||||||
m_StreamConfig.supportsHevc = false;
|
if (gfeVersion.isEmpty() || // Very old versions don't have GfeVersion at all
|
||||||
|
gfeVersion[0] < 3 ||
|
||||||
|
(gfeVersion[0] == 3 && gfeVersion[1] < 11)) {
|
||||||
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Disabling HEVC on macOS due to old GFE version");
|
||||||
|
m_StreamConfig.supportsHevc = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
m_StreamConfig.enableHdr = false;
|
m_StreamConfig.enableHdr = false;
|
||||||
break;
|
break;
|
||||||
case StreamingPreferences::VCC_FORCE_H264:
|
case StreamingPreferences::VCC_FORCE_H264:
|
||||||
@@ -366,21 +398,7 @@ Session::Session(NvComputer* computer, NvApp& app)
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (computer->activeAddress == computer->remoteAddress) {
|
switch (m_Preferences->windowMode)
|
||||||
m_StreamConfig.streamingRemotely = 1;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
m_StreamConfig.streamingRemotely = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (computer->activeAddress == computer->localAddress) {
|
|
||||||
m_StreamConfig.packetSize = 1392;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
m_StreamConfig.packetSize = 1024;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (m_Preferences.windowMode)
|
|
||||||
{
|
{
|
||||||
case StreamingPreferences::WM_FULLSCREEN_DESKTOP:
|
case StreamingPreferences::WM_FULLSCREEN_DESKTOP:
|
||||||
m_FullScreenFlag = SDL_WINDOW_FULLSCREEN_DESKTOP;
|
m_FullScreenFlag = SDL_WINDOW_FULLSCREEN_DESKTOP;
|
||||||
@@ -412,28 +430,28 @@ bool Session::validateLaunch()
|
|||||||
{
|
{
|
||||||
QStringList warningList;
|
QStringList warningList;
|
||||||
|
|
||||||
if (m_Preferences.videoDecoderSelection == StreamingPreferences::VDS_FORCE_SOFTWARE) {
|
if (m_Preferences->videoDecoderSelection == StreamingPreferences::VDS_FORCE_SOFTWARE) {
|
||||||
emitLaunchWarning("Your settings selection to force software decoding may cause poor streaming performance.");
|
emitLaunchWarning("Your settings selection to force software decoding may cause poor streaming performance.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (m_Preferences.unsupportedFps && m_StreamConfig.fps > 60) {
|
if (m_Preferences->unsupportedFps && m_StreamConfig.fps > 60) {
|
||||||
emitLaunchWarning("Using unsupported FPS options may cause stuttering or lag.");
|
emitLaunchWarning("Using unsupported FPS options may cause stuttering or lag.");
|
||||||
|
|
||||||
if (m_Preferences.enableVsync) {
|
if (m_Preferences->enableVsync) {
|
||||||
emitLaunchWarning("V-sync will be disabled when streaming at a higher frame rate than the display.");
|
emitLaunchWarning("V-sync will be disabled when streaming at a higher frame rate than the display.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (m_StreamConfig.supportsHevc) {
|
if (m_StreamConfig.supportsHevc) {
|
||||||
bool hevcForced = m_Preferences.videoCodecConfig == StreamingPreferences::VCC_FORCE_HEVC ||
|
bool hevcForced = m_Preferences->videoCodecConfig == StreamingPreferences::VCC_FORCE_HEVC ||
|
||||||
m_Preferences.videoCodecConfig == StreamingPreferences::VCC_FORCE_HEVC_HDR;
|
m_Preferences->videoCodecConfig == StreamingPreferences::VCC_FORCE_HEVC_HDR;
|
||||||
|
|
||||||
if (!isHardwareDecodeAvailable(m_Preferences.videoDecoderSelection,
|
if (!isHardwareDecodeAvailable(m_Preferences->videoDecoderSelection,
|
||||||
VIDEO_FORMAT_H265,
|
VIDEO_FORMAT_H265,
|
||||||
m_StreamConfig.width,
|
m_StreamConfig.width,
|
||||||
m_StreamConfig.height,
|
m_StreamConfig.height,
|
||||||
m_StreamConfig.fps) &&
|
m_StreamConfig.fps) &&
|
||||||
m_Preferences.videoDecoderSelection == StreamingPreferences::VDS_AUTO) {
|
m_Preferences->videoDecoderSelection == StreamingPreferences::VDS_AUTO) {
|
||||||
if (hevcForced) {
|
if (hevcForced) {
|
||||||
emitLaunchWarning("Using software decoding due to your selection to force HEVC without GPU support. This may cause poor streaming performance.");
|
emitLaunchWarning("Using software decoding due to your selection to force HEVC without GPU support. This may cause poor streaming performance.");
|
||||||
}
|
}
|
||||||
@@ -469,7 +487,7 @@ bool Session::validateLaunch()
|
|||||||
emitLaunchWarning("Your host PC GPU doesn't support HDR streaming. "
|
emitLaunchWarning("Your host PC GPU doesn't support HDR streaming. "
|
||||||
"A GeForce GTX 1000-series (Pascal) or later GPU is required for HDR streaming.");
|
"A GeForce GTX 1000-series (Pascal) or later GPU is required for HDR streaming.");
|
||||||
}
|
}
|
||||||
else if (!isHardwareDecodeAvailable(m_Preferences.videoDecoderSelection,
|
else if (!isHardwareDecodeAvailable(m_Preferences->videoDecoderSelection,
|
||||||
VIDEO_FORMAT_H265_MAIN10,
|
VIDEO_FORMAT_H265_MAIN10,
|
||||||
m_StreamConfig.width,
|
m_StreamConfig.width,
|
||||||
m_StreamConfig.height,
|
m_StreamConfig.height,
|
||||||
@@ -486,36 +504,44 @@ bool Session::validateLaunch()
|
|||||||
|
|
||||||
if (m_StreamConfig.width >= 3840) {
|
if (m_StreamConfig.width >= 3840) {
|
||||||
// Only allow 4K on GFE 3.x+
|
// Only allow 4K on GFE 3.x+
|
||||||
if (m_Computer->gfeVersion.isNull() || m_Computer->gfeVersion.startsWith("2.")) {
|
if (m_Computer->gfeVersion.isEmpty() || m_Computer->gfeVersion.startsWith("2.")) {
|
||||||
emitLaunchWarning("GeForce Experience 3.0 or higher is required for 4K streaming.");
|
emitLaunchWarning("GeForce Experience 3.0 or higher is required for 4K streaming.");
|
||||||
|
|
||||||
m_StreamConfig.width = 1920;
|
m_StreamConfig.width = 1920;
|
||||||
m_StreamConfig.height = 1080;
|
m_StreamConfig.height = 1080;
|
||||||
}
|
}
|
||||||
// This list is sorted from least to greatest
|
}
|
||||||
else if (m_Computer->displayModes.last().width < 3840 ||
|
|
||||||
(m_Computer->displayModes.last().refreshRate < 60 && m_StreamConfig.fps >= 60)) {
|
|
||||||
emitLaunchWarning("Your host PC GPU doesn't support 4K streaming. "
|
|
||||||
"A GeForce GTX 900-series (Maxwell) or later GPU is required for 4K streaming.");
|
|
||||||
|
|
||||||
m_StreamConfig.width = 1920;
|
// Test if audio works at the specified audio configuration
|
||||||
m_StreamConfig.height = 1080;
|
bool audioTestPassed = testAudio(m_StreamConfig.audioConfiguration);
|
||||||
|
|
||||||
|
// Gracefully degrade to stereo if 5.1 doesn't work
|
||||||
|
if (!audioTestPassed && m_StreamConfig.audioConfiguration == AUDIO_CONFIGURATION_51_SURROUND) {
|
||||||
|
audioTestPassed = testAudio(AUDIO_CONFIGURATION_STEREO);
|
||||||
|
if (audioTestPassed) {
|
||||||
|
m_StreamConfig.audioConfiguration = AUDIO_CONFIGURATION_STEREO;
|
||||||
|
emitLaunchWarning("5.1 surround sound is not supported by the current audio device.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test that audio hardware is functional
|
// If nothing worked, warn the user that audio will not work
|
||||||
m_AudioDisabled = !testAudio(m_StreamConfig.audioConfiguration);
|
m_AudioDisabled = !audioTestPassed;
|
||||||
if (m_AudioDisabled) {
|
if (m_AudioDisabled) {
|
||||||
emitLaunchWarning("Failed to open audio device. Audio will be unavailable during this session.");
|
emitLaunchWarning("Failed to open audio device. Audio will be unavailable during this session.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (m_Preferences.videoDecoderSelection == StreamingPreferences::VDS_FORCE_HARDWARE &&
|
// Check for unmapped gamepads
|
||||||
!isHardwareDecodeAvailable(m_Preferences.videoDecoderSelection,
|
if (!SdlInputHandler::getUnmappedGamepads().isEmpty()) {
|
||||||
|
emitLaunchWarning("An attached gamepad has no mapping and won't be usable. Visit the Moonlight help to resolve this.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_Preferences->videoDecoderSelection == StreamingPreferences::VDS_FORCE_HARDWARE &&
|
||||||
|
!isHardwareDecodeAvailable(m_Preferences->videoDecoderSelection,
|
||||||
m_StreamConfig.supportsHevc ? VIDEO_FORMAT_H265 : VIDEO_FORMAT_H264,
|
m_StreamConfig.supportsHevc ? VIDEO_FORMAT_H265 : VIDEO_FORMAT_H264,
|
||||||
m_StreamConfig.width,
|
m_StreamConfig.width,
|
||||||
m_StreamConfig.height,
|
m_StreamConfig.height,
|
||||||
m_StreamConfig.fps)) {
|
m_StreamConfig.fps)) {
|
||||||
if (m_Preferences.videoCodecConfig == StreamingPreferences::VCC_AUTO) {
|
if (m_Preferences->videoCodecConfig == StreamingPreferences::VCC_AUTO) {
|
||||||
emit displayLaunchError("Your selection to force hardware decoding cannot be satisfied due to missing hardware decoding support on this PC's GPU.");
|
emit displayLaunchError("Your selection to force hardware decoding cannot be satisfied due to missing hardware decoding support on this PC's GPU.");
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -527,7 +553,7 @@ bool Session::validateLaunch()
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add the capability flags from the chosen decoder/renderer
|
// Add the capability flags from the chosen decoder/renderer
|
||||||
m_VideoCallbacks.capabilities |= getDecoderCapabilities(m_Preferences.videoDecoderSelection,
|
m_VideoCallbacks.capabilities |= getDecoderCapabilities(m_Preferences->videoDecoderSelection,
|
||||||
m_StreamConfig.supportsHevc ? VIDEO_FORMAT_H265 : VIDEO_FORMAT_H264,
|
m_StreamConfig.supportsHevc ? VIDEO_FORMAT_H265 : VIDEO_FORMAT_H264,
|
||||||
m_StreamConfig.width,
|
m_StreamConfig.width,
|
||||||
m_StreamConfig.height,
|
m_StreamConfig.height,
|
||||||
@@ -539,25 +565,65 @@ bool Session::validateLaunch()
|
|||||||
|
|
||||||
class DeferredSessionCleanupTask : public QRunnable
|
class DeferredSessionCleanupTask : public QRunnable
|
||||||
{
|
{
|
||||||
void run() override
|
public:
|
||||||
{
|
DeferredSessionCleanupTask(Session* session) :
|
||||||
// Finish cleanup of the connection state
|
m_Session(session) {}
|
||||||
LiStopConnection();
|
|
||||||
|
|
||||||
|
private:
|
||||||
|
virtual ~DeferredSessionCleanupTask() override
|
||||||
|
{
|
||||||
// Allow another session to start now that we're cleaned up
|
// Allow another session to start now that we're cleaned up
|
||||||
Session::s_ActiveSession = nullptr;
|
Session::s_ActiveSession = nullptr;
|
||||||
Session::s_ActiveSessionSemaphore.release();
|
Session::s_ActiveSessionSemaphore.release();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void run() override
|
||||||
|
{
|
||||||
|
// Only quit the running app if our session terminated gracefully
|
||||||
|
bool shouldQuit =
|
||||||
|
!m_Session->m_UnexpectedTermination &&
|
||||||
|
m_Session->m_Preferences->quitAppAfter;
|
||||||
|
|
||||||
|
// Notify the UI
|
||||||
|
if (shouldQuit) {
|
||||||
|
emit m_Session->quitStarting();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
emit m_Session->sessionFinished();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finish cleanup of the connection state
|
||||||
|
LiStopConnection();
|
||||||
|
|
||||||
|
// Perform a best-effort app quit
|
||||||
|
if (shouldQuit) {
|
||||||
|
NvHTTP http(m_Session->m_Computer->activeAddress, m_Session->m_Computer->serverCert);
|
||||||
|
|
||||||
|
// Logging is already done inside NvHTTP
|
||||||
|
try {
|
||||||
|
http.quitApp();
|
||||||
|
} catch (const GfeHttpResponseException&) {
|
||||||
|
} catch (const QtNetworkReplyException&) {
|
||||||
|
}
|
||||||
|
|
||||||
|
// Session is finished now
|
||||||
|
emit m_Session->sessionFinished();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Session* m_Session;
|
||||||
};
|
};
|
||||||
|
|
||||||
void Session::getWindowDimensions(int& x, int& y,
|
void Session::getWindowDimensions(int& x, int& y,
|
||||||
int& width, int& height)
|
int& width, int& height)
|
||||||
{
|
{
|
||||||
int displayIndex = 0;
|
int displayIndex = 0;
|
||||||
|
bool fullScreen;
|
||||||
|
|
||||||
if (m_Window != nullptr) {
|
if (m_Window != nullptr) {
|
||||||
displayIndex = SDL_GetWindowDisplayIndex(m_Window);
|
displayIndex = SDL_GetWindowDisplayIndex(m_Window);
|
||||||
SDL_assert(displayIndex >= 0);
|
SDL_assert(displayIndex >= 0);
|
||||||
|
fullScreen = (SDL_GetWindowFlags(m_Window) & SDL_WINDOW_FULLSCREEN);
|
||||||
}
|
}
|
||||||
// Create our window on the same display that Qt's UI
|
// Create our window on the same display that Qt's UI
|
||||||
// was being displayed on.
|
// was being displayed on.
|
||||||
@@ -584,10 +650,16 @@ void Session::getWindowDimensions(int& x, int& y,
|
|||||||
i, SDL_GetError());
|
i, SDL_GetError());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fullScreen = (m_Preferences->windowMode != StreamingPreferences::WM_WINDOWED);
|
||||||
}
|
}
|
||||||
|
|
||||||
SDL_Rect usableBounds;
|
SDL_Rect usableBounds;
|
||||||
if (SDL_GetDisplayUsableBounds(displayIndex, &usableBounds) == 0) {
|
if (fullScreen && SDL_GetDisplayBounds(displayIndex, &usableBounds) == 0) {
|
||||||
|
width = usableBounds.w;
|
||||||
|
height = usableBounds.h;
|
||||||
|
}
|
||||||
|
else if (SDL_GetDisplayUsableBounds(displayIndex, &usableBounds) == 0) {
|
||||||
width = usableBounds.w;
|
width = usableBounds.w;
|
||||||
height = usableBounds.h;
|
height = usableBounds.h;
|
||||||
|
|
||||||
@@ -672,9 +744,13 @@ void Session::updateOptimalWindowDisplayMode()
|
|||||||
bestMode = desktopMode;
|
bestMode = desktopMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
if ((SDL_GetWindowFlags(m_Window) & SDL_WINDOW_FULLSCREEN_DESKTOP) == SDL_WINDOW_FULLSCREEN) {
|
||||||
"Chosen best display mode: %dx%dx%d",
|
// Only print when the window is actually in full-screen exclusive mode,
|
||||||
bestMode.w, bestMode.h, bestMode.refresh_rate);
|
// otherwise we're not actually using the mode we've set here
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Chosen best display mode: %dx%dx%d",
|
||||||
|
bestMode.w, bestMode.h, bestMode.refresh_rate);
|
||||||
|
}
|
||||||
|
|
||||||
SDL_SetWindowDisplayMode(m_Window, &bestMode);
|
SDL_SetWindowDisplayMode(m_Window, &bestMode);
|
||||||
}
|
}
|
||||||
@@ -701,14 +777,26 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
|||||||
m_DisplayOriginX = displayOriginX;
|
m_DisplayOriginX = displayOriginX;
|
||||||
m_DisplayOriginY = displayOriginY;
|
m_DisplayOriginY = displayOriginY;
|
||||||
|
|
||||||
|
// Complete initialization in this deferred context to avoid
|
||||||
|
// calling expensive functions in the constructor (during the
|
||||||
|
// process of loading the StreamSegue).
|
||||||
|
initialize();
|
||||||
|
|
||||||
// Check for validation errors/warnings and emit
|
// Check for validation errors/warnings and emit
|
||||||
// signals for them, if appropriate
|
// signals for them, if appropriate
|
||||||
if (!validateLaunch()) {
|
if (!validateLaunch()) {
|
||||||
|
emit sessionFinished();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Manually pump the UI thread for the view
|
// Wait 1.5 seconds before connecting to let the user
|
||||||
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
|
// have time to read any messages present on the segue
|
||||||
|
uint32_t start = SDL_GetTicks();
|
||||||
|
while (!SDL_TICKS_PASSED(SDL_GetTicks(), start + 1500)) {
|
||||||
|
// Pump the UI loop while we wait
|
||||||
|
SDL_Delay(5);
|
||||||
|
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
|
||||||
|
}
|
||||||
|
|
||||||
// Wait for any old session to finish cleanup
|
// Wait for any old session to finish cleanup
|
||||||
s_ActiveSessionSemaphore.acquire();
|
s_ActiveSessionSemaphore.acquire();
|
||||||
@@ -718,39 +806,33 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
|||||||
|
|
||||||
// Initialize the gamepad code with our preferences
|
// Initialize the gamepad code with our preferences
|
||||||
StreamingPreferences prefs;
|
StreamingPreferences prefs;
|
||||||
SdlInputHandler inputHandler(prefs.multiController);
|
SdlInputHandler inputHandler(prefs, m_Computer,
|
||||||
|
m_StreamConfig.width,
|
||||||
|
m_StreamConfig.height);
|
||||||
|
|
||||||
// The UI should have ensured the old game was already quit
|
// The UI should have ensured the old game was already quit
|
||||||
// if we decide to stream a different game.
|
// if we decide to stream a different game.
|
||||||
Q_ASSERT(m_Computer->currentGameId == 0 ||
|
Q_ASSERT(m_Computer->currentGameId == 0 ||
|
||||||
m_Computer->currentGameId == m_App.id);
|
m_Computer->currentGameId == m_App.id);
|
||||||
|
|
||||||
bool enableGameOptimizations = false;
|
|
||||||
for (const NvDisplayMode &mode : m_Computer->displayModes) {
|
|
||||||
if (mode.width == m_StreamConfig.width &&
|
|
||||||
mode.height == m_StreamConfig.height) {
|
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Found host supported resolution: %dx%d",
|
|
||||||
mode.width, mode.height);
|
|
||||||
enableGameOptimizations = prefs.gameOptimizations;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
NvHTTP http(m_Computer->activeAddress);
|
NvHTTP http(m_Computer->activeAddress, m_Computer->serverCert);
|
||||||
if (m_Computer->currentGameId != 0) {
|
if (m_Computer->currentGameId != 0) {
|
||||||
http.resumeApp(&m_StreamConfig);
|
http.resumeApp(&m_StreamConfig);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
http.launchApp(m_App.id, &m_StreamConfig,
|
http.launchApp(m_App.id, &m_StreamConfig,
|
||||||
enableGameOptimizations,
|
prefs.gameOptimizations,
|
||||||
prefs.playAudioOnHost,
|
prefs.playAudioOnHost,
|
||||||
inputHandler.getAttachedGamepadMask());
|
inputHandler.getAttachedGamepadMask());
|
||||||
}
|
}
|
||||||
} catch (const GfeHttpResponseException& e) {
|
} catch (const GfeHttpResponseException& e) {
|
||||||
emit displayLaunchError(e.toQString());
|
emit displayLaunchError(e.toQString());
|
||||||
s_ActiveSessionSemaphore.release();
|
QThreadPool::globalInstance()->start(new DeferredSessionCleanupTask(this));
|
||||||
|
return;
|
||||||
|
} catch (const QtNetworkReplyException& e) {
|
||||||
|
emit displayLaunchError(e.toQString());
|
||||||
|
QThreadPool::globalInstance()->start(new DeferredSessionCleanupTask(this));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -760,7 +842,7 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
|||||||
"SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s",
|
"SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s",
|
||||||
SDL_GetError());
|
SDL_GetError());
|
||||||
emit displayLaunchError(QString::fromLocal8Bit(SDL_GetError()));
|
emit displayLaunchError(QString::fromLocal8Bit(SDL_GetError()));
|
||||||
s_ActiveSessionSemaphore.release();
|
QThreadPool::globalInstance()->start(new DeferredSessionCleanupTask(this));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -773,10 +855,10 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
|||||||
|
|
||||||
// Older GFE versions didn't have this field
|
// Older GFE versions didn't have this field
|
||||||
QByteArray siGfeVersion;
|
QByteArray siGfeVersion;
|
||||||
if (!m_Computer->gfeVersion.isNull()) {
|
if (!m_Computer->gfeVersion.isEmpty()) {
|
||||||
siGfeVersion = m_Computer->gfeVersion.toLatin1();
|
siGfeVersion = m_Computer->gfeVersion.toLatin1();
|
||||||
}
|
}
|
||||||
if (!siGfeVersion.isNull()) {
|
if (!siGfeVersion.isEmpty()) {
|
||||||
hostInfo.serverInfoGfeVersion = siGfeVersion.data();
|
hostInfo.serverInfoGfeVersion = siGfeVersion.data();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -788,7 +870,7 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
|||||||
// We already displayed an error dialog in the stage failure
|
// We already displayed an error dialog in the stage failure
|
||||||
// listener.
|
// listener.
|
||||||
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||||
s_ActiveSessionSemaphore.release();
|
QThreadPool::globalInstance()->start(new DeferredSessionCleanupTask(this));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -810,14 +892,35 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
|||||||
"SDL_CreateWindow() failed: %s",
|
"SDL_CreateWindow() failed: %s",
|
||||||
SDL_GetError());
|
SDL_GetError());
|
||||||
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||||
s_ActiveSessionSemaphore.release();
|
QThreadPool::globalInstance()->start(new DeferredSessionCleanupTask(this));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QSvgRenderer svgIconRenderer(QString(":/res/moonlight.svg"));
|
||||||
|
QImage svgImage(ICON_SIZE, ICON_SIZE, QImage::Format_RGBA8888);
|
||||||
|
svgImage.fill(0);
|
||||||
|
|
||||||
|
QPainter svgPainter(&svgImage);
|
||||||
|
svgIconRenderer.render(&svgPainter);
|
||||||
|
SDL_Surface* iconSurface = SDL_CreateRGBSurfaceWithFormatFrom((void*)svgImage.constBits(),
|
||||||
|
svgImage.width(),
|
||||||
|
svgImage.height(),
|
||||||
|
32,
|
||||||
|
4 * svgImage.width(),
|
||||||
|
SDL_PIXELFORMAT_RGBA32);
|
||||||
|
#ifndef Q_OS_DARWIN
|
||||||
|
// Other platforms seem to preserve our Qt icon when creating a new window.
|
||||||
|
if (iconSurface != nullptr) {
|
||||||
|
// This must be called before entering full-screen mode on Windows
|
||||||
|
// or our icon will not persist when toggling to windowed mode
|
||||||
|
SDL_SetWindowIcon(m_Window, iconSurface);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
// For non-full screen windows, call getWindowDimensions()
|
// For non-full screen windows, call getWindowDimensions()
|
||||||
// again after creating a window to allow it to account
|
// again after creating a window to allow it to account
|
||||||
// for window chrome size.
|
// for window chrome size.
|
||||||
if (m_Preferences.windowMode == StreamingPreferences::WM_WINDOWED) {
|
if (m_Preferences->windowMode == StreamingPreferences::WM_WINDOWED) {
|
||||||
getWindowDimensions(x, y, width, height);
|
getWindowDimensions(x, y, width, height);
|
||||||
|
|
||||||
// We must set the size before the position because centering
|
// We must set the size before the position because centering
|
||||||
@@ -837,30 +940,11 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
|||||||
SDL_SetWindowFullscreen(m_Window, m_FullScreenFlag);
|
SDL_SetWindowFullscreen(m_Window, m_FullScreenFlag);
|
||||||
}
|
}
|
||||||
|
|
||||||
QSvgRenderer svgIconRenderer(QString(":/res/moonlight.svg"));
|
|
||||||
QImage svgImage(ICON_SIZE, ICON_SIZE, QImage::Format_RGBA8888);
|
|
||||||
svgImage.fill(0);
|
|
||||||
|
|
||||||
QPainter svgPainter(&svgImage);
|
|
||||||
svgIconRenderer.render(&svgPainter);
|
|
||||||
SDL_Surface* iconSurface = SDL_CreateRGBSurfaceWithFormatFrom((void*)svgImage.constBits(),
|
|
||||||
svgImage.width(),
|
|
||||||
svgImage.height(),
|
|
||||||
32,
|
|
||||||
4 * svgImage.width(),
|
|
||||||
SDL_PIXELFORMAT_RGBA32);
|
|
||||||
#ifndef Q_OS_DARWIN
|
|
||||||
// Other platforms seem to preserve our Qt icon when creating a new window
|
|
||||||
if (iconSurface != nullptr) {
|
|
||||||
SDL_SetWindowIcon(m_Window, iconSurface);
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifndef QT_DEBUG
|
#ifndef QT_DEBUG
|
||||||
// Capture the mouse by default on release builds only.
|
// Capture the mouse by default on release builds only.
|
||||||
// This prevents the mouse from becoming trapped inside
|
// This prevents the mouse from becoming trapped inside
|
||||||
// Moonlight when it's halted at a debug break.
|
// Moonlight when it's halted at a debug break.
|
||||||
if (m_Preferences.windowMode != StreamingPreferences::WM_WINDOWED) {
|
if (m_Preferences->windowMode != StreamingPreferences::WM_WINDOWED) {
|
||||||
SDL_SetRelativeMouseMode(SDL_TRUE);
|
SDL_SetRelativeMouseMode(SDL_TRUE);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@@ -874,13 +958,6 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
|||||||
// Disable the screen saver
|
// Disable the screen saver
|
||||||
SDL_DisableScreenSaver();
|
SDL_DisableScreenSaver();
|
||||||
|
|
||||||
#ifdef Q_OS_WIN32
|
|
||||||
// HACK: SDL doesn't call this, so we must do so to disable the
|
|
||||||
// screensaver when we're in windowed mode. DirectX will disable
|
|
||||||
// it for us when we're in full-screen exclusive mode.
|
|
||||||
SetThreadExecutionState(ES_CONTINUOUS | ES_DISPLAY_REQUIRED);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Set timer resolution to 1 ms on Windows for greater
|
// Set timer resolution to 1 ms on Windows for greater
|
||||||
// sleep precision and more accurate callback timing.
|
// sleep precision and more accurate callback timing.
|
||||||
SDL_SetHint(SDL_HINT_TIMER_RESOLUTION, "1");
|
SDL_SetHint(SDL_HINT_TIMER_RESOLUTION, "1");
|
||||||
@@ -895,6 +972,11 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
|||||||
|
|
||||||
int currentDisplayIndex = SDL_GetWindowDisplayIndex(m_Window);
|
int currentDisplayIndex = SDL_GetWindowDisplayIndex(m_Window);
|
||||||
|
|
||||||
|
// Now that we're about to stream, any SDL_QUIT event is expected
|
||||||
|
// unless it comes from the connection termination callback where
|
||||||
|
// (m_UnexpectedTermination is set back to true).
|
||||||
|
m_UnexpectedTermination = false;
|
||||||
|
|
||||||
// Hijack this thread to be the SDL main thread. We have to do this
|
// Hijack this thread to be the SDL main thread. We have to do this
|
||||||
// because we want to suspend all Qt processing until the stream is over.
|
// because we want to suspend all Qt processing until the stream is over.
|
||||||
SDL_Event event;
|
SDL_Event event;
|
||||||
@@ -958,10 +1040,19 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Release mouse cursor when another window is activated (e.g. by using ALT+TAB).
|
|
||||||
// This lets user to interact with our window's title bar and with the buttons in it.
|
|
||||||
if (event.window.event == SDL_WINDOWEVENT_FOCUS_LOST) {
|
if (event.window.event == SDL_WINDOWEVENT_FOCUS_LOST) {
|
||||||
SDL_SetRelativeMouseMode(SDL_FALSE);
|
// Release mouse cursor when another window is activated (e.g. by using ALT+TAB).
|
||||||
|
// This lets user to interact with our window's title bar and with the buttons in it.
|
||||||
|
// Doing this while the window is full-screen breaks the transition out of FS
|
||||||
|
// (desktop and exclusive), so we must check for that before releasing mouse capture.
|
||||||
|
if (!(SDL_GetWindowFlags(m_Window) & SDL_WINDOW_FULLSCREEN)) {
|
||||||
|
SDL_SetRelativeMouseMode(SDL_FALSE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Raise all keys that are currently pressed. If we don't do this, certain keys
|
||||||
|
// used in shortcuts that cause focus loss (such as Alt+Tab) may get stuck down.
|
||||||
|
inputHandler.raiseAllKeys();
|
||||||
}
|
}
|
||||||
|
|
||||||
// We want to recreate the decoder for resizes (full-screen toggles) and the initial shown event.
|
// We want to recreate the decoder for resizes (full-screen toggles) and the initial shown event.
|
||||||
@@ -1020,7 +1111,7 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
|||||||
// forcefully disable V-sync to allow the stream to render faster
|
// forcefully disable V-sync to allow the stream to render faster
|
||||||
// than the display.
|
// than the display.
|
||||||
int displayHz = StreamUtils::getDisplayRefreshRate(m_Window);
|
int displayHz = StreamUtils::getDisplayRefreshRate(m_Window);
|
||||||
bool enableVsync = m_Preferences.enableVsync;
|
bool enableVsync = m_Preferences->enableVsync;
|
||||||
if (displayHz + 5 < m_StreamConfig.fps) {
|
if (displayHz + 5 < m_StreamConfig.fps) {
|
||||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"Disabling V-sync because refresh rate limit exceeded");
|
"Disabling V-sync because refresh rate limit exceeded");
|
||||||
@@ -1029,10 +1120,11 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
|||||||
|
|
||||||
// Choose a new decoder (hopefully the same one, but possibly
|
// Choose a new decoder (hopefully the same one, but possibly
|
||||||
// not if a GPU was removed or something).
|
// not if a GPU was removed or something).
|
||||||
if (!chooseDecoder(m_Preferences.videoDecoderSelection,
|
if (!chooseDecoder(m_Preferences->videoDecoderSelection,
|
||||||
m_Window, m_ActiveVideoFormat, m_ActiveVideoWidth,
|
m_Window, m_ActiveVideoFormat, m_ActiveVideoWidth,
|
||||||
m_ActiveVideoHeight, m_ActiveVideoFrameRate,
|
m_ActiveVideoHeight, m_ActiveVideoFrameRate,
|
||||||
enableVsync,
|
enableVsync,
|
||||||
|
enableVsync && m_Preferences->framePacing,
|
||||||
s_ActiveSession->m_VideoDecoder)) {
|
s_ActiveSession->m_VideoDecoder)) {
|
||||||
SDL_AtomicUnlock(&m_DecoderLock);
|
SDL_AtomicUnlock(&m_DecoderLock);
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
@@ -1073,32 +1165,43 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
|||||||
case SDL_CONTROLLERDEVICEREMOVED:
|
case SDL_CONTROLLERDEVICEREMOVED:
|
||||||
inputHandler.handleControllerDeviceEvent(&event.cdevice);
|
inputHandler.handleControllerDeviceEvent(&event.cdevice);
|
||||||
break;
|
break;
|
||||||
|
case SDL_JOYDEVICEADDED:
|
||||||
|
inputHandler.handleJoystickArrivalEvent(&event.jdevice);
|
||||||
|
break;
|
||||||
|
|
||||||
|
// SDL2 sends touch events from trackpads by default on
|
||||||
|
// macOS. This totally screws our actual mouse handling,
|
||||||
|
// so we must explicitly ignore touch events on macOS.
|
||||||
|
#ifndef Q_OS_DARWIN
|
||||||
|
case SDL_FINGERDOWN:
|
||||||
|
case SDL_FINGERMOTION:
|
||||||
|
case SDL_FINGERUP:
|
||||||
|
inputHandler.handleTouchFingerEvent(&event.tfinger);
|
||||||
|
break;
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"SDL_WaitEvent() failed: %s",
|
|
||||||
SDL_GetError());
|
|
||||||
|
|
||||||
DispatchDeferredCleanup:
|
DispatchDeferredCleanup:
|
||||||
#ifdef Q_OS_WIN32
|
|
||||||
// HACK: See comment above
|
|
||||||
SetThreadExecutionState(ES_CONTINUOUS);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Uncapture the mouse and hide the window immediately,
|
// Uncapture the mouse and hide the window immediately,
|
||||||
// so we can return to the Qt GUI ASAP.
|
// so we can return to the Qt GUI ASAP.
|
||||||
SDL_SetRelativeMouseMode(SDL_FALSE);
|
SDL_SetRelativeMouseMode(SDL_FALSE);
|
||||||
SDL_EnableScreenSaver();
|
SDL_EnableScreenSaver();
|
||||||
SDL_SetHint(SDL_HINT_TIMER_RESOLUTION, "0");
|
SDL_SetHint(SDL_HINT_TIMER_RESOLUTION, "0");
|
||||||
|
|
||||||
|
// Raise any keys that are still down
|
||||||
|
inputHandler.raiseAllKeys();
|
||||||
|
|
||||||
// Destroy the decoder, since this must be done on the main thread
|
// Destroy the decoder, since this must be done on the main thread
|
||||||
SDL_AtomicLock(&m_DecoderLock);
|
SDL_AtomicLock(&m_DecoderLock);
|
||||||
delete m_VideoDecoder;
|
delete m_VideoDecoder;
|
||||||
m_VideoDecoder = nullptr;
|
m_VideoDecoder = nullptr;
|
||||||
SDL_AtomicUnlock(&m_DecoderLock);
|
SDL_AtomicUnlock(&m_DecoderLock);
|
||||||
|
|
||||||
|
// This must be called after the decoder is deleted, because
|
||||||
|
// the renderer may want to interact with the window
|
||||||
SDL_DestroyWindow(m_Window);
|
SDL_DestroyWindow(m_Window);
|
||||||
|
|
||||||
if (iconSurface != nullptr) {
|
if (iconSurface != nullptr) {
|
||||||
SDL_FreeSurface(iconSurface);
|
SDL_FreeSurface(iconSurface);
|
||||||
}
|
}
|
||||||
@@ -1108,6 +1211,6 @@ DispatchDeferredCleanup:
|
|||||||
// Cleanup can take a while, so dispatch it to a worker thread.
|
// Cleanup can take a while, so dispatch it to a worker thread.
|
||||||
// When it is complete, it will release our s_ActiveSessionSemaphore
|
// When it is complete, it will release our s_ActiveSessionSemaphore
|
||||||
// reference.
|
// reference.
|
||||||
QThreadPool::globalInstance()->start(new DeferredSessionCleanupTask());
|
QThreadPool::globalInstance()->start(new DeferredSessionCleanupTask(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,7 +18,9 @@ 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);
|
||||||
|
|
||||||
|
virtual ~Session();
|
||||||
|
|
||||||
Q_INVOKABLE void exec(int displayOriginX, int displayOriginY);
|
Q_INVOKABLE void exec(int displayOriginX, int displayOriginY);
|
||||||
|
|
||||||
@@ -40,14 +43,22 @@ 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();
|
||||||
|
|
||||||
|
int detectAudioConfiguration();
|
||||||
|
|
||||||
bool testAudio(int audioConfiguration);
|
bool testAudio(int audioConfiguration);
|
||||||
|
|
||||||
@@ -61,7 +72,8 @@ private:
|
|||||||
static
|
static
|
||||||
bool chooseDecoder(StreamingPreferences::VideoDecoderSelection vds,
|
bool chooseDecoder(StreamingPreferences::VideoDecoderSelection vds,
|
||||||
SDL_Window* window, int videoFormat, int width, int height,
|
SDL_Window* window, int videoFormat, int width, int height,
|
||||||
int frameRate, bool enableVsync, IVideoDecoder*& chosenDecoder);
|
int frameRate, bool enableVsync, bool enableFramePacing,
|
||||||
|
IVideoDecoder*& chosenDecoder);
|
||||||
|
|
||||||
static
|
static
|
||||||
void clStageStarting(int stage);
|
void clStageStarting(int stage);
|
||||||
@@ -75,6 +87,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);
|
||||||
|
|
||||||
@@ -84,24 +107,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;
|
||||||
@@ -115,20 +121,18 @@ private:
|
|||||||
int m_DisplayOriginX;
|
int m_DisplayOriginX;
|
||||||
int m_DisplayOriginY;
|
int m_DisplayOriginY;
|
||||||
bool m_PendingWindowedTransition;
|
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;
|
||||||
@@ -8,6 +8,22 @@
|
|||||||
|
|
||||||
#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 totalPacerTime;
|
||||||
|
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() {}
|
||||||
@@ -17,7 +33,8 @@ public:
|
|||||||
int width,
|
int width,
|
||||||
int height,
|
int height,
|
||||||
int frameRate,
|
int frameRate,
|
||||||
bool enableVsync) = 0;
|
bool enableVsync,
|
||||||
|
bool enableFramePacing) = 0;
|
||||||
virtual bool isHardwareAccelerated() = 0;
|
virtual bool isHardwareAccelerated() = 0;
|
||||||
virtual int getDecoderCapabilities() = 0;
|
virtual int getDecoderCapabilities() = 0;
|
||||||
virtual int submitDecodeUnit(PDECODE_UNIT du) = 0;
|
virtual int submitDecodeUnit(PDECODE_UNIT du) = 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 {
|
||||||
@@ -635,9 +644,9 @@ int DXVA2Renderer::getDecoderCapabilities()
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
IFFmpegRenderer::VSyncConstraint DXVA2Renderer::getVsyncConstraint()
|
IFFmpegRenderer::FramePacingConstraint DXVA2Renderer::getFramePacingConstraint()
|
||||||
{
|
{
|
||||||
return VSYNC_ANY;
|
return PACING_ANY;
|
||||||
}
|
}
|
||||||
|
|
||||||
void DXVA2Renderer::renderFrameAtVsync(AVFrame *frame)
|
void DXVA2Renderer::renderFrameAtVsync(AVFrame *frame)
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ public:
|
|||||||
virtual void renderFrameAtVsync(AVFrame* frame);
|
virtual void renderFrameAtVsync(AVFrame* frame);
|
||||||
virtual bool needsTestFrame();
|
virtual bool needsTestFrame();
|
||||||
virtual int getDecoderCapabilities();
|
virtual int getDecoderCapabilities();
|
||||||
virtual VSyncConstraint getVsyncConstraint();
|
virtual FramePacingConstraint getFramePacingConstraint();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
bool initializeDecoder();
|
bool initializeDecoder();
|
||||||
|
|||||||
@@ -20,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)
|
||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -82,10 +83,10 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (m_FrameQueue.isEmpty()) {
|
if (m_FrameQueue.isEmpty()) {
|
||||||
SDL_AtomicUnlock(&m_FrameQueueLock);
|
SDL_AtomicUnlock(&m_FrameQueueLock);
|
||||||
|
|
||||||
@@ -110,22 +111,27 @@ RenderNextFrame:
|
|||||||
AVFrame* frame = m_FrameQueue.dequeue();
|
AVFrame* frame = m_FrameQueue.dequeue();
|
||||||
SDL_AtomicUnlock(&m_FrameQueueLock);
|
SDL_AtomicUnlock(&m_FrameQueueLock);
|
||||||
|
|
||||||
|
// Count time spent in Pacer's queues
|
||||||
|
Uint32 beforeRender = SDL_GetTicks();
|
||||||
|
m_VideoStats->totalPacerTime += beforeRender - frame->pts;
|
||||||
|
|
||||||
// Render it
|
// Render it
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Pacer::initialize(SDL_Window* window, int maxVideoFps, bool enableVsync)
|
bool Pacer::initialize(SDL_Window* window, int maxVideoFps, bool enablePacing)
|
||||||
{
|
{
|
||||||
m_MaxVideoFps = maxVideoFps;
|
m_MaxVideoFps = maxVideoFps;
|
||||||
m_EnableVsync = enableVsync;
|
|
||||||
m_DisplayFps = StreamUtils::getDisplayRefreshRate(window);
|
m_DisplayFps = StreamUtils::getDisplayRefreshRate(window);
|
||||||
|
|
||||||
if (m_EnableVsync) {
|
if (enablePacing) {
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"Frame pacing in tear-free mode: target %d Hz with %d FPS stream",
|
"Frame pacing active: target %d Hz with %d FPS stream",
|
||||||
m_DisplayFps, m_MaxVideoFps);
|
m_DisplayFps, m_MaxVideoFps);
|
||||||
|
|
||||||
#if defined(Q_OS_DARWIN)
|
#if defined(Q_OS_DARWIN)
|
||||||
@@ -143,7 +149,7 @@ bool Pacer::initialize(SDL_Window* window, int maxVideoFps, bool enableVsync)
|
|||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"Minimal latency tearing mode: target %d Hz with %d FPS stream",
|
"Frame pacing disabled: target %d Hz with %d FPS stream",
|
||||||
m_DisplayFps, m_MaxVideoFps);
|
m_DisplayFps, m_MaxVideoFps);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,7 +170,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,13 +14,13 @@ public:
|
|||||||
class Pacer
|
class Pacer
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
Pacer(IFFmpegRenderer* renderer);
|
Pacer(IFFmpegRenderer* renderer, PVIDEO_STATS videoStats);
|
||||||
|
|
||||||
~Pacer();
|
~Pacer();
|
||||||
|
|
||||||
void submitFrame(AVFrame* frame);
|
void submitFrame(AVFrame* frame);
|
||||||
|
|
||||||
bool initialize(SDL_Window* window, int maxVideoFps, bool enableVsync);
|
bool initialize(SDL_Window* window, int maxVideoFps, bool enablePacing);
|
||||||
|
|
||||||
void vsyncCallback(int timeUntilNextVsyncMillis);
|
void vsyncCallback(int timeUntilNextVsyncMillis);
|
||||||
|
|
||||||
@@ -34,5 +35,5 @@ private:
|
|||||||
IFFmpegRenderer* m_VsyncRenderer;
|
IFFmpegRenderer* m_VsyncRenderer;
|
||||||
int m_MaxVideoFps;
|
int m_MaxVideoFps;
|
||||||
int m_DisplayFps;
|
int m_DisplayFps;
|
||||||
bool m_EnableVsync;
|
PVIDEO_STATS m_VideoStats;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ extern "C" {
|
|||||||
|
|
||||||
class IFFmpegRenderer {
|
class IFFmpegRenderer {
|
||||||
public:
|
public:
|
||||||
enum VSyncConstraint {
|
enum FramePacingConstraint {
|
||||||
VSYNC_FORCE_OFF,
|
PACING_FORCE_OFF,
|
||||||
VSYNC_FORCE_ON,
|
PACING_FORCE_ON,
|
||||||
VSYNC_ANY
|
PACING_ANY
|
||||||
};
|
};
|
||||||
|
|
||||||
virtual ~IFFmpegRenderer() {}
|
virtual ~IFFmpegRenderer() {}
|
||||||
@@ -25,7 +25,7 @@ public:
|
|||||||
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 int getDecoderCapabilities() = 0;
|
||||||
virtual VSyncConstraint getVsyncConstraint() = 0;
|
virtual FramePacingConstraint getFramePacingConstraint() = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
class SdlRenderer : public IFFmpegRenderer {
|
class SdlRenderer : public IFFmpegRenderer {
|
||||||
@@ -42,7 +42,7 @@ public:
|
|||||||
virtual void renderFrameAtVsync(AVFrame* frame);
|
virtual void renderFrameAtVsync(AVFrame* frame);
|
||||||
virtual bool needsTestFrame();
|
virtual bool needsTestFrame();
|
||||||
virtual int getDecoderCapabilities();
|
virtual int getDecoderCapabilities();
|
||||||
virtual VSyncConstraint getVsyncConstraint();
|
virtual FramePacingConstraint getFramePacingConstraint();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
SDL_Renderer* m_Renderer;
|
SDL_Renderer* m_Renderer;
|
||||||
|
|||||||
@@ -43,9 +43,9 @@ int SdlRenderer::getDecoderCapabilities()
|
|||||||
return CAPABILITY_REFERENCE_FRAME_INVALIDATION_AVC;
|
return CAPABILITY_REFERENCE_FRAME_INVALIDATION_AVC;
|
||||||
}
|
}
|
||||||
|
|
||||||
IFFmpegRenderer::VSyncConstraint SdlRenderer::getVsyncConstraint()
|
IFFmpegRenderer::FramePacingConstraint SdlRenderer::getFramePacingConstraint()
|
||||||
{
|
{
|
||||||
return VSYNC_ANY;
|
return PACING_ANY;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool SdlRenderer::initialize(SDL_Window* window,
|
bool SdlRenderer::initialize(SDL_Window* window,
|
||||||
@@ -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) {
|
||||||
@@ -155,9 +177,9 @@ VAAPIRenderer::getDecoderCapabilities()
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
IFFmpegRenderer::VSyncConstraint VAAPIRenderer::getVsyncConstraint()
|
IFFmpegRenderer::FramePacingConstraint VAAPIRenderer::getFramePacingConstraint()
|
||||||
{
|
{
|
||||||
return VSYNC_ANY;
|
return PACING_ANY;
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ public:
|
|||||||
virtual void renderFrameAtVsync(AVFrame* frame);
|
virtual void renderFrameAtVsync(AVFrame* frame);
|
||||||
virtual bool needsTestFrame();
|
virtual bool needsTestFrame();
|
||||||
virtual int getDecoderCapabilities();
|
virtual int getDecoderCapabilities();
|
||||||
virtual VSyncConstraint getVsyncConstraint();
|
virtual FramePacingConstraint getFramePacingConstraint();
|
||||||
|
|
||||||
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++) {
|
||||||
@@ -238,9 +246,9 @@ int VDPAURenderer::getDecoderCapabilities()
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
IFFmpegRenderer::VSyncConstraint VDPAURenderer::getVsyncConstraint()
|
IFFmpegRenderer::FramePacingConstraint VDPAURenderer::getFramePacingConstraint()
|
||||||
{
|
{
|
||||||
return VSYNC_ANY;
|
return PACING_ANY;
|
||||||
}
|
}
|
||||||
|
|
||||||
void VDPAURenderer::renderFrameAtVsync(AVFrame* frame)
|
void VDPAURenderer::renderFrameAtVsync(AVFrame* frame)
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ public:
|
|||||||
virtual void renderFrameAtVsync(AVFrame* frame);
|
virtual void renderFrameAtVsync(AVFrame* frame);
|
||||||
virtual bool needsTestFrame();
|
virtual bool needsTestFrame();
|
||||||
virtual int getDecoderCapabilities();
|
virtual int getDecoderCapabilities();
|
||||||
virtual VSyncConstraint getVsyncConstraint();
|
virtual FramePacingConstraint getFramePacingConstraint();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
uint32_t m_VideoWidth, m_VideoHeight;
|
uint32_t m_VideoWidth, m_VideoHeight;
|
||||||
@@ -56,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,
|
||||||
@@ -201,33 +211,15 @@ public:
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
virtual IFFmpegRenderer::VSyncConstraint getVsyncConstraint() override
|
virtual IFFmpegRenderer::FramePacingConstraint getFramePacingConstraint() override
|
||||||
{
|
{
|
||||||
// This renderer is inherently tied to V-sync due how we're
|
// This renderer is inherently tied to V-sync due how we're
|
||||||
// rendering with AVSampleBufferDisplay layer. Running without
|
// rendering with AVSampleBufferDisplay layer. Running without
|
||||||
// the V-Sync source leads to massive stuttering.
|
// the V-Sync source leads to massive stuttering.
|
||||||
return VSYNC_FORCE_ON;
|
return PACING_FORCE_ON;
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void setupDisplayLayer()
|
|
||||||
{
|
|
||||||
CALayer* oldLayer = m_DisplayLayer;
|
|
||||||
|
|
||||||
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];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AVBufferRef* m_HwContext;
|
AVBufferRef* m_HwContext;
|
||||||
AVSampleBufferDisplayLayer* m_DisplayLayer;
|
AVSampleBufferDisplayLayer* m_DisplayLayer;
|
||||||
CMVideoFormatDescriptionRef m_FormatDesc;
|
CMVideoFormatDescriptionRef m_FormatDesc;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#include <Limelight.h>
|
#include <Limelight.h>
|
||||||
#include "ffmpeg.h"
|
#include "ffmpeg.h"
|
||||||
#include "streaming/streamutils.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"
|
||||||
@@ -21,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()
|
||||||
@@ -60,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");
|
||||||
}
|
}
|
||||||
@@ -115,30 +125,33 @@ void FFmpegVideoDecoder::reset()
|
|||||||
|
|
||||||
delete m_Renderer;
|
delete m_Renderer;
|
||||||
m_Renderer = nullptr;
|
m_Renderer = nullptr;
|
||||||
|
|
||||||
|
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 enableFramePacing, bool testOnly)
|
||||||
{
|
{
|
||||||
auto vsyncConstraint = m_Renderer->getVsyncConstraint();
|
auto vsyncConstraint = m_Renderer->getFramePacingConstraint();
|
||||||
if (vsyncConstraint == IFFmpegRenderer::VSYNC_FORCE_OFF && enableVsync) {
|
if (vsyncConstraint == IFFmpegRenderer::PACING_FORCE_OFF && enableFramePacing) {
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"V-sync is forcefully disabled by the active renderer");
|
"Frame pacing is forcefully disabled by the active renderer");
|
||||||
enableVsync = false;
|
enableFramePacing = false;
|
||||||
}
|
}
|
||||||
else if (vsyncConstraint == IFFmpegRenderer::VSYNC_FORCE_ON && !enableVsync) {
|
else if (vsyncConstraint == IFFmpegRenderer::PACING_FORCE_ON && !enableFramePacing) {
|
||||||
// FIXME: This duplicates logic in Session.cpp
|
// FIXME: This duplicates logic in Session.cpp
|
||||||
int displayHz = StreamUtils::getDisplayRefreshRate(window);
|
int displayHz = StreamUtils::getDisplayRefreshRate(window);
|
||||||
if (displayHz + 5 >= maxFps) {
|
if (displayHz + 5 >= maxFps) {
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"V-sync is forcefully enabled by the active renderer");
|
"Frame pacing is forcefully enabled by the active renderer");
|
||||||
enableVsync = true;
|
enableFramePacing = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
m_Pacer = new Pacer(m_Renderer);
|
m_StreamFps = maxFps;
|
||||||
if (!m_Pacer->initialize(window, maxFps, enableVsync)) {
|
m_Pacer = new Pacer(m_Renderer, &m_ActiveWndVideoStats);
|
||||||
|
if (!m_Pacer->initialize(window, maxFps, enableFramePacing)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,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
|
||||||
@@ -225,6 +249,84 @@ 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.totalPacerTime += src.totalPacerTime;
|
||||||
|
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 frame pacing delay: %.2f ms",
|
||||||
|
(float)stats.totalPacerTime / stats.renderedFrames);
|
||||||
|
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)) {
|
||||||
@@ -261,7 +363,8 @@ bool FFmpegVideoDecoder::initialize(
|
|||||||
int width,
|
int width,
|
||||||
int height,
|
int height,
|
||||||
int maxFps,
|
int maxFps,
|
||||||
bool enableVsync)
|
bool enableVsync,
|
||||||
|
bool enableFramePacing)
|
||||||
{
|
{
|
||||||
AVCodec* decoder;
|
AVCodec* decoder;
|
||||||
|
|
||||||
@@ -297,7 +400,7 @@ bool FFmpegVideoDecoder::initialize(
|
|||||||
m_Renderer = new SdlRenderer();
|
m_Renderer = new SdlRenderer();
|
||||||
if (vds != StreamingPreferences::VDS_FORCE_HARDWARE &&
|
if (vds != StreamingPreferences::VDS_FORCE_HARDWARE &&
|
||||||
m_Renderer->initialize(window, videoFormat, width, height, maxFps, enableVsync) &&
|
m_Renderer->initialize(window, videoFormat, width, height, maxFps, enableVsync) &&
|
||||||
completeInitialization(decoder, window, videoFormat, width, height, maxFps, enableVsync, false)) {
|
completeInitialization(decoder, window, videoFormat, width, height, maxFps, enableFramePacing, false)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -314,13 +417,13 @@ bool FFmpegVideoDecoder::initialize(
|
|||||||
m_HwDecodeCfg = config;
|
m_HwDecodeCfg = config;
|
||||||
// Initialize the hardware codec and submit a test frame if the renderer needs it
|
// Initialize the hardware codec and submit a test frame if the renderer needs it
|
||||||
if (m_Renderer->initialize(window, videoFormat, width, height, maxFps, enableVsync) &&
|
if (m_Renderer->initialize(window, videoFormat, width, height, maxFps, enableVsync) &&
|
||||||
completeInitialization(decoder, window, videoFormat, width, height, maxFps, enableVsync, m_Renderer->needsTestFrame())) {
|
completeInitialization(decoder, window, videoFormat, width, height, maxFps, enableFramePacing, m_Renderer->needsTestFrame())) {
|
||||||
if (m_Renderer->needsTestFrame()) {
|
if (m_Renderer->needsTestFrame()) {
|
||||||
// The test worked, so now let's initialize it for real
|
// The test worked, so now let's initialize it for real
|
||||||
reset();
|
reset();
|
||||||
if ((m_Renderer = createAcceleratedRenderer(config)) != nullptr &&
|
if ((m_Renderer = createAcceleratedRenderer(config)) != nullptr &&
|
||||||
m_Renderer->initialize(window, videoFormat, width, height, maxFps, enableVsync) &&
|
m_Renderer->initialize(window, videoFormat, width, height, maxFps, enableVsync) &&
|
||||||
completeInitialization(decoder, window, videoFormat, width, height, maxFps, enableVsync, false)) {
|
completeInitialization(decoder, window, videoFormat, width, height, maxFps, enableFramePacing, false)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -341,28 +444,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) {
|
||||||
@@ -400,9 +582,17 @@ int FFmpegVideoDecoder::submitDecodeUnit(PDECODE_UNIT du)
|
|||||||
// Reset failed decodes count if we reached this far
|
// Reset failed decodes count if we reached this far
|
||||||
m_ConsecutiveFailedDecodes = 0;
|
m_ConsecutiveFailedDecodes = 0;
|
||||||
|
|
||||||
|
// Capture a frame timestamp to measuring pacing delay
|
||||||
|
frame->pts = SDL_GetTicks();
|
||||||
|
|
||||||
// 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);
|
||||||
@@ -446,6 +636,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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ public:
|
|||||||
int width,
|
int width,
|
||||||
int height,
|
int height,
|
||||||
int maxFps,
|
int maxFps,
|
||||||
bool enableVsync) override;
|
bool enableVsync,
|
||||||
|
bool enableFramePacing) override;
|
||||||
virtual bool isHardwareAccelerated() override;
|
virtual bool isHardwareAccelerated() override;
|
||||||
virtual int getDecoderCapabilities() override;
|
virtual int getDecoderCapabilities() override;
|
||||||
virtual int submitDecodeUnit(PDECODE_UNIT du) override;
|
virtual int submitDecodeUnit(PDECODE_UNIT du) override;
|
||||||
@@ -30,12 +31,18 @@ public:
|
|||||||
private:
|
private:
|
||||||
bool completeInitialization(AVCodec* decoder, SDL_Window* window,
|
bool 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 enableFramePacing, 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);
|
||||||
@@ -48,6 +55,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[];
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ SLVideoDecoder::getDecoderCapabilities()
|
|||||||
bool
|
bool
|
||||||
SLVideoDecoder::initialize(StreamingPreferences::VideoDecoderSelection vds,
|
SLVideoDecoder::initialize(StreamingPreferences::VideoDecoderSelection vds,
|
||||||
SDL_Window*,
|
SDL_Window*,
|
||||||
int videoFormat, int, int, int frameRate, bool)
|
int videoFormat, int, int, int frameRate, bool, bool)
|
||||||
{
|
{
|
||||||
// SLVideo only supports hardware decoding
|
// SLVideo only supports hardware decoding
|
||||||
if (vds == StreamingPreferences::VDS_FORCE_SOFTWARE) {
|
if (vds == StreamingPreferences::VDS_FORCE_SOFTWARE) {
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ public:
|
|||||||
int width,
|
int width,
|
||||||
int height,
|
int height,
|
||||||
int frameRate,
|
int frameRate,
|
||||||
bool enableVsync);
|
bool enableVsync,
|
||||||
|
bool enableFramePacing);
|
||||||
virtual bool isHardwareAccelerated();
|
virtual bool isHardwareAccelerated();
|
||||||
virtual int getDecoderCapabilities();
|
virtual int getDecoderCapabilities();
|
||||||
virtual int submitDecodeUnit(PDECODE_UNIT du);
|
virtual int submitDecodeUnit(PDECODE_UNIT du);
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
0.8.1
|
||||||
@@ -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
|
||||||
@@ -409,6 +409,7 @@ enum AVCodecID {
|
|||||||
AV_CODEC_ID_DXV,
|
AV_CODEC_ID_DXV,
|
||||||
AV_CODEC_ID_SCREENPRESSO,
|
AV_CODEC_ID_SCREENPRESSO,
|
||||||
AV_CODEC_ID_RSCC,
|
AV_CODEC_ID_RSCC,
|
||||||
|
AV_CODEC_ID_AVS2,
|
||||||
|
|
||||||
AV_CODEC_ID_Y41P = 0x8000,
|
AV_CODEC_ID_Y41P = 0x8000,
|
||||||
AV_CODEC_ID_AVRP,
|
AV_CODEC_ID_AVRP,
|
||||||
@@ -446,6 +447,11 @@ enum AVCodecID {
|
|||||||
AV_CODEC_ID_SVG,
|
AV_CODEC_ID_SVG,
|
||||||
AV_CODEC_ID_GDV,
|
AV_CODEC_ID_GDV,
|
||||||
AV_CODEC_ID_FITS,
|
AV_CODEC_ID_FITS,
|
||||||
|
AV_CODEC_ID_IMM4,
|
||||||
|
AV_CODEC_ID_PROSUMER,
|
||||||
|
AV_CODEC_ID_MWSC,
|
||||||
|
AV_CODEC_ID_WCMV,
|
||||||
|
AV_CODEC_ID_RASC,
|
||||||
|
|
||||||
/* various PCM "codecs" */
|
/* various PCM "codecs" */
|
||||||
AV_CODEC_ID_FIRST_AUDIO = 0x10000, ///< A dummy id pointing at the start of audio codecs
|
AV_CODEC_ID_FIRST_AUDIO = 0x10000, ///< A dummy id pointing at the start of audio codecs
|
||||||
@@ -485,6 +491,7 @@ enum AVCodecID {
|
|||||||
AV_CODEC_ID_PCM_S64BE,
|
AV_CODEC_ID_PCM_S64BE,
|
||||||
AV_CODEC_ID_PCM_F16LE,
|
AV_CODEC_ID_PCM_F16LE,
|
||||||
AV_CODEC_ID_PCM_F24LE,
|
AV_CODEC_ID_PCM_F24LE,
|
||||||
|
AV_CODEC_ID_PCM_VIDC,
|
||||||
|
|
||||||
/* various ADPCM codecs */
|
/* various ADPCM codecs */
|
||||||
AV_CODEC_ID_ADPCM_IMA_QT = 0x11000,
|
AV_CODEC_ID_ADPCM_IMA_QT = 0x11000,
|
||||||
@@ -1359,6 +1366,12 @@ enum AVPacketSideDataType {
|
|||||||
*/
|
*/
|
||||||
AV_PKT_DATA_ENCRYPTION_INFO,
|
AV_PKT_DATA_ENCRYPTION_INFO,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Active Format Description data consisting of a single byte as specified
|
||||||
|
* in ETSI TS 101 154 using AVActiveFormatDescription enum.
|
||||||
|
*/
|
||||||
|
AV_PKT_DATA_AFD,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The number of side data types.
|
* The number of side data types.
|
||||||
* This is not part of the public API/ABI in the sense that it may
|
* This is not part of the public API/ABI in the sense that it may
|
||||||
@@ -1614,6 +1627,7 @@ typedef struct AVCodecContext {
|
|||||||
* The allocated memory should be AV_INPUT_BUFFER_PADDING_SIZE bytes larger
|
* The allocated memory should be AV_INPUT_BUFFER_PADDING_SIZE bytes larger
|
||||||
* than extradata_size to avoid problems if it is read with the bitstream reader.
|
* than extradata_size to avoid problems if it is read with the bitstream reader.
|
||||||
* The bytewise contents of extradata must not depend on the architecture or CPU endianness.
|
* The bytewise contents of extradata must not depend on the architecture or CPU endianness.
|
||||||
|
* Must be allocated with the av_malloc() family of functions.
|
||||||
* - encoding: Set/allocated/freed by libavcodec.
|
* - encoding: Set/allocated/freed by libavcodec.
|
||||||
* - decoding: Set/allocated/freed by user.
|
* - decoding: Set/allocated/freed by user.
|
||||||
*/
|
*/
|
||||||
@@ -5768,6 +5782,7 @@ typedef struct AVBitStreamFilter {
|
|||||||
int (*init)(AVBSFContext *ctx);
|
int (*init)(AVBSFContext *ctx);
|
||||||
int (*filter)(AVBSFContext *ctx, AVPacket *pkt);
|
int (*filter)(AVBSFContext *ctx, AVPacket *pkt);
|
||||||
void (*close)(AVBSFContext *ctx);
|
void (*close)(AVBSFContext *ctx);
|
||||||
|
void (*flush)(AVBSFContext *ctx);
|
||||||
} AVBitStreamFilter;
|
} AVBitStreamFilter;
|
||||||
|
|
||||||
#if FF_API_OLD_BSF
|
#if FF_API_OLD_BSF
|
||||||
@@ -5894,6 +5909,11 @@ int av_bsf_send_packet(AVBSFContext *ctx, AVPacket *pkt);
|
|||||||
*/
|
*/
|
||||||
int av_bsf_receive_packet(AVBSFContext *ctx, AVPacket *pkt);
|
int av_bsf_receive_packet(AVBSFContext *ctx, AVPacket *pkt);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset the internal bitstream filter state / flush internal buffers.
|
||||||
|
*/
|
||||||
|
void av_bsf_flush(AVBSFContext *ctx);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Free a bitstream filter context and everything associated with it; write NULL
|
* Free a bitstream filter context and everything associated with it; write NULL
|
||||||
* into the supplied pointer.
|
* into the supplied pointer.
|
||||||
|
|||||||
@@ -85,4 +85,17 @@ typedef struct MediaCodecBuffer AVMediaCodecBuffer;
|
|||||||
*/
|
*/
|
||||||
int av_mediacodec_release_buffer(AVMediaCodecBuffer *buffer, int render);
|
int av_mediacodec_release_buffer(AVMediaCodecBuffer *buffer, int render);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Release a MediaCodec buffer and render it at the given time to the surface
|
||||||
|
* that is associated with the decoder. The timestamp must be within one second
|
||||||
|
* of the current java/lang/System#nanoTime() (which is implemented using
|
||||||
|
* CLOCK_MONOTONIC on Android). See the Android MediaCodec documentation
|
||||||
|
* of android/media/MediaCodec#releaseOutputBuffer(int,long) for more details.
|
||||||
|
*
|
||||||
|
* @param buffer the buffer to render
|
||||||
|
* @param time timestamp in nanoseconds of when to render the buffer
|
||||||
|
* @return 0 on success, < 0 otherwise
|
||||||
|
*/
|
||||||
|
int av_mediacodec_render_buffer_at_time(AVMediaCodecBuffer *buffer, int64_t time);
|
||||||
|
|
||||||
#endif /* AVCODEC_MEDIACODEC_H */
|
#endif /* AVCODEC_MEDIACODEC_H */
|
||||||
|
|||||||
@@ -28,8 +28,8 @@
|
|||||||
#include "libavutil/version.h"
|
#include "libavutil/version.h"
|
||||||
|
|
||||||
#define LIBAVCODEC_VERSION_MAJOR 58
|
#define LIBAVCODEC_VERSION_MAJOR 58
|
||||||
#define LIBAVCODEC_VERSION_MINOR 21
|
#define LIBAVCODEC_VERSION_MINOR 35
|
||||||
#define LIBAVCODEC_VERSION_MICRO 104
|
#define LIBAVCODEC_VERSION_MICRO 100
|
||||||
|
|
||||||
#define LIBAVCODEC_VERSION_INT AV_VERSION_INT(LIBAVCODEC_VERSION_MAJOR, \
|
#define LIBAVCODEC_VERSION_INT AV_VERSION_INT(LIBAVCODEC_VERSION_MAJOR, \
|
||||||
LIBAVCODEC_VERSION_MINOR, \
|
LIBAVCODEC_VERSION_MINOR, \
|
||||||
|
|||||||
@@ -66,7 +66,7 @@
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Assert that floating point opperations can be executed.
|
* Assert that floating point operations can be executed.
|
||||||
*
|
*
|
||||||
* This will av_assert0() that the cpu is not in MMX state on X86
|
* This will av_assert0() that the cpu is not in MMX state on X86
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/* Automatically generated by version.sh, do not manually edit! */
|
/* Automatically generated by version.sh, do not manually edit! */
|
||||||
#ifndef AVUTIL_FFVERSION_H
|
#ifndef AVUTIL_FFVERSION_H
|
||||||
#define AVUTIL_FFVERSION_H
|
#define AVUTIL_FFVERSION_H
|
||||||
#define FFMPEG_VERSION "N-91434-gcced03dd66"
|
#define FFMPEG_VERSION "4.1"
|
||||||
#endif /* AVUTIL_FFVERSION_H */
|
#endif /* AVUTIL_FFVERSION_H */
|
||||||
|
|||||||
@@ -33,6 +33,8 @@
|
|||||||
* allocated buffer or map it with mmap() when available.
|
* allocated buffer or map it with mmap() when available.
|
||||||
* In case of success set *bufptr to the read or mmapped buffer, and
|
* In case of success set *bufptr to the read or mmapped buffer, and
|
||||||
* *size to the size in bytes of the buffer in *bufptr.
|
* *size to the size in bytes of the buffer in *bufptr.
|
||||||
|
* Unlike mmap this function succeeds with zero sized files, in this
|
||||||
|
* case *bufptr will be set to NULL and *size will be set to 0.
|
||||||
* The returned buffer must be released with av_file_unmap().
|
* The returned buffer must be released with av_file_unmap().
|
||||||
*
|
*
|
||||||
* @param log_offset loglevel offset used for logging
|
* @param log_offset loglevel offset used for logging
|
||||||
|
|||||||
@@ -158,6 +158,14 @@ enum AVFrameSideDataType {
|
|||||||
*/
|
*/
|
||||||
AV_FRAME_DATA_QP_TABLE_DATA,
|
AV_FRAME_DATA_QP_TABLE_DATA,
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timecode which conforms to SMPTE ST 12-1. The data is an array of 4 uint32_t
|
||||||
|
* where the first uint32_t describes how many (1-3) of the other timecodes are used.
|
||||||
|
* The timecode format is described in the av_timecode_get_smpte_from_framenum()
|
||||||
|
* function in libavutil/timecode.c.
|
||||||
|
*/
|
||||||
|
AV_FRAME_DATA_S12M_TIMECODE,
|
||||||
};
|
};
|
||||||
|
|
||||||
enum AVActiveFormatDescription {
|
enum AVActiveFormatDescription {
|
||||||
|
|||||||
@@ -343,7 +343,13 @@ char *av_get_pix_fmt_string(char *buf, int buf_size,
|
|||||||
* format writes the values corresponding to the palette
|
* format writes the values corresponding to the palette
|
||||||
* component c in data[1] to dst, rather than the palette indexes in
|
* component c in data[1] to dst, rather than the palette indexes in
|
||||||
* data[0]. The behavior is undefined if the format is not paletted.
|
* data[0]. The behavior is undefined if the format is not paletted.
|
||||||
|
* @param dst_element_size size of elements in dst array (2 or 4 byte)
|
||||||
*/
|
*/
|
||||||
|
void av_read_image_line2(void *dst, const uint8_t *data[4],
|
||||||
|
const int linesize[4], const AVPixFmtDescriptor *desc,
|
||||||
|
int x, int y, int c, int w, int read_pal_component,
|
||||||
|
int dst_element_size);
|
||||||
|
|
||||||
void av_read_image_line(uint16_t *dst, const uint8_t *data[4],
|
void av_read_image_line(uint16_t *dst, const uint8_t *data[4],
|
||||||
const int linesize[4], const AVPixFmtDescriptor *desc,
|
const int linesize[4], const AVPixFmtDescriptor *desc,
|
||||||
int x, int y, int c, int w, int read_pal_component);
|
int x, int y, int c, int w, int read_pal_component);
|
||||||
@@ -361,7 +367,12 @@ void av_read_image_line(uint16_t *dst, const uint8_t *data[4],
|
|||||||
* @param y the vertical coordinate of the first pixel to write
|
* @param y the vertical coordinate of the first pixel to write
|
||||||
* @param w the width of the line to write, that is the number of
|
* @param w the width of the line to write, that is the number of
|
||||||
* values to write to the image line
|
* values to write to the image line
|
||||||
|
* @param src_element_size size of elements in src array (2 or 4 byte)
|
||||||
*/
|
*/
|
||||||
|
void av_write_image_line2(const void *src, uint8_t *data[4],
|
||||||
|
const int linesize[4], const AVPixFmtDescriptor *desc,
|
||||||
|
int x, int y, int c, int w, int src_element_size);
|
||||||
|
|
||||||
void av_write_image_line(const uint16_t *src, uint8_t *data[4],
|
void av_write_image_line(const uint16_t *src, uint8_t *data[4],
|
||||||
const int linesize[4], const AVPixFmtDescriptor *desc,
|
const int linesize[4], const AVPixFmtDescriptor *desc,
|
||||||
int x, int y, int c, int w);
|
int x, int y, int c, int w);
|
||||||
|
|||||||
@@ -42,6 +42,10 @@
|
|||||||
* This is stored as BGRA on little-endian CPU architectures and ARGB on
|
* This is stored as BGRA on little-endian CPU architectures and ARGB on
|
||||||
* big-endian CPUs.
|
* big-endian CPUs.
|
||||||
*
|
*
|
||||||
|
* @note
|
||||||
|
* If the resolution is not a multiple of the chroma subsampling factor
|
||||||
|
* then the chroma plane resolution must be rounded up.
|
||||||
|
*
|
||||||
* @par
|
* @par
|
||||||
* When the pixel format is palettized RGB32 (AV_PIX_FMT_PAL8), the palettized
|
* When the pixel format is palettized RGB32 (AV_PIX_FMT_PAL8), the palettized
|
||||||
* image data is stored in AVFrame.data[0]. The palette is transported in
|
* image data is stored in AVFrame.data[0]. The palette is transported in
|
||||||
@@ -333,6 +337,9 @@ enum AVPixelFormat {
|
|||||||
AV_PIX_FMT_GRAY14BE, ///< Y , 14bpp, big-endian
|
AV_PIX_FMT_GRAY14BE, ///< Y , 14bpp, big-endian
|
||||||
AV_PIX_FMT_GRAY14LE, ///< Y , 14bpp, little-endian
|
AV_PIX_FMT_GRAY14LE, ///< Y , 14bpp, little-endian
|
||||||
|
|
||||||
|
AV_PIX_FMT_GRAYF32BE, ///< IEEE-754 single precision Y, 32bpp, big-endian
|
||||||
|
AV_PIX_FMT_GRAYF32LE, ///< IEEE-754 single precision Y, 32bpp, little-endian
|
||||||
|
|
||||||
AV_PIX_FMT_NB ///< number of pixel formats, DO NOT USE THIS if you want to link with shared libav* because the number of formats might differ between versions
|
AV_PIX_FMT_NB ///< number of pixel formats, DO NOT USE THIS if you want to link with shared libav* because the number of formats might differ between versions
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -401,6 +408,8 @@ enum AVPixelFormat {
|
|||||||
#define AV_PIX_FMT_GBRPF32 AV_PIX_FMT_NE(GBRPF32BE, GBRPF32LE)
|
#define AV_PIX_FMT_GBRPF32 AV_PIX_FMT_NE(GBRPF32BE, GBRPF32LE)
|
||||||
#define AV_PIX_FMT_GBRAPF32 AV_PIX_FMT_NE(GBRAPF32BE, GBRAPF32LE)
|
#define AV_PIX_FMT_GBRAPF32 AV_PIX_FMT_NE(GBRAPF32BE, GBRAPF32LE)
|
||||||
|
|
||||||
|
#define AV_PIX_FMT_GRAYF32 AV_PIX_FMT_NE(GRAYF32BE, GRAYF32LE)
|
||||||
|
|
||||||
#define AV_PIX_FMT_YUVA420P9 AV_PIX_FMT_NE(YUVA420P9BE , YUVA420P9LE)
|
#define AV_PIX_FMT_YUVA420P9 AV_PIX_FMT_NE(YUVA420P9BE , YUVA420P9LE)
|
||||||
#define AV_PIX_FMT_YUVA422P9 AV_PIX_FMT_NE(YUVA422P9BE , YUVA422P9LE)
|
#define AV_PIX_FMT_YUVA422P9 AV_PIX_FMT_NE(YUVA422P9BE , YUVA422P9LE)
|
||||||
#define AV_PIX_FMT_YUVA444P9 AV_PIX_FMT_NE(YUVA444P9BE , YUVA444P9LE)
|
#define AV_PIX_FMT_YUVA444P9 AV_PIX_FMT_NE(YUVA444P9BE , YUVA444P9LE)
|
||||||
|
|||||||
@@ -79,8 +79,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#define LIBAVUTIL_VERSION_MAJOR 56
|
#define LIBAVUTIL_VERSION_MAJOR 56
|
||||||
#define LIBAVUTIL_VERSION_MINOR 18
|
#define LIBAVUTIL_VERSION_MINOR 22
|
||||||
#define LIBAVUTIL_VERSION_MICRO 102
|
#define LIBAVUTIL_VERSION_MICRO 100
|
||||||
|
|
||||||
#define LIBAVUTIL_VERSION_INT AV_VERSION_INT(LIBAVUTIL_VERSION_MAJOR, \
|
#define LIBAVUTIL_VERSION_INT AV_VERSION_INT(LIBAVUTIL_VERSION_MAJOR, \
|
||||||
LIBAVUTIL_VERSION_MINOR, \
|
LIBAVUTIL_VERSION_MINOR, \
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -97,10 +97,10 @@ typedef enum
|
|||||||
typedef enum
|
typedef enum
|
||||||
{
|
{
|
||||||
SDL_JOYSTICK_POWER_UNKNOWN = -1,
|
SDL_JOYSTICK_POWER_UNKNOWN = -1,
|
||||||
SDL_JOYSTICK_POWER_EMPTY,
|
SDL_JOYSTICK_POWER_EMPTY, /* <= 5% */
|
||||||
SDL_JOYSTICK_POWER_LOW,
|
SDL_JOYSTICK_POWER_LOW, /* <= 20% */
|
||||||
SDL_JOYSTICK_POWER_MEDIUM,
|
SDL_JOYSTICK_POWER_MEDIUM, /* <= 70% */
|
||||||
SDL_JOYSTICK_POWER_FULL,
|
SDL_JOYSTICK_POWER_FULL, /* <= 100% */
|
||||||
SDL_JOYSTICK_POWER_WIRED,
|
SDL_JOYSTICK_POWER_WIRED,
|
||||||
SDL_JOYSTICK_POWER_MAX
|
SDL_JOYSTICK_POWER_MAX
|
||||||
} SDL_JoystickPowerLevel;
|
} SDL_JoystickPowerLevel;
|
||||||
@@ -132,6 +132,12 @@ extern DECLSPEC int SDLCALL SDL_NumJoysticks(void);
|
|||||||
*/
|
*/
|
||||||
extern DECLSPEC const char *SDLCALL SDL_JoystickNameForIndex(int device_index);
|
extern DECLSPEC const char *SDLCALL SDL_JoystickNameForIndex(int device_index);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the player index of a joystick, or -1 if it's not available
|
||||||
|
* This can be called before any joysticks are opened.
|
||||||
|
*/
|
||||||
|
extern DECLSPEC int SDLCALL SDL_JoystickGetDevicePlayerIndex(int device_index);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the GUID for the joystick at this index
|
* Return the GUID for the joystick at this index
|
||||||
* This can be called before any joysticks are opened.
|
* This can be called before any joysticks are opened.
|
||||||
@@ -194,6 +200,13 @@ extern DECLSPEC SDL_Joystick *SDLCALL SDL_JoystickFromInstanceID(SDL_JoystickID
|
|||||||
*/
|
*/
|
||||||
extern DECLSPEC const char *SDLCALL SDL_JoystickName(SDL_Joystick * joystick);
|
extern DECLSPEC const char *SDLCALL SDL_JoystickName(SDL_Joystick * joystick);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the player index of an opened joystick, or -1 if it's not available
|
||||||
|
*
|
||||||
|
* For XInput controllers this returns the XInput user index.
|
||||||
|
*/
|
||||||
|
extern DECLSPEC int SDLCALL SDL_JoystickGetPlayerIndex(SDL_Joystick * joystick);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the GUID for this opened joystick
|
* Return the GUID for this opened joystick
|
||||||
*/
|
*/
|
||||||
@@ -361,6 +374,19 @@ extern DECLSPEC int SDLCALL SDL_JoystickGetBall(SDL_Joystick * joystick,
|
|||||||
extern DECLSPEC Uint8 SDLCALL SDL_JoystickGetButton(SDL_Joystick * joystick,
|
extern DECLSPEC Uint8 SDLCALL SDL_JoystickGetButton(SDL_Joystick * joystick,
|
||||||
int button);
|
int 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 joystick The joystick 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_JoystickRumble(SDL_Joystick * joystick, Uint16 low_frequency_rumble, Uint16 high_frequency_rumble, Uint32 duration_ms);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Close a joystick previously opened with SDL_JoystickOpen().
|
* Close a joystick previously opened with SDL_JoystickOpen().
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
#define SDL_REVISION "hg-11914:f1084c419f33"
|
#define SDL_REVISION "hg-12373:8feb5da6f2fb"
|
||||||
#define SDL_REVISION_NUMBER 11914
|
#define SDL_REVISION_NUMBER 12373
|
||||||
|
|||||||
@@ -0,0 +1,251 @@
|
|||||||
|
/*
|
||||||
|
Simple DirectMedia Layer
|
||||||
|
Copyright (C) 1997-2018 Sam Lantinga <slouken@libsdl.org>
|
||||||
|
|
||||||
|
This software is provided 'as-is', without any express or implied
|
||||||
|
warranty. In no event will the authors be held liable for any damages
|
||||||
|
arising from the use of this software.
|
||||||
|
|
||||||
|
Permission is granted to anyone to use this software for any purpose,
|
||||||
|
including commercial applications, and to alter it and redistribute it
|
||||||
|
freely, subject to the following restrictions:
|
||||||
|
|
||||||
|
1. The origin of this software must not be misrepresented; you must not
|
||||||
|
claim that you wrote the original software. If you use this software
|
||||||
|
in a product, an acknowledgment in the product documentation would be
|
||||||
|
appreciated but is not required.
|
||||||
|
2. Altered source versions must be plainly marked as such, and must not be
|
||||||
|
misrepresented as being the original software.
|
||||||
|
3. This notice may not be removed or altered from any source distribution.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* \file SDL_sensor.h
|
||||||
|
*
|
||||||
|
* Include file for SDL sensor event handling
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef _SDL_sensor_h
|
||||||
|
#define _SDL_sensor_h
|
||||||
|
|
||||||
|
#include "SDL_stdinc.h"
|
||||||
|
#include "SDL_error.h"
|
||||||
|
|
||||||
|
#include "begin_code.h"
|
||||||
|
/* Set up for C function definitions, even when using C++ */
|
||||||
|
#ifdef __cplusplus
|
||||||
|
/* *INDENT-OFF* */
|
||||||
|
extern "C" {
|
||||||
|
/* *INDENT-ON* */
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/**
|
||||||
|
* \brief SDL_sensor.h
|
||||||
|
*
|
||||||
|
* In order to use these functions, SDL_Init() must have been called
|
||||||
|
* with the ::SDL_INIT_SENSOR flag. This causes SDL to scan the system
|
||||||
|
* for sensors, and load appropriate drivers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
struct _SDL_Sensor;
|
||||||
|
typedef struct _SDL_Sensor SDL_Sensor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is a unique ID for a sensor for the time it is connected to the system,
|
||||||
|
* and is never reused for the lifetime of the application.
|
||||||
|
*
|
||||||
|
* The ID value starts at 0 and increments from there. The value -1 is an invalid ID.
|
||||||
|
*/
|
||||||
|
typedef Sint32 SDL_SensorID;
|
||||||
|
|
||||||
|
/* The different sensors defined by SDL
|
||||||
|
*
|
||||||
|
* Additional sensors may be available, using platform dependent semantics.
|
||||||
|
*
|
||||||
|
* Hare are the additional Android sensors:
|
||||||
|
* https://developer.android.com/reference/android/hardware/SensorEvent.html#values
|
||||||
|
*/
|
||||||
|
typedef enum
|
||||||
|
{
|
||||||
|
SDL_SENSOR_INVALID = -1, /**< Returned for an invalid sensor */
|
||||||
|
SDL_SENSOR_UNKNOWN, /**< Unknown sensor type */
|
||||||
|
SDL_SENSOR_ACCEL, /**< Accelerometer */
|
||||||
|
SDL_SENSOR_GYRO /**< Gyroscope */
|
||||||
|
} SDL_SensorType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Accelerometer sensor
|
||||||
|
*
|
||||||
|
* The accelerometer returns the current acceleration in SI meters per
|
||||||
|
* second squared. This includes gravity, so a device at rest will have
|
||||||
|
* an acceleration of SDL_STANDARD_GRAVITY straight down.
|
||||||
|
*
|
||||||
|
* values[0]: Acceleration on the x axis
|
||||||
|
* values[1]: Acceleration on the y axis
|
||||||
|
* values[2]: Acceleration on the z axis
|
||||||
|
*
|
||||||
|
* For phones held in portrait mode, the axes are defined as follows:
|
||||||
|
* -X ... +X : left ... right
|
||||||
|
* -Y ... +Y : bottom ... top
|
||||||
|
* -Z ... +Z : farther ... closer
|
||||||
|
*
|
||||||
|
* The axis data is not changed when the phone is rotated.
|
||||||
|
*
|
||||||
|
* \sa SDL_GetDisplayOrientation()
|
||||||
|
*/
|
||||||
|
#define SDL_STANDARD_GRAVITY 9.80665f
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gyroscope sensor
|
||||||
|
*
|
||||||
|
* The gyroscope returns the current rate of rotation in radians per second.
|
||||||
|
* The rotation is positive in the counter-clockwise direction. That is,
|
||||||
|
* an observer looking from a positive location on one of the axes would
|
||||||
|
* see positive rotation on that axis when it appeared to be rotating
|
||||||
|
* counter-clockwise.
|
||||||
|
*
|
||||||
|
* values[0]: Angular speed around the x axis
|
||||||
|
* values[1]: Angular speed around the y axis
|
||||||
|
* values[2]: Angular speed around the z axis
|
||||||
|
*
|
||||||
|
* For phones held in portrait mode, the axes are defined as follows:
|
||||||
|
* -X ... +X : left ... right
|
||||||
|
* -Y ... +Y : bottom ... top
|
||||||
|
* -Z ... +Z : farther ... closer
|
||||||
|
*
|
||||||
|
* The axis data is not changed when the phone is rotated.
|
||||||
|
*
|
||||||
|
* \sa SDL_GetDisplayOrientation()
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Function prototypes */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* \brief Count the number of sensors attached to the system right now
|
||||||
|
*/
|
||||||
|
extern DECLSPEC int SDLCALL SDL_NumSensors(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* \brief Get the implementation dependent name of a sensor.
|
||||||
|
*
|
||||||
|
* This can be called before any sensors are opened.
|
||||||
|
*
|
||||||
|
* \return The sensor name, or NULL if device_index is out of range.
|
||||||
|
*/
|
||||||
|
extern DECLSPEC const char *SDLCALL SDL_SensorGetDeviceName(int device_index);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* \brief Get the type of a sensor.
|
||||||
|
*
|
||||||
|
* This can be called before any sensors are opened.
|
||||||
|
*
|
||||||
|
* \return The sensor type, or SDL_SENSOR_INVALID if device_index is out of range.
|
||||||
|
*/
|
||||||
|
extern DECLSPEC SDL_SensorType SDLCALL SDL_SensorGetDeviceType(int device_index);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* \brief Get the platform dependent type of a sensor.
|
||||||
|
*
|
||||||
|
* This can be called before any sensors are opened.
|
||||||
|
*
|
||||||
|
* \return The sensor platform dependent type, or -1 if device_index is out of range.
|
||||||
|
*/
|
||||||
|
extern DECLSPEC int SDLCALL SDL_SensorGetDeviceNonPortableType(int device_index);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* \brief Get the instance ID of a sensor.
|
||||||
|
*
|
||||||
|
* This can be called before any sensors are opened.
|
||||||
|
*
|
||||||
|
* \return The sensor instance ID, or -1 if device_index is out of range.
|
||||||
|
*/
|
||||||
|
extern DECLSPEC SDL_SensorID SDLCALL SDL_SensorGetDeviceInstanceID(int device_index);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* \brief Open a sensor for use.
|
||||||
|
*
|
||||||
|
* The index passed as an argument refers to the N'th sensor on the system.
|
||||||
|
*
|
||||||
|
* \return A sensor identifier, or NULL if an error occurred.
|
||||||
|
*/
|
||||||
|
extern DECLSPEC SDL_Sensor *SDLCALL SDL_SensorOpen(int device_index);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the SDL_Sensor associated with an instance id.
|
||||||
|
*/
|
||||||
|
extern DECLSPEC SDL_Sensor *SDLCALL SDL_SensorFromInstanceID(SDL_SensorID instance_id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* \brief Get the implementation dependent name of a sensor.
|
||||||
|
*
|
||||||
|
* \return The sensor name, or NULL if the sensor is NULL.
|
||||||
|
*/
|
||||||
|
extern DECLSPEC const char *SDLCALL SDL_SensorGetName(SDL_Sensor *sensor);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* \brief Get the type of a sensor.
|
||||||
|
*
|
||||||
|
* This can be called before any sensors are opened.
|
||||||
|
*
|
||||||
|
* \return The sensor type, or SDL_SENSOR_INVALID if the sensor is NULL.
|
||||||
|
*/
|
||||||
|
extern DECLSPEC SDL_SensorType SDLCALL SDL_SensorGetType(SDL_Sensor *sensor);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* \brief Get the platform dependent type of a sensor.
|
||||||
|
*
|
||||||
|
* This can be called before any sensors are opened.
|
||||||
|
*
|
||||||
|
* \return The sensor platform dependent type, or -1 if the sensor is NULL.
|
||||||
|
*/
|
||||||
|
extern DECLSPEC int SDLCALL SDL_SensorGetNonPortableType(SDL_Sensor *sensor);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* \brief Get the instance ID of a sensor.
|
||||||
|
*
|
||||||
|
* This can be called before any sensors are opened.
|
||||||
|
*
|
||||||
|
* \return The sensor instance ID, or -1 if the sensor is NULL.
|
||||||
|
*/
|
||||||
|
extern DECLSPEC SDL_SensorID SDLCALL SDL_SensorGetInstanceID(SDL_Sensor *sensor);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current state of an opened sensor.
|
||||||
|
*
|
||||||
|
* The number of values and interpretation of the data is sensor dependent.
|
||||||
|
*
|
||||||
|
* \param sensor The sensor to query
|
||||||
|
* \param data A pointer filled with the current sensor state
|
||||||
|
* \param num_values The number of values to write to data
|
||||||
|
*
|
||||||
|
* \return 0 or -1 if an error occurred.
|
||||||
|
*/
|
||||||
|
extern DECLSPEC int SDLCALL SDL_SensorGetData(SDL_Sensor * sensor, float *data, int num_values);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close a sensor previously opened with SDL_SensorOpen()
|
||||||
|
*/
|
||||||
|
extern DECLSPEC void SDLCALL SDL_SensorClose(SDL_Sensor * sensor);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the current state of the open sensors.
|
||||||
|
*
|
||||||
|
* This is called automatically by the event loop if sensor events are enabled.
|
||||||
|
*
|
||||||
|
* This needs to be called from the thread that initialized the sensor subsystem.
|
||||||
|
*/
|
||||||
|
extern DECLSPEC void SDLCALL SDL_SensorUpdate(void);
|
||||||
|
|
||||||
|
|
||||||
|
/* Ends C function definitions when using C++ */
|
||||||
|
#ifdef __cplusplus
|
||||||
|
/* *INDENT-OFF* */
|
||||||
|
}
|
||||||
|
/* *INDENT-ON* */
|
||||||
|
#endif
|
||||||
|
#include "close_code.h"
|
||||||
|
|
||||||
|
#endif /* _SDL_sensor_h */
|
||||||
|
|
||||||
|
/* vi: set ts=4 sw=4 expandtab: */
|
||||||
@@ -86,6 +86,28 @@
|
|||||||
#ifdef HAVE_FLOAT_H
|
#ifdef HAVE_FLOAT_H
|
||||||
# include <float.h>
|
# include <float.h>
|
||||||
#endif
|
#endif
|
||||||
|
#if defined(HAVE_ALLOCA) && !defined(alloca)
|
||||||
|
# if defined(HAVE_ALLOCA_H)
|
||||||
|
# include <alloca.h>
|
||||||
|
# elif defined(__GNUC__)
|
||||||
|
# define alloca __builtin_alloca
|
||||||
|
# elif defined(_MSC_VER)
|
||||||
|
# include <malloc.h>
|
||||||
|
# define alloca _alloca
|
||||||
|
# elif defined(__WATCOMC__)
|
||||||
|
# include <malloc.h>
|
||||||
|
# elif defined(__BORLANDC__)
|
||||||
|
# include <malloc.h>
|
||||||
|
# elif defined(__DMC__)
|
||||||
|
# include <stdlib.h>
|
||||||
|
# elif defined(__AIX__)
|
||||||
|
#pragma alloca
|
||||||
|
# elif defined(__MRC__)
|
||||||
|
void *alloca(unsigned);
|
||||||
|
# else
|
||||||
|
char *alloca();
|
||||||
|
# endif
|
||||||
|
#endif
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The number of elements in an array.
|
* The number of elements in an array.
|
||||||
@@ -328,28 +350,6 @@ SDL_COMPILE_TIME_ASSERT(enum, sizeof(SDL_DUMMY_ENUM) == sizeof(int));
|
|||||||
extern "C" {
|
extern "C" {
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if defined(HAVE_ALLOCA) && !defined(alloca)
|
|
||||||
# if defined(HAVE_ALLOCA_H)
|
|
||||||
# include <alloca.h>
|
|
||||||
# elif defined(__GNUC__)
|
|
||||||
# define alloca __builtin_alloca
|
|
||||||
# elif defined(_MSC_VER)
|
|
||||||
# include <malloc.h>
|
|
||||||
# define alloca _alloca
|
|
||||||
# elif defined(__WATCOMC__)
|
|
||||||
# include <malloc.h>
|
|
||||||
# elif defined(__BORLANDC__)
|
|
||||||
# include <malloc.h>
|
|
||||||
# elif defined(__DMC__)
|
|
||||||
# include <stdlib.h>
|
|
||||||
# elif defined(__AIX__)
|
|
||||||
#pragma alloca
|
|
||||||
# elif defined(__MRC__)
|
|
||||||
void *alloca(unsigned);
|
|
||||||
# else
|
|
||||||
char *alloca();
|
|
||||||
# endif
|
|
||||||
#endif
|
|
||||||
#ifdef HAVE_ALLOCA
|
#ifdef HAVE_ALLOCA
|
||||||
#define SDL_stack_alloc(type, count) (type*)alloca(sizeof(type)*(count))
|
#define SDL_stack_alloc(type, count) (type*)alloca(sizeof(type)*(count))
|
||||||
#define SDL_stack_free(data)
|
#define SDL_stack_free(data)
|
||||||
@@ -445,12 +445,12 @@ SDL_FORCE_INLINE void SDL_memset4(void *dst, Uint32 val, size_t dwords)
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
extern DECLSPEC void *SDLCALL SDL_memcpy(SDL_OUT_BYTECAP(len) void *dst, SDL_IN_BYTECAP(len) const void *src, size_t len);
|
extern DECLSPEC void *SDLCALL SDL_memcpy(SDL_OUT_BYTECAP(len) void *dst, SDL_IN_BYTECAP(len) const void *src, size_t len);
|
||||||
|
|
||||||
extern DECLSPEC void *SDLCALL SDL_memmove(SDL_OUT_BYTECAP(len) void *dst, SDL_IN_BYTECAP(len) const void *src, size_t len);
|
extern DECLSPEC void *SDLCALL SDL_memmove(SDL_OUT_BYTECAP(len) void *dst, SDL_IN_BYTECAP(len) const void *src, size_t len);
|
||||||
extern DECLSPEC int SDLCALL SDL_memcmp(const void *s1, const void *s2, size_t len);
|
extern DECLSPEC int SDLCALL SDL_memcmp(const void *s1, const void *s2, size_t len);
|
||||||
|
|
||||||
|
extern DECLSPEC wchar_t *SDLCALL SDL_wcsdup(const wchar_t *wstr);
|
||||||
extern DECLSPEC size_t SDLCALL SDL_wcslen(const wchar_t *wstr);
|
extern DECLSPEC size_t SDLCALL SDL_wcslen(const wchar_t *wstr);
|
||||||
extern DECLSPEC size_t SDLCALL SDL_wcslcpy(SDL_OUT_Z_CAP(maxlen) wchar_t *dst, const wchar_t *src, size_t maxlen);
|
extern DECLSPEC size_t SDLCALL SDL_wcslcpy(SDL_OUT_Z_CAP(maxlen) wchar_t *dst, const wchar_t *src, size_t maxlen);
|
||||||
extern DECLSPEC size_t SDLCALL SDL_wcslcat(SDL_INOUT_Z_CAP(maxlen) wchar_t *dst, const wchar_t *src, size_t maxlen);
|
extern DECLSPEC size_t SDLCALL SDL_wcslcat(SDL_INOUT_Z_CAP(maxlen) wchar_t *dst, const wchar_t *src, size_t maxlen);
|
||||||
@@ -514,6 +514,8 @@ extern DECLSPEC double SDLCALL SDL_copysign(double x, double y);
|
|||||||
extern DECLSPEC float SDLCALL SDL_copysignf(float x, float y);
|
extern DECLSPEC float SDLCALL SDL_copysignf(float x, float y);
|
||||||
extern DECLSPEC double SDLCALL SDL_cos(double x);
|
extern DECLSPEC double SDLCALL SDL_cos(double x);
|
||||||
extern DECLSPEC float SDLCALL SDL_cosf(float x);
|
extern DECLSPEC float SDLCALL SDL_cosf(float x);
|
||||||
|
extern DECLSPEC double SDLCALL SDL_exp(double x);
|
||||||
|
extern DECLSPEC float SDLCALL SDL_expf(float x);
|
||||||
extern DECLSPEC double SDLCALL SDL_fabs(double x);
|
extern DECLSPEC double SDLCALL SDL_fabs(double x);
|
||||||
extern DECLSPEC float SDLCALL SDL_fabsf(float x);
|
extern DECLSPEC float SDLCALL SDL_fabsf(float x);
|
||||||
extern DECLSPEC double SDLCALL SDL_floor(double x);
|
extern DECLSPEC double SDLCALL SDL_floor(double x);
|
||||||
|
|||||||
@@ -248,6 +248,13 @@ extern DECLSPEC int SDLCALL SDL_SetSurfaceRLE(SDL_Surface * surface,
|
|||||||
extern DECLSPEC int SDLCALL SDL_SetColorKey(SDL_Surface * surface,
|
extern DECLSPEC int SDLCALL SDL_SetColorKey(SDL_Surface * surface,
|
||||||
int flag, Uint32 key);
|
int flag, Uint32 key);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* \brief Returns whether the surface has a color key
|
||||||
|
*
|
||||||
|
* \return SDL_TRUE if the surface has a color key, or SDL_FALSE if the surface is NULL or has no color key
|
||||||
|
*/
|
||||||
|
extern DECLSPEC SDL_bool SDLCALL SDL_HasColorKey(SDL_Surface * surface);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* \brief Gets the color key (transparent pixel) in a blittable surface.
|
* \brief Gets the color key (transparent pixel) in a blittable surface.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -76,6 +76,18 @@ extern DECLSPEC SDL_bool SDLCALL SDL_DXGIGetOutputInfo( int displayIndex, int *a
|
|||||||
#endif /* __WIN32__ */
|
#endif /* __WIN32__ */
|
||||||
|
|
||||||
|
|
||||||
|
/* Platform specific functions for Linux */
|
||||||
|
#ifdef __LINUX__
|
||||||
|
|
||||||
|
/**
|
||||||
|
\brief Sets the UNIX nice value for a thread, using setpriority() if possible, and RealtimeKit if available.
|
||||||
|
|
||||||
|
\return 0 on success, or -1 on error.
|
||||||
|
*/
|
||||||
|
extern DECLSPEC int SDLCALL SDL_LinuxSetThreadPriority(Sint64 threadID, int priority);
|
||||||
|
|
||||||
|
#endif /* __LINUX__ */
|
||||||
|
|
||||||
/* Platform specific functions for iOS */
|
/* Platform specific functions for iOS */
|
||||||
#if defined(__IPHONEOS__) && __IPHONEOS__
|
#if defined(__IPHONEOS__) && __IPHONEOS__
|
||||||
|
|
||||||
@@ -113,6 +125,21 @@ extern DECLSPEC void * SDLCALL SDL_AndroidGetActivity(void);
|
|||||||
*/
|
*/
|
||||||
extern DECLSPEC SDL_bool SDLCALL SDL_IsAndroidTV(void);
|
extern DECLSPEC SDL_bool SDLCALL SDL_IsAndroidTV(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
\brief Return true if the application is running on a Chromebook
|
||||||
|
*/
|
||||||
|
extern DECLSPEC SDL_bool SDLCALL SDL_IsChromebook(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
\brief Return true is the application is running on a Samsung DeX docking station
|
||||||
|
*/
|
||||||
|
extern DECLSPEC SDL_bool SDLCALL SDL_IsDeXMode(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
\brief Trigger the Android system back button behavior.
|
||||||
|
*/
|
||||||
|
extern DECLSPEC void SDLCALL SDL_AndroidBackButton(void);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
See the official Android developer guide for more information:
|
See the official Android developer guide for more information:
|
||||||
http://developer.android.com/guide/topics/data/data-storage.html
|
http://developer.android.com/guide/topics/data/data-storage.html
|
||||||
@@ -236,6 +263,11 @@ extern DECLSPEC SDL_WinRT_DeviceFamily SDLCALL SDL_WinRTGetDeviceFamily();
|
|||||||
|
|
||||||
#endif /* __WINRT__ */
|
#endif /* __WINRT__ */
|
||||||
|
|
||||||
|
/**
|
||||||
|
\brief Return true if the current device is a tablet.
|
||||||
|
*/
|
||||||
|
extern DECLSPEC SDL_bool SDLCALL SDL_IsTablet(void);
|
||||||
|
|
||||||
/* Ends C function definitions when using C++ */
|
/* Ends C function definitions when using C++ */
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,12 +33,6 @@
|
|||||||
#include "SDL_video.h"
|
#include "SDL_video.h"
|
||||||
#include "SDL_version.h"
|
#include "SDL_version.h"
|
||||||
|
|
||||||
#include "begin_code.h"
|
|
||||||
/* Set up for C function definitions, even when using C++ */
|
|
||||||
#ifdef __cplusplus
|
|
||||||
extern "C" {
|
|
||||||
#endif
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* \file SDL_syswm.h
|
* \file SDL_syswm.h
|
||||||
*
|
*
|
||||||
@@ -110,6 +104,12 @@ typedef void *EGLSurface;
|
|||||||
#include "SDL_egl.h"
|
#include "SDL_egl.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#include "begin_code.h"
|
||||||
|
/* Set up for C function definitions, even when using C++ */
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* These are the various supported windowing subsystems
|
* These are the various supported windowing subsystems
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -54,12 +54,13 @@ typedef unsigned int SDL_TLSID;
|
|||||||
/**
|
/**
|
||||||
* The SDL thread priority.
|
* The SDL thread priority.
|
||||||
*
|
*
|
||||||
* \note On many systems you require special privileges to set high priority.
|
* \note On many systems you require special privileges to set high or time critical priority.
|
||||||
*/
|
*/
|
||||||
typedef enum {
|
typedef enum {
|
||||||
SDL_THREAD_PRIORITY_LOW,
|
SDL_THREAD_PRIORITY_LOW,
|
||||||
SDL_THREAD_PRIORITY_NORMAL,
|
SDL_THREAD_PRIORITY_NORMAL,
|
||||||
SDL_THREAD_PRIORITY_HIGH
|
SDL_THREAD_PRIORITY_HIGH,
|
||||||
|
SDL_THREAD_PRIORITY_TIME_CRITICAL
|
||||||
} SDL_ThreadPriority;
|
} SDL_ThreadPriority;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -105,14 +106,24 @@ SDL_CreateThread(SDL_ThreadFunction fn, const char *name, void *data,
|
|||||||
pfnSDL_CurrentBeginThread pfnBeginThread,
|
pfnSDL_CurrentBeginThread pfnBeginThread,
|
||||||
pfnSDL_CurrentEndThread pfnEndThread);
|
pfnSDL_CurrentEndThread pfnEndThread);
|
||||||
|
|
||||||
|
extern DECLSPEC SDL_Thread *SDLCALL
|
||||||
|
SDL_CreateThreadWithStackSize(int (SDLCALL * fn) (void *),
|
||||||
|
const char *name, const size_t stacksize, void *data,
|
||||||
|
pfnSDL_CurrentBeginThread pfnBeginThread,
|
||||||
|
pfnSDL_CurrentEndThread pfnEndThread);
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a thread.
|
* Create a thread.
|
||||||
*/
|
*/
|
||||||
#if defined(SDL_CreateThread) && SDL_DYNAMIC_API
|
#if defined(SDL_CreateThread) && SDL_DYNAMIC_API
|
||||||
#undef SDL_CreateThread
|
#undef SDL_CreateThread
|
||||||
#define SDL_CreateThread(fn, name, data) SDL_CreateThread_REAL(fn, name, data, (pfnSDL_CurrentBeginThread)_beginthreadex, (pfnSDL_CurrentEndThread)_endthreadex)
|
#define SDL_CreateThread(fn, name, data) SDL_CreateThread_REAL(fn, name, data, (pfnSDL_CurrentBeginThread)_beginthreadex, (pfnSDL_CurrentEndThread)_endthreadex)
|
||||||
|
#undef SDL_CreateThreadWithStackSize
|
||||||
|
#define SDL_CreateThreadWithStackSize(fn, name, stacksize, data) SDL_CreateThreadWithStackSize_REAL(fn, name, stacksize, data, (pfnSDL_CurrentBeginThread)_beginthreadex, (pfnSDL_CurrentEndThread)_endthreadex)
|
||||||
#else
|
#else
|
||||||
#define SDL_CreateThread(fn, name, data) SDL_CreateThread(fn, name, data, (pfnSDL_CurrentBeginThread)_beginthreadex, (pfnSDL_CurrentEndThread)_endthreadex)
|
#define SDL_CreateThread(fn, name, data) SDL_CreateThread(fn, name, data, (pfnSDL_CurrentBeginThread)_beginthreadex, (pfnSDL_CurrentEndThread)_endthreadex)
|
||||||
|
#define SDL_CreateThreadWithStackSize(fn, name, stacksize, data) SDL_CreateThreadWithStackSize(fn, name, data, (pfnSDL_CurrentBeginThread)_beginthreadex, (pfnSDL_CurrentEndThread)_endthreadex)
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#elif defined(__OS2__)
|
#elif defined(__OS2__)
|
||||||
@@ -132,15 +143,31 @@ extern DECLSPEC SDL_Thread *SDLCALL
|
|||||||
SDL_CreateThread(SDL_ThreadFunction fn, const char *name, void *data,
|
SDL_CreateThread(SDL_ThreadFunction fn, const char *name, void *data,
|
||||||
pfnSDL_CurrentBeginThread pfnBeginThread,
|
pfnSDL_CurrentBeginThread pfnBeginThread,
|
||||||
pfnSDL_CurrentEndThread pfnEndThread);
|
pfnSDL_CurrentEndThread pfnEndThread);
|
||||||
|
extern DECLSPEC SDL_Thread *SDLCALL
|
||||||
|
SDL_CreateThreadWithStackSize(SDL_ThreadFunction fn, const char *name, const size_t stacksize, void *data,
|
||||||
|
pfnSDL_CurrentBeginThread pfnBeginThread,
|
||||||
|
pfnSDL_CurrentEndThread pfnEndThread);
|
||||||
#if defined(SDL_CreateThread) && SDL_DYNAMIC_API
|
#if defined(SDL_CreateThread) && SDL_DYNAMIC_API
|
||||||
#undef SDL_CreateThread
|
#undef SDL_CreateThread
|
||||||
#define SDL_CreateThread(fn, name, data) SDL_CreateThread_REAL(fn, name, data, (pfnSDL_CurrentBeginThread)_beginthread, (pfnSDL_CurrentEndThread)_endthread)
|
#define SDL_CreateThread(fn, name, data) SDL_CreateThread_REAL(fn, name, data, (pfnSDL_CurrentBeginThread)_beginthread, (pfnSDL_CurrentEndThread)_endthread)
|
||||||
|
#undef SDL_CreateThreadWithStackSize
|
||||||
|
#define SDL_CreateThreadWithStackSize(fn, name, stacksize, data) SDL_CreateThreadWithStackSize_REAL(fn, name, data, (pfnSDL_CurrentBeginThread)_beginthread, (pfnSDL_CurrentEndThread)_endthread)
|
||||||
#else
|
#else
|
||||||
#define SDL_CreateThread(fn, name, data) SDL_CreateThread(fn, name, data, (pfnSDL_CurrentBeginThread)_beginthread, (pfnSDL_CurrentEndThread)_endthread)
|
#define SDL_CreateThread(fn, name, data) SDL_CreateThread(fn, name, data, (pfnSDL_CurrentBeginThread)_beginthread, (pfnSDL_CurrentEndThread)_endthread)
|
||||||
|
#define SDL_CreateThreadWithStackSize(fn, name, stacksize, data) SDL_CreateThreadWithStackSize(fn, name, stacksize, data, (pfnSDL_CurrentBeginThread)_beginthread, (pfnSDL_CurrentEndThread)_endthread)
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#else
|
#else
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a thread with a default stack size.
|
||||||
|
*
|
||||||
|
* This is equivalent to calling:
|
||||||
|
* SDL_CreateThreadWithStackSize(fn, name, 0, data);
|
||||||
|
*/
|
||||||
|
extern DECLSPEC SDL_Thread *SDLCALL
|
||||||
|
SDL_CreateThread(SDL_ThreadFunction fn, const char *name, void *data);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a thread.
|
* Create a thread.
|
||||||
*
|
*
|
||||||
@@ -158,9 +185,17 @@ SDL_CreateThread(SDL_ThreadFunction fn, const char *name, void *data,
|
|||||||
* If a system imposes requirements, SDL will try to munge the string for
|
* If a system imposes requirements, SDL will try to munge the string for
|
||||||
* it (truncate, etc), but the original string contents will be available
|
* it (truncate, etc), but the original string contents will be available
|
||||||
* from SDL_GetThreadName().
|
* from SDL_GetThreadName().
|
||||||
|
*
|
||||||
|
* The size (in bytes) of the new stack can be specified. Zero means "use
|
||||||
|
* the system default" which might be wildly different between platforms
|
||||||
|
* (x86 Linux generally defaults to eight megabytes, an embedded device
|
||||||
|
* might be a few kilobytes instead).
|
||||||
|
*
|
||||||
|
* In SDL 2.1, stacksize will be folded into the original SDL_CreateThread
|
||||||
|
* function.
|
||||||
*/
|
*/
|
||||||
extern DECLSPEC SDL_Thread *SDLCALL
|
extern DECLSPEC SDL_Thread *SDLCALL
|
||||||
SDL_CreateThread(SDL_ThreadFunction fn, const char *name, void *data);
|
SDL_CreateThreadWithStackSize(SDL_ThreadFunction fn, const char *name, const size_t stacksize, void *data);
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ typedef struct SDL_version
|
|||||||
*/
|
*/
|
||||||
#define SDL_MAJOR_VERSION 2
|
#define SDL_MAJOR_VERSION 2
|
||||||
#define SDL_MINOR_VERSION 0
|
#define SDL_MINOR_VERSION 0
|
||||||
#define SDL_PATCHLEVEL 8
|
#define SDL_PATCHLEVEL 9
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* \brief Macro to determine SDL version program was compiled against.
|
* \brief Macro to determine SDL version program was compiled against.
|
||||||
|
|||||||
@@ -169,6 +169,24 @@ typedef enum
|
|||||||
SDL_WINDOWEVENT_HIT_TEST /**< Window had a hit test that wasn't SDL_HITTEST_NORMAL. */
|
SDL_WINDOWEVENT_HIT_TEST /**< Window had a hit test that wasn't SDL_HITTEST_NORMAL. */
|
||||||
} SDL_WindowEventID;
|
} SDL_WindowEventID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* \brief Event subtype for display events
|
||||||
|
*/
|
||||||
|
typedef enum
|
||||||
|
{
|
||||||
|
SDL_DISPLAYEVENT_NONE, /**< Never used */
|
||||||
|
SDL_DISPLAYEVENT_ORIENTATION /**< Display orientation has changed to data1 */
|
||||||
|
} SDL_DisplayEventID;
|
||||||
|
|
||||||
|
typedef enum
|
||||||
|
{
|
||||||
|
SDL_ORIENTATION_UNKNOWN, /**< The display orientation can't be determined */
|
||||||
|
SDL_ORIENTATION_LANDSCAPE, /**< The display is in landscape mode, with the right side up, relative to portrait mode */
|
||||||
|
SDL_ORIENTATION_LANDSCAPE_FLIPPED, /**< The display is in landscape mode, with the left side up, relative to portrait mode */
|
||||||
|
SDL_ORIENTATION_PORTRAIT, /**< The display is in portrait mode */
|
||||||
|
SDL_ORIENTATION_PORTRAIT_FLIPPED /**< The display is in portrait mode, upside down */
|
||||||
|
} SDL_DisplayOrientation;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* \brief An opaque handle to an OpenGL context.
|
* \brief An opaque handle to an OpenGL context.
|
||||||
*/
|
*/
|
||||||
@@ -316,18 +334,6 @@ extern DECLSPEC const char * SDLCALL SDL_GetDisplayName(int displayIndex);
|
|||||||
*/
|
*/
|
||||||
extern DECLSPEC int SDLCALL SDL_GetDisplayBounds(int displayIndex, SDL_Rect * rect);
|
extern DECLSPEC int SDLCALL SDL_GetDisplayBounds(int displayIndex, SDL_Rect * rect);
|
||||||
|
|
||||||
/**
|
|
||||||
* \brief Get the dots/pixels-per-inch for a display
|
|
||||||
*
|
|
||||||
* \note Diagonal, horizontal and vertical DPI can all be optionally
|
|
||||||
* returned if the parameter is non-NULL.
|
|
||||||
*
|
|
||||||
* \return 0 on success, or -1 if no DPI information is available or the index is out of range.
|
|
||||||
*
|
|
||||||
* \sa SDL_GetNumVideoDisplays()
|
|
||||||
*/
|
|
||||||
extern DECLSPEC int SDLCALL SDL_GetDisplayDPI(int displayIndex, float * ddpi, float * hdpi, float * vdpi);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* \brief Get the usable desktop area represented by a display, with the
|
* \brief Get the usable desktop area represented by a display, with the
|
||||||
* primary display located at 0,0
|
* primary display located at 0,0
|
||||||
@@ -347,6 +353,27 @@ extern DECLSPEC int SDLCALL SDL_GetDisplayDPI(int displayIndex, float * ddpi, fl
|
|||||||
*/
|
*/
|
||||||
extern DECLSPEC int SDLCALL SDL_GetDisplayUsableBounds(int displayIndex, SDL_Rect * rect);
|
extern DECLSPEC int SDLCALL SDL_GetDisplayUsableBounds(int displayIndex, SDL_Rect * rect);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* \brief Get the dots/pixels-per-inch for a display
|
||||||
|
*
|
||||||
|
* \note Diagonal, horizontal and vertical DPI can all be optionally
|
||||||
|
* returned if the parameter is non-NULL.
|
||||||
|
*
|
||||||
|
* \return 0 on success, or -1 if no DPI information is available or the index is out of range.
|
||||||
|
*
|
||||||
|
* \sa SDL_GetNumVideoDisplays()
|
||||||
|
*/
|
||||||
|
extern DECLSPEC int SDLCALL SDL_GetDisplayDPI(int displayIndex, float * ddpi, float * hdpi, float * vdpi);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* \brief Get the orientation of a display
|
||||||
|
*
|
||||||
|
* \return The orientation of the display, or SDL_ORIENTATION_UNKNOWN if it isn't available.
|
||||||
|
*
|
||||||
|
* \sa SDL_GetNumVideoDisplays()
|
||||||
|
*/
|
||||||
|
extern DECLSPEC SDL_DisplayOrientation SDLCALL SDL_GetDisplayOrientation(int displayIndex);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* \brief Returns the number of available display modes.
|
* \brief Returns the number of available display modes.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -135,11 +135,11 @@ extern DECLSPEC void SDLCALL SDL_Vulkan_UnloadLibrary(void);
|
|||||||
* \brief Get the names of the Vulkan instance extensions needed to create
|
* \brief Get the names of the Vulkan instance extensions needed to create
|
||||||
* a surface with \c SDL_Vulkan_CreateSurface().
|
* a surface with \c SDL_Vulkan_CreateSurface().
|
||||||
*
|
*
|
||||||
* \param [in] window Window for which the required Vulkan instance
|
* \param [in] \c NULL or window Window for which the required Vulkan instance
|
||||||
* extensions should be retrieved
|
* extensions should be retrieved
|
||||||
* \param [in,out] count pointer to an \c unsigned related to the number of
|
* \param [in,out] pCount pointer to an \c unsigned related to the number of
|
||||||
* required Vulkan instance extensions
|
* required Vulkan instance extensions
|
||||||
* \param [out] names \c NULL or a pointer to an array to be filled with the
|
* \param [out] pNames \c NULL or a pointer to an array to be filled with the
|
||||||
* required Vulkan instance extensions
|
* required Vulkan instance extensions
|
||||||
*
|
*
|
||||||
* \return \c SDL_TRUE on success, \c SDL_FALSE on error.
|
* \return \c SDL_TRUE on success, \c SDL_FALSE on error.
|
||||||
@@ -154,18 +154,23 @@ extern DECLSPEC void SDLCALL SDL_Vulkan_UnloadLibrary(void);
|
|||||||
* returned instead of \c SDL_TRUE, to indicate that not all the required
|
* returned instead of \c SDL_TRUE, to indicate that not all the required
|
||||||
* extensions were returned.
|
* extensions were returned.
|
||||||
*
|
*
|
||||||
|
* \note If \c window is not NULL, it will be checked against its creation
|
||||||
|
* flags to ensure that the Vulkan flag is present. This parameter
|
||||||
|
* will be removed in a future major release.
|
||||||
|
*
|
||||||
* \note The returned list of extensions will contain \c VK_KHR_surface
|
* \note The returned list of extensions will contain \c VK_KHR_surface
|
||||||
* and zero or more platform specific extensions
|
* and zero or more platform specific extensions
|
||||||
*
|
*
|
||||||
* \note The extension names queried here must be enabled when calling
|
* \note The extension names queried here must be enabled when calling
|
||||||
* VkCreateInstance, otherwise surface creation will fail.
|
* VkCreateInstance, otherwise surface creation will fail.
|
||||||
*
|
*
|
||||||
* \note \c window should have been created with the \c SDL_WINDOW_VULKAN flag.
|
* \note \c window should have been created with the \c SDL_WINDOW_VULKAN flag
|
||||||
|
* or be \c NULL
|
||||||
*
|
*
|
||||||
* \code
|
* \code
|
||||||
* unsigned int count;
|
* unsigned int count;
|
||||||
* // get count of required extensions
|
* // get count of required extensions
|
||||||
* if(!SDL_Vulkan_GetInstanceExtensions(window, &count, NULL))
|
* if(!SDL_Vulkan_GetInstanceExtensions(NULL, &count, NULL))
|
||||||
* handle_error();
|
* handle_error();
|
||||||
*
|
*
|
||||||
* static const char *const additionalExtensions[] =
|
* static const char *const additionalExtensions[] =
|
||||||
@@ -179,7 +184,7 @@ extern DECLSPEC void SDLCALL SDL_Vulkan_UnloadLibrary(void);
|
|||||||
* handle_error();
|
* handle_error();
|
||||||
*
|
*
|
||||||
* // get names of required extensions
|
* // get names of required extensions
|
||||||
* if(!SDL_Vulkan_GetInstanceExtensions(window, &count, names))
|
* if(!SDL_Vulkan_GetInstanceExtensions(NULL, &count, names))
|
||||||
* handle_error();
|
* handle_error();
|
||||||
*
|
*
|
||||||
* // copy additional extensions after required extensions
|
* // copy additional extensions after required extensions
|
||||||
|
|||||||
@@ -409,6 +409,7 @@ enum AVCodecID {
|
|||||||
AV_CODEC_ID_DXV,
|
AV_CODEC_ID_DXV,
|
||||||
AV_CODEC_ID_SCREENPRESSO,
|
AV_CODEC_ID_SCREENPRESSO,
|
||||||
AV_CODEC_ID_RSCC,
|
AV_CODEC_ID_RSCC,
|
||||||
|
AV_CODEC_ID_AVS2,
|
||||||
|
|
||||||
AV_CODEC_ID_Y41P = 0x8000,
|
AV_CODEC_ID_Y41P = 0x8000,
|
||||||
AV_CODEC_ID_AVRP,
|
AV_CODEC_ID_AVRP,
|
||||||
@@ -446,6 +447,11 @@ enum AVCodecID {
|
|||||||
AV_CODEC_ID_SVG,
|
AV_CODEC_ID_SVG,
|
||||||
AV_CODEC_ID_GDV,
|
AV_CODEC_ID_GDV,
|
||||||
AV_CODEC_ID_FITS,
|
AV_CODEC_ID_FITS,
|
||||||
|
AV_CODEC_ID_IMM4,
|
||||||
|
AV_CODEC_ID_PROSUMER,
|
||||||
|
AV_CODEC_ID_MWSC,
|
||||||
|
AV_CODEC_ID_WCMV,
|
||||||
|
AV_CODEC_ID_RASC,
|
||||||
|
|
||||||
/* various PCM "codecs" */
|
/* various PCM "codecs" */
|
||||||
AV_CODEC_ID_FIRST_AUDIO = 0x10000, ///< A dummy id pointing at the start of audio codecs
|
AV_CODEC_ID_FIRST_AUDIO = 0x10000, ///< A dummy id pointing at the start of audio codecs
|
||||||
@@ -485,6 +491,7 @@ enum AVCodecID {
|
|||||||
AV_CODEC_ID_PCM_S64BE,
|
AV_CODEC_ID_PCM_S64BE,
|
||||||
AV_CODEC_ID_PCM_F16LE,
|
AV_CODEC_ID_PCM_F16LE,
|
||||||
AV_CODEC_ID_PCM_F24LE,
|
AV_CODEC_ID_PCM_F24LE,
|
||||||
|
AV_CODEC_ID_PCM_VIDC,
|
||||||
|
|
||||||
/* various ADPCM codecs */
|
/* various ADPCM codecs */
|
||||||
AV_CODEC_ID_ADPCM_IMA_QT = 0x11000,
|
AV_CODEC_ID_ADPCM_IMA_QT = 0x11000,
|
||||||
@@ -1359,6 +1366,12 @@ enum AVPacketSideDataType {
|
|||||||
*/
|
*/
|
||||||
AV_PKT_DATA_ENCRYPTION_INFO,
|
AV_PKT_DATA_ENCRYPTION_INFO,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Active Format Description data consisting of a single byte as specified
|
||||||
|
* in ETSI TS 101 154 using AVActiveFormatDescription enum.
|
||||||
|
*/
|
||||||
|
AV_PKT_DATA_AFD,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The number of side data types.
|
* The number of side data types.
|
||||||
* This is not part of the public API/ABI in the sense that it may
|
* This is not part of the public API/ABI in the sense that it may
|
||||||
@@ -1614,6 +1627,7 @@ typedef struct AVCodecContext {
|
|||||||
* The allocated memory should be AV_INPUT_BUFFER_PADDING_SIZE bytes larger
|
* The allocated memory should be AV_INPUT_BUFFER_PADDING_SIZE bytes larger
|
||||||
* than extradata_size to avoid problems if it is read with the bitstream reader.
|
* than extradata_size to avoid problems if it is read with the bitstream reader.
|
||||||
* The bytewise contents of extradata must not depend on the architecture or CPU endianness.
|
* The bytewise contents of extradata must not depend on the architecture or CPU endianness.
|
||||||
|
* Must be allocated with the av_malloc() family of functions.
|
||||||
* - encoding: Set/allocated/freed by libavcodec.
|
* - encoding: Set/allocated/freed by libavcodec.
|
||||||
* - decoding: Set/allocated/freed by user.
|
* - decoding: Set/allocated/freed by user.
|
||||||
*/
|
*/
|
||||||
@@ -5768,6 +5782,7 @@ typedef struct AVBitStreamFilter {
|
|||||||
int (*init)(AVBSFContext *ctx);
|
int (*init)(AVBSFContext *ctx);
|
||||||
int (*filter)(AVBSFContext *ctx, AVPacket *pkt);
|
int (*filter)(AVBSFContext *ctx, AVPacket *pkt);
|
||||||
void (*close)(AVBSFContext *ctx);
|
void (*close)(AVBSFContext *ctx);
|
||||||
|
void (*flush)(AVBSFContext *ctx);
|
||||||
} AVBitStreamFilter;
|
} AVBitStreamFilter;
|
||||||
|
|
||||||
#if FF_API_OLD_BSF
|
#if FF_API_OLD_BSF
|
||||||
@@ -5894,6 +5909,11 @@ int av_bsf_send_packet(AVBSFContext *ctx, AVPacket *pkt);
|
|||||||
*/
|
*/
|
||||||
int av_bsf_receive_packet(AVBSFContext *ctx, AVPacket *pkt);
|
int av_bsf_receive_packet(AVBSFContext *ctx, AVPacket *pkt);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset the internal bitstream filter state / flush internal buffers.
|
||||||
|
*/
|
||||||
|
void av_bsf_flush(AVBSFContext *ctx);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Free a bitstream filter context and everything associated with it; write NULL
|
* Free a bitstream filter context and everything associated with it; write NULL
|
||||||
* into the supplied pointer.
|
* into the supplied pointer.
|
||||||
|
|||||||
@@ -85,4 +85,17 @@ typedef struct MediaCodecBuffer AVMediaCodecBuffer;
|
|||||||
*/
|
*/
|
||||||
int av_mediacodec_release_buffer(AVMediaCodecBuffer *buffer, int render);
|
int av_mediacodec_release_buffer(AVMediaCodecBuffer *buffer, int render);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Release a MediaCodec buffer and render it at the given time to the surface
|
||||||
|
* that is associated with the decoder. The timestamp must be within one second
|
||||||
|
* of the current java/lang/System#nanoTime() (which is implemented using
|
||||||
|
* CLOCK_MONOTONIC on Android). See the Android MediaCodec documentation
|
||||||
|
* of android/media/MediaCodec#releaseOutputBuffer(int,long) for more details.
|
||||||
|
*
|
||||||
|
* @param buffer the buffer to render
|
||||||
|
* @param time timestamp in nanoseconds of when to render the buffer
|
||||||
|
* @return 0 on success, < 0 otherwise
|
||||||
|
*/
|
||||||
|
int av_mediacodec_render_buffer_at_time(AVMediaCodecBuffer *buffer, int64_t time);
|
||||||
|
|
||||||
#endif /* AVCODEC_MEDIACODEC_H */
|
#endif /* AVCODEC_MEDIACODEC_H */
|
||||||
|
|||||||
@@ -28,8 +28,8 @@
|
|||||||
#include "libavutil/version.h"
|
#include "libavutil/version.h"
|
||||||
|
|
||||||
#define LIBAVCODEC_VERSION_MAJOR 58
|
#define LIBAVCODEC_VERSION_MAJOR 58
|
||||||
#define LIBAVCODEC_VERSION_MINOR 21
|
#define LIBAVCODEC_VERSION_MINOR 35
|
||||||
#define LIBAVCODEC_VERSION_MICRO 104
|
#define LIBAVCODEC_VERSION_MICRO 100
|
||||||
|
|
||||||
#define LIBAVCODEC_VERSION_INT AV_VERSION_INT(LIBAVCODEC_VERSION_MAJOR, \
|
#define LIBAVCODEC_VERSION_INT AV_VERSION_INT(LIBAVCODEC_VERSION_MAJOR, \
|
||||||
LIBAVCODEC_VERSION_MINOR, \
|
LIBAVCODEC_VERSION_MINOR, \
|
||||||
|
|||||||
@@ -66,7 +66,7 @@
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Assert that floating point opperations can be executed.
|
* Assert that floating point operations can be executed.
|
||||||
*
|
*
|
||||||
* This will av_assert0() that the cpu is not in MMX state on X86
|
* This will av_assert0() that the cpu is not in MMX state on X86
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/* Automatically generated by version.sh, do not manually edit! */
|
/* Automatically generated by version.sh, do not manually edit! */
|
||||||
#ifndef AVUTIL_FFVERSION_H
|
#ifndef AVUTIL_FFVERSION_H
|
||||||
#define AVUTIL_FFVERSION_H
|
#define AVUTIL_FFVERSION_H
|
||||||
#define FFMPEG_VERSION "N-91434-gcced03dd66"
|
#define FFMPEG_VERSION "4.1"
|
||||||
#endif /* AVUTIL_FFVERSION_H */
|
#endif /* AVUTIL_FFVERSION_H */
|
||||||
|
|||||||
@@ -33,6 +33,8 @@
|
|||||||
* allocated buffer or map it with mmap() when available.
|
* allocated buffer or map it with mmap() when available.
|
||||||
* In case of success set *bufptr to the read or mmapped buffer, and
|
* In case of success set *bufptr to the read or mmapped buffer, and
|
||||||
* *size to the size in bytes of the buffer in *bufptr.
|
* *size to the size in bytes of the buffer in *bufptr.
|
||||||
|
* Unlike mmap this function succeeds with zero sized files, in this
|
||||||
|
* case *bufptr will be set to NULL and *size will be set to 0.
|
||||||
* The returned buffer must be released with av_file_unmap().
|
* The returned buffer must be released with av_file_unmap().
|
||||||
*
|
*
|
||||||
* @param log_offset loglevel offset used for logging
|
* @param log_offset loglevel offset used for logging
|
||||||
|
|||||||
@@ -158,6 +158,14 @@ enum AVFrameSideDataType {
|
|||||||
*/
|
*/
|
||||||
AV_FRAME_DATA_QP_TABLE_DATA,
|
AV_FRAME_DATA_QP_TABLE_DATA,
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timecode which conforms to SMPTE ST 12-1. The data is an array of 4 uint32_t
|
||||||
|
* where the first uint32_t describes how many (1-3) of the other timecodes are used.
|
||||||
|
* The timecode format is described in the av_timecode_get_smpte_from_framenum()
|
||||||
|
* function in libavutil/timecode.c.
|
||||||
|
*/
|
||||||
|
AV_FRAME_DATA_S12M_TIMECODE,
|
||||||
};
|
};
|
||||||
|
|
||||||
enum AVActiveFormatDescription {
|
enum AVActiveFormatDescription {
|
||||||
|
|||||||
@@ -343,7 +343,13 @@ char *av_get_pix_fmt_string(char *buf, int buf_size,
|
|||||||
* format writes the values corresponding to the palette
|
* format writes the values corresponding to the palette
|
||||||
* component c in data[1] to dst, rather than the palette indexes in
|
* component c in data[1] to dst, rather than the palette indexes in
|
||||||
* data[0]. The behavior is undefined if the format is not paletted.
|
* data[0]. The behavior is undefined if the format is not paletted.
|
||||||
|
* @param dst_element_size size of elements in dst array (2 or 4 byte)
|
||||||
*/
|
*/
|
||||||
|
void av_read_image_line2(void *dst, const uint8_t *data[4],
|
||||||
|
const int linesize[4], const AVPixFmtDescriptor *desc,
|
||||||
|
int x, int y, int c, int w, int read_pal_component,
|
||||||
|
int dst_element_size);
|
||||||
|
|
||||||
void av_read_image_line(uint16_t *dst, const uint8_t *data[4],
|
void av_read_image_line(uint16_t *dst, const uint8_t *data[4],
|
||||||
const int linesize[4], const AVPixFmtDescriptor *desc,
|
const int linesize[4], const AVPixFmtDescriptor *desc,
|
||||||
int x, int y, int c, int w, int read_pal_component);
|
int x, int y, int c, int w, int read_pal_component);
|
||||||
@@ -361,7 +367,12 @@ void av_read_image_line(uint16_t *dst, const uint8_t *data[4],
|
|||||||
* @param y the vertical coordinate of the first pixel to write
|
* @param y the vertical coordinate of the first pixel to write
|
||||||
* @param w the width of the line to write, that is the number of
|
* @param w the width of the line to write, that is the number of
|
||||||
* values to write to the image line
|
* values to write to the image line
|
||||||
|
* @param src_element_size size of elements in src array (2 or 4 byte)
|
||||||
*/
|
*/
|
||||||
|
void av_write_image_line2(const void *src, uint8_t *data[4],
|
||||||
|
const int linesize[4], const AVPixFmtDescriptor *desc,
|
||||||
|
int x, int y, int c, int w, int src_element_size);
|
||||||
|
|
||||||
void av_write_image_line(const uint16_t *src, uint8_t *data[4],
|
void av_write_image_line(const uint16_t *src, uint8_t *data[4],
|
||||||
const int linesize[4], const AVPixFmtDescriptor *desc,
|
const int linesize[4], const AVPixFmtDescriptor *desc,
|
||||||
int x, int y, int c, int w);
|
int x, int y, int c, int w);
|
||||||
|
|||||||
@@ -42,6 +42,10 @@
|
|||||||
* This is stored as BGRA on little-endian CPU architectures and ARGB on
|
* This is stored as BGRA on little-endian CPU architectures and ARGB on
|
||||||
* big-endian CPUs.
|
* big-endian CPUs.
|
||||||
*
|
*
|
||||||
|
* @note
|
||||||
|
* If the resolution is not a multiple of the chroma subsampling factor
|
||||||
|
* then the chroma plane resolution must be rounded up.
|
||||||
|
*
|
||||||
* @par
|
* @par
|
||||||
* When the pixel format is palettized RGB32 (AV_PIX_FMT_PAL8), the palettized
|
* When the pixel format is palettized RGB32 (AV_PIX_FMT_PAL8), the palettized
|
||||||
* image data is stored in AVFrame.data[0]. The palette is transported in
|
* image data is stored in AVFrame.data[0]. The palette is transported in
|
||||||
@@ -333,6 +337,9 @@ enum AVPixelFormat {
|
|||||||
AV_PIX_FMT_GRAY14BE, ///< Y , 14bpp, big-endian
|
AV_PIX_FMT_GRAY14BE, ///< Y , 14bpp, big-endian
|
||||||
AV_PIX_FMT_GRAY14LE, ///< Y , 14bpp, little-endian
|
AV_PIX_FMT_GRAY14LE, ///< Y , 14bpp, little-endian
|
||||||
|
|
||||||
|
AV_PIX_FMT_GRAYF32BE, ///< IEEE-754 single precision Y, 32bpp, big-endian
|
||||||
|
AV_PIX_FMT_GRAYF32LE, ///< IEEE-754 single precision Y, 32bpp, little-endian
|
||||||
|
|
||||||
AV_PIX_FMT_NB ///< number of pixel formats, DO NOT USE THIS if you want to link with shared libav* because the number of formats might differ between versions
|
AV_PIX_FMT_NB ///< number of pixel formats, DO NOT USE THIS if you want to link with shared libav* because the number of formats might differ between versions
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -401,6 +408,8 @@ enum AVPixelFormat {
|
|||||||
#define AV_PIX_FMT_GBRPF32 AV_PIX_FMT_NE(GBRPF32BE, GBRPF32LE)
|
#define AV_PIX_FMT_GBRPF32 AV_PIX_FMT_NE(GBRPF32BE, GBRPF32LE)
|
||||||
#define AV_PIX_FMT_GBRAPF32 AV_PIX_FMT_NE(GBRAPF32BE, GBRAPF32LE)
|
#define AV_PIX_FMT_GBRAPF32 AV_PIX_FMT_NE(GBRAPF32BE, GBRAPF32LE)
|
||||||
|
|
||||||
|
#define AV_PIX_FMT_GRAYF32 AV_PIX_FMT_NE(GRAYF32BE, GRAYF32LE)
|
||||||
|
|
||||||
#define AV_PIX_FMT_YUVA420P9 AV_PIX_FMT_NE(YUVA420P9BE , YUVA420P9LE)
|
#define AV_PIX_FMT_YUVA420P9 AV_PIX_FMT_NE(YUVA420P9BE , YUVA420P9LE)
|
||||||
#define AV_PIX_FMT_YUVA422P9 AV_PIX_FMT_NE(YUVA422P9BE , YUVA422P9LE)
|
#define AV_PIX_FMT_YUVA422P9 AV_PIX_FMT_NE(YUVA422P9BE , YUVA422P9LE)
|
||||||
#define AV_PIX_FMT_YUVA444P9 AV_PIX_FMT_NE(YUVA444P9BE , YUVA444P9LE)
|
#define AV_PIX_FMT_YUVA444P9 AV_PIX_FMT_NE(YUVA444P9BE , YUVA444P9LE)
|
||||||
|
|||||||
@@ -79,8 +79,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#define LIBAVUTIL_VERSION_MAJOR 56
|
#define LIBAVUTIL_VERSION_MAJOR 56
|
||||||
#define LIBAVUTIL_VERSION_MINOR 18
|
#define LIBAVUTIL_VERSION_MINOR 22
|
||||||
#define LIBAVUTIL_VERSION_MICRO 102
|
#define LIBAVUTIL_VERSION_MICRO 100
|
||||||
|
|
||||||
#define LIBAVUTIL_VERSION_INT AV_VERSION_INT(LIBAVUTIL_VERSION_MAJOR, \
|
#define LIBAVUTIL_VERSION_INT AV_VERSION_INT(LIBAVUTIL_VERSION_MAJOR, \
|
||||||
LIBAVUTIL_VERSION_MINOR, \
|
LIBAVUTIL_VERSION_MINOR, \
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ macx {
|
|||||||
unix:!macx {
|
unix:!macx {
|
||||||
CONFIG += link_pkgconfig
|
CONFIG += link_pkgconfig
|
||||||
PKGCONFIG += openssl
|
PKGCONFIG += openssl
|
||||||
|
DEFINES += HAVE_CLOCK_GETTIME=1
|
||||||
}
|
}
|
||||||
|
|
||||||
COMMON_C_DIR = $$PWD/moonlight-common-c
|
COMMON_C_DIR = $$PWD/moonlight-common-c
|
||||||
@@ -55,6 +56,7 @@ SOURCES += \
|
|||||||
$$COMMON_C_DIR/src/RtspConnection.c \
|
$$COMMON_C_DIR/src/RtspConnection.c \
|
||||||
$$COMMON_C_DIR/src/RtspParser.c \
|
$$COMMON_C_DIR/src/RtspParser.c \
|
||||||
$$COMMON_C_DIR/src/SdpGenerator.c \
|
$$COMMON_C_DIR/src/SdpGenerator.c \
|
||||||
|
$$COMMON_C_DIR/src/SimpleStun.c \
|
||||||
$$COMMON_C_DIR/src/VideoDepacketizer.c \
|
$$COMMON_C_DIR/src/VideoDepacketizer.c \
|
||||||
$$COMMON_C_DIR/src/VideoStream.c
|
$$COMMON_C_DIR/src/VideoStream.c
|
||||||
HEADERS += \
|
HEADERS += \
|
||||||
@@ -66,7 +68,7 @@ INCLUDEPATH += \
|
|||||||
CONFIG += warn_off staticlib
|
CONFIG += warn_off staticlib
|
||||||
DEFINES += HAS_SOCKLEN_T
|
DEFINES += HAS_SOCKLEN_T
|
||||||
|
|
||||||
debug {
|
CONFIG(debug, debug|release) {
|
||||||
# Enable asserts on debug builds
|
# Enable asserts on debug builds
|
||||||
DEFINES += LC_DEBUG
|
DEFINES += LC_DEBUG
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,16 @@ TEMPLATE = subdirs
|
|||||||
SUBDIRS = \
|
SUBDIRS = \
|
||||||
moonlight-common-c \
|
moonlight-common-c \
|
||||||
qmdnsengine \
|
qmdnsengine \
|
||||||
app
|
app \
|
||||||
|
soundio \
|
||||||
|
h264bitstream
|
||||||
|
|
||||||
# Build the dependencies in parallel before the final app
|
# Build the dependencies in parallel before the final app
|
||||||
app.depends = qmdnsengine moonlight-common-c
|
app.depends = qmdnsengine moonlight-common-c soundio h264bitstream
|
||||||
|
win32 {
|
||||||
|
SUBDIRS += AntiHooking
|
||||||
|
app.depends += AntiHooking
|
||||||
|
}
|
||||||
|
|
||||||
# Support debug and release builds from command line for CI
|
# Support debug and release builds from command line for CI
|
||||||
CONFIG += debug_and_release
|
CONFIG += debug_and_release
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
./configure --extra-cflags="-mmacosx-version-min=10.10" --enable-pic --enable-shared --disable-static --disable-all --enable-avcodec --enable-decoder=h264 --enable-decoder=hevc --enable-hwaccel=h264_videotoolbox --enable-hwaccel=hevc_videotoolbox
|
./configure --extra-cflags="-mmacosx-version-min=10.11" --install-name-dir=@loader_path/../lib --enable-pic --enable-shared --disable-static --disable-all --enable-avcodec --enable-decoder=h264 --enable-decoder=hevc --enable-hwaccel=h264_videotoolbox --enable-hwaccel=hevc_videotoolbox
|
||||||
make clean
|
make clean
|
||||||
make -j4
|
make -j4
|
||||||
|
codesign -s 45U78722YL libavcodec/libavcodec.dylib
|
||||||
|
codesign -s 45U78722YL libavutil/libavutil.dylib
|
||||||
|
codesign -vv libavcodec/libavcodec.dylib
|
||||||
|
codesign -vv libavutil/libavutil.dylib
|
||||||
@@ -11,10 +11,13 @@ if [ "$BUILD_CONFIG" != "Debug" ] && [ "$BUILD_CONFIG" != "Release" ]; then
|
|||||||
fail "Invalid build configuration"
|
fail "Invalid build configuration"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
[ "$SIGNING_KEY" == "" ] || git diff-index --quiet HEAD -- || fail "Signed release builds must not have unstaged changes!"
|
||||||
|
|
||||||
BUILD_ROOT=$PWD/build
|
BUILD_ROOT=$PWD/build
|
||||||
SOURCE_ROOT=$PWD
|
SOURCE_ROOT=$PWD
|
||||||
BUILD_FOLDER=$BUILD_ROOT/build-$BUILD_CONFIG
|
BUILD_FOLDER=$BUILD_ROOT/build-$BUILD_CONFIG
|
||||||
INSTALLER_FOLDER=$BUILD_ROOT/installer-$BUILD_CONFIG
|
INSTALLER_FOLDER=$BUILD_ROOT/installer-$BUILD_CONFIG
|
||||||
|
VERSION=`cat $SOURCE_ROOT/app/version.txt`
|
||||||
|
|
||||||
echo Cleaning output directories
|
echo Cleaning output directories
|
||||||
rm -rf $BUILD_FOLDER
|
rm -rf $BUILD_FOLDER
|
||||||
@@ -35,18 +38,14 @@ popd
|
|||||||
|
|
||||||
echo Saving dSYM file
|
echo Saving dSYM file
|
||||||
pushd $BUILD_FOLDER
|
pushd $BUILD_FOLDER
|
||||||
dsymutil app/Moonlight.app/Contents/MacOS/Moonlight -o Moonlight.dsym || fail "dSYM creation failed!"
|
dsymutil app/Moonlight.app/Contents/MacOS/Moonlight -o Moonlight-$VERSION.dsym || fail "dSYM creation failed!"
|
||||||
cp -R Moonlight.dsym $INSTALLER_FOLDER || fail "dSYM copy failed!"
|
cp -R Moonlight-$VERSION.dsym $INSTALLER_FOLDER || fail "dSYM copy failed!"
|
||||||
popd
|
popd
|
||||||
|
|
||||||
echo Copying dylib dependencies
|
echo Copying dylib dependencies
|
||||||
mkdir $BUILD_FOLDER/app/Moonlight.app/Contents/lib
|
mkdir $BUILD_FOLDER/app/Moonlight.app/Contents/lib
|
||||||
cp $SOURCE_ROOT/libs/mac/lib/*.dylib $BUILD_FOLDER/app/Moonlight.app/Contents/lib/ || fail "Dylib copy failed!"
|
cp $SOURCE_ROOT/libs/mac/lib/*.dylib $BUILD_FOLDER/app/Moonlight.app/Contents/lib/ || fail "Dylib copy failed!"
|
||||||
|
|
||||||
echo Copying frameworks dependencies
|
|
||||||
mkdir $BUILD_FOLDER/app/Moonlight.app/Contents/Frameworks
|
|
||||||
cp -R $SOURCE_ROOT/libs/mac/Frameworks/ $BUILD_FOLDER/app/Moonlight.app/Contents/Frameworks/ || fail "Framework copy failed!"
|
|
||||||
|
|
||||||
echo Creating app bundle
|
echo Creating app bundle
|
||||||
EXTRA_ARGS=
|
EXTRA_ARGS=
|
||||||
if [ "$BUILD_CONFIG" == "Debug" ]; then EXTRA_ARGS="$EXTRA_ARGS -use-debug-libs"; fi
|
if [ "$BUILD_CONFIG" == "Debug" ]; then EXTRA_ARGS="$EXTRA_ARGS -use-debug-libs"; fi
|
||||||
@@ -56,3 +55,4 @@ macdeployqt $BUILD_FOLDER/app/Moonlight.app $EXTRA_ARGS -qmldir=$SOURCE_ROOT/app
|
|||||||
|
|
||||||
echo Creating DMG
|
echo Creating DMG
|
||||||
create-dmg $BUILD_FOLDER/app/Moonlight.app $INSTALLER_FOLDER || fail "create-dmg failed!"
|
create-dmg $BUILD_FOLDER/app/Moonlight.app $INSTALLER_FOLDER || fail "create-dmg failed!"
|
||||||
|
mv $INSTALLER_FOLDER/Moonlight\ $VERSION.dmg $INSTALLER_FOLDER/Moonlight-$VERSION.dmg
|
||||||
@@ -17,6 +17,13 @@ if /I "%BUILD_CONFIG%"=="debug" (
|
|||||||
set BUILD_CONFIG=release
|
set BUILD_CONFIG=release
|
||||||
set SIGN=1
|
set SIGN=1
|
||||||
set MUST_DEPLOY_SYMBOLS=1
|
set MUST_DEPLOY_SYMBOLS=1
|
||||||
|
|
||||||
|
rem Fail if there are unstaged changes
|
||||||
|
git diff-index --quiet HEAD --
|
||||||
|
if !ERRORLEVEL! NEQ 0 (
|
||||||
|
echo Signed release builds must not have unstaged changes!
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
) else (
|
) else (
|
||||||
echo Invalid build configuration
|
echo Invalid build configuration
|
||||||
exit /b 1
|
exit /b 1
|
||||||
@@ -43,6 +50,7 @@ set BUILD_FOLDER=%BUILD_ROOT%\build-%ARCH%-%BUILD_CONFIG%
|
|||||||
set DEPLOY_FOLDER=%BUILD_ROOT%\deploy-%ARCH%-%BUILD_CONFIG%
|
set DEPLOY_FOLDER=%BUILD_ROOT%\deploy-%ARCH%-%BUILD_CONFIG%
|
||||||
set INSTALLER_FOLDER=%BUILD_ROOT%\installer-%ARCH%-%BUILD_CONFIG%
|
set INSTALLER_FOLDER=%BUILD_ROOT%\installer-%ARCH%-%BUILD_CONFIG%
|
||||||
set SYMBOLS_FOLDER=%BUILD_ROOT%\symbols-%ARCH%-%BUILD_CONFIG%
|
set SYMBOLS_FOLDER=%BUILD_ROOT%\symbols-%ARCH%-%BUILD_CONFIG%
|
||||||
|
set /p VERSION=<%SOURCE_ROOT%\app\version.txt
|
||||||
|
|
||||||
echo Cleaning output directories
|
echo Cleaning output directories
|
||||||
rmdir /s /q %DEPLOY_FOLDER%
|
rmdir /s /q %DEPLOY_FOLDER%
|
||||||
@@ -69,10 +77,12 @@ popd
|
|||||||
|
|
||||||
echo Saving PDBs
|
echo Saving PDBs
|
||||||
for /r "%BUILD_FOLDER%" %%f in (*.pdb) do (
|
for /r "%BUILD_FOLDER%" %%f in (*.pdb) do (
|
||||||
copy "%%f" "%SYMBOLS_FOLDER%\"
|
copy "%%f" %SYMBOLS_FOLDER%
|
||||||
if !ERRORLEVEL! NEQ 0 goto Error
|
if !ERRORLEVEL! NEQ 0 goto Error
|
||||||
)
|
)
|
||||||
7z a %SYMBOLS_FOLDER%\MoonlightDebuggingSymbols.zip %SYMBOLS_FOLDER%\*.pdb
|
copy %SOURCE_ROOT%\libs\windows\lib\%ARCH%\*.pdb %SYMBOLS_FOLDER%
|
||||||
|
if !ERRORLEVEL! NEQ 0 goto Error
|
||||||
|
7z a %SYMBOLS_FOLDER%\MoonlightDebuggingSymbols-%ARCH%-%VERSION%.zip %SYMBOLS_FOLDER%\*.pdb
|
||||||
if !ERRORLEVEL! NEQ 0 goto Error
|
if !ERRORLEVEL! NEQ 0 goto Error
|
||||||
|
|
||||||
if "%ML_SYMBOL_STORE%" NEQ "" (
|
if "%ML_SYMBOL_STORE%" NEQ "" (
|
||||||
@@ -86,10 +96,29 @@ if "%ML_SYMBOL_STORE%" NEQ "" (
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if "%ML_SYMBOL_ARCHIVE%" NEQ "" (
|
||||||
|
echo Copying PDB ZIP to symbol archive: %ML_SYMBOL_ARCHIVE%
|
||||||
|
copy %SYMBOLS_FOLDER%\MoonlightDebuggingSymbols-%ARCH%-%VERSION%.zip %ML_SYMBOL_ARCHIVE%
|
||||||
|
if !ERRORLEVEL! NEQ 0 goto Error
|
||||||
|
) else (
|
||||||
|
if "%MUST_DEPLOY_SYMBOLS%"=="1" (
|
||||||
|
echo "A symbol archive directory must be specified in ML_SYMBOL_ARCHIVE for signed release builds"
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
echo Copying DLL dependencies
|
echo Copying DLL dependencies
|
||||||
copy %SOURCE_ROOT%\libs\windows\lib\%ARCH%\*.dll %DEPLOY_FOLDER%
|
copy %SOURCE_ROOT%\libs\windows\lib\%ARCH%\*.dll %DEPLOY_FOLDER%
|
||||||
if !ERRORLEVEL! NEQ 0 goto Error
|
if !ERRORLEVEL! NEQ 0 goto Error
|
||||||
|
|
||||||
|
echo Copying AntiHooking.dll
|
||||||
|
copy %BUILD_FOLDER%\AntiHooking\%BUILD_CONFIG%\AntiHooking.dll %DEPLOY_FOLDER%
|
||||||
|
if !ERRORLEVEL! NEQ 0 goto Error
|
||||||
|
|
||||||
|
echo Copying GC mapping list
|
||||||
|
copy %SOURCE_ROOT%\app\SDL_GameControllerDB\gamecontrollerdb.txt %DEPLOY_FOLDER%
|
||||||
|
if !ERRORLEVEL! NEQ 0 goto Error
|
||||||
|
|
||||||
echo Deploying Qt dependencies
|
echo Deploying Qt dependencies
|
||||||
windeployqt.exe --dir %DEPLOY_FOLDER% --%BUILD_CONFIG% --qmldir %SOURCE_ROOT%\app\gui --no-opengl-sw --no-compiler-runtime %BUILD_FOLDER%\app\%BUILD_CONFIG%\Moonlight.exe
|
windeployqt.exe --dir %DEPLOY_FOLDER% --%BUILD_CONFIG% --qmldir %SOURCE_ROOT%\app\gui --no-opengl-sw --no-compiler-runtime %BUILD_FOLDER%\app\%BUILD_CONFIG%\Moonlight.exe
|
||||||
if !ERRORLEVEL! NEQ 0 goto Error
|
if !ERRORLEVEL! NEQ 0 goto Error
|
||||||
@@ -104,10 +133,12 @@ if !ERRORLEVEL! NEQ 0 goto Error
|
|||||||
|
|
||||||
if "%SIGN%"=="1" (
|
if "%SIGN%"=="1" (
|
||||||
echo Signing deployed binaries
|
echo Signing deployed binaries
|
||||||
|
set FILES_TO_SIGN=
|
||||||
for /r "%DEPLOY_FOLDER%" %%f in (*.dll *.exe) do (
|
for /r "%DEPLOY_FOLDER%" %%f in (*.dll *.exe) do (
|
||||||
signtool %SIGNTOOL_PARAMS% %%f
|
set FILES_TO_SIGN=!FILES_TO_SIGN! %%f
|
||||||
if !ERRORLEVEL! NEQ 0 goto Error
|
|
||||||
)
|
)
|
||||||
|
signtool %SIGNTOOL_PARAMS% !FILES_TO_SIGN!
|
||||||
|
if !ERRORLEVEL! NEQ 0 goto Error
|
||||||
)
|
)
|
||||||
|
|
||||||
echo Building MSI
|
echo Building MSI
|
||||||
@@ -138,6 +169,9 @@ if "%SIGN%"=="1" (
|
|||||||
if !ERRORLEVEL! NEQ 0 goto Error
|
if !ERRORLEVEL! NEQ 0 goto Error
|
||||||
)
|
)
|
||||||
|
|
||||||
|
rem Rename the installer to match the publishing convention
|
||||||
|
ren %INSTALLER_FOLDER%\MoonlightSetup.exe MoonlightSetup-%ARCH%-%VERSION%.exe
|
||||||
|
|
||||||
echo Building portable package
|
echo Building portable package
|
||||||
rem This must be done after WiX harvesting and signing, since the VCRT dlls are MS signed
|
rem This must be done after WiX harvesting and signing, since the VCRT dlls are MS signed
|
||||||
rem and should not be harvested for inclusion in the full installer
|
rem and should not be harvested for inclusion in the full installer
|
||||||
@@ -145,19 +179,19 @@ copy "%VCToolsRedistDir%%ARCH%\Microsoft.VC141.CRT\*.dll" %DEPLOY_FOLDER%
|
|||||||
rem This file tells Moonlight that it's a portable installation
|
rem This file tells Moonlight that it's a portable installation
|
||||||
echo. > %DEPLOY_FOLDER%\portable.dat
|
echo. > %DEPLOY_FOLDER%\portable.dat
|
||||||
if !ERRORLEVEL! NEQ 0 goto Error
|
if !ERRORLEVEL! NEQ 0 goto Error
|
||||||
7z a %INSTALLER_FOLDER%\MoonlightPortable.zip %DEPLOY_FOLDER%\*
|
7z a %INSTALLER_FOLDER%\MoonlightPortable-%ARCH%-%VERSION%.zip %DEPLOY_FOLDER%\*
|
||||||
if !ERRORLEVEL! NEQ 0 goto Error
|
if !ERRORLEVEL! NEQ 0 goto Error
|
||||||
|
|
||||||
if /i "%APPVEYOR%"=="true" (
|
if /i "%APPVEYOR%"=="true" (
|
||||||
echo Pushing artifacts
|
echo Pushing artifacts
|
||||||
appveyor PushArtifact %INSTALLER_FOLDER%\MoonlightSetup.exe -FileName MoonlightSetup-%ARCH%-%BUILD_CONFIG%.exe
|
appveyor PushArtifact %INSTALLER_FOLDER%\MoonlightSetup-%ARCH%-%VERSION%.exe -FileName MoonlightSetup-%ARCH%-%BUILD_CONFIG%.exe
|
||||||
if !ERRORLEVEL! NEQ 0 goto Error
|
if !ERRORLEVEL! NEQ 0 goto Error
|
||||||
appveyor PushArtifact %INSTALLER_FOLDER%\MoonlightPortable.zip -FileName MoonlightPortable-%ARCH%-%BUILD_CONFIG%.zip
|
appveyor PushArtifact %INSTALLER_FOLDER%\MoonlightPortable-%ARCH%-%VERSION%.zip -FileName MoonlightPortable-%ARCH%-%BUILD_CONFIG%.zip
|
||||||
if !ERRORLEVEL! NEQ 0 goto Error
|
if !ERRORLEVEL! NEQ 0 goto Error
|
||||||
appveyor PushArtifact %SYMBOLS_FOLDER%\MoonlightDebuggingSymbols.zip -FileName MoonlightDebuggingSymbols-%ARCH%-%BUILD_CONFIG%.zip
|
appveyor PushArtifact %SYMBOLS_FOLDER%\MoonlightDebuggingSymbols-%ARCH%-%VERSION%.zip -FileName MoonlightDebuggingSymbols-%ARCH%-%BUILD_CONFIG%.zip
|
||||||
)
|
)
|
||||||
|
|
||||||
echo Build successful!
|
echo Build successful for Moonlight v%VERSION%!
|
||||||
exit /b 0
|
exit /b 0
|
||||||
|
|
||||||
:Error
|
:Error
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
#-------------------------------------------------
|
||||||
|
#
|
||||||
|
# Project created by QtCreator 2018-10-02T15:27:07
|
||||||
|
#
|
||||||
|
#-------------------------------------------------
|
||||||
|
|
||||||
|
QT -= core gui
|
||||||
|
|
||||||
|
TARGET = soundio
|
||||||
|
TEMPLATE = lib
|
||||||
|
|
||||||
|
# Support debug and release builds from command line for CI
|
||||||
|
CONFIG += debug_and_release
|
||||||
|
|
||||||
|
# Ensure symbols are always generated
|
||||||
|
CONFIG += force_debug_info
|
||||||
|
|
||||||
|
# Build a static library
|
||||||
|
CONFIG += staticlib
|
||||||
|
|
||||||
|
# Force MSVC to compile C as C++ for atomic support
|
||||||
|
*-msvc {
|
||||||
|
QMAKE_CFLAGS += /TP
|
||||||
|
}
|
||||||
|
|
||||||
|
# Older GCC versions defaulted to GNU89
|
||||||
|
*-g++ {
|
||||||
|
QMAKE_CFLAGS += -std=gnu99
|
||||||
|
}
|
||||||
|
|
||||||
|
# Disable warnings
|
||||||
|
CONFIG += warn_off
|
||||||
|
|
||||||
|
unix:!macx {
|
||||||
|
CONFIG += link_pkgconfig
|
||||||
|
|
||||||
|
packagesExist(libpulse) {
|
||||||
|
PKGCONFIG += libpulse
|
||||||
|
CONFIG += pulseaudio
|
||||||
|
}
|
||||||
|
packagesExist(alsa) {
|
||||||
|
PKGCONFIG += alsa
|
||||||
|
CONFIG += alsa
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CONFIG(release, debug|release) {
|
||||||
|
# Disable asserts on release builds
|
||||||
|
DEFINES += NDEBUG
|
||||||
|
}
|
||||||
|
|
||||||
|
DEFINES += \
|
||||||
|
SOUNDIO_STATIC_LIBRARY \
|
||||||
|
SOUNDIO_VERSION_MAJOR=1 \
|
||||||
|
SOUNDIO_VERSION_MINOR=1 \
|
||||||
|
SOUNDIO_VERSION_PATCH=0 \
|
||||||
|
SOUNDIO_VERSION_STRING=\\\"1.1.0\\\"
|
||||||
|
|
||||||
|
SRC_DIR = $$PWD/libsoundio/src
|
||||||
|
INC_DIR = $$PWD/libsoundio
|
||||||
|
|
||||||
|
SOURCES += \
|
||||||
|
$$SRC_DIR/channel_layout.c \
|
||||||
|
$$SRC_DIR/dummy.c \
|
||||||
|
$$SRC_DIR/os.c \
|
||||||
|
$$SRC_DIR/ring_buffer.c \
|
||||||
|
$$SRC_DIR/soundio.c \
|
||||||
|
$$SRC_DIR/util.c
|
||||||
|
|
||||||
|
HEADERS += \
|
||||||
|
$$SRC_DIR/atomics.h \
|
||||||
|
$$SRC_DIR/dummy.h \
|
||||||
|
$$SRC_DIR/list.h \
|
||||||
|
$$SRC_DIR/os.h \
|
||||||
|
$$SRC_DIR/ring_buffer.h \
|
||||||
|
$$SRC_DIR/soundio_internal.h \
|
||||||
|
$$SRC_DIR/soundio_private.h \
|
||||||
|
$$SRC_DIR/util.h \
|
||||||
|
$$INC_DIR/soundio/soundio.h \
|
||||||
|
$$INC_DIR/soundio/endian.h
|
||||||
|
|
||||||
|
INCLUDEPATH += $$INC_DIR
|
||||||
|
|
||||||
|
win32 {
|
||||||
|
message(WASAPI backend selected)
|
||||||
|
|
||||||
|
DEFINES += SOUNDIO_HAVE_WASAPI
|
||||||
|
SOURCES += $$SRC_DIR/wasapi.c
|
||||||
|
HEADERS += $$SRC_DIR/wasapi.h
|
||||||
|
}
|
||||||
|
macx {
|
||||||
|
message(CoreAudio backend selected)
|
||||||
|
|
||||||
|
DEFINES += SOUNDIO_HAVE_COREAUDIO
|
||||||
|
SOURCES += $$SRC_DIR/coreaudio.c
|
||||||
|
HEADERS += $$SRC_DIR/coreaudio.h
|
||||||
|
}
|
||||||
|
pulseaudio {
|
||||||
|
message(PulseAudio backend selected)
|
||||||
|
|
||||||
|
DEFINES += SOUNDIO_HAVE_PULSEAUDIO
|
||||||
|
SOURCES += $$SRC_DIR/pulseaudio.c
|
||||||
|
HEADERS += $$SRC_DIR/pulseaudio.h
|
||||||
|
}
|
||||||
|
alsa {
|
||||||
|
message(ALSA backend selected)
|
||||||
|
|
||||||
|
DEFINES += SOUNDIO_HAVE_ALSA
|
||||||
|
SOURCES += $$SRC_DIR/alsa.c
|
||||||
|
HEADERS += $$SRC_DIR/alsa.h
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
<?define FullName = "Moonlight Game Streaming Client" ?>
|
<?define FullName = "Moonlight Game Streaming Client" ?>
|
||||||
|
|
||||||
<?define ShortcutName = "$(var.ShortName)" ?>
|
<?define ShortcutName = "$(var.ShortName)" ?>
|
||||||
<?define ShortcutDesc = "Stream games from a NVIDIA GameStream-compatible PC" ?>
|
<?define ShortcutDesc = "Stream games from your NVIDIA GameStream-enabled PC" ?>
|
||||||
<?define InstallFolder = "Moonlight Game Streaming" ?>
|
<?define InstallFolder = "Moonlight Game Streaming" ?>
|
||||||
|
|
||||||
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi"
|
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi"
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
Name="$(var.FullName)"
|
Name="$(var.FullName)"
|
||||||
Language="1033"
|
Language="1033"
|
||||||
Version="!(bind.fileVersion.MoonlightExe)"
|
Version="!(bind.fileVersion.MoonlightExe)"
|
||||||
Manufacturer="Moonlight Game Streaming Team"
|
Manufacturer="Moonlight Game Streaming Project"
|
||||||
UpgradeCode="5c09f94e-f809-4c6a-9b7b-597c99f041fe">
|
UpgradeCode="5c09f94e-f809-4c6a-9b7b-597c99f041fe">
|
||||||
|
|
||||||
<Package InstallerVersion="500" Compressed="yes" InstallScope="perMachine" />
|
<Package InstallerVersion="500" Compressed="yes" InstallScope="perMachine" />
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
<Bundle Name="Moonlight Game Streaming Client"
|
<Bundle Name="Moonlight Game Streaming Client"
|
||||||
Version="!(bind.PackageVersion.Moonlight)"
|
Version="!(bind.PackageVersion.Moonlight)"
|
||||||
Manufacturer="Moonlight Game Streaming Team"
|
Manufacturer="Moonlight Game Streaming Project"
|
||||||
UpgradeCode="466fa35d-4be4-40ef-9ce5-afadc3b63bc5"
|
UpgradeCode="466fa35d-4be4-40ef-9ce5-afadc3b63bc5"
|
||||||
HelpUrl="https://github.com/moonlight-stream/moonlight-docs/wiki/Setup-Guide"
|
HelpUrl="https://github.com/moonlight-stream/moonlight-docs/wiki/Setup-Guide"
|
||||||
UpdateUrl="https://github.com/moonlight-stream/moonlight-qt/releases"
|
UpdateUrl="https://github.com/moonlight-stream/moonlight-qt/releases"
|
||||||
|
|||||||