Commit f7b7cdbf authored by Matthijs Tijink's avatar Matthijs Tijink Committed by Nicolas Fella
Browse files

Adds an MPRIS server for controlling other device's MPRIS services

By creating a DBus connection per player, we are able to support
multiple remote players.

The album art is not yet supported.
parent 0240c43b
Pipeline #16559 failed with stage
in 6 minutes and 23 seconds
......@@ -69,8 +69,8 @@ MprisControlPlugin::MprisControlPlugin(QObject* parent, const QVariantList& args
// 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 (!serviceName.startsWith(QStringLiteral("org.mpris.MediaPlayer2."))) return;
if (serviceName.startsWith(QStringLiteral("org.mpris.MediaPlayer2.kdeconnect."))) return;
if (!oldOwner.isEmpty()) {
qCDebug(KDECONNECT_PLUGIN_MPRIS) << "MPRIS service" << serviceName << "just went offline";
......
kdeconnect_add_plugin(kdeconnect_mprisremote JSON kdeconnect_mprisremote.json SOURCES mprisremoteplugin.cpp mprisremoteplayer.cpp)
kdeconnect_add_plugin(kdeconnect_mprisremote JSON kdeconnect_mprisremote.json SOURCES mprisremoteplugin.cpp mprisremoteplayer.cpp mprisremoteplayermediaplayer2.cpp mprisremoteplayermediaplayer2player.cpp)
target_link_libraries(kdeconnect_mprisremote
kdeconnectcore
......
......@@ -19,13 +19,19 @@
*/
#include "mprisremoteplayer.h"
#include "mprisremoteplugin.h"
#include "mprisremoteplayermediaplayer2.h"
#include "mprisremoteplayermediaplayer2player.h"
#include <QDateTime>
#include <networkpacket.h>
#include <QUuid>
MprisRemotePlayer::MprisRemotePlayer() :
m_playing(false)
MprisRemotePlayer::MprisRemotePlayer(QString id, MprisRemotePlugin *plugin) :
QObject(plugin)
, id(id)
, m_playing(false)
, m_canPlay(true)
, m_canPause(true)
, m_canGoPrevious(true)
......@@ -39,31 +45,91 @@ MprisRemotePlayer::MprisRemotePlayer() :
, m_artist()
, m_album()
, m_canSeek(false)
, m_dbusConnectionName(QStringLiteral("mpris_") + QUuid::createUuid().toString(QUuid::Id128))
, m_dbusConnection(QDBusConnection::connectToBus(QDBusConnection::SessionBus, m_dbusConnectionName))
{
//Expose this player on the newly created connection. This allows multiple mpris services in the same Qt process
new MprisRemotePlayerMediaPlayer2(this, plugin);
new MprisRemotePlayerMediaPlayer2Player(this, plugin);
m_dbusConnection.registerObject(QStringLiteral("/org/mpris/MediaPlayer2"), this);
//Make sure our service name is unique. Reuse the connection name for this.
m_dbusConnection.registerService(QStringLiteral("org.mpris.MediaPlayer2.kdeconnect.") + m_dbusConnectionName);
}
MprisRemotePlayer::~MprisRemotePlayer()
{
//Drop the DBus connection (it was only used for this class)
QDBusConnection::disconnectFromBus(m_dbusConnectionName);
}
void MprisRemotePlayer::parseNetworkPacket(const NetworkPacket& np)
{
m_nowPlaying = np.get<QString>(QStringLiteral("nowPlaying"), m_nowPlaying);
m_title = np.get<QString>(QStringLiteral("title"), m_title);
m_artist = np.get<QString>(QStringLiteral("artist"), m_artist);
m_album = np.get<QString>(QStringLiteral("album"), m_album);
m_volume = np.get<int>(QStringLiteral("volume"), m_volume);
m_length = np.get<int>(QStringLiteral("length"), m_length);
bool trackInfoHasChanged = false;
//Track properties
QString newNowPlaying = np.get<QString>(QStringLiteral("nowPlaying"), m_nowPlaying);
QString newTitle = np.get<QString>(QStringLiteral("title"), m_title);
QString newArtist = np.get<QString>(QStringLiteral("artist"), m_artist);
QString newAlbum = np.get<QString>(QStringLiteral("album"), m_album);
int newLength = np.get<int>(QStringLiteral("length"), m_length);
//Check if they changed
if (newNowPlaying != m_nowPlaying || newTitle != m_title || newArtist != m_artist || newAlbum != m_album || newLength != m_length) {
trackInfoHasChanged = true;
Q_EMIT trackInfoChanged();
}
//Set the new values
m_nowPlaying = newNowPlaying;
m_title = newTitle;
m_artist = newArtist;
m_album = newAlbum;
m_length = newLength;
//Check volume changes
int newVolume = np.get<int>(QStringLiteral("volume"), m_volume);
if (newVolume != m_volume) {
Q_EMIT volumeChanged();
}
m_volume = newVolume;
if (np.has(QStringLiteral("pos"))) {
m_lastPosition = np.get<int>(QStringLiteral("pos"), m_lastPosition);
//Check position
int newLastPosition = np.get<int>(QStringLiteral("pos"), m_lastPosition);
int positionDiff = qAbs(position() - newLastPosition);
m_lastPosition = newLastPosition;
m_lastPositionTime = QDateTime::currentMSecsSinceEpoch();
//Only consider it seeking if the position changed more than 1 second, and the track has not changed
if (qAbs(positionDiff) >= 1000 && !trackInfoHasChanged) {
Q_EMIT positionChanged();
}
}
//Check if we started/stopped playing
bool newPlaying = np.get<bool>(QStringLiteral("isPlaying"), m_playing);
if (newPlaying != m_playing) {
Q_EMIT playingChanged();
}
m_playing = newPlaying;
//Control properties
bool newCanSeek = np.get<bool>(QStringLiteral("canSeek"), m_canSeek);
bool newCanPlay = np.get<bool>(QStringLiteral("canPlay"), m_canPlay);
bool newCanPause = np.get<bool>(QStringLiteral("canPause"), m_canPause);
bool newCanGoPrevious = np.get<bool>(QStringLiteral("canGoPrevious"), m_canGoPrevious);
bool newCanGoNext = np.get<bool>(QStringLiteral("canGoNext"), m_canGoNext);
//Check if they changed
if (newCanSeek != m_canSeek || newCanPlay != m_canPlay || newCanPause != m_canPause || newCanGoPrevious != m_canGoPrevious || newCanGoNext != m_canGoNext) {
Q_EMIT controlsChanged();
}
m_playing = np.get<bool>(QStringLiteral("isPlaying"), m_playing);
m_canSeek = np.get<bool>(QStringLiteral("canSeek"), m_canSeek);
m_canPlay = np.get<bool>(QStringLiteral("canPlay"), m_canPlay);
m_canPause = np.get<bool>(QStringLiteral("canPause"), m_canPause);
m_canGoPrevious = np.get<bool>(QStringLiteral("canGoPrevious"), m_canGoPrevious);
m_canGoNext = np.get<bool>(QStringLiteral("canGoNext"), m_canGoNext);
//Set the new values
m_canSeek = newCanSeek;
m_canPlay = newCanPlay;
m_canPause = newCanPause;
m_canGoPrevious = newCanGoPrevious;
m_canGoNext = newCanGoNext;
}
long MprisRemotePlayer::position() const
......@@ -121,6 +187,10 @@ bool MprisRemotePlayer::canSeek() const
return m_canSeek;
}
QString MprisRemotePlayer::identity() const {
return id;
}
bool MprisRemotePlayer::canPlay() const {
return m_canPlay;
}
......@@ -136,3 +206,7 @@ bool MprisRemotePlayer::canGoPrevious() const {
bool MprisRemotePlayer::canGoNext() const {
return m_canGoNext;
}
QDBusConnection & MprisRemotePlayer::dbus() {
return m_dbusConnection;
}
......@@ -20,13 +20,16 @@
#pragma once
#include <QString>
#include <QDBusConnection>
class NetworkPacket;
class MprisRemotePlugin;
class MprisRemotePlayer {
class MprisRemotePlayer : public QObject {
Q_OBJECT
public:
explicit MprisRemotePlayer();
explicit MprisRemotePlayer(QString id, MprisRemotePlugin *plugin);
virtual ~MprisRemotePlayer();
void parseNetworkPacket(const NetworkPacket& np);
......@@ -39,13 +42,23 @@ public:
QString title() const;
QString artist() const;
QString album() const;
bool canSeek() const;
QString identity() const;
bool canSeek() const;
bool canPlay() const;
bool canPause() const;
bool canGoPrevious() const;
bool canGoNext() const;
QDBusConnection &dbus();
Q_SIGNALS:
void controlsChanged();
void trackInfoChanged();
void positionChanged();
void volumeChanged();
void playingChanged();
private:
QString id;
......@@ -63,4 +76,8 @@ private:
QString m_artist;
QString m_album;
bool m_canSeek;
//Use an unique connection for every player, otherwise we can't distinguish which mpris player is being controlled
QString m_dbusConnectionName;
QDBusConnection m_dbusConnection;
};
/**
* Copyright 2019 Matthijs Tijink <matthijstijink@gmail.com>
*
* 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 <https://www.gnu.org/licenses/>.
*/
#include "mprisremoteplayer.h"
#include "mprisremoteplayermediaplayer2.h"
#include "mprisremoteplugin.h"
MprisRemotePlayerMediaPlayer2::MprisRemotePlayerMediaPlayer2(MprisRemotePlayer* parent, const MprisRemotePlugin *plugin) :
QDBusAbstractAdaptor{parent}, m_parent{parent}, m_plugin{plugin} {}
MprisRemotePlayerMediaPlayer2::~MprisRemotePlayerMediaPlayer2() = default;
bool MprisRemotePlayerMediaPlayer2::CanQuit() const {
return false;
}
bool MprisRemotePlayerMediaPlayer2::CanRaise() const {
return false;
}
bool MprisRemotePlayerMediaPlayer2::HasTrackList() const {
return false;
}
QString MprisRemotePlayerMediaPlayer2::DesktopEntry() const {
//Allows controlling mpris from the KDE Connect application's taskbar entry.
return QStringLiteral("org.kde.kdeconnect.app");
}
QString MprisRemotePlayerMediaPlayer2::Identity() const {
return m_parent->identity() + QStringLiteral(" - ") + m_plugin->device()->name();
}
void MprisRemotePlayerMediaPlayer2::Quit() {}
void MprisRemotePlayerMediaPlayer2::Raise() {}
QStringList MprisRemotePlayerMediaPlayer2::SupportedUriSchemes() const {
return {};
}
QStringList MprisRemotePlayerMediaPlayer2::SupportedMimeTypes() const {
return {};
}
/**
* Copyright 2019 Matthijs Tijink <matthijstijink@gmail.com>
*
* 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 <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <QDBusAbstractAdaptor>
class MprisRemotePlayer;
class MprisRemotePlugin;
class MprisRemotePlayerMediaPlayer2 : public QDBusAbstractAdaptor {
Q_OBJECT
Q_CLASSINFO("D-Bus Interface", "org.mpris.MediaPlayer2")
Q_PROPERTY(bool CanQuit READ CanQuit CONSTANT)
Q_PROPERTY(bool CanRaise READ CanRaise CONSTANT)
Q_PROPERTY(bool HasTrackList READ HasTrackList CONSTANT)
Q_PROPERTY(QString DesktopEntry READ DesktopEntry CONSTANT)
Q_PROPERTY(QString Identity READ Identity CONSTANT)
Q_PROPERTY(QStringList SupportedUriSchemes READ SupportedUriSchemes CONSTANT)
Q_PROPERTY(QStringList SupportedMimeTypes READ SupportedMimeTypes CONSTANT)
public:
explicit MprisRemotePlayerMediaPlayer2(MprisRemotePlayer *parent, const MprisRemotePlugin *plugin);
~MprisRemotePlayerMediaPlayer2();
public Q_SLOTS:
void Raise();
void Quit();
public:
bool CanQuit() const;
bool CanRaise() const;
bool HasTrackList() const;
QString DesktopEntry() const;
QString Identity() const;
QStringList SupportedUriSchemes() const;
QStringList SupportedMimeTypes() const;
private:
const MprisRemotePlayer *m_parent;
const MprisRemotePlugin *m_plugin;
};
/**
* Copyright 2019 Matthijs Tijink <matthijstijink@gmail.com>
*
* 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 <https://www.gnu.org/licenses/>.
*/
#include "mprisremoteplayer.h"
#include "mprisremoteplayermediaplayer2player.h"
#include "mprisremoteplugin.h"
#include <QDBusMessage>
MprisRemotePlayerMediaPlayer2Player::MprisRemotePlayerMediaPlayer2Player(MprisRemotePlayer* parent, MprisRemotePlugin *plugin) :
QDBusAbstractAdaptor{parent}, m_parent{parent}, m_plugin{plugin},
m_controlsChanged{false}, m_trackInfoChanged{false}, m_positionChanged{false}, m_volumeChanged{false}, m_playingChanged{false} {
connect(m_parent, &MprisRemotePlayer::controlsChanged, this, &MprisRemotePlayerMediaPlayer2Player::controlsChanged);
connect(m_parent, &MprisRemotePlayer::trackInfoChanged, this, &MprisRemotePlayerMediaPlayer2Player::trackInfoChanged);
connect(m_parent, &MprisRemotePlayer::positionChanged, this, &MprisRemotePlayerMediaPlayer2Player::positionChanged);
connect(m_parent, &MprisRemotePlayer::volumeChanged, this, &MprisRemotePlayerMediaPlayer2Player::volumeChanged);
connect(m_parent, &MprisRemotePlayer::playingChanged, this, &MprisRemotePlayerMediaPlayer2Player::playingChanged);
}
MprisRemotePlayerMediaPlayer2Player::~MprisRemotePlayerMediaPlayer2Player() = default;
QString MprisRemotePlayerMediaPlayer2Player::PlaybackStatus() const {
if (m_parent->playing()) {
return QStringLiteral("Playing");
} else {
return QStringLiteral("Paused");
}
}
double MprisRemotePlayerMediaPlayer2Player::Rate() const {
return 1.0;
}
QVariantMap MprisRemotePlayerMediaPlayer2Player::Metadata() const {
QVariantMap metadata;
metadata[QStringLiteral("mpris:trackid")] = QVariant::fromValue<QDBusObjectPath>(QDBusObjectPath("/org/mpris/MediaPlayer2"));
if (m_parent->length() > 0) {
metadata[QStringLiteral("mpris:length")] = QVariant::fromValue<qlonglong>(m_parent->length() * qlonglong(1000));
}
if (!m_parent->title().isEmpty()) {
metadata[QStringLiteral("xesam:title")] = m_parent->title();
}
if (!m_parent->artist().isEmpty()) {
metadata[QStringLiteral("xesam:artist")] = m_parent->artist();
}
if (!m_parent->album().isEmpty()) {
metadata[QStringLiteral("xesam:album")] = m_parent->album();
}
return metadata;
}
double MprisRemotePlayerMediaPlayer2Player::Volume() const {
return m_parent->volume() / 100.0;
}
void MprisRemotePlayerMediaPlayer2Player::setVolume(double volume) const {
m_plugin->setPlayer(m_parent->identity());
m_plugin->setVolume(volume * 100.0 + 0.5);
}
qlonglong MprisRemotePlayerMediaPlayer2Player::Position() const {
return m_plugin->position() * qlonglong(1000);
}
double MprisRemotePlayerMediaPlayer2Player::MinimumRate() const {
return 1.0;
}
double MprisRemotePlayerMediaPlayer2Player::MaximumRate() const {
return 1.0;
}
bool MprisRemotePlayerMediaPlayer2Player::CanGoNext() const {
return m_parent->canGoNext();
}
bool MprisRemotePlayerMediaPlayer2Player::CanGoPrevious() const {
return m_parent->canGoPrevious();
}
bool MprisRemotePlayerMediaPlayer2Player::CanPlay() const {
return m_parent->canPlay();
}
bool MprisRemotePlayerMediaPlayer2Player::CanPause() const {
return m_parent->canPause();
}
bool MprisRemotePlayerMediaPlayer2Player::CanSeek() const {
return m_parent->canSeek();
}
bool MprisRemotePlayerMediaPlayer2Player::CanControl() const {
return true;
}
void MprisRemotePlayerMediaPlayer2Player::Next() {
m_plugin->setPlayer(m_parent->identity());
m_plugin->sendAction(QStringLiteral("Next"));
}
void MprisRemotePlayerMediaPlayer2Player::Previous() {
m_plugin->setPlayer(m_parent->identity());
m_plugin->sendAction(QStringLiteral("Previous"));
}
void MprisRemotePlayerMediaPlayer2Player::Pause() {
m_plugin->setPlayer(m_parent->identity());
m_plugin->sendAction(QStringLiteral("Pause"));
}
void MprisRemotePlayerMediaPlayer2Player::PlayPause() {
m_plugin->setPlayer(m_parent->identity());
m_plugin->sendAction(QStringLiteral("PlayPause"));
}
void MprisRemotePlayerMediaPlayer2Player::Stop() {
m_plugin->setPlayer(m_parent->identity());
m_plugin->sendAction(QStringLiteral("Stop"));
}
void MprisRemotePlayerMediaPlayer2Player::Play() {
m_plugin->setPlayer(m_parent->identity());
m_plugin->sendAction(QStringLiteral("Play"));
}
void MprisRemotePlayerMediaPlayer2Player::Seek(qlonglong Offset) {
m_plugin->setPlayer(m_parent->identity());
m_plugin->seek(Offset);
}
void MprisRemotePlayerMediaPlayer2Player::SetPosition(QDBusObjectPath TrackId, qlonglong Position) {
Q_UNUSED(TrackId)
m_plugin->setPlayer(m_parent->identity());
m_plugin->setPosition(Position / 1000);
}
void MprisRemotePlayerMediaPlayer2Player::OpenUri(QString Uri) {
Q_UNUSED(Uri)
}
void MprisRemotePlayerMediaPlayer2Player::controlsChanged() {
m_controlsChanged = true;
QMetaObject::invokeMethod(this, &MprisRemotePlayerMediaPlayer2Player::emitPropertiesChanged, Qt::QueuedConnection);
}
void MprisRemotePlayerMediaPlayer2Player::playingChanged() {
m_playingChanged = true;
QMetaObject::invokeMethod(this, &MprisRemotePlayerMediaPlayer2Player::emitPropertiesChanged, Qt::QueuedConnection);
}
void MprisRemotePlayerMediaPlayer2Player::positionChanged() {
m_positionChanged = true;
QMetaObject::invokeMethod(this, &MprisRemotePlayerMediaPlayer2Player::emitPropertiesChanged, Qt::QueuedConnection);
}
void MprisRemotePlayerMediaPlayer2Player::trackInfoChanged() {
m_trackInfoChanged = true;
QMetaObject::invokeMethod(this, &MprisRemotePlayerMediaPlayer2Player::emitPropertiesChanged, Qt::QueuedConnection);
}
void MprisRemotePlayerMediaPlayer2Player::volumeChanged() {
m_volumeChanged = true;
QMetaObject::invokeMethod(this, &MprisRemotePlayerMediaPlayer2Player::emitPropertiesChanged, Qt::QueuedConnection);
}
void MprisRemotePlayerMediaPlayer2Player::emitPropertiesChanged() {
//Always invoked "queued", so we can send all changes at once
//Check if things really changed (we might get called multiple times)
if (!m_controlsChanged && !m_trackInfoChanged && !m_positionChanged && !m_volumeChanged && !m_playingChanged) return;
//Qt doesn't automatically send the "org.freedesktop.DBus.Properties PropertiesChanged" signal, so do it manually
//With the current setup, it's hard to discover what properties changed. So just send all properties (not too large, usually)
QVariantMap properties;
if (m_trackInfoChanged) {
properties[QStringLiteral("Metadata")] = Metadata();
}
if (m_trackInfoChanged || m_positionChanged) {
properties[QStringLiteral("Position")] = Position();
}
if (m_controlsChanged) {
properties[QStringLiteral("CanGoNext")] = CanGoNext();
properties[QStringLiteral("CanGoPrevious")] = CanGoPrevious();
properties[QStringLiteral("CanPlay")] = CanPlay();
properties[QStringLiteral("CanPause")] = CanPause();
properties[QStringLiteral("CanSeek")] = CanSeek();
}
if (m_playingChanged) {
properties[QStringLiteral("PlaybackStatus")] = PlaybackStatus();
}
if (m_volumeChanged) {
properties[QStringLiteral("Volume")] = Volume();
}
QList<QVariant> args;
args.push_back(QVariant(QStringLiteral("org.mpris.MediaPlayer2.Player")));
args.push_back(properties);
args.push_back(QStringList{});
QDBusMessage message = QDBusMessage::createSignal(QStringLiteral("/org/mpris/MediaPlayer2"), QStringLiteral("org.freedesktop.DBus.Properties"), QStringLiteral("PropertiesChanged"));
message.setArguments(args);
//Send it over the correct DBus connection
m_parent->dbus().send(message);
if (m_positionChanged) {
Q_EMIT Seeked(Position());
}
m_controlsChanged = false;
m_trackInfoChanged = false;
m_playingChanged = false;
m_positionChanged = false;
m_volumeChanged = false;
}
/**
* Copyright 2019 Matthijs Tijink <matthijstijink@gmail.com>
*
* 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