From 5d8c6b8ff3b12a43893dcee02290332acfeb9ad1 Mon Sep 17 00:00:00 2001 From: evgeny Date: Wed, 14 Oct 2026 21:32:02 +0200 Subject: [PATCH] Add measured x265 bitrate adaptation probe for call backends --- tools/chatgui/CMakeLists.txt | 9 +++ tools/chatgui/tests/probe_call_x265.cpp | 100 ++++++++++++++++++++++++ 2 files changed, 109 insertions(+) create mode 100644 tools/chatgui/tests/probe_call_x265.cpp diff --git a/tools/chatgui/CMakeLists.txt b/tools/chatgui/CMakeLists.txt index eab15ac5..7e53c364 100644 --- a/tools/chatgui/CMakeLists.txt +++ b/tools/chatgui/CMakeLists.txt @@ -129,6 +129,15 @@ endif() # uTun core library (C, built from ../src/ and ../lib/) add_subdirectory(libutun) +# Ручная проверка codec backend перед включением в capabilities звонка; не часть ctest. +if(PkgConfig_FOUND) + pkg_check_modules(CALL_X265 QUIET x265 IMPORTED_TARGET) + if(CALL_X265_FOUND) + add_executable(probe_call_x265 EXCLUDE_FROM_ALL tests/probe_call_x265.cpp) + target_link_libraries(probe_call_x265 PRIVATE utun PkgConfig::CALL_X265) + endif() +endif() + # Общий sender остаётся в libutun; Qt захватывает видео, FFmpeg/Opus работают в publisher workers. add_library(chatgui_stream_publish STATIC src/stream_video_encoder.cpp src/stream_publish_engine.cpp src/stream_publish_window.cpp) target_link_libraries(chatgui_stream_publish PUBLIC ${QT_LIBS} utun PRIVATE ${FFMPEG_TARGET} ${FFMPEG_LIBRARIES}) diff --git a/tools/chatgui/tests/probe_call_x265.cpp b/tools/chatgui/tests/probe_call_x265.cpp new file mode 100644 index 00000000..81e81ebb --- /dev/null +++ b/tools/chatgui/tests/probe_call_x265.cpp @@ -0,0 +1,100 @@ +/* Диагностика пригодности x265 для звонка: один encoder, 4M -> 300k -> 4M, + * фиксированный Main 8-bit / Main tier / level 3.1, без lookahead/B-кадров. + * Проверяем фактический выход, а не только return/getter reconfigure. + * Запускается вручную; код 2 означает, что backend не прошёл пробу адаптации. */ +#include +#include +#include +#include +#include "debug_config.h" + +struct Probe { + const x265_api* api = nullptr; + x265_param *parameters = nullptr, *actual = nullptr; + x265_encoder* encoder = nullptr; + ~Probe() { + if (encoder) api->encoder_close(encoder); + if (parameters) api->param_free(parameters); + if (actual) api->param_free(actual); + } +}; + +int main(int argc, char** argv) { + debug_config_init(); debug_set_level(DEBUG_LEVEL_INFO); + bool frameRc = argc == 2 && !strcmp(argv[1], "--frame-rc"); + if (argc > 1 && !frameRc) { + DEBUG_ERROR(DEBUG_CATEGORY_CALL, "Usage: probe_call_x265 [--frame-rc]"); return 1; + } + Probe p; p.api = x265_api_get(8); + if (!p.api || p.api->sizeof_param != sizeof(x265_param) || p.api->sizeof_picture != sizeof(x265_picture)) { + DEBUG_ERROR(DEBUG_CATEGORY_CALL, "x265 8-bit API unavailable or ABI mismatch"); return 1; + } + p.parameters = p.api->param_alloc(); p.actual = p.api->param_alloc(); + if (!p.parameters || !p.actual || p.api->param_default_preset(p.parameters, "ultrafast", "zerolatency")) { + DEBUG_ERROR(DEBUG_CATEGORY_CALL, "x265 probe parameter allocation/preset failed"); return 1; + } + auto* config = p.parameters; + config->sourceWidth = 640; config->sourceHeight = 360; config->fpsNum = 30; config->fpsDenom = 1; + config->internalCsp = X265_CSP_I420; config->internalBitDepth = 8; + config->levelIdc = 31; config->bHighTier = 0; config->maxNumReferences = 1; + config->bframes = 0; config->lookaheadDepth = 0; config->frameNumThreads = 1; + config->keyframeMax = 30; config->keyframeMin = 1; config->bOpenGOP = 0; + config->bRepeatHeaders = 0; config->bEmitInfoSEI = 0; config->bEmitHRDSEI = 0; + config->rc.rateControlMode = X265_RC_ABR; config->rc.bitrate = 4000; + config->rc.vbvMaxBitrate = 4000; config->rc.vbvBufferSize = 1000; + config->logLevel = X265_LOG_WARNING; + if (p.api->param_parse(config, "pools", "none") + || (frameRc && p.api->param_parse(config, "frame-rc", "1")) || p.api->param_apply_profile(config, "main")) { + DEBUG_ERROR(DEBUG_CATEGORY_CALL, "x265 probe option/profile rejected frame_rc=%d", frameRc); return 1; + } + p.encoder = p.api->encoder_open(config); + if (!p.encoder) { DEBUG_ERROR(DEBUG_CATEGORY_CALL, "x265 probe open failed"); return 1; } + DEBUG_INFO(DEBUG_CATEGORY_CALL, "x265 probe version=%s frame_rc=%d size=640x360 fps=30 level=31 tier=main vbv_buffer_kbits=1000", + p.api->version_str, frameRc); + std::vector pixels(640 * 360 * 3 / 2, 128); + uint32_t random = 1; + bool passed = true; + for (int phase = 0; phase < 3; phase++) { + int requested = phase == 1 ? 300 : 4000; + p.api->encoder_parameters(p.encoder, config); + config->rc.bitrate = config->rc.vbvMaxBitrate = requested; + int result = phase ? p.api->encoder_reconfig(p.encoder, config) : 0; + if (result) { DEBUG_ERROR(DEBUG_CATEGORY_CALL, "x265 reconfig failed phase=%d result=%d", phase, result); return 1; } + uint64_t bytes = 0; + for (int frame = 0; frame < 120; frame++) { + // Детерминированная сложная сцена: малая скорость не объясняется статичным изображением. + for (int i = 0; i < 640 * 360; i++) { + random = random * 1664525u + 1013904223u; + pixels[i] = 16 + ((random >> 24) * 219 / 255); + } + x265_picture input, output; + p.api->picture_init(config, &input); p.api->picture_init(config, &output); + input.planes[0] = pixels.data(); input.planes[1] = pixels.data() + 640 * 360; + input.planes[2] = pixels.data() + 640 * 360 * 5 / 4; + input.stride[0] = 640; input.stride[1] = input.stride[2] = 320; + input.pts = (phase * 120 + frame) * 1000000ll / 30; + x265_nal* nals = nullptr; uint32_t count = 0; + result = p.api->encoder_encode(p.encoder, &nals, &count, &input, &output); + if (result != 1 || output.pts != input.pts) { + DEBUG_ERROR(DEBUG_CATEGORY_CALL, "x265 probe delay/error phase=%d frame=%d result=%d pts=%lld expected=%lld", + phase, frame, result, (long long)output.pts, (long long)input.pts); return 1; + } + if (frame >= 30) for (unsigned i = 0; i < count; i++) bytes += nals[i].sizeBytes; + if (frame < 3) { + p.api->encoder_parameters(p.encoder, p.actual); + DEBUG_INFO(DEBUG_CATEGORY_CALL, "x265 probe phase=%d frame=%d requested_kbps=%d getter_kbps=%d vbv_kbps=%d level=%d", + phase, frame, requested, p.actual->rc.bitrate, p.actual->rc.vbvMaxBitrate, p.actual->levelIdc); + } + } + uint64_t measured = bytes * 8 / 3; + bool matches = measured >= (uint64_t)requested * 500 && measured <= (uint64_t)requested * 1500; + DEBUG_INFO(DEBUG_CATEGORY_CALL, "x265 probe phase=%d target_bps=%d measured_bps=%llu acceptable=%d", + phase, requested * 1000, (unsigned long long)measured, matches); + if (!matches) passed = false; + } + if (!passed) { + DEBUG_ERROR(DEBUG_CATEGORY_CALL, "x265 backend unsuitable: measured bitrate does not follow commands"); return 2; + } + DEBUG_INFO(DEBUG_CATEGORY_CALL, "x265 adaptation probe PASS (camera/decoder/other modes still require validation)"); + return 0; +}