diff --git a/app/app.pro b/app/app.pro index 56794409..e0de8038 100644 --- a/app/app.pro +++ b/app/app.pro @@ -108,6 +108,10 @@ win32 { } win32:!winrt { CONFIG += soundio + + # Discord RPC for rich presence + LIBS += -ldiscord-rpc + DEFINES += HAVE_DISCORD } macx { LIBS += -lssl -lcrypto -lavcodec.58 -lavutil.56 -lopus -framework SDL2 -framework SDL2_ttf @@ -128,6 +132,7 @@ SOURCES += \ backend/nvpairingmanager.cpp \ backend/computermanager.cpp \ backend/boxartmanager.cpp \ + backend/richpresencemanager.cpp \ cli/commandlineparser.cpp \ cli/quitstream.cpp \ cli/startstream.cpp \ @@ -155,6 +160,7 @@ HEADERS += \ backend/nvpairingmanager.h \ backend/computermanager.h \ backend/boxartmanager.h \ + backend/richpresencemanager.h \ cli/commandlineparser.h \ cli/quitstream.h \ cli/startstream.h \ diff --git a/app/backend/richpresencemanager.cpp b/app/backend/richpresencemanager.cpp new file mode 100644 index 00000000..eecb4ea4 --- /dev/null +++ b/app/backend/richpresencemanager.cpp @@ -0,0 +1,63 @@ +#include "richpresencemanager.h" + +#include + +RichPresenceManager::RichPresenceManager(StreamingPreferences& prefs, QString gameName) + : m_DiscordActive(false) +{ +#ifdef HAVE_DISCORD + if (prefs.richPresence) { + DiscordEventHandlers handlers = {}; + handlers.ready = discordReady; + handlers.disconnected = discordDisconnected; + handlers.errored = discordErrored; + Discord_Initialize("594668102021677159", &handlers, 0, nullptr); + m_DiscordActive = true; + } + + if (m_DiscordActive) { + QByteArray stateStr = (QString("Streaming ") + gameName).toUtf8(); + + DiscordRichPresence discordPresence = {}; + discordPresence.state = stateStr.data(); + discordPresence.startTimestamp = time(nullptr); + Discord_UpdatePresence(&discordPresence); + } +#endif +} + +RichPresenceManager::~RichPresenceManager() +{ +#ifdef HAVE_DISCORD + if (m_DiscordActive) { + Discord_ClearPresence(); + Discord_Shutdown(); + } +#endif +} + +void RichPresenceManager::runCallbacks() +{ +#ifdef HAVE_DISCORD + if (m_DiscordActive) { + Discord_RunCallbacks(); + } +#endif +} + +#ifdef HAVE_DISCORD +void RichPresenceManager::discordReady(const DiscordUser* request) +{ + qInfo() << "Discord integration ready for user:" << request->username; +} + +void RichPresenceManager::discordDisconnected(int errorCode, const char *message) +{ + qInfo() << "Discord integration disconnected:" << errorCode << message; +} + +void RichPresenceManager::discordErrored(int errorCode, const char *message) +{ + qWarning() << "Discord integration error:" << errorCode << message; +} +#endif diff --git a/app/backend/richpresencemanager.h b/app/backend/richpresencemanager.h new file mode 100644 index 00000000..b006e2be --- /dev/null +++ b/app/backend/richpresencemanager.h @@ -0,0 +1,26 @@ +#pragma once + +#include "settings/streamingpreferences.h" + +#ifdef HAVE_DISCORD +#include +#endif + +class RichPresenceManager +{ +public: + RichPresenceManager(StreamingPreferences& prefs, QString gameName); + ~RichPresenceManager(); + + void runCallbacks(); + +private: +#ifdef HAVE_DISCORD + static void discordReady(const DiscordUser* request); + static void discordDisconnected(int errorCode, const char* message); + static void discordErrored(int errorCode, const char* message); +#endif + + bool m_DiscordActive; +}; + diff --git a/app/backend/systemproperties.cpp b/app/backend/systemproperties.cpp index 1f01591e..b5f88588 100644 --- a/app/backend/systemproperties.cpp +++ b/app/backend/systemproperties.cpp @@ -22,6 +22,12 @@ SystemProperties::SystemProperties() hasBrowser = false; #endif +#ifdef HAVE_DISCORD + hasDiscordIntegration = true; +#else + hasDiscordIntegration = false; +#endif + unmappedGamepads = SdlInputHandler::getUnmappedGamepads(); // Populate data that requires talking to SDL. We do it all in one shot diff --git a/app/backend/systemproperties.h b/app/backend/systemproperties.h index 351bbb16..4d59049a 100644 --- a/app/backend/systemproperties.h +++ b/app/backend/systemproperties.h @@ -15,6 +15,7 @@ public: Q_PROPERTY(bool isRunningXWayland MEMBER isRunningXWayland CONSTANT) Q_PROPERTY(bool isWow64 MEMBER isWow64 CONSTANT) Q_PROPERTY(bool hasBrowser MEMBER hasBrowser CONSTANT) + Q_PROPERTY(bool hasDiscordIntegration MEMBER hasDiscordIntegration CONSTANT) Q_PROPERTY(QString unmappedGamepads MEMBER unmappedGamepads NOTIFY unmappedGamepadsChanged) Q_PROPERTY(int maximumStreamingFrameRate MEMBER maximumStreamingFrameRate CONSTANT) @@ -32,6 +33,7 @@ private: bool isRunningXWayland; bool isWow64; bool hasBrowser; + bool hasDiscordIntegration; QString unmappedGamepads; int maximumStreamingFrameRate; QList monitorDesktopResolutions; diff --git a/app/gui/SettingsView.qml b/app/gui/SettingsView.qml index 431eb0cf..71422d22 100644 --- a/app/gui/SettingsView.qml +++ b/app/gui/SettingsView.qml @@ -499,6 +499,22 @@ Flickable { StreamingPreferences.connectionWarnings = checked } } + + CheckBox { + visible: SystemProperties.hasDiscordIntegration + id: discordPresenceCheck + text: "Discord Rich Presence integration" + font.pointSize: 12 + checked: StreamingPreferences.richPresence + onCheckedChanged: { + StreamingPreferences.richPresence = checked + } + + ToolTip.delay: 1000 + ToolTip.timeout: 5000 + ToolTip.visible: hovered + ToolTip.text: "Updates your Discord status to display the name of the game you're streaming." + } } } } diff --git a/app/settings/streamingpreferences.cpp b/app/settings/streamingpreferences.cpp index da8d7738..f4464be3 100644 --- a/app/settings/streamingpreferences.cpp +++ b/app/settings/streamingpreferences.cpp @@ -23,6 +23,7 @@ #define SER_STARTWINDOWED "startwindowed" #define SER_FRAMEPACING "framepacing" #define SER_CONNWARNINGS "connwarnings" +#define SER_RICHPRESENCE "richpresence" StreamingPreferences::StreamingPreferences(QObject *parent) : QObject(parent) @@ -55,6 +56,7 @@ void StreamingPreferences::reload() startWindowed = settings.value(SER_STARTWINDOWED, false).toBool(); framePacing = settings.value(SER_FRAMEPACING, false).toBool(); connectionWarnings = settings.value(SER_CONNWARNINGS, true).toBool(); + richPresence = settings.value(SER_RICHPRESENCE, true).toBool(); audioConfig = static_cast(settings.value(SER_AUDIOCFG, static_cast(AudioConfig::AC_STEREO)).toInt()); videoCodecConfig = static_cast(settings.value(SER_VIDEOCFG, @@ -86,6 +88,7 @@ void StreamingPreferences::save() settings.setValue(SER_STARTWINDOWED, startWindowed); settings.setValue(SER_FRAMEPACING, framePacing); settings.setValue(SER_CONNWARNINGS, connectionWarnings); + settings.setValue(SER_RICHPRESENCE, richPresence); settings.setValue(SER_AUDIOCFG, static_cast(audioConfig)); settings.setValue(SER_VIDEOCFG, static_cast(videoCodecConfig)); settings.setValue(SER_VIDEODEC, static_cast(videoDecoderSelection)); diff --git a/app/settings/streamingpreferences.h b/app/settings/streamingpreferences.h index 761cc42e..531ab3c0 100644 --- a/app/settings/streamingpreferences.h +++ b/app/settings/streamingpreferences.h @@ -64,6 +64,7 @@ public: Q_PROPERTY(bool startWindowed MEMBER startWindowed NOTIFY startWindowedChanged) Q_PROPERTY(bool framePacing MEMBER framePacing NOTIFY framePacingChanged) Q_PROPERTY(bool connectionWarnings MEMBER connectionWarnings NOTIFY connectionWarningsChanged) + Q_PROPERTY(bool richPresence MEMBER richPresence NOTIFY richPresenceChanged); Q_PROPERTY(AudioConfig audioConfig MEMBER audioConfig NOTIFY audioConfigChanged) Q_PROPERTY(VideoCodecConfig videoCodecConfig MEMBER videoCodecConfig NOTIFY videoCodecConfigChanged) Q_PROPERTY(VideoDecoderSelection videoDecoderSelection MEMBER videoDecoderSelection NOTIFY videoDecoderSelectionChanged) @@ -86,6 +87,7 @@ public: bool startWindowed; bool framePacing; bool connectionWarnings; + bool richPresence; AudioConfig audioConfig; VideoCodecConfig videoCodecConfig; VideoDecoderSelection videoDecoderSelection; @@ -110,5 +112,6 @@ signals: void startWindowedChanged(); void framePacingChanged(); void connectionWarningsChanged(); + void richPresenceChanged(); }; diff --git a/app/streaming/session.cpp b/app/streaming/session.cpp index 3738b1ee..48da8f1e 100644 --- a/app/streaming/session.cpp +++ b/app/streaming/session.cpp @@ -1,6 +1,7 @@ #include "session.h" #include "settings/streamingpreferences.h" #include "streaming/streamutils.h" +#include "backend/richpresencemanager.h" #include #include @@ -1047,6 +1048,9 @@ void Session::exec(int displayOriginX, int displayOriginY) // (m_UnexpectedTermination is set back to true). m_UnexpectedTermination = false; + // Start rich presence to indicate we're in game + RichPresenceManager presence(prefs, m_App.name); + // Hijack this thread to be the SDL main thread. We have to do this // because we want to suspend all Qt processing until the stream is over. SDL_Event event; @@ -1063,6 +1067,7 @@ void Session::exec(int displayOriginX, int displayOriginY) // ARM core in the Steam Link, so we will wait 10 ms instead. SDL_Delay(10); #endif + presence.runCallbacks(); continue; } switch (event.type) { diff --git a/libs/windows/include/discord_register.h b/libs/windows/include/discord_register.h new file mode 100644 index 00000000..16fb42f3 --- /dev/null +++ b/libs/windows/include/discord_register.h @@ -0,0 +1,26 @@ +#pragma once + +#if defined(DISCORD_DYNAMIC_LIB) +#if defined(_WIN32) +#if defined(DISCORD_BUILDING_SDK) +#define DISCORD_EXPORT __declspec(dllexport) +#else +#define DISCORD_EXPORT __declspec(dllimport) +#endif +#else +#define DISCORD_EXPORT __attribute__((visibility("default"))) +#endif +#else +#define DISCORD_EXPORT +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +DISCORD_EXPORT void Discord_Register(const char* applicationId, const char* command); +DISCORD_EXPORT void Discord_RegisterSteamGame(const char* applicationId, const char* steamId); + +#ifdef __cplusplus +} +#endif diff --git a/libs/windows/include/discord_rpc.h b/libs/windows/include/discord_rpc.h new file mode 100644 index 00000000..3e1441e0 --- /dev/null +++ b/libs/windows/include/discord_rpc.h @@ -0,0 +1,87 @@ +#pragma once +#include + +// clang-format off + +#if defined(DISCORD_DYNAMIC_LIB) +# if defined(_WIN32) +# if defined(DISCORD_BUILDING_SDK) +# define DISCORD_EXPORT __declspec(dllexport) +# else +# define DISCORD_EXPORT __declspec(dllimport) +# endif +# else +# define DISCORD_EXPORT __attribute__((visibility("default"))) +# endif +#else +# define DISCORD_EXPORT +#endif + +// clang-format on + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct DiscordRichPresence { + const char* state; /* max 128 bytes */ + const char* details; /* max 128 bytes */ + int64_t startTimestamp; + int64_t endTimestamp; + const char* largeImageKey; /* max 32 bytes */ + const char* largeImageText; /* max 128 bytes */ + const char* smallImageKey; /* max 32 bytes */ + const char* smallImageText; /* max 128 bytes */ + const char* partyId; /* max 128 bytes */ + int partySize; + int partyMax; + const char* matchSecret; /* max 128 bytes */ + const char* joinSecret; /* max 128 bytes */ + const char* spectateSecret; /* max 128 bytes */ + int8_t instance; +} DiscordRichPresence; + +typedef struct DiscordUser { + const char* userId; + const char* username; + const char* discriminator; + const char* avatar; +} DiscordUser; + +typedef struct DiscordEventHandlers { + void (*ready)(const DiscordUser* request); + void (*disconnected)(int errorCode, const char* message); + void (*errored)(int errorCode, const char* message); + void (*joinGame)(const char* joinSecret); + void (*spectateGame)(const char* spectateSecret); + void (*joinRequest)(const DiscordUser* request); +} DiscordEventHandlers; + +#define DISCORD_REPLY_NO 0 +#define DISCORD_REPLY_YES 1 +#define DISCORD_REPLY_IGNORE 2 + +DISCORD_EXPORT void Discord_Initialize(const char* applicationId, + DiscordEventHandlers* handlers, + int autoRegister, + const char* optionalSteamId); +DISCORD_EXPORT void Discord_Shutdown(void); + +/* checks for incoming messages, dispatches callbacks */ +DISCORD_EXPORT void Discord_RunCallbacks(void); + +/* If you disable the lib starting its own io thread, you'll need to call this from your own */ +#ifdef DISCORD_DISABLE_IO_THREAD +DISCORD_EXPORT void Discord_UpdateConnection(void); +#endif + +DISCORD_EXPORT void Discord_UpdatePresence(const DiscordRichPresence* presence); +DISCORD_EXPORT void Discord_ClearPresence(void); + +DISCORD_EXPORT void Discord_Respond(const char* userid, /* DISCORD_REPLY_ */ int reply); + +DISCORD_EXPORT void Discord_UpdateHandlers(DiscordEventHandlers* handlers); + +#ifdef __cplusplus +} /* extern "C" */ +#endif diff --git a/libs/windows/lib/x64/discord-rpc.dll b/libs/windows/lib/x64/discord-rpc.dll new file mode 100644 index 00000000..8493c549 Binary files /dev/null and b/libs/windows/lib/x64/discord-rpc.dll differ diff --git a/libs/windows/lib/x64/discord-rpc.lib b/libs/windows/lib/x64/discord-rpc.lib new file mode 100644 index 00000000..fcd009d8 Binary files /dev/null and b/libs/windows/lib/x64/discord-rpc.lib differ diff --git a/libs/windows/lib/x86/discord-rpc.dll b/libs/windows/lib/x86/discord-rpc.dll new file mode 100644 index 00000000..88c7d0ce Binary files /dev/null and b/libs/windows/lib/x86/discord-rpc.dll differ diff --git a/libs/windows/lib/x86/discord-rpc.lib b/libs/windows/lib/x86/discord-rpc.lib new file mode 100644 index 00000000..d8b6689f Binary files /dev/null and b/libs/windows/lib/x86/discord-rpc.lib differ