feat: report authoritative playback progress from shared backend
This commit is contained in:
+95
-6
@@ -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));
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user