Compare commits

...

2 Commits

Author SHA1 Message Date
outfoxxed 0f11e1e810
service/mpris: finish mpris implementation 2024-05-21 04:07:40 -07:00
kossLAN d654afe19c
feat: mpris
refactor: removed unneeded files
2024-05-21 04:07:30 -07:00
14 changed files with 1101 additions and 20 deletions

View File

@ -19,6 +19,7 @@ option(HYPRLAND_GLOBAL_SHORTCUTS "Hyprland Global Shortcuts" ON)
option(HYPRLAND_FOCUS_GRAB "Hyprland Focus Grabbing" ON)
option(SERVICE_STATUS_NOTIFIER "StatusNotifierItem service" ON)
option(SERVICE_PIPEWIRE "PipeWire service" ON)
option(SERVICE_MPRIS "Mpris service" ON)
message(STATUS "Quickshell configuration")
message(STATUS " NVIDIA workarounds: ${NVIDIA_COMPAT}")
@ -32,6 +33,7 @@ endif ()
message(STATUS " Services")
message(STATUS " StatusNotifier: ${SERVICE_STATUS_NOTIFIER}")
message(STATUS " PipeWire: ${SERVICE_PIPEWIRE}")
message(STATUS " Mpris: ${SERVICE_MPRIS}")
message(STATUS " Hyprland: ${HYPRLAND}")
if (HYPRLAND)
message(STATUS " Focus Grabbing: ${HYPRLAND_FOCUS_GRAB}")
@ -87,7 +89,7 @@ if (WAYLAND)
list(APPEND QT_FPDEPS WaylandClient)
endif()
if (SERVICE_STATUS_NOTIFIER)
if (SERVICE_STATUS_NOTIFIER OR SERVICE_MPRIS)
set(DBUS ON)
endif()

View File

@ -188,9 +188,9 @@ public:
dbus::DBusPropertyGroup properties;
dbus::DBusProperty<quint32> version {this->properties, "Version"};
dbus::DBusProperty<QString> textDirection {this->properties, "TextDirection"};
dbus::DBusProperty<QString> textDirection {this->properties, "TextDirection", "", false};
dbus::DBusProperty<QString> status {this->properties, "Status"};
dbus::DBusProperty<QStringList> iconThemePath {this->properties, "IconThemePath"};
dbus::DBusProperty<QStringList> iconThemePath {this->properties, "IconThemePath", {}, false};
void prepareToShow(qint32 item, bool sendOpened);
void updateLayout(qint32 parent, qint32 depth);

View File

@ -112,6 +112,8 @@ void asyncReadPropertyInternal(
}
void AbstractDBusProperty::tryUpdate(const QVariant& variant) {
this->mExists = true;
auto error = this->read(variant);
if (error.isValid()) {
qCWarning(logDbusProperties).noquote()
@ -159,6 +161,44 @@ void AbstractDBusProperty::update() {
}
}
void AbstractDBusProperty::write() {
if (this->group == nullptr) {
qFatal(logDbusProperties) << "Tried to write dbus property" << this->name
<< "which is not attached to a group";
} else {
const QString propStr = this->toString();
if (this->group->interface == nullptr) {
qFatal(logDbusProperties).noquote()
<< "Tried to write property" << propStr << "of a disconnected interface";
}
qCDebug(logDbusProperties).noquote() << "Writing property" << propStr;
auto pendingCall = this->group->propertyInterface->Set(
this->group->interface->interface(),
this->name,
QDBusVariant(this->serialize())
);
auto* call = new QDBusPendingCallWatcher(pendingCall, this);
auto responseCallback = [propStr](QDBusPendingCallWatcher* call) {
const QDBusPendingReply<> reply = *call;
if (reply.isError()) {
qCWarning(logDbusProperties).noquote() << "Error writing property" << propStr;
qCWarning(logDbusProperties) << reply.error();
}
delete call;
};
QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback);
}
}
bool AbstractDBusProperty::exists() const { return this->mExists; }
QString AbstractDBusProperty::toString() const {
const QString group = this->group == nullptr ? "{ NO GROUP }" : this->group->toString();
return group + ':' + this->name;
@ -232,7 +272,7 @@ void DBusPropertyGroup::updateAllViaGetAll() {
} else {
qCDebug(logDbusProperties).noquote()
<< "Received GetAll property set for" << this->toString();
this->updatePropertySet(reply.value());
this->updatePropertySet(reply.value(), true);
}
delete call;
@ -242,7 +282,7 @@ void DBusPropertyGroup::updateAllViaGetAll() {
QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback);
}
void DBusPropertyGroup::updatePropertySet(const QVariantMap& properties) {
void DBusPropertyGroup::updatePropertySet(const QVariantMap& properties, bool complainMissing) {
for (const auto [name, value]: properties.asKeyValueRange()) {
auto prop = std::find_if(
this->properties.begin(),
@ -251,11 +291,21 @@ void DBusPropertyGroup::updatePropertySet(const QVariantMap& properties) {
);
if (prop == this->properties.end()) {
qCDebug(logDbusProperties) << "Ignoring untracked property update" << name << "for" << this;
qCDebug(logDbusProperties) << "Ignoring untracked property update" << name << "for"
<< this->toString();
} else {
(*prop)->tryUpdate(value);
}
}
if (complainMissing) {
for (const auto* prop: this->properties) {
if (prop->required && !properties.contains(prop->name)) {
qCWarning(logDbusProperties)
<< prop->name << "missing from property set for" << this->toString();
}
}
}
}
QString DBusPropertyGroup::toString() const {
@ -291,7 +341,7 @@ void DBusPropertyGroup::onPropertiesChanged(
}
}
this->updatePropertySet(changedProperties);
this->updatePropertySet(changedProperties, false);
}
} // namespace qs::dbus

View File

@ -79,22 +79,31 @@ class AbstractDBusProperty: public QObject {
Q_OBJECT;
public:
explicit AbstractDBusProperty(QString name, const QMetaType& type, QObject* parent = nullptr)
explicit AbstractDBusProperty(
QString name,
const QMetaType& type,
bool required,
QObject* parent = nullptr
)
: QObject(parent)
, name(std::move(name))
, type(type) {}
, type(type)
, required(required) {}
[[nodiscard]] bool exists() const;
[[nodiscard]] QString toString() const;
[[nodiscard]] virtual QString valueString() = 0;
public slots:
void update();
void write();
signals:
void changed();
protected:
virtual QDBusError read(const QVariant& variant) = 0;
virtual QVariant serialize() = 0;
private:
void tryUpdate(const QVariant& variant);
@ -103,6 +112,8 @@ private:
QString name;
QMetaType type;
bool required;
bool mExists = false;
friend class DBusPropertyGroup;
};
@ -133,7 +144,7 @@ private slots:
);
private:
void updatePropertySet(const QVariantMap& properties);
void updatePropertySet(const QVariantMap& properties, bool complainMissing);
DBusPropertiesInterface* propertyInterface = nullptr;
QDBusAbstractInterface* interface = nullptr;
@ -145,17 +156,23 @@ private:
template <typename T>
class DBusProperty: public AbstractDBusProperty {
public:
explicit DBusProperty(QString name, QObject* parent = nullptr, T value = T())
: AbstractDBusProperty(std::move(name), QMetaType::fromType<T>(), parent)
explicit DBusProperty(
QString name,
T value = T(),
bool required = true,
QObject* parent = nullptr
)
: AbstractDBusProperty(std::move(name), QMetaType::fromType<T>(), required, parent)
, value(std::move(value)) {}
explicit DBusProperty(
DBusPropertyGroup& group,
QString name,
QObject* parent = nullptr,
T value = T()
T value = T(),
bool required = true,
QObject* parent = nullptr
)
: DBusProperty(std::move(name), parent, std::move(value)) {
: DBusProperty(std::move(name), std::move(value), required, parent) {
group.attachProperty(this);
}
@ -165,7 +182,7 @@ public:
return str;
}
[[nodiscard]] T get() const { return this->value; }
[[nodiscard]] const T& get() const { return this->value; }
void set(T value) {
this->value = std::move(value);
@ -183,6 +200,8 @@ protected:
return result.error;
}
QVariant serialize() override { return QVariant::fromValue(this->value); }
private:
T value;

View File

@ -5,3 +5,7 @@ endif()
if (SERVICE_PIPEWIRE)
add_subdirectory(pipewire)
endif()
if (SERVICE_MPRIS)
add_subdirectory(mpris)
endif()

View File

@ -0,0 +1,39 @@
set_source_files_properties(org.mpris.MediaPlayer2.Player.xml PROPERTIES
CLASSNAME DBusMprisPlayer
NO_NAMESPACE TRUE
)
qt_add_dbus_interface(DBUS_INTERFACES
org.mpris.MediaPlayer2.Player.xml
dbus_player
)
set_source_files_properties(org.mpris.MediaPlayer2.xml PROPERTIES
CLASSNAME DBusMprisPlayerApp
NO_NAMESPACE TRUE
)
qt_add_dbus_interface(DBUS_INTERFACES
org.mpris.MediaPlayer2.xml
dbus_player_app
)
qt_add_library(quickshell-service-mpris STATIC
player.cpp
watcher.cpp
${DBUS_INTERFACES}
)
# dbus headers
target_include_directories(quickshell-service-mpris PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
qt_add_qml_module(quickshell-service-mpris
URI Quickshell.Services.Mpris
VERSION 0.1
)
target_link_libraries(quickshell-service-mpris PRIVATE ${QT_DEPS} quickshell-dbus)
target_link_libraries(quickshell PRIVATE quickshell-service-mprisplugin)
qs_pch(quickshell-service-mpris)
qs_pch(quickshell-service-mprisplugin)

View File

@ -0,0 +1,7 @@
name = "Quickshell.Services.Mpris"
description = "Mpris Service"
headers = [
"player.hpp",
"watcher.hpp",
]
-----

View File

@ -0,0 +1,24 @@
<node>
<interface name="org.mpris.MediaPlayer2.Player">
<method name="OpenUri">
<arg direction="in" type="s" name="Uri"/>
</method>
<method name="SetPosition">
<arg direction="in" type="o" name="TrackId"/>
<arg direction="in" type="x" name="Position"/>
</method>
<method name="Seek">
<arg direction="in" type="x" name="Offset"/>
</method>
<method name="PlayPause"/>
<method name="Next"/>
<method name="Previous"/>
<method name="Stop"/>
<method name="Play"/>
<method name="Pause"/>
<signal name="Seeked">
<arg type="x" name="Position"/>
</signal>
</interface>
</node>

View File

@ -0,0 +1,6 @@
<node>
<interface name="org.mpris.MediaPlayer2">
<method name="Raise"/>
<method name="Quit"/>
</interface>
</node>

View File

@ -0,0 +1,427 @@
#include "player.hpp"
#include <qcontainerfwd.h>
#include <qdatetime.h>
#include <qdbusconnection.h>
#include <qdbusextratypes.h>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qobject.h>
#include <qstring.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include "../../dbus/properties.hpp"
#include "dbus_player.h"
#include "dbus_player_app.h"
using namespace qs::dbus;
namespace qs::service::mpris {
Q_LOGGING_CATEGORY(logMprisPlayer, "quickshell.service.mp.player", QtWarningMsg);
QString MprisPlaybackState::toString(MprisPlaybackState::Enum status) {
switch (status) {
case MprisPlaybackState::Stopped: return "Stopped";
case MprisPlaybackState::Playing: return "Playing";
case MprisPlaybackState::Paused: return "Paused";
default: return "Unknown Status";
}
}
QString MprisLoopState::toString(MprisLoopState::Enum status) {
switch (status) {
case MprisLoopState::None: return "None";
case MprisLoopState::Track: return "Track";
case MprisLoopState::Playlist: return "Playlist";
default: return "Unknown Status";
}
}
MprisPlayer::MprisPlayer(const QString& address, QObject* parent): QObject(parent) {
this->app = new DBusMprisPlayerApp(
address,
"/org/mpris/MediaPlayer2",
QDBusConnection::sessionBus(),
this
);
this->player =
new DBusMprisPlayer(address, "/org/mpris/MediaPlayer2", QDBusConnection::sessionBus(), this);
if (!this->player->isValid() || !this->app->isValid()) {
qCWarning(logMprisPlayer) << "Cannot create MprisPlayer for" << address;
return;
}
// clang-format off
QObject::connect(&this->pCanQuit, &AbstractDBusProperty::changed, this, &MprisPlayer::canQuitChanged);
QObject::connect(&this->pCanRaise, &AbstractDBusProperty::changed, this, &MprisPlayer::canRaiseChanged);
QObject::connect(&this->pCanSetFullscreen, &AbstractDBusProperty::changed, this, &MprisPlayer::canSetFullscreenChanged);
QObject::connect(&this->pIdentity, &AbstractDBusProperty::changed, this, &MprisPlayer::identityChanged);
QObject::connect(&this->pFullscreen, &AbstractDBusProperty::changed, this, &MprisPlayer::fullscreenChanged);
QObject::connect(&this->pSupportedUriSchemes, &AbstractDBusProperty::changed, this, &MprisPlayer::supportedUriSchemesChanged);
QObject::connect(&this->pSupportedMimeTypes, &AbstractDBusProperty::changed, this, &MprisPlayer::supportedMimeTypesChanged);
QObject::connect(&this->pCanControl, &AbstractDBusProperty::changed, this, &MprisPlayer::canControlChanged);
QObject::connect(&this->pCanSeek, &AbstractDBusProperty::changed, this, &MprisPlayer::canSeekChanged);
QObject::connect(&this->pCanGoNext, &AbstractDBusProperty::changed, this, &MprisPlayer::canGoNextChanged);
QObject::connect(&this->pCanGoPrevious, &AbstractDBusProperty::changed, this, &MprisPlayer::canGoPreviousChanged);
QObject::connect(&this->pCanPlay, &AbstractDBusProperty::changed, this, &MprisPlayer::canPlayChanged);
QObject::connect(&this->pCanPause, &AbstractDBusProperty::changed, this, &MprisPlayer::canPauseChanged);
QObject::connect(&this->pPosition, &AbstractDBusProperty::changed, this, &MprisPlayer::onPositionChanged);
QObject::connect(this->player, &DBusMprisPlayer::Seeked, this, &MprisPlayer::onSeek);
QObject::connect(&this->pVolume, &AbstractDBusProperty::changed, this, &MprisPlayer::volumeChanged);
QObject::connect(&this->pMetadata, &AbstractDBusProperty::changed, this, &MprisPlayer::onMetadataChanged);
QObject::connect(&this->pPlaybackStatus, &AbstractDBusProperty::changed, this, &MprisPlayer::onPlaybackStatusChanged);
QObject::connect(&this->pLoopStatus, &AbstractDBusProperty::changed, this, &MprisPlayer::onLoopStatusChanged);
QObject::connect(&this->pRate, &AbstractDBusProperty::changed, this, &MprisPlayer::rateChanged);
QObject::connect(&this->pMinRate, &AbstractDBusProperty::changed, this, &MprisPlayer::minRateChanged);
QObject::connect(&this->pMaxRate, &AbstractDBusProperty::changed, this, &MprisPlayer::maxRateChanged);
QObject::connect(&this->pShuffle, &AbstractDBusProperty::changed, this, &MprisPlayer::shuffleChanged);
QObject::connect(&this->playerProperties, &DBusPropertyGroup::getAllFinished, this, &MprisPlayer::onGetAllFinished);
// Ensure user triggered position updates can update length.
QObject::connect(this, &MprisPlayer::positionChanged, this, &MprisPlayer::onExportedPositionChanged);
// clang-format on
this->appProperties.setInterface(this->app);
this->playerProperties.setInterface(this->player);
this->appProperties.updateAllViaGetAll();
this->playerProperties.updateAllViaGetAll();
}
void MprisPlayer::raise() {
if (!this->canRaise()) {
qWarning() << "Cannot call raise() on" << this << "because canRaise is false.";
return;
}
this->app->Raise();
}
void MprisPlayer::quit() {
if (!this->canQuit()) {
qWarning() << "Cannot call quit() on" << this << "because canQuit is false.";
return;
}
this->app->Quit();
}
void MprisPlayer::openUri(const QString& uri) { this->player->OpenUri(uri); }
void MprisPlayer::next() {
if (!this->canGoNext()) {
qWarning() << "Cannot call next() on" << this << "because canGoNext is false.";
return;
}
this->player->Next();
}
void MprisPlayer::previous() {
if (!this->canGoPrevious()) {
qWarning() << "Cannot call previous() on" << this << "because canGoPrevious is false.";
return;
}
this->player->Previous();
}
void MprisPlayer::seek(qreal offset) {
if (!this->canSeek()) {
qWarning() << "Cannot call seek() on" << this << "because canSeek is false.";
return;
}
auto target = static_cast<qlonglong>(offset * 1000) * 1000;
this->player->Seek(target);
}
bool MprisPlayer::isValid() const { return this->player->isValid(); }
QString MprisPlayer::address() const { return this->player->service(); }
bool MprisPlayer::canControl() const { return this->pCanControl.get(); }
bool MprisPlayer::canPlay() const { return this->canControl() && this->pCanPlay.get(); }
bool MprisPlayer::canPause() const { return this->canControl() && this->pCanPause.get(); }
bool MprisPlayer::canSeek() const { return this->canControl() && this->pCanSeek.get(); }
bool MprisPlayer::canGoNext() const { return this->canControl() && this->pCanGoNext.get(); }
bool MprisPlayer::canGoPrevious() const { return this->canControl() && this->pCanGoPrevious.get(); }
bool MprisPlayer::canQuit() const { return this->pCanQuit.get(); }
bool MprisPlayer::canRaise() const { return this->pCanRaise.get(); }
bool MprisPlayer::canSetFullscreen() const { return this->pCanSetFullscreen.get(); }
QString MprisPlayer::identity() const { return this->pIdentity.get(); }
qlonglong MprisPlayer::positionMs() const {
if (!this->positionSupported()) return 0; // unsupported
if (this->mPlaybackState == MprisPlaybackState::Stopped) return 0;
auto paused = this->mPlaybackState == MprisPlaybackState::Paused;
auto time = paused ? this->pausedTime : QDateTime::currentDateTime();
auto offset = time - this->lastPositionTimestamp;
auto rateMul = static_cast<qlonglong>(this->pRate.get() * 1000);
offset = (offset * rateMul) / 1000;
return (this->pPosition.get() / 1000) + offset.count();
}
qreal MprisPlayer::position() const {
if (!this->positionSupported()) return 0; // unsupported
if (this->mPlaybackState == MprisPlaybackState::Stopped) return 0;
return static_cast<qreal>(this->positionMs()) / 1000.0; // NOLINT
}
bool MprisPlayer::positionSupported() const { return this->pPosition.exists(); }
void MprisPlayer::setPosition(qreal position) {
if (this->pPosition.get() == -1) {
qWarning() << "Cannot set position of" << this << "because position is not supported.";
return;
}
if (!this->canSeek()) {
qWarning() << "Cannot set position of" << this << "because canSeek is false.";
return;
}
auto target = static_cast<qlonglong>(position * 1000) * 1000;
this->pPosition.set(target);
if (!this->mTrackId.isEmpty()) {
this->player->SetPosition(QDBusObjectPath(this->mTrackId), target);
return;
} else {
auto pos = this->positionMs() * 1000;
this->player->Seek(target - pos);
}
}
void MprisPlayer::onPositionChanged() {
const bool firstChange = !this->lastPositionTimestamp.isValid();
this->lastPositionTimestamp = QDateTime::currentDateTimeUtc();
emit this->positionChanged();
if (firstChange) emit this->positionSupportedChanged();
}
void MprisPlayer::onExportedPositionChanged() {
if (!this->lengthSupported()) emit this->lengthChanged();
}
void MprisPlayer::onSeek(qlonglong time) { this->pPosition.set(time); }
qreal MprisPlayer::length() const {
if (this->mLength == -1) {
return this->position(); // unsupported
} else {
return static_cast<qreal>(this->mLength / 1000) / 1000; // NOLINT
}
}
bool MprisPlayer::lengthSupported() const { return this->mLength != -1; }
qreal MprisPlayer::volume() const { return this->pVolume.get(); }
bool MprisPlayer::volumeSupported() const { return this->pVolume.exists(); }
void MprisPlayer::setVolume(qreal volume) {
if (!this->canControl()) {
qWarning() << "Cannot set volume of" << this << "because canControl is false.";
return;
}
if (!this->volumeSupported()) {
qWarning() << "Cannot set volume of" << this << "because volume is not supported.";
return;
}
this->pVolume.set(volume);
this->pVolume.write();
}
QVariantMap MprisPlayer::metadata() const { return this->pMetadata.get(); }
void MprisPlayer::onMetadataChanged() {
auto lengthVariant = this->pMetadata.get().value("mpris:length");
qlonglong length = -1;
if (lengthVariant.isValid() && lengthVariant.canConvert<qlonglong>()) {
length = lengthVariant.value<qlonglong>();
}
if (length != this->mLength) {
this->mLength = length;
emit this->lengthChanged();
}
auto trackidVariant = this->pMetadata.get().value("mpris:trackid");
if (trackidVariant.isValid() && trackidVariant.canConvert<QString>()) {
this->mTrackId = trackidVariant.value<QString>();
this->onSeek(0);
}
emit this->metadataChanged();
}
MprisPlaybackState::Enum MprisPlayer::playbackState() const { return this->mPlaybackState; }
void MprisPlayer::setPlaybackState(MprisPlaybackState::Enum playbackState) {
if (playbackState == this->mPlaybackState) return;
switch (playbackState) {
case MprisPlaybackState::Stopped:
if (!this->canControl()) {
qWarning() << "Cannot set playbackState of" << this
<< "to Stopped because canControl is false.";
return;
}
this->player->Stop();
break;
case MprisPlaybackState::Playing:
if (!this->canPlay()) {
qWarning() << "Cannot set playbackState of" << this << "to Playing because canPlay is false.";
return;
}
this->player->Play();
break;
case MprisPlaybackState::Paused:
if (!this->canPause()) {
qWarning() << "Cannot set playbackState of" << this << "to Paused because canPause is false.";
return;
}
this->player->Pause();
break;
default:
qWarning() << "Cannot set playbackState of" << this << "to unknown value" << playbackState;
return;
}
}
void MprisPlayer::onPlaybackStatusChanged() {
const auto& status = this->pPlaybackStatus.get();
if (status == "Playing") {
this->mPlaybackState = MprisPlaybackState::Playing;
} else if (status == "Paused") {
this->mPlaybackState = MprisPlaybackState::Paused;
} else if (status == "Stopped") {
this->mPlaybackState = MprisPlaybackState::Stopped;
} else {
this->mPlaybackState = MprisPlaybackState::Stopped;
qWarning() << "Received unexpected PlaybackStatus for" << this << status;
}
emit this->playbackStateChanged();
}
MprisLoopState::Enum MprisPlayer::loopState() const { return this->mLoopState; }
bool MprisPlayer::loopSupported() const { return this->pLoopStatus.exists(); }
void MprisPlayer::setLoopState(MprisLoopState::Enum loopState) {
if (!this->canControl()) {
qWarning() << "Cannot set loopState of" << this << "because canControl is false.";
return;
}
if (!this->loopSupported()) {
qWarning() << "Cannot set loopState of" << this << "because loop state is not supported.";
return;
}
if (loopState == this->mLoopState) return;
QString loopStatusStr;
switch (loopState) {
case MprisLoopState::None: loopStatusStr = "None"; break;
case MprisLoopState::Track: loopStatusStr = "Track"; break;
case MprisLoopState::Playlist: loopStatusStr = "Playlist"; break;
default:
qWarning() << "Cannot set loopState of" << this << "to unknown value" << loopState;
return;
}
this->pLoopStatus.set(loopStatusStr);
this->pLoopStatus.write();
}
void MprisPlayer::onLoopStatusChanged() {
const auto& status = this->pLoopStatus.get();
if (status == "None") {
this->mLoopState = MprisLoopState::None;
} else if (status == "Track") {
this->mLoopState = MprisLoopState::Track;
} else if (status == "Playlist") {
this->mLoopState = MprisLoopState::Playlist;
} else {
this->mLoopState = MprisLoopState::None;
qWarning() << "Received unexpected LoopStatus for" << this << status;
}
emit this->loopStateChanged();
}
qreal MprisPlayer::rate() const { return this->pRate.get(); }
qreal MprisPlayer::minRate() const { return this->pMinRate.get(); }
qreal MprisPlayer::maxRate() const { return this->pMaxRate.get(); }
void MprisPlayer::setRate(qreal rate) {
if (rate == this->pRate.get()) return;
if (rate < this->pMinRate.get() || rate > this->pMaxRate.get()) {
qWarning() << "Cannot set rate for" << this << "to" << rate
<< "which is outside of minRate and maxRate" << this->pMinRate.get()
<< this->pMaxRate.get();
return;
}
this->pRate.set(rate);
this->pRate.write();
}
bool MprisPlayer::shuffle() const { return this->pShuffle.get(); }
bool MprisPlayer::shuffleSupported() const { return this->pShuffle.exists(); }
void MprisPlayer::setShuffle(bool shuffle) {
if (!this->shuffleSupported()) {
qWarning() << "Cannot set shuffle for" << this << "because shuffle is not supported.";
return;
}
if (!this->canControl()) {
qWarning() << "Cannot set shuffle state of" << this << "because canControl is false.";
return;
}
this->pShuffle.set(shuffle);
this->pShuffle.write();
}
bool MprisPlayer::fullscreen() const { return this->pFullscreen.get(); }
void MprisPlayer::setFullscreen(bool fullscreen) {
if (!this->canSetFullscreen()) {
qWarning() << "Cannot set fullscreen for" << this << "because canSetFullscreen is false.";
return;
}
this->pFullscreen.set(fullscreen);
this->pFullscreen.write();
}
QList<QString> MprisPlayer::supportedUriSchemes() const { return this->pSupportedUriSchemes.get(); }
QList<QString> MprisPlayer::supportedMimeTypes() const { return this->pSupportedMimeTypes.get(); }
void MprisPlayer::onGetAllFinished() {
if (this->volumeSupported()) emit this->volumeSupportedChanged();
if (this->loopSupported()) emit this->loopSupportedChanged();
if (this->shuffleSupported()) emit this->shuffleSupportedChanged();
emit this->ready();
}
} // namespace qs::service::mpris

View File

@ -0,0 +1,324 @@
#pragma once
#include <qcontainerfwd.h>
#include <qobject.h>
#include <qqmlintegration.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include "../../core/doc.hpp"
#include "../../dbus/properties.hpp"
#include "dbus_player.h"
#include "dbus_player_app.h"
namespace qs::service::mpris {
class MprisPlaybackState: public QObject {
Q_OBJECT;
QML_ELEMENT;
QML_SINGLETON;
public:
enum Enum {
Stopped = 0,
Playing = 1,
Paused = 2,
};
Q_ENUM(Enum);
Q_INVOKABLE static QString toString(MprisPlaybackState::Enum status);
};
class MprisLoopState: public QObject {
Q_OBJECT;
QML_ELEMENT;
QML_SINGLETON;
public:
enum Enum {
None = 0,
Track = 1,
Playlist = 2,
};
Q_ENUM(Enum);
Q_INVOKABLE static QString toString(MprisLoopState::Enum status);
};
///! A media player exposed over MPRIS.
/// A media player exposed over MPRIS.
///
/// > [!WARNING] Support for various functionality and general compliance to
/// > the MPRIS specification varies wildly by player.
/// > Always check the associated `canXyz` and `xyzSupported` properties if available.
///
/// > [!INFO] The TrackList and Playlist interfaces were not implemented as we could not
/// > find any media players using them to test against.
class MprisPlayer: public QObject {
Q_OBJECT;
// clang-format off
Q_PROPERTY(bool canControl READ canControl NOTIFY canControlChanged);
Q_PROPERTY(bool canPlay READ canPlay NOTIFY canPlayChanged);
Q_PROPERTY(bool canPause READ canPause NOTIFY canPauseChanged);
Q_PROPERTY(bool canSeek READ canSeek NOTIFY canSeekChanged);
Q_PROPERTY(bool canGoNext READ canGoNext NOTIFY canGoNextChanged);
Q_PROPERTY(bool canGoPrevious READ canGoPrevious NOTIFY canGoPreviousChanged);
Q_PROPERTY(bool canQuit READ canQuit NOTIFY canQuitChanged);
Q_PROPERTY(bool canRaise READ canRaise NOTIFY canRaiseChanged);
Q_PROPERTY(bool canSetFullscreen READ canSetFullscreen NOTIFY canSetFullscreenChanged);
/// The human readable name of the media player.
Q_PROPERTY(QString identity READ identity NOTIFY identityChanged);
/// The current position in the playing track, as seconds, with millisecond precision,
/// or `0` if `positionSupported` is false.
///
/// May only be written to if `canSeek` and `positionSupported` are true.
///
/// > [!WARNING] To avoid excessive property updates wasting CPU while `position` is not
/// > actively monitored, `position` usually will not update reactively, unless a nonlinear
/// > change in position occurs, however reading it will always return the current position.
/// >
/// > If you want to actively monitor the position, the simplest way it to emit the `positionChanged`
/// > signal manually for the duration you are monitoring it, Using a [FrameAnimation] if you need
/// > the value to update smoothly, such as on a slider, or a [Timer] if not, as shown below.
/// >
/// > ```qml {filename="Using a FrameAnimation"}
/// > FrameAnimation {
/// > // only emit the signal when the position is actually changing.
/// > running: player.playbackState == MprisPlaybackState.Playing
/// > // emit the positionChanged signal every frame.
/// > onTriggered: player.positionChanged()
/// > }
/// > ```
/// >
/// > ```qml {filename="Using a Timer"}
/// > Timer {
/// > // only emit the signal when the position is actually changing.
/// > running: player.playbackState == MprisPlaybackState.Playing
/// > // Make sure the position updates at least once per second.
/// > interval: 1000
/// > repeat: true
/// > // emit the positionChanged signal every second.
/// > onTriggered: player.positionChanged()
/// > }
/// > ```
///
/// [FrameAnimation]: https://doc.qt.io/qt-6/qml-qtquick-frameanimation.html
/// [Timer]: https://doc.qt.io/qt-6/qml-qtqml-timer.html
Q_PROPERTY(qreal position READ position WRITE setPosition NOTIFY positionChanged);
Q_PROPERTY(bool positionSupported READ positionSupported NOTIFY positionSupportedChanged);
/// The length of the playing track, as seconds, with millisecond precision,
/// or the value of `position` if `lengthSupported` is false.
Q_PROPERTY(qreal length READ length NOTIFY lengthChanged);
Q_PROPERTY(bool lengthSupported READ lengthSupported NOTIFY lengthSupportedChanged);
/// The volume of the playing track from 0.0 to 1.0, or 1.0 if `volumeSupported` is false.
///
/// May only be written to if `canControl` and `volumeSupported` are true.
Q_PROPERTY(qreal volume READ volume WRITE setVolume NOTIFY volumeChanged);
Q_PROPERTY(bool volumeSupported READ volumeSupported NOTIFY volumeSupportedChanged);
/// Metadata of the current track.
///
/// A map of common properties is available [here](https://www.freedesktop.org/wiki/Specifications/mpris-spec/metadata).
/// Do not count on any of them actually being present.
Q_PROPERTY(QVariantMap metadata READ metadata NOTIFY metadataChanged);
/// The playback state of the media player.
///
/// - If `canPlay` is false, you cannot assign the `Playing` state.
/// - If `canPause` is false, you cannot assign the `Paused` state.
/// - If `canControl` is false, you cannot assign the `Stopped` state.
/// (or any of the others, though their repsective properties will also be false)
Q_PROPERTY(MprisPlaybackState::Enum playbackState READ playbackState WRITE setPlaybackState NOTIFY playbackStateChanged);
/// The loop state of the media player, or `None` if `loopSupported` is false.
///
/// May only be written to if `canControl` and `loopSupported` are true.
Q_PROPERTY(MprisLoopState::Enum loopState READ loopState WRITE setLoopState NOTIFY loopStateChanged);
Q_PROPERTY(bool loopSupported READ loopSupported NOTIFY loopSupportedChanged);
/// The speed the song is playing at, as a multiplier.
///
/// Only values between `minRate` and `maxRate` (inclusive) may be written to the property.
/// Additionally, It is recommended that you only write common values such as `0.25`, `0.5`, `1.0`, `2.0`
/// to the property, as media players are free to ignore the value, and are more likely to
/// accept common ones.
Q_PROPERTY(qreal rate READ rate WRITE setRate NOTIFY rateChanged);
Q_PROPERTY(qreal minRate READ minRate NOTIFY minRateChanged);
Q_PROPERTY(qreal maxRate READ maxRate NOTIFY maxRateChanged);
/// If the play queue is currently being shuffled, or false if `shuffleSupported` is false.
///
/// May only be written if `canControl` and `shuffleSupported` are true.
Q_PROPERTY(bool shuffle READ shuffle WRITE setShuffle NOTIFY shuffleChanged);
Q_PROPERTY(bool shuffleSupported READ shuffleSupported NOTIFY shuffleSupportedChanged);
/// If the player is currently shown in fullscreen.
///
/// May only be written to if `canSetFullscreen` is true.
Q_PROPERTY(bool fullscreen READ fullscreen WRITE setFullscreen NOTIFY fullscreenChanged);
/// Uri schemes supported by `openUri`.
Q_PROPERTY(QList<QString> supportedUriSchemes READ supportedUriSchemes NOTIFY supportedUriSchemesChanged);
/// Mime types supported by `openUri`.
Q_PROPERTY(QList<QString> supportedMimeTypes READ supportedMimeTypes NOTIFY supportedMimeTypesChanged);
// clang-format on
QML_ELEMENT;
QML_UNCREATABLE("MprisPlayers can only be acquired from Mpris");
public:
explicit MprisPlayer(const QString& address, QObject* parent = nullptr);
/// Bring the media player to the front of the window stack.
///
/// May only be called if `canRaise` is true.
Q_INVOKABLE void raise();
/// Quit the media player.
///
/// May only be called if `canQuit` is true.
Q_INVOKABLE void quit();
/// Open the given URI in the media player.
///
/// Many players will silently ignore this, especially if the uri
/// does not match `supportedUriSchemes` and `supportedMimeTypes`.
Q_INVOKABLE void openUri(const QString& uri);
/// Play the next song.
///
/// May only be called if `canGoNext` is true.
Q_INVOKABLE void next();
/// Play the previous song, or go back to the beginning of the current one.
///
/// May only be called if `canGoPrevious` is true.
Q_INVOKABLE void previous();
/// Change `position` by an offset.
///
/// Even if `positionSupported` is false and you cannot set `position`,
/// this function may work.
///
/// May only be called if `canSeek` is true.
Q_INVOKABLE void seek(qreal offset);
[[nodiscard]] bool isValid() const;
[[nodiscard]] QString address() const;
[[nodiscard]] bool canControl() const;
[[nodiscard]] bool canSeek() const;
[[nodiscard]] bool canGoNext() const;
[[nodiscard]] bool canGoPrevious() const;
[[nodiscard]] bool canPlay() const;
[[nodiscard]] bool canPause() const;
[[nodiscard]] bool canQuit() const;
[[nodiscard]] bool canRaise() const;
[[nodiscard]] bool canSetFullscreen() const;
[[nodiscard]] QString identity() const;
[[nodiscard]] qlonglong positionMs() const;
[[nodiscard]] qreal position() const;
[[nodiscard]] bool positionSupported() const;
void setPosition(qreal position);
[[nodiscard]] qreal length() const;
[[nodiscard]] bool lengthSupported() const;
[[nodiscard]] qreal volume() const;
[[nodiscard]] bool volumeSupported() const;
void setVolume(qreal volume);
[[nodiscard]] QVariantMap metadata() const;
[[nodiscard]] MprisPlaybackState::Enum playbackState() const;
void setPlaybackState(MprisPlaybackState::Enum playbackState);
[[nodiscard]] MprisLoopState::Enum loopState() const;
[[nodiscard]] bool loopSupported() const;
void setLoopState(MprisLoopState::Enum loopState);
[[nodiscard]] qreal rate() const;
[[nodiscard]] qreal minRate() const;
[[nodiscard]] qreal maxRate() const;
void setRate(qreal rate);
[[nodiscard]] bool shuffle() const;
[[nodiscard]] bool shuffleSupported() const;
void setShuffle(bool shuffle);
[[nodiscard]] bool fullscreen() const;
void setFullscreen(bool fullscreen);
[[nodiscard]] QList<QString> supportedUriSchemes() const;
[[nodiscard]] QList<QString> supportedMimeTypes() const;
signals:
QSDOC_HIDE void ready();
void canControlChanged();
void canPlayChanged();
void canPauseChanged();
void canSeekChanged();
void canGoNextChanged();
void canGoPreviousChanged();
void canQuitChanged();
void canRaiseChanged();
void canSetFullscreenChanged();
void identityChanged();
void positionChanged();
void positionSupportedChanged();
void lengthChanged();
void lengthSupportedChanged();
void volumeChanged();
void volumeSupportedChanged();
void metadataChanged();
void playbackStateChanged();
void loopStateChanged();
void loopSupportedChanged();
void rateChanged();
void minRateChanged();
void maxRateChanged();
void shuffleChanged();
void shuffleSupportedChanged();
void fullscreenChanged();
void supportedUriSchemesChanged();
void supportedMimeTypesChanged();
private slots:
void onGetAllFinished();
void onPositionChanged();
void onExportedPositionChanged();
void onSeek(qlonglong time);
void onMetadataChanged();
void onPlaybackStatusChanged();
void onLoopStatusChanged();
private:
// clang-format off
dbus::DBusPropertyGroup appProperties;
dbus::DBusProperty<QString> pIdentity {this->appProperties, "Identity"};
dbus::DBusProperty<bool> pCanQuit {this->appProperties, "CanQuit"};
dbus::DBusProperty<bool> pCanRaise {this->appProperties, "CanRaise"};
dbus::DBusProperty<bool> pFullscreen {this->appProperties, "Fullscreen", false, false};
dbus::DBusProperty<bool> pCanSetFullscreen {this->appProperties, "CanSetFullscreen", false, false};
dbus::DBusProperty<QList<QString>> pSupportedUriSchemes {this->appProperties, "SupportedUriSchemes"};
dbus::DBusProperty<QList<QString>> pSupportedMimeTypes {this->appProperties, "SupportedMimeTypes"};
dbus::DBusPropertyGroup playerProperties;
dbus::DBusProperty<bool> pCanControl {this->playerProperties, "CanControl"};
dbus::DBusProperty<bool> pCanPlay {this->playerProperties, "CanPlay"};
dbus::DBusProperty<bool> pCanPause {this->playerProperties, "CanPause"};
dbus::DBusProperty<bool> pCanSeek {this->playerProperties, "CanSeek"};
dbus::DBusProperty<bool> pCanGoNext {this->playerProperties, "CanGoNext"};
dbus::DBusProperty<bool> pCanGoPrevious {this->playerProperties, "CanGoPrevious"};
dbus::DBusProperty<qlonglong> pPosition {this->playerProperties, "Position", 0, false}; // "required"
dbus::DBusProperty<double> pVolume {this->playerProperties, "Volume", 1, false}; // "required"
dbus::DBusProperty<QVariantMap> pMetadata {this->playerProperties, "Metadata"};
dbus::DBusProperty<QString> pPlaybackStatus {this->playerProperties, "PlaybackStatus"};
dbus::DBusProperty<QString> pLoopStatus {this->playerProperties, "LoopStatus", "", false};
dbus::DBusProperty<double> pRate {this->playerProperties, "Rate", 1, false}; // "required"
dbus::DBusProperty<double> pMinRate {this->playerProperties, "MinimumRate", 1, false}; // "required"
dbus::DBusProperty<double> pMaxRate {this->playerProperties, "MaximumRate", 1, false}; // "required"
dbus::DBusProperty<bool> pShuffle {this->playerProperties, "Shuffle", false, false};
// clang-format on
MprisPlaybackState::Enum mPlaybackState = MprisPlaybackState::Stopped;
MprisLoopState::Enum mLoopState = MprisLoopState::None;
QDateTime lastPositionTimestamp;
QDateTime pausedTime;
qlonglong mLength = -1;
DBusMprisPlayerApp* app = nullptr;
DBusMprisPlayer* player = nullptr;
QString mTrackId;
};
} // namespace qs::service::mpris

View File

@ -0,0 +1,126 @@
#include "watcher.hpp"
#include <qcontainerfwd.h>
#include <qdbusconnection.h>
#include <qdbusconnectioninterface.h>
#include <qdbusservicewatcher.h>
#include <qlist.h>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qobject.h>
#include <qqmllist.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include "player.hpp"
namespace qs::service::mpris {
Q_LOGGING_CATEGORY(logMprisWatcher, "quickshell.service.mpris.watcher", QtWarningMsg);
MprisWatcher::MprisWatcher(QObject* parent): QObject(parent) {
qCDebug(logMprisWatcher) << "Starting MprisWatcher";
auto bus = QDBusConnection::sessionBus();
if (!bus.isConnected()) {
qCWarning(logMprisWatcher) << "Could not connect to DBus. Mpris service will not work.";
return;
}
// clang-format off
QObject::connect(&this->serviceWatcher, &QDBusServiceWatcher::serviceRegistered, this, &MprisWatcher::onServiceRegistered);
QObject::connect(&this->serviceWatcher, &QDBusServiceWatcher::serviceUnregistered, this, &MprisWatcher::onServiceUnregistered);
// clang-format on
this->serviceWatcher.setWatchMode(
QDBusServiceWatcher::WatchForUnregistration | QDBusServiceWatcher::WatchForRegistration
);
this->serviceWatcher.addWatchedService("org.mpris.MediaPlayer2*");
this->serviceWatcher.setConnection(bus);
this->registerExisting();
}
void MprisWatcher::registerExisting() {
const QStringList& list = QDBusConnection::sessionBus().interface()->registeredServiceNames();
for (const QString& service: list) {
if (service.startsWith("org.mpris.MediaPlayer2")) {
qCDebug(logMprisWatcher).noquote() << "Found Mpris service" << service;
this->registerPlayer(service);
}
}
}
void MprisWatcher::onServiceRegistered(const QString& service) {
if (service.startsWith("org.mpris.MediaPlayer2")) {
qCDebug(logMprisWatcher).noquote() << "Mpris service " << service << " registered.";
this->registerPlayer(service);
} else {
qCWarning(logMprisWatcher) << "Got a registration event for untracked service" << service;
}
}
void MprisWatcher::onServiceUnregistered(const QString& service) {
if (auto* player = this->mPlayers.value(service)) {
player->deleteLater();
this->mPlayers.remove(service);
qCDebug(logMprisWatcher) << "Unregistered MprisPlayer" << service;
} else {
qCWarning(logMprisWatcher) << "Got service unregister event for untracked service" << service;
}
}
void MprisWatcher::onPlayerReady() {
auto* player = qobject_cast<MprisPlayer*>(this->sender());
this->readyPlayers.push_back(player);
emit this->playersChanged();
}
void MprisWatcher::onPlayerDestroyed(QObject* object) {
auto* player = static_cast<MprisPlayer*>(object); // NOLINT
if (this->readyPlayers.removeOne(player)) {
emit this->playersChanged();
}
}
QQmlListProperty<MprisPlayer> MprisWatcher::players() {
return QQmlListProperty<MprisPlayer>(
this,
nullptr,
&MprisWatcher::playersCount,
&MprisWatcher::playerAt
);
}
qsizetype MprisWatcher::playersCount(QQmlListProperty<MprisPlayer>* property) {
return static_cast<MprisWatcher*>(property->object)->readyPlayers.count(); // NOLINT
}
MprisPlayer* MprisWatcher::playerAt(QQmlListProperty<MprisPlayer>* property, qsizetype index) {
return static_cast<MprisWatcher*>(property->object)->readyPlayers.at(index); // NOLINT
}
void MprisWatcher::registerPlayer(const QString& address) {
if (this->mPlayers.contains(address)) {
qCDebug(logMprisWatcher) << "Skipping duplicate registration of MprisPlayer" << address;
return;
}
auto* player = new MprisPlayer(address, this);
if (!player->isValid()) {
qCWarning(logMprisWatcher) << "Ignoring invalid MprisPlayer registration of" << address;
delete player;
return;
}
this->mPlayers.insert(address, player);
QObject::connect(player, &MprisPlayer::ready, this, &MprisWatcher::onPlayerReady);
QObject::connect(player, &QObject::destroyed, this, &MprisWatcher::onPlayerDestroyed);
qCDebug(logMprisWatcher) << "Registered MprisPlayer" << address;
}
} // namespace qs::service::mpris

View File

@ -0,0 +1,53 @@
#pragma once
#include <qdbuscontext.h>
#include <qdbusinterface.h>
#include <qdbusservicewatcher.h>
#include <qhash.h>
#include <qlist.h>
#include <qloggingcategory.h>
#include <qobject.h>
#include <qqmlintegration.h>
#include <qqmllist.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include "player.hpp"
namespace qs::service::mpris {
///! Provides access to MprisPlayers.
class MprisWatcher: public QObject {
Q_OBJECT;
QML_NAMED_ELEMENT(Mpris);
QML_SINGLETON;
/// All connected MPRIS players.
Q_PROPERTY(QQmlListProperty<MprisPlayer> players READ players NOTIFY playersChanged);
public:
explicit MprisWatcher(QObject* parent = nullptr);
[[nodiscard]] QQmlListProperty<MprisPlayer> players();
signals:
void playersChanged();
private slots:
void onServiceRegistered(const QString& service);
void onServiceUnregistered(const QString& service);
void onPlayerReady();
void onPlayerDestroyed(QObject* object);
private:
static qsizetype playersCount(QQmlListProperty<MprisPlayer>* property);
static MprisPlayer* playerAt(QQmlListProperty<MprisPlayer>* property, qsizetype index);
void registerExisting();
void registerPlayer(const QString& address);
QDBusServiceWatcher serviceWatcher;
QHash<QString, MprisPlayer*> mPlayers;
QList<MprisPlayer*> readyPlayers;
};
} // namespace qs::service::mpris

View File

@ -54,14 +54,14 @@ public:
dbus::DBusProperty<QString> status {this->properties, "Status"};
dbus::DBusProperty<QString> category {this->properties, "Category"};
dbus::DBusProperty<quint32> windowId {this->properties, "WindowId"};
dbus::DBusProperty<QString> iconThemePath {this->properties, "IconThemePath"};
dbus::DBusProperty<QString> iconName {this->properties, "IconName"};
dbus::DBusProperty<DBusSniIconPixmapList> iconPixmaps {this->properties, "IconPixmap"};
dbus::DBusProperty<QString> iconThemePath {this->properties, "IconThemePath", "", false};
dbus::DBusProperty<QString> iconName {this->properties, "IconName", "", false}; // IconPixmap may be set
dbus::DBusProperty<DBusSniIconPixmapList> iconPixmaps {this->properties, "IconPixmap", {}, false}; // IconName may be set
dbus::DBusProperty<QString> overlayIconName {this->properties, "OverlayIconName"};
dbus::DBusProperty<DBusSniIconPixmapList> overlayIconPixmaps {this->properties, "OverlayIconPixmap"};
dbus::DBusProperty<QString> attentionIconName {this->properties, "AttentionIconName"};
dbus::DBusProperty<DBusSniIconPixmapList> attentionIconPixmaps {this->properties, "AttentionIconPixmap"};
dbus::DBusProperty<QString> attentionMovieName {this->properties, "AttentionMovieName"};
dbus::DBusProperty<QString> attentionMovieName {this->properties, "AttentionMovieName", "", false};
dbus::DBusProperty<DBusSniTooltip> tooltip {this->properties, "ToolTip"};
dbus::DBusProperty<bool> isMenu {this->properties, "ItemIsMenu"};
dbus::DBusProperty<QDBusObjectPath> menuPath {this->properties, "Menu"};