diff --git a/tools/chatgui/src/stream_publish_engine.cpp b/tools/chatgui/src/stream_publish_engine.cpp index f5bde8bd..f86dc29e 100644 --- a/tools/chatgui/src/stream_publish_engine.cpp +++ b/tools/chatgui/src/stream_publish_engine.cpp @@ -7,6 +7,8 @@ #include #include #include +#include +#include #include #include #include @@ -33,6 +35,19 @@ namespace { int64_t monotonicUs() { return std::chrono::duration_cast( std::chrono::steady_clock::now().time_since_epoch()).count(); } QString text(const char* value) { return QCoreApplication::translate("ChatGui", value); } +#ifdef HAVE_STREAM_CAPTURE +/* Имя QScreen не уникально (например, несколько X11 screens); номер различает одинаковые дескрипторы. */ +QByteArray screenId(QScreen* screen) { + int duplicate = 0; + for (auto* other : QGuiApplication::screens()) { + if (other == screen) break; + if (other->name() == screen->name() && other->manufacturer() == screen->manufacturer() + && other->model() == screen->model() && other->serialNumber() == screen->serialNumber()) duplicate++; + } + return QJsonDocument(QJsonArray{screen->name(), screen->manufacturer(), screen->model(), screen->serialNumber(), duplicate}) + .toJson(QJsonDocument::Compact); +} +#endif } struct StreamPublishEngine::Impl { struct Audio { int64_t pts = 0, created = 0; uint64_t sequence = 0; unsigned frames = 0; int16_t pcm[1920]{}; }; @@ -148,9 +163,10 @@ QList StreamPublishEngine::sources() { || qEnvironmentVariableIsSet("WAYLAND_DISPLAY")) { sources.push_back({StreamVideoSource::Screen, text("Screen (system selection)"), "portal", {}}); } else { + int number = 0; for (auto* screen : QGuiApplication::screens()) { QSize size = screen->size() * screen->devicePixelRatio(); - sources.push_back({StreamVideoSource::Screen, screen->name(), screen->name().toUtf8(), {}, size}); + sources.push_back({StreamVideoSource::Screen, QStringLiteral("%1: %2").arg(++number).arg(screen->name()), screenId(screen), {}, size}); DEBUG_DEBUG(DEBUG_CATEGORY_VIDEO, "desktop screen capability source=%s logical=%dx%d scale=%.3f pixels=%dx%d", qPrintable(screen->name()), screen->size().width(), screen->size().height(), screen->devicePixelRatio(), size.width(), size.height()); } @@ -225,7 +241,7 @@ bool StreamPublishEngine::openSource(const StreamPublishConfig& config) { m->screen = std::make_unique(); if (config.source.id != "portal") { QScreen* selected = nullptr; - for (auto* screen : QGuiApplication::screens()) if (screen->name().toUtf8() == config.source.id) selected = screen; + for (auto* screen : QGuiApplication::screens()) if (screenId(screen) == config.source.id) { selected = screen; break; } if (!selected) { DEBUG_ERROR(DEBUG_CATEGORY_VIDEO, "desktop screen disappeared"); return false; } m->screen->setScreen(selected); } diff --git a/tools/chatgui/tests/test_stream_publish.cpp b/tools/chatgui/tests/test_stream_publish.cpp index 9d4826ca..1a424a01 100644 --- a/tools/chatgui/tests/test_stream_publish.cpp +++ b/tools/chatgui/tests/test_stream_publish.cpp @@ -223,9 +223,11 @@ private slots: auto* modeBox = controls.findChild("streamVideoMode"); auto sources = StreamPublishEngine::sources(); QSignalSpy changes(&controls, &StreamPublishWindow::settingsChanged); + QList screenIds; unsigned screens = 0; for (int i = 0; i < sources.size(); i++) { if (sources[i].kind != StreamVideoSource::Screen || sources[i].size.isEmpty()) continue; + QVERIFY(!screenIds.contains(sources[i].id)); screenIds.append(sources[i].id); screens++; sourceBox->setCurrentIndex(-1); changes.clear(); sourceBox->setCurrentIndex(i); QSize size = stream_video_screen_size(sources[i].size); auto mode = modeBox->currentData().toList(); @@ -237,6 +239,41 @@ private slots: } if (!screens) QSKIP("No enumerated screens"); } + void nativeScreenCapture() { +#ifdef HAVE_STREAM_CAPTURE + if (!qEnvironmentVariableIsSet("UTUN_TEST_SCREEN_CAPTURE")) QSKIP("Run with X11 screen capture enabled"); + StreamPublishEngine engine; QTRY_VERIFY_WITH_TIMEOUT(!engine.codecs().isEmpty(), 15000); + QSignalSpy errors(&engine, &StreamPublishEngine::errorOccurred); + StreamPublishConfig config; config.audio = false; config.nativeSize = true; config.automaticBitrate = true; + config.video.width = 640; config.video.height = 360; config.video.fps = 15; + uint64_t epoch = 0; unsigned captured = 0; + for (const auto& source : engine.sources()) { + if (source.kind != StreamVideoSource::Screen || source.size.isEmpty()) continue; + QSize expected = stream_video_screen_size(source.size); + config.source = source; config.source.size = {}; // Как portal: размер станет известен только из первого кадра. + if (!engine.active()) { + QVERIFY(engine.start(group, config)); + stream_member_state member{}; + QTRY_VERIFY_WITH_TIMEOUT(stream_member_load(inst, group, inst->node_id, &member, nullptr) == STREAM_OK && member.published, 5000); + epoch = member.source.source_epoch; QVERIFY(gui_stream_open(204, group, inst->node_id, epoch)); + StreamRecord opened; QVERIFY(record(204, StreamRecordKind::Opened, opened)); + } else QVERIFY(engine.reconfigure(config)); + bool matched = false; StreamRecord frame; QElapsedTimer elapsed; elapsed.start(); + while (!matched && elapsed.elapsed() < 8000) { + if (gui_stream_pull(204, frame, 0, true, 60000000) && frame.kind == StreamRecordKind::Video) + matched = frame.format.width == expected.width() && frame.format.height == expected.height(); + QTest::qWait(1); + } + QVERIFY(matched); QCOMPARE(engine.config().source.id, source.id); QCOMPARE(engine.config().source.size, source.size); + QCOMPARE(engine.config().video.width, expected.width()); QCOMPARE(engine.config().video.height, expected.height()); + QVERIFY(errors.isEmpty()); captured++; + } + if (!captured) QSKIP("No enumerated screens"); + engine.stop(); QTRY_VERIFY_WITH_TIMEOUT(!engine.active(), 4000); gui_stream_stop(204); +#else + QSKIP("Screen capture unavailable"); +#endif + } void broadcastAllowsRadioMicrophone() { AudioCaptureLease call, radio(AudioCaptureLease::Mode::Radio), recorder, publisher(AudioCaptureLease::Mode::Exclusive); QVERIFY(call.acquire()); QVERIFY(radio.acquire()); QVERIFY(recorder.acquire()); QVERIFY(!publisher.acquire()); @@ -368,7 +405,8 @@ private slots: pattern.setStyleSheet("background: #c04020; color: white; font-size: 20px;"); pattern.show(); QTest::qWait(100); StreamPublishEngine engine; QTRY_VERIFY_WITH_TIMEOUT(!engine.codecs().isEmpty(), 15000); StreamPublishConfig config; config.video.width = 640; config.video.height = 360; config.video.fps = 15; - config.source = {StreamVideoSource::Screen, "Virtual screen", QGuiApplication::primaryScreen()->name().toUtf8(), {}}; + for (const auto& source : engine.sources()) if (source.kind == StreamVideoSource::Screen) { config.source = source; break; } + QVERIFY(!config.source.id.isEmpty()); QSignalSpy errors(&engine, &StreamPublishEngine::errorOccurred); QVERIFY(engine.start(group, config)); QTRY_VERIFY_WITH_TIMEOUT(engine.config().video.codec == STREAM_MEDIA_H264, 10000);