File indexing completed on 2024-04-21 04:56:50

0001 /**
0002  * SPDX-FileCopyrightText: 2018 Jun Bo Bi <jambonmcyeah@gmail.com>
0003  *
0004  * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0005  */
0006 
0007 #include "mpriscontrolplugin-win.h"
0008 
0009 #include "plugin_mpriscontrol_debug.h"
0010 #include <core/device.h>
0011 
0012 #include <KLocalizedString>
0013 #include <KPluginFactory>
0014 
0015 #include <QBuffer>
0016 
0017 #include <chrono>
0018 #include <random>
0019 
0020 #include <ppltasks.h>
0021 #include <windows.h>
0022 
0023 using namespace Windows::Foundation;
0024 
0025 K_PLUGIN_CLASS_WITH_JSON(MprisControlPlugin, "kdeconnect_mpriscontrol.json")
0026 
0027 namespace
0028 {
0029 const QString DEFAULT_PLAYER =
0030     i18nc("@title Users select this to control the current media player when we can't detect a specific player name like VLC", "Current Player");
0031 }
0032 
0033 MprisControlPlugin::MprisControlPlugin(QObject *parent, const QVariantList &args)
0034     : KdeConnectPlugin(parent, args)
0035     , sessionManager(GlobalSystemMediaTransportControlsSessionManager::RequestAsync().get())
0036 {
0037     sessionManager.SessionsChanged([this](GlobalSystemMediaTransportControlsSessionManager, SessionsChangedEventArgs) {
0038         this->updatePlayerList();
0039     });
0040     this->updatePlayerList();
0041 }
0042 
0043 std::optional<QString> MprisControlPlugin::getPlayerName(GlobalSystemMediaTransportControlsSession const &player)
0044 {
0045     auto entry = std::find(this->playerList.constBegin(), this->playerList.constEnd(), player);
0046 
0047     if (entry == this->playerList.constEnd()) {
0048         qCWarning(KDECONNECT_PLUGIN_MPRISCONTROL) << "PlaybackInfoChanged received for no longer tracked session" << player.SourceAppUserModelId().c_str();
0049         return std::nullopt;
0050     }
0051 
0052     return entry.key();
0053 }
0054 
0055 QString MprisControlPlugin::randomUrl()
0056 {
0057     const QString VALID_CHARS = QStringLiteral("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ");
0058     std::default_random_engine generator;
0059     std::uniform_int_distribution<int> distribution(0, VALID_CHARS.size() - 1);
0060 
0061     const int size = 10;
0062     QString fileUrl(size, QChar());
0063     for (int i = 0; i < size; i++) {
0064         fileUrl[i] = VALID_CHARS[distribution(generator)];
0065     }
0066 
0067     return QStringLiteral("file://") + fileUrl;
0068 }
0069 
0070 void MprisControlPlugin::sendMediaProperties(std::variant<NetworkPacket, QString> const &packetOrName, GlobalSystemMediaTransportControlsSession const &player)
0071 {
0072     NetworkPacket np = packetOrName.index() == 0 ? std::get<0>(packetOrName) : NetworkPacket(PACKET_TYPE_MPRIS);
0073     if (packetOrName.index() == 1)
0074         np.set(QStringLiteral("player"), std::get<1>(packetOrName));
0075 
0076     auto mediaProperties = player.TryGetMediaPropertiesAsync().get();
0077 
0078     np.set(QStringLiteral("title"), QString::fromWCharArray(mediaProperties.Title().c_str()));
0079     np.set(QStringLiteral("artist"), QString::fromWCharArray(mediaProperties.Artist().c_str()));
0080     np.set(QStringLiteral("album"), QString::fromWCharArray(mediaProperties.AlbumTitle().c_str()));
0081     np.set(QStringLiteral("albumArtUrl"), randomUrl());
0082 
0083     np.set(QStringLiteral("url"), QString());
0084     sendTimelineProperties(np, player, true); // "length"
0085 
0086     if (packetOrName.index() == 1)
0087         sendPacket(np);
0088 }
0089 
0090 void MprisControlPlugin::sendPlaybackInfo(std::variant<NetworkPacket, QString> const &packetOrName, GlobalSystemMediaTransportControlsSession const &player)
0091 {
0092     NetworkPacket np = packetOrName.index() == 0 ? std::get<0>(packetOrName) : NetworkPacket(PACKET_TYPE_MPRIS);
0093     if (packetOrName.index() == 1)
0094         np.set(QStringLiteral("player"), std::get<1>(packetOrName));
0095 
0096     sendMediaProperties(np, player);
0097 
0098     auto playbackInfo = player.GetPlaybackInfo();
0099     auto playbackControls = playbackInfo.Controls();
0100 
0101     np.set(QStringLiteral("isPlaying"), playbackInfo.PlaybackStatus() == GlobalSystemMediaTransportControlsSessionPlaybackStatus::Playing);
0102     np.set(QStringLiteral("canPause"), playbackControls.IsPauseEnabled());
0103     np.set(QStringLiteral("canPlay"), playbackControls.IsPlayEnabled());
0104     np.set(QStringLiteral("canGoNext"), playbackControls.IsNextEnabled());
0105     np.set(QStringLiteral("canGoPrevious"), playbackControls.IsPreviousEnabled());
0106     np.set(QStringLiteral("canSeek"), playbackControls.IsPlaybackPositionEnabled());
0107 
0108     if (playbackInfo.IsShuffleActive()) {
0109         const bool shuffleStatus = playbackInfo.IsShuffleActive().Value();
0110         np.set(QStringLiteral("shuffle"), shuffleStatus);
0111     }
0112 
0113     if (playbackInfo.AutoRepeatMode()) {
0114         QString loopStatus;
0115         switch (playbackInfo.AutoRepeatMode().Value()) {
0116         case Windows::Media::MediaPlaybackAutoRepeatMode::List: {
0117             loopStatus = QStringLiteral("Playlist");
0118             break;
0119         }
0120         case Windows::Media::MediaPlaybackAutoRepeatMode::Track: {
0121             loopStatus = QStringLiteral("Track");
0122             break;
0123         }
0124         default: {
0125             loopStatus = QStringLiteral("None");
0126             break;
0127         }
0128         }
0129         np.set(QStringLiteral("loopStatus"), loopStatus);
0130     }
0131 
0132     sendTimelineProperties(np, player);
0133 
0134     if (packetOrName.index() == 1)
0135         sendPacket(np);
0136 }
0137 
0138 void MprisControlPlugin::sendTimelineProperties(std::variant<NetworkPacket, QString> const &packetOrName,
0139                                                 GlobalSystemMediaTransportControlsSession const &player,
0140                                                 bool lengthOnly)
0141 {
0142     NetworkPacket np = packetOrName.index() == 0 ? std::get<0>(packetOrName) : NetworkPacket(PACKET_TYPE_MPRIS);
0143     if (packetOrName.index() == 1)
0144         np.set(QStringLiteral("player"), std::get<1>(packetOrName));
0145 
0146     auto timelineProperties = player.GetTimelineProperties();
0147 
0148     if (!lengthOnly) {
0149         const auto playbackInfo = player.GetPlaybackInfo();
0150         const auto playbackControls = playbackInfo.Controls();
0151         np.set(QStringLiteral("canSeek"), playbackControls.IsPlaybackPositionEnabled());
0152         np.set(QStringLiteral("pos"),
0153                std::chrono::duration_cast<std::chrono::milliseconds>(timelineProperties.Position() - timelineProperties.StartTime()).count());
0154     }
0155     np.set(QStringLiteral("length"),
0156            std::chrono::duration_cast<std::chrono::milliseconds>(timelineProperties.EndTime() - timelineProperties.StartTime()).count());
0157 
0158     if (packetOrName.index() == 1)
0159         sendPacket(np);
0160 }
0161 
0162 void MprisControlPlugin::updatePlayerList()
0163 {
0164     playerList.clear();
0165     playbackInfoChangedHandlers.clear();
0166     mediaPropertiesChangedHandlers.clear();
0167     timelinePropertiesChangedHandlers.clear();
0168 
0169     auto sessions = sessionManager.GetSessions();
0170     playbackInfoChangedHandlers.resize(sessions.Size());
0171     mediaPropertiesChangedHandlers.resize(sessions.Size());
0172     timelinePropertiesChangedHandlers.resize(sessions.Size());
0173 
0174     for (uint32_t i = 0; i < sessions.Size(); i++) {
0175         const auto player = sessions.GetAt(i);
0176         auto playerName = player.SourceAppUserModelId();
0177 
0178 #if WIN_SDK_VERSION >= 19041
0179         // try to resolve the AUMID to a user-friendly name
0180         try {
0181             playerName = AppInfo::GetFromAppUserModelId(playerName).DisplayInfo().DisplayName();
0182         } catch (winrt::hresult_error e) {
0183             qCDebug(KDECONNECT_PLUGIN_MPRISCONTROL) << QString::fromWCharArray(playerName.c_str()) << "doesn\'t have a valid AppUserModelID! Sending as-is..";
0184         }
0185 #endif
0186         QString uniqueName = QString::fromWCharArray(playerName.c_str());
0187         for (int i = 2; playerList.contains(uniqueName); ++i) {
0188             uniqueName += QStringLiteral(" [") + QString::number(i) + QStringLiteral("]");
0189         }
0190 
0191         playerList.insert(uniqueName, player);
0192 
0193         player
0194             .PlaybackInfoChanged(auto_revoke,
0195                                  [this](GlobalSystemMediaTransportControlsSession player, PlaybackInfoChangedEventArgs args) {
0196                                      if (auto name = getPlayerName(player))
0197                                          this->sendPlaybackInfo(name.value(), player);
0198                                  })
0199             .swap(playbackInfoChangedHandlers[i]);
0200         concurrency::create_task([this, player] {
0201             std::chrono::milliseconds timespan(50);
0202             std::this_thread::sleep_for(timespan);
0203 
0204             if (auto name = getPlayerName(player))
0205                 this->sendPlaybackInfo(name.value(), player);
0206         });
0207 
0208         if (auto name = getPlayerName(player))
0209             sendPlaybackInfo(name.value(), player);
0210 
0211         player
0212             .MediaPropertiesChanged(auto_revoke,
0213                                     [this](GlobalSystemMediaTransportControlsSession player, MediaPropertiesChangedEventArgs args) {
0214                                         if (auto name = getPlayerName(player))
0215                                             this->sendMediaProperties(name.value(), player);
0216                                     })
0217             .swap(mediaPropertiesChangedHandlers[i]);
0218         concurrency::create_task([this, player] {
0219             std::chrono::milliseconds timespan(50);
0220             std::this_thread::sleep_for(timespan);
0221 
0222             if (auto name = getPlayerName(player))
0223                 this->sendMediaProperties(name.value(), player);
0224         });
0225 
0226         player
0227             .TimelinePropertiesChanged(auto_revoke,
0228                                        [this](GlobalSystemMediaTransportControlsSession player, TimelinePropertiesChangedEventArgs args) {
0229                                            if (auto name = getPlayerName(player))
0230                                                this->sendTimelineProperties(name.value(), player);
0231                                        })
0232             .swap(timelinePropertiesChangedHandlers[i]);
0233         concurrency::create_task([this, player] {
0234             std::chrono::milliseconds timespan(50);
0235             std::this_thread::sleep_for(timespan);
0236 
0237             if (auto name = getPlayerName(player))
0238                 this->sendTimelineProperties(name.value(), player);
0239         });
0240     }
0241 
0242     sendPlayerList();
0243 }
0244 
0245 void MprisControlPlugin::sendPlayerList()
0246 {
0247     NetworkPacket np(PACKET_TYPE_MPRIS);
0248 
0249     np.set(QStringLiteral("playerList"), playerList.keys() + QStringList(DEFAULT_PLAYER));
0250     np.set(QStringLiteral("supportAlbumArtPayload"), false); // TODO: Sending albumArt doesn't work
0251 
0252     sendPacket(np);
0253 }
0254 
0255 bool MprisControlPlugin::sendAlbumArt(std::variant<NetworkPacket, QString> const &packetOrName,
0256                                       GlobalSystemMediaTransportControlsSession const &player,
0257                                       QString artUrl)
0258 {
0259     qWarning(KDECONNECT_PLUGIN_MPRISCONTROL) << "Sending Album Art";
0260     NetworkPacket np = packetOrName.index() == 0 ? std::get<0>(packetOrName) : NetworkPacket(PACKET_TYPE_MPRIS);
0261     if (packetOrName.index() == 1)
0262         np.set(QStringLiteral("player"), std::get<1>(packetOrName));
0263 
0264     auto thumbnail = player.TryGetMediaPropertiesAsync().get().Thumbnail();
0265     if (thumbnail) {
0266         auto stream = thumbnail.OpenReadAsync().get();
0267         if (stream && stream.CanRead()) {
0268             IBuffer data = Buffer(stream.Size());
0269             data = stream.ReadAsync(data, stream.Size(), InputStreamOptions::None).get();
0270             QSharedPointer<QBuffer> qdata = QSharedPointer<QBuffer>(new QBuffer());
0271             qdata->setData((char *)data.data(), data.Capacity());
0272 
0273             np.set(QStringLiteral("transferringAlbumArt"), true);
0274             np.set(QStringLiteral("albumArtUrl"), artUrl);
0275 
0276             np.setPayload(qdata, qdata->size());
0277 
0278             if (packetOrName.index() == 1)
0279                 sendPacket(np);
0280 
0281             return true;
0282         }
0283 
0284         return false;
0285     } else {
0286         return false;
0287     }
0288 }
0289 
0290 void MprisControlPlugin::handleDefaultPlayer(const NetworkPacket &np)
0291 {
0292     if (np.has(QStringLiteral("action"))) {
0293         INPUT input = {0};
0294         input.type = INPUT_KEYBOARD;
0295 
0296         input.ki.time = 0;
0297         input.ki.dwExtraInfo = 0;
0298         input.ki.wScan = 0;
0299         input.ki.dwFlags = 0;
0300 
0301         const QString &action = np.get<QString>(QStringLiteral("action"));
0302 
0303         if (action == QStringLiteral("PlayPause") || (action == QStringLiteral("Play")) || (action == QStringLiteral("Pause"))) {
0304             input.ki.wVk = VK_MEDIA_PLAY_PAUSE;
0305             ::SendInput(1, &input, sizeof(INPUT));
0306         } else if (action == QStringLiteral("Stop")) {
0307             input.ki.wVk = VK_MEDIA_STOP;
0308             ::SendInput(1, &input, sizeof(INPUT));
0309         } else if (action == QStringLiteral("Next")) {
0310             input.ki.wVk = VK_MEDIA_NEXT_TRACK;
0311             ::SendInput(1, &input, sizeof(INPUT));
0312         } else if (action == QStringLiteral("Previous")) {
0313             input.ki.wVk = VK_MEDIA_PREV_TRACK;
0314             ::SendInput(1, &input, sizeof(INPUT));
0315         } else if (action == QStringLiteral("Stop")) {
0316             input.ki.wVk = VK_MEDIA_STOP;
0317             ::SendInput(1, &input, sizeof(INPUT));
0318         }
0319     }
0320 
0321     // Send something read from the mpris interface
0322     NetworkPacket answer(PACKET_TYPE_MPRIS);
0323     answer.set(QStringLiteral("player"), DEFAULT_PLAYER);
0324     bool somethingToSend = false;
0325     if (np.get<bool>(QStringLiteral("requestNowPlaying"))) {
0326         answer.set(QStringLiteral("pos"), 0);
0327         answer.set(QStringLiteral("isPlaying"), false);
0328         answer.set(QStringLiteral("canPause"), false);
0329         answer.set(QStringLiteral("canPlay"), true);
0330         answer.set(QStringLiteral("canGoNext"), true);
0331         answer.set(QStringLiteral("canGoPrevious"), true);
0332         answer.set(QStringLiteral("canSeek"), false);
0333         somethingToSend = true;
0334     }
0335 
0336     if (np.get<bool>(QStringLiteral("requestVolume"))) {
0337         // we don't support setting per-app volume levels yet
0338         answer.set(QStringLiteral("volume"), -1);
0339         somethingToSend = true;
0340     }
0341 
0342     if (somethingToSend) {
0343         sendPacket(answer);
0344     }
0345 }
0346 
0347 void MprisControlPlugin::receivePacket(const NetworkPacket &np)
0348 {
0349     if (np.has(QStringLiteral("playerList"))) {
0350         return; // Whoever sent this is an mpris client and not an mpris control!
0351     }
0352 
0353     // Send the player list
0354     const QString name = np.get<QString>(QStringLiteral("player"));
0355 
0356     if (name == DEFAULT_PLAYER) {
0357         handleDefaultPlayer(np);
0358         return;
0359     }
0360 
0361     auto it = playerList.find(name);
0362     bool valid_player = (it != playerList.end());
0363     if (!valid_player || np.get<bool>(QStringLiteral("requestPlayerList"))) {
0364         updatePlayerList();
0365         sendPlayerList();
0366         if (!valid_player) {
0367             return;
0368         }
0369     }
0370 
0371     auto player = it.value();
0372 
0373     if (np.has(QStringLiteral("albumArtUrl"))) {
0374         sendAlbumArt(name, player, np.get<QString>(QStringLiteral("albumArtUrl")));
0375         return;
0376     }
0377 
0378     if (np.has(QStringLiteral("action"))) {
0379         const QString &action = np.get<QString>(QStringLiteral("action"));
0380         if (action == QStringLiteral("Next")) {
0381             player.TrySkipNextAsync().get();
0382         } else if (action == QStringLiteral("Previous")) {
0383             player.TrySkipPreviousAsync().get();
0384         } else if (action == QStringLiteral("Pause")) {
0385             player.TryPauseAsync().get();
0386         } else if (action == QStringLiteral("PlayPause")) {
0387             player.TryTogglePlayPauseAsync().get();
0388         } else if (action == QStringLiteral("Stop")) {
0389             player.TryStopAsync().get();
0390         } else if (action == QStringLiteral("Play")) {
0391             player.TryPlayAsync().get();
0392         }
0393     }
0394     if (np.has(QStringLiteral("setVolume"))) {
0395         qWarning(KDECONNECT_PLUGIN_MPRISCONTROL) << "Setting volume is not supported";
0396     }
0397     if (np.has(QStringLiteral("Seek"))) {
0398         TimeSpan offset = std::chrono::microseconds(np.get<int>(QStringLiteral("Seek")));
0399         qWarning(KDECONNECT_PLUGIN_MPRISCONTROL) << "Seeking" << offset.count() << "ns to" << name;
0400         player.TryChangePlaybackPositionAsync((player.GetTimelineProperties().Position() + offset).count()).get();
0401     }
0402 
0403     if (np.has(QStringLiteral("SetPosition"))) {
0404         TimeSpan position = std::chrono::milliseconds(np.get<qlonglong>(QStringLiteral("SetPosition"), 0));
0405         player.TryChangePlaybackPositionAsync((player.GetTimelineProperties().StartTime() + position).count()).get();
0406     }
0407 
0408     if (np.has(QStringLiteral("setShuffle"))) {
0409         player.TryChangeShuffleActiveAsync(np.get<bool>(QStringLiteral("setShuffle")));
0410     }
0411 
0412     if (np.has(QStringLiteral("setLoopStatus"))) {
0413         QString loopStatus = np.get<QString>(QStringLiteral("setLoopStatus"));
0414         enum class winrt::Windows::Media::MediaPlaybackAutoRepeatMode loopStatusEnumVal;
0415         if (loopStatus == QStringLiteral("Track")) {
0416             loopStatusEnumVal = Windows::Media::MediaPlaybackAutoRepeatMode::Track;
0417         } else if (loopStatus == QStringLiteral("Playlist")) {
0418             loopStatusEnumVal = Windows::Media::MediaPlaybackAutoRepeatMode::List;
0419         } else {
0420             loopStatusEnumVal = Windows::Media::MediaPlaybackAutoRepeatMode::None;
0421         }
0422         player.TryChangeAutoRepeatModeAsync(loopStatusEnumVal);
0423     }
0424 
0425     // Send something read from the mpris interface
0426     NetworkPacket answer(PACKET_TYPE_MPRIS);
0427     answer.set(QStringLiteral("player"), name);
0428     bool somethingToSend = false;
0429     if (np.get<bool>(QStringLiteral("requestNowPlaying"))) {
0430         sendPlaybackInfo(answer, player);
0431         somethingToSend = true;
0432     }
0433     if (np.get<bool>(QStringLiteral("requestVolume"))) {
0434         // we don't support setting per-app volume levels yet
0435         answer.set(QStringLiteral("volume"), -1);
0436         somethingToSend = true;
0437     }
0438 
0439     if (somethingToSend) {
0440         sendPacket(answer);
0441     }
0442 }
0443 
0444 #include "moc_mpriscontrolplugin-win.cpp"
0445 #include "mpriscontrolplugin-win.moc"