SDL: camera: Emscripten support!

From 67708f91100e901c309a62beb4a39c8f6d434c9a Mon Sep 17 00:00:00 2001
From: "Ryan C. Gordon" <[EMAIL REDACTED]>
Date: Fri, 22 Dec 2023 01:23:49 -0500
Subject: [PATCH] camera: Emscripten support!

This also adds code to deal with waiting for the user to approve camera
access, reworks testcameraminimal to use main callbacks, etc.
---
 CMakeLists.txt                                |   6 +
 include/SDL3/SDL_camera.h                     |  65 ++++-
 include/SDL3/SDL_events.h                     |   4 +-
 include/build_config/SDL_build_config.h.cmake |   1 +
 .../SDL_build_config_emscripten.h             |   4 +-
 src/camera/SDL_camera.c                       | 118 ++++++--
 src/camera/SDL_syscamera.h                    |   7 +
 src/camera/emscripten/SDL_camera_emscripten.c | 269 ++++++++++++++++++
 src/camera/v4l2/SDL_camera_v4l2.c             |   3 +
 src/dynapi/SDL_dynapi.sym                     |   1 +
 src/dynapi/SDL_dynapi_overrides.h             |   1 +
 src/dynapi/SDL_dynapi_procs.h                 |   1 +
 src/events/SDL_events.c                       |   6 +
 test/testcameraminimal.c                      | 214 +++++++-------
 14 files changed, 557 insertions(+), 143 deletions(-)
 create mode 100644 src/camera/emscripten/SDL_camera_emscripten.c

diff --git a/CMakeLists.txt b/CMakeLists.txt
index b6eefb29650d..74ed3a10de2d 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1423,6 +1423,12 @@ elseif(EMSCRIPTEN)
   sdl_glob_sources("${SDL3_SOURCE_DIR}/src/filesystem/emscripten/*.c")
   set(HAVE_SDL_FILESYSTEM TRUE)
 
+  if(SDL_CAMERA)
+    set(SDL_CAMERA_DRIVER_EMSCRIPTEN 1)
+    set(HAVE_CAMERA TRUE)
+    sdl_glob_sources("${SDL3_SOURCE_DIR}/src/camera/emscripten/*.c")
+  endif()
+
   if(SDL_JOYSTICK)
     set(SDL_JOYSTICK_EMSCRIPTEN 1)
     sdl_glob_sources("${SDL3_SOURCE_DIR}/src/joystick/emscripten/*.c")
diff --git a/include/SDL3/SDL_camera.h b/include/SDL3/SDL_camera.h
index 0204fe9bdca5..a57a66a06996 100644
--- a/include/SDL3/SDL_camera.h
+++ b/include/SDL3/SDL_camera.h
@@ -171,6 +171,13 @@ extern DECLSPEC SDL_CameraDeviceID *SDLCALL SDL_GetCameraDevices(int *count);
  * The returned list is owned by the caller, and should be released with
  * SDL_free() when no longer needed.
  *
+ * Note that it's legal for a camera to supply a list with only the zeroed
+ * final element and `*count` set to zero; this is what will happen on
+ * Emscripten builds, since that platform won't tell _anything_ about
+ * available cameras until you've opened one, and won't even tell if there
+ * _is_ a camera until the user has given you permission to check through
+ * a scary warning popup.
+ *
  * \param devid the camera device instance ID to query.
  * \param count a pointer filled in with the number of elements in the list. Can be NULL.
  * \returns a 0 terminated array of SDL_CameraSpecs, which should be
@@ -224,6 +231,16 @@ extern DECLSPEC char * SDLCALL SDL_GetCameraDeviceName(SDL_CameraDeviceID instan
  * SDL_GetCameraFormat() to see the actual framerate of the opened the device,
  * and check your timestamps if this is crucial to your app!
  *
+ * Note that the camera is not usable until the user approves its use! On
+ * some platforms, the operating system will prompt the user to permit access
+ * to the camera, and they can choose Yes or No at that point. Until they do,
+ * the camera will not be usable. The app should either wait for an
+ * SDL_EVENT_CAMERA_DEVICE_APPROVED (or SDL_EVENT_CAMERA_DEVICE_DENIED) event,
+ * or poll SDL_IsCameraApproved() occasionally until it returns non-zero. On
+ * platforms that don't require explicit user approval (and perhaps in places
+ * where the user previously permitted access), the approval event might come
+ * immediately, but it might come seconds, minutes, or hours later!
+ *
  * \param instance_id the camera device instance ID
  * \param spec The desired format for data the device will provide. Can be NULL.
  * \returns device, or NULL on failure; call SDL_GetError() for more
@@ -238,6 +255,38 @@ extern DECLSPEC char * SDLCALL SDL_GetCameraDeviceName(SDL_CameraDeviceID instan
  */
 extern DECLSPEC SDL_Camera *SDLCALL SDL_OpenCameraDevice(SDL_CameraDeviceID instance_id, const SDL_CameraSpec *spec);
 
+/**
+ * Query if camera access has been approved by the user.
+ *
+ * Cameras will not function between when the device is opened by the app
+ * and when the user permits access to the hardware. On some platforms, this
+ * presents as a popup dialog where the user has to explicitly approve access;
+ * on others the approval might be implicit and not alert the user at all.
+ *
+ * This function can be used to check the status of that approval. It will
+ * return 0 if still waiting for user response, 1 if the camera is approved
+ * for use, and -1 if the user denied access.
+ *
+ * Instead of polling with this function, you can wait for a
+ * SDL_EVENT_CAMERA_DEVICE_APPROVED (or SDL_EVENT_CAMERA_DEVICE_DENIED) event
+ * in the standard SDL event loop, which is guaranteed to be sent once when
+ * permission to use the camera is decided.
+ *
+ * If a camera is declined, there's nothing to be done but call
+ * SDL_CloseCamera() to dispose of it.
+ *
+ * \param camera the opened camera device to query
+ * \returns -1 if user denied access to the camera, 1 if user approved access, 0 if no decision has been made yet.
+ *
+ * \threadsafety It is safe to call this function from any thread.
+ *
+ * \since This function is available since SDL 3.0.0.
+ *
+ * \sa SDL_OpenCameraDevice
+ * \sa SDL_CloseCamera
+ */
+extern DECLSPEC int SDLCALL SDL_GetCameraPermissionState(SDL_Camera *camera);
+
 /**
  * Get the instance ID of an opened camera.
  *
@@ -275,6 +324,12 @@ extern DECLSPEC SDL_PropertiesID SDLCALL SDL_GetCameraProperties(SDL_Camera *cam
  * Note that this might not be the native format of the hardware, as SDL
  * might be converting to this format behind the scenes.
  *
+ * If the system is waiting for the user to approve access to the camera, as
+ * some platforms require, this will return -1, but this isn't necessarily a
+ * fatal error; you should either wait for an SDL_EVENT_CAMERA_DEVICE_APPROVED
+ * (or SDL_EVENT_CAMERA_DEVICE_DENIED) event, or poll SDL_IsCameraApproved()
+ * occasionally until it returns non-zero.
+ *
  * \param camera opened camera device
  * \param spec The SDL_CameraSpec to be initialized by this function.
  * \returns 0 on success or a negative error code on failure; call
@@ -305,13 +360,17 @@ extern DECLSPEC int SDLCALL SDL_GetCameraFormat(SDL_Camera *camera, SDL_CameraSp
  * failure here is almost always an out of memory condition.
  *
  * After use, the frame should be released with SDL_ReleaseCameraFrame(). If you
- * don't do this, the system may stop providing more video! If the hardware is
- * using DMA to write directly into memory, frames held too long may be overwritten
- * with new data.
+ * don't do this, the system may stop providing more video!
  *
  * Do not call SDL_FreeSurface() on the returned surface! It must be given back
  * to the camera subsystem with SDL_ReleaseCameraFrame!
  *
+ * If the system is waiting for the user to approve access to the camera, as
+ * some platforms require, this will return NULL (no frames available); you should
+ * either wait for an SDL_EVENT_CAMERA_DEVICE_APPROVED (or
+ * SDL_EVENT_CAMERA_DEVICE_DENIED) event, or poll SDL_IsCameraApproved()
+ * occasionally until it returns non-zero.
+ *
  * \param camera opened camera device
  * \param timestampNS a pointer filled in with the frame's timestamp, or 0 on error. Can be NULL.
  * \returns A new frame of video on success, NULL if none is currently available.
diff --git a/include/SDL3/SDL_events.h b/include/SDL3/SDL_events.h
index 74a39267c188..cbd1f2a8af6d 100644
--- a/include/SDL3/SDL_events.h
+++ b/include/SDL3/SDL_events.h
@@ -208,6 +208,8 @@ typedef enum
     /* Camera hotplug events */
     SDL_EVENT_CAMERA_DEVICE_ADDED = 0x1400,  /**< A new camera device is available */
     SDL_EVENT_CAMERA_DEVICE_REMOVED,         /**< A camera device has been removed. */
+    SDL_EVENT_CAMERA_DEVICE_APPROVED,        /**< A camera device has been approved for use by the user. */
+    SDL_EVENT_CAMERA_DEVICE_DENIED,          /**< A camera device has been denied for use by the user. */
 
     /* Render events */
     SDL_EVENT_RENDER_TARGETS_RESET = 0x2000, /**< The render targets have been reset and their contents need to be updated */
@@ -535,7 +537,7 @@ typedef struct SDL_AudioDeviceEvent
  */
 typedef struct SDL_CameraDeviceEvent
 {
-    Uint32 type;        /**< ::SDL_EVENT_CAMERA_DEVICE_ADDED, or ::SDL_EVENT_CAMERA_DEVICE_REMOVED */
+    Uint32 type;        /**< ::SDL_EVENT_CAMERA_DEVICE_ADDED, ::SDL_EVENT_CAMERA_DEVICE_REMOVED, ::SDL_EVENT_CAMERA_DEVICE_APPROVED, ::SDL_EVENT_CAMERA_DEVICE_DENIED */
     Uint64 timestamp;   /**< In nanoseconds, populated using SDL_GetTicksNS() */
     SDL_CameraDeviceID which;       /**< SDL_CameraDeviceID for the device being added or removed or changing */
     Uint8 padding1;
diff --git a/include/build_config/SDL_build_config.h.cmake b/include/build_config/SDL_build_config.h.cmake
index 7985065dcbfc..9f4302a79a5f 100644
--- a/include/build_config/SDL_build_config.h.cmake
+++ b/include/build_config/SDL_build_config.h.cmake
@@ -472,6 +472,7 @@
 #cmakedefine SDL_CAMERA_DRIVER_V4L2 @SDL_CAMERA_DRIVER_V4L2@
 #cmakedefine SDL_CAMERA_DRIVER_COREMEDIA @SDL_CAMERA_DRIVER_COREMEDIA@
 #cmakedefine SDL_CAMERA_DRIVER_ANDROID @SDL_CAMERA_DRIVER_ANDROID@
+#cmakedefine SDL_CAMERA_DRIVER_EMSCRIPTEN @SDL_CAMERA_DRIVER_EMSCRIPTEND@
 
 /* Enable misc subsystem */
 #cmakedefine SDL_MISC_DUMMY @SDL_MISC_DUMMY@
diff --git a/include/build_config/SDL_build_config_emscripten.h b/include/build_config/SDL_build_config_emscripten.h
index 07a94d615dce..9e4ad6aa25ee 100644
--- a/include/build_config/SDL_build_config_emscripten.h
+++ b/include/build_config/SDL_build_config_emscripten.h
@@ -209,7 +209,7 @@
 /* Enable system filesystem support */
 #define SDL_FILESYSTEM_EMSCRIPTEN 1
 
-/* Enable the camera driver (src/camera/dummy/\*.c) */  /* !!! FIXME */
-#define SDL_CAMERA_DRIVER_DUMMY  1
+/* Enable the camera driver */
+#define SDL_CAMERA_DRIVER_EMSCRIPTEN  1
 
 #endif /* SDL_build_config_emscripten_h */
diff --git a/src/camera/SDL_camera.c b/src/camera/SDL_camera.c
index c8af9f0e2e28..01179dc44f97 100644
--- a/src/camera/SDL_camera.c
+++ b/src/camera/SDL_camera.c
@@ -40,6 +40,9 @@ static const CameraBootStrap *const bootstrap[] = {
 #ifdef SDL_CAMERA_DRIVER_ANDROID
     &ANDROIDCAMERA_bootstrap,
 #endif
+#ifdef SDL_CAMERA_DRIVER_EMSCRIPTEN
+    &EMSCRIPTENCAMERA_bootstrap,
+#endif
 #ifdef SDL_CAMERA_DRIVER_DUMMY
     &DUMMYCAMERA_bootstrap,
 #endif
@@ -247,8 +250,8 @@ static int SDLCALL CameraSpecCmp(const void *vpa, const void *vpb)
 SDL_CameraDevice *SDL_AddCameraDevice(const char *name, int num_specs, const SDL_CameraSpec *specs, void *handle)
 {
     SDL_assert(name != NULL);
-    SDL_assert(num_specs > 0);
-    SDL_assert(specs != NULL);
+    SDL_assert(num_specs >= 0);
+    SDL_assert((specs != NULL) == (num_specs > 0));
     SDL_assert(handle != NULL);
 
     SDL_LockRWLockForReading(camera_driver.device_hash_lock);
@@ -284,22 +287,24 @@ SDL_CameraDevice *SDL_AddCameraDevice(const char *name, int num_specs, const SDL
         return NULL;
     }
 
-    SDL_memcpy(device->all_specs, specs, sizeof (*specs) * num_specs);
-    SDL_qsort(device->all_specs, num_specs, sizeof (*specs), CameraSpecCmp);
+    if (num_specs > 0) {
+        SDL_memcpy(device->all_specs, specs, sizeof (*specs) * num_specs);
+        SDL_qsort(device->all_specs, num_specs, sizeof (*specs), CameraSpecCmp);
 
-    // weed out duplicates, just in case.
-    for (int i = 0; i < num_specs; i++) {
-        SDL_CameraSpec *a = &device->all_specs[i];
-        SDL_CameraSpec *b = &device->all_specs[i + 1];
-        if (SDL_memcmp(a, b, sizeof (*a)) == 0) {
-            SDL_memmove(a, b, sizeof (*specs) * (num_specs - i));
-            i--;
-            num_specs--;
+        // weed out duplicates, just in case.
+        for (int i = 0; i < num_specs; i++) {
+            SDL_CameraSpec *a = &device->all_specs[i];
+            SDL_CameraSpec *b = &device->all_specs[i + 1];
+            if (SDL_memcmp(a, b, sizeof (*a)) == 0) {
+                SDL_memmove(a, b, sizeof (*specs) * (num_specs - i));
+                i--;
+                num_specs--;
+            }
         }
     }
 
     #if DEBUG_CAMERA
-    SDL_Log("CAMERA: Adding device ('%s') with %d spec%s:", name, num_specs, (num_specs == 1) ? "" : "s");
+    SDL_Log("CAMERA: Adding device ('%s') with %d spec%s%s", name, num_specs, (num_specs == 1) ? "" : "s", (num_specs == 0) ? "" : ":");
     for (int i = 0; i < num_specs; i++) {
         const SDL_CameraSpec *spec = &device->all_specs[i];
         SDL_Log("CAMERA:   - fmt=%s, w=%d, h=%d, numerator=%d, denominator=%d", SDL_GetPixelFormatName(spec->format), spec->width, spec->height, spec->interval_numerator, spec->interval_denominator);
@@ -398,6 +403,42 @@ sdfsdf
     }
 }
 
+void SDL_CameraDevicePermissionOutcome(SDL_CameraDevice *device, SDL_bool approved)
+{
+    if (!device) {
+        return;
+    }
+
+    SDL_PendingCameraDeviceEvent pending;
+    pending.next = NULL;
+    SDL_PendingCameraDeviceEvent *pending_tail = &pending;
+
+    const int permission = approved ? 1 : -1;
+
+    ObtainPhysicalCameraDeviceObj(device);
+    if (device->permission != permission) {
+        device->permission = permission;
+        SDL_PendingCameraDeviceEvent *p = (SDL_PendingCameraDeviceEvent *) SDL_malloc(sizeof (SDL_PendingCameraDeviceEvent));
+        if (p) {  // if this failed, no event for you, but you have deeper problems anyhow.
+            p->type = approved ? SDL_EVENT_CAMERA_DEVICE_APPROVED : SDL_EVENT_CAMERA_DEVICE_DENIED;
+            p->devid = device->instance_id;
+            p->next = NULL;
+            pending_tail->next = p;
+            pending_tail = p;
+        }
+    }
+
+    ReleaseCameraDevice(device);
+
+    SDL_LockRWLockForWriting(camera_driver.device_hash_lock);
+    SDL_assert(camera_driver.pending_events_tail != NULL);
+    SDL_assert(camera_driver.pending_events_tail->next == NULL);
+    camera_driver.pending_events_tail->next = pending.next;
+    camera_driver.pending_events_tail = pending_tail;
+    SDL_UnlockRWLock(camera_driver.device_hash_lock);
+}
+
+
 SDL_CameraDevice *SDL_FindPhysicalCameraDeviceByCallback(SDL_bool (*callback)(SDL_CameraDevice *device, void *userdata), void *userdata)
 {
     if (!SDL_GetCurrentCameraDriver()) {
@@ -439,7 +480,14 @@ int SDL_GetCameraFormat(SDL_Camera *camera, SDL_CameraSpec *spec)
     }
 
     SDL_CameraDevice *device = (SDL_CameraDevice *) camera;  // currently there's no separation between physical and logical device.
-    SDL_copyp(spec, &device->spec);
+    ObtainPhysicalCameraDeviceObj(device);
+    const int retval = (device->permission > 0) ? 0 : SDL_SetError("Camera permission has not been granted");
+    if (retval == 0) {
+        SDL_copyp(spec, &device->spec);
+    } else {
+        SDL_zerop(spec);
+    }
+    ReleaseCameraDevice(device);
     return 0;
 }
 
@@ -545,7 +593,11 @@ SDL_bool SDL_CameraThreadIterate(SDL_CameraDevice *device)
         return SDL_FALSE;  // we're done, shut it down.
     }
 
-    // !!! FIXME: this should block elsewhere without holding the lock until a frame is available, like the audio subsystem does.
+    const int permission = device->permission;
+    if (permission <= 0) {
+        SDL_UnlockMutex(device->lock);
+        return (permission < 0) ? SDL_FALSE : SDL_TRUE;  // if permission was denied, shut it down. if undecided, we're done for now.
+    }
 
     SDL_bool failed = SDL_FALSE;  // set to true if disaster worthy of treating the device as lost has happened.
     SDL_Surface *acquired = NULL;
@@ -661,7 +713,7 @@ static int SDLCALL CameraThread(void *devicep)
     SDL_CameraDevice *device = (SDL_CameraDevice *) devicep;
 
     #if DEBUG_CAMERA
-    SDL_Log("CAMERA: Start thread 'SDL_CameraThread'");
+    SDL_Log("CAMERA: dev[%p] Start thread 'CameraThread'", devicep);
     #endif
 
     SDL_assert(device != NULL);
@@ -676,7 +728,7 @@ static int SDLCALL CameraThread(void *devicep)
     SDL_CameraThreadShutdown(device);
 
     #if DEBUG_CAMERA
-    SDL_Log("CAMERA: dev[%p] End thread 'SDL_CameraThread'", (void *)device);
+    SDL_Log("CAMERA: dev[%p] End thread 'CameraThread'", devicep);
     #endif
 
     return 0;
@@ -697,9 +749,13 @@ static void ChooseBestCameraSpec(SDL_CameraDevice *device, const SDL_CameraSpec
     SDL_zerop(closest);
     SDL_assert(((Uint32) SDL_PIXELFORMAT_UNKNOWN) == 0);  // since we SDL_zerop'd to this value.
 
-    if (!spec) {  // nothing specifically requested, get the best format we can...
+    if (device->num_specs == 0) {  // device listed no specs! You get whatever you want!
+        if (spec) {
+            SDL_copyp(closest, spec);
+        }
+        return;
+    } else if (!spec) {  // nothing specifically requested, get the best format we can...
         // we sorted this into the "best" format order when adding the camera.
-        SDL_assert(device->num_specs > 0);
         SDL_copyp(closest, &device->all_specs[0]);
     } else {  // specific thing requested, try to get as close to that as possible...
         const int num_specs = device->num_specs;
@@ -924,6 +980,12 @@ SDL_Surface *SDL_AcquireCameraFrame(SDL_Camera *camera, Uint64 *timestampNS)
 
     ObtainPhysicalCameraDeviceObj(device);
 
+    if (device->permission <= 0) {
+        ReleaseCameraDevice(device);
+        SDL_SetError("Camera permission has not been granted");
+        return NULL;
+    }
+
     SDL_Surface *retval = NULL;
 
     // frames are in this list from newest to oldest, so find the end of the list...
@@ -996,8 +1058,6 @@ int SDL_ReleaseCameraFrame(SDL_Camera *camera, SDL_Surface *frame)
     return 0;
 }
 
-// !!! FIXME: add a way to "pause" camera output.
-
 SDL_CameraDeviceID SDL_GetCameraInstanceID(SDL_Camera *camera)
 {
     SDL_CameraDeviceID retval = 0;
@@ -1031,6 +1091,22 @@ SDL_PropertiesID SDL_GetCameraProperties(SDL_Camera *camera)
     return retval;
 }
 
+int SDL_GetCameraPermissionState(SDL_Camera *camera)
+{
+    int retval;
+    if (!camera) {
+        retval = SDL_InvalidParamError("camera");
+    } else {
+        SDL_CameraDevice *device = (SDL_CameraDevice *) camera;  // currently there's no separation between physical and logical device.
+        ObtainPhysicalCameraDeviceObj(device);
+        retval = device->permission;
+        ReleaseCameraDevice(device);
+    }
+
+    return retval;
+}
+
+
 static void CompleteCameraEntryPoints(void)
 {
     // this doesn't currently fill in stub implementations, it just asserts the backend filled them all in.
diff --git a/src/camera/SDL_syscamera.h b/src/camera/SDL_syscamera.h
index 27672890b54c..e6b204771a44 100644
--- a/src/camera/SDL_syscamera.h
+++ b/src/camera/SDL_syscamera.h
@@ -50,6 +50,9 @@ extern void SDL_CameraDeviceDisconnected(SDL_CameraDevice *device);
 // Find an SDL_CameraDevice, selected by a callback. NULL if not found. DOES NOT LOCK THE DEVICE.
 extern SDL_CameraDevice *SDL_FindPhysicalCameraDeviceByCallback(SDL_bool (*callback)(SDL_CameraDevice *device, void *userdata), void *userdata);
 
+// Backends should call this when the user has approved/denied access to a camera.
+extern void SDL_CameraDevicePermissionOutcome(SDL_CameraDevice *device, SDL_bool approved);
+
 // These functions are the heart of the camera threads. Backends can call them directly if they aren't using the SDL-provided thread.
 extern void SDL_CameraThreadSetup(SDL_CameraDevice *device);
 extern SDL_bool SDL_CameraThreadIterate(SDL_CameraDevice *device);
@@ -129,6 +132,9 @@ struct SDL_CameraDevice
     // Optional properties.
     SDL_PropertiesID props;
 
+    // -1: user denied permission, 0: waiting for user response, 1: user approved permission.
+    int permission;
+
     // Data private to this driver, used when device is opened and running.
     struct SDL_PrivateCameraData *hidden;
 };
@@ -182,5 +188,6 @@ extern CameraBootStrap DUMMYCAMERA_bootstrap;
 extern CameraBootStrap V4L2_bootstrap;
 extern CameraBootStrap COREMEDIA_bootstrap;
 extern CameraBootStrap ANDROIDCAMERA_bootstrap;
+extern CameraBootStrap EMSCRIPTENCAMERA_bootstrap;
 
 #endif // SDL_syscamera_h_
diff --git a/src/camera/emscripten/SDL_camera_emscripten.c b/src/camera/emscripten/SDL_camera_emscripten.c
new file mode 100644
index 000000000000..a04a8e454666
--- /dev/null
+++ b/src/camera/emscripten/SDL_camera_emscripten.c
@@ -0,0 +1,269 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2023 Sam Lantinga <slouken@libsdl.org>
+
+  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_EMSCRIPTEN
+
+#include "../SDL_syscamera.h"
+#include "../SDL_camera_c.h"
+#include "../../video/SDL_pixels_c.h"
+
+#include <emscripten/emscripten.h>
+
+// just turn off clang-format for this whole file, this INDENT_OFF stuff on
+//  each EM_ASM section is ugly.
+/* *INDENT-OFF* */ /* clang-format off */
+
+EM_JS_DEPS(sdlcamera, "$dynCall");
+
+static int EMSCRIPTENCAMERA_WaitDevice(SDL_CameraDevice *device)
+{
+    SDL_assert(!"This shouldn't be called");  // we aren't using SDL's internal thread.
+    return -1;
+}
+
+static int EMSCRIPTENCAMERA_AcquireFrame(SDL_CameraDevice *device, SDL_Surface *frame, Uint64 *timestampNS)
+{
+    void *rgba = SDL_malloc(device->actual_spec.width * device->actual_spec.height * 4);
+    if (!rgba) {
+        return SDL_OutOfMemory();
+    }
+
+    *timestampNS = SDL_GetTicksNS();  // best we can do here.
+
+    const int rc = MAIN_THREAD_EM_ASM_INT({
+        const w = $0;
+        const h = $1;
+        const rgba = $2;
+        const SDL3 = Module['SDL3'];
+        if ((typeof(SDL3) === 'undefined') || (typeof(SDL3.camera) === 'undefined') || (typeof(SDL3.camera.ctx2d) === 'undefined')) {
+            return 0;  // don't have something we need, oh well.
+        }
+
+        SDL3.camera.ctx2d.drawImage(SDL3.camera.video, 0, 0, w, h);
+        const imgrgba = SDL3.camera.ctx2d.getImageData(0, 0, w, h).data;
+        Module.HEAPU8.set(imgrgba, rgba);
+
+        return 1;
+    }, device->actual_spec.width, device->actual_spec.height, rgba);
+
+    if (!rc) {
+        SDL_free(rgba);
+        return 0;  // something went wrong, maybe shutting down; just don't return a frame.
+    }
+
+    frame->pixels = rgba;
+    frame->pitch = device->actual_spec.width * 4;
+
+    return 1;
+}
+
+static void EMSCRIPTENCAMERA_ReleaseFrame(SDL_CameraDevice *device, SDL_Surface *frame)
+{
+    SDL_free(frame->pixels);
+    frame->pixels = NULL;
+    frame->pitch = 0;
+}
+
+static void EMSCRIPTENCAMERA_CloseDevice(SDL_CameraDevice *device)
+{
+    if (device) {
+        MAIN_THREAD_EM_ASM({
+            const SDL3 = Module['SDL3'];
+            if ((typeof(SDL3) === 'undefined') || (typeof(SDL3.camera) === 'undefined') || (typeof(SDL3.camera.stream) === 'undefined')) {
+                return;  // camera was closed and/or subsystem was shut down, we're already done.
+            }
+            SDL3.camera.stream.getTracks().forEach(track => track.stop());  // stop all recording.
+            _SDL_free(SDL3.camera.rgba);
+            SDL3.camera = {};  // dump our references to everything.
+        });
+        SDL_free(device->hidden);
+        device->hidden = NULL;
+    }
+}
+
+static void SDLEmscriptenCameraDevicePermissionOutcome(SDL_CameraDevice *device, int approved, int w, int h, int fps)
+{
+    device->spec.width = device->actual_spec.width = w;
+    device->spec.height = device->actual_spec.height = h;
+    device->spec.interval_numerator = device->actual_spec.interval_numerator = 1;
+    device->spec.interval_denominator = device->actual_spec.interval_denominator = fps;
+    SDL_CameraDevicePermissionOutcome(device, approved ? SDL_TRUE : SDL_FALSE);
+}
+
+static int EMSCRIPTENCAMERA_OpenDevice(SDL_CameraDevice *device, const SDL_CameraSpec *spec)
+{
+    MAIN_THREAD_EM_ASM({
+        // Since we can't get actual specs until we make a move that prompts the user for
+        // permission, we don't list any specs for the device and wrangle it during device open.
+        const device = $0;
+        const w = $1;
+        const h = $2;
+        const interval_numerator = $3;
+        const interval_denominator = $4;
+        const outcome = $5;
+        const iterate = $6;
+
+        const constraints = {};
+        if ((w <= 0) || (h <= 0)) {
+            constraints.video = true;   // didn't ask for anything, let the system choose.
+        } else {
+            constraints.video = {};  // asked for a specific thing: request it as "ideal" but take closest hardware will offer.
+            constraints.video.width = w;
+            constraints.video.height = h;
+        }
+
+        if ((interval_numerator > 0) && (interval_denominator > 0)) {
+            var fps = interval_denominator / interval_numerator;
+            constraints.video.frameRate = { ideal: fps };
+        }
+
+        function grabNextCameraFrame() {  // !!! FIXME: this (currently) runs as a requestAnimationFrame callback, for lack of a better option.
+            const SDL3 = Module['SDL3'];
+            if ((typeof(SDL3) === 'undefined') || (typeof(SDL3.camera) === 'undefined') || (typeof(SDL3.camera.stream) === 'undefined')) {
+                return;  // camera was closed and/or subsystem was shut down, stop iterating here.
+            }
+
+            // time for a new frame from the camera?
+            const nextframems = SDL3.camera.next_frame_time;
+            const now = performance.now();
+            if (now >= nextframems) {
+                dynCall('vi', iterate, [device]);  // calls SDL_CameraThreadIterate, which will call our AcquireFrame implementation.
+
+                // bump ahead but try to stay consistent on timing, in case we dropped frames.
+                while (SDL3.camera.next_frame_time < now) {
+                    SDL3.camera.next_frame_time += SDL3.camera.fpsincrms;
+                }
+            }
+
+            requestAnimationFrame(grabNextCameraFrame);  // run this function again at the display framerate.  (!!! FIXME: would this be better as requestIdleCallback?)
+        }
+
+        navigator.mediaDevices.getUserMedia(constraints)
+            .then((stream) => {
+                const settings = stream.getVideoTracks()[0].getSettings();
+                const actualw = settings.width;
+                const actualh = settings.height;
+                const actualfps = settings.frameRate;
+                console.log("Camera is opened! Actual spec: (" + actualw + "x" + actualh + "), fps=" + actualfps);
+
+                dynCall('viiiii', outcome, [device, 1, actualw, actualh, actualfps]);
+
+                const video = document.createElement("video");
+                video.width = actualw;
+                video.height = actualh;
+                video.style.display = 'none';    // we need to attach this to a hidden video node so we can read it as pixels.
+                video.srcObject = stream;
+
+                const canvas = document.createElement("canvas");
+                canvas.width = actualw;
+                canvas.height = actualh;
+                canvas.style.display = 'none';    // we need to attach this to a hidden video node so we can read it as pixels.
+
+                const ctx2d = canvas.getContext('2d');
+
+                const SDL3 = Module['SDL3'];
+                SDL3.camera.width = actualw;
+                SDL3.camera.height = actualh;
+                SDL3.camera.fps = actualfps;
+                SDL3.camera.fpsincrms = 1000.0 / actualfps;
+                SDL3.camera.stream = stream;
+                SDL3.camera.video = video;
+                SDL3.camera.canvas = canvas;
+                SDL3.camera.ctx2d = ctx2d;
+                SDL3.camera.rgba = 0;
+                SDL3.camera.next_frame_time = performance.now();
+
+                video.play();
+                video.addEventListener('loadedmetadata', () => {
+                    grabNextCameraFrame();  // start this loop going.
+                });
+            })
+            .catch((err) => {
+                console.error("Tried to open camera but it threw an error! " + err.name + ": " +  err.message);
+                dynCall('viiiii', outcome, [device, 0, 0, 0, 0]);   // we call this a permission error, because it probably is.
+            });
+    }, device, spec->width, spec->height, spec->interval_numerator, spec->interval_denominator, SDLEmscriptenCameraDevicePermissionOutcome, SDL_CameraThreadIterate);
+
+    return 0;  // the real work waits until the user approves a camera.
+}
+
+static void EMSCRIPTENCAMERA_FreeDeviceHandle(SDL_CameraDevice *device)
+{
+    // no-op.
+}
+
+static void EMSCRIPTENCAMERA_Deinitialize(void)
+{
+    MAIN_THREAD_EM_ASM({
+        if (typeof(Module['SDL3']) !== 'undefined') {
+            Module['SDL3'].camera = undefined;
+        }
+    });
+}
+
+static void EMSCRIPTENCAMERA_DetectDevices(void)
+{
+    // `navigator.mediaDevices` is not defined if unsupported or not in a secure context!
+    const int supported = MAIN_THREAD_EM_ASM_INT({ return (navigator.mediaDevices === undefined) ? 0 : 1; });
+
+    // if we have support at all, report a single generic camera with no specs.
+    //  We'll find out if there really _is_ a camera when we try to open it, but querying it for real here
+    //  will pop up a user permission dialog warning them we're trying to access the camera, and we generally
+    //  don't want that during SDL_Init().
+    if (supported) {
+        SDL_AddCameraDevice("Web browser's camera", 0, NULL, (void *) (size_t) 0x1);
+    }
+}
+
+static SDL_bool EMSCRIPTENCAMERA_Init(SDL_CameraDriverImpl *impl)
+{
+SDL_Log("EMSCRIPTENCAMERA_Init, %s:%d", __FILE__, __LINE__);
+    MAIN_THREAD_EM_ASM({
+        if (typeof(Module['SDL3']) === 'undefined') {
+            Module['SDL3'] = {};
+        }
+        Module['SDL3'].camera = {};
+    });
+
+SDL_Log("EMSCRIPTENCAMERA_Init, %s:%d", __FILE__, __LINE__);
+    impl->DetectDevices = EMSCRIPTENCAMERA_DetectDevices;
+    impl->OpenDevice = EMSCRIPTENCAMERA_OpenDevice;
+    impl->CloseDevice = EMSCRIPTENCAMERA_CloseDevice;
+    impl->WaitDevice = EMSCRIPTENCAMERA_WaitDevice;
+    impl->AcquireFrame = EMSCRIPTENCAMERA_AcquireFrame;
+    impl->ReleaseFrame = EMSCRIPTENCAMERA_ReleaseFrame;
+    impl->FreeDeviceHandle = EMSCRIPTENCAMERA_FreeDeviceHandle;
+    impl->Deinitialize = EMSCRIPTENCAMERA_Deinitialize;
+
+    impl->ProvidesOwnCallbackThread = SDL_TRUE;
+
+    return SDL_TRUE;
+}
+
+CameraBootStrap EMSCRIPTENCAMERA_bootstrap = {
+    "emscripten", "SDL Emscripten MediaStream camera driver", EMSCRIPTENCAMERA_Init, SDL_FALSE
+};
+
+/* *INDENT-ON* */ /* clang-format on */
+
+#endif // SDL

(Patch may be truncated, please check the link at the top of this post.)