/* * Copyright 2012 Alex Merry * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, * Boston, MA 02110-1301 USA */ #include "playercontainer.h" #include #include #include #define MPRIS2_PATH "/org/mpris/MediaPlayer2" #define POS_UPD_STRING "Position last updated (UTC)" #include #include #include #include #include static QVariant::Type expPropType(const QString& propName) { if (propName == QLatin1String("Identity")) return QVariant::String; else if (propName == QLatin1String("DesktopEntry")) return QVariant::String; else if (propName == QLatin1String("SupportedUriSchemes")) return QVariant::StringList; else if (propName == QLatin1String("SupportedMimeTypes")) return QVariant::StringList; else if (propName == QLatin1String("Fullscreen")) return QVariant::Bool; else if (propName == QLatin1String("PlaybackStatus")) return QVariant::String; else if (propName == QLatin1String("LoopStatus")) return QVariant::String; else if (propName == QLatin1String("Shuffle")) return QVariant::Bool; else if (propName == QLatin1String("Rate")) return QVariant::Double; else if (propName == QLatin1String("MinimumRate")) return QVariant::Double; else if (propName == QLatin1String("MaximumRate")) return QVariant::Double; else if (propName == QLatin1String("Volume")) return QVariant::Double; else if (propName == QLatin1String("Position")) return QVariant::LongLong; else if (propName == QLatin1String("Metadata")) return QVariant::Map; // we give out CanControl, as this may completely // change the UI of the widget else if (propName == QLatin1String("CanControl")) return QVariant::Bool; return QVariant::Invalid; } static PlayerContainer::Cap capFromName(const QString& capName) { if (capName == QLatin1String("CanQuit")) return PlayerContainer::CanQuit; else if (capName == QLatin1String("CanRaise")) return PlayerContainer::CanRaise; else if (capName == QLatin1String("CanSetFullscreen")) return PlayerContainer::CanSetFullscreen; else if (capName == QLatin1String("CanControl")) return PlayerContainer::CanControl; else if (capName == QLatin1String("CanPlay")) return PlayerContainer::CanPlay; else if (capName == QLatin1String("CanPause")) return PlayerContainer::CanPause; else if (capName == QLatin1String("CanSeek")) return PlayerContainer::CanSeek; else if (capName == QLatin1String("CanGoNext")) return PlayerContainer::CanGoNext; else if (capName == QLatin1String("CanGoPrevious")) return PlayerContainer::CanGoPrevious; return PlayerContainer::NoCaps; } PlayerContainer::PlayerContainer(const QString& busAddress, QObject* parent) : DataContainer(parent) , m_caps(NoCaps) , m_fetchesPending(0) , m_dbusAddress(busAddress) , m_currentRate(0.0) { Q_ASSERT(!busAddress.isEmpty()); Q_ASSERT(busAddress.startsWith(QLatin1String("org.mpris.MediaPlayer2."))); m_propsIface = new OrgFreedesktopDBusPropertiesInterface( busAddress, MPRIS2_PATH, QDBusConnection::sessionBus(), this); m_playerIface = new OrgMprisMediaPlayer2PlayerInterface( busAddress, MPRIS2_PATH, QDBusConnection::sessionBus(), this); m_rootIface = new OrgMprisMediaPlayer2Interface( busAddress, MPRIS2_PATH, QDBusConnection::sessionBus(), this); connect(m_propsIface, SIGNAL(PropertiesChanged(QString,QVariantMap,QStringList)), this, SLOT(propertiesChanged(QString,QVariantMap,QStringList))); connect(m_playerIface, SIGNAL(Seeked(qlonglong)), this, SLOT(seeked(qlonglong))); refresh(); } void PlayerContainer::refresh() { // despite these calls being async, we should never update values in the // wrong order (eg: a stale GetAll response overwriting a more recent value // from a PropertiesChanged signal) due to D-Bus message ordering guarantees. QDBusPendingCall async = m_propsIface->GetAll(OrgMprisMediaPlayer2Interface::staticInterfaceName()); QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(async, this); connect(watcher, SIGNAL(finished(QDBusPendingCallWatcher*)), this, SLOT(getPropsFinished(QDBusPendingCallWatcher*))); ++m_fetchesPending; async = m_propsIface->GetAll(OrgMprisMediaPlayer2PlayerInterface::staticInterfaceName()); watcher = new QDBusPendingCallWatcher(async, this); connect(watcher, SIGNAL(finished(QDBusPendingCallWatcher*)), this, SLOT(getPropsFinished(QDBusPendingCallWatcher*))); ++m_fetchesPending; } static bool decodeUri(QVariantMap &map, const QString& entry) { if (map.contains(entry)) { QString urlString = map.value(entry).toString(); QUrl url = QUrl::fromEncoded(urlString.toAscii()); if (!url.isValid()) { // try to be lenient url = QUrl(urlString); } if (url.isValid()) { map.insert(entry, QVariant(url)); return true; } else { map.remove(entry); return false; } } // count it as a success if it doesn't exist return true; } void PlayerContainer::copyProperty(const QString& propName, const QVariant& _value, QVariant::Type expType, UpdateType updType) { QVariant value = _value; // we protect our users from bogus values if (value.userType() == qMetaTypeId()) { if (expType == QVariant::Map) { QDBusArgument arg = value.value(); if (arg.currentType() != QDBusArgument::MapType) { kWarning() << m_dbusAddress << "exports" << propName << "with the wrong type; it should be D-Bus type \"a{sv}\""; return; } QVariantMap map; arg >> map; if (propName == QLatin1String("Metadata")) { if (!decodeUri(map, QLatin1String("mpris:artUrl"))) { kWarning() << m_dbusAddress << "has an invalid URL for the mpris:artUrl entry of the \"Metadata\" property"; } if (!decodeUri(map, QLatin1String("xesam:url"))) { kWarning() << m_dbusAddress << "has an invalid URL for the xesam:url entry of the \"Metadata\" property"; } } value = QVariant(map); } } if (value.type() != expType) { const char * gotTypeCh = QDBusMetaType::typeToSignature(value.userType()); QString gotType = gotTypeCh ? QString::fromAscii(gotTypeCh) : ""; const char * expTypeCh = QDBusMetaType::typeToSignature(expType); QString expType = expTypeCh ? QString::fromAscii(expTypeCh) : ""; kWarning() << m_dbusAddress << "exports" << propName << "as D-Bus type" << gotType << "but it should be D-Bus type" << expType; } if (value.convert(expType)) { if (propName == QLatin1String("Position")) { setData(POS_UPD_STRING, QDateTime::currentDateTimeUtc()); } else if (propName == QLatin1String("Metadata")) { if (updType == UpdatedSignal) { QDBusObjectPath oldTrackId = data().value("Metadata").toMap().value("mpris:trackid").value(); QDBusObjectPath newTrackId = value.toMap().value("mpris:trackid").value(); if (oldTrackId != newTrackId) { setData("Position", static_cast(0)); setData(POS_UPD_STRING, QDateTime::currentDateTimeUtc()); } } } else if (propName == QLatin1String("Rate") && data().value("PlaybackStatus").toString() == QLatin1String("Playing")) { if (data().contains("Position")) recalculatePosition(); m_currentRate = value.toDouble(); } else if (propName == QLatin1String("PlaybackStatus")) { if (data().contains("Position") && data().contains("PlaybackStatus")) { recalculatePosition(); if (data().value("PlaybackStatus").toString() == QLatin1String("Playing") && value.toString() == QLatin1String("Paused")) { // Every time something that affects the predicted position changes // (Rate or PlaybackStatus), we get a hundred milliseconds or so // out, due to the time it takes for a D-Bus message to be delivered // and processed. // // To try to prevent these errors from accumulating, we re-fetch // the position when we enter paused mode; when paused, the position // does not change, so we'll be about as accurate as we can be. updatePosition(); } } // update the effective rate if (data().contains("Rate")) { if (value.toString() == QLatin1String("Playing")) m_currentRate = data().value("Rate").toDouble(); else m_currentRate = 0.0; } if (value.toString() == QLatin1String("Stopped")) { // assume the position has reset to 0, since this is really the // only sensible value for a stopped track setData("Position", static_cast(0)); setData(POS_UPD_STRING, QDateTime::currentDateTimeUtc()); } } else if (propName == QLatin1String("DesktopEntry")) { QString filename = value.toString() + QLatin1String(".desktop"); KDesktopFile desktopFile("xdgdata-apps", filename); QString iconName = desktopFile.readIcon(); if (!iconName.isEmpty()) { setData("Desktop Icon Name", iconName); } } setData(propName, value); } } void PlayerContainer::updateFromMap(const QVariantMap& map, UpdateType updType) { QMap::const_iterator i = map.constBegin(); while (i != map.constEnd()) { QVariant::Type type = expPropType(i.key()); if (type != QVariant::Invalid) { copyProperty(i.key(), i.value(), type, updType); } Cap cap = capFromName(i.key()); if (cap != NoCaps) { if (i.value().type() == QVariant::Bool) { if (i.value().toBool()) { m_caps |= cap; } else { m_caps &= ~cap; } } else { const char * gotTypeCh = QDBusMetaType::typeToSignature(i.value().userType()); QString gotType = gotTypeCh ? QString::fromAscii(gotTypeCh) : ""; kWarning() << m_dbusAddress << "exports" << i.key() << "as D-Bus type" << gotType << "but it should be D-Bus type \"b\""; } } // fake the CanStop capability if (cap == CanControl || i.key() == QLatin1String("PlaybackStatus")) { if ((m_caps & CanControl) && i.value().toString() != QLatin1String("Stopped")) { kDebug() << "Enabling stop action"; m_caps |= CanStop; } else { kDebug() << "Disabling stop action"; m_caps &= ~CanStop; } } ++i; } } void PlayerContainer::getPropsFinished(QDBusPendingCallWatcher* watcher) { QDBusPendingReply propsReply = *watcher; watcher->deleteLater(); if (m_fetchesPending < 1) { // we already failed return; } if (propsReply.isError()) { kWarning() << m_dbusAddress << "does not implement" << OrgFreedesktopDBusPropertiesInterface::staticInterfaceName() << "correctly"; kDebug() << "Error message was" << propsReply.error().name() << propsReply.error().message(); m_fetchesPending = 0; emit initialFetchFailed(this); return; } updateFromMap(propsReply.value(), FetchAll); checkForUpdate(); --m_fetchesPending; if (m_fetchesPending == 0) { emit initialFetchFinished(this); } } void PlayerContainer::updatePosition() { QDBusPendingCall async = m_propsIface->Get(OrgMprisMediaPlayer2PlayerInterface::staticInterfaceName(), "Position"); QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(async, this); connect(watcher, SIGNAL(finished(QDBusPendingCallWatcher*)), this, SLOT(getPositionFinished(QDBusPendingCallWatcher*))); } void PlayerContainer::getPositionFinished(QDBusPendingCallWatcher* watcher) { QDBusPendingReply propsReply = *watcher; watcher->deleteLater(); if (propsReply.isError()) { kWarning() << m_dbusAddress << "does not implement" << OrgFreedesktopDBusPropertiesInterface::staticInterfaceName() << "correctly"; kDebug() << "Error message was" << propsReply.error().name() << propsReply.error().message(); return; } setData("Position", propsReply.value().toLongLong()); setData(POS_UPD_STRING, QDateTime::currentDateTimeUtc()); checkForUpdate(); } void PlayerContainer::propertiesChanged( const QString& interface, const QVariantMap& changedProperties, const QStringList& invalidatedProperties) { Q_UNUSED(interface) updateFromMap(changedProperties, UpdatedSignal); if (!invalidatedProperties.isEmpty()) { refresh(); } checkForUpdate(); } void PlayerContainer::seeked(qlonglong position) { setData("Position", position); setData(POS_UPD_STRING, QDateTime::currentDateTimeUtc()); checkForUpdate(); } void PlayerContainer::recalculatePosition() { Q_ASSERT(data().contains("Position")); qint64 pos = data().value("Position").toLongLong(); QDateTime lastUpdated = data().value(POS_UPD_STRING).toDateTime(); QDateTime now = QDateTime::currentDateTimeUtc(); qint64 diff = lastUpdated.msecsTo(now) * 1000; qint64 newPos = pos + static_cast(diff * m_currentRate); setData("Position", newPos); setData(POS_UPD_STRING, now); } #include "moc_playercontainer.cpp"