/** * SPDX-FileCopyrightText: 2018 Jun Bo Bi * * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL */ #include "mpriscontrolplugin-win.h" #include "plugin_mpris_debug.h" #include #include #include #include #include #include using namespace Windows::Foundation; K_PLUGIN_CLASS_WITH_JSON(MprisControlPlugin, "kdeconnect_mpriscontrol.json") MprisControlPlugin::MprisControlPlugin(QObject *parent, const QVariantList &args) : KdeConnectPlugin(parent, args) { sessionManager = GlobalSystemMediaTransportControlsSessionManager::RequestAsync().get(); sessionManager->SessionsChanged([this](GlobalSystemMediaTransportControlsSessionManager, SessionsChangedEventArgs){ this->updatePlayerList(); }); this->updatePlayerList(); } std::optional MprisControlPlugin::getPlayerName(GlobalSystemMediaTransportControlsSession const& player) { auto entry = std::find(this->playerList.constBegin(), this->playerList.constEnd(), player); if(entry == this->playerList.constEnd()) { qCWarning(KDECONNECT_PLUGIN_MPRIS) << "PlaybackInfoChanged received for no longer tracked session" << player.SourceAppUserModelId().c_str(); return std::nullopt; } return entry.key(); } QString MprisControlPlugin::randomUrl() { const QString VALID_CHARS = QStringLiteral("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"); std::default_random_engine generator; std::uniform_int_distribution distribution(0,VALID_CHARS.size() - 1); const int size = 10; QString fileUrl(size, QChar()); for(int i = 0; i < size; i++) { fileUrl[i] = VALID_CHARS[distribution(generator)]; } return QStringLiteral("file://") + fileUrl; } void MprisControlPlugin::sendMediaProperties(std::variant const& packetOrName, GlobalSystemMediaTransportControlsSession const& player) { NetworkPacket np = packetOrName.index() == 0 ? std::get<0>(packetOrName) : NetworkPacket(PACKET_TYPE_MPRIS); if(packetOrName.index() == 1) np.set(QStringLiteral("player"), std::get<1>(packetOrName)); auto mediaProperties = player.TryGetMediaPropertiesAsync().get(); np.set(QStringLiteral("title"), QString::fromWCharArray(mediaProperties.Title().c_str())); np.set(QStringLiteral("artist"), QString::fromWCharArray(mediaProperties.Artist().c_str())); np.set(QStringLiteral("album"), QString::fromWCharArray(mediaProperties.AlbumTitle().c_str())); np.set(QStringLiteral("albumArtUrl"), randomUrl()); np.set(QStringLiteral("nowPlaying"), mediaProperties.Artist().empty() ? QString::fromWCharArray(mediaProperties.Title().c_str()) : (QString::fromWCharArray(mediaProperties.Artist().c_str()) + QStringLiteral(" - ") + QString::fromWCharArray(mediaProperties.Title().c_str()))); np.set(QStringLiteral("url"), QString()); sendTimelineProperties(np, player, true); // "length" if(packetOrName.index() == 1) sendPacket(np); } void MprisControlPlugin::sendPlaybackInfo(std::variant const& packetOrName, GlobalSystemMediaTransportControlsSession const& player) { NetworkPacket np = packetOrName.index() == 0 ? std::get<0>(packetOrName) : NetworkPacket(PACKET_TYPE_MPRIS); if(packetOrName.index() == 1) np.set(QStringLiteral("player"), std::get<1>(packetOrName)); sendMediaProperties(np, player); auto playbackInfo = player.GetPlaybackInfo(); auto playbackControls = playbackInfo.Controls(); np.set(QStringLiteral("isPlaying"), playbackInfo.PlaybackStatus() == GlobalSystemMediaTransportControlsSessionPlaybackStatus::Playing); np.set(QStringLiteral("canPause"), playbackControls.IsPauseEnabled()); np.set(QStringLiteral("canPlay"), playbackControls.IsPlayEnabled()); np.set(QStringLiteral("canGoNext"), playbackControls.IsNextEnabled()); np.set(QStringLiteral("canGoPrevious"), playbackControls.IsPreviousEnabled()); sendTimelineProperties(np, player); if(packetOrName.index() == 1) sendPacket(np); } void MprisControlPlugin::sendTimelineProperties(std::variant const& packetOrName, GlobalSystemMediaTransportControlsSession const& player, bool lengthOnly) { NetworkPacket np = packetOrName.index() == 0 ? std::get<0>(packetOrName) : NetworkPacket(PACKET_TYPE_MPRIS); if(packetOrName.index() == 1) np.set(QStringLiteral("player"), std::get<1>(packetOrName)); auto timelineProperties = player.GetTimelineProperties(); if(!lengthOnly){ np.set(QStringLiteral("canSeek"), timelineProperties.MinSeekTime() != timelineProperties.MaxSeekTime()); np.set(QStringLiteral("pos"), std::chrono::duration_cast(timelineProperties.Position() - timelineProperties.StartTime()).count()); } np.set(QStringLiteral("length"), std::chrono::duration_cast(timelineProperties.EndTime() - timelineProperties.StartTime()).count()); if(packetOrName.index() == 1) sendPacket(np); } void MprisControlPlugin::updatePlayerList() { playerList.clear(); playbackInfoChangedHandlers.clear(); mediaPropertiesChangedHandlers.clear(); timelinePropertiesChangedHandlers.clear(); auto sessions = sessionManager->GetSessions(); playbackInfoChangedHandlers.resize(sessions.Size()); mediaPropertiesChangedHandlers.resize(sessions.Size()); timelinePropertiesChangedHandlers.resize(sessions.Size()); for(uint32_t i = 0; i < sessions.Size(); i++) { const auto player = sessions.GetAt(i); QString name = QString::fromWCharArray(player.SourceAppUserModelId().c_str()); QString uniqueName = name; for (int i = 2; playerList.contains(uniqueName); ++i) { uniqueName = name + QStringLiteral(" [") + QString::number(i) + QStringLiteral("]"); } playerList.insert(uniqueName, player); player.PlaybackInfoChanged(auto_revoke, [this](GlobalSystemMediaTransportControlsSession player, PlaybackInfoChangedEventArgs args){ if(auto name = getPlayerName(player)) this->sendPlaybackInfo(name.value(), player); }).swap(playbackInfoChangedHandlers[i]); concurrency::create_task([this, player]{ std::chrono::milliseconds timespan(50); std::this_thread::sleep_for(timespan); if(auto name = getPlayerName(player)) this->sendPlaybackInfo(name.value(), player); }); if(auto name = getPlayerName(player)) sendPlaybackInfo(name.value(), player); player.MediaPropertiesChanged(auto_revoke, [this](GlobalSystemMediaTransportControlsSession player, MediaPropertiesChangedEventArgs args){ if(auto name = getPlayerName(player)) this->sendMediaProperties(name.value(), player); }).swap(mediaPropertiesChangedHandlers[i]); concurrency::create_task([this, player]{ std::chrono::milliseconds timespan(50); std::this_thread::sleep_for(timespan); if(auto name = getPlayerName(player)) this->sendMediaProperties(name.value(), player); }); player.TimelinePropertiesChanged(auto_revoke, [this](GlobalSystemMediaTransportControlsSession player, TimelinePropertiesChangedEventArgs args){ if(auto name = getPlayerName(player)) this->sendTimelineProperties(name.value(), player); }).swap(timelinePropertiesChangedHandlers[i]); concurrency::create_task([this, player]{ std::chrono::milliseconds timespan(50); std::this_thread::sleep_for(timespan); if(auto name = getPlayerName(player)) this->sendTimelineProperties(name.value(), player); }); } sendPlayerList(); } void MprisControlPlugin::sendPlayerList() { NetworkPacket np(PACKET_TYPE_MPRIS); np.set(QStringLiteral("playerList"), playerList.keys()); np.set(QStringLiteral("supportAlbumArtPayload"), false); // TODO: Sending albumArt doesn't work sendPacket(np); } bool MprisControlPlugin::sendAlbumArt(std::variant const& packetOrName, GlobalSystemMediaTransportControlsSession const& player, QString artUrl) { qWarning(KDECONNECT_PLUGIN_MPRIS) << "Sending Album Art"; NetworkPacket np = packetOrName.index() == 0 ? std::get<0>(packetOrName) : NetworkPacket(PACKET_TYPE_MPRIS); if(packetOrName.index() == 1) np.set(QStringLiteral("player"), std::get<1>(packetOrName)); auto thumbnail = player.TryGetMediaPropertiesAsync().get().Thumbnail(); if(thumbnail) { auto stream = thumbnail.OpenReadAsync().get(); if(stream && stream.CanRead()) { IBuffer data = Buffer(stream.Size()); data = stream.ReadAsync(data, stream.Size(), InputStreamOptions::None).get(); QSharedPointer qdata = QSharedPointer(new QBuffer()); qdata->setData((char*)data.data(), data.Capacity()); np.set(QStringLiteral("transferringAlbumArt"), true); np.set(QStringLiteral("albumArtUrl"), artUrl); np.setPayload(qdata, qdata->size()); if(packetOrName.index() == 1) sendPacket(np); return true; } return false; } else { return false; } } bool MprisControlPlugin::receivePacket(const NetworkPacket &np) { if (np.has(QStringLiteral("playerList"))) { return false; //Whoever sent this is an mpris client and not an mpris control! } //Send the player list const QString name = np.get(QStringLiteral("player")); auto it = playerList.find(name); bool valid_player = (it != playerList.end()); if (!valid_player || np.get(QStringLiteral("requestPlayerList"))) { sendPlayerList(); if (!valid_player) { return true; } } auto player = it.value(); if (np.has(QStringLiteral("albumArtUrl"))) { return sendAlbumArt(name, player, np.get(QStringLiteral("albumArtUrl"))); } if (np.has(QStringLiteral("action"))) { const QString& action = np.get(QStringLiteral("action")); if(action == QStringLiteral("Next")) { player.TrySkipNextAsync().get(); } else if(action == QStringLiteral("Previous")) { player.TrySkipPreviousAsync().get(); } else if (action == QStringLiteral("Pause")) { player.TryPauseAsync().get(); } else if (action == QStringLiteral("PlayPause")) { player.TryTogglePlayPauseAsync().get(); } else if (action == QStringLiteral("Stop")) { player.TryStopAsync().get(); } else if (action == QStringLiteral("Play")) { player.TryPlayAsync().get(); } } if (np.has(QStringLiteral("setVolume"))) { qWarning(KDECONNECT_PLUGIN_MPRIS) << "Setting volume is not supported"; } if (np.has(QStringLiteral("Seek"))) { TimeSpan offset = std::chrono::microseconds(np.get(QStringLiteral("Seek"))); qWarning(KDECONNECT_PLUGIN_MPRIS) << "Seeking" << offset.count() << "ns to" << name; player.TryChangePlaybackPositionAsync((player.GetTimelineProperties().Position() + offset).count()).get(); } if (np.has(QStringLiteral("SetPosition"))){ TimeSpan position = std::chrono::milliseconds(np.get(QStringLiteral("SetPosition"), 0)); player.TryChangePlaybackPositionAsync((player.GetTimelineProperties().StartTime() + position).count()).get(); } //Send something read from the mpris interface NetworkPacket answer(PACKET_TYPE_MPRIS); answer.set(QStringLiteral("player"), name); bool somethingToSend = false; if (np.get(QStringLiteral("requestNowPlaying"))) { sendPlaybackInfo(answer, player); somethingToSend = true; } if (np.get(QStringLiteral("requestVolume"))) { answer.set(QStringLiteral("volume"), 100); somethingToSend = true; } if (somethingToSend) { sendPacket(answer); } return true; } #include "mpriscontrolplugin-win.moc"