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{}; spa_hook coreHook{}, streamHook{};
std::unique_ptr<QSocketNotifier> notifier; std::unique_ptr<QSocketNotifier> notifier;
QVideoFrameFormat format; QVideoFrameFormat format;
int bytesPerPixel = 4;
uint64_t frames = 0; 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{}; 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; } if (spa_format_video_raw_parse(param, &info) < 0) { self->fail("Invalid PipeWire video format"); return; }
QVideoFrameFormat::PixelFormat pixel = QVideoFrameFormat::Format_Invalid; QVideoFrameFormat::PixelFormat pixel = QVideoFrameFormat::Format_Invalid;
self->m->bytesPerPixel = 4;
switch (info.format) { 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_BGRx: pixel = QVideoFrameFormat::Format_BGRX8888; break;
case SPA_VIDEO_FORMAT_BGRA: pixel = QVideoFrameFormat::Format_BGRA8888; break; case SPA_VIDEO_FORMAT_BGRA: pixel = QVideoFrameFormat::Format_BGRA8888; break;
case SPA_VIDEO_FORMAT_RGBx: pixel = QVideoFrameFormat::Format_RGBX8888; 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}; 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, 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_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))); 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, 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); 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.size) return; // Статичный экран может прислать пустой буфер.
if (chunk.flags & SPA_CHUNK_FLAG_CORRUPTED) { DEBUG_WARN(DEBUG_CATEGORY_VIDEO, "Wayland capture discarded corrupted frame"); 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(); int width = m->format.frameWidth(), height = m->format.frameHeight();
uint64_t stride = chunk.stride > 0 ? chunk.stride : width * 4; int rowBytes = width * m->bytesPerPixel;
uint64_t required = height > 0 ? (height - 1) * stride + width * 4 : 0; uint64_t stride = chunk.stride > 0 ? chunk.stride : rowBytes;
if (width <= 0 || height <= 0 || chunk.stride < 0 || stride < uint64_t(width * 4) || chunk.offset > data.maxsize 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) { || required > data.maxsize - chunk.offset || required > chunk.size) {
fail(QString("Invalid PipeWire buffer size=%1x%2 stride=%3 offset=%4 bytes=%5 capacity=%6") 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; .arg(width).arg(height).arg(chunk.stride).arg(chunk.offset).arg(chunk.size).arg(data.maxsize)); return;
} }
QVideoFrame frame(m->format); QVideoFrame frame(m->format);
if (!frame.map(QVideoFrame::WriteOnly)) { fail("Cannot allocate captured video frame"); return; } 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), for (int y = 0; y < height; y++) {
static_cast<const uint8_t*>(data.data) + chunk.offset + y * stride, width * 4); 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(); frame.unmap();
auto* header = static_cast<spa_meta_header*>(spa_buffer_find_meta_data(raw, SPA_META_Header, sizeof(spa_meta_header))); 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); 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: private slots:
void initTestCase() { void initTestCase() {
debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN); debug_set_category_level(DEBUG_CATEGORY_VIDEO, DEBUG_LEVEL_DEBUG); 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)); QVERIFY(QDBusConnection::sessionBus().registerService(service));
} }
void init() { 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.spa-libs = { support.* = support/libspa-support videotestsrc = videotestsrc/libspa-videotestsrc }
context.modules = [ context.modules = [
{ name = libpipewire-module-protocol-native } { name = libpipewire-module-protocol-native }
{ name = libpipewire-module-metadata }
{ name = libpipewire-module-spa-node-factory } { name = libpipewire-module-spa-node-factory }
{ name = libpipewire-module-client-node } { name = libpipewire-module-client-node }
{ name = libpipewire-module-link-factory } { name = libpipewire-module-link-factory }
{ name = libpipewire-module-access args = { access.force = unrestricted } } { name = libpipewire-module-access args = { access.legacy = true } }
] ]
context.objects = [ context.objects = [
{ factory = spa-node-factory args = { factory.name = videotestsrc node.name = cursor-test node.description = cursor-test { factory = spa-node-factory args = { factory.name = videotestsrc node.name = cursor-test node.description = cursor-test
@ -164,13 +165,19 @@ context.objects = [
)"); config.close(); )"); config.close();
QProcessEnvironment environment = QProcessEnvironment::systemEnvironment(); QProcessEnvironment environment = QProcessEnvironment::systemEnvironment();
environment.insert("PIPEWIRE_RUNTIME_DIR", runtime.path()); 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()); server.start("pipewire", {"-c", config.fileName()}); QVERIFY(server.waitForStarted());
auto stop = qScopeGuard([&] { server.terminate(); if (!server.waitForFinished(3000)) { server.kill(); server.waitForFinished(); } }); auto stop = qScopeGuard([&] { server.terminate(); if (!server.waitForFinished(3000)) { server.kill(); server.waitForFinished(); } });
portal->socket = runtime.filePath("pipewire-0"); portal->socket = runtime.filePath("pipewire-0");
QTRY_VERIFY(QFile::exists(portal->socket)); QTRY_VERIFY(QFile::exists(portal->socket));
QProcess dump; dump.setProcessEnvironment(environment); dump.start("pw-dump"); 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; uint node = 0; QVariantMap props;
for (const auto& value : QJsonDocument::fromJson(dump.readAllStandardOutput()).array()) { for (const auto& value : QJsonDocument::fromJson(dump.readAllStandardOutput()).array()) {
auto object = value.toObject(), properties = object.value("info").toObject().value("props").toObject(); auto object = value.toObject(), properties = object.value("info").toObject().value("props").toObject();
@ -184,11 +191,15 @@ context.objects = [
// Без менеджера сессий соединяем только тестовые video ports, сохраняя настоящий транспорт PipeWire. // Без менеджера сессий соединяем только тестовые video ports, сохраняя настоящий транспорт PipeWire.
QElapsedTimer elapsed; elapsed.start(); QElapsedTimer elapsed; elapsed.start();
while (frames.isEmpty() && errors.isEmpty() && elapsed.elapsed() < 6000) { 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); QTest::qWait(50);
} }
if (!errors.isEmpty() || frames.isEmpty()) { if (!errors.isEmpty() || frames.isEmpty()) {
qWarning().noquote() << "PipeWire server:" << server.readAll() << "link:" << link.readAllStandardError(); 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); QVERIFY(errors.isEmpty()); QVERIFY(!frames.isEmpty()); QCOMPARE(portal->cursor, 2u);
auto frame = frames.last().front().value<QVideoFrame>(); QVERIFY(frame.isValid()); QVERIFY(!frame.toImage().isNull()); auto frame = frames.last().front().value<QVideoFrame>(); QVERIFY(frame.isValid()); QVERIFY(!frame.toImage().isNull());

Loading…
Cancel
Save