From a340748c0635c3655762c4e2b7f3d2c9f369e0b0 Mon Sep 17 00:00:00 2001 From: Wim Taymans Date: Tue, 7 May 2024 18:26:00 +0200 Subject: [PATCH] camera: add PipeWire camera support The PipeWire camera will enumerate the pipewire Video/Source nodes with their formats. When capturing is started, a stream to the node will be created and frames will be captured. --- cmake/sdlchecks.cmake | 3 + include/build_config/SDL_build_config.h.cmake | 2 + src/camera/SDL_camera.c | 3 + src/camera/SDL_syscamera.h | 1 + src/camera/pipewire/SDL_camera_pipewire.c | 1181 +++++++++++++++++ 5 files changed, 1190 insertions(+) create mode 100644 src/camera/pipewire/SDL_camera_pipewire.c diff --git a/cmake/sdlchecks.cmake b/cmake/sdlchecks.cmake index 181e75161..85816a2c5 100644 --- a/cmake/sdlchecks.cmake +++ b/cmake/sdlchecks.cmake @@ -139,13 +139,16 @@ macro(CheckPipewire) if(PC_PIPEWIRE_FOUND) set(HAVE_PIPEWIRE TRUE) sdl_glob_sources("${SDL3_SOURCE_DIR}/src/audio/pipewire/*.c") + sdl_glob_sources("${SDL3_SOURCE_DIR}/src/camera/pipewire/*.c") set(SDL_AUDIO_DRIVER_PIPEWIRE 1) + set(SDL_CAMERA_DRIVER_PIPEWIRE 1) if(SDL_PIPEWIRE_SHARED AND NOT HAVE_SDL_LOADSO) message(WARNING "You must have SDL_LoadObject() support for dynamic PipeWire loading") endif() FindLibraryAndSONAME("pipewire-0.3" LIBDIRS ${PC_PIPEWIRE_LIBRARY_DIRS}) if(SDL_PIPEWIRE_SHARED AND PIPEWIRE_0.3_LIB AND HAVE_SDL_LOADSO) set(SDL_AUDIO_DRIVER_PIPEWIRE_DYNAMIC "\"${PIPEWIRE_0.3_LIB_SONAME}\"") + set(SDL_CAMERA_DRIVER_PIPEWIRE_DYNAMIC "\"${PIPEWIRE_0.3_LIB_SONAME}\"") set(HAVE_PIPEWIRE_SHARED TRUE) sdl_link_dependency(pipewire INCLUDES $) else() diff --git a/include/build_config/SDL_build_config.h.cmake b/include/build_config/SDL_build_config.h.cmake index b30699bfa..cf9e837fd 100644 --- a/include/build_config/SDL_build_config.h.cmake +++ b/include/build_config/SDL_build_config.h.cmake @@ -485,6 +485,8 @@ #cmakedefine SDL_CAMERA_DRIVER_ANDROID @SDL_CAMERA_DRIVER_ANDROID@ #cmakedefine SDL_CAMERA_DRIVER_EMSCRIPTEN @SDL_CAMERA_DRIVER_EMSCRIPTEN@ #cmakedefine SDL_CAMERA_DRIVER_MEDIAFOUNDATION @SDL_CAMERA_DRIVER_MEDIAFOUNDATION@ +#cmakedefine SDL_CAMERA_DRIVER_PIPEWIRE @SDL_CAMERA_DRIVER_PIPEWIRE@ +#cmakedefine SDL_CAMERA_DRIVER_PIPEWIRE_DYNAMIC @SDL_CAMERA_DRIVER_PIPEWIRE_DYNAMIC@ /* Enable misc subsystem */ #cmakedefine SDL_MISC_DUMMY @SDL_MISC_DUMMY@ diff --git a/src/camera/SDL_camera.c b/src/camera/SDL_camera.c index a9d19fc62..aaa9e9b41 100644 --- a/src/camera/SDL_camera.c +++ b/src/camera/SDL_camera.c @@ -34,6 +34,9 @@ static const CameraBootStrap *const bootstrap[] = { #ifdef SDL_CAMERA_DRIVER_V4L2 &V4L2_bootstrap, #endif +#ifdef SDL_CAMERA_DRIVER_PIPEWIRE + &PIPEWIRECAMERA_bootstrap, +#endif #ifdef SDL_CAMERA_DRIVER_COREMEDIA &COREMEDIA_bootstrap, #endif diff --git a/src/camera/SDL_syscamera.h b/src/camera/SDL_syscamera.h index 335ce4b18..ace3a5649 100644 --- a/src/camera/SDL_syscamera.h +++ b/src/camera/SDL_syscamera.h @@ -205,6 +205,7 @@ typedef struct CameraBootStrap // Not all of these are available in a given build. Use #ifdefs, etc. extern CameraBootStrap DUMMYCAMERA_bootstrap; extern CameraBootStrap V4L2_bootstrap; +extern CameraBootStrap PIPEWIRECAMERA_bootstrap; extern CameraBootStrap COREMEDIA_bootstrap; extern CameraBootStrap ANDROIDCAMERA_bootstrap; extern CameraBootStrap EMSCRIPTENCAMERA_bootstrap; diff --git a/src/camera/pipewire/SDL_camera_pipewire.c b/src/camera/pipewire/SDL_camera_pipewire.c new file mode 100644 index 000000000..b2d6b8481 --- /dev/null +++ b/src/camera/pipewire/SDL_camera_pipewire.c @@ -0,0 +1,1181 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2023 Sam Lantinga + Copyright (C) 2024 Wim Taymans + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +#ifdef SDL_CAMERA_DRIVER_PIPEWIRE + +#include "../SDL_syscamera.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#define PW_POD_BUFFER_LENGTH 1024 +#define PW_THREAD_NAME_BUFFER_LENGTH 128 +#define PW_MAX_IDENTIFIER_LENGTH 256 + +enum PW_READY_FLAGS +{ + PW_READY_FLAG_BUFFER_ADDED = 0x1, + PW_READY_FLAG_STREAM_READY = 0x2, + PW_READY_FLAG_ALL_BITS = 0x3 +}; + +#define PW_ID_TO_HANDLE(x) (void *)((uintptr_t)x) +#define PW_HANDLE_TO_ID(x) (uint32_t)((uintptr_t)x) + +static SDL_bool pipewire_initialized = SDL_FALSE; + +// Pipewire entry points +static const char *(*PIPEWIRE_pw_get_library_version)(void); +static void (*PIPEWIRE_pw_init)(int *, char ***); +static void (*PIPEWIRE_pw_deinit)(void); +static struct pw_main_loop *(*PIPEWIRE_pw_main_loop_new)(struct pw_main_loop *loop); +static struct pw_loop *(*PIPEWIRE_pw_main_loop_get_loop)(struct pw_main_loop *loop); +static int (*PIPEWIRE_pw_main_loop_run)(struct pw_main_loop *loop); +static int (*PIPEWIRE_pw_main_loop_quit)(struct pw_main_loop *loop); +static void(*PIPEWIRE_pw_main_loop_destroy)(struct pw_main_loop *loop); +static struct pw_thread_loop *(*PIPEWIRE_pw_thread_loop_new)(const char *, const struct spa_dict *); +static void (*PIPEWIRE_pw_thread_loop_destroy)(struct pw_thread_loop *); +static void (*PIPEWIRE_pw_thread_loop_stop)(struct pw_thread_loop *); +static struct pw_loop *(*PIPEWIRE_pw_thread_loop_get_loop)(struct pw_thread_loop *); +static void (*PIPEWIRE_pw_thread_loop_lock)(struct pw_thread_loop *); +static void (*PIPEWIRE_pw_thread_loop_unlock)(struct pw_thread_loop *); +static void (*PIPEWIRE_pw_thread_loop_signal)(struct pw_thread_loop *, bool); +static void (*PIPEWIRE_pw_thread_loop_wait)(struct pw_thread_loop *); +static int (*PIPEWIRE_pw_thread_loop_start)(struct pw_thread_loop *); +static struct pw_context *(*PIPEWIRE_pw_context_new)(struct pw_loop *, struct pw_properties *, size_t); +static void (*PIPEWIRE_pw_context_destroy)(struct pw_context *); +static struct pw_core *(*PIPEWIRE_pw_context_connect)(struct pw_context *, struct pw_properties *, size_t); +static void (*PIPEWIRE_pw_proxy_add_object_listener)(struct pw_proxy *, struct spa_hook *, const void *, void *); +static void (*PIPEWIRE_pw_proxy_add_listener)(struct pw_proxy *, struct spa_hook *, const void *, void *); +static void *(*PIPEWIRE_pw_proxy_get_user_data)(struct pw_proxy *); +static void (*PIPEWIRE_pw_proxy_destroy)(struct pw_proxy *); +static int (*PIPEWIRE_pw_core_disconnect)(struct pw_core *); +static struct pw_node_info * (*PIPEWIRE_pw_node_info_merge)(struct pw_node_info *info, const struct pw_node_info *update, bool reset); +static void (*PIPEWIRE_pw_node_info_free)(struct pw_node_info *info); +static struct pw_stream *(*PIPEWIRE_pw_stream_new)(struct pw_core *, const char *, struct pw_properties *); +static void (*PIPEWIRE_pw_stream_add_listener)(struct pw_stream *stream, struct spa_hook *listener, + const struct pw_stream_events *events, void *data); +static void (*PIPEWIRE_pw_stream_destroy)(struct pw_stream *); +static int (*PIPEWIRE_pw_stream_connect)(struct pw_stream *, enum pw_direction, uint32_t, enum pw_stream_flags, + const struct spa_pod **, uint32_t); +static enum pw_stream_state (*PIPEWIRE_pw_stream_get_state)(struct pw_stream *stream, const char **error); +static struct pw_buffer *(*PIPEWIRE_pw_stream_dequeue_buffer)(struct pw_stream *); +static int (*PIPEWIRE_pw_stream_queue_buffer)(struct pw_stream *, struct pw_buffer *); +static struct pw_properties *(*PIPEWIRE_pw_properties_new)(const char *, ...)SPA_SENTINEL; +static struct pw_properties *(*PIPEWIRE_pw_properties_new_dict)(const struct spa_dict *dict); +static int (*PIPEWIRE_pw_properties_set)(struct pw_properties *, const char *, const char *); +static int (*PIPEWIRE_pw_properties_setf)(struct pw_properties *, const char *, const char *, ...) SPA_PRINTF_FUNC(3, 4); + +#ifdef SDL_AUDIO_DRIVER_PIPEWIRE_DYNAMIC + +static const char *pipewire_library = SDL_AUDIO_DRIVER_PIPEWIRE_DYNAMIC; +static void *pipewire_handle = NULL; + +static int pipewire_dlsym(const char *fn, void **addr) +{ + *addr = SDL_LoadFunction(pipewire_handle, fn); + if (!*addr) { + // Don't call SDL_SetError(): SDL_LoadFunction already did. + return 0; + } + + return 1; +} + +#define SDL_PIPEWIRE_SYM(x) \ + if (!pipewire_dlsym(#x, (void **)(char *)&PIPEWIRE_##x)) { \ + return -1; \ + } + +static int load_pipewire_library(void) +{ + pipewire_handle = SDL_LoadObject(pipewire_library); + return pipewire_handle ? 0 : -1; +} + +static void unload_pipewire_library(void) +{ + if (pipewire_handle) { + SDL_UnloadObject(pipewire_handle); + pipewire_handle = NULL; + } +} + +#else + +#define SDL_PIPEWIRE_SYM(x) PIPEWIRE_##x = x + +static int load_pipewire_library(void) +{ + return 0; +} + +static void unload_pipewire_library(void) +{ + // Nothing to do +} + +#endif // SDL_AUDIO_DRIVER_PIPEWIRE_DYNAMIC + +static int load_pipewire_syms(void) +{ + SDL_PIPEWIRE_SYM(pw_get_library_version); + SDL_PIPEWIRE_SYM(pw_init); + SDL_PIPEWIRE_SYM(pw_deinit); + SDL_PIPEWIRE_SYM(pw_main_loop_new); + SDL_PIPEWIRE_SYM(pw_main_loop_get_loop); + SDL_PIPEWIRE_SYM(pw_main_loop_run); + SDL_PIPEWIRE_SYM(pw_main_loop_quit); + SDL_PIPEWIRE_SYM(pw_main_loop_destroy); + SDL_PIPEWIRE_SYM(pw_thread_loop_new); + SDL_PIPEWIRE_SYM(pw_thread_loop_destroy); + SDL_PIPEWIRE_SYM(pw_thread_loop_stop); + SDL_PIPEWIRE_SYM(pw_thread_loop_get_loop); + SDL_PIPEWIRE_SYM(pw_thread_loop_lock); + SDL_PIPEWIRE_SYM(pw_thread_loop_unlock); + SDL_PIPEWIRE_SYM(pw_thread_loop_signal); + SDL_PIPEWIRE_SYM(pw_thread_loop_wait); + SDL_PIPEWIRE_SYM(pw_thread_loop_start); + SDL_PIPEWIRE_SYM(pw_context_new); + SDL_PIPEWIRE_SYM(pw_context_destroy); + SDL_PIPEWIRE_SYM(pw_context_connect); + SDL_PIPEWIRE_SYM(pw_proxy_add_listener); + SDL_PIPEWIRE_SYM(pw_proxy_add_object_listener); + SDL_PIPEWIRE_SYM(pw_proxy_get_user_data); + SDL_PIPEWIRE_SYM(pw_proxy_destroy); + SDL_PIPEWIRE_SYM(pw_core_disconnect); + SDL_PIPEWIRE_SYM(pw_node_info_merge); + SDL_PIPEWIRE_SYM(pw_node_info_free); + SDL_PIPEWIRE_SYM(pw_stream_new); + SDL_PIPEWIRE_SYM(pw_stream_add_listener); + SDL_PIPEWIRE_SYM(pw_stream_destroy); + SDL_PIPEWIRE_SYM(pw_stream_connect); + SDL_PIPEWIRE_SYM(pw_stream_get_state); + SDL_PIPEWIRE_SYM(pw_stream_dequeue_buffer); + SDL_PIPEWIRE_SYM(pw_stream_queue_buffer); + SDL_PIPEWIRE_SYM(pw_properties_new); + SDL_PIPEWIRE_SYM(pw_properties_new_dict); + SDL_PIPEWIRE_SYM(pw_properties_set); + SDL_PIPEWIRE_SYM(pw_properties_setf); + + return 0; +} + +/* When in a container, the library version can differ from the underlying core version, + * so make sure the underlying Pipewire implementation meets the version requirement. + */ +struct version_data +{ + struct pw_main_loop *loop; + int major, minor, patch; + int seq; +}; + +static void version_check_core_info_callback(void *data, const struct pw_core_info *info) +{ + struct version_data *v = data; + + if (SDL_sscanf(info->version, "%d.%d.%d", &v->major, &v->minor, &v->patch) < 3) { + v->major = 0; + v->minor = 0; + v->patch = 0; + } +} + +static void version_check_core_done_callback(void *data, uint32_t id, int seq) +{ + struct version_data *v = data; + + if (id == PW_ID_CORE && v->seq == seq) { + PIPEWIRE_pw_main_loop_quit(v->loop); + } +} + +static const struct pw_core_events version_check_core_events = +{ + .version = PW_VERSION_CORE_EVENTS, + .info = version_check_core_info_callback, + .done = version_check_core_done_callback +}; + +static SDL_bool pipewire_core_version_at_least(int major, int minor, int patch) +{ + struct pw_main_loop *loop = NULL; + struct pw_context *context = NULL; + struct pw_core *core = NULL; + struct version_data version_data; + struct spa_hook core_listener; + SDL_bool ret = SDL_FALSE; + + loop = PIPEWIRE_pw_main_loop_new(NULL); + if (!loop) { + goto done; + } + + context = PIPEWIRE_pw_context_new(PIPEWIRE_pw_main_loop_get_loop(loop), NULL, 0); + if (!context) { + goto done; + } + + core = PIPEWIRE_pw_context_connect(context, NULL, 0); + if (!core) { + goto done; + } + + /* Attach a core listener and get the version. */ + spa_zero(version_data); + version_data.loop = loop; + pw_core_add_listener(core, &core_listener, &version_check_core_events, &version_data); + version_data.seq = pw_core_sync(core, PW_ID_CORE, 0); + + PIPEWIRE_pw_main_loop_run(loop); + + spa_hook_remove(&core_listener); + + ret = (version_data.major >= major) && + (version_data.major > major || version_data.minor >= minor) && + (version_data.major > major || version_data.minor > minor || version_data.patch >= patch); + +done: + if (core) { + PIPEWIRE_pw_core_disconnect(core); + } + if (context) { + PIPEWIRE_pw_context_destroy(context); + } + if (loop) { + PIPEWIRE_pw_main_loop_destroy(loop); + } + + return ret; +} + +static int init_pipewire_library(SDL_bool check_preferred_version) +{ + if (!load_pipewire_library()) { + if (!load_pipewire_syms()) { + PIPEWIRE_pw_init(NULL, NULL); + + if (!check_preferred_version || pipewire_core_version_at_least(1, 0, 0)) { + return 0; + } + } + } + + return -1; +} + +static void deinit_pipewire_library(void) +{ + PIPEWIRE_pw_deinit(); + unload_pipewire_library(); +} + +// The global hotplug thread and associated objects. +static struct +{ + struct pw_thread_loop *loop; + + struct pw_context *context; + + struct pw_core *core; + struct spa_hook core_listener; + int last_seq; + int pending_seq; + + struct pw_registry *registry; + struct spa_hook registry_listener; + + struct spa_list global_list; + + SDL_bool init_complete; + SDL_bool events_enabled; +} hotplug; + +struct global +{ + struct spa_list link; + + const struct global_class *class; + + uint32_t id; + uint32_t permissions; + struct pw_properties *props; + + char *name; + + struct pw_proxy *proxy; + struct spa_hook proxy_listener; + struct spa_hook object_listener; + + int changed; + void *info; + struct spa_list pending_list; + struct spa_list param_list; + + SDL_bool added; +}; + +struct global_class +{ + const char *type; + uint32_t version; + const void *events; + int (*init) (struct global *g); + void (*destroy) (struct global *g); +}; + +struct param { + uint32_t id; + int32_t seq; + struct spa_list link; + struct spa_pod *param; +}; + +static uint32_t param_clear(struct spa_list *param_list, uint32_t id) +{ + struct param *p, *t; + uint32_t count = 0; + + spa_list_for_each_safe(p, t, param_list, link) { + if (id == SPA_ID_INVALID || p->id == id) { + spa_list_remove(&p->link); + free(p); + count++; + } + } + return count; +} + +static struct param *param_add(struct spa_list *params, + int seq, uint32_t id, const struct spa_pod *param) +{ + struct param *p; + + if (id == SPA_ID_INVALID) { + if (param == NULL || !spa_pod_is_object(param)) { + errno = EINVAL; + return NULL; + } + id = SPA_POD_OBJECT_ID(param); + } + + p = malloc(sizeof(*p) + (param != NULL ? SPA_POD_SIZE(param) : 0)); + if (p == NULL) + return NULL; + + p->id = id; + p->seq = seq; + if (param != NULL) { + p->param = SPA_PTROFF(p, sizeof(*p), struct spa_pod); + memcpy(p->param, param, SPA_POD_SIZE(param)); + } else { + param_clear(params, id); + p->param = NULL; + } + spa_list_append(params, &p->link); + + return p; +} + +static void param_update(struct spa_list *param_list, struct spa_list *pending_list, + uint32_t n_params, struct spa_param_info *params) +{ + struct param *p, *t; + uint32_t i; + + for (i = 0; i < n_params; i++) { + spa_list_for_each_safe(p, t, pending_list, link) { + if (p->id == params[i].id && + p->seq != params[i].seq && + p->param != NULL) { + spa_list_remove(&p->link); + free(p); + } + } + } + spa_list_consume(p, pending_list, link) { + spa_list_remove(&p->link); + if (p->param == NULL) { + param_clear(param_list, p->id); + free(p); + } else { + spa_list_append(param_list, &p->link); + } + } +} + +static struct { + Uint32 format; + uint32_t id; +} sdl_video_formats[] = { +#if SDL_BYTEORDER == SDL_BIG_ENDIAN + { SDL_PIXELFORMAT_UNKNOWN, SPA_VIDEO_FORMAT_UNKNOWN,}, + { SDL_PIXELFORMAT_INDEX1LSB, SPA_VIDEO_FORMAT_UNKNOWN,}, + { SDL_PIXELFORMAT_UNKNOWN, SPA_VIDEO_FORMAT_UNKNOWN,}, + { SDL_PIXELFORMAT_INDEX1LSB, SPA_VIDEO_FORMAT_UNKNOWN,}, + { SDL_PIXELFORMAT_INDEX1MSB, SPA_VIDEO_FORMAT_UNKNOWN,}, + { SDL_PIXELFORMAT_INDEX4LSB, SPA_VIDEO_FORMAT_UNKNOWN,}, + { SDL_PIXELFORMAT_INDEX4MSB, SPA_VIDEO_FORMAT_UNKNOWN,}, + { SDL_PIXELFORMAT_INDEX8, SPA_VIDEO_FORMAT_UNKNOWN,}, + { SDL_PIXELFORMAT_RGB332, SPA_VIDEO_FORMAT_UNKNOWN,}, + { SDL_PIXELFORMAT_RGB444, SPA_VIDEO_FORMAT_UNKNOWN,}, + { SDL_PIXELFORMAT_RGB555, SPA_VIDEO_FORMAT_UNKNOWN,}, + { SDL_PIXELFORMAT_BGR555, SPA_VIDEO_FORMAT_UNKNOWN,}, + { SDL_PIXELFORMAT_ARGB4444, SPA_VIDEO_FORMAT_UNKNOWN,}, + { SDL_PIXELFORMAT_RGBA4444, SPA_VIDEO_FORMAT_UNKNOWN,}, + { SDL_PIXELFORMAT_ABGR4444, SPA_VIDEO_FORMAT_UNKNOWN,}, + { SDL_PIXELFORMAT_BGRA4444, SPA_VIDEO_FORMAT_UNKNOWN,}, + { SDL_PIXELFORMAT_ARGB1555, SPA_VIDEO_FORMAT_UNKNOWN,}, + { SDL_PIXELFORMAT_RGBA5551, SPA_VIDEO_FORMAT_UNKNOWN,}, + { SDL_PIXELFORMAT_ABGR1555, SPA_VIDEO_FORMAT_UNKNOWN,}, + { SDL_PIXELFORMAT_BGRA5551, SPA_VIDEO_FORMAT_UNKNOWN,}, + { SDL_PIXELFORMAT_RGB565, SPA_VIDEO_FORMAT_UNKNOWN,}, + { SDL_PIXELFORMAT_BGR565, SPA_VIDEO_FORMAT_UNKNOWN,}, + { SDL_PIXELFORMAT_RGB24, SPA_VIDEO_FORMAT_RGB,}, + { SDL_PIXELFORMAT_RGBX8888, SPA_VIDEO_FORMAT_RGBx,}, + { SDL_PIXELFORMAT_BGR24, SPA_VIDEO_FORMAT_BGR,}, + { SDL_PIXELFORMAT_BGR888, SPA_VIDEO_FORMAT_BGR,}, + { SDL_PIXELFORMAT_BGRX8888, SPA_VIDEO_FORMAT_BGRx,}, + { SDL_PIXELFORMAT_ARGB2101010, SPA_VIDEO_FORMAT_UNKNOWN,}, + { SDL_PIXELFORMAT_RGBA8888, SPA_VIDEO_FORMAT_RGBA,}, + { SDL_PIXELFORMAT_ARGB8888, SPA_VIDEO_FORMAT_ARGB,}, + { SDL_PIXELFORMAT_BGRA8888, SPA_VIDEO_FORMAT_BGRA,}, + { SDL_PIXELFORMAT_ABGR8888, SPA_VIDEO_FORMAT_ABGR,}, + { SDL_PIXELFORMAT_YV12, SPA_VIDEO_FORMAT_YV12,}, + { SDL_PIXELFORMAT_IYUV, SPA_VIDEO_FORMAT_I420,}, + { SDL_PIXELFORMAT_YUY2, SPA_VIDEO_FORMAT_YUY2,}, + { SDL_PIXELFORMAT_UYVY, SPA_VIDEO_FORMAT_UYVY,}, + { SDL_PIXELFORMAT_YVYU, SPA_VIDEO_FORMAT_YVYU,}, +#if SDL_VERSION_ATLEAST(2,0,4) + { SDL_PIXELFORMAT_NV12, SPA_VIDEO_FORMAT_NV12,}, + { SDL_PIXELFORMAT_NV21, SPA_VIDEO_FORMAT_NV21,}, +#endif +#else + { SDL_PIXELFORMAT_UNKNOWN, SPA_VIDEO_FORMAT_UNKNOWN,}, + { SDL_PIXELFORMAT_INDEX1LSB, SPA_VIDEO_FORMAT_UNKNOWN,}, + { SDL_PIXELFORMAT_UNKNOWN, SPA_VIDEO_FORMAT_UNKNOWN,}, + { SDL_PIXELFORMAT_INDEX1LSB, SPA_VIDEO_FORMAT_UNKNOWN,}, + { SDL_PIXELFORMAT_INDEX1MSB, SPA_VIDEO_FORMAT_UNKNOWN,}, + { SDL_PIXELFORMAT_INDEX4LSB, SPA_VIDEO_FORMAT_UNKNOWN,}, + { SDL_PIXELFORMAT_INDEX4MSB, SPA_VIDEO_FORMAT_UNKNOWN,}, + { SDL_PIXELFORMAT_INDEX8, SPA_VIDEO_FORMAT_UNKNOWN,}, + { SDL_PIXELFORMAT_RGB332, SPA_VIDEO_FORMAT_UNKNOWN,}, + { SDL_PIXELFORMAT_RGB444, SPA_VIDEO_FORMAT_UNKNOWN,}, + { SDL_PIXELFORMAT_RGB555, SPA_VIDEO_FORMAT_UNKNOWN,}, + { SDL_PIXELFORMAT_BGR555, SPA_VIDEO_FORMAT_UNKNOWN,}, + { SDL_PIXELFORMAT_ARGB4444, SPA_VIDEO_FORMAT_UNKNOWN,}, + { SDL_PIXELFORMAT_RGBA4444, SPA_VIDEO_FORMAT_UNKNOWN,}, + { SDL_PIXELFORMAT_ABGR4444, SPA_VIDEO_FORMAT_UNKNOWN,}, + { SDL_PIXELFORMAT_BGRA4444, SPA_VIDEO_FORMAT_UNKNOWN,}, + { SDL_PIXELFORMAT_ARGB1555, SPA_VIDEO_FORMAT_UNKNOWN,}, + { SDL_PIXELFORMAT_RGBA5551, SPA_VIDEO_FORMAT_UNKNOWN,}, + { SDL_PIXELFORMAT_ABGR1555, SPA_VIDEO_FORMAT_UNKNOWN,}, + { SDL_PIXELFORMAT_BGRA5551, SPA_VIDEO_FORMAT_UNKNOWN,}, + { SDL_PIXELFORMAT_RGB565, SPA_VIDEO_FORMAT_UNKNOWN,}, + { SDL_PIXELFORMAT_BGR565, SPA_VIDEO_FORMAT_UNKNOWN,}, + { SDL_PIXELFORMAT_RGB24, SPA_VIDEO_FORMAT_BGR,}, + { SDL_PIXELFORMAT_RGBX8888, SPA_VIDEO_FORMAT_xBGR,}, + { SDL_PIXELFORMAT_BGR24, SPA_VIDEO_FORMAT_RGB,}, + { SDL_PIXELFORMAT_BGRX8888, SPA_VIDEO_FORMAT_xRGB,}, + { SDL_PIXELFORMAT_ARGB2101010, SPA_VIDEO_FORMAT_UNKNOWN,}, + { SDL_PIXELFORMAT_RGBA8888, SPA_VIDEO_FORMAT_ABGR,}, + { SDL_PIXELFORMAT_ARGB8888, SPA_VIDEO_FORMAT_BGRA,}, + { SDL_PIXELFORMAT_BGRA8888, SPA_VIDEO_FORMAT_ARGB,}, + { SDL_PIXELFORMAT_ABGR8888, SPA_VIDEO_FORMAT_RGBA,}, + { SDL_PIXELFORMAT_YV12, SPA_VIDEO_FORMAT_YV12,}, + { SDL_PIXELFORMAT_IYUV, SPA_VIDEO_FORMAT_I420,}, + { SDL_PIXELFORMAT_YUY2, SPA_VIDEO_FORMAT_YUY2,}, + { SDL_PIXELFORMAT_UYVY, SPA_VIDEO_FORMAT_UYVY,}, + { SDL_PIXELFORMAT_YVYU, SPA_VIDEO_FORMAT_YVYU,}, +#if SDL_VERSION_ATLEAST(2,0,4) + { SDL_PIXELFORMAT_NV12, SPA_VIDEO_FORMAT_NV12,}, + { SDL_PIXELFORMAT_NV21, SPA_VIDEO_FORMAT_NV21,}, +#endif +#endif +}; + +static inline uint32_t sdl_format_to_id(Uint32 format) +{ + SPA_FOR_EACH_ELEMENT_VAR(sdl_video_formats, f) { + if (f->format == format) + return f->id; + } + return SPA_VIDEO_FORMAT_UNKNOWN; +} + +static inline Uint32 id_to_sdl_format(uint32_t id) +{ + SPA_FOR_EACH_ELEMENT_VAR(sdl_video_formats, f) { + if (f->id == id) + return f->format; + } + return SDL_PIXELFORMAT_UNKNOWN; +} + +struct SDL_PrivateCameraData +{ + struct pw_stream *stream; + struct spa_hook stream_listener; + + struct pw_array buffers; +}; + +static void on_process(void *data) +{ + PIPEWIRE_pw_thread_loop_signal(hotplug.loop, false); +} + +static void on_stream_state_changed(void *data, enum pw_stream_state old, + enum pw_stream_state state, const char *error) +{ + SDL_CameraDevice *device = data; + switch (state) { + case PW_STREAM_STATE_UNCONNECTED: + break; + case PW_STREAM_STATE_STREAMING: + SDL_CameraDevicePermissionOutcome(device, SDL_TRUE); + break; + default: + break; + } +} + +static void on_stream_param_changed(void *data, uint32_t id, const struct spa_pod *param) +{ +} + +static void on_add_buffer(void *data, struct pw_buffer *buffer) +{ + SDL_CameraDevice *device = data; + pw_array_add_ptr(&device->hidden->buffers, buffer); +} + +static void on_remove_buffer(void *data, struct pw_buffer *buffer) +{ + SDL_CameraDevice *device = data; + struct pw_buffer **p; + pw_array_for_each(p, &device->hidden->buffers) { + if (*p == buffer) { + pw_array_remove(&device->hidden->buffers, p); + return; + } + } +} + +static const struct pw_stream_events stream_events = { + .version = PW_VERSION_STREAM_EVENTS, + .add_buffer = on_add_buffer, + .remove_buffer = on_remove_buffer, + .state_changed = on_stream_state_changed, + .param_changed = on_stream_param_changed, + .process = on_process, +}; + +static int PIPEWIRECAMERA_OpenDevice(SDL_CameraDevice *device, const SDL_CameraSpec *spec) +{ + struct pw_properties *props; + const struct spa_pod *params[3]; + int res, n_params = 0; + uint8_t buffer[1024]; + struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer)); + + if (!device) { + return -1; + } + device->hidden = (struct SDL_PrivateCameraData *) SDL_calloc(1, sizeof (struct SDL_PrivateCameraData)); + if (device->hidden == NULL) { + return -1; + } + pw_array_init(&device->hidden->buffers, 64); + + PIPEWIRE_pw_thread_loop_lock(hotplug.loop); + + props = PIPEWIRE_pw_properties_new(PW_KEY_MEDIA_TYPE, "Video", + PW_KEY_MEDIA_CATEGORY, "Capture", + PW_KEY_MEDIA_ROLE, "Camera", + PW_KEY_TARGET_OBJECT, device->name, + NULL); + if (props == NULL) { + return -1; + } + + device->hidden->stream = PIPEWIRE_pw_stream_new(hotplug.core, + "SDL PipeWire Camera", spa_steal_ptr(props)); + if (device->hidden->stream == NULL) { + return -1; + } + + PIPEWIRE_pw_stream_add_listener(device->hidden->stream, + &device->hidden->stream_listener, + &stream_events, device); + + params[n_params++] = spa_pod_builder_add_object(&b, + 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_Id(sdl_format_to_id(spec->format)), + SPA_FORMAT_VIDEO_size, SPA_POD_Rectangle(&SPA_RECTANGLE(spec->width, spec->height)), + SPA_FORMAT_VIDEO_framerate, + SPA_POD_Fraction(&SPA_FRACTION(spec->interval_numerator, spec->interval_denominator))); + + if ((res = PIPEWIRE_pw_stream_connect(device->hidden->stream, + PW_DIRECTION_INPUT, + PW_ID_ANY, + PW_STREAM_FLAG_AUTOCONNECT | + PW_STREAM_FLAG_MAP_BUFFERS, + params, n_params)) < 0) { + return -1; + } + + PIPEWIRE_pw_thread_loop_unlock(hotplug.loop); + + return 0; +} + +static void PIPEWIRECAMERA_CloseDevice(SDL_CameraDevice *device) +{ + if (!device) { + return; + } + + PIPEWIRE_pw_thread_loop_lock(hotplug.loop); + if (device->hidden) { + if (device->hidden->stream) + PIPEWIRE_pw_stream_destroy(device->hidden->stream); + pw_array_clear(&device->hidden->buffers); + SDL_free(device->hidden); + device->hidden = NULL; + } + PIPEWIRE_pw_thread_loop_unlock(hotplug.loop); +} + +static int PIPEWIRECAMERA_WaitDevice(SDL_CameraDevice *device) +{ + PIPEWIRE_pw_thread_loop_lock(hotplug.loop); + PIPEWIRE_pw_thread_loop_wait(hotplug.loop); + PIPEWIRE_pw_thread_loop_unlock(hotplug.loop); + return 0; +} + +static int PIPEWIRECAMERA_AcquireFrame(SDL_CameraDevice *device, SDL_Surface *frame, Uint64 *timestampNS) +{ + struct pw_buffer *b; + + PIPEWIRE_pw_thread_loop_lock(hotplug.loop); + b = NULL; + while (true) { + struct pw_buffer *t; + if ((t = PIPEWIRE_pw_stream_dequeue_buffer(device->hidden->stream)) == NULL) + break; + if (b) + PIPEWIRE_pw_stream_queue_buffer(device->hidden->stream, b); + b = t; + } + if (b == NULL) { + PIPEWIRE_pw_thread_loop_unlock(hotplug.loop); + return 0; + } + + *timestampNS = b->time; + frame->pixels = b->buffer->datas[0].data; + frame->pitch = b->buffer->datas[0].chunk->stride; + + PIPEWIRE_pw_thread_loop_unlock(hotplug.loop); + + return 1; +} + +static void PIPEWIRECAMERA_ReleaseFrame(SDL_CameraDevice *device, SDL_Surface *frame) +{ + struct pw_buffer **p; + PIPEWIRE_pw_thread_loop_lock(hotplug.loop); + pw_array_for_each(p, &device->hidden->buffers) { + if ((*p)->buffer->datas[0].data == frame->pixels) { + PIPEWIRE_pw_stream_queue_buffer(device->hidden->stream, (*p)); + break; + } + } + PIPEWIRE_pw_thread_loop_unlock(hotplug.loop); +} + +static void collect_rates(CameraFormatAddData *data, struct param *p, const Uint32 sdlfmt, + const struct spa_rectangle *size) +{ + const struct spa_pod_prop *prop; + struct spa_pod * values; + uint32_t i, n_vals, choice; + struct spa_fraction *rates; + + prop = spa_pod_find_prop(p->param, NULL, SPA_FORMAT_VIDEO_framerate); + if (prop == NULL) + return; + + values = spa_pod_get_values(&prop->value, &n_vals, &choice); + if (values->type != SPA_TYPE_Fraction || n_vals == 0) + return; + + rates = SPA_POD_BODY(values); + switch (choice) { + case SPA_CHOICE_None: + n_vals = 1; + SPA_FALLTHROUGH; + case SPA_CHOICE_Enum: + for (i = 0; i < n_vals; i++) { + if (SDL_AddCameraFormat(data, sdlfmt, size->width, size->height, + rates[i].num, rates[i].denom) == -1) { + return; // Probably out of memory; we'll go with what we have, if anything. + } + } + break; + default: + SDL_Log("CAMERA: unimplemented choice:%d", choice); + break; + } +} + +static void collect_size(CameraFormatAddData *data, struct param *p, const Uint32 sdlfmt) +{ + const struct spa_pod_prop *prop; + struct spa_pod * values; + uint32_t i, n_vals, choice; + struct spa_rectangle *rectangles; + + prop = spa_pod_find_prop(p->param, NULL, SPA_FORMAT_VIDEO_size); + if (prop == NULL) + return; + + values = spa_pod_get_values(&prop->value, &n_vals, &choice); + if (values->type != SPA_TYPE_Rectangle || n_vals == 0) + return; + + rectangles = SPA_POD_BODY(values); + switch (choice) { + case SPA_CHOICE_None: + n_vals = 1; + SPA_FALLTHROUGH; + case SPA_CHOICE_Enum: + for (i = 0; i < n_vals; i++) { + collect_rates(data, p, sdlfmt, &rectangles[i]); + } + break; + default: + SDL_Log("CAMERA: unimplemented choice:%d", choice); + break; + } +} + +static void collect_format(CameraFormatAddData *data, struct param *p) +{ + const struct spa_pod_prop *prop; + Uint32 sdlfmt; + struct spa_pod * values; + uint32_t i, n_vals, choice, *ids; + + prop = spa_pod_find_prop(p->param, NULL, SPA_FORMAT_VIDEO_format); + if (prop == NULL) + return; + + values = spa_pod_get_values(&prop->value, &n_vals, &choice); + if (values->type != SPA_TYPE_Id || n_vals == 0) + return; + + ids = SPA_POD_BODY(values); + switch (choice) { + case SPA_CHOICE_None: + n_vals = 1; + SPA_FALLTHROUGH; + case SPA_CHOICE_Enum: + for (i = 0; i < n_vals; i++) { + sdlfmt = id_to_sdl_format(ids[i]); + if (sdlfmt == SDL_PIXELFORMAT_UNKNOWN) + continue; + collect_size(data, p, sdlfmt); + } + break; + default: + SDL_Log("CAMERA: unimplemented choice:%d", choice); + break; + } +} + +static void add_device(struct global *g) +{ + struct param *p; + CameraFormatAddData data; + + SDL_zero(data); + + SDL_Log("CAMERA: found %d", g->id); + + spa_list_for_each(p, &g->param_list, link) { + if (p->id != SPA_PARAM_EnumFormat) + continue; + + collect_format(&data, p); + } + if (data.num_specs > 0) { + SDL_AddCameraDevice(g->name, SDL_CAMERA_POSITION_UNKNOWN, + data.num_specs, data.specs, g); + } + SDL_free(data.specs); + + g->added = SDL_TRUE; +} + +static void PIPEWIRECAMERA_DetectDevices(void) +{ + struct global *g; + + PIPEWIRE_pw_thread_loop_lock(hotplug.loop); + + // Wait until the initial registry enumeration is complete + while (!hotplug.init_complete) { + PIPEWIRE_pw_thread_loop_wait(hotplug.loop); + } + + spa_list_for_each (g, &hotplug.global_list, link) { + if (!g->added) { + add_device(g); + } + } + + hotplug.events_enabled = SDL_TRUE; + + PIPEWIRE_pw_thread_loop_unlock(hotplug.loop); +} + +static void PIPEWIRECAMERA_FreeDeviceHandle(SDL_CameraDevice *device) +{ +} + +static void do_resync(void) +{ + hotplug.pending_seq = pw_core_sync(hotplug.core, PW_ID_CORE, 0); +} + +/** node */ +static void node_event_info(void *object, const struct pw_node_info *info) +{ + struct global *g = object; + uint32_t i; + + info = g->info = PIPEWIRE_pw_node_info_merge(g->info, info, g->changed == 0); + if (info == NULL) + return; + + if (info->change_mask & PW_NODE_CHANGE_MASK_PARAMS) { + for (i = 0; i < info->n_params; i++) { + uint32_t id = info->params[i].id; + int res; + + if (info->params[i].user == 0) + continue; + info->params[i].user = 0; + + if (id != SPA_PARAM_EnumFormat) + continue; + + param_add(&g->pending_list, info->params[i].seq, id, NULL); + if (!(info->params[i].flags & SPA_PARAM_INFO_READ)) + continue; + + res = pw_node_enum_params((struct pw_node*)g->proxy, + ++info->params[i].seq, id, 0, -1, NULL); + if (SPA_RESULT_IS_ASYNC(res)) + info->params[i].seq = res; + + g->changed++; + } + } + do_resync(); +} + +static void node_event_param(void *object, int seq, + uint32_t id, uint32_t index, uint32_t next, + const struct spa_pod *param) +{ + struct global *g = object; + param_add(&g->pending_list, seq, id, param); +} + +static const struct pw_node_events node_events = { + .version = PW_VERSION_NODE_EVENTS, + .info = node_event_info, + .param = node_event_param, +}; + +static void node_destroy(struct global *g) +{ + if (g->info) { + PIPEWIRE_pw_node_info_free(g->info); + g->info = NULL; + } +} + + +static const struct global_class node_class = { + .type = PW_TYPE_INTERFACE_Node, + .version = PW_VERSION_NODE, + .events = &node_events, + .destroy = node_destroy, +}; + +/** proxy */ +static void proxy_removed(void *data) +{ + struct global *g = data; + PIPEWIRE_pw_proxy_destroy(g->proxy); +} + +static void proxy_destroy(void *data) +{ + struct global *g = data; + spa_list_remove(&g->link); + g->proxy = NULL; + if (g->class) { + if (g->class->events) + spa_hook_remove(&g->object_listener); + if (g->class->destroy) + g->class->destroy(g); + } + param_clear(&g->param_list, SPA_ID_INVALID); + param_clear(&g->pending_list, SPA_ID_INVALID); + free(g->name); +} + +static const struct pw_proxy_events proxy_events = { + .version = PW_VERSION_PROXY_EVENTS, + .removed = proxy_removed, + .destroy = proxy_destroy +}; + +// called with thread_loop lock +static void hotplug_registry_global_callback(void *object, uint32_t id, + uint32_t permissions, const char *type, uint32_t version, + const struct spa_dict *props) +{ + const struct global_class *class = NULL; + struct pw_proxy *proxy; + const char *str, *name = NULL; + + if (spa_streq(type, PW_TYPE_INTERFACE_Node)) { + if (props == NULL) + return; + if (((str = spa_dict_lookup(props, PW_KEY_MEDIA_CLASS)) == NULL) || + (!spa_streq(str, "Video/Source"))) + return; + + if ((name = spa_dict_lookup(props, PW_KEY_NODE_DESCRIPTION)) == NULL && + (name = spa_dict_lookup(props, PW_KEY_NODE_NAME)) == NULL) + name = "unnamed camera"; + + class = &node_class; + } + if (class) { + struct global *g; + + proxy = pw_registry_bind(hotplug.registry, + id, class->type, class->version, + sizeof(struct global)); + + g = PIPEWIRE_pw_proxy_get_user_data(proxy); + g->class = class; + g->id = id; + g->permissions = permissions; + g->props = props ? PIPEWIRE_pw_properties_new_dict(props) : NULL; + g->proxy = proxy; + g->name = strdup(name); + spa_list_init(&g->pending_list); + spa_list_init(&g->param_list); + spa_list_append(&hotplug.global_list, &g->link); + + PIPEWIRE_pw_proxy_add_listener(proxy, + &g->proxy_listener, + &proxy_events, g); + + if (class->events) { + PIPEWIRE_pw_proxy_add_object_listener(proxy, + &g->object_listener, + class->events, g); + } + if (class->init) + class->init(g); + + do_resync(); + } +} + +// called with thread_loop lock +static void hotplug_registry_global_remove_callback(void *object, uint32_t id) +{ +} + +static const struct pw_registry_events hotplug_registry_events = +{ + .version = PW_VERSION_REGISTRY_EVENTS, + .global = hotplug_registry_global_callback, + .global_remove = hotplug_registry_global_remove_callback +}; + +// Core sync points, called with thread_loop lock +static void hotplug_core_done_callback(void *object, uint32_t id, int seq) +{ + hotplug.last_seq = seq; + if (id == PW_ID_CORE && seq == hotplug.pending_seq) { + struct global *g; + struct pw_node_info *info; + + spa_list_for_each(g, &hotplug.global_list, link) { + if (!g->changed) + continue; + + info = g->info; + param_update(&g->param_list, &g->pending_list, info->n_params, info->params); + + if (!g->added && hotplug.events_enabled) { + add_device(g); + } + } + hotplug.init_complete = SDL_TRUE; + PIPEWIRE_pw_thread_loop_signal(hotplug.loop, false); + } +} +static const struct pw_core_events hotplug_core_events = +{ + .version = PW_VERSION_CORE_EVENTS, + .done = hotplug_core_done_callback +}; + +// The hotplug thread +static int hotplug_loop_init(void) +{ + int res; + + spa_list_init(&hotplug.global_list); + + hotplug.loop = PIPEWIRE_pw_thread_loop_new("SDLAudioHotplug", NULL); + if (!hotplug.loop) { + return SDL_SetError("Pipewire: Failed to create hotplug detection loop (%i)", errno); + } + + hotplug.context = PIPEWIRE_pw_context_new(PIPEWIRE_pw_thread_loop_get_loop(hotplug.loop), NULL, 0); + if (!hotplug.context) { + return SDL_SetError("Pipewire: Failed to create hotplug detection context (%i)", errno); + } + + hotplug.core = PIPEWIRE_pw_context_connect(hotplug.context, NULL, 0); + if (!hotplug.core) { + return SDL_SetError("Pipewire: Failed to connect hotplug detection context (%i)", errno); + } + spa_zero(hotplug.core_listener); + pw_core_add_listener(hotplug.core, &hotplug.core_listener, &hotplug_core_events, NULL); + + hotplug.registry = pw_core_get_registry(hotplug.core, PW_VERSION_REGISTRY, 0); + if (!hotplug.registry) { + return SDL_SetError("Pipewire: Failed to acquire hotplug detection registry (%i)", errno); + } + + spa_zero(hotplug.registry_listener); + pw_registry_add_listener(hotplug.registry, &hotplug.registry_listener, &hotplug_registry_events, NULL); + + hotplug.pending_seq = pw_core_sync(hotplug.core, PW_ID_CORE, 0); + + res = PIPEWIRE_pw_thread_loop_start(hotplug.loop); + if (res != 0) { + return SDL_SetError("Pipewire: Failed to start hotplug detection loop"); + } + + return 0; +} + + +static void PIPEWIRECAMERA_Deinitialize(void) +{ + if (pipewire_initialized) { + if (hotplug.loop) { + PIPEWIRE_pw_thread_loop_lock(hotplug.loop); + } + if (hotplug.registry) { + spa_hook_remove(&hotplug.registry_listener); + PIPEWIRE_pw_proxy_destroy((struct pw_proxy*)hotplug.registry); + } + if (hotplug.core) { + spa_hook_remove(&hotplug.core_listener); + PIPEWIRE_pw_core_disconnect(hotplug.core); + } + if (hotplug.context) { + PIPEWIRE_pw_context_destroy(hotplug.context); + } + if (hotplug.loop) { + PIPEWIRE_pw_thread_loop_unlock(hotplug.loop); + PIPEWIRE_pw_thread_loop_destroy(hotplug.loop); + } + deinit_pipewire_library(); + spa_zero(hotplug); + pipewire_initialized = SDL_FALSE; + } +} + +static SDL_bool PIPEWIRECAMERA_Init(SDL_CameraDriverImpl *impl) +{ + if (!pipewire_initialized) { + + if (init_pipewire_library(true) < 0) { + return SDL_FALSE; + } + + pipewire_initialized = SDL_TRUE; + + if (hotplug_loop_init() < 0) { + PIPEWIRECAMERA_Deinitialize(); + return SDL_FALSE; + } + } + + impl->DetectDevices = PIPEWIRECAMERA_DetectDevices; + impl->OpenDevice = PIPEWIRECAMERA_OpenDevice; + impl->CloseDevice = PIPEWIRECAMERA_CloseDevice; + impl->WaitDevice = PIPEWIRECAMERA_WaitDevice; + impl->AcquireFrame = PIPEWIRECAMERA_AcquireFrame; + impl->ReleaseFrame = PIPEWIRECAMERA_ReleaseFrame; + impl->FreeDeviceHandle = PIPEWIRECAMERA_FreeDeviceHandle; + impl->Deinitialize = PIPEWIRECAMERA_Deinitialize; + + return SDL_TRUE; +} + +CameraBootStrap PIPEWIRECAMERA_bootstrap = { + "pipewire", "SDL PipeWire camera driver", PIPEWIRECAMERA_Init, SDL_TRUE +}; + +#endif // SDL_CAMERA_DRIVER_PIPEWIRE