Add real support for mpriscontrol on Windows
This commit is contained in:
parent
f98953380f
commit
4c011458c2
4 changed files with 290 additions and 61 deletions
|
@ -17,7 +17,9 @@ endif()
|
|||
|
||||
if(NOT SAILFISHOS)
|
||||
add_subdirectory(sendnotifications)
|
||||
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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -19,16 +19,222 @@
|
|||
*/
|
||||
|
||||
#include "mpriscontrolplugin-win.h"
|
||||
#include <core/device.h>
|
||||
#include "plugin_mpris_debug.h"
|
||||
|
||||
#include <core/device.h>
|
||||
|
||||
#include <KPluginFactory>
|
||||
|
||||
#include <Windows.h>
|
||||
#include <QBuffer>
|
||||
|
||||
#include <chrono>
|
||||
#include <random>
|
||||
|
||||
#include <ppltasks.h>
|
||||
|
||||
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<QString> 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<int> 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<NetworkPacket, QString> 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<NetworkPacket, QString> 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<NetworkPacket, QString> 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<std::chrono::milliseconds>(timelineProperties.Position() - timelineProperties.StartTime()).count());
|
||||
}
|
||||
np.set(QStringLiteral("length"), std::chrono::duration_cast<std::chrono::milliseconds>(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<NetworkPacket, QString> 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<QBuffer> qdata = QSharedPointer<QBuffer>(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<QString>(QStringLiteral("player"));
|
||||
bool valid_player = (player == playername);
|
||||
const QString name = np.get<QString>(QStringLiteral("player"));
|
||||
auto it = playerList.find(name);
|
||||
bool valid_player = (it != playerList.end());
|
||||
if (!valid_player || np.get<bool>(QStringLiteral("requestPlayerList"))) {
|
||||
const QList<QString> 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;
|
||||
auto player = it.value();
|
||||
|
||||
input.ki.time = 0;
|
||||
input.ki.dwExtraInfo = 0;
|
||||
input.ki.wScan = 0;
|
||||
input.ki.dwFlags = 0;
|
||||
if (np.has(QStringLiteral("albumArtUrl"))) {
|
||||
return sendAlbumArt(name, player, np.get<QString>(QStringLiteral("albumArtUrl")));
|
||||
}
|
||||
|
||||
if (np.has(QStringLiteral("action"))) {
|
||||
const QString& action = np.get<QString>(QStringLiteral("action"));
|
||||
if (action == QStringLiteral("PlayPause") || (action == QStringLiteral("Play")) || (action == QStringLiteral("Pause")) ) {
|
||||
input.ki.wVk = VK_MEDIA_PLAY_PAUSE;
|
||||
::SendInput(1,&input,sizeof(INPUT));
|
||||
if(action == QStringLiteral("Next")) {
|
||||
player.TrySkipNextAsync().get();
|
||||
}
|
||||
else if (action == QStringLiteral("Stop")) {
|
||||
input.ki.wVk = VK_MEDIA_STOP;
|
||||
::SendInput(1,&input,sizeof(INPUT));
|
||||
else if(action == QStringLiteral("Previous")) {
|
||||
player.TrySkipPreviousAsync().get();
|
||||
}
|
||||
else if (action == QStringLiteral("Next")) {
|
||||
input.ki.wVk = VK_MEDIA_NEXT_TRACK;
|
||||
::SendInput(1,&input,sizeof(INPUT));
|
||||
else if (action == QStringLiteral("Pause"))
|
||||
{
|
||||
player.TryPauseAsync().get();
|
||||
}
|
||||
else if (action == QStringLiteral("Previous")) {
|
||||
input.ki.wVk = VK_MEDIA_PREV_TRACK;
|
||||
::SendInput(1,&input,sizeof(INPUT));
|
||||
else if (action == QStringLiteral("PlayPause"))
|
||||
{
|
||||
player.TryTogglePlayPauseAsync().get();
|
||||
}
|
||||
else if (action == QStringLiteral("Stop")) {
|
||||
input.ki.wVk = VK_MEDIA_STOP;
|
||||
::SendInput(1,&input,sizeof(INPUT));
|
||||
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<int>(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<qlonglong>(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<bool>(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<bool>(QStringLiteral("requestVolume"))) {
|
||||
|
@ -108,7 +312,6 @@ bool MprisControlPlugin::receivePacket(const NetworkPacket &np)
|
|||
}
|
||||
|
||||
if (somethingToSend) {
|
||||
answer.set(QStringLiteral("player"), player);
|
||||
sendPacket(answer);
|
||||
}
|
||||
|
||||
|
|
|
@ -23,13 +23,21 @@
|
|||
|
||||
#include <core/kdeconnectplugin.h>
|
||||
|
||||
#include <QString>
|
||||
#include <variant>
|
||||
|
||||
#define PLAYERNAME QStringLiteral("Media Player")
|
||||
#include <QHash>
|
||||
#include <vector>
|
||||
|
||||
#include <winrt/Windows.Media.Control.h>
|
||||
#include <winrt/Windows.Storage.Streams.h>
|
||||
|
||||
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<GlobalSystemMediaTransportControlsSessionManager> sessionManager;
|
||||
QHash<QString, GlobalSystemMediaTransportControlsSession> playerList;
|
||||
|
||||
std::vector<GlobalSystemMediaTransportControlsSession::PlaybackInfoChanged_revoker> playbackInfoChangedHandlers;
|
||||
std::vector<GlobalSystemMediaTransportControlsSession::MediaPropertiesChanged_revoker> mediaPropertiesChangedHandlers;
|
||||
std::vector<GlobalSystemMediaTransportControlsSession::TimelinePropertiesChanged_revoker> timelinePropertiesChangedHandlers;
|
||||
|
||||
std::optional<QString> getPlayerName(GlobalSystemMediaTransportControlsSession const& player);
|
||||
void sendMediaProperties(std::variant<NetworkPacket, QString> const& packetOrName, GlobalSystemMediaTransportControlsSession const& player);
|
||||
void sendPlaybackInfo(std::variant<NetworkPacket, QString> const& packetOrName, GlobalSystemMediaTransportControlsSession const& player);
|
||||
void sendTimelineProperties(std::variant<NetworkPacket, QString> const& packetOrName, GlobalSystemMediaTransportControlsSession const& player, bool lengthOnly = false);
|
||||
void updatePlayerList();
|
||||
void sendPlayerList();
|
||||
bool sendAlbumArt(std::variant<NetworkPacket, QString> const& packetOrName, GlobalSystemMediaTransportControlsSession const& player, QString artUrl);
|
||||
|
||||
QString randomUrl();
|
||||
};
|
||||
#endif //MPRISCONTROLPLUGINWIN_H
|
||||
|
|
Loading…
Reference in a new issue