519 lines
17 KiB
C++
519 lines
17 KiB
C++
|
|
#include <QCoreApplication>
|
||
|
|
#include <QDBusArgument>
|
||
|
|
#include <QDBusConnection>
|
||
|
|
#include <QDBusError>
|
||
|
|
#include <QDBusInterface>
|
||
|
|
#include <QDBusMetaType>
|
||
|
|
#include <QDBusObjectPath>
|
||
|
|
#include <QDBusReply>
|
||
|
|
#include <QDBusUnixFileDescriptor>
|
||
|
|
#include <QDBusVariant>
|
||
|
|
#include <QFile>
|
||
|
|
#include <QImage>
|
||
|
|
#include <QJsonArray>
|
||
|
|
#include <QJsonDocument>
|
||
|
|
#include <QJsonObject>
|
||
|
|
#include <QRandomGenerator>
|
||
|
|
#include <QTextStream>
|
||
|
|
#include <QTimer>
|
||
|
|
#include <QVariantMap>
|
||
|
|
|
||
|
|
#include <gst/app/gstappsink.h>
|
||
|
|
#include <gst/gst.h>
|
||
|
|
#include <gst/video/video.h>
|
||
|
|
|
||
|
|
#include <unistd.h>
|
||
|
|
|
||
|
|
static const char *PORTAL_SERVICE = "org.freedesktop.portal.Desktop";
|
||
|
|
static const char *PORTAL_PATH = "/org/freedesktop/portal/desktop";
|
||
|
|
static const char *SCREENCAST_IFACE = "org.freedesktop.portal.ScreenCast";
|
||
|
|
static const uint SOURCE_TYPE_MONITOR = 1;
|
||
|
|
static const uint SOURCE_TYPE_WINDOW = 2;
|
||
|
|
static const uint CURSOR_MODE_HIDDEN = 1;
|
||
|
|
|
||
|
|
struct PipeWireStream
|
||
|
|
{
|
||
|
|
uint nodeId = 0;
|
||
|
|
QVariantMap properties;
|
||
|
|
};
|
||
|
|
|
||
|
|
Q_DECLARE_METATYPE(PipeWireStream)
|
||
|
|
Q_DECLARE_METATYPE(QList<PipeWireStream>)
|
||
|
|
|
||
|
|
const QDBusArgument &operator>>(const QDBusArgument &argument, PipeWireStream &stream)
|
||
|
|
{
|
||
|
|
argument.beginStructure();
|
||
|
|
argument >> stream.nodeId >> stream.properties;
|
||
|
|
argument.endStructure();
|
||
|
|
return argument;
|
||
|
|
}
|
||
|
|
|
||
|
|
QDBusArgument &operator<<(QDBusArgument &argument, const PipeWireStream &stream)
|
||
|
|
{
|
||
|
|
argument.beginStructure();
|
||
|
|
argument << stream.nodeId << stream.properties;
|
||
|
|
argument.endStructure();
|
||
|
|
return argument;
|
||
|
|
}
|
||
|
|
|
||
|
|
class RequestWatcher : public QObject
|
||
|
|
{
|
||
|
|
Q_OBJECT
|
||
|
|
|
||
|
|
public:
|
||
|
|
uint response = 2;
|
||
|
|
QVariantMap results;
|
||
|
|
bool done = false;
|
||
|
|
QString error;
|
||
|
|
|
||
|
|
public slots:
|
||
|
|
void Response(uint responseCode, const QVariantMap &responseResults)
|
||
|
|
{
|
||
|
|
response = responseCode;
|
||
|
|
results = responseResults;
|
||
|
|
done = true;
|
||
|
|
QCoreApplication::quit();
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
QDBusConnection sessionBus()
|
||
|
|
{
|
||
|
|
QDBusConnection bus = QDBusConnection::sessionBus();
|
||
|
|
if (bus.isConnected()) {
|
||
|
|
return bus;
|
||
|
|
}
|
||
|
|
|
||
|
|
QString runtimeDir = qEnvironmentVariable("XDG_RUNTIME_DIR");
|
||
|
|
if (runtimeDir.isEmpty()) {
|
||
|
|
runtimeDir = QStringLiteral("/run/user/%1").arg(getuid());
|
||
|
|
}
|
||
|
|
|
||
|
|
return QDBusConnection::connectToBus(
|
||
|
|
QStringLiteral("unix:path=%1/bus").arg(runtimeDir),
|
||
|
|
QStringLiteral("reforger-portal-session"));
|
||
|
|
}
|
||
|
|
|
||
|
|
QString token()
|
||
|
|
{
|
||
|
|
return QStringLiteral("reforger_%1_%2")
|
||
|
|
.arg(QCoreApplication::applicationPid())
|
||
|
|
.arg(QString::number(QRandomGenerator::global()->generate(), 16));
|
||
|
|
}
|
||
|
|
|
||
|
|
bool waitForRequest(QCoreApplication &app, QDBusConnection &bus, const QDBusObjectPath &handle, int timeoutMs, RequestWatcher &watcher, QTextStream &err)
|
||
|
|
{
|
||
|
|
const bool connected = bus.connect(
|
||
|
|
QString::fromLatin1(PORTAL_SERVICE),
|
||
|
|
handle.path(),
|
||
|
|
QStringLiteral("org.freedesktop.portal.Request"),
|
||
|
|
QStringLiteral("Response"),
|
||
|
|
&watcher,
|
||
|
|
SLOT(Response(uint,QVariantMap)));
|
||
|
|
if (!connected) {
|
||
|
|
err << "failed to listen for portal request response on " << handle.path() << ": "
|
||
|
|
<< bus.lastError().message() << "\n";
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
QTimer::singleShot(timeoutMs, &app, [&watcher]() {
|
||
|
|
if (!watcher.done) {
|
||
|
|
watcher.error = QStringLiteral("timed out waiting for portal response");
|
||
|
|
watcher.done = true;
|
||
|
|
QCoreApplication::quit();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
app.exec();
|
||
|
|
bus.disconnect(
|
||
|
|
QString::fromLatin1(PORTAL_SERVICE),
|
||
|
|
handle.path(),
|
||
|
|
QStringLiteral("org.freedesktop.portal.Request"),
|
||
|
|
QStringLiteral("Response"),
|
||
|
|
&watcher,
|
||
|
|
SLOT(Response(uint,QVariantMap)));
|
||
|
|
|
||
|
|
if (!watcher.error.isEmpty()) {
|
||
|
|
err << watcher.error << "\n";
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
if (watcher.response == 1) {
|
||
|
|
err << "portal request was cancelled\n";
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
if (watcher.response != 0) {
|
||
|
|
err << "portal request failed with response code " << watcher.response << "\n";
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
QVariantMap callRequest(
|
||
|
|
QCoreApplication &app,
|
||
|
|
QDBusConnection &bus,
|
||
|
|
QDBusInterface &portal,
|
||
|
|
const QString &method,
|
||
|
|
const QList<QVariant> &args,
|
||
|
|
int timeoutMs,
|
||
|
|
QTextStream &err,
|
||
|
|
bool *ok)
|
||
|
|
{
|
||
|
|
*ok = false;
|
||
|
|
QDBusMessage reply = portal.callWithArgumentList(QDBus::Block, method, args);
|
||
|
|
if (reply.type() == QDBusMessage::ErrorMessage) {
|
||
|
|
err << method << " failed: " << reply.errorName() << ": " << reply.errorMessage() << "\n";
|
||
|
|
return {};
|
||
|
|
}
|
||
|
|
if (reply.arguments().isEmpty()) {
|
||
|
|
err << method << " returned no request handle\n";
|
||
|
|
return {};
|
||
|
|
}
|
||
|
|
|
||
|
|
QDBusObjectPath handle = qdbus_cast<QDBusObjectPath>(reply.arguments().at(0));
|
||
|
|
if (handle.path().isEmpty()) {
|
||
|
|
err << method << " returned an invalid request handle\n";
|
||
|
|
return {};
|
||
|
|
}
|
||
|
|
|
||
|
|
RequestWatcher watcher;
|
||
|
|
if (!waitForRequest(app, bus, handle, timeoutMs, watcher, err)) {
|
||
|
|
return {};
|
||
|
|
}
|
||
|
|
*ok = true;
|
||
|
|
return watcher.results;
|
||
|
|
}
|
||
|
|
|
||
|
|
uint portalUIntProperty(QDBusConnection &bus, const QString &name)
|
||
|
|
{
|
||
|
|
QDBusInterface props(
|
||
|
|
QString::fromLatin1(PORTAL_SERVICE),
|
||
|
|
QString::fromLatin1(PORTAL_PATH),
|
||
|
|
QStringLiteral("org.freedesktop.DBus.Properties"),
|
||
|
|
bus);
|
||
|
|
QDBusReply<QDBusVariant> reply = props.call(QStringLiteral("Get"), QString::fromLatin1(SCREENCAST_IFACE), name);
|
||
|
|
if (!reply.isValid()) {
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
return reply.value().variant().toUInt();
|
||
|
|
}
|
||
|
|
|
||
|
|
QList<PipeWireStream> parseStreams(const QVariant &value)
|
||
|
|
{
|
||
|
|
QList<PipeWireStream> streams = qdbus_cast<QList<PipeWireStream>>(value);
|
||
|
|
if (!streams.isEmpty()) {
|
||
|
|
return streams;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (value.canConvert<QDBusArgument>()) {
|
||
|
|
return qdbus_cast<QList<PipeWireStream>>(value.value<QDBusArgument>());
|
||
|
|
}
|
||
|
|
|
||
|
|
return {};
|
||
|
|
}
|
||
|
|
|
||
|
|
QString variantString(const QVariantMap &map, const QString &key)
|
||
|
|
{
|
||
|
|
const QVariant value = map.value(key);
|
||
|
|
if (value.canConvert<QDBusObjectPath>()) {
|
||
|
|
return value.value<QDBusObjectPath>().path();
|
||
|
|
}
|
||
|
|
return value.toString();
|
||
|
|
}
|
||
|
|
|
||
|
|
void closePortalSession(QDBusConnection &bus, const QString &sessionHandle)
|
||
|
|
{
|
||
|
|
if (sessionHandle.isEmpty()) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
QDBusInterface session(
|
||
|
|
QString::fromLatin1(PORTAL_SERVICE),
|
||
|
|
sessionHandle,
|
||
|
|
QStringLiteral("org.freedesktop.portal.Session"),
|
||
|
|
bus);
|
||
|
|
if (session.isValid()) {
|
||
|
|
session.call(QStringLiteral("Close"));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
int writePipeWireFrame(int fd, uint nodeId, const QString &outputPath, int timeoutMs, QTextStream &err)
|
||
|
|
{
|
||
|
|
GstElement *pipeline = gst_pipeline_new("reforger-portal-capture");
|
||
|
|
GstElement *source = gst_element_factory_make("pipewiresrc", "source");
|
||
|
|
GstElement *convert = gst_element_factory_make("videoconvert", "convert");
|
||
|
|
GstElement *filter = gst_element_factory_make("capsfilter", "filter");
|
||
|
|
GstElement *sink = gst_element_factory_make("appsink", "sink");
|
||
|
|
|
||
|
|
if (!pipeline || !source || !convert || !filter || !sink) {
|
||
|
|
err << "failed to create GStreamer PipeWire capture pipeline\n";
|
||
|
|
if (pipeline) {
|
||
|
|
gst_object_unref(pipeline);
|
||
|
|
}
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
const QByteArray node = QByteArray::number(nodeId);
|
||
|
|
g_object_set(source, "fd", fd, "path", node.constData(), "num-buffers", 1, nullptr);
|
||
|
|
|
||
|
|
GstCaps *caps = gst_caps_new_simple("video/x-raw", "format", G_TYPE_STRING, "BGRA", nullptr);
|
||
|
|
g_object_set(filter, "caps", caps, nullptr);
|
||
|
|
gst_caps_unref(caps);
|
||
|
|
|
||
|
|
g_object_set(sink, "emit-signals", FALSE, "sync", FALSE, "max-buffers", 1, "drop", TRUE, nullptr);
|
||
|
|
|
||
|
|
gst_bin_add_many(GST_BIN(pipeline), source, convert, filter, sink, nullptr);
|
||
|
|
if (!gst_element_link_many(source, convert, filter, sink, nullptr)) {
|
||
|
|
err << "failed to link GStreamer PipeWire capture pipeline\n";
|
||
|
|
gst_object_unref(pipeline);
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (gst_element_set_state(pipeline, GST_STATE_PLAYING) == GST_STATE_CHANGE_FAILURE) {
|
||
|
|
err << "failed to start GStreamer PipeWire capture pipeline\n";
|
||
|
|
gst_object_unref(pipeline);
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
GstSample *sample = gst_app_sink_try_pull_sample(GST_APP_SINK(sink), timeoutMs * GST_MSECOND);
|
||
|
|
if (!sample) {
|
||
|
|
GstBus *bus = gst_element_get_bus(pipeline);
|
||
|
|
GstMessage *message = gst_bus_pop_filtered(bus, static_cast<GstMessageType>(GST_MESSAGE_ERROR | GST_MESSAGE_EOS));
|
||
|
|
if (message && GST_MESSAGE_TYPE(message) == GST_MESSAGE_ERROR) {
|
||
|
|
GError *error = nullptr;
|
||
|
|
gchar *debug = nullptr;
|
||
|
|
gst_message_parse_error(message, &error, &debug);
|
||
|
|
err << "GStreamer PipeWire capture failed: " << (error ? error->message : "unknown error") << "\n";
|
||
|
|
if (debug) {
|
||
|
|
err << debug << "\n";
|
||
|
|
}
|
||
|
|
if (error) {
|
||
|
|
g_error_free(error);
|
||
|
|
}
|
||
|
|
g_free(debug);
|
||
|
|
} else {
|
||
|
|
err << "timed out waiting for a PipeWire video frame\n";
|
||
|
|
}
|
||
|
|
if (message) {
|
||
|
|
gst_message_unref(message);
|
||
|
|
}
|
||
|
|
gst_object_unref(bus);
|
||
|
|
gst_element_set_state(pipeline, GST_STATE_NULL);
|
||
|
|
gst_object_unref(pipeline);
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
GstCaps *sampleCaps = gst_sample_get_caps(sample);
|
||
|
|
GstVideoInfo info;
|
||
|
|
if (!sampleCaps || !gst_video_info_from_caps(&info, sampleCaps)) {
|
||
|
|
err << "failed to read video frame format from PipeWire sample\n";
|
||
|
|
gst_sample_unref(sample);
|
||
|
|
gst_element_set_state(pipeline, GST_STATE_NULL);
|
||
|
|
gst_object_unref(pipeline);
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
GstBuffer *buffer = gst_sample_get_buffer(sample);
|
||
|
|
GstMapInfo map;
|
||
|
|
if (!buffer || !gst_buffer_map(buffer, &map, GST_MAP_READ)) {
|
||
|
|
err << "failed to map PipeWire video frame\n";
|
||
|
|
gst_sample_unref(sample);
|
||
|
|
gst_element_set_state(pipeline, GST_STATE_NULL);
|
||
|
|
gst_object_unref(pipeline);
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
const int width = GST_VIDEO_INFO_WIDTH(&info);
|
||
|
|
const int height = GST_VIDEO_INFO_HEIGHT(&info);
|
||
|
|
const int stride = GST_VIDEO_INFO_PLANE_STRIDE(&info, 0);
|
||
|
|
QImage image(map.data, width, height, stride, QImage::Format_ARGB32);
|
||
|
|
const bool saved = image.copy().save(outputPath, "PNG");
|
||
|
|
|
||
|
|
gst_buffer_unmap(buffer, &map);
|
||
|
|
gst_sample_unref(sample);
|
||
|
|
gst_element_set_state(pipeline, GST_STATE_NULL);
|
||
|
|
gst_object_unref(pipeline);
|
||
|
|
|
||
|
|
if (!saved) {
|
||
|
|
err << "failed to write PNG output: " << outputPath << "\n";
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
int captureWindow(QCoreApplication &app, const QString &outputPath, const QString &restoreToken, bool persist, int timeoutMs, QTextStream &out, QTextStream &err)
|
||
|
|
{
|
||
|
|
QDBusConnection bus = sessionBus();
|
||
|
|
if (!bus.isConnected()) {
|
||
|
|
err << "failed to connect to the session D-Bus: " << bus.lastError().message() << "\n";
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
const uint sourceTypes = portalUIntProperty(bus, QStringLiteral("AvailableSourceTypes"));
|
||
|
|
if (!(sourceTypes & SOURCE_TYPE_WINDOW)) {
|
||
|
|
err << "desktop portal does not advertise window capture support";
|
||
|
|
if (sourceTypes) {
|
||
|
|
err << " (AvailableSourceTypes=" << sourceTypes << ")";
|
||
|
|
}
|
||
|
|
err << "\n";
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
QDBusInterface portal(
|
||
|
|
QString::fromLatin1(PORTAL_SERVICE),
|
||
|
|
QString::fromLatin1(PORTAL_PATH),
|
||
|
|
QString::fromLatin1(SCREENCAST_IFACE),
|
||
|
|
bus);
|
||
|
|
if (!portal.isValid()) {
|
||
|
|
err << "failed to connect to desktop ScreenCast portal: " << portal.lastError().message() << "\n";
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
QVariantMap createOptions;
|
||
|
|
createOptions.insert(QStringLiteral("handle_token"), token());
|
||
|
|
createOptions.insert(QStringLiteral("session_handle_token"), token());
|
||
|
|
bool ok = false;
|
||
|
|
QVariantMap createResults = callRequest(app, bus, portal, QStringLiteral("CreateSession"), {createOptions}, timeoutMs, err, &ok);
|
||
|
|
if (!ok) {
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
const QString sessionHandle = variantString(createResults, QStringLiteral("session_handle"));
|
||
|
|
if (sessionHandle.isEmpty()) {
|
||
|
|
err << "portal CreateSession did not return a session handle\n";
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
QVariantMap selectOptions;
|
||
|
|
selectOptions.insert(QStringLiteral("handle_token"), token());
|
||
|
|
selectOptions.insert(QStringLiteral("types"), SOURCE_TYPE_WINDOW);
|
||
|
|
selectOptions.insert(QStringLiteral("multiple"), false);
|
||
|
|
selectOptions.insert(QStringLiteral("cursor_mode"), CURSOR_MODE_HIDDEN);
|
||
|
|
if (!restoreToken.isEmpty()) {
|
||
|
|
selectOptions.insert(QStringLiteral("restore_token"), restoreToken);
|
||
|
|
}
|
||
|
|
if (persist) {
|
||
|
|
selectOptions.insert(QStringLiteral("persist_mode"), 2u);
|
||
|
|
}
|
||
|
|
callRequest(app, bus, portal, QStringLiteral("SelectSources"), {QVariant::fromValue(QDBusObjectPath(sessionHandle)), selectOptions}, timeoutMs, err, &ok);
|
||
|
|
if (!ok) {
|
||
|
|
closePortalSession(bus, sessionHandle);
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
QVariantMap startOptions;
|
||
|
|
startOptions.insert(QStringLiteral("handle_token"), token());
|
||
|
|
QVariantMap startResults = callRequest(app, bus, portal, QStringLiteral("Start"), {QVariant::fromValue(QDBusObjectPath(sessionHandle)), QString(), startOptions}, timeoutMs, err, &ok);
|
||
|
|
if (!ok) {
|
||
|
|
closePortalSession(bus, sessionHandle);
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
const QList<PipeWireStream> streams = parseStreams(startResults.value(QStringLiteral("streams")));
|
||
|
|
if (streams.isEmpty()) {
|
||
|
|
err << "portal Start did not return any PipeWire streams\n";
|
||
|
|
closePortalSession(bus, sessionHandle);
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
const PipeWireStream stream = streams.first();
|
||
|
|
if (stream.nodeId == 0) {
|
||
|
|
err << "portal returned an invalid PipeWire node id\n";
|
||
|
|
closePortalSession(bus, sessionHandle);
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
QDBusReply<QDBusUnixFileDescriptor> fdReply = portal.call(
|
||
|
|
QStringLiteral("OpenPipeWireRemote"),
|
||
|
|
QVariant::fromValue(QDBusObjectPath(sessionHandle)),
|
||
|
|
QVariantMap());
|
||
|
|
if (!fdReply.isValid()) {
|
||
|
|
err << "OpenPipeWireRemote failed: " << fdReply.error().name() << ": " << fdReply.error().message() << "\n";
|
||
|
|
closePortalSession(bus, sessionHandle);
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
const QDBusUnixFileDescriptor remoteFd = fdReply.value();
|
||
|
|
if (!remoteFd.isValid()) {
|
||
|
|
err << "OpenPipeWireRemote returned an invalid file descriptor\n";
|
||
|
|
closePortalSession(bus, sessionHandle);
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
const int status = writePipeWireFrame(remoteFd.fileDescriptor(), stream.nodeId, outputPath, timeoutMs, err);
|
||
|
|
|
||
|
|
closePortalSession(bus, sessionHandle);
|
||
|
|
|
||
|
|
if (status != 0) {
|
||
|
|
return status;
|
||
|
|
}
|
||
|
|
|
||
|
|
QJsonObject info;
|
||
|
|
info.insert(QStringLiteral("node_id"), static_cast<int>(stream.nodeId));
|
||
|
|
info.insert(QStringLiteral("session_handle"), sessionHandle);
|
||
|
|
const QString newRestoreToken = variantString(startResults, QStringLiteral("restore_token"));
|
||
|
|
if (!newRestoreToken.isEmpty()) {
|
||
|
|
info.insert(QStringLiteral("restore_token"), newRestoreToken);
|
||
|
|
}
|
||
|
|
QJsonArray streamArray;
|
||
|
|
for (const PipeWireStream &pipeWireStream : streams) {
|
||
|
|
QJsonObject streamObject;
|
||
|
|
streamObject.insert(QStringLiteral("node_id"), static_cast<int>(pipeWireStream.nodeId));
|
||
|
|
QJsonObject properties;
|
||
|
|
for (auto it = pipeWireStream.properties.constBegin(); it != pipeWireStream.properties.constEnd(); ++it) {
|
||
|
|
properties.insert(it.key(), QJsonValue::fromVariant(it.value()));
|
||
|
|
}
|
||
|
|
streamObject.insert(QStringLiteral("properties"), properties);
|
||
|
|
streamArray.append(streamObject);
|
||
|
|
}
|
||
|
|
info.insert(QStringLiteral("streams"), streamArray);
|
||
|
|
out << QJsonDocument(info).toJson(QJsonDocument::Compact) << "\n";
|
||
|
|
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
int main(int argc, char *argv[])
|
||
|
|
{
|
||
|
|
QCoreApplication app(argc, argv);
|
||
|
|
gst_init(&argc, &argv);
|
||
|
|
qDBusRegisterMetaType<PipeWireStream>();
|
||
|
|
qDBusRegisterMetaType<QList<PipeWireStream>>();
|
||
|
|
|
||
|
|
QTextStream out(stdout);
|
||
|
|
QTextStream err(stderr);
|
||
|
|
const QStringList args = QCoreApplication::arguments();
|
||
|
|
|
||
|
|
QString outputPath;
|
||
|
|
QString restoreToken;
|
||
|
|
bool persist = false;
|
||
|
|
int timeoutMs = 60000;
|
||
|
|
|
||
|
|
for (int i = 1; i < args.size(); ++i) {
|
||
|
|
const QString arg = args.at(i);
|
||
|
|
if (arg == QLatin1String("--window") && i + 1 < args.size()) {
|
||
|
|
outputPath = args.at(++i);
|
||
|
|
} else if (arg == QLatin1String("--restore-token") && i + 1 < args.size()) {
|
||
|
|
restoreToken = args.at(++i);
|
||
|
|
} else if (arg == QLatin1String("--persist")) {
|
||
|
|
persist = true;
|
||
|
|
} else if (arg == QLatin1String("--timeout") && i + 1 < args.size()) {
|
||
|
|
bool ok = false;
|
||
|
|
const double seconds = args.at(++i).toDouble(&ok);
|
||
|
|
if (!ok || seconds <= 0) {
|
||
|
|
err << "invalid --timeout value\n";
|
||
|
|
return 64;
|
||
|
|
}
|
||
|
|
timeoutMs = qRound(seconds * 1000.0);
|
||
|
|
} else {
|
||
|
|
err << "unknown or incomplete argument: " << arg << "\n";
|
||
|
|
return 64;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (outputPath.isEmpty()) {
|
||
|
|
err << "usage: " << args.value(0)
|
||
|
|
<< " --window OUTPUT.png [--restore-token TOKEN] [--persist] [--timeout SECONDS]\n";
|
||
|
|
return 64;
|
||
|
|
}
|
||
|
|
|
||
|
|
return captureWindow(app, outputPath, restoreToken, persist, timeoutMs, out, err);
|
||
|
|
}
|
||
|
|
|
||
|
|
#include "portal_capture_frame.moc"
|