mirror of
https://github.com/moonlight-stream/moonlight-qt
synced 2024-11-16 08:17:58 +00:00
477 lines
17 KiB
C++
477 lines
17 KiB
C++
#include <QGuiApplication>
|
|
#include <QQmlApplicationEngine>
|
|
#include <QQmlContext>
|
|
#include <QIcon>
|
|
#include <QQuickStyle>
|
|
#include <QMutex>
|
|
#include <QtDebug>
|
|
#include <QNetworkProxyFactory>
|
|
#include <QPalette>
|
|
#include <QFont>
|
|
#include <QCursor>
|
|
|
|
// Don't let SDL hook our main function, since Qt is already
|
|
// doing the same thing. This needs to be before any headers
|
|
// that might include SDL.h themselves.
|
|
#define SDL_MAIN_HANDLED
|
|
#include <SDL.h>
|
|
|
|
#ifdef HAVE_FFMPEG
|
|
#include "streaming/video/ffmpeg.h"
|
|
#endif
|
|
|
|
#ifdef Q_OS_WIN32
|
|
#include "antihookingprotection.h"
|
|
#endif
|
|
|
|
#include "cli/quitstream.h"
|
|
#include "cli/startstream.h"
|
|
#include "cli/commandlineparser.h"
|
|
#include "path.h"
|
|
#include "gui/computermodel.h"
|
|
#include "gui/appmodel.h"
|
|
#include "backend/autoupdatechecker.h"
|
|
#include "backend/systemproperties.h"
|
|
#include "streaming/session.h"
|
|
#include "settings/streamingpreferences.h"
|
|
#include "gui/sdlgamepadkeynavigation.h"
|
|
|
|
#if !defined(QT_DEBUG) && defined(Q_OS_WIN32)
|
|
// Log to file for release Windows builds
|
|
#define USE_CUSTOM_LOGGER
|
|
#define LOG_TO_FILE
|
|
#elif defined(Q_OS_UNIX) && !defined(Q_OS_DARWIN)
|
|
// Use stdout logger on all Linux/BSD builds
|
|
#define USE_CUSTOM_LOGGER
|
|
#elif !defined(QT_DEBUG) && defined(Q_OS_DARWIN)
|
|
// Log to file for release Mac builds
|
|
#define USE_CUSTOM_LOGGER
|
|
#define LOG_TO_FILE
|
|
#else
|
|
// For debug Windows and Mac builds, use default logger
|
|
#endif
|
|
|
|
#ifdef USE_CUSTOM_LOGGER
|
|
static QTime s_LoggerTime;
|
|
static QTextStream s_LoggerStream(stdout);
|
|
static QMutex s_LoggerLock;
|
|
#ifdef LOG_TO_FILE
|
|
#define MAX_LOG_LINES 10000
|
|
static int s_LogLinesWritten = 0;
|
|
static bool s_LogLimitReached = false;
|
|
static QFile* s_LoggerFile;
|
|
#endif
|
|
|
|
void logToLoggerStream(QString& message)
|
|
{
|
|
QMutexLocker lock(&s_LoggerLock);
|
|
|
|
#ifdef LOG_TO_FILE
|
|
if (s_LogLimitReached) {
|
|
return;
|
|
}
|
|
else if (s_LogLinesWritten == MAX_LOG_LINES) {
|
|
s_LoggerStream << "Log size limit reached!" << endl;
|
|
s_LogLimitReached = true;
|
|
return;
|
|
}
|
|
else {
|
|
s_LogLinesWritten++;
|
|
}
|
|
#endif
|
|
|
|
s_LoggerStream << message;
|
|
s_LoggerStream.flush();
|
|
}
|
|
|
|
void sdlLogToDiskHandler(void*, int category, SDL_LogPriority priority, const char* message)
|
|
{
|
|
QString priorityTxt;
|
|
|
|
switch (priority) {
|
|
case SDL_LOG_PRIORITY_VERBOSE:
|
|
priorityTxt = "Verbose";
|
|
break;
|
|
case SDL_LOG_PRIORITY_DEBUG:
|
|
priorityTxt = "Debug";
|
|
break;
|
|
case SDL_LOG_PRIORITY_INFO:
|
|
priorityTxt = "Info";
|
|
break;
|
|
case SDL_LOG_PRIORITY_WARN:
|
|
priorityTxt = "Warn";
|
|
break;
|
|
case SDL_LOG_PRIORITY_ERROR:
|
|
priorityTxt = "Error";
|
|
break;
|
|
case SDL_LOG_PRIORITY_CRITICAL:
|
|
priorityTxt = "Critical";
|
|
break;
|
|
default:
|
|
priorityTxt = "Unknown";
|
|
break;
|
|
}
|
|
|
|
QTime logTime = QTime::fromMSecsSinceStartOfDay(s_LoggerTime.elapsed());
|
|
QString txt = QString("%1 - SDL %2 (%3): %4\n").arg(logTime.toString()).arg(priorityTxt).arg(category).arg(message);
|
|
|
|
logToLoggerStream(txt);
|
|
}
|
|
|
|
void qtLogToDiskHandler(QtMsgType type, const QMessageLogContext&, const QString& msg)
|
|
{
|
|
QString typeTxt;
|
|
|
|
switch (type) {
|
|
case QtDebugMsg:
|
|
typeTxt = "Debug";
|
|
break;
|
|
case QtInfoMsg:
|
|
typeTxt = "Info";
|
|
break;
|
|
case QtWarningMsg:
|
|
typeTxt = "Warning";
|
|
break;
|
|
case QtCriticalMsg:
|
|
typeTxt = "Critical";
|
|
break;
|
|
case QtFatalMsg:
|
|
typeTxt = "Fatal";
|
|
break;
|
|
}
|
|
|
|
QTime logTime = QTime::fromMSecsSinceStartOfDay(s_LoggerTime.elapsed());
|
|
QString txt = QString("%1 - Qt %2: %3\n").arg(logTime.toString()).arg(typeTxt).arg(msg);
|
|
|
|
logToLoggerStream(txt);
|
|
}
|
|
|
|
#ifdef HAVE_FFMPEG
|
|
|
|
void ffmpegLogToDiskHandler(void* ptr, int level, const char* fmt, va_list vl)
|
|
{
|
|
char lineBuffer[1024];
|
|
static int printPrefix = 1;
|
|
|
|
if ((level & 0xFF) > av_log_get_level()) {
|
|
return;
|
|
}
|
|
|
|
av_log_format_line(ptr, level, fmt, vl, lineBuffer, sizeof(lineBuffer), &printPrefix);
|
|
|
|
QTime logTime = QTime::fromMSecsSinceStartOfDay(s_LoggerTime.elapsed());
|
|
QString txt = QString("%1 - FFmpeg: %2").arg(logTime.toString()).arg(lineBuffer);
|
|
|
|
logToLoggerStream(txt);
|
|
}
|
|
|
|
#endif
|
|
|
|
#endif
|
|
|
|
#ifdef Q_OS_WIN32
|
|
#define WIN32_LEAN_AND_MEAN
|
|
#include <Windows.h>
|
|
#include <DbgHelp.h>
|
|
|
|
static UINT s_HitUnhandledException = 0;
|
|
|
|
LONG WINAPI UnhandledExceptionHandler(struct _EXCEPTION_POINTERS *ExceptionInfo)
|
|
{
|
|
// Only write a dump for the first unhandled exception
|
|
if (InterlockedCompareExchange(&s_HitUnhandledException, 1, 0) != 0) {
|
|
return EXCEPTION_CONTINUE_SEARCH;
|
|
}
|
|
|
|
WCHAR dmpFileName[MAX_PATH];
|
|
swprintf_s(dmpFileName, L"%ls\\Moonlight-%I64u.dmp",
|
|
(PWCHAR)QDir::toNativeSeparators(Path::getLogDir()).utf16(), QDateTime::currentSecsSinceEpoch());
|
|
QString qDmpFileName = QString::fromUtf16((unsigned short*)dmpFileName);
|
|
HANDLE dumpHandle = CreateFileW(dmpFileName, GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
|
|
if (dumpHandle != INVALID_HANDLE_VALUE) {
|
|
MINIDUMP_EXCEPTION_INFORMATION info;
|
|
|
|
info.ThreadId = GetCurrentThreadId();
|
|
info.ExceptionPointers = ExceptionInfo;
|
|
info.ClientPointers = FALSE;
|
|
|
|
DWORD typeFlags = MiniDumpWithIndirectlyReferencedMemory |
|
|
MiniDumpIgnoreInaccessibleMemory |
|
|
MiniDumpWithUnloadedModules |
|
|
MiniDumpWithThreadInfo;
|
|
|
|
if (MiniDumpWriteDump(GetCurrentProcess(),
|
|
GetCurrentProcessId(),
|
|
dumpHandle,
|
|
(MINIDUMP_TYPE)typeFlags,
|
|
&info,
|
|
nullptr,
|
|
nullptr)) {
|
|
qCritical() << "Unhandled exception! Minidump written to:" << qDmpFileName;
|
|
}
|
|
else {
|
|
qCritical() << "Unhandled exception! Failed to write dump:" << GetLastError();
|
|
}
|
|
|
|
CloseHandle(dumpHandle);
|
|
}
|
|
else {
|
|
qCritical() << "Unhandled exception! Failed to open dump file:" << qDmpFileName << "with error" << GetLastError();
|
|
}
|
|
|
|
// Let the program crash and WER collect a dump
|
|
return EXCEPTION_CONTINUE_SEARCH;
|
|
}
|
|
|
|
#endif
|
|
|
|
int main(int argc, char *argv[])
|
|
{
|
|
SDL_SetMainReady();
|
|
|
|
// Set the app version for the QCommandLineParser's showVersion() command
|
|
QCoreApplication::setApplicationVersion(VERSION_STR);
|
|
|
|
// Set these here to allow us to use the default QSettings constructor.
|
|
// These also ensure that our cache directory is named correctly. As such,
|
|
// it is critical that these be called before Path::initialize().
|
|
QCoreApplication::setOrganizationName("Moonlight Game Streaming Project");
|
|
QCoreApplication::setOrganizationDomain("moonlight-stream.com");
|
|
QCoreApplication::setApplicationName("Moonlight");
|
|
|
|
if (QFile(QDir::currentPath() + "/portable.dat").exists()) {
|
|
qInfo() << "Running in portable mode from:" << QDir::currentPath();
|
|
QSettings::setDefaultFormat(QSettings::IniFormat);
|
|
QSettings::setPath(QSettings::IniFormat, QSettings::UserScope, QDir::currentPath());
|
|
QSettings::setPath(QSettings::IniFormat, QSettings::SystemScope, QDir::currentPath());
|
|
|
|
// Initialize paths for portable mode
|
|
Path::initialize(true);
|
|
}
|
|
else {
|
|
// Initialize paths for standard installation
|
|
Path::initialize(false);
|
|
}
|
|
|
|
#ifdef USE_CUSTOM_LOGGER
|
|
#ifdef LOG_TO_FILE
|
|
QDir tempDir(Path::getLogDir());
|
|
s_LoggerFile = new QFile(tempDir.filePath(QString("Moonlight-%1.log").arg(QDateTime::currentSecsSinceEpoch())));
|
|
if (s_LoggerFile->open(QIODevice::WriteOnly)) {
|
|
qInfo() << "Redirecting log output to " << s_LoggerFile->fileName();
|
|
s_LoggerStream.setDevice(s_LoggerFile);
|
|
}
|
|
#endif
|
|
|
|
s_LoggerTime.start();
|
|
qInstallMessageHandler(qtLogToDiskHandler);
|
|
SDL_LogSetOutputFunction(sdlLogToDiskHandler, nullptr);
|
|
|
|
#ifdef HAVE_FFMPEG
|
|
av_log_set_callback(ffmpegLogToDiskHandler);
|
|
#endif
|
|
|
|
#endif
|
|
|
|
#ifdef Q_OS_WIN32
|
|
// Create a crash dump when we crash on Windows
|
|
SetUnhandledExceptionFilter(UnhandledExceptionHandler);
|
|
#endif
|
|
|
|
#ifdef Q_OS_WIN32
|
|
// Force AntiHooking.dll to be statically imported and loaded
|
|
// by ntdll by calling a dummy function.
|
|
AntiHookingDummyImport();
|
|
#endif
|
|
|
|
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
|
|
|
|
// This avoids using the default keychain for SSL, which may cause
|
|
// password prompts on macOS.
|
|
qputenv("QT_SSL_USE_TEMPORARY_KEYCHAIN", "1");
|
|
|
|
#ifdef Q_OS_WIN32
|
|
if (!qEnvironmentVariableIsSet("QT_OPENGL")) {
|
|
// On Windows, use ANGLE so we don't have to load OpenGL
|
|
// user-mode drivers into our app. OGL drivers (especially Intel)
|
|
// seem to crash Moonlight far more often than DirectX.
|
|
qputenv("QT_OPENGL", "angle");
|
|
}
|
|
#endif
|
|
|
|
// We don't want system proxies to apply to us
|
|
QNetworkProxyFactory::setUseSystemConfiguration(false);
|
|
|
|
// Clear any default application proxy
|
|
QNetworkProxy noProxy(QNetworkProxy::NoProxy);
|
|
QNetworkProxy::setApplicationProxy(noProxy);
|
|
|
|
// Register custom metatypes for use in signals
|
|
qRegisterMetaType<NvApp>("NvApp");
|
|
|
|
// Allow the display to sleep by default. We will manually use SDL_DisableScreenSaver()
|
|
// and SDL_EnableScreenSaver() when appropriate. This hint must be set before
|
|
// initializing the SDL video subsystem to have any effect.
|
|
SDL_SetHint(SDL_HINT_VIDEO_ALLOW_SCREENSAVER, "1");
|
|
|
|
if (SDL_InitSubSystem(SDL_INIT_TIMER) != 0) {
|
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
|
"SDL_InitSubSystem(SDL_INIT_TIMER) failed: %s",
|
|
SDL_GetError());
|
|
return -1;
|
|
}
|
|
|
|
#ifdef STEAM_LINK
|
|
// Steam Link requires that we initialize video before creating our
|
|
// QGuiApplication in order to configure the framebuffer correctly.
|
|
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
|
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
|
|
"SDL_InitSubSystem(SDL_INIT_VIDEO) failed: %s",
|
|
SDL_GetError());
|
|
return -1;
|
|
}
|
|
#endif
|
|
|
|
// Use atexit() to ensure SDL_Quit() is called. This avoids
|
|
// racing with object destruction where SDL may be used.
|
|
atexit(SDL_Quit);
|
|
|
|
// Avoid the default behavior of changing the timer resolution to 1 ms.
|
|
// We don't want this all the time that Moonlight is open. We will set
|
|
// it manually when we start streaming.
|
|
SDL_SetHint(SDL_HINT_TIMER_RESOLUTION, "0");
|
|
|
|
// Disable minimize on focus loss by default. Users seem to want this off by default.
|
|
SDL_SetHint(SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS, "0");
|
|
|
|
QGuiApplication app(argc, argv);
|
|
|
|
// After the QGuiApplication is created, the platform stuff will be initialized
|
|
// and we can set the SDL video driver to match Qt.
|
|
if (qgetenv("XDG_SESSION_TYPE") == "wayland" && QGuiApplication::platformName() == "xcb") {
|
|
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
|
"Detected XWayland. This will probably break hardware decoding! Try running with QT_QPA_PLATFORM=wayland or switch to X11.");
|
|
}
|
|
else if (QGuiApplication::platformName().startsWith("wayland")) {
|
|
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Detected Wayland. Performance may be worse than X11.");
|
|
qputenv("SDL_VIDEODRIVER", "wayland");
|
|
}
|
|
|
|
#ifdef STEAM_LINK
|
|
// Qt 5.9 from the Steam Link SDK is not able to load any fonts
|
|
// since the Steam Link doesn't include any of the ones it looks
|
|
// for. We know it has NotoSans so we will explicitly ask for that.
|
|
if (app.font().family().isEmpty()) {
|
|
qWarning() << "SL HACK: No default font - using NotoSans";
|
|
|
|
QFont fon("NotoSans");
|
|
app.setFont(fon);
|
|
}
|
|
|
|
// Move the mouse to the bottom right so it's invisible when using
|
|
// gamepad-only navigation.
|
|
QCursor().setPos(0xFFFF, 0xFFFF);
|
|
#endif
|
|
|
|
app.setWindowIcon(QIcon(":/res/moonlight.svg"));
|
|
|
|
// Register our C++ types for QML
|
|
qmlRegisterType<ComputerModel>("ComputerModel", 1, 0, "ComputerModel");
|
|
qmlRegisterType<AppModel>("AppModel", 1, 0, "AppModel");
|
|
qmlRegisterUncreatableType<Session>("Session", 1, 0, "Session", "Session cannot be created from QML");
|
|
qmlRegisterSingletonType<ComputerManager>("ComputerManager", 1, 0,
|
|
"ComputerManager",
|
|
[](QQmlEngine*, QJSEngine*) -> QObject* {
|
|
return new ComputerManager();
|
|
});
|
|
qmlRegisterSingletonType<AutoUpdateChecker>("AutoUpdateChecker", 1, 0,
|
|
"AutoUpdateChecker",
|
|
[](QQmlEngine*, QJSEngine*) -> QObject* {
|
|
return new AutoUpdateChecker();
|
|
});
|
|
qmlRegisterSingletonType<SystemProperties>("SystemProperties", 1, 0,
|
|
"SystemProperties",
|
|
[](QQmlEngine*, QJSEngine*) -> QObject* {
|
|
return new SystemProperties();
|
|
});
|
|
qmlRegisterSingletonType<SdlGamepadKeyNavigation>("SdlGamepadKeyNavigation", 1, 0,
|
|
"SdlGamepadKeyNavigation",
|
|
[](QQmlEngine*, QJSEngine*) -> QObject* {
|
|
return new SdlGamepadKeyNavigation();
|
|
});
|
|
qmlRegisterSingletonType<StreamingPreferences>("StreamingPreferences", 1, 0,
|
|
"StreamingPreferences",
|
|
[](QQmlEngine*, QJSEngine*) -> QObject* {
|
|
return new StreamingPreferences();
|
|
});
|
|
|
|
#ifndef Q_OS_WINRT
|
|
// Use the dense material dark theme by default
|
|
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_STYLE")) {
|
|
qputenv("QT_QUICK_CONTROLS_STYLE", "Material");
|
|
}
|
|
#else
|
|
// Use universal dark on WinRT
|
|
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_STYLE")) {
|
|
qputenv("QT_QUICK_CONTROLS_STYLE", "Universal");
|
|
}
|
|
#endif
|
|
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_MATERIAL_THEME")) {
|
|
qputenv("QT_QUICK_CONTROLS_MATERIAL_THEME", "Dark");
|
|
}
|
|
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_MATERIAL_ACCENT")) {
|
|
qputenv("QT_QUICK_CONTROLS_MATERIAL_ACCENT", "Purple");
|
|
}
|
|
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_MATERIAL_VARIANT")) {
|
|
qputenv("QT_QUICK_CONTROLS_MATERIAL_VARIANT", "Dense");
|
|
}
|
|
if (!qEnvironmentVariableIsSet("QT_QUICK_CONTROLS_UNIVERSAL_THEME")) {
|
|
qputenv("QT_QUICK_CONTROLS_UNIVERSAL_THEME", "Dark");
|
|
}
|
|
|
|
QQmlApplicationEngine engine;
|
|
QString initialView;
|
|
|
|
GlobalCommandLineParser parser;
|
|
switch (parser.parse(app.arguments())) {
|
|
case GlobalCommandLineParser::NormalStartRequested:
|
|
initialView = "qrc:/gui/PcView.qml";
|
|
break;
|
|
case GlobalCommandLineParser::StreamRequested:
|
|
{
|
|
initialView = "qrc:/gui/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;
|
|
}
|
|
case GlobalCommandLineParser::QuitRequested:
|
|
{
|
|
initialView = "qrc:/gui/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);
|
|
|
|
// Load the main.qml file
|
|
engine.load(QUrl(QStringLiteral("qrc:/gui/main.qml")));
|
|
if (engine.rootObjects().isEmpty())
|
|
return -1;
|
|
|
|
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;
|
|
}
|