From 64bbbdc631c29a721be857b267fd8b3de5715527 Mon Sep 17 00:00:00 2001 From: evgeny Date: Sat, 17 Oct 2026 01:28:42 +0200 Subject: [PATCH] Guard RGB conversion bounds and use padded frames in file playback --- tools/chatgui/CMakeLists.txt | 6 ++ tools/chatgui/src/videoplayer_engine.cpp | 21 ++++--- .../chatgui/tests/test_video_frame_image.cpp | 56 +++++++++++++++++++ 3 files changed, 76 insertions(+), 7 deletions(-) create mode 100644 tools/chatgui/tests/test_video_frame_image.cpp diff --git a/tools/chatgui/CMakeLists.txt b/tools/chatgui/CMakeLists.txt index b805b5df..61eeddda 100644 --- a/tools/chatgui/CMakeLists.txt +++ b/tools/chatgui/CMakeLists.txt @@ -144,6 +144,12 @@ target_link_libraries(chatgui_live_video PUBLIC ${QT_LIBS} utun PRIVATE ${FFMPEG target_include_directories(chatgui_live_video PRIVATE ${FFMPEG_INCLUDE_DIRS}) if(FFMPEG_FOUND) target_compile_definitions(chatgui_live_video PRIVATE HAVE_FFMPEG) + add_executable(test_video_frame_image tests/test_video_frame_image.cpp) + target_compile_definitions(test_video_frame_image PRIVATE HAVE_FFMPEG) + target_include_directories(test_video_frame_image PRIVATE ${FFMPEG_INCLUDE_DIRS}) + target_link_libraries(test_video_frame_image PRIVATE ${QT_LIBS} ${QT_TEST_LIB} utun ${FFMPEG_TARGET} ${FFMPEG_LIBRARIES}) + add_test(NAME test_video_frame_image COMMAND test_video_frame_image) + set_tests_properties(test_video_frame_image PROPERTIES TIMEOUT 30) 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}) diff --git a/tools/chatgui/src/videoplayer_engine.cpp b/tools/chatgui/src/videoplayer_engine.cpp index 89e94382..0e593caa 100644 --- a/tools/chatgui/src/videoplayer_engine.cpp +++ b/tools/chatgui/src/videoplayer_engine.cpp @@ -1,5 +1,6 @@ #include #include "videoplayer_engine.h" +#include "video_frame_image.h" #include "sound_manager.h" #include "../../lib/debug_config.h" @@ -200,14 +201,20 @@ void Worker::run() { int gen = epoch->loadRelaxed(); auto emitFrame = [&](AVFrame* fr) { - if (fr->pts == AV_NOPTS_VALUE) return; /* кадр без pts ломает очередь */ - QImage img(vw, vh, QImage::Format_RGB888); - uint8_t* dst[1] = { img.bits() }; - int dstStride[1] = { (int)img.bytesPerLine() }; - sws_scale(sws, fr->data, fr->linesize, 0, vh, dst, dstStride); + if (fr->pts == AV_NOPTS_VALUE) return true; /* кадр без pts ломает очередь */ + QImage img = video_frame_image(vw, vh); + if (img.isNull()) { worker_fail(this, QCoreApplication::translate("ChatGui", "alloc failed")); return false; } + uint8_t* dst[4] = { img.bits(), nullptr, nullptr, nullptr }; + int dstStride[4] = { (int)img.bytesPerLine(), 0, 0, 0 }; + int rows = sws_scale(sws, fr->data, fr->linesize, 0, vh, dst, dstStride); + if (rows != vh) { + DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "VideoEngine: RGB conversion failed size=%dx%d stride=%d rows=%d", vw, vh, dstStride[0], rows); + worker_fail(this, QCoreApplication::translate("ChatGui", "Video frame conversion failed")); return false; + } qint64 ptsMs = av_rescale_q(fr->pts, vst->time_base, AVRational{1, 1000}); buffered->fetchAndAddRelaxed(1); emit frameDecoded(img, ptsMs, gen); + return true; }; bool eof = false; @@ -232,7 +239,7 @@ void Worker::run() { int ret = av_read_frame(fmt, pkt); if (ret < 0) { avcodec_send_packet(vdec, nullptr); - while (avcodec_receive_frame(vdec, frame) == 0) emitFrame(frame); + while (avcodec_receive_frame(vdec, frame) == 0) if (!emitFrame(frame)) { cleanup(); return; } eof = true; DEBUG_INFO(DEBUG_CATEGORY_VIDEO, "VideoEngine: decode finished"); emit finished(); @@ -240,7 +247,7 @@ void Worker::run() { } if (pkt->stream_index == vidx) { avcodec_send_packet(vdec, pkt); - while (avcodec_receive_frame(vdec, frame) == 0) emitFrame(frame); + while (avcodec_receive_frame(vdec, frame) == 0) if (!emitFrame(frame)) { cleanup(); return; } } av_packet_unref(pkt); } diff --git a/tools/chatgui/tests/test_video_frame_image.cpp b/tools/chatgui/tests/test_video_frame_image.cpp new file mode 100644 index 00000000..abcbad5b --- /dev/null +++ b/tools/chatgui/tests/test_video_frame_image.cpp @@ -0,0 +1,56 @@ +/* Реальный swscale: SIMD не выходит за RGB-строки, QImage сохраняет буфер после освобождения копий. */ +#include "../src/video_frame_image.h" +#include +#include +#include +#include +extern "C" { +#include +} + +class VideoFrameImageTest : public QObject { + Q_OBJECT +private slots: + void initTestCase() { debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN); } + + void bounds_data() { + QTest::addColumn("width"); QTest::addColumn("height"); QTest::addColumn("flags"); + for (int flags : {SWS_BILINEAR, SWS_FAST_BILINEAR}) { + for (int width = 1; width <= 65; width++) + QTest::newRow(qPrintable(QString("%1x3-flags%2").arg(width).arg(flags))) << width << 3 << flags; + for (int width : {160, 1720, 1919, 1920, 3440, 4096}) + QTest::newRow(qPrintable(QString("%1x720-flags%2").arg(width).arg(flags))) << width << 720 << flags; + } + } + + void bounds() { + QFETCH(int, width); QFETCH(int, height); QFETCH(int, flags); + AVFrame* source = av_frame_alloc(); QVERIFY(source); + auto releaseSource = qScopeGuard([&] { av_frame_free(&source); }); + source->format = AV_PIX_FMT_YUV420P; source->width = width; source->height = height; + QCOMPARE(av_frame_get_buffer(source, 32), 0); + for (int plane = 0; plane < 3; plane++) + memset(source->data[plane], plane ? 128 : 32, source->linesize[plane] * (plane ? (height + 1) / 2 : height)); + QImage image = video_frame_image(width, height); QVERIFY(!image.isNull()); + QCOMPARE(image.size(), QSize(width, height)); QCOMPARE(image.format(), QImage::Format_RGB888); + SwsContext* converter = sws_getContext(width, height, AV_PIX_FMT_YUV420P, width, height, AV_PIX_FMT_RGB24, + flags, nullptr, nullptr, nullptr); + QVERIFY(converter); auto releaseConverter = qScopeGuard([&] { sws_freeContext(converter); }); + // Проверяем границу видимых строк отдельно от запаса аллокатора FFmpeg: он не должен скрывать переполнение. + size_t bytes = (size_t)image.sizeInBytes(); std::vector guarded(bytes + 256, 0xaa); + uint8_t* output[4] = {guarded.data(), nullptr, nullptr, nullptr}; int strides[4] = {(int)image.bytesPerLine(), 0, 0, 0}; + QCOMPARE(sws_scale(converter, source->data, source->linesize, 0, height, output, strides), height); + QVERIFY2(std::all_of(guarded.begin() + bytes, guarded.end(), [](uint8_t value) { return value == 0xaa; }), + "swscale wrote beyond the RGB image allocation"); + output[0] = image.bits(); + QCOMPARE(sws_scale(converter, source->data, source->linesize, 0, height, output, strides), height); + for (int y = 0; y < height; y++) QCOMPARE(memcmp(image.constScanLine(y), guarded.data() + y * strides[0], width * 3), 0); + QImage shared = image; image = {}; + QCOMPARE(shared.pixelColor(width - 1, height - 1), QColor(19, 19, 19)); + QImage detached = shared; detached.fill(Qt::red); + QCOMPARE(shared.pixelColor(0, 0), QColor(19, 19, 19)); QCOMPARE(detached.pixelColor(0, 0), QColor(Qt::red)); + } +}; + +QTEST_GUILESS_MAIN(VideoFrameImageTest) +#include "test_video_frame_image.moc"