Procházet zdrojové kódy

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.
Wim Taymans před 11 měsíci
rodič
revize
a340748c06

+ 3 - 0
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 $<TARGET_PROPERTY:PkgConfig::PC_PIPEWIRE,INTERFACE_INCLUDE_DIRECTORIES>)
       else()

+ 2 - 0
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@

+ 3 - 0
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

+ 1 - 0
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;

+ 1181 - 0
src/camera/pipewire/SDL_camera_pipewire.c

@@ -0,0 +1,1181 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2023 Sam Lantinga <slouken@libsdl.org>
+  Copyright (C) 2024 Wim Taymans <wtaymans@redhat.com>
+
+  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 <spa/utils/cleanup.h>
+#include <spa/utils/type.h>
+#include <spa/pod/builder.h>
+#include <spa/pod/iter.h>
+#include <spa/param/video/raw.h>
+#include <spa/param/video/format.h>
+#include <spa/utils/result.h>
+#include <spa/utils/json.h>
+
+#include <pipewire/pipewire.h>
+#include <pipewire/extensions/metadata.h>
+
+#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