From 4c011458c2f445d7236a50815b636f925e1f6017 Mon Sep 17 00:00:00 2001 From: Jun Bo Bi Date: Fri, 29 May 2020 12:49:20 -0400 Subject: [PATCH] Add real support for mpriscontrol on Windows --- plugins/CMakeLists.txt | 4 +- plugins/mpriscontrol/CMakeLists.txt | 3 +- .../mpriscontrol/mpriscontrolplugin-win.cpp | 313 +++++++++++++++--- plugins/mpriscontrol/mpriscontrolplugin-win.h | 31 +- 4 files changed, 290 insertions(+), 61 deletions(-) diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 175364d61..abbb0299a 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -17,7 +17,9 @@ endif() if(NOT SAILFISHOS) add_subdirectory(sendnotifications) - add_subdirectory(mpriscontrol) + if((WIN32 AND MSVC AND (${CMAKE_SYSTEM_VERSION} VERSION_GREATER_EQUAL 10.0.17763.0)) OR NOT WIN32) + add_subdirectory(mpriscontrol) + endif() add_subdirectory(photo) add_subdirectory(mprisremote) add_subdirectory(lockdevice) diff --git a/plugins/mpriscontrol/CMakeLists.txt b/plugins/mpriscontrol/CMakeLists.txt index 4ab748de4..b523da8c8 100644 --- a/plugins/mpriscontrol/CMakeLists.txt +++ b/plugins/mpriscontrol/CMakeLists.txt @@ -30,7 +30,8 @@ ecm_qt_declare_logging_category( kdeconnect_add_plugin(kdeconnect_mpriscontrol JSON kdeconnect_mpriscontrol.json SOURCES ${kdeconnect_mpriscontrol_SRCS} ${debug_file_SRCS}) if(WIN32) - target_link_libraries(kdeconnect_mpriscontrol kdeconnectcore) + target_link_libraries(kdeconnect_mpriscontrol kdeconnectcore windowsapp) + target_compile_features(kdeconnect_mpriscontrol PUBLIC cxx_std_17) else() target_link_libraries(kdeconnect_mpriscontrol Qt5::DBus kdeconnectcore) endif() diff --git a/plugins/mpriscontrol/mpriscontrolplugin-win.cpp b/plugins/mpriscontrol/mpriscontrolplugin-win.cpp index 1326a76b9..19313a127 100644 --- a/plugins/mpriscontrol/mpriscontrolplugin-win.cpp +++ b/plugins/mpriscontrol/mpriscontrolplugin-win.cpp @@ -19,16 +19,222 @@ */ #include "mpriscontrolplugin-win.h" -#include #include "plugin_mpris_debug.h" +#include + #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) { } +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) { @@ -36,70 +242,68 @@ bool MprisControlPlugin::receivePacket(const NetworkPacket &np) return false; //Whoever sent this is an mpris client and not an mpris control! } - //Send the player list - const QString player = np.get(QStringLiteral("player")); - bool valid_player = (player == playername); + 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"))) { - const QList playerlist = {playername}; - - NetworkPacket np(PACKET_TYPE_MPRIS); - np.set(QStringLiteral("playerList"), playerlist); - np.set(QStringLiteral("supportAlbumArtPayload"), false); - sendPacket(np); + sendPlayerList(); if (!valid_player) { return true; } } - 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; - - if (np.has(QStringLiteral("action"))) { - 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)); - } - } + 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"))) { - 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); - + sendPlaybackInfo(answer, player); somethingToSend = true; } if (np.get(QStringLiteral("requestVolume"))) { @@ -108,7 +312,6 @@ bool MprisControlPlugin::receivePacket(const NetworkPacket &np) } if (somethingToSend) { - answer.set(QStringLiteral("player"), player); sendPacket(answer); } diff --git a/plugins/mpriscontrol/mpriscontrolplugin-win.h b/plugins/mpriscontrol/mpriscontrolplugin-win.h index 71c2286f0..bb54afcf7 100644 --- a/plugins/mpriscontrol/mpriscontrolplugin-win.h +++ b/plugins/mpriscontrol/mpriscontrolplugin-win.h @@ -23,13 +23,21 @@ #include -#include +#include -#define PLAYERNAME QStringLiteral("Media Player") +#include +#include + +#include +#include + +using namespace winrt; +using namespace Windows::Media::Control; +using namespace Windows::Storage::Streams; #define PACKET_TYPE_MPRIS QStringLiteral("kdeconnect.mpris") -class MprisControlPlugin +class Q_DECL_EXPORT MprisControlPlugin : public KdeConnectPlugin { Q_OBJECT @@ -41,6 +49,21 @@ class MprisControlPlugin void connected() override {} private: - const QString playername = PLAYERNAME; + std::optional sessionManager; + QHash playerList; + + std::vector playbackInfoChangedHandlers; + std::vector mediaPropertiesChangedHandlers; + std::vector timelinePropertiesChangedHandlers; + + std::optional getPlayerName(GlobalSystemMediaTransportControlsSession const& player); + void sendMediaProperties(std::variant const& packetOrName, GlobalSystemMediaTransportControlsSession const& player); + void sendPlaybackInfo(std::variant const& packetOrName, GlobalSystemMediaTransportControlsSession const& player); + void sendTimelineProperties(std::variant const& packetOrName, GlobalSystemMediaTransportControlsSession const& player, bool lengthOnly = false); + void updatePlayerList(); + void sendPlayerList(); + bool sendAlbumArt(std::variant const& packetOrName, GlobalSystemMediaTransportControlsSession const& player, QString artUrl); + + QString randomUrl(); }; #endif //MPRISCONTROLPLUGINWIN_H