From ea971b08566749c0d89b7ceed81af88b29b1e652 Mon Sep 17 00:00:00 2001 From: evgeny Date: Fri, 16 Oct 2026 14:50:50 +0200 Subject: [PATCH] Preserve packed RGB formats and fix PipeWire test setup --- tools/chatgui/src/stream_portal_capture.cpp | 22 +++++++++++++++------ tools/chatgui/tests/test_stream_portal.cpp | 21 +++++++++++++++----- 2 files changed, 32 insertions(+), 11 deletions(-) diff --git a/tools/chatgui/src/stream_portal_capture.cpp b/tools/chatgui/src/stream_portal_capture.cpp index ac13cfbc..cfc080a7 100644 --- a/tools/chatgui/src/stream_portal_capture.cpp +++ b/tools/chatgui/src/stream_portal_capture.cpp @@ -38,6 +38,7 @@ struct StreamPortalCapture::Impl { spa_hook coreHook{}, streamHook{}; std::unique_ptr notifier; QVideoFrameFormat format; + int bytesPerPixel = 4; uint64_t frames = 0; }; @@ -173,7 +174,10 @@ void StreamPortalCapture::openPipeWire(int fd, uint node, const QString& serial) auto* self = static_cast(data); spa_video_info_raw info{}; if (spa_format_video_raw_parse(param, &info) < 0) { self->fail("Invalid PipeWire video format"); return; } QVideoFrameFormat::PixelFormat pixel = QVideoFrameFormat::Format_Invalid; + self->m->bytesPerPixel = 4; switch (info.format) { + case SPA_VIDEO_FORMAT_BGR: pixel = QVideoFrameFormat::Format_BGRX8888; self->m->bytesPerPixel = 3; break; + case SPA_VIDEO_FORMAT_RGB: pixel = QVideoFrameFormat::Format_RGBX8888; self->m->bytesPerPixel = 3; break; case SPA_VIDEO_FORMAT_BGRx: pixel = QVideoFrameFormat::Format_BGRX8888; break; case SPA_VIDEO_FORMAT_BGRA: pixel = QVideoFrameFormat::Format_BGRA8888; break; case SPA_VIDEO_FORMAT_RGBx: pixel = QVideoFrameFormat::Format_RGBX8888; break; @@ -203,7 +207,8 @@ void StreamPortalCapture::openPipeWire(int fd, uint node, const QString& serial) spa_rectangle size{1920, 1080}, minimum{1, 1}, maximum{16384, 16384}; const spa_pod* format = static_cast(spa_pod_builder_add_object(&builder, SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat, SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_video), SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw), - SPA_FORMAT_VIDEO_format, SPA_POD_CHOICE_ENUM_Id(4, SPA_VIDEO_FORMAT_BGRx, SPA_VIDEO_FORMAT_BGRA, SPA_VIDEO_FORMAT_RGBx, SPA_VIDEO_FORMAT_RGBA), + SPA_FORMAT_VIDEO_format, SPA_POD_CHOICE_ENUM_Id(7, SPA_VIDEO_FORMAT_BGRx, SPA_VIDEO_FORMAT_BGRx, + SPA_VIDEO_FORMAT_BGRA, SPA_VIDEO_FORMAT_RGBx, SPA_VIDEO_FORMAT_RGBA, SPA_VIDEO_FORMAT_RGB, SPA_VIDEO_FORMAT_BGR), SPA_FORMAT_VIDEO_size, SPA_POD_CHOICE_RANGE_Rectangle(&size, &minimum, &maximum))); int result = pw_stream_connect(m->stream, PW_DIRECTION_INPUT, serial.isEmpty() ? node : PW_ID_ANY, pw_stream_flags(PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS), &format, 1); @@ -234,17 +239,22 @@ void StreamPortalCapture::processFrame() { if (!chunk.size) return; // Статичный экран может прислать пустой буфер. if (chunk.flags & SPA_CHUNK_FLAG_CORRUPTED) { DEBUG_WARN(DEBUG_CATEGORY_VIDEO, "Wayland capture discarded corrupted frame"); return; } int width = m->format.frameWidth(), height = m->format.frameHeight(); - uint64_t stride = chunk.stride > 0 ? chunk.stride : width * 4; - uint64_t required = height > 0 ? (height - 1) * stride + width * 4 : 0; - if (width <= 0 || height <= 0 || chunk.stride < 0 || stride < uint64_t(width * 4) || chunk.offset > data.maxsize + int rowBytes = width * m->bytesPerPixel; + uint64_t stride = chunk.stride > 0 ? chunk.stride : rowBytes; + uint64_t required = height > 0 ? (height - 1) * stride + rowBytes : 0; + if (width <= 0 || height <= 0 || chunk.stride < 0 || stride < uint64_t(rowBytes) || chunk.offset > data.maxsize || required > data.maxsize - chunk.offset || required > chunk.size) { fail(QString("Invalid PipeWire buffer size=%1x%2 stride=%3 offset=%4 bytes=%5 capacity=%6") .arg(width).arg(height).arg(chunk.stride).arg(chunk.offset).arg(chunk.size).arg(data.maxsize)); return; } QVideoFrame frame(m->format); if (!frame.map(QVideoFrame::WriteOnly)) { fail("Cannot allocate captured video frame"); return; } - for (int y = 0; y < height; y++) std::memcpy(frame.bits(0) + y * frame.bytesPerLine(0), - static_cast(data.data) + chunk.offset + y * stride, width * 4); + for (int y = 0; y < height; y++) { + auto* target = frame.bits(0) + y * frame.bytesPerLine(0); + const auto* source = static_cast(data.data) + chunk.offset + y * stride; + if (m->bytesPerPixel == 4) std::memcpy(target, source, rowBytes); + else for (int x = 0; x < width; x++) { std::memcpy(target + x * 4, source + x * 3, 3); target[x * 4 + 3] = 255; } + } frame.unmap(); auto* header = static_cast(spa_buffer_find_meta_data(raw, SPA_META_Header, sizeof(spa_meta_header))); if (header && header->pts >= 0) frame.setStartTime(header->pts / 1000); diff --git a/tools/chatgui/tests/test_stream_portal.cpp b/tools/chatgui/tests/test_stream_portal.cpp index ceb7b425..06f23bec 100644 --- a/tools/chatgui/tests/test_stream_portal.cpp +++ b/tools/chatgui/tests/test_stream_portal.cpp @@ -105,7 +105,7 @@ class StreamPortalTest : public QObject { private slots: void initTestCase() { debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN); debug_set_category_level(DEBUG_CATEGORY_VIDEO, DEBUG_LEVEL_DEBUG); - qDBusRegisterMetaType(); + qDBusRegisterMetaType(); qDBusRegisterMetaType(); QVERIFY(QDBusConnection::sessionBus().registerService(service)); } void init() { @@ -152,10 +152,11 @@ context.properties = { core.daemon = true core.name = pipewire-0 } context.spa-libs = { support.* = support/libspa-support videotestsrc = videotestsrc/libspa-videotestsrc } context.modules = [ { name = libpipewire-module-protocol-native } + { name = libpipewire-module-metadata } { name = libpipewire-module-spa-node-factory } { name = libpipewire-module-client-node } { name = libpipewire-module-link-factory } - { name = libpipewire-module-access args = { access.force = unrestricted } } + { name = libpipewire-module-access args = { access.legacy = true } } ] context.objects = [ { factory = spa-node-factory args = { factory.name = videotestsrc node.name = cursor-test node.description = cursor-test @@ -164,13 +165,19 @@ context.objects = [ )"); config.close(); QProcessEnvironment environment = QProcessEnvironment::systemEnvironment(); environment.insert("PIPEWIRE_RUNTIME_DIR", runtime.path()); - QProcess server; server.setProcessEnvironment(environment); server.setProcessChannelMode(QProcess::MergedChannels); + QProcess server; server.setProcessChannelMode(QProcess::MergedChannels); + auto serverEnvironment = environment; serverEnvironment.insert("PIPEWIRE_DEBUG", "3"); server.setProcessEnvironment(serverEnvironment); server.start("pipewire", {"-c", config.fileName()}); QVERIFY(server.waitForStarted()); auto stop = qScopeGuard([&] { server.terminate(); if (!server.waitForFinished(3000)) { server.kill(); server.waitForFinished(); } }); portal->socket = runtime.filePath("pipewire-0"); QTRY_VERIFY(QFile::exists(portal->socket)); QProcess dump; dump.setProcessEnvironment(environment); dump.start("pw-dump"); - QVERIFY(dump.waitForFinished(5000)); QCOMPARE(dump.exitCode(), 0); + bool dumped = QTest::qWaitFor([&] { return dump.state() == QProcess::NotRunning; }, 5000); + if (!dumped || dump.exitCode()) { + qWarning().noquote() << "PipeWire server:" << server.readAll() << "dump:" << dump.readAllStandardError(); + if (!dumped) { dump.kill(); dump.waitForFinished(); } + } + QVERIFY(dumped); QCOMPARE(dump.exitCode(), 0); uint node = 0; QVariantMap props; for (const auto& value : QJsonDocument::fromJson(dump.readAllStandardOutput()).array()) { auto object = value.toObject(), properties = object.value("info").toObject().value("props").toObject(); @@ -184,11 +191,15 @@ context.objects = [ // Без менеджера сессий соединяем только тестовые video ports, сохраняя настоящий транспорт PipeWire. QElapsedTimer elapsed; elapsed.start(); while (frames.isEmpty() && errors.isEmpty() && elapsed.elapsed() < 6000) { - link.start("pw-link", {"cursor-test:out_0", "vibechat-screen:in_0"}); link.waitForFinished(1000); + link.start("pw-link", {"cursor-test:capture_1", "vibechat-screen:input_1"}); + if (!QTest::qWaitFor([&] { return link.state() == QProcess::NotRunning; }, 1000)) { link.kill(); link.waitForFinished(); } QTest::qWait(50); } if (!errors.isEmpty() || frames.isEmpty()) { qWarning().noquote() << "PipeWire server:" << server.readAll() << "link:" << link.readAllStandardError(); + link.start("pw-link", {"-io"}); + if (!QTest::qWaitFor([&] { return link.state() == QProcess::NotRunning; }, 1000)) { link.kill(); link.waitForFinished(); } + qWarning().noquote() << "PipeWire ports:" << link.readAllStandardOutput(); } QVERIFY(errors.isEmpty()); QVERIFY(!frames.isEmpty()); QCOMPARE(portal->cursor, 2u); auto frame = frames.last().front().value(); QVERIFY(frame.isValid()); QVERIFY(!frame.toImage().isNull());