feat: receive ordered stream playback queues
This commit is contained in:
+20
-2
@@ -197,7 +197,7 @@ struct Player {
|
||||
PlayerStatus status = PLAYER_EMPTY;
|
||||
bool previewShown = false;
|
||||
std::array<char, 512> error{};
|
||||
std::unique_ptr<playback::ProgressReporter> reporter;
|
||||
std::unique_ptr<playback::ProgressReports> reporter;
|
||||
bool remoteReporting = false;
|
||||
std::thread worker;
|
||||
|
||||
@@ -1432,7 +1432,7 @@ bool player_set_progress_file(Player *player, const char *path, char *error) {
|
||||
if (player->reporter || player->stopping.load(std::memory_order_relaxed) ||
|
||||
player->state.status != PLAYER_EMPTY || !player->commands.empty())
|
||||
throw std::runtime_error("Progress reporting must be initialized once, before opening media");
|
||||
player->reporter = std::make_unique<playback::ProgressReporter>(path);
|
||||
player->reporter = std::make_unique<playback::ProgressReports>(path);
|
||||
return true;
|
||||
} catch (const std::exception &failure) {
|
||||
if (error) copyError(error, failure.what());
|
||||
@@ -1441,6 +1441,24 @@ bool player_set_progress_file(Player *player, const char *path, char *error) {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
bool player_register_progress_file(Player *player, const char *identity, const char *path, char *error) {
|
||||
if (error) error[0] = '\0';
|
||||
try {
|
||||
if (!player || !identity || !path) throw std::runtime_error("Queue progress identity and path are required");
|
||||
std::lock_guard<std::mutex> lock(player->mutex);
|
||||
if (player->stopping.load(std::memory_order_relaxed) ||
|
||||
player->state.status != PLAYER_EMPTY || !player->commands.empty())
|
||||
throw std::runtime_error("Queue reports must be registered before opening media");
|
||||
if (!player->reporter) player->reporter = std::make_unique<playback::ProgressReports>();
|
||||
player->reporter->registerFile(identity, path);
|
||||
return true;
|
||||
} catch (const std::exception &failure) {
|
||||
if (error) copyError(error, failure.what());
|
||||
} catch (...) {
|
||||
if (error) copyError(error, "Could not register queue progress reporting");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
bool player_get_progress_error(Player *player, char *error) {
|
||||
if (error) error[0] = '\0';
|
||||
if (!player || !player->reporter) return false;
|
||||
|
||||
@@ -40,6 +40,9 @@ Player *player_create(void);
|
||||
// Opt-in reporting must be initialized before opening media. Creates a new 0600
|
||||
// file exclusively; errors never change playback state. Error buffers hold 512 bytes.
|
||||
bool player_set_progress_file(Player *player, const char *path, char *error);
|
||||
// Register immutable queue destinations before opening any media. Each path is new;
|
||||
// revisits append only to the original owned inode, and unmatched identities are not reported.
|
||||
bool player_register_progress_file(Player *player, const char *identity, const char *path, char *error);
|
||||
bool player_get_progress_error(Player *player, char *error);
|
||||
// Stops the worker and drains final reporting, allowing errors to be shown before
|
||||
// destroying the handle. Idempotent; do not call from an audio callback.
|
||||
|
||||
@@ -28,7 +28,7 @@ void appendNumber(std::string &record, double value) {
|
||||
}
|
||||
|
||||
ProgressReporter::ProgressReporter(const std::string &path, std::function<double()> clock)
|
||||
: clock(clock ? std::move(clock) : monotonicSeconds) {
|
||||
: path(path), clock(clock ? std::move(clock) : monotonicSeconds) {
|
||||
if (path.empty() || path.find('\0') != std::string::npos)
|
||||
throw std::invalid_argument("The progress file path must be nonempty and contain no NUL bytes");
|
||||
descriptor = ::open(path.c_str(), O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW | O_CLOEXEC, 0600);
|
||||
@@ -49,6 +49,8 @@ ProgressReporter::ProgressReporter(const std::string &path, std::function<double
|
||||
throw std::runtime_error(std::string("Could not verify progress file permissions: ") + std::strerror(errno));
|
||||
if ((info.st_mode & 07777) != 0600)
|
||||
throw std::runtime_error("The progress file must have exactly owner read/write permissions (0600)");
|
||||
device = info.st_dev;
|
||||
inode = info.st_ino;
|
||||
writer = std::thread([this] { writeRecords(); });
|
||||
} catch (...) {
|
||||
::close(descriptor);
|
||||
@@ -59,6 +61,31 @@ ProgressReporter::ProgressReporter(const std::string &path, std::function<double
|
||||
|
||||
ProgressReporter::~ProgressReporter() { finish(); }
|
||||
|
||||
void ProgressReporter::resume() {
|
||||
if (!finished) return;
|
||||
if (disabled.load(std::memory_order_acquire))
|
||||
throw std::runtime_error("Cannot resume a failed progress report");
|
||||
descriptor = ::open(path.c_str(), O_WRONLY | O_APPEND | O_NONBLOCK | O_NOFOLLOW | O_CLOEXEC);
|
||||
if (descriptor < 0) throw std::runtime_error("Could not reopen the owned progress report");
|
||||
struct stat info{};
|
||||
if (::fstat(descriptor, &info) != 0 || !S_ISREG(info.st_mode) ||
|
||||
info.st_dev != device || info.st_ino != inode || info.st_uid != ::geteuid() ||
|
||||
info.st_nlink != 1 || (info.st_mode & 07777) != 0600) {
|
||||
::close(descriptor);
|
||||
descriptor = -1;
|
||||
throw std::runtime_error("The owned progress report was replaced or is no longer private");
|
||||
}
|
||||
try {
|
||||
stopping = false;
|
||||
writer = std::thread([this] { writeRecords(); });
|
||||
finished = false;
|
||||
} catch (...) {
|
||||
::close(descriptor);
|
||||
descriptor = -1;
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
bool ProgressReporter::validMediaURL(std::string_view url) noexcept {
|
||||
constexpr std::string_view prefix = "https://www.youtube.com/watch?v=";
|
||||
if (url.size() != prefix.size() + 11 || url.substr(0, prefix.size()) != prefix) return false;
|
||||
@@ -261,4 +288,56 @@ void ProgressReporter::finish() noexcept {
|
||||
wake.notify_all();
|
||||
if (writer.joinable()) writer.join();
|
||||
}
|
||||
|
||||
ProgressReports::ProgressReports(const std::string &path)
|
||||
: single(std::make_unique<ProgressReporter>(path)), active(single.get()) {}
|
||||
|
||||
void ProgressReports::registerFile(const std::string &identity, const std::string &path) {
|
||||
if (single || !ProgressReporter::validMediaIdentity(identity) || entries.size() >= 4096 ||
|
||||
path.empty() || path.front() != '/' || path.find('\0') != std::string::npos ||
|
||||
entries.contains(identity))
|
||||
throw std::runtime_error("Invalid or duplicate queue progress registration");
|
||||
for (const auto &[key, entry] : entries)
|
||||
if (entry.path == path) throw std::runtime_error("Duplicate queue progress report path");
|
||||
entries.emplace(identity, Entry{path, nullptr});
|
||||
}
|
||||
|
||||
void ProgressReports::begin(const std::string &identity, bool live) noexcept {
|
||||
std::lock_guard lock(mutex);
|
||||
if (single) { active->begin(identity, live); return; }
|
||||
const auto found = entries.find(identity);
|
||||
auto *next = found == entries.end() ? nullptr : found->second.reporter.get();
|
||||
if (active != next && active) active->finish();
|
||||
active = nullptr; // A failed or unregistered identity must never reuse the old report.
|
||||
if (found == entries.end()) return;
|
||||
try {
|
||||
auto &entry = found->second;
|
||||
if (!entry.reporter) entry.reporter = std::make_unique<ProgressReporter>(entry.path);
|
||||
else entry.reporter->resume();
|
||||
active = entry.reporter.get();
|
||||
active->begin(identity, live);
|
||||
} catch (const std::exception &exception) {
|
||||
try { if (failure.empty()) failure = exception.what(); } catch (...) {}
|
||||
} catch (...) {
|
||||
try { if (failure.empty()) failure = "Could not select queue progress report"; } catch (...) {}
|
||||
}
|
||||
}
|
||||
|
||||
void ProgressReports::finish() noexcept {
|
||||
std::lock_guard lock(mutex);
|
||||
if (active) active->finish();
|
||||
}
|
||||
|
||||
std::string ProgressReports::error() const {
|
||||
std::lock_guard lock(mutex);
|
||||
if (!failure.empty()) return failure;
|
||||
if (single) return single->error();
|
||||
for (const auto &[identity, entry] : entries) {
|
||||
if (entry.reporter) {
|
||||
auto message = entry.reporter->error();
|
||||
if (!message.empty()) return message;
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
#include <functional>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <sys/types.h>
|
||||
#include <string_view>
|
||||
#include <thread>
|
||||
|
||||
@@ -26,6 +29,8 @@ public:
|
||||
void closeMedia() noexcept;
|
||||
// Call after the decoder joins; drains queued records and joins the writer.
|
||||
void finish() noexcept;
|
||||
// Reopen only this session's original private inode, never an arbitrary existing file.
|
||||
void resume();
|
||||
std::string error() const;
|
||||
|
||||
private:
|
||||
@@ -36,6 +41,9 @@ private:
|
||||
bool eligible = false;
|
||||
} snapshot;
|
||||
std::string mediaURL;
|
||||
std::string path;
|
||||
dev_t device = 0;
|
||||
ino_t inode = 0;
|
||||
std::function<double()> clock;
|
||||
bool live = false;
|
||||
bool hasSnapshot = false;
|
||||
@@ -58,4 +66,24 @@ private:
|
||||
void disable(const char *message, int osError = 0) noexcept;
|
||||
void writeRecords() noexcept;
|
||||
};
|
||||
|
||||
// Routes decoder-owned snapshots by immutable media identity; only the active writer runs.
|
||||
class ProgressReports {
|
||||
public:
|
||||
explicit ProgressReports(const std::string &path);
|
||||
ProgressReports() = default;
|
||||
void registerFile(const std::string &identity, const std::string &path);
|
||||
void begin(const std::string &identity, bool live = false) noexcept;
|
||||
void observe(const PlayerState &state) noexcept { if (active) active->observe(state); }
|
||||
void closeMedia() noexcept { if (active) active->closeMedia(); }
|
||||
void finish() noexcept;
|
||||
std::string error() const;
|
||||
private:
|
||||
struct Entry { std::string path; std::unique_ptr<ProgressReporter> reporter; };
|
||||
std::unique_ptr<ProgressReporter> single;
|
||||
std::map<std::string, Entry> entries;
|
||||
ProgressReporter *active = nullptr;
|
||||
mutable std::mutex mutex;
|
||||
std::string failure;
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user