mirror of
https://github.com/moonlight-stream/moonlight-qt
synced 2025-02-26 03:37:09 +00:00
* Add quit cli command and app quit option after stream session. Fixes #92 * Code review fixes.
This commit is contained in:
parent
ad47990a87
commit
0ab07303c9
22 changed files with 678 additions and 67 deletions
|
@ -100,6 +100,7 @@ macx {
|
|||
|
||||
SOURCES += \
|
||||
main.cpp \
|
||||
backend/computerseeker.cpp \
|
||||
backend/identitymanager.cpp \
|
||||
backend/nvcomputer.cpp \
|
||||
backend/nvhttp.cpp \
|
||||
|
@ -107,6 +108,7 @@ SOURCES += \
|
|||
backend/computermanager.cpp \
|
||||
backend/boxartmanager.cpp \
|
||||
cli/commandlineparser.cpp \
|
||||
cli/quitstream.cpp \
|
||||
cli/startstream.cpp \
|
||||
settings/streamingpreferences.cpp \
|
||||
streaming/input.cpp \
|
||||
|
@ -123,6 +125,7 @@ SOURCES += \
|
|||
|
||||
HEADERS += \
|
||||
utils.h \
|
||||
backend/computerseeker.h \
|
||||
backend/identitymanager.h \
|
||||
backend/nvcomputer.h \
|
||||
backend/nvhttp.h \
|
||||
|
@ -130,6 +133,7 @@ HEADERS += \
|
|||
backend/computermanager.h \
|
||||
backend/boxartmanager.h \
|
||||
cli/commandlineparser.h \
|
||||
cli/quitstream.h \
|
||||
cli/startstream.h \
|
||||
settings/streamingpreferences.h \
|
||||
streaming/input.h \
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
#include "computermanager.h"
|
||||
#include "nvhttp.h"
|
||||
#include "settings/streamingpreferences.h"
|
||||
#include "streaming/session.h"
|
||||
|
||||
#include <Limelight.h>
|
||||
#include <QtEndian>
|
||||
|
@ -478,6 +479,11 @@ void ComputerManager::quitRunningApp(NvComputer* computer)
|
|||
QThreadPool::globalInstance()->start(quit);
|
||||
}
|
||||
|
||||
void ComputerManager::quitRunningApp(Session *session)
|
||||
{
|
||||
quitRunningApp(session->getComputer());
|
||||
}
|
||||
|
||||
void ComputerManager::stopPollingAsync()
|
||||
{
|
||||
QWriteLocker lock(&m_Lock);
|
||||
|
|
|
@ -14,6 +14,8 @@
|
|||
#include <QSettings>
|
||||
#include <QRunnable>
|
||||
|
||||
class Session;
|
||||
|
||||
class MdnsPendingComputer : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
@ -72,6 +74,7 @@ public:
|
|||
void pairHost(NvComputer* computer, QString pin);
|
||||
|
||||
void quitRunningApp(NvComputer* computer);
|
||||
Q_INVOKABLE void quitRunningApp(Session* session);
|
||||
|
||||
QVector<NvComputer*> getComputers();
|
||||
|
||||
|
|
59
app/backend/computerseeker.cpp
Normal file
59
app/backend/computerseeker.cpp
Normal file
|
@ -0,0 +1,59 @@
|
|||
#include "computerseeker.h"
|
||||
#include "computermanager.h"
|
||||
#include <QTimer>
|
||||
|
||||
ComputerSeeker::ComputerSeeker(ComputerManager *manager, QString computerName, QObject *parent)
|
||||
: QObject(parent), m_ComputerManager(manager), m_ComputerName(computerName),
|
||||
m_TimeoutTimer(new QTimer(this))
|
||||
{
|
||||
m_TimeoutTimer->setSingleShot(true);
|
||||
connect(m_TimeoutTimer, &QTimer::timeout,
|
||||
this, &ComputerSeeker::onTimeout);
|
||||
connect(m_ComputerManager, &ComputerManager::computerStateChanged,
|
||||
this, &ComputerSeeker::onComputerUpdated);
|
||||
}
|
||||
|
||||
void ComputerSeeker::start(int timeout)
|
||||
{
|
||||
m_TimeoutTimer->start(timeout);
|
||||
// Seek desired computer by both connecting to it directly (this may fail
|
||||
// if m_ComputerName is UUID, or the name that doesn't resolve to an IP
|
||||
// address) and by polling it using mDNS, hopefully one of these methods
|
||||
// would find the host
|
||||
m_ComputerManager->addNewHost(m_ComputerName, false);
|
||||
m_ComputerManager->startPolling();
|
||||
}
|
||||
|
||||
void ComputerSeeker::onComputerUpdated(NvComputer *computer)
|
||||
{
|
||||
if (!m_TimeoutTimer->isActive()) {
|
||||
return;
|
||||
}
|
||||
if (matchComputer(computer) && isOnline(computer)) {
|
||||
m_ComputerManager->stopPollingAsync();
|
||||
m_TimeoutTimer->stop();
|
||||
emit computerFound(computer);
|
||||
}
|
||||
}
|
||||
|
||||
bool ComputerSeeker::matchComputer(NvComputer *computer) const
|
||||
{
|
||||
QString value = m_ComputerName.toLower();
|
||||
return computer->name.toLower() == value ||
|
||||
computer->localAddress.toLower() == value ||
|
||||
computer->remoteAddress.toLower() == value ||
|
||||
computer->manualAddress.toLower() == value ||
|
||||
computer->uuid.toLower() == value;
|
||||
}
|
||||
|
||||
bool ComputerSeeker::isOnline(NvComputer *computer) const
|
||||
{
|
||||
return computer->state == NvComputer::CS_ONLINE;
|
||||
}
|
||||
|
||||
void ComputerSeeker::onTimeout()
|
||||
{
|
||||
m_TimeoutTimer->stop();
|
||||
m_ComputerManager->stopPollingAsync();
|
||||
emit errorTimeout();
|
||||
}
|
33
app/backend/computerseeker.h
Normal file
33
app/backend/computerseeker.h
Normal file
|
@ -0,0 +1,33 @@
|
|||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
|
||||
class ComputerManager;
|
||||
class NvComputer;
|
||||
class QTimer;
|
||||
|
||||
class ComputerSeeker : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit ComputerSeeker(ComputerManager *manager, QString computerName, QObject *parent = nullptr);
|
||||
|
||||
void start(int timeout);
|
||||
|
||||
signals:
|
||||
void computerFound(NvComputer *computer);
|
||||
void errorTimeout();
|
||||
|
||||
private slots:
|
||||
void onComputerUpdated(NvComputer *computer);
|
||||
void onTimeout();
|
||||
|
||||
private:
|
||||
bool matchComputer(NvComputer *computer) const;
|
||||
bool isOnline(NvComputer *computer) const;
|
||||
|
||||
private:
|
||||
ComputerManager *m_ComputerManager;
|
||||
QString m_ComputerName;
|
||||
QTimer *m_TimeoutTimer;
|
||||
};
|
|
@ -57,8 +57,13 @@ NvHTTP::getCurrentGame(QString serverInfo)
|
|||
// GFE 2.8 started keeping currentgame set to the last game played. As a result, it no longer
|
||||
// has the semantics that its name would indicate. To contain the effects of this change as much
|
||||
// as possible, we'll force the current game to zero if the server isn't in a streaming session.
|
||||
//
|
||||
// However, current game info must be available also in other states than just _SERVER_BUSY as
|
||||
// it is required for quitting currently running app. Quitting app occurs at end of stream if
|
||||
// configured so. At that point the server state may be in some other state than _SERVER_BUSY
|
||||
// for a short while, but that must not prevent quitting of the app.
|
||||
QString serverState = getXmlString(serverInfo, "state");
|
||||
if (serverState != nullptr && serverState.endsWith("_SERVER_BUSY"))
|
||||
if (serverState != nullptr && !serverState.endsWith("_SERVER_AVAILABLE"))
|
||||
{
|
||||
return getXmlString(serverInfo, "currentgame").toInt();
|
||||
}
|
||||
|
|
|
@ -164,6 +164,7 @@ GlobalCommandLineParser::ParseResult GlobalCommandLineParser::parse(const QStrin
|
|||
"Starts Moonlight normally if no arguments are given.\n"
|
||||
"\n"
|
||||
"Available actions:\n"
|
||||
" quit Quit the currently running app\n"
|
||||
" stream Start streaming an app\n"
|
||||
"\n"
|
||||
"See 'moonlight <action> --help' for help of specific action."
|
||||
|
@ -179,6 +180,8 @@ GlobalCommandLineParser::ParseResult GlobalCommandLineParser::parse(const QStrin
|
|||
parser.handleHelpAndVersionOptions();
|
||||
parser.handleUnknownOptions();
|
||||
return NormalStartRequested;
|
||||
} else if (action == "quit") {
|
||||
return QuitRequested;
|
||||
} else if (action == "stream") {
|
||||
return StreamRequested;
|
||||
} else {
|
||||
|
@ -186,6 +189,48 @@ GlobalCommandLineParser::ParseResult GlobalCommandLineParser::parse(const QStrin
|
|||
}
|
||||
}
|
||||
|
||||
QuitCommandLineParser::QuitCommandLineParser()
|
||||
{
|
||||
}
|
||||
|
||||
QuitCommandLineParser::~QuitCommandLineParser()
|
||||
{
|
||||
}
|
||||
|
||||
void QuitCommandLineParser::parse(const QStringList &args)
|
||||
{
|
||||
CommandLineParser parser;
|
||||
parser.setupCommonOptions();
|
||||
parser.setApplicationDescription(
|
||||
"\n"
|
||||
"Quit the currently running app on the given host."
|
||||
);
|
||||
parser.addPositionalArgument("quit", "quit running app");
|
||||
parser.addPositionalArgument("host", "Host computer name, UUID, or IP address", "<host>");
|
||||
|
||||
if (!parser.parse(args)) {
|
||||
parser.showError(parser.errorText());
|
||||
}
|
||||
|
||||
parser.handleUnknownOptions();
|
||||
|
||||
// This method will not return and terminates the process if --version or
|
||||
// --help is specified
|
||||
parser.handleHelpAndVersionOptions();
|
||||
|
||||
// Verify that host has been provided
|
||||
auto posArgs = parser.positionalArguments();
|
||||
if (posArgs.length() < 2) {
|
||||
parser.showError("Host not provided");
|
||||
}
|
||||
m_Host = parser.positionalArguments().at(1);
|
||||
}
|
||||
|
||||
QString QuitCommandLineParser::getHost() const
|
||||
{
|
||||
return m_Host;
|
||||
}
|
||||
|
||||
StreamCommandLineParser::StreamCommandLineParser()
|
||||
{
|
||||
m_WindowModeMap = {
|
||||
|
@ -238,6 +283,7 @@ void StreamCommandLineParser::parse(const QStringList &args, StreamingPreference
|
|||
parser.addChoiceOption("display-mode", "display mode", m_WindowModeMap.keys());
|
||||
parser.addChoiceOption("audio-config", "audio config", m_AudioConfigMap.keys());
|
||||
parser.addToggleOption("multi-controller", "multiple controller support");
|
||||
parser.addToggleOption("quit-after", "quit app after session");
|
||||
parser.addToggleOption("mouse-acceleration", "mouse acceleration");
|
||||
parser.addToggleOption("game-optimization", "game optimizations");
|
||||
parser.addToggleOption("audio-on-host", "audio on host PC");
|
||||
|
@ -314,6 +360,9 @@ void StreamCommandLineParser::parse(const QStringList &args, StreamingPreference
|
|||
// Resolve --multi-controller and --no-multi-controller options
|
||||
preferences->multiController = parser.getToggleOptionValue("multi-controller", preferences->multiController);
|
||||
|
||||
// Resolve --quit-after and --no-quit-after options
|
||||
preferences->quitAppAfter = parser.getToggleOptionValue("quit-after", preferences->quitAppAfter);
|
||||
|
||||
// Resolve --mouse-acceleration and --no-mouse-acceleration options
|
||||
preferences->mouseAcceleration = parser.getToggleOptionValue("mouse-acceleration", preferences->mouseAcceleration);
|
||||
|
||||
|
@ -359,4 +408,3 @@ QString StreamCommandLineParser::getAppName() const
|
|||
{
|
||||
return m_AppName;
|
||||
}
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ public:
|
|||
enum ParseResult {
|
||||
NormalStartRequested,
|
||||
StreamRequested,
|
||||
QuitRequested,
|
||||
};
|
||||
|
||||
GlobalCommandLineParser();
|
||||
|
@ -20,6 +21,20 @@ public:
|
|||
|
||||
};
|
||||
|
||||
class QuitCommandLineParser
|
||||
{
|
||||
public:
|
||||
QuitCommandLineParser();
|
||||
virtual ~QuitCommandLineParser();
|
||||
|
||||
void parse(const QStringList &args);
|
||||
|
||||
QString getHost() const;
|
||||
|
||||
private:
|
||||
QString m_Host;
|
||||
};
|
||||
|
||||
class StreamCommandLineParser
|
||||
{
|
||||
public:
|
||||
|
|
171
app/cli/quitstream.cpp
Normal file
171
app/cli/quitstream.cpp
Normal file
|
@ -0,0 +1,171 @@
|
|||
#include "quitstream.h"
|
||||
|
||||
#include "backend/computermanager.h"
|
||||
#include "backend/computerseeker.h"
|
||||
#include "streaming/session.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QTimer>
|
||||
|
||||
#define COMPUTER_SEEK_TIMEOUT 10000
|
||||
|
||||
namespace CliQuitStream
|
||||
{
|
||||
|
||||
enum State {
|
||||
StateInit,
|
||||
StateSeekComputer,
|
||||
StateQuitApp,
|
||||
StateFailure,
|
||||
};
|
||||
|
||||
class Event
|
||||
{
|
||||
public:
|
||||
enum Type {
|
||||
AppQuitCompleted,
|
||||
ComputerFound,
|
||||
ComputerSeekTimedout,
|
||||
Executed,
|
||||
};
|
||||
|
||||
Event(Type type)
|
||||
: type(type), computerManager(nullptr), computer(nullptr) {}
|
||||
|
||||
Type type;
|
||||
ComputerManager *computerManager;
|
||||
NvComputer *computer;
|
||||
QString errorMessage;
|
||||
};
|
||||
|
||||
class LauncherPrivate
|
||||
{
|
||||
Q_DECLARE_PUBLIC(Launcher)
|
||||
|
||||
public:
|
||||
LauncherPrivate(Launcher *q) : q_ptr(q) {}
|
||||
|
||||
void handleEvent(Event event)
|
||||
{
|
||||
Q_Q(Launcher);
|
||||
NvApp app;
|
||||
|
||||
switch (event.type) {
|
||||
// Occurs when CliQuitStreamSegue becomes visible and the UI calls launcher's execute()
|
||||
case Event::Executed:
|
||||
if (m_State == StateInit) {
|
||||
m_State = StateSeekComputer;
|
||||
m_ComputerManager = event.computerManager;
|
||||
q->connect(m_ComputerManager, &ComputerManager::quitAppCompleted,
|
||||
q, &Launcher::onQuitAppCompleted);
|
||||
|
||||
m_ComputerSeeker = new ComputerSeeker(m_ComputerManager, m_ComputerName, q);
|
||||
q->connect(m_ComputerSeeker, &ComputerSeeker::computerFound,
|
||||
q, &Launcher::onComputerFound);
|
||||
q->connect(m_ComputerSeeker, &ComputerSeeker::errorTimeout,
|
||||
q, &Launcher::onComputerSeekTimeout);
|
||||
m_ComputerSeeker->start(COMPUTER_SEEK_TIMEOUT);
|
||||
|
||||
emit q->searchingComputer();
|
||||
}
|
||||
break;
|
||||
// Occurs when computer search timed out
|
||||
case Event::ComputerSeekTimedout:
|
||||
if (m_State == StateSeekComputer) {
|
||||
m_State = StateFailure;
|
||||
emit q->failed(QString("Failed to connect to %1").arg(m_ComputerName));
|
||||
}
|
||||
break;
|
||||
// Occurs when searched computer is found
|
||||
case Event::ComputerFound:
|
||||
if (m_State == StateSeekComputer) {
|
||||
if (event.computer->pairState == NvComputer::PS_PAIRED) {
|
||||
m_State = StateQuitApp;
|
||||
emit q->quittingApp();
|
||||
m_ComputerManager->quitRunningApp(event.computer);
|
||||
} else {
|
||||
m_State = StateFailure;
|
||||
QString msg = QString("Computer %1 has not been paired. "
|
||||
"Please open Moonlight to pair before streaming.")
|
||||
.arg(event.computer->name);
|
||||
emit q->failed(msg);
|
||||
}
|
||||
}
|
||||
break;
|
||||
// Occurs when app quit completed (error message is set if failed)
|
||||
case Event::AppQuitCompleted:
|
||||
if (m_State == StateQuitApp) {
|
||||
if (event.errorMessage.isEmpty()) {
|
||||
QCoreApplication::exit(0);
|
||||
} else {
|
||||
m_State = StateFailure;
|
||||
emit q->failed(QString("Quitting app failed, reason: %1").arg(event.errorMessage));
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Launcher *q_ptr;
|
||||
ComputerManager *m_ComputerManager;
|
||||
QString m_ComputerName;
|
||||
ComputerSeeker *m_ComputerSeeker;
|
||||
State m_State;
|
||||
QTimer *m_TimeoutTimer;
|
||||
};
|
||||
|
||||
Launcher::Launcher(QString computer, QObject *parent)
|
||||
: QObject(parent),
|
||||
m_DPtr(new LauncherPrivate(this))
|
||||
{
|
||||
Q_D(Launcher);
|
||||
d->m_ComputerName = computer;
|
||||
d->m_State = StateInit;
|
||||
d->m_TimeoutTimer = new QTimer(this);
|
||||
d->m_TimeoutTimer->setSingleShot(true);
|
||||
connect(d->m_TimeoutTimer, &QTimer::timeout,
|
||||
this, &Launcher::onComputerSeekTimeout);
|
||||
}
|
||||
|
||||
Launcher::~Launcher()
|
||||
{
|
||||
}
|
||||
|
||||
void Launcher::execute(ComputerManager *manager)
|
||||
{
|
||||
Q_D(Launcher);
|
||||
Event event(Event::Executed);
|
||||
event.computerManager = manager;
|
||||
d->handleEvent(event);
|
||||
}
|
||||
|
||||
bool Launcher::isExecuted() const
|
||||
{
|
||||
Q_D(const Launcher);
|
||||
return d->m_State != StateInit;
|
||||
}
|
||||
|
||||
void Launcher::onComputerFound(NvComputer *computer)
|
||||
{
|
||||
Q_D(Launcher);
|
||||
Event event(Event::ComputerFound);
|
||||
event.computer = computer;
|
||||
d->handleEvent(event);
|
||||
}
|
||||
|
||||
void Launcher::onComputerSeekTimeout()
|
||||
{
|
||||
Q_D(Launcher);
|
||||
Event event(Event::ComputerSeekTimedout);
|
||||
d->handleEvent(event);
|
||||
}
|
||||
|
||||
void Launcher::onQuitAppCompleted(QVariant error)
|
||||
{
|
||||
Q_D(Launcher);
|
||||
Event event(Event::AppQuitCompleted);
|
||||
event.errorMessage = error.toString();
|
||||
d->handleEvent(event);
|
||||
}
|
||||
|
||||
}
|
40
app/cli/quitstream.h
Normal file
40
app/cli/quitstream.h
Normal file
|
@ -0,0 +1,40 @@
|
|||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QVariant>
|
||||
|
||||
class ComputerManager;
|
||||
class NvComputer;
|
||||
|
||||
namespace CliQuitStream
|
||||
{
|
||||
|
||||
class LauncherPrivate;
|
||||
|
||||
class Launcher : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_DECLARE_PRIVATE_D(m_DPtr, Launcher)
|
||||
|
||||
public:
|
||||
explicit Launcher(QString computer, QObject *parent = nullptr);
|
||||
~Launcher();
|
||||
|
||||
Q_INVOKABLE void execute(ComputerManager *manager);
|
||||
Q_INVOKABLE bool isExecuted() const;
|
||||
|
||||
signals:
|
||||
void searchingComputer();
|
||||
void quittingApp();
|
||||
void failed(QString text);
|
||||
|
||||
private slots:
|
||||
void onComputerFound(NvComputer *computer);
|
||||
void onComputerSeekTimeout();
|
||||
void onQuitAppCompleted(QVariant error);
|
||||
|
||||
private:
|
||||
QScopedPointer<LauncherPrivate> m_DPtr;
|
||||
};
|
||||
|
||||
}
|
|
@ -1,7 +1,9 @@
|
|||
#include "startstream.h"
|
||||
#include "backend/computermanager.h"
|
||||
#include "backend/computerseeker.h"
|
||||
#include "streaming/session.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QTimer>
|
||||
|
||||
#define COMPUTER_SEEK_TIMEOUT 10000
|
||||
|
@ -22,6 +24,9 @@ class Event
|
|||
{
|
||||
public:
|
||||
enum Type {
|
||||
AppQuitCompleted,
|
||||
AppQuitRequested,
|
||||
ComputerFound,
|
||||
ComputerUpdated,
|
||||
Executed,
|
||||
Timedout,
|
||||
|
@ -33,6 +38,7 @@ public:
|
|||
Type type;
|
||||
ComputerManager *computerManager;
|
||||
NvComputer *computer;
|
||||
QString errorMessage;
|
||||
};
|
||||
|
||||
class LauncherPrivate
|
||||
|
@ -49,58 +55,78 @@ public:
|
|||
NvApp app;
|
||||
|
||||
switch (event.type) {
|
||||
// Occurs when CliStartStreamSegue becomes visible and the UI calls launcher's execute()
|
||||
case Event::Executed:
|
||||
if (m_State == StateInit) {
|
||||
m_State = StateSeekComputer;
|
||||
m_TimeoutTimer->start(COMPUTER_SEEK_TIMEOUT);
|
||||
m_ComputerManager = event.computerManager;
|
||||
|
||||
m_ComputerSeeker = new ComputerSeeker(m_ComputerManager, m_ComputerName, q);
|
||||
q->connect(m_ComputerSeeker, &ComputerSeeker::computerFound,
|
||||
q, &Launcher::onComputerFound);
|
||||
q->connect(m_ComputerSeeker, &ComputerSeeker::errorTimeout,
|
||||
q, &Launcher::onTimeout);
|
||||
m_ComputerSeeker->start(COMPUTER_SEEK_TIMEOUT);
|
||||
|
||||
q->connect(m_ComputerManager, &ComputerManager::computerStateChanged,
|
||||
q, &Launcher::onComputerUpdated);
|
||||
// Seek desired computer by both connecting to it directly (this may fail
|
||||
// if m_ComputerName is UUID, or the name that doesn't resolve to an IP
|
||||
// address) and by polling it using mDNS, hopefully one of these methods
|
||||
// would find the host
|
||||
m_ComputerManager->addNewHost(m_ComputerName, false);
|
||||
m_ComputerManager->startPolling();
|
||||
q->connect(m_ComputerManager, &ComputerManager::quitAppCompleted,
|
||||
q, &Launcher::onQuitAppCompleted);
|
||||
|
||||
emit q->searchingComputer();
|
||||
}
|
||||
break;
|
||||
case Event::ComputerUpdated:
|
||||
// Occurs when searched computer is found
|
||||
case Event::ComputerFound:
|
||||
if (m_State == StateSeekComputer) {
|
||||
if (matchComputer(event.computer) && isOnline(event.computer)) {
|
||||
if (isPaired(event.computer)) {
|
||||
m_State = StateSeekApp;
|
||||
m_TimeoutTimer->start(APP_SEEK_TIMEOUT);
|
||||
m_Computer = event.computer;
|
||||
m_ComputerManager->stopPollingAsync();
|
||||
emit q->searchingApp();
|
||||
} else {
|
||||
m_State = StateFailure;
|
||||
QString msg = QString("Computer %1 has not been paired. "
|
||||
"Please open Moonlight to pair before streaming.")
|
||||
.arg(event.computer->name);
|
||||
emit q->failed(msg);
|
||||
}
|
||||
if (event.computer->pairState == NvComputer::PS_PAIRED) {
|
||||
m_State = StateSeekApp;
|
||||
m_Computer = event.computer;
|
||||
m_TimeoutTimer->start(APP_SEEK_TIMEOUT);
|
||||
emit q->searchingApp();
|
||||
} else {
|
||||
m_State = StateFailure;
|
||||
QString msg = QString("Computer %1 has not been paired. "
|
||||
"Please open Moonlight to pair before streaming.")
|
||||
.arg(event.computer->name);
|
||||
emit q->failed(msg);
|
||||
}
|
||||
}
|
||||
break;
|
||||
// Occurs when a computer is updated
|
||||
case Event::ComputerUpdated:
|
||||
if (m_State == StateSeekApp) {
|
||||
int index = getAppIndex();
|
||||
if (-1 != index) {
|
||||
app = m_Computer->appList[index];
|
||||
m_TimeoutTimer->stop();
|
||||
if (isNotStreaming() || isStreamingApp(app)) {
|
||||
m_State = StateStartSession;
|
||||
m_TimeoutTimer->stop();
|
||||
session = new Session(m_Computer, app, m_Preferences);
|
||||
emit q->sessionCreated(app.name, session);
|
||||
} else {
|
||||
m_State = StateFailure;
|
||||
QString msg = QString("%1 is already running. Please quit %1 to stream %2.")
|
||||
.arg(getCurrentAppName(), app.name);
|
||||
emit q->failed(msg);
|
||||
emit q->appQuitRequired(getCurrentAppName());
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
// Occurs when there was another app running on computer and user accepted quit
|
||||
// confirmation dialog
|
||||
case Event::AppQuitRequested:
|
||||
if (m_State == StateSeekApp) {
|
||||
m_ComputerManager->quitRunningApp(m_Computer);
|
||||
}
|
||||
break;
|
||||
// Occurs when the previous app quit has been completed, handles quitting errors if any
|
||||
// happended. ComputerUpdated event's handler handles session start when previous app has
|
||||
// quit.
|
||||
case Event::AppQuitCompleted:
|
||||
if (m_State == StateSeekApp && !event.errorMessage.isEmpty()) {
|
||||
m_State = StateFailure;
|
||||
emit q->failed(QString("Quitting app failed, reason: %1").arg(event.errorMessage));
|
||||
}
|
||||
break;
|
||||
// Occurs when computer or app search timed out
|
||||
case Event::Timedout:
|
||||
if (m_State == StateSeekComputer) {
|
||||
m_State = StateFailure;
|
||||
|
@ -114,26 +140,6 @@ public:
|
|||
}
|
||||
}
|
||||
|
||||
bool matchComputer(NvComputer *computer) const
|
||||
{
|
||||
QString value = m_ComputerName.toLower();
|
||||
return computer->name.toLower() == value ||
|
||||
computer->localAddress.toLower() == value ||
|
||||
computer->remoteAddress.toLower() == value ||
|
||||
computer->manualAddress.toLower() == value ||
|
||||
computer->uuid.toLower() == value;
|
||||
}
|
||||
|
||||
bool isOnline(NvComputer *computer) const
|
||||
{
|
||||
return computer->state == NvComputer::CS_ONLINE;
|
||||
}
|
||||
|
||||
bool isPaired(NvComputer *computer) const
|
||||
{
|
||||
return computer->pairState == NvComputer::PS_PAIRED;
|
||||
}
|
||||
|
||||
int getAppIndex() const
|
||||
{
|
||||
for (int i = 0; i < m_Computer->appList.length(); i++) {
|
||||
|
@ -169,6 +175,7 @@ public:
|
|||
QString m_AppName;
|
||||
StreamingPreferences *m_Preferences;
|
||||
ComputerManager *m_ComputerManager;
|
||||
ComputerSeeker *m_ComputerSeeker;
|
||||
NvComputer *m_Computer;
|
||||
State m_State;
|
||||
QTimer *m_TimeoutTimer;
|
||||
|
@ -202,6 +209,27 @@ void Launcher::execute(ComputerManager *manager)
|
|||
d->handleEvent(event);
|
||||
}
|
||||
|
||||
void Launcher::quitRunningApp()
|
||||
{
|
||||
Q_D(Launcher);
|
||||
Event event(Event::AppQuitRequested);
|
||||
d->handleEvent(event);
|
||||
}
|
||||
|
||||
bool Launcher::isExecuted() const
|
||||
{
|
||||
Q_D(const Launcher);
|
||||
return d->m_State != StateInit;
|
||||
}
|
||||
|
||||
void Launcher::onComputerFound(NvComputer *computer)
|
||||
{
|
||||
Q_D(Launcher);
|
||||
Event event(Event::ComputerFound);
|
||||
event.computer = computer;
|
||||
d->handleEvent(event);
|
||||
}
|
||||
|
||||
void Launcher::onComputerUpdated(NvComputer *computer)
|
||||
{
|
||||
Q_D(Launcher);
|
||||
|
@ -217,4 +245,12 @@ void Launcher::onTimeout()
|
|||
d->handleEvent(event);
|
||||
}
|
||||
|
||||
void Launcher::onQuitAppCompleted(QVariant error)
|
||||
{
|
||||
Q_D(Launcher);
|
||||
Event event(Event::AppQuitCompleted);
|
||||
event.errorMessage = error.toString();
|
||||
d->handleEvent(event);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QVariant>
|
||||
|
||||
class ComputerManager;
|
||||
class NvComputer;
|
||||
|
@ -24,16 +25,21 @@ public:
|
|||
QObject *parent = nullptr);
|
||||
~Launcher();
|
||||
Q_INVOKABLE void execute(ComputerManager *manager);
|
||||
Q_INVOKABLE void quitRunningApp();
|
||||
Q_INVOKABLE bool isExecuted() const;
|
||||
|
||||
signals:
|
||||
void searchingComputer();
|
||||
void searchingApp();
|
||||
void sessionCreated(QString appName, Session *session);
|
||||
void failed(QString text);
|
||||
void appQuitRequired(QString appName);
|
||||
|
||||
private slots:
|
||||
void onComputerFound(NvComputer *computer);
|
||||
void onComputerUpdated(NvComputer *computer);
|
||||
void onTimeout();
|
||||
void onQuitAppCompleted(QVariant error);
|
||||
|
||||
private:
|
||||
QScopedPointer<LauncherPrivate> m_DPtr;
|
||||
|
|
70
app/gui/CliQuitStreamSegue.qml
Normal file
70
app/gui/CliQuitStreamSegue.qml
Normal file
|
@ -0,0 +1,70 @@
|
|||
import QtQuick 2.0
|
||||
import QtQuick.Controls 2.2
|
||||
import QtQuick.Dialogs 1.2
|
||||
|
||||
import ComputerManager 1.0
|
||||
import Session 1.0
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
|
||||
function onSearchingComputer() {
|
||||
stageLabel.text = "Establishing connection to PC..."
|
||||
}
|
||||
|
||||
function onQuittingApp() {
|
||||
stageLabel.text = "Quitting app..."
|
||||
}
|
||||
|
||||
function onFailure(message) {
|
||||
errorDialog.text = message
|
||||
errorDialog.open()
|
||||
}
|
||||
|
||||
// The StackView will trigger a visibility change when
|
||||
// we're pushed onto it, causing our onVisibleChanged
|
||||
// routine to run, but only if we start as invisible
|
||||
visible: false
|
||||
|
||||
onVisibleChanged: {
|
||||
if (visible && !launcher.isExecuted()) {
|
||||
toolBar.visible = false
|
||||
launcher.searchingComputer.connect(onSearchingComputer)
|
||||
launcher.quittingApp.connect(onQuittingApp)
|
||||
launcher.failed.connect(onFailure)
|
||||
launcher.execute(ComputerManager)
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: 5
|
||||
|
||||
BusyIndicator {
|
||||
id: stageSpinner
|
||||
}
|
||||
|
||||
Label {
|
||||
id: stageLabel
|
||||
height: stageSpinner.height
|
||||
text: stageText
|
||||
font.pointSize: 20
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
}
|
||||
|
||||
MessageDialog {
|
||||
id: errorDialog
|
||||
modality:Qt.WindowModal
|
||||
icon: StandardIcon.Critical
|
||||
standardButtons: StandardButton.Ok
|
||||
|
||||
onVisibleChanged: {
|
||||
if (!visible) {
|
||||
Qt.quit()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
import QtQml 2.2
|
||||
import QtQuick 2.0
|
||||
import QtQuick.Controls 2.2
|
||||
import QtQuick.Dialogs 1.2
|
||||
|
@ -30,13 +31,19 @@ Item {
|
|||
errorDialog.open()
|
||||
}
|
||||
|
||||
function onAppQuitRequired(appName) {
|
||||
quitAppDialog.appName = appName
|
||||
quitAppDialog.open()
|
||||
}
|
||||
|
||||
onVisibleChanged: {
|
||||
if (visible) {
|
||||
if (visible && !launcher.isExecuted()) {
|
||||
toolBar.visible = false
|
||||
launcher.searchingComputer.connect(onSearchingComputer)
|
||||
launcher.searchingApp.connect(onSearchingApp)
|
||||
launcher.sessionCreated.connect(onSessionCreated)
|
||||
launcher.failed.connect(onLaunchFailed)
|
||||
launcher.appQuitRequired.connect(onAppQuitRequired)
|
||||
launcher.execute(ComputerManager)
|
||||
}
|
||||
}
|
||||
|
@ -74,4 +81,44 @@ Item {
|
|||
}
|
||||
}
|
||||
|
||||
MessageDialog {
|
||||
id: quitAppDialog
|
||||
modality:Qt.WindowModal
|
||||
text:"Are you sure you want to quit " + appName +"? Any unsaved progress will be lost."
|
||||
standardButtons: StandardButton.Yes | StandardButton.No
|
||||
property string appName : ""
|
||||
|
||||
function quitApp() {
|
||||
var component = Qt.createComponent("QuitSegue.qml")
|
||||
var params = {"appName": appName}
|
||||
stackView.push(component.createObject(stackView, params))
|
||||
// Trigger the quit after pushing the quit segue on screen
|
||||
launcher.quitRunningApp()
|
||||
}
|
||||
|
||||
onYes: quitApp()
|
||||
|
||||
// For keyboard/gamepad navigation
|
||||
onAccepted: quitApp()
|
||||
|
||||
// Exit process if app quit is rejected (reacts also to closing of the
|
||||
// dialog from title bar's close button).
|
||||
// Note: this depends on undocumented behavior of visibleChanged()
|
||||
// signal being emitted before yes() or accepted() has been emitted.
|
||||
onVisibleChanged: {
|
||||
if (!visible) {
|
||||
quitTimer.start()
|
||||
}
|
||||
}
|
||||
Component.onCompleted: {
|
||||
yes.connect(quitTimer.stop)
|
||||
accepted.connect(quitTimer.stop)
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: quitTimer
|
||||
interval: 100
|
||||
onTriggered: Qt.quit()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -701,6 +701,16 @@ Flickable {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
CheckBox {
|
||||
id: quitAppAfter
|
||||
text: "Quit app after quitting session"
|
||||
font.pointSize: 12
|
||||
checked: prefs.quitAppAfter
|
||||
onCheckedChanged: {
|
||||
prefs.quitAppAfter = checked
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ import QtQuick.Controls 2.2
|
|||
import QtQuick.Dialogs 1.2
|
||||
import QtQuick.Window 2.2
|
||||
|
||||
import ComputerManager 1.0
|
||||
import SdlGamepadKeyNavigation 1.0
|
||||
import Session 1.0
|
||||
|
||||
|
@ -11,6 +12,7 @@ Item {
|
|||
property string appName
|
||||
property string stageText : "Starting " + appName + "..."
|
||||
property bool quitAfter : false
|
||||
property bool sessionLaunched : false
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
|
@ -59,6 +61,27 @@ Item {
|
|||
toast.visible = true
|
||||
}
|
||||
|
||||
function streamingFinished() {
|
||||
if (quitAfter) {
|
||||
window.visible = false
|
||||
Qt.quit()
|
||||
} else {
|
||||
// Show the Qt window again after streaming
|
||||
window.visible = true
|
||||
|
||||
// Exit this view
|
||||
stackView.pop()
|
||||
|
||||
// Display any launch errors. We do this after
|
||||
// the Qt UI is visible again to prevent losing
|
||||
// focus on the dialog which would impact gamepad
|
||||
// users.
|
||||
if (errorDialog.text) {
|
||||
errorDialog.open()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// It's important that we don't call enable() here
|
||||
// or it may interfere with the Session instance
|
||||
// getting notified of initial connected gamepads.
|
||||
|
@ -68,6 +91,12 @@ Item {
|
|||
|
||||
onVisibleChanged: {
|
||||
if (visible) {
|
||||
// Prevent session restart after execution returns from QuitSegue
|
||||
if (sessionLaunched) {
|
||||
return
|
||||
}
|
||||
sessionLaunched = true
|
||||
|
||||
// Hide the toolbar before we start loading
|
||||
toolBar.visible = false
|
||||
|
||||
|
@ -87,27 +116,21 @@ Item {
|
|||
session.displayLaunchWarning.connect(displayLaunchWarning)
|
||||
|
||||
// Run the streaming session to completion
|
||||
session.exec(Screen.virtualX, Screen.virtualY)
|
||||
session.exec(Screen.virtualX, Screen.virtualY);
|
||||
|
||||
if (quitAfter) {
|
||||
Qt.quit()
|
||||
} else {
|
||||
// Show the Qt window again after streaming
|
||||
if (!errorDialog.text && session.shouldQuitAppAfter()) {
|
||||
// Show the Qt window again to show quit segue
|
||||
window.visible = true
|
||||
|
||||
// Exit this view
|
||||
stackView.pop()
|
||||
|
||||
// Display any launch errors. We do this after
|
||||
// the Qt UI is visible again to prevent losing
|
||||
// focus on the dialog which would impact gamepad
|
||||
// users.
|
||||
if (errorDialog.text) {
|
||||
errorDialog.open()
|
||||
}
|
||||
var component = Qt.createComponent("QuitSegue.qml")
|
||||
stackView.push(component.createObject(stackView, {"appName": appName}))
|
||||
// Quit app
|
||||
ComputerManager.quitAppCompleted.connect(streamingFinished)
|
||||
ComputerManager.quitRunningApp(session)
|
||||
} else {
|
||||
streamingFinished()
|
||||
}
|
||||
}
|
||||
else {
|
||||
else if (!quitAfter) {
|
||||
// Show the toolbar again when we become hidden
|
||||
toolBar.visible = true
|
||||
}
|
||||
|
|
14
app/main.cpp
14
app/main.cpp
|
@ -21,6 +21,7 @@
|
|||
#include "antihookingprotection.h"
|
||||
#endif
|
||||
|
||||
#include "cli/quitstream.h"
|
||||
#include "cli/startstream.h"
|
||||
#include "cli/commandlineparser.h"
|
||||
#include "path.h"
|
||||
|
@ -342,6 +343,15 @@ int main(int argc, char *argv[])
|
|||
engine.rootContext()->setContextProperty("launcher", launcher);
|
||||
break;
|
||||
}
|
||||
case GlobalCommandLineParser::QuitRequested:
|
||||
{
|
||||
initialView = "CliQuitStreamSegue.qml";
|
||||
QuitCommandLineParser quitParser;
|
||||
quitParser.parse(app.arguments());
|
||||
auto launcher = new CliQuitStream::Launcher(quitParser.getHost(), &app);
|
||||
engine.rootContext()->setContextProperty("launcher", launcher);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
engine.rootContext()->setContextProperty("initialView", initialView);
|
||||
|
@ -369,5 +379,9 @@ int main(int argc, char *argv[])
|
|||
|
||||
int err = app.exec();
|
||||
|
||||
// Give worker tasks time to properly exit. Fixes PendingQuitTask
|
||||
// sometimes freezing and blocking process exit.
|
||||
QThreadPool::globalInstance()->waitForDone(30000);
|
||||
|
||||
return err;
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
<file>gui/NavigableToolButton.qml</file>
|
||||
<file>gui/NavigableItemDelegate.qml</file>
|
||||
<file>gui/NavigableMenuItem.qml</file>
|
||||
<file>gui/CliQuitStreamSegue.qml</file>
|
||||
<file>gui/CliStartStreamSegue.qml</file>
|
||||
<file>gui/AutoResizingComboBox.qml</file>
|
||||
</qresource>
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
#define SER_WINDOWMODE "windowmode"
|
||||
#define SER_UNSUPPORTEDFPS "unsupportedfps"
|
||||
#define SER_MDNS "mdns"
|
||||
#define SER_QUITAPPAFTER "quitAppAfter"
|
||||
#define SER_MOUSEACCELERATION "mouseacceleration"
|
||||
#define SER_STARTWINDOWED "startwindowed"
|
||||
|
||||
|
@ -43,6 +44,7 @@ void StreamingPreferences::reload()
|
|||
multiController = settings.value(SER_MULTICONT, true).toBool();
|
||||
unsupportedFps = settings.value(SER_UNSUPPORTEDFPS, false).toBool();
|
||||
enableMdns = settings.value(SER_MDNS, true).toBool();
|
||||
quitAppAfter = settings.value(SER_QUITAPPAFTER, false).toBool();
|
||||
mouseAcceleration = settings.value(SER_MOUSEACCELERATION, false).toBool();
|
||||
startWindowed = settings.value(SER_STARTWINDOWED, false).toBool();
|
||||
audioConfig = static_cast<AudioConfig>(settings.value(SER_AUDIOCFG,
|
||||
|
@ -71,6 +73,7 @@ void StreamingPreferences::save()
|
|||
settings.setValue(SER_MULTICONT, multiController);
|
||||
settings.setValue(SER_UNSUPPORTEDFPS, unsupportedFps);
|
||||
settings.setValue(SER_MDNS, enableMdns);
|
||||
settings.setValue(SER_QUITAPPAFTER, quitAppAfter);
|
||||
settings.setValue(SER_MOUSEACCELERATION, mouseAcceleration);
|
||||
settings.setValue(SER_STARTWINDOWED, startWindowed);
|
||||
settings.setValue(SER_AUDIOCFG, static_cast<int>(audioConfig));
|
||||
|
|
|
@ -73,6 +73,7 @@ public:
|
|||
Q_PROPERTY(bool multiController MEMBER multiController NOTIFY multiControllerChanged)
|
||||
Q_PROPERTY(bool unsupportedFps MEMBER unsupportedFps NOTIFY unsupportedFpsChanged)
|
||||
Q_PROPERTY(bool enableMdns MEMBER enableMdns NOTIFY enableMdnsChanged)
|
||||
Q_PROPERTY(bool quitAppAfter MEMBER quitAppAfter NOTIFY quitAppAfterChanged)
|
||||
Q_PROPERTY(bool mouseAcceleration MEMBER mouseAcceleration NOTIFY mouseAccelerationChanged)
|
||||
Q_PROPERTY(bool startWindowed MEMBER startWindowed NOTIFY startWindowedChanged)
|
||||
Q_PROPERTY(AudioConfig audioConfig MEMBER audioConfig NOTIFY audioConfigChanged)
|
||||
|
@ -91,6 +92,7 @@ public:
|
|||
bool multiController;
|
||||
bool unsupportedFps;
|
||||
bool enableMdns;
|
||||
bool quitAppAfter;
|
||||
bool mouseAcceleration;
|
||||
bool startWindowed;
|
||||
AudioConfig audioConfig;
|
||||
|
@ -107,6 +109,7 @@ signals:
|
|||
void multiControllerChanged();
|
||||
void unsupportedFpsChanged();
|
||||
void enableMdnsChanged();
|
||||
void quitAppAfterChanged();
|
||||
void mouseAccelerationChanged();
|
||||
void audioConfigChanged();
|
||||
void videoCodecConfigChanged();
|
||||
|
|
|
@ -275,6 +275,16 @@ int Session::getDecoderCapabilities(StreamingPreferences::VideoDecoderSelection
|
|||
return caps;
|
||||
}
|
||||
|
||||
NvComputer *Session::getComputer() const
|
||||
{
|
||||
return m_Computer;
|
||||
}
|
||||
|
||||
bool Session::shouldQuitAppAfter() const
|
||||
{
|
||||
return m_Preferences->quitAppAfter;
|
||||
}
|
||||
|
||||
Session::Session(NvComputer* computer, NvApp& app, StreamingPreferences *preferences)
|
||||
: m_Preferences(preferences ? preferences : new StreamingPreferences(this)),
|
||||
m_Computer(computer),
|
||||
|
|
|
@ -30,6 +30,10 @@ public:
|
|||
int getDecoderCapabilities(StreamingPreferences::VideoDecoderSelection vds,
|
||||
int videoFormat, int width, int height, int frameRate);
|
||||
|
||||
NvComputer* getComputer() const;
|
||||
|
||||
Q_INVOKABLE bool shouldQuitAppAfter() const;
|
||||
|
||||
signals:
|
||||
void stageStarting(QString stage);
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue