[OpenDocString] kdeconnect-kde (cpp)
mpriscontrolplugin-win.cpp
MprisControlPlugin::MprisControlPlugin(QObject *parent, const QVariantList &args)
    : KdeConnectPlugin(parent, args)
    , sessionManager(GlobalSystemMediaTransportControlsSessionManager::RequestAsync().get())
{
    sessionManager->SessionsChanged([this](GlobalSystemMediaTransportControlsSessionManager, SessionsChangedEventArgs) {
        this->updatePlayerList();
    });
    this->updatePlayerList();
}
This constructor builds a KdeConnectPlugin object and sets up the sessionsChanged signal. It requests the session manager from the internal request list, and sets up the player list in the session manager.
std::optional 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();
}
Returns the name of the player, or std::nullopt if it doesn't exist.
QString MprisControlPlugin::randomUrl()
{
    const QString VALID_CHARS = QStringLiteral("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ");
    std::default_random_engine generator;
    std::uniform_int_distribution 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;
}
Returns a random url with a fixed size.
void MprisControlPlugin::sendMediaProperties(std::variant 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);
}
This sends media properties to the given network packet, or create a new one if it doesn't exist. It takes the packet name, from the config file, and tries to get the media properties from the player asynchronously.
void MprisControlPlugin::sendPlaybackInfo(std::variant 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);
}
This sends the playback info for the given packet and the player. The method first constructs a network packet, sets all the properties of the packet to its internal state, and then sends the timeline properties for the packet. Then it checks if the playback status is enabled. If it is not enabled, it assumes the player is already playing. If the playback status is enabled, the method returns false.
void MprisControlPlugin::sendTimelineProperties(std::variant 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(timelineProperties.Position() - timelineProperties.StartTime()).count());
    }
    np.set(QStringLiteral("length"),
           std::chrono::duration_cast(timelineProperties.EndTime() - timelineProperties.StartTime()).count());

    if (packetOrName.index() == 1)
        sendPacket(np);
}
This sends the timeline properties of a given network packet, if it exists, and sets the position and length fields of the network packet to its internal representation. It also sets the canSeek value on the player and the length field to true.
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();
}
This sends the playback info and timeline info for all players, and sends the media and timeline properties for each player.
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);
}
This sends a list of players to the network layer. It takes the playerList map from the internal data structures and sets the supportAlbumArtPayload to false.
bool MprisControlPlugin::sendAlbumArt(std::variant 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 qdata = QSharedPointer(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;
    }
}
This sends an album art to the player if it exists. It first constructs a network packet, then checks if the packet is a 1st item in the list, if it is a 2nd item, it gets the thumbnail of the media and if it is able to read it from the stream. If the thumbnail is not able to read the data, it reads it from the stream and writes it to the np. The method first checks if the packet is a 1st item, if it is a 2nd item, it is a 3rd item, it is a QSharedPointer object that stores the data of the packet in the list, and sets the payload field to the packet. The payload of the packet is sent to the media transport. If the packet is not a network packet, the function returns true.
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(QStringLiteral("player"));
    auto it = playerList.find(name);
    bool valid_player = (it != playerList.end());
    if (!valid_player || np.get(QStringLiteral("requestPlayerList"))) {
        sendPlayerList();
        if (!valid_player) {
            return true;
        }
    }

    auto player = it.value();

    if (np.has(QStringLiteral("albumArtUrl"))) {
        return sendAlbumArt(name, player, np.get(QStringLiteral("albumArtUrl")));
    }

    if (np.has(QStringLiteral("action"))) {
        const QString &action = np.get(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(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(QStringLiteral("SetPosition"), 0));
        player.TryChangePlaybackPositionAsync((player.GetTimelineProperties().StartTime() + position).count()).get();
    }

    if (np.has(QStringLiteral("setShuffle"))) {
        player.TryChangeShuffleActiveAsync(np.get(QStringLiteral("setShuffle")));
    }

    if (np.has(QStringLiteral("setLoopStatus"))) {
        QString loopStatus = np.get(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(QStringLiteral("requestNowPlaying"))) {
        sendPlaybackInfo(answer, player);
        somethingToSend = true;
    }
    if (np.get(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;
}
This method builds a network packet from an existing network package and sends it to the appropriate player. It returns true if the packet is valid. If the packet is not valid, the method returns false.