Browse Source

Test X11 cursor placement and live visibility controls

master
evgeny 2 days ago
parent
commit
cf3c8c7c34
  1. 69
      tools/chatgui/tests/test_stream_publish.cpp

69
tools/chatgui/tests/test_stream_publish.cpp

@ -11,6 +11,8 @@
#include <QJsonObject>
#include <QScreen>
#include <QLoggingCategory>
#include <QScopeGuard>
#include <QPainter>
#include <atomic>
#include <thread>
#include <cmath>
@ -40,6 +42,13 @@ extern "C" {
#include <QVideoSink>
#include <QVideoFrame>
#endif
#ifdef UTUN_STREAM_X11_CURSOR
#include "../src/stream_screen_cursor.h"
#include <QtGui/qguiapplication_platform.h>
#include <X11/Xlib.h>
#undef None
#undef Status
#endif
#ifdef TEST_NULL_AUDIO
static ma_device_data_proc captureCallback;
@ -137,6 +146,8 @@ private slots:
for (int i = 0; i < sources.size(); i++) if (sources[i].kind == StreamVideoSource::Screen && !sources[i].size.isEmpty()) { screen = i; break; }
if (screen < 0) QSKIP("No enumerated screen for capture settings");
boxes[0]->setCurrentIndex(screen);
auto* cursor = controls.findChild<QCheckBox*>("streamShowCursor"); QVERIFY(cursor); QVERIFY(cursor->isChecked());
cursor->setChecked(false);
QSize maximum = stream_video_screen_size(sources[screen].size);
QCOMPARE(boxes[1]->currentData().toList(), (QVariantList{maximum.width(), maximum.height(), 30}));
QVERIFY(boxes[1]->count() >= 6);
@ -153,6 +164,7 @@ private slots:
QVERIFY(!stored.value("native_size").toBool());
QCOMPARE(stored.value("bitrate").toInt(), 4000000); QCOMPARE(stored.value("rotation").toInt(), 270);
QVERIFY(!stored.value("audio").toBool());
QVERIFY(!stored.value("show_cursor").toBool(true));
StreamPublishEngine reopenedEngine;
StreamPublishWindow reopened(&reopenedEngine, group, nullptr, saved);
QSignalSpy changes(&reopened, &StreamPublishWindow::settingsChanged);
@ -163,6 +175,7 @@ private slots:
if (StreamPublishEngine::supported()) QTRY_COMPARE(boxes[2]->currentData().toInt(), stored.value("codec").toInt());
QCOMPARE(reopened.findChild<QButtonGroup*>()->checkedId(), 270);
QVERIFY(!reopened.findChildren<QCheckBox*>().first()->isChecked());
QVERIFY(!reopened.findChild<QCheckBox*>("streamShowCursor")->isChecked());
QVERIFY(changes.isEmpty()); // Асинхронный список кодеков не перезаписывает сохранённый выбор.
boxes[3]->setCurrentIndex(0);
QCOMPARE(changes.size(), 1);
@ -272,6 +285,62 @@ private slots:
engine.stop(); QTRY_VERIFY_WITH_TIMEOUT(!engine.active(), 4000); gui_stream_stop(204);
#else
QSKIP("Screen capture unavailable");
#endif
}
void screenCursorOverlay() {
#ifdef UTUN_STREAM_X11_CURSOR
if (!qEnvironmentVariableIsSet("UTUN_TEST_SCREEN_CAPTURE")) QSKIP("Run under isolated Xvfb");
auto* native = qGuiApp->nativeInterface<QNativeInterface::QX11Application>(); QVERIFY(native);
Display* display = native->display();
for (auto* screen : QGuiApplication::screens()) {
QWindow target(screen); target.setFlags(Qt::FramelessWindowHint | Qt::BypassWindowManagerHint);
target.setGeometry(QRect(screen->geometry().topLeft() + QPoint(20, 20), QSize(200, 160))); target.show();
QVERIFY(QTest::qWaitForWindowExposed(&target));
char bits[32]; memset(bits, 0xff, sizeof(bits));
Pixmap bitmap = XCreateBitmapFromData(display, target.winId(), bits, 16, 16); QVERIFY(bitmap);
auto releaseBitmap = qScopeGuard([&] { XFreePixmap(display, bitmap); });
XColor foreground{}, background{}; foreground.red = foreground.blue = 65535;
Cursor cursor = XCreatePixmapCursor(display, bitmap, bitmap, &foreground, &background, 3, 4); QVERIFY(cursor);
auto releaseCursor = qScopeGuard([&] { XFreeCursor(display, cursor); });
XDefineCursor(display, target.winId(), cursor);
XWarpPointer(display, 0, target.winId(), 0, 0, 0, 0, 40, 50); XSync(display, false);
QPoint position(qRound(20 * screen->devicePixelRatio()) + 40, qRound(20 * screen->devicePixelRatio()) + 50);
StreamScreenCursor overlay(screen);
QImage frame(screen->size() * screen->devicePixelRatio(), QImage::Format_RGB32);
frame.setDevicePixelRatio(screen->devicePixelRatio()); frame.fill(Qt::black);
QVERIFY(overlay.draw(frame));
QImage expected(frame.size(), QImage::Format_RGB32); expected.fill(Qt::black);
{ QPainter painter(&expected); painter.fillRect(QRect(position - QPoint(3, 4), QSize(16, 16)), Qt::magenta); }
QCOMPARE(frame, expected); // Включая hotspot, 64-битный Xlib и физические пиксели HiDPI.
for (auto* other : QGuiApplication::screens()) {
if (other == screen) continue;
StreamScreenCursor otherOverlay(other);
QImage otherFrame(other->size() * other->devicePixelRatio(), QImage::Format_RGB32); otherFrame.fill(Qt::black);
QImage blank = otherFrame;
QVERIFY(otherOverlay.draw(otherFrame)); QCOMPARE(otherFrame, blank);
}
StreamPublishEngine engine; QTRY_VERIFY_WITH_TIMEOUT(!engine.codecs().isEmpty(), 15000);
StreamPublishConfig config; config.audio = false;
for (const auto& source : engine.sources()) if (source.kind == StreamVideoSource::Screen && source.screen == screen) config.source = source;
QVERIFY(config.source.screen); QVERIFY(config.showCursor);
config.video.width = 320; config.video.height = 180;
QSignalSpy errors(&engine, &StreamPublishEngine::errorOccurred);
QVERIFY(engine.start(group, config));
auto cursorShown = [&] {
QImage preview = engine.previewFrame().value<QVideoFrame>().toImage();
return !preview.isNull() && preview.pixelColor(position) == QColor(Qt::magenta);
};
QTRY_VERIFY_WITH_TIMEOUT(cursorShown(), 5000);
StreamPublishWindow controls(&engine, group);
auto* toggle = controls.findChild<QCheckBox*>("streamShowCursor"); QVERIFY(toggle); QVERIFY(toggle->isChecked());
toggle->setChecked(false); QVERIFY(!engine.config().showCursor);
QTRY_VERIFY_WITH_TIMEOUT(!cursorShown(), 5000);
toggle->setChecked(true); QVERIFY(engine.config().showCursor);
QTRY_VERIFY_WITH_TIMEOUT(cursorShown(), 5000);
QVERIFY(errors.isEmpty()); engine.stop(); QTRY_VERIFY_WITH_TIMEOUT(!engine.active(), 4000);
}
#else
QSKIP("X11 cursor unavailable");
#endif
}
void broadcastAllowsRadioMicrophone() {

Loading…
Cancel
Save