kdeconnect-kde/plugins/mpriscontrol/mpriscontrolplugin-win.cpp
Albert Vaca Cintora 2e0550651e Remove mpris "nowPlaying" field
We've had separate title & artist for a while, and all clients should be
using those by now.

Also fixes the position change not being emitted when the song changes,
and fixes the values being written after emitting that they changed.
2023-06-08 21:58:58 +00:00

373 lines
15 KiB
C++

/**
* SPDX-FileCopyrightText: 2018 Jun Bo Bi <jambonmcyeah@gmail.com>
*
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
#include "mpriscontrolplugin-win.h"
#include "plugin_mpris_debug.h"
#include <core/device.h>
#include <KPluginFactory>
#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)
, 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("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());
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<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) {
const auto playbackInfo = player.GetPlaybackInfo();
const auto playbackControls = playbackInfo.Controls();
np.set(QStringLiteral("canSeek"), playbackControls.IsPlaybackPositionEnabled());
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);
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_MPRIS) << 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());
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)
{
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 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"))) {
sendPlayerList();
if (!valid_player) {
return true;
}
}
auto player = it.value();
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("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<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();
}
if (np.has(QStringLiteral("setShuffle"))) {
player.TryChangeShuffleActiveAsync(np.get<bool>(QStringLiteral("setShuffle")));
}
if (np.has(QStringLiteral("setLoopStatus"))) {
QString loopStatus = np.get<QString>(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<bool>(QStringLiteral("requestNowPlaying"))) {
sendPlaybackInfo(answer, player);
somethingToSend = true;
}
if (np.get<bool>(QStringLiteral("requestVolume"))) {
// we don't support setting per-app volume levels yet
answer.set(QStringLiteral("volume"), -1);
somethingToSend = true;
}
if (somethingToSend) {
sendPacket(answer);
}
return true;
}
#include "mpriscontrolplugin-win.moc"