6#include <private/qsyntheticvideoscene_p.h>
7#include <private/qsyntheticpipewirevideosource_p.h>
9#include <QtMultimedia/private/qmultimedia_ranges_p.h>
10#include <QtMultimedia/private/qpipewire_support_p.h>
11#include <QtCore/qchronotimer.h>
12#include <QtCore/qcommandlineparser.h>
13#include <QtCore/qcoreapplication.h>
14#include <QtCore/qdebug.h>
15#include <QtCore/qhash.h>
16#include <QtCore/qspan.h>
17#include <QtCore/qtimer.h>
18#include <QtCore/private/quniquehandle_types_p.h>
19#include <QtDBus/qdbusabstractadaptor.h>
20#include <QtDBus/qdbusargument.h>
21#include <QtDBus/qdbusconnection.h>
22#include <QtDBus/qdbuscontext.h>
23#include <QtDBus/qdbuserror.h>
24#include <QtDBus/qdbusmessage.h>
25#include <QtDBus/qdbusmetatype.h>
26#include <QtDBus/qdbusextratypes.h>
27#include <QtDBus/qdbusunixfiledescriptor.h>
35#include <sys/socket.h>
41using namespace Qt::StringLiterals;
42using namespace std::chrono_literals;
55struct FakePortalStream
57 QtPipeWire::ObjectId nodeId{ 0 };
58 QVariantMap properties;
61QDBusArgument &operator<<(QDBusArgument &argument,
const FakePortalPoint &point)
63 argument.beginStructure();
64 argument << point.x << point.y;
65 argument.endStructure();
69const QDBusArgument &operator>>(
const QDBusArgument &argument, FakePortalPoint &point)
71 argument.beginStructure();
72 argument >> point.x >> point.y;
73 argument.endStructure();
77QDBusArgument &operator<<(QDBusArgument &argument,
const FakePortalStream &stream)
79 argument.beginStructure();
80 argument << stream.nodeId.value << stream.properties;
81 argument.endStructure();
85const QDBusArgument &operator>>(
const QDBusArgument &argument, FakePortalStream &stream)
87 argument.beginStructure();
88 argument >> stream.nodeId.value >> stream.properties;
89 argument.endStructure();
96q23::expected<QUniqueFileDescriptorHandle, QString> connectToPipeWire()
98 QByteArray runtimeDir = qgetenv(
"PIPEWIRE_RUNTIME_DIR");
99 if (runtimeDir.isEmpty())
100 runtimeDir = qgetenv(
"XDG_RUNTIME_DIR");
101 QByteArray coreName = qgetenv(
"PIPEWIRE_CORE");
102 if (coreName.isEmpty())
103 coreName =
"pipewire-0"_ba;
105 const QByteArray path = runtimeDir +
'/' + coreName;
108 .sun_family = AF_UNIX,
111 if (size_t(path.size()) >=
sizeof(address.sun_path)) {
112 return q23::unexpected(u"pipewire socket path exceeds sockaddr_un limits: "_s
113 + QString::fromLocal8Bit(path));
115 memcpy(address.sun_path, path.constData(), size_t(path.size()));
117 QUniqueFileDescriptorHandle fd{ ::socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0) };
119 return q23::unexpected(u"socket(): "_s + QString::fromLocal8Bit(strerror(errno)));
120 if (::connect(fd.get(),
reinterpret_cast<sockaddr *>(&address),
sizeof(address)) < 0) {
121 return q23::unexpected(u"connect("_s + QString::fromLocal8Bit(path) + u"): "_s
122 + QString::fromLocal8Bit(strerror(errno)));
127class FakePortalRequest :
public QObject
130 Q_CLASSINFO(
"D-Bus Interface",
"org.freedesktop.portal.Request")
132 using QObject::QObject;
135 Q_SCRIPTABLE
void Close() { }
138 Q_SCRIPTABLE
void Response(uint response, QVariantMap results);
141class FakePortalSession :
public QObject
144 Q_CLASSINFO(
"D-Bus Interface",
"org.freedesktop.portal.Session")
146 using QObject::QObject;
149 Q_SCRIPTABLE
void Close() { Q_EMIT Closed(); }
152 Q_SCRIPTABLE
void Closed();
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170class FakePortalObject :
public QObject,
public QDBusContext
176
177
178
179
180 enum class RequestScope { ScreenCast, Camera };
182 FakePortalObject(QSyntheticPipeWireVideoSource &source, QSize frameSize,
183 std::chrono::milliseconds responseDelay, QObject *parent)
184 : QObject(parent), m_source(source), m_frameSize(frameSize), m_responseDelay(responseDelay)
188 QSyntheticPipeWireVideoSource &source()
const {
return m_source; }
189 QSize frameSize()
const {
return m_frameSize; }
192 void failWith(QDBusError::ErrorType type,
const QString &message)
194 sendErrorReply(type, message);
198 QString ensureSession(
const QString &token)
200 const QString sessionPath =
201 u"/org/freedesktop/portal/desktop/session/%1/%2"_s.arg(callerName(), token);
203 if (!m_sessions.contains(sessionPath)) {
204 auto *session =
new FakePortalSession(
this);
205 QDBusConnection::sessionBus().registerObject(sessionPath, session,
206 QDBusConnection::ExportScriptableContents);
207 m_sessions.insert(sessionPath, session);
213
214
215
216
217
218
219 void supersedePending(RequestScope scope) { ++generationFor(scope); }
221 QDBusObjectPath respond(
const QVariantMap &options,
const QVariantMap &results,
224 const QString token =
225 options.value(u"handle_token"_s, u"qt%1"_s.arg(++m_fallbackToken)).toString();
227 u"/org/freedesktop/portal/desktop/request/%1/%2"_s.arg(callerName(), token);
235 FakePortalRequest *request = m_requests.value(path);
237 request =
new FakePortalRequest(
this);
238 if (!QDBusConnection::sessionBus().registerObject(
239 path, request, QDBusConnection::ExportScriptableContents)) {
240 qWarning() <<
"FakePortalObject: cannot export request object at" << path;
242 return QDBusObjectPath(path);
244 m_requests.insert(path, request);
247 const uint responseCode = std::exchange(m_nextResponseCode, 0u);
248 const quint64 generation = generationFor(scope);
254 QTimer::singleShot(m_responseDelay, request,
255#if __cplusplus
>= 202002L
260 if (generation != generationFor(scope))
262 Q_EMIT request->Response(responseCode, results);
265 return QDBusObjectPath(path);
271 void rememberSelectSourcesOptions(
const QVariantMap &options)
273 m_selectSourcesOptions = options;
275 QVariantMap takeSelectSourcesOptions() {
return std::exchange(m_selectSourcesOptions, {}); }
276 void setNextResponseCode(uint code) { m_nextResponseCode = code; }
279 for (FakePortalSession *session : std::as_const(m_sessions))
280 Q_EMIT session->Closed();
284 quint64 &generationFor(RequestScope scope)
286 return scope == RequestScope::ScreenCast ? m_screenCastGeneration : m_cameraGeneration;
290 QString callerName()
const
292 QString name = message().service();
293 if (name.startsWith(u':'))
295 return name.replace(u'.', u'_');
298 QSyntheticPipeWireVideoSource &m_source;
299 const QSize m_frameSize;
300 const std::chrono::milliseconds m_responseDelay;
301 QHash<QString, FakePortalRequest *> m_requests;
302 QHash<QString, FakePortalSession *> m_sessions;
303 QVariantMap m_selectSourcesOptions;
304 uint m_nextResponseCode = 0;
305 quint64 m_screenCastGeneration = 0;
306 quint64 m_cameraGeneration = 0;
307 int m_fallbackToken = 0;
310class FakeScreenCastAdaptor :
public QDBusAbstractAdaptor
313 Q_CLASSINFO(
"D-Bus Interface",
"org.freedesktop.portal.ScreenCast")
314 Q_PROPERTY(uint version READ version)
315 Q_PROPERTY(uint AvailableSourceTypes READ availableSourceTypes)
316 Q_PROPERTY(uint AvailableCursorModes READ availableCursorModes)
319 explicit FakeScreenCastAdaptor(FakePortalObject *portal)
320 : QDBusAbstractAdaptor(portal), m_portal(portal)
324 uint version()
const {
return 5; }
325 uint availableSourceTypes()
const {
return 3; }
326 uint availableCursorModes()
const {
return 7; }
331 QDBusObjectPath CreateSession(
const QVariantMap &options)
333 m_portal->supersedePending(FakePortalObject::RequestScope::ScreenCast);
335 const QString sessionPath =
336 m_portal->ensureSession(options.value(u"session_handle_token"_s).toString());
338 return m_portal->respond(options, QVariantMap{ { u"session_handle"_s, sessionPath } },
339 FakePortalObject::RequestScope::ScreenCast);
342 QDBusObjectPath SelectSources(
const QDBusObjectPath &session,
const QVariantMap &options)
345 m_portal->rememberSelectSourcesOptions(options);
346 return m_portal->respond(options, {}, FakePortalObject::RequestScope::ScreenCast);
349 QDBusObjectPath Start(
const QDBusObjectPath &session,
const QString &parentWindow,
350 const QVariantMap &options)
353 Q_UNUSED(parentWindow);
355 const QSize frameSize = m_portal->frameSize();
357 FakePortalStream stream;
358 stream.nodeId = m_portal->source().nodeId();
359 stream.properties = QVariantMap{
360 { u"id"_s, u"qt-fake-monitor-0"_s },
361 { u"position"_s, QVariant::fromValue(FakePortalPoint{ 0, 0 }) },
363 QVariant::fromValue(FakePortalPoint{ frameSize.width(), frameSize.height() }) },
364 { u"source_type"_s, uint(1) },
367 return m_portal->respond(
370 { u"streams"_s, QVariant::fromValue(QList<FakePortalStream>{ stream }) } },
371 FakePortalObject::RequestScope::ScreenCast);
374 QDBusUnixFileDescriptor OpenPipeWireRemote(
const QDBusObjectPath &session,
375 const QVariantMap &options)
380 auto fd = connectToPipeWire();
382 m_portal->failWith(QDBusError::Failed, fd.error());
383 return QDBusUnixFileDescriptor();
386 return QDBusUnixFileDescriptor(fd->get());
390 FakePortalObject *m_portal;
394class FakeXdgPortalControl :
public QObject
397 Q_CLASSINFO(
"D-Bus Interface",
"org.qtproject.qtmultimedia.FakeXdgPortal")
400 FakeXdgPortalControl(QSyntheticPipeWireVideoSource &source, FakePortalObject &portal,
401 QSize frameSize, QObject *parent)
402 : QObject(parent), m_source(source), m_portal(portal), m_frameSize(frameSize)
407 Q_SCRIPTABLE
void SetScene(
const QVariantMap &sceneMap)
409 SyntheticVideoScene scene = SyntheticVideoScene::fromVariantMap(sceneMap);
412 scene.frameSize = m_frameSize;
413 m_source.setScene(scene);
416 Q_SCRIPTABLE uint NodeId() {
return m_source.nodeId().value; }
419 Q_SCRIPTABLE QVariantMap TakeSelectSourcesOptions()
421 return m_portal.takeSelectSourcesOptions();
425 Q_SCRIPTABLE
void SetNextResponseCode(uint code) { m_portal.setNextResponseCode(code); }
428 Q_SCRIPTABLE
void CloseSessions() { m_portal.closeSessions(); }
431 QSyntheticPipeWireVideoSource &m_source;
432 FakePortalObject &m_portal;
433 const QSize m_frameSize;
443 std::chrono::milliseconds responseDelay;
447std::optional<CLIArgs> parseArgs(
int argc,
char **argv)
449 QCommandLineOption frameSizeOption(u"frame-size"_s, u"virtual screen size, WxH"_s, u"size"_s,
451 QCommandLineOption responseDelayOption(u"response-delay"_s,
452 u"milliseconds before answering a portal request"_s,
454 QCommandLineOption frameRateOption(u"frame-rate"_s, u"frames per second to publish"_s, u"fps"_s,
456 QCommandLineParser parser;
457 parser.addOptions({ frameSizeOption, responseDelayOption, frameRateOption });
459 QCommandLineOption(QString::fromLatin1(QtFakeXdgPortal::HelperArgument).mid(2)));
461 using namespace QtMultimediaPrivate;
463 QStringList arguments = views::transform(QSpan(argv, argc), [](
const char *arg) {
464 return QString::fromLocal8Bit(arg);
465 }) | ranges::to<QStringList>();
467 parser.process(arguments);
469 const QStringList dimensions = parser.value(frameSizeOption).split(u'x');
470 const QSize frameSize(dimensions.value(0).toInt(), dimensions.value(1).toInt());
471 if (frameSize.isEmpty()) {
472 qCritical(
"fake xdg portal helper: invalid --frame-size");
477 .frameSize = frameSize,
478 .responseDelay =
std::chrono::milliseconds(parser.value(responseDelayOption).toInt()),
479 .frameRate =
std::max(1, parser.value(frameRateOption).toInt()),
489 QCoreApplication app(argc, argv);
491 const std::optional<CLIArgs> args = parseArgs(argc, argv);
494 const QSize frameSize = args->frameSize;
496 qDBusRegisterMetaType<FakePortalPoint>();
497 qDBusRegisterMetaType<FakePortalStream>();
498 qDBusRegisterMetaType<QList<FakePortalStream>>();
500 QDBusConnection bus = QDBusConnection::sessionBus();
501 if (!bus.isConnected()) {
502 qCritical(
"fake xdg portal helper: no session bus");
506 SyntheticVideoScene scene;
507 scene.frameSize = frameSize;
509 QSyntheticPipeWireVideoSource source;
510 if (
auto result = source.start(scene); !result) {
511 qCritical(
"fake xdg portal helper: %s", qPrintable(result.error()));
516 QChronoTimer frameTimer(1000ms / args->frameRate);
517 frameTimer.callOnTimeout(&app, [&] {
518 source.renderNextFrame();
522 auto *portal =
new FakePortalObject(source, frameSize, args->responseDelay, &app);
523 new FakeScreenCastAdaptor(portal);
525 if (!bus.registerObject(u"/org/freedesktop/portal/desktop"_s, portal,
526 QDBusConnection::ExportAdaptors)) {
527 qCritical(
"fake xdg portal helper: cannot export the portal object");
531 auto *control =
new FakeXdgPortalControl(source, *portal, frameSize, &app);
532 bus.registerObject(u"/org/qtproject/qtmultimedia/FakeXdgPortal"_s, control,
533 QDBusConnection::ExportScriptableContents);
537 if (!bus.registerService(u"org.freedesktop.portal.Desktop"_s)) {
538 qCritical(
"fake xdg portal helper: cannot own org.freedesktop.portal.Desktop: %s",
539 qPrintable(bus.lastError().message()));
544 std::printf(
"QT_FAKE_XDG_PORTAL_READY %u\n", source.nodeId().value);
547 const int result = app.exec();
556#include "qfakexdgportal.moc"
int runPortalHelper(int argc, char **argv)