From 8ad5c09474bbe0cbb7e32ee58514897c44b67c8b Mon Sep 17 00:00:00 2001 From: evgeny Date: Tue, 20 Oct 2026 01:07:31 +0200 Subject: [PATCH] Finish decoder capability probes after first paced output and drain --- tools/chatgui/src/call_video_decoder.cpp | 27 +++++++++++++++------ tools/chatgui/tests/test_call_video_gpu.cpp | 24 +++++++++++++++++- 2 files changed, 42 insertions(+), 9 deletions(-) diff --git a/tools/chatgui/src/call_video_decoder.cpp b/tools/chatgui/src/call_video_decoder.cpp index 9dccd1ef..1854769c 100644 --- a/tools/chatgui/src/call_video_decoder.cpp +++ b/tools/chatgui/src/call_video_decoder.cpp @@ -232,6 +232,7 @@ int CallVideoDecoder::receive(call_video_frame& f, QImage& image) { } call_video_caps CallVideoDecoder::capabilities(const std::atomic* running) { + const int64_t started = call_video_playout_now_us(); static call_video_caps caps{}; static std::mutex lock; static bool initialized = false; @@ -241,7 +242,11 @@ call_video_caps CallVideoDecoder::capabilities(const std::atomic* running) }; std::unique_lock guard(lock, std::defer_lock); while (!guard.try_lock()) { if (cancelled()) return {}; QThread::usleep(1000); } - if (initialized) return caps; + if (initialized) { + DEBUG_INFO(DEBUG_CATEGORY_CALL, "video decoder capabilities cache hit wait_us=%lld", + (long long)(call_video_playout_now_us() - started)); + return caps; + } call_video_caps probed{}; for (unsigned codec = 1; codec <= 2; ++codec) { call_video_limit limit{}; @@ -269,9 +274,10 @@ call_video_caps CallVideoDecoder::capabilities(const std::atomic* running) 4000000, (uint8_t)profile}; unsigned sent = 0, received = 0; bool valid = true, outputBeforeEos = false; const int64_t start = call_video_playout_now_us(); - while (valid && received < 8 && call_video_playout_now_us() - start < 1500000) { + // Pacing нужен до первого выхода для измерения pipeline, затем проверяем drain принятых входов. + while (valid && call_video_playout_now_us() - start < 1500000) { if (cancelled()) return {}; - if (sent < 8 && call_video_playout_now_us() - start >= sent * 1000000ll / fps) { + if (!outputBeforeEos && sent < 8 && call_video_playout_now_us() - start >= sent * 1000000ll / fps) { input.frame_id = sent + 1; input.pts_us = 1000000 + sent * 1000000ll / fps; int result = decoder.submit(input); if (result < 0) valid = false; else if (result) ++sent; } @@ -283,12 +289,15 @@ call_video_caps CallVideoDecoder::capabilities(const std::atomic* running) if (!decoder.m->eos) outputBeforeEos = true; ++received; } - if (sent == 8 && decoder.finish() < 0) valid = false; - if (valid && received < 8) QThread::usleep(1000); + if ((outputBeforeEos || sent == 8) && decoder.finish() < 0) valid = false; + if (decoder.m->eos && received == sent) break; + if (valid) QThread::usleep(1000); } - valid = valid && received == 8 && outputBeforeEos; - DEBUG_INFO(DEBUG_CATEGORY_CALL, "video decoder probe codec=%u profile=%u size=%ux%u fps=%u backend=%d valid=%d inputs=%u outputs=%u pipeline=%u", - codec, profile, w, h, fps, int(backend), valid, sent, received, decoder.pipelineFrames()); + valid = valid && sent && received == sent && outputBeforeEos; + DEBUG_INFO(DEBUG_CATEGORY_CALL, "video decoder probe codec=%u profile=%u size=%ux%u fps=%u backend=%d " + "valid=%d inputs=%u outputs=%u pipeline=%u elapsed_us=%lld", + codec, profile, w, h, fps, int(backend), valid, sent, received, decoder.pipelineFrames(), + (long long)(call_video_playout_now_us() - start)); bothOrientations = valid; backendPipeline = std::max(backendPipeline, decoder.pipelineFrames()); } @@ -314,5 +323,7 @@ call_video_caps CallVideoDecoder::capabilities(const std::atomic* running) } } caps = probed; initialized = true; + DEBUG_INFO(DEBUG_CATEGORY_CALL, "video decoder capabilities verified elapsed_us=%lld", + (long long)(call_video_playout_now_us() - started)); return caps; } diff --git a/tools/chatgui/tests/test_call_video_gpu.cpp b/tools/chatgui/tests/test_call_video_gpu.cpp index 28eeb9b7..139cb186 100644 --- a/tools/chatgui/tests/test_call_video_gpu.cpp +++ b/tools/chatgui/tests/test_call_video_gpu.cpp @@ -25,9 +25,10 @@ static QByteArray selected; static unsigned opened = 0, software = 0; static int depth = 1; static bool burst = false, badReplacement = false, badProbe = false; -struct DecodePending { std::deque frames; bool eos = false; }; +struct DecodePending { std::deque frames; bool eos = false; unsigned inputs = 0; }; static std::map decoderContexts; static bool delayedDecoder = false; +static unsigned probeInputs = 0, probeOutputs = 0, probeMaxInputs = 0; extern "C" const AVCodec* __real_avcodec_find_encoder_by_name(const char*); extern "C" const AVCodec* __wrap_avcodec_find_encoder_by_name(const char* name) { @@ -116,6 +117,9 @@ extern "C" void __wrap_avcodec_free_context(AVCodecContext** context) { extern "C" int __real_avcodec_send_packet(AVCodecContext*, const AVPacket*); extern "C" int __wrap_avcodec_send_packet(AVCodecContext* context, const AVPacket* packet) { int result = __real_avcodec_send_packet(context, packet); + if (delayedDecoder && packet && result >= 0) { + ++probeInputs; probeMaxInputs = std::max(probeMaxInputs, ++decoderContexts[context].inputs); + } if (delayedDecoder && !packet && result >= 0) decoderContexts[context].eos = true; return result; } @@ -129,6 +133,7 @@ extern "C" int __wrap_avcodec_receive_frame(AVCodecContext* context, AVFrame* ou if (result < 0 && result != AVERROR(EAGAIN) && result != AVERROR_EOF) return result; if (state.frames.empty()) return result; if (!state.eos && state.frames.size() <= 2) return AVERROR(EAGAIN); + ++probeOutputs; av_frame_move_ref(output, state.frames.front()); av_frame_free(&state.frames.front()); state.frames.pop_front(); return 0; } @@ -138,9 +143,26 @@ private slots: void initTestCase() { debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN); av_log_set_level(AV_LOG_ERROR); } void init() { selected.clear(); opened = software = 0; depth = 1; burst = badReplacement = badProbe = delayedDecoder = false; + probeInputs = probeOutputs = probeMaxInputs = 0; QVERIFY(contexts.empty()); QVERIFY(decoderContexts.empty()); } void cleanup() { QVERIFY(contexts.empty()); QVERIFY(decoderContexts.empty()); } + void capabilityProbeDrainsFirstOutput() { + // Реальный software decoder выдаёт первый кадр только после третьего input, затем хранит хвост до EOS. + delayedDecoder = true; + QElapsedTimer timer; timer.start(); + const auto caps = CallVideoDecoder::capabilities(); + for (const auto& limit : caps.decode) { + QCOMPARE(limit.profiles, uint8_t(3)); QCOMPARE(limit.fps, uint16_t(60)); + QCOMPARE(limit.width, uint16_t(1920)); QCOMPARE(limit.height, uint16_t(1920)); + } + QVERIFY(probeInputs > 0); QCOMPARE(probeMaxInputs, 3u); QCOMPARE(probeOutputs, probeInputs); + const unsigned before = probeInputs; + const auto cached = CallVideoDecoder::capabilities(); + QCOMPARE(cached.decode[0].profiles, caps.decode[0].profiles); QCOMPARE(probeInputs, before); + qInfo("Decoder capability probe with delayed output: %lldms, inputs=%u outputs=%u max_per_mode=%u", + timer.elapsed(), probeInputs, probeOutputs, probeMaxInputs); + } void delayedDecodeAndGenerationDrain() { LiveVideoEncoder encoder; LiveVideoConfig config; config.width = 640; config.height = 360; config.codec = 1; config.backend = "libx264";