461 lines
		
	
	
	
		
			14 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			461 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
 |