diff mbox series

[FFmpeg-devel,RFC,1/2] libavdevice/pipewiregrab: add pipewire based grab

Message ID 20230920184058.1263793-1-abhishek.ojha@savoirfairelinux.com
State New
Headers show
Series [FFmpeg-devel,RFC,1/2] libavdevice/pipewiregrab: add pipewire based grab | expand

Checks

Context Check Description
andriy/make_x86 success Make finished
andriy/make_fate_x86 success Make fate finished

Commit Message

Abhishek Ojha Sept. 20, 2023, 6:40 p.m. UTC
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 <abhishek.ojha@savoirfairelinux.com>
---
 configure                  |    9 +
 libavdevice/Makefile       |    1 +
 libavdevice/alldevices.c   |    1 +
 libavdevice/pipewiregrab.c | 1836 ++++++++++++++++++++++++++++++++++++
 4 files changed, 1847 insertions(+)
 create mode 100644 libavdevice/pipewiregrab.c

Comments

Leo Izen Sept. 20, 2023, 10:25 p.m. UTC | #1
On 9/20/23 14:40, Abhishek Ojha wrote:
> 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 <abhishek.ojha@savoirfairelinux.com>
> ---
>   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 <abhishek.ojha@savoirfairelinux.com>
> + */
> +
> +#include "config.h"
> +
> +#include <fcntl.h>
> +#include <linux/dma-buf.h>
> +#include <math.h>
> +#include <pthread.h>
> +#include <stdatomic.h>
> +#include <stdlib.h>
> +#include <string.h>
> +#include <sys/mman.h>
> +#include <sys/queue.h>
> +
> +#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 <pipewire/pipewire.h>
> +#include <pipewire/thread-loop.h>
> +#include <spa/debug/types.h>
> +#include <spa/param/video/format-utils.h>
> +#include <spa/param/video/type-info.h>
> +
> +#include <gio/gio.h>
> +#include <gio/gunixfdlist.h>
> +
> +#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;
bool should be int.

> +        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;
size_t?

> +    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,
AV_OPT_TYPE_VIDEO_RATE points to an AVRational

> +      { .str = "ntsc" },
> +      0,
> +      0,
> +      D },
> +    { "draw_mouse",
> +      "Draw the mouse pointer.",
> +      OFFSET(draw_mouse),
> +      AV_OPT_TYPE_INT,
AV_OPT_TYPE_BOOL

> +      { .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)
Convention is to return int >= 0 on success and an int < 0 on failure. I 
don't think we use the bool type in FFmpeg except when external 
libraries require it.

> +{
> +    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 pw_ctw can be NULL here (which I don't think it can be, tbh), then 
you leak the avstream you just created.


> +    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;
avstream->avg_frame_rate = pw_ctx->user_frame_rate;

> +
> +    avpriv_set_pts_info(avstream, 64, 1, 1000000);
What's the point of storing the timebase if you don't use it?

> +
> +    pw_ctx->time_base = (AVRational){ avstream->avg_frame_rate.den,
> +                                      avstream->avg_frame_rate.num };

This won't compile with -pedantic. Use av_make_q instead.

> +    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;
If ret can't be nonzero here, just put return 0 at the bottom and ditch 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;
Unnecessary initialization.

> +    if (!ctx) {
> +        return;
> +    }Code style, one-liners don't use {} braces.
> +
> +    pw_ctx = ctx->priv_data;
> +    if (!pw_ctx) {
> +        av_log(ctx, AV_LOG_ERROR,
> +               "[pipewiregrab] Invalid private context data!\n");
> +        return;
> +    }Is this something that actually might happen where AVFormatContext is 
defined but not populated with priv_data? And if so, does the user need 
to know about it? If not, you could do something like this:

if (!ctx || !ctx->priv_data)
     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)Convention is to avoid "!= NULL" in FFmpeg, and use standard C nonzero 
checking.

> +        pw_thread_loop_signal(pw_ctx->thread_loop, false);
> +
> +    if (pw_ctx->glib_main_loop != NULL &&
Same.
> +        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;
Unnecessary initialization.
> +
> +    (void)seq;
What?

> +
> +    if (!ctx) {
> +        return;
> +    }
Code style again.
> +
> +    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;
Unnecessary initialization.
> +    if (!ctx) {
> +        return;
> +    }
Code style.
> +
> +    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;
You need to store this in the private context. Static non-const fields 
aren't going to work because multiple clients can call libavdevice at 
the same time. Protecting it with a mutex won't work either.

> +    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);
> +    }
Code style, braces.

> +
> +    if (out_path) {
> +        *out_path = g_strdup_printf(REQUEST_PATH, pw_ctx->sender_name,
> +                                    request_token_count);
> +    }Code style, braces.

> +}
> +
> +/**
> + * 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
> + */
This looks suspiciously like the previous function, can the code be 
deduplicated?
> +static void new_session_path(AVFormatContext *ctx, char **out_path,
> +                             char **out_token)
> +{
> +    static uint32_t session_token_count = 0;
See earlier comments for above function.
> +    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;
> +
Inline a return statement here, and return AV_PIX_FMT_NONE at the end of 
the function.


> +    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);
> +    }
You should nullify these various fields after you disconnect, so calling 
this twice won't create errors.
> +
> +    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);
> +    }
Code style, braces.

> +
> +    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;
Unnecessary initializations.
> +
> +    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;
What?

> +
> +    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;
> +    }
Code style, braces.
> +
> +    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;
Check for overflow.

> +    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);
This can fail, so you should return its value.

> +}
> +
> +/**
> + * 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;
What does this even do?

> +
> +    if (!ctx) {
> +        return;
> +    }
Code style.

> +
> +    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)
Does pipewire accept null callbacks?

> +{
> +    (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);
Where'd mmap come from?

> +    }Code Style braces.
> +
> +    /* 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);
> +    }
Code style, braces.
> +    free(buffer);

How was this allocated? Possibly you want av_free.
> +}
> +
> +/**
> + * 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;
Misplaced *
> +    while (true) {
while (1)
> +        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);
> +        }Code style braces;
> +        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;
> +    }
Braces.
> +
> +    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;
> +    }
!pw_buf, tho is there a reason you can't allocate one here?

> +
> +    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);
Why are we mmapping here? Are you prepared to catch SIGBUS?

> +        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);
Better off having a cleanup at the end with goto end; or goto fail; or 
something.

> +        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);
This can fail.

> +    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);
This can fail.

> +    pthread_mutex_unlock(&pw_ctx->current_pkt_mutex);
> +
> +    av_packet_free(&pkt);
> +    return;
Return an int, in case of failure.
> +}
> +
> +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);
Can you use av_calloc or av_mallocz here?

> +    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];
It looks like you're using an array of 1 to stack-allocate the location 
and make it static. Don't do that, this variable can disappear with the 
stack frame.

> +    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) {
!pw_ctx->thread_loop;
> +        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) {
Same here.
> +        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;
Why several goto here?

> +    }
> +
> +    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(
See earlier comment about the stack frame.

> +        &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);
If pw_stream_connect somehow takes ownership of ptr_spa_pod[0] then you 
should just use &ptr_spa_pod instead and don't declare it as an array.


> +    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);
> +
Too many gotos here. One cleanup is fine.

> +    return ret;
> +}
> +
> +static void on_pipewire_remote_opened_callback(GObject *source,
> +                                               GAsyncResult *res,
> +                                               gpointer user_data)
> +{
Since this function can fail it should have a return value.

> +    g_autoptr(GUnixFDList) fd_list = NULL;
> +    g_autoptr(GVariant) result = NULL;
> +    g_autoptr(GError) error = NULL;
What is g_autoptr and why can't we use standard C?

> +    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;
What does this even do?

> +
> +    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);
If you're allocating it with av_mallocz you have to free it with av_free.
> +
> +    g_variant_get(parameters, "(u@a{sv})", &response, &result);
> +
> +    if (response != 0) {
if (response) {
> +        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;
> +    }
Since this function can fail, it should return a failure.
> +
> +    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);No asserts. Use av_assert if you must, since it lets you control the 
assert level on the configure line.
> +
> +    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;This function can fail, so you should return an int.
> +    }
> +}
> +
> +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;
> +    }
This function can fail so it should return an int.

> +
> +    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)
> +{
This looks like an earlier function. Can code be deduplicated?
> +    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)
> +{
This looks like an earlier function. Can code be deduplicated?
> +    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)
> +{
This looks like an earlier function. Can code be deduplicated?
> +    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)
> +{
This looks like an earlier function. Can code be deduplicated?
> +    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)
> +{
This looks like an earlier function. Can code be deduplicated?
> +    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 */
av_strireplace
> +    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,
> +};
Put this at the end of the file, not in the middle.

> +
> +/**
> + * 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 (;;) {
while (1)
> +        curtime = av_gettime_relative();
> +        delay   = c->time_frame - curtime;
> +        if (delay <= 0)
> +            break;
> +        av_usleep(delay);
I'm not sure you need to do this, check other demuxers.

> +    }
> +
> +    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);
This can fail, so you need to return an error message.

> +    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);
Can fail.

> +        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)
if (pw_ctx->thread_loop)

Also nullify it when you free it.
> +        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);
> +    }av_packet_free(&foo); works even if foo is NULL, so this check is 
superfluous.

> +
> +    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)
Why is the return type of this function void *? You only return zero or 
AVERROR values. Make it int.

> +{
> +    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;
return AVERROR(ENOMEM);

> +    }
> +
> +    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);
Check the return value, this can fail.

> +    pthread_mutex_lock(&pw_ctx->avstream_codec_mutex);
> +    while (atomic_load(&pw_ctx->avstream_codec_flag) == 0) {
while (!atomic_load(foo))
> +        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,
> +};
Paul B Mahol Sept. 21, 2023, 10:39 a.m. UTC | #2
On Wed, Sep 20, 2023 at 8:41 PM Abhishek Ojha <
abhishek.ojha@savoirfairelinux.com> wrote:

> 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 <abhishek.ojha@savoirfairelinux.com>
> ---
>  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 <abhishek.ojha@savoirfairelinux.com>
> + */
> +
> +#include "config.h"
> +
> +#include <fcntl.h>
> +#include <linux/dma-buf.h>
> +#include <math.h>
> +#include <pthread.h>
> +#include <stdatomic.h>
> +#include <stdlib.h>
> +#include <string.h>
> +#include <sys/mman.h>
> +#include <sys/queue.h>
> +
> +#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 <pipewire/pipewire.h>
> +#include <pipewire/thread-loop.h>
> +#include <spa/debug/types.h>
> +#include <spa/param/video/format-utils.h>
> +#include <spa/param/video/type-info.h>
> +
> +#include <gio/gio.h>
> +#include <gio/gunixfdlist.h>
> +
> +#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);
>

All logs appear to have this [pipewiregrab] part, but this looks unneeded
as logs normally write module name out.
llyyr Sept. 22, 2023, 11:37 a.m. UTC | #3
On 9/21/23 00:10, Abhishek Ojha wrote:
> 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 <abhishek.ojha@savoirfairelinux.com>
> ---
>   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
> +
I just tried this and it failed to build due to missing locale_t and 
other locale related issues in pipewire's spa library. Upon further 
inspection, it seems Pipewire wants _XOPEN_SOURCE=700 but ffmpeg's 
configure sets _XOPEN_SOURCE=600. Not sure who's at fault here but I was 
able to get it to build after explicitly setting that.
diff mbox series

Patch

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 <abhishek.ojha@savoirfairelinux.com>
+ */
+
+#include "config.h"
+
+#include <fcntl.h>
+#include <linux/dma-buf.h>
+#include <math.h>
+#include <pthread.h>
+#include <stdatomic.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/mman.h>
+#include <sys/queue.h>
+
+#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 <pipewire/pipewire.h>
+#include <pipewire/thread-loop.h>
+#include <spa/debug/types.h>
+#include <spa/param/video/format-utils.h>
+#include <spa/param/video/type-info.h>
+
+#include <gio/gio.h>
+#include <gio/gunixfdlist.h>
+
+#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,
+};