diff --git a/tools/chatgui/tests/test_stream_publish.cpp b/tools/chatgui/tests/test_stream_publish.cpp index 1a424a01..811ed619 100644 --- a/tools/chatgui/tests/test_stream_publish.cpp +++ b/tools/chatgui/tests/test_stream_publish.cpp @@ -11,6 +11,8 @@ #include #include #include +#include +#include #include #include #include @@ -40,6 +42,13 @@ extern "C" { #include #include #endif +#ifdef UTUN_STREAM_X11_CURSOR +#include "../src/stream_screen_cursor.h" +#include +#include +#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("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()->checkedId(), 270); QVERIFY(!reopened.findChildren().first()->isChecked()); + QVERIFY(!reopened.findChild("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(); 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().toImage(); + return !preview.isNull() && preview.pixelColor(position) == QColor(Qt::magenta); + }; + QTRY_VERIFY_WITH_TIMEOUT(cursorShown(), 5000); + StreamPublishWindow controls(&engine, group); + auto* toggle = controls.findChild("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() {