// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 #pragma once #include #include "volume_control.h" #include "queue_uri_log.h" #include "podcast_settings.h" #include #include #include #include namespace bridge { struct ModeStatus { std::string requested = "stream", active = "stream", state = "active", reason = "default-stream"; std::string path; bool owned = false; uint64_t occurrence = 0; uint32_t renderedRate = 0; std::vector ownedUris; std::string currentUri, lastFault, title; }; // Passive snapshot registry, never a mode controller. A single room worker // publishes queue observations and the complete diagnostic file synchronously. class RoomStatus { mutable std::mutex mutex; ModeStatus value; public: ModeStatus status() const { std::lock_guard lock(mutex); return value; } void publish(ModeStatus v) { std::lock_guard lock(mutex); value = std::move(v); } static std::string quote(const std::string &t) { std::string out = "\""; for (unsigned char c : t) { if (c == '\n') out += "\\n"; else if (c == '\r') out += "\\r"; else if (c == '\t') out += "\\t"; else { if (c == '"' || c == '\\') out += '\\'; out += c >= 32 ? char(c) : '?'; } } return out + '"'; } static std::string json(const ModeStatus &v) { auto q = [](const std::string &t) { std::string out = "\""; for (unsigned char c : t) { if (c == '"' || c == '\\') out += '\\'; out += c >= 32 ? char(c) : '?'; } return out + '"'; }; std::ostringstream out; out << "{\"requested\":" << q(v.requested) << ",\"active\":" << q(v.active) << ",\"state\":" << q(v.state) << ",\"reason\":" << q(v.reason); const auto &volume = roomVolumeSettings(); out << ",\"lms_volume\":" << q(volume.speaker ? "speaker" : "off") << ",\"lms_volume_source\":" << q(volume.source) << ",\"volume_max\":" << volume.maximum; for (const auto &setting : roomPodcastSettings()) out << "," << q(setting.key) << ":" << q(setting.enabled ? "yes" : "no") << "," << q(std::string(setting.key)+"_source") << ":" << q(setting.source); if (!v.path.empty()) out << ",\"path\":" << q(v.path); if (v.active == "queue") { out << ",\"occurrence\":" << v.occurrence << ",\"owned\":" << (v.owned ? "true" : "false") << ",\"rendered_rate\":" << v.renderedRate << ",\"current_owned_item\":" << q(queueUriLog(v.currentUri)) << ",\"last_fault\":" << q(queueUriLog(v.lastFault)) << ",\"owned_item_uris\":["; for (size_t i = 0; i < v.ownedUris.size(); ++i) { if (i) out << ','; out << q(queueUriLog(v.ownedUris[i])); } out << "]"; if (v.path == "relay") out << ",\"title\":" << quote(v.title); out << '}'; } else { // Preserve the stream diagnostic's established representation. out << ",\"occurrence\":0,\"epoch\":0,\"generation\":1,\"owned\":false,\"mirrored_ahead\":0," "\"last_fallback\":\"\",\"handoff_gap_ms\":-1,\"latched\":false"; if (v.requested == "queue") out << ",\"lms_cli_slow_replies\":0,\"lms_cli_failed_replies\":0"; out << '}'; } return out.str(); } }; inline std::atomic &statusRegistry() { static auto *registry = new std::atomic(nullptr); return *registry; } inline RoomStatus *roomMode() { return statusRegistry().load(); } inline void setRoomStatus(RoomStatus *value) { statusRegistry() = value; } } // namespace bridge