Files
jellyfin-desktop/src/main.cpp
T
iZack 52d5067676 Working 2.0.0 build for Windows
It is really late so I'm calling it here. I wanted to put this out for relevant people to get eyes on, in order to aid in getting the Windows build of v2.0.0 out. Apologies in advance for not following any contrib guidelines or there being messes.  I'm leaving in the build scripts I used for now. The build scripts still contain a few hardcoded visual studio related values for now.  At the moment when I run the app on a different machine to the dev VM I'm using the qt web view doesn't seem to be rendering.

summary of where this is at:
the whole thing builds
works on my Windows 11 virtual machine

short summary of what I did:

A couple of qt6 updates specific to windows were addressed.

Some build system issues were addressed

did some minor refactoring to sieve out binary build dependencies from generated build files since its a lot easier when you can just delete the build folder to run a clean build
2025-12-19 01:43:38 -08:00

679 lines
22 KiB
C++

#include <QGuiApplication>
#include <QApplication>
#include <QDateTime>
#include <QFileInfo>
#include <QIcon>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QUuid>
#include <QtQml>
#include <Qt>
#include <QtWebEngineQuick>
#include <qtwebenginecoreglobal.h>
#include <QtWebEngineCore/QWebEngineProfile>
#include <QErrorMessage>
#include <QtWebEngineCore/QWebEngineScript>
#include <QCommandLineOption>
#include <QDebug>
#include <QRegularExpression>
#include <QSettings>
#include "shared/Names.h"
#include "system/SystemComponent.h"
#include <QDebug>
#include "Paths.h"
#include "player/CodecsComponent.h"
#include "player/PlayerComponent.h"
#include "player/OpenGLDetect.h"
#include "display/DisplayComponent.h"
#include "Version.h"
#include "settings/SettingsComponent.h"
#include "settings/SettingsSection.h"
#include "ui/EventFilter.h"
#include "ui/WindowManager.h"
#include "Globals.h"
#include "ui/ErrorMessage.h"
#include "UniqueApplication.h"
#include "utils/Log.h"
#ifdef Q_OS_MAC
#include "PFMoveApplication.h"
#endif
#if defined(Q_OS_MAC) || defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
#include "SignalManager.h"
#endif
/////////////////////////////////////////////////////////////////////////////////////////
static void preinitQt()
{
QCoreApplication::setApplicationName(Names::MainName());
QCoreApplication::setApplicationVersion(Version::GetVersionString());
QCoreApplication::setOrganizationDomain("jellyfin.org");
#ifdef Q_OS_WIN32
QVariant useOpengl = SettingsComponent::readPreinitValue(SETTINGS_SECTION_MAIN, "useOpenGL");
// Warning: this must be the same as the default value as declared in
// the settings_description.json file, or confusion will result.
if (useOpengl.type() != QMetaType::Bool)
useOpengl = false;
if (useOpengl.toBool())
QCoreApplication::setAttribute(Qt::AA_UseDesktopOpenGL);
else
QCoreApplication::setAttribute(Qt::AA_UseOpenGLES);
#endif
}
/////////////////////////////////////////////////////////////////////////////////////////
char** appendCommandLineArguments(int argc, char **argv, const QStringList& args)
{
size_t newSize = static_cast<size_t>(argc + args.length() + 1) * sizeof(char*);
char** newArgv = static_cast<char**>(calloc(1, newSize));
memcpy(newArgv, argv, static_cast<size_t>(argc) * sizeof(char*));
int pos = argc;
for(const QString& str : args)
newArgv[pos++] = qstrdup(str.toUtf8().data());
return newArgv;
}
/////////////////////////////////////////////////////////////////////////////////////////
void ShowLicenseInfo()
{
QFile licenses(":/misc/licenses.txt");
if (!licenses.open(QIODevice::ReadOnly | QIODevice::Text))
{
fprintf(stderr, "Error: Could not open license file\n");
return;
}
QByteArray contents = licenses.readAll();
printf("%.*s\n", static_cast<int>(contents.size()), contents.data());
}
/////////////////////////////////////////////////////////////////////////////////////////
QStringList g_qtFlags = {
"--enable-gpu-rasterization",
"--disable-features=MediaSessionService"
};
/////////////////////////////////////////////////////////////////////////////////////////
int main(int argc, char *argv[])
{
try
{
QCommandLineParser parser;
parser.setApplicationDescription("Jellyfin Desktop");
parser.addVersionOption();
parser.addOptions({{{"h", "help"}, "Show this help"},
{{"l", "licenses"}, "Show license information"},
{"desktop", "Start in desktop mode"},
{"tv", "Start in TV mode"},
{"windowed", "Start in windowed mode"},
{"fullscreen", "Start in fullscreen"},
{"disable-gpu", "Disable QtWebEngine gpu accel"},
{"software-rendering", "Use software rendering (compatibility mode)"},
{"ignore-certificate-errors", "Ignore certificate errors"}});
auto scaleOption = QCommandLineOption("scale-factor", "Set to a integer or default auto which controls" \
"the scale (DPI) of the desktop interface.");
scaleOption.setValueName("scale");
scaleOption.setDefaultValue("auto");
auto platformOption = QCommandLineOption("platform", "Equivalent to QT_QPA_PLATFORM.");
platformOption.setValueName("platform");
platformOption.setDefaultValue("default");
auto devOption = QCommandLineOption("remote-debugging-port", "Port number for devtools.");
devOption.setValueName("port");
auto configDirOption = QCommandLineOption("config-dir", "Override config directory path.");
configDirOption.setValueName("path");
auto logLevelOption = QCommandLineOption("log-level", "Log level: debug, info, warn, error, fatal (default: error)");
logLevelOption.setValueName("level");
auto profileOption = QCommandLineOption("profile", "Use specific profile for this session.");
profileOption.setValueName("name");
auto setDefaultProfileOption = QCommandLineOption("set-default-profile", "Set the default profile and exit.");
setDefaultProfileOption.setValueName("name");
auto listProfilesOption = QCommandLineOption("list-profiles", "List all profiles and exit.");
auto deleteProfileOption = QCommandLineOption("delete-profile", "Delete a profile and exit.");
deleteProfileOption.setValueName("name");
auto createProfileOption = QCommandLineOption("create-profile", "Create a new profile and exit.");
createProfileOption.setValueName("name");
parser.addOption(scaleOption);
parser.addOption(devOption);
parser.addOption(platformOption);
parser.addOption(configDirOption);
parser.addOption(logLevelOption);
parser.addOption(profileOption);
parser.addOption(setDefaultProfileOption);
parser.addOption(listProfilesOption);
parser.addOption(deleteProfileOption);
parser.addOption(createProfileOption);
char **newArgv = appendCommandLineArguments(argc, argv, g_qtFlags);
int newArgc = argc + g_qtFlags.size();
preinitQt();
detectOpenGLEarly();
QQuickWindow::setGraphicsApi(QSGRendererInterface::OpenGL);
QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts);
QStringList arguments;
for (int i = 0; i < argc; i++)
arguments << QString::fromLatin1(argv[i]);
{
// This is kinda dumb. But in order for the QCommandLineParser
// to work properly we need to init if before we call process
// but we don't want to do that for the main application since
// we need to set the scale factor before we do that. So it becomes
// a small chicken-or-egg problem, which we "solve" by making
// this temporary console app.
//
QCoreApplication core(newArgc, newArgv);
// Now parse the command line.
parser.process(arguments);
}
if (parser.isSet("help"))
{
// Get Qt's generated help, insert section header before profile options
QString help = parser.helpText();
help.replace(" --profile", "\nProfile Options:\n --profile");
printf("%s", qPrintable(help));
return EXIT_SUCCESS;
}
if (parser.isSet("licenses"))
{
ShowLicenseInfo();
return EXIT_SUCCESS;
}
// Handle config-dir first (affects where profiles.json is located)
auto configDir = parser.value("config-dir");
if (!configDir.isEmpty())
{
QFileInfo fi(configDir);
QString absPath = fi.absoluteFilePath();
QDir parentDir = fi.dir();
if (!parentDir.exists())
{
fprintf(stderr, "Config directory parent does not exist: %s\n", qPrintable(parentDir.absolutePath()));
return EXIT_FAILURE;
}
Paths::setConfigDir(absPath);
QSettings::setPath(QSettings::NativeFormat, QSettings::UserScope, absPath);
}
// Helper lambdas for profile operations
auto profileExists = [](const QString& id) {
return QDir(Paths::globalDataDir("profiles/" + id)).exists();
};
auto getProfilesList = []() {
QStringList profiles;
QDir profilesDir(Paths::globalDataDir("profiles"));
if (profilesDir.exists())
{
for (const QString& dir : profilesDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot, QDir::Name))
{
if (dir.length() == 32 && dir.contains(QRegularExpression("^[0-9a-f]+$")))
profiles << dir;
}
}
return profiles;
};
auto getProfileName = [](const QString& id) {
QFile meta(Paths::globalDataDir("profiles/" + id + "/profile.json"));
if (meta.open(QIODevice::ReadOnly))
{
QJsonDocument doc = QJsonDocument::fromJson(meta.readAll());
meta.close();
return doc.object()["name"].toString();
}
return QString();
};
auto findProfileByName = [&getProfilesList, &getProfileName](const QString& name) {
for (const QString& id : getProfilesList())
{
if (getProfileName(id) == name)
return id;
}
return QString();
};
auto readDefaultProfile = []() {
QString profilesFile = Paths::globalDataDir("profiles.json");
QFile file(profilesFile);
if (file.open(QIODevice::ReadOnly))
{
QJsonDocument doc = QJsonDocument::fromJson(file.readAll());
file.close();
return doc.object()["defaultProfile"].toString();
}
return QString();
};
auto writeDefaultProfile = [](const QString& id) {
QString profilesFile = Paths::globalDataDir("profiles.json");
QJsonObject root;
root["defaultProfile"] = id;
QFile out(profilesFile);
if (out.open(QIODevice::WriteOnly))
{
out.write(QJsonDocument(root).toJson());
out.close();
return true;
}
return false;
};
auto createNewProfile = [](const QString& name) {
QString id = QUuid::createUuid().toString(QUuid::Id128);
QDir().mkpath(Paths::globalDataDir("profiles/" + id));
QDir().mkpath(Paths::globalDataDir("profiles/" + id + "/logs"));
// Always write profile.json with name
QFile meta(Paths::globalDataDir("profiles/" + id + "/profile.json"));
if (meta.open(QIODevice::WriteOnly))
{
QJsonObject obj;
obj["name"] = name;
meta.write(QJsonDocument(obj).toJson());
meta.close();
}
return id;
};
// Handle --list-profiles
if (parser.isSet("list-profiles"))
{
QStringList profiles = getProfilesList();
QString defaultId = readDefaultProfile();
if (profiles.isEmpty())
{
printf("No profiles found.\n");
}
else
{
for (const QString& id : profiles)
{
QString marker = (id == defaultId) ? " *" : "";
QString name = getProfileName(id);
if (name.isEmpty())
printf("%s%s\n", qPrintable(id), qPrintable(marker));
else
printf("%s%s\n", qPrintable(name), qPrintable(marker));
}
}
return EXIT_SUCCESS;
}
// Handle --create-profile
if (parser.isSet("create-profile"))
{
QString name = parser.value("create-profile");
if (name.isEmpty())
{
fprintf(stderr, "Profile name is required\n");
return EXIT_FAILURE;
}
if (!findProfileByName(name).isEmpty())
{
fprintf(stderr, "Profile already exists: %s\n", qPrintable(name));
return EXIT_FAILURE;
}
createNewProfile(name);
printf("Created profile: %s\n", qPrintable(name));
return EXIT_SUCCESS;
}
// Handle --delete-profile
if (parser.isSet("delete-profile"))
{
QString name = parser.value("delete-profile");
QString id = findProfileByName(name);
if (id.isEmpty())
{
fprintf(stderr, "Profile not found: %s\n", qPrintable(name));
return EXIT_FAILURE;
}
QString profilePath = Paths::globalDataDir("profiles/" + id);
QDir dir(profilePath);
if (!dir.removeRecursively())
{
fprintf(stderr, "Failed to delete profile: %s\n", qPrintable(name));
return EXIT_FAILURE;
}
// Update default if we deleted it
if (readDefaultProfile() == id)
{
QStringList remaining = getProfilesList();
writeDefaultProfile(remaining.isEmpty() ? QString() : remaining.first());
}
printf("Deleted profile: %s\n", qPrintable(name));
return EXIT_SUCCESS;
}
// Handle --set-default-profile
if (parser.isSet("set-default-profile"))
{
QString name = parser.value("set-default-profile");
QString id = findProfileByName(name);
if (id.isEmpty())
{
fprintf(stderr, "Profile not found: %s\n", qPrintable(name));
return EXIT_FAILURE;
}
if (!writeDefaultProfile(id))
{
fprintf(stderr, "Failed to write profiles.json\n");
return EXIT_FAILURE;
}
printf("Default profile set to: %s\n", qPrintable(name));
return EXIT_SUCCESS;
}
// Determine which profile to use for this session
QString profileId;
// --profile overrides everything
if (parser.isSet("profile"))
{
QString name = parser.value("profile");
profileId = findProfileByName(name);
if (profileId.isEmpty())
{
fprintf(stderr, "Profile not found: %s\n", qPrintable(name));
return EXIT_FAILURE;
}
}
else
{
// Use default profile
profileId = readDefaultProfile();
bool needsWrite = false;
// Validate default exists
if (!profileId.isEmpty() && !profileExists(profileId))
{
profileId.clear();
needsWrite = true;
}
// Find first profile
if (profileId.isEmpty())
{
QStringList profiles = getProfilesList();
if (!profiles.isEmpty())
{
profileId = profiles.first();
needsWrite = true;
}
}
// Create new profile
if (profileId.isEmpty())
{
profileId = createNewProfile("Default");
needsWrite = true;
}
if (needsWrite)
writeDefaultProfile(profileId);
}
Paths::setActiveProfileId(profileId);
// Now everything else - all paths are profile-specific from here on
QString logLevel = parser.value("log-level");
if (parser.isSet("log-level") && (logLevel.isEmpty() || Log::ParseLogLevel(logLevel) == -1))
{
fprintf(stderr, "Error: invalid log level '%s'. Valid levels: debug, info, warn, error, fatal\n", qPrintable(logLevel));
return EXIT_FAILURE;
}
if (parser.isSet("log-level"))
Log::SetLogLevel(logLevel);
Log::Init();
auto scale = parser.value("scale-factor");
if (scale.isEmpty() || scale == "auto")
{
#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
#endif
}
else if (scale != "none")
{
qputenv("QT_SCALE_FACTOR", scale.toUtf8());
}
auto platform = parser.value("platform");
if (!(platform.isEmpty() || platform == "default"))
{
qputenv("QT_QPA_PLATFORM", platform.toUtf8());
}
#if defined(Q_OS_UNIX) && !defined(Q_OS_DARWIN) && !defined(Q_OS_ANDROID) && QT_VERSION < QT_VERSION_CHECK(6, 5, 0)
// Force xcb on Qt < 6.5 due to mpvqt wayland requiring >=6.5
else if (platform.isEmpty() || platform == "default")
{
qputenv("QT_QPA_PLATFORM", "xcb");
}
#endif
QStringList chromiumFlags;
#ifdef Q_OS_LINUX
// Disable QtWebEngine's automatic MPRIS registration - we handle it ourselves
chromiumFlags << "--disable-features=MediaSessionService,HardwareMediaKeyHandling";
#endif
// Disable pinch-to-zoom if browser zoom is not allowed
QVariant allowZoom = SettingsComponent::readPreinitValue(SETTINGS_SECTION_MAIN, "allowBrowserZoom");
if (allowZoom.isValid() && !allowZoom.toBool())
chromiumFlags << "--disable-pinch";
if (parser.isSet("ignore-certificate-errors"))
chromiumFlags << "--ignore-certificate-errors";
// Software rendering mode for compatibility with problematic GPUs
if (parser.isSet("software-rendering"))
{
chromiumFlags << "--disable-gpu";
chromiumFlags << "--disable-gpu-compositing";
chromiumFlags << "--disable-accelerated-2d-canvas";
chromiumFlags << "--disable-accelerated-video-decode";
qInfo() << "Software rendering mode enabled (GPU disabled)";
}
if (!chromiumFlags.isEmpty())
qputenv("QTWEBENGINE_CHROMIUM_FLAGS", chromiumFlags.join(" ").toUtf8());
if (parser.isSet("remote-debugging-port"))
qputenv("QTWEBENGINE_REMOTE_DEBUGGING", parser.value("remote-debugging-port").toUtf8());
QtWebEngineQuick::initialize();
QApplication app(newArgc, newArgv);
#if defined(Q_OS_WIN)
// Setting window icon on OSX will break user ability to change it
app.setWindowIcon(QIcon(":/images/icon.png"));
#endif
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
// Set window icon on Linux using system icon theme
app.setWindowIcon(QIcon::fromTheme("org.jellyfin.JellyfinDesktop", QIcon(":/images/icon.png")));
// Set app id for Wayland compositor window icon
app.setDesktopFileName("org.jellyfin.JellyfinDesktop");
#endif
// Configure default WebEngineProfile paths early (profile is already set)
{
QString webEngineDir = Paths::dataDir("QtWebEngine");
QWebEngineProfile* defaultProfile = QWebEngineProfile::defaultProfile();
defaultProfile->setCachePath(webEngineDir);
defaultProfile->setPersistentStoragePath(webEngineDir);
}
#if defined(Q_OS_MAC) && defined(NDEBUG)
PFMoveToApplicationsFolderIfNecessary();
#endif
UniqueApplication* uniqueApp = new UniqueApplication(&app);
if (!uniqueApp->ensureUnique())
{
Log::Cleanup();
return EXIT_SUCCESS;
}
#ifdef Q_OS_UNIX
// install signals handlers for proper app closing.
SignalManager signalManager(&app);
Q_UNUSED(signalManager);
#endif
detectOpenGLLate();
// Initialize all the components. This needs to be done
// early since most everything else relies on it
//
ComponentManager::Get().initialize();
// Rotate temp log to main log file and rotate old logs
Log::RotateLog();
qInfo() << "Config directory:" << qPrintable(Paths::dataDir());
Codecs::preinitCodecs();
Log::ApplyConfigLogLevel();
SettingsComponent::Get().setCommandLineValues(parser.optionNames());
// Set user agent now that SystemComponent is available
QWebEngineProfile::defaultProfile()->setHttpUserAgent(SystemComponent::Get().getUserAgent());
// load QtWebChannel so that we can register our components with it.
QQmlApplicationEngine *engine = Globals::Engine();
Globals::SetContextProperty("components", &ComponentManager::Get().getQmlPropertyMap());
// the only way to detect if QML parsing fails is to hook to this signal and then see
// if we get a valid object passed to it. Any error messages will be reported on stderr
// but since no normal user should ever see this it should be fine
//
QObject::connect(engine, &QQmlApplicationEngine::objectCreated, [&](QObject* object, const QUrl& url)
{
Q_UNUSED(url);
if (object == nullptr)
throw FatalException(QObject::tr("Failed to parse application engine script."));
QQuickWindow* window = Globals::MainWindow();
// Set window flags for proper popup handling (e.g., WebEngineView dropdowns)
window->setFlags(window->flags() | Qt::WindowFullscreenButtonHint);
// Install event filter for proper event handling
window->installEventFilter(new EventFilter(window));
// Install application event filter to catch popup window creation early
class PopupFixer : public QObject {
QQuickWindow* m_mainWindow;
public:
PopupFixer(QQuickWindow* mainWin) : m_mainWindow(mainWin) {}
bool eventFilter(QObject* obj, QEvent* event) override {
auto* win = qobject_cast<QWindow*>(obj);
if (!win || win == m_mainWindow) {
return QObject::eventFilter(obj, event);
}
// Fix WebEngineView popup flags to accept focus
if (event->type() == QEvent::Show) {
Qt::WindowFlags flags = win->flags();
// Only fix WebEngineView dropdowns (Tool + FramelessWindowHint + WindowStaysOnTopHint)
// Don't touch other windows (e.g., MPV-related)
bool isWebEnginePopup = (flags & Qt::Tool) &&
(flags & Qt::FramelessWindowHint) &&
(flags & Qt::WindowStaysOnTopHint);
if (!isWebEnginePopup) {
return QObject::eventFilter(obj, event);
}
if (win->transientParent() == nullptr) {
win->setTransientParent(m_mainWindow);
}
if (win->modality() != Qt::NonModal) {
win->setModality(Qt::NonModal);
}
// WebEngineView creates popups with Qt::Tool | WindowDoesNotAcceptFocus
// which prevents interaction. Change to Qt::Popup to accept focus.
flags &= ~Qt::Tool;
flags |= Qt::Popup;
flags &= ~Qt::WindowDoesNotAcceptFocus;
win->setFlags(flags);
}
return QObject::eventFilter(obj, event);
}
};
app.installEventFilter(new PopupFixer(window));
QObject* webChannel = qvariant_cast<QObject*>(window->property("webChannel"));
Q_ASSERT(webChannel);
ComponentManager::Get().setWebChannel(qobject_cast<QWebChannel*>(webChannel));
// Initialize WindowManager with window reference
WindowManager::Get().initializeWindow(window);
// Handle other app focus by raising window
QObject::connect(uniqueApp, &UniqueApplication::otherApplicationStarted, []() {
WindowManager::Get().raiseWindow();
});
});
engine->load(QUrl(QStringLiteral("qrc:/ui/webview.qml")));
// run our application
int ret = app.exec();
delete uniqueApp;
Globals::EngineDestroy();
Codecs::Uninit();
return ret;
}
catch (FatalException& e)
{
qFatal("Unhandled FatalException: %s", qPrintable(e.message()));
QApplication errApp(argc, argv);
auto msg = new ErrorMessage(e.message(), true);
msg->show();
errApp.exec();
Codecs::Uninit();
return 1;
}
}