mirror of
https://github.com/gnif/LookingGlass.git
synced 2024-11-14 21:17:54 +00:00
[host] pipewire: implement basic capture
It works, with the following limitations: 1. user is forced to select the monitor through platform-specific mechanisms every time the client starts. 2. cursor is composed onto the screen, and no position can be reported.
This commit is contained in:
parent
28eae3bd86
commit
1e2caf4c9f
4 changed files with 840 additions and 6 deletions
|
@ -1,11 +1,21 @@
|
|||
cmake_minimum_required(VERSION 3.0)
|
||||
project(capture_pipewire LANGUAGES C)
|
||||
|
||||
find_package(PkgConfig)
|
||||
pkg_check_modules(CAPTURE_PIPEWIRE REQUIRED IMPORTED_TARGET
|
||||
gio-2.0
|
||||
gio-unix-2.0
|
||||
libpipewire-0.3
|
||||
libspa-0.2
|
||||
)
|
||||
|
||||
add_library(capture_pipewire STATIC
|
||||
src/pipewire.c
|
||||
src/portal.c
|
||||
)
|
||||
|
||||
target_link_libraries(capture_pipewire
|
||||
PkgConfig::CAPTURE_PIPEWIRE
|
||||
lg_common
|
||||
)
|
||||
|
||||
|
|
|
@ -18,14 +18,38 @@
|
|||
* Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
*/
|
||||
|
||||
#include "portal.h"
|
||||
#include "interface/capture.h"
|
||||
#include "interface/platform.h"
|
||||
#include "common/debug.h"
|
||||
#include "common/stringutils.h"
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <pipewire/pipewire.h>
|
||||
#include <spa/pod/builder.h>
|
||||
#include <spa/param/format.h>
|
||||
#include <spa/param/video/format-utils.h>
|
||||
|
||||
struct pipewire
|
||||
{
|
||||
struct Portal * portal;
|
||||
char * sessionHandle;
|
||||
struct pw_thread_loop * threadLoop;
|
||||
struct pw_context * context;
|
||||
struct pw_core * core;
|
||||
struct spa_hook coreListener;
|
||||
struct pw_stream * stream;
|
||||
struct spa_hook streamListener;
|
||||
|
||||
bool stop;
|
||||
bool hasFormat;
|
||||
bool formatChanged;
|
||||
int width, height;
|
||||
CaptureFormat format;
|
||||
uint8_t * frameData;
|
||||
unsigned int formatVer;
|
||||
};
|
||||
|
||||
static struct pipewire * this = NULL;
|
||||
|
@ -38,50 +62,381 @@ static bool pipewire_deinit();
|
|||
|
||||
static const char * pipewire_getName(void)
|
||||
{
|
||||
return "Pipewire";
|
||||
return "PipeWire";
|
||||
}
|
||||
|
||||
static bool pipewire_create(CaptureGetPointerBuffer getPointerBufferFn, CapturePostPointerBuffer postPointerBufferFn)
|
||||
{
|
||||
DEBUG_ASSERT(!this);
|
||||
pw_init(NULL, NULL);
|
||||
this = calloc(1, sizeof(*this));
|
||||
return true;
|
||||
}
|
||||
|
||||
static void coreErrorCallback(void * opaque, uint32_t id, int seq, int res, const char * message)
|
||||
{
|
||||
DEBUG_ERROR("pipewire error: id %" PRIu32 ", seq: %d, res: %d (%s): %s",
|
||||
id, seq, res, strerror(res), message);
|
||||
}
|
||||
|
||||
static const struct pw_core_events coreEvents = {
|
||||
PW_VERSION_CORE_EVENTS,
|
||||
.error = coreErrorCallback,
|
||||
};
|
||||
|
||||
static bool startStream(struct pw_stream * stream, uint32_t node)
|
||||
{
|
||||
char buffer[1024];
|
||||
struct spa_pod_builder builder = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
|
||||
const struct spa_pod * param = 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(6,
|
||||
SPA_VIDEO_FORMAT_BGRA, SPA_VIDEO_FORMAT_RGBA,
|
||||
SPA_VIDEO_FORMAT_BGRx, SPA_VIDEO_FORMAT_RGBx,
|
||||
SPA_VIDEO_FORMAT_r210, SPA_VIDEO_FORMAT_RGBA_F16),
|
||||
SPA_FORMAT_VIDEO_size, SPA_POD_CHOICE_RANGE_Rectangle(
|
||||
&SPA_RECTANGLE(1920, 1080), &SPA_RECTANGLE(1, 1), &SPA_RECTANGLE(8192, 4320)),
|
||||
SPA_FORMAT_VIDEO_framerate, SPA_POD_CHOICE_RANGE_Fraction(
|
||||
&SPA_FRACTION(60, 1), &SPA_FRACTION(0, 1), &SPA_FRACTION(360, 1)));
|
||||
|
||||
return pw_stream_connect(stream, PW_DIRECTION_INPUT, node,
|
||||
PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS, ¶m, 1) >= 0;
|
||||
}
|
||||
|
||||
static void streamProcessCallback(void * opaque)
|
||||
{
|
||||
if (!this->hasFormat)
|
||||
return;
|
||||
|
||||
struct pw_buffer * pwBuffer = NULL;
|
||||
|
||||
// dequeue all buffers to get the latest one
|
||||
while (true)
|
||||
{
|
||||
struct pw_buffer * tmp = pw_stream_dequeue_buffer(this->stream);
|
||||
if (!tmp)
|
||||
break;
|
||||
if (pwBuffer)
|
||||
pw_stream_queue_buffer(this->stream, pwBuffer);
|
||||
pwBuffer = tmp;
|
||||
}
|
||||
|
||||
if (!pwBuffer)
|
||||
{
|
||||
DEBUG_WARN("Pipewire out of buffers");
|
||||
return;
|
||||
}
|
||||
|
||||
struct spa_buffer * buffer = pwBuffer->buffer;
|
||||
if (!buffer->datas[0].chunk->size)
|
||||
return;
|
||||
|
||||
this->frameData = buffer->datas[0].data;
|
||||
|
||||
pw_thread_loop_signal(this->threadLoop, true);
|
||||
pw_stream_queue_buffer(this->stream, pwBuffer);
|
||||
}
|
||||
|
||||
static CaptureFormat convertSpaFormat(enum spa_video_format spa)
|
||||
{
|
||||
switch (spa)
|
||||
{
|
||||
case SPA_VIDEO_FORMAT_RGBA:
|
||||
case SPA_VIDEO_FORMAT_RGBx:
|
||||
return CAPTURE_FMT_RGBA;
|
||||
|
||||
case SPA_VIDEO_FORMAT_BGRA:
|
||||
case SPA_VIDEO_FORMAT_BGRx:
|
||||
return CAPTURE_FMT_BGRA;
|
||||
|
||||
case SPA_VIDEO_FORMAT_r210:
|
||||
return CAPTURE_FMT_RGBA10;
|
||||
|
||||
case SPA_VIDEO_FORMAT_RGBA_F16:
|
||||
return CAPTURE_FMT_RGBA16F;
|
||||
|
||||
default:
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
static void streamParamChangedCallback(void * opaque, uint32_t id,
|
||||
const struct spa_pod * param)
|
||||
{
|
||||
if (!param || id != SPA_PARAM_Format)
|
||||
return;
|
||||
|
||||
uint32_t mediaType, mediaSubtype;
|
||||
if (spa_format_parse(param, &mediaType, &mediaSubtype) < 0 ||
|
||||
mediaType != SPA_MEDIA_TYPE_video ||
|
||||
mediaSubtype != SPA_MEDIA_SUBTYPE_raw)
|
||||
return;
|
||||
|
||||
struct spa_video_info_raw info;
|
||||
if (spa_format_video_raw_parse(param, &info) < 0)
|
||||
{
|
||||
DEBUG_ERROR("Failed to parse video info");
|
||||
return;
|
||||
}
|
||||
|
||||
this->width = info.size.width;
|
||||
this->height = info.size.height;
|
||||
this->format = convertSpaFormat(info.format);
|
||||
|
||||
if (this->hasFormat)
|
||||
{
|
||||
this->formatChanged = true;
|
||||
return;
|
||||
}
|
||||
|
||||
char buffer[1024];
|
||||
struct spa_pod_builder builder = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
|
||||
|
||||
param = spa_pod_builder_add_object(
|
||||
&builder, SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers,
|
||||
SPA_PARAM_BUFFERS_dataType, SPA_POD_Int(1 << SPA_DATA_MemPtr));
|
||||
pw_stream_update_params(this->stream, ¶m, 1);
|
||||
|
||||
this->hasFormat = true;
|
||||
pw_thread_loop_signal(this->threadLoop, true);
|
||||
}
|
||||
|
||||
static const struct pw_stream_events streamEvents = {
|
||||
PW_VERSION_STREAM_EVENTS,
|
||||
.process = streamProcessCallback,
|
||||
.param_changed = streamParamChangedCallback,
|
||||
};
|
||||
|
||||
static bool pipewire_init(void)
|
||||
{
|
||||
goto fail;
|
||||
DEBUG_ASSERT(this);
|
||||
this->stop = false;
|
||||
|
||||
this->portal = portal_create();
|
||||
if (!this->portal)
|
||||
{
|
||||
DEBUG_ERROR("Failed to create xdg-desktop-portal for screencasting");
|
||||
goto fail;
|
||||
}
|
||||
|
||||
if (!portal_createScreenCastSession(this->portal, &this->sessionHandle))
|
||||
{
|
||||
DEBUG_ERROR("Failed to create ScreenCast session");
|
||||
goto fail;
|
||||
}
|
||||
|
||||
DEBUG_INFO("Got session handle: %s", this->sessionHandle);
|
||||
|
||||
if (!portal_selectSource(this->portal, this->sessionHandle))
|
||||
{
|
||||
DEBUG_ERROR("Failed to select source");
|
||||
goto fail;
|
||||
}
|
||||
|
||||
uint32_t pipewireNode = portal_getPipewireNode(this->portal, this->sessionHandle);
|
||||
if (!pipewireNode)
|
||||
{
|
||||
DEBUG_ERROR("Failed to get pipewire node");
|
||||
goto fail;
|
||||
}
|
||||
|
||||
int pipewireFd = portal_openPipewireRemote(this->portal, this->sessionHandle);
|
||||
if (pipewireFd < 0)
|
||||
{
|
||||
DEBUG_ERROR("Failed to get pipewire fd");
|
||||
goto fail;
|
||||
}
|
||||
|
||||
this->threadLoop = pw_thread_loop_new("lg-pipewire-capture", NULL);
|
||||
if (!this->threadLoop)
|
||||
{
|
||||
DEBUG_ERROR("Failed to create pipewire thread loop");
|
||||
close(pipewireFd);
|
||||
goto fail;
|
||||
}
|
||||
|
||||
this->context = pw_context_new(pw_thread_loop_get_loop(this->threadLoop), NULL, 0);
|
||||
if (!this->context)
|
||||
{
|
||||
DEBUG_ERROR("Failed to create pipewire context");
|
||||
close(pipewireFd);
|
||||
goto fail;
|
||||
}
|
||||
|
||||
if (pw_thread_loop_start(this->threadLoop) < 0)
|
||||
{
|
||||
DEBUG_ERROR("Failed to start pipewire thread loop");
|
||||
close(pipewireFd);
|
||||
goto fail;
|
||||
}
|
||||
|
||||
pw_thread_loop_lock(this->threadLoop);
|
||||
|
||||
this->core = pw_context_connect_fd(this->context, pipewireFd, NULL, 0);
|
||||
if (!this->core)
|
||||
{
|
||||
DEBUG_ERROR("Failed to create pipewire core: %s", strerror(errno));
|
||||
pw_thread_loop_unlock(this->threadLoop);
|
||||
close(pipewireFd);
|
||||
goto fail;
|
||||
}
|
||||
|
||||
pw_core_add_listener(this->core, &this->coreListener, &coreEvents, NULL);
|
||||
|
||||
this->stream = pw_stream_new(this->core, "Looking Glass (host)", pw_properties_new(
|
||||
PW_KEY_MEDIA_TYPE, "Video",
|
||||
PW_KEY_MEDIA_CATEGORY, "Capture",
|
||||
PW_KEY_MEDIA_ROLE, "Screen", NULL));
|
||||
if (!this->stream)
|
||||
{
|
||||
DEBUG_ERROR("Failed to create pipewire stream");
|
||||
pw_thread_loop_unlock(this->threadLoop);
|
||||
goto fail;
|
||||
}
|
||||
|
||||
this->hasFormat = false;
|
||||
this->formatChanged = false;
|
||||
this->frameData = NULL;
|
||||
pw_stream_add_listener(this->stream, &this->streamListener, &streamEvents, NULL);
|
||||
|
||||
if (!startStream(this->stream, pipewireNode))
|
||||
{
|
||||
DEBUG_ERROR("Failed to start pipewire stream");
|
||||
pw_thread_loop_unlock(this->threadLoop);
|
||||
goto fail;
|
||||
}
|
||||
|
||||
pw_thread_loop_unlock(this->threadLoop);
|
||||
|
||||
while (!this->hasFormat)
|
||||
pw_thread_loop_wait(this->threadLoop);
|
||||
|
||||
if (this->format < 0)
|
||||
{
|
||||
DEBUG_ERROR("Unknown frame format");
|
||||
pw_thread_loop_accept(this->threadLoop);
|
||||
goto fail;
|
||||
}
|
||||
|
||||
DEBUG_INFO("Frame size : %dx%d", this->width, this->height);
|
||||
|
||||
pw_thread_loop_accept(this->threadLoop);
|
||||
|
||||
return true;
|
||||
fail:
|
||||
pipewire_deinit();
|
||||
return false;
|
||||
}
|
||||
|
||||
static void pipewire_stop(void)
|
||||
{
|
||||
this->stop = true;
|
||||
pw_stream_disconnect(this->stream);
|
||||
pw_thread_loop_signal(this->threadLoop, false);
|
||||
}
|
||||
|
||||
static bool pipewire_deinit(void)
|
||||
{
|
||||
return false;
|
||||
if (this->stream)
|
||||
{
|
||||
pw_stream_disconnect(this->stream);
|
||||
this->stream = NULL;
|
||||
}
|
||||
|
||||
if (this->core)
|
||||
{
|
||||
pw_core_disconnect(this->core);
|
||||
this->core = NULL;
|
||||
}
|
||||
|
||||
if (this->threadLoop)
|
||||
{
|
||||
pw_thread_loop_stop(this->threadLoop);
|
||||
pw_thread_loop_destroy(this->threadLoop);
|
||||
this->threadLoop = NULL;
|
||||
}
|
||||
|
||||
if (this->sessionHandle)
|
||||
portal_destroySession(this->portal, &this->sessionHandle);
|
||||
|
||||
if (this->portal)
|
||||
{
|
||||
portal_free(this->portal);
|
||||
this->portal = NULL;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void pipewire_free(void)
|
||||
{
|
||||
DEBUG_ASSERT(this);
|
||||
pw_deinit();
|
||||
free(this);
|
||||
this = NULL;
|
||||
}
|
||||
|
||||
static CaptureResult pipewire_capture(void)
|
||||
{
|
||||
return CAPTURE_RESULT_ERROR;
|
||||
int result;
|
||||
|
||||
restart:
|
||||
result = pw_thread_loop_timed_wait(this->threadLoop, 1);
|
||||
|
||||
if (this->stop)
|
||||
return CAPTURE_RESULT_REINIT;
|
||||
|
||||
if (result == ETIMEDOUT)
|
||||
return CAPTURE_RESULT_TIMEOUT;
|
||||
|
||||
if (this->formatChanged)
|
||||
{
|
||||
++this->formatVer;
|
||||
this->formatChanged = false;
|
||||
pw_thread_loop_accept(this->threadLoop);
|
||||
goto restart;
|
||||
}
|
||||
|
||||
return CAPTURE_RESULT_OK;
|
||||
}
|
||||
|
||||
static CaptureResult pipewire_waitFrame(CaptureFrame * frame,
|
||||
const size_t maxFrameSize)
|
||||
{
|
||||
return CAPTURE_RESULT_ERROR;
|
||||
if (this->stop)
|
||||
return CAPTURE_RESULT_REINIT;
|
||||
|
||||
const int bpp = this->format == CAPTURE_FMT_RGBA16F ? 8 : 4;
|
||||
const unsigned int maxHeight = maxFrameSize / (this->width * bpp);
|
||||
|
||||
frame->formatVer = this->formatVer;
|
||||
frame->format = this->format;
|
||||
frame->width = this->width;
|
||||
frame->height = maxHeight > this->height ? this->height : maxHeight;
|
||||
frame->realHeight = this->height;
|
||||
frame->pitch = this->width * bpp;
|
||||
frame->stride = this->width;
|
||||
frame->rotation = CAPTURE_ROT_0;
|
||||
|
||||
// TODO: implement damage.
|
||||
frame->damageRectsCount = 0;
|
||||
|
||||
return CAPTURE_RESULT_OK;
|
||||
}
|
||||
|
||||
static CaptureResult pipewire_getFrame(FrameBuffer * frame,
|
||||
const unsigned int height, int frameIndex)
|
||||
{
|
||||
return CAPTURE_RESULT_ERROR;
|
||||
if (this->stop || !this->frameData)
|
||||
return CAPTURE_RESULT_REINIT;
|
||||
|
||||
const int bpp = this->format == CAPTURE_FMT_RGBA16F ? 8 : 4;
|
||||
framebuffer_write(frame, this->frameData, height * this->width * bpp);
|
||||
|
||||
pw_thread_loop_accept(this->threadLoop);
|
||||
return CAPTURE_RESULT_OK;
|
||||
}
|
||||
|
||||
struct CaptureInterface Capture_pipewire =
|
||||
|
@ -91,6 +446,7 @@ struct CaptureInterface Capture_pipewire =
|
|||
.getName = pipewire_getName,
|
||||
.create = pipewire_create,
|
||||
.init = pipewire_init,
|
||||
.stop = pipewire_stop,
|
||||
.deinit = pipewire_deinit,
|
||||
.free = pipewire_free,
|
||||
.capture = pipewire_capture,
|
||||
|
|
428
host/platform/Linux/capture/pipewire/src/portal.c
Normal file
428
host/platform/Linux/capture/pipewire/src/portal.c
Normal file
|
@ -0,0 +1,428 @@
|
|||
/**
|
||||
* Looking Glass
|
||||
* Copyright © 2017-2021 The Looking Glass Authors
|
||||
* https://looking-glass.io
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation; either version 2 of the License, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc., 59
|
||||
* Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
*/
|
||||
|
||||
#include "portal.h"
|
||||
#include "common/debug.h"
|
||||
#include "common/stringutils.h"
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#include <gio/gio.h>
|
||||
#include <gio/gunixfdlist.h>
|
||||
|
||||
struct Portal
|
||||
{
|
||||
GDBusConnection * conn;
|
||||
GDBusProxy * screenCast;
|
||||
char * senderName;
|
||||
};
|
||||
|
||||
struct DBusCallback
|
||||
{
|
||||
guint id;
|
||||
bool completed;
|
||||
void * opaque;
|
||||
};
|
||||
|
||||
|
||||
struct Portal * portal_create(void)
|
||||
{
|
||||
struct Portal * portal = calloc(1, sizeof(*portal));
|
||||
if (!portal)
|
||||
{
|
||||
DEBUG_ERROR("Failed to allocate memory");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
g_autoptr(GError) err = NULL;
|
||||
portal->conn = g_bus_get_sync(G_BUS_TYPE_SESSION, NULL, &err);
|
||||
if (err)
|
||||
{
|
||||
DEBUG_ERROR("Failed to get dbus session: %s", err->message);
|
||||
goto fail;
|
||||
}
|
||||
|
||||
const gchar * uniqueName = g_dbus_connection_get_unique_name(portal->conn);
|
||||
if (!uniqueName)
|
||||
{
|
||||
DEBUG_ERROR("Failed to get dbus connection unique name");
|
||||
goto fail;
|
||||
}
|
||||
|
||||
portal->senderName = strdup(uniqueName + 1);
|
||||
if (!portal->senderName)
|
||||
{
|
||||
DEBUG_ERROR("Failed to allocate memory");
|
||||
goto fail;
|
||||
}
|
||||
|
||||
char * ptr = portal->senderName;
|
||||
while ((ptr = strchr(ptr, '.')))
|
||||
*ptr++ = '_';
|
||||
|
||||
portal->screenCast = g_dbus_proxy_new_sync(portal->conn, G_DBUS_PROXY_FLAGS_NONE, NULL,
|
||||
"org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop",
|
||||
"org.freedesktop.portal.ScreenCast", NULL, &err);
|
||||
if (err)
|
||||
{
|
||||
DEBUG_ERROR("Failed to get ScreenCast portal: %s", err->message);
|
||||
goto fail;
|
||||
}
|
||||
|
||||
return portal;
|
||||
|
||||
fail:
|
||||
portal_free(portal);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void portal_free(struct Portal * portal)
|
||||
{
|
||||
if (!portal)
|
||||
return;
|
||||
|
||||
if (portal->screenCast)
|
||||
g_object_unref(portal->screenCast);
|
||||
|
||||
free(portal->senderName);
|
||||
|
||||
if (portal->conn)
|
||||
g_object_unref(portal->conn);
|
||||
|
||||
free(portal);
|
||||
}
|
||||
|
||||
static void callbackRegister(struct Portal * portal, struct DBusCallback * data,
|
||||
const char * path, GDBusSignalCallback func)
|
||||
{
|
||||
data->id = g_dbus_connection_signal_subscribe(portal->conn, "org.freedesktop.portal.Desktop",
|
||||
"org.freedesktop.portal.Request", "Response", path, NULL,
|
||||
G_DBUS_SIGNAL_FLAGS_NO_MATCH_RULE, func, data, NULL);
|
||||
}
|
||||
|
||||
static void callbackUnregister(struct Portal * portal, struct DBusCallback * data)
|
||||
{
|
||||
if (!data->id)
|
||||
return;
|
||||
g_dbus_connection_signal_unsubscribe(portal->conn, data->id);
|
||||
}
|
||||
|
||||
static bool getRequestPath(struct Portal * portal, char ** path, char ** token)
|
||||
{
|
||||
static uint64_t counter = 0;
|
||||
|
||||
alloc_sprintf(token, "lg%" PRIu64, counter);
|
||||
if (!*token)
|
||||
return false;
|
||||
|
||||
alloc_sprintf(path, "/org/freedesktop/portal/desktop/request/%s/lg%" PRIu64,
|
||||
portal->senderName, counter);
|
||||
if (!path)
|
||||
{
|
||||
free(*token);
|
||||
*token = NULL;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool getSessionToken(char ** token)
|
||||
{
|
||||
static uint64_t counter = 0;
|
||||
|
||||
alloc_sprintf(token, "lg%" PRIu64, counter);
|
||||
return !!*token;
|
||||
}
|
||||
|
||||
static void createSessionCallback(GDBusConnection * conn, const char * senderName,
|
||||
const char *objectPath, const char * interfaceName, const char * signalName,
|
||||
GVariant * params, void * opaque)
|
||||
{
|
||||
struct DBusCallback * callback = opaque;
|
||||
uint32_t status;
|
||||
g_autoptr(GVariant) result = NULL;
|
||||
g_variant_get(params, "(u@a{sv})", &status, &result);
|
||||
|
||||
if (status != 0)
|
||||
DEBUG_ERROR("Failed to create ScreenCast: %" PRIu32, status);
|
||||
else
|
||||
g_variant_lookup(result, "session_handle", "s", callback->opaque);
|
||||
|
||||
callback->completed = true;
|
||||
}
|
||||
|
||||
bool portal_createScreenCastSession(struct Portal * portal, char ** handle)
|
||||
{
|
||||
g_autoptr(GError) err = NULL;
|
||||
char * requestPath = NULL;
|
||||
char * requestToken = NULL;
|
||||
char * sessionToken = NULL;
|
||||
bool result = false;
|
||||
struct DBusCallback callback = {0, .opaque = handle};
|
||||
|
||||
if (!getRequestPath(portal, &requestPath, &requestToken))
|
||||
{
|
||||
DEBUG_ERROR("Failed to get request path and token");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
if (!getSessionToken(&sessionToken))
|
||||
{
|
||||
DEBUG_ERROR("Failed to get session token");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
*handle = NULL;
|
||||
callbackRegister(portal, &callback, requestPath, createSessionCallback);
|
||||
|
||||
GVariantBuilder builder;
|
||||
g_variant_builder_init(&builder, G_VARIANT_TYPE_VARDICT);
|
||||
g_variant_builder_add(&builder, "{sv}", "handle_token", g_variant_new_string(requestToken));
|
||||
g_variant_builder_add(&builder, "{sv}", "session_handle_token", g_variant_new_string(sessionToken));
|
||||
|
||||
g_autoptr(GVariant) response = g_dbus_proxy_call_sync(portal->screenCast, "CreateSession",
|
||||
g_variant_new("(a{sv})", &builder), G_DBUS_CALL_FLAGS_NONE, -1, NULL, &err);
|
||||
|
||||
if (err)
|
||||
{
|
||||
DEBUG_ERROR("Failed to create ScreenCast session: %s", err->message);
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
while (!callback.completed)
|
||||
g_main_context_iteration(NULL, TRUE);
|
||||
|
||||
if (*handle)
|
||||
result = true;
|
||||
|
||||
cleanup:
|
||||
callbackUnregister(portal, &callback);
|
||||
free(requestPath);
|
||||
free(requestToken);
|
||||
free(sessionToken);
|
||||
return result;
|
||||
}
|
||||
|
||||
void portal_destroySession(struct Portal * portal, char ** sessionHandle)
|
||||
{
|
||||
if (!*sessionHandle)
|
||||
return;
|
||||
|
||||
g_dbus_connection_call_sync(portal->conn, "org.freedesktop.portal.Desktop",
|
||||
*sessionHandle, "org.freedesktop.portal.Session", "Close", NULL, NULL,
|
||||
G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL);
|
||||
g_free(*sessionHandle);
|
||||
sessionHandle = NULL;
|
||||
}
|
||||
|
||||
static void selectSourceCallback(GDBusConnection * conn, const char * senderName,
|
||||
const char *objectPath, const char * interfaceName, const char * signalName,
|
||||
GVariant * params, void * opaque)
|
||||
{
|
||||
struct DBusCallback * callback = opaque;
|
||||
uint32_t status;
|
||||
g_autoptr(GVariant) result = NULL;
|
||||
g_variant_get(params, "(u@a{sv})", &status, &result);
|
||||
|
||||
if (status != 0)
|
||||
DEBUG_ERROR("Failed select sources: %" PRIu32, status);
|
||||
else
|
||||
callback->opaque = (void *) 1;
|
||||
|
||||
callback->completed = true;
|
||||
}
|
||||
|
||||
bool portal_selectSource(struct Portal * portal, const char * sessionHandle)
|
||||
{
|
||||
g_autoptr(GError) err = NULL;
|
||||
bool result = false;
|
||||
char * requestPath = NULL;
|
||||
char * requestToken = NULL;
|
||||
struct DBusCallback callback = {0};
|
||||
|
||||
if (!getRequestPath(portal, &requestPath, &requestToken))
|
||||
{
|
||||
DEBUG_ERROR("Failed to get request path and token");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
callbackRegister(portal, &callback, requestPath, selectSourceCallback);
|
||||
|
||||
GVariantBuilder builder;
|
||||
g_variant_builder_init(&builder, G_VARIANT_TYPE_VARDICT);
|
||||
g_variant_builder_add(&builder, "{sv}", "types", g_variant_new_uint32(PIPEWIRE_CAPTURE_DESKTOP));
|
||||
g_variant_builder_add(&builder, "{sv}", "multiple", g_variant_new_boolean(FALSE));
|
||||
g_variant_builder_add(&builder, "{sv}", "handle_token", g_variant_new_string(requestToken));
|
||||
|
||||
g_autoptr(GVariant) cursorModes_ = g_dbus_proxy_get_cached_property(
|
||||
portal->screenCast, "AvailableCursorModes");
|
||||
uint32_t cursorModes = cursorModes_ ? g_variant_get_uint32(cursorModes_) : 0;
|
||||
|
||||
// TODO: support mode 4 (separate cursor)
|
||||
if (cursorModes & 2)
|
||||
{
|
||||
DEBUG_INFO("Cursor mode : embedded");
|
||||
g_variant_builder_add(&builder, "{sv}", "cursor_mode", g_variant_new_uint32(2));
|
||||
}
|
||||
else if (cursorModes & 1)
|
||||
{
|
||||
DEBUG_INFO("Cursor mode : none");
|
||||
g_variant_builder_add(&builder, "{sv}", "cursor_mode", g_variant_new_uint32(1));
|
||||
}
|
||||
else
|
||||
{
|
||||
DEBUG_ERROR("No known cursor mode found");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
g_autoptr(GVariant) response = g_dbus_proxy_call_sync(portal->screenCast, "SelectSources",
|
||||
g_variant_new("(oa{sv})", sessionHandle, &builder), G_DBUS_CALL_FLAGS_NONE, -1, NULL, &err);
|
||||
|
||||
if (err)
|
||||
{
|
||||
DEBUG_ERROR("Failed to call SelectSources: %s", err->message);
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
while (!callback.completed)
|
||||
g_main_context_iteration(NULL, TRUE);
|
||||
|
||||
result = callback.opaque;
|
||||
|
||||
cleanup:
|
||||
callbackUnregister(portal, &callback);
|
||||
free(requestPath);
|
||||
free(requestToken);
|
||||
return result;
|
||||
}
|
||||
|
||||
static void pipewireNodeCallback(GDBusConnection * conn, const char * senderName,
|
||||
const char *objectPath, const char * interfaceName, const char * signalName,
|
||||
GVariant * params, void * opaque)
|
||||
{
|
||||
struct DBusCallback * callback = opaque;
|
||||
callback->completed = true;
|
||||
|
||||
uint32_t status;
|
||||
g_autoptr(GVariant) result = NULL;
|
||||
g_variant_get(params, "(u@a{sv})", &status, &result);
|
||||
|
||||
if (status != 0)
|
||||
{
|
||||
DEBUG_ERROR("Failed start screencast: %" PRIu32, status);
|
||||
return;
|
||||
}
|
||||
|
||||
g_autoptr(GVariant) streams = g_variant_lookup_value(result, "streams", G_VARIANT_TYPE_ARRAY);
|
||||
|
||||
GVariantIter iter;
|
||||
g_variant_iter_init(&iter, streams);
|
||||
size_t count = g_variant_iter_n_children(&iter);
|
||||
|
||||
if (count != 1)
|
||||
{
|
||||
DEBUG_WARN("Received more than one stream, discarding all but last one");
|
||||
while (count-- > 1)
|
||||
{
|
||||
g_autoptr(GVariant) prop = NULL;
|
||||
uint32_t node;
|
||||
g_variant_iter_loop(&iter, "(u@a{sv})", &node, &prop);
|
||||
}
|
||||
}
|
||||
|
||||
g_autoptr(GVariant) prop = NULL;
|
||||
g_variant_iter_loop(&iter, "(u@a{sv})", callback->opaque, &prop);
|
||||
}
|
||||
|
||||
uint32_t portal_getPipewireNode(struct Portal * portal, const char * sessionHandle)
|
||||
{
|
||||
g_autoptr(GError) err = NULL;
|
||||
char * requestPath = NULL;
|
||||
char * requestToken = NULL;
|
||||
uint32_t result = 0;
|
||||
struct DBusCallback callback = {0, .opaque = &result};
|
||||
|
||||
if (!getRequestPath(portal, &requestPath, &requestToken))
|
||||
{
|
||||
DEBUG_ERROR("Failed to get request path and token");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
callbackRegister(portal, &callback, requestPath, pipewireNodeCallback);
|
||||
|
||||
GVariantBuilder builder;
|
||||
g_variant_builder_init(&builder, G_VARIANT_TYPE_VARDICT);
|
||||
g_variant_builder_add(&builder, "{sv}", "handle_token", g_variant_new_string(requestToken));
|
||||
|
||||
g_autoptr(GVariant) response = g_dbus_proxy_call_sync(portal->screenCast, "Start",
|
||||
g_variant_new("(osa{sv})", sessionHandle, "", &builder), G_DBUS_CALL_FLAGS_NONE, -1, NULL, &err);
|
||||
|
||||
if (err)
|
||||
{
|
||||
DEBUG_ERROR("Failed to call Start on session: %s", err->message);
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
while (!callback.completed)
|
||||
g_main_context_iteration(NULL, TRUE);
|
||||
|
||||
cleanup:
|
||||
callbackUnregister(portal, &callback);
|
||||
free(requestPath);
|
||||
free(requestToken);
|
||||
return result;
|
||||
}
|
||||
|
||||
int portal_openPipewireRemote(struct Portal * portal, const char * sessionHandle)
|
||||
{
|
||||
g_autoptr(GError) err = NULL;
|
||||
g_autoptr(GUnixFDList) fdList = NULL;
|
||||
|
||||
GVariantBuilder builder;
|
||||
g_variant_builder_init(&builder, G_VARIANT_TYPE_VARDICT);
|
||||
|
||||
g_autoptr(GVariant) response = g_dbus_proxy_call_with_unix_fd_list_sync(portal->screenCast,
|
||||
"OpenPipeWireRemote", g_variant_new("(oa{sv})", sessionHandle, &builder),
|
||||
G_DBUS_CALL_FLAGS_NONE, -1, NULL, &fdList, NULL, &err);
|
||||
|
||||
if (err)
|
||||
{
|
||||
DEBUG_ERROR("Failed to call OpenPipeWireRemote on session: %s", err->message);
|
||||
return -1;
|
||||
}
|
||||
|
||||
gint32 index;
|
||||
g_variant_get(response, "(h)", &index, &err);
|
||||
if (err)
|
||||
{
|
||||
DEBUG_ERROR("Failed to get pipewire fd index: %s", err->message);
|
||||
return -1;
|
||||
}
|
||||
|
||||
int fd = g_unix_fd_list_get(fdList, index, &err);
|
||||
if (err)
|
||||
{
|
||||
DEBUG_ERROR("Failed to get pipewire fd: %s", err->message);
|
||||
return -1;
|
||||
}
|
||||
|
||||
return fd;
|
||||
}
|
40
host/platform/Linux/capture/pipewire/src/portal.h
Normal file
40
host/platform/Linux/capture/pipewire/src/portal.h
Normal file
|
@ -0,0 +1,40 @@
|
|||
/**
|
||||
* Looking Glass
|
||||
* Copyright © 2017-2021 The Looking Glass Authors
|
||||
* https://looking-glass.io
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation; either version 2 of the License, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program; if not, write to the Free Software Foundation, Inc., 59
|
||||
* Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
enum PipewireCaptureType
|
||||
{
|
||||
PIPEWIRE_CAPTURE_DESKTOP = 1,
|
||||
PIPEWIRE_CAPTURE_WINDOW = 2,
|
||||
};
|
||||
|
||||
struct Portal;
|
||||
|
||||
struct Portal * portal_create(void);
|
||||
void portal_free(struct Portal * portal);
|
||||
bool portal_createScreenCastSession(struct Portal * portal, char ** handle);
|
||||
void portal_destroySession(struct Portal * portal, char ** sessionHandle);
|
||||
bool portal_selectSource(struct Portal * portal, const char * sessionHandle);
|
||||
uint32_t portal_getPipewireNode(struct Portal * portal, const char * sessionHandle);
|
||||
int portal_openPipewireRemote(struct Portal * portal, const char * sessionHandle);
|
Loading…
Reference in a new issue