feat: visualize retained network playback buffer

This commit is contained in:
2026-10-11 02:54:01 +02:00
parent 4d4c3e6b0a
commit 5087a0a0f0
8 changed files with 326 additions and 39 deletions
+56 -3
View File
@@ -61,6 +61,8 @@ struct Track {
AVPacket *packet = nullptr;
bool packetPending = false;
bool receiving = false;
double packetBufferedEnd = 0;
double decoderBufferedEnd = 0;
bool sourceEnded = false;
bool drainSent = false;
bool drained = false;
@@ -247,6 +249,8 @@ struct Player {
if (last.repetitions == std::numeric_limits<uint64_t>::max()) return false;
++last.repetitions;
state.seek_pending = true;
state.buffered_start = state.buffered_end = 0;
++state.revision;
wake.notify_one();
return true;
}
@@ -260,6 +264,11 @@ struct Player {
state.status = PLAYER_OPENING;
state.revision = revision;
}
if (commands.back().action == Action::Seek || commands.back().action == Action::Step ||
commands.back().action == Action::Close) {
state.buffered_start = state.buffered_end = 0;
++state.revision;
}
if (commands.back().action == Action::Seek || commands.back().action == Action::Step)
state.seek_pending = true;
wake.notify_one();
@@ -298,6 +307,41 @@ struct Player {
throw std::runtime_error(std::string(operation) + ": " + details);
}
double retainedTrackEnd(int slot) const {
const auto &track = tracks[slot];
double end = slot == 0 ? std::max(audioWriteEnd, resampleNextPts) : videoEnd;
if (track.packetPending) end = std::max(end, track.packetBufferedEnd);
end = std::max(end, track.decoderBufferedEnd);
if (track.input) end = std::max(end, track.input->bufferedEnd(slot));
return end;
}
void publishBufferedRange() {
state.buffered_start = state.buffered_end = 0;
if (!network || state.seek_pending || frameStepping ||
(status != PLAYER_PAUSED && status != PLAYER_PLAYING) ||
!std::isfinite(duration) || duration <= 0 ||
!std::isfinite(position) || position < 0 || position >= duration) return;
const bool audio = tracks[0].codec != nullptr;
const bool video = tracks[1].codec != nullptr;
if (!audio && !video) return;
const double audioEnd = audio ? retainedTrackEnd(0) : 0;
const double videoEnd = video ? retainedTrackEnd(1) : 0;
double end = audio ? audioEnd : videoEnd;
if (audio && video) {
const bool audioFinished = audioDrained();
const bool videoFinished = tracks[1].drained;
// Exhausted audio is synthesized as silence; exhausted video holds its last frame.
// Neither shorter track caps the other's retained tail, but EOF is not a full cache.
end = audioFinished ? (videoFinished ? std::max(audioEnd, videoEnd) : videoEnd) :
(videoFinished ? audioEnd : std::min(audioEnd, videoEnd));
}
end = std::min(end, duration);
if (!std::isfinite(end) || end <= position) return;
state.buffered_start = position;
state.buffered_end = end;
}
void publish() {
std::lock_guard<std::mutex> lock(mutex);
if (activeEpoch != requestedEpoch.load(std::memory_order_relaxed)) return;
@@ -315,6 +359,7 @@ struct Player {
return (command.action == Action::Seek || command.action == Action::Step) &&
command.epoch == activeEpoch;
});
publishBufferedRange();
state.video_aspect_ratio = 0;
if (const auto *video = tracks[1].codec; video && video->width > 0 && video->height > 0) {
const auto &track = tracks[1];
@@ -401,6 +446,7 @@ struct Player {
track.drainSent = track.drained = false;
av_packet_unref(track.packet);
track.packetPending = track.receiving = track.sourceEnded = false;
track.packetBufferedEnd = track.decoderBufferedEnd = 0;
track.countedTimestamp = AV_NOPTS_VALUE;
track.timestampOccurrence = 0;
const AVStream *stream = track.stream();
@@ -562,7 +608,7 @@ struct Player {
std::array<int, 2> indices{-1, -1};
for (int slot = 0; slot < 2; ++slot)
if (tracks[slot].input == input.get()) indices[slot] = tracks[slot].index;
input->start(indices);
input->start(indices, origin);
}
}
status = PLAYER_PAUSED;
@@ -1000,9 +1046,11 @@ struct Player {
const int result = avcodec_receive_frame(track.codec, decoded);
if (result == AVERROR(EAGAIN)) {
track.receiving = false;
track.decoderBufferedEnd = 0;
} else if (result == AVERROR_EOF) {
track.drained = true;
track.receiving = false;
track.decoderBufferedEnd = 0;
} else {
check(result, "Decoding media");
if (slot == 0) convertAudio(); else queueVideo(stepping);
@@ -1015,6 +1063,7 @@ struct Player {
const int result = avcodec_send_packet(track.codec, track.packet);
if (result != AVERROR(EAGAIN)) {
check(result, "Sending media packet to decoder");
track.decoderBufferedEnd = std::max(track.decoderBufferedEnd, track.packetBufferedEnd);
av_packet_unref(track.packet);
track.packetPending = false;
}
@@ -1023,7 +1072,7 @@ struct Player {
return true;
}
if (track.input && !track.sourceEnded) {
const int result = track.input->take(slot, track.packet);
const int result = track.input->take(slot, track.packet, track.packetBufferedEnd);
if (result == AVERROR_EOF) track.sourceEnded = true;
else if (result == 1) track.packetPending = true;
else check(result, "Reading network media packet");
@@ -1304,7 +1353,11 @@ PlayerState player_get_state(Player *player) {
try {
if (player) {
std::lock_guard<std::mutex> lock(player->mutex);
return player->state;
PlayerState result = player->state;
if (result.seek_pending ||
player->publishedEpoch != player->requestedEpoch.load(std::memory_order_relaxed))
result.buffered_start = result.buffered_end = 0;
return result;
}
} catch (const std::exception &exception) {
PlayerState result{};