Drain GPU outputs independently of capture and verify lossless recovery

This commit is contained in:
evgeny committed 2026-10-19 17:28:13 +02:00
1 parent 0bc2715a30
commit 5f772fa97f
4 files changed
+31 -16

No files matched your search

+3 -3
View File
@@ -8,7 +8,7 @@ static const int64_t origin = 1000000;
static void simulate(unsigned pause_ms, int cpu, int64_t quantum, int accelerated_audio) {
struct call_video_playout p; call_video_playout_init(&p, 7);
enum { COUNT = 240 };
enum { COUNT = 480 };
struct call_video_playout_frame frames[COUNT];
for (int i = 0; i < COUNT; ++i) {
int64_t arrival = origin + i * 33333ll;
@@ -17,8 +17,8 @@ static void simulate(unsigned pause_ms, int cpu, int64_t quantum, int accelerate
frames[i] = (struct call_video_playout_frame){origin + i * 33333ll, arrival};
}
unsigned shown = 0, accelerated = 0;
int64_t last_show = 0, recovered_lag = INT64_MAX, audio_pts = origin;
for (int64_t now = origin; now < origin + 12000000 && shown < COUNT; now += quantum) {
int64_t last_show = 0, recovered_lag = INT64_MAX, audio_pts = origin - (accelerated_audio ? 150000 : 0);
for (int64_t now = origin; now < origin + 20000000 && shown < COUNT; now += quantum) {
uint32_t rate = accelerated_audio && now >= origin + 1500000 && now < origin + 2500000 ? 1100000 : 1000000;
if (now > origin) audio_pts += quantum * rate / 1000000;
if (cpu && now >= origin + 1000000 && now < origin + 1000000 + pause_ms * 1000ll) continue;
+19 -8
View File
@@ -49,6 +49,7 @@ LiveVideoEncoder::~LiveVideoEncoder() = default;
void LiveVideoEncoder::close() { m = std::make_unique<Impl>(); }
QString LiveVideoEncoder::error() const { return m->error; }
QString LiveVideoEncoder::name() const { return m->name; }
unsigned LiveVideoEncoder::pendingFrames() const { return static_cast<unsigned>(m->timestamps.size()); }
QString LiveVideoEncoder::codecName(int codec) {
static const char* names[] = {"", "H.264", "H.265", "VP9", "AV1", "H.266"};
return codec >= 1 && codec <= 5 ? QString::fromLatin1(names[codec]) : QString();
@@ -373,19 +374,23 @@ bool LiveVideoEncoder::open(const LiveVideoConfig& config, Backend backend) {
config.codec, int(backend), qPrintable(m->error)); return false;
}
bool LiveVideoEncoder::encode(const QImage& image, uint64_t pts, bool key, QList<LiveEncodedFrame>& output) {
bool LiveVideoEncoder::encode(const QImage& image, uint64_t pts, bool key, QList<LiveEncodedFrame>& output, bool finish) {
#ifdef HAVE_FFMPEG
if (!m->codec || !m->frame || image.isNull() || pts > INT64_MAX) {
if (!m->codec || !m->frame || pts > INT64_MAX || (finish && !image.isNull())) {
m->error = "Invalid video frame"; DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "desktop encode invalid frame/state"); return false;
}
QElapsedTimer timer; timer.start();
if (!m->reportTimer.isValid()) m->reportTimer.start();
const auto outputBase = output.size();
int result = 0;
const bool constrained = m->config.profile != LiveVideoProfile::Automatic;
qint64 preparedUs = 0, submittedUs = 0;
if (!image.isNull()) {
QImage canvas(m->config.width, m->config.height, QImage::Format_RGB32); canvas.fill(Qt::black);
QSize fitted = image.size().scaled(canvas.size(), Qt::KeepAspectRatio);
{ QPainter painter(&canvas); painter.drawImage(QRect(QPoint((canvas.width() - fitted.width()) / 2,
(canvas.height() - fitted.height()) / 2), fitted), image); }
int result = av_frame_make_writable(m->frame);
result = av_frame_make_writable(m->frame);
m->scale = sws_getCachedContext(m->scale, canvas.width(), canvas.height(), AV_PIX_FMT_RGB32,
m->frame->width, m->frame->height, static_cast<AVPixelFormat>(m->frame->format), SWS_FAST_BILINEAR, nullptr, nullptr, nullptr);
if (!m->scale) result = AVERROR(ENOMEM);
@@ -412,7 +417,6 @@ bool LiveVideoEncoder::encode(const QImage& image, uint64_t pts, bool key, QList
qPrintable(m->name), (unsigned long long)pts, image.width(), image.height(), m->frame->width, m->frame->height,
m->frame->format, minimum, maximum, (unsigned long long)(samples ? edges * 1000 / samples : 0));
}
bool constrained = m->config.profile != LiveVideoProfile::Automatic;
if (constrained && m->timestamps.size() >= 8) {
m->error = "GPU encoder exceeded the bounded input pipeline";
DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "call encoder stalled name=%s pending=%zu pts=%llu",
@@ -429,14 +433,15 @@ bool LiveVideoEncoder::encode(const QImage& image, uint64_t pts, bool key, QList
if (result >= 0) result = av_hwframe_transfer_data(hardware, m->frame, 0);
if (result >= 0) result = av_frame_copy_props(hardware, m->frame);
}
const qint64 preparedUs = timer.nsecsElapsed() / 1000;
preparedUs = timer.nsecsElapsed() / 1000;
if (result >= 0) result = avcodec_send_frame(m->codec, hardware ? hardware : m->frame);
const qint64 submittedUs = timer.nsecsElapsed() / 1000;
submittedUs = timer.nsecsElapsed() / 1000;
if (result >= 0) {
m->submittedFrames++;
if (constrained) m->timestamps.emplace_back(m->frame->pts, pts);
}
av_frame_free(&hardware);
} else if (finish) result = avcodec_send_frame(m->codec, nullptr);
AVPacket* packet = av_packet_alloc();
if (!packet) result = AVERROR(ENOMEM);
while (result >= 0) {
@@ -505,7 +510,8 @@ bool LiveVideoEncoder::encode(const QImage& image, uint64_t pts, bool key, QList
if (result == AVERROR(EAGAIN) || result == AVERROR_EOF) {
// Сводка по этапам отличает конвертацию, синхронный encoder и получение/упаковку пакета.
const qint64 totalUs = timer.nsecsElapsed() / 1000;
++m->inputs; m->outputs += output.size() - outputBase;
if (!image.isNull()) ++m->inputs;
m->outputs += output.size() - outputBase;
bool producedKey = false;
for (auto i = outputBase; i < output.size(); ++i) if (output[i].key) { ++m->keys; producedKey = true; }
m->prepareMax = std::max(m->prepareMax, preparedUs);
@@ -535,13 +541,18 @@ bool LiveVideoEncoder::encode(const QImage& image, uint64_t pts, bool key, QList
}
if (key && output.isEmpty()) DEBUG_DEBUG(DEBUG_CATEGORY_VIDEO, "desktop requested KEY buffered name=%s PTS=%llu",
qPrintable(m->name), (unsigned long long)pts);
if (finish && !m->timestamps.empty()) {
m->error = "Encoder did not drain all accepted inputs";
DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "call encoder drain incomplete name=%s pending=%zu",
qPrintable(m->name), m->timestamps.size()); return false;
}
return true;
}
if (result < 0) { m->error = ffError(result); DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "desktop encode failed name=%s PTS=%llu: %s",
qPrintable(m->name), (unsigned long long)pts, qPrintable(m->error)); return false; }
return true;
#else
Q_UNUSED(image); Q_UNUSED(pts); Q_UNUSED(key); Q_UNUSED(output); return false;
Q_UNUSED(image); Q_UNUSED(pts); Q_UNUSED(key); Q_UNUSED(output); Q_UNUSED(finish); return false;
#endif
}
+3 -1
View File
@@ -32,7 +32,9 @@ public:
bool canSetMaxQp() const; // Поддерживается изменение потолка без пересоздания encoder.
bool setMaxQp(int qp); // 0..51, применяется при следующей подаче кадра; false — ошибка/нет поддержки.
void close();
bool encode(const QImage& image, uint64_t pts, bool key, QList<LiveEncodedFrame>& output);
// Пустое image забирает готовый выход без нового input; finish завершает старый поток перед заменой.
bool encode(const QImage& image, uint64_t pts, bool key, QList<LiveEncodedFrame>& output, bool finish = false);
unsigned pendingFrames() const;
QString error() const;
QString name() const;
static QList<int> available(Backend backend = Backend::Automatic); // Установленные кодеки, без открытия и кодирования.
@@ -51,14 +51,16 @@ int main(int argc, char** argv) {
if (!encoder.encode(image, 1000000 + i * 33333ll, i == 0, output, completion) || output.size() != 1) return 3;
frames.push_back(output.front());
}
CallVideoEngine engine; std::vector<int64_t> published;
QObject::connect(&engine, &CallVideoEngine::imageChanged, [&](const QImage& frame) {
CallVideoEngine engine; engine.setPresentationDriven(true); std::vector<int64_t> published;
QObject::connect(&engine, &CallVideoEngine::imageChanged, [&](const QImage& frame, quint64 serial) {
if (!frame.isNull()) published.push_back(nowUs());
// Подтверждение потребления отдельно от доставки signal, как paintEvent окна.
QTimer::singleShot(5, &engine, [&engine, serial] { engine.imagePresented(serial); });
});
for (int scenario = 0; scenario < 2; ++scenario) {
disturbed = scenario != 0; cursor = 0; published.clear();
start = nowUs(); engine.start(7); QElapsedTimer timer; timer.start(); bool uiPaused = false;
while (timer.elapsed() < 3200) {
while (timer.elapsed() < 4000) {
if (disturbed && !uiPaused && timer.elapsed() >= 1500) { QThread::msleep(60); uiPaused = true; }
app.processEvents(); QThread::msleep(2);
}
@@ -71,7 +73,7 @@ int main(int argc, char** argv) {
"gap_p50_us=%lld gap_p95_us=%lld gap_max_us=%lld audio_epochs=32", disturbed, published.size(), requests.load(), rejects.load(),
(long long)(gaps.empty() ? 0 : gaps[gaps.size() / 2]), (long long)(gaps.empty() ? 0 : gaps[gaps.size() * 95 / 100]),
(long long)(gaps.empty() ? 0 : gaps.back()));
if (published.size() < (disturbed ? 82u : 85u) || requests || rejects) return 1;
if (published.size() != frames.size() || requests || rejects) return 1;
}
return 0;
}