/** * 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_mpriscontrol_debug.h" #include #include #include #include #include #include #include #include using namespace Windows::Foundation; K_PLUGIN_CLASS_WITH_JSON(MprisControlPlugin, "kdeconnect_mpriscontrol.json") namespace { const QString DEFAULT_PLAYER = i18nc("@title Users select this to control the current media player when we can't detect a specific player name like VLC", "Current Player"); } 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_MPRISCONTROL) << "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("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()); np.set(QStringLiteral("canSeek"), playbackControls.IsPlaybackPositionEnabled()); if (playbackInfo.IsShuffleActive()) { const bool shuffleStatus = playbackInfo.IsShuffleActive().Value(); np.set(QStringLiteral("shuffle"), shuffleStatus); } if (playbackInfo.AutoRepeatMode()) { QString loopStatus; switch (playbackInfo.AutoRepeatMode().Value()) { case Windows::Media::MediaPlaybackAutoRepeatMode::List: { loopStatus = QStringLiteral("Playlist"); break; } case Windows::Media::MediaPlaybackAutoRepeatMode::Track: { loopStatus = QStringLiteral("Track"); break; } default: { loopStatus = QStringLiteral("None"); break; } } np.set(QStringLiteral("loopStatus"), loopStatus); } 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) { const auto playbackInfo = player.GetPlaybackInfo(); const auto playbackControls = playbackInfo.Controls(); np.set(QStringLiteral("canSeek"), playbackControls.IsPlaybackPositionEnabled()); 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); auto playerName = player.SourceAppUserModelId(); #if WIN_SDK_VERSION >= 19041 // try to resolve the AUMID to a user-friendly name try { playerName = AppInfo::GetFromAppUserModelId(playerName).DisplayInfo().DisplayName(); } catch (winrt::hresult_error e) { qCDebug(KDECONNECT_PLUGIN_MPRISCONTROL) << QString::fromWCharArray(playerName.c_str()) << "doesn\'t have a valid AppUserModelID! Sending as-is.."; } #endif QString uniqueName = QString::fromWCharArray(playerName.c_str()); for (int i = 2; playerList.contains(uniqueName); ++i) { uniqueName += 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() + QStringList(DEFAULT_PLAYER)); 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_MPRISCONTROL) << "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; } } void MprisControlPlugin::handleDefaultPlayer(const NetworkPacket &np) { if (np.has(QStringLiteral("action"))) { INPUT input = {0}; input.type = INPUT_KEYBOARD; input.ki.time = 0; input.ki.dwExtraInfo = 0; input.ki.wScan = 0; input.ki.dwFlags = 0; const QString &action = np.get(QStringLiteral("action")); if (action == QStringLiteral("PlayPause") || (action == QStringLiteral("Play")) || (action == QStringLiteral("Pause"))) { input.ki.wVk = VK_MEDIA_PLAY_PAUSE; ::SendInput(1, &input, sizeof(INPUT)); } else if (action == QStringLiteral("Stop")) { input.ki.wVk = VK_MEDIA_STOP; ::SendInput(1, &input, sizeof(INPUT)); } else if (action == QStringLiteral("Next")) { input.ki.wVk = VK_MEDIA_NEXT_TRACK; ::SendInput(1, &input, sizeof(INPUT)); } else if (action == QStringLiteral("Previous")) { input.ki.wVk = VK_MEDIA_PREV_TRACK; ::SendInput(1, &input, sizeof(INPUT)); } else if (action == QStringLiteral("Stop")) { input.ki.wVk = VK_MEDIA_STOP; ::SendInput(1, &input, sizeof(INPUT)); } } // Send something read from the mpris interface NetworkPacket answer(PACKET_TYPE_MPRIS); answer.set(QStringLiteral("player"), DEFAULT_PLAYER); bool somethingToSend = false; if (np.get(QStringLiteral("requestNowPlaying"))) { answer.set(QStringLiteral("pos"), 0); answer.set(QStringLiteral("isPlaying"), false); answer.set(QStringLiteral("canPause"), false); answer.set(QStringLiteral("canPlay"), true); answer.set(QStringLiteral("canGoNext"), true); answer.set(QStringLiteral("canGoPrevious"), true); answer.set(QStringLiteral("canSeek"), false); somethingToSend = true; } if (np.get(QStringLiteral("requestVolume"))) { // we don't support setting per-app volume levels yet answer.set(QStringLiteral("volume"), -1); somethingToSend = true; } if (somethingToSend) { sendPacket(answer); } } void MprisControlPlugin::receivePacket(const NetworkPacket &np) { if (np.has(QStringLiteral("playerList"))) { return; // Whoever sent this is an mpris client and not an mpris control! } // Send the player list const QString name = np.get(QStringLiteral("player")); if (name == DEFAULT_PLAYER) { handleDefaultPlayer(np); return; } auto it = playerList.find(name); bool valid_player = (it != playerList.end()); if (!valid_player || np.get(QStringLiteral("requestPlayerList"))) { updatePlayerList(); sendPlayerList(); if (!valid_player) { return; } } auto player = it.value(); if (np.has(QStringLiteral("albumArtUrl"))) { sendAlbumArt(name, player, np.get(QStringLiteral("albumArtUrl"))); return; } 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_MPRISCONTROL) << "Setting volume is not supported"; } if (np.has(QStringLiteral("Seek"))) { TimeSpan offset = std::chrono::microseconds(np.get(QStringLiteral("Seek"))); qWarning(KDECONNECT_PLUGIN_MPRISCONTROL) << "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(); } if (np.has(QStringLiteral("setShuffle"))) { player.TryChangeShuffleActiveAsync(np.get(QStringLiteral("setShuffle"))); } if (np.has(QStringLiteral("setLoopStatus"))) { QString loopStatus = np.get(QStringLiteral("setLoopStatus")); enum class winrt::Windows::Media::MediaPlaybackAutoRepeatMode loopStatusEnumVal; if (loopStatus == QStringLiteral("Track")) { loopStatusEnumVal = Windows::Media::MediaPlaybackAutoRepeatMode::Track; } else if (loopStatus == QStringLiteral("Playlist")) { loopStatusEnumVal = Windows::Media::MediaPlaybackAutoRepeatMode::List; } else { loopStatusEnumVal = Windows::Media::MediaPlaybackAutoRepeatMode::None; } player.TryChangeAutoRepeatModeAsync(loopStatusEnumVal); } // 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"))) { // we don't support setting per-app volume levels yet answer.set(QStringLiteral("volume"), -1); somethingToSend = true; } if (somethingToSend) { sendPacket(answer); } } #include "moc_mpriscontrolplugin-win.cpp" #include "mpriscontrolplugin-win.moc"