From 85ac983a278ca447b2a2eec79011956120f80303 Mon Sep 17 00:00:00 2001 From: Ilya Fedin Date: Wed, 12 Aug 2020 19:10:55 +0400 Subject: [PATCH] Add MPRIS support --- Telegram/CMakeLists.txt | 4 + .../platform/linux/linux_mpris_support.cpp | 461 ++++++++++++++++++ .../platform/linux/linux_mpris_support.h | 24 + .../platform/linux/specific_linux.cpp | 24 +- 4 files changed, 508 insertions(+), 5 deletions(-) create mode 100644 Telegram/SourceFiles/platform/linux/linux_mpris_support.cpp create mode 100644 Telegram/SourceFiles/platform/linux/linux_mpris_support.h diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index 37b7de590..8a999813d 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -821,6 +821,8 @@ PRIVATE platform/linux/linux_gtk_integration.h platform/linux/linux_gtk_open_with_dialog.cpp platform/linux/linux_gtk_open_with_dialog.h + platform/linux/linux_mpris_support.cpp + platform/linux/linux_mpris_support.h platform/linux/linux_notification_service_watcher.cpp platform/linux/linux_notification_service_watcher.h platform/linux/linux_wayland_integration.cpp @@ -1113,6 +1115,8 @@ if (DESKTOP_APP_DISABLE_DBUS_INTEGRATION) remove_target_sources(Telegram ${src_loc} platform/linux/linux_gsd_media_keys.cpp platform/linux/linux_gsd_media_keys.h + platform/linux/linux_mpris_support.cpp + platform/linux/linux_mpris_support.h platform/linux/linux_notification_service_watcher.cpp platform/linux/linux_notification_service_watcher.h platform/linux/linux_xdp_file_dialog.cpp diff --git a/Telegram/SourceFiles/platform/linux/linux_mpris_support.cpp b/Telegram/SourceFiles/platform/linux/linux_mpris_support.cpp new file mode 100644 index 000000000..3a3d05a13 --- /dev/null +++ b/Telegram/SourceFiles/platform/linux/linux_mpris_support.cpp @@ -0,0 +1,461 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "platform/linux/linux_mpris_support.h" + +#include "base/platform/base_platform_info.h" +#include "base/platform/linux/base_linux_glibmm_helper.h" +#include "media/audio/media_audio.h" +#include "media/player/media_player_instance.h" +#include "data/data_document.h" +#include "core/sandbox.h" +#include "core/application.h" +#include "core/core_settings.h" +#include "mainwindow.h" +#include "app.h" + +#include + +#include +#include + +namespace Platform { +namespace internal { +namespace { + +constexpr auto kService = "org.mpris.MediaPlayer2.tdesktop"_cs; +constexpr auto kObjectPath = "/org/mpris/MediaPlayer2"_cs; +constexpr auto kFakeTrackPath = "/org/telegram/desktop/track/0"_cs; +constexpr auto kInterface = "org.mpris.MediaPlayer2"_cs; +constexpr auto kPlayerInterface = "org.mpris.MediaPlayer2.Player"_cs; +constexpr auto kPropertiesInterface = "org.freedesktop.DBus.Properties"_cs; +constexpr auto kSongType = AudioMsgId::Type::Song; + +constexpr auto kIntrospectionXML = R"INTROSPECTION( + + + + + + + + + + + + + + )INTROSPECTION"_cs; + +constexpr auto kPlayerIntrospectionXML = R"INTROSPECTION( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + )INTROSPECTION"_cs; + +auto CreateMetadata(const Media::Player::TrackState &state) { + std::map result; + + if (!Media::Player::IsStoppedOrStopping(state.state)) { + result["mpris:trackid"] = Glib::wrap(g_variant_new_object_path( + kFakeTrackPath.utf8().constData())); + result["mpris:length"] = Glib::Variant::create( + state.length * 1000); + + const auto audioData = state.id.audio(); + if (audioData) { + result["xesam:title"] = Glib::Variant< + Glib::ustring + >::create(audioData->filename().toStdString()); + + if (audioData->isSong()) { + const auto songData = audioData->song(); + if (!songData->performer.isEmpty()) { + result["xesam:artist"] = Glib::Variant< + std::vector + >::create({ songData->performer.toStdString() }); + } + if (!songData->performer.isEmpty()) { + result["xesam:title"] = Glib::Variant< + Glib::ustring + >::create(songData->title.toStdString()); + } + } + } + } + + return result; +} + +auto PlaybackStatus(Media::Player::State state) { + return (state == Media::Player::State::Playing) + ? "Playing" + : Media::Player::IsPausedOrPausing(state) + ? "Paused" + : "Stopped"; +} + +void HandleMethodCall( + const Glib::RefPtr &connection, + const Glib::ustring &sender, + const Glib::ustring &object_path, + const Glib::ustring &interface_name, + const Glib::ustring &method_name, + const Glib::VariantContainerBase ¶meters, + const Glib::RefPtr &invocation) { + Core::Sandbox::Instance().customEnterFromEventLoop([&] { + try { + auto parametersCopy = parameters; + + if (method_name == "Quit") { + App::quit(); + } else if (method_name == "Raise") { + App::wnd()->showFromTray(); + } else if (method_name == "Next") { + Media::Player::instance()->next(); + } else if (method_name == "Pause") { + Media::Player::instance()->pause(); + } else if (method_name == "Play") { + Media::Player::instance()->play(); + } else if (method_name == "PlayPause") { + Media::Player::instance()->playPause(); + } else if (method_name == "Previous") { + Media::Player::instance()->previous(); + } else if (method_name == "Seek") { + const auto offset = base::Platform::GlibVariantCast( + parametersCopy.get_child(0)); + + const auto state = Media::Player::instance()->getState( + kSongType); + + Media::Player::instance()->finishSeeking( + kSongType, + float64(state.position * 1000 + offset) + / (state.length * 1000)); + } else if (method_name == "SetPosition") { + const auto position = base::Platform::GlibVariantCast( + parametersCopy.get_child(1)); + + const auto state = Media::Player::instance()->getState( + kSongType); + + Media::Player::instance()->finishSeeking( + kSongType, + float64(position) / (state.length * 1000)); + } else if (method_name == "Stop") { + Media::Player::instance()->stop(); + } else { + return; + } + + invocation->return_value({}); + } catch (...) { + } + }); +} + +void HandleGetProperty( + Glib::VariantBase &property, + const Glib::RefPtr &connection, + const Glib::ustring &sender, + const Glib::ustring &object_path, + const Glib::ustring &interface_name, + const Glib::ustring &property_name) { + Core::Sandbox::Instance().customEnterFromEventLoop([&] { + if (property_name == "CanQuit") { + property = Glib::Variant::create(true); + } else if (property_name == "CanRaise") { + property = Glib::Variant::create(!IsWayland()); + } else if (property_name == "CanSetFullscreen") { + property = Glib::Variant::create(false); + } else if (property_name == "DesktopEntry") { + property = Glib::Variant::create( + QGuiApplication::desktopFileName().chopped(8).toStdString()); + } else if (property_name == "Fullscreen") { + property = Glib::Variant::create(false); + } else if (property_name == "HasTrackList") { + property = Glib::Variant::create(false); + } else if (property_name == "Identity") { + property = Glib::Variant::create( + std::string(AppName)); + } else if (property_name == "SupportedMimeTypes") { + property = Glib::Variant>::create({}); + } else if (property_name == "SupportedUriSchemes") { + property = Glib::Variant>::create({}); + } else if (property_name == "CanControl") { + property = Glib::Variant::create(true); + } else if (property_name == "CanGoNext") { + property = Glib::Variant::create(true); + } else if (property_name == "CanGoPrevious") { + property = Glib::Variant::create(true); + } else if (property_name == "CanPause") { + property = Glib::Variant::create(true); + } else if (property_name == "CanPlay") { + property = Glib::Variant::create(true); + } else if (property_name == "CanSeek") { + property = Glib::Variant::create(true); + } else if (property_name == "MaximumRate") { + property = Glib::Variant::create(1.0); + } else if (property_name == "Metadata") { + const auto state = Media::Player::instance()->getState( + kSongType); + + property = base::Platform::MakeGlibVariant( + CreateMetadata(state)); + } else if (property_name == "MinimumRate") { + property = Glib::Variant::create(1.0); + } else if (property_name == "PlaybackStatus") { + const auto state = Media::Player::instance()->getState( + kSongType); + + property = Glib::Variant::create( + PlaybackStatus(state.state)); + } else if (property_name == "Position") { + const auto state = Media::Player::instance()->getState( + kSongType); + + property = Glib::Variant::create(state.position * 1000); + } else if (property_name == "Rate") { + property = Glib::Variant::create(1.0); + } else if (property_name == "Volume") { + property = Glib::Variant::create( + Core::App().settings().songVolume()); + } + }); +} + +bool HandleSetProperty( + const Glib::RefPtr &connection, + const Glib::ustring &sender, + const Glib::ustring &object_path, + const Glib::ustring &interface_name, + const Glib::ustring &property_name, + const Glib::VariantBase &value) { + try { + if (property_name == "Fullscreen") { + } else if (property_name == "Rate") { + } else if (property_name == "Volume") { + Core::Sandbox::Instance().customEnterFromEventLoop([&] { + Core::App().settings().setSongVolume( + base::Platform::GlibVariantCast(value)); + }); + } else { + return false; + } + + return true; + } catch (...) { + } + + return false; +} + +const Gio::DBus::InterfaceVTable InterfaceVTable( + sigc::ptr_fun(&HandleMethodCall), + sigc::ptr_fun(&HandleGetProperty), + sigc::ptr_fun(&HandleSetProperty)); + +void PlayerPropertyChanged( + const Glib::ustring &name, + const Glib::VariantBase &value) { + try { + const auto connection = Gio::DBus::Connection::get_sync( + Gio::DBus::BusType::BUS_TYPE_SESSION); + + connection->emit_signal( + std::string(kObjectPath), + std::string(kPropertiesInterface), + "PropertiesChanged", + {}, + base::Platform::MakeGlibVariant(std::tuple{ + Glib::ustring(std::string(kPlayerInterface)), + std::map{ + { name, value }, + }, + std::vector{}, + })); + } catch (...) { + } +} + +void Seeked(long position) { + try { + const auto connection = Gio::DBus::Connection::get_sync( + Gio::DBus::BusType::BUS_TYPE_SESSION); + + connection->emit_signal( + std::string(kObjectPath), + std::string(kPlayerInterface), + "Seeked", + {}, + base::Platform::MakeGlibVariant(std::tuple{ + position, + })); + } catch (...) { + } +} + +} // namespace + +class MPRISSupport::Private { +public: + void updateTrackState(const Media::Player::TrackState &state); + + Glib::RefPtr dbusConnection; + Glib::RefPtr introspectionData; + Glib::RefPtr playerIntrospectionData; + + uint ownId = 0; + uint registerId = 0; + uint playerRegisterId = 0; + + std::map metadata; + Glib::ustring playbackStatus; + long position = 0; + + rpl::lifetime lifetime; +}; + +void MPRISSupport::Private::updateTrackState( + const Media::Player::TrackState &state) { + if (state.id.type() != kSongType) { + return; + } + + const auto currentMetadata = CreateMetadata(state); + const auto currentPosition = state.position * 1000; + const auto currentPlaybackStatus = PlaybackStatus(state.state); + + if (!ranges::equal(currentMetadata, metadata, [&]( + const auto &item1, + const auto &item2) { + return item1.first == item2.first + && item1.second.equal(item2.second); + })) { + metadata = currentMetadata; + PlayerPropertyChanged( + "Metadata", + Glib::Variant< + std::map + >::create(metadata)); + } + + if (currentPlaybackStatus != playbackStatus) { + playbackStatus = currentPlaybackStatus; + PlayerPropertyChanged( + "PlaybackStatus", + Glib::Variant::create(playbackStatus)); + } + + if (currentPosition != position) { + const auto positionDifference = position - currentPosition; + if (positionDifference > 1000000 || positionDifference < -1000000) { + Seeked(currentPosition); + } + + position = currentPosition; + } +} + +MPRISSupport::MPRISSupport() +: _private(std::make_unique()) { + try { + _private->introspectionData = Gio::DBus::NodeInfo::create_for_xml( + std::string(kIntrospectionXML)); + + _private->playerIntrospectionData = Gio::DBus::NodeInfo::create_for_xml( + std::string(kPlayerIntrospectionXML)); + + _private->ownId = Gio::DBus::own_name( + Gio::DBus::BusType::BUS_TYPE_SESSION, + std::string(kService)); + + _private->dbusConnection = Gio::DBus::Connection::get_sync( + Gio::DBus::BusType::BUS_TYPE_SESSION); + + _private->registerId = _private->dbusConnection->register_object( + std::string(kObjectPath), + _private->introspectionData->lookup_interface(), + InterfaceVTable); + + _private->playerRegisterId = _private->dbusConnection->register_object( + std::string(kObjectPath), + _private->playerIntrospectionData->lookup_interface(), + InterfaceVTable); + + _private->updateTrackState( + Media::Player::instance()->getState(kSongType)); + + Media::Player::instance()->updatedNotifier( + ) | rpl::start_with_next([=]( + const Media::Player::TrackState &state) { + _private->updateTrackState(state); + }, _private->lifetime); + + Core::App().settings().songVolumeChanges( + ) | rpl::start_with_next([=](float64 volume) { + PlayerPropertyChanged( + "Volume", + Glib::Variant::create(volume)); + }, _private->lifetime); + } catch (...) { + } +} + +MPRISSupport::~MPRISSupport() { + if (_private->dbusConnection) { + if (_private->playerRegisterId) { + _private->dbusConnection->unregister_object( + _private->playerRegisterId); + } + + if (_private->registerId) { + _private->dbusConnection->unregister_object( + _private->registerId); + } + } + + if (_private->ownId) { + Gio::DBus::unown_name(_private->ownId); + } +} + +} // namespace internal +} // namespace Platform diff --git a/Telegram/SourceFiles/platform/linux/linux_mpris_support.h b/Telegram/SourceFiles/platform/linux/linux_mpris_support.h new file mode 100644 index 000000000..850e35fbc --- /dev/null +++ b/Telegram/SourceFiles/platform/linux/linux_mpris_support.h @@ -0,0 +1,24 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +namespace Platform { +namespace internal { + +class MPRISSupport { +public: + MPRISSupport(); + ~MPRISSupport(); + +private: + class Private; + const std::unique_ptr _private; +}; + +} // namespace internal +} // namespace Platform diff --git a/Telegram/SourceFiles/platform/linux/specific_linux.cpp b/Telegram/SourceFiles/platform/linux/specific_linux.cpp index 277c14dac..f297918ea 100644 --- a/Telegram/SourceFiles/platform/linux/specific_linux.cpp +++ b/Telegram/SourceFiles/platform/linux/specific_linux.cpp @@ -27,6 +27,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #ifndef DESKTOP_APP_DISABLE_DBUS_INTEGRATION #include "base/platform/linux/base_linux_dbus_utilities.h" #include "platform/linux/linux_notification_service_watcher.h" +#include "platform/linux/linux_mpris_support.h" #include "platform/linux/linux_gsd_media_keys.h" #endif // !DESKTOP_APP_DISABLE_DBUS_INTEGRATION @@ -347,12 +348,25 @@ void SetGtkScaleFactor() { void SetWatchingMediaKeys(bool watching) { #ifndef DESKTOP_APP_DISABLE_DBUS_INTEGRATION - static std::unique_ptr Instance; + static std::unique_ptr MPRISInstance; + static std::unique_ptr GSDInstance; - if (watching && !Instance) { - Instance = std::make_unique(); - } else if (!watching && Instance) { - Instance = nullptr; + if (watching) { + if (!MPRISInstance) { + MPRISInstance = std::make_unique(); + } + + if (!GSDInstance) { + GSDInstance = std::make_unique(); + } + } else { + if (MPRISInstance) { + MPRISInstance = nullptr; + } + + if (GSDInstance) { + GSDInstance = nullptr; + } } #endif // !DESKTOP_APP_DISABLE_DBUS_INTEGRATION }