From patchwork Wed Sep 20 18:40:57 2023 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: Abhishek Ojha X-Patchwork-Id: 43857 Delivered-To: ffmpegpatchwork2@gmail.com Received: by 2002:a05:6a20:1e62:b0:149:dfde:5c0a with SMTP id cy34csp138413pzb; Wed, 20 Sep 2023 11:41:26 -0700 (PDT) X-Google-Smtp-Source: AGHT+IEJn5iHvSsMfMEky8nU+v1Qpa1yBnqo79qeRu2DhofB8A6C6f2bfxvEXaKj1blg1dd1Jhgm X-Received: by 2002:a17:906:304a:b0:9a2:28dc:4166 with SMTP id d10-20020a170906304a00b009a228dc4166mr2890632ejd.75.1695235286323; Wed, 20 Sep 2023 11:41:26 -0700 (PDT) ARC-Seal: i=1; a=rsa-sha256; t=1695235286; cv=none; d=google.com; s=arc-20160816; b=y13USYZo/DTwqvqeCDvjjpg2/VnAROkfR2NEk0jY5FKQxt0N9eStAjVg5vqYW8Mx7L s+/ldva6I72iWG/fl7QpXoNBHxqccdK2+E3/pyPbPUL+y+h1jGP37C7+4D3qqemd3GHg ar4Btv6Jr5YBjJWLppw5pgPBR0pJmbMc9DOdRSkviYq4eaVaml+R7nxaqz7bbq2VosKh iNAolAYRIK5b7qfe98FxVhjygVhVsFxnTd5JiZ/Mwb+3XpB1fl369gUsG6NSFU1dxmgB 3e8u8qpGpFwnuyMhxs6lxAkFL6jGzdxkMCpk+nNeK28jdlUYp7atiI98nWkRuW5bKO6h Z05A== ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=arc-20160816; h=sender:errors-to:content-transfer-encoding:cc:reply-to :list-subscribe:list-help:list-post:list-archive:list-unsubscribe :list-id:precedence:subject:mime-version:message-id:date:to:from :dkim-signature:dkim-filter:delivered-to; bh=dMKIYuzN6WKiK52LwsARP4wSlIKhsxux9cUMzpTEGHM=; fh=EoDQIxrGX5YoxE06pcRJp2YbYXp56qGDzCRNcLmuGVo=; b=kH3+N1nvaj2DBm2gOWNwRAW9DRY2R3BhUL3yROgSs7feGPntTGHv3B8wuZDhP3hfHp 3u7Z0Gb4rWbYPBQrz3PNRqq69fePQCEDGTNL3xrtByC73AFGrBVjl/QG5GtiMdm0Bc8p +kvhN2ynN7a0Py/kkMnpiuGLW0MFfnB0L7oqrsr5hHjyZk9NSfnXYUwOfUsVyGNxxbXG YpOxPqCSnTjJtMZ5ONQ6yEPYc8nAbi13u9uaQnErcJMXiKCxgNB1v2UpAxZqElEBGGxL +HWq2XeQRLj7QQkYgSx/gWpLEeEEx0lnptLs1fisE6hf24cG1XmcCTxiyIF/g0m9t0/j UW8Q== ARC-Authentication-Results: i=1; mx.google.com; dkim=neutral (body hash did not verify) header.i=@savoirfairelinux.com header.s=DFC430D2-D198-11EC-948E-34200CB392D2 header.b=ZLYLIZf6; spf=pass (google.com: domain of ffmpeg-devel-bounces@ffmpeg.org designates 79.124.17.100 as permitted sender) smtp.mailfrom=ffmpeg-devel-bounces@ffmpeg.org Return-Path: Received: from ffbox0-bg.mplayerhq.hu (ffbox0-bg.ffmpeg.org. [79.124.17.100]) by mx.google.com with ESMTP id ks18-20020a170906f85200b0099bcae257e3si12388912ejb.832.2023.09.20.11.41.25; Wed, 20 Sep 2023 11:41:26 -0700 (PDT) Received-SPF: pass (google.com: domain of ffmpeg-devel-bounces@ffmpeg.org designates 79.124.17.100 as permitted sender) client-ip=79.124.17.100; Authentication-Results: mx.google.com; dkim=neutral (body hash did not verify) header.i=@savoirfairelinux.com header.s=DFC430D2-D198-11EC-948E-34200CB392D2 header.b=ZLYLIZf6; spf=pass (google.com: domain of ffmpeg-devel-bounces@ffmpeg.org designates 79.124.17.100 as permitted sender) smtp.mailfrom=ffmpeg-devel-bounces@ffmpeg.org Received: from [127.0.1.1] (localhost [127.0.0.1]) by ffbox0-bg.mplayerhq.hu (Postfix) with ESMTP id 172E768C8E3; Wed, 20 Sep 2023 21:41:12 +0300 (EEST) X-Original-To: ffmpeg-devel@ffmpeg.org Delivered-To: ffmpeg-devel@ffmpeg.org Received: from mail.savoirfairelinux.com (mail.savoirfairelinux.com [208.88.110.44]) by ffbox0-bg.mplayerhq.hu (Postfix) with ESMTPS id 0EFA068C7E9 for ; Wed, 20 Sep 2023 21:41:03 +0300 (EEST) Received: from localhost (localhost [127.0.0.1]) by mail.savoirfairelinux.com (Postfix) with ESMTP id 64C8B9C2AE5 for ; Wed, 20 Sep 2023 14:41:02 -0400 (EDT) Received: from mail.savoirfairelinux.com ([127.0.0.1]) by localhost (mail.savoirfairelinux.com [127.0.0.1]) (amavis, port 10032) with ESMTP id ulbjPIP2ZjbI; Wed, 20 Sep 2023 14:40:59 -0400 (EDT) Received: from localhost (localhost [127.0.0.1]) by mail.savoirfairelinux.com (Postfix) with ESMTP id 44CA19C29BE; Wed, 20 Sep 2023 14:40:59 -0400 (EDT) DKIM-Filter: OpenDKIM Filter v2.10.3 mail.savoirfairelinux.com 44CA19C29BE DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=savoirfairelinux.com; s=DFC430D2-D198-11EC-948E-34200CB392D2; t=1695235259; bh=+ePUFF1sK9ALoLrnizNQ8C7jemTAJPBtG8GqpBhsrdU=; h=From:To:Date:Message-Id:MIME-Version; b=ZLYLIZf6pLVhE4c/le++PLZcQXQnG+KTeUBj1maaGOAorGS4QM8sH7A/W+7LGLmnf YE3N9M7P07KMw/fgiDyUuJhGE2FC5DPUnoEUfhuIBaZCwD099Y6c3xNpAjF+VpVcFM 2pMim6F4n93i2t8xlr0/xs8n703cqY5Xq2tTudXYwgNQAQDXn2H2ToLrZNZAPwjEZW 8c/BjrTyl1CIsj41BJJInflS7gVl+0rLSmxg6M8JoMfcSlAT4FEG7CBu1RZPWTvCs/ OhglxayvUYYvDw2t2dZVGW++hr3n0bhZ1J7RR6+vVOJW+zsmUeBD2pCXyE7+JDy1th QPz6Z5Ssn+7ng== X-Virus-Scanned: amavis at mail.savoirfairelinux.com Received: from mail.savoirfairelinux.com ([127.0.0.1]) by localhost (mail.savoirfairelinux.com [127.0.0.1]) (amavis, port 10026) with ESMTP id Mrs3xXalxy1q; Wed, 20 Sep 2023 14:40:59 -0400 (EDT) Received: from asuspc2.taile0726.ts.net (unknown [192.168.51.254]) by mail.savoirfairelinux.com (Postfix) with ESMTP id 295989C0E4C; Wed, 20 Sep 2023 14:40:59 -0400 (EDT) From: Abhishek Ojha To: ffmpeg-devel@ffmpeg.org Date: Wed, 20 Sep 2023 14:40:57 -0400 Message-Id: <20230920184058.1263793-1-abhishek.ojha@savoirfairelinux.com> X-Mailer: git-send-email 2.34.1 MIME-Version: 1.0 Subject: [FFmpeg-devel] [RFC PATCH 1/2] libavdevice/pipewiregrab: add pipewire based grab X-BeenThere: ffmpeg-devel@ffmpeg.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: FFmpeg development discussions and patches List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Reply-To: FFmpeg development discussions and patches Cc: Abhishek Ojha Errors-To: ffmpeg-devel-bounces@ffmpeg.org Sender: "ffmpeg-devel" X-TUID: oLBxNCTU7+BP This is an proof of concept for pipewire grab to enable screen capture on wayland. Add a new Linux capture based on [1] PipeWire and the [2] Desktop portal. This new capture starts by asking the Desktop portal for a screencapture session.There are quite a few D-Bus calls involved in this, but the key points are: 1. A connection to org.freedesktop.portal.ScreenCast is estabilished, and the available cursor modes are updated. Currently only embedded and hidden currsor mode enabled. 2. Call CreateSession via dbus call. This is the first step of the communication. Response callback return the status of created session. 3. Call SelectSources . This is when a system dialog pops up asking the user to either select a monitor (desktop capture).Only monitor capture is enabled in current implementation. 4. Call Start . This signals the compositor that it can setup a PipeWire stream, and start sending buffers. Above flow is implemented as per the [2] xdg-desktop-portal. Once flow is completed, pipewire fd is received and using this pipewire stream is created and receive buffer from the created stream. For cursor implementation, embedded cursor mode is enabled that means cursor metadata is not handled in current implementation and has no control over the cursor bitmap. gdbus/pipewire logic, this is based on obs-xdg, gstpipewire and pipewire examples, and initial pipewire grab logic, this is based on libavdevice/xcbgrab and libavdevice/v4l2 This implementation shows the skeleton implementation and enables basic functionality. I'd like to hear opinions and suggestions to improve and properly use this. [1] https://pipewire.org/ [2] https://github.com/flatpak/xdg-desktop-portal/ Below are the arguments for pipewiregrab. ffplay -f pipewiregrab -draw_mouse 1 -i :0.0 Signed-off-by: Abhishek Ojha --- configure | 9 + libavdevice/Makefile | 1 + libavdevice/alldevices.c | 1 + libavdevice/pipewiregrab.c | 1836 ++++++++++++++++++++++++++++++++++++ 4 files changed, 1847 insertions(+) create mode 100644 libavdevice/pipewiregrab.c diff --git a/configure b/configure index e40dcce09e..325b10484f 100755 --- a/configure +++ b/configure @@ -299,6 +299,7 @@ External library support: --enable-libxcb-shm enable X11 grabbing shm communication [autodetect] --enable-libxcb-xfixes enable X11 grabbing mouse rendering [autodetect] --enable-libxcb-shape enable X11 grabbing shape rendering [autodetect] + --enable-libpipewire enable screen grabbing using pipewire [autodetect] --enable-libxvid enable Xvid encoding via xvidcore, native MPEG-4/Xvid encoder exists [no] --enable-libxml2 enable XML parsing using the C library libxml2, needed @@ -1788,6 +1789,8 @@ EXTERNAL_AUTODETECT_LIBRARY_LIST=" libxcb_shm libxcb_shape libxcb_xfixes + libpipewire + libgio_unix lzma mediafoundation metal @@ -3621,6 +3624,7 @@ v4l2_outdev_suggest="libv4l2" vfwcap_indev_deps="vfw32 vfwcap_defines" xcbgrab_indev_deps="libxcb" xcbgrab_indev_suggest="libxcb_shm libxcb_shape libxcb_xfixes" +pipewiregrab_indev_deps="libpipewire libgio_unix pthreads" xv_outdev_deps="xlib_xv xlib_x11 xlib_xext" # protocols @@ -7041,6 +7045,11 @@ if enabled libxcb; then enabled libxcb_xfixes && check_pkg_config libxcb_xfixes xcb-xfixes xcb/xfixes.h xcb_xfixes_get_cursor_image fi +enabled libpipewire && check_pkg_config libpipewire "libpipewire-0.3 >= 0.3.40" pipewire/pipewire.h pw_init +if enabled libpipewire; then + enabled libgio_unix && check_pkg_config libgio_unix gio-unix-2.0 gio/gio.h g_main_loop_new +fi + check_func_headers "windows.h" CreateDIBSection "$gdigrab_indev_extralibs" # d3d11va requires linking directly to dxgi and d3d11 if not building for diff --git a/libavdevice/Makefile b/libavdevice/Makefile index c30449201d..f02960782d 100644 --- a/libavdevice/Makefile +++ b/libavdevice/Makefile @@ -49,6 +49,7 @@ OBJS-$(CONFIG_V4L2_INDEV) += v4l2.o v4l2-common.o timefilter.o OBJS-$(CONFIG_V4L2_OUTDEV) += v4l2enc.o v4l2-common.o OBJS-$(CONFIG_VFWCAP_INDEV) += vfwcap.o OBJS-$(CONFIG_XCBGRAB_INDEV) += xcbgrab.o +OBJS-$(CONFIG_PIPEWIREGRAB_INDEV) += pipewiregrab.o OBJS-$(CONFIG_XV_OUTDEV) += xv.o # external libraries diff --git a/libavdevice/alldevices.c b/libavdevice/alldevices.c index 8a90fcb5d7..1fa8563df4 100644 --- a/libavdevice/alldevices.c +++ b/libavdevice/alldevices.c @@ -53,6 +53,7 @@ extern const AVInputFormat ff_v4l2_demuxer; extern const FFOutputFormat ff_v4l2_muxer; extern const AVInputFormat ff_vfwcap_demuxer; extern const AVInputFormat ff_xcbgrab_demuxer; +extern const AVInputFormat ff_pipewiregrab_demuxer; extern const FFOutputFormat ff_xv_muxer; /* external libraries */ diff --git a/libavdevice/pipewiregrab.c b/libavdevice/pipewiregrab.c new file mode 100644 index 0000000000..5b96d43863 --- /dev/null +++ b/libavdevice/pipewiregrab.c @@ -0,0 +1,1836 @@ +/* + * PipeWire input grabber (ScreenCast) + * Copyright (C) 2023 Savoir-faire Linux, Inc. + * + * This file is part of FFmpeg. + * + * FFmpeg is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * FFmpeg 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with FFmpeg; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +/** + * @file + * Pipewire Grab demuxer + * @author Abhishek Ojha + */ + +#include "config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "libavutil/internal.h" +#include "libavutil/mathematics.h" +#include "libavutil/opt.h" +#include "libavutil/parseutils.h" +#include "libavutil/time.h" + +#include "libavformat/avformat.h" +#include "libavformat/internal.h" + +#include +#include +#include +#include +#include + +#include +#include + +#ifndef __USE_XOPEN2K8 +#define F_DUPFD_CLOEXEC \ + 1030 /* Duplicate file descriptor with close-on-exit set. */ +#endif + +#define BYTES_PER_PIXEL 4 /* currently all formats assume 4 bytes per pixel */ +#define REQUEST_PATH "/org/freedesktop/portal/desktop/request/%s/obs%u" +#define SESSION_PATH "/org/freedesktop/portal/desktop/session/%s/obs%u" +#define MAX_SPA_PARAM 4 /* max number of params for spa pod */ + +#define CURSOR_META_SIZE(width, height) \ + (sizeof(struct spa_meta_cursor) + sizeof(struct spa_meta_bitmap) + \ + width * height * 4) + +/** + * Pipewire capture types + */ +typedef enum { + DESKTOP_CAPTURE = 1, + WINDOW_CAPTURE = 2, +} pw_capture_type; + +/** + * Pipewire version structure + */ +struct FFmpegPwVersion { + int major; + int minor; + int micro; +}; + +/** + * Pipewire structure for frame processing + */ +struct PwStreamAndBuffer { + AVFormatContext *ctx; + struct pw_stream *pw_stream; + struct pw_buffer *pw_buf; +}; + +/** + * Pipewire supported cursor modes + */ +enum PortalCursorMode { + PORTAL_CURSOR_MODE_HIDDEN = 1 << 0, + PORTAL_CURSOR_MODE_EMBEDDED = 1 << 1, + PORTAL_CURSOR_MODE_METADATA = 1 << 2, +}; + +/** + * PipeWire Grab main structure + * Contains all necessary data that hold current state + * Initial state of this struct is allocated by libavdevice + * logic when declaring the AVInputFormat ff_pipewiregrab_demuxer. + * This structure is priv_data of AVFormatContext instance. + */ +typedef struct PipewireGrabContext { + /** thread used to intialize/start pipewire logic */ + pthread_t pipewire_pthread; + + /** conditional synchronization logic elecments between pipewire + * thread and libavdevice thread. + */ + pthread_cond_t avstream_codec_cond_var; + pthread_mutex_t avstream_codec_mutex; + atomic_int avstream_codec_flag; + + pthread_mutex_t current_pkt_mutex; + AVPacket* current_pkt; + + GDBusConnection *connection; + GDBusProxy *proxy; + GCancellable *cancellable; + + char *sender_name; + char *session_handle; + + uint32_t pipewire_node; + int pipewire_fd; + + uint32_t available_cursor_modes; + + GMainLoop *glib_main_loop; + struct pw_thread_loop *thread_loop; + struct pw_context *context; + + struct pw_core *core; + struct spa_hook core_listener; + + struct pw_stream *stream; + struct spa_hook stream_listener; + struct spa_video_info format; + + pw_capture_type capture_type; + bool negotiated; + + /**< draw_mouse is to control the enable/disable mouse cursor */ + int draw_mouse; + + /** cursor metadata */ + struct { + bool visible; + bool valid; + int x, y; + int hotspot_x, hotspot_y; + int width, height; + } cursor; + + /**< Width and height of the grab frame (private option) */ + uint32_t width, height; + + /**< Size in bytes of the frame pixel data */ + uint32_t frame_size; + uint8_t Bpp; + enum AVPixelFormat av_pxl_format; + AVRational user_frame_rate; + + int64_t time_frame; + AVRational time_base; + int64_t frame_duration; + + const AVClass *class; + + const char *framerate; + struct FFmpegPwVersion server_version; + int server_version_sync; +} PipewireGrabContext; + +/** + * dbus's method/event marshalling structure + */ +struct DbusCallData { + AVFormatContext *ctx; + char *request_path; + guint signal_id; + gulong cancelled_id; +}; + +#define FOLLOW_CENTER -1 + +#define OFFSET(x) offsetof(PipewireGrabContext, x) +#define D AV_OPT_FLAG_DECODING_PARAM +static const AVOption options[] = { + { "framerate", + "", + OFFSET(framerate), + AV_OPT_TYPE_STRING, + { .str = "ntsc" }, + 0, + 0, + D }, + { "draw_mouse", + "Draw the mouse pointer.", + OFFSET(draw_mouse), + AV_OPT_TYPE_INT, + { .i64 = 0 }, + 0, + 1, + D }, + { NULL }, +}; + +/** + * Function parse the pipewire version + * @param dst FmpegPwVersion that contains PipeWire version info + * @param version pipewire version + */ +static bool parse_pw_version(struct FFmpegPwVersion *dst, const char *version) +{ + int n_matches = sscanf(version, "%d.%d.%d", &dst->major, &dst->minor, + &dst->micro); + return n_matches == 3; +} + +/** + * Function update the pipewire version in private data + * params, then signal the blocked @ref pipewiregrab_read_header + * + * @param ctx AVFormatContext that contains PipeWire Grab main structure + * @param version pipewire version + */ +static void update_pw_versions(AVFormatContext *ctx, const char *version) +{ + PipewireGrabContext *pw_ctx = ctx->priv_data; + av_log(ctx, AV_LOG_DEBUG, "[pipewiregrab] Server version: %s\n", version); + av_log(ctx, AV_LOG_INFO, "[pipewiregrab] Library version: %s\n", + pw_get_library_version()); + av_log(ctx, AV_LOG_DEBUG, "[pipewiregrab] Header version: %s\n", + pw_get_headers_version()); + + if (!parse_pw_version(&pw_ctx->server_version, version)) + av_log(ctx, AV_LOG_WARNING, + "[pipewiregrab] failed to parse server version"); +} + +/** + * AVStream creator + * This function creates a valid AVStream based on pipewire's negotiated + * params, then signal the blocked @ref pipewiregrab_read_header + * + * @param ctx AVFormatContext that contains PipeWire Grab main structure + * @return 0 on success, AVERROR on failure + */ +static int create_ffmpeg_stream(AVFormatContext *ctx) +{ + int ret = 0; + int64_t frame_size_bits; + AVStream *avstream = avformat_new_stream(ctx, NULL); + PipewireGrabContext *pw_ctx = ctx->priv_data; + if (!pw_ctx) { + av_log(ctx, AV_LOG_ERROR, + "[pipewiregrab] Invalid private context data!\n"); + return AVERROR(EINVAL); + } + + if (!avstream) { + av_log(ctx, AV_LOG_ERROR, + "[pipewiregrab] avformat_new_stream failed!\n"); + return AVERROR(ENOMEM); + } + + avstream->avg_frame_rate.num = pw_ctx->user_frame_rate.num; + avstream->avg_frame_rate.den = pw_ctx->user_frame_rate.den; + + avpriv_set_pts_info(avstream, 64, 1, 1000000); + + pw_ctx->time_base = (AVRational){ avstream->avg_frame_rate.den, + avstream->avg_frame_rate.num }; + pw_ctx->frame_duration = av_rescale_q(1, pw_ctx->time_base, AV_TIME_BASE_Q); + pw_ctx->time_frame = av_gettime_relative(); + + frame_size_bits = (int64_t)pw_ctx->width * pw_ctx->height * pw_ctx->Bpp * 8; + avstream->codecpar->codec_type = AVMEDIA_TYPE_VIDEO; + avstream->codecpar->codec_id = AV_CODEC_ID_RAWVIDEO; + avstream->codecpar->width = pw_ctx->width; + avstream->codecpar->height = pw_ctx->height; + avstream->codecpar->bit_rate = + av_rescale(frame_size_bits, avstream->avg_frame_rate.num, + avstream->avg_frame_rate.den); + avstream->codecpar->format = pw_ctx->av_pxl_format; + + atomic_store(&pw_ctx->avstream_codec_flag, 1); + pthread_cond_signal(&pw_ctx->avstream_codec_cond_var); + + return ret; +} + +/** + * Pipewire manual abort + * + * @param user_data AVFormatContext that contains PipeWire Grab main structure + * @param message abort message + */ +static void pipewiregrab_abort(void *user_data, const char *message) +{ + AVFormatContext *ctx = user_data; + PipewireGrabContext *pw_ctx = NULL; + if (!ctx) { + return; + } + + pw_ctx = ctx->priv_data; + if (!pw_ctx) { + av_log(ctx, AV_LOG_ERROR, + "[pipewiregrab] Invalid private context data!\n"); + return; + } + + atomic_store(&pw_ctx->avstream_codec_flag, 1); + pthread_cond_signal(&pw_ctx->avstream_codec_cond_var); + + av_log(ctx, AV_LOG_ERROR, "[pipewiregrab] Aborting: %s\n", message); + + if (pw_ctx->thread_loop != NULL) + pw_thread_loop_signal(pw_ctx->thread_loop, false); + + if (pw_ctx->glib_main_loop != NULL && + g_main_loop_is_running(pw_ctx->glib_main_loop)) { + g_main_loop_quit(pw_ctx->glib_main_loop); + } +} + +/** + * Pipewire core info event + * + * @param user_data AVFormatContext that contains PipeWire Grab main structure + * @param info pw_core_info + */ +static void on_core_info_cb(void *user_data, const struct pw_core_info *info) +{ + AVFormatContext *ctx = user_data; + update_pw_versions(ctx, info->version); +} + +/** + * Pipewire core completion event + * + * @param user_data AVFormatContext that contains PipeWire Grab main structure + * @param id pipewire object id of calling + * @param seq pipewire object sequence + */ +static void on_core_done_callback(void *user_data, uint32_t id, int seq) +{ + AVFormatContext *ctx = user_data; + PipewireGrabContext *pw_ctx = NULL; + + (void)seq; + + if (!ctx) { + return; + } + + pw_ctx = ctx->priv_data; + if (!pw_ctx) { + av_log(ctx, AV_LOG_ERROR, + "[pipewiregrab] Invalid private context data!\n"); + return; + } + + if (id == PW_ID_CORE) { + pw_thread_loop_signal(pw_ctx->thread_loop, false); + g_main_loop_quit(pw_ctx->glib_main_loop); + } +} + +/** + * Pipewire core error event + * + * @param user_data AVFormatContext that contains PipeWire Grab main structure + * @param id pipewire object id of calling + * @param seq pipewire object sequence + * @param res error number + * @param message error message + */ +static void on_core_error_callback(void *user_data, uint32_t id, int seq, + int res, const char *message) +{ + AVFormatContext *ctx = user_data; + PipewireGrabContext *pw_ctx = NULL; + if (!ctx) { + return; + } + + pw_ctx = ctx->priv_data; + if (!pw_ctx) { + av_log(ctx, AV_LOG_ERROR, + "[pipewiregrab] Invalid private context data!\n"); + return; + } + + av_log(ctx, AV_LOG_ERROR, + "[pipewiregrab] Error id:%u seq:%d res:%d (%s): %s\n", id, seq, res, + g_strerror(res), message); + + pw_thread_loop_signal(pw_ctx->thread_loop, false); + g_main_loop_quit(pw_ctx->glib_main_loop); +} + +/** + * Register pipewire core event callbacks here + */ +static const struct pw_core_events core_events = { + PW_VERSION_CORE_EVENTS, + .info = on_core_info_cb, + .done = on_core_done_callback, + .error = on_core_error_callback, +}; + +/** + * helper function: create a new path to dbus desktop object request method + * + * @param ctx AVFormatContext that contains PipeWire Grab main structure + * @param out_path dbus token count + * @param out_token final object/interface path for request method + */ +static void new_request_path(AVFormatContext *ctx, char **out_path, + char **out_token) +{ + static uint32_t request_token_count = 0; + PipewireGrabContext *pw_ctx = ctx->priv_data; + if (!pw_ctx) { + av_log(ctx, AV_LOG_ERROR, + "[pipewiregrab] Invalid private context data!\n"); + return; + } + + request_token_count++; + + if (out_token) { + *out_token = g_strdup_printf("obs%u", request_token_count); + } + + if (out_path) { + *out_path = g_strdup_printf(REQUEST_PATH, pw_ctx->sender_name, + request_token_count); + } +} + +/** + * helper function: create new path to dbus active desktop session + * + * @param ctx AVFormatContext that contains PipeWire Grab main structure + * @param out_path dbus session token count + * @param out_token final object/interface path for active session + */ +static void new_session_path(AVFormatContext *ctx, char **out_path, + char **out_token) +{ + static uint32_t session_token_count = 0; + PipewireGrabContext *pw_ctx = ctx->priv_data; + if (!pw_ctx) { + av_log(ctx, AV_LOG_ERROR, + "[pipewiregrab] Invalid private context data!\n"); + return; + } + + session_token_count++; + + if (out_token) { + *out_token = g_strdup_printf("obs%u", session_token_count); + } + + if (out_path) { + *out_path = g_strdup_printf(SESSION_PATH, pw_ctx->sender_name, + session_token_count); + } +} + +/** + * helper function: convert spa pixel format to av pixel format + * + * @param spa_format conatins spa format value + * @return AVPixelFormat + */ +static enum AVPixelFormat +spa_pixel_format_to_av_pixel_format(uint32_t spa_format) +{ + enum AVPixelFormat ret = AV_PIX_FMT_NONE; + switch (spa_format) { + case SPA_VIDEO_FORMAT_RGBA: + case SPA_VIDEO_FORMAT_RGBx: + ret = AV_PIX_FMT_RGBA; + break; + + case SPA_VIDEO_FORMAT_BGRA: + case SPA_VIDEO_FORMAT_BGRx: + ret = AV_PIX_FMT_BGRA; + break; + + default: + ret = AV_PIX_FMT_NONE; + } + + return ret; +} + +/** + * helper function: gracefully stop/destroy of pipewire objects + * + * @param ctx AVFormatContext that contains PipeWire Grab main structure + */ +static void teardown_pipewire(AVFormatContext *ctx) +{ + PipewireGrabContext *pw_ctx = ctx->priv_data; + if (!pw_ctx) { + av_log(ctx, AV_LOG_ERROR, + "[pipewiregrab] Invalid private context data!\n"); + return; + } + + if (pw_ctx->thread_loop) { + pw_thread_loop_unlock(pw_ctx->thread_loop); + pw_thread_loop_stop(pw_ctx->thread_loop); + } + + if (pw_ctx->stream) { + pw_stream_disconnect(pw_ctx->stream); + g_clear_pointer(&pw_ctx->stream, pw_stream_destroy); + } + + if (pw_ctx->core) + pw_core_disconnect(pw_ctx->core); + + if (pw_ctx->context) + pw_context_destroy(pw_ctx->context); + + if (pw_ctx->thread_loop) { + pw_thread_loop_destroy(pw_ctx->thread_loop); + } + + pw_ctx->negotiated = false; +} + +/** + * helper function: gracefully stop/destroy of dbus session + * + * @param ctx AVFormatContext that contains PipeWire Grab main structure + */ +static void destroy_session(AVFormatContext *ctx) +{ + PipewireGrabContext *pw_ctx = ctx->priv_data; + if (!pw_ctx) { + av_log(ctx, AV_LOG_ERROR, + "[pipewiregrab] Invalid private context data!\n"); + return; + } + + if (pw_ctx->session_handle) { + g_dbus_connection_call( + pw_ctx->connection, "org.freedesktop.portal.Desktop", + pw_ctx->session_handle, "org.freedesktop.portal.Session", "Close", + NULL, NULL, G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL); + + g_clear_pointer(&pw_ctx->session_handle, g_free); + } + + g_cancellable_cancel(pw_ctx->cancellable); + g_clear_object(&pw_ctx->cancellable); + g_clear_object(&pw_ctx->connection); + g_clear_object(&pw_ctx->proxy); + g_clear_pointer(&pw_ctx->sender_name, g_free); +} + +/** + * helper function: gracefully stop/destroy of pipewire/dbus session + * + * @param ctx AVFormatContext that contains PipeWire Grab main structure + */ +static void pipewire_destroy(AVFormatContext *ctx) +{ + teardown_pipewire(ctx); + destroy_session(ctx); +} + +/** + * callback to stop/disconnect current dbus session + * + * @param ptr_dbus_call_data dbus marshalling structure + */ +static void dbus_call_data_free(struct DbusCallData *ptr_dbus_call_data) +{ + AVFormatContext *ctx = NULL; + PipewireGrabContext *pw_ctx = NULL; + + if (!ptr_dbus_call_data) + return; + + ctx = ptr_dbus_call_data->ctx; + if (!ctx) + return; + + pw_ctx = ctx->priv_data; + if (!pw_ctx) { + av_log(ctx, AV_LOG_ERROR, + "[pipewiregrab] Invalid private context data!\n"); + return; + } + + if (ptr_dbus_call_data->signal_id) + g_dbus_connection_signal_unsubscribe(pw_ctx->connection, + ptr_dbus_call_data->signal_id); + + if (ptr_dbus_call_data->cancelled_id > 0) + g_signal_handler_disconnect(pw_ctx->cancellable, + ptr_dbus_call_data->cancelled_id); + + g_clear_pointer(&ptr_dbus_call_data->request_path, g_free); + g_free(ptr_dbus_call_data); +} + +/** + * dbus callback of cancelled events + * + * @param cancellable not used + * @param user_data dbus marshalling structure + */ +static void on_cancelled_callback(GCancellable *cancellable, gpointer user_data) +{ + struct DbusCallData *ptr_dbus_call_data = user_data; + AVFormatContext *ctx = ptr_dbus_call_data->ctx; + PipewireGrabContext *pw_ctx = ctx->priv_data; + + (void)cancellable; + + if (!pw_ctx) { + av_log(ctx, AV_LOG_ERROR, + "[pipewiregrab] Invalid private context data!\n"); + return; + } + + av_log(ctx, AV_LOG_INFO, "[pipewiregrab] Screencast session cancelled!\n"); + + g_dbus_connection_call(pw_ctx->connection, "org.freedesktop.portal.Desktop", + ptr_dbus_call_data->request_path, + "org.freedesktop.portal.Request", "Close", NULL, + NULL, G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL); +} + +/** + * pipewire callback of parameters changed events + * + * @param user_data dbus marshalling structure + * @param id contains chan param type + * @param param pointer to changed param structure + */ +static void on_stream_param_changed_callback(void *user_data, uint32_t id, + const struct spa_pod *param) +{ + struct spa_pod_builder pod_builder; + const struct spa_pod *params[MAX_SPA_PARAM]; + uint32_t n_params = 0; + uint8_t params_buffer[4096]; + int result; + + AVFormatContext *ctx = user_data; + PipewireGrabContext *pw_ctx = NULL; + if (!ctx) { + return; + } + + pw_ctx = ctx->priv_data; + if (!pw_ctx) { + av_log(ctx, AV_LOG_ERROR, + "[pipewiregrab] Invalid private context data!\n"); + return; + } + + if (!param || id != SPA_PARAM_Format) { + av_log(ctx, AV_LOG_WARNING, + "[pipewiregrab] Ignoring none stream param format change!\n"); + return; + } + + result = spa_format_parse(param, &pw_ctx->format.media_type, + &pw_ctx->format.media_subtype); + if (result < 0) + return; + + if (pw_ctx->format.media_type != SPA_MEDIA_TYPE_video || + pw_ctx->format.media_subtype != SPA_MEDIA_SUBTYPE_raw) + return; + + spa_format_video_raw_parse(param, &pw_ctx->format.info.raw); + + av_log(ctx, AV_LOG_INFO, "[pipewiregrab] Negotiated format:\n"); + + av_log(ctx, AV_LOG_INFO, "[pipewiregrab] Format: %d (%s)\n", + pw_ctx->format.info.raw.format, + spa_debug_type_find_name(spa_type_video_format, + pw_ctx->format.info.raw.format)); + av_log(ctx, AV_LOG_INFO, "[pipewiregrab] Size: %dx%d\n", + pw_ctx->format.info.raw.size.width, + pw_ctx->format.info.raw.size.height); + av_log(ctx, AV_LOG_INFO, "[pipewiregrab] Framerate: %d/%d\n", + pw_ctx->format.info.raw.framerate.num, + pw_ctx->format.info.raw.framerate.denom); + + pw_ctx->width = pw_ctx->format.info.raw.size.width; + pw_ctx->height = pw_ctx->format.info.raw.size.height; + pw_ctx->Bpp = BYTES_PER_PIXEL; + pw_ctx->frame_size = pw_ctx->width * pw_ctx->height * pw_ctx->Bpp; + pw_ctx->av_pxl_format = + spa_pixel_format_to_av_pixel_format(pw_ctx->format.info.raw.format); + + /* Video crop */ + pod_builder = SPA_POD_BUILDER_INIT(params_buffer, sizeof(params_buffer)); + params[n_params++] = spa_pod_builder_add_object( + &pod_builder, SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta, + SPA_PARAM_META_type, SPA_POD_Id(SPA_META_VideoCrop), + SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_region))); + + /* Cursor */ + params[n_params++] = spa_pod_builder_add_object( + &pod_builder, SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta, + SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Cursor), SPA_PARAM_META_size, + SPA_POD_CHOICE_RANGE_Int(CURSOR_META_SIZE(64, 64), + CURSOR_META_SIZE(1, 1), + CURSOR_META_SIZE(1024, 1024))); + + /* Buffer options */ + params[n_params++] = spa_pod_builder_add_object( + &pod_builder, SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers, + SPA_PARAM_BUFFERS_dataType, + SPA_POD_Int((1 << SPA_DATA_MemPtr) | (1 << SPA_DATA_MemFd))); + + /* Meta header */ + params[n_params++] = spa_pod_builder_add_object( + &pod_builder, SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta, + SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Header), + SPA_PARAM_META_size, + SPA_POD_Int(sizeof(struct spa_meta_header))); + + pw_stream_update_params(pw_ctx->stream, params, n_params); + + pw_ctx->negotiated = true; + + create_ffmpeg_stream(ctx); +} + +/** + * pipewire callback of state changed events + * + * @param user_data dbus marshalling structure + * @param old pipewire stream old state + * @param state pipewire stream current state + * @param error received error information + */ +static void on_stream_state_changed_callback(void *user_data, + enum pw_stream_state old, + enum pw_stream_state state, + const char *error) +{ + AVFormatContext *ctx = user_data; + + (void)old; + (void)error; + + if (!ctx) { + return; + } + + av_log(ctx, AV_LOG_INFO, "[pipewiregrab] stream state: \"%s\"\n", + pw_stream_state_as_string(state)); +} + +/** + * pipewire done callback + * + * @param user_data dbus marshalling structure + */ +static void on_stream_trigger_done_callback(void *user_data) +{ + (void)user_data; + /* nothing to do for now */ +} + +/** + * pipewire recycle buffer to put buffer back to queue and free + * allocated space after processing of frame + * + * @param opaque reference to PwStreamAndBuffer + * @param data spa_data for handling memeory mapping + */ +static void pw_recycle(void *opaque, uint8_t *data) +{ + enum pw_stream_state state; + struct PwStreamAndBuffer *buffer = (struct PwStreamAndBuffer *)opaque; + struct spa_buffer *spa_buf = buffer->pw_buf->buffer; + struct spa_data *d = &spa_buf->datas[0]; + + if (d->type == SPA_DATA_MemFd) { + munmap(buffer->pw_buf->user_data, d->maxsize + d->mapoffset); + } + + /* only put buffer in queue when stream state is streaming */ + state = pw_stream_get_state (buffer->pw_stream, NULL); + if (state == PW_STREAM_STATE_STREAMING) { + pw_stream_queue_buffer(buffer->pw_stream, buffer->pw_buf); + } + free(buffer); +} + +/** + * find most recent buffer received form pipewire + * + * @param pw_ctx PipewireGrabContext private data + * @return pw_buf most recent buffer + */ +static struct pw_buffer* find_most_recent_buffer_and_recycle_olders(PipewireGrabContext *pw_ctx) +{ + struct pw_buffer* pw_buf = NULL; + while (true) { + struct pw_buffer *aux = pw_stream_dequeue_buffer(pw_ctx->stream); + if (!aux) { + break; + } + if (pw_buf) { + pw_stream_queue_buffer(pw_ctx->stream, pw_buf); + } + pw_buf = aux; + } + return pw_buf; +} + +/** + * our data processing function + * + * @param user_data dbus marshalling structure + */ +static void on_stream_process_callback(void *user_data) +{ + struct spa_buffer *spa_buf = NULL; + struct pw_buffer *pw_buf = NULL; + uint8_t *map = NULL; + void *sdata = NULL; + struct spa_meta_header *header = NULL; + AVFormatContext *ctx = user_data; + PipewireGrabContext *pw_ctx = NULL; + AVPacket *pkt = NULL; + struct PwStreamAndBuffer* buffer = NULL; + + ctx = user_data; + if (!ctx) { + return; + } + + pw_ctx = ctx->priv_data; + if (!pw_ctx) { + av_log(ctx, AV_LOG_ERROR, + "[pipewiregrab] Invalid private context data!\n"); + return; + } + + pw_buf = find_most_recent_buffer_and_recycle_olders(pw_ctx); + if (pw_buf == NULL) { + av_log(ctx, AV_LOG_ERROR, "[pipewiregrab] Out of buffers!\n"); + return; + } + + spa_buf = pw_buf->buffer; + header = spa_buffer_find_meta_data(spa_buf, + SPA_META_Header, sizeof(*header)); + if (header && (header->flags & SPA_META_HEADER_FLAG_CORRUPTED)) { + av_log(ctx, AV_LOG_ERROR, "[pipewiregrab] buffer is corrupt"); + pw_stream_queue_buffer(pw_ctx->stream, pw_buf); + return; + } + + if (pw_ctx->av_pxl_format == AV_PIX_FMT_NONE) { + av_log(ctx, AV_LOG_WARNING, + "[pipewiregrab] unsupported buffer format: %d\n", + pw_ctx->format.info.raw.format); + return; + } + + if (spa_buf->datas[0].type == SPA_DATA_MemFd ) { + map = + mmap(NULL, spa_buf->datas[0].maxsize + spa_buf->datas[0].mapoffset, + PROT_READ, MAP_PRIVATE, spa_buf->datas[0].fd, 0); + if (map == MAP_FAILED) { + av_log(ctx, AV_LOG_ERROR, "[pipewiregrab] mmap failed! error %s\n", + g_strerror(errno)); + return; + } + pw_buf->user_data = map; + sdata = SPA_PTROFF(map, spa_buf->datas[0].mapoffset, uint8_t); + } else if (spa_buf->datas[0].type == SPA_DATA_MemPtr) { + if (spa_buf->datas[0].data == NULL) { + av_log(ctx, AV_LOG_ERROR, "[pipewiregrab] no data!\n"); + return; + } + map = NULL; + sdata = spa_buf->datas[0].data; + } else { + av_log(ctx, AV_LOG_ERROR, "[pipewiregrab] Buffer is not valid!\n"); + return; + } + + /* Create a new AVPacket on top of the pw buffer */ + pkt = av_packet_alloc(); + if (pkt == NULL) { + av_log(ctx, AV_LOG_ERROR, + "[pipewiregrab] Failed to allocate new av packet\n"); + return; + } + + buffer = (struct PwStreamAndBuffer*)malloc(sizeof(struct PwStreamAndBuffer)); + if (!buffer) { + av_log(ctx, AV_LOG_ERROR, + "[pipewiregrab] Failed to allocate PwStreamAndBuffer entry\n"); + av_packet_free(&pkt); + return; + } + buffer->ctx = ctx; + buffer->pw_stream = pw_ctx->stream; + buffer->pw_buf = pw_buf; + + pkt->buf = av_buffer_create(sdata, pw_ctx->frame_size, pw_recycle, buffer, 0); + pkt->data = sdata; + pkt->size = pw_ctx->frame_size; + + /* Free the last current_pkt and make the new frame available */ + pthread_mutex_lock(&pw_ctx->current_pkt_mutex); + av_packet_unref(pw_ctx->current_pkt); + av_packet_ref(pw_ctx->current_pkt, pkt); + pthread_mutex_unlock(&pw_ctx->current_pkt_mutex); + + av_packet_free(&pkt); + return; +} + +static const struct pw_stream_events stream_events = { + PW_VERSION_STREAM_EVENTS, + .state_changed = on_stream_state_changed_callback, + .param_changed = on_stream_param_changed_callback, + .process = on_stream_process_callback, + .trigger_done = on_stream_trigger_done_callback, +}; + +static struct DbusCallData *subscribe_to_signal(AVFormatContext *ctx, + const char *path, + GDBusSignalCallback callback) +{ + struct DbusCallData *ptr_dbus_call_data; + PipewireGrabContext *pw_ctx = ctx->priv_data; + if (!pw_ctx) { + av_log(ctx, AV_LOG_ERROR, + "[pipewiregrab] Invalid private context data!\n"); + return NULL; + } + + ptr_dbus_call_data = g_new0(struct DbusCallData, 1); + ptr_dbus_call_data->ctx = ctx; + ptr_dbus_call_data->request_path = g_strdup(path); + ptr_dbus_call_data->cancelled_id = + g_signal_connect(pw_ctx->cancellable, "cancelled", + G_CALLBACK(on_cancelled_callback), + ptr_dbus_call_data /* user_data */); + ptr_dbus_call_data->signal_id = g_dbus_connection_signal_subscribe( + pw_ctx->connection, "org.freedesktop.portal.Desktop" /*sender*/, + "org.freedesktop.portal.Request" /*interface_name*/, + "Response" /*member: dbus signal name*/, + ptr_dbus_call_data->request_path /*object_path*/, NULL, + G_DBUS_SIGNAL_FLAGS_NO_MATCH_RULE, callback, ptr_dbus_call_data, NULL); + + return ptr_dbus_call_data; +} + +static int play_pipewire_stream(AVFormatContext *ctx) +{ + int ret = 0; + const struct spa_pod *ptr_spa_pod[1]; + uint8_t buffer[4096]; + struct spa_pod_builder spa_pod_bldr = { + 0, + }; + + PipewireGrabContext *pw_ctx = ctx->priv_data; + if (!pw_ctx) { + av_log(ctx, AV_LOG_ERROR, + "[pipewiregrab] Invalid private context data!\n"); + return ret; + } + + pw_ctx->thread_loop = + pw_thread_loop_new("[pipewiregrab] thread loop", NULL); + if (pw_ctx->thread_loop == NULL) { + av_log(ctx, AV_LOG_ERROR, + "[pipewiregrab] pw_thread_loop_new failed!\n"); + ret = AVERROR(ENOMEM); + return ret; + } + + pw_ctx->context = + pw_context_new(pw_thread_loop_get_loop(pw_ctx->thread_loop), NULL, 0); + if (pw_ctx->context == NULL) { + av_log(ctx, AV_LOG_ERROR, "[pipewiregrab] pw_context_new failed!\n"); + ret = AVERROR(ENOMEM); + goto thread_loop_destroy; + } + + if (pw_thread_loop_start(pw_ctx->thread_loop) < 0) { + av_log(ctx, AV_LOG_ERROR, + "[pipewiregrab] pw_thread_loop_start failed!\n"); + ret = AVERROR(EFAULT); + goto context_destroy; + } + + pw_thread_loop_lock(pw_ctx->thread_loop); + + /* Core */ + pw_ctx->core = + pw_context_connect_fd(pw_ctx->context, + fcntl(pw_ctx->pipewire_fd, F_DUPFD_CLOEXEC, 3), + NULL, 0); + if (pw_ctx->core == NULL) { + av_log(ctx, AV_LOG_ERROR, + "[pipewiregrab] pw_context_connect_fd failed!\n"); + ret = AVERROR(EFAULT); + pw_thread_loop_unlock(pw_ctx->thread_loop); + goto context_destroy; + } + + pw_core_add_listener(pw_ctx->core, &pw_ctx->core_listener, &core_events, + ctx /* user_data */); + + /* Stream */ + pw_ctx->stream = pw_stream_new( + pw_ctx->core, "wayland grab", + pw_properties_new(PW_KEY_MEDIA_TYPE, "Video", PW_KEY_MEDIA_CATEGORY, + "Capture", PW_KEY_MEDIA_ROLE, "Screen", NULL)); + + if (pw_ctx->stream == NULL) { + av_log(ctx, AV_LOG_ERROR, "[pipewiregrab] pw_stream_new failed!\n"); + ret = AVERROR(ENOMEM); + pw_thread_loop_unlock(pw_ctx->thread_loop); + goto core_disconnect; + } + + pw_stream_add_listener(pw_ctx->stream, &pw_ctx->stream_listener, + &stream_events, ctx /* user_data */); + + /* Stream parameters */ + spa_pod_bldr = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer)); + ptr_spa_pod[0] = spa_pod_builder_add_object( + &spa_pod_bldr, 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(4, SPA_VIDEO_FORMAT_RGBA, SPA_VIDEO_FORMAT_RGBx, + SPA_VIDEO_FORMAT_BGRx, SPA_VIDEO_FORMAT_BGRA), + SPA_FORMAT_VIDEO_size, + SPA_POD_CHOICE_RANGE_Rectangle(&SPA_RECTANGLE(320, 240), + &SPA_RECTANGLE(1, 1), + &SPA_RECTANGLE(4096, 4096)), + SPA_FORMAT_VIDEO_framerate, + SPA_POD_CHOICE_RANGE_Fraction( + &SPA_FRACTION(pw_ctx->user_frame_rate.num, + pw_ctx->user_frame_rate.den), + &SPA_FRACTION(0, 1), &SPA_FRACTION(144, 1))); + + ret = pw_stream_connect( + pw_ctx->stream, PW_DIRECTION_INPUT, pw_ctx->pipewire_node, + PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS, ptr_spa_pod, + 1); + + av_log(ctx, AV_LOG_INFO, "[pipewiregrab] Starting screen capture ...\n"); + + pw_thread_loop_unlock(pw_ctx->thread_loop); + + return ret; + +/* + * disconnect from the server/daemon + * destroy the core proxy object and will remove the proxies + * that might have been created on this connection + */ +core_disconnect: + pw_core_disconnect(pw_ctx->core); + +context_destroy: + pw_context_destroy(pw_ctx->context); + +thread_loop_destroy: + pw_thread_loop_destroy(pw_ctx->thread_loop); + + return ret; +} + +static void on_pipewire_remote_opened_callback(GObject *source, + GAsyncResult *res, + gpointer user_data) +{ + g_autoptr(GUnixFDList) fd_list = NULL; + g_autoptr(GVariant) result = NULL; + g_autoptr(GError) error = NULL; + int fd_index; + + AVFormatContext *ctx = user_data; + PipewireGrabContext *pw_ctx = ctx->priv_data; + if (!pw_ctx) { + av_log(ctx, AV_LOG_ERROR, + "[pipewiregrab] Invalid private context data!\n"); + return; + } + + result = g_dbus_proxy_call_with_unix_fd_list_finish(G_DBUS_PROXY(source), + &fd_list, res, &error); + if (error) { + if (!g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + av_log(ctx, AV_LOG_ERROR, + "[pipewiregrab] Error retrieving pipewire fd: %s\n", + error->message); + + return; + } + + g_variant_get(result, "(h)", &fd_index, &error); + + pw_ctx->pipewire_fd = g_unix_fd_list_get(fd_list, fd_index, &error); + if (error) { + if (!g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + av_log(ctx, AV_LOG_ERROR, + "[pipewiregrab] Error retrieving pipewire fd: %s\n", + error->message); + + return; + } + + play_pipewire_stream(ctx); +} + +static void open_pipewire_remote(AVFormatContext *ctx) +{ + GVariantBuilder builder; + PipewireGrabContext *pw_ctx = ctx->priv_data; + if (!pw_ctx) { + av_log(ctx, AV_LOG_ERROR, + "[pipewiregrab] Invalid private context data!\n"); + return; + } + + g_variant_builder_init(&builder, G_VARIANT_TYPE_VARDICT); + + g_dbus_proxy_call_with_unix_fd_list( + pw_ctx->proxy, "OpenPipeWireRemote", + g_variant_new("(oa{sv})", pw_ctx->session_handle, &builder), + G_DBUS_CALL_FLAGS_NONE, -1, NULL, pw_ctx->cancellable, + on_pipewire_remote_opened_callback, ctx /* user_data */); +} + +static void on_start_response_received_callback( + GDBusConnection *connection, const char *sender_name, + const char *object_path, const char *interface_name, + const char *signal_name, GVariant *parameters, gpointer user_data) +{ + g_autoptr(GVariant) stream_properties = NULL; + g_autoptr(GVariant) streams = NULL; + g_autoptr(GVariant) result = NULL; + GVariantIter iter; + uint32_t response; + + struct DbusCallData *ptr_dbus_call_data = user_data; + AVFormatContext *ctx = ptr_dbus_call_data->ctx; + PipewireGrabContext *pw_ctx = ctx->priv_data; + + (void)connection; + (void)sender_name; + (void)object_path; + (void)interface_name; + (void)signal_name; + + if (!pw_ctx) { + av_log(ctx, AV_LOG_ERROR, + "[pipewiregrab] Invalid private context data!\n"); + return; + } + + g_clear_pointer(&ptr_dbus_call_data, dbus_call_data_free); + + g_variant_get(parameters, "(u@a{sv})", &response, &result); + + if (response != 0) { + av_log( + ctx, AV_LOG_ERROR, + "[pipewiregrab] Failed to start screencast, denied or cancelled by user!\n"); + pipewiregrab_abort( + ctx, "Failed to start screencast, denied or cancelled by user!"); + return; + } + + streams = g_variant_lookup_value(result, "streams", G_VARIANT_TYPE_ARRAY); + + g_variant_iter_init(&iter, streams); + g_assert(g_variant_iter_n_children(&iter) == 1); + + g_variant_iter_loop(&iter, "(u@a{sv})", &pw_ctx->pipewire_node, + &stream_properties); + + av_log(ctx, AV_LOG_INFO, + "[pipewiregrab] Monitor selected, setting up screencast\n\n"); + + open_pipewire_remote(ctx); +} + +static void on_started_callback(GObject *source, GAsyncResult *res, + gpointer user_data) +{ + g_autoptr(GVariant) result = NULL; + g_autoptr(GError) error = NULL; + struct DbusCallData *ptr_dbus_call_data = user_data; + AVFormatContext *ctx = ptr_dbus_call_data->ctx; + (void)user_data; + + result = g_dbus_proxy_call_finish(G_DBUS_PROXY(source), res, &error); + if (error) { + if (!g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + av_log(ctx, AV_LOG_ERROR, + "[pipewiregrab] Error selecting screencast source: %s\n", + error->message); + + return; + } +} + +static void start(AVFormatContext *ctx) +{ + g_autofree char *request_token = NULL; + g_autofree char *request_path = NULL; + GVariantBuilder builder; + struct DbusCallData *ptr_dbus_call_data; + PipewireGrabContext *pw_ctx = ctx->priv_data; + if (!pw_ctx) { + av_log(ctx, AV_LOG_ERROR, + "[pipewiregrab] Invalid private context data!\n"); + return; + } + + new_request_path(ctx, &request_path, &request_token); + + av_log(ctx, AV_LOG_WARNING, "[pipewiregrab] Asking for monitor…\n"); + + ptr_dbus_call_data = subscribe_to_signal( + ctx /* user_data */, request_path, on_start_response_received_callback); + if (ptr_dbus_call_data == NULL) { + av_log(ctx, AV_LOG_ERROR, + "[pipewiregrab] subscribe_to_signal failed!\n"); + return; + } + + g_variant_builder_init(&builder, G_VARIANT_TYPE_VARDICT); + g_variant_builder_add(&builder, "{sv}", "handle_token", + g_variant_new_string(request_token)); + + g_dbus_proxy_call(pw_ctx->proxy, "Start", + g_variant_new("(osa{sv})", pw_ctx->session_handle, "", + &builder), + G_DBUS_CALL_FLAGS_NONE, -1, pw_ctx->cancellable, + on_started_callback, ptr_dbus_call_data /* user_data */); +} + +static void on_select_source_response_received_callback( + GDBusConnection *connection, const char *sender_name, + const char *object_path, const char *interface_name, + const char *signal_name, GVariant *parameters, gpointer user_data) +{ + g_autoptr(GVariant) ret = NULL; + uint32_t response; + struct DbusCallData *ptr_dbus_call_data = user_data; + AVFormatContext *ctx = ptr_dbus_call_data->ctx; + (void)connection; + (void)sender_name; + (void)object_path; + (void)interface_name; + (void)signal_name; + + av_log(ctx, AV_LOG_INFO, + "[pipewiregrab] Response to select source received\n"); + + g_clear_pointer(&ptr_dbus_call_data, dbus_call_data_free); + + g_variant_get(parameters, "(u@a{sv})", &response, &ret); + + if (response != 0) { + av_log( + ctx, AV_LOG_ERROR, + "[pipewiregrab] Failed to select source, denied or cancelled by user!\n"); + pipewiregrab_abort( + ctx, "Failed to select source, denied or cancelled by user!"); + return; + } + + start(ctx); +} + +static void on_source_selected_callback(GObject *source, GAsyncResult *res, + gpointer user_data) +{ + g_autoptr(GVariant) result = NULL; + g_autoptr(GError) error = NULL; + + struct DbusCallData *ptr_dbus_call_data = user_data; + AVFormatContext *ctx = ptr_dbus_call_data->ctx; + + result = g_dbus_proxy_call_finish(G_DBUS_PROXY(source), res, &error); + if (error) { + if (!g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + av_log(ctx, AV_LOG_ERROR, + "[pipewiregrab] Error selecting screencast source: %s\n", + error->message); + + return; + } +} + +static void select_source(AVFormatContext *ctx) +{ + g_autofree char *request_token = NULL; + g_autofree char *request_path = NULL; + GVariantBuilder builder; + struct DbusCallData *ptr_dbus_call_data; + PipewireGrabContext *pw_ctx = ctx->priv_data; + if (!pw_ctx) { + av_log(ctx, AV_LOG_ERROR, + "[pipewiregrab] Invalid private context data!\n"); + return; + } + + new_request_path(ctx, &request_path, &request_token); + + ptr_dbus_call_data = + subscribe_to_signal(ctx /* user_data */, request_path, + on_select_source_response_received_callback); + if (ptr_dbus_call_data == NULL) { + av_log(ctx, AV_LOG_ERROR, + "[pipewiregrab] subscribe_to_signal failed!\n"); + return; + } + + g_variant_builder_init(&builder, G_VARIANT_TYPE_VARDICT); + g_variant_builder_add(&builder, "{sv}", "types", + g_variant_new_uint32(pw_ctx->capture_type)); + g_variant_builder_add(&builder, "{sv}", "multiple", + g_variant_new_boolean(FALSE)); + g_variant_builder_add(&builder, "{sv}", "handle_token", + g_variant_new_string(request_token)); + + if (pw_ctx->available_cursor_modes & PORTAL_CURSOR_MODE_METADATA) + g_variant_builder_add(&builder, "{sv}", "cursor_mode", + g_variant_new_uint32(PORTAL_CURSOR_MODE_METADATA)); + else if ((pw_ctx->available_cursor_modes & PORTAL_CURSOR_MODE_EMBEDDED) + && pw_ctx->cursor.visible) + g_variant_builder_add(&builder, "{sv}", "cursor_mode", + g_variant_new_uint32(PORTAL_CURSOR_MODE_EMBEDDED)); + else + g_variant_builder_add(&builder, "{sv}", "cursor_mode", + g_variant_new_uint32(PORTAL_CURSOR_MODE_HIDDEN)); + + g_dbus_proxy_call(pw_ctx->proxy, "SelectSources", + g_variant_new("(oa{sv})", pw_ctx->session_handle, + &builder), + G_DBUS_CALL_FLAGS_NONE, -1, pw_ctx->cancellable, + (GAsyncReadyCallback)on_source_selected_callback, + ptr_dbus_call_data /* user_data */); +} + +static void on_create_session_response_received_callback( + GDBusConnection *connection, const char *sender_name, + const char *object_path, const char *interface_name, + const char *signal_name, GVariant *parameters, gpointer user_data) +{ + uint32_t response; + g_autoptr(GVariant) result = NULL; + struct DbusCallData *ptr_dbus_call_data = user_data; + AVFormatContext *ctx = ptr_dbus_call_data->ctx; + PipewireGrabContext *pw_ctx = ctx->priv_data; + + (void)connection; + (void)sender_name; + (void)object_path; + (void)interface_name; + (void)signal_name; + + if (!pw_ctx) { + av_log(ctx, AV_LOG_ERROR, + "[pipewiregrab] Invalid private context data!\n"); + return; + } + + g_clear_pointer(&ptr_dbus_call_data, dbus_call_data_free); + + g_variant_get(parameters, "(u@a{sv})", &response, &result); + + if (response != 0) { + av_log( + ctx, AV_LOG_ERROR, + "[pipewiregrab] Pipewire failed to create session, denied or cancelled by user!\n"); + return; + } + + av_log(ctx, AV_LOG_DEBUG, "[pipewiregrab] Screencast session created\n"); + + g_variant_lookup(result, "session_handle", "s", &pw_ctx->session_handle); + + /* enable/disable mouse cursor */ + pw_ctx->cursor.visible = pw_ctx->draw_mouse; + select_source(ctx); +} + +static void on_session_created_callback(GObject *source, GAsyncResult *res, + gpointer user_data) +{ + g_autoptr(GVariant) result = NULL; + g_autoptr(GError) error = NULL; + struct DbusCallData *ptr_dbus_call_data = user_data; + AVFormatContext *ctx = ptr_dbus_call_data->ctx; + + result = g_dbus_proxy_call_finish(G_DBUS_PROXY(source), res, &error); + if (error) { + if (!g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + av_log(ctx, AV_LOG_ERROR, + "[pipewiregrab] Error creating screencast session: %s\n", + error->message); + + return; + } +} + +/** + * function to start the negotation + * + * @param ctx ctx AVFormatContext avformat context + */ +static void create_session(AVFormatContext *ctx) +{ + GVariantBuilder builder; + g_autofree char *request_token = NULL; + g_autofree char *request_path = NULL; + g_autofree char *session_token = NULL; + struct DbusCallData *ptr_dbus_call_data; + + PipewireGrabContext *pw_ctx = ctx->priv_data; + if (!pw_ctx) { + av_log(ctx, AV_LOG_ERROR, + "[pipewiregrab] Invalid private context data!\n"); + return; + } + + new_request_path(ctx, &request_path, &request_token); + new_session_path(ctx, NULL, &session_token); + + ptr_dbus_call_data = + subscribe_to_signal(ctx /* user_data */, request_path, + on_create_session_response_received_callback); + if (ptr_dbus_call_data == NULL) { + av_log(ctx, AV_LOG_ERROR, + "[pipewiregrab] subscribe_to_signal failed!\n"); + return; + } + + g_variant_builder_init(&builder, G_VARIANT_TYPE_VARDICT); + g_variant_builder_add(&builder, "{sv}", "handle_token", + g_variant_new_string(request_token)); + g_variant_builder_add(&builder, "{sv}", "session_handle_token", + g_variant_new_string(session_token)); + + g_dbus_proxy_call(pw_ctx->proxy, "CreateSession", + g_variant_new("(a{sv})", &builder), + G_DBUS_CALL_FLAGS_NONE, -1, pw_ctx->cancellable, + on_session_created_callback, + ptr_dbus_call_data /* user_data */); +} + +/** + * helper function: get available cursor mode and update + * + * @param ctx ctx AVFormatContext avformat context + */ +static void update_available_cursor_modes(AVFormatContext *ctx) +{ + g_autoptr(GVariant) cached_cursor_modes = NULL; + uint32_t available_cursor_modes; + + PipewireGrabContext *pw_ctx = ctx->priv_data; + if (!pw_ctx) { + av_log(ctx, AV_LOG_ERROR, + "[pipewiregrab] Invalid private context data!\n"); + return; + } + + cached_cursor_modes = + g_dbus_proxy_get_cached_property(pw_ctx->proxy, "AvailableCursorModes"); + available_cursor_modes = + cached_cursor_modes ? g_variant_get_uint32(cached_cursor_modes) : 0; + + /* Use embedded or hidden mode for now*/ + available_cursor_modes = available_cursor_modes & + (PORTAL_CURSOR_MODE_EMBEDDED | + PORTAL_CURSOR_MODE_HIDDEN); + pw_ctx->available_cursor_modes = available_cursor_modes; +} + +/** + * callback function of created proxy + * + * @param source GObject + * @param res GAsyncResult + * @param user_data ctx AVFormatContext avformat context + */ +static void on_pipewire_proxy_created_callback(GObject *source, + GAsyncResult *res, + gpointer user_data) +{ + g_autoptr(GError) error = NULL; + AVFormatContext *ctx = user_data; + PipewireGrabContext *pw_ctx = ctx->priv_data; + + (void)source; + + if (!pw_ctx) { + av_log(ctx, AV_LOG_ERROR, + "[pipewiregrab] Invalid private context data!\n"); + return; + } + + pw_ctx->proxy = g_dbus_proxy_new_finish(res, &error); + + if (error) { + if (!g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + av_log(ctx, AV_LOG_ERROR, + "[pipewiregrab] Error creating proxy: %s\n", error->message); + + return; + } + + update_available_cursor_modes(ctx); + create_session(ctx); +} + +/** + * Get the proxy from desktop-portal + * + * @param ctx AVFormatContext avformat context + */ +static void create_pipewire_proxy(AVFormatContext *ctx) +{ + PipewireGrabContext *pw_ctx = ctx->priv_data; + if (!pw_ctx) { + av_log(ctx, AV_LOG_ERROR, + "[pipewiregrab] Invalid private context data!\n"); + return; + } + + g_dbus_proxy_new(pw_ctx->connection, G_DBUS_PROXY_FLAGS_NONE, NULL, + "org.freedesktop.portal.Desktop", + "/org/freedesktop/portal/desktop", + "org.freedesktop.portal.ScreenCast", pw_ctx->cancellable, + (GAsyncReadyCallback)on_pipewire_proxy_created_callback, + ctx /* user_data */); +} + +/** + * Initiate the pipewiregrab + * + * @param ctx AVFormatContext avformat context + */ +static int init_pipewiregrab(AVFormatContext *ctx) +{ + char *aux; + g_autoptr(GError) error = NULL; + PipewireGrabContext *pw_ctx = ctx->priv_data; + if (!pw_ctx) { + av_log(ctx, AV_LOG_ERROR, + "[pipewiregrab] Invalid private context data!\n"); + return AVERROR(EINVAL); + } + + pw_ctx->capture_type = DESKTOP_CAPTURE; + pw_ctx->cancellable = g_cancellable_new(); + + pw_ctx->connection = g_bus_get_sync(G_BUS_TYPE_SESSION, NULL, &error); + if (error) { + av_log(ctx, AV_LOG_ERROR, + "[pipewiregrab] Error getting session bus: %s\n", + error->message); + return error->code; + } + + pw_ctx->sender_name = + g_strdup(g_dbus_connection_get_unique_name(pw_ctx->connection) + 1); + + /* Replace dots by underscores */ + while ((aux = g_strstr_len(pw_ctx->sender_name, -1, ".")) != NULL) + *aux = '_'; + + av_log(ctx, AV_LOG_DEBUG, "[pipewiregrab] Initialized (sender name: %s)\n", + pw_ctx->sender_name); + + create_pipewire_proxy(ctx); + + return 0; +} + +static const AVClass pipewiregrab = { + .class_name = "pipewiregrab indev", + .item_name = av_default_item_name, + .option = options, + .version = LIBAVUTIL_VERSION_INT, + .category = AV_CLASS_CATEGORY_DEVICE_VIDEO_INPUT, +}; + +/** + * helper function: calculate the wait time based + * on the frame duration + * + * @param s AVFormatContext avformat context + * @return current time + */ +static int64_t wait_frame(AVFormatContext *s) +{ + PipewireGrabContext *c = s->priv_data; + int64_t curtime, delay; + + c->time_frame += c->frame_duration; + + for (;;) { + curtime = av_gettime_relative(); + delay = c->time_frame - curtime; + if (delay <= 0) + break; + av_usleep(delay); + } + + return curtime; +} + +/** + * helper function: Grabs a frame from pipewire + * + * @param ctx AVFormatContext avformat context + * @param pkt Packet holding the grabbed frame + */ +static void grab_frame(AVFormatContext *ctx, AVPacket *pkt) +{ + PipewireGrabContext *pw_ctx = ctx->priv_data; + + pthread_mutex_lock(&pw_ctx->current_pkt_mutex); + av_packet_ref(pkt, pw_ctx->current_pkt); + pthread_mutex_unlock(&pw_ctx->current_pkt_mutex); +} + +/** + * read the frame from pipewire + * + * @param ctx Context from avformat core + * @param pkt Packet holding the grabbed frame + * @return frame size in bytes + */ +static int pipewiregrab_read_packet(AVFormatContext *ctx, AVPacket *pkt) +{ + int64_t pts; + PipewireGrabContext *pw_ctx = ctx->priv_data; + if (!pw_ctx) { + av_log(ctx, AV_LOG_ERROR, + "[pipewiregrab] Invalid private context data!\n"); + return AVERROR(EINVAL); + } + do { + wait_frame(ctx); + pts = av_gettime(); + grab_frame(ctx, pkt); + pkt->dts = pkt->pts = pts; + pkt->duration = pw_ctx->frame_duration; + } while(!pkt->data); + + return pkt->size; +} + +/** + * Closes pipewire frame grabber. + * + * @param ctx Context from avformat core + * @return 0 success, !0 failure + */ +static int pipewiregrab_read_close(AVFormatContext *ctx) +{ + PipewireGrabContext *pw_ctx = ctx->priv_data; + if (!pw_ctx) { + av_log(ctx, AV_LOG_ERROR, + "[pipewiregrab] Invalid private context data!\n"); + return AVERROR(EINVAL); + } + + if (pw_ctx->thread_loop != NULL) + pw_thread_loop_signal(pw_ctx->thread_loop, false); + + if (pw_ctx->glib_main_loop != NULL && + g_main_loop_is_running(pw_ctx->glib_main_loop)) { + g_main_loop_quit(pw_ctx->glib_main_loop); + } + + pthread_join(pw_ctx->pipewire_pthread, NULL); + + if (pw_ctx->current_pkt) { + av_packet_free(&pw_ctx->current_pkt); + } + + return 0; +} + +/** + * Thread to initialize the Pipewire library and + * initiate the connetion flow + * + * @param argo thread argument ctx + */ +static void *pipewire_gdbus_init_pthread(void *argo) +{ + intptr_t ret = 0; + AVFormatContext *ctx = (AVFormatContext *)argo; + PipewireGrabContext *pw_ctx = ctx->priv_data; + if (!pw_ctx) { + av_log(ctx, AV_LOG_ERROR, + "[pipewiregrab] Invalid private context data!\n"); + return (void *)AVERROR(EINVAL); + } + + /* initialize the PipeWire library */ + pw_init(NULL, NULL); + + pw_ctx->glib_main_loop = g_main_loop_new(NULL, FALSE); + if (pw_ctx->glib_main_loop == NULL) { + av_log(ctx, AV_LOG_ERROR, "[pipewiregrab] g_main_loop_new failed!\n"); + ret = AVERROR(ENOMEM); + goto exit_pw_deinit; + } + + if (init_pipewiregrab(ctx) != 0) { + av_log(ctx, AV_LOG_ERROR, "[pipewiregrab] init_pipewiregrab failed!\n"); + ret = AVERROR(EPERM); + goto exit_glib_loop; + } + + av_log(ctx, AV_LOG_INFO, "[pipewiregrab] starting glib main loop!\n"); + g_main_loop_run(pw_ctx->glib_main_loop); + + /* g_main_loop quited, destroy pipewire*/ + pipewire_destroy(ctx); + +exit_glib_loop: + g_main_loop_unref(pw_ctx->glib_main_loop); + pw_ctx->glib_main_loop = NULL; +exit_pw_deinit: + pw_deinit(); + + return (void *)ret; +} + +/** + * Initializes the pipewire grab device demuxer + * + * @param ctx Context from avformat core + * @return AVERROR_IO error, 0 success + */ +static int pipewiregrab_read_header(AVFormatContext *ctx) +{ + int ret = 0; + PipewireGrabContext *pw_ctx = ctx->priv_data; + + if (!pw_ctx) { + av_log(ctx, AV_LOG_ERROR, + "[pipewiregrab] Invalid private context data!\n"); + return AVERROR(EINVAL); + } + + atomic_init(&pw_ctx->avstream_codec_flag, 0); + + pthread_cond_init(&pw_ctx->avstream_codec_cond_var, NULL); + pthread_mutex_init(&pw_ctx->avstream_codec_mutex, NULL); + pthread_mutex_init(&pw_ctx->current_pkt_mutex, NULL); + + pw_ctx->current_pkt = av_packet_alloc(); + if (pw_ctx->current_pkt == NULL) { + av_log(ctx, AV_LOG_ERROR, + "[pipewiregrab] Failed to allocate new av packet\n"); + return -1; + } + + ret = av_parse_video_rate(&pw_ctx->user_frame_rate, pw_ctx->framerate); + if (ret < 0) { + av_log(ctx, AV_LOG_ERROR, + "[pipewiregrab] av_parse_video_rate failed!\n"); + return ret; + } + + ret = pthread_create(&pw_ctx->pipewire_pthread, NULL, + &pipewire_gdbus_init_pthread, ctx); + pthread_mutex_lock(&pw_ctx->avstream_codec_mutex); + while (atomic_load(&pw_ctx->avstream_codec_flag) == 0) { + pthread_cond_wait(&pw_ctx->avstream_codec_cond_var, + &pw_ctx->avstream_codec_mutex); + } + /* wait until signaled */ + pthread_mutex_unlock(&pw_ctx->avstream_codec_mutex); + + return ret; +} + +/** pipewire grabber device demuxer declaration */ +const AVInputFormat ff_pipewiregrab_demuxer = { + .name = "pipewiregrab", + .long_name = NULL_IF_CONFIG_SMALL("screen capture, using pipewire"), + .priv_data_size = sizeof(struct PipewireGrabContext), + .read_header = pipewiregrab_read_header, + .read_packet = pipewiregrab_read_packet, + .read_close = pipewiregrab_read_close, + .flags = AVFMT_NOFILE, + .priv_class = &pipewiregrab, +};