Guard RGB conversion bounds and use padded frames in file playback
This commit is contained in:
1 parent
f81b93ad0d
commit
64bbbdc631
3 files changed
+76
-7
No files matched your search
@@ -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})
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include <QCoreApplication>
|
||||
#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);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
/* Реальный swscale: SIMD не выходит за RGB-строки, QImage сохраняет буфер после освобождения копий. */
|
||||
#include "../src/video_frame_image.h"
|
||||
#include <QtTest>
|
||||
#include <QScopeGuard>
|
||||
#include <algorithm>
|
||||
#include <vector>
|
||||
extern "C" {
|
||||
#include <libswscale/swscale.h>
|
||||
}
|
||||
|
||||
class VideoFrameImageTest : public QObject {
|
||||
Q_OBJECT
|
||||
private slots:
|
||||
void initTestCase() { debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN); }
|
||||
|
||||
void bounds_data() {
|
||||
QTest::addColumn<int>("width"); QTest::addColumn<int>("height"); QTest::addColumn<int>("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<uint8_t> 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"
|
||||
Reference in new issue
Block a user