2e67f95017
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.
374 lines
15 KiB
C++
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"
|