feat: report authoritative playback progress from shared backend

This commit is contained in:
2026-10-11 19:29:35 +02:00
parent a4c38bfa51
commit cc99a2a4f8
14 changed files with 1369 additions and 62 deletions
+95 -6
View File
@@ -1,6 +1,7 @@
#include "player.h"
#include "player_macos.h"
#include "output.hpp"
#include "progress_reporter.hpp"
extern "C" {
#include <libavcodec/avcodec.h>
@@ -85,6 +86,8 @@ struct Command {
double sourceDuration = 0;
bool network = false;
bool live = false;
std::string mediaURL{};
double initialPosition = 0;
};
void copyError(char *destination, const char *message) noexcept {
@@ -223,14 +226,19 @@ struct Player {
PlayerStatus status = PLAYER_EMPTY;
bool previewShown = false;
std::array<char, 512> error{};
std::unique_ptr<playback::ProgressReporter> reporter;
std::thread worker;
Player() : worker([this] { run(); }) {}
~Player() {
stopping.store(true, std::memory_order_relaxed);
requestedEpoch.fetch_add(1, std::memory_order_relaxed);
wake.notify_one();
~Player() { shutdown(); }
void shutdown() {
if (!stopping.exchange(true, std::memory_order_relaxed)) {
requestedEpoch.fetch_add(1, std::memory_order_relaxed);
wake.notify_one();
}
if (worker.joinable()) worker.join();
if (reporter) reporter->finish();
}
bool enqueue(Command command) {
@@ -380,6 +388,33 @@ struct Player {
}
std::memcpy(state.error, error.data(), error.size());
++state.revision;
if (reporter && reportingInputsHealthy()) reporter->observe(state);
}
bool reportingInputsHealthy() const {
for (const auto &input : inputs) if (input && input->error()) return false;
return true;
}
void reportBoundary() {
if (!reporter || !reportingInputsHealthy()) return;
// This is the old worker-owned timeline, even if a new open has already
// replaced the public state with OPENING. Never sample a queued seek target.
PlayerState snapshot{};
snapshot.status = status;
snapshot.duration = duration;
snapshot.has_video = tracks[1].codec != nullptr;
snapshot.can_seek = canSeek;
{
std::lock_guard<std::mutex> lock(mutex);
snapshot.seek_pending = networkSeeksPending() ||
std::any_of(commands.begin(), commands.end(), [this](const Command &command) {
return (command.action == Action::Seek || command.action == Action::Step) &&
command.epoch == activeEpoch;
});
}
snapshot.position = status == PLAYER_PLAYING && !snapshot.seek_pending ? clockPosition() : position;
reporter->observe(snapshot);
}
void release() {
@@ -420,6 +455,7 @@ struct Player {
// Preserve messages that point into one of this object's own state buffers.
std::array<char, 512> saved{};
copyError(saved.data(), message);
if (reporter) reporter->closeMedia();
release();
error = saved;
status = PLAYER_ERROR;
@@ -519,6 +555,8 @@ struct Player {
}
void open(const Command &command) {
reportBoundary();
if (reporter) reporter->begin(command.mediaURL, command.live);
release();
network = command.network;
status = PLAYER_OPENING;
@@ -627,6 +665,8 @@ struct Player {
}
}
status = PLAYER_PAUSED;
if (command.initialPosition > 0 && command.initialPosition < duration && canSeek)
seek(command.initialPosition, false);
publish();
}
@@ -868,13 +908,23 @@ struct Player {
void execute(const Command &command) {
if (command.epoch != requestedEpoch.load(std::memory_order_relaxed)) return;
activeEpoch = command.epoch;
if (reporter && (command.action == Action::Seek || command.action == Action::Step)) {
// Local seeks can finish without a pending publication. Preserve
// the transition so their acknowledgement still emits a record.
std::lock_guard<std::mutex> lock(mutex);
reporter->observe(state);
}
switch (command.action) {
case Action::Open: open(command); break;
case Action::Play: start(); break;
case Action::Pause: pause(); break;
case Action::Seek: seek(command.seconds, status == PLAYER_PLAYING); break;
case Action::Step: step(command.direction); break;
case Action::Close: release(); break;
case Action::Close:
reportBoundary();
if (reporter) reporter->closeMedia();
release();
break;
}
publish();
}
@@ -1294,7 +1344,11 @@ struct Player {
try { fail("Unexpected playback failure"); } catch (...) {}
}
}
try { release(); } catch (...) {}
try {
reportBoundary();
if (reporter) reporter->closeMedia();
release();
} catch (...) {}
}
};
@@ -1317,12 +1371,45 @@ extern "C" {
Player *player_create(void) {
try { return new Player; } catch (...) { return nullptr; }
}
bool player_set_progress_file(Player *player, const char *path, char *error) {
if (error) error[0] = '\0';
try {
if (!player || !path || !*path) throw std::runtime_error("A progress file path is required");
std::lock_guard<std::mutex> lock(player->mutex);
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);
return true;
} catch (const std::exception &failure) {
if (error) copyError(error, failure.what());
} catch (...) {
if (error) copyError(error, "Could not initialize progress reporting");
}
return false;
}
bool player_get_progress_error(Player *player, char *error) {
if (error) error[0] = '\0';
if (!player || !player->reporter) return false;
const auto failure = player->reporter->error();
if (failure.empty()) return false;
if (error) copyError(error, failure.c_str());
return true;
}
bool player_shutdown(Player *player, char *progress_error) {
if (progress_error) progress_error[0] = '\0';
if (!player) return false;
player->shutdown();
return !player_get_progress_error(player, progress_error);
}
bool player_open(Player *player, const char *path) {
if (!path || !*path) return false;
return submit(player, [=] { return player->enqueue({Action::Open, path}); });
}
bool player_open_network(Player *player, const PlayerNetworkSource *source) {
if (!source || !std::isfinite(source->duration) || source->duration < 0 ||
!std::isfinite(source->initial_position) || source->initial_position < 0 ||
(source->media_url && *source->media_url && !playback::ProgressReporter::validMediaURL(source->media_url)) ||
!validNetworkURL(source->video_url) ||
(source->audio_url && *source->audio_url && !validNetworkURL(source->audio_url)) ||
!validHeaders(source->video_headers) || !validHeaders(source->audio_headers)) return false;
@@ -1334,6 +1421,8 @@ bool player_open_network(Player *player, const PlayerNetworkSource *source) {
command.sourceDuration = source->duration;
command.network = true;
command.live = source->is_live;
command.mediaURL = source->media_url ? source->media_url : "";
command.initialPosition = source->initial_position;
return player->enqueue(std::move(command));
});
}