From 705a47b0e439f649af8a81d0a68d03f3b83ea314 Mon Sep 17 00:00:00 2001 From: Kurt Kartaltepe Date: Thu, 25 Mar 2021 19:52:29 -0700 Subject: [PATCH 1/7] libobs, libobs-opengl: add drm format param This adds the drmbuf format as a parameter separate from the obs texture format that will be used. drmbuf's may have a variety of formats that we need to pass correctly to get a usable texture which may correspond to multi-platform texture formats. --- libobs-opengl/gl-egl-common.c | 72 +---------------------------- libobs-opengl/gl-egl-common.h | 2 +- libobs-opengl/gl-nix.c | 10 ++-- libobs-opengl/gl-nix.h | 4 +- libobs-opengl/gl-wayland-egl.c | 10 ++-- libobs-opengl/gl-x11-egl.c | 10 ++-- libobs-opengl/gl-x11-glx.c | 7 +-- libobs/graphics/device-exports.h | 6 +-- libobs/graphics/graphics-internal.h | 4 +- libobs/graphics/graphics.c | 16 +++---- libobs/graphics/graphics.h | 11 ++--- 11 files changed, 40 insertions(+), 112 deletions(-) diff --git a/libobs-opengl/gl-egl-common.c b/libobs-opengl/gl-egl-common.c index 0696839edd3bae..079eae39cbd475 100644 --- a/libobs-opengl/gl-egl-common.c +++ b/libobs-opengl/gl-egl-common.c @@ -50,33 +50,6 @@ typedef void(APIENTRYP PFNGLEGLIMAGETARGETTEXTURE2DOESPROC)( GLenum target, GLeglImageOES image); static PFNGLEGLIMAGETARGETTEXTURE2DOESPROC glEGLImageTargetTexture2DOES; -/* copied from drm_fourcc.h */ - -#define fourcc_code(a, b, c, d) \ - ((__u32)(a) | ((__u32)(b) << 8) | ((__u32)(c) << 16) | \ - ((__u32)(d) << 24)) -#define DRM_FORMAT_INVALID 0 -#define DRM_FORMAT_R8 fourcc_code('R', '8', ' ', ' ') /* [7:0] R */ -#define DRM_FORMAT_R16 \ - fourcc_code('R', '1', '6', ' ') /* [15:0] R little endian */ -#define DRM_FORMAT_RG88 \ - fourcc_code('R', 'G', '8', '8') /* [15:0] R:G 8:8 little endian */ -#define DRM_FORMAT_ABGR8888 \ - fourcc_code('A', 'B', '2', \ - '4') /* [31:0] A:B:G:R 8:8:8:8 little endian */ -#define DRM_FORMAT_ABGR2101010 \ - fourcc_code('A', 'B', '3', \ - '0') /* [31:0] A:B:G:R 2:10:10:10 little endian */ -#define DRM_FORMAT_ABGR16161616F \ - fourcc_code('A', 'B', '4', \ - 'H') /* [63:0] A:B:G:R 16:16:16:16 little endian */ -#define DRM_FORMAT_ARGB8888 \ - fourcc_code('A', 'R', '2', \ - '4') /* [31:0] A:R:G:B 8:8:8:8 little endian */ -#define DRM_FORMAT_XRGB8888 \ - fourcc_code('X', 'R', '2', \ - '4') /* [31:0] x:R:G:B 8:8:8:8 little endian */ - static bool find_gl_extension(const char *extension) { GLint n, i; @@ -113,42 +86,6 @@ static bool init_egl_image_target_texture_2d_ext(void) return true; } -static inline enum gs_color_format gs_format_to_drm_format(uint32_t drm_format) -{ - switch (drm_format) { - case GS_R8: - return DRM_FORMAT_R8; - case GS_RGBA: - return DRM_FORMAT_ABGR8888; - case GS_BGRX: - return DRM_FORMAT_XRGB8888; - case GS_BGRA: - return DRM_FORMAT_ARGB8888; - case GS_R10G10B10A2: - return DRM_FORMAT_ABGR2101010; - case GS_R16: - return DRM_FORMAT_R16; - case GS_RGBA16F: - return DRM_FORMAT_ABGR16161616F; - case GS_R8G8: - return DRM_FORMAT_RG88; - case GS_A8: - case GS_R16F: - case GS_RGBA16: - case GS_RG16F: - case GS_R32F: - case GS_RG32F: - case GS_RGBA32F: - case GS_DXT1: - case GS_DXT3: - case GS_DXT5: - case GS_UNKNOWN: - return DRM_FORMAT_INVALID; - } - - return DRM_FORMAT_INVALID; -} - static EGLImageKHR create_dmabuf_egl_image(EGLDisplay egl_display, unsigned int width, unsigned int height, uint32_t drm_format, @@ -242,24 +179,17 @@ create_dmabuf_egl_image(EGLDisplay egl_display, unsigned int width, struct gs_texture * gl_egl_create_dmabuf_image(EGLDisplay egl_display, unsigned int width, - unsigned int height, + unsigned int height, uint32_t drm_format, enum gs_color_format color_format, uint32_t n_planes, const int *fds, const uint32_t *strides, const uint32_t *offsets, const uint64_t *modifiers) { struct gs_texture *texture = NULL; EGLImage egl_image; - uint32_t drm_format; if (!init_egl_image_target_texture_2d_ext()) return NULL; - drm_format = gs_format_to_drm_format(color_format); - if (drm_format == DRM_FORMAT_INVALID) { - blog(LOG_ERROR, "Invalid or unsupported image format"); - return NULL; - } - egl_image = create_dmabuf_egl_image(egl_display, width, height, drm_format, n_planes, fds, strides, offsets, modifiers); diff --git a/libobs-opengl/gl-egl-common.h b/libobs-opengl/gl-egl-common.h index 3581138764580e..45f58d5e2d0e4b 100644 --- a/libobs-opengl/gl-egl-common.h +++ b/libobs-opengl/gl-egl-common.h @@ -8,7 +8,7 @@ const char *gl_egl_error_to_string(EGLint error_number); struct gs_texture * gl_egl_create_dmabuf_image(EGLDisplay egl_display, unsigned int width, - unsigned int height, + unsigned int height, uint32_t drm_format, enum gs_color_format color_format, uint32_t n_planes, const int *fds, const uint32_t *strides, const uint32_t *offsets, const uint64_t *modifiers); diff --git a/libobs-opengl/gl-nix.c b/libobs-opengl/gl-nix.c index acbd154a864bd9..aa93150a926d70 100644 --- a/libobs-opengl/gl-nix.c +++ b/libobs-opengl/gl-nix.c @@ -126,11 +126,11 @@ extern void device_present(gs_device_t *device) extern struct gs_texture *device_texture_create_from_dmabuf( gs_device_t *device, unsigned int width, unsigned int height, - enum gs_color_format color_format, uint32_t n_planes, const int *fds, - const uint32_t *strides, const uint32_t *offsets, - const uint64_t *modifiers) + uint32_t drm_format, enum gs_color_format color_format, + uint32_t n_planes, const int *fds, const uint32_t *strides, + const uint32_t *offsets, const uint64_t *modifiers) { return gl_vtable->device_texture_create_from_dmabuf( - device, width, height, color_format, n_planes, fds, strides, - offsets, modifiers); + device, width, height, drm_format, color_format, n_planes, fds, + strides, offsets, modifiers); } diff --git a/libobs-opengl/gl-nix.h b/libobs-opengl/gl-nix.h index 3038c0cff31f0f..a772bb5f4f7781 100644 --- a/libobs-opengl/gl-nix.h +++ b/libobs-opengl/gl-nix.h @@ -56,7 +56,7 @@ struct gl_winsys_vtable { struct gs_texture *(*device_texture_create_from_dmabuf)( gs_device_t *device, unsigned int width, unsigned int height, - enum gs_color_format color_format, uint32_t n_planes, - const int *fds, const uint32_t *strides, + uint32_t drm_format, enum gs_color_format color_format, + uint32_t n_planes, const int *fds, const uint32_t *strides, const uint32_t *offsets, const uint64_t *modifiers); }; diff --git a/libobs-opengl/gl-wayland-egl.c b/libobs-opengl/gl-wayland-egl.c index 3dbb4ac2cf8dcc..8d4084dd823736 100644 --- a/libobs-opengl/gl-wayland-egl.c +++ b/libobs-opengl/gl-wayland-egl.c @@ -323,15 +323,15 @@ static void gl_wayland_egl_device_present(gs_device_t *device) static struct gs_texture *gl_wayland_egl_device_texture_create_from_dmabuf( gs_device_t *device, unsigned int width, unsigned int height, - enum gs_color_format color_format, uint32_t n_planes, const int *fds, - const uint32_t *strides, const uint32_t *offsets, - const uint64_t *modifiers) + uint32_t drm_format, enum gs_color_format color_format, + uint32_t n_planes, const int *fds, const uint32_t *strides, + const uint32_t *offsets, const uint64_t *modifiers) { struct gl_platform *plat = device->plat; return gl_egl_create_dmabuf_image(plat->display, width, height, - color_format, n_planes, fds, strides, - offsets, modifiers); + drm_format, color_format, n_planes, + fds, strides, offsets, modifiers); } static const struct gl_winsys_vtable egl_wayland_winsys_vtable = { diff --git a/libobs-opengl/gl-x11-egl.c b/libobs-opengl/gl-x11-egl.c index d56f6b712dca04..295540fa0c8dca 100644 --- a/libobs-opengl/gl-x11-egl.c +++ b/libobs-opengl/gl-x11-egl.c @@ -637,15 +637,15 @@ static void gl_x11_egl_device_present(gs_device_t *device) static struct gs_texture *gl_x11_egl_device_texture_create_from_dmabuf( gs_device_t *device, unsigned int width, unsigned int height, - enum gs_color_format color_format, uint32_t n_planes, const int *fds, - const uint32_t *strides, const uint32_t *offsets, - const uint64_t *modifiers) + uint32_t drm_format, enum gs_color_format color_format, + uint32_t n_planes, const int *fds, const uint32_t *strides, + const uint32_t *offsets, const uint64_t *modifiers) { struct gl_platform *plat = device->plat; return gl_egl_create_dmabuf_image(plat->edisplay, width, height, - color_format, n_planes, fds, strides, - offsets, modifiers); + drm_format, color_format, n_planes, + fds, strides, offsets, modifiers); } static const struct gl_winsys_vtable egl_x11_winsys_vtable = { diff --git a/libobs-opengl/gl-x11-glx.c b/libobs-opengl/gl-x11-glx.c index 802be3ef9e2e7d..13a4ce362ce748 100644 --- a/libobs-opengl/gl-x11-glx.c +++ b/libobs-opengl/gl-x11-glx.c @@ -581,13 +581,14 @@ static void gl_x11_glx_device_present(gs_device_t *device) static struct gs_texture *gl_x11_glx_device_texture_create_from_dmabuf( gs_device_t *device, unsigned int width, unsigned int height, - enum gs_color_format color_format, uint32_t n_planes, const int *fds, - const uint32_t *strides, const uint32_t *offsets, - const uint64_t *modifiers) + uint32_t drm_format, enum gs_color_format color_format, + uint32_t n_planes, const int *fds, const uint32_t *strides, + const uint32_t *offsets, const uint64_t *modifiers) { UNUSED_PARAMETER(device); UNUSED_PARAMETER(width); UNUSED_PARAMETER(height); + UNUSED_PARAMETER(drm_format); UNUSED_PARAMETER(color_format); UNUSED_PARAMETER(n_planes); UNUSED_PARAMETER(fds); diff --git a/libobs/graphics/device-exports.h b/libobs/graphics/device-exports.h index ecf7f603e06ff0..5ea55e71cba4c5 100644 --- a/libobs/graphics/device-exports.h +++ b/libobs/graphics/device-exports.h @@ -174,9 +174,9 @@ EXPORT void device_debug_marker_end(gs_device_t *device); EXPORT gs_texture_t *device_texture_create_from_dmabuf( gs_device_t *device, unsigned int width, unsigned int height, - enum gs_color_format color_format, uint32_t n_planes, const int *fds, - const uint32_t *strides, const uint32_t *offsets, - const uint64_t *modifiers); + uint32_t drm_format, enum gs_color_format color_format, + uint32_t n_planes, const int *fds, const uint32_t *strides, + const uint32_t *offsets, const uint64_t *modifiers); #endif diff --git a/libobs/graphics/graphics-internal.h b/libobs/graphics/graphics-internal.h index 46a57ed39867b2..afa356fb1aa03a 100644 --- a/libobs/graphics/graphics-internal.h +++ b/libobs/graphics/graphics-internal.h @@ -330,8 +330,8 @@ struct gs_exports { #elif __linux__ struct gs_texture *(*device_texture_create_from_dmabuf)( gs_device_t *device, unsigned int width, unsigned int height, - enum gs_color_format color_format, uint32_t n_planes, - const int *fds, const uint32_t *strides, + uint32_t drm_format, enum gs_color_format color_format, + uint32_t n_planes, const int *fds, const uint32_t *strides, const uint32_t *offsets, const uint64_t *modifiers); #endif }; diff --git a/libobs/graphics/graphics.c b/libobs/graphics/graphics.c index 6aaf3ff7c9b40b..9d59509102ad63 100644 --- a/libobs/graphics/graphics.c +++ b/libobs/graphics/graphics.c @@ -1365,19 +1365,17 @@ gs_texture_t *gs_texture_create(uint32_t width, uint32_t height, #if __linux__ -gs_texture_t *gs_texture_create_from_dmabuf(unsigned int width, - unsigned int height, - enum gs_color_format color_format, - uint32_t n_planes, const int *fds, - const uint32_t *strides, - const uint32_t *offsets, - const uint64_t *modifiers) +gs_texture_t *gs_texture_create_from_dmabuf( + unsigned int width, unsigned int height, uint32_t drm_format, + enum gs_color_format color_format, uint32_t n_planes, const int *fds, + const uint32_t *strides, const uint32_t *offsets, + const uint64_t *modifiers) { graphics_t *graphics = thread_graphics; return graphics->exports.device_texture_create_from_dmabuf( - graphics->device, width, height, color_format, n_planes, fds, - strides, offsets, modifiers); + graphics->device, width, height, drm_format, color_format, + n_planes, fds, strides, offsets, modifiers); } #endif diff --git a/libobs/graphics/graphics.h b/libobs/graphics/graphics.h index 16f59ee06e1978..a8d9d59f7c8303 100644 --- a/libobs/graphics/graphics.h +++ b/libobs/graphics/graphics.h @@ -917,12 +917,11 @@ EXPORT void gs_unregister_loss_callbacks(void *data); #elif __linux__ -EXPORT gs_texture_t * -gs_texture_create_from_dmabuf(unsigned int width, unsigned int height, - enum gs_color_format color_format, - uint32_t n_planes, const int *fds, - const uint32_t *strides, const uint32_t *offsets, - const uint64_t *modifiers); +EXPORT gs_texture_t *gs_texture_create_from_dmabuf( + unsigned int width, unsigned int height, uint32_t drm_format, + enum gs_color_format color_format, uint32_t n_planes, const int *fds, + const uint32_t *strides, const uint32_t *offsets, + const uint64_t *modifiers); #endif From afb7bfa941d642852b2e9ef2ae79cc4f67096157 Mon Sep 17 00:00:00 2001 From: Georges Basile Stavracas Neto Date: Sun, 28 Feb 2021 18:32:21 -0300 Subject: [PATCH 2/7] build: Include gio-unix-2.0 This will be needed for fd-passing by the new capture, specifically for g_dbus_proxy_call_with_unix_fd_list(). --- cmake/Modules/FindGio.cmake | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmake/Modules/FindGio.cmake b/cmake/Modules/FindGio.cmake index 9656c5229530d8..d857ec69bfc7d7 100644 --- a/cmake/Modules/FindGio.cmake +++ b/cmake/Modules/FindGio.cmake @@ -9,12 +9,12 @@ # Use pkg-config to get the directories and then use these values # in the find_path() and find_library() calls find_package(PkgConfig) -pkg_check_modules(PC_GIO gio-2.0) +pkg_check_modules(PC_GIO gio-2.0 gio-unix-2.0) set(GIO_DEFINITIONS ${PC_GIO_CFLAGS}) find_path(GIO_INCLUDE_DIRS gio.h PATHS ${PC_GIO_INCLUDEDIR} ${PC_GIO_INCLUDE_DIRS} PATH_SUFFIXES glib-2.0/gio/) -find_library(GIO_LIBRARIES NAMES gio-2.0 libgio-2.0 PATHS ${PC_GIO_LIBDIR} ${PC_GIO_LIBRARY_DIRS}) +find_library(GIO_LIBRARIES NAMES gio-2.0 libgio-2.0 gio-unix-2.0 PATHS ${PC_GIO_LIBDIR} ${PC_GIO_LIBRARY_DIRS}) mark_as_advanced(GIO_INCLUDE_DIRS GIO_LIBRARIES) include(FindPackageHandleStandardArgs) From a0464b0f8f1d01a216da4e73fb5a9769b5f1094e Mon Sep 17 00:00:00 2001 From: Georges Basile Stavracas Neto Date: Sun, 28 Feb 2021 17:59:17 -0300 Subject: [PATCH 3/7] linux-capture: Shuffle around CMake code In preparation for the introduction of the new PipeWire-based capture, use variables for include_directories() and target_link_libraries(), and move them to the bottom of the file. --- plugins/linux-capture/CMakeLists.txt | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/plugins/linux-capture/CMakeLists.txt b/plugins/linux-capture/CMakeLists.txt index 7f876954c95f71..7ebdc500c04ee4 100644 --- a/plugins/linux-capture/CMakeLists.txt +++ b/plugins/linux-capture/CMakeLists.txt @@ -9,7 +9,7 @@ endif() find_package(XCB COMPONENTS XCB RANDR SHM XFIXES XINERAMA REQUIRED) find_package(X11_XCB REQUIRED) -include_directories(SYSTEM +set(linux-capture_INCLUDES "${CMAKE_SOURCE_DIR}/libobs" ${X11_Xcomposite_INCLUDE_PATH} ${X11_X11_INCLUDE_PATH} @@ -34,11 +34,7 @@ set(linux-capture_HEADERS xcompcap-helper.hpp ) -add_library(linux-capture MODULE - ${linux-capture_SOURCES} - ${linux-capture_HEADERS} -) -target_link_libraries(linux-capture +set(linux-capture_LIBRARIES libobs glad ${X11_LIBRARIES} @@ -47,6 +43,18 @@ target_link_libraries(linux-capture ${X11_Xcomposite_LIB} ${XCB_LIBRARIES} ) + +include_directories(SYSTEM + ${linux-capture_INCLUDES} +) +add_library(linux-capture MODULE + ${linux-capture_SOURCES} + ${linux-capture_HEADERS} +) +target_link_libraries(linux-capture + ${linux-capture_LIBRARIES} +) + set_target_properties(linux-capture PROPERTIES FOLDER "plugins") install_obs_plugin_with_data(linux-capture data) From c2f8b2058b77fad3bc09492207c74665d7f8b63d Mon Sep 17 00:00:00 2001 From: Georges Basile Stavracas Neto Date: Thu, 25 Feb 2021 16:57:33 -0300 Subject: [PATCH 4/7] linux-capture: Add PipeWire-based capture Add a new Linux capture based on PipeWire [1] and the Desktop portal [2]. 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. 2. CreateSession() is called. This is the first step of the negotiation. 3. SelectSources() is called. This is when a system dialog pops up asking the user to either select a monitor (desktop capture) or a window (window capture). 4. Start() is called. This signals the compositor that it can setup a PipeWire stream, and start sending buffers. The reply to this fourth call gives OBS Studio the PipeWire fd, and the id of the PipeWire node where the buffers are being sent to. This allows creating a consumer PipeWire stream, and receive the buffers. Metadata cursor is always preferred, but on the lack of it, we ask the stream for an embedded cursor (i.e. the cursor is drawn at the buffer, and OBS Studio has no control over it.) Window capturing is implemented as a crop operation on the buffer. Compositors can send big buffers, and a crop rectangle, and this is used to paint a subregion of the buffer in the scene. The new capture is only loaded when running on EGL, since it depends on EGL to call gs_texture_create_from_dmabuf(). [1] https://pipewire.org/ [2] https://github.com/flatpak/xdg-desktop-portal/ --- cmake/Modules/FindPipeWire.cmake | 121 ++ plugins/linux-capture/CMakeLists.txt | 35 + plugins/linux-capture/data/locale/en-US.ini | 5 + plugins/linux-capture/linux-capture.c | 23 +- plugins/linux-capture/pipewire-capture.c | 155 +++ plugins/linux-capture/pipewire-capture.h | 24 + plugins/linux-capture/pipewire.c | 1232 +++++++++++++++++++ plugins/linux-capture/pipewire.h | 53 + 8 files changed, 1642 insertions(+), 6 deletions(-) create mode 100644 cmake/Modules/FindPipeWire.cmake create mode 100644 plugins/linux-capture/pipewire-capture.c create mode 100644 plugins/linux-capture/pipewire-capture.h create mode 100644 plugins/linux-capture/pipewire.c create mode 100644 plugins/linux-capture/pipewire.h diff --git a/cmake/Modules/FindPipeWire.cmake b/cmake/Modules/FindPipeWire.cmake new file mode 100644 index 00000000000000..79215fac81de39 --- /dev/null +++ b/cmake/Modules/FindPipeWire.cmake @@ -0,0 +1,121 @@ +#.rst: +# FindPipeWire +# ------- +# +# Try to find PipeWire on a Unix system. +# +# This will define the following variables: +# +# ``PIPEWIRE_FOUND`` +# True if (the requested version of) PipeWire is available +# ``PIPEWIRE_VERSION`` +# The version of PipeWire +# ``PIPEWIRE_LIBRARIES`` +# This can be passed to target_link_libraries() instead of the ``PipeWire::PipeWire`` +# target +# ``PIPEWIRE_INCLUDE_DIRS`` +# This should be passed to target_include_directories() if the target is not +# used for linking +# ``PIPEWIRE_DEFINITIONS`` +# This should be passed to target_compile_options() if the target is not +# used for linking +# +# If ``PIPEWIRE_FOUND`` is TRUE, it will also define the following imported target: +# +# ``PipeWire::PipeWire`` +# The PipeWire library +# +# In general we recommend using the imported target, as it is easier to use. +# Bear in mind, however, that if the target is in the link interface of an +# exported library, it must be made available by the package config file. + +#============================================================================= +# Copyright 2014 Alex Merry +# Copyright 2014 Martin Gräßlin +# Copyright 2018-2020 Jan Grulich +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# 1. Redistributions of source code must retain the copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. The name of the author may not be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +#============================================================================= + +# Use pkg-config to get the directories and then use these values +# in the FIND_PATH() and FIND_LIBRARY() calls +find_package(PkgConfig QUIET) + +pkg_search_module(PKG_PIPEWIRE QUIET libpipewire-0.3) +pkg_search_module(PKG_SPA QUIET libspa-0.2) + +set(PIPEWIRE_DEFINITIONS "${PKG_PIPEWIRE_CFLAGS}" "${PKG_SPA_CFLAGS}") +set(PIPEWIRE_VERSION "${PKG_PIPEWIRE_VERSION}") + +find_path(PIPEWIRE_INCLUDE_DIRS + NAMES + pipewire/pipewire.h + HINTS + ${PKG_PIPEWIRE_INCLUDE_DIRS} + ${PKG_PIPEWIRE_INCLUDE_DIRS}/pipewire-0.3 +) + +find_path(SPA_INCLUDE_DIRS + NAMES + spa/param/props.h + HINTS + ${PKG_SPA_INCLUDE_DIRS} + ${PKG_SPA_INCLUDE_DIRS}/spa-0.2 +) + +find_library(PIPEWIRE_LIBRARIES + NAMES + pipewire-0.3 + HINTS + ${PKG_PIPEWIRE_LIBRARY_DIRS} +) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(PipeWire + FOUND_VAR + PIPEWIRE_FOUND + REQUIRED_VARS + PIPEWIRE_LIBRARIES + PIPEWIRE_INCLUDE_DIRS + SPA_INCLUDE_DIRS + VERSION_VAR + PIPEWIRE_VERSION +) + +if(PIPEWIRE_FOUND AND NOT TARGET PipeWire::PipeWire) + add_library(PipeWire::PipeWire UNKNOWN IMPORTED) + set_target_properties(PipeWire::PipeWire PROPERTIES + IMPORTED_LOCATION "${PIPEWIRE_LIBRARIES}" + INTERFACE_COMPILE_OPTIONS "${PIPEWIRE_DEFINITIONS}" + INTERFACE_INCLUDE_DIRECTORIES "${PIPEWIRE_INCLUDE_DIRS};${SPA_INCLUDE_DIRS}" + ) +endif() + +mark_as_advanced(PIPEWIRE_LIBRARIES PIPEWIRE_INCLUDE_DIRS) + +include(FeatureSummary) +set_package_properties(PipeWire PROPERTIES + URL "https://www.pipewire.org" + DESCRIPTION "PipeWire - multimedia processing" +) diff --git a/plugins/linux-capture/CMakeLists.txt b/plugins/linux-capture/CMakeLists.txt index 7ebdc500c04ee4..65d811ff7780e2 100644 --- a/plugins/linux-capture/CMakeLists.txt +++ b/plugins/linux-capture/CMakeLists.txt @@ -44,6 +44,41 @@ set(linux-capture_LIBRARIES ${XCB_LIBRARIES} ) +option(ENABLE_PIPEWIRE "Enable PipeWire support" ON) +if(ENABLE_PIPEWIRE) + find_package(PipeWire REQUIRED) + find_package(Gio REQUIRED) + + add_definitions(-DENABLE_PIPEWIRE) + + set(linux-capture_INCLUDES + ${linux-capture_INCLUDES} + ${GIO_INCLUDE_DIRS} + ${PIPEWIRE_INCLUDE_DIRS} + ) + + add_definitions( + ${GIO_DEFINITIONS} + ${PIPEWIRE_DEFINITIONS} + ) + + set(linux-capture_SOURCES + ${linux-capture_SOURCES} + pipewire.c + pipewire-capture.c + ) + set(linux-capture_HEADERS + ${linux-capture_HEADERS} + pipewire.h + pipewire-capture.h + ) + set(linux-capture_LIBRARIES + ${linux-capture_LIBRARIES} + ${GIO_LIBRARIES} + ${PIPEWIRE_LIBRARIES} + ) +endif() + include_directories(SYSTEM ${linux-capture_INCLUDES} ) diff --git a/plugins/linux-capture/data/locale/en-US.ini b/plugins/linux-capture/data/locale/en-US.ini index 5661f3833f42c6..b5a8e4147d7e0f 100644 --- a/plugins/linux-capture/data/locale/en-US.ini +++ b/plugins/linux-capture/data/locale/en-US.ini @@ -13,3 +13,8 @@ SwapRedBlue="Swap red and blue" LockX="Lock X server when capturing" IncludeXBorder="Include X Border" ExcludeAlpha="Use alpha-less texture format (Mesa workaround)" +PipeWireDesktopCapture="Screen Capture (PipeWire)" +PipeWireSelectMonitor="Select Monitor" +PipeWireSelectWindow="Select Window" +PipeWireWindowCapture="Window Capture (PipeWire)" +ShowCursor="Show Cursor" diff --git a/plugins/linux-capture/linux-capture.c b/plugins/linux-capture/linux-capture.c index 56ff485c4bf308..6797d949232ec6 100644 --- a/plugins/linux-capture/linux-capture.c +++ b/plugins/linux-capture/linux-capture.c @@ -17,6 +17,10 @@ along with this program. If not, see . #include #include +#ifdef ENABLE_PIPEWIRE +#include "pipewire-capture.h" +#endif + OBS_DECLARE_MODULE() OBS_MODULE_USE_DEFAULT_LOCALE("linux-xshm", "en-US") MODULE_EXPORT const char *obs_module_description(void) @@ -31,17 +35,24 @@ extern void xcomposite_unload(void); bool obs_module_load(void) { - if (obs_get_nix_platform() != OBS_NIX_PLATFORM_X11_GLX) { - blog(LOG_ERROR, "linux-capture cannot run on EGL platforms"); - return false; + if (obs_get_nix_platform() == OBS_NIX_PLATFORM_X11_GLX) { + obs_register_source(&xshm_input); + xcomposite_load(); +#ifdef ENABLE_PIPEWIRE + } else { + pipewire_capture_load(); +#endif } - obs_register_source(&xshm_input); - xcomposite_load(); return true; } void obs_module_unload(void) { - xcomposite_unload(); + if (obs_get_nix_platform() == OBS_NIX_PLATFORM_X11_GLX) + xcomposite_unload(); +#ifdef ENABLE_PIPEWIRE + else + pipewire_capture_unload(); +#endif } diff --git a/plugins/linux-capture/pipewire-capture.c b/plugins/linux-capture/pipewire-capture.c new file mode 100644 index 00000000000000..deb6bfb9312588 --- /dev/null +++ b/plugins/linux-capture/pipewire-capture.c @@ -0,0 +1,155 @@ +/* pipewire-capture.c + * + * Copyright 2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "pipewire.h" + +/* obs_source_info methods */ + +static const char *pipewire_desktop_capture_get_name(void *data) +{ + UNUSED_PARAMETER(data); + return obs_module_text("PipeWireDesktopCapture"); +} + +static const char *pipewire_window_capture_get_name(void *data) +{ + UNUSED_PARAMETER(data); + return obs_module_text("PipeWireWindowCapture"); +} + +static void *pipewire_desktop_capture_create(obs_data_t *settings, + obs_source_t *source) +{ + return obs_pipewire_create(DESKTOP_CAPTURE, settings, source); +} +static void *pipewire_window_capture_create(obs_data_t *settings, + obs_source_t *source) +{ + return obs_pipewire_create(WINDOW_CAPTURE, settings, source); +} + +static void pipewire_capture_destroy(void *data) +{ + obs_pipewire_destroy(data); +} + +static void pipewire_capture_get_defaults(obs_data_t *settings) +{ + obs_pipewire_get_defaults(settings); +} + +static obs_properties_t *pipewire_capture_get_properties(void *data) +{ + enum obs_pw_capture_type capture_type; + obs_pipewire_data *obs_pw = data; + + capture_type = obs_pipewire_get_capture_type(obs_pw); + + switch (capture_type) { + case DESKTOP_CAPTURE: + return obs_pipewire_get_properties(data, + "PipeWireSelectMonitor"); + case WINDOW_CAPTURE: + return obs_pipewire_get_properties(data, + "PipeWireSelectWindow"); + default: + return NULL; + } +} + +static void pipewire_capture_update(void *data, obs_data_t *settings) +{ + obs_pipewire_update(data, settings); +} + +static void pipewire_capture_show(void *data) +{ + obs_pipewire_show(data); +} + +static void pipewire_capture_hide(void *data) +{ + obs_pipewire_hide(data); +} + +static uint32_t pipewire_capture_get_width(void *data) +{ + return obs_pipewire_get_width(data); +} + +static uint32_t pipewire_capture_get_height(void *data) +{ + return obs_pipewire_get_height(data); +} + +static void pipewire_capture_video_render(void *data, gs_effect_t *effect) +{ + obs_pipewire_video_render(data, effect); +} + +void pipewire_capture_load(void) +{ + // Desktop capture + const struct obs_source_info pipewire_desktop_capture_info = { + .id = "pipewire-desktop-capture-source", + .type = OBS_SOURCE_TYPE_INPUT, + .output_flags = OBS_SOURCE_VIDEO, + .get_name = pipewire_desktop_capture_get_name, + .create = pipewire_desktop_capture_create, + .destroy = pipewire_capture_destroy, + .get_defaults = pipewire_capture_get_defaults, + .get_properties = pipewire_capture_get_properties, + .update = pipewire_capture_update, + .show = pipewire_capture_show, + .hide = pipewire_capture_hide, + .get_width = pipewire_capture_get_width, + .get_height = pipewire_capture_get_height, + .video_render = pipewire_capture_video_render, + .icon_type = OBS_ICON_TYPE_DESKTOP_CAPTURE, + }; + obs_register_source(&pipewire_desktop_capture_info); + + // Window capture + const struct obs_source_info pipewire_window_capture_info = { + .id = "pipewire-window-capture-source", + .type = OBS_SOURCE_TYPE_INPUT, + .output_flags = OBS_SOURCE_VIDEO, + .get_name = pipewire_window_capture_get_name, + .create = pipewire_window_capture_create, + .destroy = pipewire_capture_destroy, + .get_defaults = pipewire_capture_get_defaults, + .get_properties = pipewire_capture_get_properties, + .update = pipewire_capture_update, + .show = pipewire_capture_show, + .hide = pipewire_capture_hide, + .get_width = pipewire_capture_get_width, + .get_height = pipewire_capture_get_height, + .video_render = pipewire_capture_video_render, + .icon_type = OBS_ICON_TYPE_WINDOW_CAPTURE, + }; + obs_register_source(&pipewire_window_capture_info); + + pw_init(NULL, NULL); +} + +void pipewire_capture_unload(void) +{ + pw_deinit(); +} diff --git a/plugins/linux-capture/pipewire-capture.h b/plugins/linux-capture/pipewire-capture.h new file mode 100644 index 00000000000000..10d520e04b56d4 --- /dev/null +++ b/plugins/linux-capture/pipewire-capture.h @@ -0,0 +1,24 @@ +/* pipewire-capture.h + * + * Copyright 2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#pragma once + +void pipewire_capture_load(void); +void pipewire_capture_unload(void); diff --git a/plugins/linux-capture/pipewire.c b/plugins/linux-capture/pipewire.c new file mode 100644 index 00000000000000..42d59980786818 --- /dev/null +++ b/plugins/linux-capture/pipewire.c @@ -0,0 +1,1232 @@ +/* pipewire.c + * + * Copyright 2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "pipewire.h" + +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#define REQUEST_PATH "/org/freedesktop/portal/desktop/request/%s/obs%u" +#define SESSION_PATH "/org/freedesktop/portal/desktop/session/%s/obs%u" + +#define CURSOR_META_SIZE(width, height) \ + (sizeof(struct spa_meta_cursor) + sizeof(struct spa_meta_bitmap) + \ + width * height * 4) + +#define fourcc_code(a, b, c, d) \ + ((__u32)(a) | ((__u32)(b) << 8) | ((__u32)(c) << 16) | \ + ((__u32)(d) << 24)) + +#define DRM_FORMAT_XRGB8888 \ + fourcc_code('X', 'R', '2', \ + '4') /* [31:0] x:R:G:B 8:8:8:8 little endian */ +#define DRM_FORMAT_XBGR8888 \ + fourcc_code('X', 'B', '2', \ + '4') /* [31:0] x:B:G:R 8:8:8:8 little endian */ +#define DRM_FORMAT_ARGB8888 \ + fourcc_code('A', 'R', '2', \ + '4') /* [31:0] A:R:G:B 8:8:8:8 little endian */ +#define DRM_FORMAT_ABGR8888 \ + fourcc_code('A', 'B', '2', \ + '4') /* [31:0] A:B:G:R 8:8:8:8 little endian */ + +struct _obs_pipewire_data { + GDBusConnection *connection; + GDBusProxy *proxy; + GCancellable *cancellable; + + char *sender_name; + char *session_handle; + + uint32_t pipewire_node; + int pipewire_fd; + + uint32_t available_cursor_modes; + + obs_source_t *source; + obs_data_t *settings; + + gs_texture_t *texture; + + 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; + + struct { + bool valid; + int x, y; + uint32_t width, height; + } crop; + + struct { + bool visible; + bool valid; + int x, y; + int hotspot_x, hotspot_y; + int width, height; + gs_texture_t *texture; + } cursor; + + enum obs_pw_capture_type capture_type; + struct obs_video_info video_info; + bool negotiated; +}; + +struct dbus_call_data { + obs_pipewire_data *obs_pw; + char *request_path; + guint signal_id; + gulong cancelled_id; +}; + +/* auxiliary methods */ + +static const char *capture_type_to_string(enum obs_pw_capture_type capture_type) +{ + switch (capture_type) { + case DESKTOP_CAPTURE: + return "desktop"; + case WINDOW_CAPTURE: + return "window"; + } + return "unknown"; +} + +static void new_request_path(obs_pipewire_data *data, char **out_path, + char **out_token) +{ + static uint32_t request_token_count = 0; + + request_token_count++; + + if (out_token) { + struct dstr str; + dstr_init(&str); + dstr_printf(&str, "obs%u", request_token_count); + *out_token = str.array; + } + + if (out_path) { + struct dstr str; + dstr_init(&str); + dstr_printf(&str, REQUEST_PATH, data->sender_name, + request_token_count); + *out_path = str.array; + } +} + +static void new_session_path(obs_pipewire_data *data, char **out_path, + char **out_token) +{ + static uint32_t session_token_count = 0; + + session_token_count++; + + if (out_token) { + struct dstr str; + dstr_init(&str); + dstr_printf(&str, "obs%u", session_token_count); + *out_token = str.array; + } + + if (out_path) { + struct dstr str; + dstr_init(&str); + dstr_printf(&str, SESSION_PATH, data->sender_name, + session_token_count); + *out_path = str.array; + } +} + +static void on_cancelled_cb(GCancellable *cancellable, void *data) +{ + UNUSED_PARAMETER(cancellable); + + struct dbus_call_data *call = data; + + blog(LOG_INFO, "[pipewire] screencast session cancelled"); + + g_dbus_connection_call( + call->obs_pw->connection, "org.freedesktop.portal.Desktop", + call->request_path, "org.freedesktop.portal.Request", "Close", + NULL, NULL, G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL); +} + +static struct dbus_call_data *subscribe_to_signal(obs_pipewire_data *obs_pw, + const char *path, + GDBusSignalCallback callback) +{ + struct dbus_call_data *call; + + call = bzalloc(sizeof(struct dbus_call_data)); + call->obs_pw = obs_pw; + call->request_path = bstrdup(path); + call->cancelled_id = g_signal_connect(obs_pw->cancellable, "cancelled", + G_CALLBACK(on_cancelled_cb), + call); + call->signal_id = g_dbus_connection_signal_subscribe( + obs_pw->connection, "org.freedesktop.portal.Desktop", + "org.freedesktop.portal.Request", "Response", + call->request_path, NULL, G_DBUS_SIGNAL_FLAGS_NO_MATCH_RULE, + callback, call, NULL); + + return call; +} + +static void dbus_call_data_free(struct dbus_call_data *call) +{ + if (!call) + return; + + if (call->signal_id) + g_dbus_connection_signal_unsubscribe(call->obs_pw->connection, + call->signal_id); + + if (call->cancelled_id > 0) + g_signal_handler_disconnect(call->obs_pw->cancellable, + call->cancelled_id); + + g_clear_pointer(&call->request_path, bfree); + bfree(call); +} + +static void teardown_pipewire(obs_pipewire_data *obs_pw) +{ + if (obs_pw->thread_loop) { + pw_thread_loop_wait(obs_pw->thread_loop); + pw_thread_loop_stop(obs_pw->thread_loop); + } + + if (obs_pw->stream) + pw_stream_disconnect(obs_pw->stream); + g_clear_pointer(&obs_pw->stream, pw_stream_destroy); + g_clear_pointer(&obs_pw->context, pw_context_destroy); + g_clear_pointer(&obs_pw->thread_loop, pw_thread_loop_destroy); + + if (obs_pw->pipewire_fd > 0) { + close(obs_pw->pipewire_fd); + obs_pw->pipewire_fd = 0; + } + + obs_pw->negotiated = false; +} + +static void destroy_session(obs_pipewire_data *obs_pw) +{ + if (obs_pw->session_handle) { + g_dbus_connection_call( + obs_pw->connection, "org.freedesktop.portal.Desktop", + obs_pw->session_handle, + "org.freedesktop.portal.Session", "Close", NULL, NULL, + G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL); + + g_clear_pointer(&obs_pw->session_handle, g_free); + } + + g_clear_pointer(&obs_pw->sender_name, bfree); + g_clear_pointer(&obs_pw->cursor.texture, gs_texture_destroy); + g_clear_pointer(&obs_pw->texture, gs_texture_destroy); + g_cancellable_cancel(obs_pw->cancellable); + g_clear_object(&obs_pw->cancellable); + g_clear_object(&obs_pw->connection); + g_clear_object(&obs_pw->proxy); +} + +static inline bool has_effective_crop(obs_pipewire_data *obs_pw) +{ + return obs_pw->crop.valid && + (obs_pw->crop.x != 0 || obs_pw->crop.y != 0 || + obs_pw->crop.width < obs_pw->format.info.raw.size.width || + obs_pw->crop.height < obs_pw->format.info.raw.size.height); +} + +static bool spa_pixel_format_to_drm_format(uint32_t spa_format, + uint32_t *out_format) +{ + switch (spa_format) { + case SPA_VIDEO_FORMAT_RGBA: + *out_format = DRM_FORMAT_ABGR8888; + break; + + case SPA_VIDEO_FORMAT_RGBx: + *out_format = DRM_FORMAT_XBGR8888; + break; + + case SPA_VIDEO_FORMAT_BGRA: + *out_format = DRM_FORMAT_ARGB8888; + break; + + case SPA_VIDEO_FORMAT_BGRx: + *out_format = DRM_FORMAT_XRGB8888; + break; + + default: + return false; + } + + return true; +} + +static bool spa_pixel_format_to_obs_format(uint32_t spa_format, + enum gs_color_format *out_format, + bool *swap_red_blue) +{ + switch (spa_format) { + case SPA_VIDEO_FORMAT_RGBA: + *out_format = GS_RGBA; + *swap_red_blue = false; + break; + + case SPA_VIDEO_FORMAT_RGBx: + *out_format = GS_BGRX; + *swap_red_blue = true; + break; + + case SPA_VIDEO_FORMAT_BGRA: + *out_format = GS_BGRA; + *swap_red_blue = false; + break; + + case SPA_VIDEO_FORMAT_BGRx: + *out_format = GS_BGRX; + *swap_red_blue = false; + break; + + default: + return false; + } + + return true; +} + +static void swap_texture_red_blue(gs_texture_t *texture) +{ + GLuint gl_texure = *(GLuint *)gs_texture_get_obj(texture); + + glBindTexture(GL_TEXTURE_2D, gl_texure); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_B, GL_RED); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_R, GL_BLUE); + glBindTexture(GL_TEXTURE_2D, 0); +} + +/* ------------------------------------------------- */ + +static void on_process_cb(void *user_data) +{ + obs_pipewire_data *obs_pw = user_data; + struct spa_meta_cursor *cursor; + uint32_t drm_format; + struct spa_meta_region *region; + struct spa_buffer *buffer; + struct pw_buffer *b; + bool swap_red_blue = false; + bool has_buffer; + + /* Find the most recent buffer */ + b = NULL; + while (true) { + struct pw_buffer *aux = + pw_stream_dequeue_buffer(obs_pw->stream); + if (!aux) + break; + if (b) + pw_stream_queue_buffer(obs_pw->stream, b); + b = aux; + } + + if (!b) { + blog(LOG_DEBUG, "[pipewire] Out of buffers!"); + return; + } + + buffer = b->buffer; + has_buffer = buffer->datas[0].chunk->size != 0; + + obs_enter_graphics(); + + if (!has_buffer) + goto read_metadata; + + if (buffer->datas[0].type == SPA_DATA_DmaBuf) { + uint32_t offsets[1]; + uint32_t strides[1]; + uint64_t modifiers[1]; + int fds[1]; + + blog(LOG_DEBUG, + "[pipewire] DMA-BUF info: fd:%ld, stride:%d, offset:%u, size:%dx%d", + buffer->datas[0].fd, buffer->datas[0].chunk->stride, + buffer->datas[0].chunk->offset, + obs_pw->format.info.raw.size.width, + obs_pw->format.info.raw.size.height); + + if (!spa_pixel_format_to_drm_format( + obs_pw->format.info.raw.format, &drm_format)) { + blog(LOG_ERROR, + "[pipewire] unsupported DMA buffer format: %d", + obs_pw->format.info.raw.format); + goto read_metadata; + } + + fds[0] = buffer->datas[0].fd; + offsets[0] = buffer->datas[0].chunk->offset; + strides[0] = buffer->datas[0].chunk->stride; + modifiers[0] = obs_pw->format.info.raw.modifier; + + g_clear_pointer(&obs_pw->texture, gs_texture_destroy); + obs_pw->texture = gs_texture_create_from_dmabuf( + obs_pw->format.info.raw.size.width, + obs_pw->format.info.raw.size.height, drm_format, + GS_BGRX, 1, fds, strides, offsets, modifiers); + } else { + blog(LOG_DEBUG, "[pipewire] Buffer has memory texture"); + enum gs_color_format obs_format; + + if (!spa_pixel_format_to_obs_format( + obs_pw->format.info.raw.format, &obs_format, + &swap_red_blue)) { + blog(LOG_ERROR, + "[pipewire] unsupported DMA buffer format: %d", + obs_pw->format.info.raw.format); + goto read_metadata; + } + + g_clear_pointer(&obs_pw->texture, gs_texture_destroy); + obs_pw->texture = gs_texture_create( + obs_pw->format.info.raw.size.width, + obs_pw->format.info.raw.size.height, obs_format, 1, + (const uint8_t **)&buffer->datas[0].data, GS_DYNAMIC); + } + + if (swap_red_blue) + swap_texture_red_blue(obs_pw->texture); + + /* Video Crop */ + region = spa_buffer_find_meta_data(buffer, SPA_META_VideoCrop, + sizeof(*region)); + if (region && spa_meta_region_is_valid(region)) { + blog(LOG_DEBUG, + "[pipewire] Crop Region available (%dx%d+%d+%d)", + region->region.position.x, region->region.position.y, + region->region.size.width, region->region.size.height); + + obs_pw->crop.x = region->region.position.x; + obs_pw->crop.y = region->region.position.y; + obs_pw->crop.width = region->region.size.width; + obs_pw->crop.height = region->region.size.height; + obs_pw->crop.valid = true; + } else { + obs_pw->crop.valid = false; + } + +read_metadata: + + /* Cursor */ + cursor = spa_buffer_find_meta_data(buffer, SPA_META_Cursor, + sizeof(*cursor)); + obs_pw->cursor.valid = cursor && spa_meta_cursor_is_valid(cursor); + if (obs_pw->cursor.visible && obs_pw->cursor.valid) { + struct spa_meta_bitmap *bitmap = NULL; + enum gs_color_format format; + + if (cursor->bitmap_offset) + bitmap = SPA_MEMBER(cursor, cursor->bitmap_offset, + struct spa_meta_bitmap); + + if (bitmap && bitmap->size.width > 0 && + bitmap->size.height > 0 && + spa_pixel_format_to_obs_format(bitmap->format, &format, + &swap_red_blue)) { + const uint8_t *bitmap_data; + + bitmap_data = + SPA_MEMBER(bitmap, bitmap->offset, uint8_t); + obs_pw->cursor.hotspot_x = cursor->hotspot.x; + obs_pw->cursor.hotspot_y = cursor->hotspot.y; + obs_pw->cursor.width = bitmap->size.width; + obs_pw->cursor.height = bitmap->size.height; + + g_clear_pointer(&obs_pw->cursor.texture, + gs_texture_destroy); + obs_pw->cursor.texture = gs_texture_create( + obs_pw->cursor.width, obs_pw->cursor.height, + format, 1, &bitmap_data, GS_DYNAMIC); + + if (swap_red_blue) + swap_texture_red_blue(obs_pw->cursor.texture); + } + + obs_pw->cursor.x = cursor->position.x; + obs_pw->cursor.y = cursor->position.y; + } + + pw_stream_queue_buffer(obs_pw->stream, b); + + obs_leave_graphics(); +} + +static void on_param_changed_cb(void *user_data, uint32_t id, + const struct spa_pod *param) +{ + obs_pipewire_data *obs_pw = user_data; + struct spa_pod_builder pod_builder; + const struct spa_pod *params[3]; + uint8_t params_buffer[1024]; + int result; + + if (!param || id != SPA_PARAM_Format) + return; + + result = spa_format_parse(param, &obs_pw->format.media_type, + &obs_pw->format.media_subtype); + if (result < 0) + return; + + if (obs_pw->format.media_type != SPA_MEDIA_TYPE_video || + obs_pw->format.media_subtype != SPA_MEDIA_SUBTYPE_raw) + return; + + spa_format_video_raw_parse(param, &obs_pw->format.info.raw); + + blog(LOG_DEBUG, "[pipewire] Negotiated format:"); + + blog(LOG_DEBUG, "[pipewire] Format: %d (%s)", + obs_pw->format.info.raw.format, + spa_debug_type_find_name(spa_type_video_format, + obs_pw->format.info.raw.format)); + + blog(LOG_DEBUG, "[pipewire] Size: %dx%d", + obs_pw->format.info.raw.size.width, + obs_pw->format.info.raw.size.height); + + blog(LOG_DEBUG, "[pipewire] Framerate: %d/%d", + obs_pw->format.info.raw.framerate.num, + obs_pw->format.info.raw.framerate.denom); + + /* Video crop */ + pod_builder = + SPA_POD_BUILDER_INIT(params_buffer, sizeof(params_buffer)); + params[0] = 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[1] = 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[2] = 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_DmaBuf))); + + pw_stream_update_params(obs_pw->stream, params, 3); + + obs_pw->negotiated = true; +} + +static void on_state_changed_cb(void *user_data, enum pw_stream_state old, + enum pw_stream_state state, const char *error) +{ + UNUSED_PARAMETER(old); + UNUSED_PARAMETER(error); + + obs_pipewire_data *obs_pw = user_data; + + blog(LOG_DEBUG, "[pipewire] stream %p state: \"%s\" (error: %s)", + obs_pw->stream, pw_stream_state_as_string(state), + error ? error : "none"); +} + +static const struct pw_stream_events stream_events = { + PW_VERSION_STREAM_EVENTS, + .state_changed = on_state_changed_cb, + .param_changed = on_param_changed_cb, + .process = on_process_cb, +}; + +static void on_core_error_cb(void *user_data, uint32_t id, int seq, int res, + const char *message) +{ + UNUSED_PARAMETER(seq); + + obs_pipewire_data *obs_pw = user_data; + + blog(LOG_ERROR, "[pipewire] Error id:%u seq:%d res:%d (%s): %s", id, + seq, res, g_strerror(res), message); + + pw_thread_loop_signal(obs_pw->thread_loop, FALSE); +} + +static void on_core_done_cb(void *user_data, uint32_t id, int seq) +{ + UNUSED_PARAMETER(seq); + + obs_pipewire_data *obs_pw = user_data; + + if (id == PW_ID_CORE) + pw_thread_loop_signal(obs_pw->thread_loop, FALSE); +} + +static const struct pw_core_events core_events = { + PW_VERSION_CORE_EVENTS, + .done = on_core_done_cb, + .error = on_core_error_cb, +}; + +static void play_pipewire_stream(obs_pipewire_data *obs_pw) +{ + struct spa_pod_builder pod_builder; + const struct spa_pod *params[1]; + uint8_t params_buffer[1024]; + struct obs_video_info ovi; + + obs_pw->thread_loop = pw_thread_loop_new("PipeWire thread loop", NULL); + obs_pw->context = pw_context_new( + pw_thread_loop_get_loop(obs_pw->thread_loop), NULL, 0); + + if (pw_thread_loop_start(obs_pw->thread_loop) < 0) { + blog(LOG_WARNING, "Error starting threaded mainloop"); + return; + } + + pw_thread_loop_lock(obs_pw->thread_loop); + + /* Core */ + obs_pw->core = pw_context_connect_fd( + obs_pw->context, fcntl(obs_pw->pipewire_fd, F_DUPFD_CLOEXEC, 5), + NULL, 0); + if (!obs_pw->core) { + blog(LOG_WARNING, "Error creating PipeWire core: %m"); + pw_thread_loop_unlock(obs_pw->thread_loop); + return; + } + + pw_core_add_listener(obs_pw->core, &obs_pw->core_listener, &core_events, + obs_pw); + + /* Stream */ + obs_pw->stream = pw_stream_new( + obs_pw->core, "OBS Studio", + pw_properties_new(PW_KEY_MEDIA_TYPE, "Video", + PW_KEY_MEDIA_CATEGORY, "Capture", + PW_KEY_MEDIA_ROLE, "Screen", NULL)); + pw_stream_add_listener(obs_pw->stream, &obs_pw->stream_listener, + &stream_events, obs_pw); + blog(LOG_INFO, "[pipewire] created stream %p", obs_pw->stream); + + /* Stream parameters */ + pod_builder = + SPA_POD_BUILDER_INIT(params_buffer, sizeof(params_buffer)); + + obs_get_video_info(&ovi); + params[0] = spa_pod_builder_add_object( + &pod_builder, SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat, + SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_video), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw), + SPA_FORMAT_VIDEO_format, + SPA_POD_CHOICE_ENUM_Id( + 4, SPA_VIDEO_FORMAT_BGRA, SPA_VIDEO_FORMAT_RGBA, + SPA_VIDEO_FORMAT_BGRx, SPA_VIDEO_FORMAT_RGBx), + SPA_FORMAT_VIDEO_size, + SPA_POD_CHOICE_RANGE_Rectangle( + &SPA_RECTANGLE(320, 240), // Arbitrary + &SPA_RECTANGLE(1, 1), &SPA_RECTANGLE(8192, 4320)), + SPA_FORMAT_VIDEO_framerate, + SPA_POD_CHOICE_RANGE_Fraction( + &SPA_FRACTION(ovi.fps_num, ovi.fps_den), + &SPA_FRACTION(0, 1), &SPA_FRACTION(360, 1))); + obs_pw->video_info = ovi; + + pw_stream_connect( + obs_pw->stream, PW_DIRECTION_INPUT, obs_pw->pipewire_node, + PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS, params, + 1); + + blog(LOG_INFO, "[pipewire] playing stream…"); + + pw_thread_loop_unlock(obs_pw->thread_loop); +} + +/* ------------------------------------------------- */ + +static void on_pipewire_remote_opened_cb(GObject *source, GAsyncResult *res, + void *user_data) +{ + g_autoptr(GUnixFDList) fd_list = NULL; + g_autoptr(GVariant) result = NULL; + g_autoptr(GError) error = NULL; + obs_pipewire_data *obs_pw = user_data; + int fd_index; + + 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)) + blog(LOG_ERROR, + "[pipewire] Error retrieving pipewire fd: %s", + error->message); + return; + } + + g_variant_get(result, "(h)", &fd_index, &error); + + obs_pw->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)) + blog(LOG_ERROR, + "[pipewire] Error retrieving pipewire fd: %s", + error->message); + return; + } + + play_pipewire_stream(obs_pw); +} + +static void open_pipewire_remote(obs_pipewire_data *obs_pw) +{ + GVariantBuilder builder; + + g_variant_builder_init(&builder, G_VARIANT_TYPE_VARDICT); + + g_dbus_proxy_call_with_unix_fd_list( + obs_pw->proxy, "OpenPipeWireRemote", + g_variant_new("(oa{sv})", obs_pw->session_handle, &builder), + G_DBUS_CALL_FLAGS_NONE, -1, NULL, obs_pw->cancellable, + on_pipewire_remote_opened_cb, obs_pw); +} + +/* ------------------------------------------------- */ + +static void on_start_response_received_cb(GDBusConnection *connection, + const char *sender_name, + const char *object_path, + const char *interface_name, + const char *signal_name, + GVariant *parameters, void *user_data) +{ + UNUSED_PARAMETER(connection); + UNUSED_PARAMETER(sender_name); + UNUSED_PARAMETER(object_path); + UNUSED_PARAMETER(interface_name); + UNUSED_PARAMETER(signal_name); + + g_autoptr(GVariant) stream_properties = NULL; + g_autoptr(GVariant) streams = NULL; + g_autoptr(GVariant) result = NULL; + struct dbus_call_data *call = user_data; + obs_pipewire_data *obs_pw = call->obs_pw; + GVariantIter iter; + uint32_t response; + + g_clear_pointer(&call, dbus_call_data_free); + + g_variant_get(parameters, "(u@a{sv})", &response, &result); + + if (response != 0) { + blog(LOG_WARNING, + "[pipewire] 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})", &obs_pw->pipewire_node, + &stream_properties); + + blog(LOG_INFO, "[pipewire] %s selected, setting up screencast", + capture_type_to_string(obs_pw->capture_type)); + + open_pipewire_remote(obs_pw); +} + +static void on_started_cb(GObject *source, GAsyncResult *res, void *user_data) +{ + UNUSED_PARAMETER(user_data); + + g_autoptr(GVariant) result = NULL; + g_autoptr(GError) error = NULL; + + 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)) + blog(LOG_ERROR, + "[pipewire] Error selecting screencast source: %s", + error->message); + return; + } +} + +static void start(obs_pipewire_data *obs_pw) +{ + GVariantBuilder builder; + struct dbus_call_data *call; + char *request_token; + char *request_path; + + new_request_path(obs_pw, &request_path, &request_token); + + blog(LOG_INFO, "[pipewire] asking for %s…", + capture_type_to_string(obs_pw->capture_type)); + + call = subscribe_to_signal(obs_pw, request_path, + on_start_response_received_cb); + + 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(obs_pw->proxy, "Start", + g_variant_new("(osa{sv})", obs_pw->session_handle, "", + &builder), + G_DBUS_CALL_FLAGS_NONE, -1, obs_pw->cancellable, + on_started_cb, call); + + bfree(request_token); + bfree(request_path); +} + +/* ------------------------------------------------- */ + +static void on_select_source_response_received_cb( + GDBusConnection *connection, const char *sender_name, + const char *object_path, const char *interface_name, + const char *signal_name, GVariant *parameters, void *user_data) +{ + UNUSED_PARAMETER(connection); + UNUSED_PARAMETER(sender_name); + UNUSED_PARAMETER(object_path); + UNUSED_PARAMETER(interface_name); + UNUSED_PARAMETER(signal_name); + + g_autoptr(GVariant) ret = NULL; + struct dbus_call_data *call = user_data; + obs_pipewire_data *obs_pw = call->obs_pw; + uint32_t response; + + blog(LOG_DEBUG, "[pipewire] Response to select source received"); + + g_clear_pointer(&call, dbus_call_data_free); + + g_variant_get(parameters, "(u@a{sv})", &response, &ret); + + if (response != 0) { + blog(LOG_WARNING, + "[pipewire] Failed to select source, denied or cancelled by user"); + return; + } + + start(obs_pw); +} + +static void on_source_selected_cb(GObject *source, GAsyncResult *res, + void *user_data) +{ + UNUSED_PARAMETER(user_data); + + g_autoptr(GVariant) result = NULL; + g_autoptr(GError) error = NULL; + + 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)) + blog(LOG_ERROR, + "[pipewire] Error selecting screencast source: %s", + error->message); + return; + } +} + +static void select_source(obs_pipewire_data *obs_pw) +{ + struct dbus_call_data *call; + GVariantBuilder builder; + char *request_token; + char *request_path; + + new_request_path(obs_pw, &request_path, &request_token); + + call = subscribe_to_signal(obs_pw, request_path, + on_select_source_response_received_cb); + + g_variant_builder_init(&builder, G_VARIANT_TYPE_VARDICT); + g_variant_builder_add(&builder, "{sv}", "types", + g_variant_new_uint32(obs_pw->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 (obs_pw->available_cursor_modes & 4) + g_variant_builder_add(&builder, "{sv}", "cursor_mode", + g_variant_new_uint32(4)); + else if ((obs_pw->available_cursor_modes & 2) && obs_pw->cursor.visible) + g_variant_builder_add(&builder, "{sv}", "cursor_mode", + g_variant_new_uint32(2)); + else + g_variant_builder_add(&builder, "{sv}", "cursor_mode", + g_variant_new_uint32(1)); + + g_dbus_proxy_call(obs_pw->proxy, "SelectSources", + g_variant_new("(oa{sv})", obs_pw->session_handle, + &builder), + G_DBUS_CALL_FLAGS_NONE, -1, obs_pw->cancellable, + on_source_selected_cb, call); + + bfree(request_token); + bfree(request_path); +} + +/* ------------------------------------------------- */ + +static void on_create_session_response_received_cb( + GDBusConnection *connection, const char *sender_name, + const char *object_path, const char *interface_name, + const char *signal_name, GVariant *parameters, void *user_data) +{ + UNUSED_PARAMETER(connection); + UNUSED_PARAMETER(sender_name); + UNUSED_PARAMETER(object_path); + UNUSED_PARAMETER(interface_name); + UNUSED_PARAMETER(signal_name); + + g_autoptr(GVariant) result = NULL; + struct dbus_call_data *call = user_data; + obs_pipewire_data *obs_pw = call->obs_pw; + uint32_t response; + + g_clear_pointer(&call, dbus_call_data_free); + + g_variant_get(parameters, "(u@a{sv})", &response, &result); + + if (response != 0) { + blog(LOG_WARNING, + "[pipewire] Failed to create session, denied or cancelled by user"); + return; + } + + blog(LOG_INFO, "[pipewire] screencast session created"); + + g_variant_lookup(result, "session_handle", "s", + &obs_pw->session_handle); + + select_source(obs_pw); +} + +static void on_session_created_cb(GObject *source, GAsyncResult *res, + void *user_data) +{ + UNUSED_PARAMETER(user_data); + + g_autoptr(GVariant) result = NULL; + g_autoptr(GError) error = NULL; + + 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)) + blog(LOG_ERROR, + "[pipewire] Error creating screencast session: %s", + error->message); + return; + } +} + +static void create_session(obs_pipewire_data *obs_pw) +{ + struct dbus_call_data *call; + GVariantBuilder builder; + char *session_token; + char *request_token; + char *request_path; + + new_request_path(obs_pw, &request_path, &request_token); + new_session_path(obs_pw, NULL, &session_token); + + call = subscribe_to_signal(obs_pw, request_path, + on_create_session_response_received_cb); + + 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(obs_pw->proxy, "CreateSession", + g_variant_new("(a{sv})", &builder), + G_DBUS_CALL_FLAGS_NONE, -1, obs_pw->cancellable, + on_session_created_cb, call); + + bfree(session_token); + bfree(request_token); + bfree(request_path); +} + +/* ------------------------------------------------- */ + +static void update_available_cursor_modes(obs_pipewire_data *obs_pw) +{ + g_autoptr(GVariant) cached_cursor_modes = NULL; + uint32_t available_cursor_modes; + + cached_cursor_modes = g_dbus_proxy_get_cached_property( + obs_pw->proxy, "AvailableCursorModes"); + available_cursor_modes = + cached_cursor_modes ? g_variant_get_uint32(cached_cursor_modes) + : 0; + + obs_pw->available_cursor_modes = available_cursor_modes; + + blog(LOG_INFO, "[pipewire] available cursor modes:"); + if (available_cursor_modes & 4) + blog(LOG_INFO, "[pipewire] - Metadata"); + if (available_cursor_modes & 2) + blog(LOG_INFO, "[pipewire] - Always visible"); + if (available_cursor_modes & 1) + blog(LOG_INFO, "[pipewire] - Hidden"); +} + +static void on_proxy_created_cb(GObject *source, GAsyncResult *res, + void *user_data) +{ + UNUSED_PARAMETER(source); + + g_autoptr(GError) error = NULL; + obs_pipewire_data *obs_pw = user_data; + + obs_pw->proxy = g_dbus_proxy_new_finish(res, &error); + + if (error) { + if (!g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + blog(LOG_ERROR, "[pipewire] Error creating proxy: %s", + error->message); + return; + } + + update_available_cursor_modes(obs_pw); + create_session(obs_pw); +} + +static void create_proxy(obs_pipewire_data *obs_pw) +{ + g_dbus_proxy_new(obs_pw->connection, G_DBUS_PROXY_FLAGS_NONE, NULL, + "org.freedesktop.portal.Desktop", + "/org/freedesktop/portal/desktop", + "org.freedesktop.portal.ScreenCast", + obs_pw->cancellable, on_proxy_created_cb, obs_pw); +} + +/* ------------------------------------------------- */ + +static gboolean init_obs_pipewire(obs_pipewire_data *obs_pw) +{ + g_autoptr(GError) error = NULL; + char *aux; + + obs_pw->cancellable = g_cancellable_new(); + obs_pw->connection = g_bus_get_sync(G_BUS_TYPE_SESSION, NULL, &error); + if (error) { + g_error("Error getting session bus: %s", error->message); + return FALSE; + } + + obs_pw->sender_name = bstrdup( + g_dbus_connection_get_unique_name(obs_pw->connection) + 1); + + /* Replace dots by underscores */ + while ((aux = strstr(obs_pw->sender_name, ".")) != NULL) + *aux = '_'; + + blog(LOG_INFO, "PipeWire initialized (sender name: %s)", + obs_pw->sender_name); + + create_proxy(obs_pw); + + return TRUE; +} + +static bool reload_session_cb(obs_properties_t *properties, + obs_property_t *property, void *data) +{ + UNUSED_PARAMETER(properties); + UNUSED_PARAMETER(property); + + obs_pipewire_data *obs_pw = data; + + teardown_pipewire(obs_pw); + destroy_session(obs_pw); + + init_obs_pipewire(obs_pw); + + return false; +} + +/* obs_source_info methods */ + +void *obs_pipewire_create(enum obs_pw_capture_type capture_type, + obs_data_t *settings, obs_source_t *source) +{ + obs_pipewire_data *obs_pw = bzalloc(sizeof(obs_pipewire_data)); + + obs_pw->source = source; + obs_pw->settings = settings; + obs_pw->capture_type = capture_type; + obs_pw->cursor.visible = obs_data_get_bool(settings, "ShowCursor"); + + if (!init_obs_pipewire(obs_pw)) + g_clear_pointer(&obs_pw, bfree); + + return obs_pw; +} + +void obs_pipewire_destroy(obs_pipewire_data *obs_pw) +{ + if (!obs_pw) + return; + + teardown_pipewire(obs_pw); + destroy_session(obs_pw); + + bfree(obs_pw); +} + +void obs_pipewire_get_defaults(obs_data_t *settings) +{ + obs_data_set_default_bool(settings, "ShowCursor", true); +} + +obs_properties_t *obs_pipewire_get_properties(obs_pipewire_data *obs_pw, + const char *reload_string_id) +{ + obs_properties_t *properties; + + properties = obs_properties_create(); + obs_properties_add_button2(properties, "Reload", + obs_module_text(reload_string_id), + reload_session_cb, obs_pw); + obs_properties_add_bool(properties, "ShowCursor", + obs_module_text("ShowCursor")); + + return properties; +} + +void obs_pipewire_update(obs_pipewire_data *obs_pw, obs_data_t *settings) +{ + obs_pw->cursor.visible = obs_data_get_bool(settings, "ShowCursor"); +} + +void obs_pipewire_show(obs_pipewire_data *obs_pw) +{ + if (obs_pw->stream) + pw_stream_set_active(obs_pw->stream, true); +} + +void obs_pipewire_hide(obs_pipewire_data *obs_pw) +{ + if (obs_pw->stream) + pw_stream_set_active(obs_pw->stream, false); +} + +uint32_t obs_pipewire_get_width(obs_pipewire_data *obs_pw) +{ + if (!obs_pw->negotiated) + return 0; + + if (obs_pw->crop.valid) + return obs_pw->crop.width; + else + return obs_pw->format.info.raw.size.width; +} + +uint32_t obs_pipewire_get_height(obs_pipewire_data *obs_pw) +{ + if (!obs_pw->negotiated) + return 0; + + if (obs_pw->crop.valid) + return obs_pw->crop.height; + else + return obs_pw->format.info.raw.size.height; +} + +void obs_pipewire_video_render(obs_pipewire_data *obs_pw, gs_effect_t *effect) +{ + gs_eparam_t *image; + + if (!obs_pw->texture) + return; + + image = gs_effect_get_param_by_name(effect, "image"); + gs_effect_set_texture(image, obs_pw->texture); + + if (has_effective_crop(obs_pw)) { + gs_draw_sprite_subregion(obs_pw->texture, 0, obs_pw->crop.x, + obs_pw->crop.y, + obs_pw->crop.x + obs_pw->crop.width, + obs_pw->crop.y + obs_pw->crop.height); + } else { + gs_draw_sprite(obs_pw->texture, 0, 0, 0); + } + + if (obs_pw->cursor.visible && obs_pw->cursor.valid && + obs_pw->cursor.texture) { + gs_matrix_push(); + gs_matrix_translate3f((float)obs_pw->cursor.x, + (float)obs_pw->cursor.y, 0.0f); + + gs_effect_set_texture(image, obs_pw->cursor.texture); + gs_draw_sprite(obs_pw->texture, 0, obs_pw->cursor.width, + obs_pw->cursor.height); + + gs_matrix_pop(); + } +} + +enum obs_pw_capture_type +obs_pipewire_get_capture_type(obs_pipewire_data *obs_pw) +{ + return obs_pw->capture_type; +} diff --git a/plugins/linux-capture/pipewire.h b/plugins/linux-capture/pipewire.h new file mode 100644 index 00000000000000..975a1df64943eb --- /dev/null +++ b/plugins/linux-capture/pipewire.h @@ -0,0 +1,53 @@ +/* pipewire.h + * + * Copyright 2020 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#pragma once + +#include +#include + +typedef struct _obs_pipewire_data obs_pipewire_data; + +enum obs_pw_capture_type { + DESKTOP_CAPTURE = 1, + WINDOW_CAPTURE = 2, +}; + +void *obs_pipewire_create(enum obs_pw_capture_type capture_type, + obs_data_t *settings, obs_source_t *source); + +void obs_pipewire_destroy(obs_pipewire_data *obs_pw); + +void obs_pipewire_get_defaults(obs_data_t *settings); + +obs_properties_t *obs_pipewire_get_properties(obs_pipewire_data *obs_pw, + const char *reload_string_id); + +void obs_pipewire_update(obs_pipewire_data *obs_pw, obs_data_t *settings); + +void obs_pipewire_show(obs_pipewire_data *obs_pw); + +void obs_pipewire_hide(obs_pipewire_data *obs_pw); +uint32_t obs_pipewire_get_width(obs_pipewire_data *obs_pw); +uint32_t obs_pipewire_get_height(obs_pipewire_data *obs_pw); +void obs_pipewire_video_render(obs_pipewire_data *obs_pw, gs_effect_t *effect); + +enum obs_pw_capture_type +obs_pipewire_get_capture_type(obs_pipewire_data *obs_pw); From 10a1a708fb001ebd9a4e69097eba91648acaf9c7 Mon Sep 17 00:00:00 2001 From: Georges Basile Stavracas Neto Date: Tue, 23 Mar 2021 12:56:54 -0300 Subject: [PATCH 5/7] linux-capture: Return different descriptions for different captures Use the current description for X11 / GLX, and a new description for all PipeWire-based captures (EGL/X11 and EGL/Wayland) --- plugins/linux-capture/linux-capture.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/plugins/linux-capture/linux-capture.c b/plugins/linux-capture/linux-capture.c index 6797d949232ec6..5c656d3e0b7238 100644 --- a/plugins/linux-capture/linux-capture.c +++ b/plugins/linux-capture/linux-capture.c @@ -25,7 +25,12 @@ OBS_DECLARE_MODULE() OBS_MODULE_USE_DEFAULT_LOCALE("linux-xshm", "en-US") MODULE_EXPORT const char *obs_module_description(void) { - return "xcomposite/xshm based window/screen capture for X11"; +#ifdef ENABLE_PIPEWIRE + if (obs_get_nix_platform() != OBS_NIX_PLATFORM_X11_GLX) + return "PipeWire based window/screen capture for X11 and Wayland"; + else +#endif + return "xcomposite/xshm based window/screen capture for X11"; } extern struct obs_source_info xshm_input; From 706192b551c4aac741ba145a51b79ffabde62041 Mon Sep 17 00:00:00 2001 From: Georges Basile Stavracas Neto Date: Mon, 15 Mar 2021 15:12:02 -0300 Subject: [PATCH 6/7] flatpak: Expose PipeWire socket So that it can be talked to from the sandbox. --- CI/flatpak/com.obsproject.Studio.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CI/flatpak/com.obsproject.Studio.json b/CI/flatpak/com.obsproject.Studio.json index a49fc771ed2bab..faa2931e394e45 100644 --- a/CI/flatpak/com.obsproject.Studio.json +++ b/CI/flatpak/com.obsproject.Studio.json @@ -11,7 +11,7 @@ "--device=all", "--share=network", "--share=ipc", - "--filesystem=xdg-run/obs-xdg-portal:create", + "--filesystem=xdg-run/pipewire-0", "--filesystem=host", "--talk-name=org.kde.StatusNotifierWatcher", "--talk-name=org.freedesktop.ScreenSaver", From d0681b6db8201d5e76004fdfc4919660f0232ab6 Mon Sep 17 00:00:00 2001 From: Georges Basile Stavracas Neto Date: Mon, 15 Mar 2021 15:13:56 -0300 Subject: [PATCH 7/7] CI: Disable PipeWire on Ubuntu Unfortunately, neither Ubuntu 20.04 nor 18.04 have a recent enough PipeWire package. Disable the PipeWire bits of linux-capture there. The Flatpak workflow is still able to build it, so keep it enabled there. --- .github/workflows/main.yml | 2 +- CI/before-script-linux.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 48a9f2a8180f65..120235dcabf585 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -444,7 +444,7 @@ jobs: run: | mkdir ./build cd ./build - cmake -DUNIX_STRUCTURE=0 -DCMAKE_INSTALL_PREFIX="${{ github.workspace }}/obs-studio-portable" -DENABLE_UNIT_TESTS=ON -DENABLE_VLC=ON -DWITH_RTMPS=ON -DBUILD_BROWSER=ON -DCEF_ROOT_DIR="${{ github.workspace }}/cmbuild/cef_binary_${{ env.LINUX_CEF_BUILD_VERSION }}_linux64" -DTWITCH_CLIENTID='${{ env.TWITCH_CLIENTID }}' -DTWITCH_HASH='${{ env.TWITCH_HASH }}' -DRESTREAM_CLIENTID='${{ env.RESTREAM_CLIENTID }}' -DRESTREAM_HASH='${{ env.RESTREAM_HASH }}' .. + cmake -DENABLE_PIPEWIRE=OFF -DUNIX_STRUCTURE=0 -DCMAKE_INSTALL_PREFIX="${{ github.workspace }}/obs-studio-portable" -DENABLE_UNIT_TESTS=ON -DENABLE_VLC=ON -DWITH_RTMPS=ON -DBUILD_BROWSER=ON -DCEF_ROOT_DIR="${{ github.workspace }}/cmbuild/cef_binary_${{ env.LINUX_CEF_BUILD_VERSION }}_linux64" -DTWITCH_CLIENTID='${{ env.TWITCH_CLIENTID }}' -DTWITCH_HASH='${{ env.TWITCH_HASH }}' -DRESTREAM_CLIENTID='${{ env.RESTREAM_CLIENTID }}' -DRESTREAM_HASH='${{ env.RESTREAM_HASH }}' .. - name: 'Build' shell: bash working-directory: ${{ github.workspace }}/build diff --git a/CI/before-script-linux.sh b/CI/before-script-linux.sh index 9d805294510e33..397810ef2eac9d 100755 --- a/CI/before-script-linux.sh +++ b/CI/before-script-linux.sh @@ -3,4 +3,4 @@ set -ex ccache -s || echo "CCache is not available." mkdir build && cd build -cmake -DBUILD_BROWSER=ON -DCEF_ROOT_DIR="../cef_binary_${LINUX_CEF_BUILD_VERSION}_linux64" .. +cmake -DENABLE_PIPEWIRE=OFF -DBUILD_BROWSER=ON -DCEF_ROOT_DIR="../cef_binary_${LINUX_CEF_BUILD_VERSION}_linux64" ..