/** * KMix -- MPRIS2 backend * * Copyright (C) 2011 Christian Esken * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * 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 * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public * License along with this program; if not, write to the Free * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ #include "mixer_mpris2.h" #include "core/mixer.h" #include "core/ControlManager.h" #include "core/GlobalConfig.h" #include #include #include #include #include #include // Set the QDBUS_DEBUG env variable for debugging Qt DBUS calls. Mixer_Backend* MPRIS2_getMixer(Mixer *mixer, int device ) { return new Mixer_MPRIS2(mixer, device ); } Mixer_MPRIS2::Mixer_MPRIS2(Mixer *mixer, int device) : Mixer_Backend(mixer, device ) { } int Mixer_MPRIS2::open() { if ( m_devnum != 0 ) return Mixer::ERR_OPEN; registerCard(i18n("Playback Streams")); _id = "Playback Streams"; _mixer->setDynamic(); return addAllRunningPlayersAndInitHotplug(); } int Mixer_MPRIS2::close() { m_isOpen = false; closeCommon(); qDeleteAll(controls); controls.clear(); return 0; } int Mixer_MPRIS2::mediaPlay(QString id) { return mediaControl(id, "PlayPause"); } int Mixer_MPRIS2::mediaPrev(QString id) { return mediaControl(id, "Previous"); } int Mixer_MPRIS2::mediaNext(QString id) { return mediaControl(id, "Next"); } /** * Sends a media control command to the given application. * @param applicationId The MPRIS applicationId * @returns Always 0. Hint: Currently nobody uses the return code */ int Mixer_MPRIS2::mediaControl(QString applicationId, QString commandName) { MPrisControl* mad = controls.value(applicationId); if ( mad == 0 ) return 0; // Might have disconnected recently => simply ignore command kDebug() << "Send " << commandName << " to id=" << applicationId; QDBusPendingReply<> repl2 = mad->playerIfc->asyncCall(commandName); QDBusPendingCallWatcher* watchMediaControlReply = new QDBusPendingCallWatcher(repl2, mad); connect(watchMediaControlReply, SIGNAL(finished(QDBusPendingCallWatcher *)), this, SLOT(watcherMediaControl(QDBusPendingCallWatcher *))); return 0; // Presume everything went well. Can't do more for ASYNC calls } void Mixer_MPRIS2::watcherMediaControl(QDBusPendingCallWatcher* watcher) { MPrisControl* mprisCtl = watcherHelperGetMPrisControl(watcher); if (mprisCtl == 0) { return; // Reply for unknown media player. Probably "unplugged" (or not yet plugged) } // Actually the code below in this method is more or less just debugging const QDBusMessage& msg = watcher->reply(); QString id = mprisCtl->getId(); QString busDestination = mprisCtl->getBusDestination(); kDebug() << "Media control for id=" << id << ", path=" << msg.path() << ", interface=" << msg.interface() << ", busDestination" << busDestination; } /** * readVolumeFromHW() should be used only for hotplug (and even that should go away). Everything should operate via * the slot volumeChanged in the future. */ int Mixer_MPRIS2::readVolumeFromHW( const QString& /*id*/, std::shared_ptr /*md*/) { // Everything is done by notifications => no code neccessary return Mixer::OK_UNCHANGED; } /** * A slot that processes data from the MPrisControl that emit the signal. * * @param The emitting MPrisControl * @param newVolume The new volume */ void Mixer_MPRIS2::playbackStateChanged(MPrisControl* mad, MediaController::PlayState playState) { std::shared_ptr md = m_mixDevices.get(mad->getId()); md->getMediaController()->setPlayState(playState); QMetaObject::invokeMethod(this, "announceGUI", Qt::QueuedConnection); // ControlManager::instance().announce(_mixer->id(), ControlChangeType::GUI, QString("MixerMPRIS2.playbackStateChanged")); } /** * A slot that processes data from the MPrisControl that emit the signal. * * @param The emitting MPrisControl * @param newVolume The new volume */ void Mixer_MPRIS2::volumeChanged(MPrisControl* mad, double newVolume) { std::shared_ptr md = m_mixDevices.get(mad->getId()); int volInt = newVolume *100; if (GlobalConfig::instance().data.debugVolume) kDebug() << "changed" << volInt; volumeChangedInternal(md, volInt); } void Mixer_MPRIS2::volumeChangedInternal(std::shared_ptr md, int volumePercentage) { if ( md->isVirtuallyMuted() && volumePercentage == 0) { // Special code path for virtual mute switches. Don't write back the volume if it is muted in the KMix GUI return; } Volume& vol = md->playbackVolume(); vol.setVolume( Volume::LEFT, volumePercentage); md->setMuted(volumePercentage == 0); QMetaObject::invokeMethod(this, "announceVolume", Qt::QueuedConnection); // ControlManager::instance().announce(_mixer->id(), ControlChangeType::Volume, QString("MixerMPRIS2.volumeChanged")); } // The following is an example message for an incoming volume change: /* signal sender=:1.125 -> dest=(null destination) serial=503 path=/org/mpris/MediaPlayer2; interface=org.freedesktop.DBus.Properties; member=PropertiesChanged string "org.mpris.MediaPlayer2.Player" array [ dict entry( string "Volume" variant double 0.81 ) ] array [ ] */ /** * @overload * * @param id * @param md * @return */ int Mixer_MPRIS2::writeVolumeToHW( const QString& id, std::shared_ptr md ) { Volume& vol = md->playbackVolume(); double volFloat = 0; if ( ! md->isMuted() ) { int volInt = vol.getVolume(Volume::LEFT); volFloat = volInt/100.0; } QList arg; arg.append(QString("org.mpris.MediaPlayer2.Player")); arg.append(QString("Volume")); arg << QVariant::fromValue(QDBusVariant(volFloat)); MPrisControl* mad = controls.value(id); QVariant v1 = QVariant(QString("org.mpris.MediaPlayer2.Player")); QVariant v2 = QVariant(QString("Volume")); QVariant v3 = QVariant::fromValue(QDBusVariant(volFloat)); // QVariant v3 = QVariant(volFloat); // I don't care too much for the reply, as I won't receive a result. Thus fire-and-forget here. mad->propertyIfc->asyncCall("Set", v1, v2, v3); return 0; } void Mixer_MPRIS2::setEnumIdHW(const QString&, unsigned int) { // no enums in MPRIS } unsigned int Mixer_MPRIS2::enumIdHW(const QString&) { // no enums in MPRIS return 0; } bool Mixer_MPRIS2::moveStream( const QString&, const QString& ) { // not supported in MPRIS return false; } /** * Adds all currently running players and then starts listening * for changes (new players, and disappearing players).
* * @return int **/ int Mixer_MPRIS2::addAllRunningPlayersAndInitHotplug() { QDBusConnection dbusConn = QDBusConnection::sessionBus(); if (! dbusConn.isConnected() ) { kError(67100) << "Cannot connect to the D-Bus session bus.\n" << "To start it, run:\n" <<"\teval `dbus-launch --auto-syntax`\n"; return Mixer::ERR_OPEN; } // Start listening for new Mediaplayers bool connected = dbusConn.connect("", QString("/org/freedesktop/DBus"), "org.freedesktop.DBus", "NameOwnerChanged", this, SLOT(newMediaPlayer(QString,QString,QString)) ); if (!connected) { kWarning() << "MPRIS2 hotplug init failure. New Media Players will not be detected."; } /* Here is a small concurrency issue. * If new players appear between registeredServiceNames() below and the connect() above these players *might* show up doubled in KMix. * There is no simple solution (reversing could have the problem of not-adding), so we live for now with it. */ /* * Bug 311189: Introspecting via "dbusConn.interface()->registeredServiceNames()" does not work too well. * Comment: I am not so sure that registeredServiceNames() is really an issue. It is more likely * in a later step, when talking to the probed apps. Still, I now do a hand crafted 3-line version of * registeredServiceNames() via "ListNames", so I can later more easily change to async. */ QDBusInterface dbusIfc("org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus", dbusConn); QDBusPendingReply repl = dbusIfc.asyncCall("ListNames"); repl.waitForFinished(); if (! repl.isValid() ) { kError() << "Invalid reply while listing Media Players. MPRIS2 players will not be available." << repl.error(); return 1; } foreach (QString busDestination , repl.value() ) { if ( busDestination.startsWith("org.mpris.MediaPlayer2") ) { addMprisControlAsync(busDestination); kDebug() << "MPRIS2: Attached media player on busDestination=" << busDestination; } } return 0; } QString Mixer_MPRIS2::busDestinationToControlId(const QString& busDestination) { const QString prefix = "org.mpris.MediaPlayer2."; if (! busDestination.startsWith(prefix)) { kWarning() << "Ignoring unsupported control, busDestination=" << busDestination; return QString(); } return busDestination.mid(prefix.length()); } /** * Asynchronously add the MPRIS control designated by the DBUS busDestination. * to the internal apps list. * * @param conn An open connection to the DBUS Session Bus * @param busDestination The DBUS busDestination, e.g. "org.mpris.MediaPlayer2.amarok" */ void Mixer_MPRIS2::addMprisControlAsync(QString busDestination) { // -1- Create a MPrisControl. Its fields will be filled partially here, partially via ASYNC DUBUS replies QString id = busDestinationToControlId(busDestination); kDebug() << "Get control of busDestination=" << busDestination << "id=" << id; QDBusConnection conn = QDBusConnection::sessionBus(); QDBusInterface *qdbiProps = new QDBusInterface(QString(busDestination), QString("/org/mpris/MediaPlayer2"), "org.freedesktop.DBus.Properties", conn, this); QDBusInterface *qdbiPlayer = new QDBusInterface(QString(busDestination), QString("/org/mpris/MediaPlayer2"), "org.mpris.MediaPlayer2.Player", conn, this); // -2- Add the control to our official control list MPrisControl* mad = new MPrisControl(id, busDestination); mad->propertyIfc = qdbiProps; mad->playerIfc = qdbiPlayer; controls.insert(id, mad); /* * WTF: - asyncCall("Get", arg) : returns an error message (see below) * - asyncCallWithArgumentList("Get", arg) : returns an error message (see below) * - callWithArgumentList(QDBus::Block, "Get", arg) : works * - syncCall("Get", v1, v2) : works * * kmix(13543) Mixer_MPRIS2::addMPrisControl: (marok), msg2= QDBusMessage(type=Error, service=":1.44", error name="org.freedesktop.DBus.Error.UnknownMethod", error message="No such method 'Get' in interface 'org.freedesktop.DBus.Properties' at object path '/org/mpris/MediaPlayer2' (signature 'av')", signature="s", contents=("No such method 'Get' in interface 'org.freedesktop.DBus.Properties' at object path '/org/mpris/MediaPlayer2' (signature 'av')") ) , isValid= false , isFinished= true , isError= true * * This behavior is total counter-intuitive :-((( */ // Create ASYNC DBUS queries for the new control. This effectively starts a chain of async DBUS commands. QVariant v1 = QVariant(QString("org.mpris.MediaPlayer2")); QVariant v2 = QVariant(QString("Identity")); QDBusPendingReply repl2 = mad->propertyIfc->asyncCall("Get", v1, v2); QDBusPendingCallWatcher* watchIdentity = new QDBusPendingCallWatcher(repl2, mad); connect(watchIdentity, SIGNAL(finished(QDBusPendingCallWatcher *)), this, SLOT(watcherPlugControlId(QDBusPendingCallWatcher *))); } MixDevice::ChannelType Mixer_MPRIS2::getChannelTypeFromPlayerId(const QString& id) { // TODO This hardcoded application list is a quick hack. It should be generalized. MixDevice::ChannelType ct = MixDevice::APPLICATION_STREAM; if (id.startsWith("amarok")) { ct = MixDevice::APPLICATION_AMAROK; } else if (id.startsWith("banshee")) { ct = MixDevice::APPLICATION_BANSHEE; } else if (id.startsWith("vlc")) { ct = MixDevice::APPLICATION_VLC; } else if (id.startsWith("xmms")) { ct = MixDevice::APPLICATION_XMM2; } else if (id.startsWith("tomahawk")) { ct = MixDevice::APPLICATION_TOMAHAWK; } else if (id.startsWith("clementine")) { ct = MixDevice::APPLICATION_CLEMENTINE; } return ct; } void Mixer_MPRIS2::watcherInitialVolume(QDBusPendingCallWatcher* watcher) { MPrisControl* mprisCtl = watcherHelperGetMPrisControl(watcher); if (mprisCtl == 0) return; // Reply for unknown media player. Probably "unplugged" (or not yet plugged) const QDBusMessage& msg = watcher->reply(); QList repl = msg.arguments(); if ( ! repl.isEmpty() ) { QDBusVariant dbusVariant = qvariant_cast(repl.at(0)); QVariant result2 = dbusVariant.variant(); double volume = result2.toDouble(); volumeChanged(mprisCtl, volume); } watcher->deleteLater(); } void Mixer_MPRIS2::watcherInitialPlayState(QDBusPendingCallWatcher* watcher) { MPrisControl* mprisCtl = watcherHelperGetMPrisControl(watcher); if (mprisCtl == 0) return; // Reply for unknown media player. Probably "unplugged" (or not yet plugged) const QDBusMessage& msg = watcher->reply(); QList repl = msg.arguments(); if ( ! repl.isEmpty() ) { QDBusVariant dbusVariant = qvariant_cast(repl.at(0)); QVariant result2 = dbusVariant.variant(); QString playbackStateString = result2.toString(); MediaController::PlayState playState = Mixer_MPRIS2::mprisPlayStateString2PlayState(playbackStateString); playbackStateChanged(mprisCtl, playState); } watcher->deleteLater(); } /** * Convenience method for the watcher*() methods. * Returns the MPrisControl that is parent of the given watcher, if the reply is valid. In this case you can * use the result and call watcher->deleteLater() after processing the result. * * Otherwise 0 is returned, and watcher->deleteLater() is called. Important You must call watcher->deleteLater() * yourself for the other (normal/good) case. * * @param watcher * @return */ MPrisControl* Mixer_MPRIS2::watcherHelperGetMPrisControl(QDBusPendingCallWatcher* watcher) { const QDBusMessage& msg = watcher->reply(); if ( msg.type() == QDBusMessage::ReplyMessage ) { QObject* obj = watcher->parent(); MPrisControl* mad = qobject_cast(obj); if (mad != 0) { return mad; } kWarning() << "Ignoring unexpected Control Id. object=" << obj; } else if ( msg.type() == QDBusMessage::ErrorMessage ) { kError() << "ERROR in Media control operation, path=" << msg.path() << ", msg=" << msg; } watcher->deleteLater(); return 0; } void Mixer_MPRIS2::watcherPlugControlId(QDBusPendingCallWatcher* watcher) { MPrisControl* mprisCtl = watcherHelperGetMPrisControl(watcher); if (mprisCtl == 0) { return; // Reply for unknown media player. Probably "unplugged" (or not yet plugged) } const QDBusMessage& msg = watcher->reply(); QString id = mprisCtl->getId(); QString busDestination = mprisCtl->getBusDestination(); QString readableName = id; // Start with ID, but replace with reply (if exists) kDebug() << "Plugging id=" << id << ", busDestination" << busDestination << ", name= " << readableName; QList repl = msg.arguments(); if ( ! repl.isEmpty() ) { // We have to do some very ugly casting from QVariant to QDBusVariant to QVariant. This API totally sucks. QDBusVariant dbusVariant = qvariant_cast(repl.at(0)); QVariant result2 = dbusVariant.variant(); readableName = result2.toString(); // kDebug() << "REPLY " << result2.type() << ": " << readableName; MixDevice::ChannelType ct = getChannelTypeFromPlayerId(id); MixDevice* mdNew = new MixDevice(_mixer, id, readableName, ct); // MPRIS2 doesn't support an actual mute switch. Mute is defined as volume = 0.0 // Thus we won't add the playback switch Volume* vol = new Volume( 100, 0, false, false); vol->addVolumeChannel(VolumeChannel(Volume::LEFT)); // MPRIS is only one control ("Mono") MediaController* mediaContoller = mdNew->getMediaController(); mediaContoller->addMediaPlayControl(); mediaContoller->addMediaNextControl(); mediaContoller->addMediaPrevControl(); mdNew->setApplicationStream(true); mdNew->addPlaybackVolume(*vol); m_mixDevices.append( mdNew->addToPool() ); delete vol; // vol is only temporary. mdNew has its own volume object. => delete QDBusConnection sessionBus = QDBusConnection::sessionBus(); sessionBus.connect(busDestination, QString("/org/mpris/MediaPlayer2"), "org.freedesktop.DBus.Properties", "PropertiesChanged", mprisCtl, SLOT(onPropertyChange(QString,QVariantMap,QStringList)) ); connect(mprisCtl, SIGNAL(volumeChanged(MPrisControl*,double)), this, SLOT(volumeChanged(MPrisControl*,double)) ); connect(mprisCtl, SIGNAL(playbackStateChanged(MPrisControl*,MediaController::PlayState)), SLOT (playbackStateChanged(MPrisControl*,MediaController::PlayState)) ); sessionBus.connect(busDestination, QString("/Player"), "org.freedesktop.MediaPlayer", "TrackChange", mprisCtl, SLOT(trackChangedIncoming(QVariantMap)) ); // The following line is evil: mad->playerIfc->property("Volume") is in fact a synchronous call, and // sync calls are strictly forbidden, see bug 317926 //volumeChanged(mad, mad->playerIfc->property("Volume").toDouble()); // --- Query initial state -------------------------------------------------------------------------------- QVariant v1 = QVariant(QString("org.mpris.MediaPlayer2.Player")); QVariant v2 = QVariant(QString("Volume")); QDBusPendingReply repl2 = mprisCtl->propertyIfc->asyncCall("Get", v1, v2); QDBusPendingCallWatcher* watcherOutgoing = new QDBusPendingCallWatcher(repl2, mprisCtl); connect(watcherOutgoing, SIGNAL(finished(QDBusPendingCallWatcher *)), this, SLOT(watcherInitialVolume(QDBusPendingCallWatcher *))); v2 = QVariant(QString("PlaybackStatus")); repl2 = mprisCtl->propertyIfc->asyncCall("Get", v1, v2); watcherOutgoing = new QDBusPendingCallWatcher(repl2, mprisCtl); connect(watcherOutgoing, SIGNAL(finished(QDBusPendingCallWatcher *)), this, SLOT(watcherInitialPlayState(QDBusPendingCallWatcher *))); // Push notifyToReconfigureControls to stack, so it will not be executed synchronously announceControlListAsync(id); } watcher->deleteLater(); } // ----------------------------------------------------------------------------------------------------------- // ASYNC announce slots, including convenience wrappers // ----------------------------------------------------------------------------------------------------------- /** * Convenience wrapper to do the ASYNC call to #announceControlList() * @param */ void Mixer_MPRIS2::announceControlListAsync(QString /*streamId*/) { // currently we do not use the streamId QMetaObject::invokeMethod(this, "announceControlList", Qt::QueuedConnection); } void Mixer_MPRIS2::announceControlList() { ControlManager::instance().announce(_mixer->id(), ControlChangeType::ControlList, getDriverName()); } void Mixer_MPRIS2::announceGUI() { ControlManager::instance().announce(_mixer->id(), ControlChangeType::GUI, getDriverName()); } void Mixer_MPRIS2::announceVolume() { ControlManager::instance().announce(_mixer->id(), ControlChangeType::Volume, getDriverName()); } // ----------------------------------------------------------------------------------------------------------- /** * Handles the hotplug of new MPRIS2 enabled Media Players */ void Mixer_MPRIS2::newMediaPlayer(QString name, QString oldOwner, QString newOwner) { if ( name.startsWith("org.mpris.MediaPlayer2") ) { if ( oldOwner.isEmpty() && !newOwner.isEmpty()) { kDebug() << "Mediaplayer registers: " << name; addMprisControlAsync(name); } else if ( !oldOwner.isEmpty() && newOwner.isEmpty()) { QString id = busDestinationToControlId(name); kDebug() << "Mediaplayer unregisters: " << name << " , id=" << id; // -1- Remove Mediaplayer connection if (controls.contains(id)) { const MPrisControl *control = controls.value(id); QObject::disconnect(control,0,0,0); controls.remove(id); } // -2- Remove MixDevice from internal list std::shared_ptr md = m_mixDevices.get(id); if (md) { // We know about the player that is unregistering => remove internally md->close(); m_mixDevices.removeById(id); announceControlListAsync(id); kDebug() << "MixDevice 4 useCount=" << md.use_count(); } } else { kWarning() << "Mediaplayer has registered under a new name. This is currently not supported by KMix"; } } } /** * This slot is a simple proxy that enriches the DBUS signal with our data, which especially contains the id of the MixDevice. */ void MPrisControl::trackChangedIncoming(QVariantMap /*msg*/) { kDebug() << "Track changed"; } MediaController::PlayState Mixer_MPRIS2::mprisPlayStateString2PlayState(const QString& playbackStatus) { MediaController::PlayState playState = MediaController::PlayUnknown; if (playbackStatus == "Playing") { playState = MediaController::PlayPlaying; } else if (playbackStatus == "Stopped") { playState = MediaController::PlayStopped; } else if (playbackStatus == "Paused") { playState = MediaController::PlayPaused; } return playState; } /** * This slot is a simple proxy that enriches the DBUS signal with our data, which especially contains the id of the MixDevice. */ void MPrisControl::onPropertyChange(QString /*ifc*/,QVariantMap msg ,QStringList /*sl*/) { QMap::iterator v = msg.find("Volume"); if (v != msg.end() ) { double volDouble = v.value().toDouble(); kDebug(67100) << "volumeChanged incoming: vol=" << volDouble; emit volumeChanged( this, volDouble); } v = msg.find("PlaybackStatus"); if (v != msg.end() ) { QString playbackStatus = v.value().toString(); MediaController::PlayState playState = Mixer_MPRIS2::mprisPlayStateString2PlayState(playbackStatus); kDebug() << "PlaybackStatus is now " << playbackStatus; emit playbackStateChanged(this, playState); } } Mixer_MPRIS2::~Mixer_MPRIS2() { close(); } MPrisControl::MPrisControl(QString id, QString busDestination) : propertyIfc(0) , playerIfc(0) { volume = 0; this->id = id; this->busDestination = busDestination; retrievedElems = MPrisControl::NONE; } MPrisControl::~MPrisControl() { delete propertyIfc; delete playerIfc; } QString Mixer_MPRIS2::getDriverName() { return "MPRIS2"; } QString MPRIS2_getDriverName() { return "MPRIS2"; } #include "moc_mixer_mpris2.cpp"