diff --git a/plugins/mpriscontrol/mpriscontrolplugin.cpp b/plugins/mpriscontrol/mpriscontrolplugin.cpp index cc6bd14f..87e4b55e 100644 --- a/plugins/mpriscontrol/mpriscontrolplugin.cpp +++ b/plugins/mpriscontrol/mpriscontrolplugin.cpp @@ -1,292 +1,339 @@ /** * Copyright 2013 Albert Vaca * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "mpriscontrolplugin.h" #include #include #include #include #include #include #include #include #include #include "mprisdbusinterface.h" #include "propertiesdbusinterface.h" K_PLUGIN_FACTORY_WITH_JSON( KdeConnectPluginFactory, "kdeconnect_mpriscontrol.json", registerPlugin< MprisControlPlugin >(); ) Q_LOGGING_CATEGORY(KDECONNECT_PLUGIN_MPRIS, "kdeconnect.plugin.mpris") + +MprisPlayer::MprisPlayer(const QString& serviceName, const QString& dbusObjectPath, const QDBusConnection& busConnection) + : m_serviceName(serviceName) + , m_propertiesInterface(new OrgFreedesktopDBusPropertiesInterface(serviceName, dbusObjectPath, busConnection)) + , m_mediaPlayer2PlayerInterface(new OrgMprisMediaPlayer2PlayerInterface(serviceName, dbusObjectPath, busConnection)) +{ + m_mediaPlayer2PlayerInterface->setTimeout(500); +} + + MprisControlPlugin::MprisControlPlugin(QObject* parent, const QVariantList& args) : KdeConnectPlugin(parent, args) , prevVolume(-1) { m_watcher = new QDBusServiceWatcher(QString(), QDBusConnection::sessionBus(), QDBusServiceWatcher::WatchForOwnerChange, this); // TODO: QDBusConnectionInterface::serviceOwnerChanged is deprecated, maybe query org.freedesktop.DBus directly? connect(QDBusConnection::sessionBus().interface(), &QDBusConnectionInterface::serviceOwnerChanged, this, &MprisControlPlugin::serviceOwnerChanged); //Add existing interfaces const QStringList services = QDBusConnection::sessionBus().interface()->registeredServiceNames().value(); for (const QString& service : services) { // The string doesn't matter, it just needs to be empty/non-empty serviceOwnerChanged(service, QLatin1String(""), QStringLiteral("1")); } } // Copied from the mpris2 dataengine in the plasma-workspace repository void MprisControlPlugin::serviceOwnerChanged(const QString& serviceName, const QString& oldOwner, const QString& newOwner) { if (!serviceName.startsWith(QLatin1String("org.mpris.MediaPlayer2."))) return; if (!oldOwner.isEmpty()) { qCDebug(KDECONNECT_PLUGIN_MPRIS) << "MPRIS service" << serviceName << "just went offline"; removePlayer(serviceName); } if (!newOwner.isEmpty()) { qCDebug(KDECONNECT_PLUGIN_MPRIS) << "MPRIS service" << serviceName << "just came online"; addPlayer(serviceName); } } void MprisControlPlugin::addPlayer(const QString& service) { - QDBusInterface mprisInterface(service, QStringLiteral("/org/mpris/MediaPlayer2"), QStringLiteral("org.mpris.MediaPlayer2")); + const QString mediaPlayerObjectPath = QStringLiteral("/org/mpris/MediaPlayer2"); + + // estimate identifier string + QDBusInterface mprisInterface(service, mediaPlayerObjectPath, QStringLiteral("org.mpris.MediaPlayer2")); //FIXME: This call hangs and returns an empty string if KDED is still starting! QString identity = mprisInterface.property("Identity").toString(); if (identity.isEmpty()) { identity = service.mid(sizeof("org.mpris.MediaPlayer2")); } QString uniqueName = identity; - for (int i = 1 ; !playerList[uniqueName].isEmpty() ; i++) { + for (int i = 1; playerList.contains(uniqueName); ++i) { uniqueName = identity + " [" + i + "]"; } - playerList[uniqueName] = service; - qCDebug(KDECONNECT_PLUGIN_MPRIS) << "Mpris addPlayer" << service << "->" << uniqueName; - sendPlayerList(); + MprisPlayer player(service, mediaPlayerObjectPath, QDBusConnection::sessionBus()); + + playerList.insert(uniqueName, player); - OrgFreedesktopDBusPropertiesInterface* freedesktopInterface = new OrgFreedesktopDBusPropertiesInterface(service, QStringLiteral("/org/mpris/MediaPlayer2"), QDBusConnection::sessionBus(), this); - connect(freedesktopInterface, &OrgFreedesktopDBusPropertiesInterface::PropertiesChanged, this, &MprisControlPlugin::propertiesChanged); + connect(player.propertiesInterface(), &OrgFreedesktopDBusPropertiesInterface::PropertiesChanged, + this, &MprisControlPlugin::propertiesChanged); + connect(player.mediaPlayer2PlayerInterface(), &OrgMprisMediaPlayer2PlayerInterface::Seeked, + this, &MprisControlPlugin::seeked); - OrgMprisMediaPlayer2PlayerInterface* mprisInterface0 = new OrgMprisMediaPlayer2PlayerInterface(service, QStringLiteral("/org/mpris/MediaPlayer2"), QDBusConnection::sessionBus()); - connect(mprisInterface0, &OrgMprisMediaPlayer2PlayerInterface::Seeked, this, &MprisControlPlugin::seeked); + qCDebug(KDECONNECT_PLUGIN_MPRIS) << "Mpris addPlayer" << service << "->" << uniqueName; + sendPlayerList(); } void MprisControlPlugin::seeked(qlonglong position){ //qCDebug(KDECONNECT_PLUGIN_MPRIS) << "Seeked in player"; - OrgFreedesktopDBusPropertiesInterface* interface = (OrgFreedesktopDBusPropertiesInterface*)sender(); - const QString& service = interface->service(); - const QString& player = playerList.key(service); + OrgMprisMediaPlayer2PlayerInterface* mediaPlayer2PlayerInterface = (OrgMprisMediaPlayer2PlayerInterface*)sender(); + const auto end = playerList.constEnd(); + const auto it = std::find_if(playerList.constBegin(), end, [mediaPlayer2PlayerInterface](const MprisPlayer& player) { + return (player.mediaPlayer2PlayerInterface() == mediaPlayer2PlayerInterface); + }); + if (it == end) { + qCWarning(KDECONNECT_PLUGIN_MPRIS) << "Seeked signal received for no longer tracked service" << mediaPlayer2PlayerInterface->service(); + return; + } + + const QString& playerName = it.key(); NetworkPacket np(PACKET_TYPE_MPRIS, { {"pos", position/1000}, //Send milis instead of nanos - {"player", player} + {"player", playerName} }); sendPacket(np); } void MprisControlPlugin::propertiesChanged(const QString& propertyInterface, const QVariantMap& properties) { Q_UNUSED(propertyInterface); + OrgFreedesktopDBusPropertiesInterface* propertiesInterface = (OrgFreedesktopDBusPropertiesInterface*)sender(); + const auto end = playerList.constEnd(); + const auto it = std::find_if(playerList.constBegin(), end, [propertiesInterface](const MprisPlayer& player) { + return (player.propertiesInterface() == propertiesInterface); + }); + if (it == end) { + qCWarning(KDECONNECT_PLUGIN_MPRIS) << "PropertiesChanged signal received for no longer tracked service" << propertiesInterface->service(); + return; + } + + OrgMprisMediaPlayer2PlayerInterface* const mediaPlayer2PlayerInterface = it.value().mediaPlayer2PlayerInterface(); + const QString& playerName = it.key(); + NetworkPacket np(PACKET_TYPE_MPRIS); bool somethingToSend = false; if (properties.contains(QStringLiteral("Volume"))) { int volume = (int) (properties[QStringLiteral("Volume")].toDouble()*100); if (volume != prevVolume) { np.set(QStringLiteral("volume"),volume); prevVolume = volume; somethingToSend = true; } } if (properties.contains(QStringLiteral("Metadata"))) { QDBusArgument bullshit = qvariant_cast(properties[QStringLiteral("Metadata")]); QVariantMap nowPlayingMap; bullshit >> nowPlayingMap; mprisPlayerMetadataToNetworkPacket(np, nowPlayingMap); somethingToSend = true; } if (properties.contains(QStringLiteral("PlaybackStatus"))) { bool playing = (properties[QStringLiteral("PlaybackStatus")].toString() == QLatin1String("Playing")); np.set(QStringLiteral("isPlaying"), playing); somethingToSend = true; } if (properties.contains(QStringLiteral("CanPause"))) { np.set(QStringLiteral("canPause"), properties[QStringLiteral("CanPause")].toBool()); somethingToSend = true; } if (properties.contains(QStringLiteral("CanPlay"))) { np.set(QStringLiteral("canPlay"), properties[QStringLiteral("CanPlay")].toBool()); somethingToSend = true; } if (properties.contains(QStringLiteral("CanGoNext"))) { np.set(QStringLiteral("canGoNext"), properties[QStringLiteral("CanGoNext")].toBool()); somethingToSend = true; } if (properties.contains(QStringLiteral("CanGoPrevious"))) { np.set(QStringLiteral("canGoPrevious"), properties[QStringLiteral("CanGoPrevious")].toBool()); somethingToSend = true; } if (properties.contains(QStringLiteral("CanSeek"))) { np.set(QStringLiteral("canSeek"), properties[QStringLiteral("CanSeek")].toBool()); somethingToSend = true; } if (somethingToSend) { - OrgFreedesktopDBusPropertiesInterface* interface = (OrgFreedesktopDBusPropertiesInterface*)sender(); - const QString& service = interface->service(); - const QString& player = playerList.key(service); - np.set(QStringLiteral("player"), player); + np.set(QStringLiteral("player"), playerName); // Always also update the position - OrgMprisMediaPlayer2PlayerInterface mprisInterface(service, QStringLiteral("/org/mpris/MediaPlayer2"), QDBusConnection::sessionBus()); - if (mprisInterface.canSeek()) { - long long pos = mprisInterface.position(); + if (mediaPlayer2PlayerInterface->canSeek()) { + long long pos = mediaPlayer2PlayerInterface->position(); np.set(QStringLiteral("pos"), pos/1000); //Send milis instead of nanos } sendPacket(np); } } -void MprisControlPlugin::removePlayer(const QString& ifaceName) +void MprisControlPlugin::removePlayer(const QString& serviceName) { - const QString identity = playerList.key(ifaceName); - qCDebug(KDECONNECT_PLUGIN_MPRIS) << "Mpris removePlayer" << ifaceName << "->" << identity; - playerList.remove(identity); + const auto end = playerList.end(); + const auto it = std::find_if(playerList.begin(), end, [serviceName](const MprisPlayer& player) { + return (player.serviceName() == serviceName); + }); + if (it == end) { + qCWarning(KDECONNECT_PLUGIN_MPRIS) << "Could not find player for serviceName" << serviceName; + return; + } + + const QString& playerName = it.key(); + qCDebug(KDECONNECT_PLUGIN_MPRIS) << "Mpris removePlayer" << serviceName << "->" << playerName; + + playerList.erase(it); + sendPlayerList(); } bool MprisControlPlugin::receivePacket (const NetworkPacket& np) { if (np.has(QStringLiteral("playerList"))) { return false; //Whoever sent this is an mpris client and not an mpris control! } //Send the player list const QString player = np.get(QStringLiteral("player")); - bool valid_player = playerList.contains(player); + auto it = playerList.find(player); + bool valid_player = (it != playerList.end()); if (!valid_player || np.get(QStringLiteral("requestPlayerList"))) { sendPlayerList(); if (!valid_player) { return true; } } //Do something to the mpris interface - OrgMprisMediaPlayer2PlayerInterface mprisInterface(playerList[player], QStringLiteral("/org/mpris/MediaPlayer2"), QDBusConnection::sessionBus()); - mprisInterface.setTimeout(500); + const QString& serviceName = it.value().serviceName(); + // turn from pointer to reference to keep the patch diff small, + // actual patch would change all "mprisInterface." into "mprisInterface->" + auto& mprisInterface = *it.value().mediaPlayer2PlayerInterface(); if (np.has(QStringLiteral("action"))) { const QString& action = np.get(QStringLiteral("action")); - //qCDebug(KDECONNECT_PLUGIN_MPRIS) << "Calling action" << action << "in" << playerList[player]; + //qCDebug(KDECONNECT_PLUGIN_MPRIS) << "Calling action" << action << "in" << serviceName; //TODO: Check for valid actions, currently we trust anything the other end sends us mprisInterface.call(action); } if (np.has(QStringLiteral("setVolume"))) { double volume = np.get(QStringLiteral("setVolume"))/100.f; - qCDebug(KDECONNECT_PLUGIN_MPRIS) << "Setting volume" << volume << "to" << playerList[player]; + qCDebug(KDECONNECT_PLUGIN_MPRIS) << "Setting volume" << volume << "to" << serviceName; mprisInterface.setVolume(volume); } if (np.has(QStringLiteral("Seek"))) { int offset = np.get(QStringLiteral("Seek")); - //qCDebug(KDECONNECT_PLUGIN_MPRIS) << "Seeking" << offset << "to" << playerList[player]; + //qCDebug(KDECONNECT_PLUGIN_MPRIS) << "Seeking" << offset << "to" << serviceName; mprisInterface.Seek(offset); } if (np.has(QStringLiteral("SetPosition"))){ qlonglong position = np.get(QStringLiteral("SetPosition"),0)*1000; qlonglong seek = position - mprisInterface.position(); - //qCDebug(KDECONNECT_PLUGIN_MPRIS) << "Setting position by seeking" << seek << "to" << playerList[player]; + //qCDebug(KDECONNECT_PLUGIN_MPRIS) << "Setting position by seeking" << seek << "to" << serviceName; mprisInterface.Seek(seek); } //Send something read from the mpris interface NetworkPacket answer(PACKET_TYPE_MPRIS); bool somethingToSend = false; if (np.get(QStringLiteral("requestNowPlaying"))) { QVariantMap nowPlayingMap = mprisInterface.metadata(); mprisPlayerMetadataToNetworkPacket(answer, nowPlayingMap); qlonglong pos = mprisInterface.position(); answer.set(QStringLiteral("pos"), pos/1000); bool playing = (mprisInterface.playbackStatus() == QLatin1String("Playing")); answer.set(QStringLiteral("isPlaying"), playing); answer.set(QStringLiteral("canPause"), mprisInterface.canPause()); answer.set(QStringLiteral("canPlay"), mprisInterface.canPlay()); answer.set(QStringLiteral("canGoNext"), mprisInterface.canGoNext()); answer.set(QStringLiteral("canGoPrevious"), mprisInterface.canGoPrevious()); answer.set(QStringLiteral("canSeek"), mprisInterface.canSeek()); somethingToSend = true; } if (np.get(QStringLiteral("requestVolume"))) { int volume = (int)(mprisInterface.volume() * 100); answer.set(QStringLiteral("volume"),volume); somethingToSend = true; } + if (somethingToSend) { answer.set(QStringLiteral("player"), player); sendPacket(answer); } return true; } void MprisControlPlugin::sendPlayerList() { NetworkPacket np(PACKET_TYPE_MPRIS); np.set(QStringLiteral("playerList"),playerList.keys()); sendPacket(np); } void MprisControlPlugin::mprisPlayerMetadataToNetworkPacket(NetworkPacket& np, const QVariantMap& nowPlayingMap) const { QString title = nowPlayingMap[QStringLiteral("xesam:title")].toString(); QString artist = nowPlayingMap[QStringLiteral("xesam:artist")].toString(); QString album = nowPlayingMap[QStringLiteral("xesam:album")].toString(); QString albumArtUrl = nowPlayingMap[QStringLiteral("mpris:artUrl")].toString(); QString nowPlaying = title; if (!artist.isEmpty()) { nowPlaying = artist + " - " + title; } np.set(QStringLiteral("title"), title); np.set(QStringLiteral("artist"), artist); np.set(QStringLiteral("album"), album); np.set(QStringLiteral("albumArtUrl"), albumArtUrl); np.set(QStringLiteral("nowPlaying"), nowPlaying); bool hasLength = false; long long length = nowPlayingMap[QStringLiteral("mpris:length")].toLongLong(&hasLength) / 1000; //nanoseconds to milliseconds if (!hasLength) { length = -1; } np.set(QStringLiteral("length"), length); } #include "mpriscontrolplugin.moc" diff --git a/plugins/mpriscontrol/mpriscontrolplugin.h b/plugins/mpriscontrol/mpriscontrolplugin.h index 94304c09..a2fed490 100644 --- a/plugins/mpriscontrol/mpriscontrolplugin.h +++ b/plugins/mpriscontrol/mpriscontrolplugin.h @@ -1,63 +1,86 @@ /** * Copyright 2013 Albert Vaca * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef MPRISCONTROLPLUGIN_H #define MPRISCONTROLPLUGIN_H #include #include #include #include +#include #include + +class OrgFreedesktopDBusPropertiesInterface; +class OrgMprisMediaPlayer2PlayerInterface; + +class MprisPlayer +{ +public: + MprisPlayer(const QString& serviceName, const QString& dbusObjectPath, const QDBusConnection& busConnection); + MprisPlayer() = delete; + +public: + const QString& serviceName() const { return m_serviceName; } + OrgFreedesktopDBusPropertiesInterface* propertiesInterface() const { return m_propertiesInterface.data(); } + OrgMprisMediaPlayer2PlayerInterface* mediaPlayer2PlayerInterface() const { return m_mediaPlayer2PlayerInterface.data(); } + +private: + QString m_serviceName; + QSharedPointer m_propertiesInterface; + QSharedPointer m_mediaPlayer2PlayerInterface; +}; + + #define PACKET_TYPE_MPRIS QStringLiteral("kdeconnect.mpris") Q_DECLARE_LOGGING_CATEGORY(KDECONNECT_PLUGIN_MPRIS) class MprisControlPlugin : public KdeConnectPlugin { Q_OBJECT public: explicit MprisControlPlugin(QObject* parent, const QVariantList& args); bool receivePacket(const NetworkPacket& np) override; void connected() override { } private Q_SLOTS: void propertiesChanged(const QString& propertyInterface, const QVariantMap& properties); void seeked(qlonglong); private: void serviceOwnerChanged(const QString& serviceName, const QString& oldOwner, const QString& newOwner); - void addPlayer(const QString& ifaceName); - void removePlayer(const QString& ifaceName); + void addPlayer(const QString& serviceName); + void removePlayer(const QString& serviceName); void sendPlayerList(); void mprisPlayerMetadataToNetworkPacket(NetworkPacket& np, const QVariantMap& nowPlayingMap) const; - QHash playerList; + QHash playerList; int prevVolume; QDBusServiceWatcher* m_watcher; }; #endif