Browse Source

Exercise real H264 and H265 calls across delayed bitrate switches

master
evgeny 5 days ago
parent
commit
0f570edbd0
  1. 7
      tools/chatgui/CMakeLists.txt
  2. 120
      tools/chatgui/tests/test_call_video_encoder.cpp

7
tools/chatgui/CMakeLists.txt

@ -139,11 +139,16 @@ if(PkgConfig_FOUND)
endif() endif()
# Общий sender остаётся в libutun; Qt захватывает видео, FFmpeg/Opus работают в publisher workers. # Общий sender остаётся в libutun; Qt захватывает видео, FFmpeg/Opus работают в publisher workers.
add_library(chatgui_live_video STATIC src/live_video_encoder.cpp) add_library(chatgui_live_video STATIC src/live_video_encoder.cpp src/call_video_encoder.cpp)
target_link_libraries(chatgui_live_video PUBLIC ${QT_LIBS} utun PRIVATE ${FFMPEG_TARGET} ${FFMPEG_LIBRARIES}) target_link_libraries(chatgui_live_video PUBLIC ${QT_LIBS} utun PRIVATE ${FFMPEG_TARGET} ${FFMPEG_LIBRARIES})
target_include_directories(chatgui_live_video PRIVATE ${FFMPEG_INCLUDE_DIRS}) target_include_directories(chatgui_live_video PRIVATE ${FFMPEG_INCLUDE_DIRS})
if(FFMPEG_FOUND) if(FFMPEG_FOUND)
target_compile_definitions(chatgui_live_video PRIVATE HAVE_FFMPEG) target_compile_definitions(chatgui_live_video PRIVATE HAVE_FFMPEG)
add_executable(test_call_video_encoder tests/test_call_video_encoder.cpp)
target_include_directories(test_call_video_encoder PRIVATE ${FFMPEG_INCLUDE_DIRS})
target_link_libraries(test_call_video_encoder PRIVATE chatgui_live_video ${QT_TEST_LIB} ${FFMPEG_TARGET} ${FFMPEG_LIBRARIES})
add_test(NAME test_call_video_encoder COMMAND test_call_video_encoder)
set_tests_properties(test_call_video_encoder PROPERTIES TIMEOUT 120)
endif() endif()
add_library(chatgui_stream_publish STATIC src/stream_publish_engine.cpp src/stream_publish_window.cpp) add_library(chatgui_stream_publish STATIC src/stream_publish_engine.cpp src/stream_publish_window.cpp)
target_link_libraries(chatgui_stream_publish PUBLIC chatgui_live_video) target_link_libraries(chatgui_stream_publish PUBLIC chatgui_live_video)

120
tools/chatgui/tests/test_call_video_encoder.cpp

@ -0,0 +1,120 @@
/* Настоящие encoder/decoder: отложенное переключение 4M -> 300k -> 4M,
* один decoder на весь поток, непрерывные capture PTS и измеренный bitrate. */
#include <QtTest>
#include "../src/call_video_encoder.h"
extern "C" {
#include "debug_config.h"
#include "stream/stream_media.h"
#include <libavcodec/avcodec.h>
}
struct Decoder {
AVCodecContext* context = nullptr;
AVPacket* packet = av_packet_alloc();
AVFrame* frame = av_frame_alloc();
~Decoder() { av_frame_free(&frame); av_packet_free(&packet); avcodec_free_context(&context); }
bool open(int codec) {
const auto* implementation = avcodec_find_decoder(codec == 1 ? AV_CODEC_ID_H264 : AV_CODEC_ID_HEVC);
context = implementation ? avcodec_alloc_context3(implementation) : nullptr;
if (!context || !packet || !frame) return false;
context->thread_count = 1; return avcodec_open2(context, implementation, nullptr) >= 0;
}
bool decode(const LiveEncodedFrame& input) {
const uint8_t *header = nullptr, *data = reinterpret_cast<const uint8_t*>(input.data.constData());
size_t headerSize = 0, size = input.data.size();
if (input.key && stream_media_key_parse(data, size, &header, &headerSize, &data, &size) != STREAM_OK) return false;
av_packet_unref(packet); av_frame_unref(frame);
if (av_new_packet(packet, headerSize + size) < 0) return false;
if (headerSize) memcpy(packet->data, header, headerSize);
memcpy(packet->data + headerSize, data, size); packet->pts = packet->dts = input.pts;
return avcodec_send_packet(context, packet) == 0 && avcodec_receive_frame(context, frame) == 0;
}
};
class CallVideoEncoderTest : public QObject {
Q_OBJECT
LiveVideoConfig configuration(int codec) {
LiveVideoConfig config; config.width = 640; config.height = 360; config.fps = 30; config.bitrate = 4000000;
config.codec = codec; config.references = 1; config.vbvBits = 1000000;
config.profile = codec == 1 ? LiveVideoProfile::H264ConstrainedBaseline : LiveVideoProfile::H265Main;
config.level = codec == 1 ? 31 : 93; config.backend = codec == 1 ? "libx264" : "libx265"; return config;
}
private slots:
void initTestCase() {
debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN); debug_set_category_level(DEBUG_CATEGORY_CALL, DEBUG_LEVEL_DEBUG);
av_log_set_level(AV_LOG_ERROR);
}
void switchesAtScheduledIdr_data() {
QTest::addColumn<int>("codec"); QTest::newRow("H264") << 1; QTest::newRow("H265") << 2;
}
void switchesAtScheduledIdr() {
QFETCH(int, codec);
auto config = configuration(codec);
if (!avcodec_find_encoder_by_name(config.backend.toLatin1().constData())) QSKIP("Software encoder unavailable");
CallVideoEncoder encoder; QVERIFY2(encoder.open(config, 17, 4000000), qPrintable(encoder.error()));
Decoder decoder; QVERIFY(decoder.open(codec));
QImage image(config.width, config.height, QImage::Format_RGB32);
uint32_t random = 1; unsigned completions = 0;
for (int phase = 0; phase < 3; phase++) {
uint64_t bytes = 0; uint32_t target = phase == 1 ? 300000 : 4000000;
for (int frame = 0; frame < 120; frame++) {
for (int y = 0; y < image.height(); y++) {
auto* row = reinterpret_cast<QRgb*>(image.scanLine(y));
for (int x = 0; x < image.width(); x++) {
random = random * 1664525u + 1013904223u;
int v = random >> 24; row[x] = qRgb(v, v, v);
}
}
if (phase && frame == 7) {
QVERIFY(!encoder.requestBitrate(16, phase, target)); // Чужой generation.
QVERIFY(encoder.requestBitrate(17, phase, target));
QVERIFY(!encoder.requestBitrate(17, phase + 100, target)); // Нельзя затереть pending.
}
uint64_t pts = 5000000 + (phase * 120 + frame) * 1000000ull / 30;
LiveEncodedFrame output; CallVideoRateCompletion completion;
QVERIFY2(encoder.encode(image, pts, false, output, completion), qPrintable(encoder.error()));
QCOMPARE(output.pts, pts); QCOMPARE(output.key, frame % 30 == 0);
if (phase && frame == 30) {
QCOMPARE(completion.generation, uint64_t(17)); QCOMPARE(completion.request, uint64_t(phase));
QCOMPARE(completion.bitrate, target); QCOMPARE(encoder.bitrate(), target); completions++;
QVERIFY(!encoder.requestBitrate(17, phase, target)); // Replay уже выполненного request.
} else QCOMPARE(completion.request, uint64_t(0));
if (phase && frame < 30) QCOMPARE(encoder.bitrate(), phase == 1 ? 4000000u : 300000u);
QVERIFY(decoder.decode(output)); QCOMPARE(decoder.frame->pts, (int64_t)pts);
QCOMPARE(decoder.frame->width, config.width); QCOMPARE(decoder.frame->height, config.height);
QCOMPARE(decoder.frame->format, int(AV_PIX_FMT_YUV420P)); QCOMPARE(decoder.context->has_b_frames, 0);
QCOMPARE(decoder.context->profile, codec == 1 ? AV_PROFILE_H264_CONSTRAINED_BASELINE : AV_PROFILE_HEVC_MAIN);
QCOMPARE(decoder.context->level, config.level);
if (frame >= 60) bytes += output.data.size();
}
uint64_t measured = bytes * 8 / 2;
DEBUG_INFO(DEBUG_CATEGORY_CALL, "call encoder test codec=%d phase=%d target=%u measured=%llu",
codec, phase, target, (unsigned long long)measured);
QVERIFY(measured > target / 2 && measured < uint64_t(target) * 3 / 2);
}
QCOMPARE(completions, 2u);
}
void keepsOldEncoderAndRejectsInvalidCommands() {
auto config = configuration(1); config.width = 160; config.height = 90; config.bitrate = 500000;
if (!avcodec_find_encoder_by_name("libx264")) QSKIP("libx264 unavailable");
CallVideoEncoder encoder; QVERIFY(encoder.open(config, 9, 700000));
QImage image(160, 90, QImage::Format_RGB32); image.fill(Qt::blue);
LiveEncodedFrame output; CallVideoRateCompletion completion;
QVERIFY(encoder.encode(image, 100000, false, output, completion));
QVERIFY(!encoder.requestBitrate(9, 1, 700001)); QVERIFY(!encoder.requestBitrate(9, 1, 299999));
auto bad = config; bad.backend = "unavailable"; QVERIFY(!encoder.open(bad, 10, 700000));
QVERIFY(encoder.requestBitrate(9, 1, 500000)); // Неизменный target подтверждается на KEY.
QVERIFY(encoder.encode(image, 133333, false, output, completion)); QVERIFY(!completion.request && !output.key);
QVERIFY(encoder.encode(image, 166666, true, output, completion));
QCOMPARE(completion.request, uint64_t(1)); QCOMPARE(completion.bitrate, 500000u); QVERIFY(output.key);
auto saved = output; QVERIFY(!encoder.encode(image, 166666, false, output, completion)); QCOMPARE(output.data, saved.data);
// Пропуск capture-кадров не откладывает IDR на количество пропущенных кадров.
QVERIFY(encoder.requestBitrate(9, 2, 300000));
QVERIFY(encoder.encode(image, 1500000, false, output, completion));
QCOMPARE(completion.request, uint64_t(2)); QCOMPARE(completion.bitrate, 300000u); QVERIFY(output.key);
encoder.close(); QVERIFY(!encoder.requestBitrate(9, 3, 500000));
}
};
QTEST_GUILESS_MAIN(CallVideoEncoderTest)
#include "test_call_video_encoder.moc"
Loading…
Cancel
Save