Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a7c463c52 | ||
|
|
eab77fba7a | ||
|
|
61d90a5a88 | ||
|
|
ef99f02bc7 | ||
|
|
9166a604d6 | ||
|
|
d5a1189053 | ||
|
|
cb52a53922 | ||
|
|
1aff2ff0c3 | ||
|
|
fd63aad3b8 | ||
|
|
9a807a0685 | ||
|
|
40fc9fa26f | ||
|
|
cf0d29d452 | ||
|
|
e411c207ae | ||
|
|
5df2704ccc | ||
|
|
36f468a599 | ||
|
|
dfae69834a | ||
|
|
e53b32fa57 | ||
|
|
969afac696 | ||
|
|
50d4f267ba | ||
|
|
33b9caaca9 | ||
|
|
4f84843b00 | ||
|
|
44f415f94d | ||
|
|
6e4dd161ac | ||
|
|
c724f01c98 | ||
|
|
1fc63a6529 | ||
|
|
5d84ab4a47 | ||
|
|
fc315feac4 | ||
|
|
82a87e96b3 | ||
|
|
7d1ecc8d82 | ||
|
|
f9daf2a45c | ||
|
|
113751a4bc | ||
|
|
e182445593 | ||
|
|
6661ca17c2 | ||
|
|
1b4e75f49e | ||
|
|
22ed7f4e49 | ||
|
|
1d691cf763 | ||
|
|
9075f309ad | ||
|
|
18d97e9f73 | ||
|
|
bccd118abb | ||
|
|
3dc049ee5f | ||
|
|
a614a693e5 | ||
|
|
8dddcd04d5 | ||
|
|
209b4a1b02 | ||
|
|
b14a0fa2ce | ||
|
|
5f05363996 | ||
|
|
d5770ad8cc | ||
|
|
c239d0814b | ||
|
|
f7789a1e4a | ||
|
|
6d382078d3 | ||
|
|
162a16ed3a | ||
|
|
d706e81cd4 | ||
|
|
9fb0bffd61 | ||
|
|
4a452a2467 | ||
|
|
396d65b003 | ||
|
|
fa52e7c1b7 | ||
|
|
6758d6c43e | ||
|
|
4ad27670ec | ||
|
|
1ad072236d | ||
|
|
76387aacad | ||
|
|
200b1c5095 | ||
|
|
d14cfb577b | ||
|
|
c99b4f1559 | ||
|
|
45fdbd16b4 | ||
|
|
c04113c9fa | ||
|
|
4620fa5001 | ||
|
|
ba7dd31035 | ||
|
|
3928155b9c | ||
|
|
a0249b4337 | ||
|
|
d89982237d | ||
|
|
7e414f0af3 | ||
|
|
8fb37ae229 | ||
|
|
2e3de32810 | ||
|
|
f2e40889b2 | ||
|
|
896b247bc0 | ||
|
|
605d52f160 | ||
|
|
afb2b465a8 | ||
|
|
b1de66d878 | ||
|
|
71b625081a | ||
|
|
2cd4851601 | ||
|
|
987d104a6c | ||
|
|
60e6398dfa | ||
|
|
14ec0259df | ||
|
|
9f0617f6ee | ||
|
|
98297e0aed | ||
|
|
926b192722 | ||
|
|
97e5acecd7 | ||
|
|
2c960059ee | ||
|
|
e20bf3fbd9 | ||
|
|
af9fb0e091 | ||
|
|
f96911e1ba | ||
|
|
fdbb1c8d72 | ||
|
|
8a0089eb81 | ||
|
|
821d4ac9dd | ||
|
|
f4408473e6 | ||
|
|
1f759577ce | ||
|
|
04376ca047 | ||
|
|
7ac108f495 | ||
|
|
a2eece0f5c | ||
|
|
a03279df3b | ||
|
|
bef6c2b665 | ||
|
|
8002c65693 | ||
|
|
5c7247b8b5 | ||
|
|
1a18e68441 | ||
|
|
72afa55897 | ||
|
|
eed9fca10c | ||
|
|
25d97e187a | ||
|
|
7ca2ca8045 | ||
|
|
54885c3e5f | ||
|
|
b8ffb6e53d | ||
|
|
7bc43c226d | ||
|
|
da4697794d | ||
|
|
879c524658 | ||
|
|
e3a8f010c3 | ||
|
|
f2f8f92172 | ||
|
|
265ec96c6f | ||
|
|
6e06174db6 | ||
|
|
9677569293 | ||
|
|
c8e00195ed | ||
|
|
64bc189010 | ||
|
|
12496e4432 | ||
|
|
9e2fd67487 | ||
|
|
af17d56cbd | ||
|
|
6e0a657b76 | ||
|
|
b0a9fc7085 | ||
|
|
5e47b8a835 | ||
|
|
0b94ad1fef | ||
|
|
9dcd856099 | ||
|
|
cfbcc4093c | ||
|
|
b8169ae08b | ||
|
|
103625fbdd | ||
|
|
4712080a1b | ||
|
|
93c683be3a | ||
|
|
c9a7c15f98 | ||
|
|
64a08f0533 | ||
|
|
52c6455ca5 | ||
|
|
238681d72f | ||
|
|
dbcc6feb3f | ||
|
|
300b53f28c | ||
|
|
3dabb1c891 | ||
|
|
4381f7a973 | ||
|
|
77b767ae5b | ||
|
|
2c4f78133f | ||
|
|
ec2c181f4e | ||
|
|
fae98eb13a | ||
|
|
f1d117d5d7 | ||
|
|
529ac8041a | ||
|
|
391e84e0f4 | ||
|
|
6a2b717ff1 | ||
|
|
475a39cb73 | ||
|
|
4dba74e2f9 | ||
|
|
587d783955 | ||
|
|
3bfe87d516 | ||
|
|
d3e43012fb | ||
|
|
5da156b433 | ||
|
|
b2eeb63b6f | ||
|
|
a7517dace3 | ||
|
|
bd9f2d2009 | ||
|
|
78730f7235 | ||
|
|
8a2491d955 | ||
|
|
0d68faf40c | ||
|
|
957a2b2169 | ||
|
|
856318f947 | ||
|
|
4df9d6b20a | ||
|
|
8519d09141 | ||
|
|
1b4fe83739 | ||
|
|
8fc825b2eb | ||
|
|
6d0fc22ab5 | ||
|
|
9a71b4c237 | ||
|
|
9aa487418f | ||
|
|
203b2fcaca | ||
|
|
dae21f2638 | ||
|
|
af6e99cbac | ||
|
|
6d6acf4308 | ||
|
|
7dd4815edf | ||
|
|
2c068a99a3 | ||
|
|
fa93364ddd | ||
|
|
9b1eef3f63 | ||
|
|
ed6c07fd4b | ||
|
|
5ad96f5287 | ||
|
|
fa8845c7e2 | ||
|
|
b96ace7f04 | ||
|
|
234a8dc3d5 | ||
|
|
0467c2f893 | ||
|
|
762fe92933 | ||
|
|
16d105d462 | ||
|
|
8a8b624f47 | ||
|
|
9470615c63 | ||
|
|
8f0ea700a5 | ||
|
|
b7cbec0b1c | ||
|
|
9be9934b8c | ||
|
|
55f0e1e1d5 | ||
|
|
bbd784fc85 | ||
|
|
408f93732b | ||
|
|
fb53c95207 | ||
|
|
960e083152 | ||
|
|
7e560aa09e | ||
|
|
7d9478b774 | ||
|
|
0fa0ab43c9 | ||
|
|
f9cdd49d2e | ||
|
|
7661f5a3d7 | ||
|
|
395f337bbb | ||
|
|
9e9fe2a2cf | ||
|
|
5bf88cfe96 | ||
|
|
f7d3c10c9d | ||
|
|
6b395c816f | ||
|
|
d6e7173af0 | ||
|
|
1a60484abc | ||
|
|
62f765b2b4 | ||
|
|
57f2fb07f0 | ||
|
|
1b8c3aae73 | ||
|
|
845e84adb7 | ||
|
|
91c0429e2c | ||
|
|
30f673efe1 | ||
|
|
c1b1719914 | ||
|
|
9fbdd794b9 | ||
|
|
38ff2bf5cb | ||
|
|
345e800abd | ||
|
|
404eaa44e4 | ||
|
|
8e257ab241 | ||
|
|
fabbad5370 | ||
|
|
7d61acb2a8 | ||
|
|
f6a451d6e7 | ||
|
|
e68a15c825 | ||
|
|
f929cffce7 | ||
|
|
f171588616 | ||
|
|
e467ce7ce8 | ||
|
|
1fda7550da | ||
|
|
f714a5d0cb | ||
|
|
2a3d5a27a8 | ||
|
|
c9bf5ae79d | ||
|
|
cf2d8f71d6 | ||
|
|
611d9ac1ba | ||
|
|
f3309087ab |
@@ -0,0 +1,31 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us address broken functionality
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**Steps to reproduce**
|
||||
Any special steps that are required for the bug to appear.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem. If the issue is related to video glitching or poor quality, please include screenshots.
|
||||
|
||||
**Moonlight settings (please complete the following information)**
|
||||
- Have any settings been adjusted from defaults?
|
||||
- If so, which settings have been changed?
|
||||
- Does the problem still occur after reverting settings back to default?
|
||||
|
||||
**Client PC details (please complete the following information)**
|
||||
- OS: [e.g. Windows 10 1803]
|
||||
- Moonlight Version: [e.g. v0.2.0]
|
||||
|
||||
**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 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`
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
@@ -0,0 +1,17 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
@@ -4,3 +4,12 @@
|
||||
[submodule "qmdnsengine/qmdnsengine"]
|
||||
path = qmdnsengine/qmdnsengine
|
||||
url = https://github.com/cgutman/qmdnsengine.git
|
||||
[submodule "app/SDL_GameControllerDB"]
|
||||
path = app/SDL_GameControllerDB
|
||||
url = https://github.com/gabomdq/SDL_GameControllerDB.git
|
||||
[submodule "soundio/libsoundio"]
|
||||
path = soundio/libsoundio
|
||||
url = https://github.com/cgutman/libsoundio.git
|
||||
[submodule "h264bitstream/h264bitstream"]
|
||||
path = h264bitstream/h264bitstream
|
||||
url = https://github.com/aizvorski/h264bitstream.git
|
||||
|
||||
@@ -20,6 +20,7 @@ matrix:
|
||||
- qt59base
|
||||
- qt59quickcontrols2
|
||||
- qt59svg
|
||||
- qt59multimedia
|
||||
- libesd0-dev
|
||||
- libgl-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 LoadLibraryAHook(LPCSTR lpLibFileName)
|
||||
{
|
||||
if (lpLibFileName && isImageBlacklistedA(lpLibFileName)) {
|
||||
SetLastError(ERROR_ACCESS_DISABLED_BY_POLICY);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return s_RealLoadLibraryA(lpLibFileName);
|
||||
}
|
||||
|
||||
static HMODULE LoadLibraryWHook(LPCWSTR lpLibFileName)
|
||||
{
|
||||
if (lpLibFileName && isImageBlacklistedW(lpLibFileName)) {
|
||||
SetLastError(ERROR_ACCESS_DISABLED_BY_POLICY);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return s_RealLoadLibraryW(lpLibFileName);
|
||||
}
|
||||
|
||||
static HMODULE 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 LoadLibraryExWHook(LPCWSTR lpLibFileName, HANDLE hFile, DWORD dwFlags)
|
||||
{
|
||||
if (lpLibFileName && isImageBlacklistedW(lpLibFileName)) {
|
||||
SetLastError(ERROR_ACCESS_DISABLED_BY_POLICY);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return s_RealLoadLibraryExW(lpLibFileName, hFile, dwFlags);
|
||||
}
|
||||
|
||||
static CNktHookLib s_HookManager;
|
||||
static LoadLibraryAFunc s_RealLoadLibraryA;
|
||||
static LoadLibraryWFunc s_RealLoadLibraryW;
|
||||
static LoadLibraryExAFunc s_RealLoadLibraryExA;
|
||||
static LoadLibraryExWFunc s_RealLoadLibraryExW;
|
||||
|
||||
static constexpr LPCWSTR k_BlacklistedDlls[] = {
|
||||
// This DLL shipped with ASUS Sonic Radar 3 improperly handles
|
||||
// D3D9 exclusive fullscreen in a way that causes CreateDeviceEx()
|
||||
// to deadlock. https://github.com/moonlight-stream/moonlight-qt/issues/102
|
||||
L"NahimicOSD.dll"
|
||||
};
|
||||
};
|
||||
|
||||
CNktHookLib AntiHookingProtection::s_HookManager;
|
||||
LoadLibraryAFunc AntiHookingProtection::s_RealLoadLibraryA;
|
||||
LoadLibraryWFunc AntiHookingProtection::s_RealLoadLibraryW;
|
||||
LoadLibraryExAFunc AntiHookingProtection::s_RealLoadLibraryExA;
|
||||
LoadLibraryExWFunc AntiHookingProtection::s_RealLoadLibraryExW;
|
||||
|
||||
AH_EXPORT void AntiHookingDummyImport() {}
|
||||
|
||||
extern "C"
|
||||
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID)
|
||||
{
|
||||
switch (fdwReason)
|
||||
{
|
||||
case DLL_PROCESS_ATTACH:
|
||||
AntiHookingProtection::enable();
|
||||
DisableThreadLibraryCalls(hinstDLL);
|
||||
break;
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef ANTIHOOKING_LIBRARY
|
||||
#define AH_EXPORT extern "C" __declspec(dllexport)
|
||||
#else
|
||||
#define AH_EXPORT extern "C" __declspec(dllimport)
|
||||
#endif
|
||||
|
||||
AH_EXPORT void AntiHookingDummyImport();
|
||||
|
||||
@@ -13,20 +13,42 @@ You can follow development on our [Discord server](https://discord.gg/6ERtzFY).
|
||||
|
||||
## Features
|
||||
- Hardware accelerated video decoding on Windows, Mac, and Linux
|
||||
- Supports streaming at up to 120 FPS on high refresh rate monitors
|
||||
- Supports streaming at 720p, 1080p, 1440p, or 4K
|
||||
- Supports streaming at up to 120 FPS (high refresh rate monitor recommended)
|
||||
- Supports streaming at 720p, 1080p, 1440p, 4K, and the client PC's native screen resolution
|
||||
- 5.1 surround sound audio
|
||||
- HEVC support for better image quality at reduced bandwidth
|
||||
- Keyboard and mouse support
|
||||
- Gamepad support with SDL gamepad mappings
|
||||
|
||||
## Building
|
||||
### General Build Requirements
|
||||
* Qt 5.9 SDK or later
|
||||
|
||||
### Windows-specific Requirements
|
||||
* Windows 7 or later
|
||||
* Visual Studio 2015 or later
|
||||
* Select MSVC Desktop toolchain during Qt installation
|
||||
* 7-Zip (only if building installers for non-development PCs)
|
||||
* WiX Toolset v3.11 or later (only if building installers for non-development PCs)
|
||||
|
||||
### Mac-specific Requirements
|
||||
* macOS El Capitan (10.11) or later
|
||||
* Xcode with High Sierra (10.13) SDK (Mojave SDK not yet supported until Qt 5.12 later this year)
|
||||
* [create-dmg](https://github.com/sindresorhus/create-dmg) (only if building DMGs for use on non-development Macs)
|
||||
|
||||
### Linux-specific Requirements
|
||||
* GCC or Clang
|
||||
* Install your distro equivalents of: `openssl-devel qt5-devel SDL2-devel ffmpeg-devel qt5-qtquickcontrols2-devel libva-devel libvdpau-devel opus-devel`
|
||||
* FFmpeg 4.0 is required to build. If your distro doesn't package FFmpeg 4.0, you can build and install it from source on http://ffmpeg.org/
|
||||
|
||||
### Build Setup Steps
|
||||
1. Install the latest Qt SDK (and optionally, the Qt Creator IDE) from https://www.qt.io/download
|
||||
- On Windows, install Visual Studio 2017 and select MSVC 2017 toolchain during Qt installation
|
||||
- On Mac, install the latest version of XCode
|
||||
- On Linux, install your distro equivalents of: `openssl-devel qt5-devel SDL2-devel ffmpeg-devel qt5-qtquickcontrols2-devel libva-devel libvdpau-devel`
|
||||
* You may also use Homebrew on macOS or your Linux distro's package manager for the Qt SDK as long as the packages are Qt 5.9 or later.
|
||||
2. Run `git submodule update --init --recursive` from within `moonlight-qt/`
|
||||
3. Open the project in Qt Creator or build from qmake on the command line
|
||||
3. Open the project in Qt Creator or build from qmake on the command line.
|
||||
* To build a binary for use on non-development machines, use the scripts in the `scripts` folder.
|
||||
* For Windows builds, use `scripts\generate-installers.bat`. Execute this script from the root of the repository within a Qt command prompt. Ensure WiX and 7-Zip binary directories are in your `%PATH%`.
|
||||
* For macOS builds, use `scripts/generate-dmg.sh`. Execute this script from the root of the repository and ensure Qt's `bin` folder is in your `$PATH`.
|
||||
|
||||
## Contribute
|
||||
1. Fork us
|
||||
|
||||
+2
-2
@@ -23,9 +23,9 @@
|
||||
<key>NSSupportsAutomaticGraphicsSwitching</key>
|
||||
<true/>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>0.2.0</string>
|
||||
<string>0.6.1</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>0.2.0</string>
|
||||
<string>0.6.1</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Moonlight</string>
|
||||
</dict>
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3" manifestVersion="1.0">
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<security>
|
||||
<requestedPrivileges>
|
||||
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
|
||||
</requestedPrivileges>
|
||||
</security>
|
||||
</trustInfo>
|
||||
<description>Moonlight Game Streaming</description>
|
||||
<asmv3:application>
|
||||
<asmv3:windowsSettings>
|
||||
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">permonitor</dpiAwareness>
|
||||
</asmv3:windowsSettings>
|
||||
</asmv3:application>
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<!-- Windows 10 -->
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
|
||||
<!-- Windows 8.1 -->
|
||||
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
|
||||
<!-- Windows 8 -->
|
||||
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>
|
||||
<!-- Windows 7 -->
|
||||
<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/>
|
||||
<!-- Windows Vista -->
|
||||
<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}"/>
|
||||
</application>
|
||||
</compatibility>
|
||||
</assembly>
|
||||
Submodule
+1
Submodule app/SDL_GameControllerDB added at 19944691bf
+115
-22
@@ -11,6 +11,16 @@ unix:!macx {
|
||||
# Support debug and release builds from command line for CI
|
||||
CONFIG += debug_and_release
|
||||
|
||||
# Ensure symbols are always generated
|
||||
CONFIG += force_debug_info
|
||||
|
||||
# Precompile QML files to avoid writing qmlcache on portable versions.
|
||||
# Since this binds the app against the Qt runtime version, we will only
|
||||
# do this for Windows and Mac, since they ship with the Qt runtime.
|
||||
win32|macx {
|
||||
CONFIG += qtquickcompiler
|
||||
}
|
||||
|
||||
TEMPLATE = app
|
||||
|
||||
# The following define makes your compiler emit warnings if you use
|
||||
@@ -34,7 +44,7 @@ win32 {
|
||||
LIBS += -L$$PWD/../libs/windows/lib/x64
|
||||
}
|
||||
|
||||
LIBS += ws2_32.lib winmm.lib dxva2.lib ole32.lib
|
||||
LIBS += ws2_32.lib winmm.lib dxva2.lib ole32.lib gdi32.lib user32.lib d3d9.lib dwmapi.lib dbghelp.lib
|
||||
}
|
||||
macx {
|
||||
INCLUDEPATH += $$PWD/../libs/mac/include $$PWD/../libs/mac/Frameworks/SDL2.framework/Versions/A/Headers
|
||||
@@ -45,6 +55,14 @@ unix:!macx {
|
||||
CONFIG += link_pkgconfig
|
||||
PKGCONFIG += openssl sdl2 opus
|
||||
|
||||
# For libsoundio
|
||||
packagesExist(libpulse) {
|
||||
PKGCONFIG += libpulse
|
||||
}
|
||||
packagesExist(alsa) {
|
||||
PKGCONFIG += alsa
|
||||
}
|
||||
|
||||
packagesExist(libavcodec) {
|
||||
PKGCONFIG += libavcodec libavutil
|
||||
CONFIG += ffmpeg
|
||||
@@ -66,45 +84,64 @@ unix:!macx {
|
||||
}
|
||||
win32 {
|
||||
LIBS += -llibssl -llibcrypto -lSDL2 -lavcodec -lavutil -lopus
|
||||
CONFIG += ffmpeg
|
||||
CONFIG += ffmpeg soundio
|
||||
}
|
||||
macx {
|
||||
LIBS += -lssl -lcrypto -lavcodec.58 -lavutil.56 -lopus -framework SDL2
|
||||
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 += \
|
||||
main.cpp \
|
||||
backend/identitymanager.cpp \
|
||||
backend/nvcomputer.cpp \
|
||||
backend/nvhttp.cpp \
|
||||
backend/nvpairingmanager.cpp \
|
||||
backend/computermanager.cpp \
|
||||
backend/boxartmanager.cpp \
|
||||
cli/commandlineparser.cpp \
|
||||
cli/startstream.cpp \
|
||||
settings/streamingpreferences.cpp \
|
||||
streaming/input.cpp \
|
||||
streaming/session.cpp \
|
||||
streaming/audio.cpp \
|
||||
streaming/audio/audio.cpp \
|
||||
streaming/audio/renderers/sdlaud.cpp \
|
||||
gui/computermodel.cpp \
|
||||
gui/appmodel.cpp \
|
||||
streaming/streamutils.cpp \
|
||||
backend/autoupdatechecker.cpp
|
||||
backend/autoupdatechecker.cpp \
|
||||
path.cpp \
|
||||
settings/mappingmanager.cpp \
|
||||
gui/sdlgamepadkeynavigation.cpp
|
||||
|
||||
HEADERS += \
|
||||
utils.h \
|
||||
backend/identitymanager.h \
|
||||
backend/nvcomputer.h \
|
||||
backend/nvhttp.h \
|
||||
backend/nvpairingmanager.h \
|
||||
backend/computermanager.h \
|
||||
backend/boxartmanager.h \
|
||||
cli/commandlineparser.h \
|
||||
cli/startstream.h \
|
||||
settings/streamingpreferences.h \
|
||||
streaming/input.hpp \
|
||||
streaming/session.hpp \
|
||||
streaming/input.h \
|
||||
streaming/session.h \
|
||||
streaming/audio/renderers/renderer.h \
|
||||
streaming/audio/renderers/sdl.h \
|
||||
gui/computermodel.h \
|
||||
gui/appmodel.h \
|
||||
streaming/video/decoder.h \
|
||||
streaming/streamutils.h \
|
||||
backend/autoupdatechecker.h
|
||||
backend/autoupdatechecker.h \
|
||||
path.h \
|
||||
settings/mappingmanager.h \
|
||||
gui/sdlgamepadkeynavigation.h
|
||||
|
||||
# Platform-specific renderers and decoders
|
||||
ffmpeg {
|
||||
@@ -113,10 +150,16 @@ ffmpeg {
|
||||
DEFINES += HAVE_FFMPEG
|
||||
SOURCES += \
|
||||
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/nullthreadedvsyncsource.cpp
|
||||
|
||||
|
||||
HEADERS += \
|
||||
streaming/video/ffmpeg.h \
|
||||
streaming/video/ffmpeg-renderers/renderer.h
|
||||
streaming/video/ffmpeg-renderers/renderer.h \
|
||||
streaming/video/ffmpeg-renderers/pacer/pacer.h \
|
||||
streaming/video/ffmpeg-renderers/pacer/nullthreadedvsyncsource.h
|
||||
}
|
||||
libva {
|
||||
message(VAAPI renderer selected)
|
||||
@@ -150,20 +193,38 @@ config_SLVideo {
|
||||
|
||||
DEFINES += HAVE_SLVIDEO
|
||||
LIBS += -lSLVideo
|
||||
|
||||
SOURCES += streaming/video/sl.cpp
|
||||
HEADERS += streaming/video/sl.h
|
||||
}
|
||||
win32 {
|
||||
message(DXVA2 renderer selected)
|
||||
|
||||
SOURCES += streaming/video/ffmpeg-renderers/dxva2.cpp
|
||||
HEADERS += streaming/video/ffmpeg-renderers/dxva2.h
|
||||
SOURCES += \
|
||||
streaming/video/ffmpeg-renderers/dxva2.cpp \
|
||||
streaming/video/ffmpeg-renderers/pacer/dxvsyncsource.cpp
|
||||
|
||||
HEADERS += \
|
||||
streaming/video/ffmpeg-renderers/dxva2.h \
|
||||
streaming/video/ffmpeg-renderers/pacer/dxvsyncsource.h
|
||||
}
|
||||
macx {
|
||||
message(VideoToolbox renderer selected)
|
||||
|
||||
SOURCES += streaming/video/ffmpeg-renderers/vt.mm
|
||||
HEADERS += streaming/video/ffmpeg-renderers/vt.h
|
||||
SOURCES += \
|
||||
streaming/video/ffmpeg-renderers/vt.mm \
|
||||
streaming/video/ffmpeg-renderers/pacer/displaylinkvsyncsource.mm
|
||||
|
||||
HEADERS += \
|
||||
streaming/video/ffmpeg-renderers/vt.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 += \
|
||||
@@ -190,6 +251,26 @@ else:unix: LIBS += -L$$OUT_PWD/../qmdnsengine/ -lqmdnsengine
|
||||
INCLUDEPATH += $$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: {
|
||||
isEmpty(PREFIX) {
|
||||
PREFIX = /usr/local
|
||||
@@ -197,30 +278,42 @@ unix:!macx: {
|
||||
isEmpty(BINDIR) {
|
||||
BINDIR = bin
|
||||
}
|
||||
isEmpty(DATADIR) {
|
||||
DATADIR = share
|
||||
}
|
||||
|
||||
target.path = $$PREFIX/$$BINDIR/
|
||||
|
||||
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.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
|
||||
appdata.path = $$PREFIX/share/metainfo/
|
||||
appstream.files = deploy/linux/com.moonlight_stream.Moonlight.appdata.xml
|
||||
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 {
|
||||
RC_ICONS = moonlight.ico
|
||||
QMAKE_TARGET_COMPANY = Moonlight Game Streaming Project
|
||||
QMAKE_TARGET_DESCRIPTION = Moonlight Game Streaming Client
|
||||
QMAKE_TARGET_PRODUCT = Moonlight
|
||||
|
||||
CONFIG -= embed_manifest_exe
|
||||
QMAKE_LFLAGS += /MANIFEST:embed /MANIFESTINPUT:$${PWD}/Moonlight.exe.manifest
|
||||
}
|
||||
macx {
|
||||
QMAKE_INFO_PLIST = $$PWD/Info.plist
|
||||
|
||||
APP_BUNDLE_RESOURCES.files = moonlight.icns
|
||||
APP_BUNDLE_RESOURCES.files = moonlight.icns SDL_GameControllerDB/gamecontrollerdb.txt
|
||||
APP_BUNDLE_RESOURCES.path = Contents/Resources
|
||||
QMAKE_BUNDLE_DATA += APP_BUNDLE_RESOURCES
|
||||
}
|
||||
|
||||
VERSION = 0.2.0
|
||||
DEFINES += VERSION_STR=\\\"0.2.0\\\"
|
||||
VERSION = 0.6.1
|
||||
DEFINES += VERSION_STR=\\\"0.6.1\\\"
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
#include "boxartmanager.h"
|
||||
#include "../path.h"
|
||||
|
||||
#include <QStandardPaths>
|
||||
#include <QImageReader>
|
||||
#include <QImageWriter>
|
||||
|
||||
BoxArtManager::BoxArtManager(QObject *parent) :
|
||||
QObject(parent),
|
||||
m_BoxArtDir(
|
||||
QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/boxart")
|
||||
m_BoxArtDir(Path::getBoxArtCacheDir()),
|
||||
m_ThreadPool(this)
|
||||
{
|
||||
// 4 is a good balance between fast loading for large
|
||||
// app grids and not crushing GFE with tons of requests
|
||||
// and causing UI jank from constantly stalling to decode
|
||||
// new images.
|
||||
m_ThreadPool.setMaxThreadCount(4);
|
||||
if (!m_BoxArtDir.exists()) {
|
||||
m_BoxArtDir.mkpath(".");
|
||||
}
|
||||
@@ -31,6 +36,39 @@ BoxArtManager::getFilePathForBoxArt(NvComputer* computer, int appId)
|
||||
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)
|
||||
{
|
||||
// Try to open the cached file
|
||||
@@ -42,7 +80,7 @@ QUrl BoxArtManager::loadBoxArt(NvComputer* computer, NvApp& app)
|
||||
// If we get here, we need to fetch asynchronously.
|
||||
// Kick off a worker on our thread pool to do just that.
|
||||
NetworkBoxArtLoadTask* netLoadTask = new NetworkBoxArtLoadTask(this, computer, app);
|
||||
QThreadPool::globalInstance()->start(netLoadTask);
|
||||
m_ThreadPool.start(netLoadTask);
|
||||
|
||||
// Return the placeholder then we can notify the caller
|
||||
// later when the real image is ready.
|
||||
@@ -76,3 +114,5 @@ QUrl BoxArtManager::loadBoxArtFromNetwork(NvComputer* computer, int appId)
|
||||
|
||||
return QUrl();
|
||||
}
|
||||
|
||||
#include "boxartmanager.moc"
|
||||
|
||||
@@ -36,37 +36,5 @@ private:
|
||||
getFilePathForBoxArt(NvComputer* computer, int appId);
|
||||
|
||||
QDir m_BoxArtDir;
|
||||
};
|
||||
|
||||
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;
|
||||
QThreadPool m_ThreadPool;
|
||||
};
|
||||
|
||||
+384
-290
@@ -1,278 +1,143 @@
|
||||
#include "computermanager.h"
|
||||
#include "nvhttp.h"
|
||||
#include "settings/streamingpreferences.h"
|
||||
|
||||
#include <QThread>
|
||||
#include <QUdpSocket>
|
||||
#include <QHostInfo>
|
||||
#include <QThreadPool>
|
||||
|
||||
#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"
|
||||
#define SER_APPID "id"
|
||||
#define SER_APPHDR "hdr"
|
||||
|
||||
NvComputer::NvComputer(QSettings& settings)
|
||||
class PcMonitorThread : public QThread
|
||||
{
|
||||
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();
|
||||
Q_OBJECT
|
||||
|
||||
int appCount = settings.beginReadArray(SER_APPLIST);
|
||||
for (int i = 0; i < appCount; i++) {
|
||||
NvApp app;
|
||||
#define TRIES_BEFORE_OFFLINING 2
|
||||
#define POLLS_PER_APPLIST_FETCH 10
|
||||
|
||||
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);
|
||||
public:
|
||||
PcMonitorThread(NvComputer* computer)
|
||||
: m_Computer(computer)
|
||||
{
|
||||
setObjectName("Polling thread for " + computer->name);
|
||||
}
|
||||
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;
|
||||
}
|
||||
private:
|
||||
bool tryPollComputer(QString address, bool& changed)
|
||||
{
|
||||
NvHTTP http(address);
|
||||
|
||||
void
|
||||
NvComputer::serialize(QSettings& settings)
|
||||
{
|
||||
QReadLocker lock(&this->lock);
|
||||
|
||||
settings.setValue(SER_NAME, name);
|
||||
settings.setValue(SER_UUID, uuid);
|
||||
settings.setValue(SER_MAC, macAddress);
|
||||
settings.setValue(SER_LOCALADDR, localAddress);
|
||||
settings.setValue(SER_REMOTEADDR, remoteAddress);
|
||||
settings.setValue(SER_MANUALADDR, manualAddress);
|
||||
|
||||
// Avoid deleting an existing applist if we couldn't get one
|
||||
if (!appList.isEmpty()) {
|
||||
settings.remove(SER_APPLIST);
|
||||
settings.beginWriteArray(SER_APPLIST);
|
||||
for (int i = 0; i < appList.count(); i++) {
|
||||
settings.setArrayIndex(i);
|
||||
|
||||
settings.setValue(SER_APPNAME, appList[i].name);
|
||||
settings.setValue(SER_APPID, appList[i].id);
|
||||
settings.setValue(SER_APPHDR, appList[i].hdrSupported);
|
||||
QString serverInfo;
|
||||
try {
|
||||
serverInfo = http.getServerInfo(NvHTTP::NvLogLevel::NONE);
|
||||
} catch (...) {
|
||||
return false;
|
||||
}
|
||||
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 newState(address, serverInfo);
|
||||
|
||||
NvComputer::NvComputer(QString address, QString serverInfo)
|
||||
{
|
||||
this->name = NvHTTP::getXmlString(serverInfo, "hostname");
|
||||
if (this->name.isNull()) {
|
||||
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));
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
QString codecSupport = NvHTTP::getXmlString(serverInfo, "ServerCodecModeSupport");
|
||||
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";
|
||||
changed = m_Computer->update(newState);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (macAddress.isNull()) {
|
||||
qWarning() << name << "has no MAC address stored";
|
||||
return false;
|
||||
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;
|
||||
}
|
||||
|
||||
const quint16 WOL_PORTS[] = {
|
||||
7, 9, // Standard WOL ports
|
||||
47998, 47999, 48000, // Ports opened by GFE
|
||||
};
|
||||
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;
|
||||
}
|
||||
|
||||
// Create the WoL payload
|
||||
QByteArray wolPayload;
|
||||
wolPayload.append(QByteArray::fromHex("FFFFFFFFFFFF"));
|
||||
for (int i = 0; i < 16; i++) {
|
||||
wolPayload.append(macAddress);
|
||||
}
|
||||
Q_ASSERT(wolPayload.count() == 102);
|
||||
|
||||
// Add the addresses that we know this host to be
|
||||
// and broadcast addresses for this link just in
|
||||
// case the host has timed out in ARP entries.
|
||||
QVector<QString> addressList = uniqueAddresses();
|
||||
addressList.append("255.255.255.255");
|
||||
|
||||
// Try all unique address strings or host names
|
||||
bool success = false;
|
||||
for (QString& addressString : addressList) {
|
||||
QHostInfo hostInfo = QHostInfo::fromName(addressString);
|
||||
|
||||
// Try all IP addresses that this string resolves to
|
||||
for (QHostAddress& address : hostInfo.addresses()) {
|
||||
QUdpSocket sock;
|
||||
|
||||
// Bind to any address on the correct protocol
|
||||
if (sock.bind(address.protocol() == QUdpSocket::IPv4Protocol ?
|
||||
QHostAddress::AnyIPv4 : QHostAddress::AnyIPv6)) {
|
||||
|
||||
// Send to all ports
|
||||
for (quint16 port : WOL_PORTS) {
|
||||
if (sock.writeDatagram(wolPayload, address, port)) {
|
||||
qInfo().nospace().noquote() << "Send WoL packet to " << name << " via " << address.toString() << ":" << port;
|
||||
success = true;
|
||||
if (tryPollComputer(address, stateChanged)) {
|
||||
if (!wasOnline) {
|
||||
qInfo() << m_Computer->name << "is now online at" << m_Computer->activeAddress;
|
||||
}
|
||||
online = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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[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--;
|
||||
// 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)) {
|
||||
// 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
|
||||
Q_ASSERT(!uniqueAddressList.isEmpty());
|
||||
signals:
|
||||
void computerStateChanged(NvComputer* computer);
|
||||
|
||||
return uniqueAddressList;
|
||||
}
|
||||
|
||||
bool NvComputer::update(NvComputer& that)
|
||||
{
|
||||
bool changed = false;
|
||||
|
||||
// Lock us for write and them for read
|
||||
QWriteLocker thisLock(&this->lock);
|
||||
QReadLocker thatLock(&that.lock);
|
||||
|
||||
// UUID may not change or we're talking to a new PC
|
||||
Q_ASSERT(this->uuid == that.uuid);
|
||||
|
||||
#define ASSIGN_IF_CHANGED(field) \
|
||||
if (this->field != that.field) { \
|
||||
this->field = that.field; \
|
||||
changed = true; \
|
||||
}
|
||||
|
||||
#define ASSIGN_IF_CHANGED_AND_NONEMPTY(field) \
|
||||
if (!that.field.isEmpty() && \
|
||||
this->field != that.field) { \
|
||||
this->field = that.field; \
|
||||
changed = true; \
|
||||
}
|
||||
|
||||
ASSIGN_IF_CHANGED(name);
|
||||
ASSIGN_IF_CHANGED_AND_NONEMPTY(macAddress);
|
||||
ASSIGN_IF_CHANGED_AND_NONEMPTY(localAddress);
|
||||
ASSIGN_IF_CHANGED_AND_NONEMPTY(remoteAddress);
|
||||
ASSIGN_IF_CHANGED_AND_NONEMPTY(manualAddress);
|
||||
ASSIGN_IF_CHANGED(pairState);
|
||||
ASSIGN_IF_CHANGED(serverCodecModeSupport);
|
||||
ASSIGN_IF_CHANGED(currentGameId);
|
||||
ASSIGN_IF_CHANGED(activeAddress);
|
||||
ASSIGN_IF_CHANGED(state);
|
||||
ASSIGN_IF_CHANGED(gfeVersion);
|
||||
ASSIGN_IF_CHANGED(appVersion);
|
||||
ASSIGN_IF_CHANGED(maxLumaPixelsHEVC);
|
||||
ASSIGN_IF_CHANGED(gpuModel);
|
||||
ASSIGN_IF_CHANGED_AND_NONEMPTY(appList);
|
||||
ASSIGN_IF_CHANGED_AND_NONEMPTY(displayModes);
|
||||
return changed;
|
||||
}
|
||||
private:
|
||||
NvComputer* m_Computer;
|
||||
};
|
||||
|
||||
ComputerManager::ComputerManager(QObject *parent)
|
||||
: QObject(parent),
|
||||
@@ -363,17 +228,24 @@ void ComputerManager::startPolling()
|
||||
return;
|
||||
}
|
||||
|
||||
// Start an MDNS query for GameStream hosts
|
||||
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();
|
||||
StreamingPreferences prefs;
|
||||
|
||||
MdnsPendingComputer* pendingComputer = new MdnsPendingComputer(&m_MdnsServer, &m_MdnsCache, service);
|
||||
connect(pendingComputer, SIGNAL(resolvedv4(MdnsPendingComputer*,QHostAddress)),
|
||||
this, SLOT(handleMdnsServiceResolved(MdnsPendingComputer*,QHostAddress)));
|
||||
m_PendingResolution.append(pendingComputer);
|
||||
});
|
||||
if (prefs.enableMdns) {
|
||||
// Start an MDNS query for GameStream hosts
|
||||
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);
|
||||
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
|
||||
QMapIterator<QString, NvComputer*> i(m_KnownHosts);
|
||||
@@ -383,6 +255,25 @@ void ComputerManager::startPolling()
|
||||
}
|
||||
}
|
||||
|
||||
// Must hold m_Lock for write
|
||||
void ComputerManager::startPollingComputer(NvComputer* computer)
|
||||
{
|
||||
if (m_PollingRef == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_PollThreads.contains(computer->uuid)) {
|
||||
Q_ASSERT(m_PollThreads[computer->uuid]->isRunning());
|
||||
return;
|
||||
}
|
||||
|
||||
PcMonitorThread* thread = new PcMonitorThread(computer);
|
||||
connect(thread, SIGNAL(computerStateChanged(NvComputer*)),
|
||||
this, SLOT(handleComputerStateChanged(NvComputer*)));
|
||||
m_PollThreads[computer->uuid] = thread;
|
||||
thread->start();
|
||||
}
|
||||
|
||||
void ComputerManager::handleMdnsServiceResolved(MdnsPendingComputer* computer,
|
||||
const QHostAddress& address)
|
||||
{
|
||||
@@ -394,6 +285,19 @@ void ComputerManager::handleMdnsServiceResolved(MdnsPendingComputer* computer,
|
||||
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()
|
||||
{
|
||||
QReadLocker lock(&m_Lock);
|
||||
@@ -410,9 +314,23 @@ public:
|
||||
|
||||
void run()
|
||||
{
|
||||
QWriteLocker lock(&m_ComputerManager->m_Lock);
|
||||
QThread* pollingThread;
|
||||
|
||||
QThread* pollingThread = m_ComputerManager->m_PollThreads[m_Computer->uuid];
|
||||
// Only do the minimum amount of work while holding the writer lock.
|
||||
// We must release it before calling saveHosts().
|
||||
{
|
||||
QWriteLocker lock(&m_ComputerManager->m_Lock);
|
||||
|
||||
pollingThread = m_ComputerManager->m_PollThreads[m_Computer->uuid];
|
||||
|
||||
m_ComputerManager->m_PollThreads.remove(m_Computer->uuid);
|
||||
m_ComputerManager->m_KnownHosts.remove(m_Computer->uuid);
|
||||
}
|
||||
|
||||
// Persist the new host list
|
||||
m_ComputerManager->saveHosts();
|
||||
|
||||
// Delete the polling thread
|
||||
if (pollingThread != nullptr) {
|
||||
pollingThread->requestInterruption();
|
||||
|
||||
@@ -425,9 +343,8 @@ public:
|
||||
delete pollingThread;
|
||||
}
|
||||
|
||||
m_ComputerManager->m_PollThreads.remove(m_Computer->uuid);
|
||||
m_ComputerManager->m_KnownHosts.remove(m_Computer->uuid);
|
||||
|
||||
// Finally, delete the computer itself. This must be done
|
||||
// last because the polling thread might be using it.
|
||||
delete m_Computer;
|
||||
}
|
||||
|
||||
@@ -443,6 +360,55 @@ void ComputerManager::deleteHost(NvComputer* computer)
|
||||
QThreadPool::globalInstance()->start(new DeferredHostDeletionTask(this, computer));
|
||||
}
|
||||
|
||||
class PendingPairingTask : public QObject, public QRunnable
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
PendingPairingTask(ComputerManager* computerManager, NvComputer* computer, QString pin)
|
||||
: m_Computer(computer),
|
||||
m_Pin(pin)
|
||||
{
|
||||
connect(this, &PendingPairingTask::pairingCompleted,
|
||||
computerManager, &ComputerManager::pairingCompleted);
|
||||
}
|
||||
|
||||
signals:
|
||||
void pairingCompleted(NvComputer* computer, QString error);
|
||||
|
||||
private:
|
||||
void run()
|
||||
{
|
||||
NvPairingManager pairingManager(m_Computer->activeAddress);
|
||||
|
||||
try {
|
||||
NvPairingManager::PairState result = pairingManager.pair(m_Computer->appVersion, m_Pin);
|
||||
switch (result)
|
||||
{
|
||||
case NvPairingManager::PairState::PIN_WRONG:
|
||||
emit pairingCompleted(m_Computer, "The PIN from the PC didn't match. Please try again.");
|
||||
break;
|
||||
case NvPairingManager::PairState::FAILED:
|
||||
emit pairingCompleted(m_Computer, "Pairing failed. Please try again.");
|
||||
break;
|
||||
case NvPairingManager::PairState::ALREADY_IN_PROGRESS:
|
||||
emit pairingCompleted(m_Computer, "Another pairing attempt is already in progress.");
|
||||
break;
|
||||
case NvPairingManager::PairState::PAIRED:
|
||||
emit pairingCompleted(m_Computer, nullptr);
|
||||
break;
|
||||
}
|
||||
} catch (const GfeHttpResponseException& e) {
|
||||
emit pairingCompleted(m_Computer, e.toQString());
|
||||
} catch (const QtNetworkReplyException& e) {
|
||||
emit pairingCompleted(m_Computer, e.toQString());
|
||||
}
|
||||
}
|
||||
|
||||
NvComputer* m_Computer;
|
||||
QString m_Pin;
|
||||
};
|
||||
|
||||
void ComputerManager::pairHost(NvComputer* computer, QString pin)
|
||||
{
|
||||
// Punt to a worker thread to avoid stalling the
|
||||
@@ -451,6 +417,55 @@ void ComputerManager::pairHost(NvComputer* computer, QString pin)
|
||||
QThreadPool::globalInstance()->start(pairing);
|
||||
}
|
||||
|
||||
class PendingQuitTask : public QObject, public QRunnable
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
PendingQuitTask(ComputerManager* computerManager, NvComputer* computer)
|
||||
: m_Computer(computer)
|
||||
{
|
||||
connect(this, &PendingQuitTask::quitAppFailed,
|
||||
computerManager, &ComputerManager::quitAppCompleted);
|
||||
}
|
||||
|
||||
signals:
|
||||
void quitAppFailed(QString error);
|
||||
|
||||
private:
|
||||
void run()
|
||||
{
|
||||
NvHTTP http(m_Computer->activeAddress);
|
||||
|
||||
try {
|
||||
if (m_Computer->currentGameId != 0) {
|
||||
http.quitApp();
|
||||
}
|
||||
} catch (const GfeHttpResponseException& e) {
|
||||
{
|
||||
QWriteLocker lock(&m_Computer->lock);
|
||||
m_Computer->pendingQuit = false;
|
||||
}
|
||||
if (e.getStatusCode() == 599) {
|
||||
// 599 is a special code we make a custom message for
|
||||
emit quitAppFailed("The running game wasn't started by this PC. "
|
||||
"You must quit the game on the host PC manually or use the device that originally started the game.");
|
||||
}
|
||||
else {
|
||||
emit quitAppFailed(e.toQString());
|
||||
}
|
||||
} catch (const QtNetworkReplyException& e) {
|
||||
{
|
||||
QWriteLocker lock(&m_Computer->lock);
|
||||
m_Computer->pendingQuit = false;
|
||||
}
|
||||
emit quitAppFailed(e.toQString());
|
||||
}
|
||||
}
|
||||
|
||||
NvComputer* m_Computer;
|
||||
};
|
||||
|
||||
void ComputerManager::quitRunningApp(NvComputer* computer)
|
||||
{
|
||||
QWriteLocker lock(&computer->lock);
|
||||
@@ -498,6 +513,118 @@ void ComputerManager::stopPollingAsync()
|
||||
}
|
||||
}
|
||||
|
||||
class PendingAddTask : public QObject, public QRunnable
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
PendingAddTask(ComputerManager* computerManager, QString address, bool mdns)
|
||||
: m_ComputerManager(computerManager),
|
||||
m_Address(address),
|
||||
m_Mdns(mdns)
|
||||
{
|
||||
connect(this, &PendingAddTask::computerAddCompleted,
|
||||
computerManager, &ComputerManager::computerAddCompleted);
|
||||
connect(this, &PendingAddTask::computerStateChanged,
|
||||
computerManager, &ComputerManager::handleComputerStateChanged);
|
||||
}
|
||||
|
||||
signals:
|
||||
void computerAddCompleted(QVariant success);
|
||||
|
||||
void computerStateChanged(NvComputer* computer);
|
||||
|
||||
private:
|
||||
void run()
|
||||
{
|
||||
NvHTTP http(m_Address);
|
||||
|
||||
qInfo() << "Processing new PC at" << m_Address << "from" << (m_Mdns ? "mDNS" : "user");
|
||||
|
||||
QString serverInfo;
|
||||
try {
|
||||
// There's a race condition between GameStream servers reporting presence over
|
||||
// mDNS and the HTTPS server being ready to respond to our queries. To work
|
||||
// around this issue, we will issue the request again after a few seconds if
|
||||
// we see a ServiceUnavailableError error.
|
||||
try {
|
||||
serverInfo = http.getServerInfo(NvHTTP::NvLogLevel::VERBOSE);
|
||||
} catch (const QtNetworkReplyException& e) {
|
||||
if (e.getError() == QNetworkReply::ServiceUnavailableError) {
|
||||
qWarning() << "Retrying request in 5 seconds after ServiceUnavailableError";
|
||||
QThread::sleep(5);
|
||||
serverInfo = http.getServerInfo(NvHTTP::NvLogLevel::VERBOSE);
|
||||
qInfo() << "Retry successful";
|
||||
}
|
||||
else {
|
||||
// Rethrow other errors
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
} catch (...) {
|
||||
if (!m_Mdns) {
|
||||
emit computerAddCompleted(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
NvComputer* newComputer = new NvComputer(m_Address, serverInfo);
|
||||
|
||||
// Update addresses depending on the context
|
||||
if (m_Mdns) {
|
||||
newComputer->localAddress = m_Address;
|
||||
}
|
||||
else {
|
||||
newComputer->manualAddress = m_Address;
|
||||
}
|
||||
|
||||
// Check if this PC already exists
|
||||
QWriteLocker lock(&m_ComputerManager->m_Lock);
|
||||
NvComputer* existingComputer = m_ComputerManager->m_KnownHosts[newComputer->uuid];
|
||||
if (existingComputer != nullptr) {
|
||||
// Fold it into the existing PC
|
||||
bool changed = existingComputer->update(*newComputer);
|
||||
delete newComputer;
|
||||
|
||||
// Drop the lock before notifying
|
||||
lock.unlock();
|
||||
|
||||
// For non-mDNS clients, let them know it succeeded
|
||||
if (!m_Mdns) {
|
||||
emit computerAddCompleted(true);
|
||||
}
|
||||
|
||||
// Tell our client if something changed
|
||||
if (changed) {
|
||||
qInfo() << existingComputer->name << "is now at" << existingComputer->activeAddress;
|
||||
emit computerStateChanged(existingComputer);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Store this in our active sets
|
||||
m_ComputerManager->m_KnownHosts[newComputer->uuid] = newComputer;
|
||||
|
||||
// Start polling if enabled (write lock required)
|
||||
m_ComputerManager->startPollingComputer(newComputer);
|
||||
|
||||
// Drop the lock before notifying
|
||||
lock.unlock();
|
||||
|
||||
// For non-mDNS clients, let them know it succeeded
|
||||
if (!m_Mdns) {
|
||||
emit computerAddCompleted(true);
|
||||
}
|
||||
|
||||
// Tell our client about this new PC
|
||||
emit computerStateChanged(newComputer);
|
||||
}
|
||||
}
|
||||
|
||||
ComputerManager* m_ComputerManager;
|
||||
QString m_Address;
|
||||
bool m_Mdns;
|
||||
};
|
||||
|
||||
void ComputerManager::addNewHost(QString address, bool mdns)
|
||||
{
|
||||
// Punt to a worker thread to avoid stalling the
|
||||
@@ -506,37 +633,4 @@ void ComputerManager::addNewHost(QString address, bool mdns)
|
||||
QThreadPool::globalInstance()->start(addTask);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
#include "computermanager.moc"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
#include "nvhttp.h"
|
||||
|
||||
#include "nvcomputer.h"
|
||||
#include "nvpairingmanager.h"
|
||||
|
||||
#include <qmdnsengine/server.h>
|
||||
@@ -13,195 +14,6 @@
|
||||
#include <QSettings>
|
||||
#include <QRunnable>
|
||||
|
||||
class NvComputer
|
||||
{
|
||||
friend class PcMonitorThread;
|
||||
friend class ComputerManager;
|
||||
friend class PendingQuitTask;
|
||||
|
||||
private:
|
||||
void sortAppList();
|
||||
|
||||
bool pendingQuit;
|
||||
|
||||
public:
|
||||
explicit NvComputer(QString address, QString serverInfo);
|
||||
|
||||
explicit NvComputer(QSettings& settings);
|
||||
|
||||
bool
|
||||
update(NvComputer& that);
|
||||
|
||||
bool
|
||||
wake();
|
||||
|
||||
QVector<QString>
|
||||
uniqueAddresses();
|
||||
|
||||
void
|
||||
serialize(QSettings& settings);
|
||||
|
||||
enum PairState
|
||||
{
|
||||
PS_UNKNOWN,
|
||||
PS_PAIRED,
|
||||
PS_NOT_PAIRED
|
||||
};
|
||||
|
||||
enum ComputerState
|
||||
{
|
||||
CS_UNKNOWN,
|
||||
CS_ONLINE,
|
||||
CS_OFFLINE
|
||||
};
|
||||
|
||||
// Ephemeral traits
|
||||
ComputerState state;
|
||||
PairState pairState;
|
||||
QString activeAddress;
|
||||
int currentGameId;
|
||||
QString gfeVersion;
|
||||
QString appVersion;
|
||||
QVector<NvDisplayMode> displayModes;
|
||||
int maxLumaPixelsHEVC;
|
||||
int serverCodecModeSupport;
|
||||
QString gpuModel;
|
||||
|
||||
// Persisted traits
|
||||
QString localAddress;
|
||||
QString remoteAddress;
|
||||
QString manualAddress;
|
||||
QByteArray macAddress;
|
||||
QString name;
|
||||
QString uuid;
|
||||
QVector<NvApp> appList;
|
||||
|
||||
// Synchronization
|
||||
QReadWriteLock lock;
|
||||
};
|
||||
|
||||
// FIXME: MOC isn't finding Q_OBJECT properly when this is confined
|
||||
// to computermanager.cpp as it should be.
|
||||
class PcMonitorThread : public QThread
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
#define TRIES_BEFORE_OFFLINING 2
|
||||
#define POLLS_PER_APPLIST_FETCH 10
|
||||
|
||||
public:
|
||||
PcMonitorThread(NvComputer* computer)
|
||||
: m_Computer(computer)
|
||||
{
|
||||
setObjectName("Polling thread for " + computer->name);
|
||||
}
|
||||
|
||||
private:
|
||||
bool tryPollComputer(QString address, bool& changed)
|
||||
{
|
||||
NvHTTP http(address);
|
||||
|
||||
QString serverInfo;
|
||||
try {
|
||||
serverInfo = http.getServerInfo();
|
||||
} catch (...) {
|
||||
return false;
|
||||
}
|
||||
|
||||
NvComputer newState(address, serverInfo);
|
||||
|
||||
// Ensure the machine that responded is the one we intended to contact
|
||||
if (m_Computer->uuid != newState.uuid) {
|
||||
qInfo() << "Found unexpected PC " << newState.name << " looking for " << m_Computer->name;
|
||||
return false;
|
||||
}
|
||||
|
||||
changed = m_Computer->update(newState);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool updateAppList(bool& changed)
|
||||
{
|
||||
Q_ASSERT(m_Computer->activeAddress != nullptr);
|
||||
|
||||
NvHTTP http(m_Computer->activeAddress);
|
||||
|
||||
QVector<NvApp> appList;
|
||||
|
||||
try {
|
||||
appList = http.getAppList();
|
||||
if (appList.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
} catch (...) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QWriteLocker lock(&m_Computer->lock);
|
||||
if (m_Computer->appList != appList) {
|
||||
m_Computer->appList = appList;
|
||||
m_Computer->sortAppList();
|
||||
changed = true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void run() override
|
||||
{
|
||||
// Always fetch the applist the first time
|
||||
int pollsSinceLastAppListFetch = POLLS_PER_APPLIST_FETCH;
|
||||
while (!isInterruptionRequested()) {
|
||||
bool stateChanged = false;
|
||||
bool online = false;
|
||||
for (int i = 0; i < TRIES_BEFORE_OFFLINING && !online; i++) {
|
||||
for (auto& address : m_Computer->uniqueAddresses()) {
|
||||
if (isInterruptionRequested()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (tryPollComputer(address, stateChanged)) {
|
||||
online = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we failed after all retry attempts
|
||||
// Note: we don't need to acquire the read lock here,
|
||||
// because we're on the writing thread.
|
||||
if (!online && m_Computer->state != NvComputer::CS_OFFLINE) {
|
||||
m_Computer->state = NvComputer::CS_OFFLINE;
|
||||
stateChanged = true;
|
||||
}
|
||||
|
||||
// Grab the applist if it's empty or it's been long enough that we need to refresh
|
||||
pollsSinceLastAppListFetch++;
|
||||
if (m_Computer->state == NvComputer::CS_ONLINE &&
|
||||
m_Computer->pairState == NvComputer::PS_PAIRED &&
|
||||
(m_Computer->appList.isEmpty() || pollsSinceLastAppListFetch >= POLLS_PER_APPLIST_FETCH)) {
|
||||
if (updateAppList(stateChanged)) {
|
||||
pollsSinceLastAppListFetch = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (stateChanged) {
|
||||
// Tell anyone listening that we've changed state
|
||||
emit computerStateChanged(m_Computer);
|
||||
}
|
||||
|
||||
// Wait a bit to poll again
|
||||
QThread::sleep(3);
|
||||
}
|
||||
}
|
||||
|
||||
signals:
|
||||
void computerStateChanged(NvComputer* computer);
|
||||
|
||||
private:
|
||||
NvComputer* m_Computer;
|
||||
};
|
||||
|
||||
class MdnsPendingComputer : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
@@ -294,185 +106,3 @@ private:
|
||||
QMdnsEngine::Cache m_MdnsCache;
|
||||
QVector<MdnsPendingComputer*> m_PendingResolution;
|
||||
};
|
||||
|
||||
class PendingPairingTask : public QObject, public QRunnable
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
PendingPairingTask(ComputerManager* computerManager, NvComputer* computer, QString pin)
|
||||
: m_Computer(computer),
|
||||
m_Pin(pin)
|
||||
{
|
||||
connect(this, &PendingPairingTask::pairingCompleted,
|
||||
computerManager, &ComputerManager::pairingCompleted);
|
||||
}
|
||||
|
||||
signals:
|
||||
void pairingCompleted(NvComputer* computer, QString error);
|
||||
|
||||
private:
|
||||
void run()
|
||||
{
|
||||
NvPairingManager pairingManager(m_Computer->activeAddress);
|
||||
|
||||
try {
|
||||
NvPairingManager::PairState result = pairingManager.pair(m_Computer->appVersion, m_Pin);
|
||||
switch (result)
|
||||
{
|
||||
case NvPairingManager::PairState::PIN_WRONG:
|
||||
emit pairingCompleted(m_Computer, "The PIN from the PC didn't match. Please try again.");
|
||||
break;
|
||||
case NvPairingManager::PairState::FAILED:
|
||||
emit pairingCompleted(m_Computer, "Pairing failed. Please try again.");
|
||||
break;
|
||||
case NvPairingManager::PairState::ALREADY_IN_PROGRESS:
|
||||
emit pairingCompleted(m_Computer, "Another pairing attempt is already in progress.");
|
||||
break;
|
||||
case NvPairingManager::PairState::PAIRED:
|
||||
emit pairingCompleted(m_Computer, nullptr);
|
||||
break;
|
||||
}
|
||||
} catch (const GfeHttpResponseException& e) {
|
||||
emit pairingCompleted(m_Computer, e.toQString());
|
||||
}
|
||||
}
|
||||
|
||||
NvComputer* m_Computer;
|
||||
QString m_Pin;
|
||||
};
|
||||
|
||||
class PendingQuitTask : public QObject, public QRunnable
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
PendingQuitTask(ComputerManager* computerManager, NvComputer* computer)
|
||||
: m_Computer(computer)
|
||||
{
|
||||
connect(this, &PendingQuitTask::quitAppFailed,
|
||||
computerManager, &ComputerManager::quitAppCompleted);
|
||||
}
|
||||
|
||||
signals:
|
||||
void quitAppFailed(QString error);
|
||||
|
||||
private:
|
||||
void run()
|
||||
{
|
||||
NvHTTP http(m_Computer->activeAddress);
|
||||
|
||||
try {
|
||||
if (m_Computer->currentGameId != 0) {
|
||||
http.quitApp();
|
||||
}
|
||||
} catch (const GfeHttpResponseException& e) {
|
||||
{
|
||||
QWriteLocker lock(&m_Computer->lock);
|
||||
m_Computer->pendingQuit = false;
|
||||
}
|
||||
if (e.getStatusCode() == 599) {
|
||||
// 599 is a special code we make a custom message for
|
||||
emit quitAppFailed("The running game wasn't started by this PC. "
|
||||
"You must quit the game on the host PC manually or use the device that originally started the game.");
|
||||
}
|
||||
else {
|
||||
emit quitAppFailed(e.toQString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NvComputer* m_Computer;
|
||||
};
|
||||
|
||||
class PendingAddTask : public QObject, public QRunnable
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
PendingAddTask(ComputerManager* computerManager, QString address, bool mdns)
|
||||
: m_ComputerManager(computerManager),
|
||||
m_Address(address),
|
||||
m_Mdns(mdns)
|
||||
{
|
||||
connect(this, &PendingAddTask::computerAddCompleted,
|
||||
computerManager, &ComputerManager::computerAddCompleted);
|
||||
connect(this, &PendingAddTask::computerStateChanged,
|
||||
computerManager, &ComputerManager::handleComputerStateChanged);
|
||||
}
|
||||
|
||||
signals:
|
||||
void computerAddCompleted(QVariant success);
|
||||
|
||||
void computerStateChanged(NvComputer* computer);
|
||||
|
||||
private:
|
||||
void run()
|
||||
{
|
||||
NvHTTP http(m_Address);
|
||||
|
||||
QString serverInfo;
|
||||
try {
|
||||
serverInfo = http.getServerInfo();
|
||||
} catch (...) {
|
||||
if (!m_Mdns) {
|
||||
emit computerAddCompleted(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
NvComputer* newComputer = new NvComputer(m_Address, serverInfo);
|
||||
|
||||
// Update addresses depending on the context
|
||||
if (m_Mdns) {
|
||||
newComputer->localAddress = m_Address;
|
||||
}
|
||||
else {
|
||||
newComputer->manualAddress = m_Address;
|
||||
}
|
||||
|
||||
// Check if this PC already exists
|
||||
QWriteLocker lock(&m_ComputerManager->m_Lock);
|
||||
NvComputer* existingComputer = m_ComputerManager->m_KnownHosts[newComputer->uuid];
|
||||
if (existingComputer != nullptr) {
|
||||
// Fold it into the existing PC
|
||||
bool changed = existingComputer->update(*newComputer);
|
||||
delete newComputer;
|
||||
|
||||
// Drop the lock before notifying
|
||||
lock.unlock();
|
||||
|
||||
// For non-mDNS clients, let them know it succeeded
|
||||
if (!m_Mdns) {
|
||||
emit computerAddCompleted(true);
|
||||
}
|
||||
|
||||
// Tell our client if something changed
|
||||
if (changed) {
|
||||
emit computerStateChanged(existingComputer);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Store this in our active sets
|
||||
m_ComputerManager->m_KnownHosts[newComputer->uuid] = newComputer;
|
||||
|
||||
// Start polling if enabled (write lock required)
|
||||
m_ComputerManager->startPollingComputer(newComputer);
|
||||
|
||||
// Drop the lock before notifying
|
||||
lock.unlock();
|
||||
|
||||
// For non-mDNS clients, let them know it succeeded
|
||||
if (!m_Mdns) {
|
||||
emit computerAddCompleted(true);
|
||||
}
|
||||
|
||||
// Tell our client about this new PC
|
||||
emit computerStateChanged(newComputer);
|
||||
}
|
||||
}
|
||||
|
||||
ComputerManager* m_ComputerManager;
|
||||
QString m_Address;
|
||||
bool m_Mdns;
|
||||
};
|
||||
|
||||
@@ -185,12 +185,12 @@ IdentityManager::getSslConfig()
|
||||
QString
|
||||
IdentityManager::getUniqueId()
|
||||
{
|
||||
if (m_CachedUniqueId.isNull()) {
|
||||
if (m_CachedUniqueId.isEmpty()) {
|
||||
QSettings settings;
|
||||
|
||||
// Load the unique ID from settings
|
||||
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;
|
||||
}
|
||||
else {
|
||||
|
||||
@@ -0,0 +1,275 @@
|
||||
#include "nvcomputer.h"
|
||||
|
||||
#include <QUdpSocket>
|
||||
#include <QHostInfo>
|
||||
|
||||
#define SER_NAME "hostname"
|
||||
#define SER_UUID "uuid"
|
||||
#define SER_MAC "mac"
|
||||
#define SER_LOCALADDR "localaddress"
|
||||
#define SER_REMOTEADDR "remoteaddress"
|
||||
#define SER_MANUALADDR "manualaddress"
|
||||
#define SER_APPLIST "apps"
|
||||
|
||||
#define SER_APPNAME "name"
|
||||
#define SER_APPID "id"
|
||||
#define SER_APPHDR "hdr"
|
||||
|
||||
NvComputer::NvComputer(QSettings& settings)
|
||||
{
|
||||
this->name = settings.value(SER_NAME).toString();
|
||||
this->uuid = settings.value(SER_UUID).toString();
|
||||
this->macAddress = settings.value(SER_MAC).toByteArray();
|
||||
this->localAddress = settings.value(SER_LOCALADDR).toString();
|
||||
this->remoteAddress = settings.value(SER_REMOTEADDR).toString();
|
||||
this->manualAddress = settings.value(SER_MANUALADDR).toString();
|
||||
|
||||
int appCount = settings.beginReadArray(SER_APPLIST);
|
||||
for (int i = 0; i < appCount; i++) {
|
||||
NvApp app;
|
||||
|
||||
settings.setArrayIndex(i);
|
||||
|
||||
app.name = settings.value(SER_APPNAME).toString();
|
||||
app.id = settings.value(SER_APPID).toInt();
|
||||
app.hdrSupported = settings.value(SER_APPHDR).toBool();
|
||||
|
||||
this->appList.append(app);
|
||||
}
|
||||
settings.endArray();
|
||||
sortAppList();
|
||||
|
||||
this->activeAddress = nullptr;
|
||||
this->currentGameId = 0;
|
||||
this->pairState = PS_UNKNOWN;
|
||||
this->state = CS_UNKNOWN;
|
||||
this->gfeVersion = nullptr;
|
||||
this->appVersion = nullptr;
|
||||
this->maxLumaPixelsHEVC = 0;
|
||||
this->serverCodecModeSupport = 0;
|
||||
this->pendingQuit = false;
|
||||
this->gpuModel = nullptr;
|
||||
}
|
||||
|
||||
void NvComputer::serialize(QSettings& settings)
|
||||
{
|
||||
QReadLocker lock(&this->lock);
|
||||
|
||||
settings.setValue(SER_NAME, name);
|
||||
settings.setValue(SER_UUID, uuid);
|
||||
settings.setValue(SER_MAC, macAddress);
|
||||
settings.setValue(SER_LOCALADDR, localAddress);
|
||||
settings.setValue(SER_REMOTEADDR, remoteAddress);
|
||||
settings.setValue(SER_MANUALADDR, manualAddress);
|
||||
|
||||
// Avoid deleting an existing applist if we couldn't get one
|
||||
if (!appList.isEmpty()) {
|
||||
settings.remove(SER_APPLIST);
|
||||
settings.beginWriteArray(SER_APPLIST);
|
||||
for (int i = 0; i < appList.count(); i++) {
|
||||
settings.setArrayIndex(i);
|
||||
|
||||
settings.setValue(SER_APPNAME, appList[i].name);
|
||||
settings.setValue(SER_APPID, appList[i].id);
|
||||
settings.setValue(SER_APPHDR, appList[i].hdrSupported);
|
||||
}
|
||||
settings.endArray();
|
||||
}
|
||||
}
|
||||
|
||||
void NvComputer::sortAppList()
|
||||
{
|
||||
std::stable_sort(appList.begin(), appList.end(), [](const NvApp& app1, const NvApp& app2) {
|
||||
return app1.name.toLower() < app2.name.toLower();
|
||||
});
|
||||
}
|
||||
|
||||
NvComputer::NvComputer(QString address, QString serverInfo)
|
||||
{
|
||||
this->name = NvHTTP::getXmlString(serverInfo, "hostname");
|
||||
if (this->name.isEmpty()) {
|
||||
this->name = "UNKNOWN";
|
||||
}
|
||||
|
||||
this->uuid = NvHTTP::getXmlString(serverInfo, "uniqueid");
|
||||
QString newMacString = NvHTTP::getXmlString(serverInfo, "mac");
|
||||
if (newMacString != "00:00:00:00:00:00") {
|
||||
QStringList macOctets = newMacString.split(':');
|
||||
for (QString macOctet : macOctets) {
|
||||
this->macAddress.append((char) macOctet.toInt(nullptr, 16));
|
||||
}
|
||||
}
|
||||
|
||||
QString codecSupport = NvHTTP::getXmlString(serverInfo, "ServerCodecModeSupport");
|
||||
if (!codecSupport.isEmpty()) {
|
||||
this->serverCodecModeSupport = codecSupport.toInt();
|
||||
}
|
||||
else {
|
||||
this->serverCodecModeSupport = 0;
|
||||
}
|
||||
|
||||
QString maxLumaPixelsHEVC = NvHTTP::getXmlString(serverInfo, "MaxLumaPixelsHEVC");
|
||||
if (!maxLumaPixelsHEVC.isEmpty()) {
|
||||
this->maxLumaPixelsHEVC = maxLumaPixelsHEVC.toInt();
|
||||
}
|
||||
else {
|
||||
this->maxLumaPixelsHEVC = 0;
|
||||
}
|
||||
|
||||
this->displayModes = NvHTTP::getDisplayModeList(serverInfo);
|
||||
std::stable_sort(this->displayModes.begin(), this->displayModes.end(),
|
||||
[](const NvDisplayMode& mode1, const NvDisplayMode& mode2) {
|
||||
return mode1.width * mode1.height * mode1.refreshRate <
|
||||
mode2.width * mode2.height * mode2.refreshRate;
|
||||
});
|
||||
|
||||
this->localAddress = NvHTTP::getXmlString(serverInfo, "LocalIP");
|
||||
this->remoteAddress = NvHTTP::getXmlString(serverInfo, "ExternalIP");
|
||||
this->pairState = NvHTTP::getXmlString(serverInfo, "PairStatus") == "1" ?
|
||||
PS_PAIRED : PS_NOT_PAIRED;
|
||||
this->currentGameId = NvHTTP::getCurrentGame(serverInfo);
|
||||
this->appVersion = NvHTTP::getXmlString(serverInfo, "appversion");
|
||||
this->gfeVersion = NvHTTP::getXmlString(serverInfo, "GfeVersion");
|
||||
this->gpuModel = NvHTTP::getXmlString(serverInfo, "gputype");
|
||||
this->activeAddress = address;
|
||||
this->state = NvComputer::CS_ONLINE;
|
||||
this->pendingQuit = false;
|
||||
}
|
||||
|
||||
bool NvComputer::wake()
|
||||
{
|
||||
if (state == NvComputer::CS_ONLINE) {
|
||||
qWarning() << name << "is already online";
|
||||
return true;
|
||||
}
|
||||
|
||||
if (macAddress.isEmpty()) {
|
||||
qWarning() << name << "has no MAC address stored";
|
||||
return false;
|
||||
}
|
||||
|
||||
const quint16 WOL_PORTS[] = {
|
||||
7, 9, // Standard WOL ports
|
||||
47998, 47999, 48000, // Ports opened by GFE
|
||||
};
|
||||
|
||||
// Create the WoL payload
|
||||
QByteArray wolPayload;
|
||||
wolPayload.append(QByteArray::fromHex("FFFFFFFFFFFF"));
|
||||
for (int i = 0; i < 16; i++) {
|
||||
wolPayload.append(macAddress);
|
||||
}
|
||||
Q_ASSERT(wolPayload.count() == 102);
|
||||
|
||||
// Add the addresses that we know this host to be
|
||||
// and broadcast addresses for this link just in
|
||||
// case the host has timed out in ARP entries.
|
||||
QVector<QString> addressList = uniqueAddresses();
|
||||
addressList.append("255.255.255.255");
|
||||
|
||||
// Try all unique address strings or host names
|
||||
bool success = false;
|
||||
for (QString& addressString : addressList) {
|
||||
QHostInfo hostInfo = QHostInfo::fromName(addressString);
|
||||
|
||||
if (hostInfo.error() != QHostInfo::NoError) {
|
||||
qWarning() << "Error resolving" << addressString << ":" << hostInfo.errorString();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Try all IP addresses that this string resolves to
|
||||
for (QHostAddress& address : hostInfo.addresses()) {
|
||||
QUdpSocket sock;
|
||||
|
||||
// Bind to any address on the correct protocol
|
||||
if (sock.bind(address.protocol() == QUdpSocket::IPv4Protocol ?
|
||||
QHostAddress::AnyIPv4 : QHostAddress::AnyIPv6)) {
|
||||
|
||||
// Send to all ports
|
||||
for (quint16 port : WOL_PORTS) {
|
||||
if (sock.writeDatagram(wolPayload, address, port)) {
|
||||
qInfo().nospace().noquote() << "Send WoL packet to " << name << " via " << address.toString() << ":" << port;
|
||||
success = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
QVector<QString> NvComputer::uniqueAddresses()
|
||||
{
|
||||
QVector<QString> uniqueAddressList;
|
||||
|
||||
// Start with addresses correctly ordered
|
||||
uniqueAddressList.append(activeAddress);
|
||||
uniqueAddressList.append(localAddress);
|
||||
uniqueAddressList.append(remoteAddress);
|
||||
uniqueAddressList.append(manualAddress);
|
||||
|
||||
// Prune duplicates (always giving precedence to the first)
|
||||
for (int i = 0; i < uniqueAddressList.count(); i++) {
|
||||
if (uniqueAddressList[i].isEmpty()) {
|
||||
uniqueAddressList.remove(i);
|
||||
i--;
|
||||
continue;
|
||||
}
|
||||
for (int j = i + 1; j < uniqueAddressList.count(); j++) {
|
||||
if (uniqueAddressList[i] == uniqueAddressList[j]) {
|
||||
// Always remove the later occurrence
|
||||
uniqueAddressList.remove(j);
|
||||
j--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We must have at least 1 address
|
||||
Q_ASSERT(!uniqueAddressList.isEmpty());
|
||||
|
||||
return uniqueAddressList;
|
||||
}
|
||||
|
||||
bool NvComputer::update(NvComputer& that)
|
||||
{
|
||||
bool changed = false;
|
||||
|
||||
// Lock us for write and them for read
|
||||
QWriteLocker thisLock(&this->lock);
|
||||
QReadLocker thatLock(&that.lock);
|
||||
|
||||
// UUID may not change or we're talking to a new PC
|
||||
Q_ASSERT(this->uuid == that.uuid);
|
||||
|
||||
#define ASSIGN_IF_CHANGED(field) \
|
||||
if (this->field != that.field) { \
|
||||
this->field = that.field; \
|
||||
changed = true; \
|
||||
}
|
||||
|
||||
#define ASSIGN_IF_CHANGED_AND_NONEMPTY(field) \
|
||||
if (!that.field.isEmpty() && \
|
||||
this->field != that.field) { \
|
||||
this->field = that.field; \
|
||||
changed = true; \
|
||||
}
|
||||
|
||||
ASSIGN_IF_CHANGED(name);
|
||||
ASSIGN_IF_CHANGED_AND_NONEMPTY(macAddress);
|
||||
ASSIGN_IF_CHANGED_AND_NONEMPTY(localAddress);
|
||||
ASSIGN_IF_CHANGED_AND_NONEMPTY(remoteAddress);
|
||||
ASSIGN_IF_CHANGED_AND_NONEMPTY(manualAddress);
|
||||
ASSIGN_IF_CHANGED(pairState);
|
||||
ASSIGN_IF_CHANGED(serverCodecModeSupport);
|
||||
ASSIGN_IF_CHANGED(currentGameId);
|
||||
ASSIGN_IF_CHANGED(activeAddress);
|
||||
ASSIGN_IF_CHANGED(state);
|
||||
ASSIGN_IF_CHANGED(gfeVersion);
|
||||
ASSIGN_IF_CHANGED(appVersion);
|
||||
ASSIGN_IF_CHANGED(maxLumaPixelsHEVC);
|
||||
ASSIGN_IF_CHANGED(gpuModel);
|
||||
ASSIGN_IF_CHANGED_AND_NONEMPTY(appList);
|
||||
ASSIGN_IF_CHANGED_AND_NONEMPTY(displayModes);
|
||||
return changed;
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
#pragma once
|
||||
|
||||
#include "nvhttp.h"
|
||||
|
||||
#include <QThread>
|
||||
#include <QReadWriteLock>
|
||||
#include <QSettings>
|
||||
#include <QRunnable>
|
||||
|
||||
class NvComputer
|
||||
{
|
||||
friend class PcMonitorThread;
|
||||
friend class ComputerManager;
|
||||
friend class PendingQuitTask;
|
||||
|
||||
private:
|
||||
void sortAppList();
|
||||
|
||||
bool pendingQuit;
|
||||
|
||||
public:
|
||||
explicit NvComputer(QString address, QString serverInfo);
|
||||
|
||||
explicit NvComputer(QSettings& settings);
|
||||
|
||||
bool
|
||||
update(NvComputer& that);
|
||||
|
||||
bool
|
||||
wake();
|
||||
|
||||
QVector<QString>
|
||||
uniqueAddresses();
|
||||
|
||||
void
|
||||
serialize(QSettings& settings);
|
||||
|
||||
enum PairState
|
||||
{
|
||||
PS_UNKNOWN,
|
||||
PS_PAIRED,
|
||||
PS_NOT_PAIRED
|
||||
};
|
||||
|
||||
enum ComputerState
|
||||
{
|
||||
CS_UNKNOWN,
|
||||
CS_ONLINE,
|
||||
CS_OFFLINE
|
||||
};
|
||||
|
||||
// Ephemeral traits
|
||||
ComputerState state;
|
||||
PairState pairState;
|
||||
QString activeAddress;
|
||||
int currentGameId;
|
||||
QString gfeVersion;
|
||||
QString appVersion;
|
||||
QVector<NvDisplayMode> displayModes;
|
||||
int maxLumaPixelsHEVC;
|
||||
int serverCodecModeSupport;
|
||||
QString gpuModel;
|
||||
|
||||
// Persisted traits
|
||||
QString localAddress;
|
||||
QString remoteAddress;
|
||||
QString manualAddress;
|
||||
QByteArray macAddress;
|
||||
QString name;
|
||||
QString uuid;
|
||||
QVector<NvApp> appList;
|
||||
|
||||
// Synchronization
|
||||
QReadWriteLock lock;
|
||||
};
|
||||
+23
-14
@@ -16,6 +16,8 @@
|
||||
NvHTTP::NvHTTP(QString address) :
|
||||
m_Address(address)
|
||||
{
|
||||
Q_ASSERT(!address.isEmpty());
|
||||
|
||||
m_BaseUrlHttp.setScheme("http");
|
||||
m_BaseUrlHttps.setScheme("https");
|
||||
m_BaseUrlHttp.setHost(address);
|
||||
@@ -27,9 +29,15 @@ NvHTTP::NvHTTP(QString address) :
|
||||
QVector<int>
|
||||
NvHTTP::parseQuad(QString quad)
|
||||
{
|
||||
QStringList parts = quad.split(".");
|
||||
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++)
|
||||
{
|
||||
ret.append(parts.at(i).toInt());
|
||||
@@ -56,7 +64,7 @@ NvHTTP::getCurrentGame(QString serverInfo)
|
||||
}
|
||||
|
||||
QString
|
||||
NvHTTP::getServerInfo()
|
||||
NvHTTP::getServerInfo(NvLogLevel logLevel)
|
||||
{
|
||||
QString serverInfo;
|
||||
|
||||
@@ -68,7 +76,7 @@ NvHTTP::getServerInfo()
|
||||
"serverinfo",
|
||||
nullptr,
|
||||
true,
|
||||
true);
|
||||
logLevel);
|
||||
// Throws if the request failed
|
||||
verifyResponseStatus(serverInfo);
|
||||
}
|
||||
@@ -81,7 +89,7 @@ NvHTTP::getServerInfo()
|
||||
"serverinfo",
|
||||
nullptr,
|
||||
true,
|
||||
true);
|
||||
logLevel);
|
||||
verifyResponseStatus(serverInfo);
|
||||
}
|
||||
else
|
||||
@@ -187,7 +195,7 @@ NvHTTP::quitApp()
|
||||
|
||||
// Newer GFE versions will just return success even if quitting fails
|
||||
// if we're not the original requestor.
|
||||
if (getCurrentGame(getServerInfo()) != 0) {
|
||||
if (getCurrentGame(getServerInfo(NvHTTP::ERROR)) != 0) {
|
||||
// Generate a synthetic GfeResponseException letting the caller know
|
||||
// that they can't kill someone else's stream.
|
||||
throw GfeHttpResponseException(599, "");
|
||||
@@ -228,7 +236,7 @@ NvHTTP::getAppList()
|
||||
"applist",
|
||||
nullptr,
|
||||
true,
|
||||
true);
|
||||
NvLogLevel::ERROR);
|
||||
verifyResponseStatus(appxml);
|
||||
|
||||
QXmlStreamReader xmlReader(appxml);
|
||||
@@ -295,7 +303,8 @@ NvHTTP::getBoxArt(int appId)
|
||||
"appasset",
|
||||
"appid="+QString::number(appId)+
|
||||
"&AssetType=2&AssetIdx=0",
|
||||
true);
|
||||
true,
|
||||
NvLogLevel::VERBOSE);
|
||||
QImage image = QImageReader(reply).read();
|
||||
delete reply;
|
||||
|
||||
@@ -342,9 +351,9 @@ NvHTTP::openConnectionToString(QUrl baseUrl,
|
||||
QString command,
|
||||
QString arguments,
|
||||
bool enableTimeout,
|
||||
bool suppressLogging)
|
||||
NvLogLevel logLevel)
|
||||
{
|
||||
QNetworkReply* reply = openConnection(baseUrl, command, arguments, enableTimeout, suppressLogging);
|
||||
QNetworkReply* reply = openConnection(baseUrl, command, arguments, enableTimeout, logLevel);
|
||||
QString ret;
|
||||
|
||||
QTextStream stream(reply);
|
||||
@@ -360,7 +369,7 @@ NvHTTP::openConnection(QUrl baseUrl,
|
||||
QString command,
|
||||
QString arguments,
|
||||
bool enableTimeout,
|
||||
bool suppressLogging)
|
||||
NvLogLevel logLevel)
|
||||
{
|
||||
// Build a URL for the request
|
||||
QUrl url(baseUrl);
|
||||
@@ -386,7 +395,7 @@ NvHTTP::openConnection(QUrl baseUrl,
|
||||
{
|
||||
QTimer::singleShot(REQUEST_TIMEOUT_MS, &loop, SLOT(quit()));
|
||||
}
|
||||
if (!suppressLogging) {
|
||||
if (logLevel >= NvLogLevel::VERBOSE) {
|
||||
qInfo() << "Executing request:" << url.toString();
|
||||
}
|
||||
loop.exec(QEventLoop::ExcludeUserInputEvents);
|
||||
@@ -394,7 +403,7 @@ NvHTTP::openConnection(QUrl baseUrl,
|
||||
// Abort the request if it timed out
|
||||
if (!reply->isFinished())
|
||||
{
|
||||
if (!suppressLogging) {
|
||||
if (logLevel >= NvLogLevel::ERROR) {
|
||||
qWarning() << "Aborting timed out request for" << url.toString();
|
||||
}
|
||||
reply->abort();
|
||||
@@ -407,10 +416,10 @@ NvHTTP::openConnection(QUrl baseUrl,
|
||||
// Handle error
|
||||
if (reply->error() != QNetworkReply::NoError)
|
||||
{
|
||||
if (!suppressLogging) {
|
||||
if (logLevel >= NvLogLevel::ERROR) {
|
||||
qWarning() << command << " request failed with error " << reply->error();
|
||||
}
|
||||
GfeHttpResponseException exception(reply->error(), reply->errorString());
|
||||
QtNetworkReplyException exception(reply->error(), reply->errorString());
|
||||
delete reply;
|
||||
throw exception;
|
||||
}
|
||||
|
||||
+47
-5
@@ -5,7 +5,8 @@
|
||||
#include <Limelight.h>
|
||||
|
||||
#include <QUrl>
|
||||
#include <QtNetwork/QNetworkAccessManager>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkReply>
|
||||
|
||||
class NvApp
|
||||
{
|
||||
@@ -17,7 +18,7 @@ public:
|
||||
|
||||
bool isInitialized()
|
||||
{
|
||||
return id != 0 && !name.isNull();
|
||||
return id != 0 && !name.isEmpty();
|
||||
}
|
||||
|
||||
int id;
|
||||
@@ -77,9 +78,50 @@ private:
|
||||
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
|
||||
{
|
||||
public:
|
||||
enum NvLogLevel {
|
||||
NONE,
|
||||
ERROR,
|
||||
VERBOSE
|
||||
};
|
||||
|
||||
explicit NvHTTP(QString address);
|
||||
|
||||
static
|
||||
@@ -87,7 +129,7 @@ public:
|
||||
getCurrentGame(QString serverInfo);
|
||||
|
||||
QString
|
||||
getServerInfo();
|
||||
getServerInfo(NvLogLevel logLevel);
|
||||
|
||||
static
|
||||
void
|
||||
@@ -108,7 +150,7 @@ public:
|
||||
QString command,
|
||||
QString arguments,
|
||||
bool enableTimeout,
|
||||
bool suppressLogging = false);
|
||||
NvLogLevel logLevel = NvLogLevel::VERBOSE);
|
||||
|
||||
static
|
||||
QVector<int>
|
||||
@@ -145,7 +187,7 @@ private:
|
||||
QString command,
|
||||
QString arguments,
|
||||
bool enableTimeout,
|
||||
bool suppressLogging = false);
|
||||
NvLogLevel logLevel);
|
||||
|
||||
QString m_Address;
|
||||
QNetworkAccessManager m_Nam;
|
||||
|
||||
@@ -0,0 +1,362 @@
|
||||
#include "commandlineparser.h"
|
||||
|
||||
#include <QCommandLineParser>
|
||||
#include <QRegularExpression>
|
||||
|
||||
#if defined(Q_OS_WIN)
|
||||
#include <qt_windows.h>
|
||||
#endif
|
||||
|
||||
static bool inRange(int value, int min, int max)
|
||||
{
|
||||
return value >= min && value <= max;
|
||||
}
|
||||
|
||||
// This method returns key's value from QMap where the key is a QString.
|
||||
// Key matching is case insensitive.
|
||||
template <typename T>
|
||||
static T mapValue(QMap<QString, T> map, QString key)
|
||||
{
|
||||
for(auto& item : map.toStdMap()) {
|
||||
if (QString::compare(item.first, key, Qt::CaseInsensitive) == 0) {
|
||||
return item.second;
|
||||
}
|
||||
}
|
||||
return T();
|
||||
}
|
||||
|
||||
class CommandLineParser : public QCommandLineParser
|
||||
{
|
||||
public:
|
||||
enum MessageType {
|
||||
Info,
|
||||
Error
|
||||
};
|
||||
|
||||
void setupCommonOptions()
|
||||
{
|
||||
setSingleDashWordOptionMode(QCommandLineParser::ParseAsLongOptions);
|
||||
addHelpOption();
|
||||
addVersionOption();
|
||||
}
|
||||
|
||||
void handleHelpAndVersionOptions()
|
||||
{
|
||||
if (isSet("help")) {
|
||||
showInfo(helpText());
|
||||
}
|
||||
if (isSet("version")) {
|
||||
showVersion();
|
||||
}
|
||||
}
|
||||
|
||||
void handleUnknownOptions()
|
||||
{
|
||||
if (unknownOptionNames().length()) {
|
||||
showError(QString("Unknown options: %1").arg(unknownOptionNames().join(", ")));
|
||||
}
|
||||
}
|
||||
|
||||
void showMessage(QString message, MessageType type) const
|
||||
{
|
||||
#if defined(Q_OS_WIN)
|
||||
UINT flags = MB_OK | MB_TOPMOST | MB_SETFOREGROUND;
|
||||
flags |= (type == Info ? MB_ICONINFORMATION : MB_ICONERROR);
|
||||
QString title = "Moonlight";
|
||||
MessageBoxW(nullptr, reinterpret_cast<const wchar_t *>(message.utf16()),
|
||||
reinterpret_cast<const wchar_t *>(title.utf16()), flags);
|
||||
#endif
|
||||
message = message.endsWith('\n') ? message : message + '\n';
|
||||
fputs(qPrintable(message), type == Info ? stdout : stderr);
|
||||
}
|
||||
|
||||
[[ noreturn ]] void showInfo(QString message) const
|
||||
{
|
||||
showMessage(message, Info);
|
||||
exit(0);
|
||||
}
|
||||
|
||||
[[ noreturn ]] void showError(QString message) const
|
||||
{
|
||||
showMessage(message + "\n\n" + helpText(), Error);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
int getIntOption(QString name) const
|
||||
{
|
||||
bool ok;
|
||||
int intValue = value(name).toInt(&ok);
|
||||
if (!ok) {
|
||||
showError(QString("Invalid %1 value: %2").arg(name, value(name)));
|
||||
}
|
||||
return intValue;
|
||||
}
|
||||
|
||||
bool getToggleOptionValue(QString name, bool defaultValue) const
|
||||
{
|
||||
QRegularExpression re(QString("^(%1|no-%1)$").arg(name));
|
||||
QStringList options = optionNames().filter(re);
|
||||
if (options.isEmpty()) {
|
||||
return defaultValue;
|
||||
} else {
|
||||
return options.last() == name;
|
||||
}
|
||||
}
|
||||
|
||||
QString getChoiceOptionValue(QString name) const
|
||||
{
|
||||
if (!m_Choices[name].contains(value(name), Qt::CaseInsensitive)) {
|
||||
showError(QString("Invalid %1 choice: %2").arg(name, value(name)));
|
||||
}
|
||||
return value(name);
|
||||
}
|
||||
|
||||
QPair<int,int> getResolutionOptionValue(QString name) const
|
||||
{
|
||||
QRegularExpression re("^(\\d+)x(\\d+)$", QRegularExpression::CaseInsensitiveOption);
|
||||
auto match = re.match(value(name));
|
||||
if (!match.hasMatch()) {
|
||||
showError(QString("Invalid %1 format: %2").arg(name, value(name)));
|
||||
}
|
||||
return qMakePair(match.captured(1).toInt(), match.captured(2).toInt());
|
||||
}
|
||||
|
||||
void addFlagOption(QString name, QString descriptiveName)
|
||||
{
|
||||
addOption(QCommandLineOption(name, QString("Use %1.").arg(descriptiveName)));
|
||||
}
|
||||
|
||||
void addToggleOption(QString name, QString descriptiveName)
|
||||
{
|
||||
addOption(QCommandLineOption(name, QString("Use %1.").arg(descriptiveName)));
|
||||
addOption(QCommandLineOption("no-" + name, QString("Do not use %1.").arg(descriptiveName)));
|
||||
}
|
||||
|
||||
void addValueOption(QString name, QString descriptiveName)
|
||||
{
|
||||
addOption(QCommandLineOption(name, QString("Specify %1 to use.").arg(descriptiveName), name));
|
||||
}
|
||||
|
||||
void addChoiceOption(QString name, QString descriptiveName, QStringList choices)
|
||||
{
|
||||
addOption(QCommandLineOption(name, QString("Select %1: %2.").arg(descriptiveName, choices.join('/')), name));
|
||||
m_Choices[name] = choices;
|
||||
}
|
||||
|
||||
private:
|
||||
QMap<QString, QStringList> m_Choices;
|
||||
};
|
||||
|
||||
GlobalCommandLineParser::GlobalCommandLineParser()
|
||||
{
|
||||
}
|
||||
|
||||
GlobalCommandLineParser::~GlobalCommandLineParser()
|
||||
{
|
||||
}
|
||||
|
||||
GlobalCommandLineParser::ParseResult GlobalCommandLineParser::parse(const QStringList &args)
|
||||
{
|
||||
CommandLineParser parser;
|
||||
parser.setupCommonOptions();
|
||||
parser.setApplicationDescription(
|
||||
"\n"
|
||||
"Starts Moonlight normally if no arguments are given.\n"
|
||||
"\n"
|
||||
"Available actions:\n"
|
||||
" stream Start streaming an app\n"
|
||||
"\n"
|
||||
"See 'moonlight <action> --help' for help of specific action."
|
||||
);
|
||||
parser.addPositionalArgument("action", "Action to execute", "<action>");
|
||||
parser.parse(args);
|
||||
auto posArgs = parser.positionalArguments();
|
||||
QString action = posArgs.isEmpty() ? QString() : posArgs.first().toLower();
|
||||
|
||||
if (action == "") {
|
||||
// This method will not return and terminates the process if --version
|
||||
// or --help is specified
|
||||
parser.handleHelpAndVersionOptions();
|
||||
parser.handleUnknownOptions();
|
||||
return NormalStartRequested;
|
||||
} else if (action == "stream") {
|
||||
return StreamRequested;
|
||||
} else {
|
||||
parser.showError(QString("Invalid action: %1").arg(action));
|
||||
}
|
||||
}
|
||||
|
||||
StreamCommandLineParser::StreamCommandLineParser()
|
||||
{
|
||||
m_WindowModeMap = {
|
||||
{"fullscreen", StreamingPreferences::WM_FULLSCREEN},
|
||||
{"windowed", StreamingPreferences::WM_WINDOWED},
|
||||
{"borderless", StreamingPreferences::WM_FULLSCREEN_DESKTOP},
|
||||
};
|
||||
m_AudioConfigMap = {
|
||||
{"stereo", StreamingPreferences::AC_STEREO},
|
||||
{"5.1-surround", StreamingPreferences::AC_51_SURROUND},
|
||||
};
|
||||
m_VideoCodecMap = {
|
||||
{"auto", StreamingPreferences::VCC_AUTO},
|
||||
{"H.264", StreamingPreferences::VCC_FORCE_H264},
|
||||
{"HEVC", StreamingPreferences::VCC_FORCE_HEVC},
|
||||
};
|
||||
m_VideoDecoderMap = {
|
||||
{"auto", StreamingPreferences::VDS_AUTO},
|
||||
{"software", StreamingPreferences::VDS_FORCE_HARDWARE},
|
||||
{"hardware", StreamingPreferences::VDS_FORCE_SOFTWARE},
|
||||
};
|
||||
}
|
||||
|
||||
StreamCommandLineParser::~StreamCommandLineParser()
|
||||
{
|
||||
}
|
||||
|
||||
void StreamCommandLineParser::parse(const QStringList &args, StreamingPreferences *preferences)
|
||||
{
|
||||
CommandLineParser parser;
|
||||
parser.setupCommonOptions();
|
||||
parser.setApplicationDescription(
|
||||
"\n"
|
||||
"Starts directly streaming a given app."
|
||||
);
|
||||
parser.addPositionalArgument("stream", "Start stream");
|
||||
|
||||
// Add other arguments and options
|
||||
parser.addPositionalArgument("host", "Host computer name, UUID, or IP address", "<host>");
|
||||
parser.addPositionalArgument("app", "App to stream", "\"<app>\"");
|
||||
|
||||
parser.addFlagOption("720", "1280x720 resolution");
|
||||
parser.addFlagOption("1080", "1920x1080 resolution");
|
||||
parser.addFlagOption("1440", "2560x1440 resolution");
|
||||
parser.addFlagOption("4K", "3840x2160 resolution");
|
||||
parser.addValueOption("resolution", "custom <width>x<height> resolution");
|
||||
parser.addToggleOption("vsync", "V-Sync");
|
||||
parser.addValueOption("fps", "FPS");
|
||||
parser.addValueOption("bitrate", "bitrate in Kbps");
|
||||
parser.addChoiceOption("display-mode", "display mode", m_WindowModeMap.keys());
|
||||
parser.addChoiceOption("audio-config", "audio config", m_AudioConfigMap.keys());
|
||||
parser.addToggleOption("multi-controller", "multiple controller support");
|
||||
parser.addToggleOption("mouse-acceleration", "mouse acceleration");
|
||||
parser.addToggleOption("game-optimization", "game optimizations");
|
||||
parser.addToggleOption("audio-on-host", "audio on host PC");
|
||||
parser.addChoiceOption("video-codec", "video codec", m_VideoCodecMap.keys());
|
||||
parser.addChoiceOption("video-decoder", "video decoder", m_VideoDecoderMap.keys());
|
||||
|
||||
if (!parser.parse(args)) {
|
||||
parser.showError(parser.errorText());
|
||||
}
|
||||
|
||||
parser.handleUnknownOptions();
|
||||
|
||||
// Resolve display's width and height
|
||||
QRegularExpression resolutionRexExp("^(720|1080|1440|4K|resolution)$");
|
||||
QStringList resoOptions = parser.optionNames().filter(resolutionRexExp);
|
||||
bool displaySet = resoOptions.length();
|
||||
if (displaySet) {
|
||||
QString name = resoOptions.last();
|
||||
if (name == "720") {
|
||||
preferences->width = 1280;
|
||||
preferences->height = 720;
|
||||
displaySet = true;
|
||||
} else if (name == "1080") {
|
||||
preferences->width = 1920;
|
||||
preferences->height = 1080;
|
||||
displaySet = true;
|
||||
} else if (name == "1440") {
|
||||
preferences->width = 2560;
|
||||
preferences->height = 1440;
|
||||
displaySet = true;
|
||||
} else if (name == "4K") {
|
||||
preferences->width = 3840;
|
||||
preferences->height = 2160;
|
||||
displaySet = true;
|
||||
} else if (name == "resolution") {
|
||||
auto resolution = parser.getResolutionOptionValue(name);
|
||||
preferences->width = resolution.first;
|
||||
preferences->height = resolution.second;
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve --fps option
|
||||
if (parser.isSet("fps")) {
|
||||
preferences->fps = parser.getIntOption("fps");
|
||||
if (!inRange(preferences->fps, 30, 120)) {
|
||||
parser.showError("FPS must be in range: 30 - 120");
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve --bitrate option
|
||||
if (parser.isSet("bitrate")) {
|
||||
preferences->bitrateKbps = parser.getIntOption("bitrate");
|
||||
if (!inRange(preferences->bitrateKbps, 500, 150000)) {
|
||||
parser.showError("Bitrate must be in range: 500 - 150000");
|
||||
}
|
||||
} else if (displaySet || parser.isSet("fps")) {
|
||||
preferences->bitrateKbps = preferences->getDefaultBitrate(
|
||||
preferences->width, preferences->height, preferences->fps);
|
||||
}
|
||||
|
||||
// Resolve --display option
|
||||
if (parser.isSet("display-mode")) {
|
||||
preferences->windowMode = mapValue(m_WindowModeMap, parser.getChoiceOptionValue("display-mode"));
|
||||
}
|
||||
|
||||
// Resolve --vsync and --no-vsync options
|
||||
preferences->enableVsync = parser.getToggleOptionValue("vsync", preferences->enableVsync);
|
||||
|
||||
// Resolve --audio-config option
|
||||
if (parser.isSet("audio-config")) {
|
||||
preferences->audioConfig = mapValue(m_AudioConfigMap, parser.getChoiceOptionValue("audio-config"));
|
||||
}
|
||||
|
||||
// Resolve --multi-controller and --no-multi-controller options
|
||||
preferences->multiController = parser.getToggleOptionValue("multi-controller", preferences->multiController);
|
||||
|
||||
// Resolve --mouse-acceleration and --no-mouse-acceleration options
|
||||
preferences->mouseAcceleration = parser.getToggleOptionValue("mouse-acceleration", preferences->mouseAcceleration);
|
||||
|
||||
// Resolve --game-optimization and --no-game-optimization options
|
||||
preferences->gameOptimizations = parser.getToggleOptionValue("game-optimization", preferences->gameOptimizations);
|
||||
|
||||
// Resolve --audio-on-host and --no-audio-on-host options
|
||||
preferences->playAudioOnHost = parser.getToggleOptionValue("audio-on-host", preferences->playAudioOnHost);
|
||||
|
||||
// Resolve --video-codec option
|
||||
if (parser.isSet("video-codec")) {
|
||||
preferences->videoCodecConfig = mapValue(m_VideoCodecMap, parser.getChoiceOptionValue("video-codec"));
|
||||
}
|
||||
|
||||
// Resolve --video-decoder option
|
||||
if (parser.isSet("video-decoder")) {
|
||||
preferences->videoDecoderSelection = mapValue(m_VideoDecoderMap, parser.getChoiceOptionValue("video-decoder"));
|
||||
}
|
||||
|
||||
// This method will not return and terminates the process if --version or
|
||||
// --help is specified
|
||||
parser.handleHelpAndVersionOptions();
|
||||
|
||||
// Verify that both host and app has been provided
|
||||
auto posArgs = parser.positionalArguments();
|
||||
if (posArgs.length() < 2) {
|
||||
parser.showError("Host not provided");
|
||||
}
|
||||
m_Host = parser.positionalArguments().at(1);
|
||||
|
||||
if (posArgs.length() < 3) {
|
||||
parser.showError("App not provided");
|
||||
}
|
||||
m_AppName = parser.positionalArguments().at(2);
|
||||
}
|
||||
|
||||
QString StreamCommandLineParser::getHost() const
|
||||
{
|
||||
return m_Host;
|
||||
}
|
||||
|
||||
QString StreamCommandLineParser::getAppName() const
|
||||
{
|
||||
return m_AppName;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
#pragma once
|
||||
|
||||
#include "settings/streamingpreferences.h"
|
||||
|
||||
#include <QMap>
|
||||
#include <QString>
|
||||
|
||||
class GlobalCommandLineParser
|
||||
{
|
||||
public:
|
||||
enum ParseResult {
|
||||
NormalStartRequested,
|
||||
StreamRequested,
|
||||
};
|
||||
|
||||
GlobalCommandLineParser();
|
||||
virtual ~GlobalCommandLineParser();
|
||||
|
||||
ParseResult parse(const QStringList &args);
|
||||
|
||||
};
|
||||
|
||||
class StreamCommandLineParser
|
||||
{
|
||||
public:
|
||||
StreamCommandLineParser();
|
||||
virtual ~StreamCommandLineParser();
|
||||
|
||||
void parse(const QStringList &args, StreamingPreferences *preferences);
|
||||
|
||||
QString getHost() const;
|
||||
QString getAppName() const;
|
||||
|
||||
private:
|
||||
QString m_Host;
|
||||
QString m_AppName;
|
||||
QMap<QString, StreamingPreferences::WindowMode> m_WindowModeMap;
|
||||
QMap<QString, StreamingPreferences::AudioConfig> m_AudioConfigMap;
|
||||
QMap<QString, StreamingPreferences::VideoCodecConfig> m_VideoCodecMap;
|
||||
QMap<QString, StreamingPreferences::VideoDecoderSelection> m_VideoDecoderMap;
|
||||
};
|
||||
@@ -0,0 +1,220 @@
|
||||
#include "startstream.h"
|
||||
#include "backend/computermanager.h"
|
||||
#include "streaming/session.h"
|
||||
|
||||
#include <QTimer>
|
||||
|
||||
#define COMPUTER_SEEK_TIMEOUT 10000
|
||||
#define APP_SEEK_TIMEOUT 10000
|
||||
|
||||
namespace CliStartStream
|
||||
{
|
||||
|
||||
enum State {
|
||||
StateInit,
|
||||
StateSeekComputer,
|
||||
StateSeekApp,
|
||||
StateStartSession,
|
||||
StateFailure,
|
||||
};
|
||||
|
||||
class Event
|
||||
{
|
||||
public:
|
||||
enum Type {
|
||||
ComputerUpdated,
|
||||
Executed,
|
||||
Timedout,
|
||||
};
|
||||
|
||||
Event(Type type)
|
||||
: type(type), computerManager(nullptr), computer(nullptr) {}
|
||||
|
||||
Type type;
|
||||
ComputerManager *computerManager;
|
||||
NvComputer *computer;
|
||||
};
|
||||
|
||||
class LauncherPrivate
|
||||
{
|
||||
Q_DECLARE_PUBLIC(Launcher)
|
||||
|
||||
public:
|
||||
LauncherPrivate(Launcher *q) : q_ptr(q) {}
|
||||
|
||||
void handleEvent(Event event)
|
||||
{
|
||||
Q_Q(Launcher);
|
||||
Session* session;
|
||||
NvApp app;
|
||||
|
||||
switch (event.type) {
|
||||
case Event::Executed:
|
||||
if (m_State == StateInit) {
|
||||
m_State = StateSeekComputer;
|
||||
m_TimeoutTimer->start(COMPUTER_SEEK_TIMEOUT);
|
||||
m_ComputerManager = event.computerManager;
|
||||
q->connect(m_ComputerManager, &ComputerManager::computerStateChanged,
|
||||
q, &Launcher::onComputerUpdated);
|
||||
// Seek desired computer by both connecting to it directly (this may fail
|
||||
// if m_ComputerName is UUID, or the name that doesn't resolve to an IP
|
||||
// address) and by polling it using mDNS, hopefully one of these methods
|
||||
// would find the host
|
||||
m_ComputerManager->addNewHost(m_ComputerName, false);
|
||||
m_ComputerManager->startPolling();
|
||||
emit q->searchingComputer();
|
||||
}
|
||||
break;
|
||||
case Event::ComputerUpdated:
|
||||
if (m_State == StateSeekComputer) {
|
||||
if (matchComputer(event.computer) && isOnline(event.computer)) {
|
||||
if (isPaired(event.computer)) {
|
||||
m_State = StateSeekApp;
|
||||
m_TimeoutTimer->start(APP_SEEK_TIMEOUT);
|
||||
m_Computer = event.computer;
|
||||
m_ComputerManager->stopPollingAsync();
|
||||
emit q->searchingApp();
|
||||
} else {
|
||||
m_State = StateFailure;
|
||||
QString msg = QString("Computer %1 has not been paired. "
|
||||
"Please open Moonlight to pair before streaming.")
|
||||
.arg(event.computer->name);
|
||||
emit q->failed(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (m_State == StateSeekApp) {
|
||||
int index = getAppIndex();
|
||||
if (-1 != index) {
|
||||
app = m_Computer->appList[index];
|
||||
if (isNotStreaming() || isStreamingApp(app)) {
|
||||
m_State = StateStartSession;
|
||||
m_TimeoutTimer->stop();
|
||||
session = new Session(m_Computer, app, m_Preferences);
|
||||
emit q->sessionCreated(app.name, session);
|
||||
} else {
|
||||
m_State = StateFailure;
|
||||
QString msg = QString("%1 is already running. Please quit %1 to stream %2.")
|
||||
.arg(getCurrentAppName(), app.name);
|
||||
emit q->failed(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case Event::Timedout:
|
||||
if (m_State == StateSeekComputer) {
|
||||
m_State = StateFailure;
|
||||
emit q->failed(QString("Failed to connect to %1").arg(m_ComputerName));
|
||||
}
|
||||
if (m_State == StateSeekApp) {
|
||||
m_State = StateFailure;
|
||||
emit q->failed(QString("Failed to find application %1").arg(m_AppName));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
bool matchComputer(NvComputer *computer) const
|
||||
{
|
||||
QString value = m_ComputerName.toLower();
|
||||
return computer->name.toLower() == value ||
|
||||
computer->localAddress.toLower() == value ||
|
||||
computer->remoteAddress.toLower() == value ||
|
||||
computer->manualAddress.toLower() == value ||
|
||||
computer->uuid.toLower() == value;
|
||||
}
|
||||
|
||||
bool isOnline(NvComputer *computer) const
|
||||
{
|
||||
return computer->state == NvComputer::CS_ONLINE;
|
||||
}
|
||||
|
||||
bool isPaired(NvComputer *computer) const
|
||||
{
|
||||
return computer->pairState == NvComputer::PS_PAIRED;
|
||||
}
|
||||
|
||||
int getAppIndex() const
|
||||
{
|
||||
for (int i = 0; i < m_Computer->appList.length(); i++) {
|
||||
if (m_Computer->appList[i].name.toLower() == m_AppName.toLower()) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool isNotStreaming() const
|
||||
{
|
||||
return m_Computer->currentGameId == 0;
|
||||
}
|
||||
|
||||
bool isStreamingApp(NvApp app) const
|
||||
{
|
||||
return m_Computer->currentGameId == app.id;
|
||||
}
|
||||
|
||||
QString getCurrentAppName() const
|
||||
{
|
||||
for (NvApp app : m_Computer->appList) {
|
||||
if (m_Computer->currentGameId == app.id) {
|
||||
return app.name;
|
||||
}
|
||||
}
|
||||
return "<UNKNOWN>";
|
||||
}
|
||||
|
||||
Launcher *q_ptr;
|
||||
QString m_ComputerName;
|
||||
QString m_AppName;
|
||||
StreamingPreferences *m_Preferences;
|
||||
ComputerManager *m_ComputerManager;
|
||||
NvComputer *m_Computer;
|
||||
State m_State;
|
||||
QTimer *m_TimeoutTimer;
|
||||
};
|
||||
|
||||
Launcher::Launcher(QString computer, QString app,
|
||||
StreamingPreferences* preferences, QObject *parent)
|
||||
: QObject(parent),
|
||||
m_DPtr(new LauncherPrivate(this))
|
||||
{
|
||||
Q_D(Launcher);
|
||||
d->m_ComputerName = computer;
|
||||
d->m_AppName = app;
|
||||
d->m_Preferences = preferences;
|
||||
d->m_State = StateInit;
|
||||
d->m_TimeoutTimer = new QTimer(this);
|
||||
d->m_TimeoutTimer->setSingleShot(true);
|
||||
connect(d->m_TimeoutTimer, &QTimer::timeout,
|
||||
this, &Launcher::onTimeout);
|
||||
}
|
||||
|
||||
Launcher::~Launcher()
|
||||
{
|
||||
}
|
||||
|
||||
void Launcher::execute(ComputerManager *manager)
|
||||
{
|
||||
Q_D(Launcher);
|
||||
Event event(Event::Executed);
|
||||
event.computerManager = manager;
|
||||
d->handleEvent(event);
|
||||
}
|
||||
|
||||
void Launcher::onComputerUpdated(NvComputer *computer)
|
||||
{
|
||||
Q_D(Launcher);
|
||||
Event event(Event::ComputerUpdated);
|
||||
event.computer = computer;
|
||||
d->handleEvent(event);
|
||||
}
|
||||
|
||||
void Launcher::onTimeout()
|
||||
{
|
||||
Q_D(Launcher);
|
||||
Event event(Event::Timedout);
|
||||
d->handleEvent(event);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
|
||||
class ComputerManager;
|
||||
class NvComputer;
|
||||
class Session;
|
||||
class StreamingPreferences;
|
||||
|
||||
namespace CliStartStream
|
||||
{
|
||||
|
||||
class Event;
|
||||
class LauncherPrivate;
|
||||
|
||||
class Launcher : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_DECLARE_PRIVATE_D(m_DPtr, Launcher)
|
||||
|
||||
public:
|
||||
explicit Launcher(QString computer, QString app,
|
||||
StreamingPreferences* preferences,
|
||||
QObject *parent = nullptr);
|
||||
~Launcher();
|
||||
Q_INVOKABLE void execute(ComputerManager *manager);
|
||||
|
||||
signals:
|
||||
void searchingComputer();
|
||||
void searchingApp();
|
||||
void sessionCreated(QString appName, Session *session);
|
||||
void failed(QString text);
|
||||
|
||||
private slots:
|
||||
void onComputerUpdated(NvComputer *computer);
|
||||
void onTimeout();
|
||||
|
||||
private:
|
||||
QScopedPointer<LauncherPrivate> m_DPtr;
|
||||
};
|
||||
|
||||
}
|
||||
@@ -11,7 +11,7 @@
|
||||
<p>Features include:</p>
|
||||
<ul>
|
||||
<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>5.1 surround sound audio support</li>
|
||||
<li>HEVC support for better video compression efficiency</li>
|
||||
@@ -33,6 +33,90 @@
|
||||
</screenshots>
|
||||
|
||||
<releases>
|
||||
<release version="0.6.1" date="2018-10-14">
|
||||
<description>
|
||||
<p>New features:</p>
|
||||
<ul>
|
||||
<li>Added support for quitting Moonlight via gamepad</li>
|
||||
<li>Added tooltips for games with very long names</li>
|
||||
</ul>
|
||||
<p>Bugfixes:</p>
|
||||
<ul>
|
||||
<li>Added a workaround for a memory leak in the VAAPI driver for AMD GPUs</li>
|
||||
<li>Fixed combo boxes on the settings page being too small for certain DPI scaling</li>
|
||||
<li>Reduced power usage when Moonlight is idling in the background for a while</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="0.6.0" date="2018-10-06">
|
||||
<description>
|
||||
<p>New features:</p>
|
||||
<ul>
|
||||
<li>Added keyboard and gamepad UI navigation</li>
|
||||
<li>Added support for mouse X1 and X2 buttons (back and forward)</li>
|
||||
<li>Added support for mouse movement with touchscreens</li>
|
||||
<li>Improved HEVC performance at high bitrates</li>
|
||||
<li>Added support for launching streaming sessions directly from a terminal or script</li>
|
||||
<li>Added quit shortcut for gamepads: Select+Start+L1+R1</li>
|
||||
</ul>
|
||||
<p>Bugfixes:</p>
|
||||
<ul>
|
||||
<li>Fixed not saving deletion of PCs</li>
|
||||
<li>Fixed keys being stuck down on the host PC after Moonlight loses focus or quits</li>
|
||||
<li>Fixed some warning dialogs being stuck behind the UI window</li>
|
||||
<li>Updated included gamepad mapping list</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="0.5.0" date="2018-09-15">
|
||||
<description>
|
||||
<p>New features:</p>
|
||||
<ul>
|
||||
<li>Added option to enable mouse acceleration for remote desktop usage</li>
|
||||
<li>Added option to disable automatic PC discovery</li>
|
||||
<li>Added gamepad mappings from gabomdq's SDL_GameControllerDB</li>
|
||||
<li>Added help links to error dialogs</li>
|
||||
<li>Added quit shortcut tip to stream loading page</li>
|
||||
<li>Restored the autodetect audio configuration option</li>
|
||||
</ul>
|
||||
<p>Bugfixes:</p>
|
||||
<ul>
|
||||
<li>Fixed quit app dialog starting the wrong app</li>
|
||||
<li>Fixed very high input lag on GFE 3.14.0 and earlier</li>
|
||||
<li>Fixed sending errant mouse click when capturing the mouse</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="0.4.0" date="2018-09-08">
|
||||
<description>
|
||||
<p>New features:</p>
|
||||
<ul>
|
||||
<li>Added option to enable (unsupported) 90/120 FPS streaming on 60 Hz monitors</li>
|
||||
<li>Added refresh rate matching for better frame pacing on high refresh rate monitors</li>
|
||||
<li>The streaming window now appears on the same monitor the UI was displayed on</li>
|
||||
<li>The streaming window is now centered on the target monitor</li>
|
||||
<li>Display a dialog when the connection is terminated by the remote PC</li>
|
||||
<li>Added option to select between full-screen exclusive and borderless windowed mode</li>
|
||||
</ul>
|
||||
<p>Bugfixes:</p>
|
||||
<ul>
|
||||
<li>Fixed streaming with no audio device</li>
|
||||
<li>Fixed mouse lag with some high polling rate mice</li>
|
||||
<li>Improved UI performance on the app grid page</li>
|
||||
<li>1440p and 4K 120 FPS performance is improved to avoid encoder bottlenecks</li>
|
||||
<li>Autodetect audio configuration option removed because it was broken</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="0.3.0" date="2018-08-21">
|
||||
<description>
|
||||
<p>Bugfixes:</p>
|
||||
<ul>
|
||||
<li>Reduced power consumption when the app is minimized</li>
|
||||
<li>Fixed malformed asset requests being sent when the first host in the grid is offline</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="0.2.0" date="2018-08-12">
|
||||
<description>
|
||||
<p>Bugfixes:</p>
|
||||
|
||||
+82
-59
@@ -1,27 +1,24 @@
|
||||
import QtQuick 2.9
|
||||
import QtQuick.Dialogs 1.3
|
||||
import QtQuick.Dialogs 1.2
|
||||
import QtQuick.Controls 2.2
|
||||
|
||||
import AppModel 1.0
|
||||
|
||||
import ComputerManager 1.0
|
||||
import SdlGamepadKeyNavigation 1.0
|
||||
|
||||
GridView {
|
||||
property int computerIndex
|
||||
property AppModel appModel : createModel()
|
||||
|
||||
id: appGrid
|
||||
focus: true
|
||||
activeFocusOnTab: true
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: (parent.width % (cellWidth + anchors.rightMargin)) / 2
|
||||
anchors.topMargin: 20
|
||||
anchors.rightMargin: 5
|
||||
anchors.bottomMargin: 5
|
||||
cellWidth: 225; cellHeight: 350;
|
||||
focus: true
|
||||
|
||||
// Cache delegates for 1000px in both directions to improve
|
||||
// scrolling and resizing performance
|
||||
cacheBuffer: 1000
|
||||
cellWidth: 225; cellHeight: 385;
|
||||
|
||||
// The StackView will trigger a visibility change when
|
||||
// we're pushed onto it, causing our onVisibleChanged
|
||||
@@ -34,18 +31,23 @@ GridView {
|
||||
stackView.pop()
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
// Don't show any highlighted item until interacting with them
|
||||
currentIndex = -1
|
||||
}
|
||||
|
||||
SdlGamepadKeyNavigation {
|
||||
id: gamepadKeyNav
|
||||
}
|
||||
|
||||
onVisibleChanged: {
|
||||
if (visible) {
|
||||
// Start polling when this view is shown
|
||||
ComputerManager.startPolling()
|
||||
|
||||
appModel.computerLost.connect(computerLost)
|
||||
gamepadKeyNav.enable()
|
||||
}
|
||||
else {
|
||||
// Stop polling when this view is not on top
|
||||
ComputerManager.stopPollingAsync()
|
||||
|
||||
appModel.computerLost.disconnect(computerLost)
|
||||
gamepadKeyNav.disable()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,17 +60,22 @@ GridView {
|
||||
|
||||
model: appModel
|
||||
|
||||
delegate: Item {
|
||||
width: 200; height: 300;
|
||||
delegate: NavigableItemDelegate {
|
||||
width: 200; height: 335;
|
||||
grid: appGrid
|
||||
|
||||
Image {
|
||||
id: appIcon
|
||||
anchors.horizontalCenter: parent.horizontalCenter;
|
||||
y: 20
|
||||
source: model.boxart
|
||||
sourceSize {
|
||||
width: 150
|
||||
height: 200
|
||||
}
|
||||
width: sourceSize.width
|
||||
height: sourceSize.height
|
||||
fillMode: Image.Pad
|
||||
}
|
||||
|
||||
Image {
|
||||
@@ -93,6 +100,12 @@ GridView {
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
wrapMode: Text.Wrap
|
||||
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()
|
||||
@@ -101,72 +114,47 @@ GridView {
|
||||
if (runningIndex >= 0 && runningIndex !== index) {
|
||||
quitAppDialog.appName = appModel.getRunningAppName()
|
||||
quitAppDialog.segueToStream = true
|
||||
quitAppDialog.nextAppName = model.name
|
||||
quitAppDialog.nextAppIndex = index
|
||||
quitAppDialog.open()
|
||||
return
|
||||
}
|
||||
|
||||
var component = Qt.createComponent("StreamSegue.qml")
|
||||
var segue = component.createObject(stackView)
|
||||
segue.appName = model.name
|
||||
segue.session = appModel.createSessionForApp(index)
|
||||
var segue = component.createObject(stackView, {"appName": model.name, "session": appModel.createSessionForApp(index)})
|
||||
stackView.push(segue)
|
||||
}
|
||||
|
||||
MessageDialog {
|
||||
id: quitAppDialog
|
||||
modality:Qt.WindowModal
|
||||
property string appName : "";
|
||||
property bool segueToStream : false
|
||||
text:"Are you sure you want to quit " + appName +"? Any unsaved progress will be lost."
|
||||
standardButtons: StandardButton.Yes | StandardButton.No
|
||||
onYes: {
|
||||
var component = Qt.createComponent("QuitSegue.qml")
|
||||
var segue = component.createObject(stackView)
|
||||
segue.appName = appName
|
||||
if (segueToStream) {
|
||||
// Store the session and app name if we're going to stream after
|
||||
// successfully quitting the old app.
|
||||
segue.nextAppName = model.name
|
||||
segue.nextSession = appModel.createSessionForApp(index)
|
||||
}
|
||||
else {
|
||||
segue.nextAppName = null
|
||||
segue.nextSession = null
|
||||
}
|
||||
|
||||
stackView.push(segue)
|
||||
|
||||
// Trigger the quit after pushing the quit segue on screen
|
||||
appModel.quitRunningApp()
|
||||
}
|
||||
onClicked: {
|
||||
// Nothing is running or this app is running
|
||||
launchOrResumeSelectedApp()
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
acceptedButtons: Qt.RightButton
|
||||
onClicked: {
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
// Nothing is running or this app is running
|
||||
launchOrResumeSelectedApp()
|
||||
}
|
||||
else {
|
||||
// Right click
|
||||
appContextMenu.open()
|
||||
}
|
||||
// popup() ensures the menu appears under the mouse cursor
|
||||
appContextMenu.popup()
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
id: appContextMenu
|
||||
MenuItem {
|
||||
NavigableMenuItem {
|
||||
text: model.running ? "Resume Game" : "Launch Game"
|
||||
onTriggered: {
|
||||
appContextMenu.close()
|
||||
launchOrResumeSelectedApp()
|
||||
}
|
||||
height: visible ? implicitHeight : 0
|
||||
}
|
||||
MenuItem {
|
||||
NavigableMenuItem {
|
||||
text: "Quit Game"
|
||||
onTriggered: {
|
||||
quitAppDialog.appName = appModel.getRunningAppName()
|
||||
@@ -174,11 +162,46 @@ GridView {
|
||||
quitAppDialog.open()
|
||||
}
|
||||
visible: model.running
|
||||
height: visible ? implicitHeight : 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MessageDialog {
|
||||
id: quitAppDialog
|
||||
modality:Qt.WindowModal
|
||||
property string appName : ""
|
||||
property bool segueToStream : false
|
||||
property string nextAppName: ""
|
||||
property int nextAppIndex: 0
|
||||
text:"Are you sure you want to quit " + appName +"? Any unsaved progress will be lost."
|
||||
standardButtons: StandardButton.Yes | StandardButton.No
|
||||
|
||||
function quitApp() {
|
||||
var component = Qt.createComponent("QuitSegue.qml")
|
||||
var params = {"appName": appName}
|
||||
if (segueToStream) {
|
||||
// Store the session and app name if we're going to stream after
|
||||
// successfully quitting the old app.
|
||||
params.nextAppName = nextAppName
|
||||
params.nextSession = appModel.createSessionForApp(nextAppIndex)
|
||||
}
|
||||
else {
|
||||
params.nextAppName = null
|
||||
params.nextSession = null
|
||||
}
|
||||
|
||||
stackView.push(component.createObject(stackView, params))
|
||||
|
||||
// Trigger the quit after pushing the quit segue on screen
|
||||
appModel.quitRunningApp()
|
||||
}
|
||||
|
||||
onYes: quitApp()
|
||||
|
||||
// For keyboard/gamepad navigation
|
||||
onAccepted: quitApp()
|
||||
}
|
||||
|
||||
ScrollBar.vertical: ScrollBar {
|
||||
parent: appGrid.parent
|
||||
anchors {
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import QtQuick 2.9
|
||||
import QtQuick.Controls 2.2
|
||||
|
||||
// https://stackoverflow.com/questions/45029968/how-do-i-set-the-combobox-width-to-fit-the-largest-item
|
||||
ComboBox {
|
||||
property int textWidth
|
||||
|
||||
implicitWidth: leftPadding + textWidth + indicator.width + rightPadding
|
||||
|
||||
TextMetrics {
|
||||
id: popupMetrics
|
||||
}
|
||||
|
||||
TextMetrics {
|
||||
id: textMetrics
|
||||
}
|
||||
|
||||
// We call this every time the options change (and init)
|
||||
// so we can adjust the combo box width here too
|
||||
onActivated: {
|
||||
textMetrics.font = font
|
||||
popupMetrics.font = popup.font
|
||||
textWidth = 0
|
||||
for (var i = 0; i < count; i++){
|
||||
textMetrics.text = textAt(i)
|
||||
popupMetrics.text = textAt(i)
|
||||
textWidth = Math.max(textMetrics.width, textWidth)
|
||||
textWidth = Math.max(popupMetrics.width, textWidth)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import QtQuick 2.0
|
||||
import QtQuick.Controls 2.2
|
||||
import QtQuick.Dialogs 1.2
|
||||
|
||||
import ComputerManager 1.0
|
||||
|
||||
Item {
|
||||
visible: false
|
||||
|
||||
function onSearchingComputer() {
|
||||
stageLabel.text = "Establishing connection to PC..."
|
||||
}
|
||||
|
||||
function onSearchingApp() {
|
||||
stageLabel.text = "Loading app list..."
|
||||
}
|
||||
|
||||
function onSessionCreated(appName, session) {
|
||||
var component = Qt.createComponent("StreamSegue.qml")
|
||||
var segue = component.createObject(stackView, {
|
||||
"appName": appName,
|
||||
"session": session,
|
||||
"quitAfter": true
|
||||
})
|
||||
stackView.push(segue)
|
||||
}
|
||||
|
||||
function onLaunchFailed(message) {
|
||||
errorDialog.text = message
|
||||
errorDialog.open()
|
||||
}
|
||||
|
||||
onVisibleChanged: {
|
||||
if (visible) {
|
||||
toolBar.visible = false
|
||||
launcher.searchingComputer.connect(onSearchingComputer)
|
||||
launcher.searchingApp.connect(onSearchingApp)
|
||||
launcher.sessionCreated.connect(onSessionCreated)
|
||||
launcher.failed.connect(onLaunchFailed)
|
||||
launcher.execute(ComputerManager)
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: 5
|
||||
|
||||
BusyIndicator {
|
||||
id: stageSpinner
|
||||
}
|
||||
|
||||
Label {
|
||||
id: stageLabel
|
||||
height: stageSpinner.height
|
||||
font.pointSize: 20
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
|
||||
wrapMode: Text.Wrap
|
||||
color: "white"
|
||||
}
|
||||
}
|
||||
|
||||
MessageDialog {
|
||||
id: errorDialog
|
||||
modality:Qt.WindowModal
|
||||
icon: StandardIcon.Critical
|
||||
standardButtons: StandardButton.Ok | StandardButton.Help
|
||||
onHelp: {
|
||||
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Troubleshooting");
|
||||
}
|
||||
onVisibleChanged: {
|
||||
if (!visible) {
|
||||
Qt.quit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import QtQuick 2.0
|
||||
import QtQuick.Controls 2.2
|
||||
|
||||
ItemDelegate {
|
||||
property GridView grid
|
||||
|
||||
highlighted: grid.activeFocus && grid.currentItem === this
|
||||
|
||||
Keys.onLeftPressed: {
|
||||
grid.moveCurrentIndexLeft()
|
||||
}
|
||||
Keys.onRightPressed: {
|
||||
grid.moveCurrentIndexRight()
|
||||
}
|
||||
Keys.onDownPressed: {
|
||||
grid.moveCurrentIndexDown()
|
||||
}
|
||||
Keys.onUpPressed: {
|
||||
grid.moveCurrentIndexUp()
|
||||
}
|
||||
Keys.onReturnPressed: {
|
||||
clicked()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import QtQuick 2.0
|
||||
import QtQuick.Controls 2.2
|
||||
|
||||
MenuItem {
|
||||
// Ensure focus can't be given to an invisible item
|
||||
enabled: visible
|
||||
height: visible ? implicitHeight : 0
|
||||
|
||||
Keys.onReturnPressed: {
|
||||
triggered()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import QtQuick 2.0
|
||||
import QtQuick.Controls 2.2
|
||||
|
||||
ToolButton {
|
||||
activeFocusOnTab: true
|
||||
|
||||
Keys.onReturnPressed: {
|
||||
clicked()
|
||||
}
|
||||
|
||||
Keys.onRightPressed: {
|
||||
nextItemInFocusChain(true).forceActiveFocus(Qt.TabFocus)
|
||||
}
|
||||
|
||||
Keys.onLeftPressed: {
|
||||
nextItemInFocusChain(false).forceActiveFocus(Qt.TabFocus)
|
||||
}
|
||||
}
|
||||
+79
-92
@@ -1,24 +1,27 @@
|
||||
import QtQuick 2.9
|
||||
import QtQuick.Controls 2.2
|
||||
import QtQuick.Dialogs 1.3
|
||||
import QtQuick.Dialogs 1.2
|
||||
import QtQuick.Layouts 1.3
|
||||
import QtQuick.Window 2.2
|
||||
|
||||
import ComputerModel 1.0
|
||||
|
||||
import ComputerManager 1.0
|
||||
import StreamingPreferences 1.0
|
||||
import SdlGamepadKeyNavigation 1.0
|
||||
|
||||
GridView {
|
||||
property ComputerModel computerModel : createModel()
|
||||
|
||||
id: pcGrid
|
||||
focus: true
|
||||
activeFocusOnTab: true
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: (parent.width % (cellWidth + anchors.rightMargin)) / 2
|
||||
anchors.topMargin: 20
|
||||
anchors.rightMargin: 5
|
||||
anchors.bottomMargin: 5
|
||||
cellWidth: 350; cellHeight: 350;
|
||||
focus: true
|
||||
objectName: "Computers"
|
||||
|
||||
// The StackView will trigger a visibility change when
|
||||
@@ -30,39 +33,34 @@ GridView {
|
||||
id: prefs
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
// Setup signals on CM
|
||||
ComputerManager.computerAddCompleted.connect(addComplete)
|
||||
|
||||
if (prefs.isRunningWayland()) {
|
||||
waylandDialog.open()
|
||||
}
|
||||
else if (!prefs.hasAnyHardwareAcceleration()) {
|
||||
noHwDecoderDialog.open()
|
||||
}
|
||||
SdlGamepadKeyNavigation {
|
||||
id: gamepadKeyNav
|
||||
}
|
||||
|
||||
onVisibleChanged: {
|
||||
if (visible) {
|
||||
// Start polling when this view is shown
|
||||
ComputerManager.startPolling()
|
||||
gamepadKeyNav.enable()
|
||||
}
|
||||
else {
|
||||
// Stop polling when this view is not top-most
|
||||
ComputerManager.stopPollingAsync()
|
||||
gamepadKeyNav.disable()
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
// Setup signals on CM
|
||||
ComputerManager.computerAddCompleted.connect(addComplete)
|
||||
|
||||
// Don't show any highlighted item until interacting with them
|
||||
currentIndex = -1
|
||||
}
|
||||
|
||||
function pairingComplete(error)
|
||||
{
|
||||
// Close the PIN dialog
|
||||
pairDialog.close()
|
||||
|
||||
// Start polling again
|
||||
ComputerManager.startPolling()
|
||||
|
||||
// Display a failed dialog if we got an error
|
||||
if (error !== null) {
|
||||
if (error !== undefined) {
|
||||
errorDialog.text = error
|
||||
errorDialog.open()
|
||||
}
|
||||
@@ -71,7 +69,7 @@ GridView {
|
||||
function addComplete(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()
|
||||
}
|
||||
}
|
||||
@@ -86,8 +84,9 @@ GridView {
|
||||
|
||||
model: computerModel
|
||||
|
||||
delegate: Item {
|
||||
delegate: NavigableItemDelegate {
|
||||
width: 300; height: 300;
|
||||
grid: pcGrid
|
||||
|
||||
Image {
|
||||
id: pcIcon
|
||||
@@ -139,13 +138,12 @@ GridView {
|
||||
|
||||
Menu {
|
||||
id: pcContextMenu
|
||||
MenuItem {
|
||||
NavigableMenuItem {
|
||||
text: "Wake PC"
|
||||
onTriggered: computerModel.wakeComputer(index)
|
||||
visible: !model.addPc && !model.online && model.wakeable
|
||||
height: visible ? implicitHeight : 0
|
||||
}
|
||||
MenuItem {
|
||||
NavigableMenuItem {
|
||||
text: "Delete PC"
|
||||
onTriggered: {
|
||||
deletePcDialog.pcIndex = index
|
||||
@@ -155,55 +153,58 @@ GridView {
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
if (model.addPc) {
|
||||
addPcDialog.open()
|
||||
}
|
||||
else if (model.online) {
|
||||
if (model.paired) {
|
||||
// go to game view
|
||||
var component = Qt.createComponent("AppView.qml")
|
||||
var appView = component.createObject(stackView, {"computerIndex": index, "objectName": model.name})
|
||||
stackView.push(appView)
|
||||
}
|
||||
else {
|
||||
if (!model.busy) {
|
||||
var pin = ("0000" + Math.floor(Math.random() * 10000)).slice(-4)
|
||||
|
||||
// Kick off pairing in the background
|
||||
computerModel.pairComputer(index, pin)
|
||||
|
||||
// Display the pairing dialog
|
||||
pairDialog.pin = pin
|
||||
pairDialog.open()
|
||||
}
|
||||
else {
|
||||
// cannot pair while something is streaming or attempting to pair
|
||||
errorDialog.text = "This PC is currently busy. Make sure to quit any running games and try again."
|
||||
errorDialog.open()
|
||||
}
|
||||
}
|
||||
} else if (!model.online) {
|
||||
// Using open() here because it may be activated by keyboard
|
||||
pcContextMenu.open()
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton;
|
||||
acceptedButtons: Qt.RightButton;
|
||||
onClicked: {
|
||||
if(mouse.button === Qt.LeftButton) {
|
||||
if (model.addPc) {
|
||||
addPcDialog.open()
|
||||
}
|
||||
else if (model.online) {
|
||||
if (model.paired) {
|
||||
// go to game view
|
||||
var component = Qt.createComponent("AppView.qml")
|
||||
var appView = component.createObject(stackView)
|
||||
appView.computerIndex = index
|
||||
appView.objectName = model.name
|
||||
stackView.push(appView)
|
||||
}
|
||||
else {
|
||||
if (!model.busy) {
|
||||
var pin = ("0000" + Math.floor(Math.random() * 10000)).slice(-4)
|
||||
|
||||
// Stop polling, since pairing may make GFE unresponsive
|
||||
ComputerManager.stopPollingAsync()
|
||||
|
||||
// Kick off pairing in the background
|
||||
computerModel.pairComputer(index, pin)
|
||||
|
||||
// Display the pairing dialog
|
||||
pairDialog.pin = pin
|
||||
pairDialog.open()
|
||||
}
|
||||
else {
|
||||
// cannot pair while something is streaming or attempting to pair
|
||||
errorDialog.text = "This PC is currently busy. Make sure to quit any running games and try again."
|
||||
errorDialog.open()
|
||||
}
|
||||
}
|
||||
} else if(!model.online) {
|
||||
pcContextMenu.open()
|
||||
}
|
||||
|
||||
}
|
||||
else { // right click
|
||||
if(!model.addPc) { // but only for actual PCs, not the add-pc option
|
||||
pcContextMenu.open()
|
||||
}
|
||||
if (!model.addPc) {
|
||||
// popup() ensures the menu appears under the mouse cursor
|
||||
pcContextMenu.popup()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -211,31 +212,11 @@ GridView {
|
||||
// don't allow edits to the rest of the window while open
|
||||
modality:Qt.WindowModal
|
||||
icon: StandardIcon.Critical
|
||||
standardButtons: StandardButton.Ok
|
||||
}
|
||||
|
||||
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");
|
||||
// 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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -258,10 +239,16 @@ GridView {
|
||||
property int pcIndex : -1;
|
||||
text:"Are you sure you want to remove this PC?"
|
||||
standardButtons: StandardButton.Yes | StandardButton.No
|
||||
onYes: {
|
||||
|
||||
function deletePc() {
|
||||
console.log("deleting PC pairing for PC at index: " + pcIndex)
|
||||
computerModel.deleteComputer(pcIndex);
|
||||
}
|
||||
|
||||
onYes: deletePc()
|
||||
|
||||
// For keyboard/gamepad activation
|
||||
onAccepted: deletePc()
|
||||
}
|
||||
|
||||
Dialog {
|
||||
|
||||
+2
-10
@@ -1,6 +1,6 @@
|
||||
import QtQuick 2.0
|
||||
import QtQuick.Controls 2.2
|
||||
import QtQuick.Dialogs 1.3
|
||||
import QtQuick.Dialogs 1.2
|
||||
|
||||
import ComputerManager 1.0
|
||||
import Session 1.0
|
||||
@@ -33,9 +33,7 @@ Item {
|
||||
// If we're supposed to launch another game after this, do so now
|
||||
if (error === undefined && nextSession !== null) {
|
||||
var component = Qt.createComponent("StreamSegue.qml")
|
||||
var segue = component.createObject(stackView)
|
||||
segue.appName = nextAppName
|
||||
segue.session = nextSession
|
||||
var segue = component.createObject(stackView, {"appName": nextAppName, "session": nextSession})
|
||||
stackView.push(segue)
|
||||
}
|
||||
}
|
||||
@@ -44,16 +42,10 @@ Item {
|
||||
if (visible) {
|
||||
// Connect the quit completion signal
|
||||
ComputerManager.quitAppCompleted.connect(quitAppCompleted)
|
||||
|
||||
// We must be polling or we won't get the quitAppCompleted() callback
|
||||
ComputerManager.startPolling()
|
||||
}
|
||||
else {
|
||||
// Disconnect the signal
|
||||
ComputerManager.quitAppCompleted.disconnect(quitAppCompleted)
|
||||
|
||||
// Stop polling when this view is not on top
|
||||
ComputerManager.stopPollingAsync()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+249
-85
@@ -2,6 +2,8 @@ import QtQuick 2.9
|
||||
import QtQuick.Controls 2.2
|
||||
|
||||
import StreamingPreferences 1.0
|
||||
import ComputerManager 1.0
|
||||
import SdlGamepadKeyNavigation 1.0
|
||||
|
||||
ScrollView {
|
||||
id: settingsPage
|
||||
@@ -11,6 +13,25 @@ ScrollView {
|
||||
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: {
|
||||
prefs.save()
|
||||
}
|
||||
@@ -52,41 +73,55 @@ ScrollView {
|
||||
Row {
|
||||
spacing: 5
|
||||
|
||||
ComboBox {
|
||||
AutoResizingComboBox {
|
||||
// ignore setting the index at first, and actually set it when the component is loaded
|
||||
Component.onCompleted: {
|
||||
// Add native resolutions for all attached displays
|
||||
for (var displayIndex = 0;; displayIndex++) {
|
||||
var screenRect = prefs.getDisplayResolution(displayIndex)
|
||||
if (screenRect.width === 0) {
|
||||
// Exceeded max count of displays
|
||||
break
|
||||
}
|
||||
var done = false
|
||||
for (var displayIndex = 0; !done; displayIndex++) {
|
||||
for (var displayResIndex = 0; displayResIndex < 2; displayResIndex++) {
|
||||
var screenRect;
|
||||
|
||||
var indexToAdd = 0
|
||||
for (var j = 0; j < resolutionComboBox.count; j++) {
|
||||
var existing_width = parseInt(resolutionListModel.get(j).video_width);
|
||||
var existing_height = parseInt(resolutionListModel.get(j).video_height);
|
||||
// Some platforms have different desktop resolutions
|
||||
// and native resolutions (like macOS with Retina displays)
|
||||
if (displayResIndex == 0) {
|
||||
screenRect = prefs.getDesktopResolution(displayIndex)
|
||||
}
|
||||
else {
|
||||
screenRect = prefs.getNativeResolution(displayIndex)
|
||||
}
|
||||
|
||||
if (screenRect.width === existing_width && screenRect.height === existing_height) {
|
||||
// Duplicate entry, skip
|
||||
indexToAdd = -1
|
||||
if (screenRect.width === 0) {
|
||||
// Exceeded max count of displays
|
||||
done = true
|
||||
break
|
||||
}
|
||||
else if (screenRect.width * screenRect.height > existing_width * existing_height) {
|
||||
// Candidate entrypoint after this entry
|
||||
indexToAdd = j + 1
|
||||
}
|
||||
}
|
||||
|
||||
// Insert this display's resolution if it's not a duplicate
|
||||
if (indexToAdd >= 0) {
|
||||
resolutionListModel.insert(indexToAdd,
|
||||
{
|
||||
"text": "Native ("+screenRect.width+"x"+screenRect.height+")",
|
||||
"video_width": ""+screenRect.width,
|
||||
"video_height": ""+screenRect.height
|
||||
})
|
||||
var indexToAdd = 0
|
||||
for (var j = 0; j < resolutionComboBox.count; j++) {
|
||||
var existing_width = parseInt(resolutionListModel.get(j).video_width);
|
||||
var existing_height = parseInt(resolutionListModel.get(j).video_height);
|
||||
|
||||
if (screenRect.width === existing_width && screenRect.height === existing_height) {
|
||||
// Duplicate entry, skip
|
||||
indexToAdd = -1
|
||||
break
|
||||
}
|
||||
else if (screenRect.width * screenRect.height > existing_width * existing_height) {
|
||||
// Candidate entrypoint after this entry
|
||||
indexToAdd = j + 1
|
||||
}
|
||||
}
|
||||
|
||||
// Insert this display's resolution if it's not a duplicate
|
||||
if (indexToAdd >= 0) {
|
||||
resolutionListModel.insert(indexToAdd,
|
||||
{
|
||||
"text": "Native ("+screenRect.width+"x"+screenRect.height+")",
|
||||
"video_width": ""+screenRect.width,
|
||||
"video_height": ""+screenRect.height
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,7 +130,7 @@ ScrollView {
|
||||
var saved_width = prefs.width
|
||||
var saved_height = prefs.height
|
||||
currentIndex = 0
|
||||
for (var i = 0; i < resolutionComboBox.count; i++) {
|
||||
for (var i = 0; i < resolutionListModel.count; i++) {
|
||||
var el_width = parseInt(resolutionListModel.get(i).video_width);
|
||||
var el_height = parseInt(resolutionListModel.get(i).video_height);
|
||||
|
||||
@@ -104,10 +139,12 @@ ScrollView {
|
||||
currentIndex = i
|
||||
}
|
||||
}
|
||||
|
||||
// Persist the selected value
|
||||
activated(currentIndex)
|
||||
}
|
||||
|
||||
id: resolutionComboBox
|
||||
width: 200
|
||||
textRole: "text"
|
||||
model: ListModel {
|
||||
id: resolutionListModel
|
||||
@@ -136,59 +173,96 @@ ScrollView {
|
||||
}
|
||||
// ::onActivated must be used, as it only listens for when the index is changed by a human
|
||||
onActivated : {
|
||||
prefs.width = parseInt(resolutionListModel.get(currentIndex).video_width)
|
||||
prefs.height = parseInt(resolutionListModel.get(currentIndex).video_height)
|
||||
var selectedWidth = parseInt(resolutionListModel.get(currentIndex).video_width)
|
||||
var selectedHeight = parseInt(resolutionListModel.get(currentIndex).video_height)
|
||||
|
||||
prefs.bitrateKbps = prefs.getDefaultBitrate(prefs.width, prefs.height, prefs.fps);
|
||||
slider.value = prefs.bitrateKbps
|
||||
// Only modify the bitrate if the values actually changed
|
||||
if (prefs.width !== selectedWidth || prefs.height !== selectedHeight) {
|
||||
prefs.width = selectedWidth
|
||||
prefs.height = selectedHeight
|
||||
|
||||
prefs.bitrateKbps = prefs.getDefaultBitrate(prefs.width, prefs.height, prefs.fps);
|
||||
slider.value = prefs.bitrateKbps
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ComboBox {
|
||||
// ignore setting the index at first, and actually set it when the component is loaded
|
||||
Component.onCompleted: {
|
||||
AutoResizingComboBox {
|
||||
function createModel() {
|
||||
var fpsListModel = Qt.createQmlObject('import QtQuick 2.0; ListModel {}', parent, '')
|
||||
|
||||
// Get the max supported FPS on this system
|
||||
var max_fps = prefs.getMaximumStreamingFrameRate();
|
||||
|
||||
// Default entries
|
||||
fpsListModel.append({"text": "30 FPS", "video_fps": "30"})
|
||||
fpsListModel.append({"text": "60 FPS", "video_fps": "60"})
|
||||
|
||||
// Add unsupported FPS values that come before the display max FPS
|
||||
if (prefs.unsupportedFps) {
|
||||
if (max_fps > 90) {
|
||||
fpsListModel.append({"text": "90 FPS (Unsupported)", "video_fps": "90"})
|
||||
}
|
||||
if (max_fps > 120) {
|
||||
fpsListModel.append({"text": "120 FPS (Unsupported)", "video_fps": "120"})
|
||||
}
|
||||
}
|
||||
|
||||
// Use 64 as the cutoff for adding a separate option to
|
||||
// handle wonky displays that report just over 60 Hz.
|
||||
if (max_fps > 64) {
|
||||
fpsListModel.append({"text": max_fps+" FPS", "video_fps": ""+max_fps})
|
||||
}
|
||||
|
||||
// Add unsupported FPS values that come after the display max FPS
|
||||
if (prefs.unsupportedFps) {
|
||||
if (max_fps < 90) {
|
||||
fpsListModel.append({"text": "90 FPS (Unsupported)", "video_fps": "90"})
|
||||
}
|
||||
if (max_fps < 120) {
|
||||
fpsListModel.append({"text": "120 FPS (Unsupported)", "video_fps": "120"})
|
||||
}
|
||||
}
|
||||
|
||||
return fpsListModel
|
||||
}
|
||||
|
||||
function reinitialize() {
|
||||
model = createModel()
|
||||
|
||||
var saved_fps = prefs.fps
|
||||
currentIndex = 0
|
||||
for (var i = 0; i < fpsComboBox.count; i++) {
|
||||
var el_fps = parseInt(fpsListModel.get(i).video_fps);
|
||||
for (var i = 0; i < model.count; i++) {
|
||||
var el_fps = parseInt(model.get(i).video_fps);
|
||||
|
||||
// Pick the highest value lesser or equal to the saved FPS
|
||||
if (saved_fps >= el_fps) {
|
||||
currentIndex = i
|
||||
}
|
||||
}
|
||||
|
||||
// Persist the selected value
|
||||
activated(currentIndex)
|
||||
}
|
||||
|
||||
// ignore setting the index at first, and actually set it when the component is loaded
|
||||
Component.onCompleted: {
|
||||
reinitialize()
|
||||
}
|
||||
|
||||
id: fpsComboBox
|
||||
textRole: "text"
|
||||
model: ListModel {
|
||||
id: fpsListModel
|
||||
ListElement {
|
||||
text: "30 FPS"
|
||||
video_fps: "30"
|
||||
}
|
||||
ListElement {
|
||||
text: "60 FPS"
|
||||
video_fps: "60"
|
||||
}
|
||||
// A higher value may be added at runtime
|
||||
// based on the attached display refresh rate
|
||||
}
|
||||
// ::onActivated must be used, as it only listens for when the index is changed by a human
|
||||
onActivated : {
|
||||
prefs.fps = parseInt(fpsListModel.get(currentIndex).video_fps)
|
||||
var selectedFps = parseInt(model.get(currentIndex).video_fps)
|
||||
|
||||
prefs.bitrateKbps = prefs.getDefaultBitrate(prefs.width, prefs.height, prefs.fps);
|
||||
slider.value = prefs.bitrateKbps
|
||||
// Only modify the bitrate if the values actually changed
|
||||
if (prefs.fps !== selectedFps) {
|
||||
prefs.fps = selectedFps
|
||||
|
||||
prefs.bitrateKbps = prefs.getDefaultBitrate(prefs.width, prefs.height, prefs.fps);
|
||||
slider.value = prefs.bitrateKbps
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -230,13 +304,59 @@ ScrollView {
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
width: parent.width
|
||||
id: windowModeTitle
|
||||
text: qsTr("Display mode")
|
||||
font.pointSize: 12
|
||||
wrapMode: Text.Wrap
|
||||
color: "white"
|
||||
}
|
||||
|
||||
AutoResizingComboBox {
|
||||
// ignore setting the index at first, and actually set it when the component is loaded
|
||||
Component.onCompleted: {
|
||||
var savedWm = prefs.windowMode
|
||||
currentIndex = 0
|
||||
for (var i = 0; i < windowModeListModel.count; i++) {
|
||||
var thisWm = windowModeListModel.get(i).val;
|
||||
if (savedWm === thisWm) {
|
||||
currentIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
activated(currentIndex)
|
||||
}
|
||||
|
||||
id: windowModeComboBox
|
||||
textRole: "text"
|
||||
model: ListModel {
|
||||
id: windowModeListModel
|
||||
ListElement {
|
||||
text: "Full-screen (Recommended)"
|
||||
val: StreamingPreferences.WM_FULLSCREEN
|
||||
}
|
||||
ListElement {
|
||||
text: "Borderless windowed"
|
||||
val: StreamingPreferences.WM_FULLSCREEN_DESKTOP
|
||||
}
|
||||
ListElement {
|
||||
text: "Windowed"
|
||||
val: StreamingPreferences.WM_WINDOWED
|
||||
}
|
||||
}
|
||||
onActivated: {
|
||||
prefs.windowMode = windowModeListModel.get(currentIndex).val
|
||||
}
|
||||
}
|
||||
|
||||
CheckBox {
|
||||
id: fullScreenCheck
|
||||
text: "<font color=\"white\">Full-screen</font>"
|
||||
id: vsyncCheck
|
||||
text: "<font color=\"white\">Enable V-Sync</font>"
|
||||
font.pointSize: 12
|
||||
checked: prefs.fullScreen
|
||||
checked: prefs.enableVsync
|
||||
onCheckedChanged: {
|
||||
prefs.fullScreen = checked
|
||||
prefs.enableVsync = checked
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -263,35 +383,32 @@ ScrollView {
|
||||
color: "white"
|
||||
}
|
||||
|
||||
ComboBox {
|
||||
AutoResizingComboBox {
|
||||
// ignore setting the index at first, and actually set it when the component is loaded
|
||||
Component.onCompleted: {
|
||||
var saved_audio = prefs.audioConfig
|
||||
currentIndex = 0
|
||||
for(var i = 0; i < audioListModel.count; i++) {
|
||||
var el_audio= audioListModel.get(i).val;
|
||||
if(saved_audio === el_audio) {
|
||||
currentIndex = i
|
||||
}
|
||||
for (var i = 0; i < audioListModel.count; i++) {
|
||||
var el_audio = audioListModel.get(i).val;
|
||||
if (saved_audio === el_audio) {
|
||||
currentIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
activated(currentIndex)
|
||||
}
|
||||
|
||||
id: audioComboBox
|
||||
width: Math.min(bitrateDesc.implicitWidth, parent.width)
|
||||
textRole: "text"
|
||||
model: ListModel {
|
||||
id: audioListModel
|
||||
ListElement {
|
||||
text: "Autodetect"
|
||||
val: StreamingPreferences.AC_AUTO
|
||||
}
|
||||
ListElement {
|
||||
text: "Stereo"
|
||||
val: StreamingPreferences.AC_FORCE_STEREO
|
||||
val: StreamingPreferences.AC_STEREO
|
||||
}
|
||||
ListElement {
|
||||
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
|
||||
@@ -314,10 +431,10 @@ ScrollView {
|
||||
id: gamepadSettingsGroupBox
|
||||
width: (parent.width - parent.padding)
|
||||
padding: 12
|
||||
title: "<font color=\"skyblue\">Gamepad Settings</font>"
|
||||
title: "<font color=\"skyblue\">Input Settings</font>"
|
||||
font.pointSize: 12
|
||||
|
||||
Column {
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
spacing: 5
|
||||
|
||||
@@ -330,6 +447,16 @@ ScrollView {
|
||||
prefs.multiController = checked
|
||||
}
|
||||
}
|
||||
|
||||
CheckBox {
|
||||
id: mouseAccelerationCheck
|
||||
text: "<font color=\"white\">Enable mouse acceleration</font>"
|
||||
font.pointSize: 12
|
||||
checked: prefs.mouseAcceleration
|
||||
onCheckedChanged: {
|
||||
prefs.mouseAcceleration = checked
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -340,13 +467,15 @@ ScrollView {
|
||||
title: "<font color=\"skyblue\">Host Settings</font>"
|
||||
font.pointSize: 12
|
||||
|
||||
Column {
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
spacing: 5
|
||||
|
||||
CheckBox {
|
||||
id: optimizeGameSettingsCheck
|
||||
text: "<font color=\"white\">Optimize game settings</font>"
|
||||
// HACK: Match width of the other checkbox to make the UI not look bad
|
||||
width: multiControllerCheck.width
|
||||
font.pointSize: 12
|
||||
checked: prefs.gameOptimizations
|
||||
onCheckedChanged: {
|
||||
@@ -386,21 +515,22 @@ ScrollView {
|
||||
color: "white"
|
||||
}
|
||||
|
||||
ComboBox {
|
||||
AutoResizingComboBox {
|
||||
// ignore setting the index at first, and actually set it when the component is loaded
|
||||
Component.onCompleted: {
|
||||
var saved_vds = prefs.videoDecoderSelection
|
||||
currentIndex = 0
|
||||
for(var i = 0; i < decoderListModel.count; i++) {
|
||||
for (var i = 0; i < decoderListModel.count; i++) {
|
||||
var el_vds = decoderListModel.get(i).val;
|
||||
if(saved_vds === el_vds) {
|
||||
currentIndex = i
|
||||
}
|
||||
if (saved_vds === el_vds) {
|
||||
currentIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
activated(currentIndex)
|
||||
}
|
||||
|
||||
id: decoderComboBox
|
||||
width: Math.min(bitrateDesc.implicitWidth, parent.width)
|
||||
textRole: "text"
|
||||
model: ListModel {
|
||||
id: decoderListModel
|
||||
@@ -432,21 +562,22 @@ ScrollView {
|
||||
color: "white"
|
||||
}
|
||||
|
||||
ComboBox {
|
||||
AutoResizingComboBox {
|
||||
// ignore setting the index at first, and actually set it when the component is loaded
|
||||
Component.onCompleted: {
|
||||
var saved_vcc = prefs.videoCodecConfig
|
||||
currentIndex = 0
|
||||
for(var i = 0; i < codecListModel.count; i++) {
|
||||
var el_vcc = codecListModel.get(i).val;
|
||||
if(saved_vcc === el_vcc) {
|
||||
currentIndex = i
|
||||
}
|
||||
if (saved_vcc === el_vcc) {
|
||||
currentIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
activated(currentIndex)
|
||||
}
|
||||
|
||||
id: codecComboBox
|
||||
width: Math.min(bitrateDesc.implicitWidth, parent.width)
|
||||
textRole: "text"
|
||||
model: ListModel {
|
||||
id: codecListModel
|
||||
@@ -476,6 +607,39 @@ ScrollView {
|
||||
}
|
||||
}
|
||||
|
||||
CheckBox {
|
||||
id: unlockUnsupportedFps
|
||||
text: "<font color=\"white\">Unlock unsupported FPS options</font>"
|
||||
font.pointSize: 12
|
||||
checked: prefs.unsupportedFps
|
||||
onCheckedChanged: {
|
||||
prefs.unsupportedFps = checked
|
||||
|
||||
// The selectable FPS values depend on whether
|
||||
// this option is enabled or not
|
||||
fpsComboBox.reinitialize()
|
||||
}
|
||||
}
|
||||
|
||||
CheckBox {
|
||||
id: enableMdns
|
||||
text: "<font color=\"white\">Automatically find PCs on the local network (Recommended)</font>"
|
||||
font.pointSize: 12
|
||||
checked: prefs.enableMdns
|
||||
onCheckedChanged: {
|
||||
prefs.enableMdns = checked
|
||||
|
||||
// We must save the updated preference to ensure
|
||||
// ComputerManager can observe the change internally.
|
||||
prefs.save()
|
||||
|
||||
// Restart polling so the mDNS change takes effect
|
||||
if (window.pollingActive) {
|
||||
ComputerManager.stopPollingAsync()
|
||||
ComputerManager.startPolling()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+37
-8
@@ -1,6 +1,7 @@
|
||||
import QtQuick 2.0
|
||||
import QtQuick.Controls 2.2
|
||||
import QtQuick.Dialogs 1.3
|
||||
import QtQuick.Dialogs 1.2
|
||||
import QtQuick.Window 2.2
|
||||
|
||||
import Session 1.0
|
||||
|
||||
@@ -8,6 +9,7 @@ Item {
|
||||
property Session session
|
||||
property string appName
|
||||
property string stageText : "Starting " + appName + "..."
|
||||
property bool quitAfter : false
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
@@ -41,8 +43,8 @@ Item {
|
||||
|
||||
function displayLaunchError(text)
|
||||
{
|
||||
// Display the error dialog after Session::exec() returns
|
||||
errorDialog.text = text
|
||||
errorDialog.open()
|
||||
}
|
||||
|
||||
function displayLaunchWarning(text)
|
||||
@@ -69,13 +71,25 @@ Item {
|
||||
session.displayLaunchWarning.connect(displayLaunchWarning)
|
||||
|
||||
// Run the streaming session to completion
|
||||
session.exec()
|
||||
session.exec(Screen.virtualX, Screen.virtualY)
|
||||
|
||||
// Show the Qt window again after streaming
|
||||
window.visible = true
|
||||
if (quitAfter) {
|
||||
Qt.quit()
|
||||
} else {
|
||||
// Show the Qt window again after streaming
|
||||
window.visible = true
|
||||
|
||||
// Exit this view
|
||||
stackView.pop()
|
||||
// Exit this view
|
||||
stackView.pop()
|
||||
|
||||
// Display any launch errors. We do this after
|
||||
// the Qt UI is visible again to prevent losing
|
||||
// focus on the dialog which would impact gamepad
|
||||
// users.
|
||||
if (errorDialog.text) {
|
||||
errorDialog.open()
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Show the toolbar again when we become hidden
|
||||
@@ -103,10 +117,25 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
text: "Tip: Press Ctrl+Alt+Shift+Q to disconnect your session"
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: 50
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
font.pointSize: 18
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
|
||||
wrapMode: Text.Wrap
|
||||
color: "white"
|
||||
}
|
||||
|
||||
MessageDialog {
|
||||
id: errorDialog
|
||||
modality:Qt.WindowModal
|
||||
icon: StandardIcon.Critical
|
||||
standardButtons: StandardButton.Ok
|
||||
standardButtons: StandardButton.Ok | StandardButton.Help
|
||||
onHelp: {
|
||||
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Troubleshooting");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
#include "backend/boxartmanager.h"
|
||||
#include "backend/computermanager.h"
|
||||
#include "streaming/session.hpp"
|
||||
#include "streaming/session.h"
|
||||
|
||||
#include <QAbstractListModel>
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#include "computermodel.h"
|
||||
|
||||
#include <QThreadPool>
|
||||
|
||||
ComputerModel::ComputerModel(QObject* object)
|
||||
: QAbstractListModel(object) {}
|
||||
|
||||
@@ -119,11 +121,27 @@ void ComputerModel::deleteComputer(int computerIndex)
|
||||
endRemoveRows();
|
||||
}
|
||||
|
||||
bool ComputerModel::wakeComputer(int computerIndex)
|
||||
class DeferredWakeHostTask : public QRunnable
|
||||
{
|
||||
public:
|
||||
DeferredWakeHostTask(NvComputer* computer)
|
||||
: m_Computer(computer) {}
|
||||
|
||||
void run()
|
||||
{
|
||||
m_Computer->wake();
|
||||
}
|
||||
|
||||
private:
|
||||
NvComputer* m_Computer;
|
||||
};
|
||||
|
||||
void ComputerModel::wakeComputer(int computerIndex)
|
||||
{
|
||||
Q_ASSERT(computerIndex < m_Computers.count());
|
||||
|
||||
return m_Computers[computerIndex]->wake();
|
||||
DeferredWakeHostTask* wakeTask = new DeferredWakeHostTask(m_Computers[computerIndex]);
|
||||
QThreadPool::globalInstance()->start(wakeTask);
|
||||
}
|
||||
|
||||
void ComputerModel::pairComputer(int computerIndex, QString pin)
|
||||
@@ -135,7 +153,7 @@ void ComputerModel::pairComputer(int computerIndex, QString pin)
|
||||
|
||||
void ComputerModel::handlePairingCompleted(NvComputer*, QString error)
|
||||
{
|
||||
emit pairingCompleted(error.isNull() ? QVariant() : error);
|
||||
emit pairingCompleted(error.isEmpty() ? QVariant() : error);
|
||||
}
|
||||
|
||||
void ComputerModel::handleComputerStateChanged(NvComputer* computer)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#include "backend/computermanager.h"
|
||||
#include "streaming/session.hpp"
|
||||
#include "streaming/session.h"
|
||||
|
||||
#include <QAbstractListModel>
|
||||
|
||||
@@ -34,7 +34,7 @@ public:
|
||||
|
||||
Q_INVOKABLE void pairComputer(int computerIndex, QString pin);
|
||||
|
||||
Q_INVOKABLE bool wakeComputer(int computerIndex);
|
||||
Q_INVOKABLE void wakeComputer(int computerIndex);
|
||||
|
||||
Q_INVOKABLE Session* createSessionForCurrentGame(int computerIndex);
|
||||
|
||||
|
||||
+268
-35
@@ -1,24 +1,141 @@
|
||||
import QtQuick 2.9
|
||||
import QtQuick.Controls 2.2
|
||||
import QtQuick.Dialogs 1.2
|
||||
import QtQuick.Layouts 1.3
|
||||
import QtQuick.Window 2.2
|
||||
|
||||
import QtQuick.Controls.Material 2.1
|
||||
|
||||
import ComputerManager 1.0
|
||||
import AutoUpdateChecker 1.0
|
||||
import StreamingPreferences 1.0
|
||||
|
||||
ApplicationWindow {
|
||||
property bool pollingActive: false
|
||||
|
||||
id: window
|
||||
visible: true
|
||||
width: 1280
|
||||
height: 600
|
||||
|
||||
// Maximize the window by default when the stream is configured
|
||||
// for full-screen or borderless windowed. This is ideal for TV
|
||||
// setups where the user doesn't want a tiny window in the middle
|
||||
// of their screen when starting Moonlight.
|
||||
visibility: prefs.windowMode != StreamingPreferences.WM_WINDOWED ? "Maximized" : "Windowed"
|
||||
|
||||
Material.theme: Material.Dark
|
||||
Material.accent: Material.Purple
|
||||
|
||||
StreamingPreferences {
|
||||
id: prefs
|
||||
}
|
||||
|
||||
StackView {
|
||||
id: stackView
|
||||
initialItem: "PcView.qml"
|
||||
initialItem: initialView
|
||||
anchors.fill: parent
|
||||
focus: true
|
||||
|
||||
onCurrentItemChanged: {
|
||||
// Ensure focus travels to the next view when going back
|
||||
if (currentItem) {
|
||||
currentItem.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onEscapePressed: {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onActiveChanged: {
|
||||
if (active) {
|
||||
// Stop the inactivity timer
|
||||
inactivityTimer.stop()
|
||||
|
||||
// Restart polling if it was stopped
|
||||
if (!pollingActive) {
|
||||
ComputerManager.startPolling()
|
||||
pollingActive = true
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Start the inactivity timer to stop polling
|
||||
// if focus does not return within a few minutes.
|
||||
inactivityTimer.restart()
|
||||
}
|
||||
}
|
||||
|
||||
property bool initialized: false
|
||||
|
||||
// BUG: Using onAfterSynchronizing: here causes very strange
|
||||
// failures on Linux. Many shaders fail to compile and we
|
||||
// eventually segfault deep inside the Qt OpenGL code.
|
||||
onAfterRendering: {
|
||||
// We use this callback to trigger dialog display because
|
||||
// it only happens once the window is fully constructed.
|
||||
// Doing it earlier can lead to the dialog appearing behind
|
||||
// the window or otherwise without input focus.
|
||||
if (!initialized) {
|
||||
if (prefs.isRunningWayland()) {
|
||||
waylandDialog.open()
|
||||
}
|
||||
else if (prefs.isWow64()) {
|
||||
wow64Dialog.open()
|
||||
}
|
||||
else if (!prefs.hasAnyHardwareAcceleration()) {
|
||||
noHwDecoderDialog.open()
|
||||
}
|
||||
|
||||
var unmappedGamepads = prefs.getUnmappedGamepads()
|
||||
if (unmappedGamepads) {
|
||||
unmappedGamepadDialog.unmappedGamepads = unmappedGamepads
|
||||
unmappedGamepadDialog.open()
|
||||
}
|
||||
|
||||
initialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
function navigateTo(url, objectName)
|
||||
@@ -39,22 +156,33 @@ ApplicationWindow {
|
||||
|
||||
header: ToolBar {
|
||||
id: toolBar
|
||||
anchors.topMargin: 5
|
||||
anchors.bottomMargin: 5
|
||||
|
||||
RowLayout {
|
||||
spacing: 20
|
||||
anchors.fill: parent
|
||||
|
||||
ToolButton {
|
||||
NavigableToolButton {
|
||||
// Only make the button visible if the user has navigated somewhere.
|
||||
visible: stackView.depth > 1
|
||||
|
||||
icon.source: "qrc:/res/arrow_left.svg"
|
||||
// FIXME: We're using an Image here rather than icon.source because
|
||||
// icons don't work on Qt 5.9 LTS.
|
||||
Image {
|
||||
source: "qrc:/res/arrow_left.svg"
|
||||
anchors.centerIn: parent
|
||||
sourceSize {
|
||||
// The icon should be square so use the height as the width too
|
||||
width: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
|
||||
height: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: stackView.pop()
|
||||
|
||||
Menu {
|
||||
id: backButton
|
||||
x: parent.width - width
|
||||
transformOrigin: Menu.TopRight
|
||||
Keys.onDownPressed: {
|
||||
stackView.currentItem.forceActiveFocus(Qt.TabFocus)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,12 +196,19 @@ ApplicationWindow {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
ToolButton {
|
||||
NavigableToolButton {
|
||||
property string browserUrl: ""
|
||||
|
||||
id: updateButton
|
||||
icon.source: "qrc:/res/update.svg"
|
||||
icon.color: "#0BF200"
|
||||
|
||||
Image {
|
||||
source: "qrc:/res/update.svg"
|
||||
anchors.centerIn: parent
|
||||
sourceSize {
|
||||
width: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
|
||||
height: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
|
||||
}
|
||||
}
|
||||
|
||||
ToolTip.delay: 1000
|
||||
ToolTip.timeout: 3000
|
||||
@@ -86,12 +221,6 @@ ApplicationWindow {
|
||||
|
||||
onClicked: Qt.openUrlExternally(browserUrl);
|
||||
|
||||
Menu {
|
||||
x: parent.width
|
||||
transformOrigin: Menu.TopRight
|
||||
}
|
||||
|
||||
|
||||
function updateAvailable(url)
|
||||
{
|
||||
updateButton.browserUrl = url
|
||||
@@ -102,26 +231,44 @@ ApplicationWindow {
|
||||
AutoUpdateChecker.onUpdateAvailable.connect(updateAvailable)
|
||||
AutoUpdateChecker.start()
|
||||
}
|
||||
|
||||
Keys.onDownPressed: {
|
||||
stackView.currentItem.forceActiveFocus(Qt.TabFocus)
|
||||
}
|
||||
}
|
||||
|
||||
ToolButton {
|
||||
icon.source: "qrc:/res/question_mark.svg"
|
||||
NavigableToolButton {
|
||||
id: helpButton
|
||||
|
||||
Image {
|
||||
source: "qrc:/res/question_mark.svg"
|
||||
anchors.centerIn: parent
|
||||
sourceSize {
|
||||
width: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
|
||||
height: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
|
||||
}
|
||||
}
|
||||
|
||||
ToolTip.delay: 1000
|
||||
ToolTip.timeout: 3000
|
||||
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.
|
||||
onClicked: Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Setup-Guide");
|
||||
|
||||
Menu {
|
||||
x: parent.width
|
||||
transformOrigin: Menu.TopRight
|
||||
Keys.onDownPressed: {
|
||||
stackView.currentItem.forceActiveFocus(Qt.TabFocus)
|
||||
}
|
||||
}
|
||||
|
||||
ToolButton {
|
||||
NavigableToolButton {
|
||||
// TODO: Implement gamepad mapping then unhide this button
|
||||
visible: false
|
||||
|
||||
@@ -130,29 +277,115 @@ ApplicationWindow {
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.text: "Gamepad Mapper"
|
||||
|
||||
icon.source: "qrc:/res/ic_videogame_asset_white_48px.svg"
|
||||
Image {
|
||||
source: "qrc:/res/ic_videogame_asset_white_48px.svg"
|
||||
anchors.centerIn: parent
|
||||
sourceSize {
|
||||
width: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
|
||||
height: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: navigateTo("qrc:/gui/GamepadMapper.qml", "Gamepad Mapping")
|
||||
|
||||
Menu {
|
||||
x: parent.width
|
||||
transformOrigin: Menu.TopRight
|
||||
Keys.onDownPressed: {
|
||||
stackView.currentItem.forceActiveFocus(Qt.TabFocus)
|
||||
}
|
||||
}
|
||||
|
||||
ToolButton {
|
||||
icon.source: "qrc:/res/settings.svg"
|
||||
NavigableToolButton {
|
||||
id: settingsButton
|
||||
|
||||
Image {
|
||||
source: "qrc:/res/settings.svg"
|
||||
anchors.centerIn: parent
|
||||
sourceSize {
|
||||
width: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
|
||||
height: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: navigateTo("qrc:/gui/SettingsView.qml", "Settings")
|
||||
|
||||
Keys.onDownPressed: {
|
||||
stackView.currentItem.forceActiveFocus(Qt.TabFocus)
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
id: settingsShortcut
|
||||
sequence: StandardKey.Preferences
|
||||
onActivated: settingsButton.clicked()
|
||||
}
|
||||
|
||||
ToolTip.delay: 1000
|
||||
ToolTip.timeout: 3000
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.text: "Settings"
|
||||
|
||||
Menu {
|
||||
x: parent.width
|
||||
transformOrigin: Menu.TopRight
|
||||
}
|
||||
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,246 @@
|
||||
#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;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
#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);
|
||||
|
||||
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;
|
||||
};
|
||||
+175
-18
@@ -1,5 +1,6 @@
|
||||
#include <QGuiApplication>
|
||||
#include <QQmlApplicationEngine>
|
||||
#include <QQmlContext>
|
||||
#include <QIcon>
|
||||
#include <QQuickStyle>
|
||||
#include <QMutex>
|
||||
@@ -11,11 +12,23 @@
|
||||
#define SDL_MAIN_HANDLED
|
||||
#include <SDL.h>
|
||||
|
||||
#ifdef HAVE_FFMPEG
|
||||
#include "streaming/video/ffmpeg.h"
|
||||
#endif
|
||||
|
||||
#ifdef Q_OS_WIN32
|
||||
#include "antihookingprotection.h"
|
||||
#endif
|
||||
|
||||
#include "cli/startstream.h"
|
||||
#include "cli/commandlineparser.h"
|
||||
#include "path.h"
|
||||
#include "gui/computermodel.h"
|
||||
#include "gui/appmodel.h"
|
||||
#include "backend/autoupdatechecker.h"
|
||||
#include "streaming/session.hpp"
|
||||
#include "streaming/session.h"
|
||||
#include "settings/streamingpreferences.h"
|
||||
#include "gui/sdlgamepadkeynavigation.h"
|
||||
|
||||
#if !defined(QT_DEBUG) && defined(Q_OS_WIN32)
|
||||
// Log to file for release Windows builds
|
||||
@@ -61,7 +74,8 @@ void logToLoggerStream(QString& message)
|
||||
}
|
||||
#endif
|
||||
|
||||
s_LoggerStream << message << endl;
|
||||
s_LoggerStream << message;
|
||||
s_LoggerStream.flush();
|
||||
}
|
||||
|
||||
void sdlLogToDiskHandler(void*, int category, SDL_LogPriority priority, const char* message)
|
||||
@@ -93,7 +107,7 @@ void sdlLogToDiskHandler(void*, int category, SDL_LogPriority priority, const ch
|
||||
}
|
||||
|
||||
QTime logTime = QTime::fromMSecsSinceStartOfDay(s_LoggerTime.elapsed());
|
||||
QString txt = QString("%1 - SDL %2 (%3): %4").arg(logTime.toString()).arg(priorityTxt).arg(category).arg(message);
|
||||
QString txt = QString("%1 - SDL %2 (%3): %4\n").arg(logTime.toString()).arg(priorityTxt).arg(category).arg(message);
|
||||
|
||||
logToLoggerStream(txt);
|
||||
}
|
||||
@@ -121,24 +135,116 @@ void qtLogToDiskHandler(QtMsgType type, const QMessageLogContext&, const QString
|
||||
}
|
||||
|
||||
QTime logTime = QTime::fromMSecsSinceStartOfDay(s_LoggerTime.elapsed());
|
||||
QString txt = QString("%1 - Qt %2: %3").arg(logTime.toString()).arg(typeTxt).arg(msg);
|
||||
QString txt = QString("%1 - Qt %2: %3\n").arg(logTime.toString()).arg(typeTxt).arg(msg);
|
||||
|
||||
logToLoggerStream(txt);
|
||||
}
|
||||
|
||||
#ifdef HAVE_FFMPEG
|
||||
|
||||
void ffmpegLogToDiskHandler(void* ptr, int level, const char* fmt, va_list vl)
|
||||
{
|
||||
char lineBuffer[1024];
|
||||
static int printPrefix = 1;
|
||||
|
||||
if ((level & 0xFF) > av_log_get_level()) {
|
||||
return;
|
||||
}
|
||||
|
||||
av_log_format_line(ptr, level, fmt, vl, lineBuffer, sizeof(lineBuffer), &printPrefix);
|
||||
|
||||
QTime logTime = QTime::fromMSecsSinceStartOfDay(s_LoggerTime.elapsed());
|
||||
QString txt = QString("%1 - FFmpeg: %2").arg(logTime.toString()).arg(lineBuffer);
|
||||
|
||||
logToLoggerStream(txt);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
#endif
|
||||
|
||||
#ifdef Q_OS_WIN32
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <Windows.h>
|
||||
#include <DbgHelp.h>
|
||||
|
||||
static UINT s_HitUnhandledException = 0;
|
||||
|
||||
LONG WINAPI UnhandledExceptionHandler(struct _EXCEPTION_POINTERS *ExceptionInfo)
|
||||
{
|
||||
// Only write a dump for the first unhandled exception
|
||||
if (InterlockedCompareExchange(&s_HitUnhandledException, 1, 0) != 0) {
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
}
|
||||
|
||||
WCHAR dmpFileName[MAX_PATH];
|
||||
swprintf_s(dmpFileName, L"%ls\\Moonlight-%I64u.dmp",
|
||||
(PWCHAR)QDir::toNativeSeparators(Path::getLogDir()).utf16(), QDateTime::currentSecsSinceEpoch());
|
||||
QString qDmpFileName = QString::fromUtf16((unsigned short*)dmpFileName);
|
||||
HANDLE dumpHandle = CreateFileW(dmpFileName, GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
|
||||
if (dumpHandle != INVALID_HANDLE_VALUE) {
|
||||
MINIDUMP_EXCEPTION_INFORMATION info;
|
||||
|
||||
info.ThreadId = GetCurrentThreadId();
|
||||
info.ExceptionPointers = ExceptionInfo;
|
||||
info.ClientPointers = FALSE;
|
||||
|
||||
DWORD typeFlags = MiniDumpWithIndirectlyReferencedMemory |
|
||||
MiniDumpIgnoreInaccessibleMemory |
|
||||
MiniDumpWithUnloadedModules |
|
||||
MiniDumpWithThreadInfo;
|
||||
|
||||
if (MiniDumpWriteDump(GetCurrentProcess(),
|
||||
GetCurrentProcessId(),
|
||||
dumpHandle,
|
||||
(MINIDUMP_TYPE)typeFlags,
|
||||
&info,
|
||||
nullptr,
|
||||
nullptr)) {
|
||||
qCritical() << "Unhandled exception! Minidump written to:" << qDmpFileName;
|
||||
}
|
||||
else {
|
||||
qCritical() << "Unhandled exception! Failed to write dump:" << GetLastError();
|
||||
}
|
||||
|
||||
CloseHandle(dumpHandle);
|
||||
}
|
||||
else {
|
||||
qCritical() << "Unhandled exception! Failed to open dump file:" << qDmpFileName << "with error" << GetLastError();
|
||||
}
|
||||
|
||||
// Let the program crash and WER collect a dump
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
// Set these here to allow us to use the default QSettings constructor.
|
||||
// These also ensure that our cache directory is named correctly. As such,
|
||||
// it is critical that these be called before Path::initialize().
|
||||
QCoreApplication::setOrganizationName("Moonlight Game Streaming Project");
|
||||
QCoreApplication::setOrganizationDomain("moonlight-stream.com");
|
||||
QCoreApplication::setApplicationName("Moonlight");
|
||||
|
||||
if (QFile(QDir::currentPath() + "/portable.dat").exists()) {
|
||||
qInfo() << "Running in portable mode from:" << QDir::currentPath();
|
||||
QSettings::setDefaultFormat(QSettings::IniFormat);
|
||||
QSettings::setPath(QSettings::IniFormat, QSettings::UserScope, QDir::currentPath());
|
||||
QSettings::setPath(QSettings::IniFormat, QSettings::SystemScope, QDir::currentPath());
|
||||
|
||||
// Initialize paths for portable mode
|
||||
Path::initialize(true);
|
||||
}
|
||||
else {
|
||||
// Initialize paths for standard installation
|
||||
Path::initialize(false);
|
||||
}
|
||||
|
||||
#ifdef USE_CUSTOM_LOGGER
|
||||
#ifdef LOG_TO_FILE
|
||||
#ifdef Q_OS_DARWIN
|
||||
// On macOS, $TMPDIR is some random folder under /var/folders/ that nobody can
|
||||
// easily find, so use the system's global tmp directory instead.
|
||||
QDir tempDir("/tmp");
|
||||
#else
|
||||
QDir tempDir(QDir::tempPath());
|
||||
#endif
|
||||
QDir tempDir(Path::getLogDir());
|
||||
s_LoggerFile = new QFile(tempDir.filePath(QString("Moonlight-%1.log").arg(QDateTime::currentSecsSinceEpoch())));
|
||||
if (s_LoggerFile->open(QIODevice::WriteOnly)) {
|
||||
qInfo() << "Redirecting log output to " << s_LoggerFile->fileName();
|
||||
@@ -149,6 +255,22 @@ int main(int argc, char *argv[])
|
||||
s_LoggerTime.start();
|
||||
qInstallMessageHandler(qtLogToDiskHandler);
|
||||
SDL_LogSetOutputFunction(sdlLogToDiskHandler, nullptr);
|
||||
|
||||
#ifdef HAVE_FFMPEG
|
||||
av_log_set_callback(ffmpegLogToDiskHandler);
|
||||
#endif
|
||||
|
||||
#endif
|
||||
|
||||
#ifdef Q_OS_WIN32
|
||||
// Create a crash dump when we crash on Windows
|
||||
SetUnhandledExceptionFilter(UnhandledExceptionHandler);
|
||||
#endif
|
||||
|
||||
#ifdef Q_OS_WIN32
|
||||
// Force AntiHooking.dll to be statically imported and loaded
|
||||
// by ntdll by calling a dummy function.
|
||||
AntiHookingDummyImport();
|
||||
#endif
|
||||
|
||||
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
|
||||
@@ -157,10 +279,14 @@ int main(int argc, char *argv[])
|
||||
// password prompts on macOS.
|
||||
qputenv("QT_SSL_USE_TEMPORARY_KEYCHAIN", QByteArray("1"));
|
||||
|
||||
// Set these here to allow us to use the default QSettings constructor
|
||||
QCoreApplication::setOrganizationName("Moonlight Game Streaming Project");
|
||||
QCoreApplication::setOrganizationDomain("moonlight-stream.com");
|
||||
QCoreApplication::setApplicationName("Moonlight");
|
||||
#ifdef Q_OS_WIN32
|
||||
if (!qEnvironmentVariableIsSet("QT_OPENGL")) {
|
||||
// On Windows, use ANGLE so we don't have to load OpenGL
|
||||
// user-mode drivers into our app. OGL drivers (especially Intel)
|
||||
// seem to crash Moonlight far more often than DirectX.
|
||||
qputenv("QT_OPENGL", "angle");
|
||||
}
|
||||
#endif
|
||||
|
||||
// Register custom metatypes for use in signals
|
||||
qRegisterMetaType<NvApp>("NvApp");
|
||||
@@ -173,6 +299,7 @@ int main(int argc, char *argv[])
|
||||
qmlRegisterType<ComputerModel>("ComputerModel", 1, 0, "ComputerModel");
|
||||
qmlRegisterType<AppModel>("AppModel", 1, 0, "AppModel");
|
||||
qmlRegisterType<StreamingPreferences>("StreamingPreferences", 1, 0, "StreamingPreferences");
|
||||
qmlRegisterType<SdlGamepadKeyNavigation>("SdlGamepadKeyNavigation", 1, 0, "SdlGamepadKeyNavigation");
|
||||
qmlRegisterUncreatableType<Session>("Session", 1, 0, "Session", "Session cannot be created from QML");
|
||||
qmlRegisterSingletonType<ComputerManager>("ComputerManager", 1, 0,
|
||||
"ComputerManager",
|
||||
@@ -187,8 +314,31 @@ int main(int argc, char *argv[])
|
||||
|
||||
QQuickStyle::setStyle("Material");
|
||||
|
||||
// Load the main.qml file
|
||||
QQmlApplicationEngine engine;
|
||||
QString initialView;
|
||||
|
||||
GlobalCommandLineParser parser;
|
||||
switch (parser.parse(app.arguments())) {
|
||||
case GlobalCommandLineParser::NormalStartRequested:
|
||||
initialView = "PcView.qml";
|
||||
break;
|
||||
case GlobalCommandLineParser::StreamRequested:
|
||||
{
|
||||
initialView = "CliStartStreamSegue.qml";
|
||||
StreamingPreferences* preferences = new StreamingPreferences(&app);
|
||||
StreamCommandLineParser streamParser;
|
||||
streamParser.parse(app.arguments(), preferences);
|
||||
QString host = streamParser.getHost();
|
||||
QString appName = streamParser.getAppName();
|
||||
auto launcher = new CliStartStream::Launcher(host, appName, preferences, &app);
|
||||
engine.rootContext()->setContextProperty("launcher", launcher);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
engine.rootContext()->setContextProperty("initialView", initialView);
|
||||
|
||||
// Load the main.qml file
|
||||
engine.load(QUrl(QStringLiteral("qrc:/gui/main.qml")));
|
||||
if (engine.rootObjects().isEmpty())
|
||||
return -1;
|
||||
@@ -200,9 +350,16 @@ int main(int argc, char *argv[])
|
||||
SDL_GetError());
|
||||
}
|
||||
|
||||
int err = app.exec();
|
||||
// Use atexit() to ensure SDL_Quit() is called. This avoids
|
||||
// racing with object destruction where SDL may be used.
|
||||
atexit(SDL_Quit);
|
||||
|
||||
SDL_Quit();
|
||||
// Avoid the default behavior of changing the timer resolution to 1 ms.
|
||||
// We don't want this all the time that Moonlight is open. We will set
|
||||
// it manually when we start streaming.
|
||||
SDL_SetHint(SDL_HINT_TIMER_RESOLUTION, "0");
|
||||
|
||||
int err = app.exec();
|
||||
|
||||
return err;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
#include "path.h"
|
||||
|
||||
#include <QtDebug>
|
||||
#include <QDir>
|
||||
#include <QStandardPaths>
|
||||
#include <QSettings>
|
||||
#include <QCoreApplication>
|
||||
|
||||
QString Path::s_LogDir;
|
||||
QString Path::s_BoxArtCacheDir;
|
||||
const QString Path::k_GameControllerMappingFile("gamecontrollerdb.txt");
|
||||
|
||||
QString Path::getLogDir()
|
||||
{
|
||||
Q_ASSERT(!s_LogDir.isEmpty());
|
||||
return s_LogDir;
|
||||
}
|
||||
|
||||
QString Path::getBoxArtCacheDir()
|
||||
{
|
||||
Q_ASSERT(!s_BoxArtCacheDir.isEmpty());
|
||||
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)
|
||||
{
|
||||
if (portable) {
|
||||
s_LogDir = QDir::currentPath();
|
||||
s_BoxArtCacheDir = QDir::currentPath() + "/boxart";
|
||||
}
|
||||
else {
|
||||
#ifdef Q_OS_DARWIN
|
||||
// On macOS, $TMPDIR is some random folder under /var/folders/ that nobody can
|
||||
// easily find, so use the system's global tmp directory instead.
|
||||
s_LogDir = "/tmp";
|
||||
#else
|
||||
s_LogDir = QDir::tempPath();
|
||||
#endif
|
||||
s_BoxArtCacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/boxart";
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
#include <QString>
|
||||
|
||||
class Path
|
||||
{
|
||||
public:
|
||||
static QString getLogDir();
|
||||
|
||||
static QString getBoxArtCacheDir();
|
||||
|
||||
static QString getGamepadMappingFile();
|
||||
|
||||
static void initialize(bool portable);
|
||||
|
||||
private:
|
||||
static QString s_LogDir;
|
||||
static QString s_BoxArtCacheDir;
|
||||
static const QString k_GameControllerMappingFile;
|
||||
};
|
||||
@@ -7,5 +7,10 @@
|
||||
<file>gui/StreamSegue.qml</file>
|
||||
<file>gui/GamepadMapper.qml</file>
|
||||
<file>gui/QuitSegue.qml</file>
|
||||
<file>gui/NavigableToolButton.qml</file>
|
||||
<file>gui/NavigableItemDelegate.qml</file>
|
||||
<file>gui/NavigableMenuItem.qml</file>
|
||||
<file>gui/CliStartStreamSegue.qml</file>
|
||||
<file>gui/AutoResizingComboBox.qml</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="24" height="24" viewBox="0 0 24 24">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="24" height="24" viewBox="0 0 24 24" fill="#0BF200">
|
||||
<path d="M21,10.12H14.22L16.96,7.3C14.23,4.6 9.81,4.5 7.08,7.2C4.35,9.91 4.35,14.28 7.08,17C9.81,19.7 14.23,19.7 16.96,17C18.32,15.65 19,14.08 19,12.1H21C21,14.08 20.12,16.65 18.36,18.39C14.85,21.87 9.15,21.87 5.64,18.39C2.14,14.92 2.11,9.28 5.62,5.81C9.13,2.34 14.76,2.34 18.27,5.81L21,3V10.12M12.5,8V12.25L16,14.33L15.28,15.54L11,13V8H12.5Z" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 484 B After Width: | Height: | Size: 499 B |
@@ -0,0 +1,108 @@
|
||||
#include "mappingmanager.h"
|
||||
#include "path.h"
|
||||
|
||||
#include <QDir>
|
||||
|
||||
#include <SDL.h>
|
||||
|
||||
#define SER_GAMEPADMAPPING "gcmapping"
|
||||
|
||||
#define SER_GUID "guid"
|
||||
#define SER_MAPPING "mapping"
|
||||
|
||||
MappingManager::MappingManager()
|
||||
{
|
||||
QSettings settings;
|
||||
|
||||
// First load existing saved mappings. This ensures the user's
|
||||
// hints can always override the old data.
|
||||
int mappingCount = settings.beginReadArray(SER_GAMEPADMAPPING);
|
||||
for (int i = 0; i < mappingCount; i++) {
|
||||
settings.setArrayIndex(i);
|
||||
|
||||
SdlGamepadMapping mapping(settings.value(SER_GUID).toString(), settings.value(SER_MAPPING).toString());
|
||||
addMapping(mapping);
|
||||
}
|
||||
settings.endArray();
|
||||
|
||||
// Finally load mappings from SDL_HINT_GAMECONTROLLERCONFIG
|
||||
QStringList sdlMappings = QString::fromLocal8Bit(SDL_GetHint(SDL_HINT_GAMECONTROLLERCONFIG)).split('\n', QString::SkipEmptyParts);
|
||||
for (QString sdlMapping : sdlMappings) {
|
||||
SdlGamepadMapping mapping(sdlMapping);
|
||||
addMapping(mapping);
|
||||
}
|
||||
|
||||
// Save the updated mappings to settings
|
||||
save();
|
||||
}
|
||||
|
||||
void MappingManager::save()
|
||||
{
|
||||
QSettings settings;
|
||||
|
||||
settings.remove(SER_GAMEPADMAPPING);
|
||||
settings.beginWriteArray(SER_GAMEPADMAPPING);
|
||||
QList<SdlGamepadMapping> mappings = m_Mappings.values();
|
||||
for (int i = 0; i < mappings.count(); i++) {
|
||||
settings.setArrayIndex(i);
|
||||
|
||||
settings.setValue(SER_GUID, mappings[i].getGuid());
|
||||
settings.setValue(SER_MAPPING, mappings[i].getMapping());
|
||||
}
|
||||
settings.endArray();
|
||||
}
|
||||
|
||||
void MappingManager::applyMappings()
|
||||
{
|
||||
QString mappingFile = Path::getGamepadMappingFile();
|
||||
if (!mappingFile.isEmpty()) {
|
||||
std::string mappingFileNative = QDir::toNativeSeparators(mappingFile).toStdString();
|
||||
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Loading gamepad mappings from: %s",
|
||||
mappingFileNative.c_str());
|
||||
|
||||
int newMappings = SDL_GameControllerAddMappingsFromFile(mappingFileNative.c_str());
|
||||
if (newMappings < 0) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Error loading gamepad mappings: %s",
|
||||
SDL_GetError());
|
||||
}
|
||||
else {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Loaded %d new gamepad mappings",
|
||||
newMappings);
|
||||
}
|
||||
}
|
||||
else {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"No gamepad mapping file found");
|
||||
}
|
||||
|
||||
QList<SdlGamepadMapping> mappings = m_Mappings.values();
|
||||
for (const SdlGamepadMapping& mapping : mappings) {
|
||||
QString sdlMappingString = mapping.getSdlMappingString();
|
||||
int ret = SDL_GameControllerAddMapping(qPrintable(sdlMappingString));
|
||||
if (ret < 0) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Unable to add mapping: %s",
|
||||
qPrintable(sdlMappingString));
|
||||
}
|
||||
else if (ret == 1) {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Loaded saved user mapping: %s",
|
||||
qPrintable(sdlMappingString));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MappingManager::addMapping(QString mappingString)
|
||||
{
|
||||
SdlGamepadMapping mapping(mappingString);
|
||||
addMapping(mapping);
|
||||
}
|
||||
|
||||
void MappingManager::addMapping(SdlGamepadMapping& mapping)
|
||||
{
|
||||
m_Mappings[mapping.getGuid()] = mapping;
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
#pragma once
|
||||
|
||||
#include <QSettings>
|
||||
|
||||
class SdlGamepadMapping
|
||||
{
|
||||
public:
|
||||
SdlGamepadMapping() {}
|
||||
|
||||
SdlGamepadMapping(QString string)
|
||||
{
|
||||
QStringList mapping = string.split(",");
|
||||
if (!mapping.isEmpty()) {
|
||||
m_Guid = mapping[0];
|
||||
|
||||
string.remove(0, m_Guid.length() + 1);
|
||||
m_Mapping = string;
|
||||
}
|
||||
}
|
||||
|
||||
SdlGamepadMapping(QString guid, QString mapping)
|
||||
: m_Guid(guid),
|
||||
m_Mapping(mapping)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
bool operator==(const SdlGamepadMapping& other) const
|
||||
{
|
||||
return m_Guid == other.m_Guid && m_Mapping == other.m_Mapping;
|
||||
}
|
||||
|
||||
QString getGuid() const
|
||||
{
|
||||
return m_Guid;
|
||||
}
|
||||
|
||||
QString getMapping() const
|
||||
{
|
||||
return m_Mapping;
|
||||
}
|
||||
|
||||
QString getSdlMappingString() const
|
||||
{
|
||||
if (m_Guid.isEmpty() || m_Mapping.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
else {
|
||||
return m_Guid + "," + m_Mapping;
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
QString m_Guid;
|
||||
QString m_Mapping;
|
||||
};
|
||||
|
||||
class MappingManager
|
||||
{
|
||||
public:
|
||||
MappingManager();
|
||||
|
||||
void addMapping(QString gamepadString);
|
||||
|
||||
void addMapping(SdlGamepadMapping& gamepadMapping);
|
||||
|
||||
void applyMappings();
|
||||
|
||||
void save();
|
||||
|
||||
private:
|
||||
QMap<QString, SdlGamepadMapping> m_Mappings;
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "streamingpreferences.h"
|
||||
#include "streaming/session.hpp"
|
||||
#include "streaming/session.h"
|
||||
#include "streaming/streamutils.h"
|
||||
|
||||
#include <QSettings>
|
||||
|
||||
@@ -9,14 +10,20 @@
|
||||
#define SER_FPS "fps"
|
||||
#define SER_BITRATE "bitrate"
|
||||
#define SER_FULLSCREEN "fullscreen"
|
||||
#define SER_VSYNC "vsync"
|
||||
#define SER_GAMEOPTS "gameopts"
|
||||
#define SER_HOSTAUDIO "hostaudio"
|
||||
#define SER_MULTICONT "multicontroller"
|
||||
#define SER_AUDIOCFG "audiocfg"
|
||||
#define SER_VIDEOCFG "videocfg"
|
||||
#define SER_VIDEODEC "videodec"
|
||||
#define SER_WINDOWMODE "windowmode"
|
||||
#define SER_UNSUPPORTEDFPS "unsupportedfps"
|
||||
#define SER_MDNS "mdns"
|
||||
#define SER_MOUSEACCELERATION "mouseacceleration"
|
||||
|
||||
StreamingPreferences::StreamingPreferences()
|
||||
StreamingPreferences::StreamingPreferences(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
reload();
|
||||
}
|
||||
@@ -29,16 +36,23 @@ void StreamingPreferences::reload()
|
||||
height = settings.value(SER_HEIGHT, 720).toInt();
|
||||
fps = settings.value(SER_FPS, 60).toInt();
|
||||
bitrateKbps = settings.value(SER_BITRATE, getDefaultBitrate(width, height, fps)).toInt();
|
||||
fullScreen = settings.value(SER_FULLSCREEN, true).toBool();
|
||||
enableVsync = settings.value(SER_VSYNC, true).toBool();
|
||||
gameOptimizations = settings.value(SER_GAMEOPTS, true).toBool();
|
||||
playAudioOnHost = settings.value(SER_HOSTAUDIO, false).toBool();
|
||||
multiController = settings.value(SER_MULTICONT, true).toBool();
|
||||
unsupportedFps = settings.value(SER_UNSUPPORTEDFPS, false).toBool();
|
||||
enableMdns = settings.value(SER_MDNS, true).toBool();
|
||||
mouseAcceleration = settings.value(SER_MOUSEACCELERATION, false).toBool();
|
||||
audioConfig = static_cast<AudioConfig>(settings.value(SER_AUDIOCFG,
|
||||
static_cast<int>(AudioConfig::AC_AUTO)).toInt());
|
||||
static_cast<int>(AudioConfig::AC_STEREO)).toInt());
|
||||
videoCodecConfig = static_cast<VideoCodecConfig>(settings.value(SER_VIDEOCFG,
|
||||
static_cast<int>(VideoCodecConfig::VCC_AUTO)).toInt());
|
||||
videoDecoderSelection = static_cast<VideoDecoderSelection>(settings.value(SER_VIDEODEC,
|
||||
static_cast<int>(VideoDecoderSelection::VDS_AUTO)).toInt());
|
||||
windowMode = static_cast<WindowMode>(settings.value(SER_WINDOWMODE,
|
||||
// Try to load from the old preference value too
|
||||
static_cast<int>(settings.value(SER_FULLSCREEN, true).toBool() ?
|
||||
WindowMode::WM_FULLSCREEN : WindowMode::WM_WINDOWED)).toInt());
|
||||
}
|
||||
|
||||
void StreamingPreferences::save()
|
||||
@@ -49,13 +63,17 @@ void StreamingPreferences::save()
|
||||
settings.setValue(SER_HEIGHT, height);
|
||||
settings.setValue(SER_FPS, fps);
|
||||
settings.setValue(SER_BITRATE, bitrateKbps);
|
||||
settings.setValue(SER_FULLSCREEN, fullScreen);
|
||||
settings.setValue(SER_VSYNC, enableVsync);
|
||||
settings.setValue(SER_GAMEOPTS, gameOptimizations);
|
||||
settings.setValue(SER_HOSTAUDIO, playAudioOnHost);
|
||||
settings.setValue(SER_MULTICONT, multiController);
|
||||
settings.setValue(SER_UNSUPPORTEDFPS, unsupportedFps);
|
||||
settings.setValue(SER_MDNS, enableMdns);
|
||||
settings.setValue(SER_MOUSEACCELERATION, mouseAcceleration);
|
||||
settings.setValue(SER_AUDIOCFG, static_cast<int>(audioConfig));
|
||||
settings.setValue(SER_VIDEOCFG, static_cast<int>(videoCodecConfig));
|
||||
settings.setValue(SER_VIDEODEC, static_cast<int>(videoDecoderSelection));
|
||||
settings.setValue(SER_WINDOWMODE, static_cast<int>(windowMode));
|
||||
}
|
||||
|
||||
bool StreamingPreferences::hasAnyHardwareAcceleration()
|
||||
@@ -72,6 +90,20 @@ bool StreamingPreferences::isRunningWayland()
|
||||
return qgetenv("XDG_SESSION_TYPE") == QByteArray("wayland");
|
||||
}
|
||||
|
||||
bool StreamingPreferences::isWow64()
|
||||
{
|
||||
#ifdef Q_OS_WIN32
|
||||
return QSysInfo::currentCpuArchitecture() != QSysInfo::buildCpuArchitecture();
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
QString StreamingPreferences::getUnmappedGamepads()
|
||||
{
|
||||
return SdlInputHandler::getUnmappedGamepads();
|
||||
}
|
||||
|
||||
int StreamingPreferences::getMaximumStreamingFrameRate()
|
||||
{
|
||||
// Never let the maximum drop below 60 FPS
|
||||
@@ -84,12 +116,26 @@ int StreamingPreferences::getMaximumStreamingFrameRate()
|
||||
return maxFrameRate;
|
||||
}
|
||||
|
||||
for (int i = 0; i < SDL_GetNumVideoDisplays(); i++) {
|
||||
SDL_DisplayMode mode;
|
||||
if (SDL_GetCurrentDisplayMode(i, &mode) == 0) {
|
||||
SDL_DisplayMode mode, bestMode, desktopMode;
|
||||
for (int displayIndex = 0; displayIndex < SDL_GetNumVideoDisplays(); displayIndex++) {
|
||||
// Get the native desktop resolution
|
||||
if (StreamUtils::getRealDesktopMode(displayIndex, &desktopMode)) {
|
||||
|
||||
// Start at desktop mode and work our way up
|
||||
bestMode = desktopMode;
|
||||
for (int i = 0; i < SDL_GetNumDisplayModes(displayIndex); i++) {
|
||||
if (SDL_GetDisplayMode(displayIndex, i, &mode) == 0) {
|
||||
if (mode.w == desktopMode.w && mode.h == desktopMode.h) {
|
||||
if (mode.refresh_rate > bestMode.refresh_rate) {
|
||||
bestMode = mode;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cap the frame rate at 120 FPS. Past this, the encoders start
|
||||
// to max out and drop frames.
|
||||
maxFrameRate = qMax(maxFrameRate, qMin(120, mode.refresh_rate));
|
||||
maxFrameRate = qMax(maxFrameRate, qMin(120, bestMode.refresh_rate));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,7 +144,7 @@ int StreamingPreferences::getMaximumStreamingFrameRate()
|
||||
return maxFrameRate;
|
||||
}
|
||||
|
||||
QRect StreamingPreferences::getDisplayResolution(int displayIndex)
|
||||
QRect StreamingPreferences::getDesktopResolution(int displayIndex)
|
||||
{
|
||||
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
@@ -113,7 +159,7 @@ QRect StreamingPreferences::getDisplayResolution(int displayIndex)
|
||||
}
|
||||
|
||||
SDL_DisplayMode mode;
|
||||
int err = SDL_GetCurrentDisplayMode(displayIndex, &mode);
|
||||
int err = SDL_GetDesktopDisplayMode(displayIndex, &mode);
|
||||
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||
|
||||
if (err == 0) {
|
||||
@@ -121,12 +167,38 @@ QRect StreamingPreferences::getDisplayResolution(int displayIndex)
|
||||
}
|
||||
else {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"SDL_GetCurrentDisplayMode() failed: %s",
|
||||
"SDL_GetDesktopDisplayMode() failed: %s",
|
||||
SDL_GetError());
|
||||
return QRect();
|
||||
}
|
||||
}
|
||||
|
||||
QRect StreamingPreferences::getNativeResolution(int displayIndex)
|
||||
{
|
||||
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s",
|
||||
SDL_GetError());
|
||||
return QRect();
|
||||
}
|
||||
|
||||
if (displayIndex >= SDL_GetNumVideoDisplays()) {
|
||||
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||
return QRect();
|
||||
}
|
||||
|
||||
SDL_DisplayMode mode;
|
||||
bool success = StreamUtils::getRealDesktopMode(displayIndex, &mode);
|
||||
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||
|
||||
if (success) {
|
||||
return QRect(0, 0, mode.w, mode.h);
|
||||
}
|
||||
else {
|
||||
return QRect();
|
||||
}
|
||||
}
|
||||
|
||||
int StreamingPreferences::getDefaultBitrate(int width, int height, int fps)
|
||||
{
|
||||
// This table prefers 16:10 resolutions because they are
|
||||
|
||||
@@ -8,7 +8,7 @@ class StreamingPreferences : public QObject
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
StreamingPreferences();
|
||||
StreamingPreferences(QObject *parent = nullptr);
|
||||
|
||||
Q_INVOKABLE static int
|
||||
getDefaultBitrate(int width, int height, int fps);
|
||||
@@ -19,17 +19,22 @@ public:
|
||||
|
||||
Q_INVOKABLE static bool isRunningWayland();
|
||||
|
||||
Q_INVOKABLE static bool isWow64();
|
||||
|
||||
Q_INVOKABLE static int getMaximumStreamingFrameRate();
|
||||
|
||||
Q_INVOKABLE QRect getDisplayResolution(int displayIndex);
|
||||
Q_INVOKABLE QRect getDesktopResolution(int displayIndex);
|
||||
|
||||
Q_INVOKABLE QRect getNativeResolution(int displayIndex);
|
||||
|
||||
Q_INVOKABLE QString getUnmappedGamepads();
|
||||
|
||||
void reload();
|
||||
|
||||
enum AudioConfig
|
||||
{
|
||||
AC_AUTO,
|
||||
AC_FORCE_STEREO,
|
||||
AC_FORCE_SURROUND
|
||||
AC_STEREO,
|
||||
AC_51_SURROUND
|
||||
};
|
||||
Q_ENUM(AudioConfig)
|
||||
|
||||
@@ -50,40 +55,60 @@ public:
|
||||
};
|
||||
Q_ENUM(VideoDecoderSelection)
|
||||
|
||||
enum WindowMode
|
||||
{
|
||||
WM_FULLSCREEN,
|
||||
WM_FULLSCREEN_DESKTOP,
|
||||
WM_WINDOWED
|
||||
};
|
||||
Q_ENUM(WindowMode)
|
||||
|
||||
Q_PROPERTY(int width MEMBER width NOTIFY displayModeChanged)
|
||||
Q_PROPERTY(int height MEMBER height NOTIFY displayModeChanged)
|
||||
Q_PROPERTY(int fps MEMBER fps NOTIFY displayModeChanged)
|
||||
Q_PROPERTY(int bitrateKbps MEMBER bitrateKbps NOTIFY bitrateChanged)
|
||||
Q_PROPERTY(bool fullScreen MEMBER fullScreen NOTIFY fullScreenChanged)
|
||||
Q_PROPERTY(bool enableVsync MEMBER enableVsync NOTIFY enableVsyncChanged)
|
||||
Q_PROPERTY(bool gameOptimizations MEMBER gameOptimizations NOTIFY gameOptimizationsChanged)
|
||||
Q_PROPERTY(bool playAudioOnHost MEMBER playAudioOnHost NOTIFY playAudioOnHostChanged)
|
||||
Q_PROPERTY(bool multiController MEMBER multiController NOTIFY multiControllerChanged)
|
||||
Q_PROPERTY(bool unsupportedFps MEMBER unsupportedFps NOTIFY unsupportedFpsChanged)
|
||||
Q_PROPERTY(bool enableMdns MEMBER enableMdns NOTIFY enableMdnsChanged)
|
||||
Q_PROPERTY(bool mouseAcceleration MEMBER mouseAcceleration NOTIFY mouseAccelerationChanged)
|
||||
Q_PROPERTY(AudioConfig audioConfig MEMBER audioConfig NOTIFY audioConfigChanged)
|
||||
Q_PROPERTY(VideoCodecConfig videoCodecConfig MEMBER videoCodecConfig NOTIFY videoCodecConfigChanged)
|
||||
Q_PROPERTY(VideoDecoderSelection videoDecoderSelection MEMBER videoDecoderSelection NOTIFY videoDecoderSelectionChanged)
|
||||
Q_PROPERTY(WindowMode windowMode MEMBER windowMode NOTIFY windowModeChanged)
|
||||
|
||||
// Directly accessible members for preferences
|
||||
int width;
|
||||
int height;
|
||||
int fps;
|
||||
int bitrateKbps;
|
||||
bool fullScreen;
|
||||
bool enableVsync;
|
||||
bool gameOptimizations;
|
||||
bool playAudioOnHost;
|
||||
bool multiController;
|
||||
bool unsupportedFps;
|
||||
bool enableMdns;
|
||||
bool mouseAcceleration;
|
||||
AudioConfig audioConfig;
|
||||
VideoCodecConfig videoCodecConfig;
|
||||
VideoDecoderSelection videoDecoderSelection;
|
||||
WindowMode windowMode;
|
||||
|
||||
signals:
|
||||
void displayModeChanged();
|
||||
void bitrateChanged();
|
||||
void fullScreenChanged();
|
||||
void enableVsyncChanged();
|
||||
void gameOptimizationsChanged();
|
||||
void playAudioOnHostChanged();
|
||||
void multiControllerChanged();
|
||||
void unsupportedFpsChanged();
|
||||
void enableMdnsChanged();
|
||||
void mouseAccelerationChanged();
|
||||
void audioConfigChanged();
|
||||
void videoCodecConfigChanged();
|
||||
void videoDecoderSelectionChanged();
|
||||
void windowModeChanged();
|
||||
};
|
||||
|
||||
|
||||
@@ -1,228 +0,0 @@
|
||||
#include "session.hpp"
|
||||
|
||||
#include <Limelight.h>
|
||||
#include <SDL.h>
|
||||
|
||||
#define MAX_CHANNELS 6
|
||||
#define SAMPLES_PER_FRAME 240
|
||||
#define MIN_QUEUED_FRAMES 2
|
||||
#define MAX_QUEUED_FRAMES 4
|
||||
#define STOP_THE_WORLD_LIMIT 20
|
||||
#define DROP_RATIO_DENOM 32
|
||||
|
||||
SDL_AudioDeviceID Session::s_AudioDevice;
|
||||
OpusMSDecoder* Session::s_OpusDecoder;
|
||||
short Session::s_OpusDecodeBuffer[MAX_CHANNELS * SAMPLES_PER_FRAME];
|
||||
int Session::s_ChannelCount;
|
||||
int Session::s_PendingDrops;
|
||||
int Session::s_PendingHardDrops;
|
||||
unsigned int Session::s_SampleIndex;
|
||||
Uint32 Session::s_BaselinePendingData;
|
||||
|
||||
int Session::sdlDetermineAudioConfiguration()
|
||||
{
|
||||
SDL_AudioSpec want, have;
|
||||
SDL_AudioDeviceID dev;
|
||||
int ret;
|
||||
|
||||
if (SDL_InitSubSystem(SDL_INIT_AUDIO) != 0) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"SDL_InitSubSystem(SDL_INIT_AUDIO) failed: %s",
|
||||
SDL_GetError());
|
||||
return AUDIO_CONFIGURATION_STEREO;
|
||||
}
|
||||
|
||||
SDL_zero(want);
|
||||
want.freq = 48000;
|
||||
want.format = AUDIO_S16;
|
||||
want.channels = 6;
|
||||
want.samples = 1024;
|
||||
|
||||
// Try to open for 5.1 surround sound, but allow SDL to tell us that's
|
||||
// not available.
|
||||
dev = SDL_OpenAudioDevice(NULL, 0, &want, &have, SDL_AUDIO_ALLOW_CHANNELS_CHANGE);
|
||||
if (dev == 0) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Failed to open audio device");
|
||||
// We'll probably have issues during audio stream init, but we'll
|
||||
// try anyway
|
||||
ret = AUDIO_CONFIGURATION_STEREO;
|
||||
goto Exit;
|
||||
}
|
||||
|
||||
SDL_CloseAudioDevice(dev);
|
||||
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Audio device has %d channels", have.channels);
|
||||
|
||||
if (have.channels > 2) {
|
||||
// We don't support quadraphonic or 7.1 surround, but SDL
|
||||
// should be able to downmix or upmix better from 5.1 than
|
||||
// from stereo, so use 5.1 in non-stereo cases.
|
||||
ret = AUDIO_CONFIGURATION_51_SURROUND;
|
||||
}
|
||||
else {
|
||||
ret = AUDIO_CONFIGURATION_STEREO;
|
||||
}
|
||||
|
||||
Exit:
|
||||
SDL_QuitSubSystem(SDL_INIT_AUDIO);
|
||||
return ret;
|
||||
}
|
||||
|
||||
int Session::sdlAudioInit(int /* audioConfiguration */,
|
||||
POPUS_MULTISTREAM_CONFIGURATION opusConfig,
|
||||
void* /* arContext */, int /* arFlags */)
|
||||
{
|
||||
SDL_AudioSpec want, have;
|
||||
int error;
|
||||
|
||||
SDL_assert(!SDL_WasInit(SDL_INIT_AUDIO));
|
||||
if (SDL_InitSubSystem(SDL_INIT_AUDIO) != 0) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"SDL_InitSubSystem(SDL_INIT_AUDIO) failed: %s",
|
||||
SDL_GetError());
|
||||
return AUDIO_CONFIGURATION_STEREO;
|
||||
}
|
||||
|
||||
SDL_zero(want);
|
||||
want.freq = opusConfig->sampleRate;
|
||||
want.format = AUDIO_S16;
|
||||
want.channels = opusConfig->channelCount;
|
||||
|
||||
// This is supposed to be a power of 2, but our
|
||||
// frames contain a non-power of 2 number of samples,
|
||||
// so the slop would require buffering another full frame.
|
||||
// Specifying non-Po2 seems to work for our supported platforms.
|
||||
want.samples = SAMPLES_PER_FRAME;
|
||||
|
||||
s_AudioDevice = SDL_OpenAudioDevice(NULL, 0, &want, &have, 0);
|
||||
if (s_AudioDevice == 0) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Failed to open audio device: %s",
|
||||
SDL_GetError());
|
||||
SDL_QuitSubSystem(SDL_INIT_AUDIO);
|
||||
return -1;
|
||||
}
|
||||
|
||||
s_OpusDecoder = opus_multistream_decoder_create(opusConfig->sampleRate,
|
||||
opusConfig->channelCount,
|
||||
opusConfig->streams,
|
||||
opusConfig->coupledStreams,
|
||||
opusConfig->mapping,
|
||||
&error);
|
||||
if (s_OpusDecoder == NULL) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Failed to create decoder: %d",
|
||||
error);
|
||||
SDL_CloseAudioDevice(s_AudioDevice);
|
||||
s_AudioDevice = 0;
|
||||
SDL_QuitSubSystem(SDL_INIT_AUDIO);
|
||||
return -2;
|
||||
}
|
||||
|
||||
// SDL counts pending samples in the queued
|
||||
// audio size using the WASAPI backend. This
|
||||
// includes silence, which can throw off our
|
||||
// pending data count. Get a baseline so we
|
||||
// can exclude that data.
|
||||
s_BaselinePendingData = 0;
|
||||
#ifdef Q_OS_WIN32
|
||||
for (int i = 0; i < 100; i++) {
|
||||
s_BaselinePendingData = qMax(s_BaselinePendingData, SDL_GetQueuedAudioSize(s_AudioDevice));
|
||||
SDL_Delay(10);
|
||||
}
|
||||
#endif
|
||||
s_BaselinePendingData *= 2;
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Baseline pending audio data: %d bytes",
|
||||
s_BaselinePendingData);
|
||||
|
||||
s_ChannelCount = opusConfig->channelCount;
|
||||
s_SampleIndex = 0;
|
||||
s_PendingDrops = s_PendingHardDrops = 0;
|
||||
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Audio stream has %d channels",
|
||||
opusConfig->channelCount);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void Session::sdlAudioStart()
|
||||
{
|
||||
// Unpause the audio device
|
||||
SDL_PauseAudioDevice(s_AudioDevice, 0);
|
||||
}
|
||||
|
||||
void Session::sdlAudioStop()
|
||||
{
|
||||
// Pause the audio device
|
||||
SDL_PauseAudioDevice(s_AudioDevice, 1);
|
||||
}
|
||||
|
||||
void Session::sdlAudioCleanup()
|
||||
{
|
||||
SDL_CloseAudioDevice(s_AudioDevice);
|
||||
s_AudioDevice = 0;
|
||||
|
||||
opus_multistream_decoder_destroy(s_OpusDecoder);
|
||||
s_OpusDecoder = nullptr;
|
||||
|
||||
SDL_QuitSubSystem(SDL_INIT_AUDIO);
|
||||
SDL_assert(!SDL_WasInit(SDL_INIT_AUDIO));
|
||||
}
|
||||
|
||||
void Session::sdlAudioDecodeAndPlaySample(char* sampleData, int sampleLength)
|
||||
{
|
||||
int samplesDecoded;
|
||||
|
||||
s_SampleIndex++;
|
||||
|
||||
Uint32 queuedAudio = qMax((int)SDL_GetQueuedAudioSize(s_AudioDevice) - (int)s_BaselinePendingData, 0);
|
||||
Uint32 framesQueued = queuedAudio / (SAMPLES_PER_FRAME * s_ChannelCount * sizeof(short));
|
||||
|
||||
// Pend enough drops to get us back to MIN_QUEUED_FRAMES
|
||||
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 (framesQueued <= MIN_QUEUED_FRAMES) {
|
||||
s_PendingDrops = s_PendingHardDrops = 0;
|
||||
}
|
||||
else if (s_PendingHardDrops != 0) {
|
||||
// Hard drops happen all at once to forcefully
|
||||
// resync with the source.
|
||||
s_PendingHardDrops--;
|
||||
return;
|
||||
}
|
||||
else if (s_PendingDrops != 0 && s_SampleIndex % DROP_RATIO_DENOM == 0) {
|
||||
// Normal drops are interspersed with the audio data
|
||||
// to hide the glitches.
|
||||
s_PendingDrops--;
|
||||
return;
|
||||
}
|
||||
|
||||
samplesDecoded = opus_multistream_decode(s_OpusDecoder,
|
||||
(unsigned char*)sampleData,
|
||||
sampleLength,
|
||||
s_OpusDecodeBuffer,
|
||||
SAMPLES_PER_FRAME,
|
||||
0);
|
||||
if (samplesDecoded > 0) {
|
||||
if (SDL_QueueAudio(s_AudioDevice,
|
||||
s_OpusDecodeBuffer,
|
||||
sizeof(short) * samplesDecoded * s_ChannelCount) < 0) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Failed to queue audio sample: %s",
|
||||
SDL_GetError());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
#include "../session.h"
|
||||
#include "renderers/renderer.h"
|
||||
|
||||
#ifdef HAVE_SOUNDIO
|
||||
#include "renderers/soundioaudiorenderer.h"
|
||||
#else
|
||||
#include "renderers/sdl.h"
|
||||
#endif
|
||||
|
||||
#include <Limelight.h>
|
||||
|
||||
IAudioRenderer* Session::createAudioRenderer()
|
||||
{
|
||||
#ifdef HAVE_SOUNDIO
|
||||
return new SoundIoAudioRenderer();
|
||||
#else
|
||||
return new SdlAudioRenderer();
|
||||
#endif
|
||||
}
|
||||
|
||||
bool Session::testAudio(int audioConfiguration)
|
||||
{
|
||||
IAudioRenderer* audioRenderer;
|
||||
|
||||
audioRenderer = createAudioRenderer();
|
||||
if (audioRenderer == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Build a fake OPUS_MULTISTREAM_CONFIGURATION to give
|
||||
// the renderer the channel count and sample rate.
|
||||
OPUS_MULTISTREAM_CONFIGURATION opusConfig = {};
|
||||
opusConfig.sampleRate = 48000;
|
||||
|
||||
switch (audioConfiguration)
|
||||
{
|
||||
case AUDIO_CONFIGURATION_STEREO:
|
||||
opusConfig.channelCount = 2;
|
||||
break;
|
||||
case AUDIO_CONFIGURATION_51_SURROUND:
|
||||
opusConfig.channelCount = 6;
|
||||
break;
|
||||
default:
|
||||
SDL_assert(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ret = audioRenderer->prepareForPlayback(&opusConfig);
|
||||
|
||||
delete audioRenderer;
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
int Session::arInit(int /* audioConfiguration */,
|
||||
const POPUS_MULTISTREAM_CONFIGURATION opusConfig,
|
||||
void* /* arContext */, int /* arFlags */)
|
||||
{
|
||||
int error;
|
||||
|
||||
SDL_memcpy(&s_ActiveSession->m_AudioConfig, opusConfig, sizeof(*opusConfig));
|
||||
|
||||
s_ActiveSession->m_OpusDecoder =
|
||||
opus_multistream_decoder_create(opusConfig->sampleRate,
|
||||
opusConfig->channelCount,
|
||||
opusConfig->streams,
|
||||
opusConfig->coupledStreams,
|
||||
opusConfig->mapping,
|
||||
&error);
|
||||
if (s_ActiveSession->m_OpusDecoder == NULL) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Failed to create decoder: %d",
|
||||
error);
|
||||
return -1;
|
||||
}
|
||||
|
||||
s_ActiveSession->m_AudioRenderer = s_ActiveSession->createAudioRenderer();
|
||||
if (!s_ActiveSession->m_AudioRenderer->prepareForPlayback(opusConfig)) {
|
||||
delete s_ActiveSession->m_AudioRenderer;
|
||||
opus_multistream_decoder_destroy(s_ActiveSession->m_OpusDecoder);
|
||||
return -2;
|
||||
}
|
||||
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Audio stream has %d channels",
|
||||
opusConfig->channelCount);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void Session::arCleanup()
|
||||
{
|
||||
delete s_ActiveSession->m_AudioRenderer;
|
||||
s_ActiveSession->m_AudioRenderer = nullptr;
|
||||
|
||||
opus_multistream_decoder_destroy(s_ActiveSession->m_OpusDecoder);
|
||||
s_ActiveSession->m_OpusDecoder = nullptr;
|
||||
}
|
||||
|
||||
void Session::arDecodeAndPlaySample(char* sampleData, int sampleLength)
|
||||
{
|
||||
int samplesDecoded;
|
||||
|
||||
// Set this thread to high priority to reduce
|
||||
// the chance of missing our sample delivery time
|
||||
if (s_ActiveSession->m_AudioSampleCount == 0) {
|
||||
if (SDL_SetThreadPriority(SDL_THREAD_PRIORITY_HIGH) < 0) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Unable to set audio thread to high priority: %s",
|
||||
SDL_GetError());
|
||||
}
|
||||
}
|
||||
|
||||
s_ActiveSession->m_AudioSampleCount++;
|
||||
|
||||
if (s_ActiveSession->m_AudioRenderer != nullptr) {
|
||||
samplesDecoded = opus_multistream_decode(s_ActiveSession->m_OpusDecoder,
|
||||
(unsigned char*)sampleData,
|
||||
sampleLength,
|
||||
s_ActiveSession->m_OpusDecodeBuffer,
|
||||
SAMPLES_PER_FRAME,
|
||||
0);
|
||||
if (samplesDecoded > 0) {
|
||||
if (!s_ActiveSession->m_AudioRenderer->submitAudio(s_ActiveSession->m_OpusDecodeBuffer,
|
||||
static_cast<int>(
|
||||
sizeof(short) *
|
||||
samplesDecoded *
|
||||
s_ActiveSession->m_AudioConfig.channelCount))) {
|
||||
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Reinitializing audio renderer after failure");
|
||||
|
||||
delete s_ActiveSession->m_AudioRenderer;
|
||||
s_ActiveSession->m_AudioRenderer = nullptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Only try to recreate the audio renderer every 200 samples (1 second)
|
||||
// to avoid thrashing if the audio device is unavailable. It is
|
||||
// safe to reinitialize here because we can't be torn down while
|
||||
// the audio decoder/playback thread is still alive.
|
||||
if (s_ActiveSession->m_AudioRenderer == nullptr && (s_ActiveSession->m_AudioSampleCount % 200) == 0) {
|
||||
s_ActiveSession->m_AudioRenderer = s_ActiveSession->createAudioRenderer();
|
||||
if (!s_ActiveSession->m_AudioRenderer->prepareForPlayback(&s_ActiveSession->m_AudioConfig)) {
|
||||
delete s_ActiveSession->m_AudioRenderer;
|
||||
s_ActiveSession->m_AudioRenderer = nullptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
#include "qtaud.h"
|
||||
|
||||
#include <QDebug>
|
||||
|
||||
QtAudioRenderer::QtAudioRenderer()
|
||||
: m_AudioOutput(nullptr),
|
||||
m_OutputDevice(nullptr)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
QtAudioRenderer::~QtAudioRenderer()
|
||||
{
|
||||
delete m_AudioOutput;
|
||||
}
|
||||
|
||||
bool QtAudioRenderer::prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig)
|
||||
{
|
||||
QAudioFormat format;
|
||||
|
||||
// testAudio() assumes this sample rate
|
||||
Q_ASSERT(opusConfig->sampleRate == 48000);
|
||||
|
||||
format.setSampleRate(opusConfig->sampleRate);
|
||||
format.setChannelCount(opusConfig->channelCount);
|
||||
format.setSampleSize(16);
|
||||
format.setSampleType(QAudioFormat::SignedInt);
|
||||
format.setCodec("audio/pcm");
|
||||
|
||||
m_AudioOutput = new QAudioOutput(format);
|
||||
|
||||
// Buffer 40 ms of audio data
|
||||
m_AudioOutput->setBufferSize(format.bytesForDuration(40000));
|
||||
|
||||
m_OutputDevice = m_AudioOutput->start();
|
||||
if (m_OutputDevice == nullptr) {
|
||||
qWarning() << "Audio start failed:" << m_AudioOutput->error();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check what buffer size was really used after starting
|
||||
qInfo() << "Audio stream buffer:" << m_AudioOutput->bufferSize() << "bytes";
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void QtAudioRenderer::submitAudio(short* audioBuffer, int audioSize)
|
||||
{
|
||||
m_OutputDevice->write((const char *)audioBuffer, audioSize);
|
||||
}
|
||||
|
||||
bool QtAudioRenderer::testAudio(int audioConfiguration)
|
||||
{
|
||||
QAudioFormat format;
|
||||
|
||||
format.setSampleRate(48000);
|
||||
format.setSampleSize(16);
|
||||
format.setSampleType(QAudioFormat::SignedInt);
|
||||
format.setCodec("audio/pcm");
|
||||
|
||||
switch (audioConfiguration) {
|
||||
case AUDIO_CONFIGURATION_STEREO:
|
||||
format.setChannelCount(2);
|
||||
break;
|
||||
case AUDIO_CONFIGURATION_51_SURROUND:
|
||||
format.setChannelCount(6);
|
||||
break;
|
||||
default:
|
||||
Q_ASSERT(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (QAudioDeviceInfo::defaultOutputDevice().isFormatSupported(format)) {
|
||||
qInfo() << "Audio test successful with" << format.channelCount() << "channels";
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
qWarning() << "Audio test failed with" << format.channelCount() << "channels";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
int QtAudioRenderer::detectAudioConfiguration()
|
||||
{
|
||||
int preferredChannelCount = QAudioDeviceInfo::defaultOutputDevice().preferredFormat().channelCount();
|
||||
|
||||
qInfo() << "Audio output device prefers" << preferredChannelCount << "channel configuration";
|
||||
|
||||
// We can better downmix 5.1 to quad than we can upmix stereo
|
||||
if (preferredChannelCount > 2) {
|
||||
return AUDIO_CONFIGURATION_51_SURROUND;
|
||||
}
|
||||
else {
|
||||
return AUDIO_CONFIGURATION_STEREO;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include "renderer.h"
|
||||
#include <QAudioOutput>
|
||||
|
||||
class QtAudioRenderer : public IAudioRenderer
|
||||
{
|
||||
public:
|
||||
QtAudioRenderer();
|
||||
|
||||
virtual ~QtAudioRenderer();
|
||||
|
||||
virtual bool prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig);
|
||||
|
||||
virtual void submitAudio(short* audioBuffer, int audioSize);
|
||||
|
||||
virtual bool testAudio(int audioConfiguration);
|
||||
|
||||
virtual int detectAudioConfiguration();
|
||||
|
||||
private:
|
||||
QAudioOutput* m_AudioOutput;
|
||||
QIODevice* m_OutputDevice;
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#include <Limelight.h>
|
||||
|
||||
#define MAX_CHANNELS 6
|
||||
#define SAMPLES_PER_FRAME 240
|
||||
|
||||
class IAudioRenderer
|
||||
{
|
||||
public:
|
||||
virtual ~IAudioRenderer() {}
|
||||
|
||||
virtual bool prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig) = 0;
|
||||
|
||||
// Return false if an unrecoverable error has occurred and the renderer must be reinitialized
|
||||
virtual bool submitAudio(short* audioBuffer, int audioSize) = 0;
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include "renderer.h"
|
||||
#include <SDL.h>
|
||||
|
||||
class SdlAudioRenderer : public IAudioRenderer
|
||||
{
|
||||
public:
|
||||
SdlAudioRenderer();
|
||||
|
||||
virtual ~SdlAudioRenderer();
|
||||
|
||||
virtual bool prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig);
|
||||
|
||||
virtual bool submitAudio(short* audioBuffer, int audioSize);
|
||||
|
||||
private:
|
||||
SDL_AudioDeviceID m_AudioDevice;
|
||||
Uint32 m_ChannelCount;
|
||||
Uint32 m_PendingDrops;
|
||||
Uint32 m_PendingHardDrops;
|
||||
Uint32 m_SampleIndex;
|
||||
Uint32 m_BaselinePendingData;
|
||||
};
|
||||
@@ -0,0 +1,159 @@
|
||||
#include "sdl.h"
|
||||
|
||||
#include <Limelight.h>
|
||||
#include <SDL.h>
|
||||
|
||||
#include <QtGlobal>
|
||||
|
||||
#define MIN_QUEUED_FRAMES 2
|
||||
#define MAX_QUEUED_FRAMES 4
|
||||
#define STOP_THE_WORLD_LIMIT 20
|
||||
#define DROP_RATIO_DENOM 32
|
||||
|
||||
SdlAudioRenderer::SdlAudioRenderer()
|
||||
: m_AudioDevice(0),
|
||||
m_ChannelCount(0),
|
||||
m_PendingDrops(0),
|
||||
m_PendingHardDrops(0),
|
||||
m_SampleIndex(0),
|
||||
m_BaselinePendingData(0)
|
||||
{
|
||||
SDL_assert(!SDL_WasInit(SDL_INIT_AUDIO));
|
||||
if (SDL_InitSubSystem(SDL_INIT_AUDIO) != 0) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"SDL_InitSubSystem(SDL_INIT_AUDIO) failed: %s",
|
||||
SDL_GetError());
|
||||
SDL_assert(SDL_WasInit(SDL_INIT_AUDIO));
|
||||
}
|
||||
}
|
||||
|
||||
bool SdlAudioRenderer::prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig)
|
||||
{
|
||||
SDL_AudioSpec want, have;
|
||||
|
||||
SDL_zero(want);
|
||||
want.freq = opusConfig->sampleRate;
|
||||
want.format = AUDIO_S16;
|
||||
want.channels = opusConfig->channelCount;
|
||||
|
||||
// This is supposed to be a power of 2, but our
|
||||
// frames contain a non-power of 2 number of samples,
|
||||
// so the slop would require buffering another full frame.
|
||||
// Specifying non-Po2 seems to work for our supported platforms.
|
||||
want.samples = SAMPLES_PER_FRAME;
|
||||
|
||||
m_AudioDevice = SDL_OpenAudioDevice(NULL, 0, &want, &have, 0);
|
||||
if (m_AudioDevice == 0) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Failed to open audio device: %s",
|
||||
SDL_GetError());
|
||||
SDL_QuitSubSystem(SDL_INIT_AUDIO);
|
||||
return false;
|
||||
}
|
||||
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Desired audio buffer: %u samples (%lu bytes)",
|
||||
want.samples,
|
||||
want.samples * sizeof(short) * want.channels);
|
||||
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Obtained audio buffer: %u samples (%u bytes)",
|
||||
have.samples,
|
||||
have.size);
|
||||
|
||||
// SDL counts pending samples in the queued
|
||||
// audio size using the WASAPI backend. This
|
||||
// includes silence, which can throw off our
|
||||
// pending data count. Get a baseline so we
|
||||
// can exclude that data.
|
||||
m_BaselinePendingData = 0;
|
||||
#ifdef Q_OS_WIN32
|
||||
for (int i = 0; i < 100; i++) {
|
||||
m_BaselinePendingData = qMax(m_BaselinePendingData, SDL_GetQueuedAudioSize(m_AudioDevice));
|
||||
SDL_Delay(10);
|
||||
}
|
||||
#endif
|
||||
m_BaselinePendingData *= 2;
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Baseline pending audio data: %d bytes",
|
||||
m_BaselinePendingData);
|
||||
|
||||
m_ChannelCount = opusConfig->channelCount;
|
||||
m_SampleIndex = 0;
|
||||
m_PendingDrops = m_PendingHardDrops = 0;
|
||||
|
||||
// Start playback
|
||||
SDL_PauseAudioDevice(m_AudioDevice, 0);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
SdlAudioRenderer::~SdlAudioRenderer()
|
||||
{
|
||||
if (m_AudioDevice != 0) {
|
||||
// Stop playback
|
||||
SDL_PauseAudioDevice(m_AudioDevice, 1);
|
||||
SDL_CloseAudioDevice(m_AudioDevice);
|
||||
}
|
||||
|
||||
SDL_QuitSubSystem(SDL_INIT_AUDIO);
|
||||
SDL_assert(!SDL_WasInit(SDL_INIT_AUDIO));
|
||||
}
|
||||
|
||||
bool SdlAudioRenderer::submitAudio(short* audioBuffer, int audioSize)
|
||||
{
|
||||
m_SampleIndex++;
|
||||
|
||||
Uint32 queuedAudio = SDL_GetQueuedAudioSize(m_AudioDevice);
|
||||
if (queuedAudio > m_BaselinePendingData) {
|
||||
queuedAudio -= m_BaselinePendingData;
|
||||
}
|
||||
else {
|
||||
queuedAudio = 0;
|
||||
}
|
||||
|
||||
Uint32 framesQueued = queuedAudio / (SAMPLES_PER_FRAME * m_ChannelCount * sizeof(short));
|
||||
|
||||
// We must check this prior to the below checks to ensure we don't
|
||||
// underflow if framesQueued - m_PendingHardDrops < 0.
|
||||
if (framesQueued <= MIN_QUEUED_FRAMES) {
|
||||
m_PendingDrops = m_PendingHardDrops = 0;
|
||||
}
|
||||
// Pend enough drops to get us back to MIN_QUEUED_FRAMES, checking first
|
||||
// to ensure we don't underflow.
|
||||
else if (framesQueued > m_PendingHardDrops &&
|
||||
framesQueued - m_PendingHardDrops > STOP_THE_WORLD_LIMIT) {
|
||||
m_PendingHardDrops = framesQueued - MIN_QUEUED_FRAMES;
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Pending hard drop of %u audio frames",
|
||||
m_PendingHardDrops);
|
||||
}
|
||||
// If we're under the stop the world limit, we can drop samples
|
||||
// gracefully over the next little while.
|
||||
else if (framesQueued > m_PendingHardDrops + m_PendingDrops &&
|
||||
framesQueued - m_PendingHardDrops - m_PendingDrops > MAX_QUEUED_FRAMES) {
|
||||
m_PendingDrops = framesQueued - MIN_QUEUED_FRAMES;
|
||||
}
|
||||
|
||||
// Determine if this frame should be dropped
|
||||
if (m_PendingHardDrops != 0) {
|
||||
// Hard drops happen all at once to forcefully
|
||||
// resync with the source.
|
||||
m_PendingHardDrops--;
|
||||
return true;
|
||||
}
|
||||
else if (m_PendingDrops != 0 && m_SampleIndex % DROP_RATIO_DENOM == 0) {
|
||||
// Normal drops are interspersed with the audio data
|
||||
// to hide the glitches.
|
||||
m_PendingDrops--;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (SDL_QueueAudio(m_AudioDevice, audioBuffer, audioSize) < 0) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Failed to queue audio sample: %s",
|
||||
SDL_GetError());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,436 @@
|
||||
#include "soundioaudiorenderer.h"
|
||||
|
||||
#include <SDL.h>
|
||||
|
||||
#include <QtGlobal>
|
||||
|
||||
SoundIoAudioRenderer::SoundIoAudioRenderer()
|
||||
: m_OpusChannelCount(0),
|
||||
m_SoundIo(nullptr),
|
||||
m_Device(nullptr),
|
||||
m_OutputStream(nullptr),
|
||||
m_RingBuffer(nullptr),
|
||||
m_Errored(false)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
SoundIoAudioRenderer::~SoundIoAudioRenderer()
|
||||
{
|
||||
if (m_OutputStream != nullptr) {
|
||||
soundio_outstream_destroy(m_OutputStream);
|
||||
}
|
||||
|
||||
// Must be destroyed after the stream is stopped
|
||||
// or we could still get sioWriteCallback() calls.
|
||||
if (m_RingBuffer != nullptr) {
|
||||
soundio_ring_buffer_destroy(m_RingBuffer);
|
||||
}
|
||||
|
||||
if (m_Device != nullptr) {
|
||||
soundio_device_unref(m_Device);
|
||||
}
|
||||
|
||||
if (m_SoundIo != nullptr) {
|
||||
soundio_destroy(m_SoundIo);
|
||||
}
|
||||
}
|
||||
|
||||
int SoundIoAudioRenderer::scoreChannelLayout(const struct SoundIoChannelLayout* layout, const OPUS_MULTISTREAM_CONFIGURATION* opusConfig)
|
||||
{
|
||||
int score = 50;
|
||||
|
||||
// Compute a score for this layout based on how many matching channels
|
||||
// we find (or acceptable alternatives).
|
||||
for (int i = 0; i < layout->channel_count; i++) {
|
||||
if (opusConfig->channelCount >= 2) {
|
||||
switch (layout->channels[i]) {
|
||||
case SoundIoChannelIdFrontLeft:
|
||||
case SoundIoChannelIdFrontRight:
|
||||
score += 2;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (opusConfig->channelCount >= 6) {
|
||||
switch (layout->channels[i]) {
|
||||
case SoundIoChannelIdFrontCenter:
|
||||
case SoundIoChannelIdLfe:
|
||||
score += 2;
|
||||
break;
|
||||
|
||||
case SoundIoChannelIdSideLeft:
|
||||
case SoundIoChannelIdSideRight:
|
||||
score++;
|
||||
break;
|
||||
|
||||
// Score layouts using the back L/R as higher
|
||||
// value than those using side L/R.
|
||||
case SoundIoChannelIdBackLeft:
|
||||
case SoundIoChannelIdBackRight:
|
||||
score += 2;
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Now subtract the difference between the desired and actual channel count
|
||||
// to punish layouts that have extra unused speakers.
|
||||
if (opusConfig->channelCount < layout->channel_count) {
|
||||
score -= layout->channel_count - opusConfig->channelCount;
|
||||
}
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
bool SoundIoAudioRenderer::prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig)
|
||||
{
|
||||
m_SoundIo = soundio_create();
|
||||
if (m_SoundIo == nullptr) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"soundio_create() failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
m_SoundIo->app_name = "Moonlight";
|
||||
m_SoundIo->userdata = this;
|
||||
m_SoundIo->on_backend_disconnect = sioBackendDisconnect;
|
||||
m_SoundIo->on_devices_change = sioDevicesChanged;
|
||||
|
||||
int err = soundio_connect(m_SoundIo);
|
||||
if (err != SoundIoErrorNone) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"soundio_connect() failed: %s",
|
||||
soundio_strerror(err));
|
||||
return false;
|
||||
}
|
||||
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Audio backend: %s",
|
||||
soundio_backend_name(m_SoundIo->current_backend));
|
||||
|
||||
// Don't continue if we could only open the dummy backend
|
||||
if (m_SoundIo->current_backend == SoundIoBackendDummy) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Flush events to update with new device arrivals
|
||||
soundio_flush_events(m_SoundIo);
|
||||
|
||||
// Remember the actual channel count for later
|
||||
m_OpusChannelCount = opusConfig->channelCount;
|
||||
|
||||
int outputDeviceIndex = soundio_default_output_device_index(m_SoundIo);
|
||||
if (outputDeviceIndex < 0) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"No output device found");
|
||||
return false;
|
||||
}
|
||||
|
||||
m_Device = soundio_get_output_device(m_SoundIo, outputDeviceIndex);
|
||||
if (m_Device == nullptr) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"soundio_get_output_device() failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Selected audio device: %s",
|
||||
m_Device->name);
|
||||
|
||||
m_OutputStream = soundio_outstream_create(m_Device);
|
||||
if (m_OutputStream == nullptr) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"soundio_outstream_create() failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
m_OutputStream->format = SoundIoFormatS16NE;
|
||||
m_OutputStream->sample_rate = opusConfig->sampleRate;
|
||||
m_OutputStream->name = "Moonlight";
|
||||
m_OutputStream->userdata = this;
|
||||
m_OutputStream->error_callback = sioErrorCallback;
|
||||
m_OutputStream->write_callback = sioWriteCallback;
|
||||
|
||||
// This determines the size of the buffers we'll
|
||||
// get from CoreAudio. Since GFE sends us packets
|
||||
// in 5 ms chunks, we'll give them to the OS in
|
||||
// buffers of the same size.
|
||||
m_OutputStream->software_latency = 0.005;
|
||||
|
||||
SoundIoChannelLayout bestLayout = m_Device->current_layout;
|
||||
for (int i = 0; i < m_Device->layout_count; i++) {
|
||||
if (scoreChannelLayout(&bestLayout, opusConfig) <
|
||||
scoreChannelLayout(&m_Device->layouts[i], opusConfig)) {
|
||||
bestLayout = m_Device->layouts[i];
|
||||
}
|
||||
}
|
||||
|
||||
if (bestLayout.channel_count < opusConfig->channelCount) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"No compatible channel layouts found. Some channels may not be played!");
|
||||
}
|
||||
|
||||
m_OutputStream->layout = bestLayout;
|
||||
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Native layout: %s (%d channels)",
|
||||
m_OutputStream->layout.name ?
|
||||
m_OutputStream->layout.name : "<UNKNOWN>",
|
||||
m_OutputStream->layout.channel_count);
|
||||
|
||||
err = soundio_outstream_open(m_OutputStream);
|
||||
if (err != SoundIoErrorNone) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"soundio_outstream_open() failed: %s",
|
||||
soundio_strerror(err));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_OutputStream->layout_error) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Channel layout failed: %s",
|
||||
soundio_strerror(m_OutputStream->layout_error));
|
||||
|
||||
// ALSA through PulseAudio appears to fail snd_pcm_set_chmap()
|
||||
// even after claiming the layout is supported (and even on totally
|
||||
// standard layouts like Stereo). We'll just ignore this for ALSA
|
||||
// and only bail if we get an actual failure out of one of these APIs.
|
||||
if (m_SoundIo->current_backend != SoundIoBackendAlsa) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
m_EffectiveLayout = m_OutputStream->layout;
|
||||
for (int i = 0; i < m_EffectiveLayout.channel_count; i++) {
|
||||
// Fixup the layout to use back L/R so our channel position
|
||||
// logic in sioWriteCallback() works.
|
||||
if (m_EffectiveLayout.channels[i] == SoundIoChannelIdSideLeft) {
|
||||
m_EffectiveLayout.channels[i] = SoundIoChannelIdBackLeft;
|
||||
}
|
||||
if (m_EffectiveLayout.channels[i] == SoundIoChannelIdSideRight) {
|
||||
m_EffectiveLayout.channels[i] = SoundIoChannelIdBackRight;
|
||||
}
|
||||
}
|
||||
|
||||
// Buffer up to 6 packets of audio (30 ms) to smooth
|
||||
// out network packet delivery jitter
|
||||
m_RingBuffer = soundio_ring_buffer_create(m_SoundIo,
|
||||
m_OutputStream->bytes_per_sample *
|
||||
m_OpusChannelCount *
|
||||
SAMPLES_PER_FRAME * 6);
|
||||
if (m_RingBuffer == nullptr) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"soundio_ring_buffer_create() failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
err = soundio_outstream_start(m_OutputStream);
|
||||
if (err != SoundIoErrorNone) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"soundio_outstream_start() failed: %s",
|
||||
soundio_strerror(err));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SoundIoAudioRenderer::submitAudio(short* audioBuffer, int audioSize)
|
||||
{
|
||||
if (m_Errored) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Flush events to update with new device arrivals
|
||||
soundio_flush_events(m_SoundIo);
|
||||
|
||||
// We must always write a full frame of audio. If we don't,
|
||||
// the reader will get out of sync with the writer and our
|
||||
// channels will get all mixed up. To ensure this is always
|
||||
// the case, round our bytes free down to the next multiple
|
||||
// of our frame size.
|
||||
int bytesFree = soundio_ring_buffer_free_count(m_RingBuffer);
|
||||
int bytesPerFrame = m_OpusChannelCount * m_OutputStream->bytes_per_sample;
|
||||
int bytesToWrite = qMin(audioSize, (bytesFree / bytesPerFrame) * bytesPerFrame);
|
||||
memcpy(soundio_ring_buffer_write_ptr(m_RingBuffer), audioBuffer, bytesToWrite);
|
||||
soundio_ring_buffer_advance_write_ptr(m_RingBuffer, bytesToWrite);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void SoundIoAudioRenderer::sioErrorCallback(SoundIoOutStream* stream, int err)
|
||||
{
|
||||
auto me = reinterpret_cast<SoundIoAudioRenderer*>(stream->userdata);
|
||||
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Audio rendering error: %s",
|
||||
soundio_strerror(err));
|
||||
|
||||
// Trigger reinitialization
|
||||
me->m_Errored = true;
|
||||
}
|
||||
|
||||
void SoundIoAudioRenderer::sioBackendDisconnect(SoundIo* soundio, int err)
|
||||
{
|
||||
auto me = reinterpret_cast<SoundIoAudioRenderer*>(soundio->userdata);
|
||||
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Audio backend disconnected: %s",
|
||||
soundio_strerror(err));
|
||||
|
||||
// Trigger reinitialization
|
||||
me->m_Errored = true;
|
||||
}
|
||||
|
||||
void SoundIoAudioRenderer::sioDevicesChanged(SoundIo* soundio)
|
||||
{
|
||||
auto me = reinterpret_cast<SoundIoAudioRenderer*>(soundio->userdata);
|
||||
|
||||
if (me->m_Device == nullptr) {
|
||||
// Ignore calls that take place during initialization
|
||||
return;
|
||||
}
|
||||
|
||||
int outputDeviceIndex = soundio_default_output_device_index(soundio);
|
||||
if (outputDeviceIndex >= 0) {
|
||||
struct SoundIoDevice* outputDevice = soundio_get_output_device(soundio, outputDeviceIndex);
|
||||
if (outputDevice == nullptr) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"soundio_get_output_device() failed");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!soundio_device_equal(outputDevice, me->m_Device)) {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Default audio output device changed");
|
||||
|
||||
// Trigger reinitialization
|
||||
me->m_Errored = true;
|
||||
}
|
||||
|
||||
soundio_device_unref(outputDevice);
|
||||
}
|
||||
}
|
||||
|
||||
// bytes_per_frame should never be used on the ring buffer! It's not always
|
||||
// the same number of bytes per frames as the output stream!
|
||||
void SoundIoAudioRenderer::sioWriteCallback(SoundIoOutStream* stream, int frameCountMin, int frameCountMax)
|
||||
{
|
||||
auto me = reinterpret_cast<SoundIoAudioRenderer*>(stream->userdata);
|
||||
char* readPtr = soundio_ring_buffer_read_ptr(me->m_RingBuffer);
|
||||
int framesLeft = soundio_ring_buffer_fill_count(me->m_RingBuffer) /
|
||||
(me->m_OpusChannelCount * stream->bytes_per_sample);
|
||||
int bytesRead = 0;
|
||||
|
||||
// Clamp framesLeft to frameCountMax
|
||||
framesLeft = qMin(framesLeft, frameCountMax);
|
||||
|
||||
// Place an upper-bound on audio stream latency to
|
||||
// avoid accumulating packets in queue-based backends
|
||||
// like WASAPI. This bound was set by testing on several
|
||||
// Windows machines. The highest latency was found on
|
||||
// a XPS 9343 running Windows 7 in Steam Big Picture
|
||||
// and the 5.1 audio test clip from Fraunhofer.
|
||||
if (me->m_SoundIo->current_backend == SoundIoBackendWasapi) {
|
||||
double latency;
|
||||
if (soundio_outstream_get_latency(stream, &latency) == SoundIoErrorNone) {
|
||||
if (latency > 0.050) {
|
||||
// If our latency is higher than desired, drop these samples to gracefully lower
|
||||
// the latency without glitching too much. Dropping the whole buffer causes
|
||||
// a much more noticeable glitch. This approach also ensures that we don't
|
||||
// accidentally underflow if the driver/kernel side is delayed and isn't
|
||||
// consuming data fast enough. Dropping a frame at a time and re-evaluating
|
||||
// each time ensures that we'll stop dropping if latency returns to normal.
|
||||
readPtr += framesLeft * stream->bytes_per_sample * me->m_OpusChannelCount;
|
||||
bytesRead += framesLeft * stream->bytes_per_sample * me->m_OpusChannelCount;
|
||||
framesLeft = 0;
|
||||
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Latency exceeded drop cap: %f",
|
||||
latency);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
int frameCount;
|
||||
int err;
|
||||
struct SoundIoChannelArea* areas;
|
||||
|
||||
// Always meet the minimum but don't write more than that
|
||||
// if we'll have to insert silence
|
||||
frameCount = qMax(framesLeft, frameCountMin);
|
||||
|
||||
if (frameCount == 0) {
|
||||
// Nothing more to write
|
||||
break;
|
||||
}
|
||||
|
||||
err = soundio_outstream_begin_write(stream, &areas, &frameCount);
|
||||
if (err != SoundIoErrorNone) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"soundio_outstream_begin_write() failed: %s",
|
||||
soundio_strerror(err));
|
||||
break;
|
||||
}
|
||||
|
||||
for (int frame = 0; frame < frameCount; frame++) {
|
||||
for (int ch = 0; ch < me->m_EffectiveLayout.channel_count; ch++) {
|
||||
// SoundIoChannelId - 1 happens to match Moonlight's channel layout.
|
||||
// For side L/R, this logic depends on us fixing those up
|
||||
// in m_EffectiveLayout to back L/R.
|
||||
int readPtrChannel = me->m_EffectiveLayout.channels[ch] - 1;
|
||||
|
||||
if (frame >= framesLeft || readPtrChannel >= me->m_OpusChannelCount) {
|
||||
// Write silence if we have no buffered frames left or
|
||||
// nothing in the audio stream for this channel
|
||||
memset(areas[ch].ptr, 0, stream->bytes_per_sample);
|
||||
}
|
||||
else {
|
||||
// Write audio data from our ring buffer
|
||||
memcpy(areas[ch].ptr,
|
||||
&readPtr[readPtrChannel * stream->bytes_per_sample],
|
||||
stream->bytes_per_sample);
|
||||
}
|
||||
|
||||
areas[ch].ptr += areas[ch].step;
|
||||
}
|
||||
|
||||
// Move on to the next frame if we aren't inserting silence
|
||||
if (frame < framesLeft) {
|
||||
readPtr += stream->bytes_per_sample * me->m_OpusChannelCount;
|
||||
bytesRead += stream->bytes_per_sample * me->m_OpusChannelCount;
|
||||
}
|
||||
}
|
||||
|
||||
err = soundio_outstream_end_write(stream);
|
||||
if (err != SoundIoErrorNone && err != SoundIoErrorUnderflow) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"soundio_outstream_end_write() failed: %s",
|
||||
soundio_strerror(err));
|
||||
break;
|
||||
}
|
||||
|
||||
if (framesLeft >= frameCount) {
|
||||
framesLeft -= frameCount;
|
||||
}
|
||||
else {
|
||||
framesLeft = 0;
|
||||
}
|
||||
|
||||
if (frameCountMin >= frameCount) {
|
||||
frameCountMin -= frameCount;
|
||||
}
|
||||
else {
|
||||
frameCountMin = 0;
|
||||
}
|
||||
}
|
||||
|
||||
soundio_ring_buffer_advance_read_ptr(me->m_RingBuffer, bytesRead);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
#pragma once
|
||||
|
||||
#include "renderer.h"
|
||||
|
||||
#include <soundio/soundio.h>
|
||||
|
||||
class SoundIoAudioRenderer : public IAudioRenderer
|
||||
{
|
||||
public:
|
||||
SoundIoAudioRenderer();
|
||||
|
||||
~SoundIoAudioRenderer();
|
||||
|
||||
virtual bool prepareForPlayback(const OPUS_MULTISTREAM_CONFIGURATION* opusConfig);
|
||||
|
||||
virtual bool submitAudio(short* audioBuffer, int audioSize);
|
||||
|
||||
private:
|
||||
int scoreChannelLayout(const struct SoundIoChannelLayout* layout, const OPUS_MULTISTREAM_CONFIGURATION* opusConfig);
|
||||
|
||||
static void sioErrorCallback(struct SoundIoOutStream* stream, int err);
|
||||
|
||||
static void sioWriteCallback(struct SoundIoOutStream* stream, int frameCountMin, int frameCountMax);
|
||||
|
||||
static void sioBackendDisconnect(struct SoundIo* soundio, int err);
|
||||
|
||||
static void sioDevicesChanged(SoundIo* soundio);
|
||||
|
||||
int m_OpusChannelCount;
|
||||
struct SoundIo* m_SoundIo;
|
||||
struct SoundIoDevice* m_Device;
|
||||
struct SoundIoOutStream* m_OutputStream;
|
||||
struct SoundIoRingBuffer* m_RingBuffer;
|
||||
struct SoundIoChannelLayout m_EffectiveLayout;
|
||||
bool m_Errored;
|
||||
};
|
||||
+417
-28
@@ -1,8 +1,11 @@
|
||||
#include <Limelight.h>
|
||||
#include <SDL.h>
|
||||
#include "streaming/session.hpp"
|
||||
#include "streaming/session.h"
|
||||
#include "settings/mappingmanager.h"
|
||||
#include "path.h"
|
||||
|
||||
#include <QtGlobal>
|
||||
#include <QDir>
|
||||
|
||||
#define VK_0 0x30
|
||||
#define VK_A 0x41
|
||||
@@ -14,6 +17,15 @@
|
||||
#define VK_NUMPAD0 0x60
|
||||
#endif
|
||||
|
||||
// How long the mouse button will be pressed for a tap to click gesture
|
||||
#define TAP_BUTTON_RELEASE_DELAY 100
|
||||
|
||||
// How long the fingers must be stationary to start a drag
|
||||
#define DRAG_ACTIVATION_DELAY 650
|
||||
|
||||
// How far the finger can move before it cancels a drag or tap
|
||||
#define DEAD_ZONE_DELTA 0.1f
|
||||
|
||||
const int SdlInputHandler::k_ButtonMap[] = {
|
||||
A_FLAG, B_FLAG, X_FLAG, Y_FLAG,
|
||||
BACK_FLAG, SPECIAL_FLAG, PLAY_FLAG,
|
||||
@@ -22,12 +34,28 @@ const int SdlInputHandler::k_ButtonMap[] = {
|
||||
UP_FLAG, DOWN_FLAG, LEFT_FLAG, RIGHT_FLAG
|
||||
};
|
||||
|
||||
SdlInputHandler::SdlInputHandler(bool multiController)
|
||||
: m_MultiController(multiController)
|
||||
SdlInputHandler::SdlInputHandler(StreamingPreferences& prefs, NvComputer* computer, int streamWidth, int streamHeight)
|
||||
: m_LastMouseMotionTime(0),
|
||||
m_MultiController(prefs.multiController),
|
||||
m_NeedsInputDelay(false),
|
||||
m_LeftButtonReleaseTimer(0),
|
||||
m_RightButtonReleaseTimer(0),
|
||||
m_DragTimer(0),
|
||||
m_DragButton(0),
|
||||
m_NumFingersDown(0),
|
||||
m_StreamWidth(streamWidth),
|
||||
m_StreamHeight(streamHeight)
|
||||
{
|
||||
// Allow gamepad input when the app doesn't have focus
|
||||
SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1");
|
||||
|
||||
// If mouse acceleration is enabled, use relative mode warp (which
|
||||
// is via normal motion events that are influenced by mouse acceleration).
|
||||
// Otherwise, we'll use raw input capture which is straight from the device
|
||||
// without modification by the OS.
|
||||
SDL_SetHint(SDL_HINT_MOUSE_RELATIVE_MODE_WARP,
|
||||
prefs.mouseAcceleration ? "1" : "0");
|
||||
|
||||
// We need to reinit this each time, since you only get
|
||||
// an initial set of gamepad arrival events once per init.
|
||||
SDL_assert(!SDL_WasInit(SDL_INIT_GAMECONTROLLER));
|
||||
@@ -37,6 +65,9 @@ SdlInputHandler::SdlInputHandler(bool multiController)
|
||||
SDL_GetError());
|
||||
}
|
||||
|
||||
MappingManager mappingManager;
|
||||
mappingManager.applyMappings();
|
||||
|
||||
if (!m_MultiController) {
|
||||
// Player 1 is always present in non-MC mode
|
||||
m_GamepadMask = 0x1;
|
||||
@@ -46,7 +77,22 @@ SdlInputHandler::SdlInputHandler(bool multiController)
|
||||
m_GamepadMask = 0;
|
||||
}
|
||||
|
||||
// Prior to GFE 3.14.1, sending too many mouse motion events can cause
|
||||
// GFE to choke and input latency to increase significantly. We will
|
||||
// artificially throttle them to avoid this situation.
|
||||
QVector<int> gfeVersion = NvHTTP::parseQuad(computer->gfeVersion);
|
||||
if (gfeVersion.isEmpty() || // Very old versions don't have GfeVersion at all
|
||||
gfeVersion[0] < 3 ||
|
||||
(gfeVersion[0] == 3 && gfeVersion[1] < 14) ||
|
||||
(gfeVersion[0] == 3 && gfeVersion[1] == 14 && gfeVersion[2] < 1)) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"This older version of GFE requires input delay hack");
|
||||
m_NeedsInputDelay = true;
|
||||
}
|
||||
|
||||
SDL_zero(m_GamepadState);
|
||||
SDL_zero(m_TouchDownEvent);
|
||||
SDL_zero(m_CumulativeDelta);
|
||||
}
|
||||
|
||||
SdlInputHandler::~SdlInputHandler()
|
||||
@@ -57,6 +103,10 @@ SdlInputHandler::~SdlInputHandler()
|
||||
}
|
||||
}
|
||||
|
||||
SDL_RemoveTimer(m_LeftButtonReleaseTimer);
|
||||
SDL_RemoveTimer(m_RightButtonReleaseTimer);
|
||||
SDL_RemoveTimer(m_DragTimer);
|
||||
|
||||
SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER);
|
||||
SDL_assert(!SDL_WasInit(SDL_INIT_GAMECONTROLLER));
|
||||
}
|
||||
@@ -71,28 +121,13 @@ void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
|
||||
(event->keysym.mod & KMOD_CTRL) &&
|
||||
(event->keysym.mod & KMOD_ALT) &&
|
||||
(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)
|
||||
if (event->keysym.sym == SDLK_q) {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"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
|
||||
SDL_Event event;
|
||||
event.type = SDL_QUIT;
|
||||
event.quit.timestamp = SDL_GetTicks();
|
||||
SDL_PushEvent(&event);
|
||||
@@ -102,7 +137,13 @@ void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
|
||||
else if (event->keysym.sym == SDLK_z) {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Detected mouse capture toggle combo");
|
||||
|
||||
// Stop handling future input
|
||||
SDL_SetRelativeMouseMode((SDL_bool)!SDL_GetRelativeMouseMode());
|
||||
|
||||
// Force raise all keys to ensure they aren't stuck,
|
||||
// since we won't get their key up events.
|
||||
raiseAllKeys();
|
||||
return;
|
||||
}
|
||||
// Check for the full-screen combo (Ctrl+Alt+Shift+X)
|
||||
@@ -110,6 +151,10 @@ void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Detected full-screen toggle combo");
|
||||
Session::s_ActiveSession->toggleFullscreen();
|
||||
|
||||
// Force raise all keys just be safe across this full-screen/windowed
|
||||
// transition just in case key events get lost.
|
||||
raiseAllKeys();
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -324,6 +369,9 @@ void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
|
||||
case SDL_SCANCODE_APOSTROPHE:
|
||||
keyCode = 0xDE;
|
||||
break;
|
||||
case SDL_SCANCODE_NONUSBACKSLASH:
|
||||
keyCode = 0xE2;
|
||||
break;
|
||||
default:
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Unhandled button event: %d",
|
||||
@@ -332,6 +380,14 @@ void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
|
||||
}
|
||||
}
|
||||
|
||||
// Track the key state so we always know which keys are down
|
||||
if (event->state == SDL_PRESSED) {
|
||||
m_KeysDown.insert(keyCode);
|
||||
}
|
||||
else {
|
||||
m_KeysDown.remove(keyCode);
|
||||
}
|
||||
|
||||
LiSendKeyboardEvent(keyCode,
|
||||
event->state == SDL_PRESSED ?
|
||||
KEY_ACTION_DOWN : KEY_ACTION_UP,
|
||||
@@ -342,9 +398,13 @@ void SdlInputHandler::handleMouseButtonEvent(SDL_MouseButtonEvent* event)
|
||||
{
|
||||
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 &&
|
||||
event->state == SDL_PRESSED &&
|
||||
event->state == SDL_RELEASED &&
|
||||
!SDL_GetRelativeMouseMode()) {
|
||||
SDL_SetRelativeMouseMode(SDL_TRUE);
|
||||
return;
|
||||
@@ -353,6 +413,10 @@ void SdlInputHandler::handleMouseButtonEvent(SDL_MouseButtonEvent* event)
|
||||
// Not capturing
|
||||
return;
|
||||
}
|
||||
else if (event->which == SDL_TOUCH_MOUSEID) {
|
||||
// Ignore synthetic mouse events
|
||||
return;
|
||||
}
|
||||
|
||||
switch (event->button)
|
||||
{
|
||||
@@ -366,9 +430,11 @@ void SdlInputHandler::handleMouseButtonEvent(SDL_MouseButtonEvent* event)
|
||||
button = BUTTON_RIGHT;
|
||||
break;
|
||||
case SDL_BUTTON_X1:
|
||||
button = BUTTON_X1;
|
||||
break;
|
||||
case SDL_BUTTON_X2:
|
||||
// Unsupported by GameStream
|
||||
return;
|
||||
button = BUTTON_X2;
|
||||
break;
|
||||
default:
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Unhandled button event: %d",
|
||||
@@ -388,10 +454,44 @@ void SdlInputHandler::handleMouseMotionEvent(SDL_MouseMotionEvent* event)
|
||||
// Not capturing
|
||||
return;
|
||||
}
|
||||
else if (event->which == SDL_TOUCH_MOUSEID) {
|
||||
// Ignore synthetic mouse events
|
||||
return;
|
||||
}
|
||||
|
||||
if (event->xrel != 0 || event->yrel != 0) {
|
||||
LiSendMouseMoveEvent((short)event->xrel,
|
||||
(short)event->yrel);
|
||||
short xdelta = (short)event->xrel;
|
||||
short ydelta = (short)event->yrel;
|
||||
|
||||
// If we're sending more than one motion event per millisecond,
|
||||
// delay for 1 ms to allow batching of mouse move events. On older
|
||||
// versions of GFE, we will unconditionally wait this 1 ms to
|
||||
// work around an input processing issue that causes massive mouse latency.
|
||||
Uint32 currentTime = SDL_GetTicks();
|
||||
if (m_NeedsInputDelay || !SDL_TICKS_PASSED(currentTime, m_LastMouseMotionTime + 1)) {
|
||||
SDL_Delay(1);
|
||||
currentTime = SDL_GetTicks();
|
||||
}
|
||||
m_LastMouseMotionTime = currentTime;
|
||||
|
||||
// Pump even if we didn't delay since we might get some extra events
|
||||
SDL_PumpEvents();
|
||||
|
||||
// Batch all of the pending mouse motion events
|
||||
SDL_Event nextEvent;
|
||||
while (SDL_PeepEvents(&nextEvent,
|
||||
1,
|
||||
SDL_GETEVENT,
|
||||
SDL_MOUSEMOTION,
|
||||
SDL_MOUSEMOTION) == 1) {
|
||||
// In theory, these can overflow but in practice
|
||||
// it should be highly unlikely since it would require
|
||||
// moving 64K pixels in 1 ms.
|
||||
xdelta += nextEvent.motion.xrel;
|
||||
ydelta += nextEvent.motion.yrel;
|
||||
}
|
||||
|
||||
if (xdelta != 0 || ydelta != 0) {
|
||||
LiSendMouseMoveEvent(xdelta, ydelta);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -401,6 +501,10 @@ void SdlInputHandler::handleMouseWheelEvent(SDL_MouseWheelEvent* event)
|
||||
// Not capturing
|
||||
return;
|
||||
}
|
||||
else if (event->which == SDL_TOUCH_MOUSEID) {
|
||||
// Ignore synthetic mouse events
|
||||
return;
|
||||
}
|
||||
|
||||
if (event->y != 0) {
|
||||
LiSendScrollEvent((signed char)event->y);
|
||||
@@ -438,6 +542,36 @@ void SdlInputHandler::sendGamepadState(GamepadState* state)
|
||||
state->rsY);
|
||||
}
|
||||
|
||||
Uint32 SdlInputHandler::releaseLeftButtonTimerCallback(Uint32, void*)
|
||||
{
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
|
||||
return 0;
|
||||
}
|
||||
|
||||
Uint32 SdlInputHandler::releaseRightButtonTimerCallback(Uint32, void*)
|
||||
{
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_RIGHT);
|
||||
return 0;
|
||||
}
|
||||
|
||||
Uint32 SdlInputHandler::dragTimerCallback(Uint32, void *param)
|
||||
{
|
||||
auto me = reinterpret_cast<SdlInputHandler*>(param);
|
||||
|
||||
// Check how many fingers are down now to decide
|
||||
// which button to hold down
|
||||
if (me->m_NumFingersDown == 2) {
|
||||
me->m_DragButton = BUTTON_RIGHT;
|
||||
}
|
||||
else if (me->m_NumFingersDown == 1) {
|
||||
me->m_DragButton = BUTTON_LEFT;
|
||||
}
|
||||
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, me->m_DragButton);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void SdlInputHandler::handleControllerAxisEvent(SDL_ControllerAxisEvent* event)
|
||||
{
|
||||
GamepadState* state = findStateForGamepad(event->which);
|
||||
@@ -494,6 +628,23 @@ void SdlInputHandler::handleControllerButtonEvent(SDL_ControllerButtonEvent* eve
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -583,9 +734,172 @@ void SdlInputHandler::handleControllerDeviceEvent(SDL_ControllerDeviceEvent* eve
|
||||
}
|
||||
}
|
||||
|
||||
void SdlInputHandler::handleJoystickArrivalEvent(SDL_JoyDeviceEvent* event)
|
||||
{
|
||||
SDL_assert(event->type == SDL_JOYDEVICEADDED);
|
||||
|
||||
if (!SDL_IsGameController(event->which)) {
|
||||
char guidStr[33];
|
||||
SDL_JoystickGetGUIDString(SDL_JoystickGetDeviceGUID(event->which),
|
||||
guidStr, sizeof(guidStr));
|
||||
const char* name = SDL_JoystickNameForIndex(event->which);
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Joystick discovered with no mapping: %s %s",
|
||||
name ? name : "<UNKNOWN>",
|
||||
guidStr);
|
||||
SDL_Joystick* joy = SDL_JoystickOpen(event->which);
|
||||
if (joy != nullptr) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Number of axes: %d | Number of buttons: %d | Number of hats: %d",
|
||||
SDL_JoystickNumAxes(joy), SDL_JoystickNumButtons(joy),
|
||||
SDL_JoystickNumHats(joy));
|
||||
SDL_JoystickClose(joy);
|
||||
}
|
||||
else {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Unable to open joystick for query: %s",
|
||||
SDL_GetError());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SdlInputHandler::handleTouchFingerEvent(SDL_TouchFingerEvent* event)
|
||||
{
|
||||
int fingerIndex = -1;
|
||||
|
||||
// Observations on Windows 10: x and y appear to be relative to 0,0 of the window client area.
|
||||
// Although SDL documentation states they are 0.0 - 1.0 float values, they can actually be higher
|
||||
// or lower than those values as touch events continue for touches started within the client area that
|
||||
// leave the client area during a drag motion.
|
||||
// dx and dy are deltas from the last touch event, not the first touch down.
|
||||
|
||||
// Determine the index of this finger using our list
|
||||
// of fingers that are currently active on screen.
|
||||
// This is also required to handle finger up which
|
||||
// where the finger will not be in SDL_GetTouchFinger()
|
||||
// anymore.
|
||||
if (event->type != SDL_FINGERDOWN) {
|
||||
for (int i = 0; i < MAX_FINGERS; i++) {
|
||||
if (event->fingerId == m_TouchDownEvent[i].fingerId) {
|
||||
fingerIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Resolve the new finger by determining the ID of each
|
||||
// finger on the display.
|
||||
int numTouchFingers = SDL_GetNumTouchFingers(event->touchId);
|
||||
for (int i = 0; i < numTouchFingers; i++) {
|
||||
SDL_Finger* finger = SDL_GetTouchFinger(event->touchId, i);
|
||||
SDL_assert(finger != nullptr);
|
||||
if (finger != nullptr) {
|
||||
if (finger->id == event->fingerId) {
|
||||
fingerIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (fingerIndex < 0 || fingerIndex >= MAX_FINGERS) {
|
||||
// Too many fingers
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle cursor motion based on the position of the
|
||||
// primary finger on screen
|
||||
if (fingerIndex == 0) {
|
||||
// The event x and y values are relative to our window width
|
||||
// and height. However, we want to scale them to be relative
|
||||
// to the host resolution. Fortunately this is easy since we
|
||||
// already have normalized values. We'll just multiply them
|
||||
// by the stream dimensions to get real X and Y values rather
|
||||
// than the client window dimensions.
|
||||
short deltaX = static_cast<short>(event->dx * m_StreamWidth);
|
||||
short deltaY = static_cast<short>(event->dy * m_StreamHeight);
|
||||
if (deltaX != 0 || deltaY != 0) {
|
||||
LiSendMouseMoveEvent(deltaX, deltaY);
|
||||
}
|
||||
}
|
||||
|
||||
// Start a drag timer when primary or secondary
|
||||
// fingers go down
|
||||
if (event->type == SDL_FINGERDOWN &&
|
||||
(fingerIndex == 0 || fingerIndex == 1)) {
|
||||
SDL_RemoveTimer(m_DragTimer);
|
||||
m_DragTimer = SDL_AddTimer(DRAG_ACTIVATION_DELAY,
|
||||
dragTimerCallback,
|
||||
this);
|
||||
}
|
||||
|
||||
if (event->type == SDL_FINGERMOTION) {
|
||||
// Count the total cumulative dx/dy that the finger
|
||||
// has moved.
|
||||
m_CumulativeDelta[fingerIndex] += qAbs(event->x);
|
||||
m_CumulativeDelta[fingerIndex] += qAbs(event->y);
|
||||
|
||||
// If it's outside the deadzone delta, cancel drags and taps
|
||||
if (m_CumulativeDelta[fingerIndex] > DEAD_ZONE_DELTA) {
|
||||
SDL_RemoveTimer(m_DragTimer);
|
||||
m_DragTimer = 0;
|
||||
|
||||
// This effectively cancels the tap logic below
|
||||
m_TouchDownEvent[fingerIndex].timestamp = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (event->type == SDL_FINGERUP) {
|
||||
// Cancel the drag timer on finger up
|
||||
SDL_RemoveTimer(m_DragTimer);
|
||||
m_DragTimer = 0;
|
||||
|
||||
// Release any drag
|
||||
if (m_DragButton != 0) {
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, m_DragButton);
|
||||
m_DragButton = 0;
|
||||
}
|
||||
// 2 finger tap
|
||||
else if (event->timestamp - m_TouchDownEvent[1].timestamp < 250) {
|
||||
// Zero timestamp of the primary finger to ensure we won't
|
||||
// generate a left click if the primary finger comes up soon.
|
||||
m_TouchDownEvent[0].timestamp = 0;
|
||||
|
||||
// Press down the right mouse button
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_RIGHT);
|
||||
|
||||
// Queue a timer to release it in 100 ms
|
||||
SDL_RemoveTimer(m_RightButtonReleaseTimer);
|
||||
m_RightButtonReleaseTimer = SDL_AddTimer(TAP_BUTTON_RELEASE_DELAY,
|
||||
releaseRightButtonTimerCallback,
|
||||
nullptr);
|
||||
}
|
||||
// 1 finger tap
|
||||
else if (event->timestamp - m_TouchDownEvent[0].timestamp < 250) {
|
||||
// Press down the left mouse button
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT);
|
||||
|
||||
// Queue a timer to release it in 100 ms
|
||||
SDL_RemoveTimer(m_LeftButtonReleaseTimer);
|
||||
m_LeftButtonReleaseTimer = SDL_AddTimer(TAP_BUTTON_RELEASE_DELAY,
|
||||
releaseLeftButtonTimerCallback,
|
||||
nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
m_NumFingersDown = SDL_GetNumTouchFingers(event->touchId);
|
||||
|
||||
if (event->type == SDL_FINGERDOWN) {
|
||||
m_TouchDownEvent[fingerIndex] = *event;
|
||||
m_CumulativeDelta[fingerIndex] = 0;
|
||||
}
|
||||
else if (event->type == SDL_FINGERUP) {
|
||||
m_TouchDownEvent[fingerIndex] = {};
|
||||
}
|
||||
}
|
||||
|
||||
int SdlInputHandler::getAttachedGamepadMask()
|
||||
{
|
||||
int i;
|
||||
int count;
|
||||
int mask;
|
||||
|
||||
@@ -595,7 +909,7 @@ int SdlInputHandler::getAttachedGamepadMask()
|
||||
}
|
||||
|
||||
count = mask = 0;
|
||||
for (i = 0; i < SDL_NumJoysticks(); i++) {
|
||||
for (int i = 0; i < SDL_NumJoysticks(); i++) {
|
||||
if (SDL_IsGameController(i)) {
|
||||
mask |= (1 << count++);
|
||||
}
|
||||
@@ -603,3 +917,78 @@ int SdlInputHandler::getAttachedGamepadMask()
|
||||
|
||||
return mask;
|
||||
}
|
||||
|
||||
void SdlInputHandler::raiseAllKeys()
|
||||
{
|
||||
if (m_KeysDown.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Raising %d keys",
|
||||
m_KeysDown.count());
|
||||
|
||||
for (auto keyDown : m_KeysDown) {
|
||||
LiSendKeyboardEvent(keyDown, KEY_ACTION_UP, 0);
|
||||
}
|
||||
|
||||
m_KeysDown.clear();
|
||||
}
|
||||
|
||||
QString SdlInputHandler::getUnmappedGamepads()
|
||||
{
|
||||
QString ret;
|
||||
|
||||
if (SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) != 0) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER) failed: %s",
|
||||
SDL_GetError());
|
||||
}
|
||||
|
||||
MappingManager mappingManager;
|
||||
mappingManager.applyMappings();
|
||||
|
||||
for (int i = 0; i < SDL_NumJoysticks(); i++) {
|
||||
if (!SDL_IsGameController(i)) {
|
||||
char guidStr[33];
|
||||
SDL_JoystickGetGUIDString(SDL_JoystickGetDeviceGUID(i),
|
||||
guidStr, sizeof(guidStr));
|
||||
const char* name = SDL_JoystickNameForIndex(i);
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Unmapped joystick: %s %s",
|
||||
name ? name : "<UNKNOWN>",
|
||||
guidStr);
|
||||
SDL_Joystick* joy = SDL_JoystickOpen(i);
|
||||
if (joy != nullptr) {
|
||||
int numButtons = SDL_JoystickNumButtons(joy);
|
||||
int numHats = SDL_JoystickNumHats(joy);
|
||||
int numAxes = SDL_JoystickNumAxes(joy);
|
||||
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Number of axes: %d | Number of buttons: %d | Number of hats: %d",
|
||||
numAxes, numButtons, numHats);
|
||||
|
||||
if (numAxes >= 4 && numButtons >= 8 && numHats <= 1) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Joystick likely to be an unmapped game controller");
|
||||
if (!ret.isEmpty()) {
|
||||
ret += ", ";
|
||||
}
|
||||
|
||||
ret += name;
|
||||
}
|
||||
|
||||
SDL_JoystickClose(joy);
|
||||
}
|
||||
else {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Unable to open joystick for query: %s",
|
||||
SDL_GetError());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
#pragma once
|
||||
|
||||
#include "settings/streamingpreferences.h"
|
||||
#include "backend/computermanager.h"
|
||||
|
||||
#include <SDL.h>
|
||||
|
||||
struct GamepadState {
|
||||
SDL_GameController* controller;
|
||||
SDL_JoystickID jsId;
|
||||
short index;
|
||||
|
||||
short buttons;
|
||||
short lsX, lsY;
|
||||
short rsX, rsY;
|
||||
unsigned char lt, rt;
|
||||
};
|
||||
|
||||
#define MAX_GAMEPADS 4
|
||||
#define MAX_FINGERS 2
|
||||
|
||||
class SdlInputHandler
|
||||
{
|
||||
public:
|
||||
explicit SdlInputHandler(StreamingPreferences& prefs, NvComputer* computer,
|
||||
int streamWidth, int streamHeight);
|
||||
|
||||
~SdlInputHandler();
|
||||
|
||||
void handleKeyEvent(SDL_KeyboardEvent* event);
|
||||
|
||||
void handleMouseButtonEvent(SDL_MouseButtonEvent* event);
|
||||
|
||||
void handleMouseMotionEvent(SDL_MouseMotionEvent* event);
|
||||
|
||||
void handleMouseWheelEvent(SDL_MouseWheelEvent* event);
|
||||
|
||||
void handleControllerAxisEvent(SDL_ControllerAxisEvent* event);
|
||||
|
||||
void handleControllerButtonEvent(SDL_ControllerButtonEvent* event);
|
||||
|
||||
void handleControllerDeviceEvent(SDL_ControllerDeviceEvent* event);
|
||||
|
||||
void handleJoystickArrivalEvent(SDL_JoyDeviceEvent* event);
|
||||
|
||||
void handleTouchFingerEvent(SDL_TouchFingerEvent* event);
|
||||
|
||||
int getAttachedGamepadMask();
|
||||
|
||||
void raiseAllKeys();
|
||||
|
||||
static
|
||||
QString getUnmappedGamepads();
|
||||
|
||||
private:
|
||||
GamepadState*
|
||||
findStateForGamepad(SDL_JoystickID id);
|
||||
|
||||
void sendGamepadState(GamepadState* state);
|
||||
|
||||
static
|
||||
Uint32 releaseLeftButtonTimerCallback(Uint32 interval, void* param);
|
||||
|
||||
static
|
||||
Uint32 releaseRightButtonTimerCallback(Uint32 interval, void* param);
|
||||
|
||||
static
|
||||
Uint32 dragTimerCallback(Uint32 interval, void* param);
|
||||
|
||||
Uint32 m_LastMouseMotionTime;
|
||||
bool m_MultiController;
|
||||
bool m_NeedsInputDelay;
|
||||
int m_GamepadMask;
|
||||
GamepadState m_GamepadState[MAX_GAMEPADS];
|
||||
QSet<short> m_KeysDown;
|
||||
|
||||
SDL_TouchFingerEvent m_TouchDownEvent[MAX_FINGERS];
|
||||
float m_CumulativeDelta[MAX_FINGERS];
|
||||
SDL_TimerID m_LeftButtonReleaseTimer;
|
||||
SDL_TimerID m_RightButtonReleaseTimer;
|
||||
SDL_TimerID m_DragTimer;
|
||||
char m_DragButton;
|
||||
int m_NumFingersDown;
|
||||
int m_StreamWidth;
|
||||
int m_StreamHeight;
|
||||
|
||||
static const int k_ButtonMap[];
|
||||
};
|
||||
@@ -1,52 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL.h>
|
||||
|
||||
struct GamepadState {
|
||||
SDL_GameController* controller;
|
||||
SDL_JoystickID jsId;
|
||||
short index;
|
||||
|
||||
short buttons;
|
||||
short lsX, lsY;
|
||||
short rsX, rsY;
|
||||
unsigned char lt, rt;
|
||||
};
|
||||
|
||||
#define MAX_GAMEPADS 4
|
||||
|
||||
class SdlInputHandler
|
||||
{
|
||||
public:
|
||||
explicit SdlInputHandler(bool multiController);
|
||||
|
||||
~SdlInputHandler();
|
||||
|
||||
void handleKeyEvent(SDL_KeyboardEvent* event);
|
||||
|
||||
void handleMouseButtonEvent(SDL_MouseButtonEvent* event);
|
||||
|
||||
void handleMouseMotionEvent(SDL_MouseMotionEvent* event);
|
||||
|
||||
void handleMouseWheelEvent(SDL_MouseWheelEvent* event);
|
||||
|
||||
void handleControllerAxisEvent(SDL_ControllerAxisEvent* event);
|
||||
|
||||
void handleControllerButtonEvent(SDL_ControllerButtonEvent* event);
|
||||
|
||||
void handleControllerDeviceEvent(SDL_ControllerDeviceEvent* event);
|
||||
|
||||
int getAttachedGamepadMask();
|
||||
|
||||
private:
|
||||
GamepadState*
|
||||
findStateForGamepad(SDL_JoystickID id);
|
||||
|
||||
void sendGamepadState(GamepadState* state);
|
||||
|
||||
bool m_MultiController;
|
||||
int m_GamepadMask;
|
||||
GamepadState m_GamepadState[MAX_GAMEPADS];
|
||||
|
||||
static const int k_ButtonMap[];
|
||||
};
|
||||
+503
-207
@@ -1,10 +1,17 @@
|
||||
#include "session.hpp"
|
||||
#include "session.h"
|
||||
#include "settings/streamingpreferences.h"
|
||||
#include "streaming/streamutils.h"
|
||||
|
||||
#include <Limelight.h>
|
||||
#include <SDL.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
|
||||
#include "video/ffmpeg.h"
|
||||
#endif
|
||||
@@ -13,15 +20,6 @@
|
||||
#include "video/sl.h"
|
||||
#endif
|
||||
|
||||
#if defined(Q_OS_WIN32) || defined(Q_OS_DARWIN)
|
||||
// Using full-screen desktop allows us to avoid needing to enable V-sync on Windows
|
||||
// and it also avoids some strange flickering issues on my Win7 test machine
|
||||
// with Intel HD 5500 graphics. It also allows Spaces to work on macOS.
|
||||
#define SDL_OS_FULLSCREEN_FLAG SDL_WINDOW_FULLSCREEN_DESKTOP
|
||||
#else
|
||||
#define SDL_OS_FULLSCREEN_FLAG SDL_WINDOW_FULLSCREEN
|
||||
#endif
|
||||
|
||||
#ifdef Q_OS_WIN32
|
||||
// Scaling the icon down on Win32 looks dreadful, so render at lower res
|
||||
#define ICON_SIZE 32
|
||||
@@ -50,11 +48,11 @@ CONNECTION_LISTENER_CALLBACKS Session::k_ConnCallbacks = {
|
||||
};
|
||||
|
||||
AUDIO_RENDERER_CALLBACKS Session::k_AudioCallbacks = {
|
||||
Session::sdlAudioInit,
|
||||
Session::sdlAudioStart,
|
||||
Session::sdlAudioStop,
|
||||
Session::sdlAudioCleanup,
|
||||
Session::sdlAudioDecodeAndPlaySample,
|
||||
Session::arInit,
|
||||
nullptr,
|
||||
nullptr,
|
||||
Session::arCleanup,
|
||||
Session::arDecodeAndPlaySample,
|
||||
CAPABILITY_DIRECT_SUBMIT
|
||||
};
|
||||
|
||||
@@ -81,6 +79,8 @@ void Session::clStageFailed(int stage, long errorCode)
|
||||
|
||||
void Session::clConnectionTerminated(long errorCode)
|
||||
{
|
||||
emit s_ActiveSession->displayLaunchError("Connection terminated");
|
||||
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Connection terminated: %ld",
|
||||
errorCode);
|
||||
@@ -104,13 +104,15 @@ void Session::clLogMessage(const char* format, ...)
|
||||
va_end(ap);
|
||||
}
|
||||
|
||||
#define CALL_INITIALIZE(dec) (dec)->initialize(vds, window, videoFormat, width, height, frameRate, enableVsync)
|
||||
|
||||
bool Session::chooseDecoder(StreamingPreferences::VideoDecoderSelection vds,
|
||||
SDL_Window* window, int videoFormat, int width, int height,
|
||||
int frameRate, IVideoDecoder*& chosenDecoder)
|
||||
int frameRate, bool enableVsync, IVideoDecoder*& chosenDecoder)
|
||||
{
|
||||
#ifdef HAVE_SLVIDEO
|
||||
chosenDecoder = new SLVideoDecoder();
|
||||
if (chosenDecoder->initialize(vds, window, videoFormat, width, height, frameRate)) {
|
||||
if (CALL_INITIALIZE(chosenDecoder)) {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"SLVideo video decoder chosen");
|
||||
return true;
|
||||
@@ -125,7 +127,7 @@ bool Session::chooseDecoder(StreamingPreferences::VideoDecoderSelection vds,
|
||||
|
||||
#ifdef HAVE_FFMPEG
|
||||
chosenDecoder = new FFmpegVideoDecoder();
|
||||
if (chosenDecoder->initialize(vds, window, videoFormat, width, height, frameRate)) {
|
||||
if (CALL_INITIALIZE(chosenDecoder)) {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"FFmpeg-based video decoder chosen");
|
||||
return true;
|
||||
@@ -168,25 +170,33 @@ int Session::drSubmitDecodeUnit(PDECODE_UNIT du)
|
||||
// Use a lock since we'll be yanking this decoder out
|
||||
// from underneath the session when we initiate destruction.
|
||||
// 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) {
|
||||
// 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;
|
||||
}
|
||||
|
||||
IVideoDecoder* decoder = s_ActiveSession->m_VideoDecoder;
|
||||
if (decoder != nullptr) {
|
||||
int ret = decoder->submitDecodeUnit(du);
|
||||
SDL_AtomicUnlock(&s_ActiveSession->m_DecoderLock);
|
||||
return ret;
|
||||
IVideoDecoder* decoder = s_ActiveSession->m_VideoDecoder;
|
||||
if (decoder != nullptr) {
|
||||
int ret = decoder->submitDecodeUnit(du);
|
||||
SDL_AtomicUnlock(&s_ActiveSession->m_DecoderLock);
|
||||
return ret;
|
||||
}
|
||||
else {
|
||||
SDL_AtomicUnlock(&s_ActiveSession->m_DecoderLock);
|
||||
return DR_OK;
|
||||
}
|
||||
}
|
||||
else {
|
||||
SDL_AtomicUnlock(&s_ActiveSession->m_DecoderLock);
|
||||
// Decoder is going away. Ignore anything coming in until
|
||||
// the lock is released.
|
||||
return DR_OK;
|
||||
}
|
||||
}
|
||||
@@ -212,7 +222,7 @@ bool Session::isHardwareDecodeAvailable(StreamingPreferences::VideoDecoderSelect
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!chooseDecoder(vds, window, videoFormat, width, height, frameRate, decoder)) {
|
||||
if (!chooseDecoder(vds, window, videoFormat, width, height, frameRate, true, decoder)) {
|
||||
SDL_DestroyWindow(window);
|
||||
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||
return false;
|
||||
@@ -228,23 +238,70 @@ bool Session::isHardwareDecodeAvailable(StreamingPreferences::VideoDecoderSelect
|
||||
return ret;
|
||||
}
|
||||
|
||||
Session::Session(NvComputer* computer, NvApp& app)
|
||||
: m_Computer(computer),
|
||||
int Session::getDecoderCapabilities(StreamingPreferences::VideoDecoderSelection vds,
|
||||
int videoFormat, int width, int height, int frameRate)
|
||||
{
|
||||
IVideoDecoder* decoder;
|
||||
|
||||
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s",
|
||||
SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
|
||||
SDL_Window* window = SDL_CreateWindow("", 0, 0, width, height, SDL_WINDOW_HIDDEN);
|
||||
if (!window) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Failed to create window for hardware decode test: %s",
|
||||
SDL_GetError());
|
||||
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!chooseDecoder(vds, window, videoFormat, width, height, frameRate, true, decoder)) {
|
||||
SDL_DestroyWindow(window);
|
||||
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||
return false;
|
||||
}
|
||||
|
||||
SDL_DestroyWindow(window);
|
||||
|
||||
int caps = decoder->getDecoderCapabilities();
|
||||
|
||||
delete decoder;
|
||||
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||
|
||||
return caps;
|
||||
}
|
||||
|
||||
Session::Session(NvComputer* computer, NvApp& app, StreamingPreferences *preferences)
|
||||
: m_Preferences(preferences ? preferences : new StreamingPreferences(this)),
|
||||
m_Computer(computer),
|
||||
m_App(app),
|
||||
m_Window(nullptr),
|
||||
m_VideoDecoder(nullptr),
|
||||
m_DecoderLock(0),
|
||||
m_NeedsIdr(false)
|
||||
m_NeedsIdr(false),
|
||||
m_AudioDisabled(false),
|
||||
m_DisplayOriginX(0),
|
||||
m_DisplayOriginY(0),
|
||||
m_PendingWindowedTransition(false),
|
||||
m_OpusDecoder(nullptr),
|
||||
m_AudioRenderer(nullptr),
|
||||
m_AudioSampleCount(0)
|
||||
{
|
||||
qDebug() << "Server GPU:" << m_Computer->gpuModel;
|
||||
}
|
||||
|
||||
void Session::initialize()
|
||||
{
|
||||
qInfo() << "Server GPU:" << m_Computer->gpuModel;
|
||||
qInfo() << "Server GFE version:" << m_Computer->gfeVersion;
|
||||
|
||||
LiInitializeVideoCallbacks(&m_VideoCallbacks);
|
||||
m_VideoCallbacks.setup = drSetup;
|
||||
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
|
||||
int slices = qMin(MAX_SLICES, SDL_GetCPUCount());
|
||||
m_VideoCallbacks.capabilities |= CAPABILITY_SLICES_PER_FRAME(slices);
|
||||
@@ -253,10 +310,10 @@ Session::Session(NvComputer* computer, NvApp& app)
|
||||
slices);
|
||||
|
||||
LiInitializeStreamConfiguration(&m_StreamConfig);
|
||||
m_StreamConfig.width = m_Preferences.width;
|
||||
m_StreamConfig.height = m_Preferences.height;
|
||||
m_StreamConfig.fps = m_Preferences.fps;
|
||||
m_StreamConfig.bitrate = m_Preferences.bitrateKbps;
|
||||
m_StreamConfig.width = m_Preferences->width;
|
||||
m_StreamConfig.height = m_Preferences->height;
|
||||
m_StreamConfig.fps = m_Preferences->fps;
|
||||
m_StreamConfig.bitrate = m_Preferences->bitrateKbps;
|
||||
m_StreamConfig.hevcBitratePercentageMultiplier = 75;
|
||||
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
@@ -269,16 +326,12 @@ Session::Session(NvComputer* computer, NvApp& app)
|
||||
// Only the first 4 bytes are populated in the RI key IV
|
||||
RAND_bytes(reinterpret_cast<unsigned char*>(m_StreamConfig.remoteInputAesIv), 4);
|
||||
|
||||
switch (m_Preferences.audioConfig)
|
||||
switch (m_Preferences->audioConfig)
|
||||
{
|
||||
case StreamingPreferences::AC_AUTO:
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Autodetecting audio configuration");
|
||||
m_StreamConfig.audioConfiguration = sdlDetermineAudioConfiguration();
|
||||
break;
|
||||
case StreamingPreferences::AC_FORCE_STEREO:
|
||||
case StreamingPreferences::AC_STEREO:
|
||||
m_StreamConfig.audioConfiguration = AUDIO_CONFIGURATION_STEREO;
|
||||
break;
|
||||
case StreamingPreferences::AC_FORCE_SURROUND:
|
||||
case StreamingPreferences::AC_51_SURROUND:
|
||||
m_StreamConfig.audioConfiguration = AUDIO_CONFIGURATION_51_SURROUND;
|
||||
break;
|
||||
}
|
||||
@@ -287,16 +340,32 @@ Session::Session(NvComputer* computer, NvApp& app)
|
||||
"Audio configuration: %d",
|
||||
m_StreamConfig.audioConfiguration);
|
||||
|
||||
switch (m_Preferences.videoCodecConfig)
|
||||
switch (m_Preferences->videoCodecConfig)
|
||||
{
|
||||
case StreamingPreferences::VCC_AUTO:
|
||||
// TODO: Determine if HEVC is better depending on the decoder
|
||||
m_StreamConfig.supportsHevc =
|
||||
isHardwareDecodeAvailable(m_Preferences.videoDecoderSelection,
|
||||
isHardwareDecodeAvailable(m_Preferences->videoDecoderSelection,
|
||||
VIDEO_FORMAT_H265,
|
||||
m_StreamConfig.width,
|
||||
m_StreamConfig.height,
|
||||
m_StreamConfig.fps);
|
||||
#ifdef Q_OS_DARWIN
|
||||
{
|
||||
// Prior to GFE 3.11, GFE did not allow us to constrain
|
||||
// the number of reference frames, so we have to fixup the SPS
|
||||
// to allow decoding via VideoToolbox on macOS. Since we don't
|
||||
// have fixup code for HEVC, just avoid it if GFE is too old.
|
||||
QVector<int> gfeVersion = NvHTTP::parseQuad(m_Computer->gfeVersion);
|
||||
if (gfeVersion.isEmpty() || // Very old versions don't have GfeVersion at all
|
||||
gfeVersion[0] < 3 ||
|
||||
(gfeVersion[0] == 3 && gfeVersion[1] < 11)) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Disabling HEVC on macOS due to old GFE version");
|
||||
m_StreamConfig.supportsHevc = false;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
m_StreamConfig.enableHdr = false;
|
||||
break;
|
||||
case StreamingPreferences::VCC_FORCE_H264:
|
||||
@@ -313,19 +382,30 @@ Session::Session(NvComputer* computer, NvApp& app)
|
||||
break;
|
||||
}
|
||||
|
||||
if (computer->activeAddress == computer->remoteAddress) {
|
||||
if (m_Computer->activeAddress == m_Computer->remoteAddress) {
|
||||
m_StreamConfig.streamingRemotely = 1;
|
||||
}
|
||||
else {
|
||||
m_StreamConfig.streamingRemotely = 0;
|
||||
}
|
||||
|
||||
if (computer->activeAddress == computer->localAddress) {
|
||||
if (m_Computer->activeAddress == m_Computer->localAddress) {
|
||||
m_StreamConfig.packetSize = 1392;
|
||||
}
|
||||
else {
|
||||
m_StreamConfig.packetSize = 1024;
|
||||
}
|
||||
|
||||
switch (m_Preferences->windowMode)
|
||||
{
|
||||
case StreamingPreferences::WM_FULLSCREEN_DESKTOP:
|
||||
m_FullScreenFlag = SDL_WINDOW_FULLSCREEN_DESKTOP;
|
||||
break;
|
||||
case StreamingPreferences::WM_FULLSCREEN:
|
||||
default:
|
||||
m_FullScreenFlag = SDL_WINDOW_FULLSCREEN;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void Session::emitLaunchWarning(QString text)
|
||||
@@ -348,20 +428,28 @@ bool Session::validateLaunch()
|
||||
{
|
||||
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.");
|
||||
}
|
||||
|
||||
if (m_StreamConfig.supportsHevc) {
|
||||
bool hevcForced = m_Preferences.videoCodecConfig == StreamingPreferences::VCC_FORCE_HEVC ||
|
||||
m_Preferences.videoCodecConfig == StreamingPreferences::VCC_FORCE_HEVC_HDR;
|
||||
if (m_Preferences->unsupportedFps && m_StreamConfig.fps > 60) {
|
||||
emitLaunchWarning("Using unsupported FPS options may cause stuttering or lag.");
|
||||
|
||||
if (!isHardwareDecodeAvailable(m_Preferences.videoDecoderSelection,
|
||||
if (m_Preferences->enableVsync) {
|
||||
emitLaunchWarning("V-sync will be disabled when streaming at a higher frame rate than the display.");
|
||||
}
|
||||
}
|
||||
|
||||
if (m_StreamConfig.supportsHevc) {
|
||||
bool hevcForced = m_Preferences->videoCodecConfig == StreamingPreferences::VCC_FORCE_HEVC ||
|
||||
m_Preferences->videoCodecConfig == StreamingPreferences::VCC_FORCE_HEVC_HDR;
|
||||
|
||||
if (!isHardwareDecodeAvailable(m_Preferences->videoDecoderSelection,
|
||||
VIDEO_FORMAT_H265,
|
||||
m_StreamConfig.width,
|
||||
m_StreamConfig.height,
|
||||
m_StreamConfig.fps) &&
|
||||
m_Preferences.videoDecoderSelection == StreamingPreferences::VDS_AUTO) {
|
||||
m_Preferences->videoDecoderSelection == StreamingPreferences::VDS_AUTO) {
|
||||
if (hevcForced) {
|
||||
emitLaunchWarning("Using software decoding due to your selection to force HEVC without GPU support. This may cause poor streaming performance.");
|
||||
}
|
||||
@@ -397,7 +485,7 @@ bool Session::validateLaunch()
|
||||
emitLaunchWarning("Your host PC GPU doesn't support 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,
|
||||
m_StreamConfig.width,
|
||||
m_StreamConfig.height,
|
||||
@@ -414,7 +502,7 @@ bool Session::validateLaunch()
|
||||
|
||||
if (m_StreamConfig.width >= 3840) {
|
||||
// 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.");
|
||||
|
||||
m_StreamConfig.width = 1920;
|
||||
@@ -431,13 +519,36 @@ bool Session::validateLaunch()
|
||||
}
|
||||
}
|
||||
|
||||
if (m_Preferences.videoDecoderSelection == StreamingPreferences::VDS_FORCE_HARDWARE &&
|
||||
!isHardwareDecodeAvailable(m_Preferences.videoDecoderSelection,
|
||||
// Test if audio works at the specified audio configuration
|
||||
bool audioTestPassed = testAudio(m_StreamConfig.audioConfiguration);
|
||||
|
||||
// Gracefully degrade to stereo if 5.1 doesn't work
|
||||
if (!audioTestPassed && m_StreamConfig.audioConfiguration == AUDIO_CONFIGURATION_51_SURROUND) {
|
||||
audioTestPassed = testAudio(AUDIO_CONFIGURATION_STEREO);
|
||||
if (audioTestPassed) {
|
||||
m_StreamConfig.audioConfiguration = AUDIO_CONFIGURATION_STEREO;
|
||||
emitLaunchWarning("5.1 surround sound is not supported by the current audio device.");
|
||||
}
|
||||
}
|
||||
|
||||
// If nothing worked, warn the user that audio will not work
|
||||
m_AudioDisabled = !audioTestPassed;
|
||||
if (m_AudioDisabled) {
|
||||
emitLaunchWarning("Failed to open audio device. Audio will be unavailable during this session.");
|
||||
}
|
||||
|
||||
// Check for unmapped gamepads
|
||||
if (!SdlInputHandler::getUnmappedGamepads().isEmpty()) {
|
||||
emitLaunchWarning("An attached gamepad has no mapping and won't be usable. Visit the Moonlight help to resolve this.");
|
||||
}
|
||||
|
||||
if (m_Preferences->videoDecoderSelection == StreamingPreferences::VDS_FORCE_HARDWARE &&
|
||||
!isHardwareDecodeAvailable(m_Preferences->videoDecoderSelection,
|
||||
m_StreamConfig.supportsHevc ? VIDEO_FORMAT_H265 : VIDEO_FORMAT_H264,
|
||||
m_StreamConfig.width,
|
||||
m_StreamConfig.height,
|
||||
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.");
|
||||
}
|
||||
else {
|
||||
@@ -448,6 +559,13 @@ bool Session::validateLaunch()
|
||||
return false;
|
||||
}
|
||||
|
||||
// Add the capability flags from the chosen decoder/renderer
|
||||
m_VideoCallbacks.capabilities |= getDecoderCapabilities(m_Preferences->videoDecoderSelection,
|
||||
m_StreamConfig.supportsHevc ? VIDEO_FORMAT_H265 : VIDEO_FORMAT_H264,
|
||||
m_StreamConfig.width,
|
||||
m_StreamConfig.height,
|
||||
m_StreamConfig.fps);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -465,140 +583,180 @@ class DeferredSessionCleanupTask : public QRunnable
|
||||
}
|
||||
};
|
||||
|
||||
void Session::getWindowDimensions(bool fullScreen,
|
||||
int& x, int& y,
|
||||
void Session::getWindowDimensions(int& x, int& y,
|
||||
int& width, int& height)
|
||||
{
|
||||
int displayIndex = 0;
|
||||
|
||||
// If there's a display matching this exact resolution, pick that
|
||||
// one (for native full-screen streaming). Otherwise, assume
|
||||
// display 0 for now. TODO: Default to the screen that the Qt window is on
|
||||
if (fullScreen) {
|
||||
for (int i = 0; i < SDL_GetNumVideoDisplays(); i++) {
|
||||
SDL_DisplayMode mode;
|
||||
if (SDL_GetCurrentDisplayMode(i, &mode) == 0 &&
|
||||
m_ActiveVideoWidth == mode.w &&
|
||||
m_ActiveVideoHeight == mode.h) {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Found exact resolution match on display: %d",
|
||||
i);
|
||||
displayIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (m_Window != nullptr) {
|
||||
displayIndex = SDL_GetWindowDisplayIndex(m_Window);
|
||||
if (displayIndex < 0) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"SDL_GetWindowDisplayIndex() failed: %s",
|
||||
SDL_GetError());
|
||||
|
||||
// Assume display 0
|
||||
displayIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (fullScreen) {
|
||||
SDL_DisplayMode currentMode;
|
||||
|
||||
if (SDL_GetCurrentDisplayMode(displayIndex, ¤tMode) == 0) {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Using current display mode: %dx%dx%d",
|
||||
currentMode.w, currentMode.h, currentMode.refresh_rate);
|
||||
width = currentMode.w;
|
||||
height = currentMode.h;
|
||||
}
|
||||
else {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Unable to get current display mode: %s",
|
||||
SDL_GetError());
|
||||
width = m_StreamConfig.width;
|
||||
height = m_StreamConfig.height;
|
||||
}
|
||||
|
||||
// Full-screen modes always start at the origin
|
||||
x = y = SDL_WINDOWPOS_UNDEFINED_DISPLAY(displayIndex);
|
||||
SDL_assert(displayIndex >= 0);
|
||||
}
|
||||
// Create our window on the same display that Qt's UI
|
||||
// was being displayed on.
|
||||
else {
|
||||
SDL_Rect usableBounds;
|
||||
if (SDL_GetDisplayUsableBounds(displayIndex, &usableBounds) == 0) {
|
||||
width = usableBounds.w;
|
||||
height = usableBounds.h;
|
||||
x = usableBounds.x;
|
||||
y = usableBounds.y;
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Qt UI screen is at (%d,%d)",
|
||||
m_DisplayOriginX, m_DisplayOriginY);
|
||||
for (int i = 0; i < SDL_GetNumVideoDisplays(); i++) {
|
||||
SDL_Rect displayBounds;
|
||||
|
||||
if (m_Window != nullptr) {
|
||||
int top, left, bottom, right;
|
||||
|
||||
if (SDL_GetWindowBordersSize(m_Window, &top, &left, &bottom, &right) < 0) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Unable to get window border size");
|
||||
return;
|
||||
if (SDL_GetDisplayBounds(i, &displayBounds) == 0) {
|
||||
if (displayBounds.x == m_DisplayOriginX &&
|
||||
displayBounds.y == m_DisplayOriginY) {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"SDL found matching display %d",
|
||||
i);
|
||||
displayIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
else {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"SDL_GetDisplayBounds(%d) failed: %s",
|
||||
i, SDL_GetError());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
x += left;
|
||||
y += top;
|
||||
SDL_Rect usableBounds;
|
||||
if (SDL_GetDisplayUsableBounds(displayIndex, &usableBounds) == 0) {
|
||||
width = usableBounds.w;
|
||||
height = usableBounds.h;
|
||||
|
||||
if (m_Window != nullptr) {
|
||||
int top, left, bottom, right;
|
||||
|
||||
if (SDL_GetWindowBordersSize(m_Window, &top, &left, &bottom, &right) == 0) {
|
||||
width -= left + right;
|
||||
height -= top + bottom;
|
||||
}
|
||||
else {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Unable to get window border size: %s",
|
||||
SDL_GetError());
|
||||
}
|
||||
|
||||
// If the stream window can fit within the usable drawing area with 1:1
|
||||
// scaling, do that rather than filling the screen.
|
||||
if (m_StreamConfig.width < width && m_StreamConfig.height < height) {
|
||||
width = m_StreamConfig.width;
|
||||
height = m_StreamConfig.height;
|
||||
// If the stream window can fit within the usable drawing area with 1:1
|
||||
// scaling, do that rather than filling the screen.
|
||||
if (m_StreamConfig.width < width && m_StreamConfig.height < height) {
|
||||
width = m_StreamConfig.width;
|
||||
height = m_StreamConfig.height;
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"SDL_GetDisplayUsableBounds() failed: %s",
|
||||
SDL_GetError());
|
||||
|
||||
width = m_StreamConfig.width;
|
||||
height = m_StreamConfig.height;
|
||||
}
|
||||
|
||||
x = y = SDL_WINDOWPOS_CENTERED_DISPLAY(displayIndex);
|
||||
}
|
||||
|
||||
void Session::updateOptimalWindowDisplayMode()
|
||||
{
|
||||
SDL_DisplayMode desktopMode, bestMode, mode;
|
||||
int displayIndex = SDL_GetWindowDisplayIndex(m_Window);
|
||||
|
||||
// Try the current display mode first. On macOS, this will be the normal
|
||||
// scaled desktop resolution setting.
|
||||
if (SDL_GetDesktopDisplayMode(displayIndex, &desktopMode) == 0) {
|
||||
// If this doesn't fit the selected resolution, use the native
|
||||
// resolution of the panel (unscaled).
|
||||
if (desktopMode.w < m_ActiveVideoWidth || desktopMode.h < m_ActiveVideoHeight) {
|
||||
if (!StreamUtils::getRealDesktopMode(displayIndex, &desktopMode)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"SDL_GetDesktopDisplayMode() failed: %s",
|
||||
SDL_GetError());
|
||||
return;
|
||||
}
|
||||
|
||||
// Start with the native desktop resolution and try to find
|
||||
// the highest refresh rate that our stream FPS evenly divides.
|
||||
bestMode = desktopMode;
|
||||
bestMode.refresh_rate = 0;
|
||||
for (int i = 0; i < SDL_GetNumDisplayModes(displayIndex); i++) {
|
||||
if (SDL_GetDisplayMode(displayIndex, i, &mode) == 0) {
|
||||
if (mode.w == desktopMode.w && mode.h == desktopMode.h &&
|
||||
mode.refresh_rate % m_StreamConfig.fps == 0) {
|
||||
if (mode.refresh_rate > bestMode.refresh_rate) {
|
||||
bestMode = mode;
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"SDL_GetDisplayUsableBounds() failed: %s",
|
||||
SDL_GetError());
|
||||
|
||||
width = m_StreamConfig.width;
|
||||
height = m_StreamConfig.height;
|
||||
x = y = SDL_WINDOWPOS_UNDEFINED_DISPLAY(displayIndex);
|
||||
}
|
||||
}
|
||||
|
||||
if (bestMode.refresh_rate == 0) {
|
||||
// We may find no match if the user has moved a 120 FPS
|
||||
// stream onto a 60 Hz monitor (since no refresh rate can
|
||||
// divide our FPS setting). We'll stick to the default in
|
||||
// this case.
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"No matching refresh rate found; using desktop mode");
|
||||
bestMode = desktopMode;
|
||||
}
|
||||
|
||||
if ((SDL_GetWindowFlags(m_Window) & SDL_WINDOW_FULLSCREEN_DESKTOP) == SDL_WINDOW_FULLSCREEN) {
|
||||
// Only print when the window is actually in full-screen exclusive mode,
|
||||
// otherwise we're not actually using the mode we've set here
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Chosen best display mode: %dx%dx%d",
|
||||
bestMode.w, bestMode.h, bestMode.refresh_rate);
|
||||
}
|
||||
|
||||
SDL_SetWindowDisplayMode(m_Window, &bestMode);
|
||||
}
|
||||
|
||||
void Session::toggleFullscreen()
|
||||
{
|
||||
bool fullScreen = !(SDL_GetWindowFlags(m_Window) & SDL_OS_FULLSCREEN_FLAG);
|
||||
|
||||
int x, y, width, height;
|
||||
|
||||
// If we're leaving full screen, toggle out before setting our size and position
|
||||
// that way we have accurate display size metrics (not the size our stream changed it to).
|
||||
if (!fullScreen) {
|
||||
SDL_SetWindowFullscreen(m_Window, 0);
|
||||
SDL_SetWindowResizable(m_Window, SDL_TRUE);
|
||||
}
|
||||
|
||||
getWindowDimensions(fullScreen, x, y, width, height);
|
||||
|
||||
SDL_SetWindowPosition(m_Window, x, y);
|
||||
SDL_SetWindowSize(m_Window, width, height);
|
||||
bool fullScreen = !(SDL_GetWindowFlags(m_Window) & m_FullScreenFlag);
|
||||
|
||||
if (fullScreen) {
|
||||
SDL_SetWindowResizable(m_Window, SDL_FALSE);
|
||||
SDL_SetWindowFullscreen(m_Window, SDL_OS_FULLSCREEN_FLAG);
|
||||
SDL_SetWindowFullscreen(m_Window, m_FullScreenFlag);
|
||||
}
|
||||
else {
|
||||
SDL_SetWindowFullscreen(m_Window, 0);
|
||||
SDL_SetWindowResizable(m_Window, SDL_TRUE);
|
||||
|
||||
// Reposition the window when the resize is complete
|
||||
m_PendingWindowedTransition = true;
|
||||
}
|
||||
}
|
||||
|
||||
void Session::exec()
|
||||
void Session::exec(int displayOriginX, int displayOriginY)
|
||||
{
|
||||
m_DisplayOriginX = displayOriginX;
|
||||
m_DisplayOriginY = displayOriginY;
|
||||
|
||||
// Complete initialization in this deferred context to avoid
|
||||
// calling expensive functions in the constructor (during the
|
||||
// process of loading the StreamSegue).
|
||||
initialize();
|
||||
|
||||
// Check for validation errors/warnings and emit
|
||||
// signals for them, if appropriate
|
||||
if (!validateLaunch()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Manually pump the UI thread for the view
|
||||
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
|
||||
// Wait 1.5 seconds before connecting to let the user
|
||||
// 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
|
||||
s_ActiveSessionSemaphore.acquire();
|
||||
@@ -608,7 +766,9 @@ void Session::exec()
|
||||
|
||||
// Initialize the gamepad code with our preferences
|
||||
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
|
||||
// if we decide to stream a different game.
|
||||
@@ -642,6 +802,10 @@ void Session::exec()
|
||||
emit displayLaunchError(e.toQString());
|
||||
s_ActiveSessionSemaphore.release();
|
||||
return;
|
||||
} catch (const QtNetworkReplyException& e) {
|
||||
emit displayLaunchError(e.toQString());
|
||||
s_ActiveSessionSemaphore.release();
|
||||
return;
|
||||
}
|
||||
|
||||
SDL_assert(!SDL_WasInit(SDL_INIT_VIDEO));
|
||||
@@ -663,21 +827,22 @@ void Session::exec()
|
||||
|
||||
// Older GFE versions didn't have this field
|
||||
QByteArray siGfeVersion;
|
||||
if (!m_Computer->gfeVersion.isNull()) {
|
||||
if (!m_Computer->gfeVersion.isEmpty()) {
|
||||
siGfeVersion = m_Computer->gfeVersion.toLatin1();
|
||||
}
|
||||
if (!siGfeVersion.isNull()) {
|
||||
if (!siGfeVersion.isEmpty()) {
|
||||
hostInfo.serverInfoGfeVersion = siGfeVersion.data();
|
||||
}
|
||||
|
||||
int err = LiStartConnection(&hostInfo, &m_StreamConfig, &k_ConnCallbacks,
|
||||
&m_VideoCallbacks, &k_AudioCallbacks,
|
||||
&m_VideoCallbacks,
|
||||
m_AudioDisabled ? nullptr : &k_AudioCallbacks,
|
||||
NULL, 0, NULL, 0);
|
||||
if (err != 0) {
|
||||
// We already displayed an error dialog in the stage failure
|
||||
// listener.
|
||||
s_ActiveSessionSemaphore.release();
|
||||
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||
s_ActiveSessionSemaphore.release();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -686,39 +851,23 @@ void Session::exec()
|
||||
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
|
||||
|
||||
int x, y, width, height;
|
||||
getWindowDimensions(m_Preferences.fullScreen,
|
||||
x, y, width, height);
|
||||
getWindowDimensions(x, y, width, height);
|
||||
|
||||
m_Window = SDL_CreateWindow("Moonlight",
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
m_Preferences.fullScreen ?
|
||||
SDL_OS_FULLSCREEN_FLAG : 0);
|
||||
SDL_WINDOW_ALLOW_HIGHDPI);
|
||||
if (!m_Window) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"SDL_CreateWindow() failed: %s",
|
||||
SDL_GetError());
|
||||
s_ActiveSessionSemaphore.release();
|
||||
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||
s_ActiveSessionSemaphore.release();
|
||||
return;
|
||||
}
|
||||
|
||||
// For non-full screen windows, call getWindowDimensions()
|
||||
// again after creating a window to allow it to account
|
||||
// for window chrome size.
|
||||
if (!m_Preferences.fullScreen) {
|
||||
getWindowDimensions(false, x, y, width, height);
|
||||
|
||||
SDL_SetWindowPosition(m_Window, x, y);
|
||||
SDL_SetWindowSize(m_Window, width, height);
|
||||
|
||||
// Passing SDL_WINDOW_RESIZABLE to set this during window
|
||||
// creation causes our window to be full screen for some reason
|
||||
SDL_SetWindowResizable(m_Window, SDL_TRUE);
|
||||
}
|
||||
|
||||
QSvgRenderer svgIconRenderer(QString(":/res/moonlight.svg"));
|
||||
QImage svgImage(ICON_SIZE, ICON_SIZE, QImage::Format_RGBA8888);
|
||||
svgImage.fill(0);
|
||||
@@ -732,17 +881,42 @@ void Session::exec()
|
||||
4 * svgImage.width(),
|
||||
SDL_PIXELFORMAT_RGBA32);
|
||||
#ifndef Q_OS_DARWIN
|
||||
// Other platforms seem to preserve our Qt icon when creating a new window
|
||||
// Other platforms seem to preserve our Qt icon when creating a new window.
|
||||
if (iconSurface != nullptr) {
|
||||
// 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()
|
||||
// again after creating a window to allow it to account
|
||||
// for window chrome size.
|
||||
if (m_Preferences->windowMode == StreamingPreferences::WM_WINDOWED) {
|
||||
getWindowDimensions(x, y, width, height);
|
||||
|
||||
// We must set the size before the position because centering
|
||||
// won't work unless it knows the final size of the window.
|
||||
SDL_SetWindowSize(m_Window, width, height);
|
||||
SDL_SetWindowPosition(m_Window, x, y);
|
||||
|
||||
// Passing SDL_WINDOW_RESIZABLE to set this during window
|
||||
// creation causes our window to be full screen for some reason
|
||||
SDL_SetWindowResizable(m_Window, SDL_TRUE);
|
||||
}
|
||||
else {
|
||||
// Update the window display mode based on our current monitor
|
||||
updateOptimalWindowDisplayMode();
|
||||
|
||||
// Enter full screen
|
||||
SDL_SetWindowFullscreen(m_Window, m_FullScreenFlag);
|
||||
}
|
||||
|
||||
#ifndef QT_DEBUG
|
||||
// Capture the mouse by default on release builds only.
|
||||
// This prevents the mouse from becoming trapped inside
|
||||
// Moonlight when it's halted at a debug break.
|
||||
if (m_Preferences.fullScreen) {
|
||||
if (m_Preferences->windowMode != StreamingPreferences::WM_WINDOWED) {
|
||||
SDL_SetRelativeMouseMode(SDL_TRUE);
|
||||
}
|
||||
#endif
|
||||
@@ -756,6 +930,17 @@ void Session::exec()
|
||||
// Disable the screen saver
|
||||
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
|
||||
// sleep precision and more accurate callback timing.
|
||||
SDL_SetHint(SDL_HINT_TIMER_RESOLUTION, "1");
|
||||
|
||||
// Raise the priority of the main thread, since it handles
|
||||
// time-sensitive video rendering
|
||||
if (SDL_SetThreadPriority(SDL_THREAD_PRIORITY_HIGH) < 0) {
|
||||
@@ -764,6 +949,8 @@ void Session::exec()
|
||||
SDL_GetError());
|
||||
}
|
||||
|
||||
int currentDisplayIndex = SDL_GetWindowDisplayIndex(m_Window);
|
||||
|
||||
// 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.
|
||||
SDL_Event event;
|
||||
@@ -802,32 +989,122 @@ void Session::exec()
|
||||
}
|
||||
|
||||
case SDL_WINDOWEVENT:
|
||||
// Capture mouse cursor when user actives the window by clicking on
|
||||
// window's client area (borders and title bar excluded).
|
||||
// Without this you would have to click the window twice (once to
|
||||
// activate it, second time to enable capture). With this you need to
|
||||
// click it only once.
|
||||
// On Linux, the button press event is delivered after the focus gain
|
||||
// so this is not neccessary (and leads to a click sent to the host
|
||||
// when focusing the window by clicking).
|
||||
// By excluding window's borders and title bar out, lets user still
|
||||
// interact with them without mouse capture kicking in.
|
||||
#if defined(Q_OS_WIN32) || defined(Q_OS_DARWIN)
|
||||
if (event.window.event == SDL_WINDOWEVENT_FOCUS_GAINED) {
|
||||
int mouseX, mouseY;
|
||||
Uint32 mouseState = SDL_GetGlobalMouseState(&mouseX, &mouseY);
|
||||
if (mouseState & SDL_BUTTON(SDL_BUTTON_LEFT)) {
|
||||
int x, y, width, height;
|
||||
SDL_GetWindowPosition(m_Window, &x, &y);
|
||||
SDL_GetWindowSize(m_Window, &width, &height);
|
||||
if (mouseX > x && mouseX < x+width && mouseY > y && mouseY < y+height) {
|
||||
SDL_SetRelativeMouseMode(SDL_TRUE);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
if (event.window.event == SDL_WINDOWEVENT_FOCUS_LOST) {
|
||||
// Release mouse cursor when another window is activated (e.g. by using ALT+TAB).
|
||||
// This lets user to interact with our window's title bar and with the buttons in it.
|
||||
// Doing this while the window is full-screen breaks the transition out of FS
|
||||
// (desktop and exclusive), so we must check for that before releasing mouse capture.
|
||||
if (!(SDL_GetWindowFlags(m_Window) & SDL_WINDOW_FULLSCREEN)) {
|
||||
SDL_SetRelativeMouseMode(SDL_FALSE);
|
||||
}
|
||||
|
||||
// Raise all keys that are currently pressed. If we don't do this, certain keys
|
||||
// used in shortcuts that cause focus loss (such as Alt+Tab) may get stuck down.
|
||||
inputHandler.raiseAllKeys();
|
||||
}
|
||||
|
||||
// We want to recreate the decoder for resizes (full-screen toggles) and the initial shown event.
|
||||
// We use SDL_WINDOWEVENT_SIZE_CHANGED rather than SDL_WINDOWEVENT_RESIZED because the latter doesn't
|
||||
// seem to fire when switching from windowed to full-screen on X11.
|
||||
if (event.window.event != SDL_WINDOWEVENT_SIZE_CHANGED && event.window.event != SDL_WINDOWEVENT_SHOWN) {
|
||||
break;
|
||||
// Check that the window display hasn't changed. If it has, we want
|
||||
// to recreate the decoder to allow it to adapt to the new display.
|
||||
// This will allow Pacer to pull the new display refresh rate.
|
||||
if (SDL_GetWindowDisplayIndex(m_Window) == currentDisplayIndex) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Complete any repositioning that was deferred until
|
||||
// the resize from full-screen to windowed had completed.
|
||||
// If we try to do this immediately, the resize won't take effect
|
||||
// properly on Windows.
|
||||
if (m_PendingWindowedTransition) {
|
||||
m_PendingWindowedTransition = false;
|
||||
|
||||
int x, y, width, height;
|
||||
getWindowDimensions(x, y, width, height);
|
||||
|
||||
SDL_SetWindowSize(m_Window, width, height);
|
||||
SDL_SetWindowPosition(m_Window, x, y);
|
||||
}
|
||||
|
||||
// Fall through
|
||||
case SDL_RENDER_DEVICE_RESET:
|
||||
case SDL_RENDER_TARGETS_RESET:
|
||||
|
||||
SDL_AtomicLock(&m_DecoderLock);
|
||||
|
||||
// Destroy the old decoder
|
||||
delete m_VideoDecoder;
|
||||
|
||||
// Chose a new decoder (hopefully the same one, but possibly
|
||||
// not if a GPU was removed or something).
|
||||
if (!chooseDecoder(m_Preferences.videoDecoderSelection,
|
||||
m_Window, m_ActiveVideoFormat, m_ActiveVideoWidth,
|
||||
m_ActiveVideoHeight, m_ActiveVideoFrameRate,
|
||||
s_ActiveSession->m_VideoDecoder)) {
|
||||
SDL_AtomicUnlock(&m_DecoderLock);
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Failed to recreate decoder after reset");
|
||||
emit displayLaunchError("Unable to initialize video decoder. Please check your streaming settings and try again.");
|
||||
goto DispatchDeferredCleanup;
|
||||
// Flush any other pending window events that could
|
||||
// send us back here immediately
|
||||
SDL_PumpEvents();
|
||||
SDL_FlushEvent(SDL_WINDOWEVENT);
|
||||
|
||||
// Update the window display mode based on our current monitor
|
||||
currentDisplayIndex = SDL_GetWindowDisplayIndex(m_Window);
|
||||
updateOptimalWindowDisplayMode();
|
||||
|
||||
// Now that the old decoder is dead, flush any events it may
|
||||
// have queued to reset itself (if this reset was the result
|
||||
// of state loss).
|
||||
SDL_PumpEvents();
|
||||
SDL_FlushEvent(SDL_RENDER_DEVICE_RESET);
|
||||
SDL_FlushEvent(SDL_RENDER_TARGETS_RESET);
|
||||
|
||||
{
|
||||
// If the stream exceeds the display refresh rate (plus some slack),
|
||||
// forcefully disable V-sync to allow the stream to render faster
|
||||
// than the display.
|
||||
int displayHz = StreamUtils::getDisplayRefreshRate(m_Window);
|
||||
bool enableVsync = m_Preferences->enableVsync;
|
||||
if (displayHz + 5 < m_StreamConfig.fps) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Disabling V-sync because refresh rate limit exceeded");
|
||||
enableVsync = false;
|
||||
}
|
||||
|
||||
// Choose a new decoder (hopefully the same one, but possibly
|
||||
// not if a GPU was removed or something).
|
||||
if (!chooseDecoder(m_Preferences->videoDecoderSelection,
|
||||
m_Window, m_ActiveVideoFormat, m_ActiveVideoWidth,
|
||||
m_ActiveVideoHeight, m_ActiveVideoFrameRate,
|
||||
enableVsync,
|
||||
s_ActiveSession->m_VideoDecoder)) {
|
||||
SDL_AtomicUnlock(&m_DecoderLock);
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Failed to recreate decoder after reset");
|
||||
emit displayLaunchError("Unable to initialize video decoder. Please check your streaming settings and try again.");
|
||||
goto DispatchDeferredCleanup;
|
||||
}
|
||||
}
|
||||
|
||||
// Request an IDR frame to complete the reset
|
||||
@@ -861,18 +1138,37 @@ void Session::exec()
|
||||
case SDL_CONTROLLERDEVICEREMOVED:
|
||||
inputHandler.handleControllerDeviceEvent(&event.cdevice);
|
||||
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:
|
||||
#ifdef Q_OS_WIN32
|
||||
// HACK: See comment above
|
||||
SetThreadExecutionState(ES_CONTINUOUS);
|
||||
#endif
|
||||
|
||||
// Uncapture the mouse and hide the window immediately,
|
||||
// so we can return to the Qt GUI ASAP.
|
||||
SDL_SetRelativeMouseMode(SDL_FALSE);
|
||||
SDL_EnableScreenSaver();
|
||||
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
|
||||
SDL_AtomicLock(&m_DecoderLock);
|
||||
|
||||
@@ -6,8 +6,9 @@
|
||||
#include <opus_multistream.h>
|
||||
#include "backend/computermanager.h"
|
||||
#include "settings/streamingpreferences.h"
|
||||
#include "input.hpp"
|
||||
#include "input.h"
|
||||
#include "video/decoder.h"
|
||||
#include "audio/renderers/renderer.h"
|
||||
|
||||
class Session : public QObject
|
||||
{
|
||||
@@ -17,14 +18,18 @@ class Session : public QObject
|
||||
friend class DeferredSessionCleanupTask;
|
||||
|
||||
public:
|
||||
explicit Session(NvComputer* computer, NvApp& app);
|
||||
explicit Session(NvComputer* computer, NvApp& app, StreamingPreferences *preferences = nullptr);
|
||||
|
||||
Q_INVOKABLE void exec();
|
||||
Q_INVOKABLE void exec(int displayOriginX, int displayOriginY);
|
||||
|
||||
static
|
||||
bool isHardwareDecodeAvailable(StreamingPreferences::VideoDecoderSelection vds,
|
||||
int videoFormat, int width, int height, int frameRate);
|
||||
|
||||
static
|
||||
int getDecoderCapabilities(StreamingPreferences::VideoDecoderSelection vds,
|
||||
int videoFormat, int width, int height, int frameRate);
|
||||
|
||||
signals:
|
||||
void stageStarting(QString stage);
|
||||
|
||||
@@ -37,24 +42,31 @@ signals:
|
||||
void displayLaunchWarning(QString text);
|
||||
|
||||
private:
|
||||
void initialize();
|
||||
|
||||
bool validateLaunch();
|
||||
|
||||
void emitLaunchWarning(QString text);
|
||||
|
||||
int getDecoderCapabilities();
|
||||
|
||||
int sdlDetermineAudioConfiguration();
|
||||
IAudioRenderer* createAudioRenderer();
|
||||
|
||||
void getWindowDimensions(bool fullScreen,
|
||||
int& x, int& y,
|
||||
int detectAudioConfiguration();
|
||||
|
||||
bool testAudio(int audioConfiguration);
|
||||
|
||||
void getWindowDimensions(int& x, int& y,
|
||||
int& width, int& height);
|
||||
|
||||
void toggleFullscreen();
|
||||
|
||||
void updateOptimalWindowDisplayMode();
|
||||
|
||||
static
|
||||
bool chooseDecoder(StreamingPreferences::VideoDecoderSelection vds,
|
||||
SDL_Window* window, int videoFormat, int width, int height,
|
||||
int frameRate, IVideoDecoder*& chosenDecoder);
|
||||
int frameRate, bool enableVsync, IVideoDecoder*& chosenDecoder);
|
||||
|
||||
static
|
||||
void clStageStarting(int stage);
|
||||
@@ -68,6 +80,17 @@ private:
|
||||
static
|
||||
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
|
||||
int drSetup(int videoFormat, int width, int height, int frameRate, void*, int);
|
||||
|
||||
@@ -77,24 +100,7 @@ private:
|
||||
static
|
||||
int drSubmitDecodeUnit(PDECODE_UNIT du);
|
||||
|
||||
static
|
||||
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;
|
||||
StreamingPreferences* m_Preferences;
|
||||
STREAM_CONFIGURATION m_StreamConfig;
|
||||
DECODER_RENDERER_CALLBACKS m_VideoCallbacks;
|
||||
NvComputer* m_Computer;
|
||||
@@ -103,20 +109,22 @@ private:
|
||||
IVideoDecoder* m_VideoDecoder;
|
||||
SDL_SpinLock m_DecoderLock;
|
||||
bool m_NeedsIdr;
|
||||
bool m_AudioDisabled;
|
||||
Uint32 m_FullScreenFlag;
|
||||
int m_DisplayOriginX;
|
||||
int m_DisplayOriginY;
|
||||
bool m_PendingWindowedTransition;
|
||||
|
||||
int m_ActiveVideoFormat;
|
||||
int m_ActiveVideoWidth;
|
||||
int m_ActiveVideoHeight;
|
||||
int m_ActiveVideoFrameRate;
|
||||
|
||||
static SDL_AudioDeviceID s_AudioDevice;
|
||||
static OpusMSDecoder* s_OpusDecoder;
|
||||
static short s_OpusDecodeBuffer[];
|
||||
static int s_ChannelCount;
|
||||
static int s_PendingDrops;
|
||||
static int s_PendingHardDrops;
|
||||
static unsigned int s_SampleIndex;
|
||||
static Uint32 s_BaselinePendingData;
|
||||
OpusMSDecoder* m_OpusDecoder;
|
||||
short m_OpusDecodeBuffer[MAX_CHANNELS * SAMPLES_PER_FRAME];
|
||||
IAudioRenderer* m_AudioRenderer;
|
||||
OPUS_MULTISTREAM_CONFIGURATION m_AudioConfig;
|
||||
int m_AudioSampleCount;
|
||||
|
||||
static AUDIO_RENDERER_CALLBACKS k_AudioCallbacks;
|
||||
static CONNECTION_LISTENER_CALLBACKS k_ConnCallbacks;
|
||||
@@ -1,5 +1,11 @@
|
||||
#include "streamutils.h"
|
||||
|
||||
#include <Qt>
|
||||
|
||||
#ifdef Q_OS_DARWIN
|
||||
#include <ApplicationServices/ApplicationServices.h>
|
||||
#endif
|
||||
|
||||
void StreamUtils::scaleSourceToDestinationSurface(SDL_Rect* src, SDL_Rect* dst)
|
||||
{
|
||||
int dstH = dst->w * src->h / src->w;
|
||||
@@ -18,3 +24,104 @@ void StreamUtils::scaleSourceToDestinationSurface(SDL_Rect* src, SDL_Rect* dst)
|
||||
SDL_assert(dst->h * src->w / src->h <= dst->w);
|
||||
}
|
||||
}
|
||||
|
||||
int StreamUtils::getDisplayRefreshRate(SDL_Window* window)
|
||||
{
|
||||
int displayIndex = SDL_GetWindowDisplayIndex(window);
|
||||
if (displayIndex < 0) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Failed to get current display: %s",
|
||||
SDL_GetError());
|
||||
|
||||
// Assume display 0 if it fails
|
||||
displayIndex = 0;
|
||||
}
|
||||
|
||||
SDL_DisplayMode mode;
|
||||
if ((SDL_GetWindowFlags(window) & SDL_WINDOW_FULLSCREEN_DESKTOP) == SDL_WINDOW_FULLSCREEN) {
|
||||
// Use the window display mode for full-screen exclusive mode
|
||||
if (SDL_GetWindowDisplayMode(window, &mode) != 0) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"SDL_GetWindowDisplayMode() failed: %s",
|
||||
SDL_GetError());
|
||||
|
||||
// Assume 60 Hz
|
||||
return 60;
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Use the current display mode for windowed and borderless
|
||||
if (SDL_GetCurrentDisplayMode(displayIndex, &mode) != 0) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"SDL_GetCurrentDisplayMode() failed: %s",
|
||||
SDL_GetError());
|
||||
|
||||
// Assume 60 Hz
|
||||
return 60;
|
||||
}
|
||||
}
|
||||
|
||||
// May be zero if undefined
|
||||
if (mode.refresh_rate == 0) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Refresh rate unknown; assuming 60 Hz");
|
||||
mode.refresh_rate = 60;
|
||||
}
|
||||
|
||||
return mode.refresh_rate;
|
||||
}
|
||||
|
||||
bool StreamUtils::getRealDesktopMode(int displayIndex, SDL_DisplayMode* mode)
|
||||
{
|
||||
#ifdef Q_OS_DARWIN
|
||||
#define MAX_DISPLAYS 16
|
||||
CGDirectDisplayID displayIds[MAX_DISPLAYS];
|
||||
uint32_t displayCount = 0;
|
||||
CGGetActiveDisplayList(MAX_DISPLAYS, displayIds, &displayCount);
|
||||
if (displayIndex >= displayCount) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Too many displays: %d vs %d",
|
||||
displayIndex, displayCount);
|
||||
return false;
|
||||
}
|
||||
|
||||
SDL_zerop(mode);
|
||||
|
||||
// Retina displays have non-native resolutions both below and above (!) their
|
||||
// native resolution, so it's impossible for us to figure out what's actually
|
||||
// native on macOS using the SDL API alone. We'll talk to CoreGraphics to
|
||||
// find the correct resolution and match it in our SDL list.
|
||||
CFArrayRef modeList = CGDisplayCopyAllDisplayModes(displayIds[displayIndex], nullptr);
|
||||
CFIndex count = CFArrayGetCount(modeList);
|
||||
for (CFIndex i = 0; i < count; i++) {
|
||||
auto cgMode = (CGDisplayModeRef)(CFArrayGetValueAtIndex(modeList, i));
|
||||
if ((CGDisplayModeGetIOFlags(cgMode) & kDisplayModeNativeFlag) != 0) {
|
||||
mode->w = static_cast<int>(CGDisplayModeGetWidth(cgMode));
|
||||
mode->h = static_cast<int>(CGDisplayModeGetHeight(cgMode));
|
||||
break;
|
||||
}
|
||||
}
|
||||
CFRelease(modeList);
|
||||
|
||||
// Now find the SDL mode that matches the CG native mode
|
||||
for (int i = 0; i < SDL_GetNumDisplayModes(displayIndex); i++) {
|
||||
SDL_DisplayMode thisMode;
|
||||
if (SDL_GetDisplayMode(displayIndex, i, &thisMode) == 0) {
|
||||
if (thisMode.w == mode->w && thisMode.h == mode->h &&
|
||||
thisMode.refresh_rate >= mode->refresh_rate) {
|
||||
*mode = thisMode;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
#else
|
||||
if (SDL_GetDesktopDisplayMode(displayIndex, mode) != 0) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"SDL_GetDesktopDisplayMode() failed: %s",
|
||||
SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -7,4 +7,10 @@ class StreamUtils
|
||||
public:
|
||||
static
|
||||
void scaleSourceToDestinationSurface(SDL_Rect* src, SDL_Rect* dst);
|
||||
|
||||
static
|
||||
bool getRealDesktopMode(int displayIndex, SDL_DisplayMode* mode);
|
||||
|
||||
static
|
||||
int getDisplayRefreshRate(SDL_Window* window);
|
||||
};
|
||||
|
||||
@@ -8,6 +8,21 @@
|
||||
|
||||
#define MAX_SLICES 4
|
||||
|
||||
typedef struct _VIDEO_STATS {
|
||||
uint32_t receivedFrames;
|
||||
uint32_t decodedFrames;
|
||||
uint32_t renderedFrames;
|
||||
uint32_t networkDroppedFrames;
|
||||
uint32_t pacerDroppedFrames;
|
||||
uint32_t totalReassemblyTime;
|
||||
uint32_t totalDecodeTime;
|
||||
uint32_t totalRenderTime;
|
||||
float receivedFps;
|
||||
float decodedFps;
|
||||
float renderedFps;
|
||||
uint32_t measurementStartTimestamp;
|
||||
} VIDEO_STATS, *PVIDEO_STATS;
|
||||
|
||||
class IVideoDecoder {
|
||||
public:
|
||||
virtual ~IVideoDecoder() {}
|
||||
@@ -16,8 +31,10 @@ public:
|
||||
int videoFormat,
|
||||
int width,
|
||||
int height,
|
||||
int frameRate) = 0;
|
||||
int frameRate,
|
||||
bool enableVsync) = 0;
|
||||
virtual bool isHardwareAccelerated() = 0;
|
||||
virtual int getDecoderCapabilities() = 0;
|
||||
virtual int submitDecodeUnit(PDECODE_UNIT du) = 0;
|
||||
virtual void renderFrame(SDL_UserEvent* event) = 0;
|
||||
virtual void dropFrame(SDL_UserEvent* event) = 0;
|
||||
|
||||
@@ -3,6 +3,13 @@
|
||||
#include "../ffmpeg.h"
|
||||
#include <streaming/streamutils.h>
|
||||
|
||||
#include <SDL_syswm.h>
|
||||
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <Windows.h>
|
||||
#include <VersionHelpers.h>
|
||||
#include <dwmapi.h>
|
||||
|
||||
#include <Limelight.h>
|
||||
|
||||
DEFINE_GUID(DXVADDI_Intel_ModeH264_E, 0x604F8E68,0x4951,0x4C54,0x88,0xFE,0xAB,0xD2,0x5C,0x15,0xB3,0xD6);
|
||||
@@ -10,7 +17,6 @@ DEFINE_GUID(DXVADDI_Intel_ModeH264_E, 0x604F8E68,0x4951,0x4C54,0x88,0xFE,0xAB,0x
|
||||
#define SAFE_COM_RELEASE(x) if (x) { (x)->Release(); }
|
||||
|
||||
DXVA2Renderer::DXVA2Renderer() :
|
||||
m_SdlRenderer(nullptr),
|
||||
m_DecService(nullptr),
|
||||
m_Decoder(nullptr),
|
||||
m_SurfacesUsed(0),
|
||||
@@ -23,10 +29,15 @@ DXVA2Renderer::DXVA2Renderer() :
|
||||
{
|
||||
RtlZeroMemory(m_DecSurfaces, sizeof(m_DecSurfaces));
|
||||
RtlZeroMemory(&m_DXVAContext, sizeof(m_DXVAContext));
|
||||
|
||||
// Use MMCSS scheduling for lower scheduling latency while we're streaming
|
||||
DwmEnableMMCSS(TRUE);
|
||||
}
|
||||
|
||||
DXVA2Renderer::~DXVA2Renderer()
|
||||
{
|
||||
DwmEnableMMCSS(FALSE);
|
||||
|
||||
SAFE_COM_RELEASE(m_DecService);
|
||||
SAFE_COM_RELEASE(m_Decoder);
|
||||
SAFE_COM_RELEASE(m_Device);
|
||||
@@ -41,10 +52,6 @@ DXVA2Renderer::~DXVA2Renderer()
|
||||
if (m_Pool != nullptr) {
|
||||
av_buffer_pool_uninit(&m_Pool);
|
||||
}
|
||||
|
||||
if (m_SdlRenderer != nullptr) {
|
||||
SDL_DestroyRenderer(m_SdlRenderer);
|
||||
}
|
||||
}
|
||||
|
||||
void DXVA2Renderer::ffPoolDummyDelete(void*, uint8_t*)
|
||||
@@ -259,74 +266,52 @@ bool DXVA2Renderer::initializeRenderer()
|
||||
return false;
|
||||
}
|
||||
|
||||
UINT guidCount;
|
||||
GUID* guids;
|
||||
hr = m_ProcService->GetVideoProcessorDeviceGuids(&m_Desc, &guidCount, &guids);
|
||||
DXVA2_VideoProcessorCaps caps;
|
||||
hr = m_ProcService->GetVideoProcessorCaps(DXVA2_VideoProcProgressiveDevice, &m_Desc, renderTargetDesc.Format, &caps);
|
||||
if (FAILED(hr)) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"GetVideoProcessorDeviceGuids() failed: %x",
|
||||
"GetVideoProcessorCaps() failed for DXVA2_VideoProcProgressiveDevice: %x",
|
||||
hr);
|
||||
return false;
|
||||
}
|
||||
|
||||
UINT i;
|
||||
for (i = 0; i < guidCount; i++) {
|
||||
DXVA2_VideoProcessorCaps caps;
|
||||
hr = m_ProcService->GetVideoProcessorCaps(guids[i], &m_Desc, renderTargetDesc.Format, &caps);
|
||||
if (SUCCEEDED(hr)) {
|
||||
if (!(caps.DeviceCaps & DXVA2_VPDev_HardwareDevice)) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Device %d is not hardware: %x",
|
||||
i,
|
||||
caps.DeviceCaps);
|
||||
continue;
|
||||
}
|
||||
else if (!(caps.VideoProcessorOperations & DXVA2_VideoProcess_YUV2RGB) &&
|
||||
!(caps.VideoProcessorOperations & DXVA2_VideoProcess_YUV2RGBExtended)) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Device %d can't convert YUV2RGB: %x",
|
||||
i,
|
||||
caps.VideoProcessorOperations);
|
||||
continue;
|
||||
}
|
||||
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);
|
||||
}
|
||||
if (!(caps.DeviceCaps & DXVA2_VPDev_HardwareDevice)) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"DXVA2_VideoProcProgressiveDevice is not hardware: %x",
|
||||
caps.DeviceCaps);
|
||||
return false;
|
||||
}
|
||||
else if (!(caps.VideoProcessorOperations & DXVA2_VideoProcess_YUV2RGB) &&
|
||||
!(caps.VideoProcessorOperations & DXVA2_VideoProcess_YUV2RGBExtended)) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"DXVA2_VideoProcProgressiveDevice can't convert YUV2RGB: %x",
|
||||
caps.VideoProcessorOperations);
|
||||
return false;
|
||||
}
|
||||
else if (!(caps.VideoProcessorOperations & DXVA2_VideoProcess_StretchX) ||
|
||||
!(caps.VideoProcessorOperations & DXVA2_VideoProcess_StretchY)) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"DXVA2_VideoProcProgressiveDevice can't stretch video: %x",
|
||||
caps.VideoProcessorOperations);
|
||||
return false;
|
||||
}
|
||||
|
||||
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,
|
||||
"Unable to find a usable DXVA2 processor");
|
||||
"CreateVideoProcessor() failed for DXVA2_VideoProcProgressiveDevice: %x",
|
||||
hr);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -357,6 +342,13 @@ bool DXVA2Renderer::isDecoderBlacklisted()
|
||||
id.Description,
|
||||
id.VendorId,
|
||||
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) {
|
||||
// Intel seems to encode the series in the high byte of
|
||||
@@ -372,26 +364,50 @@ bool DXVA2Renderer::isDecoderBlacklisted()
|
||||
case 0x1600: // Broadwell
|
||||
case 0x2200: // Cherry Trail and Braswell
|
||||
// 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;
|
||||
break;
|
||||
default:
|
||||
// Everything else is fine with whatever it says it supports
|
||||
result = false;
|
||||
// Intel drivers from before late-2017 had a bug that caused some strange artifacts
|
||||
// when decoding HEVC. Avoid HEVC on drivers prior to build 4836 which I confirmed
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
else if (id.VendorId == 0x10DE) {
|
||||
// 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://bluesky23.yukishigure.com/en/dxvac/deviceInfo/decoder.html
|
||||
// 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 >= 0x13C0 && id.DeviceId <= 0x13FF) || // GM204
|
||||
(id.DeviceId >= 0x1617 && id.DeviceId <= 0x161A) || // GM204
|
||||
(id.DeviceId == 0x1667) || // GM204
|
||||
(id.DeviceId >= 0x17C0 && id.DeviceId <= 0x17FF)) { // GM200
|
||||
// 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;
|
||||
}
|
||||
else {
|
||||
@@ -430,34 +446,147 @@ bool DXVA2Renderer::isDecoderBlacklisted()
|
||||
return result;
|
||||
}
|
||||
|
||||
bool DXVA2Renderer::initialize(SDL_Window* window, int videoFormat, int width, int height)
|
||||
bool DXVA2Renderer::initializeDevice(SDL_Window* window, bool enableVsync)
|
||||
{
|
||||
SDL_SysWMinfo info;
|
||||
|
||||
SDL_VERSION(&info.version);
|
||||
SDL_GetWindowWMInfo(window, &info);
|
||||
|
||||
IDirect3D9Ex* d3d9ex;
|
||||
HRESULT hr = Direct3DCreate9Ex(D3D_SDK_VERSION, &d3d9ex);
|
||||
if (FAILED(hr)) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Direct3DCreate9Ex() failed: %x",
|
||||
hr);
|
||||
return false;
|
||||
}
|
||||
|
||||
int adapterIndex = SDL_Direct3D9GetAdapterIndex(SDL_GetWindowDisplayIndex(window));
|
||||
Uint32 windowFlags = SDL_GetWindowFlags(window);
|
||||
|
||||
D3DCAPS9 deviceCaps;
|
||||
d3d9ex->GetDeviceCaps(adapterIndex, D3DDEVTYPE_HAL, &deviceCaps);
|
||||
|
||||
D3DDISPLAYMODEEX currentMode;
|
||||
currentMode.Size = sizeof(currentMode);
|
||||
d3d9ex->GetAdapterDisplayModeEx(adapterIndex, ¤tMode, nullptr);
|
||||
|
||||
D3DPRESENT_PARAMETERS d3dpp = {};
|
||||
d3dpp.hDeviceWindow = info.info.win.window;
|
||||
d3dpp.Flags = D3DPRESENTFLAG_VIDEO;
|
||||
|
||||
if ((windowFlags & SDL_WINDOW_FULLSCREEN_DESKTOP) == SDL_WINDOW_FULLSCREEN) {
|
||||
d3dpp.Windowed = false;
|
||||
d3dpp.BackBufferWidth = currentMode.Width;
|
||||
d3dpp.BackBufferHeight = currentMode.Height;
|
||||
d3dpp.FullScreen_RefreshRateInHz = currentMode.RefreshRate;
|
||||
d3dpp.BackBufferFormat = currentMode.Format;
|
||||
}
|
||||
else {
|
||||
d3dpp.Windowed = true;
|
||||
d3dpp.BackBufferFormat = D3DFMT_UNKNOWN;
|
||||
|
||||
SDL_GetWindowSize(window, (int*)&d3dpp.BackBufferWidth, (int*)&d3dpp.BackBufferHeight);
|
||||
}
|
||||
|
||||
BOOL dwmEnabled;
|
||||
DwmIsCompositionEnabled(&dwmEnabled);
|
||||
if (d3dpp.Windowed && dwmEnabled) {
|
||||
// If composition enabled, disable v-sync and let DWM manage things
|
||||
// to reduce latency by avoiding double v-syncing.
|
||||
d3dpp.PresentationInterval = D3DPRESENT_INTERVAL_IMMEDIATE;
|
||||
|
||||
// If V-sync is enabled (not rendering faster than display),
|
||||
// we can use FlipEx for more efficient swapping.
|
||||
if (enableVsync) {
|
||||
// D3DSWAPEFFECT_FLIPEX requires at least 2 back buffers to allow us to
|
||||
// continue while DWM is waiting to render the surface to the display.
|
||||
d3dpp.SwapEffect = D3DSWAPEFFECT_FLIPEX;
|
||||
d3dpp.BackBufferCount = 2;
|
||||
}
|
||||
else {
|
||||
// With V-sync off, we won't use FlipEx because that will block while
|
||||
// DWM is waiting to render our surface (effectively behaving like V-Sync).
|
||||
d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
|
||||
d3dpp.BackBufferCount = 1;
|
||||
}
|
||||
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Windowed mode with DWM running");
|
||||
}
|
||||
else if (enableVsync) {
|
||||
// Uncomposited desktop or full-screen exclusive mode with V-sync enabled
|
||||
// We will enable V-sync in this scenario to avoid tearing.
|
||||
d3dpp.PresentationInterval = D3DPRESENT_INTERVAL_ONE;
|
||||
d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
|
||||
d3dpp.BackBufferCount = 1;
|
||||
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"V-Sync enabled");
|
||||
}
|
||||
else {
|
||||
// Uncomposited desktop or full-screen exclusive mode with V-sync disabled
|
||||
// We will allowing tearing for lowest latency.
|
||||
d3dpp.PresentationInterval = D3DPRESENT_INTERVAL_IMMEDIATE;
|
||||
d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
|
||||
d3dpp.BackBufferCount = 1;
|
||||
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"V-Sync disabled in tearing mode");
|
||||
}
|
||||
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Windowed: %d | Present Interval: %x",
|
||||
d3dpp.Windowed, d3dpp.PresentationInterval);
|
||||
|
||||
// FFmpeg requires this attribute for doing asynchronous decoding
|
||||
// in a separate thread with this device.
|
||||
int deviceFlags = D3DCREATE_MULTITHREADED;
|
||||
|
||||
if (deviceCaps.DevCaps & D3DDEVCAPS_HWTRANSFORMANDLIGHT) {
|
||||
deviceFlags |= D3DCREATE_HARDWARE_VERTEXPROCESSING;
|
||||
}
|
||||
else {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"No hardware vertex processing support!");
|
||||
deviceFlags |= D3DCREATE_SOFTWARE_VERTEXPROCESSING;
|
||||
}
|
||||
|
||||
hr = d3d9ex->CreateDeviceEx(adapterIndex,
|
||||
D3DDEVTYPE_HAL,
|
||||
d3dpp.hDeviceWindow,
|
||||
deviceFlags,
|
||||
&d3dpp,
|
||||
d3dpp.Windowed ? nullptr : ¤tMode,
|
||||
&m_Device);
|
||||
|
||||
d3d9ex->Release();
|
||||
|
||||
if (FAILED(hr)) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"CreateDeviceEx() failed: %x",
|
||||
hr);
|
||||
return false;
|
||||
}
|
||||
|
||||
hr = m_Device->SetMaximumFrameLatency(1);
|
||||
if (FAILED(hr)) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"SetMaximumFrameLatency() failed: %x",
|
||||
hr);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DXVA2Renderer::initialize(SDL_Window* window, int videoFormat, int width, int height, int, bool enableVsync)
|
||||
{
|
||||
m_VideoFormat = videoFormat;
|
||||
m_VideoWidth = width;
|
||||
m_VideoHeight = height;
|
||||
|
||||
// FFmpeg will be decoding on different threads than the main thread that we're
|
||||
// currently running on right now. We must set this hint so SDL will pass
|
||||
// D3DCREATE_MULTITHREADED to IDirect3D9::CreateDevice().
|
||||
SDL_SetHint(SDL_HINT_RENDER_DIRECT3D_THREADSAFE, "1");
|
||||
|
||||
// We require full-screen desktop mode to avoid having to enable V-sync
|
||||
// to synchronize frame delivery (which has a much higher latency penalty).
|
||||
m_SdlRenderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);
|
||||
if (!m_SdlRenderer) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"SDL_CreateRenderer() failed: %s",
|
||||
SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
|
||||
m_Device = SDL_RenderGetD3D9Device(m_SdlRenderer);
|
||||
|
||||
// Draw a black frame until the video stream starts rendering
|
||||
SDL_SetRenderDrawColor(m_SdlRenderer, 0, 0, 0, SDL_ALPHA_OPAQUE);
|
||||
SDL_RenderClear(m_SdlRenderer);
|
||||
SDL_RenderPresent(m_SdlRenderer);
|
||||
|
||||
RtlZeroMemory(&m_Desc, sizeof(m_Desc));
|
||||
|
||||
int alignment;
|
||||
@@ -481,6 +610,10 @@ bool DXVA2Renderer::initialize(SDL_Window* window, int videoFormat, int width, i
|
||||
m_Desc.SampleFormat.SampleFormat = DXVA2_SampleProgressiveFrame;
|
||||
m_Desc.Format = (D3DFORMAT)MAKEFOURCC('N','V','1','2');
|
||||
|
||||
if (!initializeDevice(window, enableVsync)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!initializeDecoder()) {
|
||||
return false;
|
||||
}
|
||||
@@ -489,10 +622,34 @@ bool DXVA2Renderer::initialize(SDL_Window* window, int videoFormat, int width, i
|
||||
return false;
|
||||
}
|
||||
|
||||
// For some reason, using Direct3D9Ex breaks this with multi-monitor setups.
|
||||
// When focus is lost, the window is minimized then immediately restored without
|
||||
// input focus. This glitches out the renderer and a bunch of other stuff.
|
||||
// Direct3D9Ex itself seems to have this minimize on focus loss behavior on its
|
||||
// own, so just disable SDL's handling of the focus loss event.
|
||||
SDL_SetHint(SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS, "0");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void DXVA2Renderer::renderFrame(AVFrame* frame)
|
||||
bool DXVA2Renderer::needsTestFrame()
|
||||
{
|
||||
// We validate the DXVA2 profiles are supported
|
||||
// in initialize() so no test frame is required
|
||||
return false;
|
||||
}
|
||||
|
||||
int DXVA2Renderer::getDecoderCapabilities()
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
IFFmpegRenderer::VSyncConstraint DXVA2Renderer::getVsyncConstraint()
|
||||
{
|
||||
return VSYNC_ANY;
|
||||
}
|
||||
|
||||
void DXVA2Renderer::renderFrameAtVsync(AVFrame *frame)
|
||||
{
|
||||
IDirect3DSurface9* surface = reinterpret_cast<IDirect3DSurface9*>(frame->data[3]);
|
||||
HRESULT hr;
|
||||
@@ -628,33 +785,27 @@ void DXVA2Renderer::renderFrame(AVFrame* frame)
|
||||
|
||||
bltParams.Alpha = DXVA2_Fixed32OpaqueAlpha();
|
||||
|
||||
if (SDL_RenderClear(m_SdlRenderer) != 0) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"SDL_RenderClear() failed: %s",
|
||||
SDL_GetError());
|
||||
av_frame_free(&frame);
|
||||
|
||||
// We're going to cheat a little bit here. It seems SDL's
|
||||
// renderer may flake out in scenarios like moving the window
|
||||
// between monitors, so generate a synthetic reset event for
|
||||
// the main loop to consume.
|
||||
SDL_Event event;
|
||||
event.type = SDL_RENDER_TARGETS_RESET;
|
||||
SDL_PushEvent(&event);
|
||||
|
||||
return;
|
||||
}
|
||||
m_Device->Clear(0, nullptr, D3DCLEAR_TARGET, D3DCOLOR_ARGB(255, 0, 0, 0), 0.0f, 0);
|
||||
|
||||
hr = m_Processor->VideoProcessBlt(m_RenderTarget, &bltParams, &sample, 1, nullptr);
|
||||
av_frame_free(&frame);
|
||||
|
||||
if (FAILED(hr)) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"VideoProcessBlt() failed: %x",
|
||||
hr);
|
||||
SDL_Event event;
|
||||
event.type = SDL_RENDER_TARGETS_RESET;
|
||||
SDL_PushEvent(&event);
|
||||
return;
|
||||
}
|
||||
|
||||
// We must try to present to trigger SDL's logic to recover the render target,
|
||||
// even if VideoProcessBlt() fails.
|
||||
SDL_RenderPresent(m_SdlRenderer);
|
||||
hr = m_Device->PresentEx(nullptr, nullptr, nullptr, nullptr, 0);
|
||||
if (FAILED(hr)) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"PresentEx() failed: %x",
|
||||
hr);
|
||||
SDL_Event event;
|
||||
event.type = SDL_RENDER_TARGETS_RESET;
|
||||
SDL_PushEvent(&event);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "renderer.h"
|
||||
#include "pacer/pacer.h"
|
||||
|
||||
#include <d3d9.h>
|
||||
#include <dxva2api.h>
|
||||
@@ -17,13 +18,19 @@ public:
|
||||
virtual bool initialize(SDL_Window* window,
|
||||
int videoFormat,
|
||||
int width,
|
||||
int height);
|
||||
int height,
|
||||
int maxFps,
|
||||
bool enableVsync);
|
||||
virtual bool prepareDecoderContext(AVCodecContext* context);
|
||||
virtual void renderFrame(AVFrame* frame);
|
||||
virtual void renderFrameAtVsync(AVFrame* frame);
|
||||
virtual bool needsTestFrame();
|
||||
virtual int getDecoderCapabilities();
|
||||
virtual VSyncConstraint getVsyncConstraint();
|
||||
|
||||
private:
|
||||
bool initializeDecoder();
|
||||
bool initializeRenderer();
|
||||
bool initializeDevice(SDL_Window* window, bool enableVsync);
|
||||
bool isDecoderBlacklisted();
|
||||
|
||||
static
|
||||
@@ -42,8 +49,6 @@ private:
|
||||
int m_DisplayWidth;
|
||||
int m_DisplayHeight;
|
||||
|
||||
SDL_Renderer* m_SdlRenderer;
|
||||
|
||||
struct dxva_context m_DXVAContext;
|
||||
IDirect3DSurface9* m_DecSurfaces[19];
|
||||
DXVA2_ConfigPictureDecode m_Config;
|
||||
@@ -52,7 +57,7 @@ private:
|
||||
int m_SurfacesUsed;
|
||||
AVBufferPool* m_Pool;
|
||||
|
||||
IDirect3DDevice9* m_Device;
|
||||
IDirect3DDevice9Ex* m_Device;
|
||||
IDirect3DSurface9* m_RenderTarget;
|
||||
IDirectXVideoProcessorService* m_ProcService;
|
||||
IDirectXVideoProcessor* m_Processor;
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include "pacer.h"
|
||||
|
||||
class DisplayLinkVsyncSourceFactory
|
||||
{
|
||||
public:
|
||||
static
|
||||
IVsyncSource* createVsyncSource(Pacer* pacer);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
#include "displaylinkvsyncsource.h"
|
||||
|
||||
#include <SDL_syswm.h>
|
||||
|
||||
#include <CoreVideo/CoreVideo.h>
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
class DisplayLinkVsyncSource : public IVsyncSource
|
||||
{
|
||||
public:
|
||||
DisplayLinkVsyncSource(Pacer* pacer)
|
||||
: m_Pacer(pacer),
|
||||
m_DisplayLink(nullptr)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
virtual ~DisplayLinkVsyncSource() override
|
||||
{
|
||||
if (m_DisplayLink != nullptr) {
|
||||
CVDisplayLinkStop(m_DisplayLink);
|
||||
CVDisplayLinkRelease(m_DisplayLink);
|
||||
}
|
||||
}
|
||||
|
||||
static
|
||||
CGDirectDisplayID
|
||||
getDisplayID(NSScreen* screen)
|
||||
{
|
||||
NSNumber* screenNumber = [screen deviceDescription][@"NSScreenNumber"];
|
||||
return [screenNumber unsignedIntValue];
|
||||
}
|
||||
|
||||
static
|
||||
CVReturn
|
||||
displayLinkOutputCallback(
|
||||
CVDisplayLinkRef displayLink,
|
||||
const CVTimeStamp* /* now */,
|
||||
const CVTimeStamp* /* vsyncTime */,
|
||||
CVOptionFlags,
|
||||
CVOptionFlags*,
|
||||
void *displayLinkContext)
|
||||
{
|
||||
auto me = reinterpret_cast<DisplayLinkVsyncSource*>(displayLinkContext);
|
||||
|
||||
SDL_assert(displayLink == me->m_DisplayLink);
|
||||
|
||||
// In my testing on macOS 10.13, this callback is invoked about 24 ms
|
||||
// prior to the specified v-sync time (now - vsyncTime). Since this is
|
||||
// greater than the standard v-sync interval (16 ms = 60 FPS), we will
|
||||
// draw using the current host time, rather than the actual v-sync target
|
||||
// time. Because the CVDisplayLink is in sync with the actual v-sync
|
||||
// interval, even if many ms prior, we can safely use the current host time
|
||||
// and get a consistent callback for each v-sync. This reduces video latency
|
||||
// by at least 1 frame vs. rendering with the actual vsyncTime.
|
||||
me->m_Pacer->vsyncCallback(500 / me->m_DisplayFps);
|
||||
|
||||
return kCVReturnSuccess;
|
||||
}
|
||||
|
||||
virtual bool initialize(SDL_Window* window, int displayFps) override
|
||||
{
|
||||
SDL_SysWMinfo info;
|
||||
|
||||
SDL_VERSION(&info.version);
|
||||
|
||||
if (!SDL_GetWindowWMInfo(window, &info)) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"SDL_GetWindowWMInfo() failed: %s",
|
||||
SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
|
||||
SDL_assert(info.subsystem == SDL_SYSWM_COCOA);
|
||||
|
||||
m_DisplayFps = displayFps;
|
||||
|
||||
NSScreen* screen = [info.info.cocoa.window screen];
|
||||
CVReturn status;
|
||||
if (screen == nullptr) {
|
||||
// Window not visible on any display, so use a
|
||||
// CVDisplayLink that can work with all active displays.
|
||||
// When we become visible, we'll recreate ourselves
|
||||
// and associate with the new screen.
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"NSWindow is not visible on any display");
|
||||
status = CVDisplayLinkCreateWithActiveCGDisplays(&m_DisplayLink);
|
||||
}
|
||||
else {
|
||||
CGDirectDisplayID displayId;
|
||||
displayId = getDisplayID(screen);
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"NSWindow on display: %x",
|
||||
displayId);
|
||||
status = CVDisplayLinkCreateWithCGDisplay(displayId, &m_DisplayLink);
|
||||
}
|
||||
if (status != kCVReturnSuccess) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Failed to create CVDisplayLink: %d",
|
||||
status);
|
||||
return false;
|
||||
}
|
||||
|
||||
status = CVDisplayLinkSetOutputCallback(m_DisplayLink, displayLinkOutputCallback, this);
|
||||
if (status != kCVReturnSuccess) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"CVDisplayLinkSetOutputCallback() failed: %d",
|
||||
status);
|
||||
return false;
|
||||
}
|
||||
|
||||
status = CVDisplayLinkStart(m_DisplayLink);
|
||||
if (status != kCVReturnSuccess) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"CVDisplayLinkStart() failed: %d",
|
||||
status);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
Pacer* m_Pacer;
|
||||
CVDisplayLinkRef m_DisplayLink;
|
||||
int m_DisplayFps;
|
||||
};
|
||||
|
||||
IVsyncSource* DisplayLinkVsyncSourceFactory::createVsyncSource(Pacer* pacer) {
|
||||
return new DisplayLinkVsyncSource(pacer);
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
#include "dxvsyncsource.h"
|
||||
|
||||
// Useful references:
|
||||
// https://bugs.chromium.org/p/chromium/issues/detail?id=467617
|
||||
// https://chromium.googlesource.com/chromium/src.git/+/c564f2fe339b2b2abb0c8773c90c83215670ea71/gpu/ipc/service/gpu_vsync_provider_win.cc
|
||||
|
||||
DxVsyncSource::DxVsyncSource(Pacer* pacer) :
|
||||
m_Pacer(pacer),
|
||||
m_Thread(nullptr),
|
||||
m_Gdi32Handle(nullptr)
|
||||
{
|
||||
SDL_AtomicSet(&m_Stopping, 0);
|
||||
}
|
||||
|
||||
DxVsyncSource::~DxVsyncSource()
|
||||
{
|
||||
if (m_Thread != nullptr) {
|
||||
SDL_AtomicSet(&m_Stopping, 1);
|
||||
SDL_WaitThread(m_Thread, nullptr);
|
||||
}
|
||||
|
||||
if (m_Gdi32Handle != nullptr) {
|
||||
FreeLibrary(m_Gdi32Handle);
|
||||
}
|
||||
}
|
||||
|
||||
bool DxVsyncSource::initialize(SDL_Window* window, int displayFps)
|
||||
{
|
||||
m_DisplayFps = displayFps;
|
||||
|
||||
m_Gdi32Handle = LoadLibraryA("gdi32.dll");
|
||||
if (m_Gdi32Handle == nullptr) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Failed to load gdi32.dll: %d",
|
||||
GetLastError());
|
||||
return false;
|
||||
}
|
||||
|
||||
m_D3DKMTOpenAdapterFromHdc = (PFND3DKMTOPENADAPTERFROMHDC)GetProcAddress(m_Gdi32Handle, "D3DKMTOpenAdapterFromHdc");
|
||||
m_D3DKMTCloseAdapter = (PFND3DKMTCLOSEADAPTER)GetProcAddress(m_Gdi32Handle, "D3DKMTCloseAdapter");
|
||||
m_D3DKMTWaitForVerticalBlankEvent = (PFND3DKMTWAITFORVERTICALBLANKEVENT)GetProcAddress(m_Gdi32Handle, "D3DKMTWaitForVerticalBlankEvent");
|
||||
|
||||
if (m_D3DKMTOpenAdapterFromHdc == nullptr ||
|
||||
m_D3DKMTCloseAdapter == nullptr ||
|
||||
m_D3DKMTWaitForVerticalBlankEvent == nullptr) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Missing required function in gdi32.dll");
|
||||
return false;
|
||||
}
|
||||
|
||||
SDL_SysWMinfo info;
|
||||
|
||||
SDL_VERSION(&info.version);
|
||||
|
||||
if (!SDL_GetWindowWMInfo(window, &info)) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"SDL_GetWindowWMInfo() failed: %s",
|
||||
SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
|
||||
m_Window = info.info.win.window;
|
||||
|
||||
m_Thread = SDL_CreateThread(vsyncThread, "DX Vsync Thread", this);
|
||||
if (m_Thread == nullptr) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Unable to create DX V-sync thread: %s",
|
||||
SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
int DxVsyncSource::vsyncThread(void* context)
|
||||
{
|
||||
DxVsyncSource* me = reinterpret_cast<DxVsyncSource*>(context);
|
||||
|
||||
SDL_SetThreadPriority(SDL_THREAD_PRIORITY_HIGH);
|
||||
|
||||
D3DKMT_OPENADAPTERFROMHDC openAdapterParams = {};
|
||||
HMONITOR lastMonitor = nullptr;
|
||||
DEVMODEA monitorMode;
|
||||
monitorMode.dmSize = sizeof(monitorMode);
|
||||
|
||||
while (SDL_AtomicGet(&me->m_Stopping) == 0) {
|
||||
D3DKMT_WAITFORVERTICALBLANKEVENT waitForVblankEventParams;
|
||||
NTSTATUS status;
|
||||
|
||||
// If the monitor has changed from last time, open the new adapter
|
||||
HMONITOR currentMonitor = MonitorFromWindow(me->m_Window, MONITOR_DEFAULTTONEAREST);
|
||||
if (currentMonitor != lastMonitor) {
|
||||
MONITORINFOEXA monitorInfo = {};
|
||||
monitorInfo.cbSize = sizeof(monitorInfo);
|
||||
if (!GetMonitorInfoA(currentMonitor, &monitorInfo)) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"GetMonitorInfo() failed: %d",
|
||||
GetLastError());
|
||||
SDL_Delay(10);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!EnumDisplaySettingsA(monitorInfo.szDevice, ENUM_CURRENT_SETTINGS, &monitorMode)) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"EnumDisplaySettings() failed: %d",
|
||||
GetLastError());
|
||||
SDL_Delay(10);
|
||||
continue;
|
||||
}
|
||||
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Monitor changed: %s %d Hz",
|
||||
monitorInfo.szDevice,
|
||||
monitorMode.dmDisplayFrequency);
|
||||
|
||||
if (openAdapterParams.hAdapter != 0) {
|
||||
D3DKMT_CLOSEADAPTER closeAdapterParams = {};
|
||||
closeAdapterParams.hAdapter = openAdapterParams.hAdapter;
|
||||
me->m_D3DKMTCloseAdapter(&closeAdapterParams);
|
||||
}
|
||||
|
||||
openAdapterParams.hDc = CreateDCA(nullptr, monitorInfo.szDevice, nullptr, nullptr);
|
||||
if (!openAdapterParams.hDc) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"CreateDC() failed: %d",
|
||||
GetLastError());
|
||||
SDL_Delay(10);
|
||||
continue;
|
||||
}
|
||||
|
||||
status = me->m_D3DKMTOpenAdapterFromHdc(&openAdapterParams);
|
||||
DeleteDC(openAdapterParams.hDc);
|
||||
|
||||
if (status != STATUS_SUCCESS) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"D3DKMTOpenAdapterFromHdc() failed: %x",
|
||||
status);
|
||||
SDL_Delay(10);
|
||||
continue;
|
||||
}
|
||||
|
||||
lastMonitor = currentMonitor;
|
||||
}
|
||||
|
||||
waitForVblankEventParams.hAdapter = openAdapterParams.hAdapter;
|
||||
waitForVblankEventParams.hDevice = 0;
|
||||
waitForVblankEventParams.VidPnSourceId = openAdapterParams.VidPnSourceId;
|
||||
|
||||
status = me->m_D3DKMTWaitForVerticalBlankEvent(&waitForVblankEventParams);
|
||||
if (status != STATUS_SUCCESS) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"D3DKMTWaitForVerticalBlankEvent() failed: %x",
|
||||
status);
|
||||
SDL_Delay(10);
|
||||
continue;
|
||||
}
|
||||
|
||||
me->m_Pacer->vsyncCallback(1000 / me->m_DisplayFps);
|
||||
}
|
||||
|
||||
if (openAdapterParams.hAdapter != 0) {
|
||||
D3DKMT_CLOSEADAPTER closeAdapterParams = {};
|
||||
closeAdapterParams.hAdapter = openAdapterParams.hAdapter;
|
||||
me->m_D3DKMTCloseAdapter(&closeAdapterParams);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
#pragma once
|
||||
|
||||
#include "pacer.h"
|
||||
|
||||
#include <SDL_syswm.h>
|
||||
|
||||
// from <D3dkmthk.h>
|
||||
typedef LONG NTSTATUS;
|
||||
typedef UINT D3DKMT_HANDLE;
|
||||
typedef UINT D3DDDI_VIDEO_PRESENT_SOURCE_ID;
|
||||
#define STATUS_SUCCESS ((NTSTATUS)0x00000000L)
|
||||
#define STATUS_GRAPHICS_PRESENT_OCCLUDED ((NTSTATUS)0xC01E0006L)
|
||||
typedef struct _D3DKMT_OPENADAPTERFROMHDC {
|
||||
HDC hDc;
|
||||
D3DKMT_HANDLE hAdapter;
|
||||
LUID AdapterLuid;
|
||||
D3DDDI_VIDEO_PRESENT_SOURCE_ID VidPnSourceId;
|
||||
} D3DKMT_OPENADAPTERFROMHDC;
|
||||
typedef struct _D3DKMT_CLOSEADAPTER {
|
||||
D3DKMT_HANDLE hAdapter;
|
||||
} D3DKMT_CLOSEADAPTER;
|
||||
typedef struct _D3DKMT_WAITFORVERTICALBLANKEVENT {
|
||||
D3DKMT_HANDLE hAdapter;
|
||||
D3DKMT_HANDLE hDevice;
|
||||
D3DDDI_VIDEO_PRESENT_SOURCE_ID VidPnSourceId;
|
||||
} D3DKMT_WAITFORVERTICALBLANKEVENT;
|
||||
typedef NTSTATUS(APIENTRY* PFND3DKMTOPENADAPTERFROMHDC)(D3DKMT_OPENADAPTERFROMHDC*);
|
||||
typedef NTSTATUS(APIENTRY* PFND3DKMTCLOSEADAPTER)(D3DKMT_CLOSEADAPTER*);
|
||||
typedef NTSTATUS(APIENTRY* PFND3DKMTWAITFORVERTICALBLANKEVENT)(D3DKMT_WAITFORVERTICALBLANKEVENT*);
|
||||
|
||||
class DxVsyncSource : public IVsyncSource
|
||||
{
|
||||
public:
|
||||
DxVsyncSource(Pacer* pacer);
|
||||
|
||||
virtual ~DxVsyncSource();
|
||||
|
||||
virtual bool initialize(SDL_Window* window, int displayFps);
|
||||
|
||||
private:
|
||||
static int vsyncThread(void* context);
|
||||
|
||||
Pacer* m_Pacer;
|
||||
SDL_Thread* m_Thread;
|
||||
SDL_atomic_t m_Stopping;
|
||||
HMODULE m_Gdi32Handle;
|
||||
HWND m_Window;
|
||||
int m_DisplayFps;
|
||||
|
||||
PFND3DKMTOPENADAPTERFROMHDC m_D3DKMTOpenAdapterFromHdc;
|
||||
PFND3DKMTCLOSEADAPTER m_D3DKMTCloseAdapter;
|
||||
PFND3DKMTWAITFORVERTICALBLANKEVENT m_D3DKMTWaitForVerticalBlankEvent;
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
#include "nullthreadedvsyncsource.h"
|
||||
|
||||
NullThreadedVsyncSource::NullThreadedVsyncSource(Pacer* pacer) :
|
||||
m_Pacer(pacer),
|
||||
m_Thread(nullptr)
|
||||
{
|
||||
SDL_AtomicSet(&m_Stopping, 0);
|
||||
}
|
||||
|
||||
NullThreadedVsyncSource::~NullThreadedVsyncSource()
|
||||
{
|
||||
if (m_Thread != nullptr) {
|
||||
SDL_AtomicSet(&m_Stopping, 1);
|
||||
SDL_WaitThread(m_Thread, nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
bool NullThreadedVsyncSource::initialize(SDL_Window*, int displayFps)
|
||||
{
|
||||
m_DisplayFps = displayFps;
|
||||
m_Thread = SDL_CreateThread(vsyncThread, "Null Vsync Thread", this);
|
||||
if (m_Thread == nullptr) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Unable to create DX V-sync thread: %s",
|
||||
SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
int NullThreadedVsyncSource::vsyncThread(void* context)
|
||||
{
|
||||
NullThreadedVsyncSource* me = reinterpret_cast<NullThreadedVsyncSource*>(context);
|
||||
|
||||
SDL_SetThreadPriority(SDL_THREAD_PRIORITY_HIGH);
|
||||
|
||||
while (SDL_AtomicGet(&me->m_Stopping) == 0) {
|
||||
me->m_Pacer->vsyncCallback(1000 / me->m_DisplayFps);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
#pragma once
|
||||
|
||||
#include "pacer.h"
|
||||
|
||||
class NullThreadedVsyncSource : public IVsyncSource
|
||||
{
|
||||
public:
|
||||
NullThreadedVsyncSource(Pacer* pacer);
|
||||
|
||||
virtual ~NullThreadedVsyncSource();
|
||||
|
||||
virtual bool initialize(SDL_Window* window, int displayFps);
|
||||
|
||||
private:
|
||||
static int vsyncThread(void* context);
|
||||
|
||||
Pacer* m_Pacer;
|
||||
SDL_Thread* m_Thread;
|
||||
SDL_atomic_t m_Stopping;
|
||||
int m_DisplayFps;
|
||||
};
|
||||
@@ -0,0 +1,183 @@
|
||||
#include "pacer.h"
|
||||
#include "streaming/streamutils.h"
|
||||
|
||||
#include "nullthreadedvsyncsource.h"
|
||||
|
||||
#ifdef Q_OS_DARWIN
|
||||
#include "displaylinkvsyncsource.h"
|
||||
#endif
|
||||
|
||||
#ifdef Q_OS_WIN32
|
||||
#include "dxvsyncsource.h"
|
||||
#endif
|
||||
|
||||
#define FRAME_HISTORY_ENTRIES 8
|
||||
|
||||
// We may be woken up slightly late so don't go all the way
|
||||
// up to the next V-sync since we may accidentally step into
|
||||
// the next V-sync period. It also takes some amount of time
|
||||
// to do the render itself, so we can't render right before
|
||||
// V-sync happens.
|
||||
#define TIMER_SLACK_MS 3
|
||||
|
||||
Pacer::Pacer(IFFmpegRenderer* renderer, PVIDEO_STATS videoStats) :
|
||||
m_FrameQueueLock(0),
|
||||
m_VsyncSource(nullptr),
|
||||
m_VsyncRenderer(renderer),
|
||||
m_MaxVideoFps(0),
|
||||
m_DisplayFps(0),
|
||||
m_VideoStats(videoStats)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
Pacer::~Pacer()
|
||||
{
|
||||
// Stop V-sync callbacks
|
||||
delete m_VsyncSource;
|
||||
m_VsyncSource = nullptr;
|
||||
|
||||
while (!m_FrameQueue.isEmpty()) {
|
||||
AVFrame* frame = m_FrameQueue.dequeue();
|
||||
av_frame_free(&frame);
|
||||
}
|
||||
}
|
||||
|
||||
// Called in an arbitrary thread by the IVsyncSource on V-sync
|
||||
// or an event synchronized with V-sync
|
||||
void Pacer::vsyncCallback(int timeUntilNextVsyncMillis)
|
||||
{
|
||||
// Make sure initialize() has been called
|
||||
SDL_assert(m_MaxVideoFps != 0);
|
||||
|
||||
SDL_assert(timeUntilNextVsyncMillis >= TIMER_SLACK_MS);
|
||||
|
||||
Uint32 vsyncCallbackStartTime = SDL_GetTicks();
|
||||
|
||||
SDL_AtomicLock(&m_FrameQueueLock);
|
||||
|
||||
// If the queue length history entries are large, be strict
|
||||
// about dropping excess frames.
|
||||
int frameDropTarget = 1;
|
||||
|
||||
// If we may get more frames per second than we can display, use
|
||||
// frame history to drop frames only if consistently above the
|
||||
// one queued frame mark.
|
||||
if (m_MaxVideoFps >= m_DisplayFps) {
|
||||
for (int i = 0; i < m_FrameQueueHistory.count(); i++) {
|
||||
if (m_FrameQueueHistory[i] <= 1) {
|
||||
// Be lenient as long as the queue length
|
||||
// resolves before the end of frame history
|
||||
frameDropTarget = 3;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (m_FrameQueueHistory.count() == FRAME_HISTORY_ENTRIES) {
|
||||
m_FrameQueueHistory.dequeue();
|
||||
}
|
||||
|
||||
m_FrameQueueHistory.enqueue(m_FrameQueue.count());
|
||||
}
|
||||
|
||||
// Catch up if we're several frames ahead
|
||||
while (m_FrameQueue.count() > frameDropTarget) {
|
||||
AVFrame* frame = m_FrameQueue.dequeue();
|
||||
m_VideoStats->pacerDroppedFrames++;
|
||||
av_frame_free(&frame);
|
||||
}
|
||||
|
||||
|
||||
if (m_FrameQueue.isEmpty()) {
|
||||
SDL_AtomicUnlock(&m_FrameQueueLock);
|
||||
|
||||
while (!SDL_TICKS_PASSED(SDL_GetTicks(),
|
||||
vsyncCallbackStartTime + timeUntilNextVsyncMillis - TIMER_SLACK_MS)) {
|
||||
SDL_Delay(1);
|
||||
|
||||
SDL_AtomicLock(&m_FrameQueueLock);
|
||||
if (!m_FrameQueue.isEmpty()) {
|
||||
// Don't release the lock
|
||||
goto RenderNextFrame;
|
||||
}
|
||||
SDL_AtomicUnlock(&m_FrameQueueLock);
|
||||
}
|
||||
|
||||
// Nothing to render at this time
|
||||
return;
|
||||
}
|
||||
|
||||
RenderNextFrame:
|
||||
// Grab the first frame
|
||||
AVFrame* frame = m_FrameQueue.dequeue();
|
||||
SDL_AtomicUnlock(&m_FrameQueueLock);
|
||||
|
||||
// Render it
|
||||
Uint32 beforeRender = SDL_GetTicks();
|
||||
m_VsyncRenderer->renderFrameAtVsync(frame);
|
||||
m_VideoStats->totalRenderTime += SDL_GetTicks() - beforeRender;
|
||||
m_VideoStats->renderedFrames++;
|
||||
|
||||
// Free the frame
|
||||
av_frame_free(&frame);
|
||||
}
|
||||
|
||||
bool Pacer::initialize(SDL_Window* window, int maxVideoFps, bool enableVsync)
|
||||
{
|
||||
m_MaxVideoFps = maxVideoFps;
|
||||
m_EnableVsync = enableVsync;
|
||||
m_DisplayFps = StreamUtils::getDisplayRefreshRate(window);
|
||||
|
||||
if (m_EnableVsync) {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Frame pacing in tear-free mode: target %d Hz with %d FPS stream",
|
||||
m_DisplayFps, m_MaxVideoFps);
|
||||
|
||||
#if defined(Q_OS_DARWIN)
|
||||
m_VsyncSource = DisplayLinkVsyncSourceFactory::createVsyncSource(this);
|
||||
#elif defined(Q_OS_WIN32)
|
||||
m_VsyncSource = new DxVsyncSource(this);
|
||||
#else
|
||||
// Platforms without a VsyncSource will just render frames
|
||||
// immediately like they used to.
|
||||
#endif
|
||||
|
||||
if (m_VsyncSource != nullptr && !m_VsyncSource->initialize(window, m_DisplayFps)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Minimal latency tearing mode: target %d Hz with %d FPS stream",
|
||||
m_DisplayFps, m_MaxVideoFps);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void Pacer::submitFrame(AVFrame* frame)
|
||||
{
|
||||
// Make sure initialize() has been called
|
||||
SDL_assert(m_MaxVideoFps != 0);
|
||||
|
||||
// Queue the frame until the V-sync callback if
|
||||
// we have a V-sync source, otherwise deliver it
|
||||
// immediately and hope for the best.
|
||||
if (isUsingFrameQueue()) {
|
||||
SDL_AtomicLock(&m_FrameQueueLock);
|
||||
m_FrameQueue.enqueue(frame);
|
||||
SDL_AtomicUnlock(&m_FrameQueueLock);
|
||||
}
|
||||
else {
|
||||
Uint32 beforeRender = SDL_GetTicks();
|
||||
m_VsyncRenderer->renderFrameAtVsync(frame);
|
||||
m_VideoStats->totalRenderTime += SDL_GetTicks() - beforeRender;
|
||||
m_VideoStats->renderedFrames++;
|
||||
av_frame_free(&frame);
|
||||
}
|
||||
}
|
||||
|
||||
bool Pacer::isUsingFrameQueue()
|
||||
{
|
||||
return m_VsyncSource != nullptr;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
|
||||
#include "../../decoder.h"
|
||||
#include "../renderer.h"
|
||||
|
||||
#include <QQueue>
|
||||
|
||||
class IVsyncSource {
|
||||
public:
|
||||
virtual ~IVsyncSource() {}
|
||||
virtual bool initialize(SDL_Window* window, int displayFps) = 0;
|
||||
};
|
||||
|
||||
class Pacer
|
||||
{
|
||||
public:
|
||||
Pacer(IFFmpegRenderer* renderer, PVIDEO_STATS videoStats);
|
||||
|
||||
~Pacer();
|
||||
|
||||
void submitFrame(AVFrame* frame);
|
||||
|
||||
bool initialize(SDL_Window* window, int maxVideoFps, bool enableVsync);
|
||||
|
||||
void vsyncCallback(int timeUntilNextVsyncMillis);
|
||||
|
||||
bool isUsingFrameQueue();
|
||||
|
||||
private:
|
||||
QQueue<AVFrame*> m_FrameQueue;
|
||||
QQueue<int> m_FrameQueueHistory;
|
||||
SDL_SpinLock m_FrameQueueLock;
|
||||
|
||||
IVsyncSource* m_VsyncSource;
|
||||
IFFmpegRenderer* m_VsyncRenderer;
|
||||
int m_MaxVideoFps;
|
||||
int m_DisplayFps;
|
||||
bool m_EnableVsync;
|
||||
PVIDEO_STATS m_VideoStats;
|
||||
};
|
||||
@@ -8,13 +8,24 @@ extern "C" {
|
||||
|
||||
class IFFmpegRenderer {
|
||||
public:
|
||||
enum VSyncConstraint {
|
||||
VSYNC_FORCE_OFF,
|
||||
VSYNC_FORCE_ON,
|
||||
VSYNC_ANY
|
||||
};
|
||||
|
||||
virtual ~IFFmpegRenderer() {}
|
||||
virtual bool initialize(SDL_Window* window,
|
||||
int videoFormat,
|
||||
int width,
|
||||
int height) = 0;
|
||||
int height,
|
||||
int maxFps,
|
||||
bool enableVsync) = 0;
|
||||
virtual bool prepareDecoderContext(AVCodecContext* context) = 0;
|
||||
virtual void renderFrame(AVFrame* frame) = 0;
|
||||
virtual void renderFrameAtVsync(AVFrame* frame) = 0;
|
||||
virtual bool needsTestFrame() = 0;
|
||||
virtual int getDecoderCapabilities() = 0;
|
||||
virtual VSyncConstraint getVsyncConstraint() = 0;
|
||||
};
|
||||
|
||||
class SdlRenderer : public IFFmpegRenderer {
|
||||
@@ -24,9 +35,14 @@ public:
|
||||
virtual bool initialize(SDL_Window* window,
|
||||
int videoFormat,
|
||||
int width,
|
||||
int height);
|
||||
int height,
|
||||
int maxFps,
|
||||
bool enableVsync);
|
||||
virtual bool prepareDecoderContext(AVCodecContext* context);
|
||||
virtual void renderFrame(AVFrame* frame);
|
||||
virtual void renderFrameAtVsync(AVFrame* frame);
|
||||
virtual bool needsTestFrame();
|
||||
virtual int getDecoderCapabilities();
|
||||
virtual VSyncConstraint getVsyncConstraint();
|
||||
|
||||
private:
|
||||
SDL_Renderer* m_Renderer;
|
||||
|
||||
+34
-6
@@ -30,12 +30,43 @@ bool SdlRenderer::prepareDecoderContext(AVCodecContext*)
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SdlRenderer::needsTestFrame()
|
||||
{
|
||||
// This renderer should always work
|
||||
return false;
|
||||
}
|
||||
|
||||
int SdlRenderer::getDecoderCapabilities()
|
||||
{
|
||||
// The FFmpeg CPU decoder can handle reference frame invalidation,
|
||||
// but only for H.264.
|
||||
return CAPABILITY_REFERENCE_FRAME_INVALIDATION_AVC;
|
||||
}
|
||||
|
||||
IFFmpegRenderer::VSyncConstraint SdlRenderer::getVsyncConstraint()
|
||||
{
|
||||
return VSYNC_ANY;
|
||||
}
|
||||
|
||||
bool SdlRenderer::initialize(SDL_Window* window,
|
||||
int,
|
||||
int width,
|
||||
int height)
|
||||
int height,
|
||||
int,
|
||||
bool enableVsync)
|
||||
{
|
||||
m_Renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);
|
||||
Uint32 rendererFlags = SDL_RENDERER_ACCELERATED;
|
||||
|
||||
if ((SDL_GetWindowFlags(window) & SDL_WINDOW_FULLSCREEN_DESKTOP) == SDL_WINDOW_FULLSCREEN) {
|
||||
// In full-screen exclusive mode, we enable V-sync if requested. For other modes, Windows and Mac
|
||||
// have compositors that make rendering tear-free. Linux compositor varies by distro and user
|
||||
// configuration but doesn't seem feasible to detect here.
|
||||
if (enableVsync) {
|
||||
rendererFlags |= SDL_RENDERER_PRESENTVSYNC;
|
||||
}
|
||||
}
|
||||
|
||||
m_Renderer = SDL_CreateRenderer(window, -1, rendererFlags);
|
||||
if (!m_Renderer) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"SDL_CreateRenderer() failed: %s",
|
||||
@@ -67,7 +98,7 @@ bool SdlRenderer::initialize(SDL_Window* window,
|
||||
return true;
|
||||
}
|
||||
|
||||
void SdlRenderer::renderFrame(AVFrame* frame)
|
||||
void SdlRenderer::renderFrameAtVsync(AVFrame* frame)
|
||||
{
|
||||
SDL_UpdateYUVTexture(m_Texture, nullptr,
|
||||
frame->data[0],
|
||||
@@ -77,9 +108,6 @@ void SdlRenderer::renderFrame(AVFrame* frame)
|
||||
frame->data[2],
|
||||
frame->linesize[2]);
|
||||
|
||||
// Done with the frame now
|
||||
av_frame_free(&frame);
|
||||
|
||||
SDL_RenderClear(m_Renderer);
|
||||
SDL_RenderCopy(m_Renderer, m_Texture, nullptr, nullptr);
|
||||
SDL_RenderPresent(m_Renderer);
|
||||
@@ -1,3 +1,5 @@
|
||||
#include <QString>
|
||||
|
||||
#include "vaapi.h"
|
||||
#include <streaming/streamutils.h>
|
||||
|
||||
@@ -27,7 +29,7 @@ VAAPIRenderer::~VAAPIRenderer()
|
||||
}
|
||||
|
||||
bool
|
||||
VAAPIRenderer::initialize(SDL_Window* window, int, int width, int height)
|
||||
VAAPIRenderer::initialize(SDL_Window* window, int, int width, int height, int, bool)
|
||||
{
|
||||
int err;
|
||||
SDL_SysWMinfo info;
|
||||
@@ -109,6 +111,26 @@ VAAPIRenderer::initialize(SDL_Window* window, int, int width, int height)
|
||||
"Initialized VAAPI %d.%d",
|
||||
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
|
||||
err = av_hwdevice_ctx_init(m_HwContext);
|
||||
if (err < 0) {
|
||||
@@ -141,8 +163,27 @@ VAAPIRenderer::prepareDecoderContext(AVCodecContext* context)
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
VAAPIRenderer::needsTestFrame()
|
||||
{
|
||||
// We need a test frame to see if this VAAPI driver
|
||||
// supports the profile used for streaming
|
||||
return true;
|
||||
}
|
||||
|
||||
int
|
||||
VAAPIRenderer::getDecoderCapabilities()
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
IFFmpegRenderer::VSyncConstraint VAAPIRenderer::getVsyncConstraint()
|
||||
{
|
||||
return VSYNC_ANY;
|
||||
}
|
||||
|
||||
void
|
||||
VAAPIRenderer::renderFrame(AVFrame* frame)
|
||||
VAAPIRenderer::renderFrameAtVsync(AVFrame* frame)
|
||||
{
|
||||
VASurfaceID surface = (VASurfaceID)(uintptr_t)frame->data[3];
|
||||
AVHWDeviceContext* deviceContext = (AVHWDeviceContext*)m_HwContext->data;
|
||||
@@ -197,6 +238,4 @@ VAAPIRenderer::renderFrame(AVFrame* frame)
|
||||
// We don't accept anything else in initialize().
|
||||
SDL_assert(false);
|
||||
}
|
||||
|
||||
av_frame_free(&frame);
|
||||
}
|
||||
|
||||
@@ -33,9 +33,14 @@ public:
|
||||
virtual bool initialize(SDL_Window* window,
|
||||
int videoFormat,
|
||||
int width,
|
||||
int height);
|
||||
int height,
|
||||
int maxFps,
|
||||
bool enableVsync);
|
||||
virtual bool prepareDecoderContext(AVCodecContext* context);
|
||||
virtual void renderFrame(AVFrame* frame);
|
||||
virtual void renderFrameAtVsync(AVFrame* frame);
|
||||
virtual bool needsTestFrame();
|
||||
virtual int getDecoderCapabilities();
|
||||
virtual VSyncConstraint getVsyncConstraint();
|
||||
|
||||
private:
|
||||
int m_WindowSystem;
|
||||
|
||||
@@ -54,7 +54,7 @@ VDPAURenderer::~VDPAURenderer()
|
||||
}
|
||||
}
|
||||
|
||||
bool VDPAURenderer::initialize(SDL_Window* window, int, int width, int height)
|
||||
bool VDPAURenderer::initialize(SDL_Window* window, int, int width, int height, int, bool)
|
||||
{
|
||||
int err;
|
||||
VdpStatus status;
|
||||
@@ -91,6 +91,7 @@ bool VDPAURenderer::initialize(SDL_Window* window, int, int width, int height)
|
||||
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_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);
|
||||
|
||||
@@ -130,6 +131,13 @@ bool VDPAURenderer::initialize(SDL_Window* window, int, int width, int height)
|
||||
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
|
||||
bool foundFormat = false;
|
||||
for (int i = 0; i < OUTPUT_SURFACE_FORMAT_COUNT; i++) {
|
||||
@@ -217,13 +225,33 @@ bool VDPAURenderer::prepareDecoderContext(AVCodecContext* context)
|
||||
// https://github.com/FFmpeg/FFmpeg/commit/64ecb78b7179cab2dbdf835463104679dbb7c895
|
||||
context->hwaccel_flags |= AV_HWACCEL_FLAG_ALLOW_PROFILE_MISMATCH;
|
||||
|
||||
// This flag is recommended due to hardware underreporting supported levels
|
||||
context->hwaccel_flags |= AV_HWACCEL_FLAG_IGNORE_LEVEL;
|
||||
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Using VDPAU accelerated renderer");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void VDPAURenderer::renderFrame(AVFrame* frame)
|
||||
bool VDPAURenderer::needsTestFrame()
|
||||
{
|
||||
// We need a test frame to see if this VDPAU driver
|
||||
// supports the profile used for streaming
|
||||
return true;
|
||||
}
|
||||
|
||||
int VDPAURenderer::getDecoderCapabilities()
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
IFFmpegRenderer::VSyncConstraint VDPAURenderer::getVsyncConstraint()
|
||||
{
|
||||
return VSYNC_ANY;
|
||||
}
|
||||
|
||||
void VDPAURenderer::renderFrameAtVsync(AVFrame* frame)
|
||||
{
|
||||
VdpStatus status;
|
||||
VdpVideoSurface videoSurface = (VdpVideoSurface)(uintptr_t)frame->data[3];
|
||||
@@ -235,14 +263,14 @@ void VDPAURenderer::renderFrame(AVFrame* frame)
|
||||
// We need to create the mixer on the fly, because we don't know the dimensions
|
||||
// of our video surfaces in advance of decoding
|
||||
if (m_VideoMixer == 0) {
|
||||
VdpChromaType chroma;
|
||||
VdpChromaType videoSurfaceChroma;
|
||||
uint32_t videoSurfaceWidth, videoSurfaceHeight;
|
||||
status = m_VdpVideoSurfaceGetParameters(videoSurface, &chroma, &videoSurfaceWidth, &videoSurfaceHeight);
|
||||
status = m_VdpVideoSurfaceGetParameters(videoSurface, &videoSurfaceChroma,
|
||||
&videoSurfaceWidth, &videoSurfaceHeight);
|
||||
if (status != VDP_STATUS_OK) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"VdpVideoSurfaceGetParameters() failed: %s",
|
||||
m_VdpGetErrorString(status));
|
||||
av_frame_free(&frame);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -250,14 +278,16 @@ void VDPAURenderer::renderFrame(AVFrame* frame)
|
||||
"VDPAU surface size: %dx%d",
|
||||
videoSurfaceWidth, videoSurfaceHeight);
|
||||
|
||||
#define PARAM_COUNT 2
|
||||
#define PARAM_COUNT 3
|
||||
const VdpVideoMixerParameter params[PARAM_COUNT] = {
|
||||
VDP_VIDEO_MIXER_PARAMETER_VIDEO_SURFACE_WIDTH,
|
||||
VDP_VIDEO_MIXER_PARAMETER_VIDEO_SURFACE_HEIGHT,
|
||||
VDP_VIDEO_MIXER_PARAMETER_CHROMA_TYPE,
|
||||
};
|
||||
const void* const paramValues[PARAM_COUNT] = {
|
||||
&videoSurfaceWidth,
|
||||
&videoSurfaceHeight,
|
||||
&videoSurfaceChroma,
|
||||
};
|
||||
|
||||
status = m_VdpVideoMixerCreate(m_Device, 0, nullptr,
|
||||
@@ -267,7 +297,6 @@ void VDPAURenderer::renderFrame(AVFrame* frame)
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"VdpVideoMixerCreate() failed: %s",
|
||||
m_VdpGetErrorString(status));
|
||||
av_frame_free(&frame);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -310,10 +339,6 @@ void VDPAURenderer::renderFrame(AVFrame* frame)
|
||||
nullptr,
|
||||
0,
|
||||
nullptr);
|
||||
|
||||
// The decoder can have this surface back now
|
||||
av_frame_free(&frame);
|
||||
|
||||
if (status != VDP_STATUS_OK) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"VdpVideoMixerRender() failed: %s",
|
||||
|
||||
@@ -16,9 +16,14 @@ public:
|
||||
virtual bool initialize(SDL_Window* window,
|
||||
int videoFormat,
|
||||
int width,
|
||||
int height);
|
||||
int height,
|
||||
int maxFps,
|
||||
bool enableVsync);
|
||||
virtual bool prepareDecoderContext(AVCodecContext* context);
|
||||
virtual void renderFrame(AVFrame* frame);
|
||||
virtual void renderFrameAtVsync(AVFrame* frame);
|
||||
virtual bool needsTestFrame();
|
||||
virtual int getDecoderCapabilities();
|
||||
virtual VSyncConstraint getVsyncConstraint();
|
||||
|
||||
private:
|
||||
uint32_t m_VideoWidth, m_VideoHeight;
|
||||
@@ -51,6 +56,7 @@ private:
|
||||
VdpOutputSurfaceDestroy* m_VdpOutputSurfaceDestroy;
|
||||
VdpOutputSurfaceQueryCapabilities* m_VdpOutputSurfaceQueryCapabilities;
|
||||
VdpVideoSurfaceGetParameters* m_VdpVideoSurfaceGetParameters;
|
||||
VdpGetInformationString* m_VdpGetInformationString;
|
||||
|
||||
// X11 stuff
|
||||
VdpPresentationQueueTargetCreateX11* m_VdpPresentationQueueTargetCreateX11;
|
||||
|
||||
@@ -2,19 +2,16 @@
|
||||
// libavutil both defining AVMediaType
|
||||
#define AVMediaType AVMediaType_FFmpeg
|
||||
#include "vt.h"
|
||||
#include "pacer/pacer.h"
|
||||
#undef AVMediaType
|
||||
|
||||
#include <SDL_syswm.h>
|
||||
#include <Limelight.h>
|
||||
|
||||
#include <QQueue>
|
||||
|
||||
#include <mach/mach_time.h>
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#import <VideoToolbox/VideoToolbox.h>
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
#import <CoreVideo/CoreVideo.h>
|
||||
|
||||
#define FRAME_HISTORY_ENTRIES 8
|
||||
|
||||
class VTRenderer : public IFFmpegRenderer
|
||||
{
|
||||
@@ -23,9 +20,7 @@ public:
|
||||
: m_HwContext(nullptr),
|
||||
m_DisplayLayer(nullptr),
|
||||
m_FormatDesc(nullptr),
|
||||
m_View(nullptr),
|
||||
m_DisplayLink(nullptr),
|
||||
m_FrameQueueLock(0)
|
||||
m_View(nullptr)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -39,63 +34,28 @@ public:
|
||||
CFRelease(m_FormatDesc);
|
||||
}
|
||||
|
||||
if (m_DisplayLink != nullptr) {
|
||||
CVDisplayLinkStop(m_DisplayLink);
|
||||
CVDisplayLinkRelease(m_DisplayLink);
|
||||
}
|
||||
|
||||
while (!m_FrameQueue.isEmpty()) {
|
||||
AVFrame* frame = m_FrameQueue.dequeue();
|
||||
av_frame_free(&frame);
|
||||
}
|
||||
|
||||
if (m_View != nullptr) {
|
||||
[m_View removeFromSuperview];
|
||||
}
|
||||
}
|
||||
|
||||
void drawFrame(uint64_t vsyncTime)
|
||||
// Caller frees frame after we return
|
||||
virtual void renderFrameAtVsync(AVFrame* frame) override
|
||||
{
|
||||
OSStatus status;
|
||||
CVPixelBufferRef pixBuf = reinterpret_cast<CVPixelBufferRef>(frame->data[3]);
|
||||
|
||||
SDL_AtomicLock(&m_FrameQueueLock);
|
||||
if (m_DisplayLayer.status == AVQueuedSampleBufferRenderingStatusFailed) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Resetting failed AVSampleBufferDisplay layer");
|
||||
|
||||
int frameDropTarget;
|
||||
|
||||
// If the queue length history entries are large, be strict
|
||||
// about dropping excess frames.
|
||||
frameDropTarget = 1;
|
||||
for (int i = 0; i < m_FrameQueueHistory.count(); i++) {
|
||||
if (m_FrameQueueHistory[i] <= 1) {
|
||||
// Be lenient as long as the queue length
|
||||
// resolves before the end of frame history
|
||||
frameDropTarget = 3;
|
||||
}
|
||||
}
|
||||
|
||||
if (m_FrameQueueHistory.count() == FRAME_HISTORY_ENTRIES) {
|
||||
m_FrameQueueHistory.dequeue();
|
||||
}
|
||||
|
||||
m_FrameQueueHistory.enqueue(m_FrameQueue.count());
|
||||
|
||||
// Catch up if we're several frames ahead
|
||||
while (m_FrameQueue.count() > frameDropTarget) {
|
||||
AVFrame* frame = m_FrameQueue.dequeue();
|
||||
av_frame_free(&frame);
|
||||
}
|
||||
|
||||
if (m_FrameQueue.isEmpty()) {
|
||||
SDL_AtomicUnlock(&m_FrameQueueLock);
|
||||
// Trigger the main thread to recreate the decoder
|
||||
SDL_Event event;
|
||||
event.type = SDL_RENDER_TARGETS_RESET;
|
||||
SDL_PushEvent(&event);
|
||||
return;
|
||||
}
|
||||
|
||||
// Grab the first frame
|
||||
AVFrame* frame = m_FrameQueue.dequeue();
|
||||
SDL_AtomicUnlock(&m_FrameQueueLock);
|
||||
|
||||
CVPixelBufferRef pixBuf = reinterpret_cast<CVPixelBufferRef>(frame->data[3]);
|
||||
|
||||
// If the format has changed or doesn't exist yet, construct it with the
|
||||
// pixel buffer data
|
||||
if (!m_FormatDesc || !CMVideoFormatDescriptionMatchesImageBuffer(m_FormatDesc, pixBuf)) {
|
||||
@@ -108,7 +68,6 @@ public:
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"CMVideoFormatDescriptionCreateForImageBuffer() failed: %d",
|
||||
status);
|
||||
av_frame_free(&frame);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -117,7 +76,7 @@ public:
|
||||
CMSampleTimingInfo timingInfo = {
|
||||
.duration = kCMTimeInvalid,
|
||||
.decodeTimeStamp = kCMTimeInvalid,
|
||||
.presentationTimeStamp = CMTimeMake(vsyncTime, 1000 * 1000 * 1000)
|
||||
.presentationTimeStamp = CMTimeMake(mach_absolute_time(), 1000 * 1000 * 1000)
|
||||
};
|
||||
|
||||
CMSampleBufferRef sampleBuffer;
|
||||
@@ -130,45 +89,20 @@ public:
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"CMSampleBufferCreateReadyWithImageBuffer() failed: %d",
|
||||
status);
|
||||
av_frame_free(&frame);
|
||||
return;
|
||||
}
|
||||
|
||||
[m_DisplayLayer enqueueSampleBuffer:sampleBuffer];
|
||||
|
||||
CFRelease(sampleBuffer);
|
||||
av_frame_free(&frame);
|
||||
}
|
||||
|
||||
static
|
||||
CVReturn
|
||||
displayLinkOutputCallback(
|
||||
CVDisplayLinkRef,
|
||||
const CVTimeStamp* now,
|
||||
const CVTimeStamp* /* vsyncTime */,
|
||||
CVOptionFlags,
|
||||
CVOptionFlags*,
|
||||
void *displayLinkContext)
|
||||
{
|
||||
VTRenderer* me = reinterpret_cast<VTRenderer*>(displayLinkContext);
|
||||
|
||||
// In my testing on macOS 10.13, this callback is invoked about 24 ms
|
||||
// prior to the specified v-sync time (now - vsyncTime). Since this is
|
||||
// greater than the standard v-sync interval (16 ms = 60 FPS), we will
|
||||
// draw using the current host time, rather than the actual v-sync target
|
||||
// time. Because the CVDisplayLink is in sync with the actual v-sync
|
||||
// interval, even if many ms prior, we can safely use the current host time
|
||||
// and get a consistent callback for each v-sync. This reduces video latency
|
||||
// by at least 1 frame vs. rendering with the actual vsyncTime.
|
||||
me->drawFrame(now->hostTime);
|
||||
|
||||
return kCVReturnSuccess;
|
||||
}
|
||||
|
||||
virtual bool initialize(SDL_Window* window,
|
||||
int videoFormat,
|
||||
int,
|
||||
int) override
|
||||
int,
|
||||
int,
|
||||
bool) override
|
||||
{
|
||||
int err;
|
||||
|
||||
@@ -226,10 +160,20 @@ public:
|
||||
// We need to add a subview for our display layer.
|
||||
NSView* contentView = info.info.cocoa.window.contentView;
|
||||
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,
|
||||
AV_HWDEVICE_TYPE_VIDEOTOOLBOX,
|
||||
@@ -243,10 +187,6 @@ public:
|
||||
return false;
|
||||
}
|
||||
|
||||
CVDisplayLinkCreateWithActiveCGDisplays(&m_DisplayLink);
|
||||
CVDisplayLinkSetOutputCallback(m_DisplayLink, displayLinkOutputCallback, this);
|
||||
CVDisplayLinkStart(m_DisplayLink);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -260,46 +200,30 @@ public:
|
||||
return true;
|
||||
}
|
||||
|
||||
virtual void renderFrame(AVFrame* frame) override
|
||||
virtual bool needsTestFrame() override
|
||||
{
|
||||
if (m_DisplayLayer.status == AVQueuedSampleBufferRenderingStatusFailed) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Resetting failed AVSampleBufferDisplay layer");
|
||||
setupDisplayLayer();
|
||||
}
|
||||
// We query VT to determine whether the codec is supported
|
||||
return false;
|
||||
}
|
||||
|
||||
SDL_AtomicLock(&m_FrameQueueLock);
|
||||
m_FrameQueue.enqueue(frame);
|
||||
SDL_AtomicUnlock(&m_FrameQueueLock);
|
||||
virtual int getDecoderCapabilities() override
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
virtual IFFmpegRenderer::VSyncConstraint getVsyncConstraint() override
|
||||
{
|
||||
// This renderer is inherently tied to V-sync due how we're
|
||||
// rendering with AVSampleBufferDisplay layer. Running without
|
||||
// the V-Sync source leads to massive stuttering.
|
||||
return VSYNC_FORCE_ON;
|
||||
}
|
||||
|
||||
private:
|
||||
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;
|
||||
AVSampleBufferDisplayLayer* m_DisplayLayer;
|
||||
CMVideoFormatDescriptionRef m_FormatDesc;
|
||||
NSView* m_View;
|
||||
CVDisplayLinkRef m_DisplayLink;
|
||||
QQueue<AVFrame*> m_FrameQueue;
|
||||
QQueue<int> m_FrameQueueHistory;
|
||||
SDL_SpinLock m_FrameQueueLock;
|
||||
};
|
||||
|
||||
IFFmpegRenderer* VTRendererFactory::createRenderer() {
|
||||
|
||||
+273
-29
@@ -1,5 +1,7 @@
|
||||
#include <Limelight.h>
|
||||
#include "ffmpeg.h"
|
||||
#include "streaming/streamutils.h"
|
||||
#include <h264_stream.h>
|
||||
|
||||
#ifdef Q_OS_WIN32
|
||||
#include "ffmpeg-renderers/dxva2.h"
|
||||
@@ -20,6 +22,8 @@
|
||||
// This is gross but it allows us to use sizeof()
|
||||
#include "ffmpeg_videosamples.cpp"
|
||||
|
||||
#define MAX_SPS_EXTRA_SIZE 16
|
||||
|
||||
#define FAILED_DECODES_RESET_THRESHOLD 20
|
||||
|
||||
bool FFmpegVideoDecoder::isHardwareAccelerated()
|
||||
@@ -27,6 +31,11 @@ bool FFmpegVideoDecoder::isHardwareAccelerated()
|
||||
return m_HwDecodeCfg != nullptr;
|
||||
}
|
||||
|
||||
int FFmpegVideoDecoder::getDecoderCapabilities()
|
||||
{
|
||||
return m_Renderer->getDecoderCapabilities();
|
||||
}
|
||||
|
||||
enum AVPixelFormat FFmpegVideoDecoder::ffGetFormat(AVCodecContext* context,
|
||||
const enum AVPixelFormat* pixFmts)
|
||||
{
|
||||
@@ -53,11 +62,19 @@ FFmpegVideoDecoder::FFmpegVideoDecoder()
|
||||
m_DecodeBuffer(1024 * 1024, 0),
|
||||
m_HwDecodeCfg(nullptr),
|
||||
m_Renderer(nullptr),
|
||||
m_ConsecutiveFailedDecodes(0)
|
||||
m_ConsecutiveFailedDecodes(0),
|
||||
m_Pacer(nullptr),
|
||||
m_LastFrameNumber(0),
|
||||
m_StreamFps(0),
|
||||
m_NeedsSpsFixup(false)
|
||||
{
|
||||
av_init_packet(&m_Pkt);
|
||||
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
|
||||
SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "1");
|
||||
}
|
||||
@@ -91,19 +108,53 @@ void FFmpegVideoDecoder::reset()
|
||||
dropFrame(&event.user);
|
||||
}
|
||||
else {
|
||||
SDL_Delay(100);
|
||||
SDL_Delay(10);
|
||||
SDL_PumpEvents();
|
||||
}
|
||||
}
|
||||
|
||||
delete m_Pacer;
|
||||
m_Pacer = nullptr;
|
||||
|
||||
// This must be called after deleting Pacer because it
|
||||
// may be holding AVFrames to free in its destructor.
|
||||
// However, it must be called before deleting the IFFmpegRenderer
|
||||
// since the codec context may be referencing objects that we
|
||||
// need to delete in the renderer destructor.
|
||||
avcodec_free_context(&m_VideoDecoderCtx);
|
||||
|
||||
delete m_Renderer;
|
||||
m_Renderer = nullptr;
|
||||
|
||||
logVideoStats(m_GlobalVideoStats, "Global video stats");
|
||||
}
|
||||
|
||||
bool FFmpegVideoDecoder::completeInitialization(AVCodec* decoder, int videoFormat, int width, int height, bool testOnly)
|
||||
bool FFmpegVideoDecoder::completeInitialization(AVCodec* decoder, SDL_Window* window,
|
||||
int videoFormat, int width, int height,
|
||||
int maxFps, bool enableVsync, bool testOnly)
|
||||
{
|
||||
auto vsyncConstraint = m_Renderer->getVsyncConstraint();
|
||||
if (vsyncConstraint == IFFmpegRenderer::VSYNC_FORCE_OFF && enableVsync) {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"V-sync is forcefully disabled by the active renderer");
|
||||
enableVsync = false;
|
||||
}
|
||||
else if (vsyncConstraint == IFFmpegRenderer::VSYNC_FORCE_ON && !enableVsync) {
|
||||
// FIXME: This duplicates logic in Session.cpp
|
||||
int displayHz = StreamUtils::getDisplayRefreshRate(window);
|
||||
if (displayHz + 5 >= maxFps) {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"V-sync is forcefully enabled by the active renderer");
|
||||
enableVsync = true;
|
||||
}
|
||||
}
|
||||
|
||||
m_StreamFps = maxFps;
|
||||
m_Pacer = new Pacer(m_Renderer, &m_ActiveWndVideoStats);
|
||||
if (!m_Pacer->initialize(window, maxFps, enableVsync)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
m_VideoDecoderCtx = avcodec_alloc_context3(decoder);
|
||||
if (!m_VideoDecoderCtx) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||
@@ -114,6 +165,10 @@ bool FFmpegVideoDecoder::completeInitialization(AVCodec* decoder, int videoForma
|
||||
// Always request low delay decoding
|
||||
m_VideoDecoderCtx->flags |= AV_CODEC_FLAG_LOW_DELAY;
|
||||
|
||||
// Allow display of corrupt frames and frames missing references
|
||||
m_VideoDecoderCtx->flags |= AV_CODEC_FLAG_OUTPUT_CORRUPT;
|
||||
m_VideoDecoderCtx->flags2 |= AV_CODEC_FLAG2_SHOW_ALL;
|
||||
|
||||
// Enable slice multi-threading for software decoding
|
||||
if (!m_HwDecodeCfg) {
|
||||
m_VideoDecoderCtx->thread_type = FF_THREAD_SLICE;
|
||||
@@ -174,6 +229,17 @@ bool FFmpegVideoDecoder::completeInitialization(AVCodec* decoder, int videoForma
|
||||
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
|
||||
// Restore default log level before streaming
|
||||
@@ -183,6 +249,80 @@ bool FFmpegVideoDecoder::completeInitialization(AVCodec* decoder, int videoForma
|
||||
return true;
|
||||
}
|
||||
|
||||
void FFmpegVideoDecoder::addVideoStats(VIDEO_STATS& src, VIDEO_STATS& dst)
|
||||
{
|
||||
dst.receivedFrames += src.receivedFrames;
|
||||
dst.decodedFrames += src.decodedFrames;
|
||||
dst.renderedFrames += src.renderedFrames;
|
||||
dst.networkDroppedFrames += src.networkDroppedFrames;
|
||||
dst.pacerDroppedFrames += src.pacerDroppedFrames;
|
||||
dst.totalReassemblyTime += src.totalReassemblyTime;
|
||||
dst.totalDecodeTime += src.totalDecodeTime;
|
||||
dst.totalRenderTime += src.totalRenderTime;
|
||||
|
||||
Uint32 now = SDL_GetTicks();
|
||||
|
||||
// Initialize the measurement start point if this is the first video stat window
|
||||
if (!dst.measurementStartTimestamp) {
|
||||
dst.measurementStartTimestamp = src.measurementStartTimestamp;
|
||||
}
|
||||
|
||||
// The following code assumes the global measure was already started first
|
||||
SDL_assert(dst.measurementStartTimestamp <= src.measurementStartTimestamp);
|
||||
|
||||
dst.receivedFps = (float)dst.receivedFrames / ((float)(now - dst.measurementStartTimestamp) / 1000);
|
||||
dst.decodedFps = (float)dst.decodedFrames / ((float)(now - dst.measurementStartTimestamp) / 1000);
|
||||
dst.renderedFps = (float)dst.renderedFrames / ((float)(now - dst.measurementStartTimestamp) / 1000);
|
||||
}
|
||||
|
||||
void FFmpegVideoDecoder::logVideoStats(VIDEO_STATS& stats, const char* title)
|
||||
{
|
||||
if (stats.renderedFps > 0 || stats.renderedFrames != 0) {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"%s", title);
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"----------------------------------------------------------");
|
||||
}
|
||||
|
||||
if (stats.renderedFps > 0) {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Incoming frame rate from network: %.2f FPS",
|
||||
stats.receivedFps);
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Decoding frame rate: %.2f FPS",
|
||||
stats.decodedFps);
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Rendering frame rate: %.2f FPS",
|
||||
stats.renderedFps);
|
||||
}
|
||||
if (stats.renderedFrames != 0) {
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Total frames received: %u",
|
||||
stats.receivedFrames);
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Total frames decoded: %u",
|
||||
stats.decodedFrames);
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Total frames rendered: %u",
|
||||
stats.renderedFrames);
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Average reassembly time: %.2f ms",
|
||||
(float)stats.totalReassemblyTime / stats.decodedFrames);
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Average decode time: %.2f ms",
|
||||
(float)stats.totalDecodeTime / stats.decodedFrames);
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Average render time: %.2f ms",
|
||||
(float)stats.totalRenderTime / stats.renderedFrames);
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Frames lost during network transmission: %.2f%%",
|
||||
(float)stats.networkDroppedFrames / stats.decodedFrames);
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Frames dropped by frame pacing: %.2f%%",
|
||||
(float)stats.pacerDroppedFrames / stats.decodedFrames);
|
||||
}
|
||||
}
|
||||
|
||||
IFFmpegRenderer* FFmpegVideoDecoder::createAcceleratedRenderer(const AVCodecHWConfig* hwDecodeCfg)
|
||||
{
|
||||
if (!(hwDecodeCfg->methods & AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX)) {
|
||||
@@ -218,7 +358,8 @@ bool FFmpegVideoDecoder::initialize(
|
||||
int videoFormat,
|
||||
int width,
|
||||
int height,
|
||||
int)
|
||||
int maxFps,
|
||||
bool enableVsync)
|
||||
{
|
||||
AVCodec* decoder;
|
||||
|
||||
@@ -253,8 +394,8 @@ bool FFmpegVideoDecoder::initialize(
|
||||
m_HwDecodeCfg = nullptr;
|
||||
m_Renderer = new SdlRenderer();
|
||||
if (vds != StreamingPreferences::VDS_FORCE_HARDWARE &&
|
||||
m_Renderer->initialize(window, videoFormat, width, height) &&
|
||||
completeInitialization(decoder, videoFormat, width, height, false)) {
|
||||
m_Renderer->initialize(window, videoFormat, width, height, maxFps, enableVsync) &&
|
||||
completeInitialization(decoder, window, videoFormat, width, height, maxFps, enableVsync, false)) {
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
@@ -269,20 +410,26 @@ bool FFmpegVideoDecoder::initialize(
|
||||
}
|
||||
|
||||
m_HwDecodeCfg = config;
|
||||
// Submit test frame to ensure this codec really works
|
||||
if (m_Renderer->initialize(window, videoFormat, width, height) &&
|
||||
completeInitialization(decoder, videoFormat, width, height, true)) {
|
||||
// OK, it worked, so now let's initialize it for real
|
||||
reset();
|
||||
if ((m_Renderer = createAcceleratedRenderer(config)) != nullptr &&
|
||||
m_Renderer->initialize(window, videoFormat, width, height) &&
|
||||
completeInitialization(decoder, videoFormat, width, height, false)) {
|
||||
return true;
|
||||
// Initialize the hardware codec and submit a test frame if the renderer needs it
|
||||
if (m_Renderer->initialize(window, videoFormat, width, height, maxFps, enableVsync) &&
|
||||
completeInitialization(decoder, window, videoFormat, width, height, maxFps, enableVsync, m_Renderer->needsTestFrame())) {
|
||||
if (m_Renderer->needsTestFrame()) {
|
||||
// The test worked, so now let's initialize it for real
|
||||
reset();
|
||||
if ((m_Renderer = createAcceleratedRenderer(config)) != nullptr &&
|
||||
m_Renderer->initialize(window, videoFormat, width, height, maxFps, enableVsync) &&
|
||||
completeInitialization(decoder, window, videoFormat, width, height, maxFps, enableVsync, false)) {
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
SDL_LogCritical(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Decoder failed to initialize after successful test");
|
||||
reset();
|
||||
}
|
||||
}
|
||||
else {
|
||||
SDL_LogCritical(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Decoder failed to initialize after successful test");
|
||||
reset();
|
||||
// No test required. Good to go now.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else {
|
||||
@@ -292,28 +439,107 @@ bool FFmpegVideoDecoder::initialize(
|
||||
}
|
||||
}
|
||||
|
||||
void FFmpegVideoDecoder::writeBuffer(PLENTRY entry, int& offset)
|
||||
{
|
||||
if (m_NeedsSpsFixup && entry->bufferType == BUFFER_TYPE_SPS) {
|
||||
const char naluHeader[] = {0x00, 0x00, 0x00, 0x01};
|
||||
h264_stream_t* stream = h264_new();
|
||||
int nalStart, nalEnd;
|
||||
|
||||
// Read the old NALU
|
||||
find_nal_unit((uint8_t*)entry->data, entry->length, &nalStart, &nalEnd);
|
||||
read_nal_unit(stream,
|
||||
(unsigned char *)&entry->data[nalStart],
|
||||
nalEnd - nalStart);
|
||||
|
||||
SDL_assert(nalStart == sizeof(naluHeader));
|
||||
SDL_assert(nalEnd == entry->length);
|
||||
|
||||
// Fixup the SPS to what OS X needs to use hardware acceleration
|
||||
stream->sps->num_ref_frames = 1;
|
||||
stream->sps->vui.max_dec_frame_buffering = 1;
|
||||
|
||||
int initialOffset = offset;
|
||||
|
||||
// Copy the modified NALU data. This assumes a 3 byte prefix and
|
||||
// begins writing from the 2nd byte, so we must write the data
|
||||
// first, then go back and write the Annex B prefix.
|
||||
offset += write_nal_unit(stream, (uint8_t*)&m_DecodeBuffer.data()[initialOffset + 3],
|
||||
MAX_SPS_EXTRA_SIZE + entry->length - sizeof(naluHeader));
|
||||
|
||||
// Copy the NALU prefix over from the original SPS
|
||||
memcpy(&m_DecodeBuffer.data()[initialOffset], naluHeader, sizeof(naluHeader));
|
||||
offset += sizeof(naluHeader);
|
||||
|
||||
h264_free(stream);
|
||||
}
|
||||
else {
|
||||
// Write the buffer as-is
|
||||
memcpy(&m_DecodeBuffer.data()[offset],
|
||||
entry->data,
|
||||
entry->length);
|
||||
offset += entry->length;
|
||||
}
|
||||
}
|
||||
|
||||
int FFmpegVideoDecoder::submitDecodeUnit(PDECODE_UNIT du)
|
||||
{
|
||||
PLENTRY entry = du->bufferList;
|
||||
int err;
|
||||
|
||||
if (du->fullLength + AV_INPUT_BUFFER_PADDING_SIZE > m_DecodeBuffer.length()) {
|
||||
m_DecodeBuffer = QByteArray(du->fullLength + AV_INPUT_BUFFER_PADDING_SIZE, 0);
|
||||
// Flip stats windows roughly every second
|
||||
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;
|
||||
while (entry != nullptr) {
|
||||
memcpy(&m_DecodeBuffer.data()[offset],
|
||||
entry->data,
|
||||
entry->length);
|
||||
offset += entry->length;
|
||||
writeBuffer(entry, offset);
|
||||
entry = entry->next;
|
||||
}
|
||||
|
||||
SDL_assert(offset == du->fullLength);
|
||||
|
||||
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);
|
||||
if (err < 0) {
|
||||
@@ -354,6 +580,11 @@ int FFmpegVideoDecoder::submitDecodeUnit(PDECODE_UNIT du)
|
||||
// Queue the frame for rendering from the main thread
|
||||
SDL_AtomicIncRef(&m_QueuedFrames);
|
||||
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 {
|
||||
av_frame_free(&frame);
|
||||
@@ -380,7 +611,7 @@ int FFmpegVideoDecoder::submitDecodeUnit(PDECODE_UNIT du)
|
||||
void FFmpegVideoDecoder::renderFrame(SDL_UserEvent* event)
|
||||
{
|
||||
AVFrame* frame = reinterpret_cast<AVFrame*>(event->data1);
|
||||
m_Renderer->renderFrame(frame);
|
||||
m_Pacer->submitFrame(frame);
|
||||
SDL_AtomicDecRef(&m_QueuedFrames);
|
||||
}
|
||||
|
||||
@@ -388,6 +619,19 @@ void FFmpegVideoDecoder::renderFrame(SDL_UserEvent* event)
|
||||
void FFmpegVideoDecoder::dropFrame(SDL_UserEvent* event)
|
||||
{
|
||||
AVFrame* frame = reinterpret_cast<AVFrame*>(event->data1);
|
||||
av_frame_free(&frame);
|
||||
// If Pacer is using a frame queue, we can just queue the
|
||||
// frame and let it decide whether to drop or not. If Pacer
|
||||
// is submitting directly for rendering, we drop the frame
|
||||
// ourselves.
|
||||
if (m_Pacer->isUsingFrameQueue()) {
|
||||
m_Pacer->submitFrame(frame);
|
||||
}
|
||||
else {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "decoder.h"
|
||||
#include "ffmpeg-renderers/renderer.h"
|
||||
#include "ffmpeg-renderers/pacer/pacer.h"
|
||||
|
||||
extern "C" {
|
||||
#include <libavcodec/avcodec.h>
|
||||
@@ -16,8 +17,10 @@ public:
|
||||
int videoFormat,
|
||||
int width,
|
||||
int height,
|
||||
int frameRate) override;
|
||||
int maxFps,
|
||||
bool enableVsync) override;
|
||||
virtual bool isHardwareAccelerated() override;
|
||||
virtual int getDecoderCapabilities() override;
|
||||
virtual int submitDecodeUnit(PDECODE_UNIT du) override;
|
||||
virtual void renderFrame(SDL_UserEvent* event) override;
|
||||
virtual void dropFrame(SDL_UserEvent* event) override;
|
||||
@@ -25,13 +28,20 @@ public:
|
||||
virtual IFFmpegRenderer* getRenderer();
|
||||
|
||||
private:
|
||||
bool completeInitialization(AVCodec* decoder, int videoFormat,
|
||||
int width, int height, bool testOnly);
|
||||
bool completeInitialization(AVCodec* decoder, SDL_Window* window,
|
||||
int videoFormat, int width, int height,
|
||||
int maxFps, bool enableVsync, bool testOnly);
|
||||
|
||||
void logVideoStats(VIDEO_STATS& stats, const char* title);
|
||||
|
||||
void addVideoStats(VIDEO_STATS& src, VIDEO_STATS& dst);
|
||||
|
||||
IFFmpegRenderer* createAcceleratedRenderer(const AVCodecHWConfig* hwDecodeCfg);
|
||||
|
||||
void reset();
|
||||
|
||||
void writeBuffer(PLENTRY entry, int& offset);
|
||||
|
||||
static
|
||||
enum AVPixelFormat ffGetFormat(AVCodecContext* context,
|
||||
const enum AVPixelFormat* pixFmts);
|
||||
@@ -43,6 +53,13 @@ private:
|
||||
IFFmpegRenderer* m_Renderer;
|
||||
SDL_atomic_t m_QueuedFrames;
|
||||
int m_ConsecutiveFailedDecodes;
|
||||
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_HEVCTestFrame[];
|
||||
|
||||
@@ -25,10 +25,16 @@ SLVideoDecoder::isHardwareAccelerated()
|
||||
return true;
|
||||
}
|
||||
|
||||
int
|
||||
SLVideoDecoder::getDecoderCapabilities()
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool
|
||||
SLVideoDecoder::initialize(StreamingPreferences::VideoDecoderSelection vds,
|
||||
SDL_Window*,
|
||||
int videoFormat, int, int, int frameRate)
|
||||
int videoFormat, int, int, int frameRate, bool)
|
||||
{
|
||||
// SLVideo only supports hardware decoding
|
||||
if (vds == StreamingPreferences::VDS_FORCE_SOFTWARE) {
|
||||
|
||||
@@ -14,8 +14,10 @@ public:
|
||||
int videoFormat,
|
||||
int width,
|
||||
int height,
|
||||
int frameRate);
|
||||
int frameRate,
|
||||
bool enableVsync);
|
||||
virtual bool isHardwareAccelerated();
|
||||
virtual int getDecoderCapabilities();
|
||||
virtual int submitDecodeUnit(PDECODE_UNIT du);
|
||||
|
||||
// Unused since rendering is done directly from the decode thread
|
||||
|
||||
Submodule
+1
Submodule h264bitstream/h264bitstream added at 34f3c58afa
@@ -0,0 +1,46 @@
|
||||
#-------------------------------------------------
|
||||
#
|
||||
# Project created by QtCreator 2018-10-12T15:50:59
|
||||
#
|
||||
#-------------------------------------------------
|
||||
|
||||
|
||||
QT -= core gui
|
||||
|
||||
TARGET = h264bitstream
|
||||
TEMPLATE = lib
|
||||
|
||||
# Support debug and release builds from command line for CI
|
||||
CONFIG += debug_and_release
|
||||
|
||||
# Ensure symbols are always generated
|
||||
CONFIG += force_debug_info
|
||||
|
||||
# Build a static library
|
||||
CONFIG += staticlib
|
||||
|
||||
# Disable warnings
|
||||
CONFIG += warn_off
|
||||
|
||||
# Older GCC versions defaulted to GNU89
|
||||
*-g++ {
|
||||
QMAKE_CFLAGS += -std=gnu99
|
||||
}
|
||||
|
||||
SRC_DIR = $$PWD/h264bitstream
|
||||
|
||||
SOURCES += \
|
||||
$$SRC_DIR/h264_avcc.c \
|
||||
$$SRC_DIR/h264_nal.c \
|
||||
$$SRC_DIR/h264_sei.c \
|
||||
$$SRC_DIR/h264_slice_data.c \
|
||||
$$SRC_DIR/h264_stream.c
|
||||
|
||||
HEADERS += \
|
||||
$$SRC_DIR/bs.h \
|
||||
$$SRC_DIR/h264_avcc.h \
|
||||
$$SRC_DIR/h264_sei.h \
|
||||
$$SRC_DIR/h264_slice_data.h \
|
||||
$$SRC_DIR/h264_stream.h
|
||||
|
||||
INCLUDEPATH += $$INC_DIR
|
||||
@@ -0,0 +1,271 @@
|
||||
/*
|
||||
* Copyright (C) 2010-2013 Nektra S.A., Buenos Aires, Argentina.
|
||||
* All rights reserved. Contact: http://www.nektra.com
|
||||
*
|
||||
*
|
||||
* This file is part of Deviare In-Proc
|
||||
*
|
||||
*
|
||||
* Commercial License Usage
|
||||
* ------------------------
|
||||
* Licensees holding valid commercial Deviare In-Proc licenses may use this
|
||||
* file in accordance with the commercial license agreement provided with the
|
||||
* Software or, alternatively, in accordance with the terms contained in
|
||||
* a written agreement between you and Nektra. For licensing terms and
|
||||
* conditions see http://www.nektra.com/licensing/. For further information
|
||||
* use the contact form at http://www.nektra.com/contact/.
|
||||
*
|
||||
*
|
||||
* GNU General Public License Usage
|
||||
* --------------------------------
|
||||
* Alternatively, this file may be used under the terms of the GNU
|
||||
* General Public License version 3.0 as published by the Free Software
|
||||
* Foundation and appearing in the file LICENSE.GPL included in the
|
||||
* packaging of this file. Please review the following information to
|
||||
* ensure the GNU General Public License version 3.0 requirements will be
|
||||
* met: http://www.gnu.org/copyleft/gpl.html.
|
||||
*
|
||||
**/
|
||||
|
||||
#ifndef _NKTHOOKLIB
|
||||
#define _NKTHOOKLIB
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
//-----------------------------------------------------------
|
||||
|
||||
#define NKTHOOKLIB_DontSkipInitialJumps 0x0001
|
||||
#define NKTHOOKLIB_DontRemoveOnUnhook 0x0002
|
||||
#define NKTHOOKLIB_DontSkipAnyJumps 0x0004
|
||||
#define NKTHOOKLIB_SkipNullProcsToHook 0x0008
|
||||
#define NKTHOOKLIB_UseAbsoluteIndirectJumps 0x0010
|
||||
#define NKTHOOKLIB_DisallowReentrancy 0x0020
|
||||
#define NKTHOOKLIB_DontEnableHooks 0x0040
|
||||
|
||||
#define NKTHOOKLIB_ProcessPlatformX86 1
|
||||
#define NKTHOOKLIB_ProcessPlatformX64 2
|
||||
|
||||
#define NKTHOOKLIB_CurrentProcess ((HANDLE)(LONG_PTR)-1)
|
||||
#define NKTHOOKLIB_CurrentThread ((HANDLE)(LONG_PTR)-2)
|
||||
|
||||
//-----------------------------------------------------------
|
||||
|
||||
class CNktHookLib
|
||||
{
|
||||
public:
|
||||
typedef struct tagHOOK_INFO {
|
||||
SIZE_T nHookId;
|
||||
LPVOID lpProcToHook;
|
||||
LPVOID lpNewProcAddr;
|
||||
//----
|
||||
LPVOID lpCallOriginal;
|
||||
} HOOK_INFO, *LPHOOK_INFO;
|
||||
|
||||
CNktHookLib();
|
||||
~CNktHookLib();
|
||||
|
||||
DWORD Hook(__out SIZE_T *lpnHookId, __out LPVOID *lplpCallOriginal, __in LPVOID lpProcToHook,
|
||||
__in LPVOID lpNewProcAddr, __in DWORD dwFlags=0);
|
||||
DWORD Hook(__inout HOOK_INFO aHookInfo[], __in SIZE_T nCount, __in DWORD dwFlags=0);
|
||||
DWORD Hook(__inout LPHOOK_INFO aHookInfo[], __in SIZE_T nCount, __in DWORD dwFlags = 0);
|
||||
|
||||
DWORD RemoteHook(__out SIZE_T *lpnHookId, __out LPVOID *lplpCallOriginal, __in DWORD dwPid,
|
||||
__in LPVOID lpProcToHook, __in LPVOID lpNewProcAddr, __in DWORD dwFlags);
|
||||
DWORD RemoteHook(__inout HOOK_INFO aHookInfo[], __in SIZE_T nCount, __in DWORD dwPid, __in DWORD dwFlags);
|
||||
DWORD RemoteHook(__inout LPHOOK_INFO aHookInfo[], __in SIZE_T nCount, __in DWORD dwPid, __in DWORD dwFlags);
|
||||
|
||||
DWORD RemoteHook(__out SIZE_T *lpnHookId, __out LPVOID *lplpCallOriginal, __in HANDLE hProcess,
|
||||
__in LPVOID lpProcToHook, __in LPVOID lpNewProcAddr, __in DWORD dwFlags);
|
||||
DWORD RemoteHook(__inout HOOK_INFO aHookInfo[], __in SIZE_T nCount, __in HANDLE hProcess, __in DWORD dwFlags);
|
||||
DWORD RemoteHook(__inout LPHOOK_INFO aHookInfo[], __in SIZE_T nCount, __in HANDLE hProcess, __in DWORD dwFlags);
|
||||
|
||||
DWORD Unhook(__in SIZE_T nHookId);
|
||||
DWORD Unhook(__in HOOK_INFO aHookInfo[], __in SIZE_T nCount);
|
||||
DWORD Unhook(__in LPHOOK_INFO aHookInfo[], __in SIZE_T nCount);
|
||||
VOID UnhookProcess(__in DWORD dwPid);
|
||||
VOID UnhookAll();
|
||||
|
||||
//NOTE: The following 2 (two) methods will remove the hooks from the internal list of hooks but the original
|
||||
// hook(s) will remain active.
|
||||
DWORD RemoveHook(__in SIZE_T nHookId, BOOL bDisable);
|
||||
DWORD RemoveHook(__in HOOK_INFO aHookInfo[], __in SIZE_T nCount, __in BOOL bDisable);
|
||||
DWORD RemoveHook(__in LPHOOK_INFO aHookInfo[], __in SIZE_T nCount, __in BOOL bDisable);
|
||||
|
||||
DWORD EnableHook(__in SIZE_T nHookId, __in BOOL bEnable);
|
||||
DWORD EnableHook(__in HOOK_INFO aHookInfo[], __in SIZE_T nCount, __in BOOL bEnable);
|
||||
DWORD EnableHook(__in LPHOOK_INFO aHookInfo[], __in SIZE_T nCount, __in BOOL bEnable);
|
||||
|
||||
DWORD SetSuspendThreadsWhileHooking(__in BOOL bEnable);
|
||||
BOOL GetSuspendThreadsWhileHooking();
|
||||
|
||||
DWORD SetEnableDebugOutput(__in BOOL bEnable);
|
||||
BOOL GetEnableDebugOutput();
|
||||
|
||||
void* __cdecl operator new(__in size_t nSize);
|
||||
void* __cdecl operator new[](__in size_t nSize);
|
||||
void* __cdecl operator new(__in size_t nSize, __inout void* lpInPlace);
|
||||
void __cdecl operator delete(__inout void* p);
|
||||
void __cdecl operator delete[](__inout void* p);
|
||||
#if _MSC_VER >= 1200
|
||||
void __cdecl operator delete(__inout void* p, __inout void* lpPlace);
|
||||
#endif //_MSC_VER >= 1200
|
||||
|
||||
private:
|
||||
DWORD HookCommon(__in LPVOID lpInfo, __in SIZE_T nCount, __in DWORD dwPid, __in DWORD dwFlags);
|
||||
DWORD UnhookCommon(__in LPVOID lpInfo, __in SIZE_T nCount, __in DWORD dwFlags);
|
||||
DWORD RemoveHookCommon(__in LPVOID lpInfo, __in SIZE_T nCount, __in BOOL bDisable, __in DWORD dwFlags);
|
||||
DWORD EnableHookCommon(__in LPVOID lpInfo, __in SIZE_T nCount, __in BOOL bEnable, __in DWORD dwFlags);
|
||||
|
||||
private:
|
||||
LPVOID lpInternals;
|
||||
};
|
||||
|
||||
//-----------------------------------------------------------
|
||||
|
||||
namespace NktHookLibHelpers {
|
||||
|
||||
//NOTE: See "BuildNtSysCalls" below
|
||||
typedef struct tagSYSCALLDEF {
|
||||
LPSTR szNtApiNameA;
|
||||
SIZE_T nOffset;
|
||||
} SYSCALLDEF, *LPSYSCALLDEF;
|
||||
|
||||
//--------------------------------
|
||||
|
||||
//NOTE: See "SetApiResolverCallback" below
|
||||
typedef LPVOID (__stdcall *lpfnInternalApiResolver)(__in_z LPCSTR szApiNameA, __in LPVOID lpUserParam);
|
||||
|
||||
//--------------------------------
|
||||
|
||||
HINSTANCE GetModuleBaseAddress(__in_z LPCWSTR szDllNameW);
|
||||
LPVOID GetProcedureAddress(__in HINSTANCE hDll, __in LPCSTR szProcNameA);
|
||||
|
||||
HINSTANCE GetRemoteModuleBaseAddress(__in HANDLE hProcess, __in_z LPCWSTR szDllNameW, __in BOOL bScanMappedImages);
|
||||
LPVOID GetRemoteProcedureAddress(__in HANDLE hProcess, __in HINSTANCE hDll, __in_z LPCSTR szProcNameA);
|
||||
|
||||
//--------------------------------
|
||||
|
||||
int sprintf_s(__out_z char *lpDest, __in size_t nMaxCount, __in_z const char *szFormatA, ...);
|
||||
int vsnprintf(__out_z char *lpDest, __in size_t nMaxCount, __in_z const char *szFormatA, __in va_list lpArgList);
|
||||
|
||||
//only on XP or later
|
||||
int swprintf_s(__out_z wchar_t *lpDest, __in size_t nMaxCount, __in_z const wchar_t *szFormatW, ...);
|
||||
int vsnwprintf(__out_z wchar_t *lpDest, __in size_t nMaxCount, __in_z const wchar_t *szFormatW, __in va_list lpArgList);
|
||||
|
||||
//--------------------------------
|
||||
|
||||
//Returns a PROCESSOR_ARCHITECTURE_xxx value or -1 on error.
|
||||
LONG GetProcessorArchitecture();
|
||||
|
||||
HANDLE OpenProcess(__in DWORD dwDesiredAccess, __in BOOL bInheritHandle, __in DWORD dwProcessId);
|
||||
HANDLE OpenThread(__in DWORD dwDesiredAccess, __in BOOL bInheritHandle, __in DWORD dwThreadId);
|
||||
|
||||
LONG GetProcessPlatform(__in HANDLE hProcess);
|
||||
|
||||
SIZE_T ReadMem(__in HANDLE hProcess, __out LPVOID lpDest, __in LPVOID lpSrc, __in SIZE_T nBytesCount);
|
||||
BOOL WriteMem(__in HANDLE hProcess, __out LPVOID lpDest, __in LPVOID lpSrc, __in SIZE_T nBytesCount);
|
||||
|
||||
LONG GetThreadPriority(__in HANDLE hThread, __out int *lpnPriority);
|
||||
LONG SetThreadPriority(__in HANDLE hThread, __in int nPriority);
|
||||
|
||||
DWORD GetCurrentThreadId();
|
||||
DWORD GetCurrentProcessId();
|
||||
|
||||
HANDLE GetProcessHeap();
|
||||
LPVOID MemAlloc(__in SIZE_T nSize);
|
||||
VOID MemFree(__in LPVOID lpPtr);
|
||||
|
||||
VOID MemSet(__out void *lpDest, __in int nVal, __in SIZE_T nCount);
|
||||
VOID MemCopy(__out void *lpDest, __in const void *lpSrc, __in SIZE_T nCount);
|
||||
SIZE_T TryMemCopy(__out void *lpDest, __in const void *lpSrc, __in SIZE_T nCount);
|
||||
VOID MemMove(__out void *lpDest, __in const void *lpSrc, __in SIZE_T nCount);
|
||||
int MemCompare(__in const void *lpBuf1, __in const void *lpBuf2, __in SIZE_T nCount);
|
||||
|
||||
//--------------------------------
|
||||
|
||||
VOID DebugPrint(__in LPCSTR szFormatA, ...);
|
||||
VOID DebugVPrint(__in LPCSTR szFormatA, __in va_list argptr);
|
||||
|
||||
//--------------------------------
|
||||
|
||||
SIZE_T GetInstructionLength(__in LPVOID lpAddr, __in SIZE_T nSize, __in BYTE nPlatformBits,
|
||||
__out_opt BOOL *lpbIsMemOp=NULL, __out_z_opt LPSTR szBufA=NULL, __in SIZE_T nBufLen=0);
|
||||
|
||||
//NOTE: When NktHookLib is initialized, it tries to locate needed ntdll's apis by scanning process' modules.
|
||||
// If you want to override an api call, use this method to set the resolver address. LPVOID returned by
|
||||
// the callback must have the same definition and calling convention than the one in ntdll. Return NULL
|
||||
// if you want NktHookLib to use the real ntdll api.
|
||||
VOID SetInternalApiResolverCallback(__in lpfnInternalApiResolver fnInternalApiResolver, __in LPVOID lpUserParam);
|
||||
|
||||
//This function generates a relocatable byte code with a copy of the original SysCall routine for each passes
|
||||
//ntdll api. The code is created based on the original ntdll.dll image file on disk located on System32 or SysWow64,
|
||||
//depending on the target platform.
|
||||
//
|
||||
//Useful for doing direct calls to low level apis bypassing third party hooks (like Chrome navigator does). Also, you
|
||||
//can pass the generated code to another process. You can generate the code with this method, pass it to another
|
||||
//process and then use SetApiResolverCallback above.
|
||||
//
|
||||
//NOTE: Not all NtXXX apis are SysCalls. Trying to generate code for a non-syscall api may generate an unexpected
|
||||
// behavior.
|
||||
// If 'lpCode' is NULL, the needed space is returned. Although syscalls uses less than 32 bytes, a maximum of 256
|
||||
// bytes are supported for each requested api. You can safety allocate a block of 256*nDefsCount bytes to hold
|
||||
// the generated code.
|
||||
DWORD BuildNtSysCalls(__in LPSYSCALLDEF lpDefs, __in SIZE_T nDefsCount, __in SIZE_T nPlatform,
|
||||
__out_opt LPVOID lpCode, __out SIZE_T *lpnCodeSize);
|
||||
|
||||
//--------------------------------
|
||||
|
||||
//NOTE: Return 0xFFFFFFFF if remote thread is not accessible
|
||||
DWORD GetWin32LastError(__in_opt HANDLE hThread=NULL);
|
||||
BOOL SetWin32LastError(__in DWORD dwErrorCode, __in_opt HANDLE hThread=NULL);
|
||||
|
||||
//--------------------------------
|
||||
|
||||
BOOL GetOsVersion(__out_opt LPDWORD lpdwVerMajor=NULL, __out_opt LPDWORD lpdwVerMinor=NULL,
|
||||
__out_opt LPDWORD lpdwBuildNumber=NULL);
|
||||
|
||||
//--------------------------------
|
||||
|
||||
//NOTE: CreateProcessWithDllW and related functions returns the Win32 error code directly. NOERROR => Success.
|
||||
//
|
||||
// If "szDllNameW" string ends with 'x86.dll', 'x64.dll', '32.dll', '64.dll', the dll name will be adjusted
|
||||
// in order to match the process platform. I.e.: "mydll_x86.dll" will become "mydll_x64.dll" on 64-bit processes.
|
||||
DWORD CreateProcessWithDllW(__in_z_opt LPCWSTR lpApplicationName, __inout_z_opt LPWSTR lpCommandLine,
|
||||
__in_opt LPSECURITY_ATTRIBUTES lpProcessAttributes,
|
||||
__in_opt LPSECURITY_ATTRIBUTES lpThreadAttributes, __in BOOL bInheritHandles,
|
||||
__in DWORD dwCreationFlags, __in_z_opt LPCWSTR lpEnvironment,
|
||||
__in_z_opt LPCWSTR lpCurrentDirectory, __in LPSTARTUPINFOW lpStartupInfo,
|
||||
__out LPPROCESS_INFORMATION lpProcessInformation, __in_z LPCWSTR szDllNameW,
|
||||
__in_opt HANDLE hSignalCompleted=NULL, __in_z_opt LPCSTR szInitFunctionA=NULL,
|
||||
__in_opt LPVOID lpInitFuncParams=NULL, __in_opt ULONG nInitFuncParamsSize=0);
|
||||
|
||||
DWORD CreateProcessWithLogonAndDllW(__in_z LPCWSTR lpUsername, __in_z_opt LPCWSTR lpDomain, __in_z LPCWSTR lpPassword,
|
||||
__in DWORD dwLogonFlags, __in_opt LPCWSTR lpApplicationName,
|
||||
__inout_opt LPWSTR lpCommandLine, __in DWORD dwCreationFlags,
|
||||
__in_z_opt LPCWSTR lpEnvironment, __in_z_opt LPCWSTR lpCurrentDirectory,
|
||||
__in LPSTARTUPINFOW lpStartupInfo, __out LPPROCESS_INFORMATION lpProcessInformation,
|
||||
__in_z LPCWSTR szDllNameW, __in_opt HANDLE hSignalCompleted=NULL,
|
||||
__in_z_opt LPCSTR szInitFunctionA=NULL, __in_opt LPVOID lpInitFuncParams=NULL,
|
||||
__in_opt ULONG nInitFuncParamsSize=0);
|
||||
|
||||
DWORD CreateProcessWithTokenAndDllW(__in HANDLE hToken, __in DWORD dwLogonFlags, __in_z_opt LPCWSTR lpApplicationName,
|
||||
__inout_opt LPWSTR lpCommandLine, __in DWORD dwCreationFlags,
|
||||
__in_z_opt LPCWSTR lpEnvironment, __in_z_opt LPCWSTR lpCurrentDirectory,
|
||||
__in LPSTARTUPINFOW lpStartupInfo, __out LPPROCESS_INFORMATION lpProcessInformation,
|
||||
__in_z LPCWSTR szDllNameW, __in_opt HANDLE hSignalCompleted=NULL,
|
||||
__in_z_opt LPCSTR szInitFunctionA=NULL, __in_opt LPVOID lpInitFuncParams=NULL,
|
||||
__in_opt ULONG nInitFuncParamsSize=0);
|
||||
|
||||
DWORD InjectDllByPidW(__in DWORD dwPid, __in_z LPCWSTR szDllNameW, __in_z_opt LPCSTR szInitFunctionA=NULL,
|
||||
__in_opt DWORD dwProcessInitWaitTimeoutMs=5000, __out_opt LPHANDLE lphInjectorThread=NULL,
|
||||
__in_opt LPVOID lpInitFuncParams=NULL, __in_opt ULONG nInitFuncParamsSize=0);
|
||||
|
||||
DWORD InjectDllByHandleW(__in HANDLE hProcess, __in_z LPCWSTR szDllNameW, __in_z_opt LPCSTR szInitFunctionA=NULL,
|
||||
__in_opt DWORD dwProcessInitWaitTimeoutMs=5000, __out_opt LPHANDLE lphInjectorThread=NULL,
|
||||
__in_opt LPVOID lpInitFuncParams=NULL, __in_opt ULONG nInitFuncParamsSize=0);
|
||||
|
||||
} //NktHookLibHelpers
|
||||
|
||||
//-----------------------------------------------------------
|
||||
|
||||
#endif //_NKTHOOKLIB
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Submodule moonlight-common-c/moonlight-common-c updated: feb46b978e...558ba488e8
@@ -12,6 +12,9 @@ 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
|
||||
|
||||
win32 {
|
||||
INCLUDEPATH += $$PWD/../libs/windows/include
|
||||
}
|
||||
@@ -21,6 +24,7 @@ macx {
|
||||
unix:!macx {
|
||||
CONFIG += link_pkgconfig
|
||||
PKGCONFIG += openssl
|
||||
DEFINES += HAVE_CLOCK_GETTIME=1
|
||||
}
|
||||
|
||||
COMMON_C_DIR = $$PWD/moonlight-common-c
|
||||
@@ -63,7 +67,7 @@ INCLUDEPATH += \
|
||||
CONFIG += warn_off staticlib
|
||||
DEFINES += HAS_SOCKLEN_T
|
||||
|
||||
debug {
|
||||
CONFIG(debug, debug|release) {
|
||||
# Enable asserts on debug builds
|
||||
DEFINES += LC_DEBUG
|
||||
}
|
||||
|
||||
+9
-2
@@ -2,9 +2,16 @@ TEMPLATE = subdirs
|
||||
SUBDIRS = \
|
||||
moonlight-common-c \
|
||||
qmdnsengine \
|
||||
app
|
||||
app \
|
||||
soundio \
|
||||
h264bitstream
|
||||
|
||||
CONFIG += ordered
|
||||
# Build the dependencies in parallel before the final app
|
||||
app.depends = qmdnsengine moonlight-common-c soundio h264bitstream
|
||||
win32 {
|
||||
SUBDIRS += AntiHooking
|
||||
app.depends += AntiHooking
|
||||
}
|
||||
|
||||
# Support debug and release builds from command line for CI
|
||||
CONFIG += debug_and_release
|
||||
|
||||
@@ -7,6 +7,9 @@ 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
|
||||
|
||||
QMDNSE_DIR = $$PWD/qmdnsengine/src
|
||||
DEFINES += \
|
||||
QT_NO_SIGNALS_SLOTS_KEYWORDS
|
||||
|
||||
@@ -33,6 +33,12 @@ pushd $BUILD_FOLDER
|
||||
make $(echo "$BUILD_CONFIG" | tr '[:upper:]' '[:lower:]') || fail "Make failed!"
|
||||
popd
|
||||
|
||||
echo Saving dSYM file
|
||||
pushd $BUILD_FOLDER
|
||||
dsymutil app/Moonlight.app/Contents/MacOS/Moonlight -o Moonlight.dsym || fail "dSYM creation failed!"
|
||||
cp -R Moonlight.dsym $INSTALLER_FOLDER || fail "dSYM copy failed!"
|
||||
popd
|
||||
|
||||
echo Copying dylib dependencies
|
||||
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!"
|
||||
|
||||
@@ -16,6 +16,7 @@ if /I "%BUILD_CONFIG%"=="debug" (
|
||||
if /I "%BUILD_CONFIG%"=="signed-release" (
|
||||
set BUILD_CONFIG=release
|
||||
set SIGN=1
|
||||
set MUST_DEPLOY_SYMBOLS=1
|
||||
) else (
|
||||
echo Invalid build configuration
|
||||
exit /b 1
|
||||
@@ -31,7 +32,6 @@ if /I "%ARCH%" NEQ "x86" (
|
||||
)
|
||||
|
||||
set VS_PATH=%ProgramFiles(x86)%\Microsoft Visual Studio\2017\Community
|
||||
set VCREDIST_PATH=%VS_PATH%\VC\Redist\MSVC\14.14.26405
|
||||
set SIGNTOOL_PARAMS=sign /tr http://timestamp.digicert.com /td sha256 /fd sha256 /sha1 1B3C676E831A94EC0327C3347EB32C68C26B3A67 /v
|
||||
|
||||
call "%VS_PATH%\VC\Auxiliary\Build\vcvarsall.bat" %ARCH%
|
||||
@@ -42,15 +42,18 @@ set SOURCE_ROOT=%cd%
|
||||
set BUILD_FOLDER=%BUILD_ROOT%\build-%ARCH%-%BUILD_CONFIG%
|
||||
set DEPLOY_FOLDER=%BUILD_ROOT%\deploy-%ARCH%-%BUILD_CONFIG%
|
||||
set INSTALLER_FOLDER=%BUILD_ROOT%\installer-%ARCH%-%BUILD_CONFIG%
|
||||
set SYMBOLS_FOLDER=%BUILD_ROOT%\symbols-%ARCH%-%BUILD_CONFIG%
|
||||
|
||||
echo Cleaning output directories
|
||||
rmdir /s /q %DEPLOY_FOLDER%
|
||||
rmdir /s /q %BUILD_FOLDER%
|
||||
rmdir /s /q %INSTALLER_FOLDER%
|
||||
rmdir /s /q %SYMBOLS_FOLDER%
|
||||
mkdir %BUILD_ROOT%
|
||||
mkdir %DEPLOY_FOLDER%
|
||||
mkdir %BUILD_FOLDER%
|
||||
mkdir %INSTALLER_FOLDER%
|
||||
mkdir %SYMBOLS_FOLDER%
|
||||
|
||||
echo Configuring the project
|
||||
pushd %BUILD_FOLDER%
|
||||
@@ -64,10 +67,39 @@ nmake %BUILD_CONFIG%
|
||||
if !ERRORLEVEL! NEQ 0 goto Error
|
||||
popd
|
||||
|
||||
echo Saving PDBs
|
||||
for /r "%BUILD_FOLDER%" %%f in (*.pdb) do (
|
||||
copy "%%f" %SYMBOLS_FOLDER%
|
||||
if !ERRORLEVEL! NEQ 0 goto Error
|
||||
)
|
||||
copy %SOURCE_ROOT%\libs\windows\lib\%ARCH%\*.pdb %SYMBOLS_FOLDER%
|
||||
if !ERRORLEVEL! NEQ 0 goto Error
|
||||
7z a %SYMBOLS_FOLDER%\MoonlightDebuggingSymbols.zip %SYMBOLS_FOLDER%\*.pdb
|
||||
if !ERRORLEVEL! NEQ 0 goto Error
|
||||
|
||||
if "%ML_SYMBOL_STORE%" NEQ "" (
|
||||
echo Publishing PDBs to symbol store: %ML_SYMBOL_STORE%
|
||||
symstore add /f %SYMBOLS_FOLDER%\*.pdb /s %ML_SYMBOL_STORE% /t Moonlight
|
||||
if !ERRORLEVEL! NEQ 0 goto Error
|
||||
) else (
|
||||
if "%MUST_DEPLOY_SYMBOLS%"=="1" (
|
||||
echo "A symbol server must be specified in ML_SYMBOL_STORE for signed release builds"
|
||||
exit /b 1
|
||||
)
|
||||
)
|
||||
|
||||
echo Copying DLL dependencies
|
||||
copy %SOURCE_ROOT%\libs\windows\lib\%ARCH%\*.dll %DEPLOY_FOLDER%
|
||||
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
|
||||
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
|
||||
@@ -82,27 +114,32 @@ if !ERRORLEVEL! NEQ 0 goto Error
|
||||
|
||||
if "%SIGN%"=="1" (
|
||||
echo Signing deployed binaries
|
||||
set FILES_TO_SIGN=
|
||||
for /r "%DEPLOY_FOLDER%" %%f in (*.dll *.exe) do (
|
||||
signtool %SIGNTOOL_PARAMS% %%f
|
||||
if !ERRORLEVEL! NEQ 0 goto Error
|
||||
set FILES_TO_SIGN=!FILES_TO_SIGN! %%f
|
||||
)
|
||||
signtool %SIGNTOOL_PARAMS% !FILES_TO_SIGN!
|
||||
if !ERRORLEVEL! NEQ 0 goto Error
|
||||
)
|
||||
|
||||
echo Building installer
|
||||
set VCREDIST_INSTALLER=%VCREDIST_PATH%\vcredist_%ARCH%.exe
|
||||
msbuild %SOURCE_ROOT%\wix\Moonlight.sln /p:Configuration=%BUILD_CONFIG% /p:Platform=%ARCH%
|
||||
if !ERRORLEVEL! NEQ 0 goto Error
|
||||
|
||||
echo Building portable package
|
||||
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
|
||||
copy "%VCREDIST_PATH%\%ARCH%\Microsoft.VC141.CRT\*.dll" %DEPLOY_FOLDER%
|
||||
if !ERRORLEVEL! NEQ 0 goto Error
|
||||
7z a %INSTALLER_FOLDER%\MoonlightPortable.zip %DEPLOY_FOLDER%\*
|
||||
echo Building MSI
|
||||
msbuild %SOURCE_ROOT%\wix\Moonlight\Moonlight.wixproj /p:Configuration=%BUILD_CONFIG% /p:Platform=%ARCH%
|
||||
if !ERRORLEVEL! NEQ 0 goto Error
|
||||
|
||||
if "%SIGN%"=="1" (
|
||||
echo Signing installer binary
|
||||
echo Signing MSI
|
||||
signtool %SIGNTOOL_PARAMS% %BUILD_FOLDER%\Moonlight.msi
|
||||
if !ERRORLEVEL! NEQ 0 goto Error
|
||||
)
|
||||
|
||||
echo Building bundle
|
||||
set VCREDIST_INSTALLER=%VCToolsRedistDir%vcredist_%ARCH%.exe
|
||||
rem Bundles are always x86 binaries
|
||||
msbuild %SOURCE_ROOT%\wix\MoonlightSetup\MoonlightSetup.wixproj /p:Configuration=%BUILD_CONFIG% /p:Platform=x86
|
||||
if !ERRORLEVEL! NEQ 0 goto Error
|
||||
|
||||
if "%SIGN%"=="1" (
|
||||
echo Signing bundle
|
||||
"%WIX%\bin\insignia" -ib %INSTALLER_FOLDER%\MoonlightSetup.exe -o %BUILD_FOLDER%\engine.exe
|
||||
if !ERRORLEVEL! NEQ 0 goto Error
|
||||
signtool %SIGNTOOL_PARAMS% %BUILD_FOLDER%\engine.exe
|
||||
@@ -113,12 +150,23 @@ if "%SIGN%"=="1" (
|
||||
if !ERRORLEVEL! NEQ 0 goto Error
|
||||
)
|
||||
|
||||
echo Building portable package
|
||||
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
|
||||
copy "%VCToolsRedistDir%%ARCH%\Microsoft.VC141.CRT\*.dll" %DEPLOY_FOLDER%
|
||||
rem This file tells Moonlight that it's a portable installation
|
||||
echo. > %DEPLOY_FOLDER%\portable.dat
|
||||
if !ERRORLEVEL! NEQ 0 goto Error
|
||||
7z a %INSTALLER_FOLDER%\MoonlightPortable.zip %DEPLOY_FOLDER%\*
|
||||
if !ERRORLEVEL! NEQ 0 goto Error
|
||||
|
||||
if /i "%APPVEYOR%"=="true" (
|
||||
echo Pushing artifacts
|
||||
appveyor PushArtifact %INSTALLER_FOLDER%\MoonlightSetup.exe -FileName MoonlightSetup-%ARCH%-%BUILD_CONFIG%.exe
|
||||
if !ERRORLEVEL! NEQ 0 goto Error
|
||||
appveyor PushArtifact %INSTALLER_FOLDER%\MoonlightPortable.zip -FileName MoonlightPortable-%ARCH%-%BUILD_CONFIG%.zip
|
||||
if !ERRORLEVEL! NEQ 0 goto Error
|
||||
appveyor PushArtifact %SYMBOLS_FOLDER%\MoonlightDebuggingSymbols.zip -FileName MoonlightDebuggingSymbols-%ARCH%-%BUILD_CONFIG%.zip
|
||||
)
|
||||
|
||||
echo Build successful!
|
||||
@@ -126,4 +174,4 @@ exit /b 0
|
||||
|
||||
:Error
|
||||
echo Build failed!
|
||||
exit /b !ERRORLEVEL!
|
||||
exit /b !ERRORLEVEL!
|
||||
|
||||
Submodule
+1
Submodule soundio/libsoundio added at 5d6a7210fa
@@ -0,0 +1,111 @@
|
||||
#-------------------------------------------------
|
||||
#
|
||||
# Project created by QtCreator 2018-10-02T15:27:07
|
||||
#
|
||||
#-------------------------------------------------
|
||||
|
||||
QT -= core gui
|
||||
|
||||
TARGET = soundio
|
||||
TEMPLATE = lib
|
||||
|
||||
# Support debug and release builds from command line for CI
|
||||
CONFIG += debug_and_release
|
||||
|
||||
# Ensure symbols are always generated
|
||||
CONFIG += force_debug_info
|
||||
|
||||
# Build a static library
|
||||
CONFIG += staticlib
|
||||
|
||||
# Force MSVC to compile C as C++ for atomic support
|
||||
*-msvc {
|
||||
QMAKE_CFLAGS += /TP
|
||||
}
|
||||
|
||||
# Older GCC versions defaulted to GNU89
|
||||
*-g++ {
|
||||
QMAKE_CFLAGS += -std=gnu99
|
||||
}
|
||||
|
||||
# Disable warnings
|
||||
CONFIG += warn_off
|
||||
|
||||
unix:!macx {
|
||||
CONFIG += link_pkgconfig
|
||||
|
||||
packagesExist(libpulse) {
|
||||
PKGCONFIG += libpulse
|
||||
CONFIG += pulseaudio
|
||||
}
|
||||
packagesExist(alsa) {
|
||||
PKGCONFIG += alsa
|
||||
CONFIG += alsa
|
||||
}
|
||||
}
|
||||
|
||||
CONFIG(release, debug|release) {
|
||||
# Disable asserts on release builds
|
||||
DEFINES += NDEBUG
|
||||
}
|
||||
|
||||
DEFINES += \
|
||||
SOUNDIO_STATIC_LIBRARY \
|
||||
SOUNDIO_VERSION_MAJOR=1 \
|
||||
SOUNDIO_VERSION_MINOR=1 \
|
||||
SOUNDIO_VERSION_PATCH=0 \
|
||||
SOUNDIO_VERSION_STRING=\\\"1.1.0\\\"
|
||||
|
||||
SRC_DIR = $$PWD/libsoundio/src
|
||||
INC_DIR = $$PWD/libsoundio
|
||||
|
||||
SOURCES += \
|
||||
$$SRC_DIR/channel_layout.c \
|
||||
$$SRC_DIR/dummy.c \
|
||||
$$SRC_DIR/os.c \
|
||||
$$SRC_DIR/ring_buffer.c \
|
||||
$$SRC_DIR/soundio.c \
|
||||
$$SRC_DIR/util.c
|
||||
|
||||
HEADERS += \
|
||||
$$SRC_DIR/atomics.h \
|
||||
$$SRC_DIR/dummy.h \
|
||||
$$SRC_DIR/list.h \
|
||||
$$SRC_DIR/os.h \
|
||||
$$SRC_DIR/ring_buffer.h \
|
||||
$$SRC_DIR/soundio_internal.h \
|
||||
$$SRC_DIR/soundio_private.h \
|
||||
$$SRC_DIR/util.h \
|
||||
$$INC_DIR/soundio/soundio.h \
|
||||
$$INC_DIR/soundio/endian.h
|
||||
|
||||
INCLUDEPATH += $$INC_DIR
|
||||
|
||||
win32 {
|
||||
message(WASAPI backend selected)
|
||||
|
||||
DEFINES += SOUNDIO_HAVE_WASAPI
|
||||
SOURCES += $$SRC_DIR/wasapi.c
|
||||
HEADERS += $$SRC_DIR/wasapi.h
|
||||
}
|
||||
macx {
|
||||
message(CoreAudio backend selected)
|
||||
|
||||
DEFINES += SOUNDIO_HAVE_COREAUDIO
|
||||
SOURCES += $$SRC_DIR/coreaudio.c
|
||||
HEADERS += $$SRC_DIR/coreaudio.h
|
||||
}
|
||||
pulseaudio {
|
||||
message(PulseAudio backend selected)
|
||||
|
||||
DEFINES += SOUNDIO_HAVE_PULSEAUDIO
|
||||
SOURCES += $$SRC_DIR/pulseaudio.c
|
||||
HEADERS += $$SRC_DIR/pulseaudio.h
|
||||
}
|
||||
alsa {
|
||||
message(ALSA backend selected)
|
||||
|
||||
DEFINES += SOUNDIO_HAVE_ALSA
|
||||
SOURCES += $$SRC_DIR/alsa.c
|
||||
HEADERS += $$SRC_DIR/alsa.h
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
<SchemaVersion>2.0</SchemaVersion>
|
||||
<OutputName>Moonlight</OutputName>
|
||||
<OutputType>Package</OutputType>
|
||||
<DefineSolutionProperties>false</DefineSolutionProperties>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<DefineConstants>Debug</DefineConstants>
|
||||
@@ -26,6 +27,10 @@
|
||||
<HintPath>$(WixExtDir)\WixFirewallExtension.dll</HintPath>
|
||||
<Name>WixFirewallExtension</Name>
|
||||
</WixExtension>
|
||||
<WixExtension Include="WixUtilExtension">
|
||||
<HintPath>$(WixExtDir)\WixUtilExtension.dll</HintPath>
|
||||
<Name>WixUtilExtension</Name>
|
||||
</WixExtension>
|
||||
</ItemGroup>
|
||||
<Import Project="$(WixTargetsPath)" Condition=" '$(WixTargetsPath)' != '' " />
|
||||
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\WiX\v3.x\Wix.targets" Condition=" '$(WixTargetsPath)' == '' AND Exists('$(MSBuildExtensionsPath32)\Microsoft\WiX\v3.x\Wix.targets') " />
|
||||
|
||||
@@ -4,11 +4,12 @@
|
||||
<?define FullName = "Moonlight Game Streaming Client" ?>
|
||||
|
||||
<?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" ?>
|
||||
|
||||
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi"
|
||||
xmlns:fire="http://schemas.microsoft.com/wix/FirewallExtension">
|
||||
xmlns:fire="http://schemas.microsoft.com/wix/FirewallExtension"
|
||||
xmlns:util="http://schemas.microsoft.com/wix/UtilExtension">
|
||||
|
||||
<Product Id="*"
|
||||
Name="$(var.FullName)"
|
||||
@@ -20,7 +21,7 @@
|
||||
<Package InstallerVersion="500" Compressed="yes" InstallScope="perMachine" />
|
||||
|
||||
<MajorUpgrade DowngradeErrorMessage="A newer version of [ProductName] is already installed." Schedule="afterInstallInitialize" />
|
||||
<MediaTemplate CompressionLevel="high" />
|
||||
<MediaTemplate CompressionLevel="high" EmbedCab="yes" />
|
||||
|
||||
<?if $(var.Platform) = x64 ?>
|
||||
<?define PlatformProgramFilesFolder = "ProgramFiles64Folder" ?>
|
||||
@@ -38,6 +39,8 @@
|
||||
</Directory>
|
||||
</Directory>
|
||||
|
||||
<Property Id="APPDATAFOLDER">%LOCALAPPDATA%\Moonlight Game Streaming Project</Property>
|
||||
|
||||
<!-- There's no way to delete a registry key on uninstall but not major upgrade, so
|
||||
we have to roll our own deletion via custom action -->
|
||||
<CustomAction Id="DeleteRegistryKey"
|
||||
@@ -57,15 +60,38 @@
|
||||
Target="[#MoonlightExe]"
|
||||
Directory="ApplicationProgramsFolder"
|
||||
WorkingDirectory="INSTALLFOLDER" />
|
||||
<RemoveFolder Id="CleanupStartMenuShortcut" Directory="ApplicationProgramsFolder" On="uninstall" />
|
||||
<util:RemoveFolderEx Id="CleanupAppDataFolder" On="uninstall" Property="APPDATAFOLDER" />
|
||||
<RegistryValue Root="HKCU" Key="Software\Moonlight Game Streaming Project" Name="Installed" Type="integer" Value="1" KeyPath="yes" />
|
||||
</Component>
|
||||
|
||||
<Component Id="MoonlightDesktopShortcut" Guid="*" Directory="INSTALLFOLDER">
|
||||
<Shortcut Id="DesktopShortcut"
|
||||
Name="$(var.ShortcutName)"
|
||||
Description="$(var.ShortcutDesc)"
|
||||
Target="[#MoonlightExe]"
|
||||
Directory="DesktopFolder"
|
||||
WorkingDirectory="INSTALLFOLDER" />
|
||||
<RemoveFolder Id="CleanupStartMenuShortcut" Directory="ApplicationProgramsFolder" On="uninstall" />
|
||||
<RemoveFolder Id="CleanupDesktopShortcut" Directory="DesktopFolder" On="uninstall" />
|
||||
<RegistryValue Root="HKCU" Key="Software\Moonlight Game Streaming Project" Name="Installed" Type="integer" Value="1" KeyPath="yes" />
|
||||
<RegistryValue Root="HKCU"
|
||||
Key="Software\Moonlight Game Streaming Project"
|
||||
Name="DesktopShortcutInstalled"
|
||||
Type="integer"
|
||||
Value="1"
|
||||
KeyPath="yes" />
|
||||
<Condition>ADDDESKTOPSHORTCUT=1</Condition>
|
||||
</Component>
|
||||
|
||||
<!-- Persist desktop shortcut's installed state to let Bundle.wxs know if
|
||||
the desktop shortcut should installed by default when upgrading the
|
||||
product -->
|
||||
<Component Id="MoonlightDesktopShortcutState" Guid="*" Directory="INSTALLFOLDER">
|
||||
<RegistryValue Root="HKCU"
|
||||
Key="Software\Moonlight Game Streaming Project"
|
||||
Name="DesktopShortcutInstallState"
|
||||
Type="integer"
|
||||
Value="[ADDDESKTOPSHORTCUT]"
|
||||
KeyPath="yes" />
|
||||
</Component>
|
||||
|
||||
<DirectoryRef Id="INSTALLFOLDER">
|
||||
@@ -81,6 +107,8 @@
|
||||
<Feature Id="ProductFeature" Title="Moonlight" Level="1" ConfigurableDirectory="INSTALLFOLDER">
|
||||
<ComponentRef Id="Moonlight" />
|
||||
<ComponentRef Id="MoonlightShortcuts" />
|
||||
<ComponentRef Id="MoonlightDesktopShortcutState" />
|
||||
<ComponentRef Id="MoonlightDesktopShortcut" />
|
||||
<ComponentGroupRef Id="MoonlightDependencies" />
|
||||
</Feature>
|
||||
</Product>
|
||||
|
||||
@@ -51,13 +51,32 @@
|
||||
<Variable Name="InstallFolder" Type="string" Value="[ProgramFilesFolder]Moonlight Game Streaming" />
|
||||
<?endif ?>
|
||||
|
||||
<!-- Define "Add desktop shortcut" -checkbox's state by defining a variable
|
||||
which has same name as the checkbox has. Value 1 means that checkbox
|
||||
is checked, 0 means that is unchecked-->
|
||||
<!-- Set checkbox's state as checked by default -->
|
||||
<Variable Name="AddDesktopShortcutCheckbox" Type="numeric" Value="1" />
|
||||
<!-- Get checkbox's state from registry if present. The registry value
|
||||
"DesktopShortcutInstallState" is set in Product.wxs. -->
|
||||
<util:RegistrySearch Variable="HasDesktopShortcutInstallStateRegKey"
|
||||
Root="HKCU"
|
||||
Key="Software\Moonlight Game Streaming Project"
|
||||
Value="DesktopShortcutInstallState"
|
||||
Result="exists" />
|
||||
<util:RegistrySearch Variable="AddDesktopShortcutCheckbox"
|
||||
Root="HKCU"
|
||||
Key="Software\Moonlight Game Streaming Project"
|
||||
Value="DesktopShortcutInstallState"
|
||||
Condition="HasDesktopShortcutInstallStateRegKey" />
|
||||
|
||||
<BootstrapperApplicationRef Id="WixStandardBootstrapperApplication.RtfLicense">
|
||||
<bal:WixStandardBootstrapperApplication
|
||||
ShowVersion="yes"
|
||||
LicenseFile="license.rtf"
|
||||
LogoFile="..\..\app\moonlight_wix.png"
|
||||
ShowFilesInUse="yes"
|
||||
LaunchTarget="[InstallFolder]\Moonlight.exe" />
|
||||
LaunchTarget="[InstallFolder]\Moonlight.exe"
|
||||
ThemeFile="RtfTheme.xml" />
|
||||
</BootstrapperApplicationRef>
|
||||
|
||||
<Chain>
|
||||
@@ -75,6 +94,7 @@
|
||||
<MsiPackage Id="Moonlight" SourceFile="$(var.Moonlight.TargetPath)" Vital="yes">
|
||||
<MsiProperty Name="INSTALLFOLDER" Value="[InstallFolder]" />
|
||||
<MsiProperty Name="REINSTALLMODE" Value="[REINSTALLMODE]" />
|
||||
<MsiProperty Name="ADDDESKTOPSHORTCUT" Value="[AddDesktopShortcutCheckbox]" />
|
||||
</MsiPackage>
|
||||
</Chain>
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
<SchemaVersion>2.0</SchemaVersion>
|
||||
<OutputName>MoonlightSetup</OutputName>
|
||||
<OutputType>Bundle</OutputType>
|
||||
<DefineSolutionProperties>false</DefineSolutionProperties>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<DefineConstants>Debug</DefineConstants>
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. -->
|
||||
<!-- This file has been copied from C:\Program Files (x86)\WiX Toolset v3.11\SDK\themes\RtfTheme.xml and then altered. -->
|
||||
|
||||
<Theme xmlns="http://wixtoolset.org/schemas/thmutil/2010">
|
||||
<Window Width="485" Height="300" HexStyle="100a0000" FontId="0">#(loc.Caption)</Window>
|
||||
<Font Id="0" Height="-12" Weight="500" Foreground="000000" Background="FFFFFF">Segoe UI</Font>
|
||||
<Font Id="1" Height="-24" Weight="500" Foreground="000000">Segoe UI</Font>
|
||||
<Font Id="2" Height="-22" Weight="500" Foreground="666666">Segoe UI</Font>
|
||||
<Font Id="3" Height="-12" Weight="500" Foreground="000000" Background="FFFFFF">Segoe UI</Font>
|
||||
<Font Id="4" Height="-12" Weight="500" Foreground="ff0000" Background="FFFFFF" Underline="yes">Segoe UI</Font>
|
||||
|
||||
<Image X="11" Y="11" Width="64" Height="64" ImageFile="logo.png" Visible="yes"/>
|
||||
<Text X="80" Y="11" Width="-11" Height="64" FontId="1" Visible="yes" DisablePrefix="yes">#(loc.Title)</Text>
|
||||
|
||||
<Page Name="Help">
|
||||
<Text X="11" Y="80" Width="-11" Height="30" FontId="2" DisablePrefix="yes">#(loc.HelpHeader)</Text>
|
||||
<Text X="11" Y="112" Width="-11" Height="-35" FontId="3" DisablePrefix="yes">#(loc.HelpText)</Text>
|
||||
<Button Name="HelpCancelButton" X="-11" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">#(loc.HelpCloseButton)</Button>
|
||||
</Page>
|
||||
<Page Name="Install">
|
||||
<Richedit Name="EulaRichedit" X="11" Y="80" Width="-11" Height="-70" TabStop="yes" FontId="0" HexStyle="0x800000" />
|
||||
<Checkbox Name="EulaAcceptCheckbox" X="-11" Y="-41" Width="260" Height="17" TabStop="yes" FontId="3" HideWhenDisabled="yes">#(loc.InstallAcceptCheckbox)</Checkbox>
|
||||
<Button Name="OptionsButton" X="-171" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0" HideWhenDisabled="yes">#(loc.InstallOptionsButton)</Button>
|
||||
<Button Name="InstallButton" X="-91" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">#(loc.InstallInstallButton)</Button>
|
||||
<Button Name="WelcomeCancelButton" X="-11" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">#(loc.InstallCloseButton)</Button>
|
||||
</Page>
|
||||
<Page Name="Options">
|
||||
<Text X="11" Y="80" Width="-11" Height="30" FontId="2" DisablePrefix="yes">#(loc.OptionsHeader)</Text>
|
||||
<Text X="11" Y="121" Width="-11" Height="17" FontId="3" DisablePrefix="yes">#(loc.OptionsLocationLabel)</Text>
|
||||
<Editbox Name="FolderEditbox" X="11" Y="143" Width="-91" Height="21" TabStop="yes" FontId="3" FileSystemAutoComplete="yes" />
|
||||
<Button Name="BrowseButton" X="-11" Y="142" Width="75" Height="23" TabStop="yes" FontId="3">#(loc.OptionsBrowseButton)</Button>
|
||||
|
||||
<!-- Added a checkbox for user to choose if shortcut is placed on
|
||||
desktop or not. Checkbox's current state (checked/unchecked) is
|
||||
read from a variable which has same name as this checkbox. The
|
||||
variable is defined in Bundle.wxs. -->
|
||||
<Checkbox Name="AddDesktopShortcutCheckbox" X="11" Y="178" Width="250" Height="17" TabStop="yes" FontId="3" HideWhenDisabled="yes">Add desktop shortcut</Checkbox>
|
||||
|
||||
<Button Name="OptionsOkButton" X="-91" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">#(loc.OptionsOkButton)</Button>
|
||||
<Button Name="OptionsCancelButton" X="-11" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">#(loc.OptionsCancelButton)</Button>
|
||||
</Page>
|
||||
<Page Name="FilesInUse">
|
||||
<Text X="11" Y="80" Width="-11" Height="30" FontId="2" DisablePrefix="yes">#(loc.FilesInUseHeader)</Text>
|
||||
<Text X="11" Y="121" Width="-11" Height="34" FontId="3" DisablePrefix="yes">#(loc.FilesInUseLabel)</Text>
|
||||
<Text Name="FilesInUseText" X="11" Y="150" Width="-11" Height="-86" FontId="3" DisablePrefix="yes" HexStyle="0x0000C000"></Text>
|
||||
|
||||
<Button Name="FilesInUseCloseRadioButton" X="11" Y="-60" Width="-11" Height="23" TabStop="yes" FontId="0" HideWhenDisabled="yes" HexStyle="0x000009">#(loc.FilesInUseCloseRadioButton)</Button>
|
||||
<Button Name="FilesInUseDontCloseRadioButton" X="11" Y="-40" Width="-11" Height="23" TabStop="yes" FontId="0" HideWhenDisabled="yes" HexStyle="0x000009">#(loc.FilesInUseDontCloseRadioButton)</Button>
|
||||
|
||||
<Button Name="FilesInUseOkButton" X="-91" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0" HideWhenDisabled="yes">#(loc.FilesInUseOkButton)</Button>
|
||||
<Button Name="FilesInUseCancelButton" X="-11" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">#(loc.FilesInUseCancelButton)</Button>
|
||||
</Page>
|
||||
<Page Name="Progress">
|
||||
<Text X="11" Y="80" Width="-11" Height="30" FontId="2" DisablePrefix="yes">#(loc.ProgressHeader)</Text>
|
||||
<Text X="11" Y="121" Width="70" Height="17" FontId="3" DisablePrefix="yes">#(loc.ProgressLabel)</Text>
|
||||
<Text Name="OverallProgressPackageText" X="85" Y="121" Width="-11" Height="17" FontId="3" DisablePrefix="yes">#(loc.OverallProgressPackageText)</Text>
|
||||
<Progressbar Name="OverallCalculatedProgressbar" X="11" Y="143" Width="-11" Height="15" />
|
||||
<Button Name="ProgressCancelButton" X="-11" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">#(loc.ProgressCancelButton)</Button>
|
||||
</Page>
|
||||
<Page Name="Modify">
|
||||
<Text X="11" Y="80" Width="-11" Height="30" FontId="2" DisablePrefix="yes">#(loc.ModifyHeader)</Text>
|
||||
<Button Name="RepairButton" X="-171" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0" HideWhenDisabled="yes">#(loc.ModifyRepairButton)</Button>
|
||||
<Button Name="UninstallButton" X="-91" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">#(loc.ModifyUninstallButton)</Button>
|
||||
<Button Name="ModifyCancelButton" X="-11" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">#(loc.ModifyCloseButton)</Button>
|
||||
</Page>
|
||||
<Page Name="Success">
|
||||
<Text Name="SuccessHeader" X="11" Y="80" Width="-11" Height="30" FontId="2" HideWhenDisabled="yes" DisablePrefix="yes">#(loc.SuccessHeader)</Text>
|
||||
<Text Name="SuccessInstallHeader" X="11" Y="80" Width="-11" Height="30" FontId="2" HideWhenDisabled="yes" DisablePrefix="yes">#(loc.SuccessInstallHeader)</Text>
|
||||
<Text Name="SuccessRepairHeader" X="11" Y="80" Width="-11" Height="30" FontId="2" HideWhenDisabled="yes" DisablePrefix="yes">#(loc.SuccessRepairHeader)</Text>
|
||||
<Text Name="SuccessUninstallHeader" X="11" Y="80" Width="-11" Height="30" FontId="2" HideWhenDisabled="yes" DisablePrefix="yes">#(loc.SuccessUninstallHeader)</Text>
|
||||
<Button Name="LaunchButton" X="-91" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0" HideWhenDisabled="yes">#(loc.SuccessLaunchButton)</Button>
|
||||
<Text Name="SuccessRestartText" X="-11" Y="-51" Width="400" Height="34" FontId="3" HideWhenDisabled="yes" DisablePrefix="yes">#(loc.SuccessRestartText)</Text>
|
||||
<Button Name="SuccessRestartButton" X="-91" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0" HideWhenDisabled="yes">#(loc.SuccessRestartButton)</Button>
|
||||
<Button Name="SuccessCancelButton" X="-11" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">#(loc.SuccessCloseButton)</Button>
|
||||
</Page>
|
||||
<Page Name="Failure">
|
||||
<Text Name="FailureHeader" X="11" Y="80" Width="-11" Height="30" FontId="2" HideWhenDisabled="yes" DisablePrefix="yes">#(loc.FailureHeader)</Text>
|
||||
<Text Name="FailureInstallHeader" X="11" Y="80" Width="-11" Height="30" FontId="2" HideWhenDisabled="yes" DisablePrefix="yes">#(loc.FailureInstallHeader)</Text>
|
||||
<Text Name="FailureUninstallHeader" X="11" Y="80" Width="-11" Height="30" FontId="2" HideWhenDisabled="yes" DisablePrefix="yes">#(loc.FailureUninstallHeader)</Text>
|
||||
<Text Name="FailureRepairHeader" X="11" Y="80" Width="-11" Height="30" FontId="2" HideWhenDisabled="yes" DisablePrefix="yes">#(loc.FailureRepairHeader)</Text>
|
||||
<Hypertext Name="FailureLogFileLink" X="11" Y="121" Width="-11" Height="42" FontId="3" TabStop="yes" HideWhenDisabled="yes">#(loc.FailureHyperlinkLogText)</Hypertext>
|
||||
<Hypertext Name="FailureMessageText" X="22" Y="163" Width="-11" Height="51" FontId="3" TabStop="yes" HideWhenDisabled="yes" />
|
||||
<Text Name="FailureRestartText" X="-11" Y="-51" Width="400" Height="34" FontId="3" HideWhenDisabled="yes" DisablePrefix="yes">#(loc.FailureRestartText)</Text>
|
||||
<Button Name="FailureRestartButton" X="-91" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0" HideWhenDisabled="yes">#(loc.FailureRestartButton)</Button>
|
||||
<Button Name="FailureCloseButton" X="-11" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">#(loc.FailureCloseButton)</Button>
|
||||
</Page>
|
||||
</Theme>
|
||||
Reference in New Issue
Block a user