diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 509cf93f..f49d21c8 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -319,6 +319,7 @@ jobs: build-essential \ gcc-10 \ g++-10 \ + libappindicator3-dev \ libavdevice-dev \ libcap-dev \ libcurl4-openssl-dev \ diff --git a/.gitmodules b/.gitmodules index c6b2af2c..a3e39729 100644 --- a/.gitmodules +++ b/.gitmodules @@ -46,3 +46,7 @@ path = third-party/nanors url = https://github.com/sleepybishop/nanors.git branch = master +[submodule "third-party/tray"] + path = third-party/tray + url = https://github.com/dmikushin/tray + branch = master diff --git a/CMakeLists.txt b/CMakeLists.txt index 53acbcd4..96a19d51 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -142,6 +142,9 @@ find_package(Boost COMPONENTS locale log filesystem program_options REQUIRED) list(APPEND SUNSHINE_COMPILE_OPTIONS -Wall -Wno-missing-braces -Wno-maybe-uninitialized -Wno-sign-compare) +# enable system tray, we will disable this later if we cannot find the required package config on linux +set(SUNSHINE_TRAY 1) + if(WIN32) enable_language(RC) set(CMAKE_RC_COMPILER windres) @@ -168,6 +171,7 @@ if(WIN32) src/platform/windows/display_vram.cpp src/platform/windows/display_ram.cpp src/platform/windows/audio.cpp + third-party/tray/tray_windows.c third-party/ViGEmClient/src/ViGEmClient.cpp third-party/ViGEmClient/include/ViGEm/Client.h third-party/ViGEmClient/include/ViGEm/Common.h @@ -207,6 +211,7 @@ elseif(APPLE) FIND_LIBRARY(APP_SERVICES_LIBRARY ApplicationServices ) FIND_LIBRARY(AV_FOUNDATION_LIBRARY AVFoundation ) + FIND_LIBRARY(COCOA Cocoa REQUIRED ) # tray icon FIND_LIBRARY(CORE_MEDIA_LIBRARY CoreMedia ) FIND_LIBRARY(CORE_VIDEO_LIBRARY CoreVideo ) FIND_LIBRARY(VIDEO_TOOLBOX_LIBRARY VideoToolbox ) @@ -214,6 +219,7 @@ elseif(APPLE) list(APPEND SUNSHINE_EXTERNAL_LIBRARIES ${APP_SERVICES_LIBRARY} ${AV_FOUNDATION_LIBRARY} + ${COCOA} ${CORE_MEDIA_LIBRARY} ${CORE_VIDEO_LIBRARY} ${VIDEO_TOOLBOX_LIBRARY} @@ -240,6 +246,7 @@ elseif(APPLE) src/platform/macos/publish.cpp third-party/TPCircularBuffer/TPCircularBuffer.c third-party/TPCircularBuffer/TPCircularBuffer.h + third-party/tray/tray_darwin.m ${APPLE_PLIST_FILE}) else() add_compile_definitions(SUNSHINE_PLATFORM="linux") @@ -248,6 +255,7 @@ else() option(SUNSHINE_ENABLE_X11 "Enable X11 grab if available" ON) option(SUNSHINE_ENABLE_WAYLAND "Enable building wayland specific code" ON) option(SUNSHINE_ENABLE_CUDA "Enable cuda specific code" ON) + option(SUNSHINE_ENABLE_TRAY "Enable tray icon" ON) if(${SUNSHINE_ENABLE_X11}) find_package(X11) @@ -413,10 +421,27 @@ ${CMAKE_BINARY_DIR}/generated-src/${filename}.h") src/platform/linux/wlgrab.cpp src/platform/linux/wayland.cpp) endif() - if(NOT ${X11_FOUND} AND NOT (${LIBDRM_FOUND} AND ${LIBCAP_FOUND}) AND NOT ${WAYLAND_FOUND} AND NOT ${}) + if(NOT ${X11_FOUND} AND NOT (${LIBDRM_FOUND} AND ${LIBCAP_FOUND}) AND NOT ${WAYLAND_FOUND}) message(FATAL_ERROR "Couldn't find either x11, wayland, cuda or (libdrm and libcap)") endif() + # tray icon + if(${SUNSHINE_ENABLE_TRAY}) + pkg_check_modules(APPINDICATOR appindicator3-0.1) + if(NOT APPINDICATOR_FOUND) + message(WARNING "Couldn't find appindicator, disabling tray icon") + set(SUNSHINE_TRAY 0) + else() + include_directories(${APPINDICATOR_INCLUDE_DIRS}) + link_directories(${APPINDICATOR_LIBRARY_DIRS}) + + list(APPEND PLATFORM_TARGET_FILES third-party/tray/tray_linux.c) + list(APPEND SUNSHINE_EXTERNAL_LIBRARIES ${APPINDICATOR_LIBRARIES}) + endif() + else() + set(SUNSHINE_TRAY 0) + endif() + list(APPEND PLATFORM_TARGET_FILES src/platform/linux/publish.cpp src/platform/linux/vaapi.h @@ -466,6 +491,7 @@ set(SUNSHINE_TARGET_FILES third-party/moonlight-common-c/src/Rtsp.h third-party/moonlight-common-c/src/RtspParser.c third-party/moonlight-common-c/src/Video.h + third-party/tray/tray.h src/upnp.cpp src/upnp.h src/cbs.cpp @@ -499,6 +525,8 @@ set(SUNSHINE_TARGET_FILES src/network.cpp src/network.h src/move_by_copy.h + src/system_tray.cpp + src/system_tray.h src/task_pool.h src/thread_pool.h src/thread_safe.h @@ -511,6 +539,8 @@ set_source_files_properties(src/upnp.cpp PROPERTIES COMPILE_FLAGS -Wno-pedantic) set_source_files_properties(third-party/nanors/rs.c PROPERTIES COMPILE_FLAGS "-include deps/obl/autoshim.h -ftree-vectorize") +list(APPEND SUNSHINE_DEFINITIONS SUNSHINE_TRAY=${SUNSHINE_TRAY}) + # Pre-compiled binaries if(WIN32) set(FFMPEG_PREPARED_BINARIES "${CMAKE_CURRENT_SOURCE_DIR}/third-party/ffmpeg-windows-x86_64") @@ -890,6 +920,18 @@ elseif(UNIX) pulseaudio-libs >= 10.0") # This should automatically figure out dependencies, doesn't work with the current config set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS OFF) + + if(${SUNSHINE_TRAY} STREQUAL 1) + install(FILES "${CMAKE_SOURCE_DIR}/sunshine.svg" + DESTINATION "/usr/share/icons") + + set(CPACK_DEBIAN_PACKAGE_DEPENDS "\ + ${CPACK_DEBIAN_PACKAGE_DEPENDS}, \ + libappindicator3-1") + set(CPACK_RPM_PACKAGE_REQUIRES "\ + ${CPACK_RPM_PACKAGE_REQUIRES}, \ + libappindicator-gtk3 >= 12.10.0") + endif() endif() endif() diff --git a/docker/fedora-36.dockerfile b/docker/fedora-36.dockerfile index e6d88b1b..33adf225 100644 --- a/docker/fedora-36.dockerfile +++ b/docker/fedora-36.dockerfile @@ -35,6 +35,7 @@ dnf -y install \ gcc-12.0.1* \ gcc-c++-12.0.1* \ git-2.39.2* \ + libappindicator-gtk3-devel-12.10.0* \ libcap-devel-2.48* \ libcurl-devel-7.82.0* \ libdrm-devel-2.4.110* \ diff --git a/docker/fedora-37.dockerfile b/docker/fedora-37.dockerfile index 54dfd8dd..2e1be80c 100644 --- a/docker/fedora-37.dockerfile +++ b/docker/fedora-37.dockerfile @@ -35,6 +35,7 @@ dnf -y install \ gcc-12.2.1* \ gcc-c++-12.2.1* \ git-2.39.2* \ + libappindicator-gtk3-devel-12.10.1* \ libcap-devel-2.48* \ libcurl-devel-7.85.0* \ libdrm-devel-2.4.112* \ diff --git a/docker/ubuntu-20.04.dockerfile b/docker/ubuntu-20.04.dockerfile index dbc37ecb..4de2c02c 100644 --- a/docker/ubuntu-20.04.dockerfile +++ b/docker/ubuntu-20.04.dockerfile @@ -34,6 +34,7 @@ apt-get install -y --no-install-recommends \ gcc-10=10.3.0* \ g++-10=10.3.0* \ git=1:2.25.1* \ + libappindicator3-dev=12.10.1* \ libavdevice-dev=7:4.2.* \ libboost-filesystem-dev=1.71.0* \ libboost-locale-dev=1.71.0* \ diff --git a/docker/ubuntu-22.04.dockerfile b/docker/ubuntu-22.04.dockerfile index ec13e684..e8a69361 100644 --- a/docker/ubuntu-22.04.dockerfile +++ b/docker/ubuntu-22.04.dockerfile @@ -33,6 +33,7 @@ apt-get install -y --no-install-recommends \ build-essential=12.9* \ cmake=3.22.1* \ git=1:2.34.1* \ + libappindicator3-dev=12.10.1* \ libavdevice-dev=7:4.4.* \ libboost-filesystem-dev=1.74.0* \ libboost-locale-dev=1.74.0* \ diff --git a/docs/source/building/linux.rst b/docs/source/building/linux.rst index bf095de7..2f629efb 100644 --- a/docs/source/building/linux.rst +++ b/docs/source/building/linux.rst @@ -59,6 +59,7 @@ Install Requirements gcc \ gcc-c++ \ intel-mediasdk-devel \ # x86_64 only + libappindicator-gtk3-devel \ libcap-devel \ libcurl-devel \ libdrm-devel \ @@ -94,6 +95,7 @@ Install Requirements build-essential \ cmake \ g++-10 \ + libappindicator3-dev \ libavdevice-dev \ libboost-filesystem-dev \ libboost-locale-dev \ @@ -142,6 +144,7 @@ Install Requirements sudo apt update && sudo apt install \ build-essential \ cmake \ + libappindicator3-dev \ libavdevice-dev \ libboost-filesystem-dev \ libboost-locale-dev \ diff --git a/docs/source/source/src.rst b/docs/source/source/src.rst index 22e2d50c..b5b79228 100644 --- a/docs/source/source/src.rst +++ b/docs/source/source/src.rst @@ -75,6 +75,7 @@ Code src/rtsp src/stream src/sync + src/system_tray src/task_pool src/thread_pool src/thread_safe diff --git a/docs/source/source/src/system_tray.rst b/docs/source/source/src/system_tray.rst new file mode 100644 index 00000000..3b69c246 --- /dev/null +++ b/docs/source/source/src/system_tray.rst @@ -0,0 +1,4 @@ +system_tray +=========== + +.. doxygenfile:: system_tray.h diff --git a/packaging/linux/aur/PKGBUILD b/packaging/linux/aur/PKGBUILD index 4c3c5a42..02eaf0b5 100644 --- a/packaging/linux/aur/PKGBUILD +++ b/packaging/linux/aur/PKGBUILD @@ -9,7 +9,7 @@ arch=('x86_64' 'aarch64') url=@PROJECT_HOMEPAGE_URL@ license=('GPL3') -depends=('avahi' 'boost-libs' 'curl' 'libevdev' 'libmfx' 'libpulse' 'libva' 'libvdpau' 'libx11' 'libxcb' 'libxfixes' 'libxrandr' 'libxtst' 'numactl' 'openssl' 'opus' 'udev') +depends=('avahi' 'boost-libs' 'curl' 'libappindicator-gtk3' 'libevdev' 'libmfx' 'libpulse' 'libva' 'libvdpau' 'libx11' 'libxcb' 'libxfixes' 'libxrandr' 'libxtst' 'numactl' 'openssl' 'opus' 'udev') makedepends=('boost' 'cmake' 'git' 'make' 'nodejs' 'npm') optdepends=('cuda: NvFBC capture support' 'libcap' diff --git a/src/main.cpp b/src/main.cpp index b02354fd..50099207 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -25,6 +25,7 @@ #include "platform/common.h" #include "process.h" #include "rtsp.h" +#include "system_tray.h" #include "thread_pool.h" #include "upnp.h" #include "version.h" @@ -301,6 +302,11 @@ int main(int argc, char *argv[]) { BOOST_LOG(info) << PROJECT_NAME << " version: " << PROJECT_VER << std::endl; task_pool.start(1); +#if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1 + // create tray thread and detach it + system_tray::run_tray(); +#endif + // Create signal handler after logging has been initialized auto shutdown_event = mail::man->event(mail::shutdown); on_signal(SIGINT, [&force_shutdown, shutdown_event]() { @@ -371,6 +377,11 @@ int main(int argc, char *argv[]) { task_pool.stop(); task_pool.join(); + // stop system tray +#if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1 + system_tray::end_tray(); +#endif + return 0; } diff --git a/src/main.h b/src/main.h index e159ae45..874c1fcf 100644 --- a/src/main.h +++ b/src/main.h @@ -30,6 +30,13 @@ extern boost::log::sources::severity_logger fatal; // functions int main(int argc, char *argv[]); void log_flush(); +void open_url(const std::string &url); +void tray_open_ui_cb(struct tray_menu *item); +void tray_donate_github_cb(struct tray_menu *item); +void tray_donate_mee6_cb(struct tray_menu *item); +void tray_donate_patreon_cb(struct tray_menu *item); +void tray_donate_paypal_cb(struct tray_menu *item); +void tray_quit_cb(struct tray_menu *item); void print_help(const char *name); std::string read_file(const char *path); int write_file(const char *path, const std::string_view &contents); diff --git a/src/system_tray.cpp b/src/system_tray.cpp new file mode 100644 index 00000000..a7eaab87 --- /dev/null +++ b/src/system_tray.cpp @@ -0,0 +1,184 @@ +/** + * @file system_tray.cpp + */ +// macros +#if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1 + +#if defined(_WIN32) || defined(_WIN64) +#define TRAY_ICON WEB_DIR "images/favicon.ico" +#include +#elif defined(__linux__) || defined(linux) || defined(__linux) +#define TRAY_ICON "sunshine" +#elif defined(__APPLE__) || defined(__MACH__) +#define TRAY_ICON WEB_DIR "images/logo-sunshine-16.png" +#include +#endif + +// standard includes +#include +#include + +// lib includes +#include "tray/tray.h" + +// local includes +#include "confighttp.h" +#include "main.h" + +// local includes +//#include "platform/common.h" +//#include "process.h" + +using namespace std::literals; + +// system_tray namespace +namespace system_tray { + +/** + * @brief Open a url in the default web browser. + * @param url The url to open. + */ +void open_url(const std::string &url) { +// if windows +#if defined(_WIN32) || defined(_WIN64) + ShellExecuteA(nullptr, "open", url.c_str(), nullptr, nullptr, SW_SHOWNORMAL); +#else // if unix + system(("open "s + url).c_str()); +#endif +} + +/** + * @brief Callback for opening the UI from the system tray. + * @param item The tray menu item. + */ +void tray_open_ui_cb(struct tray_menu *item) { + BOOST_LOG(info) << "Opening UI from system tray"sv; + + // create the url with the port + std::string url = "https://localhost:" + std::to_string(map_port(confighttp::PORT_HTTPS)); + + // open the url in the default web browser + open_url(url); +} + +/** + * @brief Callback for opening GitHub Sponsors from the system tray. + * @param item The tray menu item. + */ +void tray_donate_github_cb(struct tray_menu *item) { + open_url("https://github.com/sponsors/LizardByte"); +} + +/** + * @brief Callback for opening MEE6 donation from the system tray. + * @param item The tray menu item. + */ +void tray_donate_mee6_cb(struct tray_menu *item) { + open_url("https://mee6.xyz/m/804382334370578482"); +} + +/** + * @brief Callback for opening Patreon from the system tray. + * @param item The tray menu item. + */ +void tray_donate_patreon_cb(struct tray_menu *item) { + open_url("https://www.patreon.com/LizardByte"); +} + +/** + * @brief Callback for opening PayPal donation from the system tray. + * @param item The tray menu item. + */ +void tray_donate_paypal_cb(struct tray_menu *item) { + open_url("https://www.paypal.com/paypalme/ReenigneArcher"); +} + +/** + * @brief Callback for exiting Sunshine from the system tray. + * @param item The tray menu item. + */ +void tray_quit_cb(struct tray_menu *item) { + BOOST_LOG(info) << "Quiting from system tray"sv; + + std::raise(SIGINT); +} + +// Tray menu +static struct tray tray = { + .icon = TRAY_ICON, +#if defined(_WIN32) || defined(_WIN64) + .tooltip = const_cast("Sunshine"), // cast the string literal to a non-const char* pointer +#endif + .menu = + (struct tray_menu[]) { + // todo - use boost/locale to translate menu strings + { .text = "Open Sunshine", .cb = tray_open_ui_cb }, + { .text = "-" }, + { .text = "Donate", + .submenu = + (struct tray_menu[]) { + { .text = "GitHub Sponsors", .cb = tray_donate_github_cb }, + { .text = "MEE6", .cb = tray_donate_mee6_cb }, + { .text = "Patreon", .cb = tray_donate_patreon_cb }, + { .text = "PayPal", .cb = tray_donate_paypal_cb }, + { .text = nullptr } } }, + { .text = "-" }, + { .text = "Quit", .cb = tray_quit_cb }, + { .text = nullptr } }, +}; + + +/** + * @brief Create the system tray. + * @details This function has an endless loop, so it should be run in a separate thread. + * @return 1 if the system tray failed to create, otherwise 0 once the tray has been terminated. + */ +int system_tray() { + if(tray_init(&tray) < 0) { + BOOST_LOG(warning) << "Failed to create system tray"sv; + return 1; + } + else { + BOOST_LOG(info) << "System tray created"sv; + } + + while(tray_loop(1) == 0) { + BOOST_LOG(debug) << "System tray loop"sv; + } + + return 0; +} + +/** + * @brief Run the system tray with platform specific options. + * @note macOS requires that UI elements be created on the main thread, so the system tray is not implemented for macOS. + */ +void run_tray() { + // create the system tray +#if defined(__APPLE__) || defined(__MACH__) + // macOS requires that UI elements be created on the main thread + // creating tray using dispatch queue does not work, although the code doesn't actually throw any (visible) errors + + // dispatch_async(dispatch_get_main_queue(), ^{ + // system_tray(); + // }); + + BOOST_LOG(info) << "system_tray() is not yet implemented for this platform."sv; +#else // Windows, Linux + // create tray in separate thread + std::thread tray_thread(system_tray); + tray_thread.detach(); +#endif +} + +/** + * @brief Exit the system tray. + * @return 0 after exiting the system tray. + */ +int end_tray() { + tray_exit(); + return 0; +} + +} // namespace system_tray +#endif diff --git a/src/system_tray.h b/src/system_tray.h new file mode 100644 index 00000000..a760934e --- /dev/null +++ b/src/system_tray.h @@ -0,0 +1,23 @@ +/** +* @file system_tray.h +*/ +// macros +#if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1 + +// system_tray namespace +namespace system_tray { + +void open_url(const std::string &url); +void tray_open_ui_cb(struct tray_menu *item); +void tray_donate_github_cb(struct tray_menu *item); +void tray_donate_mee6_cb(struct tray_menu *item); +void tray_donate_patreon_cb(struct tray_menu *item); +void tray_donate_paypal_cb(struct tray_menu *item); +void tray_quit_cb(struct tray_menu *item); + +int system_tray(); +int run_tray(); +int end_tray(); + +} // namespace system_tray +#endif diff --git a/src_assets/common/assets/web/images/logo-sunshine-16.png b/src_assets/common/assets/web/images/logo-sunshine-16.png new file mode 100644 index 00000000..0f16ac8b Binary files /dev/null and b/src_assets/common/assets/web/images/logo-sunshine-16.png differ diff --git a/sunshine.svg b/sunshine.svg new file mode 100644 index 00000000..e8bd55b7 --- /dev/null +++ b/sunshine.svg @@ -0,0 +1,27 @@ + + + diff --git a/third-party/tray b/third-party/tray new file mode 160000 index 00000000..1c2fd286 --- /dev/null +++ b/third-party/tray @@ -0,0 +1 @@ +Subproject commit 1c2fd286a513cc7315ca5b59ae9a8a140e795c31