fix: keep the displayed frame during frame stepping
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
std::lock_guard lock(impl->video->mutex);
|
if (clear_video) {
|
||||||
av_frame_unref(impl->video->frame);
|
std::lock_guard lock(impl->video->mutex);
|
||||||
++impl->video->version;
|
av_frame_unref(impl->video->frame);
|
||||||
|
++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);
|
||||||
|
|||||||
Reference in New Issue
Block a user