Ctrl K
ComunidadGuías…

queue_speaker_changes.h

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
#pragma once
#include "upnp/gena.h"
#include "transport_source.h"
#include <chrono>
#include <iomanip>
#include <deque>
#include <map>
#include <mutex>
#include <sstream>
namespace bridge {
// Shared by the event receiver and relay worker; no playback decisions here.
class SpeakerChanges {
    std::mutex mutex;
    std::map<std::string, std::string> values;

    // A response may contain both fresh fields and fields superseded during I/O.
    using Clock = std::chrono::steady_clock;
    std::map<std::string, Clock::time_point> eventTimes;
    std::string sid;
    uint32_t sequence = 0;
    bool sequenced = false;
    struct EditEcho { std::string uri, ordinal, count, transport; };
    std::deque<EditEcho> edits;

public:
    std::string foreignTransport(const std::string &queueUri) {
        std::lock_guard<std::mutex> lock(mutex);
        const auto uri = values.find("AVTransportURI");
        return uri != values.end() && !uri->second.empty() && uri->second != queueUri
                   ? transportSourceClass(uri->second) : "";
    }
private:
    void report(const std::string &source, const std::map<std::string, std::string> &fields) {
        for (const auto &field : fields) {
            // Position advances every second; order it without routine poll logging.
            if (field.first == "RelativeTimePosition") {
                values[field.first] = field.second;
                continue;
            }
            auto old = values.find(field.first);
            if (old != values.end() && old->second == field.second)
                continue;
            const bool uri = field.first == "AVTransportURI" || field.first == "CurrentTrackURI";
            const auto display = [&](const std::string &value) { return uri ? transportSourceLog(value) : value; };
            std::ostringstream line;
            line << "yeney: queue speaker-change source=" << source << " field=" << field.first
                 << " old=" << std::quoted(old == values.end() ? "<unknown>" : display(old->second))
                 << " new=" << std::quoted(display(field.second));
            auto text = line.str();
            for (char &c : text)
                if (c == '\n' || c == '\r')
                    c = ' ';
            std::printf("%s\n", text.c_str());
            values[field.first] = field.second;
        }
    }
public:
    std::map<std::string, std::string> poll(Clock::time_point started,
                                          std::map<std::string, std::string> fields) {
        std::lock_guard<std::mutex> lock(mutex);
        bool stale = false;
        for (auto &field : fields) {
            const auto stamp = eventTimes.find(field.first);
            if (stamp != eventTimes.end() && started < stamp->second) {
                field.second = values.at(field.first);
                stale = true;
            }
        }
        if (stale)
            std::printf("yeney: queue stale poll answer discarded after GENA seq=%u\n", sequence);
        report("poll", fields);
        return fields;
    }
    // Register before SOAP: GENA may arrive before the action response.
    // Exact expected states fence delayed edit echoes without a timed blackout
    // that could swallow a real app Next or pause.
    void expectEdit(std::string uri, unsigned ordinal, unsigned count, std::string transport) {
        std::lock_guard<std::mutex> lock(mutex);
        edits.push_back({std::move(uri), std::to_string(ordinal), std::to_string(count), std::move(transport)});
        if (edits.size() > 64) edits.pop_front();
    }
    void transportSelected(const std::string &uri) {
        std::lock_guard<std::mutex> lock(mutex);
        report("command", {{"AVTransportURI", uri}});
        eventTimes.erase("AVTransportURI");
    }
    void selected() {
        std::lock_guard<std::mutex> lock(mutex);
        // A confirmed explicit seek supersedes observations of the old item.
        for (const auto *name : {"CurrentTrack", "CurrentTrackURI", "RelativeTimePosition", "TransportState"})
            eventTimes.erase(name);
    }
    bool event(const upnp::GenaEvent &event) {
        upnp::XmlNode instance;
        if (!upnp::lastChangeInstance(event.body, instance))
            return false;
        std::map<std::string, std::string> fields;
        for (const auto *name : {"TransportState", "TransportStatus", "CurrentTrack", "CurrentTrackURI",
                                 "NumberOfTracks", "AVTransportURI", "RelativeTimePosition"})
            if (const auto node = instance.child(name))
                fields[name] = node->attribute("val");
        if (const auto node = instance.child("CurrentTransportStatus"))
            fields["TransportStatus"] = node->attribute("val");
        std::lock_guard<std::mutex> lock(mutex);
        // SEQ is scoped to the subscription and wraps as an unsigned counter.
        if (sequenced && sid == event.sid &&
            (event.sequence == sequence || uint32_t(event.sequence - sequence) >= 0x80000000u))
            return false;
        sid = event.sid;
        sequence = event.sequence;
        sequenced = true;
        const auto field = [&fields](const std::string &name) -> std::string {
            const auto found = fields.find(name);
            return found == fields.end() ? std::string{} : found->second;
        };
        const auto matches = [&fields](const std::string &name, const std::string &expected) {
            const auto found = fields.find(name);
            return found == fields.end() || found->second == expected;
        };
        for (const auto &edit : edits) {
            // LastChange may contain only the changed ordinal/count, with the
            // unchanged URI omitted. Never swallow state-only app controls or
            // a playback error as queue feedback.
            const auto state = field("TransportState");
            if ((fields.count("CurrentTrack") || fields.count("NumberOfTracks")) &&
                matches("CurrentTrackURI", edit.uri) && matches("CurrentTrack", edit.ordinal) &&
                matches("NumberOfTracks", edit.count) && matches("AVTransportURI", edit.transport) &&
                (state.empty() || state == "PLAYING" || state == "TRANSITIONING" ||
                 (edit.uri.empty() && state == "STOPPED")) &&
                (field("TransportStatus").empty() || field("TransportStatus") == "OK")) {
                std::printf("yeney: queue self-edit echo fenced tracks=%s ordinal=%s\n",
                            edit.count.c_str(), edit.ordinal.c_str());
                return false;
            }
        }
        const auto now = Clock::now();
        for (const auto &field : fields) eventTimes[field.first] = now;
        report("GENA", fields);
        return true;
    }
};
} // namespace bridge