kdeconnect-kde/plugins/mpriscontrol/mpriscontrolplugin-win.cpp
Alexander Lohnau 2e67f95017 Add explicit moc includes to cpp files
The rationale is explained in https://planet.kde.org/friedrich-kossebau-2023-06-28-include-also-moc-files-of-headers/

In case of KDEConnect, it impressively speeds up compilation. Before it
took 390 seconds on a clean build and with this change it took 330 seconds.
This is due to the mocs_compilation having to include the header files
and thus all their headers. Due to the lots of small plugins we have,
this means that the same headers must be compiled plenty of times.
When we include the moc files directly in the C++ file, they are already
available.
2023-07-30 07:27:45 +00:00

374 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 "moc_mpriscontrolplugin-win.cpp"
#include "mpriscontrolplugin-win.moc"