mirror of
https://github.com/moonlight-stream/moonlight-qt.git
synced 2026-10-27 14:30:25 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
103625fbdd | ||
|
|
4712080a1b | ||
|
|
93c683be3a | ||
|
|
c9a7c15f98 | ||
|
|
64a08f0533 | ||
|
|
52c6455ca5 | ||
|
|
238681d72f | ||
|
|
dbcc6feb3f | ||
|
|
300b53f28c | ||
|
|
3dabb1c891 | ||
|
|
4381f7a973 | ||
|
|
77b767ae5b | ||
|
|
2c4f78133f | ||
|
|
ec2c181f4e | ||
|
|
fae98eb13a | ||
|
|
f1d117d5d7 | ||
|
|
529ac8041a | ||
|
|
391e84e0f4 | ||
|
|
6a2b717ff1 | ||
|
|
475a39cb73 | ||
|
|
4dba74e2f9 | ||
|
|
587d783955 | ||
|
|
3bfe87d516 | ||
|
|
d3e43012fb | ||
|
|
5da156b433 | ||
|
|
b2eeb63b6f | ||
|
|
a7517dace3 | ||
|
|
bd9f2d2009 | ||
|
|
78730f7235 | ||
|
|
8a2491d955 | ||
|
|
0d68faf40c | ||
|
|
957a2b2169 | ||
|
|
856318f947 | ||
|
|
4df9d6b20a | ||
|
|
8519d09141 | ||
|
|
1b4fe83739 | ||
|
|
8fc825b2eb | ||
|
|
6d0fc22ab5 | ||
|
|
9a71b4c237 | ||
|
|
9aa487418f | ||
|
|
203b2fcaca | ||
|
|
dae21f2638 | ||
|
|
af6e99cbac | ||
|
|
6d6acf4308 | ||
|
|
7dd4815edf | ||
|
|
2c068a99a3 | ||
|
|
fa93364ddd | ||
|
|
9b1eef3f63 | ||
|
|
ed6c07fd4b | ||
|
|
5ad96f5287 | ||
|
|
fa8845c7e2 | ||
|
|
b96ace7f04 | ||
|
|
234a8dc3d5 | ||
|
|
0467c2f893 | ||
|
|
762fe92933 | ||
|
|
16d105d462 | ||
|
|
8a8b624f47 | ||
|
|
9470615c63 | ||
|
|
8f0ea700a5 | ||
|
|
b7cbec0b1c | ||
|
|
9be9934b8c | ||
|
|
55f0e1e1d5 | ||
|
|
bbd784fc85 | ||
|
|
408f93732b |
No files matched your search
+2
-2
@@ -23,9 +23,9 @@
|
|||||||
<key>NSSupportsAutomaticGraphicsSwitching</key>
|
<key>NSSupportsAutomaticGraphicsSwitching</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>0.3.0</string>
|
<string>0.4.0</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>0.3.0</string>
|
<string>0.4.0</string>
|
||||||
<key>CFBundleDisplayName</key>
|
<key>CFBundleDisplayName</key>
|
||||||
<string>Moonlight</string>
|
<string>Moonlight</string>
|
||||||
</dict>
|
</dict>
|
||||||
|
|||||||
+11
-4
@@ -11,6 +11,9 @@ unix:!macx {
|
|||||||
# Support debug and release builds from command line for CI
|
# Support debug and release builds from command line for CI
|
||||||
CONFIG += debug_and_release
|
CONFIG += debug_and_release
|
||||||
|
|
||||||
|
# Ensure symbols are always generated
|
||||||
|
CONFIG += force_debug_info
|
||||||
|
|
||||||
TEMPLATE = app
|
TEMPLATE = app
|
||||||
|
|
||||||
# The following define makes your compiler emit warnings if you use
|
# The following define makes your compiler emit warnings if you use
|
||||||
@@ -34,7 +37,7 @@ win32 {
|
|||||||
LIBS += -L$$PWD/../libs/windows/lib/x64
|
LIBS += -L$$PWD/../libs/windows/lib/x64
|
||||||
}
|
}
|
||||||
|
|
||||||
LIBS += ws2_32.lib winmm.lib dxva2.lib ole32.lib gdi32.lib user32.lib d3d9.lib dwmapi.lib
|
LIBS += ws2_32.lib winmm.lib dxva2.lib ole32.lib gdi32.lib user32.lib d3d9.lib dwmapi.lib dbghelp.lib
|
||||||
}
|
}
|
||||||
macx {
|
macx {
|
||||||
INCLUDEPATH += $$PWD/../libs/mac/include $$PWD/../libs/mac/Frameworks/SDL2.framework/Versions/A/Headers
|
INCLUDEPATH += $$PWD/../libs/mac/include $$PWD/../libs/mac/Frameworks/SDL2.framework/Versions/A/Headers
|
||||||
@@ -177,7 +180,7 @@ macx {
|
|||||||
|
|
||||||
SOURCES += \
|
SOURCES += \
|
||||||
streaming/video/ffmpeg-renderers/vt.mm \
|
streaming/video/ffmpeg-renderers/vt.mm \
|
||||||
streaming/video/ffmpeg-renderers/pacer/displaylinkvsyncsource.cpp
|
streaming/video/ffmpeg-renderers/pacer/displaylinkvsyncsource.mm
|
||||||
|
|
||||||
HEADERS += \
|
HEADERS += \
|
||||||
streaming/video/ffmpeg-renderers/vt.h \
|
streaming/video/ffmpeg-renderers/vt.h \
|
||||||
@@ -231,6 +234,10 @@ unix:!macx: {
|
|||||||
}
|
}
|
||||||
win32 {
|
win32 {
|
||||||
RC_ICONS = moonlight.ico
|
RC_ICONS = moonlight.ico
|
||||||
|
QMAKE_TARGET_COMPANY = Moonlight Game Streaming Project
|
||||||
|
QMAKE_TARGET_DESCRIPTION = Moonlight Game Streaming Client
|
||||||
|
QMAKE_TARGET_PRODUCT = Moonlight
|
||||||
|
|
||||||
CONFIG -= embed_manifest_exe
|
CONFIG -= embed_manifest_exe
|
||||||
QMAKE_LFLAGS += /MANIFEST:embed /MANIFESTINPUT:$${PWD}/Moonlight.exe.manifest
|
QMAKE_LFLAGS += /MANIFEST:embed /MANIFESTINPUT:$${PWD}/Moonlight.exe.manifest
|
||||||
}
|
}
|
||||||
@@ -242,5 +249,5 @@ macx {
|
|||||||
QMAKE_BUNDLE_DATA += APP_BUNDLE_RESOURCES
|
QMAKE_BUNDLE_DATA += APP_BUNDLE_RESOURCES
|
||||||
}
|
}
|
||||||
|
|
||||||
VERSION = 0.3.0
|
VERSION = 0.4.0
|
||||||
DEFINES += VERSION_STR=\\\"0.3.0\\\"
|
DEFINES += VERSION_STR=\\\"0.4.0\\\"
|
||||||
@@ -6,8 +6,14 @@
|
|||||||
|
|
||||||
BoxArtManager::BoxArtManager(QObject *parent) :
|
BoxArtManager::BoxArtManager(QObject *parent) :
|
||||||
QObject(parent),
|
QObject(parent),
|
||||||
m_BoxArtDir(Path::getBoxArtCacheDir())
|
m_BoxArtDir(Path::getBoxArtCacheDir()),
|
||||||
|
m_ThreadPool(this)
|
||||||
{
|
{
|
||||||
|
// 4 is a good balance between fast loading for large
|
||||||
|
// app grids and not crushing GFE with tons of requests
|
||||||
|
// and causing UI jank from constantly stalling to decode
|
||||||
|
// new images.
|
||||||
|
m_ThreadPool.setMaxThreadCount(4);
|
||||||
if (!m_BoxArtDir.exists()) {
|
if (!m_BoxArtDir.exists()) {
|
||||||
m_BoxArtDir.mkpath(".");
|
m_BoxArtDir.mkpath(".");
|
||||||
}
|
}
|
||||||
@@ -41,7 +47,7 @@ QUrl BoxArtManager::loadBoxArt(NvComputer* computer, NvApp& app)
|
|||||||
// If we get here, we need to fetch asynchronously.
|
// If we get here, we need to fetch asynchronously.
|
||||||
// Kick off a worker on our thread pool to do just that.
|
// Kick off a worker on our thread pool to do just that.
|
||||||
NetworkBoxArtLoadTask* netLoadTask = new NetworkBoxArtLoadTask(this, computer, app);
|
NetworkBoxArtLoadTask* netLoadTask = new NetworkBoxArtLoadTask(this, computer, app);
|
||||||
QThreadPool::globalInstance()->start(netLoadTask);
|
m_ThreadPool.start(netLoadTask);
|
||||||
|
|
||||||
// Return the placeholder then we can notify the caller
|
// Return the placeholder then we can notify the caller
|
||||||
// later when the real image is ready.
|
// later when the real image is ready.
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ private:
|
|||||||
getFilePathForBoxArt(NvComputer* computer, int appId);
|
getFilePathForBoxArt(NvComputer* computer, int appId);
|
||||||
|
|
||||||
QDir m_BoxArtDir;
|
QDir m_BoxArtDir;
|
||||||
|
QThreadPool m_ThreadPool;
|
||||||
};
|
};
|
||||||
|
|
||||||
class NetworkBoxArtLoadTask : public QObject, public QRunnable
|
class NetworkBoxArtLoadTask : public QObject, public QRunnable
|
||||||
|
|||||||
@@ -177,6 +177,11 @@ bool NvComputer::wake()
|
|||||||
for (QString& addressString : addressList) {
|
for (QString& addressString : addressList) {
|
||||||
QHostInfo hostInfo = QHostInfo::fromName(addressString);
|
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
|
// Try all IP addresses that this string resolves to
|
||||||
for (QHostAddress& address : hostInfo.addresses()) {
|
for (QHostAddress& address : hostInfo.addresses()) {
|
||||||
QUdpSocket sock;
|
QUdpSocket sock;
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ private:
|
|||||||
|
|
||||||
QString serverInfo;
|
QString serverInfo;
|
||||||
try {
|
try {
|
||||||
serverInfo = http.getServerInfo();
|
serverInfo = http.getServerInfo(NvHTTP::NvLogLevel::NONE);
|
||||||
} catch (...) {
|
} catch (...) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -154,6 +154,7 @@ private:
|
|||||||
while (!isInterruptionRequested()) {
|
while (!isInterruptionRequested()) {
|
||||||
bool stateChanged = false;
|
bool stateChanged = false;
|
||||||
bool online = false;
|
bool online = false;
|
||||||
|
bool wasOnline = m_Computer->state == NvComputer::CS_ONLINE;
|
||||||
for (int i = 0; i < TRIES_BEFORE_OFFLINING && !online; i++) {
|
for (int i = 0; i < TRIES_BEFORE_OFFLINING && !online; i++) {
|
||||||
for (auto& address : m_Computer->uniqueAddresses()) {
|
for (auto& address : m_Computer->uniqueAddresses()) {
|
||||||
if (isInterruptionRequested()) {
|
if (isInterruptionRequested()) {
|
||||||
@@ -161,6 +162,9 @@ private:
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (tryPollComputer(address, stateChanged)) {
|
if (tryPollComputer(address, stateChanged)) {
|
||||||
|
if (!wasOnline) {
|
||||||
|
qInfo() << m_Computer->name << "is now online at" << m_Computer->activeAddress;
|
||||||
|
}
|
||||||
online = true;
|
online = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -171,6 +175,7 @@ private:
|
|||||||
// Note: we don't need to acquire the read lock here,
|
// Note: we don't need to acquire the read lock here,
|
||||||
// because we're on the writing thread.
|
// because we're on the writing thread.
|
||||||
if (!online && m_Computer->state != NvComputer::CS_OFFLINE) {
|
if (!online && m_Computer->state != NvComputer::CS_OFFLINE) {
|
||||||
|
qInfo() << m_Computer->name << "is now offline";
|
||||||
m_Computer->state = NvComputer::CS_OFFLINE;
|
m_Computer->state = NvComputer::CS_OFFLINE;
|
||||||
stateChanged = true;
|
stateChanged = true;
|
||||||
}
|
}
|
||||||
@@ -411,9 +416,11 @@ private:
|
|||||||
{
|
{
|
||||||
NvHTTP http(m_Address);
|
NvHTTP http(m_Address);
|
||||||
|
|
||||||
|
qInfo() << "Processing new PC at" << m_Address << "from" << (m_Mdns ? "mDNS" : "user");
|
||||||
|
|
||||||
QString serverInfo;
|
QString serverInfo;
|
||||||
try {
|
try {
|
||||||
serverInfo = http.getServerInfo();
|
serverInfo = http.getServerInfo(NvHTTP::NvLogLevel::VERBOSE);
|
||||||
} catch (...) {
|
} catch (...) {
|
||||||
if (!m_Mdns) {
|
if (!m_Mdns) {
|
||||||
emit computerAddCompleted(false);
|
emit computerAddCompleted(false);
|
||||||
@@ -449,6 +456,7 @@ private:
|
|||||||
|
|
||||||
// Tell our client if something changed
|
// Tell our client if something changed
|
||||||
if (changed) {
|
if (changed) {
|
||||||
|
qInfo() << existingComputer->name << "is now at" << existingComputer->activeAddress;
|
||||||
emit computerStateChanged(existingComputer);
|
emit computerStateChanged(existingComputer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-12
@@ -58,7 +58,7 @@ NvHTTP::getCurrentGame(QString serverInfo)
|
|||||||
}
|
}
|
||||||
|
|
||||||
QString
|
QString
|
||||||
NvHTTP::getServerInfo()
|
NvHTTP::getServerInfo(NvLogLevel logLevel)
|
||||||
{
|
{
|
||||||
QString serverInfo;
|
QString serverInfo;
|
||||||
|
|
||||||
@@ -70,7 +70,7 @@ NvHTTP::getServerInfo()
|
|||||||
"serverinfo",
|
"serverinfo",
|
||||||
nullptr,
|
nullptr,
|
||||||
true,
|
true,
|
||||||
true);
|
logLevel);
|
||||||
// Throws if the request failed
|
// Throws if the request failed
|
||||||
verifyResponseStatus(serverInfo);
|
verifyResponseStatus(serverInfo);
|
||||||
}
|
}
|
||||||
@@ -83,7 +83,7 @@ NvHTTP::getServerInfo()
|
|||||||
"serverinfo",
|
"serverinfo",
|
||||||
nullptr,
|
nullptr,
|
||||||
true,
|
true,
|
||||||
true);
|
logLevel);
|
||||||
verifyResponseStatus(serverInfo);
|
verifyResponseStatus(serverInfo);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -189,7 +189,7 @@ NvHTTP::quitApp()
|
|||||||
|
|
||||||
// Newer GFE versions will just return success even if quitting fails
|
// Newer GFE versions will just return success even if quitting fails
|
||||||
// if we're not the original requestor.
|
// if we're not the original requestor.
|
||||||
if (getCurrentGame(getServerInfo()) != 0) {
|
if (getCurrentGame(getServerInfo(NvHTTP::ERROR)) != 0) {
|
||||||
// Generate a synthetic GfeResponseException letting the caller know
|
// Generate a synthetic GfeResponseException letting the caller know
|
||||||
// that they can't kill someone else's stream.
|
// that they can't kill someone else's stream.
|
||||||
throw GfeHttpResponseException(599, "");
|
throw GfeHttpResponseException(599, "");
|
||||||
@@ -230,7 +230,7 @@ NvHTTP::getAppList()
|
|||||||
"applist",
|
"applist",
|
||||||
nullptr,
|
nullptr,
|
||||||
true,
|
true,
|
||||||
true);
|
NvLogLevel::ERROR);
|
||||||
verifyResponseStatus(appxml);
|
verifyResponseStatus(appxml);
|
||||||
|
|
||||||
QXmlStreamReader xmlReader(appxml);
|
QXmlStreamReader xmlReader(appxml);
|
||||||
@@ -297,7 +297,8 @@ NvHTTP::getBoxArt(int appId)
|
|||||||
"appasset",
|
"appasset",
|
||||||
"appid="+QString::number(appId)+
|
"appid="+QString::number(appId)+
|
||||||
"&AssetType=2&AssetIdx=0",
|
"&AssetType=2&AssetIdx=0",
|
||||||
true);
|
true,
|
||||||
|
NvLogLevel::VERBOSE);
|
||||||
QImage image = QImageReader(reply).read();
|
QImage image = QImageReader(reply).read();
|
||||||
delete reply;
|
delete reply;
|
||||||
|
|
||||||
@@ -344,9 +345,9 @@ NvHTTP::openConnectionToString(QUrl baseUrl,
|
|||||||
QString command,
|
QString command,
|
||||||
QString arguments,
|
QString arguments,
|
||||||
bool enableTimeout,
|
bool enableTimeout,
|
||||||
bool suppressLogging)
|
NvLogLevel logLevel)
|
||||||
{
|
{
|
||||||
QNetworkReply* reply = openConnection(baseUrl, command, arguments, enableTimeout, suppressLogging);
|
QNetworkReply* reply = openConnection(baseUrl, command, arguments, enableTimeout, logLevel);
|
||||||
QString ret;
|
QString ret;
|
||||||
|
|
||||||
QTextStream stream(reply);
|
QTextStream stream(reply);
|
||||||
@@ -362,7 +363,7 @@ NvHTTP::openConnection(QUrl baseUrl,
|
|||||||
QString command,
|
QString command,
|
||||||
QString arguments,
|
QString arguments,
|
||||||
bool enableTimeout,
|
bool enableTimeout,
|
||||||
bool suppressLogging)
|
NvLogLevel logLevel)
|
||||||
{
|
{
|
||||||
// Build a URL for the request
|
// Build a URL for the request
|
||||||
QUrl url(baseUrl);
|
QUrl url(baseUrl);
|
||||||
@@ -388,7 +389,7 @@ NvHTTP::openConnection(QUrl baseUrl,
|
|||||||
{
|
{
|
||||||
QTimer::singleShot(REQUEST_TIMEOUT_MS, &loop, SLOT(quit()));
|
QTimer::singleShot(REQUEST_TIMEOUT_MS, &loop, SLOT(quit()));
|
||||||
}
|
}
|
||||||
if (!suppressLogging) {
|
if (logLevel >= NvLogLevel::VERBOSE) {
|
||||||
qInfo() << "Executing request:" << url.toString();
|
qInfo() << "Executing request:" << url.toString();
|
||||||
}
|
}
|
||||||
loop.exec(QEventLoop::ExcludeUserInputEvents);
|
loop.exec(QEventLoop::ExcludeUserInputEvents);
|
||||||
@@ -396,7 +397,7 @@ NvHTTP::openConnection(QUrl baseUrl,
|
|||||||
// Abort the request if it timed out
|
// Abort the request if it timed out
|
||||||
if (!reply->isFinished())
|
if (!reply->isFinished())
|
||||||
{
|
{
|
||||||
if (!suppressLogging) {
|
if (logLevel >= NvLogLevel::ERROR) {
|
||||||
qWarning() << "Aborting timed out request for" << url.toString();
|
qWarning() << "Aborting timed out request for" << url.toString();
|
||||||
}
|
}
|
||||||
reply->abort();
|
reply->abort();
|
||||||
@@ -409,7 +410,7 @@ NvHTTP::openConnection(QUrl baseUrl,
|
|||||||
// Handle error
|
// Handle error
|
||||||
if (reply->error() != QNetworkReply::NoError)
|
if (reply->error() != QNetworkReply::NoError)
|
||||||
{
|
{
|
||||||
if (!suppressLogging) {
|
if (logLevel >= NvLogLevel::ERROR) {
|
||||||
qWarning() << command << " request failed with error " << reply->error();
|
qWarning() << command << " request failed with error " << reply->error();
|
||||||
}
|
}
|
||||||
GfeHttpResponseException exception(reply->error(), reply->errorString());
|
GfeHttpResponseException exception(reply->error(), reply->errorString());
|
||||||
|
|||||||
@@ -80,6 +80,12 @@ private:
|
|||||||
class NvHTTP
|
class NvHTTP
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
enum NvLogLevel {
|
||||||
|
NONE,
|
||||||
|
ERROR,
|
||||||
|
VERBOSE
|
||||||
|
};
|
||||||
|
|
||||||
explicit NvHTTP(QString address);
|
explicit NvHTTP(QString address);
|
||||||
|
|
||||||
static
|
static
|
||||||
@@ -87,7 +93,7 @@ public:
|
|||||||
getCurrentGame(QString serverInfo);
|
getCurrentGame(QString serverInfo);
|
||||||
|
|
||||||
QString
|
QString
|
||||||
getServerInfo();
|
getServerInfo(NvLogLevel logLevel);
|
||||||
|
|
||||||
static
|
static
|
||||||
void
|
void
|
||||||
@@ -108,7 +114,7 @@ public:
|
|||||||
QString command,
|
QString command,
|
||||||
QString arguments,
|
QString arguments,
|
||||||
bool enableTimeout,
|
bool enableTimeout,
|
||||||
bool suppressLogging = false);
|
NvLogLevel logLevel = NvLogLevel::VERBOSE);
|
||||||
|
|
||||||
static
|
static
|
||||||
QVector<int>
|
QVector<int>
|
||||||
@@ -145,7 +151,7 @@ private:
|
|||||||
QString command,
|
QString command,
|
||||||
QString arguments,
|
QString arguments,
|
||||||
bool enableTimeout,
|
bool enableTimeout,
|
||||||
bool suppressLogging = false);
|
NvLogLevel logLevel);
|
||||||
|
|
||||||
QString m_Address;
|
QString m_Address;
|
||||||
QNetworkAccessManager m_Nam;
|
QNetworkAccessManager m_Nam;
|
||||||
|
|||||||
@@ -33,6 +33,27 @@
|
|||||||
</screenshots>
|
</screenshots>
|
||||||
|
|
||||||
<releases>
|
<releases>
|
||||||
|
<release version="0.4.0" date="2018-09-08">
|
||||||
|
<description>
|
||||||
|
<p>New features:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Added option to enable (unsupported) 90/120 FPS streaming on 60 Hz monitors</li>
|
||||||
|
<li>Added refresh rate matching for better frame pacing on high refresh rate monitors</li>
|
||||||
|
<li>The streaming window now appears on the same monitor the UI was displayed on</li>
|
||||||
|
<li>The streaming window is now centered on the target monitor</li>
|
||||||
|
<li>Display a dialog when the connection is terminated by the remote PC</li>
|
||||||
|
<li>Added option to select between full-screen exclusive and borderless windowed mode</li>
|
||||||
|
</ul>
|
||||||
|
<p>Bugfixes:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Fixed streaming with no audio device</li>
|
||||||
|
<li>Fixed mouse lag with some high polling rate mice</li>
|
||||||
|
<li>Improved UI performance on the app grid page</li>
|
||||||
|
<li>1440p and 4K 120 FPS performance is improved to avoid encoder bottlenecks</li>
|
||||||
|
<li>Autodetect audio configuration option removed because it was broken</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
</release>
|
||||||
<release version="0.3.0" date="2018-08-21">
|
<release version="0.3.0" date="2018-08-21">
|
||||||
<description>
|
<description>
|
||||||
<p>Bugfixes:</p>
|
<p>Bugfixes:</p>
|
||||||
|
|||||||
+39
-44
@@ -1,5 +1,5 @@
|
|||||||
import QtQuick 2.9
|
import QtQuick 2.9
|
||||||
import QtQuick.Dialogs 1.3
|
import QtQuick.Dialogs 1.2
|
||||||
import QtQuick.Controls 2.2
|
import QtQuick.Controls 2.2
|
||||||
|
|
||||||
import AppModel 1.0
|
import AppModel 1.0
|
||||||
@@ -16,12 +16,7 @@ GridView {
|
|||||||
anchors.topMargin: 20
|
anchors.topMargin: 20
|
||||||
anchors.rightMargin: 5
|
anchors.rightMargin: 5
|
||||||
anchors.bottomMargin: 5
|
anchors.bottomMargin: 5
|
||||||
cellWidth: 225; cellHeight: 350;
|
cellWidth: 225; cellHeight: 385;
|
||||||
focus: true
|
|
||||||
|
|
||||||
// Cache delegates for 1000px in both directions to improve
|
|
||||||
// scrolling and resizing performance
|
|
||||||
cacheBuffer: 1000
|
|
||||||
|
|
||||||
// The StackView will trigger a visibility change when
|
// The StackView will trigger a visibility change when
|
||||||
// we're pushed onto it, causing our onVisibleChanged
|
// we're pushed onto it, causing our onVisibleChanged
|
||||||
@@ -52,12 +47,13 @@ GridView {
|
|||||||
|
|
||||||
model: appModel
|
model: appModel
|
||||||
|
|
||||||
delegate: Item {
|
delegate: ItemDelegate {
|
||||||
width: 200; height: 300;
|
width: 200; height: 335;
|
||||||
|
|
||||||
Image {
|
Image {
|
||||||
id: appIcon
|
id: appIcon
|
||||||
anchors.horizontalCenter: parent.horizontalCenter;
|
anchors.horizontalCenter: parent.horizontalCenter;
|
||||||
|
y: 20
|
||||||
source: model.boxart
|
source: model.boxart
|
||||||
sourceSize {
|
sourceSize {
|
||||||
width: 150
|
width: 150
|
||||||
@@ -104,46 +100,17 @@ GridView {
|
|||||||
stackView.push(segue)
|
stackView.push(segue)
|
||||||
}
|
}
|
||||||
|
|
||||||
MessageDialog {
|
onClicked: {
|
||||||
id: quitAppDialog
|
// Nothing is running or this app is running
|
||||||
modality:Qt.WindowModal
|
launchOrResumeSelectedApp()
|
||||||
property string appName : "";
|
|
||||||
property bool segueToStream : false
|
|
||||||
text:"Are you sure you want to quit " + appName +"? Any unsaved progress will be lost."
|
|
||||||
standardButtons: StandardButton.Yes | StandardButton.No
|
|
||||||
onYes: {
|
|
||||||
var component = Qt.createComponent("QuitSegue.qml")
|
|
||||||
var params = {"appName": appName}
|
|
||||||
if (segueToStream) {
|
|
||||||
// Store the session and app name if we're going to stream after
|
|
||||||
// successfully quitting the old app.
|
|
||||||
params.nextAppName = model.name
|
|
||||||
params.nextSession = appModel.createSessionForApp(index)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
params.nextAppName = null
|
|
||||||
params.nextSession = null
|
|
||||||
}
|
|
||||||
|
|
||||||
stackView.push(component.createObject(stackView, params))
|
|
||||||
|
|
||||||
// Trigger the quit after pushing the quit segue on screen
|
|
||||||
appModel.quitRunningApp()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
acceptedButtons: Qt.RightButton
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if (mouse.button === Qt.LeftButton) {
|
// Right click
|
||||||
// Nothing is running or this app is running
|
appContextMenu.open()
|
||||||
launchOrResumeSelectedApp()
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Right click
|
|
||||||
appContextMenu.open()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,6 +137,34 @@ GridView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 params = {"appName": appName}
|
||||||
|
if (segueToStream) {
|
||||||
|
// Store the session and app name if we're going to stream after
|
||||||
|
// successfully quitting the old app.
|
||||||
|
params.nextAppName = model.name
|
||||||
|
params.nextSession = appModel.createSessionForApp(index)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
params.nextAppName = null
|
||||||
|
params.nextSession = null
|
||||||
|
}
|
||||||
|
|
||||||
|
stackView.push(component.createObject(stackView, params))
|
||||||
|
|
||||||
|
// Trigger the quit after pushing the quit segue on screen
|
||||||
|
appModel.quitRunningApp()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ScrollBar.vertical: ScrollBar {
|
ScrollBar.vertical: ScrollBar {
|
||||||
parent: appGrid.parent
|
parent: appGrid.parent
|
||||||
anchors {
|
anchors {
|
||||||
|
|||||||
+40
-42
@@ -1,6 +1,6 @@
|
|||||||
import QtQuick 2.9
|
import QtQuick 2.9
|
||||||
import QtQuick.Controls 2.2
|
import QtQuick.Controls 2.2
|
||||||
import QtQuick.Dialogs 1.3
|
import QtQuick.Dialogs 1.2
|
||||||
import QtQuick.Layouts 1.3
|
import QtQuick.Layouts 1.3
|
||||||
import QtQuick.Window 2.2
|
import QtQuick.Window 2.2
|
||||||
|
|
||||||
@@ -19,7 +19,6 @@ GridView {
|
|||||||
anchors.rightMargin: 5
|
anchors.rightMargin: 5
|
||||||
anchors.bottomMargin: 5
|
anchors.bottomMargin: 5
|
||||||
cellWidth: 350; cellHeight: 350;
|
cellWidth: 350; cellHeight: 350;
|
||||||
focus: true
|
|
||||||
objectName: "Computers"
|
objectName: "Computers"
|
||||||
|
|
||||||
// The StackView will trigger a visibility change when
|
// The StackView will trigger a visibility change when
|
||||||
@@ -49,7 +48,7 @@ GridView {
|
|||||||
pairDialog.close()
|
pairDialog.close()
|
||||||
|
|
||||||
// Display a failed dialog if we got an error
|
// Display a failed dialog if we got an error
|
||||||
if (error !== null) {
|
if (error !== undefined) {
|
||||||
errorDialog.text = error
|
errorDialog.text = error
|
||||||
errorDialog.open()
|
errorDialog.open()
|
||||||
}
|
}
|
||||||
@@ -73,7 +72,7 @@ GridView {
|
|||||||
|
|
||||||
model: computerModel
|
model: computerModel
|
||||||
|
|
||||||
delegate: Item {
|
delegate: ItemDelegate {
|
||||||
width: 300; height: 300;
|
width: 300; height: 300;
|
||||||
|
|
||||||
Image {
|
Image {
|
||||||
@@ -142,47 +141,46 @@ 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) {
|
||||||
|
pcContextMenu.open()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
acceptedButtons: Qt.LeftButton | Qt.RightButton;
|
acceptedButtons: Qt.RightButton;
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if(mouse.button === Qt.LeftButton) {
|
// right click
|
||||||
if (model.addPc) {
|
if (!model.addPc) { // but only for actual PCs, not the add-pc option
|
||||||
addPcDialog.open()
|
pcContextMenu.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) {
|
|
||||||
pcContextMenu.open()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
else { // right click
|
|
||||||
if(!model.addPc) { // but only for actual PCs, not the add-pc option
|
|
||||||
pcContextMenu.open()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import QtQuick 2.0
|
import QtQuick 2.0
|
||||||
import QtQuick.Controls 2.2
|
import QtQuick.Controls 2.2
|
||||||
import QtQuick.Dialogs 1.3
|
import QtQuick.Dialogs 1.2
|
||||||
|
|
||||||
import ComputerManager 1.0
|
import ComputerManager 1.0
|
||||||
import Session 1.0
|
import Session 1.0
|
||||||
|
|||||||
+193
-81
@@ -56,37 +56,51 @@ ScrollView {
|
|||||||
// ignore setting the index at first, and actually set it when the component is loaded
|
// ignore setting the index at first, and actually set it when the component is loaded
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
// Add native resolutions for all attached displays
|
// Add native resolutions for all attached displays
|
||||||
for (var displayIndex = 0;; displayIndex++) {
|
var done = false
|
||||||
var screenRect = prefs.getDisplayResolution(displayIndex)
|
for (var displayIndex = 0; !done; displayIndex++) {
|
||||||
if (screenRect.width === 0) {
|
for (var displayResIndex = 0; displayResIndex < 2; displayResIndex++) {
|
||||||
// Exceeded max count of displays
|
var screenRect;
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
var indexToAdd = 0
|
// Some platforms have different desktop resolutions
|
||||||
for (var j = 0; j < resolutionComboBox.count; j++) {
|
// and native resolutions (like macOS with Retina displays)
|
||||||
var existing_width = parseInt(resolutionListModel.get(j).video_width);
|
if (displayResIndex == 0) {
|
||||||
var existing_height = parseInt(resolutionListModel.get(j).video_height);
|
screenRect = prefs.getDesktopResolution(displayIndex)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
screenRect = prefs.getNativeResolution(displayIndex)
|
||||||
|
}
|
||||||
|
|
||||||
if (screenRect.width === existing_width && screenRect.height === existing_height) {
|
if (screenRect.width === 0) {
|
||||||
// Duplicate entry, skip
|
// Exceeded max count of displays
|
||||||
indexToAdd = -1
|
done = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
else if (screenRect.width * screenRect.height > existing_width * existing_height) {
|
|
||||||
// Candidate entrypoint after this entry
|
|
||||||
indexToAdd = j + 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert this display's resolution if it's not a duplicate
|
var indexToAdd = 0
|
||||||
if (indexToAdd >= 0) {
|
for (var j = 0; j < resolutionComboBox.count; j++) {
|
||||||
resolutionListModel.insert(indexToAdd,
|
var existing_width = parseInt(resolutionListModel.get(j).video_width);
|
||||||
{
|
var existing_height = parseInt(resolutionListModel.get(j).video_height);
|
||||||
"text": "Native ("+screenRect.width+"x"+screenRect.height+")",
|
|
||||||
"video_width": ""+screenRect.width,
|
if (screenRect.width === existing_width && screenRect.height === existing_height) {
|
||||||
"video_height": ""+screenRect.height
|
// Duplicate entry, skip
|
||||||
})
|
indexToAdd = -1
|
||||||
|
break
|
||||||
|
}
|
||||||
|
else if (screenRect.width * screenRect.height > existing_width * existing_height) {
|
||||||
|
// Candidate entrypoint after this entry
|
||||||
|
indexToAdd = j + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert this display's resolution if it's not a duplicate
|
||||||
|
if (indexToAdd >= 0) {
|
||||||
|
resolutionListModel.insert(indexToAdd,
|
||||||
|
{
|
||||||
|
"text": "Native ("+screenRect.width+"x"+screenRect.height+")",
|
||||||
|
"video_width": ""+screenRect.width,
|
||||||
|
"video_height": ""+screenRect.height
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,7 +109,7 @@ ScrollView {
|
|||||||
var saved_width = prefs.width
|
var saved_width = prefs.width
|
||||||
var saved_height = prefs.height
|
var saved_height = prefs.height
|
||||||
currentIndex = 0
|
currentIndex = 0
|
||||||
for (var i = 0; i < resolutionComboBox.count; i++) {
|
for (var i = 0; i < resolutionListModel.count; i++) {
|
||||||
var el_width = parseInt(resolutionListModel.get(i).video_width);
|
var el_width = parseInt(resolutionListModel.get(i).video_width);
|
||||||
var el_height = parseInt(resolutionListModel.get(i).video_height);
|
var el_height = parseInt(resolutionListModel.get(i).video_height);
|
||||||
|
|
||||||
@@ -104,6 +118,9 @@ ScrollView {
|
|||||||
currentIndex = i
|
currentIndex = i
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Persist the selected value
|
||||||
|
activated(currentIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
id: resolutionComboBox
|
id: resolutionComboBox
|
||||||
@@ -136,59 +153,97 @@ ScrollView {
|
|||||||
}
|
}
|
||||||
// ::onActivated must be used, as it only listens for when the index is changed by a human
|
// ::onActivated must be used, as it only listens for when the index is changed by a human
|
||||||
onActivated : {
|
onActivated : {
|
||||||
prefs.width = parseInt(resolutionListModel.get(currentIndex).video_width)
|
var selectedWidth = parseInt(resolutionListModel.get(currentIndex).video_width)
|
||||||
prefs.height = parseInt(resolutionListModel.get(currentIndex).video_height)
|
var selectedHeight = parseInt(resolutionListModel.get(currentIndex).video_height)
|
||||||
|
|
||||||
prefs.bitrateKbps = prefs.getDefaultBitrate(prefs.width, prefs.height, prefs.fps);
|
// Only modify the bitrate if the values actually changed
|
||||||
slider.value = prefs.bitrateKbps
|
if (prefs.width !== selectedWidth || prefs.height !== selectedHeight) {
|
||||||
|
prefs.width = selectedWidth
|
||||||
|
prefs.height = selectedHeight
|
||||||
|
|
||||||
|
prefs.bitrateKbps = prefs.getDefaultBitrate(prefs.width, prefs.height, prefs.fps);
|
||||||
|
slider.value = prefs.bitrateKbps
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ComboBox {
|
ComboBox {
|
||||||
// ignore setting the index at first, and actually set it when the component is loaded
|
function createModel() {
|
||||||
Component.onCompleted: {
|
var fpsListModel = Qt.createQmlObject('import QtQuick 2.0; ListModel {}', parent, '')
|
||||||
|
|
||||||
// Get the max supported FPS on this system
|
// Get the max supported FPS on this system
|
||||||
var max_fps = prefs.getMaximumStreamingFrameRate();
|
var max_fps = prefs.getMaximumStreamingFrameRate();
|
||||||
|
|
||||||
|
// Default entries
|
||||||
|
fpsListModel.append({"text": "30 FPS", "video_fps": "30"})
|
||||||
|
fpsListModel.append({"text": "60 FPS", "video_fps": "60"})
|
||||||
|
|
||||||
|
// Add unsupported FPS values that come before the display max FPS
|
||||||
|
if (prefs.unsupportedFps) {
|
||||||
|
if (max_fps > 90) {
|
||||||
|
fpsListModel.append({"text": "90 FPS (Unsupported)", "video_fps": "90"})
|
||||||
|
}
|
||||||
|
if (max_fps > 120) {
|
||||||
|
fpsListModel.append({"text": "120 FPS (Unsupported)", "video_fps": "120"})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Use 64 as the cutoff for adding a separate option to
|
// Use 64 as the cutoff for adding a separate option to
|
||||||
// handle wonky displays that report just over 60 Hz.
|
// handle wonky displays that report just over 60 Hz.
|
||||||
if (max_fps > 64) {
|
if (max_fps > 64) {
|
||||||
fpsListModel.append({"text": max_fps+" FPS", "video_fps": ""+max_fps})
|
fpsListModel.append({"text": max_fps+" FPS", "video_fps": ""+max_fps})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add unsupported FPS values that come after the display max FPS
|
||||||
|
if (prefs.unsupportedFps) {
|
||||||
|
if (max_fps < 90) {
|
||||||
|
fpsListModel.append({"text": "90 FPS (Unsupported)", "video_fps": "90"})
|
||||||
|
}
|
||||||
|
if (max_fps < 120) {
|
||||||
|
fpsListModel.append({"text": "120 FPS (Unsupported)", "video_fps": "120"})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fpsListModel
|
||||||
|
}
|
||||||
|
|
||||||
|
function reinitialize() {
|
||||||
|
model = createModel()
|
||||||
|
|
||||||
var saved_fps = prefs.fps
|
var saved_fps = prefs.fps
|
||||||
currentIndex = 0
|
currentIndex = 0
|
||||||
for (var i = 0; i < fpsComboBox.count; i++) {
|
for (var i = 0; i < model.count; i++) {
|
||||||
var el_fps = parseInt(fpsListModel.get(i).video_fps);
|
var el_fps = parseInt(model.get(i).video_fps);
|
||||||
|
|
||||||
// Pick the highest value lesser or equal to the saved FPS
|
// Pick the highest value lesser or equal to the saved FPS
|
||||||
if (saved_fps >= el_fps) {
|
if (saved_fps >= el_fps) {
|
||||||
currentIndex = i
|
currentIndex = i
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Persist the selected value
|
||||||
|
activated(currentIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ignore setting the index at first, and actually set it when the component is loaded
|
||||||
|
Component.onCompleted: {
|
||||||
|
reinitialize()
|
||||||
}
|
}
|
||||||
|
|
||||||
id: fpsComboBox
|
id: fpsComboBox
|
||||||
textRole: "text"
|
textRole: "text"
|
||||||
model: ListModel {
|
width: 250
|
||||||
id: fpsListModel
|
|
||||||
ListElement {
|
|
||||||
text: "30 FPS"
|
|
||||||
video_fps: "30"
|
|
||||||
}
|
|
||||||
ListElement {
|
|
||||||
text: "60 FPS"
|
|
||||||
video_fps: "60"
|
|
||||||
}
|
|
||||||
// A higher value may be added at runtime
|
|
||||||
// based on the attached display refresh rate
|
|
||||||
}
|
|
||||||
// ::onActivated must be used, as it only listens for when the index is changed by a human
|
// ::onActivated must be used, as it only listens for when the index is changed by a human
|
||||||
onActivated : {
|
onActivated : {
|
||||||
prefs.fps = parseInt(fpsListModel.get(currentIndex).video_fps)
|
var selectedFps = parseInt(model.get(currentIndex).video_fps)
|
||||||
|
|
||||||
prefs.bitrateKbps = prefs.getDefaultBitrate(prefs.width, prefs.height, prefs.fps);
|
// Only modify the bitrate if the values actually changed
|
||||||
slider.value = prefs.bitrateKbps
|
if (prefs.fps !== selectedFps) {
|
||||||
|
prefs.fps = selectedFps
|
||||||
|
|
||||||
|
prefs.bitrateKbps = prefs.getDefaultBitrate(prefs.width, prefs.height, prefs.fps);
|
||||||
|
slider.value = prefs.bitrateKbps
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -230,26 +285,60 @@ ScrollView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Row {
|
Label {
|
||||||
CheckBox {
|
width: parent.width
|
||||||
id: fullScreenCheck
|
id: windowModeTitle
|
||||||
text: "<font color=\"white\">Full-screen</font>"
|
text: qsTr("Display mode")
|
||||||
font.pointSize: 12
|
font.pointSize: 12
|
||||||
checked: prefs.fullScreen
|
wrapMode: Text.Wrap
|
||||||
onCheckedChanged: {
|
color: "white"
|
||||||
prefs.fullScreen = checked
|
}
|
||||||
|
|
||||||
|
ComboBox {
|
||||||
|
// ignore setting the index at first, and actually set it when the component is loaded
|
||||||
|
Component.onCompleted: {
|
||||||
|
var savedWm = prefs.windowMode
|
||||||
|
currentIndex = 0
|
||||||
|
for (var i = 0; i < windowModeListModel.count; i++) {
|
||||||
|
var thisWm = windowModeListModel.get(i).val;
|
||||||
|
if (savedWm === thisWm) {
|
||||||
|
currentIndex = i
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
activated(currentIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
CheckBox {
|
id: windowModeComboBox
|
||||||
id: vsyncCheck
|
width: Math.min(bitrateDesc.implicitWidth, parent.width)
|
||||||
text: "<font color=\"white\">Enable V-Sync</font>"
|
textRole: "text"
|
||||||
font.pointSize: 12
|
model: ListModel {
|
||||||
visible: fullScreenCheck.checked
|
id: windowModeListModel
|
||||||
checked: prefs.enableVsync
|
ListElement {
|
||||||
onCheckedChanged: {
|
text: "Full-screen (Recommended)"
|
||||||
prefs.enableVsync = checked
|
val: StreamingPreferences.WM_FULLSCREEN
|
||||||
}
|
}
|
||||||
|
ListElement {
|
||||||
|
text: "Borderless windowed"
|
||||||
|
val: StreamingPreferences.WM_FULLSCREEN_DESKTOP
|
||||||
|
}
|
||||||
|
ListElement {
|
||||||
|
text: "Windowed"
|
||||||
|
val: StreamingPreferences.WM_WINDOWED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onActivated: {
|
||||||
|
prefs.windowMode = windowModeListModel.get(currentIndex).val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CheckBox {
|
||||||
|
id: vsyncCheck
|
||||||
|
text: "<font color=\"white\">Enable V-Sync</font>"
|
||||||
|
font.pointSize: 12
|
||||||
|
checked: prefs.enableVsync
|
||||||
|
onCheckedChanged: {
|
||||||
|
prefs.enableVsync = checked
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -281,12 +370,14 @@ ScrollView {
|
|||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
var saved_audio = prefs.audioConfig
|
var saved_audio = prefs.audioConfig
|
||||||
currentIndex = 0
|
currentIndex = 0
|
||||||
for(var i = 0; i < audioListModel.count; i++) {
|
for (var i = 0; i < audioListModel.count; i++) {
|
||||||
var el_audio= audioListModel.get(i).val;
|
var el_audio = audioListModel.get(i).val;
|
||||||
if(saved_audio === el_audio) {
|
if (saved_audio === el_audio) {
|
||||||
currentIndex = i
|
currentIndex = i
|
||||||
}
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
activated(currentIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
id: audioComboBox
|
id: audioComboBox
|
||||||
@@ -294,10 +385,14 @@ ScrollView {
|
|||||||
textRole: "text"
|
textRole: "text"
|
||||||
model: ListModel {
|
model: ListModel {
|
||||||
id: audioListModel
|
id: audioListModel
|
||||||
ListElement {
|
// Detection of audio channels only works on Windows
|
||||||
|
// and even so, it's still unreliable because audio devices
|
||||||
|
// lie about how many channels they can support. Hide the
|
||||||
|
// option to autodetect until this is resolved.
|
||||||
|
/*ListElement {
|
||||||
text: "Autodetect"
|
text: "Autodetect"
|
||||||
val: StreamingPreferences.AC_AUTO
|
val: StreamingPreferences.AC_AUTO
|
||||||
}
|
}*/
|
||||||
ListElement {
|
ListElement {
|
||||||
text: "Stereo"
|
text: "Stereo"
|
||||||
val: StreamingPreferences.AC_FORCE_STEREO
|
val: StreamingPreferences.AC_FORCE_STEREO
|
||||||
@@ -404,12 +499,14 @@ ScrollView {
|
|||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
var saved_vds = prefs.videoDecoderSelection
|
var saved_vds = prefs.videoDecoderSelection
|
||||||
currentIndex = 0
|
currentIndex = 0
|
||||||
for(var i = 0; i < decoderListModel.count; i++) {
|
for (var i = 0; i < decoderListModel.count; i++) {
|
||||||
var el_vds = decoderListModel.get(i).val;
|
var el_vds = decoderListModel.get(i).val;
|
||||||
if(saved_vds === el_vds) {
|
if (saved_vds === el_vds) {
|
||||||
currentIndex = i
|
currentIndex = i
|
||||||
}
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
activated(currentIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
id: decoderComboBox
|
id: decoderComboBox
|
||||||
@@ -452,10 +549,12 @@ ScrollView {
|
|||||||
currentIndex = 0
|
currentIndex = 0
|
||||||
for(var i = 0; i < codecListModel.count; i++) {
|
for(var i = 0; i < codecListModel.count; i++) {
|
||||||
var el_vcc = codecListModel.get(i).val;
|
var el_vcc = codecListModel.get(i).val;
|
||||||
if(saved_vcc === el_vcc) {
|
if (saved_vcc === el_vcc) {
|
||||||
currentIndex = i
|
currentIndex = i
|
||||||
}
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
activated(currentIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
id: codecComboBox
|
id: codecComboBox
|
||||||
@@ -489,6 +588,19 @@ 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import QtQuick 2.0
|
import QtQuick 2.0
|
||||||
import QtQuick.Controls 2.2
|
import QtQuick.Controls 2.2
|
||||||
import QtQuick.Dialogs 1.3
|
import QtQuick.Dialogs 1.2
|
||||||
|
import QtQuick.Window 2.2
|
||||||
|
|
||||||
import Session 1.0
|
import Session 1.0
|
||||||
|
|
||||||
@@ -69,7 +70,7 @@ Item {
|
|||||||
session.displayLaunchWarning.connect(displayLaunchWarning)
|
session.displayLaunchWarning.connect(displayLaunchWarning)
|
||||||
|
|
||||||
// Run the streaming session to completion
|
// Run the streaming session to completion
|
||||||
session.exec()
|
session.exec(Screen.virtualX, Screen.virtualY)
|
||||||
|
|
||||||
// Show the Qt window again after streaming
|
// Show the Qt window again after streaming
|
||||||
window.visible = true
|
window.visible = true
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
#include "computermodel.h"
|
#include "computermodel.h"
|
||||||
|
|
||||||
|
#include <QThreadPool>
|
||||||
|
|
||||||
ComputerModel::ComputerModel(QObject* object)
|
ComputerModel::ComputerModel(QObject* object)
|
||||||
: QAbstractListModel(object) {}
|
: QAbstractListModel(object) {}
|
||||||
|
|
||||||
@@ -119,11 +121,27 @@ void ComputerModel::deleteComputer(int computerIndex)
|
|||||||
endRemoveRows();
|
endRemoveRows();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ComputerModel::wakeComputer(int computerIndex)
|
class DeferredWakeHostTask : public QRunnable
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
DeferredWakeHostTask(NvComputer* computer)
|
||||||
|
: m_Computer(computer) {}
|
||||||
|
|
||||||
|
void run()
|
||||||
|
{
|
||||||
|
m_Computer->wake();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
NvComputer* m_Computer;
|
||||||
|
};
|
||||||
|
|
||||||
|
void ComputerModel::wakeComputer(int computerIndex)
|
||||||
{
|
{
|
||||||
Q_ASSERT(computerIndex < m_Computers.count());
|
Q_ASSERT(computerIndex < m_Computers.count());
|
||||||
|
|
||||||
return m_Computers[computerIndex]->wake();
|
DeferredWakeHostTask* wakeTask = new DeferredWakeHostTask(m_Computers[computerIndex]);
|
||||||
|
QThreadPool::globalInstance()->start(wakeTask);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ComputerModel::pairComputer(int computerIndex, QString pin)
|
void ComputerModel::pairComputer(int computerIndex, QString pin)
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ public:
|
|||||||
|
|
||||||
Q_INVOKABLE void pairComputer(int computerIndex, QString pin);
|
Q_INVOKABLE void pairComputer(int computerIndex, QString pin);
|
||||||
|
|
||||||
Q_INVOKABLE bool wakeComputer(int computerIndex);
|
Q_INVOKABLE void wakeComputer(int computerIndex);
|
||||||
|
|
||||||
Q_INVOKABLE Session* createSessionForCurrentGame(int computerIndex);
|
Q_INVOKABLE Session* createSessionForCurrentGame(int computerIndex);
|
||||||
|
|
||||||
|
|||||||
+49
-32
@@ -61,6 +61,8 @@ ApplicationWindow {
|
|||||||
|
|
||||||
header: ToolBar {
|
header: ToolBar {
|
||||||
id: toolBar
|
id: toolBar
|
||||||
|
anchors.topMargin: 5
|
||||||
|
anchors.bottomMargin: 5
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
spacing: 20
|
spacing: 20
|
||||||
@@ -70,14 +72,19 @@ ApplicationWindow {
|
|||||||
// Only make the button visible if the user has navigated somewhere.
|
// Only make the button visible if the user has navigated somewhere.
|
||||||
visible: stackView.depth > 1
|
visible: stackView.depth > 1
|
||||||
|
|
||||||
icon.source: "qrc:/res/arrow_left.svg"
|
// FIXME: We're using an Image here rather than icon.source because
|
||||||
onClicked: stackView.pop()
|
// icons don't work on Qt 5.9 LTS.
|
||||||
|
Image {
|
||||||
Menu {
|
source: "qrc:/res/arrow_left.svg"
|
||||||
id: backButton
|
anchors.centerIn: parent
|
||||||
x: parent.width - width
|
sourceSize {
|
||||||
transformOrigin: Menu.TopRight
|
// 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()
|
||||||
}
|
}
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
@@ -94,8 +101,15 @@ ApplicationWindow {
|
|||||||
property string browserUrl: ""
|
property string browserUrl: ""
|
||||||
|
|
||||||
id: updateButton
|
id: updateButton
|
||||||
icon.source: "qrc:/res/update.svg"
|
|
||||||
icon.color: "#0BF200"
|
Image {
|
||||||
|
source: "qrc:/res/update.svg"
|
||||||
|
anchors.centerIn: parent
|
||||||
|
sourceSize {
|
||||||
|
width: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
|
||||||
|
height: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ToolTip.delay: 1000
|
ToolTip.delay: 1000
|
||||||
ToolTip.timeout: 3000
|
ToolTip.timeout: 3000
|
||||||
@@ -108,11 +122,6 @@ ApplicationWindow {
|
|||||||
|
|
||||||
onClicked: Qt.openUrlExternally(browserUrl);
|
onClicked: Qt.openUrlExternally(browserUrl);
|
||||||
|
|
||||||
Menu {
|
|
||||||
x: parent.width
|
|
||||||
transformOrigin: Menu.TopRight
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function updateAvailable(url)
|
function updateAvailable(url)
|
||||||
{
|
{
|
||||||
@@ -127,7 +136,14 @@ ApplicationWindow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ToolButton {
|
ToolButton {
|
||||||
icon.source: "qrc:/res/question_mark.svg"
|
Image {
|
||||||
|
source: "qrc:/res/question_mark.svg"
|
||||||
|
anchors.centerIn: parent
|
||||||
|
sourceSize {
|
||||||
|
width: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
|
||||||
|
height: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ToolTip.delay: 1000
|
ToolTip.delay: 1000
|
||||||
ToolTip.timeout: 3000
|
ToolTip.timeout: 3000
|
||||||
@@ -136,11 +152,6 @@ ApplicationWindow {
|
|||||||
|
|
||||||
// TODO need to make sure browser is brought to foreground.
|
// TODO need to make sure browser is brought to foreground.
|
||||||
onClicked: Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Setup-Guide");
|
onClicked: Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Setup-Guide");
|
||||||
|
|
||||||
Menu {
|
|
||||||
x: parent.width
|
|
||||||
transformOrigin: Menu.TopRight
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ToolButton {
|
ToolButton {
|
||||||
@@ -152,28 +163,34 @@ ApplicationWindow {
|
|||||||
ToolTip.visible: hovered
|
ToolTip.visible: hovered
|
||||||
ToolTip.text: "Gamepad Mapper"
|
ToolTip.text: "Gamepad Mapper"
|
||||||
|
|
||||||
icon.source: "qrc:/res/ic_videogame_asset_white_48px.svg"
|
Image {
|
||||||
onClicked: navigateTo("qrc:/gui/GamepadMapper.qml", "Gamepad Mapping")
|
source: "qrc:/res/ic_videogame_asset_white_48px.svg"
|
||||||
|
anchors.centerIn: parent
|
||||||
Menu {
|
sourceSize {
|
||||||
x: parent.width
|
width: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
|
||||||
transformOrigin: Menu.TopRight
|
height: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onClicked: navigateTo("qrc:/gui/GamepadMapper.qml", "Gamepad Mapping")
|
||||||
}
|
}
|
||||||
|
|
||||||
ToolButton {
|
ToolButton {
|
||||||
icon.source: "qrc:/res/settings.svg"
|
Image {
|
||||||
|
source: "qrc:/res/settings.svg"
|
||||||
|
anchors.centerIn: parent
|
||||||
|
sourceSize {
|
||||||
|
width: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
|
||||||
|
height: toolBar.height - toolBar.anchors.bottomMargin - toolBar.anchors.topMargin
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onClicked: navigateTo("qrc:/gui/SettingsView.qml", "Settings")
|
onClicked: navigateTo("qrc:/gui/SettingsView.qml", "Settings")
|
||||||
|
|
||||||
ToolTip.delay: 1000
|
ToolTip.delay: 1000
|
||||||
ToolTip.timeout: 3000
|
ToolTip.timeout: 3000
|
||||||
ToolTip.visible: hovered
|
ToolTip.visible: hovered
|
||||||
ToolTip.text: "Settings"
|
ToolTip.text: "Settings"
|
||||||
|
|
||||||
Menu {
|
|
||||||
x: parent.width
|
|
||||||
transformOrigin: Menu.TopRight
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+107
-7
@@ -11,6 +11,10 @@
|
|||||||
#define SDL_MAIN_HANDLED
|
#define SDL_MAIN_HANDLED
|
||||||
#include <SDL.h>
|
#include <SDL.h>
|
||||||
|
|
||||||
|
#ifdef HAVE_FFMPEG
|
||||||
|
#include "streaming/video/ffmpeg.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
#include "path.h"
|
#include "path.h"
|
||||||
#include "gui/computermodel.h"
|
#include "gui/computermodel.h"
|
||||||
#include "gui/appmodel.h"
|
#include "gui/appmodel.h"
|
||||||
@@ -62,7 +66,8 @@ void logToLoggerStream(QString& message)
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
s_LoggerStream << message << endl;
|
s_LoggerStream << message;
|
||||||
|
s_LoggerStream.flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
void sdlLogToDiskHandler(void*, int category, SDL_LogPriority priority, const char* message)
|
void sdlLogToDiskHandler(void*, int category, SDL_LogPriority priority, const char* message)
|
||||||
@@ -94,7 +99,7 @@ void sdlLogToDiskHandler(void*, int category, SDL_LogPriority priority, const ch
|
|||||||
}
|
}
|
||||||
|
|
||||||
QTime logTime = QTime::fromMSecsSinceStartOfDay(s_LoggerTime.elapsed());
|
QTime logTime = QTime::fromMSecsSinceStartOfDay(s_LoggerTime.elapsed());
|
||||||
QString txt = QString("%1 - SDL %2 (%3): %4").arg(logTime.toString()).arg(priorityTxt).arg(category).arg(message);
|
QString txt = QString("%1 - SDL %2 (%3): %4\n").arg(logTime.toString()).arg(priorityTxt).arg(category).arg(message);
|
||||||
|
|
||||||
logToLoggerStream(txt);
|
logToLoggerStream(txt);
|
||||||
}
|
}
|
||||||
@@ -122,15 +127,99 @@ void qtLogToDiskHandler(QtMsgType type, const QMessageLogContext&, const QString
|
|||||||
}
|
}
|
||||||
|
|
||||||
QTime logTime = QTime::fromMSecsSinceStartOfDay(s_LoggerTime.elapsed());
|
QTime logTime = QTime::fromMSecsSinceStartOfDay(s_LoggerTime.elapsed());
|
||||||
QString txt = QString("%1 - Qt %2: %3").arg(logTime.toString()).arg(typeTxt).arg(msg);
|
QString txt = QString("%1 - Qt %2: %3\n").arg(logTime.toString()).arg(typeTxt).arg(msg);
|
||||||
|
|
||||||
|
logToLoggerStream(txt);
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef HAVE_FFMPEG
|
||||||
|
|
||||||
|
void ffmpegLogToDiskHandler(void* ptr, int level, const char* fmt, va_list vl)
|
||||||
|
{
|
||||||
|
char lineBuffer[1024];
|
||||||
|
static int printPrefix = 1;
|
||||||
|
|
||||||
|
if ((level & 0xFF) > av_log_get_level()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
av_log_format_line(ptr, level, fmt, vl, lineBuffer, sizeof(lineBuffer), &printPrefix);
|
||||||
|
|
||||||
|
QTime logTime = QTime::fromMSecsSinceStartOfDay(s_LoggerTime.elapsed());
|
||||||
|
QString txt = QString("%1 - FFmpeg: %2").arg(logTime.toString()).arg(lineBuffer);
|
||||||
|
|
||||||
logToLoggerStream(txt);
|
logToLoggerStream(txt);
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef Q_OS_WIN32
|
||||||
|
#define WIN32_LEAN_AND_MEAN
|
||||||
|
#include <Windows.h>
|
||||||
|
#include <DbgHelp.h>
|
||||||
|
|
||||||
|
static UINT s_HitUnhandledException = 0;
|
||||||
|
|
||||||
|
LONG WINAPI UnhandledExceptionHandler(struct _EXCEPTION_POINTERS *ExceptionInfo)
|
||||||
|
{
|
||||||
|
// Only write a dump for the first unhandled exception
|
||||||
|
if (InterlockedCompareExchange(&s_HitUnhandledException, 1, 0) != 0) {
|
||||||
|
return EXCEPTION_CONTINUE_SEARCH;
|
||||||
|
}
|
||||||
|
|
||||||
|
WCHAR dmpFileName[MAX_PATH];
|
||||||
|
swprintf_s(dmpFileName, L"%ls\\Moonlight-%I64u.dmp",
|
||||||
|
(PWCHAR)QDir::toNativeSeparators(Path::getLogDir()).utf16(), QDateTime::currentSecsSinceEpoch());
|
||||||
|
QString qDmpFileName = QString::fromUtf16((unsigned short*)dmpFileName);
|
||||||
|
HANDLE dumpHandle = CreateFileW(dmpFileName, GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
|
||||||
|
if (dumpHandle != INVALID_HANDLE_VALUE) {
|
||||||
|
MINIDUMP_EXCEPTION_INFORMATION info;
|
||||||
|
|
||||||
|
info.ThreadId = GetCurrentThreadId();
|
||||||
|
info.ExceptionPointers = ExceptionInfo;
|
||||||
|
info.ClientPointers = FALSE;
|
||||||
|
|
||||||
|
DWORD typeFlags = MiniDumpWithIndirectlyReferencedMemory |
|
||||||
|
MiniDumpIgnoreInaccessibleMemory |
|
||||||
|
MiniDumpWithUnloadedModules |
|
||||||
|
MiniDumpWithThreadInfo;
|
||||||
|
|
||||||
|
if (MiniDumpWriteDump(GetCurrentProcess(),
|
||||||
|
GetCurrentProcessId(),
|
||||||
|
dumpHandle,
|
||||||
|
(MINIDUMP_TYPE)typeFlags,
|
||||||
|
&info,
|
||||||
|
nullptr,
|
||||||
|
nullptr)) {
|
||||||
|
qCritical() << "Unhandled exception! Minidump written to:" << qDmpFileName;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
qCritical() << "Unhandled exception! Failed to write dump:" << GetLastError();
|
||||||
|
}
|
||||||
|
|
||||||
|
CloseHandle(dumpHandle);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
qCritical() << "Unhandled exception! Failed to open dump file:" << qDmpFileName << "with error" << GetLastError();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Let the program crash and WER collect a dump
|
||||||
|
return EXCEPTION_CONTINUE_SEARCH;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
int main(int argc, char *argv[])
|
int main(int argc, char *argv[])
|
||||||
{
|
{
|
||||||
|
// Set these here to allow us to use the default QSettings constructor.
|
||||||
|
// These also ensure that our cache directory is named correctly. As such,
|
||||||
|
// it is critical that these be called before Path::initialize().
|
||||||
|
QCoreApplication::setOrganizationName("Moonlight Game Streaming Project");
|
||||||
|
QCoreApplication::setOrganizationDomain("moonlight-stream.com");
|
||||||
|
QCoreApplication::setApplicationName("Moonlight");
|
||||||
|
|
||||||
if (QFile(QDir::currentPath() + "/portable.dat").exists()) {
|
if (QFile(QDir::currentPath() + "/portable.dat").exists()) {
|
||||||
qInfo() << "Running in portable mode from:" << QDir::currentPath();
|
qInfo() << "Running in portable mode from:" << QDir::currentPath();
|
||||||
QSettings::setDefaultFormat(QSettings::IniFormat);
|
QSettings::setDefaultFormat(QSettings::IniFormat);
|
||||||
@@ -158,6 +247,16 @@ int main(int argc, char *argv[])
|
|||||||
s_LoggerTime.start();
|
s_LoggerTime.start();
|
||||||
qInstallMessageHandler(qtLogToDiskHandler);
|
qInstallMessageHandler(qtLogToDiskHandler);
|
||||||
SDL_LogSetOutputFunction(sdlLogToDiskHandler, nullptr);
|
SDL_LogSetOutputFunction(sdlLogToDiskHandler, nullptr);
|
||||||
|
|
||||||
|
#ifdef HAVE_FFMPEG
|
||||||
|
av_log_set_callback(ffmpegLogToDiskHandler);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef Q_OS_WIN32
|
||||||
|
// Create a crash dump when we crash on Windows
|
||||||
|
SetUnhandledExceptionFilter(UnhandledExceptionHandler);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
|
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
|
||||||
@@ -166,10 +265,11 @@ int main(int argc, char *argv[])
|
|||||||
// password prompts on macOS.
|
// password prompts on macOS.
|
||||||
qputenv("QT_SSL_USE_TEMPORARY_KEYCHAIN", QByteArray("1"));
|
qputenv("QT_SSL_USE_TEMPORARY_KEYCHAIN", QByteArray("1"));
|
||||||
|
|
||||||
// Set these here to allow us to use the default QSettings constructor
|
#ifdef Q_OS_WIN32
|
||||||
QCoreApplication::setOrganizationName("Moonlight Game Streaming Project");
|
// On Windows, use ANGLE so we don't have to load both DX and OGL
|
||||||
QCoreApplication::setOrganizationDomain("moonlight-stream.com");
|
// user-mode drivers into our app.
|
||||||
QCoreApplication::setApplicationName("Moonlight");
|
qputenv("QT_OPENGL", "angle");
|
||||||
|
#endif
|
||||||
|
|
||||||
// Register custom metatypes for use in signals
|
// Register custom metatypes for use in signals
|
||||||
qRegisterMetaType<NvApp>("NvApp");
|
qRegisterMetaType<NvApp>("NvApp");
|
||||||
|
|||||||
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="24" height="24" viewBox="0 0 24 24">
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="24" height="24" viewBox="0 0 24 24" fill="#0BF200">
|
||||||
<path d="M21,10.12H14.22L16.96,7.3C14.23,4.6 9.81,4.5 7.08,7.2C4.35,9.91 4.35,14.28 7.08,17C9.81,19.7 14.23,19.7 16.96,17C18.32,15.65 19,14.08 19,12.1H21C21,14.08 20.12,16.65 18.36,18.39C14.85,21.87 9.15,21.87 5.64,18.39C2.14,14.92 2.11,9.28 5.62,5.81C9.13,2.34 14.76,2.34 18.27,5.81L21,3V10.12M12.5,8V12.25L16,14.33L15.28,15.54L11,13V8H12.5Z" />
|
<path d="M21,10.12H14.22L16.96,7.3C14.23,4.6 9.81,4.5 7.08,7.2C4.35,9.91 4.35,14.28 7.08,17C9.81,19.7 14.23,19.7 16.96,17C18.32,15.65 19,14.08 19,12.1H21C21,14.08 20.12,16.65 18.36,18.39C14.85,21.87 9.15,21.87 5.64,18.39C2.14,14.92 2.11,9.28 5.62,5.81C9.13,2.34 14.76,2.34 18.27,5.81L21,3V10.12M12.5,8V12.25L16,14.33L15.28,15.54L11,13V8H12.5Z" />
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 484 B After Width: | Height: | Size: 499 B |
@@ -1,5 +1,6 @@
|
|||||||
#include "streamingpreferences.h"
|
#include "streamingpreferences.h"
|
||||||
#include "streaming/session.hpp"
|
#include "streaming/session.hpp"
|
||||||
|
#include "streaming/streamutils.h"
|
||||||
|
|
||||||
#include <QSettings>
|
#include <QSettings>
|
||||||
|
|
||||||
@@ -16,6 +17,8 @@
|
|||||||
#define SER_AUDIOCFG "audiocfg"
|
#define SER_AUDIOCFG "audiocfg"
|
||||||
#define SER_VIDEOCFG "videocfg"
|
#define SER_VIDEOCFG "videocfg"
|
||||||
#define SER_VIDEODEC "videodec"
|
#define SER_VIDEODEC "videodec"
|
||||||
|
#define SER_WINDOWMODE "windowmode"
|
||||||
|
#define SER_UNSUPPORTEDFPS "unsupportedfps"
|
||||||
|
|
||||||
StreamingPreferences::StreamingPreferences()
|
StreamingPreferences::StreamingPreferences()
|
||||||
{
|
{
|
||||||
@@ -30,17 +33,21 @@ void StreamingPreferences::reload()
|
|||||||
height = settings.value(SER_HEIGHT, 720).toInt();
|
height = settings.value(SER_HEIGHT, 720).toInt();
|
||||||
fps = settings.value(SER_FPS, 60).toInt();
|
fps = settings.value(SER_FPS, 60).toInt();
|
||||||
bitrateKbps = settings.value(SER_BITRATE, getDefaultBitrate(width, height, fps)).toInt();
|
bitrateKbps = settings.value(SER_BITRATE, getDefaultBitrate(width, height, fps)).toInt();
|
||||||
fullScreen = settings.value(SER_FULLSCREEN, true).toBool();
|
|
||||||
enableVsync = settings.value(SER_VSYNC, true).toBool();
|
enableVsync = settings.value(SER_VSYNC, true).toBool();
|
||||||
gameOptimizations = settings.value(SER_GAMEOPTS, true).toBool();
|
gameOptimizations = settings.value(SER_GAMEOPTS, true).toBool();
|
||||||
playAudioOnHost = settings.value(SER_HOSTAUDIO, false).toBool();
|
playAudioOnHost = settings.value(SER_HOSTAUDIO, false).toBool();
|
||||||
multiController = settings.value(SER_MULTICONT, true).toBool();
|
multiController = settings.value(SER_MULTICONT, true).toBool();
|
||||||
|
unsupportedFps = settings.value(SER_UNSUPPORTEDFPS, false).toBool();
|
||||||
audioConfig = static_cast<AudioConfig>(settings.value(SER_AUDIOCFG,
|
audioConfig = static_cast<AudioConfig>(settings.value(SER_AUDIOCFG,
|
||||||
static_cast<int>(AudioConfig::AC_AUTO)).toInt());
|
static_cast<int>(AudioConfig::AC_FORCE_STEREO)).toInt());
|
||||||
videoCodecConfig = static_cast<VideoCodecConfig>(settings.value(SER_VIDEOCFG,
|
videoCodecConfig = static_cast<VideoCodecConfig>(settings.value(SER_VIDEOCFG,
|
||||||
static_cast<int>(VideoCodecConfig::VCC_AUTO)).toInt());
|
static_cast<int>(VideoCodecConfig::VCC_AUTO)).toInt());
|
||||||
videoDecoderSelection = static_cast<VideoDecoderSelection>(settings.value(SER_VIDEODEC,
|
videoDecoderSelection = static_cast<VideoDecoderSelection>(settings.value(SER_VIDEODEC,
|
||||||
static_cast<int>(VideoDecoderSelection::VDS_AUTO)).toInt());
|
static_cast<int>(VideoDecoderSelection::VDS_AUTO)).toInt());
|
||||||
|
windowMode = static_cast<WindowMode>(settings.value(SER_WINDOWMODE,
|
||||||
|
// Try to load from the old preference value too
|
||||||
|
static_cast<int>(settings.value(SER_FULLSCREEN, true).toBool() ?
|
||||||
|
WindowMode::WM_FULLSCREEN : WindowMode::WM_WINDOWED)).toInt());
|
||||||
}
|
}
|
||||||
|
|
||||||
void StreamingPreferences::save()
|
void StreamingPreferences::save()
|
||||||
@@ -51,14 +58,15 @@ void StreamingPreferences::save()
|
|||||||
settings.setValue(SER_HEIGHT, height);
|
settings.setValue(SER_HEIGHT, height);
|
||||||
settings.setValue(SER_FPS, fps);
|
settings.setValue(SER_FPS, fps);
|
||||||
settings.setValue(SER_BITRATE, bitrateKbps);
|
settings.setValue(SER_BITRATE, bitrateKbps);
|
||||||
settings.setValue(SER_FULLSCREEN, fullScreen);
|
|
||||||
settings.setValue(SER_VSYNC, enableVsync);
|
settings.setValue(SER_VSYNC, enableVsync);
|
||||||
settings.setValue(SER_GAMEOPTS, gameOptimizations);
|
settings.setValue(SER_GAMEOPTS, gameOptimizations);
|
||||||
settings.setValue(SER_HOSTAUDIO, playAudioOnHost);
|
settings.setValue(SER_HOSTAUDIO, playAudioOnHost);
|
||||||
settings.setValue(SER_MULTICONT, multiController);
|
settings.setValue(SER_MULTICONT, multiController);
|
||||||
|
settings.setValue(SER_UNSUPPORTEDFPS, unsupportedFps);
|
||||||
settings.setValue(SER_AUDIOCFG, static_cast<int>(audioConfig));
|
settings.setValue(SER_AUDIOCFG, static_cast<int>(audioConfig));
|
||||||
settings.setValue(SER_VIDEOCFG, static_cast<int>(videoCodecConfig));
|
settings.setValue(SER_VIDEOCFG, static_cast<int>(videoCodecConfig));
|
||||||
settings.setValue(SER_VIDEODEC, static_cast<int>(videoDecoderSelection));
|
settings.setValue(SER_VIDEODEC, static_cast<int>(videoDecoderSelection));
|
||||||
|
settings.setValue(SER_WINDOWMODE, static_cast<int>(windowMode));
|
||||||
}
|
}
|
||||||
|
|
||||||
bool StreamingPreferences::hasAnyHardwareAcceleration()
|
bool StreamingPreferences::hasAnyHardwareAcceleration()
|
||||||
@@ -87,12 +95,26 @@ int StreamingPreferences::getMaximumStreamingFrameRate()
|
|||||||
return maxFrameRate;
|
return maxFrameRate;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int i = 0; i < SDL_GetNumVideoDisplays(); i++) {
|
SDL_DisplayMode mode, bestMode, desktopMode;
|
||||||
SDL_DisplayMode mode;
|
for (int displayIndex = 0; displayIndex < SDL_GetNumVideoDisplays(); displayIndex++) {
|
||||||
if (SDL_GetCurrentDisplayMode(i, &mode) == 0) {
|
// Get the native desktop resolution
|
||||||
|
if (StreamUtils::getRealDesktopMode(displayIndex, &desktopMode)) {
|
||||||
|
|
||||||
|
// Start at desktop mode and work our way up
|
||||||
|
bestMode = desktopMode;
|
||||||
|
for (int i = 0; i < SDL_GetNumDisplayModes(displayIndex); i++) {
|
||||||
|
if (SDL_GetDisplayMode(displayIndex, i, &mode) == 0) {
|
||||||
|
if (mode.w == desktopMode.w && mode.h == desktopMode.h) {
|
||||||
|
if (mode.refresh_rate > bestMode.refresh_rate) {
|
||||||
|
bestMode = mode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Cap the frame rate at 120 FPS. Past this, the encoders start
|
// Cap the frame rate at 120 FPS. Past this, the encoders start
|
||||||
// to max out and drop frames.
|
// to max out and drop frames.
|
||||||
maxFrameRate = qMax(maxFrameRate, qMin(120, mode.refresh_rate));
|
maxFrameRate = qMax(maxFrameRate, qMin(120, bestMode.refresh_rate));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,7 +123,7 @@ int StreamingPreferences::getMaximumStreamingFrameRate()
|
|||||||
return maxFrameRate;
|
return maxFrameRate;
|
||||||
}
|
}
|
||||||
|
|
||||||
QRect StreamingPreferences::getDisplayResolution(int displayIndex)
|
QRect StreamingPreferences::getDesktopResolution(int displayIndex)
|
||||||
{
|
{
|
||||||
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
|
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
@@ -116,7 +138,7 @@ QRect StreamingPreferences::getDisplayResolution(int displayIndex)
|
|||||||
}
|
}
|
||||||
|
|
||||||
SDL_DisplayMode mode;
|
SDL_DisplayMode mode;
|
||||||
int err = SDL_GetCurrentDisplayMode(displayIndex, &mode);
|
int err = SDL_GetDesktopDisplayMode(displayIndex, &mode);
|
||||||
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||||
|
|
||||||
if (err == 0) {
|
if (err == 0) {
|
||||||
@@ -124,12 +146,38 @@ QRect StreamingPreferences::getDisplayResolution(int displayIndex)
|
|||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"SDL_GetCurrentDisplayMode() failed: %s",
|
"SDL_GetDesktopDisplayMode() failed: %s",
|
||||||
SDL_GetError());
|
SDL_GetError());
|
||||||
return QRect();
|
return QRect();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QRect StreamingPreferences::getNativeResolution(int displayIndex)
|
||||||
|
{
|
||||||
|
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
return QRect();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (displayIndex >= SDL_GetNumVideoDisplays()) {
|
||||||
|
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||||
|
return QRect();
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_DisplayMode mode;
|
||||||
|
bool success = StreamUtils::getRealDesktopMode(displayIndex, &mode);
|
||||||
|
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
return QRect(0, 0, mode.w, mode.h);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return QRect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
int StreamingPreferences::getDefaultBitrate(int width, int height, int fps)
|
int StreamingPreferences::getDefaultBitrate(int width, int height, int fps)
|
||||||
{
|
{
|
||||||
// This table prefers 16:10 resolutions because they are
|
// This table prefers 16:10 resolutions because they are
|
||||||
|
|||||||
@@ -21,7 +21,9 @@ public:
|
|||||||
|
|
||||||
Q_INVOKABLE static int getMaximumStreamingFrameRate();
|
Q_INVOKABLE static int getMaximumStreamingFrameRate();
|
||||||
|
|
||||||
Q_INVOKABLE QRect getDisplayResolution(int displayIndex);
|
Q_INVOKABLE QRect getDesktopResolution(int displayIndex);
|
||||||
|
|
||||||
|
Q_INVOKABLE QRect getNativeResolution(int displayIndex);
|
||||||
|
|
||||||
void reload();
|
void reload();
|
||||||
|
|
||||||
@@ -50,43 +52,54 @@ public:
|
|||||||
};
|
};
|
||||||
Q_ENUM(VideoDecoderSelection)
|
Q_ENUM(VideoDecoderSelection)
|
||||||
|
|
||||||
|
enum WindowMode
|
||||||
|
{
|
||||||
|
WM_FULLSCREEN,
|
||||||
|
WM_FULLSCREEN_DESKTOP,
|
||||||
|
WM_WINDOWED
|
||||||
|
};
|
||||||
|
Q_ENUM(WindowMode)
|
||||||
|
|
||||||
Q_PROPERTY(int width MEMBER width NOTIFY displayModeChanged)
|
Q_PROPERTY(int width MEMBER width NOTIFY displayModeChanged)
|
||||||
Q_PROPERTY(int height MEMBER height NOTIFY displayModeChanged)
|
Q_PROPERTY(int height MEMBER height NOTIFY displayModeChanged)
|
||||||
Q_PROPERTY(int fps MEMBER fps NOTIFY displayModeChanged)
|
Q_PROPERTY(int fps MEMBER fps NOTIFY displayModeChanged)
|
||||||
Q_PROPERTY(int bitrateKbps MEMBER bitrateKbps NOTIFY bitrateChanged)
|
Q_PROPERTY(int bitrateKbps MEMBER bitrateKbps NOTIFY bitrateChanged)
|
||||||
Q_PROPERTY(bool fullScreen MEMBER fullScreen NOTIFY fullScreenChanged)
|
|
||||||
Q_PROPERTY(bool enableVsync MEMBER enableVsync NOTIFY enableVsyncChanged)
|
Q_PROPERTY(bool enableVsync MEMBER enableVsync NOTIFY enableVsyncChanged)
|
||||||
Q_PROPERTY(bool gameOptimizations MEMBER gameOptimizations NOTIFY gameOptimizationsChanged)
|
Q_PROPERTY(bool gameOptimizations MEMBER gameOptimizations NOTIFY gameOptimizationsChanged)
|
||||||
Q_PROPERTY(bool playAudioOnHost MEMBER playAudioOnHost NOTIFY playAudioOnHostChanged)
|
Q_PROPERTY(bool playAudioOnHost MEMBER playAudioOnHost NOTIFY playAudioOnHostChanged)
|
||||||
Q_PROPERTY(bool multiController MEMBER multiController NOTIFY multiControllerChanged)
|
Q_PROPERTY(bool multiController MEMBER multiController NOTIFY multiControllerChanged)
|
||||||
|
Q_PROPERTY(bool unsupportedFps MEMBER unsupportedFps NOTIFY unsupportedFpsChanged)
|
||||||
Q_PROPERTY(AudioConfig audioConfig MEMBER audioConfig NOTIFY audioConfigChanged)
|
Q_PROPERTY(AudioConfig audioConfig MEMBER audioConfig NOTIFY audioConfigChanged)
|
||||||
Q_PROPERTY(VideoCodecConfig videoCodecConfig MEMBER videoCodecConfig NOTIFY videoCodecConfigChanged)
|
Q_PROPERTY(VideoCodecConfig videoCodecConfig MEMBER videoCodecConfig NOTIFY videoCodecConfigChanged)
|
||||||
Q_PROPERTY(VideoDecoderSelection videoDecoderSelection MEMBER videoDecoderSelection NOTIFY videoDecoderSelectionChanged)
|
Q_PROPERTY(VideoDecoderSelection videoDecoderSelection MEMBER videoDecoderSelection NOTIFY videoDecoderSelectionChanged)
|
||||||
|
Q_PROPERTY(WindowMode windowMode MEMBER windowMode NOTIFY windowModeChanged)
|
||||||
|
|
||||||
// Directly accessible members for preferences
|
// Directly accessible members for preferences
|
||||||
int width;
|
int width;
|
||||||
int height;
|
int height;
|
||||||
int fps;
|
int fps;
|
||||||
int bitrateKbps;
|
int bitrateKbps;
|
||||||
bool fullScreen;
|
|
||||||
bool enableVsync;
|
bool enableVsync;
|
||||||
bool gameOptimizations;
|
bool gameOptimizations;
|
||||||
bool playAudioOnHost;
|
bool playAudioOnHost;
|
||||||
bool multiController;
|
bool multiController;
|
||||||
|
bool unsupportedFps;
|
||||||
AudioConfig audioConfig;
|
AudioConfig audioConfig;
|
||||||
VideoCodecConfig videoCodecConfig;
|
VideoCodecConfig videoCodecConfig;
|
||||||
VideoDecoderSelection videoDecoderSelection;
|
VideoDecoderSelection videoDecoderSelection;
|
||||||
|
WindowMode windowMode;
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void displayModeChanged();
|
void displayModeChanged();
|
||||||
void bitrateChanged();
|
void bitrateChanged();
|
||||||
void fullScreenChanged();
|
|
||||||
void enableVsyncChanged();
|
void enableVsyncChanged();
|
||||||
void gameOptimizationsChanged();
|
void gameOptimizationsChanged();
|
||||||
void playAudioOnHostChanged();
|
void playAudioOnHostChanged();
|
||||||
void multiControllerChanged();
|
void multiControllerChanged();
|
||||||
|
void unsupportedFpsChanged();
|
||||||
void audioConfigChanged();
|
void audioConfigChanged();
|
||||||
void videoCodecConfigChanged();
|
void videoCodecConfigChanged();
|
||||||
void videoDecoderSelectionChanged();
|
void videoDecoderSelectionChanged();
|
||||||
|
void windowModeChanged();
|
||||||
};
|
};
|
||||||
|
|
||||||
+55
-1
@@ -70,6 +70,60 @@ Exit:
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool Session::testAudio(int audioConfiguration)
|
||||||
|
{
|
||||||
|
SDL_AudioSpec want, have;
|
||||||
|
SDL_AudioDeviceID dev;
|
||||||
|
bool ret;
|
||||||
|
|
||||||
|
if (SDL_InitSubSystem(SDL_INIT_AUDIO) != 0) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Audio test - SDL_InitSubSystem(SDL_INIT_AUDIO) failed: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_zero(want);
|
||||||
|
want.freq = 48000;
|
||||||
|
want.format = AUDIO_S16;
|
||||||
|
want.samples = SAMPLES_PER_FRAME;
|
||||||
|
|
||||||
|
switch (audioConfiguration) {
|
||||||
|
case AUDIO_CONFIGURATION_STEREO:
|
||||||
|
want.channels = 2;
|
||||||
|
break;
|
||||||
|
case AUDIO_CONFIGURATION_51_SURROUND:
|
||||||
|
want.channels = 6;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
SDL_assert(false);
|
||||||
|
ret = false;
|
||||||
|
goto Exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test audio device for functionality
|
||||||
|
dev = SDL_OpenAudioDevice(NULL, 0, &want, &have, 0);
|
||||||
|
if (dev == 0) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Audio test - Failed to open audio device: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
ret = false;
|
||||||
|
goto Exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_CloseAudioDevice(dev);
|
||||||
|
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Audio test - Successful with %d channels",
|
||||||
|
want.channels);
|
||||||
|
|
||||||
|
ret = true;
|
||||||
|
|
||||||
|
Exit:
|
||||||
|
SDL_QuitSubSystem(SDL_INIT_AUDIO);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
int Session::sdlAudioInit(int /* audioConfiguration */,
|
int Session::sdlAudioInit(int /* audioConfiguration */,
|
||||||
POPUS_MULTISTREAM_CONFIGURATION opusConfig,
|
POPUS_MULTISTREAM_CONFIGURATION opusConfig,
|
||||||
void* /* arContext */, int /* arFlags */)
|
void* /* arContext */, int /* arFlags */)
|
||||||
@@ -82,7 +136,7 @@ int Session::sdlAudioInit(int /* audioConfiguration */,
|
|||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"SDL_InitSubSystem(SDL_INIT_AUDIO) failed: %s",
|
"SDL_InitSubSystem(SDL_INIT_AUDIO) failed: %s",
|
||||||
SDL_GetError());
|
SDL_GetError());
|
||||||
return AUDIO_CONFIGURATION_STEREO;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
SDL_zero(want);
|
SDL_zero(want);
|
||||||
|
|||||||
+35
-4
@@ -23,7 +23,8 @@ const int SdlInputHandler::k_ButtonMap[] = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
SdlInputHandler::SdlInputHandler(bool multiController)
|
SdlInputHandler::SdlInputHandler(bool multiController)
|
||||||
: m_MultiController(multiController)
|
: m_LastMouseMotionTime(0),
|
||||||
|
m_MultiController(multiController)
|
||||||
{
|
{
|
||||||
// Allow gamepad input when the app doesn't have focus
|
// Allow gamepad input when the app doesn't have focus
|
||||||
SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1");
|
SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1");
|
||||||
@@ -324,6 +325,9 @@ void SdlInputHandler::handleKeyEvent(SDL_KeyboardEvent* event)
|
|||||||
case SDL_SCANCODE_APOSTROPHE:
|
case SDL_SCANCODE_APOSTROPHE:
|
||||||
keyCode = 0xDE;
|
keyCode = 0xDE;
|
||||||
break;
|
break;
|
||||||
|
case SDL_SCANCODE_NONUSBACKSLASH:
|
||||||
|
keyCode = 0xE2;
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"Unhandled button event: %d",
|
"Unhandled button event: %d",
|
||||||
@@ -388,10 +392,37 @@ void SdlInputHandler::handleMouseMotionEvent(SDL_MouseMotionEvent* event)
|
|||||||
// Not capturing
|
// Not capturing
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
short xdelta = (short)event->xrel;
|
||||||
|
short ydelta = (short)event->yrel;
|
||||||
|
|
||||||
if (event->xrel != 0 || event->yrel != 0) {
|
// If we're sending more than one motion event per millisecond,
|
||||||
LiSendMouseMoveEvent((short)event->xrel,
|
// delay for 1 ms to allow batching of mouse move events.
|
||||||
(short)event->yrel);
|
Uint32 currentTime = SDL_GetTicks();
|
||||||
|
if (!SDL_TICKS_PASSED(currentTime, m_LastMouseMotionTime + 1)) {
|
||||||
|
SDL_Delay(1);
|
||||||
|
currentTime = SDL_GetTicks();
|
||||||
|
}
|
||||||
|
m_LastMouseMotionTime = currentTime;
|
||||||
|
|
||||||
|
// Pump even if we didn't delay since we might get some extra events
|
||||||
|
SDL_PumpEvents();
|
||||||
|
|
||||||
|
// Batch all of the pending mouse motion events
|
||||||
|
SDL_Event nextEvent;
|
||||||
|
while (SDL_PeepEvents(&nextEvent,
|
||||||
|
1,
|
||||||
|
SDL_GETEVENT,
|
||||||
|
SDL_MOUSEMOTION,
|
||||||
|
SDL_MOUSEMOTION) == 1) {
|
||||||
|
// In theory, these can overflow but in practice
|
||||||
|
// it should be highly unlikely since it would require
|
||||||
|
// moving 64K pixels in 1 ms.
|
||||||
|
xdelta += nextEvent.motion.xrel;
|
||||||
|
ydelta += nextEvent.motion.yrel;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (xdelta != 0 || ydelta != 0) {
|
||||||
|
LiSendMouseMoveEvent(xdelta, ydelta);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ private:
|
|||||||
|
|
||||||
void sendGamepadState(GamepadState* state);
|
void sendGamepadState(GamepadState* state);
|
||||||
|
|
||||||
|
Uint32 m_LastMouseMotionTime;
|
||||||
bool m_MultiController;
|
bool m_MultiController;
|
||||||
int m_GamepadMask;
|
int m_GamepadMask;
|
||||||
GamepadState m_GamepadState[MAX_GAMEPADS];
|
GamepadState m_GamepadState[MAX_GAMEPADS];
|
||||||
|
|||||||
+283
-84
@@ -1,5 +1,6 @@
|
|||||||
#include "session.hpp"
|
#include "session.hpp"
|
||||||
#include "settings/streamingpreferences.h"
|
#include "settings/streamingpreferences.h"
|
||||||
|
#include "streaming/streamutils.h"
|
||||||
|
|
||||||
#include <Limelight.h>
|
#include <Limelight.h>
|
||||||
#include <SDL.h>
|
#include <SDL.h>
|
||||||
@@ -19,13 +20,6 @@
|
|||||||
#include "video/sl.h"
|
#include "video/sl.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if defined(Q_OS_DARWIN)
|
|
||||||
// Using full-screen desktop allows allows Spaces to work on macOS.
|
|
||||||
#define SDL_OS_FULLSCREEN_FLAG SDL_WINDOW_FULLSCREEN_DESKTOP
|
|
||||||
#else
|
|
||||||
#define SDL_OS_FULLSCREEN_FLAG SDL_WINDOW_FULLSCREEN
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef Q_OS_WIN32
|
#ifdef Q_OS_WIN32
|
||||||
// Scaling the icon down on Win32 looks dreadful, so render at lower res
|
// Scaling the icon down on Win32 looks dreadful, so render at lower res
|
||||||
#define ICON_SIZE 32
|
#define ICON_SIZE 32
|
||||||
@@ -85,6 +79,8 @@ void Session::clStageFailed(int stage, long errorCode)
|
|||||||
|
|
||||||
void Session::clConnectionTerminated(long errorCode)
|
void Session::clConnectionTerminated(long errorCode)
|
||||||
{
|
{
|
||||||
|
emit s_ActiveSession->displayLaunchError("Connection terminated");
|
||||||
|
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"Connection terminated: %ld",
|
"Connection terminated: %ld",
|
||||||
errorCode);
|
errorCode);
|
||||||
@@ -108,13 +104,15 @@ void Session::clLogMessage(const char* format, ...)
|
|||||||
va_end(ap);
|
va_end(ap);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#define CALL_INITIALIZE(dec) (dec)->initialize(vds, window, videoFormat, width, height, frameRate, enableVsync)
|
||||||
|
|
||||||
bool Session::chooseDecoder(StreamingPreferences::VideoDecoderSelection vds,
|
bool Session::chooseDecoder(StreamingPreferences::VideoDecoderSelection vds,
|
||||||
SDL_Window* window, int videoFormat, int width, int height,
|
SDL_Window* window, int videoFormat, int width, int height,
|
||||||
int frameRate, bool enableVsync, IVideoDecoder*& chosenDecoder)
|
int frameRate, bool enableVsync, IVideoDecoder*& chosenDecoder)
|
||||||
{
|
{
|
||||||
#ifdef HAVE_SLVIDEO
|
#ifdef HAVE_SLVIDEO
|
||||||
chosenDecoder = new SLVideoDecoder();
|
chosenDecoder = new SLVideoDecoder();
|
||||||
if (chosenDecoder->initialize(vds, window, videoFormat, width, height, frameRate)) {
|
if (CALL_INITIALIZE(chosenDecoder)) {
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"SLVideo video decoder chosen");
|
"SLVideo video decoder chosen");
|
||||||
return true;
|
return true;
|
||||||
@@ -129,7 +127,7 @@ bool Session::chooseDecoder(StreamingPreferences::VideoDecoderSelection vds,
|
|||||||
|
|
||||||
#ifdef HAVE_FFMPEG
|
#ifdef HAVE_FFMPEG
|
||||||
chosenDecoder = new FFmpegVideoDecoder();
|
chosenDecoder = new FFmpegVideoDecoder();
|
||||||
if (chosenDecoder->initialize(vds, window, videoFormat, width, height, frameRate, enableVsync)) {
|
if (CALL_INITIALIZE(chosenDecoder)) {
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"FFmpeg-based video decoder chosen");
|
"FFmpeg-based video decoder chosen");
|
||||||
return true;
|
return true;
|
||||||
@@ -232,13 +230,54 @@ bool Session::isHardwareDecodeAvailable(StreamingPreferences::VideoDecoderSelect
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
Session::Session(NvComputer* computer, NvApp& app)
|
||||||
: m_Computer(computer),
|
: m_Computer(computer),
|
||||||
m_App(app),
|
m_App(app),
|
||||||
m_Window(nullptr),
|
m_Window(nullptr),
|
||||||
m_VideoDecoder(nullptr),
|
m_VideoDecoder(nullptr),
|
||||||
m_DecoderLock(0),
|
m_DecoderLock(0),
|
||||||
m_NeedsIdr(false)
|
m_NeedsIdr(false),
|
||||||
|
m_AudioDisabled(false),
|
||||||
|
m_DisplayOriginX(0),
|
||||||
|
m_DisplayOriginY(0),
|
||||||
|
m_PendingWindowedTransition(false)
|
||||||
{
|
{
|
||||||
qDebug() << "Server GPU:" << m_Computer->gpuModel;
|
qDebug() << "Server GPU:" << m_Computer->gpuModel;
|
||||||
|
|
||||||
@@ -301,6 +340,16 @@ Session::Session(NvComputer* computer, NvApp& app)
|
|||||||
m_StreamConfig.width,
|
m_StreamConfig.width,
|
||||||
m_StreamConfig.height,
|
m_StreamConfig.height,
|
||||||
m_StreamConfig.fps);
|
m_StreamConfig.fps);
|
||||||
|
|
||||||
|
// Oddly enough, the H.264 encoder on my GTX 1080 is actually more
|
||||||
|
// performant than the HEVC encoder (especially because split frame encode
|
||||||
|
// is disabled for HEVC by moonlight-common-c). Use H.264 for 1440p 100 FPS and above.
|
||||||
|
if (m_StreamConfig.width * m_StreamConfig.height * m_StreamConfig.fps >= 2560 * 1440 * 100) {
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Preferring H.264 for higher performance");
|
||||||
|
m_StreamConfig.supportsHevc = false;
|
||||||
|
}
|
||||||
|
|
||||||
m_StreamConfig.enableHdr = false;
|
m_StreamConfig.enableHdr = false;
|
||||||
break;
|
break;
|
||||||
case StreamingPreferences::VCC_FORCE_H264:
|
case StreamingPreferences::VCC_FORCE_H264:
|
||||||
@@ -330,6 +379,17 @@ Session::Session(NvComputer* computer, NvApp& app)
|
|||||||
else {
|
else {
|
||||||
m_StreamConfig.packetSize = 1024;
|
m_StreamConfig.packetSize = 1024;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
switch (m_Preferences.windowMode)
|
||||||
|
{
|
||||||
|
case StreamingPreferences::WM_FULLSCREEN_DESKTOP:
|
||||||
|
m_FullScreenFlag = SDL_WINDOW_FULLSCREEN_DESKTOP;
|
||||||
|
break;
|
||||||
|
case StreamingPreferences::WM_FULLSCREEN:
|
||||||
|
default:
|
||||||
|
m_FullScreenFlag = SDL_WINDOW_FULLSCREEN;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Session::emitLaunchWarning(QString text)
|
void Session::emitLaunchWarning(QString text)
|
||||||
@@ -356,6 +416,14 @@ bool Session::validateLaunch()
|
|||||||
emitLaunchWarning("Your settings selection to force software decoding may cause poor streaming performance.");
|
emitLaunchWarning("Your settings selection to force software decoding may cause poor streaming performance.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (m_Preferences.unsupportedFps && m_StreamConfig.fps > 60) {
|
||||||
|
emitLaunchWarning("Using unsupported FPS options may cause stuttering or lag.");
|
||||||
|
|
||||||
|
if (m_Preferences.enableVsync) {
|
||||||
|
emitLaunchWarning("V-sync will be disabled when streaming at a higher frame rate than the display.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (m_StreamConfig.supportsHevc) {
|
if (m_StreamConfig.supportsHevc) {
|
||||||
bool hevcForced = m_Preferences.videoCodecConfig == StreamingPreferences::VCC_FORCE_HEVC ||
|
bool hevcForced = m_Preferences.videoCodecConfig == StreamingPreferences::VCC_FORCE_HEVC ||
|
||||||
m_Preferences.videoCodecConfig == StreamingPreferences::VCC_FORCE_HEVC_HDR;
|
m_Preferences.videoCodecConfig == StreamingPreferences::VCC_FORCE_HEVC_HDR;
|
||||||
@@ -435,6 +503,12 @@ bool Session::validateLaunch()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Test that audio hardware is functional
|
||||||
|
m_AudioDisabled = !testAudio(m_StreamConfig.audioConfiguration);
|
||||||
|
if (m_AudioDisabled) {
|
||||||
|
emitLaunchWarning("Failed to open audio device. Audio will be unavailable during this session.");
|
||||||
|
}
|
||||||
|
|
||||||
if (m_Preferences.videoDecoderSelection == StreamingPreferences::VDS_FORCE_HARDWARE &&
|
if (m_Preferences.videoDecoderSelection == StreamingPreferences::VDS_FORCE_HARDWARE &&
|
||||||
!isHardwareDecodeAvailable(m_Preferences.videoDecoderSelection,
|
!isHardwareDecodeAvailable(m_Preferences.videoDecoderSelection,
|
||||||
m_StreamConfig.supportsHevc ? VIDEO_FORMAT_H265 : VIDEO_FORMAT_H264,
|
m_StreamConfig.supportsHevc ? VIDEO_FORMAT_H265 : VIDEO_FORMAT_H264,
|
||||||
@@ -452,6 +526,13 @@ bool Session::validateLaunch()
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add the capability flags from the chosen decoder/renderer
|
||||||
|
m_VideoCallbacks.capabilities |= getDecoderCapabilities(m_Preferences.videoDecoderSelection,
|
||||||
|
m_StreamConfig.supportsHevc ? VIDEO_FORMAT_H265 : VIDEO_FORMAT_H264,
|
||||||
|
m_StreamConfig.width,
|
||||||
|
m_StreamConfig.height,
|
||||||
|
m_StreamConfig.fps);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -469,8 +550,7 @@ class DeferredSessionCleanupTask : public QRunnable
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
void Session::getWindowDimensions(bool fullScreen,
|
void Session::getWindowDimensions(int& x, int& y,
|
||||||
int& x, int& y,
|
|
||||||
int& width, int& height)
|
int& width, int& height)
|
||||||
{
|
{
|
||||||
int displayIndex = 0;
|
int displayIndex = 0;
|
||||||
@@ -479,20 +559,29 @@ void Session::getWindowDimensions(bool fullScreen,
|
|||||||
displayIndex = SDL_GetWindowDisplayIndex(m_Window);
|
displayIndex = SDL_GetWindowDisplayIndex(m_Window);
|
||||||
SDL_assert(displayIndex >= 0);
|
SDL_assert(displayIndex >= 0);
|
||||||
}
|
}
|
||||||
// If there's a display matching this exact resolution, pick that
|
// Create our window on the same display that Qt's UI
|
||||||
// one (for native full-screen streaming). Otherwise, assume
|
// was being displayed on.
|
||||||
// display 0 for now. TODO: Default to the screen that the Qt window is on
|
else {
|
||||||
else if (fullScreen) {
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Qt UI screen is at (%d,%d)",
|
||||||
|
m_DisplayOriginX, m_DisplayOriginY);
|
||||||
for (int i = 0; i < SDL_GetNumVideoDisplays(); i++) {
|
for (int i = 0; i < SDL_GetNumVideoDisplays(); i++) {
|
||||||
SDL_DisplayMode mode;
|
SDL_Rect displayBounds;
|
||||||
if (SDL_GetCurrentDisplayMode(i, &mode) == 0 &&
|
|
||||||
m_ActiveVideoWidth == mode.w &&
|
if (SDL_GetDisplayBounds(i, &displayBounds) == 0) {
|
||||||
m_ActiveVideoHeight == mode.h) {
|
if (displayBounds.x == m_DisplayOriginX &&
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
displayBounds.y == m_DisplayOriginY) {
|
||||||
"Found exact resolution match on display: %d",
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
i);
|
"SDL found matching display %d",
|
||||||
displayIndex = i;
|
i);
|
||||||
break;
|
displayIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"SDL_GetDisplayBounds(%d) failed: %s",
|
||||||
|
i, SDL_GetError());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -501,23 +590,19 @@ void Session::getWindowDimensions(bool fullScreen,
|
|||||||
if (SDL_GetDisplayUsableBounds(displayIndex, &usableBounds) == 0) {
|
if (SDL_GetDisplayUsableBounds(displayIndex, &usableBounds) == 0) {
|
||||||
width = usableBounds.w;
|
width = usableBounds.w;
|
||||||
height = usableBounds.h;
|
height = usableBounds.h;
|
||||||
x = usableBounds.x;
|
|
||||||
y = usableBounds.y;
|
|
||||||
|
|
||||||
if (m_Window != nullptr) {
|
if (m_Window != nullptr) {
|
||||||
int top, left, bottom, right;
|
int top, left, bottom, right;
|
||||||
|
|
||||||
if (SDL_GetWindowBordersSize(m_Window, &top, &left, &bottom, &right) < 0) {
|
if (SDL_GetWindowBordersSize(m_Window, &top, &left, &bottom, &right) == 0) {
|
||||||
|
width -= left + right;
|
||||||
|
height -= top + bottom;
|
||||||
|
}
|
||||||
|
else {
|
||||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"Unable to get window border size");
|
"Unable to get window border size: %s",
|
||||||
return;
|
SDL_GetError());
|
||||||
}
|
}
|
||||||
|
|
||||||
x += left;
|
|
||||||
y += top;
|
|
||||||
|
|
||||||
width -= left + right;
|
|
||||||
height -= top + bottom;
|
|
||||||
|
|
||||||
// If the stream window can fit within the usable drawing area with 1:1
|
// If the stream window can fit within the usable drawing area with 1:1
|
||||||
// scaling, do that rather than filling the screen.
|
// scaling, do that rather than filling the screen.
|
||||||
@@ -534,36 +619,88 @@ void Session::getWindowDimensions(bool fullScreen,
|
|||||||
|
|
||||||
width = m_StreamConfig.width;
|
width = m_StreamConfig.width;
|
||||||
height = m_StreamConfig.height;
|
height = m_StreamConfig.height;
|
||||||
x = y = SDL_WINDOWPOS_UNDEFINED_DISPLAY(displayIndex);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
x = y = SDL_WINDOWPOS_CENTERED_DISPLAY(displayIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Session::updateOptimalWindowDisplayMode()
|
||||||
|
{
|
||||||
|
SDL_DisplayMode desktopMode, bestMode, mode;
|
||||||
|
int displayIndex = SDL_GetWindowDisplayIndex(m_Window);
|
||||||
|
|
||||||
|
// Try the current display mode first. On macOS, this will be the normal
|
||||||
|
// scaled desktop resolution setting.
|
||||||
|
if (SDL_GetDesktopDisplayMode(displayIndex, &desktopMode) == 0) {
|
||||||
|
// If this doesn't fit the selected resolution, use the native
|
||||||
|
// resolution of the panel (unscaled).
|
||||||
|
if (desktopMode.w < m_ActiveVideoWidth || desktopMode.h < m_ActiveVideoHeight) {
|
||||||
|
if (!StreamUtils::getRealDesktopMode(displayIndex, &desktopMode)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"SDL_GetDesktopDisplayMode() failed: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start with the native desktop resolution and try to find
|
||||||
|
// the highest refresh rate that our stream FPS evenly divides.
|
||||||
|
bestMode = desktopMode;
|
||||||
|
bestMode.refresh_rate = 0;
|
||||||
|
for (int i = 0; i < SDL_GetNumDisplayModes(displayIndex); i++) {
|
||||||
|
if (SDL_GetDisplayMode(displayIndex, i, &mode) == 0) {
|
||||||
|
if (mode.w == desktopMode.w && mode.h == desktopMode.h &&
|
||||||
|
mode.refresh_rate % m_StreamConfig.fps == 0) {
|
||||||
|
if (mode.refresh_rate > bestMode.refresh_rate) {
|
||||||
|
bestMode = mode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bestMode.refresh_rate == 0) {
|
||||||
|
// We may find no match if the user has moved a 120 FPS
|
||||||
|
// stream onto a 60 Hz monitor (since no refresh rate can
|
||||||
|
// divide our FPS setting). We'll stick to the default in
|
||||||
|
// this case.
|
||||||
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"No matching refresh rate found; using desktop mode");
|
||||||
|
bestMode = desktopMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Chosen best display mode: %dx%dx%d",
|
||||||
|
bestMode.w, bestMode.h, bestMode.refresh_rate);
|
||||||
|
|
||||||
|
SDL_SetWindowDisplayMode(m_Window, &bestMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Session::toggleFullscreen()
|
void Session::toggleFullscreen()
|
||||||
{
|
{
|
||||||
bool fullScreen = !(SDL_GetWindowFlags(m_Window) & SDL_OS_FULLSCREEN_FLAG);
|
bool fullScreen = !(SDL_GetWindowFlags(m_Window) & m_FullScreenFlag);
|
||||||
|
|
||||||
int x, y, width, height;
|
|
||||||
|
|
||||||
// If we're leaving full screen, toggle out before setting our size and position
|
|
||||||
// that way we have accurate display size metrics (not the size our stream changed it to).
|
|
||||||
if (!fullScreen) {
|
|
||||||
SDL_SetWindowFullscreen(m_Window, 0);
|
|
||||||
SDL_SetWindowResizable(m_Window, SDL_TRUE);
|
|
||||||
}
|
|
||||||
|
|
||||||
getWindowDimensions(fullScreen, x, y, width, height);
|
|
||||||
|
|
||||||
SDL_SetWindowPosition(m_Window, x, y);
|
|
||||||
SDL_SetWindowSize(m_Window, width, height);
|
|
||||||
|
|
||||||
if (fullScreen) {
|
if (fullScreen) {
|
||||||
SDL_SetWindowResizable(m_Window, SDL_FALSE);
|
SDL_SetWindowResizable(m_Window, SDL_FALSE);
|
||||||
SDL_SetWindowFullscreen(m_Window, SDL_OS_FULLSCREEN_FLAG);
|
SDL_SetWindowFullscreen(m_Window, m_FullScreenFlag);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
SDL_SetWindowFullscreen(m_Window, 0);
|
||||||
|
SDL_SetWindowResizable(m_Window, SDL_TRUE);
|
||||||
|
|
||||||
|
// Reposition the window when the resize is complete
|
||||||
|
m_PendingWindowedTransition = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Session::exec()
|
void Session::exec(int displayOriginX, int displayOriginY)
|
||||||
{
|
{
|
||||||
|
m_DisplayOriginX = displayOriginX;
|
||||||
|
m_DisplayOriginY = displayOriginY;
|
||||||
|
|
||||||
// Check for validation errors/warnings and emit
|
// Check for validation errors/warnings and emit
|
||||||
// signals for them, if appropriate
|
// signals for them, if appropriate
|
||||||
if (!validateLaunch()) {
|
if (!validateLaunch()) {
|
||||||
@@ -644,7 +781,8 @@ void Session::exec()
|
|||||||
}
|
}
|
||||||
|
|
||||||
int err = LiStartConnection(&hostInfo, &m_StreamConfig, &k_ConnCallbacks,
|
int err = LiStartConnection(&hostInfo, &m_StreamConfig, &k_ConnCallbacks,
|
||||||
&m_VideoCallbacks, &k_AudioCallbacks,
|
&m_VideoCallbacks,
|
||||||
|
m_AudioDisabled ? nullptr : &k_AudioCallbacks,
|
||||||
NULL, 0, NULL, 0);
|
NULL, 0, NULL, 0);
|
||||||
if (err != 0) {
|
if (err != 0) {
|
||||||
// We already displayed an error dialog in the stage failure
|
// We already displayed an error dialog in the stage failure
|
||||||
@@ -659,15 +797,14 @@ void Session::exec()
|
|||||||
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
|
QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
|
||||||
|
|
||||||
int x, y, width, height;
|
int x, y, width, height;
|
||||||
getWindowDimensions(m_Preferences.fullScreen,
|
getWindowDimensions(x, y, width, height);
|
||||||
x, y, width, height);
|
|
||||||
|
|
||||||
m_Window = SDL_CreateWindow("Moonlight",
|
m_Window = SDL_CreateWindow("Moonlight",
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
0);
|
SDL_WINDOW_ALLOW_HIGHDPI);
|
||||||
if (!m_Window) {
|
if (!m_Window) {
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"SDL_CreateWindow() failed: %s",
|
"SDL_CreateWindow() failed: %s",
|
||||||
@@ -680,11 +817,13 @@ void Session::exec()
|
|||||||
// For non-full screen windows, call getWindowDimensions()
|
// For non-full screen windows, call getWindowDimensions()
|
||||||
// again after creating a window to allow it to account
|
// again after creating a window to allow it to account
|
||||||
// for window chrome size.
|
// for window chrome size.
|
||||||
if (!m_Preferences.fullScreen) {
|
if (m_Preferences.windowMode == StreamingPreferences::WM_WINDOWED) {
|
||||||
getWindowDimensions(false, x, y, width, height);
|
getWindowDimensions(x, y, width, height);
|
||||||
|
|
||||||
SDL_SetWindowPosition(m_Window, x, y);
|
// 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_SetWindowSize(m_Window, width, height);
|
||||||
|
SDL_SetWindowPosition(m_Window, x, y);
|
||||||
|
|
||||||
// Passing SDL_WINDOW_RESIZABLE to set this during window
|
// Passing SDL_WINDOW_RESIZABLE to set this during window
|
||||||
// creation causes our window to be full screen for some reason
|
// creation causes our window to be full screen for some reason
|
||||||
@@ -692,13 +831,10 @@ void Session::exec()
|
|||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// Update the window display mode based on our current monitor
|
// Update the window display mode based on our current monitor
|
||||||
SDL_DisplayMode mode;
|
updateOptimalWindowDisplayMode();
|
||||||
if (SDL_GetDesktopDisplayMode(SDL_GetWindowDisplayIndex(m_Window), &mode) == 0) {
|
|
||||||
SDL_SetWindowDisplayMode(m_Window, &mode);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enter full screen
|
// Enter full screen
|
||||||
SDL_SetWindowFullscreen(m_Window, SDL_OS_FULLSCREEN_FLAG);
|
SDL_SetWindowFullscreen(m_Window, m_FullScreenFlag);
|
||||||
}
|
}
|
||||||
|
|
||||||
QSvgRenderer svgIconRenderer(QString(":/res/moonlight.svg"));
|
QSvgRenderer svgIconRenderer(QString(":/res/moonlight.svg"));
|
||||||
@@ -724,7 +860,7 @@ void Session::exec()
|
|||||||
// Capture the mouse by default on release builds only.
|
// Capture the mouse by default on release builds only.
|
||||||
// This prevents the mouse from becoming trapped inside
|
// This prevents the mouse from becoming trapped inside
|
||||||
// Moonlight when it's halted at a debug break.
|
// Moonlight when it's halted at a debug break.
|
||||||
if (m_Preferences.fullScreen) {
|
if (m_Preferences.windowMode != StreamingPreferences::WM_WINDOWED) {
|
||||||
SDL_SetRelativeMouseMode(SDL_TRUE);
|
SDL_SetRelativeMouseMode(SDL_TRUE);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@@ -757,6 +893,8 @@ void Session::exec()
|
|||||||
SDL_GetError());
|
SDL_GetError());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int currentDisplayIndex = SDL_GetWindowDisplayIndex(m_Window);
|
||||||
|
|
||||||
// Hijack this thread to be the SDL main thread. We have to do this
|
// Hijack this thread to be the SDL main thread. We have to do this
|
||||||
// because we want to suspend all Qt processing until the stream is over.
|
// because we want to suspend all Qt processing until the stream is over.
|
||||||
SDL_Event event;
|
SDL_Event event;
|
||||||
@@ -795,11 +933,61 @@ void Session::exec()
|
|||||||
}
|
}
|
||||||
|
|
||||||
case SDL_WINDOWEVENT:
|
case SDL_WINDOWEVENT:
|
||||||
|
// Capture mouse cursor when user actives the window by clicking on
|
||||||
|
// window's client area (borders and title bar excluded).
|
||||||
|
// Without this you would have to click the window twice (once to
|
||||||
|
// activate it, second time to enable capture). With this you need to
|
||||||
|
// click it only once.
|
||||||
|
// On Linux, the button press event is delivered after the focus gain
|
||||||
|
// so this is not neccessary (and leads to a click sent to the host
|
||||||
|
// when focusing the window by clicking).
|
||||||
|
// By excluding window's borders and title bar out, lets user still
|
||||||
|
// interact with them without mouse capture kicking in.
|
||||||
|
#if defined(Q_OS_WIN32) || defined(Q_OS_DARWIN)
|
||||||
|
if (event.window.event == SDL_WINDOWEVENT_FOCUS_GAINED) {
|
||||||
|
int mouseX, mouseY;
|
||||||
|
Uint32 mouseState = SDL_GetGlobalMouseState(&mouseX, &mouseY);
|
||||||
|
if (mouseState & SDL_BUTTON(SDL_BUTTON_LEFT)) {
|
||||||
|
int x, y, width, height;
|
||||||
|
SDL_GetWindowPosition(m_Window, &x, &y);
|
||||||
|
SDL_GetWindowSize(m_Window, &width, &height);
|
||||||
|
if (mouseX > x && mouseX < x+width && mouseY > y && mouseY < y+height) {
|
||||||
|
SDL_SetRelativeMouseMode(SDL_TRUE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Release mouse cursor when another window is activated (e.g. by using ALT+TAB).
|
||||||
|
// This lets user to interact with our window's title bar and with the buttons in it.
|
||||||
|
if (event.window.event == SDL_WINDOWEVENT_FOCUS_LOST) {
|
||||||
|
SDL_SetRelativeMouseMode(SDL_FALSE);
|
||||||
|
}
|
||||||
|
|
||||||
// We want to recreate the decoder for resizes (full-screen toggles) and the initial shown event.
|
// We want to recreate the decoder for resizes (full-screen toggles) and the initial shown event.
|
||||||
// We use SDL_WINDOWEVENT_SIZE_CHANGED rather than SDL_WINDOWEVENT_RESIZED because the latter doesn't
|
// We use SDL_WINDOWEVENT_SIZE_CHANGED rather than SDL_WINDOWEVENT_RESIZED because the latter doesn't
|
||||||
// seem to fire when switching from windowed to full-screen on X11.
|
// seem to fire when switching from windowed to full-screen on X11.
|
||||||
if (event.window.event != SDL_WINDOWEVENT_SIZE_CHANGED && event.window.event != SDL_WINDOWEVENT_SHOWN) {
|
if (event.window.event != SDL_WINDOWEVENT_SIZE_CHANGED && event.window.event != SDL_WINDOWEVENT_SHOWN) {
|
||||||
break;
|
// Check that the window display hasn't changed. If it has, we want
|
||||||
|
// to recreate the decoder to allow it to adapt to the new display.
|
||||||
|
// This will allow Pacer to pull the new display refresh rate.
|
||||||
|
if (SDL_GetWindowDisplayIndex(m_Window) == currentDisplayIndex) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Complete any repositioning that was deferred until
|
||||||
|
// the resize from full-screen to windowed had completed.
|
||||||
|
// If we try to do this immediately, the resize won't take effect
|
||||||
|
// properly on Windows.
|
||||||
|
if (m_PendingWindowedTransition) {
|
||||||
|
m_PendingWindowedTransition = false;
|
||||||
|
|
||||||
|
int x, y, width, height;
|
||||||
|
getWindowDimensions(x, y, width, height);
|
||||||
|
|
||||||
|
SDL_SetWindowSize(m_Window, width, height);
|
||||||
|
SDL_SetWindowPosition(m_Window, x, y);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fall through
|
// Fall through
|
||||||
@@ -817,10 +1005,8 @@ void Session::exec()
|
|||||||
SDL_FlushEvent(SDL_WINDOWEVENT);
|
SDL_FlushEvent(SDL_WINDOWEVENT);
|
||||||
|
|
||||||
// Update the window display mode based on our current monitor
|
// Update the window display mode based on our current monitor
|
||||||
SDL_DisplayMode mode;
|
currentDisplayIndex = SDL_GetWindowDisplayIndex(m_Window);
|
||||||
if (SDL_GetDesktopDisplayMode(SDL_GetWindowDisplayIndex(m_Window), &mode) == 0) {
|
updateOptimalWindowDisplayMode();
|
||||||
SDL_SetWindowDisplayMode(m_Window, &mode);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now that the old decoder is dead, flush any events it may
|
// Now that the old decoder is dead, flush any events it may
|
||||||
// have queued to reset itself (if this reset was the result
|
// have queued to reset itself (if this reset was the result
|
||||||
@@ -829,18 +1015,31 @@ void Session::exec()
|
|||||||
SDL_FlushEvent(SDL_RENDER_DEVICE_RESET);
|
SDL_FlushEvent(SDL_RENDER_DEVICE_RESET);
|
||||||
SDL_FlushEvent(SDL_RENDER_TARGETS_RESET);
|
SDL_FlushEvent(SDL_RENDER_TARGETS_RESET);
|
||||||
|
|
||||||
// Choose a new decoder (hopefully the same one, but possibly
|
{
|
||||||
// not if a GPU was removed or something).
|
// If the stream exceeds the display refresh rate (plus some slack),
|
||||||
if (!chooseDecoder(m_Preferences.videoDecoderSelection,
|
// forcefully disable V-sync to allow the stream to render faster
|
||||||
m_Window, m_ActiveVideoFormat, m_ActiveVideoWidth,
|
// than the display.
|
||||||
m_ActiveVideoHeight, m_ActiveVideoFrameRate,
|
int displayHz = StreamUtils::getDisplayRefreshRate(m_Window);
|
||||||
m_Preferences.enableVsync,
|
bool enableVsync = m_Preferences.enableVsync;
|
||||||
s_ActiveSession->m_VideoDecoder)) {
|
if (displayHz + 5 < m_StreamConfig.fps) {
|
||||||
SDL_AtomicUnlock(&m_DecoderLock);
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
"Disabling V-sync because refresh rate limit exceeded");
|
||||||
"Failed to recreate decoder after reset");
|
enableVsync = false;
|
||||||
emit displayLaunchError("Unable to initialize video decoder. Please check your streaming settings and try again.");
|
}
|
||||||
goto DispatchDeferredCleanup;
|
|
||||||
|
// Choose a new decoder (hopefully the same one, but possibly
|
||||||
|
// not if a GPU was removed or something).
|
||||||
|
if (!chooseDecoder(m_Preferences.videoDecoderSelection,
|
||||||
|
m_Window, m_ActiveVideoFormat, m_ActiveVideoWidth,
|
||||||
|
m_ActiveVideoHeight, m_ActiveVideoFrameRate,
|
||||||
|
enableVsync,
|
||||||
|
s_ActiveSession->m_VideoDecoder)) {
|
||||||
|
SDL_AtomicUnlock(&m_DecoderLock);
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Failed to recreate decoder after reset");
|
||||||
|
emit displayLaunchError("Unable to initialize video decoder. Please check your streaming settings and try again.");
|
||||||
|
goto DispatchDeferredCleanup;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Request an IDR frame to complete the reset
|
// Request an IDR frame to complete the reset
|
||||||
|
|||||||
@@ -19,12 +19,16 @@ class Session : public QObject
|
|||||||
public:
|
public:
|
||||||
explicit Session(NvComputer* computer, NvApp& app);
|
explicit Session(NvComputer* computer, NvApp& app);
|
||||||
|
|
||||||
Q_INVOKABLE void exec();
|
Q_INVOKABLE void exec(int displayOriginX, int displayOriginY);
|
||||||
|
|
||||||
static
|
static
|
||||||
bool isHardwareDecodeAvailable(StreamingPreferences::VideoDecoderSelection vds,
|
bool isHardwareDecodeAvailable(StreamingPreferences::VideoDecoderSelection vds,
|
||||||
int videoFormat, int width, int height, int frameRate);
|
int videoFormat, int width, int height, int frameRate);
|
||||||
|
|
||||||
|
static
|
||||||
|
int getDecoderCapabilities(StreamingPreferences::VideoDecoderSelection vds,
|
||||||
|
int videoFormat, int width, int height, int frameRate);
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void stageStarting(QString stage);
|
void stageStarting(QString stage);
|
||||||
|
|
||||||
@@ -45,12 +49,15 @@ private:
|
|||||||
|
|
||||||
int sdlDetermineAudioConfiguration();
|
int sdlDetermineAudioConfiguration();
|
||||||
|
|
||||||
void getWindowDimensions(bool fullScreen,
|
bool testAudio(int audioConfiguration);
|
||||||
int& x, int& y,
|
|
||||||
|
void getWindowDimensions(int& x, int& y,
|
||||||
int& width, int& height);
|
int& width, int& height);
|
||||||
|
|
||||||
void toggleFullscreen();
|
void toggleFullscreen();
|
||||||
|
|
||||||
|
void updateOptimalWindowDisplayMode();
|
||||||
|
|
||||||
static
|
static
|
||||||
bool chooseDecoder(StreamingPreferences::VideoDecoderSelection vds,
|
bool chooseDecoder(StreamingPreferences::VideoDecoderSelection vds,
|
||||||
SDL_Window* window, int videoFormat, int width, int height,
|
SDL_Window* window, int videoFormat, int width, int height,
|
||||||
@@ -103,6 +110,11 @@ private:
|
|||||||
IVideoDecoder* m_VideoDecoder;
|
IVideoDecoder* m_VideoDecoder;
|
||||||
SDL_SpinLock m_DecoderLock;
|
SDL_SpinLock m_DecoderLock;
|
||||||
bool m_NeedsIdr;
|
bool m_NeedsIdr;
|
||||||
|
bool m_AudioDisabled;
|
||||||
|
Uint32 m_FullScreenFlag;
|
||||||
|
int m_DisplayOriginX;
|
||||||
|
int m_DisplayOriginY;
|
||||||
|
bool m_PendingWindowedTransition;
|
||||||
|
|
||||||
int m_ActiveVideoFormat;
|
int m_ActiveVideoFormat;
|
||||||
int m_ActiveVideoWidth;
|
int m_ActiveVideoWidth;
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
#include "streamutils.h"
|
#include "streamutils.h"
|
||||||
|
|
||||||
|
#include <Qt>
|
||||||
|
|
||||||
|
#ifdef Q_OS_DARWIN
|
||||||
|
#include <ApplicationServices/ApplicationServices.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
void StreamUtils::scaleSourceToDestinationSurface(SDL_Rect* src, SDL_Rect* dst)
|
void StreamUtils::scaleSourceToDestinationSurface(SDL_Rect* src, SDL_Rect* dst)
|
||||||
{
|
{
|
||||||
int dstH = dst->w * src->h / src->w;
|
int dstH = dst->w * src->h / src->w;
|
||||||
@@ -18,3 +24,104 @@ void StreamUtils::scaleSourceToDestinationSurface(SDL_Rect* src, SDL_Rect* dst)
|
|||||||
SDL_assert(dst->h * src->w / src->h <= dst->w);
|
SDL_assert(dst->h * src->w / src->h <= dst->w);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int StreamUtils::getDisplayRefreshRate(SDL_Window* window)
|
||||||
|
{
|
||||||
|
int displayIndex = SDL_GetWindowDisplayIndex(window);
|
||||||
|
if (displayIndex < 0) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Failed to get current display: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
|
||||||
|
// Assume display 0 if it fails
|
||||||
|
displayIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_DisplayMode mode;
|
||||||
|
if ((SDL_GetWindowFlags(window) & SDL_WINDOW_FULLSCREEN_DESKTOP) == SDL_WINDOW_FULLSCREEN) {
|
||||||
|
// Use the window display mode for full-screen exclusive mode
|
||||||
|
if (SDL_GetWindowDisplayMode(window, &mode) != 0) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"SDL_GetWindowDisplayMode() failed: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
|
||||||
|
// Assume 60 Hz
|
||||||
|
return 60;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Use the current display mode for windowed and borderless
|
||||||
|
if (SDL_GetCurrentDisplayMode(displayIndex, &mode) != 0) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"SDL_GetCurrentDisplayMode() failed: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
|
||||||
|
// Assume 60 Hz
|
||||||
|
return 60;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// May be zero if undefined
|
||||||
|
if (mode.refresh_rate == 0) {
|
||||||
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Refresh rate unknown; assuming 60 Hz");
|
||||||
|
mode.refresh_rate = 60;
|
||||||
|
}
|
||||||
|
|
||||||
|
return mode.refresh_rate;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool StreamUtils::getRealDesktopMode(int displayIndex, SDL_DisplayMode* mode)
|
||||||
|
{
|
||||||
|
#ifdef Q_OS_DARWIN
|
||||||
|
#define MAX_DISPLAYS 16
|
||||||
|
CGDirectDisplayID displayIds[MAX_DISPLAYS];
|
||||||
|
uint32_t displayCount = 0;
|
||||||
|
CGGetActiveDisplayList(MAX_DISPLAYS, displayIds, &displayCount);
|
||||||
|
if (displayIndex >= displayCount) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Too many displays: %d vs %d",
|
||||||
|
displayIndex, displayCount);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_zerop(mode);
|
||||||
|
|
||||||
|
// Retina displays have non-native resolutions both below and above (!) their
|
||||||
|
// native resolution, so it's impossible for us to figure out what's actually
|
||||||
|
// native on macOS using the SDL API alone. We'll talk to CoreGraphics to
|
||||||
|
// find the correct resolution and match it in our SDL list.
|
||||||
|
CFArrayRef modeList = CGDisplayCopyAllDisplayModes(displayIds[displayIndex], nullptr);
|
||||||
|
CFIndex count = CFArrayGetCount(modeList);
|
||||||
|
for (CFIndex i = 0; i < count; i++) {
|
||||||
|
auto cgMode = (CGDisplayModeRef)(CFArrayGetValueAtIndex(modeList, i));
|
||||||
|
if ((CGDisplayModeGetIOFlags(cgMode) & kDisplayModeNativeFlag) != 0) {
|
||||||
|
mode->w = static_cast<int>(CGDisplayModeGetWidth(cgMode));
|
||||||
|
mode->h = static_cast<int>(CGDisplayModeGetHeight(cgMode));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CFRelease(modeList);
|
||||||
|
|
||||||
|
// Now find the SDL mode that matches the CG native mode
|
||||||
|
for (int i = 0; i < SDL_GetNumDisplayModes(displayIndex); i++) {
|
||||||
|
SDL_DisplayMode thisMode;
|
||||||
|
if (SDL_GetDisplayMode(displayIndex, i, &thisMode) == 0) {
|
||||||
|
if (thisMode.w == mode->w && thisMode.h == mode->h &&
|
||||||
|
thisMode.refresh_rate >= mode->refresh_rate) {
|
||||||
|
*mode = thisMode;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
if (SDL_GetDesktopDisplayMode(displayIndex, mode) != 0) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"SDL_GetDesktopDisplayMode() failed: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@@ -7,4 +7,10 @@ class StreamUtils
|
|||||||
public:
|
public:
|
||||||
static
|
static
|
||||||
void scaleSourceToDestinationSurface(SDL_Rect* src, SDL_Rect* dst);
|
void scaleSourceToDestinationSurface(SDL_Rect* src, SDL_Rect* dst);
|
||||||
|
|
||||||
|
static
|
||||||
|
bool getRealDesktopMode(int displayIndex, SDL_DisplayMode* mode);
|
||||||
|
|
||||||
|
static
|
||||||
|
int getDisplayRefreshRate(SDL_Window* window);
|
||||||
};
|
};
|
||||||
@@ -19,6 +19,7 @@ public:
|
|||||||
int frameRate,
|
int frameRate,
|
||||||
bool enableVsync) = 0;
|
bool enableVsync) = 0;
|
||||||
virtual bool isHardwareAccelerated() = 0;
|
virtual bool isHardwareAccelerated() = 0;
|
||||||
|
virtual int getDecoderCapabilities() = 0;
|
||||||
virtual int submitDecodeUnit(PDECODE_UNIT du) = 0;
|
virtual int submitDecodeUnit(PDECODE_UNIT du) = 0;
|
||||||
virtual void renderFrame(SDL_UserEvent* event) = 0;
|
virtual void renderFrame(SDL_UserEvent* event) = 0;
|
||||||
virtual void dropFrame(SDL_UserEvent* event) = 0;
|
virtual void dropFrame(SDL_UserEvent* event) = 0;
|
||||||
|
|||||||
@@ -466,7 +466,6 @@ bool DXVA2Renderer::initializeDevice(SDL_Window* window, bool enableVsync)
|
|||||||
D3DPRESENT_PARAMETERS d3dpp = {};
|
D3DPRESENT_PARAMETERS d3dpp = {};
|
||||||
d3dpp.hDeviceWindow = info.info.win.window;
|
d3dpp.hDeviceWindow = info.info.win.window;
|
||||||
d3dpp.Flags = D3DPRESENTFLAG_VIDEO;
|
d3dpp.Flags = D3DPRESENTFLAG_VIDEO;
|
||||||
d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
|
|
||||||
|
|
||||||
if ((windowFlags & SDL_WINDOW_FULLSCREEN_DESKTOP) == SDL_WINDOW_FULLSCREEN) {
|
if ((windowFlags & SDL_WINDOW_FULLSCREEN_DESKTOP) == SDL_WINDOW_FULLSCREEN) {
|
||||||
d3dpp.Windowed = false;
|
d3dpp.Windowed = false;
|
||||||
@@ -489,10 +488,20 @@ bool DXVA2Renderer::initializeDevice(SDL_Window* window, bool enableVsync)
|
|||||||
// to reduce latency by avoiding double v-syncing.
|
// to reduce latency by avoiding double v-syncing.
|
||||||
d3dpp.PresentationInterval = D3DPRESENT_INTERVAL_IMMEDIATE;
|
d3dpp.PresentationInterval = D3DPRESENT_INTERVAL_IMMEDIATE;
|
||||||
|
|
||||||
// D3DSWAPEFFECT_FLIPEX requires at least 2 back buffers to allow us to
|
// If V-sync is enabled (not rendering faster than display),
|
||||||
// continue while DWM is waiting to render the surface to the display.
|
// we can use FlipEx for more efficient swapping.
|
||||||
d3dpp.SwapEffect = D3DSWAPEFFECT_FLIPEX;
|
if (enableVsync) {
|
||||||
d3dpp.BackBufferCount = 2;
|
// D3DSWAPEFFECT_FLIPEX requires at least 2 back buffers to allow us to
|
||||||
|
// continue while DWM is waiting to render the surface to the display.
|
||||||
|
d3dpp.SwapEffect = D3DSWAPEFFECT_FLIPEX;
|
||||||
|
d3dpp.BackBufferCount = 2;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// With V-sync off, we won't use FlipEx because that will block while
|
||||||
|
// DWM is waiting to render our surface (effectively behaving like V-Sync).
|
||||||
|
d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
|
||||||
|
d3dpp.BackBufferCount = 1;
|
||||||
|
}
|
||||||
|
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"Windowed mode with DWM running");
|
"Windowed mode with DWM running");
|
||||||
@@ -501,6 +510,7 @@ bool DXVA2Renderer::initializeDevice(SDL_Window* window, bool enableVsync)
|
|||||||
// Uncomposited desktop or full-screen exclusive mode with V-sync enabled
|
// Uncomposited desktop or full-screen exclusive mode with V-sync enabled
|
||||||
// We will enable V-sync in this scenario to avoid tearing.
|
// We will enable V-sync in this scenario to avoid tearing.
|
||||||
d3dpp.PresentationInterval = D3DPRESENT_INTERVAL_ONE;
|
d3dpp.PresentationInterval = D3DPRESENT_INTERVAL_ONE;
|
||||||
|
d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
|
||||||
d3dpp.BackBufferCount = 1;
|
d3dpp.BackBufferCount = 1;
|
||||||
|
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
@@ -510,6 +520,7 @@ bool DXVA2Renderer::initializeDevice(SDL_Window* window, bool enableVsync)
|
|||||||
// Uncomposited desktop or full-screen exclusive mode with V-sync disabled
|
// Uncomposited desktop or full-screen exclusive mode with V-sync disabled
|
||||||
// We will allowing tearing for lowest latency.
|
// We will allowing tearing for lowest latency.
|
||||||
d3dpp.PresentationInterval = D3DPRESENT_INTERVAL_IMMEDIATE;
|
d3dpp.PresentationInterval = D3DPRESENT_INTERVAL_IMMEDIATE;
|
||||||
|
d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
|
||||||
d3dpp.BackBufferCount = 1;
|
d3dpp.BackBufferCount = 1;
|
||||||
|
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
@@ -619,6 +630,16 @@ bool DXVA2Renderer::needsTestFrame()
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int DXVA2Renderer::getDecoderCapabilities()
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
IFFmpegRenderer::VSyncConstraint DXVA2Renderer::getVsyncConstraint()
|
||||||
|
{
|
||||||
|
return VSYNC_ANY;
|
||||||
|
}
|
||||||
|
|
||||||
void DXVA2Renderer::renderFrameAtVsync(AVFrame *frame)
|
void DXVA2Renderer::renderFrameAtVsync(AVFrame *frame)
|
||||||
{
|
{
|
||||||
IDirect3DSurface9* surface = reinterpret_cast<IDirect3DSurface9*>(frame->data[3]);
|
IDirect3DSurface9* surface = reinterpret_cast<IDirect3DSurface9*>(frame->data[3]);
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ public:
|
|||||||
virtual bool prepareDecoderContext(AVCodecContext* context);
|
virtual bool prepareDecoderContext(AVCodecContext* context);
|
||||||
virtual void renderFrameAtVsync(AVFrame* frame);
|
virtual void renderFrameAtVsync(AVFrame* frame);
|
||||||
virtual bool needsTestFrame();
|
virtual bool needsTestFrame();
|
||||||
|
virtual int getDecoderCapabilities();
|
||||||
|
virtual VSyncConstraint getVsyncConstraint();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
bool initializeDecoder();
|
bool initializeDecoder();
|
||||||
|
|||||||
@@ -1,50 +0,0 @@
|
|||||||
#include "displaylinkvsyncsource.h"
|
|
||||||
|
|
||||||
DisplayLinkVsyncSource::DisplayLinkVsyncSource(Pacer* pacer)
|
|
||||||
: m_Pacer(pacer),
|
|
||||||
m_DisplayLink(nullptr)
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
DisplayLinkVsyncSource::~DisplayLinkVsyncSource()
|
|
||||||
{
|
|
||||||
if (m_DisplayLink != nullptr) {
|
|
||||||
CVDisplayLinkStop(m_DisplayLink);
|
|
||||||
CVDisplayLinkRelease(m_DisplayLink);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CVReturn
|
|
||||||
DisplayLinkVsyncSource::displayLinkOutputCallback(
|
|
||||||
CVDisplayLinkRef,
|
|
||||||
const CVTimeStamp* /* now */,
|
|
||||||
const CVTimeStamp* /* vsyncTime */,
|
|
||||||
CVOptionFlags,
|
|
||||||
CVOptionFlags*,
|
|
||||||
void *displayLinkContext)
|
|
||||||
{
|
|
||||||
auto me = reinterpret_cast<DisplayLinkVsyncSource*>(displayLinkContext);
|
|
||||||
|
|
||||||
// In my testing on macOS 10.13, this callback is invoked about 24 ms
|
|
||||||
// prior to the specified v-sync time (now - vsyncTime). Since this is
|
|
||||||
// greater than the standard v-sync interval (16 ms = 60 FPS), we will
|
|
||||||
// draw using the current host time, rather than the actual v-sync target
|
|
||||||
// time. Because the CVDisplayLink is in sync with the actual v-sync
|
|
||||||
// interval, even if many ms prior, we can safely use the current host time
|
|
||||||
// and get a consistent callback for each v-sync. This reduces video latency
|
|
||||||
// by at least 1 frame vs. rendering with the actual vsyncTime.
|
|
||||||
me->m_Pacer->vsyncCallback(500 / me->m_DisplayFps);
|
|
||||||
|
|
||||||
return kCVReturnSuccess;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool DisplayLinkVsyncSource::initialize(SDL_Window*, int displayFps)
|
|
||||||
{
|
|
||||||
m_DisplayFps = displayFps;
|
|
||||||
|
|
||||||
CVDisplayLinkCreateWithActiveCGDisplays(&m_DisplayLink);
|
|
||||||
CVDisplayLinkSetOutputCallback(m_DisplayLink, displayLinkOutputCallback, this);
|
|
||||||
CVDisplayLinkStart(m_DisplayLink);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
@@ -1,31 +1,11 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <CoreVideo/CoreVideo.h>
|
|
||||||
|
|
||||||
#include "pacer.h"
|
#include "pacer.h"
|
||||||
|
|
||||||
class DisplayLinkVsyncSource : public IVsyncSource
|
class DisplayLinkVsyncSourceFactory
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
DisplayLinkVsyncSource(Pacer* pacer);
|
|
||||||
|
|
||||||
virtual ~DisplayLinkVsyncSource();
|
|
||||||
|
|
||||||
virtual bool initialize(SDL_Window* window, int displayFps);
|
|
||||||
|
|
||||||
private:
|
|
||||||
static
|
static
|
||||||
CVReturn
|
IVsyncSource* createVsyncSource(Pacer* pacer);
|
||||||
displayLinkOutputCallback(
|
|
||||||
CVDisplayLinkRef,
|
|
||||||
const CVTimeStamp* now,
|
|
||||||
const CVTimeStamp* /* vsyncTime */,
|
|
||||||
CVOptionFlags,
|
|
||||||
CVOptionFlags*,
|
|
||||||
void *displayLinkContext);
|
|
||||||
|
|
||||||
Pacer* m_Pacer;
|
|
||||||
CVDisplayLinkRef m_DisplayLink;
|
|
||||||
int m_DisplayFps;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
#include "displaylinkvsyncsource.h"
|
||||||
|
|
||||||
|
#include <SDL_syswm.h>
|
||||||
|
|
||||||
|
#include <CoreVideo/CoreVideo.h>
|
||||||
|
|
||||||
|
#import <Cocoa/Cocoa.h>
|
||||||
|
|
||||||
|
class DisplayLinkVsyncSource : public IVsyncSource
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
DisplayLinkVsyncSource(Pacer* pacer)
|
||||||
|
: m_Pacer(pacer),
|
||||||
|
m_DisplayLink(nullptr)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual ~DisplayLinkVsyncSource() override
|
||||||
|
{
|
||||||
|
if (m_DisplayLink != nullptr) {
|
||||||
|
CVDisplayLinkStop(m_DisplayLink);
|
||||||
|
CVDisplayLinkRelease(m_DisplayLink);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static
|
||||||
|
CGDirectDisplayID
|
||||||
|
getDisplayID(NSScreen* screen)
|
||||||
|
{
|
||||||
|
NSNumber* screenNumber = [screen deviceDescription][@"NSScreenNumber"];
|
||||||
|
return [screenNumber unsignedIntValue];
|
||||||
|
}
|
||||||
|
|
||||||
|
static
|
||||||
|
CVReturn
|
||||||
|
displayLinkOutputCallback(
|
||||||
|
CVDisplayLinkRef displayLink,
|
||||||
|
const CVTimeStamp* /* now */,
|
||||||
|
const CVTimeStamp* /* vsyncTime */,
|
||||||
|
CVOptionFlags,
|
||||||
|
CVOptionFlags*,
|
||||||
|
void *displayLinkContext)
|
||||||
|
{
|
||||||
|
auto me = reinterpret_cast<DisplayLinkVsyncSource*>(displayLinkContext);
|
||||||
|
|
||||||
|
SDL_assert(displayLink == me->m_DisplayLink);
|
||||||
|
|
||||||
|
// In my testing on macOS 10.13, this callback is invoked about 24 ms
|
||||||
|
// prior to the specified v-sync time (now - vsyncTime). Since this is
|
||||||
|
// greater than the standard v-sync interval (16 ms = 60 FPS), we will
|
||||||
|
// draw using the current host time, rather than the actual v-sync target
|
||||||
|
// time. Because the CVDisplayLink is in sync with the actual v-sync
|
||||||
|
// interval, even if many ms prior, we can safely use the current host time
|
||||||
|
// and get a consistent callback for each v-sync. This reduces video latency
|
||||||
|
// by at least 1 frame vs. rendering with the actual vsyncTime.
|
||||||
|
me->m_Pacer->vsyncCallback(500 / me->m_DisplayFps);
|
||||||
|
|
||||||
|
return kCVReturnSuccess;
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual bool initialize(SDL_Window* window, int displayFps) override
|
||||||
|
{
|
||||||
|
SDL_SysWMinfo info;
|
||||||
|
|
||||||
|
SDL_VERSION(&info.version);
|
||||||
|
|
||||||
|
if (!SDL_GetWindowWMInfo(window, &info)) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"SDL_GetWindowWMInfo() failed: %s",
|
||||||
|
SDL_GetError());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_assert(info.subsystem == SDL_SYSWM_COCOA);
|
||||||
|
|
||||||
|
m_DisplayFps = displayFps;
|
||||||
|
|
||||||
|
NSScreen* screen = [info.info.cocoa.window screen];
|
||||||
|
CVReturn status;
|
||||||
|
if (screen == nullptr) {
|
||||||
|
// Window not visible on any display, so use a
|
||||||
|
// CVDisplayLink that can work with all active displays.
|
||||||
|
// When we become visible, we'll recreate ourselves
|
||||||
|
// and associate with the new screen.
|
||||||
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"NSWindow is not visible on any display");
|
||||||
|
status = CVDisplayLinkCreateWithActiveCGDisplays(&m_DisplayLink);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
CGDirectDisplayID displayId;
|
||||||
|
displayId = getDisplayID(screen);
|
||||||
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"NSWindow on display: %x",
|
||||||
|
displayId);
|
||||||
|
status = CVDisplayLinkCreateWithCGDisplay(displayId, &m_DisplayLink);
|
||||||
|
}
|
||||||
|
if (status != kCVReturnSuccess) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"Failed to create CVDisplayLink: %d",
|
||||||
|
status);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
status = CVDisplayLinkSetOutputCallback(m_DisplayLink, displayLinkOutputCallback, this);
|
||||||
|
if (status != kCVReturnSuccess) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"CVDisplayLinkSetOutputCallback() failed: %d",
|
||||||
|
status);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
status = CVDisplayLinkStart(m_DisplayLink);
|
||||||
|
if (status != kCVReturnSuccess) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
|
"CVDisplayLinkStart() failed: %d",
|
||||||
|
status);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
Pacer* m_Pacer;
|
||||||
|
CVDisplayLinkRef m_DisplayLink;
|
||||||
|
int m_DisplayFps;
|
||||||
|
};
|
||||||
|
|
||||||
|
IVsyncSource* DisplayLinkVsyncSourceFactory::createVsyncSource(Pacer* pacer) {
|
||||||
|
return new DisplayLinkVsyncSource(pacer);
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
#include "pacer.h"
|
#include "pacer.h"
|
||||||
|
#include "streaming/streamutils.h"
|
||||||
|
|
||||||
#include "nullthreadedvsyncsource.h"
|
#include "nullthreadedvsyncsource.h"
|
||||||
|
|
||||||
@@ -120,22 +121,7 @@ bool Pacer::initialize(SDL_Window* window, int maxVideoFps, bool enableVsync)
|
|||||||
{
|
{
|
||||||
m_MaxVideoFps = maxVideoFps;
|
m_MaxVideoFps = maxVideoFps;
|
||||||
m_EnableVsync = enableVsync;
|
m_EnableVsync = enableVsync;
|
||||||
|
m_DisplayFps = StreamUtils::getDisplayRefreshRate(window);
|
||||||
int displayIndex = SDL_GetWindowDisplayIndex(window);
|
|
||||||
if (displayIndex < 0) {
|
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
|
||||||
"Failed to get current display: %s",
|
|
||||||
SDL_GetError());
|
|
||||||
|
|
||||||
// Assume display 0 if it fails
|
|
||||||
displayIndex = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
SDL_DisplayMode mode;
|
|
||||||
if (SDL_GetCurrentDisplayMode(displayIndex, &mode) == 0) {
|
|
||||||
// May be zero if undefined
|
|
||||||
m_DisplayFps = mode.refresh_rate;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m_EnableVsync) {
|
if (m_EnableVsync) {
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
@@ -143,7 +129,7 @@ bool Pacer::initialize(SDL_Window* window, int maxVideoFps, bool enableVsync)
|
|||||||
m_DisplayFps, m_MaxVideoFps);
|
m_DisplayFps, m_MaxVideoFps);
|
||||||
|
|
||||||
#if defined(Q_OS_DARWIN)
|
#if defined(Q_OS_DARWIN)
|
||||||
m_VsyncSource = new DisplayLinkVsyncSource(this);
|
m_VsyncSource = DisplayLinkVsyncSourceFactory::createVsyncSource(this);
|
||||||
#elif defined(Q_OS_WIN32)
|
#elif defined(Q_OS_WIN32)
|
||||||
m_VsyncSource = new DxVsyncSource(this);
|
m_VsyncSource = new DxVsyncSource(this);
|
||||||
#else
|
#else
|
||||||
|
|||||||
@@ -8,6 +8,12 @@ extern "C" {
|
|||||||
|
|
||||||
class IFFmpegRenderer {
|
class IFFmpegRenderer {
|
||||||
public:
|
public:
|
||||||
|
enum VSyncConstraint {
|
||||||
|
VSYNC_FORCE_OFF,
|
||||||
|
VSYNC_FORCE_ON,
|
||||||
|
VSYNC_ANY
|
||||||
|
};
|
||||||
|
|
||||||
virtual ~IFFmpegRenderer() {}
|
virtual ~IFFmpegRenderer() {}
|
||||||
virtual bool initialize(SDL_Window* window,
|
virtual bool initialize(SDL_Window* window,
|
||||||
int videoFormat,
|
int videoFormat,
|
||||||
@@ -18,6 +24,8 @@ public:
|
|||||||
virtual bool prepareDecoderContext(AVCodecContext* context) = 0;
|
virtual bool prepareDecoderContext(AVCodecContext* context) = 0;
|
||||||
virtual void renderFrameAtVsync(AVFrame* frame) = 0;
|
virtual void renderFrameAtVsync(AVFrame* frame) = 0;
|
||||||
virtual bool needsTestFrame() = 0;
|
virtual bool needsTestFrame() = 0;
|
||||||
|
virtual int getDecoderCapabilities() = 0;
|
||||||
|
virtual VSyncConstraint getVsyncConstraint() = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
class SdlRenderer : public IFFmpegRenderer {
|
class SdlRenderer : public IFFmpegRenderer {
|
||||||
@@ -33,6 +41,8 @@ public:
|
|||||||
virtual bool prepareDecoderContext(AVCodecContext* context);
|
virtual bool prepareDecoderContext(AVCodecContext* context);
|
||||||
virtual void renderFrameAtVsync(AVFrame* frame);
|
virtual void renderFrameAtVsync(AVFrame* frame);
|
||||||
virtual bool needsTestFrame();
|
virtual bool needsTestFrame();
|
||||||
|
virtual int getDecoderCapabilities();
|
||||||
|
virtual VSyncConstraint getVsyncConstraint();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
SDL_Renderer* m_Renderer;
|
SDL_Renderer* m_Renderer;
|
||||||
|
|||||||
@@ -36,6 +36,18 @@ bool SdlRenderer::needsTestFrame()
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int SdlRenderer::getDecoderCapabilities()
|
||||||
|
{
|
||||||
|
// The FFmpeg CPU decoder can handle reference frame invalidation,
|
||||||
|
// but only for H.264.
|
||||||
|
return CAPABILITY_REFERENCE_FRAME_INVALIDATION_AVC;
|
||||||
|
}
|
||||||
|
|
||||||
|
IFFmpegRenderer::VSyncConstraint SdlRenderer::getVsyncConstraint()
|
||||||
|
{
|
||||||
|
return VSYNC_ANY;
|
||||||
|
}
|
||||||
|
|
||||||
bool SdlRenderer::initialize(SDL_Window* window,
|
bool SdlRenderer::initialize(SDL_Window* window,
|
||||||
int,
|
int,
|
||||||
int width,
|
int width,
|
||||||
|
|||||||
@@ -149,6 +149,17 @@ VAAPIRenderer::needsTestFrame()
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int
|
||||||
|
VAAPIRenderer::getDecoderCapabilities()
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
IFFmpegRenderer::VSyncConstraint VAAPIRenderer::getVsyncConstraint()
|
||||||
|
{
|
||||||
|
return VSYNC_ANY;
|
||||||
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
VAAPIRenderer::renderFrameAtVsync(AVFrame* frame)
|
VAAPIRenderer::renderFrameAtVsync(AVFrame* frame)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ public:
|
|||||||
virtual bool prepareDecoderContext(AVCodecContext* context);
|
virtual bool prepareDecoderContext(AVCodecContext* context);
|
||||||
virtual void renderFrameAtVsync(AVFrame* frame);
|
virtual void renderFrameAtVsync(AVFrame* frame);
|
||||||
virtual bool needsTestFrame();
|
virtual bool needsTestFrame();
|
||||||
|
virtual int getDecoderCapabilities();
|
||||||
|
virtual VSyncConstraint getVsyncConstraint();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
int m_WindowSystem;
|
int m_WindowSystem;
|
||||||
|
|||||||
@@ -217,6 +217,9 @@ bool VDPAURenderer::prepareDecoderContext(AVCodecContext* context)
|
|||||||
// https://github.com/FFmpeg/FFmpeg/commit/64ecb78b7179cab2dbdf835463104679dbb7c895
|
// https://github.com/FFmpeg/FFmpeg/commit/64ecb78b7179cab2dbdf835463104679dbb7c895
|
||||||
context->hwaccel_flags |= AV_HWACCEL_FLAG_ALLOW_PROFILE_MISMATCH;
|
context->hwaccel_flags |= AV_HWACCEL_FLAG_ALLOW_PROFILE_MISMATCH;
|
||||||
|
|
||||||
|
// This flag is recommended due to hardware underreporting supported levels
|
||||||
|
context->hwaccel_flags |= AV_HWACCEL_FLAG_IGNORE_LEVEL;
|
||||||
|
|
||||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||||
"Using VDPAU accelerated renderer");
|
"Using VDPAU accelerated renderer");
|
||||||
|
|
||||||
@@ -230,6 +233,16 @@ bool VDPAURenderer::needsTestFrame()
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int VDPAURenderer::getDecoderCapabilities()
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
IFFmpegRenderer::VSyncConstraint VDPAURenderer::getVsyncConstraint()
|
||||||
|
{
|
||||||
|
return VSYNC_ANY;
|
||||||
|
}
|
||||||
|
|
||||||
void VDPAURenderer::renderFrameAtVsync(AVFrame* frame)
|
void VDPAURenderer::renderFrameAtVsync(AVFrame* frame)
|
||||||
{
|
{
|
||||||
VdpStatus status;
|
VdpStatus status;
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ public:
|
|||||||
virtual bool prepareDecoderContext(AVCodecContext* context);
|
virtual bool prepareDecoderContext(AVCodecContext* context);
|
||||||
virtual void renderFrameAtVsync(AVFrame* frame);
|
virtual void renderFrameAtVsync(AVFrame* frame);
|
||||||
virtual bool needsTestFrame();
|
virtual bool needsTestFrame();
|
||||||
|
virtual int getDecoderCapabilities();
|
||||||
|
virtual VSyncConstraint getVsyncConstraint();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
uint32_t m_VideoWidth, m_VideoHeight;
|
uint32_t m_VideoWidth, m_VideoHeight;
|
||||||
|
|||||||
@@ -196,6 +196,19 @@ public:
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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:
|
private:
|
||||||
void setupDisplayLayer()
|
void setupDisplayLayer()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#include <Limelight.h>
|
#include <Limelight.h>
|
||||||
#include "ffmpeg.h"
|
#include "ffmpeg.h"
|
||||||
|
#include "streaming/streamutils.h"
|
||||||
|
|
||||||
#ifdef Q_OS_WIN32
|
#ifdef Q_OS_WIN32
|
||||||
#include "ffmpeg-renderers/dxva2.h"
|
#include "ffmpeg-renderers/dxva2.h"
|
||||||
@@ -27,6 +28,11 @@ bool FFmpegVideoDecoder::isHardwareAccelerated()
|
|||||||
return m_HwDecodeCfg != nullptr;
|
return m_HwDecodeCfg != nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int FFmpegVideoDecoder::getDecoderCapabilities()
|
||||||
|
{
|
||||||
|
return m_Renderer->getDecoderCapabilities();
|
||||||
|
}
|
||||||
|
|
||||||
enum AVPixelFormat FFmpegVideoDecoder::ffGetFormat(AVCodecContext* context,
|
enum AVPixelFormat FFmpegVideoDecoder::ffGetFormat(AVCodecContext* context,
|
||||||
const enum AVPixelFormat* pixFmts)
|
const enum AVPixelFormat* pixFmts)
|
||||||
{
|
{
|
||||||
@@ -100,16 +106,37 @@ void FFmpegVideoDecoder::reset()
|
|||||||
delete m_Pacer;
|
delete m_Pacer;
|
||||||
m_Pacer = nullptr;
|
m_Pacer = nullptr;
|
||||||
|
|
||||||
|
// This must be called after deleting Pacer because it
|
||||||
|
// may be holding AVFrames to free in its destructor.
|
||||||
|
// However, it must be called before deleting the IFFmpegRenderer
|
||||||
|
// since the codec context may be referencing objects that we
|
||||||
|
// need to delete in the renderer destructor.
|
||||||
|
avcodec_free_context(&m_VideoDecoderCtx);
|
||||||
|
|
||||||
delete m_Renderer;
|
delete m_Renderer;
|
||||||
m_Renderer = nullptr;
|
m_Renderer = nullptr;
|
||||||
|
|
||||||
avcodec_free_context(&m_VideoDecoderCtx);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool FFmpegVideoDecoder::completeInitialization(AVCodec* decoder, SDL_Window* window,
|
bool FFmpegVideoDecoder::completeInitialization(AVCodec* decoder, SDL_Window* window,
|
||||||
int videoFormat, int width, int height,
|
int videoFormat, int width, int height,
|
||||||
int maxFps, bool enableVsync, bool testOnly)
|
int maxFps, bool enableVsync, bool testOnly)
|
||||||
{
|
{
|
||||||
|
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_Pacer = new Pacer(m_Renderer);
|
m_Pacer = new Pacer(m_Renderer);
|
||||||
if (!m_Pacer->initialize(window, maxFps, enableVsync)) {
|
if (!m_Pacer->initialize(window, maxFps, enableVsync)) {
|
||||||
return false;
|
return false;
|
||||||
@@ -125,6 +152,10 @@ bool FFmpegVideoDecoder::completeInitialization(AVCodec* decoder, SDL_Window* wi
|
|||||||
// Always request low delay decoding
|
// Always request low delay decoding
|
||||||
m_VideoDecoderCtx->flags |= AV_CODEC_FLAG_LOW_DELAY;
|
m_VideoDecoderCtx->flags |= AV_CODEC_FLAG_LOW_DELAY;
|
||||||
|
|
||||||
|
// Allow display of corrupt frames and frames missing references
|
||||||
|
m_VideoDecoderCtx->flags |= AV_CODEC_FLAG_OUTPUT_CORRUPT;
|
||||||
|
m_VideoDecoderCtx->flags2 |= AV_CODEC_FLAG2_SHOW_ALL;
|
||||||
|
|
||||||
// Enable slice multi-threading for software decoding
|
// Enable slice multi-threading for software decoding
|
||||||
if (!m_HwDecodeCfg) {
|
if (!m_HwDecodeCfg) {
|
||||||
m_VideoDecoderCtx->thread_type = FF_THREAD_SLICE;
|
m_VideoDecoderCtx->thread_type = FF_THREAD_SLICE;
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ public:
|
|||||||
int maxFps,
|
int maxFps,
|
||||||
bool enableVsync) override;
|
bool enableVsync) override;
|
||||||
virtual bool isHardwareAccelerated() override;
|
virtual bool isHardwareAccelerated() override;
|
||||||
|
virtual int getDecoderCapabilities() override;
|
||||||
virtual int submitDecodeUnit(PDECODE_UNIT du) override;
|
virtual int submitDecodeUnit(PDECODE_UNIT du) override;
|
||||||
virtual void renderFrame(SDL_UserEvent* event) override;
|
virtual void renderFrame(SDL_UserEvent* event) override;
|
||||||
virtual void dropFrame(SDL_UserEvent* event) override;
|
virtual void dropFrame(SDL_UserEvent* event) override;
|
||||||
|
|||||||
@@ -25,6 +25,12 @@ SLVideoDecoder::isHardwareAccelerated()
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int
|
||||||
|
SLVideoDecoder::getDecoderCapabilities()
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
bool
|
bool
|
||||||
SLVideoDecoder::initialize(StreamingPreferences::VideoDecoderSelection vds,
|
SLVideoDecoder::initialize(StreamingPreferences::VideoDecoderSelection vds,
|
||||||
SDL_Window*,
|
SDL_Window*,
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ public:
|
|||||||
int frameRate,
|
int frameRate,
|
||||||
bool enableVsync);
|
bool enableVsync);
|
||||||
virtual bool isHardwareAccelerated();
|
virtual bool isHardwareAccelerated();
|
||||||
|
virtual int getDecoderCapabilities();
|
||||||
virtual int submitDecodeUnit(PDECODE_UNIT du);
|
virtual int submitDecodeUnit(PDECODE_UNIT du);
|
||||||
|
|
||||||
// Unused since rendering is done directly from the decode thread
|
// Unused since rendering is done directly from the decode thread
|
||||||
|
|||||||
Submodule moonlight-common-c/moonlight-common-c updated: feb46b978e...8743ce5d08.
@@ -12,6 +12,9 @@ TEMPLATE = lib
|
|||||||
# Support debug and release builds from command line for CI
|
# Support debug and release builds from command line for CI
|
||||||
CONFIG += debug_and_release
|
CONFIG += debug_and_release
|
||||||
|
|
||||||
|
# Ensure symbols are always generated
|
||||||
|
CONFIG += force_debug_info
|
||||||
|
|
||||||
win32 {
|
win32 {
|
||||||
INCLUDEPATH += $$PWD/../libs/windows/include
|
INCLUDEPATH += $$PWD/../libs/windows/include
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ TEMPLATE = lib
|
|||||||
# Support debug and release builds from command line for CI
|
# Support debug and release builds from command line for CI
|
||||||
CONFIG += debug_and_release
|
CONFIG += debug_and_release
|
||||||
|
|
||||||
|
# Ensure symbols are always generated
|
||||||
|
CONFIG += force_debug_info
|
||||||
|
|
||||||
QMDNSE_DIR = $$PWD/qmdnsengine/src
|
QMDNSE_DIR = $$PWD/qmdnsengine/src
|
||||||
DEFINES += \
|
DEFINES += \
|
||||||
QT_NO_SIGNALS_SLOTS_KEYWORDS
|
QT_NO_SIGNALS_SLOTS_KEYWORDS
|
||||||
|
|||||||
@@ -33,6 +33,12 @@ pushd $BUILD_FOLDER
|
|||||||
make $(echo "$BUILD_CONFIG" | tr '[:upper:]' '[:lower:]') || fail "Make failed!"
|
make $(echo "$BUILD_CONFIG" | tr '[:upper:]' '[:lower:]') || fail "Make failed!"
|
||||||
popd
|
popd
|
||||||
|
|
||||||
|
echo Saving dSYM file
|
||||||
|
pushd $BUILD_FOLDER
|
||||||
|
dsymutil app/Moonlight.app/Contents/MacOS/Moonlight -o Moonlight.dsym || fail "dSYM creation failed!"
|
||||||
|
cp -R Moonlight.dsym $INSTALLER_FOLDER || fail "dSYM copy failed!"
|
||||||
|
popd
|
||||||
|
|
||||||
echo Copying dylib dependencies
|
echo Copying dylib dependencies
|
||||||
mkdir $BUILD_FOLDER/app/Moonlight.app/Contents/lib
|
mkdir $BUILD_FOLDER/app/Moonlight.app/Contents/lib
|
||||||
cp $SOURCE_ROOT/libs/mac/lib/*.dylib $BUILD_FOLDER/app/Moonlight.app/Contents/lib/ || fail "Dylib copy failed!"
|
cp $SOURCE_ROOT/libs/mac/lib/*.dylib $BUILD_FOLDER/app/Moonlight.app/Contents/lib/ || fail "Dylib copy failed!"
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ if /I "%BUILD_CONFIG%"=="debug" (
|
|||||||
if /I "%BUILD_CONFIG%"=="signed-release" (
|
if /I "%BUILD_CONFIG%"=="signed-release" (
|
||||||
set BUILD_CONFIG=release
|
set BUILD_CONFIG=release
|
||||||
set SIGN=1
|
set SIGN=1
|
||||||
|
set MUST_DEPLOY_SYMBOLS=1
|
||||||
) else (
|
) else (
|
||||||
echo Invalid build configuration
|
echo Invalid build configuration
|
||||||
exit /b 1
|
exit /b 1
|
||||||
@@ -31,7 +32,6 @@ if /I "%ARCH%" NEQ "x86" (
|
|||||||
)
|
)
|
||||||
|
|
||||||
set VS_PATH=%ProgramFiles(x86)%\Microsoft Visual Studio\2017\Community
|
set VS_PATH=%ProgramFiles(x86)%\Microsoft Visual Studio\2017\Community
|
||||||
set VCREDIST_PATH=%VS_PATH%\VC\Redist\MSVC\14.14.26405
|
|
||||||
set SIGNTOOL_PARAMS=sign /tr http://timestamp.digicert.com /td sha256 /fd sha256 /sha1 1B3C676E831A94EC0327C3347EB32C68C26B3A67 /v
|
set SIGNTOOL_PARAMS=sign /tr http://timestamp.digicert.com /td sha256 /fd sha256 /sha1 1B3C676E831A94EC0327C3347EB32C68C26B3A67 /v
|
||||||
|
|
||||||
call "%VS_PATH%\VC\Auxiliary\Build\vcvarsall.bat" %ARCH%
|
call "%VS_PATH%\VC\Auxiliary\Build\vcvarsall.bat" %ARCH%
|
||||||
@@ -42,15 +42,18 @@ set SOURCE_ROOT=%cd%
|
|||||||
set BUILD_FOLDER=%BUILD_ROOT%\build-%ARCH%-%BUILD_CONFIG%
|
set BUILD_FOLDER=%BUILD_ROOT%\build-%ARCH%-%BUILD_CONFIG%
|
||||||
set DEPLOY_FOLDER=%BUILD_ROOT%\deploy-%ARCH%-%BUILD_CONFIG%
|
set DEPLOY_FOLDER=%BUILD_ROOT%\deploy-%ARCH%-%BUILD_CONFIG%
|
||||||
set INSTALLER_FOLDER=%BUILD_ROOT%\installer-%ARCH%-%BUILD_CONFIG%
|
set INSTALLER_FOLDER=%BUILD_ROOT%\installer-%ARCH%-%BUILD_CONFIG%
|
||||||
|
set SYMBOLS_FOLDER=%BUILD_ROOT%\symbols-%ARCH%-%BUILD_CONFIG%
|
||||||
|
|
||||||
echo Cleaning output directories
|
echo Cleaning output directories
|
||||||
rmdir /s /q %DEPLOY_FOLDER%
|
rmdir /s /q %DEPLOY_FOLDER%
|
||||||
rmdir /s /q %BUILD_FOLDER%
|
rmdir /s /q %BUILD_FOLDER%
|
||||||
rmdir /s /q %INSTALLER_FOLDER%
|
rmdir /s /q %INSTALLER_FOLDER%
|
||||||
|
rmdir /s /q %SYMBOLS_FOLDER%
|
||||||
mkdir %BUILD_ROOT%
|
mkdir %BUILD_ROOT%
|
||||||
mkdir %DEPLOY_FOLDER%
|
mkdir %DEPLOY_FOLDER%
|
||||||
mkdir %BUILD_FOLDER%
|
mkdir %BUILD_FOLDER%
|
||||||
mkdir %INSTALLER_FOLDER%
|
mkdir %INSTALLER_FOLDER%
|
||||||
|
mkdir %SYMBOLS_FOLDER%
|
||||||
|
|
||||||
echo Configuring the project
|
echo Configuring the project
|
||||||
pushd %BUILD_FOLDER%
|
pushd %BUILD_FOLDER%
|
||||||
@@ -64,6 +67,25 @@ nmake %BUILD_CONFIG%
|
|||||||
if !ERRORLEVEL! NEQ 0 goto Error
|
if !ERRORLEVEL! NEQ 0 goto Error
|
||||||
popd
|
popd
|
||||||
|
|
||||||
|
echo Saving PDBs
|
||||||
|
for /r "%BUILD_FOLDER%" %%f in (*.pdb) do (
|
||||||
|
copy "%%f" "%SYMBOLS_FOLDER%\"
|
||||||
|
if !ERRORLEVEL! NEQ 0 goto Error
|
||||||
|
)
|
||||||
|
7z a %SYMBOLS_FOLDER%\MoonlightDebuggingSymbols.zip %SYMBOLS_FOLDER%\*.pdb
|
||||||
|
if !ERRORLEVEL! NEQ 0 goto Error
|
||||||
|
|
||||||
|
if "%ML_SYMBOL_STORE%" NEQ "" (
|
||||||
|
echo Publishing PDBs to symbol store: %ML_SYMBOL_STORE%
|
||||||
|
symstore add /f %SYMBOLS_FOLDER%\*.pdb /s %ML_SYMBOL_STORE% /t Moonlight
|
||||||
|
if !ERRORLEVEL! NEQ 0 goto Error
|
||||||
|
) else (
|
||||||
|
if "%MUST_DEPLOY_SYMBOLS%"=="1" (
|
||||||
|
echo "A symbol server must be specified in ML_SYMBOL_STORE for signed release builds"
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
echo Copying DLL dependencies
|
echo Copying DLL dependencies
|
||||||
copy %SOURCE_ROOT%\libs\windows\lib\%ARCH%\*.dll %DEPLOY_FOLDER%
|
copy %SOURCE_ROOT%\libs\windows\lib\%ARCH%\*.dll %DEPLOY_FOLDER%
|
||||||
if !ERRORLEVEL! NEQ 0 goto Error
|
if !ERRORLEVEL! NEQ 0 goto Error
|
||||||
@@ -99,7 +121,7 @@ if "%SIGN%"=="1" (
|
|||||||
)
|
)
|
||||||
|
|
||||||
echo Building bundle
|
echo Building bundle
|
||||||
set VCREDIST_INSTALLER=%VCREDIST_PATH%\vcredist_%ARCH%.exe
|
set VCREDIST_INSTALLER=%VCToolsRedistDir%vcredist_%ARCH%.exe
|
||||||
rem Bundles are always x86 binaries
|
rem Bundles are always x86 binaries
|
||||||
msbuild %SOURCE_ROOT%\wix\MoonlightSetup\MoonlightSetup.wixproj /p:Configuration=%BUILD_CONFIG% /p:Platform=x86
|
msbuild %SOURCE_ROOT%\wix\MoonlightSetup\MoonlightSetup.wixproj /p:Configuration=%BUILD_CONFIG% /p:Platform=x86
|
||||||
if !ERRORLEVEL! NEQ 0 goto Error
|
if !ERRORLEVEL! NEQ 0 goto Error
|
||||||
@@ -119,7 +141,7 @@ if "%SIGN%"=="1" (
|
|||||||
echo Building portable package
|
echo Building portable package
|
||||||
rem This must be done after WiX harvesting and signing, since the VCRT dlls are MS signed
|
rem This must be done after WiX harvesting and signing, since the VCRT dlls are MS signed
|
||||||
rem and should not be harvested for inclusion in the full installer
|
rem and should not be harvested for inclusion in the full installer
|
||||||
copy "%VCREDIST_PATH%\%ARCH%\Microsoft.VC141.CRT\*.dll" %DEPLOY_FOLDER%
|
copy "%VCToolsRedistDir%%ARCH%\Microsoft.VC141.CRT\*.dll" %DEPLOY_FOLDER%
|
||||||
rem This file tells Moonlight that it's a portable installation
|
rem This file tells Moonlight that it's a portable installation
|
||||||
echo. > %DEPLOY_FOLDER%\portable.dat
|
echo. > %DEPLOY_FOLDER%\portable.dat
|
||||||
if !ERRORLEVEL! NEQ 0 goto Error
|
if !ERRORLEVEL! NEQ 0 goto Error
|
||||||
@@ -132,6 +154,7 @@ if /i "%APPVEYOR%"=="true" (
|
|||||||
if !ERRORLEVEL! NEQ 0 goto Error
|
if !ERRORLEVEL! NEQ 0 goto Error
|
||||||
appveyor PushArtifact %INSTALLER_FOLDER%\MoonlightPortable.zip -FileName MoonlightPortable-%ARCH%-%BUILD_CONFIG%.zip
|
appveyor PushArtifact %INSTALLER_FOLDER%\MoonlightPortable.zip -FileName MoonlightPortable-%ARCH%-%BUILD_CONFIG%.zip
|
||||||
if !ERRORLEVEL! NEQ 0 goto Error
|
if !ERRORLEVEL! NEQ 0 goto Error
|
||||||
|
appveyor PushArtifact %SYMBOLS_FOLDER%\MoonlightDebuggingSymbols.zip -FileName MoonlightDebuggingSymbols-%ARCH%-%BUILD_CONFIG%.zip
|
||||||
)
|
)
|
||||||
|
|
||||||
echo Build successful!
|
echo Build successful!
|
||||||
|
|||||||
@@ -39,7 +39,7 @@
|
|||||||
</Directory>
|
</Directory>
|
||||||
</Directory>
|
</Directory>
|
||||||
|
|
||||||
<Property Id="APPDATAFOLDER">%LOCALAPPDATA%\Moonlight Game Streaming</Property>
|
<Property Id="APPDATAFOLDER">%LOCALAPPDATA%\Moonlight Game Streaming Project</Property>
|
||||||
|
|
||||||
<!-- There's no way to delete a registry key on uninstall but not major upgrade, so
|
<!-- There's no way to delete a registry key on uninstall but not major upgrade, so
|
||||||
we have to roll our own deletion via custom action -->
|
we have to roll our own deletion via custom action -->
|
||||||
@@ -60,16 +60,38 @@
|
|||||||
Target="[#MoonlightExe]"
|
Target="[#MoonlightExe]"
|
||||||
Directory="ApplicationProgramsFolder"
|
Directory="ApplicationProgramsFolder"
|
||||||
WorkingDirectory="INSTALLFOLDER" />
|
WorkingDirectory="INSTALLFOLDER" />
|
||||||
|
<RemoveFolder Id="CleanupStartMenuShortcut" Directory="ApplicationProgramsFolder" On="uninstall" />
|
||||||
|
<util:RemoveFolderEx Id="CleanupAppDataFolder" On="uninstall" Property="APPDATAFOLDER" />
|
||||||
|
<RegistryValue Root="HKCU" Key="Software\Moonlight Game Streaming Project" Name="Installed" Type="integer" Value="1" KeyPath="yes" />
|
||||||
|
</Component>
|
||||||
|
|
||||||
|
<Component Id="MoonlightDesktopShortcut" Guid="*" Directory="INSTALLFOLDER">
|
||||||
<Shortcut Id="DesktopShortcut"
|
<Shortcut Id="DesktopShortcut"
|
||||||
Name="$(var.ShortcutName)"
|
Name="$(var.ShortcutName)"
|
||||||
Description="$(var.ShortcutDesc)"
|
Description="$(var.ShortcutDesc)"
|
||||||
Target="[#MoonlightExe]"
|
Target="[#MoonlightExe]"
|
||||||
Directory="DesktopFolder"
|
Directory="DesktopFolder"
|
||||||
WorkingDirectory="INSTALLFOLDER" />
|
WorkingDirectory="INSTALLFOLDER" />
|
||||||
<RemoveFolder Id="CleanupStartMenuShortcut" Directory="ApplicationProgramsFolder" On="uninstall" />
|
|
||||||
<RemoveFolder Id="CleanupDesktopShortcut" Directory="DesktopFolder" On="uninstall" />
|
<RemoveFolder Id="CleanupDesktopShortcut" Directory="DesktopFolder" On="uninstall" />
|
||||||
<util:RemoveFolderEx Id="CleanupAppDataFolder" On="uninstall" Property="APPDATAFOLDER" />
|
<RegistryValue Root="HKCU"
|
||||||
<RegistryValue Root="HKCU" Key="Software\Moonlight Game Streaming Project" Name="Installed" Type="integer" Value="1" KeyPath="yes" />
|
Key="Software\Moonlight Game Streaming Project"
|
||||||
|
Name="DesktopShortcutInstalled"
|
||||||
|
Type="integer"
|
||||||
|
Value="1"
|
||||||
|
KeyPath="yes" />
|
||||||
|
<Condition>ADDDESKTOPSHORTCUT=1</Condition>
|
||||||
|
</Component>
|
||||||
|
|
||||||
|
<!-- Persist desktop shortcut's installed state to let Bundle.wxs know if
|
||||||
|
the desktop shortcut should installed by default when upgrading the
|
||||||
|
product -->
|
||||||
|
<Component Id="MoonlightDesktopShortcutState" Guid="*" Directory="INSTALLFOLDER">
|
||||||
|
<RegistryValue Root="HKCU"
|
||||||
|
Key="Software\Moonlight Game Streaming Project"
|
||||||
|
Name="DesktopShortcutInstallState"
|
||||||
|
Type="integer"
|
||||||
|
Value="[ADDDESKTOPSHORTCUT]"
|
||||||
|
KeyPath="yes" />
|
||||||
</Component>
|
</Component>
|
||||||
|
|
||||||
<DirectoryRef Id="INSTALLFOLDER">
|
<DirectoryRef Id="INSTALLFOLDER">
|
||||||
@@ -85,6 +107,8 @@
|
|||||||
<Feature Id="ProductFeature" Title="Moonlight" Level="1" ConfigurableDirectory="INSTALLFOLDER">
|
<Feature Id="ProductFeature" Title="Moonlight" Level="1" ConfigurableDirectory="INSTALLFOLDER">
|
||||||
<ComponentRef Id="Moonlight" />
|
<ComponentRef Id="Moonlight" />
|
||||||
<ComponentRef Id="MoonlightShortcuts" />
|
<ComponentRef Id="MoonlightShortcuts" />
|
||||||
|
<ComponentRef Id="MoonlightDesktopShortcutState" />
|
||||||
|
<ComponentRef Id="MoonlightDesktopShortcut" />
|
||||||
<ComponentGroupRef Id="MoonlightDependencies" />
|
<ComponentGroupRef Id="MoonlightDependencies" />
|
||||||
</Feature>
|
</Feature>
|
||||||
</Product>
|
</Product>
|
||||||
|
|||||||
@@ -51,13 +51,32 @@
|
|||||||
<Variable Name="InstallFolder" Type="string" Value="[ProgramFilesFolder]Moonlight Game Streaming" />
|
<Variable Name="InstallFolder" Type="string" Value="[ProgramFilesFolder]Moonlight Game Streaming" />
|
||||||
<?endif ?>
|
<?endif ?>
|
||||||
|
|
||||||
|
<!-- Define "Add desktop shortcut" -checkbox's state by defining a variable
|
||||||
|
which has same name as the checkbox has. Value 1 means that checkbox
|
||||||
|
is checked, 0 means that is unchecked-->
|
||||||
|
<!-- Set checkbox's state as checked by default -->
|
||||||
|
<Variable Name="AddDesktopShortcutCheckbox" Type="numeric" Value="1" />
|
||||||
|
<!-- Get checkbox's state from registry if present. The registry value
|
||||||
|
"DesktopShortcutInstallState" is set in Product.wxs. -->
|
||||||
|
<util:RegistrySearch Variable="HasDesktopShortcutInstallStateRegKey"
|
||||||
|
Root="HKCU"
|
||||||
|
Key="Software\Moonlight Game Streaming Project"
|
||||||
|
Value="DesktopShortcutInstallState"
|
||||||
|
Result="exists" />
|
||||||
|
<util:RegistrySearch Variable="AddDesktopShortcutCheckbox"
|
||||||
|
Root="HKCU"
|
||||||
|
Key="Software\Moonlight Game Streaming Project"
|
||||||
|
Value="DesktopShortcutInstallState"
|
||||||
|
Condition="HasDesktopShortcutInstallStateRegKey" />
|
||||||
|
|
||||||
<BootstrapperApplicationRef Id="WixStandardBootstrapperApplication.RtfLicense">
|
<BootstrapperApplicationRef Id="WixStandardBootstrapperApplication.RtfLicense">
|
||||||
<bal:WixStandardBootstrapperApplication
|
<bal:WixStandardBootstrapperApplication
|
||||||
ShowVersion="yes"
|
ShowVersion="yes"
|
||||||
LicenseFile="license.rtf"
|
LicenseFile="license.rtf"
|
||||||
LogoFile="..\..\app\moonlight_wix.png"
|
LogoFile="..\..\app\moonlight_wix.png"
|
||||||
ShowFilesInUse="yes"
|
ShowFilesInUse="yes"
|
||||||
LaunchTarget="[InstallFolder]\Moonlight.exe" />
|
LaunchTarget="[InstallFolder]\Moonlight.exe"
|
||||||
|
ThemeFile="RtfTheme.xml" />
|
||||||
</BootstrapperApplicationRef>
|
</BootstrapperApplicationRef>
|
||||||
|
|
||||||
<Chain>
|
<Chain>
|
||||||
@@ -75,6 +94,7 @@
|
|||||||
<MsiPackage Id="Moonlight" SourceFile="$(var.Moonlight.TargetPath)" Vital="yes">
|
<MsiPackage Id="Moonlight" SourceFile="$(var.Moonlight.TargetPath)" Vital="yes">
|
||||||
<MsiProperty Name="INSTALLFOLDER" Value="[InstallFolder]" />
|
<MsiProperty Name="INSTALLFOLDER" Value="[InstallFolder]" />
|
||||||
<MsiProperty Name="REINSTALLMODE" Value="[REINSTALLMODE]" />
|
<MsiProperty Name="REINSTALLMODE" Value="[REINSTALLMODE]" />
|
||||||
|
<MsiProperty Name="ADDDESKTOPSHORTCUT" Value="[AddDesktopShortcutCheckbox]" />
|
||||||
</MsiPackage>
|
</MsiPackage>
|
||||||
</Chain>
|
</Chain>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. -->
|
||||||
|
<!-- This file has been copied from C:\Program Files (x86)\WiX Toolset v3.11\SDK\themes\RtfTheme.xml and then altered. -->
|
||||||
|
|
||||||
|
<Theme xmlns="http://wixtoolset.org/schemas/thmutil/2010">
|
||||||
|
<Window Width="485" Height="300" HexStyle="100a0000" FontId="0">#(loc.Caption)</Window>
|
||||||
|
<Font Id="0" Height="-12" Weight="500" Foreground="000000" Background="FFFFFF">Segoe UI</Font>
|
||||||
|
<Font Id="1" Height="-24" Weight="500" Foreground="000000">Segoe UI</Font>
|
||||||
|
<Font Id="2" Height="-22" Weight="500" Foreground="666666">Segoe UI</Font>
|
||||||
|
<Font Id="3" Height="-12" Weight="500" Foreground="000000" Background="FFFFFF">Segoe UI</Font>
|
||||||
|
<Font Id="4" Height="-12" Weight="500" Foreground="ff0000" Background="FFFFFF" Underline="yes">Segoe UI</Font>
|
||||||
|
|
||||||
|
<Image X="11" Y="11" Width="64" Height="64" ImageFile="logo.png" Visible="yes"/>
|
||||||
|
<Text X="80" Y="11" Width="-11" Height="64" FontId="1" Visible="yes" DisablePrefix="yes">#(loc.Title)</Text>
|
||||||
|
|
||||||
|
<Page Name="Help">
|
||||||
|
<Text X="11" Y="80" Width="-11" Height="30" FontId="2" DisablePrefix="yes">#(loc.HelpHeader)</Text>
|
||||||
|
<Text X="11" Y="112" Width="-11" Height="-35" FontId="3" DisablePrefix="yes">#(loc.HelpText)</Text>
|
||||||
|
<Button Name="HelpCancelButton" X="-11" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">#(loc.HelpCloseButton)</Button>
|
||||||
|
</Page>
|
||||||
|
<Page Name="Install">
|
||||||
|
<Richedit Name="EulaRichedit" X="11" Y="80" Width="-11" Height="-70" TabStop="yes" FontId="0" HexStyle="0x800000" />
|
||||||
|
<Checkbox Name="EulaAcceptCheckbox" X="-11" Y="-41" Width="260" Height="17" TabStop="yes" FontId="3" HideWhenDisabled="yes">#(loc.InstallAcceptCheckbox)</Checkbox>
|
||||||
|
<Button Name="OptionsButton" X="-171" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0" HideWhenDisabled="yes">#(loc.InstallOptionsButton)</Button>
|
||||||
|
<Button Name="InstallButton" X="-91" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">#(loc.InstallInstallButton)</Button>
|
||||||
|
<Button Name="WelcomeCancelButton" X="-11" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">#(loc.InstallCloseButton)</Button>
|
||||||
|
</Page>
|
||||||
|
<Page Name="Options">
|
||||||
|
<Text X="11" Y="80" Width="-11" Height="30" FontId="2" DisablePrefix="yes">#(loc.OptionsHeader)</Text>
|
||||||
|
<Text X="11" Y="121" Width="-11" Height="17" FontId="3" DisablePrefix="yes">#(loc.OptionsLocationLabel)</Text>
|
||||||
|
<Editbox Name="FolderEditbox" X="11" Y="143" Width="-91" Height="21" TabStop="yes" FontId="3" FileSystemAutoComplete="yes" />
|
||||||
|
<Button Name="BrowseButton" X="-11" Y="142" Width="75" Height="23" TabStop="yes" FontId="3">#(loc.OptionsBrowseButton)</Button>
|
||||||
|
|
||||||
|
<!-- Added a checkbox for user to choose if shortcut is placed on
|
||||||
|
desktop or not. Checkbox's current state (checked/unchecked) is
|
||||||
|
read from a variable which has same name as this checkbox. The
|
||||||
|
variable is defined in Bundle.wxs. -->
|
||||||
|
<Checkbox Name="AddDesktopShortcutCheckbox" X="11" Y="178" Width="250" Height="17" TabStop="yes" FontId="3" HideWhenDisabled="yes">Add desktop shortcut</Checkbox>
|
||||||
|
|
||||||
|
<Button Name="OptionsOkButton" X="-91" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">#(loc.OptionsOkButton)</Button>
|
||||||
|
<Button Name="OptionsCancelButton" X="-11" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">#(loc.OptionsCancelButton)</Button>
|
||||||
|
</Page>
|
||||||
|
<Page Name="FilesInUse">
|
||||||
|
<Text X="11" Y="80" Width="-11" Height="30" FontId="2" DisablePrefix="yes">#(loc.FilesInUseHeader)</Text>
|
||||||
|
<Text X="11" Y="121" Width="-11" Height="34" FontId="3" DisablePrefix="yes">#(loc.FilesInUseLabel)</Text>
|
||||||
|
<Text Name="FilesInUseText" X="11" Y="150" Width="-11" Height="-86" FontId="3" DisablePrefix="yes" HexStyle="0x0000C000"></Text>
|
||||||
|
|
||||||
|
<Button Name="FilesInUseCloseRadioButton" X="11" Y="-60" Width="-11" Height="23" TabStop="yes" FontId="0" HideWhenDisabled="yes" HexStyle="0x000009">#(loc.FilesInUseCloseRadioButton)</Button>
|
||||||
|
<Button Name="FilesInUseDontCloseRadioButton" X="11" Y="-40" Width="-11" Height="23" TabStop="yes" FontId="0" HideWhenDisabled="yes" HexStyle="0x000009">#(loc.FilesInUseDontCloseRadioButton)</Button>
|
||||||
|
|
||||||
|
<Button Name="FilesInUseOkButton" X="-91" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0" HideWhenDisabled="yes">#(loc.FilesInUseOkButton)</Button>
|
||||||
|
<Button Name="FilesInUseCancelButton" X="-11" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">#(loc.FilesInUseCancelButton)</Button>
|
||||||
|
</Page>
|
||||||
|
<Page Name="Progress">
|
||||||
|
<Text X="11" Y="80" Width="-11" Height="30" FontId="2" DisablePrefix="yes">#(loc.ProgressHeader)</Text>
|
||||||
|
<Text X="11" Y="121" Width="70" Height="17" FontId="3" DisablePrefix="yes">#(loc.ProgressLabel)</Text>
|
||||||
|
<Text Name="OverallProgressPackageText" X="85" Y="121" Width="-11" Height="17" FontId="3" DisablePrefix="yes">#(loc.OverallProgressPackageText)</Text>
|
||||||
|
<Progressbar Name="OverallCalculatedProgressbar" X="11" Y="143" Width="-11" Height="15" />
|
||||||
|
<Button Name="ProgressCancelButton" X="-11" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">#(loc.ProgressCancelButton)</Button>
|
||||||
|
</Page>
|
||||||
|
<Page Name="Modify">
|
||||||
|
<Text X="11" Y="80" Width="-11" Height="30" FontId="2" DisablePrefix="yes">#(loc.ModifyHeader)</Text>
|
||||||
|
<Button Name="RepairButton" X="-171" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0" HideWhenDisabled="yes">#(loc.ModifyRepairButton)</Button>
|
||||||
|
<Button Name="UninstallButton" X="-91" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">#(loc.ModifyUninstallButton)</Button>
|
||||||
|
<Button Name="ModifyCancelButton" X="-11" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">#(loc.ModifyCloseButton)</Button>
|
||||||
|
</Page>
|
||||||
|
<Page Name="Success">
|
||||||
|
<Text Name="SuccessHeader" X="11" Y="80" Width="-11" Height="30" FontId="2" HideWhenDisabled="yes" DisablePrefix="yes">#(loc.SuccessHeader)</Text>
|
||||||
|
<Text Name="SuccessInstallHeader" X="11" Y="80" Width="-11" Height="30" FontId="2" HideWhenDisabled="yes" DisablePrefix="yes">#(loc.SuccessInstallHeader)</Text>
|
||||||
|
<Text Name="SuccessRepairHeader" X="11" Y="80" Width="-11" Height="30" FontId="2" HideWhenDisabled="yes" DisablePrefix="yes">#(loc.SuccessRepairHeader)</Text>
|
||||||
|
<Text Name="SuccessUninstallHeader" X="11" Y="80" Width="-11" Height="30" FontId="2" HideWhenDisabled="yes" DisablePrefix="yes">#(loc.SuccessUninstallHeader)</Text>
|
||||||
|
<Button Name="LaunchButton" X="-91" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0" HideWhenDisabled="yes">#(loc.SuccessLaunchButton)</Button>
|
||||||
|
<Text Name="SuccessRestartText" X="-11" Y="-51" Width="400" Height="34" FontId="3" HideWhenDisabled="yes" DisablePrefix="yes">#(loc.SuccessRestartText)</Text>
|
||||||
|
<Button Name="SuccessRestartButton" X="-91" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0" HideWhenDisabled="yes">#(loc.SuccessRestartButton)</Button>
|
||||||
|
<Button Name="SuccessCancelButton" X="-11" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">#(loc.SuccessCloseButton)</Button>
|
||||||
|
</Page>
|
||||||
|
<Page Name="Failure">
|
||||||
|
<Text Name="FailureHeader" X="11" Y="80" Width="-11" Height="30" FontId="2" HideWhenDisabled="yes" DisablePrefix="yes">#(loc.FailureHeader)</Text>
|
||||||
|
<Text Name="FailureInstallHeader" X="11" Y="80" Width="-11" Height="30" FontId="2" HideWhenDisabled="yes" DisablePrefix="yes">#(loc.FailureInstallHeader)</Text>
|
||||||
|
<Text Name="FailureUninstallHeader" X="11" Y="80" Width="-11" Height="30" FontId="2" HideWhenDisabled="yes" DisablePrefix="yes">#(loc.FailureUninstallHeader)</Text>
|
||||||
|
<Text Name="FailureRepairHeader" X="11" Y="80" Width="-11" Height="30" FontId="2" HideWhenDisabled="yes" DisablePrefix="yes">#(loc.FailureRepairHeader)</Text>
|
||||||
|
<Hypertext Name="FailureLogFileLink" X="11" Y="121" Width="-11" Height="42" FontId="3" TabStop="yes" HideWhenDisabled="yes">#(loc.FailureHyperlinkLogText)</Hypertext>
|
||||||
|
<Hypertext Name="FailureMessageText" X="22" Y="163" Width="-11" Height="51" FontId="3" TabStop="yes" HideWhenDisabled="yes" />
|
||||||
|
<Text Name="FailureRestartText" X="-11" Y="-51" Width="400" Height="34" FontId="3" HideWhenDisabled="yes" DisablePrefix="yes">#(loc.FailureRestartText)</Text>
|
||||||
|
<Button Name="FailureRestartButton" X="-91" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0" HideWhenDisabled="yes">#(loc.FailureRestartButton)</Button>
|
||||||
|
<Button Name="FailureCloseButton" X="-11" Y="-11" Width="75" Height="23" TabStop="yes" FontId="0">#(loc.FailureCloseButton)</Button>
|
||||||
|
</Page>
|
||||||
|
</Theme>
|
||||||
Reference in new issue
Block a user