Browse Source

Preserve packed RGB formats and fix PipeWire test setup

master
evgeny 3 days ago
parent
commit
ea971b0856
  1. 22
      tools/chatgui/src/stream_portal_capture.cpp
  2. 21
      tools/chatgui/tests/test_stream_portal.cpp

22
tools/chatgui/src/stream_portal_capture.cpp

@ -38,6 +38,7 @@ struct StreamPortalCapture::Impl {
spa_hook coreHook{}, streamHook{};
std::unique_ptr<QSocketNotifier> 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<StreamPortalCapture*>(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*>(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<const uint8_t*>(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<const uint8_t*>(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_meta_header*>(spa_buffer_find_meta_data(raw, SPA_META_Header, sizeof(spa_meta_header)));
if (header && header->pts >= 0) frame.setStartTime(header->pts / 1000);

21
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<PortalStreams>();
qDBusRegisterMetaType<PortalStream>(); qDBusRegisterMetaType<PortalStreams>();
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<QVideoFrame>(); QVERIFY(frame.isValid()); QVERIFY(!frame.toImage().isNull());

Loading…
Cancel
Save