Add bounded mouse zoom and panning to live stream viewer

This commit is contained in:
evgeny committed 2026-10-17 01:49:35 +02:00
1 parent 93dc21f3c8
commit 1dc26bf715
5 files changed
+306 -4

No files matched your search

+137
View File
@@ -1,5 +1,6 @@
/* Настоящее ядро/actor, подписанный медиаблок и H.264 decoder: каталог до подписки, эпоха, показ и teardown. */
#include "../src/streamplayer_window.h"
#include "../src/videoplayer_window.h"
#include "../src/sound_manager.h"
#include "../transport/stream_bridge.h"
#include <QtTest>
@@ -7,6 +8,8 @@
#include <QJsonArray>
#include <QLabel>
#include <QSlider>
#include <QPainter>
#include <QWheelEvent>
#include <atomic>
#include <cmath>
#include <chrono>
@@ -28,6 +31,44 @@ extern "C" {
}
static QList<QByteArray> snapshots;
/* Проверяем изображение на выходе QWidget, включая физические пиксели при QT_SCALE_FACTOR=2. */
static QImage renderedVideo(VideoRenderWidget& view) {
QImage image(QSize(qRound(view.width() * view.devicePixelRatioF()), qRound(view.height() * view.devicePixelRatioF())),
QImage::Format_RGB32);
image.setDevicePixelRatio(view.devicePixelRatioF()); view.render(&image); return image;
}
static QImage coordinateFrame() {
QImage image(1200, 900, QImage::Format_RGB32);
for (int y = 0; y < image.height(); y++) {
auto* row = reinterpret_cast<QRgb*>(image.scanLine(y));
for (int x = 0; x < image.width(); x++) row[x] = qRgb(1 + x % 251, 1 + y % 251, 1 + x / 251 + 16 * (y / 251));
}
return image;
}
static void zoomVideo(VideoRenderWidget& view, int delta, QPoint position, bool pixels = false) {
QWheelEvent event(position, view.mapToGlobal(position), pixels ? QPoint(0, delta) : QPoint(),
pixels ? QPoint() : QPoint(0, delta), Qt::NoButton, Qt::NoModifier, Qt::NoScrollPhase, false);
QApplication::sendEvent(&view, &event);
}
static void moveVideo(VideoRenderWidget& view, QPoint position, Qt::MouseButtons buttons = Qt::LeftButton) {
QMouseEvent event(QEvent::MouseMove, position, view.mapToGlobal(position), Qt::NoButton, buttons, Qt::NoModifier);
QApplication::sendEvent(&view, &event);
}
static void dragVideo(VideoRenderWidget& view, QPoint from, QPoint to) {
QTest::mousePress(&view, Qt::LeftButton, Qt::NoModifier, from); moveVideo(view, to);
QTest::mouseRelease(&view, Qt::LeftButton, Qt::NoModifier, to);
}
static QImage nativeCrop(const QImage& frame, const VideoRenderWidget& view, QPoint origin) {
QImage image = frame.copy(QRect(origin, QSize(qRound(view.width() * view.devicePixelRatioF()),
qRound(view.height() * view.devicePixelRatioF()))));
image.setDevicePixelRatio(view.devicePixelRatioF()); return image;
}
#if defined(TEST_NULL_AUDIO) && defined(HAVE_FFMPEG)
static std::atomic<bool> delayedDecoder{false};
static AVCodecContext* delayedContext = nullptr;
@@ -152,6 +193,7 @@ private slots:
if (qEnvironmentVariableIsSet("UTUN_TEST_DEBUG")) {
debug_set_category_level(DEBUG_CATEGORY_STREAM, DEBUG_LEVEL_TRACE);
debug_set_category_level(DEBUG_CATEGORY_DEBUG, DEBUG_LEVEL_TRACE);
debug_set_category_level(DEBUG_CATEGORY_VIDEO, DEBUG_LEVEL_TRACE);
}
debug_set_category_level(DEBUG_CATEGORY_SYS, DEBUG_LEVEL_ERROR);
allocations = u_get_allocated_count(); ua = uasync_create(); QVERIFY(ua);
@@ -169,6 +211,101 @@ private slots:
connect(&poll, &QTimer::timeout, this, [this] { uasync_poll(ua, 0); }); poll.start(1);
}
void zoomNativeAndFrameUpdates() {
VideoRenderWidget view(nullptr, true); view.resize(200, 150); view.show();
QImage frame = coordinateFrame(); view.setFrame(frame);
QPoint center(100, 75);
zoomVideo(view, 12000, center);
QSize physical = renderedVideo(view).size();
QPoint origin((frame.width() - physical.width()) / 2, (frame.height() - physical.height()) / 2);
QCOMPARE(renderedVideo(view), nativeCrop(frame, view, origin));
zoomVideo(view, 12000, center); // Верхняя граница достигается точно и не превышается.
QCOMPARE(renderedVideo(view), nativeCrop(frame, view, origin));
dragVideo(view, center, QPoint(5000, 5000));
QCOMPARE(renderedVideo(view), nativeCrop(frame, view, QPoint()));
frame.invertPixels(); view.setFrame(frame); // Следующий кадр сохраняет масштаб и рассматриваемый участок.
QCOMPARE(renderedVideo(view), nativeCrop(frame, view, QPoint()));
dragVideo(view, center, QPoint(-5000, -5000));
origin = QPoint(frame.width() - physical.width(), frame.height() - physical.height());
QCOMPARE(renderedVideo(view), nativeCrop(frame, view, origin));
view.resize(240, 180); QApplication::processEvents();
physical = renderedVideo(view).size(); origin = QPoint(frame.width() - physical.width(), frame.height() - physical.height());
QCOMPARE(renderedVideo(view), nativeCrop(frame, view, origin));
view.resize(1400, 1000); QApplication::processEvents(); // При 1:1 весь кадр временно помещается в большое окно.
view.resize(200, 150); QApplication::processEvents();
physical = renderedVideo(view).size(); origin = QPoint((frame.width() - physical.width()) / 2, (frame.height() - physical.height()) / 2);
QCOMPARE(renderedVideo(view), nativeCrop(frame, view, origin));
view.clearFrame();
QImage black = renderedVideo(view); black.fill(Qt::black); QCOMPARE(renderedVideo(view), black);
}
void zoomAnchorAndFineWheel() {
VideoRenderWidget view(nullptr, true); view.resize(200, 150); view.show();
QImage frame(1200, 900, QImage::Format_RGB32); frame.fill(Qt::red);
// Маркер находится под курсором (120, 80) в исходном вписанном кадре.
{ QPainter painter(&frame); painter.fillRect(QRect(700, 460, 40, 40), Qt::green); }
view.setFrame(frame); QPoint anchor(120, 80);
zoomVideo(view, 60, anchor); zoomVideo(view, 60, anchor);
zoomVideo(view, 30, anchor, true); // Устройство, присылающее только pixelDelta.
zoomVideo(view, 12000, anchor);
QImage actual = renderedVideo(view);
QPoint pixel(qRound(anchor.x() * view.devicePixelRatioF()), qRound(anchor.y() * view.devicePixelRatioF()));
QCOMPARE(actual.pixelColor(pixel), QColor(Qt::green));
QCOMPARE(actual.pixelColor(pixel + QPoint(25, 0)), QColor(Qt::red)); // Маркер при 1:1 имеет исходный размер.
QCOMPARE(actual.pixelColor(pixel - QPoint(25, 0)), QColor(Qt::red));
}
void zoomLetterboxAndReset_data() {
QTest::addColumn<QSize>("frameSize");
QTest::newRow("landscape") << QSize(1200, 300);
QTest::newRow("portrait") << QSize(300, 1200);
}
void zoomLetterboxAndReset() {
QFETCH(QSize, frameSize);
VideoRenderWidget view(nullptr, true); view.resize(200, 150); view.show();
QImage frame(frameSize, QImage::Format_RGB32); frame.fill(Qt::red); view.setFrame(frame);
QPoint center(100, 75); zoomVideo(view, 120, center);
QImage before = renderedVideo(view);
dragVideo(view, center, QPoint(5000, 5000));
QCOMPARE(renderedVideo(view), before); // Помещающаяся ось остаётся по центру, вторая полностью покрыта однотонным кадром.
QCOMPARE(before.pixelColor(0, 0), QColor(Qt::black));
QCOMPARE(before.pixelColor(before.width() / 2, before.height() / 2), QColor(Qt::red));
zoomVideo(view, -12000, center); QImage fitted = renderedVideo(view);
zoomVideo(view, -12000, center); QCOMPARE(renderedVideo(view), fitted);
dragVideo(view, center, QPoint(-5000, -5000)); QCOMPARE(renderedVideo(view), fitted);
zoomVideo(view, 12000, center);
QImage small(100, 80, QImage::Format_RGB32); small.fill(Qt::green); view.setFrame(small);
QImage expected = renderedVideo(view); expected.fill(Qt::black);
qreal dpr = view.devicePixelRatioF();
{ QPainter painter(&expected); painter.drawImage(QRectF((view.width() - 100 / dpr) / 2,
(view.height() - 80 / dpr) / 2, 100 / dpr, 80 / dpr), small, QRectF(small.rect())); }
QCOMPARE(renderedVideo(view), expected); // Смена размера сбрасывает zoom; маленький кадр остаётся в 1:1.
zoomVideo(view, 12000, center); QCOMPARE(renderedVideo(view), expected);
}
void zoomDragCancellationAndFullscreen() {
VideoRenderWidget view(nullptr, true); view.resize(200, 150); view.show();
QImage frame = coordinateFrame(); view.setFrame(frame); QPoint center(100, 75);
zoomVideo(view, 12000, center);
for (QEvent::Type type : {QEvent::UngrabMouse, QEvent::WindowDeactivate, QEvent::Hide}) {
QTest::mousePress(&view, Qt::LeftButton, Qt::NoModifier, center);
QEvent cancelled(type); QApplication::sendEvent(&view, &cancelled);
QImage before = renderedVideo(view); moveVideo(view, center + QPoint(20, 10));
QCOMPARE(renderedVideo(view), before);
QTest::mouseRelease(&view, Qt::LeftButton, Qt::NoModifier, center);
}
QImage before = renderedVideo(view);
QTest::mousePress(&view, Qt::LeftButton, Qt::NoModifier, center); moveVideo(view, center, Qt::NoButton);
moveVideo(view, center + QPoint(20, 10)); QCOMPARE(renderedVideo(view), before);
QTest::mouseRelease(&view, Qt::LeftButton, Qt::NoModifier, center);
view.showFullScreen(); QApplication::processEvents();
view.showNormal(); QApplication::processEvents(); QCOMPARE(renderedVideo(view), before);
VideoRenderWidget ordinary; ordinary.resize(200, 150); ordinary.show(); ordinary.setFrame(frame);
before = renderedVideo(ordinary); zoomVideo(ordinary, 12000, center);
dragVideo(ordinary, center, QPoint(5000, 5000)); QCOMPARE(renderedVideo(ordinary), before);
}
void catalogueWithoutViewer() {
QTRY_COMPARE(snapshots.size(), 1);
QCOMPARE(snapshots.last().mid(9), QByteArray("[]")); QVERIFY(!stream_service_needs_activity(inst));