Add bounded mouse zoom and panning to live stream viewer
This commit is contained in:
1 parent
93dc21f3c8
commit
1dc26bf715
5 files changed
+306
-4
No files matched your search
@@ -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));
|
||||
|
||||
Reference in new issue
Block a user