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)
|
cmake_minimum_required(VERSION 3.0)
|
||||||
project(capture_pipewire LANGUAGES C)
|
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
|
add_library(capture_pipewire STATIC
|
||||||
src/pipewire.c
|
src/pipewire.c
|
||||||
|
src/portal.c
|
||||||
)
|
)
|
||||||
|
|
||||||
target_link_libraries(capture_pipewire
|
target_link_libraries(capture_pipewire
|
||||||
|
PkgConfig::CAPTURE_PIPEWIRE
|
||||||
lg_common
|
lg_common
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -18,14 +18,38 @@
|
||||||
* Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
* Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
#include "portal.h"
|
||||||
#include "interface/capture.h"
|
#include "interface/capture.h"
|
||||||
#include "interface/platform.h"
|
#include "interface/platform.h"
|
||||||
#include "common/debug.h"
|
#include "common/debug.h"
|
||||||
|
#include "common/stringutils.h"
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <stdlib.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 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;
|
static struct pipewire * this = NULL;
|
||||||
|
@ -38,50 +62,381 @@ static bool pipewire_deinit();
|
||||||
|
|
||||||
static const char * pipewire_getName(void)
|
static const char * pipewire_getName(void)
|
||||||
{
|
{
|
||||||
return "Pipewire";
|
return "PipeWire";
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool pipewire_create(CaptureGetPointerBuffer getPointerBufferFn, CapturePostPointerBuffer postPointerBufferFn)
|
static bool pipewire_create(CaptureGetPointerBuffer getPointerBufferFn, CapturePostPointerBuffer postPointerBufferFn)
|
||||||
{
|
{
|
||||||
DEBUG_ASSERT(!this);
|
DEBUG_ASSERT(!this);
|
||||||
|
pw_init(NULL, NULL);
|
||||||
this = calloc(1, sizeof(*this));
|
this = calloc(1, sizeof(*this));
|
||||||
return true;
|
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)
|
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:
|
fail:
|
||||||
pipewire_deinit();
|
pipewire_deinit();
|
||||||
return false;
|
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)
|
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)
|
static void pipewire_free(void)
|
||||||
{
|
{
|
||||||
|
DEBUG_ASSERT(this);
|
||||||
|
pw_deinit();
|
||||||
free(this);
|
free(this);
|
||||||
this = NULL;
|
this = NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
static CaptureResult pipewire_capture(void)
|
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,
|
static CaptureResult pipewire_waitFrame(CaptureFrame * frame,
|
||||||
const size_t maxFrameSize)
|
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,
|
static CaptureResult pipewire_getFrame(FrameBuffer * frame,
|
||||||
const unsigned int height, int frameIndex)
|
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 =
|
struct CaptureInterface Capture_pipewire =
|
||||||
|
@ -91,6 +446,7 @@ struct CaptureInterface Capture_pipewire =
|
||||||
.getName = pipewire_getName,
|
.getName = pipewire_getName,
|
||||||
.create = pipewire_create,
|
.create = pipewire_create,
|
||||||
.init = pipewire_init,
|
.init = pipewire_init,
|
||||||
|
.stop = pipewire_stop,
|
||||||
.deinit = pipewire_deinit,
|
.deinit = pipewire_deinit,
|
||||||
.free = pipewire_free,
|
.free = pipewire_free,
|
||||||
.capture = pipewire_capture,
|
.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