Проверить GPU-конвейер на задержке, пакетной выдаче и отказе замены
This commit is contained in:
1 parent
2facdb6949
commit
09f48403c5
2 files changed
+155
No files matched your search
@@ -156,6 +156,14 @@ if(FFMPEG_FOUND)
|
|||||||
add_test(NAME test_call_video_encoder COMMAND test_call_video_encoder)
|
add_test(NAME test_call_video_encoder COMMAND test_call_video_encoder)
|
||||||
set_tests_properties(test_call_video_encoder PROPERTIES TIMEOUT 120)
|
set_tests_properties(test_call_video_encoder PROPERTIES TIMEOUT 120)
|
||||||
if(UNIX AND NOT APPLE)
|
if(UNIX AND NOT APPLE)
|
||||||
|
add_executable(test_call_video_gpu tests/test_call_video_gpu.cpp)
|
||||||
|
target_include_directories(test_call_video_gpu PRIVATE ${FFMPEG_INCLUDE_DIRS})
|
||||||
|
target_link_libraries(test_call_video_gpu PRIVATE chatgui_live_video ${QT_TEST_LIB} ${FFMPEG_TARGET} ${FFMPEG_LIBRARIES})
|
||||||
|
foreach(symbol avcodec_find_encoder_by_name avcodec_open2 avcodec_send_frame avcodec_receive_packet avcodec_free_context)
|
||||||
|
target_link_options(test_call_video_gpu PRIVATE "-Wl,--wrap=${symbol}")
|
||||||
|
endforeach()
|
||||||
|
add_test(NAME test_call_video_gpu COMMAND test_call_video_gpu)
|
||||||
|
set_tests_properties(test_call_video_gpu PROPERTIES TIMEOUT 60)
|
||||||
add_executable(test_call_video_clock tests/test_call_video_clock.cpp src/call_video_engine.cpp)
|
add_executable(test_call_video_clock tests/test_call_video_clock.cpp src/call_video_engine.cpp)
|
||||||
target_link_libraries(test_call_video_clock PRIVATE chatgui_live_video)
|
target_link_libraries(test_call_video_clock PRIVATE chatgui_live_video)
|
||||||
foreach(symbol call_audio_get_clock call_video_port_open call_video_port_close call_video_port_status
|
foreach(symbol call_audio_get_clock call_video_port_open call_video_port_close call_video_port_status
|
||||||
|
|||||||
@@ -0,0 +1,147 @@
|
|||||||
|
/* Настоящие software bitstream/decoder под GPU-границей FFmpeg: задержка, пачки,
|
||||||
|
* PTS QSV 90kHz, AMF с периодическим IDR, отказ замены и запрет CPU fallback.
|
||||||
|
* Драйвер не эмулируется: тест проверяет конвейер приложения, аппаратные прогоны нужны отдельно. */
|
||||||
|
#include "../src/call_video_encoder.h"
|
||||||
|
#include "../src/call_video_decoder.h"
|
||||||
|
#include <QtTest>
|
||||||
|
#include <QScopeGuard>
|
||||||
|
#include <deque>
|
||||||
|
#include <map>
|
||||||
|
#include <mutex>
|
||||||
|
extern "C" {
|
||||||
|
#include "debug_config.h"
|
||||||
|
#include <libavcodec/avcodec.h>
|
||||||
|
#include <libavutil/opt.h>
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Pending {
|
||||||
|
std::deque<AVPacket*> packets;
|
||||||
|
unsigned inputs = 0, serial = 0;
|
||||||
|
};
|
||||||
|
static std::mutex lock;
|
||||||
|
static std::map<AVCodecContext*, Pending> contexts;
|
||||||
|
static QByteArray selected;
|
||||||
|
static unsigned opened = 0, software = 0;
|
||||||
|
static int depth = 1;
|
||||||
|
static bool burst = false, badReplacement = false;
|
||||||
|
|
||||||
|
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) {
|
||||||
|
QByteArray requested(name);
|
||||||
|
bool hardware = requested.endsWith("_nvenc") || requested.endsWith("_amf")
|
||||||
|
|| requested.endsWith("_qsv") || requested.endsWith("_vaapi");
|
||||||
|
if (hardware) return requested == selected ? __real_avcodec_find_encoder_by_name(
|
||||||
|
requested.startsWith("h264") ? "libx264" : "libx265") : nullptr;
|
||||||
|
++software; return __real_avcodec_find_encoder_by_name(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" int __real_avcodec_open2(AVCodecContext*, const AVCodec*, AVDictionary**);
|
||||||
|
extern "C" int __wrap_avcodec_open2(AVCodecContext* context, const AVCodec* codec, AVDictionary** options) {
|
||||||
|
if (!av_codec_is_encoder(codec)) return __real_avcodec_open2(context, codec, options);
|
||||||
|
std::lock_guard<std::mutex> guard(lock);
|
||||||
|
if (selected.endsWith("_nvenc")) {
|
||||||
|
auto* delay = av_dict_get(*options, "delay", nullptr, 0);
|
||||||
|
if (!delay || QByteArray(delay->value) != "0") return AVERROR(EINVAL);
|
||||||
|
}
|
||||||
|
if (selected.endsWith("_qsv") && context->time_base.den != 90000) return AVERROR(EINVAL);
|
||||||
|
if (selected.endsWith("_amf") && context->pix_fmt != AV_PIX_FMT_NV12 && context->pix_fmt != AV_PIX_FMT_YUV420P)
|
||||||
|
return AVERROR(EINVAL);
|
||||||
|
av_dict_free(options);
|
||||||
|
av_dict_set(options, "preset", "ultrafast", 0); av_dict_set(options, "tune", "zerolatency", 0);
|
||||||
|
av_dict_set(options, "forced-idr", "1", 0);
|
||||||
|
bool h264 = context->codec_id == AV_CODEC_ID_H264;
|
||||||
|
av_dict_set(options, "profile", h264 ? "baseline" : "main", 0);
|
||||||
|
QByteArray parameters = h264 ? "scenecut=0:open-gop=0:ref=1:level=" :
|
||||||
|
"pools=1:frame-threads=1:bframes=0:rc-lookahead=0:open-gop=0:ref=1:info=0:log-level=error:high-tier=0:level-idc=";
|
||||||
|
parameters += QByteArray::number(h264 || selected.endsWith("_qsv") ? context->level : context->level / 3);
|
||||||
|
av_dict_set(options, h264 ? "x264-params" : "x265-params", parameters.constData(), 0);
|
||||||
|
int result = __real_avcodec_open2(context, codec, options);
|
||||||
|
if (result >= 0) contexts[context].serial = ++opened;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" int __real_avcodec_send_frame(AVCodecContext*, const AVFrame*);
|
||||||
|
extern "C" int __wrap_avcodec_send_frame(AVCodecContext* context, const AVFrame* frame) {
|
||||||
|
int result = __real_avcodec_send_frame(context, frame);
|
||||||
|
if (result >= 0 && frame) { std::lock_guard<std::mutex> guard(lock); ++contexts[context].inputs; }
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
extern "C" int __real_avcodec_receive_packet(AVCodecContext*, AVPacket*);
|
||||||
|
extern "C" int __wrap_avcodec_receive_packet(AVCodecContext* context, AVPacket* output) {
|
||||||
|
std::lock_guard<std::mutex> guard(lock);
|
||||||
|
auto& state = contexts[context];
|
||||||
|
AVPacket* packet = av_packet_alloc(); if (!packet) return AVERROR(ENOMEM);
|
||||||
|
int result = __real_avcodec_receive_packet(context, packet);
|
||||||
|
if (result >= 0) {
|
||||||
|
if (badReplacement && state.serial >= 4) packet->flags &= ~AV_PKT_FLAG_KEY;
|
||||||
|
state.packets.push_back(packet);
|
||||||
|
} else av_packet_free(&packet);
|
||||||
|
if (result < 0 && result != AVERROR(EAGAIN)) return result;
|
||||||
|
bool ready = burst ? state.inputs % 3 == 0 : state.packets.size() > size_t(depth);
|
||||||
|
if (!ready || state.packets.empty()) return AVERROR(EAGAIN);
|
||||||
|
av_packet_move_ref(output, state.packets.front()); av_packet_free(&state.packets.front()); state.packets.pop_front(); return 0;
|
||||||
|
}
|
||||||
|
extern "C" void __real_avcodec_free_context(AVCodecContext**);
|
||||||
|
extern "C" void __wrap_avcodec_free_context(AVCodecContext** context) {
|
||||||
|
{ std::lock_guard<std::mutex> guard(lock);
|
||||||
|
auto it = contexts.find(*context);
|
||||||
|
if (it != contexts.end()) { for (auto* packet : it->second.packets) av_packet_free(&packet); contexts.erase(it); } }
|
||||||
|
__real_avcodec_free_context(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
class GpuTest : public QObject {
|
||||||
|
Q_OBJECT
|
||||||
|
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 = false; QVERIFY(contexts.empty()); }
|
||||||
|
void cleanup() { QVERIFY(contexts.empty()); }
|
||||||
|
void gpuOnly() {
|
||||||
|
CallVideoEncoder encoder; call_video_format format{1, 640, 360, 30, 0, 1, 31, 4000000, 2};
|
||||||
|
QVERIFY(!encoder.open(format, 1000000)); QCOMPARE(software, 0u);
|
||||||
|
}
|
||||||
|
void pipeline_data() {
|
||||||
|
QTest::addColumn<QByteArray>("backend"); QTest::addColumn<bool>("batch"); QTest::addColumn<bool>("reject");
|
||||||
|
for (auto name : {"h264_nvenc", "hevc_nvenc", "h264_amf", "hevc_amf", "h264_qsv", "hevc_qsv"})
|
||||||
|
QTest::newRow(name) << QByteArray(name) << false << false;
|
||||||
|
QTest::newRow("batch") << QByteArray("h264_nvenc") << true << false;
|
||||||
|
QTest::newRow("rollback") << QByteArray("h264_nvenc") << false << true;
|
||||||
|
}
|
||||||
|
void pipeline() {
|
||||||
|
QFETCH(QByteArray, backend); QFETCH(bool, batch); QFETCH(bool, reject);
|
||||||
|
selected = backend; burst = batch; badReplacement = reject;
|
||||||
|
const bool h264 = selected.startsWith("h264");
|
||||||
|
call_video_format format{1, 640, 360, 30, 0, uint8_t(h264 ? 1 : 2), uint8_t(h264 ? 31 : 93), 4000000, uint8_t(h264 ? 2 : 1)};
|
||||||
|
CallVideoEncoder encoder; QVERIFY2(encoder.open(format, 1000000), qPrintable(encoder.error()));
|
||||||
|
QCOMPARE(software, 0u);
|
||||||
|
CallVideoDecoder decoder; QImage image(640, 360, QImage::Format_RGB32); image.fill(Qt::blue);
|
||||||
|
uint64_t last = 0, request = 0; unsigned shown = 0, maximumBatch = 0;
|
||||||
|
bool completed = false, failed = false;
|
||||||
|
for (unsigned i = 0; i < 120; ++i) {
|
||||||
|
if (i == 7) { request = 1; QVERIFY(encoder.requestBitrate(1, request, 300000)); }
|
||||||
|
if (request && !completed && !failed) QTest::qWait(5);
|
||||||
|
const uint64_t pts = 1234567 + i * 33333ull; QList<LiveEncodedFrame> output; CallVideoRateCompletion completion;
|
||||||
|
QVERIFY2(encoder.encode(image, pts, false, output, completion), qPrintable(encoder.error()));
|
||||||
|
if (completion.request) {
|
||||||
|
QCOMPARE(completion.request, request); QCOMPARE(completion.bitrate, reject ? 0u : 300000u);
|
||||||
|
completed = completion.bitrate != 0; failed = !completed;
|
||||||
|
}
|
||||||
|
maximumBatch = std::max(maximumBatch, unsigned(output.size()));
|
||||||
|
for (const auto& frame : output) {
|
||||||
|
QVERIFY(frame.pts > last); QVERIFY(frame.pts <= pts);
|
||||||
|
QCOMPARE((frame.pts - 1234567) % 33333, uint64_t(0));
|
||||||
|
QVERIFY(pts - frame.pts <= (batch ? 2u : 1u) * 33333);
|
||||||
|
call_video_frame wire{}; wire.call_id = 1; wire.frame_id = ++shown; wire.pts_us = frame.pts;
|
||||||
|
wire.format = format; wire.size = wire.length = frame.data.size(); wire.flags = frame.key ? CALL_VIDEO_KEY : 0;
|
||||||
|
wire.data = reinterpret_cast<const uint8_t*>(frame.data.constData());
|
||||||
|
QImage decoded; QVERIFY(decoder.decode(wire, decoded));
|
||||||
|
last = frame.pts;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QVERIFY(shown >= 115); QCOMPARE(completed, !reject); QCOMPARE(failed, reject);
|
||||||
|
QCOMPARE(encoder.bitrate(), reject ? 1000000u : 300000u);
|
||||||
|
if (batch) QVERIFY(maximumBatch >= 2);
|
||||||
|
qInfo("GPU pipeline %s shown=%u max_batch=%u rollback=%d", selected.constData(), shown, maximumBatch, failed);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
QTEST_GUILESS_MAIN(GpuTest)
|
||||||
|
#include "test_call_video_gpu.moc"
|
||||||
Reference in new issue
Block a user