462 lines
14 KiB
C++
462 lines
14 KiB
C++
/*
|
|
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 <QtGui/QGuiApplication>
|
|
|
|
#include <glibmm.h>
|
|
#include <giomm.h>
|
|
|
|
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(<node>
|
|
<interface name='org.mpris.MediaPlayer2'>
|
|
<method name='Raise'/>
|
|
<method name='Quit'/>
|
|
<property name='CanQuit' type='b' access='read'/>
|
|
<property name='CanRaise' type='b' access='read'/>
|
|
<property name='HasTrackList' type='b' access='read'/>
|
|
<property name='Identity' type='s' access='read'/>
|
|
<property name='DesktopEntry' type='s' access='read'/>
|
|
<property name='SupportedUriSchemes' type='as' access='read'/>
|
|
<property name='SupportedMimeTypes' type='as' access='read'/>
|
|
<property name='Fullscreen' type='b' access='readwrite'/>
|
|
<property name='CanSetFullscreen' type='b' access='read'/>
|
|
</interface>
|
|
</node>)INTROSPECTION"_cs;
|
|
|
|
constexpr auto kPlayerIntrospectionXML = R"INTROSPECTION(<node>
|
|
<interface name='org.mpris.MediaPlayer2.Player'>
|
|
<method name='Next'/>
|
|
<method name='Previous'/>
|
|
<method name='Pause'/>
|
|
<method name='PlayPause'/>
|
|
<method name='Stop'/>
|
|
<method name='Play'/>
|
|
<method name='Seek'>
|
|
<arg direction='in' name='Offset' type='x'/>
|
|
</method>
|
|
<method name='SetPosition'>
|
|
<arg direction='in' name='TrackId' type='o'/>
|
|
<arg direction='in' name='Position' type='x'/>
|
|
</method>
|
|
<method name='OpenUri'>
|
|
<arg direction='in' name='Uri' type='s'/>
|
|
</method>
|
|
<signal name='Seeked'>
|
|
<arg name='Position' type='x'/>
|
|
</signal>
|
|
<property name='PlaybackStatus' type='s' access='read'/>
|
|
<property name='Rate' type='d' access='readwrite'/>
|
|
<property name='Metadata' type='a{sv}' access='read'>
|
|
<annotation name="org.qtproject.QtDBus.QtTypeName" value="QVariantMap"/>
|
|
</property>
|
|
<property name='Volume' type='d' access='readwrite'/>
|
|
<property name='Position' type='x' access='read'/>
|
|
<property name='MinimumRate' type='d' access='read'/>
|
|
<property name='MaximumRate' type='d' access='read'/>
|
|
<property name='CanGoNext' type='b' access='read'/>
|
|
<property name='CanGoPrevious' type='b' access='read'/>
|
|
<property name='CanPlay' type='b' access='read'/>
|
|
<property name='CanPause' type='b' access='read'/>
|
|
<property name='CanSeek' type='b' access='read'/>
|
|
<property name='CanControl' type='b' access='read'/>
|
|
</interface>
|
|
</node>)INTROSPECTION"_cs;
|
|
|
|
auto CreateMetadata(const Media::Player::TrackState &state) {
|
|
std::map<Glib::ustring, Glib::VariantBase> 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<long>::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<Glib::ustring>
|
|
>::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<Gio::DBus::Connection> &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<Gio::DBus::MethodInvocation> &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<long>(
|
|
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<long>(
|
|
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<Gio::DBus::Connection> &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<bool>::create(true);
|
|
} else if (property_name == "CanRaise") {
|
|
property = Glib::Variant<bool>::create(!IsWayland());
|
|
} else if (property_name == "CanSetFullscreen") {
|
|
property = Glib::Variant<bool>::create(false);
|
|
} else if (property_name == "DesktopEntry") {
|
|
property = Glib::Variant<Glib::ustring>::create(
|
|
QGuiApplication::desktopFileName().chopped(8).toStdString());
|
|
} else if (property_name == "Fullscreen") {
|
|
property = Glib::Variant<bool>::create(false);
|
|
} else if (property_name == "HasTrackList") {
|
|
property = Glib::Variant<bool>::create(false);
|
|
} else if (property_name == "Identity") {
|
|
property = Glib::Variant<Glib::ustring>::create(
|
|
std::string(AppName));
|
|
} else if (property_name == "SupportedMimeTypes") {
|
|
property = Glib::Variant<std::vector<Glib::ustring>>::create({});
|
|
} else if (property_name == "SupportedUriSchemes") {
|
|
property = Glib::Variant<std::vector<Glib::ustring>>::create({});
|
|
} else if (property_name == "CanControl") {
|
|
property = Glib::Variant<bool>::create(true);
|
|
} else if (property_name == "CanGoNext") {
|
|
property = Glib::Variant<bool>::create(true);
|
|
} else if (property_name == "CanGoPrevious") {
|
|
property = Glib::Variant<bool>::create(true);
|
|
} else if (property_name == "CanPause") {
|
|
property = Glib::Variant<bool>::create(true);
|
|
} else if (property_name == "CanPlay") {
|
|
property = Glib::Variant<bool>::create(true);
|
|
} else if (property_name == "CanSeek") {
|
|
property = Glib::Variant<bool>::create(true);
|
|
} else if (property_name == "MaximumRate") {
|
|
property = Glib::Variant<float64>::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<float64>::create(1.0);
|
|
} else if (property_name == "PlaybackStatus") {
|
|
const auto state = Media::Player::instance()->getState(
|
|
kSongType);
|
|
|
|
property = Glib::Variant<Glib::ustring>::create(
|
|
PlaybackStatus(state.state));
|
|
} else if (property_name == "Position") {
|
|
const auto state = Media::Player::instance()->getState(
|
|
kSongType);
|
|
|
|
property = Glib::Variant<long>::create(state.position * 1000);
|
|
} else if (property_name == "Rate") {
|
|
property = Glib::Variant<float64>::create(1.0);
|
|
} else if (property_name == "Volume") {
|
|
property = Glib::Variant<float64>::create(
|
|
Core::App().settings().songVolume());
|
|
}
|
|
});
|
|
}
|
|
|
|
bool HandleSetProperty(
|
|
const Glib::RefPtr<Gio::DBus::Connection> &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<float64>(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<Glib::ustring, Glib::VariantBase>{
|
|
{ name, value },
|
|
},
|
|
std::vector<Glib::ustring>{},
|
|
}));
|
|
} 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<Gio::DBus::Connection> dbusConnection;
|
|
Glib::RefPtr<Gio::DBus::NodeInfo> introspectionData;
|
|
Glib::RefPtr<Gio::DBus::NodeInfo> playerIntrospectionData;
|
|
|
|
uint ownId = 0;
|
|
uint registerId = 0;
|
|
uint playerRegisterId = 0;
|
|
|
|
std::map<Glib::ustring, Glib::VariantBase> 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<Glib::ustring, Glib::VariantBase>
|
|
>::create(metadata));
|
|
}
|
|
|
|
if (currentPlaybackStatus != playbackStatus) {
|
|
playbackStatus = currentPlaybackStatus;
|
|
PlayerPropertyChanged(
|
|
"PlaybackStatus",
|
|
Glib::Variant<Glib::ustring>::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<Private>()) {
|
|
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<float64>::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
|