fix: count repeated frame steps and retain the forward decoder cursor

This commit is contained in:
2026-10-10 18:35:13 +02:00
parent 2bb7d9b4ee
commit 131acbe41b
6 changed files with 130 additions and 21 deletions
+1 -1
View File
@@ -159,7 +159,7 @@ final class PlayerModel: ObservableObject {
func stepFrame(_ direction: Int32) {
guard canStepFrame, let handle else { return }
let accepted = player_step_frame(handle, direction)
if accepted { seekPending = true }
if accepted { seekPending = false }
commandError = accepted ? nil : "Could not queue the frame step. Please try again."
revealControls()
}
+61 -15
View File
@@ -66,6 +66,7 @@ struct Command {
double seconds = 0;
int direction = 0;
uint64_t epoch = 0;
uint64_t repetitions = 1;
};
void copyError(char *destination, const char *message) noexcept {
@@ -110,6 +111,9 @@ struct Player {
VideoFrame presented{};
int64_t skipPresentedTimestamp = AV_NOPTS_VALUE;
uint64_t skipPresentedOccurrence = 0;
bool frameStepping = false;
// ponytail: long forward runs can make resume preroll long; refresh verified anchors if that latency matters.
int64_t steppingAnchor = AV_NOPTS_VALUE;
std::vector<float> pcm;
std::size_t pcmFrames = 0;
std::size_t pcmOffset = 0;
@@ -139,14 +143,25 @@ struct Player {
bool enqueue(Command command) {
std::lock_guard<std::mutex> lock(mutex);
if (stopping.load(std::memory_order_relaxed) || commands.size() >= commandLimit)
return false;
if (stopping.load(std::memory_order_relaxed)) return false;
if (command.action == Action::Step &&
(!state.has_video || publishedEpoch != requestedEpoch.load(std::memory_order_relaxed) ||
(state.status != PLAYER_PAUSED && state.status != PLAYER_PLAYING && state.status != PLAYER_ENDED)))
return false;
const bool newTimeline = command.action == Action::Open || command.action == Action::Close;
command.epoch = requestedEpoch.load(std::memory_order_relaxed) + (newTimeline ? 1 : 0);
if (command.action == Action::Step && !commands.empty()) {
auto &last = commands.back();
if (last.action == Action::Step && last.direction == command.direction &&
last.epoch == command.epoch) {
if (last.repetitions == std::numeric_limits<uint64_t>::max()) return false;
++last.repetitions;
state.seek_pending = true;
wake.notify_one();
return true;
}
}
if (commands.size() >= commandLimit) return false;
commands.push_back(std::move(command));
if (newTimeline) requestedEpoch.store(commands.back().epoch, std::memory_order_relaxed);
if (commands.back().action == Action::Open) {
@@ -222,6 +237,8 @@ struct Player {
videoBytes = 0;
presented = {};
skipPresentedTimestamp = AV_NOPTS_VALUE;
frameStepping = false;
steppingAnchor = AV_NOPTS_VALUE;
swr_free(&resampler);
av_channel_layout_uninit(&inputLayout);
for (auto &track : tracks) {
@@ -292,6 +309,8 @@ struct Player {
videoBytes = 0;
presented = {};
skipPresentedTimestamp = AV_NOPTS_VALUE;
frameStepping = false;
steppingAnchor = AV_NOPTS_VALUE;
pcmFrames = pcmOffset = 0;
packetPending = demuxEnded = resamplerDrained = false;
receiving = -1;
@@ -385,6 +404,18 @@ struct Player {
if (!format || status == PLAYER_ERROR || status == PLAYER_EMPTY) return;
if (status == PLAYER_ENDED) seek(0, false);
if (status == PLAYER_PLAYING) return;
if (frameStepping) {
VideoFrame selected = std::move(presented);
const int64_t timestamp = presentationTimestamp(selected.frame.get());
const uint64_t occurrence = selected.timestampOccurrence;
const double selectedPts = selected.pts;
// Audio was skipped while stepping. Rebuild it once, only on resume.
seekVideo(steppingAnchor, selectedPts);
videoEnd = selected.end;
presentVideo(std::move(selected));
skipPresentedTimestamp = timestamp;
skipPresentedOccurrence = occurrence;
}
if (tracks[0].codec) {
std::string deviceError;
if (!output.start(deviceError))
@@ -462,6 +493,13 @@ struct Player {
if (frame.pts + sampleTime * 0.5 < seekTarget) continue;
presentVideo(std::move(frame));
}
if (frameStepping && direction > 0) {
// The stepping decoder is already positioned immediately after the displayed frame.
auto next = nextSteppingFrame();
if (next.frame) presentVideo(std::move(next));
position = presented.pts;
return;
}
VideoFrame selected = std::move(presented);
const int64_t reference = presentationTimestamp(selected.frame.get());
const uint64_t referenceOccurrence = selected.timestampOccurrence;
@@ -497,7 +535,14 @@ struct Player {
break;
}
} else {
if (!beforeReference) break;
if (!beforeReference) {
if (previous.frame) {
// Preserve the adjacent successor for a subsequent forward step.
videoBytes += frame.bytes;
video.push_front(std::move(frame));
}
break;
}
if (timestamp >= beginning) previous = std::move(frame);
}
}
@@ -510,18 +555,11 @@ struct Player {
static_cast<long double>(reference) - backoff));
backoff *= 2;
}
const int64_t selectedTimestamp = presentationTimestamp(selected.frame.get());
const uint64_t selectedOccurrence = selected.timestampOccurrence;
const double selectedPts = selected.pts;
// Reuse the verified preroll anchor: timestamp seeks can land after a B-frame's PTS.
// Audio is rebuilt and trimmed to selectedPts, while video skips the displayed frame.
seekVideo(target, selectedPts);
if (interrupted()) throw Cancelled{};
videoEnd = selected.end;
position = selected.pts;
steppingAnchor = target;
frameStepping = true;
presentVideo(std::move(selected));
skipPresentedTimestamp = selectedTimestamp;
skipPresentedOccurrence = selectedOccurrence;
position = selectedPts;
}
void execute(const Command &command) {
@@ -794,6 +832,7 @@ struct Player {
bool tick() {
if (!format || interrupted() || status == PLAYER_ENDED || status == PLAYER_ERROR) return false;
if (frameStepping) return false;
bool progressed = pumpAudio();
if (status == PLAYER_PAUSED) {
if (!previewShown && !video.empty()) {
@@ -845,8 +884,15 @@ struct Player {
errorRequested = false;
haveFailure = true;
} else if (!commands.empty()) {
command = std::move(commands.front());
commands.pop_front();
auto &next = commands.front();
if (next.action == Action::Step && next.repetitions > 1 &&
next.epoch == requestedEpoch.load(std::memory_order_relaxed)) {
command = {Action::Step, {}, 0, next.direction, next.epoch};
--next.repetitions;
} else {
command = std::move(next);
commands.pop_front();
}
haveCommand = true;
}
}