From d14cfb577bb7ca185593d1154aa06849987cb071 Mon Sep 17 00:00:00 2001 From: Janne Hakonen Date: Sun, 30 Sep 2018 00:06:55 +0300 Subject: [PATCH] Add command line parameters. Fixes #30 (#89) * Add command line parameters. Fixes #30 * Fixed compile errors * Fixed code review findings * Fixed code review findings, take 2 --- app/app.pro | 4 + app/cli/commandlineparser.cpp | 363 ++++++++++++++++++++++++++ app/cli/commandlineparser.h | 41 +++ app/cli/startstream.cpp | 220 ++++++++++++++++ app/cli/startstream.h | 42 +++ app/gui/CliStartStreamSegue.qml | 78 ++++++ app/gui/StreamSegue.qml | 13 +- app/gui/main.qml | 2 +- app/main.cpp | 28 +- app/qml.qrc | 1 + app/settings/streamingpreferences.cpp | 3 +- app/settings/streamingpreferences.h | 2 +- app/streaming/session.cpp | 54 ++-- app/streaming/session.hpp | 4 +- 14 files changed, 818 insertions(+), 37 deletions(-) create mode 100644 app/cli/commandlineparser.cpp create mode 100644 app/cli/commandlineparser.h create mode 100644 app/cli/startstream.cpp create mode 100644 app/cli/startstream.h create mode 100644 app/gui/CliStartStreamSegue.qml diff --git a/app/app.pro b/app/app.pro index a2c9a939..2e0a56ec 100644 --- a/app/app.pro +++ b/app/app.pro @@ -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 \ diff --git a/app/cli/commandlineparser.cpp b/app/cli/commandlineparser.cpp new file mode 100644 index 00000000..6dc43efb --- /dev/null +++ b/app/cli/commandlineparser.cpp @@ -0,0 +1,363 @@ +#include "commandlineparser.h" + +#include +#include + +#if defined(Q_OS_WIN) +#include +#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 +static T mapValue(QMap 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(message.utf16()), + reinterpret_cast(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 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 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 --help' for help of specific action." + ); + parser.addPositionalArgument("action", "Action to execute", ""); + 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", ""); + parser.addPositionalArgument("app", "App to stream", "\"\""); + + parser.addFlagOption("720", "1280x720 resolution"); + parser.addFlagOption("1080", "1920x1080 resolution"); + parser.addFlagOption("1440", "2560x1440 resolution"); + parser.addFlagOption("4K", "3840x2160 resolution"); + parser.addValueOption("resolution", "custom x 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; +} + diff --git a/app/cli/commandlineparser.h b/app/cli/commandlineparser.h new file mode 100644 index 00000000..7f7c3c94 --- /dev/null +++ b/app/cli/commandlineparser.h @@ -0,0 +1,41 @@ +#pragma once + +#include "settings/streamingpreferences.h" + +#include +#include + +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 m_WindowModeMap; + QMap m_AudioConfigMap; + QMap m_VideoCodecMap; + QMap m_VideoDecoderMap; +}; diff --git a/app/cli/startstream.cpp b/app/cli/startstream.cpp new file mode 100644 index 00000000..2848044f --- /dev/null +++ b/app/cli/startstream.cpp @@ -0,0 +1,220 @@ +#include "startstream.h" +#include "backend/computermanager.h" +#include "streaming/session.hpp" + +#include + +#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 ""; + } + + 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); +} + +} diff --git a/app/cli/startstream.h b/app/cli/startstream.h new file mode 100644 index 00000000..63372154 --- /dev/null +++ b/app/cli/startstream.h @@ -0,0 +1,42 @@ +#pragma once + +#include + +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 m_DPtr; +}; + +} diff --git a/app/gui/CliStartStreamSegue.qml b/app/gui/CliStartStreamSegue.qml new file mode 100644 index 00000000..916b9684 --- /dev/null +++ b/app/gui/CliStartStreamSegue.qml @@ -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(); + } + } + } + +} diff --git a/app/gui/StreamSegue.qml b/app/gui/StreamSegue.qml index f76dc801..686aecf0 100644 --- a/app/gui/StreamSegue.qml +++ b/app/gui/StreamSegue.qml @@ -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 diff --git a/app/gui/main.qml b/app/gui/main.qml index b29184ac..f81c2de4 100644 --- a/app/gui/main.qml +++ b/app/gui/main.qml @@ -21,7 +21,7 @@ ApplicationWindow { StackView { id: stackView - initialItem: "PcView.qml" + initialItem: initialView anchors.fill: parent focus: true diff --git a/app/main.cpp b/app/main.cpp index 55f33e7c..3cf690f2 100644 --- a/app/main.cpp +++ b/app/main.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include #include @@ -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; diff --git a/app/qml.qrc b/app/qml.qrc index 65731193..28d69339 100644 --- a/app/qml.qrc +++ b/app/qml.qrc @@ -10,5 +10,6 @@ gui/NavigableToolButton.qml gui/NavigableItemDelegate.qml gui/NavigableMenuItem.qml + gui/CliStartStreamSegue.qml diff --git a/app/settings/streamingpreferences.cpp b/app/settings/streamingpreferences.cpp index 06ed5a38..286308d8 100644 --- a/app/settings/streamingpreferences.cpp +++ b/app/settings/streamingpreferences.cpp @@ -22,7 +22,8 @@ #define SER_MDNS "mdns" #define SER_MOUSEACCELERATION "mouseacceleration" -StreamingPreferences::StreamingPreferences() +StreamingPreferences::StreamingPreferences(QObject *parent) + : QObject(parent) { reload(); } diff --git a/app/settings/streamingpreferences.h b/app/settings/streamingpreferences.h index 2cd1395f..120d33e7 100644 --- a/app/settings/streamingpreferences.h +++ b/app/settings/streamingpreferences.h @@ -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); diff --git a/app/streaming/session.cpp b/app/streaming/session.cpp index 3bf557a8..3a9dbd54 100644 --- a/app/streaming/session.cpp +++ b/app/streaming/session.cpp @@ -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(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, diff --git a/app/streaming/session.hpp b/app/streaming/session.hpp index 55209bba..69dc7c8b 100644 --- a/app/streaming/session.hpp +++ b/app/streaming/session.hpp @@ -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;