// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 #pragma once #include "queue_item_store.h" #include "upnp/stream_server.h" #include #include #include #include namespace queue_audio { // A lazy route has an optional immutable WAV declaration and a separate // producer. Deferred routes publish the natural declaration only after binding. Selection is queued exactly once; neither HTTP nor HEAD executes // an LMS command. The room callback must only enqueue work and return promptly. class LazyItem { std::shared_ptr declaration, produced; std::function activate; std::mutex mutex; std::condition_variable changed; bool requested = false, cancelled = false, activationClaimed = false; unsigned issuedCommands = 0; std::vector readers; public: LazyItem(std::shared_ptr declared, std::function enqueue, bool deferred = false) : declaration(std::move(declared)), activate(std::move(enqueue)) { if ((!deferred && (!declaration || !declaration->contentLength())) || !activate) throw std::invalid_argument("lazy item needs an immutable declaration and selector"); } // The room worker must claim immediately before an LMS mutation. Enqueueing // from HTTP alone is not authority: the request may have left meanwhile. bool claimActivation() { std::lock_guard lock(mutex); if (cancelled || produced || !requested || readers.empty() || activationClaimed) return false; const bool connected = std::any_of(readers.begin(), readers.end(), [](auto *request) { return !request->aborted() && !request->peerClosed(); }); if (!connected) { cancelled = true; changed.notify_all(); return false; } activationClaimed = true; return true; } bool publish(std::shared_ptr item) { if (!item || (declaration && (item->room() != declaration->room() || item->descriptor().rate != declaration->descriptor().rate || item->descriptor().kind != declaration->descriptor().kind || item->contentLength() != declaration->contentLength()))) throw std::invalid_argument("lazy producer differs from declared WAV"); std::lock_guard lock(mutex); if (cancelled) return false; // late producer cannot resurrect an abandoned window if (produced && produced != item) throw std::logic_error("lazy producer already bound"); produced = std::move(item); changed.notify_all(); return true; } std::shared_ptr ready() { std::lock_guard lock(mutex); return cancelled ? nullptr : produced; } bool abandoned() { std::lock_guard lock(mutex); return cancelled; } bool waiting() { std::lock_guard lock(mutex); return !cancelled && !produced && !readers.empty(); } unsigned commandCount() { std::lock_guard lock(mutex); return issuedCommands; } void commandIssued() { std::lock_guard lock(mutex); ++issuedCommands; } void cancel() { std::lock_guard lock(mutex); cancelled = true; changed.notify_all(); } std::shared_ptr resolve(upnp::StreamRequest &request, std::chrono::milliseconds budget = std::chrono::seconds(15)) { const auto deadline = std::chrono::steady_clock::now() + budget; std::unique_lock lock(mutex); if (cancelled || request.aborted() || request.peerClosed()) return {}; readers.push_back(&request); // Every exit, including an exception from enqueue, releases its claim. // A surviving concurrent range reader keeps the same activation alive. struct ReaderLease { LazyItem &owner; std::unique_lock &lock; upnp::StreamRequest *request; ~ReaderLease() { if (!lock.owns_lock()) lock.lock(); owner.readers.erase(std::find(owner.readers.begin(), owner.readers.end(), request)); if (owner.readers.empty() && !owner.produced) { owner.cancelled = true; owner.changed.notify_all(); } } } lease{*this, lock, &request}; if (!produced && !requested) { requested = true; lock.unlock(); try { activate(); } catch (...) { cancel(); throw; } lock.lock(); } while (!produced && !cancelled && !request.aborted() && !request.peerClosed()) { if (std::chrono::steady_clock::now() >= deadline) return {}; changed.wait_until(lock, std::min(deadline, std::chrono::steady_clock::now() + std::chrono::milliseconds(50))); } return cancelled || request.aborted() || request.peerClosed() ? nullptr : produced; } }; } // namespace queue_audio