fix: skip disposable pictures during local seek preroll

This commit is contained in:
2026-10-18 20:11:39 +02:00
parent 2e61bd7864
commit ddf53ea606
5 changed files with 97 additions and 3 deletions
+68
View File
@@ -0,0 +1,68 @@
// Exercise the real local seek/decode/presentation loop without worker races.
#include "../Sources/Core/player.cpp"
#include <cstdio>
#include <cstdlib>
static void require(bool condition, const char *message) {
if (!condition) { std::fprintf(stderr, "FAIL: %s\n", message); std::exit(1); }
}
static std::vector<uint8_t> pixels(const AVFrame *frame) {
const auto format = static_cast<AVPixelFormat>(frame->format);
const int size = av_image_get_buffer_size(format, frame->width, frame->height, 1);
require(size > 0, "preview has readable pixels");
std::vector<uint8_t> bytes(size);
require(av_image_copy_to_buffer(bytes.data(), size, frame->data, frame->linesize,
format, frame->width, frame->height, 1) == size, "copy preview pixels");
return bytes;
}
int main(int argc, char **argv) {
require(argc > 1, "provide local media fixtures");
for (int i = 1; i < argc; ++i) {
av_log_set_level(AV_LOG_ERROR);
Player player;
player.shutdown();
player.stopping.store(false);
player.activeEpoch = player.requestedEpoch.load();
player.open(Command{Action::Open, argv[i]});
require(player.tracks[1].codec && player.canSeek, "seekable local video");
player.output.set_volume(0);
for (bool playing : {false, true}) {
for (double fraction : {.01, .79, .21, .59}) {
const double target = player.duration * fraction;
// Full reference decoding supplies an independent expected picture,
// including disposable B-frames at or immediately after the target.
player.seek(target, false);
VideoFrame expected{};
const auto referenceStart = Clock::now();
while (!expected.frame && Clock::now() - referenceStart < std::chrono::seconds(5)) {
player.decodeOne(true);
while (!player.video.empty()) {
auto frame = std::move(player.video.front());
player.videoBytes -= frame.bytes;
player.video.pop_front();
if (frame.pts + sampleTime * .5 >= target) {
expected = std::move(frame);
break;
}
}
}
require(bool(expected.frame), "full decode supplies reference picture");
const auto start = Clock::now();
player.seek(target, playing);
while ((!player.presented.frame || player.presented.pts < target - .002) &&
Clock::now() - start < std::chrono::seconds(5))
player.tick();
const double elapsed = std::chrono::duration<double, std::milli>(Clock::now() - start).count();
std::printf("local-seek fixture=%s playing=%d target=%.3f preview=%.3f latency_ms=%.3f\n",
argv[i], playing, target, player.presented.pts, elapsed);
require(player.presented.frame && player.presented.pts == expected.pts &&
pixels(player.presented.frame.get()) == pixels(expected.frame.get()),
"seek presents the exact fully decoded picture, including B-frame targets");
require(player.status == (playing ? PLAYER_PLAYING : PLAYER_PAUSED),
"seek preserves playback intent");
}
}
}
}
+12 -1
View File
@@ -143,7 +143,18 @@ $(BUILD_DIR)/seek-queue-check: Checks/seek-queue.cpp Sources/Core/player.cpp Sou
check-seek-queue: $(BUILD_DIR)/seek-queue-check check-seek-queue: $(BUILD_DIR)/seek-queue-check
$(BUILD_DIR)/seek-queue-check $(BUILD_DIR)/seek-queue-check
check: check-dependencies $(BUILD_DIR)/playback-check check-seek-queue check-video-decoding check-fixtures check-audio check-window check-config check-youtube check-quality check-adaptive check-network check-progress check-reporting check-cli check-runtime check-media check-startup check-bundle check-subtitles check-resolved check-cast check-queue $(BUILD_DIR)/local-seek-check: Checks/local-seek.mm Sources/Core/player.cpp Sources/Core/player.h Sources/Core/player_macos.h Sources/Core/output.hpp Sources/Core/video_decoder.hpp Sources/Core/network_input.hpp Sources/Core/network_policy.hpp Sources/Core/progress_reporter.hpp $(filter-out $(BUILD_DIR)/player.o,$(OBJECTS)) Makefile | check-dependencies
$(CXX) $(CXXFLAGS) $< $(filter-out $(BUILD_DIR)/player.o,$(OBJECTS)) $(FF_LIBS) $(FRAMEWORKS) -o $@
$(BUILD_DIR)/fixtures/local-seek.mp4: Makefile | check-dependencies
mkdir -p $(BUILD_DIR)/fixtures
ffmpeg -hide_banner -loglevel error -y -f lavfi -i testsrc2=size=1920x1080:rate=30:duration=12 -f lavfi -i sine=frequency=440:sample_rate=48000:duration=12 -c:v libx264 -preset ultrafast -threads 2 -bf 2 -g 300 -keyint_min 300 -sc_threshold 0 -c:a aac $@
.PHONY: check-local-seek
check-local-seek: $(BUILD_DIR)/local-seek-check $(BUILD_DIR)/fixtures/local-seek.mp4 $(DECODE_FIXTURES)
$(BUILD_DIR)/local-seek-check $(BUILD_DIR)/fixtures/local-seek.mp4 $(DECODE_FIXTURES)
check: check-dependencies $(BUILD_DIR)/playback-check check-seek-queue check-local-seek check-video-decoding check-fixtures check-audio check-window check-config check-youtube check-quality check-adaptive check-network check-progress check-reporting check-cli check-runtime check-media check-startup check-bundle check-subtitles check-resolved check-cast check-queue
$(BUILD_DIR)/playback-check $(BUILD_DIR)/fixtures $(BUILD_DIR)/playback-check $(BUILD_DIR)/fixtures
check-fixtures: | check-dependencies check-fixtures: | check-dependencies
+3 -1
View File
@@ -154,7 +154,7 @@ Settings load once at startup. Menu changes are session-only and never write the
## Limits ## Limits
- SDR output only; PQ/HLG HDR is rejected. No stream playlists, playlist-file import, embedded/YouTube captions, manual audio-track selection, rotation, or deinterlacing. The session-only local-file queue and external resolved-source WebVTT subtitles are supported. - SDR output only; PQ/HLG HDR is rejected. No stream playlists, playlist-file import, embedded/YouTube captions, manual audio-track selection, rotation, or deinterlacing. The session-only local-file queue and external resolved-source WebVTT subtitles are supported.
- H.264, HEVC, and VP9 prefer VideoToolbox decoding with software fallback; other codecs, including AV1, use software. Backward frame stepping can be slow on long-GOP or unindexed media. - H.264, HEVC, and VP9 prefer VideoToolbox decoding with software fallback; other codecs, including AV1, use software. Local seeks remain frame-accurate: disposable non-reference pictures strictly before the target are skipped, but reference pictures still require decoding. Seeking and backward frame stepping can remain slow on long-GOP or unindexed media.
- Network buffering is bounded and memory-only; live/non-seekable sources disable seeking and frame stepping. Expired stream URLs are not automatically refreshed. - Network buffering is bounded and memory-only; live/non-seekable sources disable seeking and frame stepping. Expired stream URLs are not automatically refreshed.
- Audio-device changes during playback are not handled. - Audio-device changes during playback are not handled.
@@ -169,6 +169,8 @@ The full suite requires a graphical macOS session and a working audio device; it
The startup checks verify that playing video remains attached, unhidden and sized to its native drag surface, including with the queue open; playback-state checks alone do not establish renderer visibility. The startup checks verify that playing video remains attached, unhidden and sized to its native drag surface, including with the queue open; playback-state checks alone do not establish renderer visibility.
`make check-local-seek` measures paused/playing local seek-to-picture latency and compares the resulting timestamps and pixels against full decoding, including B-frame targets. It covers a 1080p H.264 long-GOP file with audio and the six hardware/software decoder fixtures; timing is diagnostic, not a machine-dependent pass threshold.
Queue-open resize checks require positive native resize increments and exercise continuous horizontal/vertical resize-delegate sequences, including reversals, to preserve video aspect without snap-back. The driving axis is fixed for each live resize gesture so aspect corrections do not feed back into axis selection. Physical edge-drag resizing still needs manual verification. Queue-open resize checks require positive native resize increments and exercise continuous horizontal/vertical resize-delegate sequences, including reversals, to preserve video aspect without snap-back. The driving axis is fixed for each live resize gesture so aspect corrections do not feed back into axis selection. Physical edge-drag resizing still needs manual verification.
`make benchmark-playback` measures cold source startup, cached/uncached seeks, and sustained native/Cast playback with repeatable loopback fixtures. It requires the same macOS session/audio device and fixture-generation FFmpeg; results exclude live extraction, WAN/TLS, and physical-receiver latency. `make benchmark-playback` measures cold source startup, cached/uncached seeks, and sustained native/Cast playback with repeatable loopback fixtures. It requires the same macOS session/audio device and fixture-generation FFmpeg; results exclude live extraction, WAN/TLS, and physical-receiver latency.
+12
View File
@@ -468,6 +468,7 @@ struct Player {
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);
track.codec->skip_frame = AVDISCARD_DEFAULT;
track.drainSent = track.drained = false; track.drainSent = track.drained = false;
track.firstPacket = true; track.firstPacket = true;
av_packet_unref(track.packet); av_packet_unref(track.packet);
@@ -1151,6 +1152,17 @@ struct Player {
"Preparing video decoder preroll"); "Preparing video decoder preroll");
track.firstPacket = false; track.firstPacket = false;
} }
if (slot == 1) {
// Preroll needs reference pictures, not disposable pictures before
// the target. Restore full decoding before the target packet and
// for exact frame stepping; never guess when timestamps are absent.
const auto *packet = track.packet;
const bool preroll = !network && !stepping && !previewShown && seekTarget > 0 &&
packet->pts != AV_NOPTS_VALUE &&
packet->duration > 0 &&
(double(packet->pts) + packet->duration) * av_q2d(track.stream()->time_base) - origin < seekTarget;
track.codec->skip_frame = preroll ? AVDISCARD_NONREF : AVDISCARD_DEFAULT;
}
const int result = avcodec_send_packet(track.codec, track.packet); const int result = avcodec_send_packet(track.codec, track.packet);
if (result != AVERROR(EAGAIN)) { if (result != AVERROR(EAGAIN)) {
check(result, "Sending media packet to decoder"); check(result, "Sending media packet to decoder");
+2 -1
View File
@@ -6,6 +6,7 @@
- multi-file Open/Finder/drop, natural filename order, editable queue sidebar, explicit append, previous/next and automatic advance; Cast navigation retains the receiver; UI/drag-drop and physical-Cast verification remain unverified - multi-file Open/Finder/drop, natural filename order, editable queue sidebar, explicit append, previous/next and automatic advance; Cast navigation retains the receiver; UI/drag-drop and physical-Cast verification remain unverified
- [x] make sure the `make install` command installs the application (i can't find it in spotlight) - [x] make sure the `make install` command installs the application (i can't find it in spotlight)
- installs to ~/Applications, migrates the old hidden app, registers with Launch Services and requests Spotlight indexing; actual install, CLI, code signature, registration and Spotlight query passed; Spotlight UI remains unverified - installs to ~/Applications, migrates the old hidden app, registers with Launch Services and requests Spotlight indexing; actual install, CLI, code signature, registration and Spotlight query passed; Spotlight UI remains unverified
- [ ] takes an awfully long time seek in local videos which i find weird, since seeking in streamed youtube videos is pretty much instant - [x] takes an awfully long time seek in local videos which i find weird, since seeking in streamed youtube videos is pretty much instant
- local seek preroll skips disposable non-reference pictures without sacrificing exact target frames; measured 1080p/10-second-GOP paused seek improved from 541 ms to 400 ms and playing seek from 682 ms to 488 ms; timestamp/pixel checks passed across six decoder formats; long-GOP reference decoding remains necessary, and the originally affected files still need manual verification
- [ ] in the "choose what to play" screen, we should change the youtube button to general streaming - [ ] in the "choose what to play" screen, we should change the youtube button to general streaming
- we can figure out the stream and how to handle it from the url (i.e. if youtube url, handle as youtube stream; if miruro url, handle accordingly) - we can figure out the stream and how to handle it from the url (i.e. if youtube url, handle as youtube stream; if miruro url, handle accordingly)