|
|
|
|
@ -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() { |
|
|
|
|
|