diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 175364d6..abbb0299 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -1,54 +1,56 @@ include_directories(${CMAKE_SOURCE_DIR} ${CMAKE_BINARY_DIR}/core) add_definitions(-DTRANSLATION_DOMAIN=\"kdeconnect-plugins\") install(FILES kdeconnect_plugin.desktop DESTINATION ${SERVICETYPES_INSTALL_DIR}) add_subdirectory(ping) add_subdirectory(battery) add_subdirectory(remotecommands) add_subdirectory(remotecontrol) add_subdirectory(remotesystemvolume) add_subdirectory(clipboard) add_subdirectory(runcommand) add_subdirectory(bigscreen) if(NOT APPLE) add_subdirectory(presenter) 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) add_subdirectory(contacts) add_subdirectory(share) add_subdirectory(remotekeyboard) add_subdirectory(notifications) add_subdirectory(findmyphone) add_subdirectory(telephony) add_subdirectory(mousepad) add_subdirectory(sms) add_subdirectory(screensaver-inhibit) if(NOT APPLE) add_subdirectory(sftp) endif() if(KF5PulseAudioQt_FOUND OR WIN32) add_subdirectory(pausemusic) endif() if(Qt5Multimedia_FOUND AND (KF5PulseAudioQt_FOUND OR WIN32)) add_subdirectory(findthisdevice) endif() if (WIN32 OR APPLE OR KF5PulseAudioQt_FOUND) add_subdirectory(systemvolume) endif() endif() # If we split notifications per plugin, in several notifyrc files, they won't # appear in the same group in the Notifications KCM install(FILES kdeconnect.notifyrc DESTINATION ${KNOTIFYRC_INSTALL_DIR}) diff --git a/plugins/mpriscontrol/CMakeLists.txt b/plugins/mpriscontrol/CMakeLists.txt index 4ab748de..b523da8c 100644 --- a/plugins/mpriscontrol/CMakeLists.txt +++ b/plugins/mpriscontrol/CMakeLists.txt @@ -1,36 +1,37 @@ if(WIN32) set(kdeconnect_mpriscontrol_SRCS mpriscontrolplugin-win.cpp ) else() set(kdeconnect_mpriscontrol_SRCS mpriscontrolplugin.cpp ) set_source_files_properties( org.freedesktop.DBus.Properties.xml org.mpris.MediaPlayer2.Player.xml org.mpris.MediaPlayer2.xml PROPERTIES NO_NAMESPACE ON) qt5_add_dbus_interface(kdeconnect_mpriscontrol_SRCS org.freedesktop.DBus.Properties.xml dbusproperties) qt5_add_dbus_interface(kdeconnect_mpriscontrol_SRCS org.mpris.MediaPlayer2.Player.xml mprisplayer) qt5_add_dbus_interface(kdeconnect_mpriscontrol_SRCS org.mpris.MediaPlayer2.xml mprisroot) endif() set(debug_file_SRCS) ecm_qt_declare_logging_category( debug_file_SRCS HEADER plugin_mpris_debug.h IDENTIFIER KDECONNECT_PLUGIN_MPRIS CATEGORY_NAME kdeconnect.plugin.mpris DEFAULT_SEVERITY Warning EXPORT kdeconnect-kde DESCRIPTION "kdeconnect (plugin mpris)") 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 1326a76b..19313a12 100644 --- a/plugins/mpriscontrol/mpriscontrolplugin-win.cpp +++ b/plugins/mpriscontrol/mpriscontrolplugin-win.cpp @@ -1,118 +1,321 @@ /** * Copyright 2018 Jun Bo Bi * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #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) { 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 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; } } + auto player = it.value(); + + if (np.has(QStringLiteral("albumArtUrl"))) { + return sendAlbumArt(name, player, np.get(QStringLiteral("albumArtUrl"))); + } + 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)); - } + 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"))) { answer.set(QStringLiteral("volume"), 100); somethingToSend = true; } if (somethingToSend) { - answer.set(QStringLiteral("player"), player); sendPacket(answer); } return true; } #include "mpriscontrolplugin-win.moc" diff --git a/plugins/mpriscontrol/mpriscontrolplugin-win.h b/plugins/mpriscontrol/mpriscontrolplugin-win.h index 71c2286f..bb54afcf 100644 --- a/plugins/mpriscontrol/mpriscontrolplugin-win.h +++ b/plugins/mpriscontrol/mpriscontrolplugin-win.h @@ -1,46 +1,69 @@ /** * Copyright 2018 Jun Bo Bi * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef MPRISCONTROLPLUGINWIN_H #define MPRISCONTROLPLUGINWIN_H #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 public: explicit MprisControlPlugin(QObject *parent, const QVariantList &args); bool receivePacket(const NetworkPacket &np) override; 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