File indexing completed on 2024-04-21 04:56:50
0001 /** 0002 * SPDX-FileCopyrightText: 2018 Jun Bo Bi <jambonmcyeah@gmail.com> 0003 * 0004 * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL 0005 */ 0006 0007 #include "mpriscontrolplugin-win.h" 0008 0009 #include "plugin_mpriscontrol_debug.h" 0010 #include <core/device.h> 0011 0012 #include <KLocalizedString> 0013 #include <KPluginFactory> 0014 0015 #include <QBuffer> 0016 0017 #include <chrono> 0018 #include <random> 0019 0020 #include <ppltasks.h> 0021 #include <windows.h> 0022 0023 using namespace Windows::Foundation; 0024 0025 K_PLUGIN_CLASS_WITH_JSON(MprisControlPlugin, "kdeconnect_mpriscontrol.json") 0026 0027 namespace 0028 { 0029 const QString DEFAULT_PLAYER = 0030 i18nc("@title Users select this to control the current media player when we can't detect a specific player name like VLC", "Current Player"); 0031 } 0032 0033 MprisControlPlugin::MprisControlPlugin(QObject *parent, const QVariantList &args) 0034 : KdeConnectPlugin(parent, args) 0035 , sessionManager(GlobalSystemMediaTransportControlsSessionManager::RequestAsync().get()) 0036 { 0037 sessionManager.SessionsChanged([this](GlobalSystemMediaTransportControlsSessionManager, SessionsChangedEventArgs) { 0038 this->updatePlayerList(); 0039 }); 0040 this->updatePlayerList(); 0041 } 0042 0043 std::optional<QString> MprisControlPlugin::getPlayerName(GlobalSystemMediaTransportControlsSession const &player) 0044 { 0045 auto entry = std::find(this->playerList.constBegin(), this->playerList.constEnd(), player); 0046 0047 if (entry == this->playerList.constEnd()) { 0048 qCWarning(KDECONNECT_PLUGIN_MPRISCONTROL) << "PlaybackInfoChanged received for no longer tracked session" << player.SourceAppUserModelId().c_str(); 0049 return std::nullopt; 0050 } 0051 0052 return entry.key(); 0053 } 0054 0055 QString MprisControlPlugin::randomUrl() 0056 { 0057 const QString VALID_CHARS = QStringLiteral("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"); 0058 std::default_random_engine generator; 0059 std::uniform_int_distribution<int> distribution(0, VALID_CHARS.size() - 1); 0060 0061 const int size = 10; 0062 QString fileUrl(size, QChar()); 0063 for (int i = 0; i < size; i++) { 0064 fileUrl[i] = VALID_CHARS[distribution(generator)]; 0065 } 0066 0067 return QStringLiteral("file://") + fileUrl; 0068 } 0069 0070 void MprisControlPlugin::sendMediaProperties(std::variant<NetworkPacket, QString> const &packetOrName, GlobalSystemMediaTransportControlsSession const &player) 0071 { 0072 NetworkPacket np = packetOrName.index() == 0 ? std::get<0>(packetOrName) : NetworkPacket(PACKET_TYPE_MPRIS); 0073 if (packetOrName.index() == 1) 0074 np.set(QStringLiteral("player"), std::get<1>(packetOrName)); 0075 0076 auto mediaProperties = player.TryGetMediaPropertiesAsync().get(); 0077 0078 np.set(QStringLiteral("title"), QString::fromWCharArray(mediaProperties.Title().c_str())); 0079 np.set(QStringLiteral("artist"), QString::fromWCharArray(mediaProperties.Artist().c_str())); 0080 np.set(QStringLiteral("album"), QString::fromWCharArray(mediaProperties.AlbumTitle().c_str())); 0081 np.set(QStringLiteral("albumArtUrl"), randomUrl()); 0082 0083 np.set(QStringLiteral("url"), QString()); 0084 sendTimelineProperties(np, player, true); // "length" 0085 0086 if (packetOrName.index() == 1) 0087 sendPacket(np); 0088 } 0089 0090 void MprisControlPlugin::sendPlaybackInfo(std::variant<NetworkPacket, QString> const &packetOrName, GlobalSystemMediaTransportControlsSession const &player) 0091 { 0092 NetworkPacket np = packetOrName.index() == 0 ? std::get<0>(packetOrName) : NetworkPacket(PACKET_TYPE_MPRIS); 0093 if (packetOrName.index() == 1) 0094 np.set(QStringLiteral("player"), std::get<1>(packetOrName)); 0095 0096 sendMediaProperties(np, player); 0097 0098 auto playbackInfo = player.GetPlaybackInfo(); 0099 auto playbackControls = playbackInfo.Controls(); 0100 0101 np.set(QStringLiteral("isPlaying"), playbackInfo.PlaybackStatus() == GlobalSystemMediaTransportControlsSessionPlaybackStatus::Playing); 0102 np.set(QStringLiteral("canPause"), playbackControls.IsPauseEnabled()); 0103 np.set(QStringLiteral("canPlay"), playbackControls.IsPlayEnabled()); 0104 np.set(QStringLiteral("canGoNext"), playbackControls.IsNextEnabled()); 0105 np.set(QStringLiteral("canGoPrevious"), playbackControls.IsPreviousEnabled()); 0106 np.set(QStringLiteral("canSeek"), playbackControls.IsPlaybackPositionEnabled()); 0107 0108 if (playbackInfo.IsShuffleActive()) { 0109 const bool shuffleStatus = playbackInfo.IsShuffleActive().Value(); 0110 np.set(QStringLiteral("shuffle"), shuffleStatus); 0111 } 0112 0113 if (playbackInfo.AutoRepeatMode()) { 0114 QString loopStatus; 0115 switch (playbackInfo.AutoRepeatMode().Value()) { 0116 case Windows::Media::MediaPlaybackAutoRepeatMode::List: { 0117 loopStatus = QStringLiteral("Playlist"); 0118 break; 0119 } 0120 case Windows::Media::MediaPlaybackAutoRepeatMode::Track: { 0121 loopStatus = QStringLiteral("Track"); 0122 break; 0123 } 0124 default: { 0125 loopStatus = QStringLiteral("None"); 0126 break; 0127 } 0128 } 0129 np.set(QStringLiteral("loopStatus"), loopStatus); 0130 } 0131 0132 sendTimelineProperties(np, player); 0133 0134 if (packetOrName.index() == 1) 0135 sendPacket(np); 0136 } 0137 0138 void MprisControlPlugin::sendTimelineProperties(std::variant<NetworkPacket, QString> const &packetOrName, 0139 GlobalSystemMediaTransportControlsSession const &player, 0140 bool lengthOnly) 0141 { 0142 NetworkPacket np = packetOrName.index() == 0 ? std::get<0>(packetOrName) : NetworkPacket(PACKET_TYPE_MPRIS); 0143 if (packetOrName.index() == 1) 0144 np.set(QStringLiteral("player"), std::get<1>(packetOrName)); 0145 0146 auto timelineProperties = player.GetTimelineProperties(); 0147 0148 if (!lengthOnly) { 0149 const auto playbackInfo = player.GetPlaybackInfo(); 0150 const auto playbackControls = playbackInfo.Controls(); 0151 np.set(QStringLiteral("canSeek"), playbackControls.IsPlaybackPositionEnabled()); 0152 np.set(QStringLiteral("pos"), 0153 std::chrono::duration_cast<std::chrono::milliseconds>(timelineProperties.Position() - timelineProperties.StartTime()).count()); 0154 } 0155 np.set(QStringLiteral("length"), 0156 std::chrono::duration_cast<std::chrono::milliseconds>(timelineProperties.EndTime() - timelineProperties.StartTime()).count()); 0157 0158 if (packetOrName.index() == 1) 0159 sendPacket(np); 0160 } 0161 0162 void MprisControlPlugin::updatePlayerList() 0163 { 0164 playerList.clear(); 0165 playbackInfoChangedHandlers.clear(); 0166 mediaPropertiesChangedHandlers.clear(); 0167 timelinePropertiesChangedHandlers.clear(); 0168 0169 auto sessions = sessionManager.GetSessions(); 0170 playbackInfoChangedHandlers.resize(sessions.Size()); 0171 mediaPropertiesChangedHandlers.resize(sessions.Size()); 0172 timelinePropertiesChangedHandlers.resize(sessions.Size()); 0173 0174 for (uint32_t i = 0; i < sessions.Size(); i++) { 0175 const auto player = sessions.GetAt(i); 0176 auto playerName = player.SourceAppUserModelId(); 0177 0178 #if WIN_SDK_VERSION >= 19041 0179 // try to resolve the AUMID to a user-friendly name 0180 try { 0181 playerName = AppInfo::GetFromAppUserModelId(playerName).DisplayInfo().DisplayName(); 0182 } catch (winrt::hresult_error e) { 0183 qCDebug(KDECONNECT_PLUGIN_MPRISCONTROL) << QString::fromWCharArray(playerName.c_str()) << "doesn\'t have a valid AppUserModelID! Sending as-is.."; 0184 } 0185 #endif 0186 QString uniqueName = QString::fromWCharArray(playerName.c_str()); 0187 for (int i = 2; playerList.contains(uniqueName); ++i) { 0188 uniqueName += QStringLiteral(" [") + QString::number(i) + QStringLiteral("]"); 0189 } 0190 0191 playerList.insert(uniqueName, player); 0192 0193 player 0194 .PlaybackInfoChanged(auto_revoke, 0195 [this](GlobalSystemMediaTransportControlsSession player, PlaybackInfoChangedEventArgs args) { 0196 if (auto name = getPlayerName(player)) 0197 this->sendPlaybackInfo(name.value(), player); 0198 }) 0199 .swap(playbackInfoChangedHandlers[i]); 0200 concurrency::create_task([this, player] { 0201 std::chrono::milliseconds timespan(50); 0202 std::this_thread::sleep_for(timespan); 0203 0204 if (auto name = getPlayerName(player)) 0205 this->sendPlaybackInfo(name.value(), player); 0206 }); 0207 0208 if (auto name = getPlayerName(player)) 0209 sendPlaybackInfo(name.value(), player); 0210 0211 player 0212 .MediaPropertiesChanged(auto_revoke, 0213 [this](GlobalSystemMediaTransportControlsSession player, MediaPropertiesChangedEventArgs args) { 0214 if (auto name = getPlayerName(player)) 0215 this->sendMediaProperties(name.value(), player); 0216 }) 0217 .swap(mediaPropertiesChangedHandlers[i]); 0218 concurrency::create_task([this, player] { 0219 std::chrono::milliseconds timespan(50); 0220 std::this_thread::sleep_for(timespan); 0221 0222 if (auto name = getPlayerName(player)) 0223 this->sendMediaProperties(name.value(), player); 0224 }); 0225 0226 player 0227 .TimelinePropertiesChanged(auto_revoke, 0228 [this](GlobalSystemMediaTransportControlsSession player, TimelinePropertiesChangedEventArgs args) { 0229 if (auto name = getPlayerName(player)) 0230 this->sendTimelineProperties(name.value(), player); 0231 }) 0232 .swap(timelinePropertiesChangedHandlers[i]); 0233 concurrency::create_task([this, player] { 0234 std::chrono::milliseconds timespan(50); 0235 std::this_thread::sleep_for(timespan); 0236 0237 if (auto name = getPlayerName(player)) 0238 this->sendTimelineProperties(name.value(), player); 0239 }); 0240 } 0241 0242 sendPlayerList(); 0243 } 0244 0245 void MprisControlPlugin::sendPlayerList() 0246 { 0247 NetworkPacket np(PACKET_TYPE_MPRIS); 0248 0249 np.set(QStringLiteral("playerList"), playerList.keys() + QStringList(DEFAULT_PLAYER)); 0250 np.set(QStringLiteral("supportAlbumArtPayload"), false); // TODO: Sending albumArt doesn't work 0251 0252 sendPacket(np); 0253 } 0254 0255 bool MprisControlPlugin::sendAlbumArt(std::variant<NetworkPacket, QString> const &packetOrName, 0256 GlobalSystemMediaTransportControlsSession const &player, 0257 QString artUrl) 0258 { 0259 qWarning(KDECONNECT_PLUGIN_MPRISCONTROL) << "Sending Album Art"; 0260 NetworkPacket np = packetOrName.index() == 0 ? std::get<0>(packetOrName) : NetworkPacket(PACKET_TYPE_MPRIS); 0261 if (packetOrName.index() == 1) 0262 np.set(QStringLiteral("player"), std::get<1>(packetOrName)); 0263 0264 auto thumbnail = player.TryGetMediaPropertiesAsync().get().Thumbnail(); 0265 if (thumbnail) { 0266 auto stream = thumbnail.OpenReadAsync().get(); 0267 if (stream && stream.CanRead()) { 0268 IBuffer data = Buffer(stream.Size()); 0269 data = stream.ReadAsync(data, stream.Size(), InputStreamOptions::None).get(); 0270 QSharedPointer<QBuffer> qdata = QSharedPointer<QBuffer>(new QBuffer()); 0271 qdata->setData((char *)data.data(), data.Capacity()); 0272 0273 np.set(QStringLiteral("transferringAlbumArt"), true); 0274 np.set(QStringLiteral("albumArtUrl"), artUrl); 0275 0276 np.setPayload(qdata, qdata->size()); 0277 0278 if (packetOrName.index() == 1) 0279 sendPacket(np); 0280 0281 return true; 0282 } 0283 0284 return false; 0285 } else { 0286 return false; 0287 } 0288 } 0289 0290 void MprisControlPlugin::handleDefaultPlayer(const NetworkPacket &np) 0291 { 0292 if (np.has(QStringLiteral("action"))) { 0293 INPUT input = {0}; 0294 input.type = INPUT_KEYBOARD; 0295 0296 input.ki.time = 0; 0297 input.ki.dwExtraInfo = 0; 0298 input.ki.wScan = 0; 0299 input.ki.dwFlags = 0; 0300 0301 const QString &action = np.get<QString>(QStringLiteral("action")); 0302 0303 if (action == QStringLiteral("PlayPause") || (action == QStringLiteral("Play")) || (action == QStringLiteral("Pause"))) { 0304 input.ki.wVk = VK_MEDIA_PLAY_PAUSE; 0305 ::SendInput(1, &input, sizeof(INPUT)); 0306 } else if (action == QStringLiteral("Stop")) { 0307 input.ki.wVk = VK_MEDIA_STOP; 0308 ::SendInput(1, &input, sizeof(INPUT)); 0309 } else if (action == QStringLiteral("Next")) { 0310 input.ki.wVk = VK_MEDIA_NEXT_TRACK; 0311 ::SendInput(1, &input, sizeof(INPUT)); 0312 } else if (action == QStringLiteral("Previous")) { 0313 input.ki.wVk = VK_MEDIA_PREV_TRACK; 0314 ::SendInput(1, &input, sizeof(INPUT)); 0315 } else if (action == QStringLiteral("Stop")) { 0316 input.ki.wVk = VK_MEDIA_STOP; 0317 ::SendInput(1, &input, sizeof(INPUT)); 0318 } 0319 } 0320 0321 // Send something read from the mpris interface 0322 NetworkPacket answer(PACKET_TYPE_MPRIS); 0323 answer.set(QStringLiteral("player"), DEFAULT_PLAYER); 0324 bool somethingToSend = false; 0325 if (np.get<bool>(QStringLiteral("requestNowPlaying"))) { 0326 answer.set(QStringLiteral("pos"), 0); 0327 answer.set(QStringLiteral("isPlaying"), false); 0328 answer.set(QStringLiteral("canPause"), false); 0329 answer.set(QStringLiteral("canPlay"), true); 0330 answer.set(QStringLiteral("canGoNext"), true); 0331 answer.set(QStringLiteral("canGoPrevious"), true); 0332 answer.set(QStringLiteral("canSeek"), false); 0333 somethingToSend = true; 0334 } 0335 0336 if (np.get<bool>(QStringLiteral("requestVolume"))) { 0337 // we don't support setting per-app volume levels yet 0338 answer.set(QStringLiteral("volume"), -1); 0339 somethingToSend = true; 0340 } 0341 0342 if (somethingToSend) { 0343 sendPacket(answer); 0344 } 0345 } 0346 0347 void MprisControlPlugin::receivePacket(const NetworkPacket &np) 0348 { 0349 if (np.has(QStringLiteral("playerList"))) { 0350 return; // Whoever sent this is an mpris client and not an mpris control! 0351 } 0352 0353 // Send the player list 0354 const QString name = np.get<QString>(QStringLiteral("player")); 0355 0356 if (name == DEFAULT_PLAYER) { 0357 handleDefaultPlayer(np); 0358 return; 0359 } 0360 0361 auto it = playerList.find(name); 0362 bool valid_player = (it != playerList.end()); 0363 if (!valid_player || np.get<bool>(QStringLiteral("requestPlayerList"))) { 0364 updatePlayerList(); 0365 sendPlayerList(); 0366 if (!valid_player) { 0367 return; 0368 } 0369 } 0370 0371 auto player = it.value(); 0372 0373 if (np.has(QStringLiteral("albumArtUrl"))) { 0374 sendAlbumArt(name, player, np.get<QString>(QStringLiteral("albumArtUrl"))); 0375 return; 0376 } 0377 0378 if (np.has(QStringLiteral("action"))) { 0379 const QString &action = np.get<QString>(QStringLiteral("action")); 0380 if (action == QStringLiteral("Next")) { 0381 player.TrySkipNextAsync().get(); 0382 } else if (action == QStringLiteral("Previous")) { 0383 player.TrySkipPreviousAsync().get(); 0384 } else if (action == QStringLiteral("Pause")) { 0385 player.TryPauseAsync().get(); 0386 } else if (action == QStringLiteral("PlayPause")) { 0387 player.TryTogglePlayPauseAsync().get(); 0388 } else if (action == QStringLiteral("Stop")) { 0389 player.TryStopAsync().get(); 0390 } else if (action == QStringLiteral("Play")) { 0391 player.TryPlayAsync().get(); 0392 } 0393 } 0394 if (np.has(QStringLiteral("setVolume"))) { 0395 qWarning(KDECONNECT_PLUGIN_MPRISCONTROL) << "Setting volume is not supported"; 0396 } 0397 if (np.has(QStringLiteral("Seek"))) { 0398 TimeSpan offset = std::chrono::microseconds(np.get<int>(QStringLiteral("Seek"))); 0399 qWarning(KDECONNECT_PLUGIN_MPRISCONTROL) << "Seeking" << offset.count() << "ns to" << name; 0400 player.TryChangePlaybackPositionAsync((player.GetTimelineProperties().Position() + offset).count()).get(); 0401 } 0402 0403 if (np.has(QStringLiteral("SetPosition"))) { 0404 TimeSpan position = std::chrono::milliseconds(np.get<qlonglong>(QStringLiteral("SetPosition"), 0)); 0405 player.TryChangePlaybackPositionAsync((player.GetTimelineProperties().StartTime() + position).count()).get(); 0406 } 0407 0408 if (np.has(QStringLiteral("setShuffle"))) { 0409 player.TryChangeShuffleActiveAsync(np.get<bool>(QStringLiteral("setShuffle"))); 0410 } 0411 0412 if (np.has(QStringLiteral("setLoopStatus"))) { 0413 QString loopStatus = np.get<QString>(QStringLiteral("setLoopStatus")); 0414 enum class winrt::Windows::Media::MediaPlaybackAutoRepeatMode loopStatusEnumVal; 0415 if (loopStatus == QStringLiteral("Track")) { 0416 loopStatusEnumVal = Windows::Media::MediaPlaybackAutoRepeatMode::Track; 0417 } else if (loopStatus == QStringLiteral("Playlist")) { 0418 loopStatusEnumVal = Windows::Media::MediaPlaybackAutoRepeatMode::List; 0419 } else { 0420 loopStatusEnumVal = Windows::Media::MediaPlaybackAutoRepeatMode::None; 0421 } 0422 player.TryChangeAutoRepeatModeAsync(loopStatusEnumVal); 0423 } 0424 0425 // Send something read from the mpris interface 0426 NetworkPacket answer(PACKET_TYPE_MPRIS); 0427 answer.set(QStringLiteral("player"), name); 0428 bool somethingToSend = false; 0429 if (np.get<bool>(QStringLiteral("requestNowPlaying"))) { 0430 sendPlaybackInfo(answer, player); 0431 somethingToSend = true; 0432 } 0433 if (np.get<bool>(QStringLiteral("requestVolume"))) { 0434 // we don't support setting per-app volume levels yet 0435 answer.set(QStringLiteral("volume"), -1); 0436 somethingToSend = true; 0437 } 0438 0439 if (somethingToSend) { 0440 sendPacket(answer); 0441 } 0442 } 0443 0444 #include "moc_mpriscontrolplugin-win.cpp" 0445 #include "mpriscontrolplugin-win.moc"