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" .. 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", 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) 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/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 diff --git a/plugins/linux-capture/CMakeLists.txt b/plugins/linux-capture/CMakeLists.txt index 7f876954c95f71..65d811ff7780e2 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,53 @@ target_link_libraries(linux-capture ${X11_Xcomposite_LIB} ${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} +) +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) 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..5c656d3e0b7238 100644 --- a/plugins/linux-capture/linux-capture.c +++ b/plugins/linux-capture/linux-capture.c @@ -17,11 +17,20 @@ 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) { - 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; @@ -31,17 +40,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);