feat: visualize retained network playback buffer
This commit is contained in:
@@ -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 (...) {
|
||||
|
||||
+56
-3
@@ -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{};
|
||||
|
||||
@@ -15,6 +15,10 @@ typedef struct PlayerState {
|
||||
PlayerStatus status;
|
||||
double position;
|
||||
double duration;
|
||||
// Retained playable network media ahead, on the same timeline as position.
|
||||
// Empty/unavailable (including local playback and pending seeks) is 0, 0.
|
||||
double buffered_start;
|
||||
double buffered_end;
|
||||
bool has_audio;
|
||||
bool has_video;
|
||||
// False for live/non-seekable inputs; reflects both inputs for separate streams.
|
||||
|
||||
Reference in New Issue
Block a user