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
+40 -2
View File
@@ -4,8 +4,10 @@ extern "C" {
#include <libavformat/avformat.h>
}
#include <array>
#include <algorithm>
#include <atomic>
#include <chrono>
#include <cmath>
#include <cerrno>
#include <condition_variable>
#include <cstdint>
@@ -48,7 +50,8 @@ public:
operationSerial.store(serial.load(std::memory_order_acquire), std::memory_order_relaxed);
deadline.store(now() + 15'000'000'000LL, std::memory_order_relaxed);
}
void start(std::array<int, 2> indices) {
void start(std::array<int, 2> indices, double timelineOrigin) {
origin = timelineOrigin;
for (int slot = 0; slot < 2; ++slot) {
lanes[slot].index = indices[slot];
if (indices[slot] < 0) continue;
@@ -69,6 +72,7 @@ public:
void seek(int streamIndex, int64_t timestamp) {
std::lock_guard lock(mutex);
for (auto &lane : lanes) clear(lane);
scratchSlot = -1;
ended = false;
failure = 0;
seekIndex = streamIndex;
@@ -81,11 +85,13 @@ public:
std::lock_guard lock(mutex);
skipAudio = discard;
if (discard) clear(lanes[0]);
if (discard && scratchSlot == 0) scratchSlot = -1;
changed.notify_all();
}
void finish() {
std::lock_guard lock(mutex);
for (auto &lane : lanes) clear(lane);
scratchSlot = -1;
ended = true;
seeking = false;
failure = 0;
@@ -100,13 +106,23 @@ public:
std::lock_guard lock(mutex);
return failure;
}
double bufferedEnd(int slot) const {
std::lock_guard lock(mutex);
if (failure || seeking) return 0;
const auto &lane = lanes[slot];
const double queued = lane.count ?
lane.ends[(lane.first + lane.count - 1) % packetLimit] : 0;
return std::max(queued, scratchSlot == slot ? scratchEnd : 0);
}
// 1: packet, 0: not yet available, AVERROR_EOF: this input is drained.
int take(int slot, AVPacket *destination) {
int take(int slot, AVPacket *destination, double &bufferedEnd) {
std::lock_guard lock(mutex);
if (failure) return failure;
bufferedEnd = 0;
auto &lane = lanes[slot];
if (lane.count) {
auto *packet = lane.packets[lane.first];
bufferedEnd = lane.ends[lane.first];
lane.bytes -= packetBytes(packet);
av_packet_move_ref(destination, packet);
lane.first = (lane.first + 1) % packetLimit;
@@ -120,6 +136,7 @@ public:
private:
struct Lane {
std::array<AVPacket *, packetLimit> packets{};
std::array<double, packetLimit> ends{};
int index = -1;
std::size_t first = 0, count = 0, bytes = 0;
};
@@ -137,6 +154,8 @@ private:
bool seeking = false, ended = false, skipAudio = false;
int seekIndex = -1, failure = 0;
int64_t seekTimestamp = 0;
int scratchSlot = -1;
double scratchEnd = 0, origin = 0;
std::thread reader;
static int64_t now() {
@@ -153,6 +172,20 @@ private:
}
return bytes;
}
double packetEnd(int slot, const AVPacket *packet) const {
if (packet->pts == AV_NOPTS_VALUE || (packet->flags & AV_PKT_FLAG_DISCARD)) return 0;
int64_t timestamp = packet->pts;
if (slot == 1) {
// PTS alone can jump to a future reference frame before its B-frames arrive.
// Unknown DTS contributes no encoded-video estimate; decoded frames still count.
if (packet->dts == AV_NOPTS_VALUE) return 0;
timestamp = std::min(timestamp, packet->dts);
}
const double tick = av_q2d(format->streams[lanes[slot].index]->time_base);
const double end = static_cast<double>(timestamp) * tick - origin +
static_cast<double>(std::max<int64_t>(packet->duration, 0)) * tick;
return std::isfinite(end) && end > 0 ? end : 0;
}
static void clear(Lane &lane) {
while (lane.count) {
av_packet_unref(lane.packets[lane.first]);
@@ -233,6 +266,8 @@ private:
playerWake.notify_one();
continue;
}
scratchSlot = slot;
scratchEnd = packetEnd(slot, scratch);
changed.wait(lock, [&] {
return stoppingNow() || serial.load(std::memory_order_acquire) != current ||
(slot == 0 && skipAudio) ||
@@ -241,11 +276,14 @@ private:
if (stoppingNow()) return;
if (serial.load(std::memory_order_acquire) != current || (slot == 0 && skipAudio)) {
av_packet_unref(scratch);
scratchSlot = -1;
continue;
}
lane.bytes += bytes;
lane.ends[(lane.first + lane.count) % packetLimit] = scratchEnd;
av_packet_move_ref(lane.packets[(lane.first + lane.count) % packetLimit], scratch);
++lane.count;
scratchSlot = -1;
playerWake.notify_one();
}
} catch (...) {