fix: keep the displayed frame during frame stepping

This commit is contained in:
2026-10-10 18:12:25 +02:00
parent a99b0c1168
commit 2bb7d9b4ee
4 changed files with 14 additions and 11 deletions
+2 -2
View File
@@ -35,7 +35,7 @@ Opening video fits the window to its display aspect ratio, including non-square
Single-key shortcuts apply only to the playback window and do not intercept text editing, file-picker input, or Command/Control/Option combinations. Held toggle keys do not repeatedly toggle; held `J`/`L` continue seeking and held `,`/`.` continue stepping. `C` captions remains unbound. Single-key shortcuts apply only to the playback window and do not intercept text editing, file-picker input, or Command/Control/Option combinations. Held toggle keys do not repeatedly toggle; held `J`/`L` continue seeking and held `,`/`.` continue stepping. `C` captions remains unbound.
Frame stepping uses decoded presentation timestamps, not an average-FPS time offset, including variable-frame-rate video and reordered B-frames. Steps start from the displayed frame, stop at the first/last frame, and keep the timeline paused. Audio-only files disable stepping; normal playback resumes with audio synchronized to the selected frame. Frame stepping uses decoded presentation timestamps, not an average-FPS time offset, including variable-frame-rate video and reordered B-frames. Steps start from the displayed frame, stop at the first/last frame, and keep the timeline paused. The last presented image remains visible during decoder preroll and queued steps; opening/closing media still clears the old image. Audio-only files disable stepping; normal playback resumes with audio synchronized to the selected frame.
- Existing shortcuts remain: **⌘O** opens files, **←/→** seeks five seconds, **↑/↓** adjusts volume by five percent, **⌃⌘F** toggles fullscreen, and **Esc** leaves fullscreen. Double-clicking video also toggles fullscreen. - Existing shortcuts remain: **⌘O** opens files, **←/→** seeks five seconds, **↑/↓** adjusts volume by five percent, **⌃⌘F** toggles fullscreen, and **Esc** leaves fullscreen. Double-clicking video also toggles fullscreen.
- The timeline slider seeks on release and holds the requested position until queued seeking finishes. Both slider tracks span the native thumb-center travel range; the orange fill ends at the current volume thumb center. The volume thumb is opaque light gray. - The timeline slider seeks on release and holds the requested position until queued seeking finishes. Both slider tracks span the native thumb-center travel range; the orange fill ends at the current volume thumb center. The volume thumb is opaque light gray.
@@ -72,6 +72,6 @@ Visual correctness, perceived A/V synchronization, and control usability still n
Local regular files, automatically selected best audio/video tracks, software decoding, and SDR output. PQ/HLG HDR is rejected rather than rendered incorrectly. No subtitles, selectable tracks, VideoToolbox decoding, rotation/deinterlacing, or network playback yet. Mid-track audio format changes report an error. Local regular files, automatically selected best audio/video tracks, software decoding, and SDR output. PQ/HLG HDR is rejected rather than rendered incorrectly. No subtitles, selectable tracks, VideoToolbox decoding, rotation/deinterlacing, or network playback yet. Mid-track audio format changes report an error.
Frame stepping re-decodes from an earlier keyframe and rebuilds audio at the selected timestamp. Long-GOP or unindexed files can therefore step slowly; frames are not cached across the entire file. Stepping requires usable decoded presentation timestamps and reports an error rather than inventing frame times when they are absent. Frame stepping re-decodes from an earlier keyframe and rebuilds audio at the selected timestamp. Intermediate decoded frames are discarded, but inter-frame dependencies still require decoding them. Every accepted repeated step counts; commands are not coalesced or skipped. Long-GOP or unindexed files can therefore step slowly even though the image no longer blanks; frames are not cached across the entire file. Stepping requires usable decoded presentation timestamps and reports an error rather than inventing frame times when they are absent.
Audio buffering is bounded to one second plus one decoded block (up to four seconds); video is bounded to 64 frames/128 MiB. Extremely interleaved files can drop far-future video frames to avoid starving audio; the ceiling and upgrade path are marked at `queueVideo()`. Device/stream latency is accounted for, but device changes during playback are not handled yet. Audio buffering is bounded to one second plus one decoded block (up to four seconds); video is bounded to 64 frames/128 MiB. Extremely interleaved files can drop far-future video frames to avoid starving audio; the ceiling and upgrade path are marked at `queueVideo()`. Device/stream latency is accounted for, but device changes during playback are not handled yet.
+2 -2
View File
@@ -21,8 +21,8 @@ public:
// Atomic target gain; callback slews to it within 240 frames (5 ms). // Atomic target gain; callback slews to it within 240 frames (5 ms).
void set_volume(float normalized0to1) noexcept; void set_volume(float normalized0to1) noexcept;
float volume() const noexcept; float volume() const noexcept;
// Called with audio stopped. Clears audio and video from the old timeline. // Called with audio stopped. Clears audio; optionally keeps the displayed frame.
void reset(); void reset(bool clear_video = true);
std::size_t write(const float *samples, std::size_t frames, double pts); std::size_t write(const float *samples, std::size_t frames, double pts);
std::size_t queued() const; std::size_t queued() const;
// Latency-aware presented position; NaN until an audio callback has run. // Latency-aware presented position; NaN until an audio callback has run.
+4 -3
View File
@@ -269,9 +269,9 @@ struct Player {
check(avcodec_open2(track.codec, decoder, nullptr), "Opening decoder"); check(avcodec_open2(track.codec, decoder, nullptr), "Opening decoder");
} }
void resetTimeline(double target) { void resetTimeline(double target, bool clearVideo = true) {
output.pause(); output.pause();
output.reset(); output.reset(clearVideo);
for (auto &track : tracks) { for (auto &track : tracks) {
if (!track.codec) continue; if (!track.codec) continue;
avcodec_flush_buffers(track.codec); avcodec_flush_buffers(track.codec);
@@ -425,7 +425,8 @@ struct Player {
output.pause(); output.pause();
check(av_seek_frame(format, tracks[1].index, timestamp, AVSEEK_FLAG_BACKWARD), check(av_seek_frame(format, tracks[1].index, timestamp, AVSEEK_FLAG_BACKWARD),
"Seeking video keyframe"); "Seeking video keyframe");
resetTimeline(target); // Decoder preroll must not blank the last presented frame between steps.
resetTimeline(target, false);
status = PLAYER_PAUSED; status = PLAYER_PAUSED;
} }
+3 -1
View File
@@ -395,13 +395,15 @@ void Output::set_volume(float normalized0to1) noexcept {
float Output::volume() const noexcept { float Output::volume() const noexcept {
return impl->targetGain.load(std::memory_order_relaxed); return impl->targetGain.load(std::memory_order_relaxed);
} }
void Output::reset() { void Output::reset(bool clear_video) {
impl->read.store(0); impl->written.store(0); impl->read.store(0); impl->written.store(0);
impl->origin = 0; impl->origin = 0;
impl->anchorPTS.store(NAN); impl->frozen.store(NAN); impl->endPTS.store(0); impl->anchorPTS.store(NAN); impl->frozen.store(NAN); impl->endPTS.store(0);
if (clear_video) {
std::lock_guard lock(impl->video->mutex); std::lock_guard lock(impl->video->mutex);
av_frame_unref(impl->video->frame); av_frame_unref(impl->video->frame);
++impl->video->version; ++impl->video->version;
}
} }
size_t Output::write(const float *samples, size_t frames, double pts) { size_t Output::write(const float *samples, size_t frames, double pts) {
uint64_t w = impl->written.load(std::memory_order_relaxed); uint64_t w = impl->written.load(std::memory_order_relaxed);