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
@@ -4,6 +4,13 @@
|
||||
|
||||
## Текущие задачи
|
||||
|
||||
[ ] **Разобрать нестабильную проверку аудиопауз desktop-трансляции** — 17.10.2026:
|
||||
При проверке зума test_stream_view::realH264RenderingAndClose получил silenceGaps=3 вместо gapsBefore=2.
|
||||
В логе есть desktop audio gap cause=pcm_short, got=96/480; видео продолжает выводиться (50 кадров к stop).
|
||||
Первый полный Qt-прогон прошёл; сбой повторился с общим check.sh и после его завершения.
|
||||
Причина не установлена; нужны диагностика PCM/часов и воспроизведение под нагрузкой.
|
||||
Логи: /tmp/stream-zoom-full-view-audio-gap.log, /tmp/stream-zoom-full-view.log.
|
||||
|
||||
[ ] **Устранить коллизию порта при перезапуске узла в test_dm_e2e** — 16.10.2026:
|
||||
Общий check.sh при проверке Windows-курсора: 155/156, test_dm_e2e упал в phase=9 на создании B2.
|
||||
Повторный bind 127.0.0.1:33843 вернул Address already in use; отдельный повтор теста проходит.
|
||||
|
||||
@@ -90,7 +90,7 @@ StreamPlayerWindow::StreamPlayerWindow(uint64_t group, uint64_t source, uint64_t
|
||||
setWindowTitle(text("Live stream: %1").arg(name)); resize(800, 500);
|
||||
auto* layout = new QVBoxLayout(this); layout->addWidget(new QLabel(name, this));
|
||||
m_status = new QLabel(text("Connecting…"), this); layout->addWidget(m_status);
|
||||
m_render = new VideoRenderWidget(this); layout->addWidget(m_render, 1);
|
||||
m_render = new VideoRenderWidget(this, true); layout->addWidget(m_render, 1);
|
||||
auto* controls = new QHBoxLayout;
|
||||
auto* volume = new QSlider(Qt::Horizontal, this); volume->setRange(0, 100); volume->setValue(100);
|
||||
volume->setAccessibleName(text("Stream volume")); controls->addWidget(new QLabel(text("Volume"), this)); controls->addWidget(volume);
|
||||
|
||||
@@ -11,35 +11,178 @@
|
||||
#include <QPainter>
|
||||
#include <QKeyEvent>
|
||||
#include <QMouseEvent>
|
||||
#include <QWheelEvent>
|
||||
#include <QCloseEvent>
|
||||
#include <QResizeEvent>
|
||||
#include <QApplication>
|
||||
#include <QScreen>
|
||||
#include <QStyle>
|
||||
#include <QProxyStyle>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
/* ─── VideoRenderWidget ─── */
|
||||
|
||||
VideoRenderWidget::VideoRenderWidget(QWidget* parent) : QWidget(parent) {
|
||||
VideoRenderWidget::VideoRenderWidget(QWidget* parent, bool zoomEnabled) : QWidget(parent), m_zoomEnabled(zoomEnabled) {
|
||||
setMouseTracking(true);
|
||||
setAttribute(Qt::WA_OpaquePaintEvent);
|
||||
}
|
||||
|
||||
void VideoRenderWidget::setFrame(const QImage& img) {
|
||||
bool changed = m_frame.size() != img.size();
|
||||
m_frame = img;
|
||||
if (m_zoomEnabled && changed) {
|
||||
finishDrag(); m_scale = 0; m_offset = {}; constrainView("frame");
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_VIDEO, "VideoView %p frame=%dx%d reset to fit scale=%.4f DPR=%.3f",
|
||||
static_cast<void*>(this), img.width(), img.height(), frameScale(), devicePixelRatioF());
|
||||
}
|
||||
update();
|
||||
}
|
||||
|
||||
void VideoRenderWidget::clearFrame() {
|
||||
finishDrag(); m_scale = 0; m_offset = {}; unsetCursor();
|
||||
m_frame = QImage();
|
||||
update();
|
||||
}
|
||||
|
||||
/* Масштаб в координатах Qt; предел 1:1 относится к физическим пикселям экрана. */
|
||||
qreal VideoRenderWidget::frameScale() const {
|
||||
if (m_frame.isNull() || width() <= 0 || height() <= 0) return 0;
|
||||
qreal dpr = devicePixelRatioF();
|
||||
qreal fit = std::min({qreal(1), width() * dpr / m_frame.width(), height() * dpr / m_frame.height()});
|
||||
return (m_scale > 0 ? std::clamp(m_scale, fit, qreal(1)) : fit) / dpr;
|
||||
}
|
||||
|
||||
/* Как в Android VideoZoomState: сдвиг ограничен половиной выступающей части каждой оси. */
|
||||
void VideoRenderWidget::constrainView(const char* reason) {
|
||||
qreal scale = frameScale();
|
||||
if (scale <= 0) { finishDrag(); unsetCursor(); return; }
|
||||
if (m_scale > 0) m_scale = scale * devicePixelRatioF();
|
||||
qreal x = std::max(qreal(0), (m_frame.width() - width() / scale) / 2);
|
||||
qreal y = std::max(qreal(0), (m_frame.height() - height() / scale) / 2);
|
||||
m_offset.setX(std::clamp(m_offset.x(), -x, x));
|
||||
m_offset.setY(std::clamp(m_offset.y(), -y, y));
|
||||
if (x > 0 || y > 0) setCursor(m_panning ? Qt::ClosedHandCursor : Qt::OpenHandCursor);
|
||||
else { finishDrag(); unsetCursor(); }
|
||||
DEBUG_TRACE(DEBUG_CATEGORY_VIDEO, "VideoView %p %s viewport=%dx%d frame=%dx%d DPR=%.3f scale=%.4f offset=(%.2f,%.2f) limit=(%.2f,%.2f)",
|
||||
static_cast<void*>(this), reason, width(), height(), m_frame.width(), m_frame.height(), devicePixelRatioF(), scale,
|
||||
m_offset.x(), m_offset.y(), x, y);
|
||||
}
|
||||
|
||||
void VideoRenderWidget::finishDrag() {
|
||||
if (!m_panning) return;
|
||||
m_panning = false; setCursor(Qt::OpenHandCursor);
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_VIDEO, "VideoView %p drag end offset=(%.2f,%.2f)",
|
||||
static_cast<void*>(this), m_offset.x(), m_offset.y());
|
||||
}
|
||||
|
||||
/* Захват мыши принадлежит QWidget; отмена жеста нужна и при потере окна/захвата. */
|
||||
bool VideoRenderWidget::event(QEvent* event) {
|
||||
bool handled = QWidget::event(event);
|
||||
if (!m_zoomEnabled) return handled;
|
||||
if (event->type() == QEvent::UngrabMouse || event->type() == QEvent::WindowDeactivate || event->type() == QEvent::Hide)
|
||||
finishDrag();
|
||||
if (event->type() == QEvent::ScreenChangeInternal
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0)
|
||||
|| event->type() == QEvent::DevicePixelRatioChange
|
||||
#endif
|
||||
) {
|
||||
constrainView("screen"); update();
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_VIDEO, "VideoView %p screen changed DPR=%.3f scale=%.4f",
|
||||
static_cast<void*>(this), devicePixelRatioF(), frameScale());
|
||||
}
|
||||
return handled;
|
||||
}
|
||||
|
||||
void VideoRenderWidget::resizeEvent(QResizeEvent* event) {
|
||||
QWidget::resizeEvent(event);
|
||||
if (!m_zoomEnabled) return;
|
||||
constrainView("resize");
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_VIDEO, "VideoView %p resize=%dx%d scale=%.4f offset=(%.2f,%.2f)",
|
||||
static_cast<void*>(this), width(), height(), frameScale(), m_offset.x(), m_offset.y());
|
||||
}
|
||||
|
||||
/* Сохраняем пиксель под курсором; clamp может сдвинуть якорь только возле границ кадра. */
|
||||
void VideoRenderWidget::wheelEvent(QWheelEvent* event) {
|
||||
if (!m_zoomEnabled) { QWidget::wheelEvent(event); return; }
|
||||
qreal previous = frameScale();
|
||||
if (previous <= 0) { event->accept(); return; }
|
||||
int delta = event->angleDelta().y();
|
||||
if (!delta) delta = event->pixelDelta().y();
|
||||
if (!delta) { event->ignore(); return; }
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
|
||||
QPointF position = event->position();
|
||||
#else
|
||||
QPointF position = event->posF();
|
||||
#endif
|
||||
qreal steps = std::clamp(delta / qreal(120), qreal(-100), qreal(100));
|
||||
m_scale = previous * devicePixelRatioF() * std::pow(1.15, steps);
|
||||
qreal next = frameScale();
|
||||
if (m_frame.width() * next <= width() + 1e-6 && m_frame.height() * next <= height() + 1e-6) m_scale = 0;
|
||||
QPointF relative = position - QPointF(width() / 2.0, height() / 2.0);
|
||||
m_offset += relative * (1 / next - 1 / previous);
|
||||
constrainView("zoom"); update(); event->accept();
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_VIDEO, "VideoView %p zoom delta=%d scale=%.4f->%.4f physical=%.4f anchor=(%.1f,%.1f) offset=(%.2f,%.2f)",
|
||||
static_cast<void*>(this), delta, previous, next, next * devicePixelRatioF(), position.x(), position.y(),
|
||||
m_offset.x(), m_offset.y());
|
||||
}
|
||||
|
||||
void VideoRenderWidget::mousePressEvent(QMouseEvent* event) {
|
||||
qreal scale = frameScale();
|
||||
bool canPan = scale > 0 && (m_frame.width() * scale > width() + 1e-6 || m_frame.height() * scale > height() + 1e-6);
|
||||
if (!m_zoomEnabled || event->button() != Qt::LeftButton || !canPan) {
|
||||
QWidget::mousePressEvent(event); return;
|
||||
}
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
m_lastMouse = event->position();
|
||||
#else
|
||||
m_lastMouse = event->localPos();
|
||||
#endif
|
||||
m_panning = true; setCursor(Qt::ClosedHandCursor); event->accept();
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_VIDEO, "VideoView %p drag begin scale=%.4f offset=(%.2f,%.2f)",
|
||||
static_cast<void*>(this), frameScale(), m_offset.x(), m_offset.y());
|
||||
}
|
||||
|
||||
void VideoRenderWidget::mouseMoveEvent(QMouseEvent* event) {
|
||||
if (!m_panning) { QWidget::mouseMoveEvent(event); return; }
|
||||
if (!(event->buttons() & Qt::LeftButton)) { finishDrag(); event->accept(); return; }
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
QPointF position = event->position();
|
||||
#else
|
||||
QPointF position = event->localPos();
|
||||
#endif
|
||||
m_offset += (position - m_lastMouse) / frameScale(); m_lastMouse = position;
|
||||
constrainView("drag"); update(); event->accept();
|
||||
}
|
||||
|
||||
void VideoRenderWidget::mouseReleaseEvent(QMouseEvent* event) {
|
||||
if (!m_zoomEnabled || event->button() != Qt::LeftButton || !m_panning) { QWidget::mouseReleaseEvent(event); return; }
|
||||
finishDrag(); event->accept();
|
||||
}
|
||||
|
||||
void VideoRenderWidget::paintEvent(QPaintEvent*) {
|
||||
QPainter p(this);
|
||||
p.fillRect(rect(), Qt::black);
|
||||
if (m_frame.isNull()) return;
|
||||
|
||||
if (m_zoomEnabled) {
|
||||
qreal scale = frameScale();
|
||||
QSizeF scaled = QSizeF(m_frame.size()) * scale;
|
||||
QPointF origin = QPointF(width() / 2.0, height() / 2.0) + m_offset * scale - QPointF(scaled.width(), scaled.height()) / 2;
|
||||
// При 1:1 начало кадра совпадает с физической сеткой пикселей, без размытия текста.
|
||||
qreal dpr = devicePixelRatioF();
|
||||
if (qFuzzyCompare(scale * dpr, qreal(1))) {
|
||||
auto align = [dpr](qreal offset, int pixels, int viewport) {
|
||||
qreal rounded = std::round(offset * dpr);
|
||||
if (pixels > viewport * dpr) rounded = std::clamp(rounded, std::ceil(viewport * dpr - pixels), qreal(0));
|
||||
return rounded / dpr;
|
||||
};
|
||||
origin = QPointF(align(origin.x(), m_frame.width(), width()), align(origin.y(), m_frame.height(), height()));
|
||||
}
|
||||
p.setRenderHint(QPainter::SmoothPixmapTransform, scale * dpr < 1);
|
||||
p.drawImage(QRectF(origin, scaled), m_frame, QRectF(m_frame.rect()));
|
||||
return;
|
||||
}
|
||||
QSize sz = m_frame.size();
|
||||
sz.scale(size(), Qt::KeepAspectRatio);
|
||||
QRect dst((width() - sz.width()) / 2, (height() - sz.height()) / 2, sz.width(), sz.height());
|
||||
|
||||
@@ -11,19 +11,34 @@ class QPushButton;
|
||||
class QLabel;
|
||||
class VideoPlayerEngine;
|
||||
|
||||
/* Виджет отрисовки кадра (aspect-fit на чёрном фоне). */
|
||||
/* Отрисовка кадра на чёрном фоне. zoomEnabled включает колесо до физического 1:1 и drag с ограничением краёв. */
|
||||
class VideoRenderWidget : public QWidget {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit VideoRenderWidget(QWidget* parent = nullptr);
|
||||
explicit VideoRenderWidget(QWidget* parent = nullptr, bool zoomEnabled = false);
|
||||
void setFrame(const QImage& img);
|
||||
void clearFrame();
|
||||
|
||||
protected:
|
||||
bool event(QEvent* event) override;
|
||||
void paintEvent(QPaintEvent* event) override;
|
||||
void resizeEvent(QResizeEvent* event) override;
|
||||
void wheelEvent(QWheelEvent* event) override;
|
||||
void mousePressEvent(QMouseEvent* event) override;
|
||||
void mouseMoveEvent(QMouseEvent* event) override;
|
||||
void mouseReleaseEvent(QMouseEvent* event) override;
|
||||
|
||||
private:
|
||||
qreal frameScale() const;
|
||||
void constrainView(const char* reason);
|
||||
void finishDrag();
|
||||
|
||||
QImage m_frame;
|
||||
const bool m_zoomEnabled;
|
||||
qreal m_scale = 0; // Физические пиксели на пиксель кадра; 0 — автоматическое вписывание.
|
||||
QPointF m_offset; // Смещение от центра в пикселях исходного кадра.
|
||||
QPointF m_lastMouse;
|
||||
bool m_panning = false;
|
||||
};
|
||||
|
||||
/* Отдельное окно плеера без рамки: drag-перенос, dblclick/Alt+Enter — fullscreen,
|
||||
|
||||
@@ -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