fix: count repeated frame steps and retain the forward decoder cursor
This commit is contained in:
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user