mirror of
https://github.com/moonlight-stream/moonlight-qt
synced 2024-12-04 16:59:10 +00:00
* Add command line parameters. Fixes #30 * Fixed compile errors * Fixed code review findings * Fixed code review findings, take 2
This commit is contained in:
parent
c99b4f1559
commit
d14cfb577b
14 changed files with 818 additions and 37 deletions
|
@ -97,6 +97,8 @@ SOURCES += \
|
|||
backend/nvpairingmanager.cpp \
|
||||
backend/computermanager.cpp \
|
||||
backend/boxartmanager.cpp \
|
||||
cli/commandlineparser.cpp \
|
||||
cli/startstream.cpp \
|
||||
settings/streamingpreferences.cpp \
|
||||
streaming/input.cpp \
|
||||
streaming/session.cpp \
|
||||
|
@ -115,6 +117,8 @@ HEADERS += \
|
|||
backend/nvpairingmanager.h \
|
||||
backend/computermanager.h \
|
||||
backend/boxartmanager.h \
|
||||
cli/commandlineparser.h \
|
||||
cli/startstream.h \
|
||||
settings/streamingpreferences.h \
|
||||
streaming/input.hpp \
|
||||
streaming/session.hpp \
|
||||
|
|
363
app/cli/commandlineparser.cpp
Normal file
363
app/cli/commandlineparser.cpp
Normal file
|
@ -0,0 +1,363 @@
|
|||
#include "commandlineparser.h"
|
||||
|
||||
#include <QCommandLineParser>
|
||||
#include <QRegularExpression>
|
||||
|
||||
#if defined(Q_OS_WIN)
|
||||
#include <qt_windows.h>
|
||||
#endif
|
||||
|
||||
static bool inRange(int value, int min, int max)
|
||||
{
|
||||
return value >= min && value <= max;
|
||||
}
|
||||
|
||||
// This method returns key's value from QMap where the key is a QString.
|
||||
// Key matching is case insensitive.
|
||||
template <typename T>
|
||||
static T mapValue(QMap<QString, T> map, QString key)
|
||||
{
|
||||
for(auto& item : map.toStdMap()) {
|
||||
if (QString::compare(item.first, key, Qt::CaseInsensitive) == 0) {
|
||||
return item.second;
|
||||
}
|
||||
}
|
||||
return T();
|
||||
}
|
||||
|
||||
class CommandLineParser : public QCommandLineParser
|
||||
{
|
||||
public:
|
||||
enum MessageType {
|
||||
Info,
|
||||
Error
|
||||
};
|
||||
|
||||
void setupCommonOptions()
|
||||
{
|
||||
setSingleDashWordOptionMode(QCommandLineParser::ParseAsLongOptions);
|
||||
addHelpOption();
|
||||
addVersionOption();
|
||||
}
|
||||
|
||||
void handleHelpAndVersionOptions()
|
||||
{
|
||||
if (isSet("help")) {
|
||||
showInfo(helpText());
|
||||
}
|
||||
if (isSet("version")) {
|
||||
showVersion();
|
||||
}
|
||||
}
|
||||
|
||||
void handleUnknownOptions()
|
||||
{
|
||||
if (unknownOptionNames().length()) {
|
||||
showError(QString("Unknown options: %1").arg(unknownOptionNames().join(", ")));
|
||||
}
|
||||
}
|
||||
|
||||
void showMessage(QString message, MessageType type) const
|
||||
{
|
||||
#if defined(Q_OS_WIN)
|
||||
UINT flags = MB_OK | MB_TOPMOST | MB_SETFOREGROUND;
|
||||
flags |= (type == Info ? MB_ICONINFORMATION : MB_ICONERROR);
|
||||
QString title = "Moonlight";
|
||||
MessageBoxW(nullptr, reinterpret_cast<const wchar_t *>(message.utf16()),
|
||||
reinterpret_cast<const wchar_t *>(title.utf16()), flags);
|
||||
#endif
|
||||
message = message.endsWith('\n') ? message : message + '\n';
|
||||
fputs(qPrintable(message), type == Info ? stdout : stderr);
|
||||
}
|
||||
|
||||
[[ noreturn ]] void showInfo(QString message) const
|
||||
{
|
||||
showMessage(message, Info);
|
||||
exit(0);
|
||||
}
|
||||
|
||||
[[ noreturn ]] void showError(QString message) const
|
||||
{
|
||||
showMessage(message + "\n\n" + helpText(), Error);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
int getIntOption(QString name) const
|
||||
{
|
||||
bool ok;
|
||||
int intValue = value(name).toInt(&ok);
|
||||
if (!ok) {
|
||||
showError(QString("Invalid %1 value: %2").arg(name, value(name)));
|
||||
}
|
||||
return intValue;
|
||||
}
|
||||
|
||||
bool getToggleOptionValue(QString name, bool defaultValue) const
|
||||
{
|
||||
QRegularExpression re(QString("^(%1|no-%1)$").arg(name));
|
||||
QStringList options = optionNames().filter(re);
|
||||
if (options.isEmpty()) {
|
||||
return defaultValue;
|
||||
} else {
|
||||
return options.last() == name;
|
||||
}
|
||||
}
|
||||
|
||||
QString getChoiceOptionValue(QString name) const
|
||||
{
|
||||
if (!m_Choices[name].contains(value(name), Qt::CaseInsensitive)) {
|
||||
showError(QString("Invalid %1 choice: %2").arg(name, value(name)));
|
||||
}
|
||||
return value(name);
|
||||
}
|
||||
|
||||
QPair<int,int> getResolutionOptionValue(QString name) const
|
||||
{
|
||||
QRegularExpression re("^(\\d+)x(\\d+)$", QRegularExpression::CaseInsensitiveOption);
|
||||
auto match = re.match(value(name));
|
||||
if (!match.hasMatch()) {
|
||||
showError(QString("Invalid %1 format: %2").arg(name, value(name)));
|
||||
}
|
||||
return qMakePair(match.captured(1).toInt(), match.captured(2).toInt());
|
||||
}
|
||||
|
||||
void addFlagOption(QString name, QString descriptiveName)
|
||||
{
|
||||
addOption(QCommandLineOption(name, QString("Use %1.").arg(descriptiveName)));
|
||||
}
|
||||
|
||||
void addToggleOption(QString name, QString descriptiveName)
|
||||
{
|
||||
addOption(QCommandLineOption(name, QString("Use %1.").arg(descriptiveName)));
|
||||
addOption(QCommandLineOption("no-" + name, QString("Do not use %1.").arg(descriptiveName)));
|
||||
}
|
||||
|
||||
void addValueOption(QString name, QString descriptiveName)
|
||||
{
|
||||
addOption(QCommandLineOption(name, QString("Specify %1 to use.").arg(descriptiveName), name));
|
||||
}
|
||||
|
||||
void addChoiceOption(QString name, QString descriptiveName, QStringList choices)
|
||||
{
|
||||
addOption(QCommandLineOption(name, QString("Select %1: %2.").arg(descriptiveName, choices.join('/')), name));
|
||||
m_Choices[name] = choices;
|
||||
}
|
||||
|
||||
private:
|
||||
QMap<QString, QStringList> m_Choices;
|
||||
};
|
||||
|
||||
GlobalCommandLineParser::GlobalCommandLineParser()
|
||||
{
|
||||
}
|
||||
|
||||
GlobalCommandLineParser::~GlobalCommandLineParser()
|
||||
{
|
||||
}
|
||||
|
||||
GlobalCommandLineParser::ParseResult GlobalCommandLineParser::parse(const QStringList &args)
|
||||
{
|
||||
CommandLineParser parser;
|
||||
parser.setupCommonOptions();
|
||||
parser.setApplicationDescription(
|
||||
"\n"
|
||||
"Starts Moonlight normally if no arguments are given.\n"
|
||||
"\n"
|
||||
"Available actions:\n"
|
||||
" stream Start streaming an app\n"
|
||||
"\n"
|
||||
"See 'moonlight <action> --help' for help of specific action."
|
||||
);
|
||||
parser.addPositionalArgument("action", "Action to execute", "<action>");
|
||||
parser.parse(args);
|
||||
auto posArgs = parser.positionalArguments();
|
||||
QString action = posArgs.isEmpty() ? QString() : posArgs.first().toLower();
|
||||
|
||||
if (action == "") {
|
||||
// This method will not return and terminates the process if --version
|
||||
// or --help is specified
|
||||
parser.handleHelpAndVersionOptions();
|
||||
parser.handleUnknownOptions();
|
||||
return NormalStartRequested;
|
||||
} else if (action == "stream") {
|
||||
return StreamRequested;
|
||||
} else {
|
||||
parser.showError(QString("Invalid action: %1").arg(action));
|
||||
}
|
||||
}
|
||||
|
||||
StreamCommandLineParser::StreamCommandLineParser()
|
||||
{
|
||||
m_WindowModeMap = {
|
||||
{"fullscreen", StreamingPreferences::WM_FULLSCREEN},
|
||||
{"windowed", StreamingPreferences::WM_WINDOWED},
|
||||
{"borderless", StreamingPreferences::WM_FULLSCREEN_DESKTOP},
|
||||
};
|
||||
m_AudioConfigMap = {
|
||||
{"auto", StreamingPreferences::AC_AUTO},
|
||||
{"stereo", StreamingPreferences::AC_FORCE_STEREO},
|
||||
{"surround", StreamingPreferences::AC_FORCE_SURROUND},
|
||||
};
|
||||
m_VideoCodecMap = {
|
||||
{"auto", StreamingPreferences::VCC_AUTO},
|
||||
{"H.264", StreamingPreferences::VCC_FORCE_H264},
|
||||
{"HEVC", StreamingPreferences::VCC_FORCE_HEVC},
|
||||
};
|
||||
m_VideoDecoderMap = {
|
||||
{"auto", StreamingPreferences::VDS_AUTO},
|
||||
{"software", StreamingPreferences::VDS_FORCE_HARDWARE},
|
||||
{"hardware", StreamingPreferences::VDS_FORCE_SOFTWARE},
|
||||
};
|
||||
}
|
||||
|
||||
StreamCommandLineParser::~StreamCommandLineParser()
|
||||
{
|
||||
}
|
||||
|
||||
void StreamCommandLineParser::parse(const QStringList &args, StreamingPreferences *preferences)
|
||||
{
|
||||
CommandLineParser parser;
|
||||
parser.setupCommonOptions();
|
||||
parser.setApplicationDescription(
|
||||
"\n"
|
||||
"Starts directly streaming a given app."
|
||||
);
|
||||
parser.addPositionalArgument("stream", "Start stream");
|
||||
|
||||
// Add other arguments and options
|
||||
parser.addPositionalArgument("host", "Host computer name, UUID, or IP address", "<host>");
|
||||
parser.addPositionalArgument("app", "App to stream", "\"<app>\"");
|
||||
|
||||
parser.addFlagOption("720", "1280x720 resolution");
|
||||
parser.addFlagOption("1080", "1920x1080 resolution");
|
||||
parser.addFlagOption("1440", "2560x1440 resolution");
|
||||
parser.addFlagOption("4K", "3840x2160 resolution");
|
||||
parser.addValueOption("resolution", "custom <width>x<height> resolution");
|
||||
parser.addToggleOption("vsync", "V-Sync");
|
||||
parser.addValueOption("fps", "FPS");
|
||||
parser.addValueOption("bitrate", "bitrate in Kbps");
|
||||
parser.addChoiceOption("display-mode", "display mode", m_WindowModeMap.keys());
|
||||
parser.addChoiceOption("audio-config", "audio config", m_AudioConfigMap.keys());
|
||||
parser.addToggleOption("multi-controller", "multiple controller support");
|
||||
parser.addToggleOption("mouse-acceleration", "mouse acceleration");
|
||||
parser.addToggleOption("game-optimization", "game optimizations");
|
||||
parser.addToggleOption("audio-on-host", "audio on host PC");
|
||||
parser.addChoiceOption("video-codec", "video codec", m_VideoCodecMap.keys());
|
||||
parser.addChoiceOption("video-decoder", "video decoder", m_VideoDecoderMap.keys());
|
||||
|
||||
if (!parser.parse(args)) {
|
||||
parser.showError(parser.errorText());
|
||||
}
|
||||
|
||||
parser.handleUnknownOptions();
|
||||
|
||||
// Resolve display's width and height
|
||||
QRegularExpression resolutionRexExp("^(720|1080|1440|4K|resolution)$");
|
||||
QStringList resoOptions = parser.optionNames().filter(resolutionRexExp);
|
||||
bool displaySet = resoOptions.length();
|
||||
if (displaySet) {
|
||||
QString name = resoOptions.last();
|
||||
if (name == "720") {
|
||||
preferences->width = 1280;
|
||||
preferences->height = 720;
|
||||
displaySet = true;
|
||||
} else if (name == "1080") {
|
||||
preferences->width = 1920;
|
||||
preferences->height = 1080;
|
||||
displaySet = true;
|
||||
} else if (name == "1440") {
|
||||
preferences->width = 2560;
|
||||
preferences->height = 1440;
|
||||
displaySet = true;
|
||||
} else if (name == "4K") {
|
||||
preferences->width = 3840;
|
||||
preferences->height = 2160;
|
||||
displaySet = true;
|
||||
} else if (name == "resolution") {
|
||||
auto resolution = parser.getResolutionOptionValue(name);
|
||||
preferences->width = resolution.first;
|
||||
preferences->height = resolution.second;
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve --fps option
|
||||
if (parser.isSet("fps")) {
|
||||
preferences->fps = parser.getIntOption("fps");
|
||||
if (!inRange(preferences->fps, 30, 120)) {
|
||||
parser.showError("FPS must be in range: 30 - 120");
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve --bitrate option
|
||||
if (parser.isSet("bitrate")) {
|
||||
preferences->bitrateKbps = parser.getIntOption("bitrate");
|
||||
if (!inRange(preferences->bitrateKbps, 500, 150000)) {
|
||||
parser.showError("Bitrate must be in range: 500 - 150000");
|
||||
}
|
||||
} else if (displaySet || parser.isSet("fps")) {
|
||||
preferences->bitrateKbps = preferences->getDefaultBitrate(
|
||||
preferences->width, preferences->height, preferences->fps);
|
||||
}
|
||||
|
||||
// Resolve --display option
|
||||
if (parser.isSet("display-mode")) {
|
||||
preferences->windowMode = mapValue(m_WindowModeMap, parser.getChoiceOptionValue("display-mode"));
|
||||
}
|
||||
|
||||
// Resolve --vsync and --no-vsync options
|
||||
preferences->enableVsync = parser.getToggleOptionValue("vsync", preferences->enableVsync);
|
||||
|
||||
// Resolve --audio-config option
|
||||
if (parser.isSet("audio-config")) {
|
||||
preferences->audioConfig = mapValue(m_AudioConfigMap, parser.getChoiceOptionValue("audio-config"));
|
||||
}
|
||||
|
||||
// Resolve --multi-controller and --no-multi-controller options
|
||||
preferences->multiController = parser.getToggleOptionValue("multi-controller", preferences->multiController);
|
||||
|
||||
// Resolve --mouse-acceleration and --no-mouse-acceleration options
|
||||
preferences->mouseAcceleration = parser.getToggleOptionValue("mouse-acceleration", preferences->mouseAcceleration);
|
||||
|
||||
// Resolve --game-optimization and --no-game-optimization options
|
||||
preferences->gameOptimizations = parser.getToggleOptionValue("game-optimization", preferences->gameOptimizations);
|
||||
|
||||
// Resolve --audio-on-host and --no-audio-on-host options
|
||||
preferences->playAudioOnHost = parser.getToggleOptionValue("audio-on-host", preferences->playAudioOnHost);
|
||||
|
||||
// Resolve --video-codec option
|
||||
if (parser.isSet("video-codec")) {
|
||||
preferences->videoCodecConfig = mapValue(m_VideoCodecMap, parser.getChoiceOptionValue("video-codec"));
|
||||
}
|
||||
|
||||
// Resolve --video-decoder option
|
||||
if (parser.isSet("video-decoder")) {
|
||||
preferences->videoDecoderSelection = mapValue(m_VideoDecoderMap, parser.getChoiceOptionValue("video-decoder"));
|
||||
}
|
||||
|
||||
// This method will not return and terminates the process if --version or
|
||||
// --help is specified
|
||||
parser.handleHelpAndVersionOptions();
|
||||
|
||||
// Verify that both host and app has been provided
|
||||
auto posArgs = parser.positionalArguments();
|
||||
if (posArgs.length() < 2) {
|
||||
parser.showError("Host not provided");
|
||||
}
|
||||
m_Host = parser.positionalArguments().at(1);
|
||||
|
||||
if (posArgs.length() < 3) {
|
||||
parser.showError("App not provided");
|
||||
}
|
||||
m_AppName = parser.positionalArguments().at(2);
|
||||
}
|
||||
|
||||
QString StreamCommandLineParser::getHost() const
|
||||
{
|
||||
return m_Host;
|
||||
}
|
||||
|
||||
QString StreamCommandLineParser::getAppName() const
|
||||
{
|
||||
return m_AppName;
|
||||
}
|
||||
|
41
app/cli/commandlineparser.h
Normal file
41
app/cli/commandlineparser.h
Normal file
|
@ -0,0 +1,41 @@
|
|||
#pragma once
|
||||
|
||||
#include "settings/streamingpreferences.h"
|
||||
|
||||
#include <QMap>
|
||||
#include <QString>
|
||||
|
||||
class GlobalCommandLineParser
|
||||
{
|
||||
public:
|
||||
enum ParseResult {
|
||||
NormalStartRequested,
|
||||
StreamRequested,
|
||||
};
|
||||
|
||||
GlobalCommandLineParser();
|
||||
virtual ~GlobalCommandLineParser();
|
||||
|
||||
ParseResult parse(const QStringList &args);
|
||||
|
||||
};
|
||||
|
||||
class StreamCommandLineParser
|
||||
{
|
||||
public:
|
||||
StreamCommandLineParser();
|
||||
virtual ~StreamCommandLineParser();
|
||||
|
||||
void parse(const QStringList &args, StreamingPreferences *preferences);
|
||||
|
||||
QString getHost() const;
|
||||
QString getAppName() const;
|
||||
|
||||
private:
|
||||
QString m_Host;
|
||||
QString m_AppName;
|
||||
QMap<QString, StreamingPreferences::WindowMode> m_WindowModeMap;
|
||||
QMap<QString, StreamingPreferences::AudioConfig> m_AudioConfigMap;
|
||||
QMap<QString, StreamingPreferences::VideoCodecConfig> m_VideoCodecMap;
|
||||
QMap<QString, StreamingPreferences::VideoDecoderSelection> m_VideoDecoderMap;
|
||||
};
|
220
app/cli/startstream.cpp
Normal file
220
app/cli/startstream.cpp
Normal file
|
@ -0,0 +1,220 @@
|
|||
#include "startstream.h"
|
||||
#include "backend/computermanager.h"
|
||||
#include "streaming/session.hpp"
|
||||
|
||||
#include <QTimer>
|
||||
|
||||
#define COMPUTER_SEEK_TIMEOUT 10000
|
||||
#define APP_SEEK_TIMEOUT 10000
|
||||
|
||||
namespace CliStartStream
|
||||
{
|
||||
|
||||
enum State {
|
||||
StateInit,
|
||||
StateSeekComputer,
|
||||
StateSeekApp,
|
||||
StateStartSession,
|
||||
StateFailure,
|
||||
};
|
||||
|
||||
class Event
|
||||
{
|
||||
public:
|
||||
enum Type {
|
||||
ComputerUpdated,
|
||||
Executed,
|
||||
Timedout,
|
||||
};
|
||||
|
||||
Event(Type type)
|
||||
: type(type), computerManager(nullptr), computer(nullptr) {}
|
||||
|
||||
Type type;
|
||||
ComputerManager *computerManager;
|
||||
NvComputer *computer;
|
||||
};
|
||||
|
||||
class LauncherPrivate
|
||||
{
|
||||
Q_DECLARE_PUBLIC(Launcher)
|
||||
|
||||
public:
|
||||
LauncherPrivate(Launcher *q) : q_ptr(q) {}
|
||||
|
||||
void handleEvent(Event event)
|
||||
{
|
||||
Q_Q(Launcher);
|
||||
Session* session;
|
||||
NvApp app;
|
||||
|
||||
switch (event.type) {
|
||||
case Event::Executed:
|
||||
if (m_State == StateInit) {
|
||||
m_State = StateSeekComputer;
|
||||
m_TimeoutTimer->start(COMPUTER_SEEK_TIMEOUT);
|
||||
m_ComputerManager = event.computerManager;
|
||||
q->connect(m_ComputerManager, &ComputerManager::computerStateChanged,
|
||||
q, &Launcher::onComputerUpdated);
|
||||
// Seek desired computer by both connecting to it directly (this may fail
|
||||
// if m_ComputerName is UUID, or the name that doesn't resolve to an IP
|
||||
// address) and by polling it using mDNS, hopefully one of these methods
|
||||
// would find the host
|
||||
m_ComputerManager->addNewHost(m_ComputerName, false);
|
||||
m_ComputerManager->startPolling();
|
||||
emit q->searchingComputer();
|
||||
}
|
||||
break;
|
||||
case Event::ComputerUpdated:
|
||||
if (m_State == StateSeekComputer) {
|
||||
if (matchComputer(event.computer) && isOnline(event.computer)) {
|
||||
if (isPaired(event.computer)) {
|
||||
m_State = StateSeekApp;
|
||||
m_TimeoutTimer->start(APP_SEEK_TIMEOUT);
|
||||
m_Computer = event.computer;
|
||||
m_ComputerManager->stopPollingAsync();
|
||||
emit q->searchingApp();
|
||||
} else {
|
||||
m_State = StateFailure;
|
||||
QString msg = QString("Computer %1 has not been paired. "
|
||||
"Please open Moonlight to pair before streaming.")
|
||||
.arg(event.computer->name);
|
||||
emit q->failed(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (m_State == StateSeekApp) {
|
||||
int index = getAppIndex();
|
||||
if (-1 != index) {
|
||||
app = m_Computer->appList[index];
|
||||
if (isNotStreaming() || isStreamingApp(app)) {
|
||||
m_State = StateStartSession;
|
||||
m_TimeoutTimer->stop();
|
||||
session = new Session(m_Computer, app, m_Preferences);
|
||||
emit q->sessionCreated(app.name, session);
|
||||
} else {
|
||||
m_State = StateFailure;
|
||||
QString msg = QString("%1 is already running. Please quit %1 to stream %2.")
|
||||
.arg(getCurrentAppName(), app.name);
|
||||
emit q->failed(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case Event::Timedout:
|
||||
if (m_State == StateSeekComputer) {
|
||||
m_State = StateFailure;
|
||||
emit q->failed(QString("Failed to connect to %1").arg(m_ComputerName));
|
||||
}
|
||||
if (m_State == StateSeekApp) {
|
||||
m_State = StateFailure;
|
||||
emit q->failed(QString("Failed to find application %1").arg(m_AppName));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
bool matchComputer(NvComputer *computer) const
|
||||
{
|
||||
QString value = m_ComputerName.toLower();
|
||||
return computer->name.toLower() == value ||
|
||||
computer->localAddress.toLower() == value ||
|
||||
computer->remoteAddress.toLower() == value ||
|
||||
computer->manualAddress.toLower() == value ||
|
||||
computer->uuid.toLower() == value;
|
||||
}
|
||||
|
||||
bool isOnline(NvComputer *computer) const
|
||||
{
|
||||
return computer->state == NvComputer::CS_ONLINE;
|
||||
}
|
||||
|
||||
bool isPaired(NvComputer *computer) const
|
||||
{
|
||||
return computer->pairState == NvComputer::PS_PAIRED;
|
||||
}
|
||||
|
||||
int getAppIndex() const
|
||||
{
|
||||
for (int i = 0; i < m_Computer->appList.length(); i++) {
|
||||
if (m_Computer->appList[i].name.toLower() == m_AppName.toLower()) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool isNotStreaming() const
|
||||
{
|
||||
return m_Computer->currentGameId == 0;
|
||||
}
|
||||
|
||||
bool isStreamingApp(NvApp app) const
|
||||
{
|
||||
return m_Computer->currentGameId == app.id;
|
||||
}
|
||||
|
||||
QString getCurrentAppName() const
|
||||
{
|
||||
for (NvApp app : m_Computer->appList) {
|
||||
if (m_Computer->currentGameId == app.id) {
|
||||
return app.name;
|
||||
}
|
||||
}
|
||||
return "<UNKNOWN>";
|
||||
}
|
||||
|
||||
Launcher *q_ptr;
|
||||
QString m_ComputerName;
|
||||
QString m_AppName;
|
||||
StreamingPreferences *m_Preferences;
|
||||
ComputerManager *m_ComputerManager;
|
||||
NvComputer *m_Computer;
|
||||
State m_State;
|
||||
QTimer *m_TimeoutTimer;
|
||||
};
|
||||
|
||||
Launcher::Launcher(QString computer, QString app,
|
||||
StreamingPreferences* preferences, QObject *parent)
|
||||
: QObject(parent),
|
||||
m_DPtr(new LauncherPrivate(this))
|
||||
{
|
||||
Q_D(Launcher);
|
||||
d->m_ComputerName = computer;
|
||||
d->m_AppName = app;
|
||||
d->m_Preferences = preferences;
|
||||
d->m_State = StateInit;
|
||||
d->m_TimeoutTimer = new QTimer(this);
|
||||
d->m_TimeoutTimer->setSingleShot(true);
|
||||
connect(d->m_TimeoutTimer, &QTimer::timeout,
|
||||
this, &Launcher::onTimeout);
|
||||
}
|
||||
|
||||
Launcher::~Launcher()
|
||||
{
|
||||
}
|
||||
|
||||
void Launcher::execute(ComputerManager *manager)
|
||||
{
|
||||
Q_D(Launcher);
|
||||
Event event(Event::Executed);
|
||||
event.computerManager = manager;
|
||||
d->handleEvent(event);
|
||||
}
|
||||
|
||||
void Launcher::onComputerUpdated(NvComputer *computer)
|
||||
{
|
||||
Q_D(Launcher);
|
||||
Event event(Event::ComputerUpdated);
|
||||
event.computer = computer;
|
||||
d->handleEvent(event);
|
||||
}
|
||||
|
||||
void Launcher::onTimeout()
|
||||
{
|
||||
Q_D(Launcher);
|
||||
Event event(Event::Timedout);
|
||||
d->handleEvent(event);
|
||||
}
|
||||
|
||||
}
|
42
app/cli/startstream.h
Normal file
42
app/cli/startstream.h
Normal file
|
@ -0,0 +1,42 @@
|
|||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
|
||||
class ComputerManager;
|
||||
class NvComputer;
|
||||
class Session;
|
||||
class StreamingPreferences;
|
||||
|
||||
namespace CliStartStream
|
||||
{
|
||||
|
||||
class Event;
|
||||
class LauncherPrivate;
|
||||
|
||||
class Launcher : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_DECLARE_PRIVATE_D(m_DPtr, Launcher)
|
||||
|
||||
public:
|
||||
explicit Launcher(QString computer, QString app,
|
||||
StreamingPreferences* preferences,
|
||||
QObject *parent = nullptr);
|
||||
~Launcher();
|
||||
Q_INVOKABLE void execute(ComputerManager *manager);
|
||||
|
||||
signals:
|
||||
void searchingComputer();
|
||||
void searchingApp();
|
||||
void sessionCreated(QString appName, Session *session);
|
||||
void failed(QString text);
|
||||
|
||||
private slots:
|
||||
void onComputerUpdated(NvComputer *computer);
|
||||
void onTimeout();
|
||||
|
||||
private:
|
||||
QScopedPointer<LauncherPrivate> m_DPtr;
|
||||
};
|
||||
|
||||
}
|
78
app/gui/CliStartStreamSegue.qml
Normal file
78
app/gui/CliStartStreamSegue.qml
Normal file
|
@ -0,0 +1,78 @@
|
|||
import QtQuick 2.0
|
||||
import QtQuick.Controls 2.2
|
||||
import QtQuick.Dialogs 1.2
|
||||
|
||||
import ComputerManager 1.0
|
||||
|
||||
Item {
|
||||
visible: false
|
||||
|
||||
function onSearchingComputer() {
|
||||
stageLabel.text = "Establishing connection to PC..."
|
||||
}
|
||||
|
||||
function onSearchingApp() {
|
||||
stageLabel.text = "Loading app list..."
|
||||
}
|
||||
|
||||
function onSessionCreated(appName, session) {
|
||||
var component = Qt.createComponent("StreamSegue.qml")
|
||||
var segue = component.createObject(stackView, {
|
||||
"appName": appName,
|
||||
"session": session,
|
||||
"quitAfter": true
|
||||
})
|
||||
stackView.push(segue)
|
||||
}
|
||||
|
||||
function onLaunchFailed(message) {
|
||||
errorDialog.text = message
|
||||
errorDialog.open()
|
||||
}
|
||||
|
||||
onVisibleChanged: {
|
||||
if (visible) {
|
||||
toolBar.visible = false
|
||||
launcher.searchingComputer.connect(onSearchingComputer)
|
||||
launcher.searchingApp.connect(onSearchingApp)
|
||||
launcher.sessionCreated.connect(onSessionCreated)
|
||||
launcher.failed.connect(onLaunchFailed)
|
||||
launcher.execute(ComputerManager)
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: 5
|
||||
|
||||
BusyIndicator {
|
||||
id: stageSpinner
|
||||
}
|
||||
|
||||
Label {
|
||||
id: stageLabel
|
||||
height: stageSpinner.height
|
||||
font.pointSize: 20
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
|
||||
wrapMode: Text.Wrap
|
||||
color: "white"
|
||||
}
|
||||
}
|
||||
|
||||
MessageDialog {
|
||||
id: errorDialog
|
||||
modality:Qt.WindowModal
|
||||
icon: StandardIcon.Critical
|
||||
standardButtons: StandardButton.Ok | StandardButton.Help
|
||||
onHelp: {
|
||||
Qt.openUrlExternally("https://github.com/moonlight-stream/moonlight-docs/wiki/Troubleshooting");
|
||||
}
|
||||
onVisibleChanged: {
|
||||
if (!visible) {
|
||||
Qt.quit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -9,6 +9,7 @@ Item {
|
|||
property Session session
|
||||
property string appName
|
||||
property string stageText : "Starting " + appName + "..."
|
||||
property bool quitAfter : false
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
|
@ -72,11 +73,15 @@ Item {
|
|||
// Run the streaming session to completion
|
||||
session.exec(Screen.virtualX, Screen.virtualY)
|
||||
|
||||
// Show the Qt window again after streaming
|
||||
window.visible = true
|
||||
if (quitAfter) {
|
||||
Qt.quit()
|
||||
} else {
|
||||
// Show the Qt window again after streaming
|
||||
window.visible = true
|
||||
|
||||
// Exit this view
|
||||
stackView.pop()
|
||||
// Exit this view
|
||||
stackView.pop()
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Show the toolbar again when we become hidden
|
||||
|
|
|
@ -21,7 +21,7 @@ ApplicationWindow {
|
|||
|
||||
StackView {
|
||||
id: stackView
|
||||
initialItem: "PcView.qml"
|
||||
initialItem: initialView
|
||||
anchors.fill: parent
|
||||
focus: true
|
||||
|
||||
|
|
28
app/main.cpp
28
app/main.cpp
|
@ -1,5 +1,6 @@
|
|||
#include <QGuiApplication>
|
||||
#include <QQmlApplicationEngine>
|
||||
#include <QQmlContext>
|
||||
#include <QIcon>
|
||||
#include <QQuickStyle>
|
||||
#include <QMutex>
|
||||
|
@ -15,6 +16,8 @@
|
|||
#include "streaming/video/ffmpeg.h"
|
||||
#endif
|
||||
|
||||
#include "cli/startstream.h"
|
||||
#include "cli/commandlineparser.h"
|
||||
#include "path.h"
|
||||
#include "gui/computermodel.h"
|
||||
#include "gui/appmodel.h"
|
||||
|
@ -299,8 +302,31 @@ int main(int argc, char *argv[])
|
|||
|
||||
QQuickStyle::setStyle("Material");
|
||||
|
||||
// Load the main.qml file
|
||||
QQmlApplicationEngine engine;
|
||||
QString initialView;
|
||||
|
||||
GlobalCommandLineParser parser;
|
||||
switch (parser.parse(app.arguments())) {
|
||||
case GlobalCommandLineParser::NormalStartRequested:
|
||||
initialView = "PcView.qml";
|
||||
break;
|
||||
case GlobalCommandLineParser::StreamRequested:
|
||||
{
|
||||
initialView = "CliStartStreamSegue.qml";
|
||||
StreamingPreferences* preferences = new StreamingPreferences(&app);
|
||||
StreamCommandLineParser streamParser;
|
||||
streamParser.parse(app.arguments(), preferences);
|
||||
QString host = streamParser.getHost();
|
||||
QString appName = streamParser.getAppName();
|
||||
auto launcher = new CliStartStream::Launcher(host, appName, preferences, &app);
|
||||
engine.rootContext()->setContextProperty("launcher", launcher);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
engine.rootContext()->setContextProperty("initialView", initialView);
|
||||
|
||||
// Load the main.qml file
|
||||
engine.load(QUrl(QStringLiteral("qrc:/gui/main.qml")));
|
||||
if (engine.rootObjects().isEmpty())
|
||||
return -1;
|
||||
|
|
|
@ -10,5 +10,6 @@
|
|||
<file>gui/NavigableToolButton.qml</file>
|
||||
<file>gui/NavigableItemDelegate.qml</file>
|
||||
<file>gui/NavigableMenuItem.qml</file>
|
||||
<file>gui/CliStartStreamSegue.qml</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
|
|
@ -22,7 +22,8 @@
|
|||
#define SER_MDNS "mdns"
|
||||
#define SER_MOUSEACCELERATION "mouseacceleration"
|
||||
|
||||
StreamingPreferences::StreamingPreferences()
|
||||
StreamingPreferences::StreamingPreferences(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
reload();
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ class StreamingPreferences : public QObject
|
|||
Q_OBJECT
|
||||
|
||||
public:
|
||||
StreamingPreferences();
|
||||
StreamingPreferences(QObject *parent = nullptr);
|
||||
|
||||
Q_INVOKABLE static int
|
||||
getDefaultBitrate(int width, int height, int fps);
|
||||
|
|
|
@ -275,8 +275,9 @@ int Session::getDecoderCapabilities(StreamingPreferences::VideoDecoderSelection
|
|||
return caps;
|
||||
}
|
||||
|
||||
Session::Session(NvComputer* computer, NvApp& app)
|
||||
: m_Computer(computer),
|
||||
Session::Session(NvComputer* computer, NvApp& app, StreamingPreferences *preferences)
|
||||
: m_Preferences(preferences ? preferences : new StreamingPreferences(this)),
|
||||
m_Computer(computer),
|
||||
m_App(app),
|
||||
m_Window(nullptr),
|
||||
m_VideoDecoder(nullptr),
|
||||
|
@ -290,7 +291,6 @@ Session::Session(NvComputer* computer, NvApp& app)
|
|||
m_AudioRenderer(nullptr),
|
||||
m_AudioRendererLock(0)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
void Session::initialize()
|
||||
|
@ -310,10 +310,10 @@ void Session::initialize()
|
|||
slices);
|
||||
|
||||
LiInitializeStreamConfiguration(&m_StreamConfig);
|
||||
m_StreamConfig.width = m_Preferences.width;
|
||||
m_StreamConfig.height = m_Preferences.height;
|
||||
m_StreamConfig.fps = m_Preferences.fps;
|
||||
m_StreamConfig.bitrate = m_Preferences.bitrateKbps;
|
||||
m_StreamConfig.width = m_Preferences->width;
|
||||
m_StreamConfig.height = m_Preferences->height;
|
||||
m_StreamConfig.fps = m_Preferences->fps;
|
||||
m_StreamConfig.bitrate = m_Preferences->bitrateKbps;
|
||||
m_StreamConfig.hevcBitratePercentageMultiplier = 75;
|
||||
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
|
||||
|
@ -326,7 +326,7 @@ void Session::initialize()
|
|||
// Only the first 4 bytes are populated in the RI key IV
|
||||
RAND_bytes(reinterpret_cast<unsigned char*>(m_StreamConfig.remoteInputAesIv), 4);
|
||||
|
||||
switch (m_Preferences.audioConfig)
|
||||
switch (m_Preferences->audioConfig)
|
||||
{
|
||||
case StreamingPreferences::AC_AUTO:
|
||||
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Autodetecting audio configuration");
|
||||
|
@ -344,12 +344,12 @@ void Session::initialize()
|
|||
"Audio configuration: %d",
|
||||
m_StreamConfig.audioConfiguration);
|
||||
|
||||
switch (m_Preferences.videoCodecConfig)
|
||||
switch (m_Preferences->videoCodecConfig)
|
||||
{
|
||||
case StreamingPreferences::VCC_AUTO:
|
||||
// TODO: Determine if HEVC is better depending on the decoder
|
||||
m_StreamConfig.supportsHevc =
|
||||
isHardwareDecodeAvailable(m_Preferences.videoDecoderSelection,
|
||||
isHardwareDecodeAvailable(m_Preferences->videoDecoderSelection,
|
||||
VIDEO_FORMAT_H265,
|
||||
m_StreamConfig.width,
|
||||
m_StreamConfig.height,
|
||||
|
@ -384,7 +384,7 @@ void Session::initialize()
|
|||
m_StreamConfig.packetSize = 1024;
|
||||
}
|
||||
|
||||
switch (m_Preferences.windowMode)
|
||||
switch (m_Preferences->windowMode)
|
||||
{
|
||||
case StreamingPreferences::WM_FULLSCREEN_DESKTOP:
|
||||
m_FullScreenFlag = SDL_WINDOW_FULLSCREEN_DESKTOP;
|
||||
|
@ -416,28 +416,28 @@ bool Session::validateLaunch()
|
|||
{
|
||||
QStringList warningList;
|
||||
|
||||
if (m_Preferences.videoDecoderSelection == StreamingPreferences::VDS_FORCE_SOFTWARE) {
|
||||
if (m_Preferences->videoDecoderSelection == StreamingPreferences::VDS_FORCE_SOFTWARE) {
|
||||
emitLaunchWarning("Your settings selection to force software decoding may cause poor streaming performance.");
|
||||
}
|
||||
|
||||
if (m_Preferences.unsupportedFps && m_StreamConfig.fps > 60) {
|
||||
if (m_Preferences->unsupportedFps && m_StreamConfig.fps > 60) {
|
||||
emitLaunchWarning("Using unsupported FPS options may cause stuttering or lag.");
|
||||
|
||||
if (m_Preferences.enableVsync) {
|
||||
if (m_Preferences->enableVsync) {
|
||||
emitLaunchWarning("V-sync will be disabled when streaming at a higher frame rate than the display.");
|
||||
}
|
||||
}
|
||||
|
||||
if (m_StreamConfig.supportsHevc) {
|
||||
bool hevcForced = m_Preferences.videoCodecConfig == StreamingPreferences::VCC_FORCE_HEVC ||
|
||||
m_Preferences.videoCodecConfig == StreamingPreferences::VCC_FORCE_HEVC_HDR;
|
||||
bool hevcForced = m_Preferences->videoCodecConfig == StreamingPreferences::VCC_FORCE_HEVC ||
|
||||
m_Preferences->videoCodecConfig == StreamingPreferences::VCC_FORCE_HEVC_HDR;
|
||||
|
||||
if (!isHardwareDecodeAvailable(m_Preferences.videoDecoderSelection,
|
||||
if (!isHardwareDecodeAvailable(m_Preferences->videoDecoderSelection,
|
||||
VIDEO_FORMAT_H265,
|
||||
m_StreamConfig.width,
|
||||
m_StreamConfig.height,
|
||||
m_StreamConfig.fps) &&
|
||||
m_Preferences.videoDecoderSelection == StreamingPreferences::VDS_AUTO) {
|
||||
m_Preferences->videoDecoderSelection == StreamingPreferences::VDS_AUTO) {
|
||||
if (hevcForced) {
|
||||
emitLaunchWarning("Using software decoding due to your selection to force HEVC without GPU support. This may cause poor streaming performance.");
|
||||
}
|
||||
|
@ -473,7 +473,7 @@ bool Session::validateLaunch()
|
|||
emitLaunchWarning("Your host PC GPU doesn't support HDR streaming. "
|
||||
"A GeForce GTX 1000-series (Pascal) or later GPU is required for HDR streaming.");
|
||||
}
|
||||
else if (!isHardwareDecodeAvailable(m_Preferences.videoDecoderSelection,
|
||||
else if (!isHardwareDecodeAvailable(m_Preferences->videoDecoderSelection,
|
||||
VIDEO_FORMAT_H265_MAIN10,
|
||||
m_StreamConfig.width,
|
||||
m_StreamConfig.height,
|
||||
|
@ -525,13 +525,13 @@ bool Session::validateLaunch()
|
|||
emitLaunchWarning("Failed to open audio device. Audio will be unavailable during this session.");
|
||||
}
|
||||
|
||||
if (m_Preferences.videoDecoderSelection == StreamingPreferences::VDS_FORCE_HARDWARE &&
|
||||
!isHardwareDecodeAvailable(m_Preferences.videoDecoderSelection,
|
||||
if (m_Preferences->videoDecoderSelection == StreamingPreferences::VDS_FORCE_HARDWARE &&
|
||||
!isHardwareDecodeAvailable(m_Preferences->videoDecoderSelection,
|
||||
m_StreamConfig.supportsHevc ? VIDEO_FORMAT_H265 : VIDEO_FORMAT_H264,
|
||||
m_StreamConfig.width,
|
||||
m_StreamConfig.height,
|
||||
m_StreamConfig.fps)) {
|
||||
if (m_Preferences.videoCodecConfig == StreamingPreferences::VCC_AUTO) {
|
||||
if (m_Preferences->videoCodecConfig == StreamingPreferences::VCC_AUTO) {
|
||||
emit displayLaunchError("Your selection to force hardware decoding cannot be satisfied due to missing hardware decoding support on this PC's GPU.");
|
||||
}
|
||||
else {
|
||||
|
@ -543,7 +543,7 @@ bool Session::validateLaunch()
|
|||
}
|
||||
|
||||
// Add the capability flags from the chosen decoder/renderer
|
||||
m_VideoCallbacks.capabilities |= getDecoderCapabilities(m_Preferences.videoDecoderSelection,
|
||||
m_VideoCallbacks.capabilities |= getDecoderCapabilities(m_Preferences->videoDecoderSelection,
|
||||
m_StreamConfig.supportsHevc ? VIDEO_FORMAT_H265 : VIDEO_FORMAT_H264,
|
||||
m_StreamConfig.width,
|
||||
m_StreamConfig.height,
|
||||
|
@ -848,7 +848,7 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
|||
// For non-full screen windows, call getWindowDimensions()
|
||||
// again after creating a window to allow it to account
|
||||
// for window chrome size.
|
||||
if (m_Preferences.windowMode == StreamingPreferences::WM_WINDOWED) {
|
||||
if (m_Preferences->windowMode == StreamingPreferences::WM_WINDOWED) {
|
||||
getWindowDimensions(x, y, width, height);
|
||||
|
||||
// We must set the size before the position because centering
|
||||
|
@ -891,7 +891,7 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
|||
// Capture the mouse by default on release builds only.
|
||||
// This prevents the mouse from becoming trapped inside
|
||||
// Moonlight when it's halted at a debug break.
|
||||
if (m_Preferences.windowMode != StreamingPreferences::WM_WINDOWED) {
|
||||
if (m_Preferences->windowMode != StreamingPreferences::WM_WINDOWED) {
|
||||
SDL_SetRelativeMouseMode(SDL_TRUE);
|
||||
}
|
||||
#endif
|
||||
|
@ -1054,7 +1054,7 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
|||
// forcefully disable V-sync to allow the stream to render faster
|
||||
// than the display.
|
||||
int displayHz = StreamUtils::getDisplayRefreshRate(m_Window);
|
||||
bool enableVsync = m_Preferences.enableVsync;
|
||||
bool enableVsync = m_Preferences->enableVsync;
|
||||
if (displayHz + 5 < m_StreamConfig.fps) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Disabling V-sync because refresh rate limit exceeded");
|
||||
|
@ -1063,7 +1063,7 @@ void Session::exec(int displayOriginX, int displayOriginY)
|
|||
|
||||
// Choose a new decoder (hopefully the same one, but possibly
|
||||
// not if a GPU was removed or something).
|
||||
if (!chooseDecoder(m_Preferences.videoDecoderSelection,
|
||||
if (!chooseDecoder(m_Preferences->videoDecoderSelection,
|
||||
m_Window, m_ActiveVideoFormat, m_ActiveVideoWidth,
|
||||
m_ActiveVideoHeight, m_ActiveVideoFrameRate,
|
||||
enableVsync,
|
||||
|
|
|
@ -18,7 +18,7 @@ class Session : public QObject
|
|||
friend class DeferredSessionCleanupTask;
|
||||
|
||||
public:
|
||||
explicit Session(NvComputer* computer, NvApp& app);
|
||||
explicit Session(NvComputer* computer, NvApp& app, StreamingPreferences *preferences = nullptr);
|
||||
|
||||
Q_INVOKABLE void exec(int displayOriginX, int displayOriginY);
|
||||
|
||||
|
@ -102,7 +102,7 @@ private:
|
|||
static
|
||||
int drSubmitDecodeUnit(PDECODE_UNIT du);
|
||||
|
||||
StreamingPreferences m_Preferences;
|
||||
StreamingPreferences* m_Preferences;
|
||||
STREAM_CONFIGURATION m_StreamConfig;
|
||||
DECODER_RENDERER_CALLBACKS m_VideoCallbacks;
|
||||
NvComputer* m_Computer;
|
||||
|
|
Loading…
Reference in a new issue