Browse Source

camera: Massive code reworking.

- Simplified public API, simplified backend interface.
- Camera device hotplug events.
- Thread code is split up so it backends that provide own threads can use it.
- Added "dummy" backend.

Note that CoreMedia (Apple) and Android backends need to be updated, as does
the testcamera app (testcameraminimal works).
Ryan C. Gordon 1 year ago
parent
commit
d3e6ef3cc6

+ 123 - 192
include/SDL3/SDL_camera.h

@@ -49,23 +49,16 @@ typedef Uint32 SDL_CameraDeviceID;
 
 
 /**
- * The structure used to identify an SDL camera device
+ * The structure used to identify an opened SDL camera
  */
-struct SDL_CameraDevice;
-typedef struct SDL_CameraDevice SDL_CameraDevice;
-
-#define SDL_CAMERA_ALLOW_ANY_CHANGE          1
+struct SDL_Camera;
+typedef struct SDL_Camera SDL_Camera;
 
 /**
  *  SDL_CameraSpec structure
  *
- *  Only those field can be 'desired' when configuring the device:
- *  - format
- *  - width
- *  - height
- *
- *  \sa SDL_GetCameraFormat
- *  \sa SDL_GetCameraFrameSize
+ * \sa SDL_GetCameraDeviceSupportedSpecs
+ * \sa SDL_GetCameraSpec
  *
  */
 typedef struct SDL_CameraSpec
@@ -75,39 +68,6 @@ typedef struct SDL_CameraSpec
     int height;             /**< Frame height */
 } SDL_CameraSpec;
 
-/**
- *  SDL Camera Status
- *
- *  Change states but calling the function in this order:
- *
- *  SDL_OpenCamera()
- *  SDL_SetCameraSpec()  -> Init
- *  SDL_StartCamera()    -> Playing
- *  SDL_StopCamera()     -> Stopped
- *  SDL_CloseCamera()
- *
- */
-typedef enum
-{
-    SDL_CAMERA_FAIL = -1,    /**< Failed */
-    SDL_CAMERA_INIT = 0,     /**< Init, spec hasn't been set */
-    SDL_CAMERA_STOPPED,      /**< Stopped */
-    SDL_CAMERA_PLAYING       /**< Playing */
-} SDL_CameraStatus;
-
-/**
- *  SDL Video Capture Status
- */
-typedef struct SDL_CameraFrame
-{
-    Uint64 timestampNS;         /**< Frame timestamp in nanoseconds when read from the driver */
-    int num_planes;             /**< Number of planes */
-    Uint8 *data[3];             /**< Pointer to data of i-th plane */
-    int pitch[3];               /**< Pitch of i-th plane */
-    void *internal;             /**< Private field */
-} SDL_CameraFrame;
-
-
 /**
  * Use this function to get the number of built-in camera drivers.
  *
@@ -176,11 +136,13 @@ extern DECLSPEC const char *SDLCALL SDL_GetCurrentCameraDriver(void);
 /**
  * Get a list of currently connected camera devices.
  *
- * \param count a pointer filled in with the number of camera devices
+ * \param count a pointer filled in with the number of camera devices. Can be NULL.
  * \returns a 0 terminated array of camera instance IDs which should be
  *          freed with SDL_free(), or NULL on error; call SDL_GetError() for
  *          more details.
  *
+ * \threadsafety It is safe to call this function from any thread.
+ *
  * \since This function is available since SDL 3.0.0.
  *
  * \sa SDL_OpenCamera
@@ -188,237 +150,202 @@ extern DECLSPEC const char *SDLCALL SDL_GetCurrentCameraDriver(void);
 extern DECLSPEC SDL_CameraDeviceID *SDLCALL SDL_GetCameraDevices(int *count);
 
 /**
- * Open a Video Capture device
+ * Get the list of native formats/sizes a camera supports.
  *
- * \param instance_id the camera device instance ID
- * \returns device, or NULL on failure; call SDL_GetError() for more
- *          information.
+ * This returns a list of all formats and frame sizes that a specific
+ * camera can offer. This is useful if your app can accept a variety
+ * of image formats and sizes and so want to find the optimal spec
+ * that doesn't require conversion.
  *
- * \since This function is available since SDL 3.0.0.
+ * This function isn't strictly required; if you call SDL_OpenCameraDevice
+ * with a NULL spec, SDL will choose a native format for you, and if you
+ * instead specify a desired format, it will transparently convert to the
+ * requested format on your behalf.
  *
- * \sa SDL_GetCameraDeviceName
- * \sa SDL_GetCameraDevices
- * \sa SDL_OpenCameraWithSpec
- */
-extern DECLSPEC SDL_CameraDevice *SDLCALL SDL_OpenCamera(SDL_CameraDeviceID instance_id);
-
-/**
- * Set specification
+ * If `count` is not NULL, it will be filled with the number of elements
+ * in the returned array. Additionally, the last element of the array
+ * has all fields set to zero (this element is not included in `count`).
  *
- * \param device opened camera device
- * \param desired desired camera spec
- * \param obtained obtained camera spec
- * \param allowed_changes allow changes or not
- * \returns 0 on success or a negative error code on failure; call
- *          SDL_GetError() for more information.
+ * The returned list is owned by the caller, and should be released with
+ * SDL_free() when no longer needed.
  *
- * \since This function is available since SDL 3.0.0.
- *
- * \sa SDL_OpenCamera
- * \sa SDL_OpenCameraWithSpec
- * \sa SDL_GetCameraSpec
- */
-extern DECLSPEC int SDLCALL SDL_SetCameraSpec(SDL_CameraDevice *device,
-                                                    const SDL_CameraSpec *desired,
-                                                    SDL_CameraSpec *obtained,
-                                                    int allowed_changes);
-
-/**
- * Open a Video Capture device and set specification
+ * \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
+ *          freed with SDL_free(), or NULL on error; call SDL_GetError() for
+ *          more details.
  *
- * \param instance_id the camera device instance ID
- * \param desired desired camera spec
- * \param obtained obtained camera spec
- * \param allowed_changes allow changes or not
- * \returns device, or NULL on failure; call SDL_GetError() for more
- *          information.
+ * \threadsafety It is safe to call this function from any thread.
  *
  * \since This function is available since SDL 3.0.0.
  *
- * \sa SDL_OpenCamera
- * \sa SDL_SetCameraSpec
- * \sa SDL_GetCameraSpec
+ * \sa SDL_GetCameraDevices
+ * \sa SDL_OpenCameraDevice
  */
-extern DECLSPEC SDL_CameraDevice *SDLCALL SDL_OpenCameraWithSpec(SDL_CameraDeviceID instance_id,
-                                                                              const SDL_CameraSpec *desired,
-                                                                              SDL_CameraSpec *obtained,
-                                                                              int allowed_changes);
+extern DECLSPEC SDL_CameraSpec *SDLCALL SDL_GetCameraDeviceSupportedSpecs(SDL_CameraDeviceID devid, int *count);
 
 /**
- * Get device name
+ * Get human-readable device name for a camera.
  *
- * \param instance_id the camera device instance ID
- * \returns device name, shouldn't be freed
+ * The returned string is owned by the caller; please release it with
+ * SDL_free() when done with it.
  *
- * \since This function is available since SDL 3.0.0.
- *
- * \sa SDL_GetCameraDevices
- */
-extern DECLSPEC const char * SDLCALL SDL_GetCameraDeviceName(SDL_CameraDeviceID instance_id);
-
-/**
- * Get the obtained camera spec
+ * \param instance_id the camera device instance ID
+ * \returns Human-readable device name, or NULL on error; call SDL_GetError() for more information.
  *
- * \param device 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
- *          SDL_GetError() for more information.
+ * \threadsafety It is safe to call this function from any thread.
  *
  * \since This function is available since SDL 3.0.0.
  *
- * \sa SDL_SetCameraSpec
- * \sa SDL_OpenCameraWithSpec
+ * \sa SDL_GetCameraDevices
  */
-extern DECLSPEC int SDLCALL SDL_GetCameraSpec(SDL_CameraDevice *device, SDL_CameraSpec *spec);
-
+extern DECLSPEC char * SDLCALL SDL_GetCameraDeviceName(SDL_CameraDeviceID instance_id);
 
 /**
- * Get frame format of camera device.
+ * Open a video capture device (a "camera").
  *
- * The value can be used to fill SDL_CameraSpec structure.
+ * You can open the device with any reasonable spec, and if the hardware can't
+ * directly support it, it will convert data seamlessly to the requested
+ * format. This might incur overhead, including scaling of image data.
  *
- * \param device opened camera device
- * \param index format between 0 and num -1
- * \param format pointer output format (SDL_PixelFormatEnum)
- * \returns 0 on success or a negative error code on failure; call
- *          SDL_GetError() for more information.
+ * If you would rather accept whatever format the device offers, you can
+ * pass a NULL spec here and it will choose one for you (and you can use
+ * SDL_Surface's conversion/scaling functions directly if necessary).
  *
- * \since This function is available since SDL 3.0.0.
+ * You can call SDL_GetCameraSpec() to get the actual data format if
+ * passing a NULL spec here. You can see the exact specs a device can
+ * support without conversion with SDL_GetCameraSupportedSpecs().
  *
- * \sa SDL_GetNumCameraFormats
- */
-extern DECLSPEC int SDLCALL SDL_GetCameraFormat(SDL_CameraDevice *device,
-                                                      int index,
-                                                      Uint32 *format);
-
-/**
- * Number of available formats for the device
+ * \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
+ *          information.
  *
- * \param device opened camera device
- * \returns number of formats or a negative error code on failure; call
- *          SDL_GetError() for more information.
+ * \threadsafety It is safe to call this function from any thread.
  *
  * \since This function is available since SDL 3.0.0.
  *
- * \sa SDL_GetCameraFormat
- * \sa SDL_SetCameraSpec
+ * \sa SDL_GetCameraDeviceName
+ * \sa SDL_GetCameraDevices
+ * \sa SDL_OpenCameraWithSpec
  */
-extern DECLSPEC int SDLCALL SDL_GetNumCameraFormats(SDL_CameraDevice *device);
+extern DECLSPEC SDL_Camera *SDLCALL SDL_OpenCameraDevice(SDL_CameraDeviceID instance_id, const SDL_CameraSpec *spec);
 
 /**
- * Get frame sizes of the device and the specified input format.
+ * Get the instance ID of an opened camera.
  *
- * The value can be used to fill SDL_CameraSpec structure.
+ * \param device an SDL_Camera to query
+ * \returns the instance ID of the specified camera on success or 0 on
+ *          failure; call SDL_GetError() for more information.
  *
- * \param device opened camera device
- * \param format a format that can be used by the device (SDL_PixelFormatEnum)
- * \param index framesize between 0 and num -1
- * \param width output width
- * \param height output height
- * \returns 0 on success or a negative error code on failure; call
- *          SDL_GetError() for more information.
+ * \threadsafety It is safe to call this function from any thread.
  *
  * \since This function is available since SDL 3.0.0.
  *
- * \sa SDL_GetNumCameraFrameSizes
+ * \sa SDL_OpenCameraDevice
  */
-extern DECLSPEC int SDLCALL SDL_GetCameraFrameSize(SDL_CameraDevice *device, Uint32 format, int index, int *width, int *height);
+extern DECLSPEC SDL_CameraDeviceID SDLCALL SDL_GetCameraInstanceID(SDL_Camera *camera);
 
 /**
- * Number of different framesizes available for the device and pixel format.
+ * Get the properties associated with an opened camera.
  *
- * \param device opened camera device
- * \param format frame pixel format (SDL_PixelFormatEnum)
- * \returns number of framesizes or a negative error code on failure; call
+ * \param device the SDL_Camera obtained from SDL_OpenCameraDevice()
+ * \returns a valid property ID on success or 0 on failure; call
  *          SDL_GetError() for more information.
  *
- * \since This function is available since SDL 3.0.0.
- *
- * \sa SDL_GetCameraFrameSize
- * \sa SDL_SetCameraSpec
- */
-extern DECLSPEC int SDLCALL SDL_GetNumCameraFrameSizes(SDL_CameraDevice *device, Uint32 format);
-
-
-/**
- * Get camera status
- *
- * \param device opened camera device
- * \returns 0 on success or a negative error code on failure; call
- *          SDL_GetError() for more information.
+ * \threadsafety It is safe to call this function from any thread.
  *
  * \since This function is available since SDL 3.0.0.
  *
- * \sa SDL_CameraStatus
+ * \sa SDL_GetProperty
+ * \sa SDL_SetProperty
  */
-extern DECLSPEC SDL_CameraStatus SDLCALL SDL_GetCameraStatus(SDL_CameraDevice *device);
+extern DECLSPEC SDL_PropertiesID SDLCALL SDL_GetCameraProperties(SDL_Camera *camera);
 
 /**
- * Start camera
+ * Get the spec that a camera is using when generating images.
+ *
+ * Note that this might not be the native format of the hardware, as SDL
+ * might be converting to this format behind the scenes.
  *
  * \param device 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
  *          SDL_GetError() for more information.
  *
+ * \threadsafety It is safe to call this function from any thread.
+ *
  * \since This function is available since SDL 3.0.0.
  *
- * \sa SDL_StopCamera
+ * \sa SDL_OpenCameraDevice
  */
-extern DECLSPEC int SDLCALL SDL_StartCamera(SDL_CameraDevice *device);
+extern DECLSPEC int SDLCALL SDL_GetCameraSpec(SDL_Camera *camera, SDL_CameraSpec *spec);
 
 /**
  * Acquire a frame.
  *
  * The frame is a memory pointer to the image data, whose size and format are
- * given by the the obtained spec.
+ * given by the spec requested when opening the device.
+ *
+ * This is a non blocking API. If there is a frame available, a non-NULL surface is
+ * returned, and timestampNS will be filled with a non-zero value.
+ *
+ * Note that an error case can also return NULL, but a NULL by itself is normal
+ * and just signifies that a new frame is not yet available. Note that even if a
+ * camera device fails outright (a USB camera is unplugged while in use, etc), SDL
+ * will send an event separately to notify the app, but continue to provide blank
+ * frames at ongoing intervals until SDL_CloseCamera() is called, so real
+ * failure here is almost always an out of memory condition.
  *
- * Non blocking API. If there is a frame available, frame->num_planes is non
- * 0. If frame->num_planes is 0 and returned code is 0, there is no frame at
- * that time.
+ * 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.
  *
- * After used, the frame should be released with SDL_ReleaseCameraFrame
+ * Do not call SDL_FreeSurface() on the returned surface! It must be given back
+ * to the camera subsystem with SDL_ReleaseCameraFrame!
  *
  * \param device opened camera device
- * \param frame pointer to get the frame
- * \returns 0 on success or a negative error code on failure; call
- *          SDL_GetError() for more information.
+ * \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.
+ *
+ * \threadsafety It is safe to call this function from any thread.
  *
  * \since This function is available since SDL 3.0.0.
  *
  * \sa SDL_ReleaseCameraFrame
  */
-extern DECLSPEC int SDLCALL SDL_AcquireCameraFrame(SDL_CameraDevice *device, SDL_CameraFrame *frame);
+extern DECLSPEC SDL_Surface * SDLCALL SDL_AcquireCameraFrame(SDL_Camera *camera, Uint64 *timestampNS);
 
 /**
- * Release a frame.
+ * Release a frame of video acquired from a camera.
  *
  * Let the back-end re-use the internal buffer for camera.
  *
- * All acquired frames should be released before closing the device.
+ * This function _must_ be called only on surface objects returned by
+ * SDL_AcquireCameraFrame(). This function should be called as quickly as
+ * possible after acquisition, as SDL keeps a small FIFO queue of surfaces
+ * for video frames; if surfaces aren't released in a timely manner, SDL
+ * may drop upcoming video frames from the camera.
  *
- * \param device opened camera device
- * \param frame frame pointer.
- * \returns 0 on success or a negative error code on failure; call
- *          SDL_GetError() for more information.
+ * If the app needs to keep the surface for a significant time, they should
+ * make a copy of it and release the original.
  *
- * \since This function is available since SDL 3.0.0.
- *
- * \sa SDL_AcquireCameraFrame
- */
-extern DECLSPEC int SDLCALL SDL_ReleaseCameraFrame(SDL_CameraDevice *device, SDL_CameraFrame *frame);
-
-/**
- * Stop Video Capture
+ * The app should not use the surface again after calling this function;
+ * assume the surface is freed and the pointer is invalid.
  *
  * \param device opened camera device
+ * \param frame The video frame surface to release.
  * \returns 0 on success or a negative error code on failure; call
  *          SDL_GetError() for more information.
  *
+ * \threadsafety It is safe to call this function from any thread.
+ *
  * \since This function is available since SDL 3.0.0.
  *
- * \sa SDL_StartCamera
+ * \sa SDL_AcquireCameraFrame
  */
-extern DECLSPEC int SDLCALL SDL_StopCamera(SDL_CameraDevice *device);
+extern DECLSPEC int SDLCALL SDL_ReleaseCameraFrame(SDL_Camera *camera, SDL_Surface *frame);
 
 /**
  * Use this function to shut down camera processing and close the
@@ -426,12 +353,16 @@ extern DECLSPEC int SDLCALL SDL_StopCamera(SDL_CameraDevice *device);
  *
  * \param device opened camera device
  *
+ * \threadsafety It is safe to call this function from any thread, but
+ *               no thread may reference `device` once this function
+ *               is called.
+ *
  * \since This function is available since SDL 3.0.0.
  *
  * \sa SDL_OpenCameraWithSpec
  * \sa SDL_OpenCamera
  */
-extern DECLSPEC void SDLCALL SDL_CloseCamera(SDL_CameraDevice *device);
+extern DECLSPEC void SDLCALL SDL_CloseCamera(SDL_Camera *camera);
 
 /* Ends C function definitions when using C++ */
 #ifdef __cplusplus

+ 17 - 0
include/SDL3/SDL_events.h

@@ -205,6 +205,10 @@ typedef enum
     SDL_EVENT_PEN_BUTTON_DOWN,            /**< Pressure-sensitive pen button pressed */
     SDL_EVENT_PEN_BUTTON_UP,              /**< Pressure-sensitive pen button released */
 
+    /* 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. */
+
     /* Render events */
     SDL_EVENT_RENDER_TARGETS_RESET = 0x2000, /**< The render targets have been reset and their contents need to be updated */
     SDL_EVENT_RENDER_DEVICE_RESET, /**< The device has been reset and all textures need to be recreated */
@@ -526,6 +530,18 @@ typedef struct SDL_AudioDeviceEvent
     Uint8 padding3;
 } SDL_AudioDeviceEvent;
 
+/**
+ *  Camera device event structure (event.cdevice.*)
+ */
+typedef struct SDL_CameraDeviceEvent
+{
+    Uint32 type;        /**< ::SDL_EVENT_CAMERA_DEVICE_ADDED, or ::SDL_EVENT_CAMERA_DEVICE_REMOVED */
+    Uint64 timestamp;   /**< In nanoseconds, populated using SDL_GetTicksNS() */
+    SDL_CameraDeviceID which;       /**< SDL_CameraDeviceID for the device being added or removed or changing */
+    Uint8 padding1;
+    Uint8 padding2;
+    Uint8 padding3;
+} SDL_CameraDeviceEvent;
 
 /**
  *  Touch finger event structure (event.tfinger.*)
@@ -699,6 +715,7 @@ typedef union SDL_Event
     SDL_GamepadTouchpadEvent gtouchpad;     /**< Gamepad touchpad event data */
     SDL_GamepadSensorEvent gsensor;         /**< Gamepad sensor event data */
     SDL_AudioDeviceEvent adevice;           /**< Audio device event data */
+    SDL_CameraDeviceEvent cdevice;          /**< Camera device event data */
     SDL_SensorEvent sensor;                 /**< Sensor event data */
     SDL_QuitEvent quit;                     /**< Quit request event data */
     SDL_UserEvent user;                     /**< Custom event data */

+ 794 - 424
src/camera/SDL_camera.c

@@ -25,6 +25,10 @@
 #include "../video/SDL_pixels_c.h"
 #include "../thread/SDL_systhread.h"
 
+
+// A lot of this is a simplified version of SDL_audio.c; if fixing stuff here,
+//  maybe check that file, too.
+
 // Available camera drivers
 static const CameraBootStrap *const bootstrap[] = {
 #ifdef SDL_CAMERA_DRIVER_V4L2
@@ -45,15 +49,6 @@ static const CameraBootStrap *const bootstrap[] = {
 static SDL_CameraDriver camera_driver;
 
 
-// list node entries to share frames between SDL and user app
-// !!! FIXME: do we need this struct?
-typedef struct entry_t
-{
-    SDL_CameraFrame frame;
-} entry_t;
-
-static SDL_CameraDevice *open_devices[16];  // !!! FIXME: remove limit
-
 int SDL_GetNumCameraDrivers(void)
 {
     return SDL_arraysize(bootstrap) - 1;
@@ -72,242 +67,435 @@ const char *SDL_GetCurrentCameraDriver(void)
     return camera_driver.name;
 }
 
-
-static void CloseCameraDevice(SDL_CameraDevice *device)
+static void ClosePhysicalCameraDevice(SDL_CameraDevice *device)
 {
     if (!device) {
         return;
     }
 
     SDL_AtomicSet(&device->shutdown, 1);
-    SDL_AtomicSet(&device->enabled, 1);
+
+// !!! FIXME: the close_cond stuff from audio might help the race condition here.
 
     if (device->thread != NULL) {
         SDL_WaitThread(device->thread, NULL);
+        device->thread = NULL;
     }
-    if (device->device_lock != NULL) {
-        SDL_DestroyMutex(device->device_lock);
+
+    // release frames that are queued up somewhere...
+    if (!device->needs_conversion && !device->needs_scaling) {
+        for (SurfaceList *i = device->filled_output_surfaces.next; i != NULL; i = i->next) {
+            camera_driver.impl.ReleaseFrame(device, i->surface);
+        }
+        for (SurfaceList *i = device->app_held_output_surfaces.next; i != NULL; i = i->next) {
+            camera_driver.impl.ReleaseFrame(device, i->surface);
+        }
     }
-    if (device->acquiring_lock != NULL) {
-        SDL_DestroyMutex(device->acquiring_lock);
+
+    camera_driver.impl.CloseDevice(device);
+
+    SDL_DestroyProperties(device->props);
+
+    SDL_DestroySurface(device->acquire_surface);
+    device->acquire_surface = NULL;
+    SDL_DestroySurface(device->conversion_surface);
+    device->conversion_surface = NULL;
+
+    for (int i = 0; i < SDL_arraysize(device->output_surfaces); i++) {
+        SDL_DestroySurface(device->output_surfaces[i].surface);
     }
+    SDL_zeroa(device->output_surfaces);
 
-    const int n = SDL_arraysize(open_devices);
-    for (int i = 0; i < n; i++) {
-        if (open_devices[i] == device) {
-            open_devices[i] = NULL;
-        }
+    device->filled_output_surfaces.next = NULL;
+    device->empty_output_surfaces.next = NULL;
+    device->app_held_output_surfaces.next = NULL;
+}
+
+// this must not be called while `device` is still in a device list, or while a device's camera thread is still running.
+static void DestroyPhysicalCameraDevice(SDL_CameraDevice *device)
+{
+    if (device) {
+        // Destroy any logical devices that still exist...
+        ClosePhysicalCameraDevice(device);
+        camera_driver.impl.FreeDeviceHandle(device);
+        SDL_DestroyMutex(device->lock);
+        SDL_free(device->all_specs);
+        SDL_free(device->name);
+        SDL_free(device);
     }
+}
 
-    entry_t *entry = NULL;
-    while (device->buffer_queue != NULL) {
-        SDL_ListPop(&device->buffer_queue, (void**)&entry);
-        if (entry) {
-            SDL_CameraFrame f = entry->frame;
-            // Release frames not acquired, if any
-            if (f.timestampNS) {
-                camera_driver.impl.ReleaseFrame(device, &f);
-            }
-            SDL_free(entry);
+
+// Don't hold the device lock when calling this, as we may destroy the device!
+void UnrefPhysicalCameraDevice(SDL_CameraDevice *device)
+{
+    if (SDL_AtomicDecRef(&device->refcount)) {
+        // take it out of the device list.
+        SDL_LockRWLockForWriting(camera_driver.device_hash_lock);
+        if (SDL_RemoveFromHashTable(camera_driver.device_hash, (const void *) (uintptr_t) device->instance_id)) {
+            SDL_AtomicAdd(&camera_driver.device_count, -1);
         }
+        SDL_UnlockRWLock(camera_driver.device_hash_lock);
+        DestroyPhysicalCameraDevice(device);  // ...and nuke it.
     }
+}
 
-    camera_driver.impl.CloseDevice(device);
+void RefPhysicalCameraDevice(SDL_CameraDevice *device)
+{
+    SDL_AtomicIncRef(&device->refcount);
+}
 
-    SDL_free(device->dev_name);
-    SDL_free(device);
+static void ObtainPhysicalCameraDeviceObj(SDL_CameraDevice *device) SDL_NO_THREAD_SAFETY_ANALYSIS  // !!! FIXMEL SDL_ACQUIRE
+{
+    if (device) {
+        RefPhysicalCameraDevice(device);
+        SDL_LockMutex(device->lock);
+    }
 }
 
-void SDL_CloseCamera(SDL_CameraDevice *device)
+static SDL_CameraDevice *ObtainPhysicalCameraDevice(SDL_CameraDeviceID devid)  // !!! FIXME: SDL_ACQUIRE
 {
+    if (!SDL_GetCurrentCameraDriver()) {
+        SDL_SetError("Camera subsystem is not initialized");
+        return NULL;
+    }
+
+    SDL_CameraDevice *device = NULL;
+    SDL_LockRWLockForReading(camera_driver.device_hash_lock);
+    SDL_FindInHashTable(camera_driver.device_hash, (const void *) (uintptr_t) devid, (const void **) &device);
+    SDL_UnlockRWLock(camera_driver.device_hash_lock);
     if (!device) {
-        SDL_InvalidParamError("device");
+        SDL_SetError("Invalid camera device instance ID");
     } else {
-        CloseCameraDevice(device);
+        ObtainPhysicalCameraDeviceObj(device);
     }
+    return device;
 }
 
-int SDL_StartCamera(SDL_CameraDevice *device)
+static void ReleaseCameraDevice(SDL_CameraDevice *device) SDL_NO_THREAD_SAFETY_ANALYSIS  // !!! FIXME: SDL_RELEASE
 {
-    if (!device) {
-        return SDL_InvalidParamError("device");
-    } else if (device->is_spec_set == SDL_FALSE) {
-        return SDL_SetError("no spec set");
-    } else if (SDL_GetCameraStatus(device) != SDL_CAMERA_INIT) {
-        return SDL_SetError("invalid state");
+    if (device) {
+        SDL_UnlockMutex(device->lock);
+        UnrefPhysicalCameraDevice(device);
     }
+}
 
-    const int result = camera_driver.impl.StartCamera(device);
-    if (result < 0) {
-        return result;
-    }
+// we want these sorted by format first, so you can find a block of all
+// resolutions that are supported for a format. The formats are sorted in
+// "best" order, but that's subjective: right now, we prefer planar
+// formats, since they're likely what the cameras prefer to produce
+// anyhow, and they basically send the same information in less space
+// than an RGB-style format. After that, sort by bits-per-pixel.
 
-    SDL_AtomicSet(&device->enabled, 1);
+// we want specs sorted largest to smallest dimensions, larger width taking precedence over larger height.
+static int SDLCALL CameraSpecCmp(const void *vpa, const void *vpb)
+{
+    const SDL_CameraSpec *a = (const SDL_CameraSpec *) vpa;
+    const SDL_CameraSpec *b = (const SDL_CameraSpec *) vpb;
+
+    // driver shouldn't send specs like this, check here since we're eventually going to sniff the whole array anyhow.
+    SDL_assert(a->format != SDL_PIXELFORMAT_UNKNOWN);
+    SDL_assert(a->width > 0);
+    SDL_assert(a->height > 0);
+    SDL_assert(b->format != SDL_PIXELFORMAT_UNKNOWN);
+    SDL_assert(b->width > 0);
+    SDL_assert(b->height > 0);
+
+    const Uint32 afmt = a->format;
+    const Uint32 bfmt = b->format;
+    if (SDL_ISPIXELFORMAT_FOURCC(afmt) && !SDL_ISPIXELFORMAT_FOURCC(bfmt)) {
+        return -1;
+    } else if (!SDL_ISPIXELFORMAT_FOURCC(afmt) && SDL_ISPIXELFORMAT_FOURCC(bfmt)) {
+        return 1;
+    } else if (SDL_BITSPERPIXEL(afmt) > SDL_BITSPERPIXEL(bfmt)) {
+        return -1;
+    } else if (SDL_BITSPERPIXEL(bfmt) > SDL_BITSPERPIXEL(afmt)) {
+        return 1;
+    } else if (a->width > b->width) {
+        return -1;
+    } else if (b->width > a->width) {
+        return 1;
+    } else if (a->height > b->height) {
+        return -1;
+    } else if (b->height > a->height) {
+        return 1;
+    }
 
-    return 0;
+    return 0;  // apparently, they're equal.
 }
 
-int SDL_GetCameraSpec(SDL_CameraDevice *device, SDL_CameraSpec *spec)
+
+// The camera backends call this when a new device is plugged in.
+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(handle != NULL);
+
+    SDL_LockRWLockForReading(camera_driver.device_hash_lock);
+    const int shutting_down = SDL_AtomicGet(&camera_driver.shutting_down);
+    SDL_UnlockRWLock(camera_driver.device_hash_lock);
+    if (shutting_down) {
+        return NULL;  // we're shutting down, don't add any devices that are hotplugged at the last possible moment.
+    }
+
+    SDL_CameraDevice *device = (SDL_CameraDevice *)SDL_calloc(1, sizeof(SDL_CameraDevice));
     if (!device) {
-        return SDL_InvalidParamError("device");
-    } else if (!spec) {
-        return SDL_InvalidParamError("spec");
+        return NULL;
     }
 
-    SDL_zerop(spec);
-    return camera_driver.impl.GetDeviceSpec(device, spec);
-}
+    device->name = SDL_strdup(name);
+    if (!device->name) {
+        SDL_free(device);
+        return NULL;
+    }
 
-int SDL_StopCamera(SDL_CameraDevice *device)
-{
-    if (!device) {
-        return SDL_InvalidParamError("device");
-    } else if (SDL_GetCameraStatus(device) != SDL_CAMERA_PLAYING) {
-        return SDL_SetError("invalid state");
+    device->lock = SDL_CreateMutex();
+    if (!device->lock) {
+        SDL_free(device->name);
+        SDL_free(device);
+        return NULL;
     }
 
-    SDL_AtomicSet(&device->enabled, 0);
-    SDL_AtomicSet(&device->shutdown, 1);
+    device->all_specs = SDL_calloc(num_specs + 1, sizeof (*specs));
+    if (!device->all_specs) {
+        SDL_DestroyMutex(device->lock);
+        SDL_free(device->name);
+        SDL_free(device);
+        return NULL;
+    }
+
+    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 ((a->format == b->format) && (a->width == b->width) && (a->height == b->height)) {
+            SDL_memmove(a, b, sizeof (*specs) * (num_specs - i));
+            i--;
+            num_specs--;
+        }
+    }
+
+    device->num_specs = num_specs;
+    device->handle = handle;
+    device->instance_id = SDL_GetNextObjectID();
+    SDL_AtomicSet(&device->shutdown, 0);
+    SDL_AtomicSet(&device->zombie, 0);
+    RefPhysicalCameraDevice(device);
+
+    SDL_LockRWLockForWriting(camera_driver.device_hash_lock);
+    if (SDL_InsertIntoHashTable(camera_driver.device_hash, (const void *) (uintptr_t) device->instance_id, device)) {
+        SDL_AtomicAdd(&camera_driver.device_count, 1);
+    } else {
+        SDL_DestroyMutex(device->lock);
+        SDL_free(device->all_specs);
+        SDL_free(device->name);
+        SDL_free(device);
+        device = NULL;
+    }
 
-    SDL_LockMutex(device->acquiring_lock);
-    const int retval = camera_driver.impl.StopCamera(device);
-    SDL_UnlockMutex(device->acquiring_lock);
+    // Add a device add event to the pending list, to be pushed when the event queue is pumped (away from any of our internal threads).
+    if (device) {
+        SDL_PendingCameraDeviceEvent *p = (SDL_PendingCameraDeviceEvent *) SDL_malloc(sizeof (SDL_PendingCameraDeviceEvent));
+        if (p) {  // if allocation fails, you won't get an event, but we can't help that.
+            p->type = SDL_EVENT_CAMERA_DEVICE_ADDED;
+            p->devid = device->instance_id;
+            p->next = NULL;
+            SDL_assert(camera_driver.pending_events_tail != NULL);
+            SDL_assert(camera_driver.pending_events_tail->next == NULL);
+            camera_driver.pending_events_tail->next = p;
+            camera_driver.pending_events_tail = p;
+        }
+    }
+    SDL_UnlockRWLock(camera_driver.device_hash_lock);
 
-    return (retval < 0) ? -1 : 0;
+    return device;
 }
 
-// Check spec has valid format and frame size
-static int prepare_cameraspec(SDL_CameraDevice *device, const SDL_CameraSpec *desired, SDL_CameraSpec *obtained, int allowed_changes)
+// Called when a device is removed from the system, or it fails unexpectedly, from any thread, possibly even the camera device's thread.
+void SDL_CameraDeviceDisconnected(SDL_CameraDevice *device)
 {
-    // Check format
-    const int numfmts = SDL_GetNumCameraFormats(device);
-    SDL_bool is_format_valid = SDL_FALSE;
+    if (!device) {
+        return;
+    }
 
-    for (int i = 0; i < numfmts; i++) {
-        Uint32 format;
-        if (SDL_GetCameraFormat(device, i, &format) == 0) {
-            if (format == desired->format && format != SDL_PIXELFORMAT_UNKNOWN) {
-                is_format_valid = SDL_TRUE;
-                obtained->format = format;
-                break;
-            }
+    // Save off removal info in a list so we can send events for each, next
+    //  time the event queue pumps, in case something tries to close a device
+    //  from an event filter, as this would risk deadlocks and other disasters
+    //  if done from the device thread.
+    SDL_PendingCameraDeviceEvent pending;
+    pending.next = NULL;
+    SDL_PendingCameraDeviceEvent *pending_tail = &pending;
+
+    ObtainPhysicalCameraDeviceObj(device);
+
+    const SDL_bool first_disconnect = SDL_AtomicCAS(&device->zombie, 0, 1);
+    if (first_disconnect) {   // if already disconnected this device, don't do it twice.
+        // Swap in "Zombie" versions of the usual platform interfaces, so the device will keep
+        // making progress until the app closes it.
+#if 0 // !!! FIXME
+sdfsdf
+        device->WaitDevice = ZombieWaitDevice;
+        device->GetDeviceBuf = ZombieGetDeviceBuf;
+        device->PlayDevice = ZombiePlayDevice;
+        device->WaitCaptureDevice = ZombieWaitDevice;
+        device->CaptureFromDevice = ZombieCaptureFromDevice;
+        device->FlushCapture = ZombieFlushCapture;
+sdfsdf
+#endif
+
+        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 = SDL_EVENT_CAMERA_DEVICE_REMOVED;
+            p->devid = device->instance_id;
+            p->next = NULL;
+            pending_tail->next = p;
+            pending_tail = p;
         }
     }
 
-    if (!is_format_valid) {
-        if (allowed_changes) {
-            for (int i = 0; i < numfmts; i++) {
-                Uint32 format;
-                if (SDL_GetCameraFormat(device, i, &format) == 0) {
-                    if (format != SDL_PIXELFORMAT_UNKNOWN) {
-                        obtained->format = format;
-                        is_format_valid = SDL_TRUE;
-                        break;
-                    }
-                }
-            }
-        } else {
-            return SDL_SetError("Not allowed to change the format");
+    ReleaseCameraDevice(device);
+
+    if (first_disconnect) {
+        if (pending.next) {  // NULL if event is disabled or disaster struck.
+            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);
         }
     }
+}
 
-    if (!is_format_valid) {
-        return SDL_SetError("Invalid format");
+SDL_CameraDevice *SDL_FindPhysicalCameraDeviceByCallback(SDL_bool (*callback)(SDL_CameraDevice *device, void *userdata), void *userdata)
+{
+    if (!SDL_GetCurrentCameraDriver()) {
+        SDL_SetError("Camera subsystem is not initialized");
+        return NULL;
     }
 
-    // Check frame size
-    const int numsizes = SDL_GetNumCameraFrameSizes(device, obtained->format);
-    SDL_bool is_framesize_valid = SDL_FALSE;
+    const void *key;
+    const void *value;
+    void *iter = NULL;
 
-    for (int i = 0; i < numsizes; i++) {
-        int w, h;
-        if (SDL_GetCameraFrameSize(device, obtained->format, i, &w, &h) == 0) {
-            if (desired->width == w && desired->height == h) {
-                is_framesize_valid = SDL_TRUE;
-                obtained->width = w;
-                obtained->height = h;
-                break;
-            }
+    SDL_LockRWLockForReading(camera_driver.device_hash_lock);
+    while (SDL_IterateHashTable(camera_driver.device_hash, &key, &value, &iter)) {
+        SDL_CameraDevice *device = (SDL_CameraDevice *) value;
+        if (callback(device, userdata)) {  // found it?
+            SDL_UnlockRWLock(camera_driver.device_hash_lock);
+            return device;
         }
     }
 
-    if (!is_framesize_valid) {
-        if (allowed_changes) {
-            int w, h;
-            if (SDL_GetCameraFrameSize(device, obtained->format, 0, &w, &h) == 0) {
-                is_framesize_valid = SDL_TRUE;
-                obtained->width = w;
-                obtained->height = h;
-            }
-        } else {
-            return SDL_SetError("Not allowed to change the frame size");
-        }
-    }
+    SDL_UnlockRWLock(camera_driver.device_hash_lock);
 
-    if (!is_framesize_valid) {
-        return SDL_SetError("Invalid frame size");
+    SDL_SetError("Device not found");
+    return NULL;
+}
+
+void SDL_CloseCamera(SDL_Camera *camera)
+{
+    SDL_CameraDevice *device = (SDL_CameraDevice *) camera;  // currently there's no separation between physical and logical device.
+    ClosePhysicalCameraDevice(device);
+}
+
+int SDL_GetCameraSpec(SDL_Camera *camera, SDL_CameraSpec *spec)
+{
+    if (!camera) {
+        return SDL_InvalidParamError("camera");
+    } else if (!spec) {
+        return SDL_InvalidParamError("spec");
     }
 
+    SDL_CameraDevice *device = (SDL_CameraDevice *) camera;  // currently there's no separation between physical and logical device.
+    SDL_copyp(spec, &device->spec);
     return 0;
 }
 
-const char *SDL_GetCameraDeviceName(SDL_CameraDeviceID instance_id)
+char *SDL_GetCameraDeviceName(SDL_CameraDeviceID instance_id)
 {
-    static char buf[256];
-    buf[0] = 0;
-    buf[255] = 0;
+    char *retval = NULL;
+    SDL_CameraDevice *device = ObtainPhysicalCameraDevice(instance_id);
+    if (device) {
+        retval = SDL_strdup(device->name);
+        ReleaseCameraDevice(device);
+    }
+    return retval;
+}
 
-    if (instance_id == 0) {
-        SDL_InvalidParamError("instance_id");
+SDL_CameraDeviceID *SDL_GetCameraDevices(int *count)
+{
+    if (!SDL_GetCurrentCameraDriver()) {
+        SDL_SetError("Camera subsystem is not initialized");
         return NULL;
     }
 
-    if (camera_driver.impl.GetDeviceName(instance_id, buf, sizeof (buf)) < 0) {
-        buf[0] = 0;
+    SDL_CameraDeviceID *retval = NULL;
+
+    SDL_LockRWLockForReading(camera_driver.device_hash_lock);
+    int num_devices = SDL_AtomicGet(&camera_driver.device_count);
+    if (num_devices > 0) {
+        retval = (SDL_CameraDeviceID *) SDL_malloc((num_devices + 1) * sizeof (SDL_CameraDeviceID));
+        if (!retval) {
+            num_devices = 0;
+        } else {
+            int devs_seen = 0;
+            const void *key;
+            const void *value;
+            void *iter = NULL;
+            while (SDL_IterateHashTable(camera_driver.device_hash, &key, &value, &iter)) {
+                retval[devs_seen++] = (SDL_CameraDeviceID) (uintptr_t) key;
+            }
+
+            SDL_assert(devs_seen == num_devices);
+            retval[devs_seen] = 0;  // null-terminated.
+        }
     }
+    SDL_UnlockRWLock(camera_driver.device_hash_lock);
+
+    if (count) {
+        *count = num_devices;
+    }
+
+    return retval;
 
-    return buf;
 }
 
-SDL_CameraDeviceID *SDL_GetCameraDevices(int *count)
+SDL_CameraSpec *SDL_GetCameraDeviceSupportedSpecs(SDL_CameraDeviceID instance_id, int *count)
 {
-    int dummycount = 0;
-    if (!count) {
-        count = &dummycount;
+    if (count) {
+        *count = 0;
     }
 
-    int num = 0;
-    SDL_CameraDeviceID *retval = camera_driver.impl.GetDevices(&num);
-    if (retval) {
-        *count = num;
-        return retval;
+    SDL_CameraDevice *device = ObtainPhysicalCameraDevice(instance_id);
+    if (!device) {
+        return NULL;
     }
 
-    // return list of 0 ID, null terminated
-    retval = (SDL_CameraDeviceID *)SDL_calloc(1, sizeof(*retval));
-    if (retval == NULL) {
-        *count = 0;
-        return NULL;
+    SDL_CameraSpec *retval = (SDL_CameraSpec *) SDL_calloc(device->num_specs + 1, sizeof (SDL_CameraSpec));
+    if (retval) {
+        SDL_memcpy(retval, device->all_specs, sizeof (SDL_CameraSpec) * device->num_specs);
+        if (count) {
+            *count = device->num_specs;
+        }
     }
 
-    retval[0] = 0;
-    *count = 0;
+    ReleaseCameraDevice(device);
 
     return retval;
 }
 
-// Camera thread function
-static int SDLCALL SDL_CameraThread(void *devicep)
-{
-    const int delay = 20;
-    SDL_CameraDevice *device = (SDL_CameraDevice *) devicep;
-
-#if DEBUG_CAMERA
-    SDL_Log("Start thread 'SDL_CameraThread'");
-#endif
 
+// Camera device thread. This is split into chunks, so drivers that need to control this directly can use the pieces they need without duplicating effort.
 
+void SDL_CameraThreadSetup(SDL_CameraDevice *device)
+{
+    //camera_driver.impl.ThreadInit(device);
 #ifdef SDL_VIDEO_DRIVER_ANDROID
     // TODO
     /*
@@ -320,342 +508,464 @@ static int SDLCALL SDL_CameraThread(void *devicep)
     // The camera capture is always a high priority thread
     SDL_SetThreadPriority(SDL_THREAD_PRIORITY_HIGH);
 #endif
+}
 
-    // Perform any thread setup
-    device->threadid = SDL_GetCurrentThreadID();
+SDL_bool SDL_CameraThreadIterate(SDL_CameraDevice *device)
+{
+    SDL_LockMutex(device->lock);
 
-    // Init state
-    // !!! FIXME: use a semaphore or something
-    while (!SDL_AtomicGet(&device->enabled)) {
-        SDL_Delay(delay);
+    if (SDL_AtomicGet(&device->shutdown)) {
+        SDL_UnlockMutex(device->lock);
+        return SDL_FALSE;  // we're done, shut it down.
     }
 
-    // Loop, filling the camera buffers
-    while (!SDL_AtomicGet(&device->shutdown)) {
-        SDL_CameraFrame f;
-        int ret;
+    // !!! FIXME: this should block elsewhere without holding the lock until a frame is available, like the audio subsystem does.
 
-        SDL_zero(f);
+    SDL_bool failed = SDL_FALSE;  // set to true if disaster worthy of treating the device as lost has happened.
+    SDL_Surface *acquired = NULL;
+    SDL_Surface *output_surface = NULL;
+    SurfaceList *slist = NULL;
+    Uint64 timestampNS = 0;
 
-        SDL_LockMutex(device->acquiring_lock);
-        ret = camera_driver.impl.AcquireFrame(device, &f);
-        SDL_UnlockMutex(device->acquiring_lock);
+    // AcquireFrame SHOULD NOT BLOCK, as we are holding a lock right now. Block in WaitDevice instead!
+    const int rc = camera_driver.impl.AcquireFrame(device, device->acquire_surface, &timestampNS);
 
-        if (ret == 0) {
-            if (f.num_planes == 0) {
-                continue;
-            }
-        }
+    if (rc == 1) {  // new frame acquired!
+        #if DEBUG_CAMERA
+        SDL_Log("CAMERA: New frame available!");
+        #endif
 
-        if (ret < 0) {
-            // Flag it as an error
-#if DEBUG_CAMERA
-            SDL_Log("dev[%p] error AcquireFrame: %d %s", (void *)device, ret, SDL_GetError());
-#endif
-            f.num_planes = 0;
+        if (device->empty_output_surfaces.next == NULL) {
+            // uhoh, no output frames available! Either the app is slow, or it forgot to release frames when done with them. Drop this new frame.
+            #if DEBUG_CAMERA
+            SDL_Log("CAMERA: No empty output surfaces! Dropping frame!");
+            #endif
+            camera_driver.impl.ReleaseFrame(device, device->acquire_surface);
+            device->acquire_surface->pixels = NULL;
+            device->acquire_surface->pitch = 0;
+        } else {
+            slist = device->empty_output_surfaces.next;
+            output_surface = slist->surface;
+            device->empty_output_surfaces.next = slist->next;
+            acquired = device->acquire_surface;
+            slist->timestampNS = timestampNS;
         }
+    } else if (rc == 0) {  // no frame available yet; not an error.
+        #if 0 //DEBUG_CAMERA
+        SDL_Log("CAMERA: No frame available yet.");
+        #endif
+    } else {  // fatal error!
+        SDL_assert(rc == -1);
+        #if DEBUG_CAMERA
+        SDL_Log("CAMERA: dev[%p] error AcquireFrame: %s", device, SDL_GetError());
+        #endif
+        failed = SDL_TRUE;
+    }
 
+    // we can let go of the lock once we've tried to grab a frame of video and maybe moved the output frame from the empty to the filled list.
+    // this lets us chew up the CPU for conversion and scaling without blocking other threads.
+    SDL_UnlockMutex(device->lock);
+
+    if (failed) {
+        SDL_assert(slist == NULL);
+        SDL_assert(acquired == NULL);
+        SDL_CameraDeviceDisconnected(device);  // doh.
+    } else if (acquired) {  // we have a new frame, scale/convert if necessary and queue it for the app!
+        SDL_assert(slist != NULL);
+        if (!device->needs_scaling && !device->needs_conversion) {  // no conversion needed? Just move the pointer/pitch into the output surface.
+            output_surface->pixels = acquired->pixels;
+            output_surface->pitch = acquired->pitch;
+        } else {  // convert/scale into a different surface.
+            SDL_Surface *srcsurf = acquired;
+            if (device->needs_scaling == -1) {  // downscaling? Do it first.  -1: downscale, 0: no scaling, 1: upscale
+                SDL_Surface *dstsurf = device->needs_conversion ? device->conversion_surface : output_surface;
+                SDL_SoftStretch(srcsurf, NULL, dstsurf, NULL);  // !!! FIXME: linear scale? letterboxing?
+                srcsurf = dstsurf;
+            }
+            if (device->needs_conversion) {
+                SDL_Surface *dstsurf = (device->needs_scaling == 1) ? device->conversion_surface : output_surface;
+                SDL_ConvertPixels(srcsurf->w, srcsurf->h,
+                                  srcsurf->format->format, srcsurf->pixels, srcsurf->pitch,
+                                  dstsurf->format->format, dstsurf->pixels, dstsurf->pitch);
+                srcsurf = dstsurf;
+            }
+            if (device->needs_scaling == 1) {  // upscaling? Do it last.  -1: downscale, 0: no scaling, 1: upscale
+                SDL_SoftStretch(srcsurf, NULL, output_surface, NULL);  // !!! FIXME: linear scale? letterboxing?
+            }
 
-        entry_t *entry = SDL_malloc(sizeof (entry_t));
-        if (entry == NULL) {
-            goto error_mem;
+            // we made a copy, so we can give the driver back its resources.
+            camera_driver.impl.ReleaseFrame(device, acquired);
         }
 
-        entry->frame = f;
+        // we either released these already after we copied the data, or the pointer was migrated to output_surface.
+        acquired->pixels = NULL;
+        acquired->pitch = 0;
 
-        SDL_LockMutex(device->device_lock);
-        ret = SDL_ListAdd(&device->buffer_queue, entry);
-        SDL_UnlockMutex(device->device_lock);
-
-        if (ret < 0) {
-            SDL_free(entry);
-            goto error_mem;
-        }
+        // make the filled output surface available to the app.
+        SDL_LockMutex(device->lock);
+        slist->next = device->filled_output_surfaces.next;
+        device->filled_output_surfaces.next = slist;
+        SDL_UnlockMutex(device->lock);
     }
 
-#if DEBUG_CAMERA
-    SDL_Log("dev[%p] End thread 'SDL_CameraThread'", (void *)device);
-#endif
-    return 0;
+    return SDL_TRUE;  // always go on if not shutting down, even if device failed.
+}
 
-error_mem:
-#if DEBUG_CAMERA
-    SDL_Log("dev[%p] End thread 'SDL_CameraThread' with error: %s", (void *)device, SDL_GetError());
-#endif
-    SDL_AtomicSet(&device->shutdown, 1);
-    return 0;
+void SDL_CameraThreadShutdown(SDL_CameraDevice *device)
+{
+    //device->FlushCapture(device);
+    //camera_driver.impl.ThreadDeinit(device);
+    //SDL_CameraThreadFinalize(device);
 }
 
-SDL_CameraDevice *SDL_OpenCamera(SDL_CameraDeviceID instance_id)
+// Actual thread entry point, if driver didn't handle this itself.
+static int SDLCALL CameraThread(void *devicep)
 {
-    const int n = SDL_arraysize(open_devices);
-    SDL_CameraDevice *device = NULL;
-    const char *device_name = NULL;
-    int id = -1;
+    SDL_CameraDevice *device = (SDL_CameraDevice *) devicep;
 
-    if (!SDL_WasInit(SDL_INIT_VIDEO)) {
-        SDL_SetError("Video subsystem is not initialized");
-        goto error;
-    }
+    #if DEBUG_CAMERA
+    SDL_Log("CAMERA: Start thread 'SDL_CameraThread'");
+    #endif
 
-    // !!! FIXME: there is a race condition here if two devices open from two threads at once.
-    // Find an available device ID...
-    for (int i = 0; i < n; i++) {
-        if (open_devices[i] == NULL) {
-            id = i;
-            break;
+    SDL_assert(device != NULL);
+    SDL_CameraThreadSetup(device);
+
+    do {
+        if (camera_driver.impl.WaitDevice(device) < 0) {
+            SDL_CameraDeviceDisconnected(device);  // doh. (but don't break out of the loop, just be a zombie for now!)
         }
-    }
+    } while (SDL_CameraThreadIterate(device));
 
-    if (id == -1) {
-        SDL_SetError("Too many open camera devices");
-        goto error;
-    }
+    SDL_CameraThreadShutdown(device);
 
-    if (instance_id != 0) {
-        device_name = SDL_GetCameraDeviceName(instance_id);
-        if (device_name == NULL) {
-            goto error;
+    #if DEBUG_CAMERA
+    SDL_Log("CAMERA: dev[%p] End thread 'SDL_CameraThread'", (void *)device);
+    #endif
+
+    return 0;
+}
+
+static void ChooseBestCameraSpec(SDL_CameraDevice *device, const SDL_CameraSpec *spec, SDL_CameraSpec *closest)
+{
+    // Find the closest available native format/size...
+    //
+    // We want the exact size if possible, even if we have
+    // to convert formats, because we can _probably_ do that
+    // conversion losslessly at less expense verses scaling.
+    //
+    // Failing that, we want the size that's closest to the
+    // requested aspect ratio, then the closest size within
+    // that.
+
+    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...
+        // 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;
+        int wantw = spec->width;
+        int wanth = spec->height;
+
+        // Find the sizes with the closest aspect ratio and then find the best fit of those.
+        const float wantaspect = ((float)wantw) / ((float) wanth);
+        const float epsilon = 1e-6f;
+        float closestaspect = -9999999.0f;
+        float closestdiff = 999999.0f;
+        int closestdiffw = 9999999;
+
+        for (int i = 0; i < num_specs; i++) {
+            const SDL_CameraSpec *thisspec = &device->all_specs[i];
+            const int thisw = thisspec->width;
+            const int thish = thisspec->height;
+            const float thisaspect = ((float)thisw) / ((float) thish);
+            const float aspectdiff = SDL_fabs(wantaspect - thisaspect);
+            const float diff = SDL_fabs(closestaspect - thisaspect);
+            const int diffw = SDL_abs(thisw - wantw);
+            if (diff < epsilon) {  // matches current closestaspect? See if resolution is closer in size.
+                if (diffw < closestdiffw) {
+                    closestdiffw = diffw;
+                    closest->width = thisw;
+                    closest->height = thish;
+                }
+            } else if (aspectdiff < closestdiff) {  // this is a closer aspect ratio? Take it, reset resolution checks.
+                closestdiff = aspectdiff;
+                closestaspect = thisaspect;
+                closestdiffw = diffw;
+                closest->width = thisw;
+                closest->height = thish;
+            }
         }
-    } else {
-        SDL_CameraDeviceID *devices = SDL_GetCameraDevices(NULL);
-        if (devices && devices[0]) {
-            device_name = SDL_GetCameraDeviceName(devices[0]);
-            SDL_free(devices);
+
+        SDL_assert(closest->width > 0);
+        SDL_assert(closest->height > 0);
+
+        // okay, we have what we think is the best resolution, now we just need the best format that supports it...
+        const Uint32 wantfmt = spec->format;
+        Uint32 bestfmt = SDL_PIXELFORMAT_UNKNOWN;
+        for (int i = 0; i < num_specs; i++) {
+            const SDL_CameraSpec *thisspec = &device->all_specs[i];
+            if ((thisspec->width == closest->width) && (thisspec->height == closest->height)) {
+                if (bestfmt == SDL_PIXELFORMAT_UNKNOWN) {
+                    bestfmt = thisspec->format;  // spec list is sorted by what we consider "best" format, so unless we find an exact match later, first size match is the one!
+                }
+                if (thisspec->format == wantfmt) {
+                    bestfmt = thisspec->format;
+                    break;  // exact match, stop looking.
+                }
+            }
         }
+
+        SDL_assert(bestfmt != SDL_PIXELFORMAT_UNKNOWN);
+        closest->format = bestfmt;
     }
 
-#if 0
-    // FIXME do we need this ?
-    // Let the user override.
-    {
-        const char *dev = SDL_getenv("SDL_CAMERA_DEVICE_NAME");
-        if (dev && dev[0]) {
-            device_name = dev;
+    SDL_assert(closest->width > 0);
+    SDL_assert(closest->height > 0);
+    SDL_assert(closest->format != SDL_PIXELFORMAT_UNKNOWN);
+}
+
+SDL_Camera *SDL_OpenCameraDevice(SDL_CameraDeviceID instance_id, const SDL_CameraSpec *spec)
+{
+    if (spec) {
+        if ((spec->width <= 0) || (spec->height <= 0)) {
+            SDL_SetError("Requested spec frame size is invalid");
+            return NULL;
+        } else if (spec->format == SDL_PIXELFORMAT_UNKNOWN) {
+            SDL_SetError("Requested spec format is invalid");
+            return NULL;
         }
     }
-#endif
 
-    if (device_name == NULL) {
-        goto error;
+    SDL_CameraDevice *device = ObtainPhysicalCameraDevice(instance_id);
+    if (!device) {
+        return NULL;
     }
 
-    device = (SDL_CameraDevice *) SDL_calloc(1, sizeof (SDL_CameraDevice));
-    if (device == NULL) {
-        goto error;
+    if (device->hidden != NULL) {
+        ReleaseCameraDevice(device);
+        SDL_SetError("Camera already opened");  // we may remove this limitation at some point.
+        return NULL;
     }
-    device->dev_name = SDL_strdup(device_name);
 
     SDL_AtomicSet(&device->shutdown, 0);
-    SDL_AtomicSet(&device->enabled, 0);
 
-    device->device_lock = SDL_CreateMutex();
-    if (device->device_lock == NULL) {
-        SDL_SetError("Couldn't create acquiring_lock");
-        goto error;
-    }
+    SDL_CameraSpec closest;
+    ChooseBestCameraSpec(device, spec, &closest);
 
-    device->acquiring_lock = SDL_CreateMutex();
-    if (device->acquiring_lock == NULL) {
-        SDL_SetError("Couldn't create acquiring_lock");
-        goto error;
-    }
+    #if DEBUG_CAMERA
+    SDL_Log("CAMERA: App wanted [(%dx%d) fmt=%s], chose [(%dx%d) fmt=%s]", spec ? spec->width : -1, spec ? spec->height : -1, spec ? SDL_GetPixelFormatName(spec->format) : "(null)", closest.width, closest.height, SDL_GetPixelFormatName(closest.format));
+    #endif
 
-    if (camera_driver.impl.OpenDevice(device) < 0) {
-        goto error;
+    if (camera_driver.impl.OpenDevice(device, &closest) < 0) {
+        ClosePhysicalCameraDevice(device);  // in case anything is half-initialized.
+        ReleaseCameraDevice(device);
+        return NULL;
     }
 
-    // empty
-    device->buffer_queue = NULL;
-    open_devices[id] = device;  // add it to our list of open devices.
-
-
-    // Start the camera thread
-    char threadname[64];
-    SDL_snprintf(threadname, sizeof (threadname), "SDLCamera%d", id);
-    device->thread = SDL_CreateThreadInternal(SDL_CameraThread, threadname, 0, device);
-    if (device->thread == NULL) {
-        SDL_SetError("Couldn't create camera thread");
-        goto error;
+    if (!spec) {
+        SDL_copyp(&device->spec, &closest);
+    } else {
+        SDL_copyp(&device->spec, spec);
     }
 
-    return device;
+    SDL_copyp(&device->actual_spec, &closest);
 
-error:
-    CloseCameraDevice(device);
-    return NULL;
-}
-
-int SDL_SetCameraSpec(SDL_CameraDevice *device, const SDL_CameraSpec *desired, SDL_CameraSpec *obtained, int allowed_changes)
-{
-    SDL_CameraSpec _obtained;
-    SDL_CameraSpec _desired;
-    int result;
-
-    if (!device) {
-        return SDL_InvalidParamError("device");
-    } else if (device->is_spec_set == SDL_TRUE) {
-        return SDL_SetError("already configured");
+    if ((closest.width == device->spec.width) && (closest.height == device->spec.height)) {
+        device->needs_scaling = 0;
+    } else {
+        const Uint64 srcarea = ((Uint64) closest.width) * ((Uint64) closest.height);
+        const Uint64 dstarea = ((Uint64) device->spec.width) * ((Uint64) device->spec.height);
+        if (dstarea <= srcarea) {
+            device->needs_scaling = -1;  // downscaling (or changing to new aspect ratio with same area)
+        } else {
+            device->needs_scaling = 1;  // upscaling
+        }
     }
 
-    if (!desired) {
-        SDL_zero(_desired);
-        desired = &_desired;
-        allowed_changes = SDL_CAMERA_ALLOW_ANY_CHANGE;
-    } else {
-        // in case desired == obtained
-        _desired = *desired;
-        desired = &_desired;
+    device->needs_conversion = (closest.format != device->spec.format);
+
+    device->acquire_surface = SDL_CreateSurfaceFrom(NULL, closest.width, closest.height, 0, closest.format);
+    if (!device->acquire_surface) {
+        ClosePhysicalCameraDevice(device);
+        ReleaseCameraDevice(device);
+        return NULL;
     }
 
-    if (!obtained) {
-        obtained = &_obtained;
+    // if we have to scale _and_ convert, we need a middleman surface, since we can't do both changes at once.
+    if (device->needs_scaling && device->needs_conversion) {
+        const SDL_bool downsampling_first = (device->needs_scaling < 0);
+        const SDL_CameraSpec *s = downsampling_first ? &device->spec : &closest;
+        const Uint32 fmt = downsampling_first ? closest.format : device->spec.format;
+        device->conversion_surface = SDL_CreateSurface(s->width, s->height, fmt);
     }
 
-    SDL_zerop(obtained);
+    // output surfaces are in the app-requested format. If no conversion is necessary, we'll just use the pointers
+    // the backend fills into acquired_surface, and you can get all the way from DMA access in the camera hardware
+    // to the app without a single copy. Otherwise, these will be full surfaces that hold converted/scaled copies.
 
-    if (prepare_cameraspec(device, desired, obtained, allowed_changes) < 0) {
-        return -1;
+    for (int i = 0; i < (SDL_arraysize(device->output_surfaces) - 1); i++) {
+        device->output_surfaces[i].next = &device->output_surfaces[i + 1];
     }
+    device->empty_output_surfaces.next = device->output_surfaces;
 
-    device->spec = *obtained;
+    for (int i = 0; i < SDL_arraysize(device->output_surfaces); i++) {
+        SDL_Surface *surf;
+        if (device->needs_scaling || device->needs_conversion) {
+            surf = SDL_CreateSurface(device->spec.width, device->spec.height, device->spec.format);
+        } else {
+            surf = SDL_CreateSurfaceFrom(NULL, device->spec.width, device->spec.height, 0, device->spec.format);
+        }
+
+        if (!surf) {
+            ClosePhysicalCameraDevice(device);
+            ReleaseCameraDevice(device);
+            return NULL;
+        }
 
-    result = camera_driver.impl.InitDevice(device);
-    if (result < 0) {
-        return result;
+        device->output_surfaces[i].surface = surf;
     }
 
-    *obtained = device->spec;
+    // Start the camera thread if necessary
+    if (!camera_driver.impl.ProvidesOwnCallbackThread) {
+        char threadname[64];
+        SDL_snprintf(threadname, sizeof (threadname), "SDLCamera%d", instance_id);
+        device->thread = SDL_CreateThreadInternal(CameraThread, threadname, 0, device);
+        if (!device->thread) {
+            ClosePhysicalCameraDevice(device);
+            ReleaseCameraDevice(device);
+            SDL_SetError("Couldn't create camera thread");
+            return NULL;
+        }
+    }
 
-    device->is_spec_set = SDL_TRUE;
+    ReleaseCameraDevice(device);  // unlock, we're good to go!
 
-    return 0;
+    return (SDL_Camera *) device;  // currently there's no separation between physical and logical device.
 }
 
-int SDL_AcquireCameraFrame(SDL_CameraDevice *device, SDL_CameraFrame *frame)
+SDL_Surface *SDL_AcquireCameraFrame(SDL_Camera *camera, Uint64 *timestampNS)
 {
-    if (!device) {
-        return SDL_InvalidParamError("device");
-    } else if (!frame) {
-        return SDL_InvalidParamError("frame");
+    if (timestampNS) {
+        *timestampNS = 0;
     }
 
-    SDL_zerop(frame);
+    if (!camera) {
+        SDL_InvalidParamError("camera");
+        return NULL;
+    }
 
-    if (device->thread == NULL) {
-        int ret;
+    SDL_CameraDevice *device = (SDL_CameraDevice *) camera;  // currently there's no separation between physical and logical device.
 
-        // Wait for a frame
-        while ((ret = camera_driver.impl.AcquireFrame(device, frame)) == 0) {
-            if (frame->num_planes) {
-                return 0;
-            }
-        }
-        return -1;
-    } else {
-        entry_t *entry = NULL;
+    ObtainPhysicalCameraDeviceObj(device);
 
-        SDL_LockMutex(device->device_lock);
-        SDL_ListPop(&device->buffer_queue, (void**)&entry);
-        SDL_UnlockMutex(device->device_lock);
+    SDL_Surface *retval = NULL;
 
-        if (entry) {
-            *frame = entry->frame;
-            SDL_free(entry);
+    // frames are in this list from newest to oldest, so find the end of the list...
+    SurfaceList *slistprev = &device->filled_output_surfaces;
+    SurfaceList *slist = slistprev;
+    while (slist->next) {
+        slistprev = slist;
+        slist = slist->next;
+    }
 
-            // Error from thread
-            if (frame->num_planes == 0 && frame->timestampNS == 0) {
-                return SDL_SetError("error from acquisition thread");
-            }
-        } else {
-            // Queue is empty. Not an error.
+    const SDL_bool list_is_empty = (slist == slistprev);
+    if (!list_is_empty) { // report the oldest frame.
+        if (timestampNS) {
+            *timestampNS = slist->timestampNS;
         }
+        retval = slist->surface;
+        slistprev->next = slist->next;  // remove from filled list.
+        slist->next = device->app_held_output_surfaces.next;  // add to app_held list.
+        device->app_held_output_surfaces.next = slist;
     }
 
-    return 0;
+    ReleaseCameraDevice(device);
+
+    return retval;
 }
 
-int SDL_ReleaseCameraFrame(SDL_CameraDevice *device, SDL_CameraFrame *frame)
+int SDL_ReleaseCameraFrame(SDL_Camera *camera, SDL_Surface *frame)
 {
-    if (!device) {
-        return SDL_InvalidParamError("device");
+    if (!camera) {
+        return SDL_InvalidParamError("camera");
     } else if (frame == NULL) {
         return SDL_InvalidParamError("frame");
-    } else if (camera_driver.impl.ReleaseFrame(device, frame) < 0) {
-        return -1;
     }
 
-    SDL_zerop(frame);
-    return 0;
-}
+    SDL_CameraDevice *device = (SDL_CameraDevice *) camera;  // currently there's no separation between physical and logical device.
+    ObtainPhysicalCameraDeviceObj(device);
 
-int SDL_GetNumCameraFormats(SDL_CameraDevice *device)
-{
-    if (!device) {
-        return SDL_InvalidParamError("device");
+    SurfaceList *slistprev = &device->app_held_output_surfaces;
+    SurfaceList *slist;
+    for (slist = slistprev->next; slist != NULL; slist = slist->next) {
+        if (slist->surface == frame) {
+            break;
+        }
+        slistprev = slist;
     }
-    return camera_driver.impl.GetNumFormats(device);
-}
 
-int SDL_GetCameraFormat(SDL_CameraDevice *device, int index, Uint32 *format)
-{
-    if (!device) {
-        return SDL_InvalidParamError("device");
-    } else if (!format) {
-        return SDL_InvalidParamError("format");
+    if (!slist) {
+        ReleaseCameraDevice(device);
+        return SDL_SetError("Surface was not acquired from this camera, or was already released");
     }
-    *format = 0;
-    return camera_driver.impl.GetFormat(device, index, format);
-}
 
-int SDL_GetNumCameraFrameSizes(SDL_CameraDevice *device, Uint32 format)
-{
-    if (!device) {
-        return SDL_InvalidParamError("device");
+    // this pointer was owned by the backend (DMA memory or whatever), clear it out.
+    if (!device->needs_conversion && !device->needs_scaling) {
+        camera_driver.impl.ReleaseFrame(device, frame);
+        frame->pixels = NULL;
+        frame->pitch = 0;
     }
-    return camera_driver.impl.GetNumFrameSizes(device, format);
-}
 
-int SDL_GetCameraFrameSize(SDL_CameraDevice *device, Uint32 format, int index, int *width, int *height)
-{
-    if (!device) {
-        return SDL_InvalidParamError("device");
-    } else if (!width) {
-        return SDL_InvalidParamError("width");
-    } else if (!height) {
-        return SDL_InvalidParamError("height");
-    }
-    *width = *height = 0;
-    return camera_driver.impl.GetFrameSize(device, format, index, width, height);
+    slist->timestampNS = 0;
+
+    // remove from app_held list...
+    slistprev->next = slist->next;
+
+    // insert at front of empty list (and we'll use it first when we need to fill a new frame).
+    slist->next = device->empty_output_surfaces.next;
+    device->empty_output_surfaces.next = slist;
+
+    ReleaseCameraDevice(device);
+
+    return 0;
 }
 
-SDL_CameraDevice *SDL_OpenCameraWithSpec(SDL_CameraDeviceID instance_id, const SDL_CameraSpec *desired, SDL_CameraSpec *obtained, int allowed_changes)
-{
-    SDL_CameraDevice *device;
+// !!! FIXME: add a way to "pause" camera output.
 
-    if ((device = SDL_OpenCamera(instance_id)) == NULL) {
-        return NULL;
+SDL_CameraDeviceID SDL_GetCameraInstanceID(SDL_Camera *camera)
+{
+    SDL_CameraDeviceID retval = 0;
+    if (!camera) {
+        SDL_InvalidParamError("camera");
+    } else {
+        SDL_CameraDevice *device = (SDL_CameraDevice *) camera;  // currently there's no separation between physical and logical device.
+        ObtainPhysicalCameraDeviceObj(device);
+        retval = device->instance_id;
+        ReleaseCameraDevice(device);
     }
 
-    if (SDL_SetCameraSpec(device, desired, obtained, allowed_changes) < 0) {
-        SDL_CloseCamera(device);
-        return NULL;
-    }
-    return device;
+    return retval;
 }
 
-SDL_CameraStatus SDL_GetCameraStatus(SDL_CameraDevice *device)
+SDL_PropertiesID SDL_GetCameraProperties(SDL_Camera *camera)
 {
-    if (device == NULL) {
-        return SDL_CAMERA_INIT;
-    } else if (device->is_spec_set == SDL_FALSE) {
-        return SDL_CAMERA_INIT;
-    } else if (SDL_AtomicGet(&device->shutdown)) {
-        return SDL_CAMERA_STOPPED;
-    } else if (SDL_AtomicGet(&device->enabled)) {
-        return SDL_CAMERA_PLAYING;
+    SDL_PropertiesID retval = 0;
+    if (!camera) {
+        SDL_InvalidParamError("camera");
+    } else {
+        SDL_CameraDevice *device = (SDL_CameraDevice *) camera;  // currently there's no separation between physical and logical device.
+        ObtainPhysicalCameraDeviceObj(device);
+        if (device->props == 0) {
+            device->props = SDL_CreateProperties();
+        }
+        retval = device->props;
+        ReleaseCameraDevice(device);
     }
-    return SDL_CAMERA_INIT;
+
+    return retval;
 }
 
 static void CompleteCameraEntryPoints(void)
@@ -665,18 +975,9 @@ static void CompleteCameraEntryPoints(void)
     FILL_STUB(DetectDevices);
     FILL_STUB(OpenDevice);
     FILL_STUB(CloseDevice);
-    FILL_STUB(InitDevice);
-    FILL_STUB(GetDeviceSpec);
-    FILL_STUB(StartCamera);
-    FILL_STUB(StopCamera);
     FILL_STUB(AcquireFrame);
     FILL_STUB(ReleaseFrame);
-    FILL_STUB(GetNumFormats);
-    FILL_STUB(GetFormat);
-    FILL_STUB(GetNumFrameSizes);
-    FILL_STUB(GetFrameSize);
-    FILL_STUB(GetDeviceName);
-    FILL_STUB(GetDevices);
+    FILL_STUB(FreeDeviceHandle);
     FILL_STUB(Deinitialize);
     #undef FILL_STUB
 }
@@ -687,38 +988,70 @@ void SDL_QuitCamera(void)
         return;
     }
 
-    const int n = SDL_arraysize(open_devices);
-    for (int i = 0; i < n; i++) {
-        CloseCameraDevice(open_devices[i]);
-    }
-
-    SDL_zeroa(open_devices);
-
-#if 0 // !!! FIXME
+    SDL_LockRWLockForWriting(camera_driver.device_hash_lock);
+    SDL_AtomicSet(&camera_driver.shutting_down, 1);
+    SDL_HashTable *device_hash = camera_driver.device_hash;
+    camera_driver.device_hash = NULL;
     SDL_PendingCameraDeviceEvent *pending_events = camera_driver.pending_events.next;
     camera_driver.pending_events.next = NULL;
+    SDL_AtomicSet(&camera_driver.device_count, 0);
+    SDL_UnlockRWLock(camera_driver.device_hash_lock);
 
     SDL_PendingCameraDeviceEvent *pending_next = NULL;
     for (SDL_PendingCameraDeviceEvent *i = pending_events; i; i = pending_next) {
         pending_next = i->next;
         SDL_free(i);
     }
-#endif
+
+    const void *key;
+    const void *value;
+    void *iter = NULL;
+    while (SDL_IterateHashTable(device_hash, &key, &value, &iter)) {
+        DestroyPhysicalCameraDevice((SDL_CameraDevice *) value);
+    }
 
     // Free the driver data
     camera_driver.impl.Deinitialize();
 
+    SDL_DestroyRWLock(camera_driver.device_hash_lock);
+    SDL_DestroyHashTable(device_hash);
+
     SDL_zero(camera_driver);
 }
 
-// this is 90% the same code as the audio subsystem uses.
+
+static Uint32 HashCameraDeviceID(const void *key, void *data)
+{
+    // The values are unique incrementing integers, starting at 1, so just return minus 1 to start with bucket zero.
+    return ((Uint32) ((uintptr_t) key)) - 1;
+}
+
+static SDL_bool MatchCameraDeviceID(const void *a, const void *b, void *data)
+{
+    return (a == b);  // simple integers, just compare them as pointer values.
+}
+
+static void NukeCameraDeviceHashItem(const void *key, const void *value, void *data)
+{
+    // no-op, keys and values in this hashtable are treated as Plain Old Data and don't get freed here.
+}
+
 int SDL_CameraInit(const char *driver_name)
 {
     if (SDL_GetCurrentCameraDriver()) {
         SDL_QuitCamera(); // shutdown driver if already running.
     }
 
-    SDL_zeroa(open_devices);
+    SDL_RWLock *device_hash_lock = SDL_CreateRWLock();  // create this early, so if it fails we don't have to tear down the whole camera subsystem.
+    if (!device_hash_lock) {
+        return -1;
+    }
+
+    SDL_HashTable *device_hash = SDL_CreateHashTable(NULL, 8, HashCameraDeviceID, MatchCameraDeviceID, NukeCameraDeviceHashItem, SDL_FALSE);
+    if (!device_hash) {
+        SDL_DestroyRWLock(device_hash_lock);
+        return -1;
+    }
 
     // Select the proper camera driver
     if (!driver_name) {
@@ -733,6 +1066,8 @@ int SDL_CameraInit(const char *driver_name)
         const char *driver_attempt = driver_name_copy;
 
         if (!driver_name_copy) {
+            SDL_DestroyRWLock(device_hash_lock);
+            SDL_DestroyHashTable(device_hash);
             return -1;
         }
 
@@ -746,9 +1081,9 @@ int SDL_CameraInit(const char *driver_name)
                 if (SDL_strcasecmp(bootstrap[i]->name, driver_attempt) == 0) {
                     tried_to_init = SDL_TRUE;
                     SDL_zero(camera_driver);
-                    #if 0  // !!! FIXME
                     camera_driver.pending_events_tail = &camera_driver.pending_events;
-                    #endif
+                    camera_driver.device_hash_lock = device_hash_lock;
+                    camera_driver.device_hash = device_hash;
                     if (bootstrap[i]->init(&camera_driver.impl)) {
                         camera_driver.name = bootstrap[i]->name;
                         camera_driver.desc = bootstrap[i]->desc;
@@ -770,9 +1105,9 @@ int SDL_CameraInit(const char *driver_name)
 
             tried_to_init = SDL_TRUE;
             SDL_zero(camera_driver);
-            #if 0  // !!! FIXME
             camera_driver.pending_events_tail = &camera_driver.pending_events;
-            #endif
+            camera_driver.device_hash_lock = device_hash_lock;
+            camera_driver.device_hash = device_hash;
             if (bootstrap[i]->init(&camera_driver.impl)) {
                 camera_driver.name = bootstrap[i]->name;
                 camera_driver.desc = bootstrap[i]->desc;
@@ -792,6 +1127,8 @@ int SDL_CameraInit(const char *driver_name)
         }
 
         SDL_zero(camera_driver);
+        SDL_DestroyRWLock(device_hash_lock);
+        SDL_DestroyHashTable(device_hash);
         return -1;  // No driver was available, so fail.
     }
 
@@ -803,3 +1140,36 @@ int SDL_CameraInit(const char *driver_name)
     return 0;
 }
 
+// This is an internal function, so SDL_PumpEvents() can check for pending camera device events.
+// ("UpdateSubsystem" is the same naming that the other things that hook into PumpEvents use.)
+void SDL_UpdateCamera(void)
+{
+    SDL_LockRWLockForReading(camera_driver.device_hash_lock);
+    SDL_PendingCameraDeviceEvent *pending_events = camera_driver.pending_events.next;
+    SDL_UnlockRWLock(camera_driver.device_hash_lock);
+
+    if (!pending_events) {
+        return;  // nothing to do, check next time.
+    }
+
+    // okay, let's take this whole list of events so we can dump the lock, and new ones can queue up for a later update.
+    SDL_LockRWLockForWriting(camera_driver.device_hash_lock);
+    pending_events = camera_driver.pending_events.next;  // in case this changed...
+    camera_driver.pending_events.next = NULL;
+    camera_driver.pending_events_tail = &camera_driver.pending_events;
+    SDL_UnlockRWLock(camera_driver.device_hash_lock);
+
+    SDL_PendingCameraDeviceEvent *pending_next = NULL;
+    for (SDL_PendingCameraDeviceEvent *i = pending_events; i; i = pending_next) {
+        pending_next = i->next;
+        if (SDL_EventEnabled(i->type)) {
+            SDL_Event event;
+            SDL_zero(event);
+            event.type = i->type;
+            event.adevice.which = (Uint32) i->devid;
+            SDL_PushEvent(&event);
+        }
+        SDL_free(i);
+    }
+}
+

+ 3 - 0
src/camera/SDL_camera_c.h

@@ -29,4 +29,7 @@ int SDL_CameraInit(const char *driver_name);
 // Shutdown the camera subsystem
 void SDL_QuitCamera(void);
 
+// "Pump" the event queue.
+extern void SDL_UpdateCamera(void);
+
 #endif // SDL_camera_c_h_

+ 103 - 29
src/camera/SDL_syscamera.h

@@ -23,67 +23,141 @@
 #ifndef SDL_syscamera_h_
 #define SDL_syscamera_h_
 
-#include "../SDL_list.h"
+#include "../SDL_hashtable.h"
 
 #define DEBUG_CAMERA 1
 
-// The SDL camera driver
+
+// !!! FIXME: update these drivers!
+#ifdef SDL_CAMERA_DRIVER_COREMEDIA
+#undef SDL_CAMERA_DRIVER_COREMEDIA
+#endif
+#ifdef SDL_CAMERA_DRIVER_ANDROID
+#undef SDL_CAMERA_DRIVER_ANDROID
+#endif
+
 typedef struct SDL_CameraDevice SDL_CameraDevice;
 
+/* Backends should call this as devices are added to the system (such as
+   a USB camera being plugged in), and should also be called for
+   for every device found during DetectDevices(). */
+extern SDL_CameraDevice *SDL_AddCameraDevice(const char *name, int num_specs, const SDL_CameraSpec *specs, void *handle);
+
+/* Backends should call this if an opened camera device is lost.
+   This can happen due to i/o errors, or a device being unplugged, etc. */
+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);
+
+// 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);
+extern void SDL_CameraThreadShutdown(SDL_CameraDevice *device);
+
+typedef struct SurfaceList
+{
+    SDL_Surface *surface;
+    Uint64 timestampNS;
+    struct SurfaceList *next;
+} SurfaceList;
+
 // Define the SDL camera driver structure
 struct SDL_CameraDevice
 {
-    // The device's current camera specification
+    // A mutex for locking
+    SDL_Mutex *lock;
+
+    // Human-readable device name.
+    char *name;
+
+    // When refcount hits zero, we destroy the device object.
+    SDL_AtomicInt refcount;
+
+    // All supported formats/dimensions for this device.
+    SDL_CameraSpec *all_specs;
+
+    // Elements in all_specs.
+    int num_specs;
+
+    // The device's actual specification that the camera is outputting, before conversion.
+    SDL_CameraSpec actual_spec;
+
+    // The device's current camera specification, after conversions.
     SDL_CameraSpec spec;
 
-    // Device name
-    char *dev_name;
+    // Unique value assigned at creation time.
+    SDL_CameraDeviceID instance_id;
+
+    // Driver-specific hardware data on how to open device (`hidden` is driver-specific data _when opened_).
+    void *handle;
+
+    // Pixel data flows from the driver into these, then gets converted for the app if necessary.
+    SDL_Surface *acquire_surface;
+
+    // acquire_surface converts or scales to this surface before landing in output_surfaces, if necessary.
+    SDL_Surface *conversion_surface;
+
+    // A queue of surfaces that buffer converted/scaled frames of video until the app claims them.
+    SurfaceList output_surfaces[8];
+    SurfaceList filled_output_surfaces;        // this is FIFO
+    SurfaceList empty_output_surfaces;         // this is LIFO
+    SurfaceList app_held_output_surfaces;
+
+    // non-zero if acquire_surface needs to be scaled for final output.
+    int needs_scaling;  // -1: downscale, 0: no scaling, 1: upscale
+
+    // SDL_TRUE if acquire_surface needs to be converted for final output.
+    SDL_bool needs_conversion;
 
     // Current state flags
     SDL_AtomicInt shutdown;
-    SDL_AtomicInt enabled;
-    SDL_bool is_spec_set;
-
-    // A mutex for locking the queue buffers
-    SDL_Mutex *device_lock;
-    SDL_Mutex *acquiring_lock;
+    SDL_AtomicInt zombie;
 
     // A thread to feed the camera device
     SDL_Thread *thread;
-    SDL_ThreadID threadid;
 
-    // Queued buffers (if app not using callback).
-    SDL_ListNode *buffer_queue;
+    // Optional properties.
+    SDL_PropertiesID props;
 
-    // Data private to this driver
+    // Data private to this driver, used when device is opened and running.
     struct SDL_PrivateCameraData *hidden;
 };
 
 typedef struct SDL_CameraDriverImpl
 {
     void (*DetectDevices)(void);
-    int (*OpenDevice)(SDL_CameraDevice *_this);
-    void (*CloseDevice)(SDL_CameraDevice *_this);
-    int (*InitDevice)(SDL_CameraDevice *_this);
-    int (*GetDeviceSpec)(SDL_CameraDevice *_this, SDL_CameraSpec *spec);
-    int (*StartCamera)(SDL_CameraDevice *_this);
-    int (*StopCamera)(SDL_CameraDevice *_this);
-    int (*AcquireFrame)(SDL_CameraDevice *_this, SDL_CameraFrame *frame);
-    int (*ReleaseFrame)(SDL_CameraDevice *_this, SDL_CameraFrame *frame);
-    int (*GetNumFormats)(SDL_CameraDevice *_this);
-    int (*GetFormat)(SDL_CameraDevice *_this, int index, Uint32 *format);
-    int (*GetNumFrameSizes)(SDL_CameraDevice *_this, Uint32 format);
-    int (*GetFrameSize)(SDL_CameraDevice *_this, Uint32 format, int index, int *width, int *height);
-    int (*GetDeviceName)(SDL_CameraDeviceID instance_id, char *buf, int size);
-    SDL_CameraDeviceID *(*GetDevices)(int *count);
+    int (*OpenDevice)(SDL_CameraDevice *device, const SDL_CameraSpec *spec);
+    void (*CloseDevice)(SDL_CameraDevice *device);
+    int (*WaitDevice)(SDL_CameraDevice *device);
+    int (*AcquireFrame)(SDL_CameraDevice *device, SDL_Surface *frame, Uint64 *timestampNS); // set frame->pixels, frame->pitch, and *timestampNS!
+    void (*ReleaseFrame)(SDL_CameraDevice *device, SDL_Surface *frame); // Reclaim frame->pixels and frame->pitch!
+    void (*FreeDeviceHandle)(SDL_CameraDevice *device); // SDL is done with this device; free the handle from SDL_AddCameraDevice()
     void (*Deinitialize)(void);
+
+    SDL_bool ProvidesOwnCallbackThread;
 } SDL_CameraDriverImpl;
 
+typedef struct SDL_PendingCameraDeviceEvent
+{
+    Uint32 type;
+    SDL_CameraDeviceID devid;
+    struct SDL_PendingCameraDeviceEvent *next;
+} SDL_PendingCameraDeviceEvent;
+
 typedef struct SDL_CameraDriver
 {
     const char *name;  // The name of this camera driver
     const char *desc;  // The description of this camera driver
     SDL_CameraDriverImpl impl; // the backend's interface
+
+    SDL_RWLock *device_hash_lock;  // A rwlock that protects `device_hash`
+    SDL_HashTable *device_hash;  // the collection of currently-available camera devices
+    SDL_PendingCameraDeviceEvent pending_events;
+    SDL_PendingCameraDeviceEvent *pending_events_tail;
+
+    SDL_AtomicInt device_count;
+    SDL_AtomicInt shutting_down;  // non-zero during SDL_Quit, so we known not to accept any last-minute device hotplugs.
 } SDL_CameraDriver;
 
 typedef struct CameraBootStrap

+ 9 - 6
src/camera/coremedia/SDL_camera_coremedia.m

@@ -135,7 +135,9 @@ static Uint32 nsfourcc_to_sdlformat(NSString *nsfourcc)
     if (SDL_strcmp("yuvs", str) == 0)  return SDL_PIXELFORMAT_UYVY;
     if (SDL_strcmp("420f", str) == 0)  return SDL_PIXELFORMAT_UNKNOWN;
 
-    SDL_Log("Unknown format '%s'", str);
+    #if DEBUG_CAMERA
+    SDL_Log("CAMERA: Unknown format '%s'", str);
+    #endif
 
     return SDL_PIXELFORMAT_UNKNOWN;
 }
@@ -177,8 +179,9 @@ static NSString *sdlformat_to_nsfourcc(Uint32 fmt)
     - (void)captureOutput:(AVCaptureOutput *)output
         didDropSampleBuffer:(CMSampleBufferRef)sampleBuffer
         fromConnection:(AVCaptureConnection *)connection {
-            // !!! FIXME #if DEBUG_CAMERA
-            SDL_Log("Drop frame..");
+            #if DEBUG_CAMERA
+            SDL_Log("CAMERA: Drop frame..");
+            #endif
         }
 @end
 
@@ -362,13 +365,13 @@ static int COREMEDIA_AcquireFrame(SDL_CameraDevice *_this, SDL_CameraFrame *fram
         const int numPlanes = CVPixelBufferGetPlaneCount(image);
         const int planar = CVPixelBufferIsPlanar(image);
 
-#if 0
+        #if DEBUG_CAMERA
         const int w = CVPixelBufferGetWidth(image);
         const int h = CVPixelBufferGetHeight(image);
         const int sz = CVPixelBufferGetDataSize(image);
         const int pitch = CVPixelBufferGetBytesPerRow(image);
-        SDL_Log("buffer planar=%d count:%d %d x %d sz=%d pitch=%d", planar, numPlanes, w, h, sz, pitch);
-#endif
+        SDL_Log("CAMERA: buffer planar=%d count:%d %d x %d sz=%d pitch=%d", planar, numPlanes, w, h, sz, pitch);
+        #endif
 
         CVPixelBufferLockBaseAddress(image, 0);
 

+ 80 - 0
src/camera/dummy/SDL_camera_dummy.c

@@ -0,0 +1,80 @@
+/*
+  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_DUMMY
+
+#include "../SDL_syscamera.h"
+
+static int DUMMYCAMERA_OpenDevice(SDL_CameraDevice *device, const SDL_CameraSpec *spec)
+{
+    return SDL_Unsupported();
+}
+
+static void DUMMYCAMERA_CloseDevice(SDL_CameraDevice *device)
+{
+}
+
+static int DUMMYCAMERA_WaitDevice(SDL_CameraDevice *device)
+{
+    return SDL_Unsupported();
+}
+
+static int DUMMYCAMERA_AcquireFrame(SDL_CameraDevice *device, SDL_Surface *frame, Uint64 *timestampNS)
+{
+    return SDL_Unsupported();
+}
+
+static void DUMMYCAMERA_ReleaseFrame(SDL_CameraDevice *device, SDL_Surface *frame)
+{
+}
+
+static void DUMMYCAMERA_DetectDevices(void)
+{
+}
+
+static void DUMMYCAMERA_FreeDeviceHandle(SDL_CameraDevice *device)
+{
+}
+
+static void DUMMYCAMERA_Deinitialize(void)
+{
+}
+
+static SDL_bool DUMMYCAMERA_Init(SDL_CameraDriverImpl *impl)
+{
+    impl->DetectDevices = DUMMYCAMERA_DetectDevices;
+    impl->OpenDevice = DUMMYCAMERA_OpenDevice;
+    impl->CloseDevice = DUMMYCAMERA_CloseDevice;
+    impl->WaitDevice = DUMMYCAMERA_WaitDevice;
+    impl->AcquireFrame = DUMMYCAMERA_AcquireFrame;
+    impl->ReleaseFrame = DUMMYCAMERA_ReleaseFrame;
+    impl->FreeDeviceHandle = DUMMYCAMERA_FreeDeviceHandle;
+    impl->Deinitialize = DUMMYCAMERA_Deinitialize;
+
+    return SDL_TRUE;
+}
+
+CameraBootStrap DUMMYCAMERA_bootstrap = {
+    "dummy", "SDL dummy camera driver", DUMMYCAMERA_Init, SDL_TRUE
+};
+
+#endif

+ 434 - 736
src/camera/v4l2/SDL_camera_v4l2.c

@@ -22,43 +22,38 @@
 
 #ifdef SDL_CAMERA_DRIVER_V4L2
 
+#include <unistd.h>
+#include <sys/ioctl.h>
+#include <fcntl.h>              // low-level i/o
+#include <errno.h>
+#include <sys/mman.h>
+#include <sys/stat.h>
+#include <linux/videodev2.h>
+
 #include "../SDL_syscamera.h"
 #include "../SDL_camera_c.h"
 #include "../../video/SDL_pixels_c.h"
 #include "../../thread/SDL_systhread.h"
 #include "../../core/linux/SDL_evdev_capabilities.h"
 #include "../../core/linux/SDL_udev.h"
-#include <limits.h>      // INT_MAX
-
-#define MAX_CAMERA_DEVICES 128 // It's doubtful someone has more than that
 
-static int MaybeAddDevice(const char *path);
-#ifdef SDL_USE_LIBUDEV
-static int MaybeRemoveDevice(const char *path);
-static void CameraUdevCallback(SDL_UDEV_deviceevent udev_type, int udev_class, const char *devpath);
-#endif // SDL_USE_LIBUDEV
+#ifndef SDL_USE_LIBUDEV
+#include <dirent.h>
+#endif
 
-// List of available camera devices.
-typedef struct SDL_cameralist_item
+typedef struct V4L2DeviceHandle
 {
-    char *fname;        // Dev path name (like /dev/video0)
-    char *bus_info;     // don't add two paths with same bus_info (eg /dev/video0 and /dev/video1
-    SDL_CameraDeviceID instance_id;
-    SDL_CameraDevice *device; // Associated device
-    struct SDL_cameralist_item *next;
-} SDL_cameralist_item;
+    char *bus_info;
+    char *path;
+} V4L2DeviceHandle;
 
-static SDL_cameralist_item *SDL_cameralist = NULL;
-static SDL_cameralist_item *SDL_cameralist_tail = NULL;
-static int num_cameras = 0;
 
-
-
-enum io_method {
+typedef enum io_method {
+    IO_METHOD_INVALID,
     IO_METHOD_READ,
     IO_METHOD_MMAP,
     IO_METHOD_USERPTR
-};
+} io_method;
 
 struct buffer {
     void   *start;
@@ -69,21 +64,13 @@ struct buffer {
 struct SDL_PrivateCameraData
 {
     int fd;
-    enum io_method io;
+    io_method io;
     int nb_buffers;
     struct buffer *buffers;
     int first_start;
     int driver_pitch;
 };
 
-#include <unistd.h>
-#include <sys/ioctl.h>
-#include <fcntl.h>              // low-level i/o
-#include <errno.h>
-#include <sys/mman.h>
-#include <sys/stat.h>
-#include <linux/videodev2.h>
-
 static int xioctl(int fh, int request, void *arg)
 {
     int r;
@@ -95,17 +82,40 @@ static int xioctl(int fh, int request, void *arg)
     return r;
 }
 
-// -1:error  1:frame 0:no frame
-static int acquire_frame(SDL_CameraDevice *_this, SDL_CameraFrame *frame)
+static int V4L2_WaitDevice(SDL_CameraDevice *device)
 {
-    const int fd = _this->hidden->fd;
-    enum io_method io = _this->hidden->io;
-    size_t size = _this->hidden->buffers[0].length;
+    const int fd = device->hidden->fd;
+
+    int retval;
+
+    do {
+        fd_set fds;
+        FD_ZERO(&fds);
+        FD_SET(fd, &fds);
+
+        struct timeval tv;
+        tv.tv_sec = 0;
+        tv.tv_usec = 100 * 1000;
+
+        retval = select(fd + 1, &fds, NULL, NULL, &tv);
+        if ((retval == -1) && (errno == EINTR)) {
+            retval = 0;  // pretend it was a timeout, keep looping.
+        }
+    } while (retval == 0);
+
+    return retval;
+}
+
+static int V4L2_AcquireFrame(SDL_CameraDevice *device, SDL_Surface *frame, Uint64 *timestampNS)
+{
+    const int fd = device->hidden->fd;
+    const io_method io = device->hidden->io;
+    size_t size = device->hidden->buffers[0].length;
     struct v4l2_buffer buf;
 
     switch (io) {
         case IO_METHOD_READ:
-            if (read(fd, _this->hidden->buffers[0].start, size) == -1) {
+            if (read(fd, device->hidden->buffers[0].start, size) == -1) {
                 switch (errno) {
                     case EAGAIN:
                         return 0;
@@ -119,9 +129,8 @@ static int acquire_frame(SDL_CameraDevice *_this, SDL_CameraFrame *frame)
                 }
             }
 
-            frame->num_planes = 1;
-            frame->data[0] = _this->hidden->buffers[0].start;
-            frame->pitch[0] = _this->hidden->driver_pitch;
+            frame->pixels = device->hidden->buffers[0].start;
+            frame->pitch = device->hidden->driver_pitch;
             break;
 
         case IO_METHOD_MMAP:
@@ -144,18 +153,17 @@ static int acquire_frame(SDL_CameraDevice *_this, SDL_CameraFrame *frame)
                 }
             }
 
-            if ((int)buf.index < 0 || (int)buf.index >= _this->hidden->nb_buffers) {
+            if ((int)buf.index < 0 || (int)buf.index >= device->hidden->nb_buffers) {
                 return SDL_SetError("invalid buffer index");
             }
 
-            frame->num_planes = 1;
-            frame->data[0] = _this->hidden->buffers[buf.index].start;
-            frame->pitch[0] = _this->hidden->driver_pitch;
-            _this->hidden->buffers[buf.index].available = 1;
+            frame->pixels = device->hidden->buffers[buf.index].start;
+            frame->pitch = device->hidden->driver_pitch;
+            device->hidden->buffers[buf.index].available = 1;
 
-#if DEBUG_CAMERA
-            SDL_Log("debug mmap: image %d/%d  num_planes:%d data[0]=%p", buf.index, _this->hidden->nb_buffers, frame->num_planes, (void*)frame->data[0]);
-#endif
+            #if DEBUG_CAMERA
+            SDL_Log("CAMERA: debug mmap: image %d/%d  data[0]=%p", buf.index, device->hidden->nb_buffers, (void*)frame->pixels);
+            #endif
             break;
 
         case IO_METHOD_USERPTR:
@@ -180,45 +188,49 @@ static int acquire_frame(SDL_CameraDevice *_this, SDL_CameraFrame *frame)
             }
 
             int i;
-            for (i = 0; i < _this->hidden->nb_buffers; ++i) {
-                if (buf.m.userptr == (unsigned long)_this->hidden->buffers[i].start && buf.length == size) {
+            for (i = 0; i < device->hidden->nb_buffers; ++i) {
+                if (buf.m.userptr == (unsigned long)device->hidden->buffers[i].start && buf.length == size) {
                     break;
                 }
             }
 
-            if (i >= _this->hidden->nb_buffers) {
+            if (i >= device->hidden->nb_buffers) {
                 return SDL_SetError("invalid buffer index");
             }
 
-            frame->num_planes = 1;
-            frame->data[0] = (void*)buf.m.userptr;
-            frame->pitch[0] = _this->hidden->driver_pitch;
-            _this->hidden->buffers[i].available = 1;
-#if DEBUG_CAMERA
-            SDL_Log("debug userptr: image %d/%d  num_planes:%d data[0]=%p", buf.index, _this->hidden->nb_buffers, frame->num_planes, (void*)frame->data[0]);
-#endif
+            frame->pixels = (void*)buf.m.userptr;
+            frame->pitch = device->hidden->driver_pitch;
+            device->hidden->buffers[i].available = 1;
+
+            #if DEBUG_CAMERA
+            SDL_Log("CAMERA: debug userptr: image %d/%d  data[0]=%p", buf.index, device->hidden->nb_buffers, (void*)frame->pixels);
+            #endif
+            break;
+
+        case IO_METHOD_INVALID:
+            SDL_assert(!"Shouldn't have hit this");
             break;
     }
 
+    *timestampNS = SDL_GetTicksNS();  // !!! FIXME: can we get this info more accurately from v4l2?
     return 1;
 }
 
-
-static int V4L2_ReleaseFrame(SDL_CameraDevice *_this, SDL_CameraFrame *frame)
+static void V4L2_ReleaseFrame(SDL_CameraDevice *device, SDL_Surface *frame)
 {
     struct v4l2_buffer buf;
-    const int fd = _this->hidden->fd;
-    enum io_method io = _this->hidden->io;
+    const int fd = device->hidden->fd;
+    const io_method io = device->hidden->io;
     int i;
 
-    for (i = 0; i < _this->hidden->nb_buffers; ++i) {
-        if (frame->num_planes && frame->data[0] == _this->hidden->buffers[i].start) {
+    for (i = 0; i < device->hidden->nb_buffers; ++i) {
+        if (frame->pixels == device->hidden->buffers[i].start) {
             break;
         }
     }
 
-    if (i >= _this->hidden->nb_buffers) {
-        return SDL_SetError("invalid buffer index");
+    if (i >= device->hidden->nb_buffers) {
+        return;  // oh well, we didn't own this.
     }
 
     switch (io) {
@@ -233,9 +245,10 @@ static int V4L2_ReleaseFrame(SDL_CameraDevice *_this, SDL_CameraFrame *frame)
             buf.index = i;
 
             if (xioctl(fd, VIDIOC_QBUF, &buf) == -1) {
-                return SDL_SetError("VIDIOC_QBUF");
+                // !!! FIXME: disconnect the device.
+                return; //SDL_SetError("VIDIOC_QBUF");
             }
-            _this->hidden->buffers[i].available = 0;
+            device->hidden->buffers[i].available = 0;
             break;
 
         case IO_METHOD_USERPTR:
@@ -244,102 +257,33 @@ static int V4L2_ReleaseFrame(SDL_CameraDevice *_this, SDL_CameraFrame *frame)
             buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
             buf.memory = V4L2_MEMORY_USERPTR;
             buf.index = i;
-            buf.m.userptr = (unsigned long)frame->data[0];
-            buf.length = (int) _this->hidden->buffers[i].length;
+            buf.m.userptr = (unsigned long)frame->pixels;
+            buf.length = (int) device->hidden->buffers[i].length;
 
             if (xioctl(fd, VIDIOC_QBUF, &buf) == -1) {
-                return SDL_SetError("VIDIOC_QBUF");
+                // !!! FIXME: disconnect the device.
+                return; //SDL_SetError("VIDIOC_QBUF");
             }
-            _this->hidden->buffers[i].available = 0;
-            break;
-    }
-
-    return 0;
-}
-
-static int V4L2_AcquireFrame(SDL_CameraDevice *_this, SDL_CameraFrame *frame)
-{
-    fd_set fds;
-    struct timeval tv;
-
-    const int fd = _this->hidden->fd;
-
-    FD_ZERO(&fds);
-    FD_SET(fd, &fds);
-
-    // Timeout.
-    tv.tv_sec = 0;
-    tv.tv_usec = 300 * 1000;
-
-    int retval = select(fd + 1, &fds, NULL, NULL, &tv);
-
-    if (retval == -1) {
-        if (errno == EINTR) {
-#if DEBUG_CAMERA
-            SDL_Log("continue ..");
-#endif
-            return 0;
-        }
-        return SDL_SetError("select");
-    }
-
-    if (retval == 0) {
-        // Timeout. Not an error
-        SDL_SetError("timeout select");
-        return 0;
-    }
-
-    retval = acquire_frame(_this, frame);
-    if (retval < 0) {
-        return -1;
-    }
-
-    if (retval == 1){
-        frame->timestampNS = SDL_GetTicksNS();
-    } else if (retval == 0) {
-#if DEBUG_CAMERA
-        SDL_Log("No frame continue: %s", SDL_GetError());
-#endif
-    }
-
-    // EAGAIN - continue select loop.
-    return 0;
-}
-
-
-static int V4L2_StopCamera(SDL_CameraDevice *_this)
-{
-    enum v4l2_buf_type type;
-    const int fd = _this->hidden->fd;
-    enum io_method io = _this->hidden->io;
-
-    switch (io) {
-        case IO_METHOD_READ:
+            device->hidden->buffers[i].available = 0;
             break;
 
-        case IO_METHOD_MMAP:
-        case IO_METHOD_USERPTR:
-            type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
-            if (xioctl(fd, VIDIOC_STREAMOFF, &type) == -1) {
-                return SDL_SetError("VIDIOC_STREAMOFF");
-            }
+        case IO_METHOD_INVALID:
+            SDL_assert(!"Shouldn't have hit this");
             break;
     }
-
-    return 0;
 }
 
-static int EnqueueBuffers(SDL_CameraDevice *_this)
+static int EnqueueBuffers(SDL_CameraDevice *device)
 {
-    const int fd = _this->hidden->fd;
-    enum io_method io = _this->hidden->io;
+    const int fd = device->hidden->fd;
+    const io_method io = device->hidden->io;
     switch (io) {
         case IO_METHOD_READ:
             break;
 
         case IO_METHOD_MMAP:
-            for (int i = 0; i < _this->hidden->nb_buffers; ++i) {
-                if (_this->hidden->buffers[i].available == 0) {
+            for (int i = 0; i < device->hidden->nb_buffers; ++i) {
+                if (device->hidden->buffers[i].available == 0) {
                     struct v4l2_buffer buf;
 
                     SDL_zero(buf);
@@ -355,16 +299,16 @@ static int EnqueueBuffers(SDL_CameraDevice *_this)
             break;
 
         case IO_METHOD_USERPTR:
-            for (int i = 0; i < _this->hidden->nb_buffers; ++i) {
-                if (_this->hidden->buffers[i].available == 0) {
+            for (int i = 0; i < device->hidden->nb_buffers; ++i) {
+                if (device->hidden->buffers[i].available == 0) {
                     struct v4l2_buffer buf;
 
                     SDL_zero(buf);
                     buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
                     buf.memory = V4L2_MEMORY_USERPTR;
                     buf.index = i;
-                    buf.m.userptr = (unsigned long)_this->hidden->buffers[i].start;
-                    buf.length = (int) _this->hidden->buffers[i].length;
+                    buf.m.userptr = (unsigned long)device->hidden->buffers[i].start;
+                    buf.length = (int) device->hidden->buffers[i].length;
 
                     if (xioctl(fd, VIDIOC_QBUF, &buf) == -1) {
                         return SDL_SetError("VIDIOC_QBUF");
@@ -372,115 +316,24 @@ static int EnqueueBuffers(SDL_CameraDevice *_this)
                 }
             }
             break;
-    }
-    return 0;
-}
-
-static int PreEnqueueBuffers(SDL_CameraDevice *_this)
-{
-    struct v4l2_requestbuffers req;
-    const int fd = _this->hidden->fd;
-    enum io_method io = _this->hidden->io;
-
-    switch (io) {
-        case IO_METHOD_READ:
-            break;
-
-        case IO_METHOD_MMAP:
-            SDL_zero(req);
-            req.count = _this->hidden->nb_buffers;
-            req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
-            req.memory = V4L2_MEMORY_MMAP;
-
-            if (xioctl(fd, VIDIOC_REQBUFS, &req) == -1) {
-                if (errno == EINVAL) {
-                    return SDL_SetError("Does not support memory mapping");
-                } else {
-                    return SDL_SetError("VIDIOC_REQBUFS");
-                }
-            }
 
-            if (req.count < 2) {
-                return SDL_SetError("Insufficient buffer memory");
-            }
-
-            _this->hidden->nb_buffers = req.count;
-            break;
-
-        case IO_METHOD_USERPTR:
-            SDL_zero(req);
-            req.count  = _this->hidden->nb_buffers;
-            req.type   = V4L2_BUF_TYPE_VIDEO_CAPTURE;
-            req.memory = V4L2_MEMORY_USERPTR;
-
-            if (xioctl(fd, VIDIOC_REQBUFS, &req) == -1) {
-                if (errno == EINVAL) {
-                    return SDL_SetError("Does not support user pointer i/o");
-                } else {
-                    return SDL_SetError("VIDIOC_REQBUFS");
-                }
-            }
-            break;
+        case IO_METHOD_INVALID: SDL_assert(!"Shouldn't have hit this"); break;
     }
     return 0;
 }
 
-static int V4L2_StartCamera(SDL_CameraDevice *_this)
+static int AllocBufferRead(SDL_CameraDevice *device, size_t buffer_size)
 {
-    enum v4l2_buf_type type;
-
-    const int fd = _this->hidden->fd;
-    enum io_method io = _this->hidden->io;
-
-
-    if (_this->hidden->first_start == 0) {
-        _this->hidden->first_start = 1;
-    } else {
-        const int old = _this->hidden->nb_buffers;
-        // TODO mmap; doesn't work with stop->start
-#if 1
-        // Can change nb_buffers for mmap
-        if (PreEnqueueBuffers(_this) < 0) {
-            return -1;
-        } else if (old != _this->hidden->nb_buffers) {
-            return SDL_SetError("different nb of buffers requested");
-        }
-#endif
-        _this->hidden->first_start = 1;
-    }
-
-    if (EnqueueBuffers(_this) < 0) {
-        return -1;
-    }
-
-    switch (io) {
-        case IO_METHOD_READ:
-            break;
-
-        case IO_METHOD_MMAP:
-        case IO_METHOD_USERPTR:
-            type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
-            if (xioctl(fd, VIDIOC_STREAMON, &type) == -1) {
-                return SDL_SetError("VIDIOC_STREAMON");
-            }
-            break;
-    }
-
-    return 0;
-}
-
-static int AllocBufferRead(SDL_CameraDevice *_this, size_t buffer_size)
-{
-    _this->hidden->buffers[0].length = buffer_size;
-    _this->hidden->buffers[0].start = SDL_calloc(1, buffer_size);
-    return _this->hidden->buffers[0].start ? 0 : -1;
+    device->hidden->buffers[0].length = buffer_size;
+    device->hidden->buffers[0].start = SDL_calloc(1, buffer_size);
+    return device->hidden->buffers[0].start ? 0 : -1;
 }
 
-static int AllocBufferMmap(SDL_CameraDevice *_this)
+static int AllocBufferMmap(SDL_CameraDevice *device)
 {
-    int fd = _this->hidden->fd;
+    const int fd = device->hidden->fd;
     int i;
-    for (i = 0; i < _this->hidden->nb_buffers; ++i) {
+    for (i = 0; i < device->hidden->nb_buffers; ++i) {
         struct v4l2_buffer buf;
 
         SDL_zero(buf);
@@ -493,29 +346,29 @@ static int AllocBufferMmap(SDL_CameraDevice *_this)
             return SDL_SetError("VIDIOC_QUERYBUF");
         }
 
-        _this->hidden->buffers[i].length = buf.length;
-        _this->hidden->buffers[i].start =
+        device->hidden->buffers[i].length = buf.length;
+        device->hidden->buffers[i].start =
             mmap(NULL /* start anywhere */,
                     buf.length,
                     PROT_READ | PROT_WRITE /* required */,
                     MAP_SHARED /* recommended */,
                     fd, buf.m.offset);
 
-        if (MAP_FAILED == _this->hidden->buffers[i].start) {
+        if (MAP_FAILED == device->hidden->buffers[i].start) {
             return SDL_SetError("mmap");
         }
     }
     return 0;
 }
 
-static int AllocBufferUserPtr(SDL_CameraDevice *_this, size_t buffer_size)
+static int AllocBufferUserPtr(SDL_CameraDevice *device, size_t buffer_size)
 {
     int i;
-    for (i = 0; i < _this->hidden->nb_buffers; ++i) {
-        _this->hidden->buffers[i].length = buffer_size;
-        _this->hidden->buffers[i].start = SDL_calloc(1, buffer_size);
+    for (i = 0; i < device->hidden->nb_buffers; ++i) {
+        device->hidden->buffers[i].length = buffer_size;
+        device->hidden->buffers[i].start = SDL_calloc(1, buffer_size);
 
-        if (!_this->hidden->buffers[i].start) {
+        if (!device->hidden->buffers[i].start) {
             return -1;
         }
     }
@@ -530,7 +383,9 @@ static Uint32 format_v4l2_to_sdl(Uint32 fmt)
         CASE(V4L2_PIX_FMT_MJPEG, SDL_PIXELFORMAT_UNKNOWN);
         #undef CASE
         default:
-            SDL_Log("Unknown format V4L2_PIX_FORMAT '%d'", fmt);
+            #if DEBUG_CAMERA
+            SDL_Log("CAMERA: Unknown format V4L2_PIX_FORMAT '%d'", fmt);
+            #endif
             return SDL_PIXELFORMAT_UNKNOWN;
     }
 }
@@ -547,599 +402,442 @@ static Uint32 format_sdl_to_v4l2(Uint32 fmt)
     }
 }
 
-static int V4L2_GetNumFormats(SDL_CameraDevice *_this)
+static void V4L2_CloseDevice(SDL_CameraDevice *device)
 {
-    int fd = _this->hidden->fd;
-    int i = 0;
-    struct v4l2_fmtdesc fmtdesc;
-
-    SDL_zero(fmtdesc);
-    fmtdesc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
-    while (ioctl(fd,VIDIOC_ENUM_FMT,&fmtdesc) == 0) {
-        fmtdesc.index++;
-        i++;
-    }
-    return i;
-}
-
-static int V4L2_GetFormat(SDL_CameraDevice *_this, int index, Uint32 *format)
-{
-    int fd = _this->hidden->fd;
-    struct v4l2_fmtdesc fmtdesc;
-
-    SDL_zero(fmtdesc);
-    fmtdesc.index = index;
-    fmtdesc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
-    if (ioctl(fd,VIDIOC_ENUM_FMT,&fmtdesc) == 0) {
-        *format = format_v4l2_to_sdl(fmtdesc.pixelformat);
-
-#if DEBUG_CAMERA
-        if (fmtdesc.flags & V4L2_FMT_FLAG_EMULATED) {
-            SDL_Log("%s format emulated", SDL_GetPixelFormatName(*format));
-        }
-        if (fmtdesc.flags & V4L2_FMT_FLAG_COMPRESSED) {
-            SDL_Log("%s format compressed", SDL_GetPixelFormatName(*format));
-        }
-#endif
-        return 0;
+    if (!device) {
+        return;
     }
 
-    return -1;
-}
+    if (device->hidden) {
+        const io_method io = device->hidden->io;
+        const int fd = device->hidden->fd;
 
-static int V4L2_GetNumFrameSizes(SDL_CameraDevice *_this, Uint32 format)
-{
-    int fd = _this->hidden->fd;
-    int i = 0;
-    struct v4l2_frmsizeenum frmsizeenum;
-
-    SDL_zero(frmsizeenum);
-    frmsizeenum.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
-    frmsizeenum.pixel_format = format_sdl_to_v4l2(format);
-    while (ioctl(fd,VIDIOC_ENUM_FRAMESIZES, &frmsizeenum) == 0) {
-        frmsizeenum.index++;
-        if (frmsizeenum.type == V4L2_FRMSIZE_TYPE_DISCRETE) {
-            i++;
-        } else if (frmsizeenum.type == V4L2_FRMSIZE_TYPE_STEPWISE) {
-            i += (1 + (frmsizeenum.stepwise.max_width - frmsizeenum.stepwise.min_width) / frmsizeenum.stepwise.step_width)
-                * (1 + (frmsizeenum.stepwise.max_height - frmsizeenum.stepwise.min_height) / frmsizeenum.stepwise.step_height);
-        } else if (frmsizeenum.type == V4L2_FRMSIZE_TYPE_CONTINUOUS) {
-            SDL_SetError("V4L2_FRMSIZE_TYPE_CONTINUOUS not handled");
+        if ((io == IO_METHOD_MMAP) || (io == IO_METHOD_USERPTR)) {
+            enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+            xioctl(fd, VIDIOC_STREAMOFF, &type);
         }
-    }
-    return i;
-}
-
-static int V4L2_GetFrameSize(SDL_CameraDevice *_this, Uint32 format, int index, int *width, int *height)
-{
-    int fd = _this->hidden->fd;
-    struct v4l2_frmsizeenum frmsizeenum;
-    int i = 0;
-
-    SDL_zero(frmsizeenum);
-    frmsizeenum.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
-    frmsizeenum.pixel_format = format_sdl_to_v4l2(format);
-    while (ioctl(fd,VIDIOC_ENUM_FRAMESIZES, &frmsizeenum) == 0) {
-        frmsizeenum.index++;
-
-        if (frmsizeenum.type == V4L2_FRMSIZE_TYPE_DISCRETE) {
-            if (i == index) {
-                *width = frmsizeenum.discrete.width;
-                *height = frmsizeenum.discrete.height;
-                return 0;
-            }
-            i++;
-        } else if (frmsizeenum.type == V4L2_FRMSIZE_TYPE_STEPWISE) {
-            unsigned int w;
-            for (w = frmsizeenum.stepwise.min_width; w <= frmsizeenum.stepwise.max_width; w += frmsizeenum.stepwise.step_width) {
-                unsigned int h;
-                for (h = frmsizeenum.stepwise.min_height; h <= frmsizeenum.stepwise.max_height; h += frmsizeenum.stepwise.step_height) {
-                    if (i == index) {
-                        *width = h;
-                        *height = w;
-                        return 0;
-                    }
-                    i++;
-                }
-            }
-        } else if (frmsizeenum.type == V4L2_FRMSIZE_TYPE_CONTINUOUS) {
-        }
-    }
-
-    return -1;
-}
-static void dbg_v4l2_pixelformat(const char *str, int f)
-{
-    SDL_Log("%s  V4L2_format=%d  %c%c%c%c", str, f,
-                (f >> 0) & 0xff,
-                (f >> 8) & 0xff,
-                (f >> 16) & 0xff,
-                (f >> 24) & 0xff);
-}
-#endif
-
-static int V4L2_GetDeviceSpec(SDL_CameraDevice *_this, SDL_CameraSpec *spec)
-{
-    struct v4l2_format fmt;
-    int fd = _this->hidden->fd;
-    unsigned int min;
-
-    SDL_zero(fmt);
-    fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
-
-    // Preserve original settings as set by v4l2-ctl for example
-    if (xioctl(fd, VIDIOC_G_FMT, &fmt) == -1) {
-        return SDL_SetError("Error VIDIOC_G_FMT");
-    }
 
-    // Buggy driver paranoia.
-    min = fmt.fmt.pix.width * 2;
-    if (fmt.fmt.pix.bytesperline < min) {
-        fmt.fmt.pix.bytesperline = min;
-    }
-    min = fmt.fmt.pix.bytesperline * fmt.fmt.pix.height;
-    if (fmt.fmt.pix.sizeimage < min) {
-        fmt.fmt.pix.sizeimage = min;
-    }
-
-    //spec->width = fmt.fmt.pix.width;
-    //spec->height = fmt.fmt.pix.height;
-    _this->hidden->driver_pitch = fmt.fmt.pix.bytesperline;
-    //spec->format = format_v4l2_to_sdl(fmt.fmt.pix.pixelformat);
-
-    return 0;
-}
-
-static int V4L2_InitDevice(SDL_CameraDevice *_this)
-{
-    struct v4l2_cropcap cropcap;
-    struct v4l2_crop crop;
-
-    int fd = _this->hidden->fd;
-    enum io_method io = _this->hidden->io;
-    int retval = -1;
-
-    // Select video input, video standard and tune here.
-    SDL_zero(cropcap);
-
-    cropcap.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
-
-    if (xioctl(fd, VIDIOC_CROPCAP, &cropcap) == 0) {
-        crop.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
-        crop.c = cropcap.defrect; // reset to default
-
-        if (xioctl(fd, VIDIOC_S_CROP, &crop) == -1) {
-            switch (errno) {
-                case EINVAL:
-                    // Cropping not supported.
-                    break;
-                default:
-                    // Errors ignored.
+        if (device->hidden->buffers) {
+            switch (io) {
+                case IO_METHOD_INVALID:
                     break;
-            }
-        }
-    } else {
-        // Errors ignored.
-    }
-
-
-    {
-        struct v4l2_format fmt;
-        SDL_zero(fmt);
-
-        fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
-        fmt.fmt.pix.width       = _this->spec.width;
-        fmt.fmt.pix.height      = _this->spec.height;
-
-
-        fmt.fmt.pix.pixelformat = format_sdl_to_v4l2(_this->spec.format);
-        //    fmt.fmt.pix.field       = V4L2_FIELD_INTERLACED;
-        fmt.fmt.pix.field       = V4L2_FIELD_ANY;
-
-#if DEBUG_CAMERA
-        SDL_Log("set SDL format %s", SDL_GetPixelFormatName(_this->spec.format));
-        dbg_v4l2_pixelformat("set format", fmt.fmt.pix.pixelformat);
-#endif
-
-        if (xioctl(fd, VIDIOC_S_FMT, &fmt) == -1) {
-            return SDL_SetError("Error VIDIOC_S_FMT");
-        }
-    }
-
-    V4L2_GetDeviceSpec(_this, &_this->spec);
-
-    if (PreEnqueueBuffers(_this) < 0) {
-        return -1;
-    }
-
-    {
-        _this->hidden->buffers = SDL_calloc(_this->hidden->nb_buffers, sizeof(*_this->hidden->buffers));
-        if (!_this->hidden->buffers) {
-            return -1;
-        }
-    }
-
-    {
-        size_t size, pitch;
-        SDL_CalculateSize(_this->spec.format, _this->spec.width, _this->spec.height, &size, &pitch, SDL_FALSE);
-
-        switch (io) {
-            case IO_METHOD_READ:
-                retval = AllocBufferRead(_this, size);
-                break;
-
-            case IO_METHOD_MMAP:
-                retval = AllocBufferMmap(_this);
-                break;
-
-            case IO_METHOD_USERPTR:
-                retval = AllocBufferUserPtr(_this, size);
-                break;
-        }
-    }
-
-    return (retval < 0) ? -1 : 0;
-}
-
-static void V4L2_CloseDevice(SDL_CameraDevice *_this)
-{
-    if (!_this) {
-        return;
-    }
-
-    if (_this->hidden) {
-        if (_this->hidden->buffers) {
-            enum io_method io = _this->hidden->io;
 
-            switch (io) {
                 case IO_METHOD_READ:
-                    SDL_free(_this->hidden->buffers[0].start);
+                    SDL_free(device->hidden->buffers[0].start);
                     break;
 
                 case IO_METHOD_MMAP:
-                    for (int i = 0; i < _this->hidden->nb_buffers; ++i) {
-                        if (munmap(_this->hidden->buffers[i].start, _this->hidden->buffers[i].length) == -1) {
+                    for (int i = 0; i < device->hidden->nb_buffers; ++i) {
+                        if (munmap(device->hidden->buffers[i].start, device->hidden->buffers[i].length) == -1) {
                             SDL_SetError("munmap");
                         }
                     }
                     break;
 
                 case IO_METHOD_USERPTR:
-                    for (int i = 0; i < _this->hidden->nb_buffers; ++i) {
-                        SDL_free(_this->hidden->buffers[i].start);
+                    for (int i = 0; i < device->hidden->nb_buffers; ++i) {
+                        SDL_free(device->hidden->buffers[i].start);
                     }
                     break;
             }
 
-            SDL_free(_this->hidden->buffers);
+            SDL_free(device->hidden->buffers);
         }
 
-        if (_this->hidden->fd != -1) {
-            if (close(_this->hidden->fd)) {
-                SDL_SetError("close camera device");  // !!! FIXME: we probably won't ever see this error
-            }
+        if (fd != -1) {
+            close(fd);
         }
-        SDL_free(_this->hidden);
+        SDL_free(device->hidden);
 
-        _this->hidden = NULL;
+        device->hidden = NULL;
     }
 }
 
-static int V4L2_OpenDevice(SDL_CameraDevice *_this)
+static int V4L2_OpenDevice(SDL_CameraDevice *device, const SDL_CameraSpec *spec)
 {
+    const V4L2DeviceHandle *handle = (const V4L2DeviceHandle *) device->handle;
     struct stat st;
     struct v4l2_capability cap;
-    enum io_method io;
-    int fd;
+    const int fd = open(handle->path, O_RDWR /* required */ | O_NONBLOCK, 0);
+
+    // most of this probably shouldn't fail unless the filesystem node changed out from under us since MaybeAddDevice().
+    if (fd == -1) {
+        return SDL_SetError("Cannot open '%s': %d, %s", handle->path, errno, strerror(errno));
+    } else if (fstat(fd, &st) == -1) {
+        close(fd);
+        return SDL_SetError("Cannot identify '%s': %d, %s", handle->path, errno, strerror(errno));
+    } else if (!S_ISCHR(st.st_mode)) {
+        close(fd);
+        return SDL_SetError("%s is not a character device", handle->path);
+    } else if (xioctl(fd, VIDIOC_QUERYCAP, &cap) == -1) {
+        const int err = errno;
+        close(fd);
+        if (err == EINVAL) {
+            return SDL_SetError("%s is unexpectedly not a V4L2 device", handle->path);
+        }
+        return SDL_SetError("Error VIDIOC_QUERYCAP errno=%d device%s is no V4L2 device", err, handle->path);
+    } else if ((cap.device_caps & V4L2_CAP_VIDEO_CAPTURE) == 0) {
+        close(fd);
+        return SDL_SetError("%s is unexpectedly not a video capture device", handle->path);
+    }
 
-    _this->hidden = (struct SDL_PrivateCameraData *) SDL_calloc(1, sizeof (struct SDL_PrivateCameraData));
-    if (_this->hidden == NULL) {
+    device->hidden = (struct SDL_PrivateCameraData *) SDL_calloc(1, sizeof (struct SDL_PrivateCameraData));
+    if (device->hidden == NULL) {
+        close(fd);
         return -1;
     }
 
-    _this->hidden->fd = -1;
+    device->hidden->fd = fd;
+    device->hidden->io = IO_METHOD_INVALID;
 
-    if (stat(_this->dev_name, &st) == -1) {
-        return SDL_SetError("Cannot identify '%s': %d, %s", _this->dev_name, errno, strerror(errno));
-    } else if (!S_ISCHR(st.st_mode)) {
-        return SDL_SetError("%s is no device", _this->dev_name);
-    } else if ((fd = open(_this->dev_name, O_RDWR /* required */ | O_NONBLOCK, 0)) == -1) {
-        return SDL_SetError("Cannot open '%s': %d, %s", _this->dev_name, errno, strerror(errno));
+    // Select video input, video standard and tune here.
+    // errors in the crop code are not fatal.
+    struct v4l2_cropcap cropcap;
+    SDL_zero(cropcap);
+    cropcap.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+    if (xioctl(fd, VIDIOC_CROPCAP, &cropcap) == 0) {
+        struct v4l2_crop crop;
+        SDL_zero(crop);
+        crop.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+        crop.c = cropcap.defrect; // reset to default
+        xioctl(fd, VIDIOC_S_CROP, &crop);
     }
 
-    _this->hidden->fd = fd;
-    _this->hidden->io = IO_METHOD_MMAP;
-//    _this->hidden->io = IO_METHOD_USERPTR;
-//    _this->hidden->io = IO_METHOD_READ;
-//
-    if (_this->hidden->io == IO_METHOD_READ) {
-        _this->hidden->nb_buffers = 1;
-    } else {
-        _this->hidden->nb_buffers = 8; // Number of image as internal buffer,
+    struct v4l2_format fmt;
+    SDL_zero(fmt);
+    fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+    fmt.fmt.pix.width = spec->width;
+    fmt.fmt.pix.height = spec->height;
+    fmt.fmt.pix.pixelformat = format_sdl_to_v4l2(spec->format);
+    //fmt.fmt.pix.field = V4L2_FIELD_INTERLACED;
+    fmt.fmt.pix.field = V4L2_FIELD_ANY;
+
+    #if DEBUG_CAMERA
+    SDL_Log("CAMERA: set SDL format %s", SDL_GetPixelFormatName(spec->format));
+    { const Uint32 f = fmt.fmt.pix.pixelformat; SDL_Log("CAMERA: set format V4L2_format=%d  %c%c%c%c", f, (f >> 0) & 0xff, (f >> 8) & 0xff, (f >> 16) & 0xff, (f >> 24) & 0xff); }
+    #endif
+
+    if (xioctl(fd, VIDIOC_S_FMT, &fmt) == -1) {
+        return SDL_SetError("Error VIDIOC_S_FMT");
     }
-    io = _this->hidden->io;
 
-    if (xioctl(fd, VIDIOC_QUERYCAP, &cap) == -1) {
-        if (errno == EINVAL) {
-            return SDL_SetError("%s is no V4L2 device", _this->dev_name);
-        } else {
-            return SDL_SetError("Error VIDIOC_QUERYCAP errno=%d device%s is no V4L2 device", errno, _this->dev_name);
+    SDL_zero(fmt);
+    fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+    if (xioctl(fd, VIDIOC_G_FMT, &fmt) == -1) {
+        return SDL_SetError("Error VIDIOC_G_FMT");
+    }
+    device->hidden->driver_pitch = fmt.fmt.pix.bytesperline;
+
+    io_method io = IO_METHOD_INVALID;
+    if ((io == IO_METHOD_INVALID) && (cap.device_caps & V4L2_CAP_STREAMING)) {
+        struct v4l2_requestbuffers req;
+        SDL_zero(req);
+        req.count = 8;
+        req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+        req.memory = V4L2_MEMORY_MMAP;
+        if ((xioctl(fd, VIDIOC_REQBUFS, &req) == 0) && (req.count >= 2)) {
+            io = IO_METHOD_MMAP;
+            device->hidden->nb_buffers = req.count;
+        } else {  // mmap didn't work out? Try USERPTR.
+            SDL_zero(req);
+            req.count = 8;
+            req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+            req.memory = V4L2_MEMORY_USERPTR;
+            if (xioctl(fd, VIDIOC_REQBUFS, &req) == 0) {
+                io = IO_METHOD_USERPTR;
+                device->hidden->nb_buffers = 8;
+            }
         }
     }
 
-    if (!(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE)) {
-        return SDL_SetError("%s is no video capture device", _this->dev_name);
+    if ((io == IO_METHOD_INVALID) && (cap.device_caps & V4L2_CAP_READWRITE)) {
+        io = IO_METHOD_READ;
+        device->hidden->nb_buffers = 1;
     }
 
-#if 0
-    if ((cap.capabilities & V4L2_CAP_VIDEO_CAPTURE)) {
-        SDL_Log("%s is video capture device - single plane", _this->dev_name);
+    if (io == IO_METHOD_INVALID) {
+        return SDL_SetError("Don't have a way to talk to this device");
     }
-    if ((cap.capabilities & V4L2_CAP_VIDEO_CAPTURE_MPLANE)) {
-        SDL_Log("%s is video capture device - multiple planes", _this->dev_name);
+
+    device->hidden->io = io;
+
+    device->hidden->buffers = SDL_calloc(device->hidden->nb_buffers, sizeof(*device->hidden->buffers));
+    if (!device->hidden->buffers) {
+        return -1;
     }
-#endif
 
+    size_t size, pitch;
+    SDL_CalculateSize(device->spec.format, device->spec.width, device->spec.height, &size, &pitch, SDL_FALSE);
+
+    int rc = 0;
     switch (io) {
         case IO_METHOD_READ:
-            if (!(cap.capabilities & V4L2_CAP_READWRITE)) {
-                return SDL_SetError("%s does not support read i/o", _this->dev_name);
-            }
+            rc = AllocBufferRead(device, size);
             break;
 
         case IO_METHOD_MMAP:
+            rc = AllocBufferMmap(device);
+            break;
+
         case IO_METHOD_USERPTR:
-            if (!(cap.capabilities & V4L2_CAP_STREAMING)) {
-                return SDL_SetError("%s does not support streaming i/o", _this->dev_name);
-            }
+            rc = AllocBufferUserPtr(device, size);
             break;
-    }
 
-    return 0;
-}
+        case IO_METHOD_INVALID:
+            SDL_assert(!"Shouldn't have hit this");
+            break;
+    }
 
-static int V4L2_GetDeviceName(SDL_CameraDeviceID instance_id, char *buf, int size)
-{
-    SDL_cameralist_item *item;
-    for (item = SDL_cameralist; item; item = item->next) {
-        if (item->instance_id == instance_id) {
-            SDL_snprintf(buf, size, "%s", item->fname);
-            return 0;
+    if (rc < 0) {
+        return -1;
+    } else if (EnqueueBuffers(device) < 0) {
+        return -1;
+    } else if (io != IO_METHOD_READ) {
+        enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+        if (xioctl(fd, VIDIOC_STREAMON, &type) == -1) {
+            return SDL_SetError("VIDIOC_STREAMON");
         }
     }
 
-    // unknown instance_id
-    return -1;
+    return 0;
 }
 
-static SDL_CameraDeviceID *V4L2_GetDevices(int *count)
+static SDL_bool FindV4L2CameraDeviceByBusInfoCallback(SDL_CameraDevice *device, void *userdata)
 {
-    // real list of ID
-    const int num = num_cameras;
-    SDL_CameraDeviceID *retval = (SDL_CameraDeviceID *)SDL_malloc((num + 1) * sizeof(*retval));
-
-    if (retval == NULL) {
-        *count = 0;
-        return NULL;
-    }
-
-    int i = 0;
-    for (SDL_cameralist_item *item = SDL_cameralist; item; item = item->next) {
-        retval[i++] = item->instance_id;
-    }
-
-    retval[num] = 0;
-    *count = num;
-    return retval;
+    const V4L2DeviceHandle *handle = (const V4L2DeviceHandle *) device->handle;
+    return (SDL_strcmp(handle->bus_info, (const char *) userdata) == 0);
 }
 
+typedef struct SpecAddData
+{
+    SDL_CameraSpec *specs;
+    int num_specs;
+    int allocated_specs;
+} SpecAddData;
 
-#ifdef SDL_USE_LIBUDEV
-static void CameraUdevCallback(SDL_UDEV_deviceevent udev_type, int udev_class, const char *devpath)
+static int AddCameraSpec(SpecAddData *data, Uint32 fmt, int w, int h)
 {
-    if (!devpath || !(udev_class & SDL_UDEV_DEVICE_VIDEO_CAPTURE)) {
-        return;
+    SDL_assert(data != NULL);
+    if (data->allocated_specs <= data->num_specs) {
+        const int newalloc = data->allocated_specs ? (data->allocated_specs * 2) : 16;
+        void *ptr = SDL_realloc(data->specs, sizeof (SDL_CameraSpec) * newalloc);
+        if (!ptr) {
+            return -1;
+        }
+        data->specs = (SDL_CameraSpec *) ptr;
+        data->allocated_specs = newalloc;
     }
 
-    switch (udev_type) {
-    case SDL_UDEV_DEVICEADDED:
-        MaybeAddDevice(devpath);
-        break;
-
-    case SDL_UDEV_DEVICEREMOVED:
-        MaybeRemoveDevice(devpath);
-        break;
+    SDL_CameraSpec *spec = &data->specs[data->num_specs];
+    spec->format = fmt;
+    spec->width = w;
+    spec->height = h;
 
-    default:
-        break;
-    }
-}
-#endif // SDL_USE_LIBUDEV
+    data->num_specs++;
 
-static SDL_bool DeviceExists(const char *path, const char *bus_info) {
-    for (SDL_cameralist_item *item = SDL_cameralist; item; item = item->next) {
-        // found same dev name
-        if (SDL_strcmp(path, item->fname) == 0) {
-            return SDL_TRUE;
-        }
-        // found same bus_info
-        if (SDL_strcmp(bus_info, item->bus_info) == 0) {
-            return SDL_TRUE;
-        }
-    }
-    return SDL_FALSE;
+    return 0;
 }
 
-static int MaybeAddDevice(const char *path)
+static void MaybeAddDevice(const char *path)
 {
-    char *bus_info = NULL;
-    struct v4l2_capability vcap;
-    int err;
-    int fd;
-    SDL_cameralist_item *item;
-
     if (!path) {
-        return -1;
+        return;
     }
 
-    fd = open(path, O_RDWR);
-    if (fd < 0) {
-        return -2; // stop iterating /dev/video%d
+    struct stat st;
+    const int fd = open(path, O_RDWR /* required */ | O_NONBLOCK, 0);
+    if (fd == -1) {
+        return;  // can't open it? skip it.
+    } else if (fstat(fd, &st) == -1) {
+        close(fd);
+        return;  // can't stat it? skip it.
+    } else if (!S_ISCHR(st.st_mode)) {
+        close(fd);
+        return;  // not a character device.
     }
-    err = ioctl(fd, VIDIOC_QUERYCAP, &vcap);
-    close(fd);
-    if (err) {
-        return -1;
+
+    struct v4l2_capability vcap;
+    const int rc = ioctl(fd, VIDIOC_QUERYCAP, &vcap);
+    if (rc != 0) {
+        close(fd);
+        return;  // probably not a v4l2 device at all.
+    } else if ((vcap.device_caps & V4L2_CAP_VIDEO_CAPTURE) == 0) {
+        close(fd);
+        return;  // not a video capture device.
+    } else if (SDL_FindPhysicalCameraDeviceByCallback(FindV4L2CameraDeviceByBusInfoCallback, vcap.bus_info)) {
+        close(fd);
+        return;  // already have it.
     }
 
-    bus_info = SDL_strdup((char *)vcap.bus_info);
+    #if DEBUG_CAMERA
+    SDL_Log("CAMERA: V4L2 camera path='%s' bus_info='%s' name='%s'", path, (const char *) vcap.bus_info, vcap.card);
+    #endif
 
-    if (DeviceExists(path, bus_info)) {
-        SDL_free(bus_info);
-        return 0;
-    }
+    SpecAddData add_data;
+    SDL_zero(add_data);
 
+    struct v4l2_fmtdesc fmtdesc;
+    SDL_zero(fmtdesc);
+    fmtdesc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+    while (ioctl(fd, VIDIOC_ENUM_FMT, &fmtdesc) == 0) {
+        const Uint32 sdlfmt = format_v4l2_to_sdl(fmtdesc.pixelformat);
 
-    // Add new item
-    item = (SDL_cameralist_item *)SDL_calloc(1, sizeof(SDL_cameralist_item));
-    if (!item) {
-        SDL_free(bus_info);
-        return -1;
-    }
+        #if DEBUG_CAMERA
+        SDL_Log("CAMERA:   - Has format '%s'%s%s", SDL_GetPixelFormatName(sdlfmt),
+                (fmtdesc.flags & V4L2_FMT_FLAG_EMULATED) ? " [EMULATED]" : "",
+                (fmtdesc.flags & V4L2_FMT_FLAG_COMPRESSED) ? " [COMPRESSED]" : "");
+        #endif
 
-    item->fname = SDL_strdup(path);
-    if (!item->fname) {
-        SDL_free(item);
-        SDL_free(bus_info);
-        return -1;
-    }
+        fmtdesc.index++;  // prepare for next iteration.
 
-    item->fname = SDL_strdup(path);
-    item->bus_info = bus_info;
-    item->instance_id = SDL_GetNextObjectID();
+        if (sdlfmt == SDL_PIXELFORMAT_UNKNOWN) {
+            continue;  // unsupported by SDL atm.
+        }
 
+        struct v4l2_frmsizeenum frmsizeenum;
+        SDL_zero(frmsizeenum);
+        frmsizeenum.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+        frmsizeenum.pixel_format = fmtdesc.pixelformat;
+
+        while (ioctl(fd,VIDIOC_ENUM_FRAMESIZES, &frmsizeenum) == 0) {
+            if (frmsizeenum.type == V4L2_FRMSIZE_TYPE_DISCRETE) {
+                const int w = (int) frmsizeenum.discrete.width;
+                const int h = (int) frmsizeenum.discrete.height;
+                #if DEBUG_CAMERA
+                SDL_Log("CAMERA:     * Has discrete size %dx%d", w, h);
+                #endif
+                if (AddCameraSpec(&add_data, sdlfmt, w, h) == -1) {
+                    break;  // Probably out of memory; we'll go with what we have, if anything.
+                }
+                frmsizeenum.index++;  // set up for the next one.
+            } else if ((frmsizeenum.type == V4L2_FRMSIZE_TYPE_STEPWISE) || (frmsizeenum.type == V4L2_FRMSIZE_TYPE_CONTINUOUS)) {
+                const int minw = (int) frmsizeenum.stepwise.min_width;
+                const int minh = (int) frmsizeenum.stepwise.min_height;
+                const int maxw = (int) frmsizeenum.stepwise.max_width;
+                const int maxh = (int) frmsizeenum.stepwise.max_height;
+                const int stepw = (int) frmsizeenum.stepwise.step_width;
+                const int steph = (int) frmsizeenum.stepwise.step_height;
+                for (int w = minw; w <= maxw; w += stepw) {
+                    for (int h = minh; w <= maxh; w += steph) {
+                        #if DEBUG_CAMERA
+                        SDL_Log("CAMERA:     * Has %s size %dx%d", (frmsizeenum.type == V4L2_FRMSIZE_TYPE_STEPWISE) ? "stepwise" : "continuous", w, h);
+                        #endif
+                        if (AddCameraSpec(&add_data, sdlfmt, w, h) == -1) {
+                            break;  // Probably out of memory; we'll go with what we have, if anything.
+                        }
 
-    if (!SDL_cameralist_tail) {
-        SDL_cameralist = SDL_cameralist_tail = item;
-    } else {
-        SDL_cameralist_tail->next = item;
-        SDL_cameralist_tail = item;
+                    }
+                }
+                break;
+            }
+        }
     }
 
-    ++num_cameras;
+    close(fd);
 
-    // !!! TODO: Send a add event?
-#if DEBUG_CAMERA
-    SDL_Log("Added video camera ID: %d %s (%s) (total: %d)", item->instance_id, path, bus_info, num_cameras);
-#endif
-    return 0;
+    #if DEBUG_CAMERA
+    SDL_Log("CAMERA: (total specs: %d)", add_data.num_specs);
+    #endif
+
+    if (add_data.num_specs > 0) {
+        V4L2DeviceHandle *handle = (V4L2DeviceHandle *) SDL_calloc(1, sizeof (V4L2DeviceHandle));
+        if (handle) {
+            handle->path = SDL_strdup(path);
+            if (handle->path) {
+                handle->bus_info = SDL_strdup((char *)vcap.bus_info);
+                if (handle->bus_info) {
+                    if (SDL_AddCameraDevice((const char *) vcap.card, add_data.num_specs, add_data.specs, handle)) {
+                        SDL_free(add_data.specs);
+                        return;  // good to go.
+                    }
+                    SDL_free(handle->bus_info);
+                }
+                SDL_free(handle->path);
+            }
+            SDL_free(handle);
+        }
+    }
+    SDL_free(add_data.specs);
 }
 
-#ifdef SDL_USE_LIBUDEV
-static int MaybeRemoveDevice(const char *path)
+static void V4L2_FreeDeviceHandle(SDL_CameraDevice *device)
 {
-
-    SDL_cameralist_item *item;
-    SDL_cameralist_item *prev = NULL;
-#if DEBUG_CAMERA
-    SDL_Log("Remove video camera %s", path);
-#endif
-    if (!path) {
-        return -1;
+    if (device) {
+        V4L2DeviceHandle *handle = (V4L2DeviceHandle *) device->handle;
+        SDL_free(handle->path);
+        SDL_free(handle->bus_info);
+        SDL_free(handle);
     }
+}
 
-    for (item = SDL_cameralist; item; item = item->next) {
-        // found it, remove it.
-        if (SDL_strcmp(path, item->fname) == 0) {
-            if (prev) {
-                prev->next = item->next;
-            } else {
-                SDL_assert(SDL_cameralist == item);
-                SDL_cameralist = item->next;
-            }
-            if (item == SDL_cameralist_tail) {
-                SDL_cameralist_tail = prev;
-            }
+#ifdef SDL_USE_LIBUDEV
+static SDL_bool FindV4L2CameraDeviceByPathCallback(SDL_CameraDevice *device, void *userdata)
+{
+    const V4L2DeviceHandle *handle = (const V4L2DeviceHandle *) device->handle;
+    return (SDL_strcmp(handle->path, (const char *) userdata) == 0);
+}
 
-            // Need to decrement the count
-            --num_cameras;
-            // !!! TODO: Send a remove event?
+static void MaybeRemoveDevice(const char *path)
+{
+    if (path) {
+        SDL_CameraDeviceDisconnected(SDL_FindPhysicalCameraDeviceByCallback(FindV4L2CameraDeviceByPathCallback, (void *) path));
+    }
+}
 
-            SDL_free(item->fname);
-            SDL_free(item->bus_info);
-            SDL_free(item);
-            return 0;
+static void CameraUdevCallback(SDL_UDEV_deviceevent udev_type, int udev_class, const char *devpath)
+{
+    if (devpath && (udev_class & SDL_UDEV_DEVICE_VIDEO_CAPTURE)) {
+        if (udev_type == SDL_UDEV_DEVICEADDED) {
+            MaybeAddDevice(devpath);
+        } else if (udev_type == SDL_UDEV_DEVICEREMOVED) {
+            MaybeRemoveDevice(devpath);
         }
-        prev = item;
     }
-    return 0;
 }
 #endif // SDL_USE_LIBUDEV
 
-
 static void V4L2_Deinitialize(void)
 {
-    for (SDL_cameralist_item *item = SDL_cameralist; item; ) {
-        SDL_cameralist_item *tmp = item->next;
-        SDL_free(item->fname);
-        SDL_free(item->bus_info);
-        SDL_free(item);
-        item = tmp;
-    }
-
-    num_cameras = 0;
-    SDL_cameralist = NULL;
-    SDL_cameralist_tail = NULL;
+#ifdef SDL_USE_LIBUDEV
+    SDL_UDEV_DelCallback(CameraUdevCallback);
+    SDL_UDEV_Quit();
+#endif // SDL_USE_LIBUDEV
 }
 
 static void V4L2_DetectDevices(void)
 {
-}
-
-static SDL_bool V4L2_Init(SDL_CameraDriverImpl *impl)
-{
-    // !!! FIXME: move to DetectDevices
-    const char pattern[] = "/dev/video%d";
-    char path[PATH_MAX];
-
-    /*
-     * Limit amount of checks to MAX_CAMERA_DEVICES since we may or may not have
-     * permission to some or all devices.
-     */
-    for (int i = 0; i < MAX_CAMERA_DEVICES; i++) {
-        (void)SDL_snprintf(path, PATH_MAX, pattern, i);
-        if (MaybeAddDevice(path) == -2) {
-            break;
+#ifdef SDL_USE_LIBUDEV
+    if (SDL_UDEV_Init() == 0) {
+        if (SDL_UDEV_AddCallback(CameraUdevCallback) >= 0) {  // !!! FIXME: this should return 0 on success, it currently returns 1.
+            SDL_UDEV_Scan();  // Force a scan to build the initial device list
         }
     }
-
-#ifdef SDL_USE_LIBUDEV
-    if (SDL_UDEV_Init() < 0) {
-        return SDL_SetError("Could not initialize UDEV");
-    } else if (SDL_UDEV_AddCallback(CameraUdevCallback) < 0) {
-        SDL_UDEV_Quit();
-        return SDL_SetError("Could not setup Video Capture <-> udev callback");
+#else
+    DIR *dirp = opendir("/dev");
+    if (dirp) {
+        struct dirent *dent;
+        while ((dent = readdir(dirp)) != NULL) {
+            int num = 0;
+            if (SDL_sscanf(dent->d_name, "video%d", &num) == 1) {
+                char fullpath[64];
+                SDL_snprintf(fullpath, sizeof (fullpath), "/dev/video%d", num);
+                MaybeAddDevice(fullpath);
+            }
+        }
+        closedir(dirp);
     }
-
-    // Force a scan to build the initial device list
-    SDL_UDEV_Scan();
 #endif // SDL_USE_LIBUDEV
+}
 
+static SDL_bool V4L2_Init(SDL_CameraDriverImpl *impl)
+{
     impl->DetectDevices = V4L2_DetectDevices;
     impl->OpenDevice = V4L2_OpenDevice;
     impl->CloseDevice = V4L2_CloseDevice;
-    impl->InitDevice = V4L2_InitDevice;
-    impl->GetDeviceSpec = V4L2_GetDeviceSpec;
-    impl->StartCamera = V4L2_StartCamera;
-    impl->StopCamera = V4L2_StopCamera;
+    impl->WaitDevice = V4L2_WaitDevice;
     impl->AcquireFrame = V4L2_AcquireFrame;
     impl->ReleaseFrame = V4L2_ReleaseFrame;
-    impl->GetNumFormats = V4L2_GetNumFormats;
-    impl->GetFormat = V4L2_GetFormat;
-    impl->GetNumFrameSizes = V4L2_GetNumFrameSizes;
-    impl->GetFrameSize = V4L2_GetFrameSize;
-    impl->GetDeviceName = V4L2_GetDeviceName;
-    impl->GetDevices = V4L2_GetDevices;
+    impl->FreeDeviceHandle = V4L2_FreeDeviceHandle;
     impl->Deinitialize = V4L2_Deinitialize;
 
     return SDL_TRUE;

+ 7 - 10
src/dynapi/SDL_dynapi.sym

@@ -957,21 +957,18 @@ SDL3_0.0.0 {
     SDL_SetWindowShape;
     SDL_RenderViewportSet;
     SDL_HasProperty;
+    SDL_GetNumCameraDrivers;
+    SDL_GetCameraDriver;
+    SDL_GetCurrentCameraDriver;
     SDL_GetCameraDevices;
-    SDL_OpenCamera;
-    SDL_SetCameraSpec;
-    SDL_OpenCameraWithSpec;
+    SDL_GetCameraDeviceSupportedSpecs;
     SDL_GetCameraDeviceName;
+    SDL_OpenCameraDevice;
+    SDL_GetCameraInstanceID;
+    SDL_GetCameraProperties;
     SDL_GetCameraSpec;
-    SDL_GetCameraFormat;
-    SDL_GetNumCameraFormats;
-    SDL_GetCameraFrameSize;
-    SDL_GetNumCameraFrameSizes;
-    SDL_GetCameraStatus;
-    SDL_StartCamera;
     SDL_AcquireCameraFrame;
     SDL_ReleaseCameraFrame;
-    SDL_StopCamera;
     SDL_CloseCamera;
     # extra symbols go here (don't modify this line)
   local: *;

+ 7 - 10
src/dynapi/SDL_dynapi_overrides.h

@@ -982,19 +982,16 @@
 #define SDL_SetWindowShape SDL_SetWindowShape_REAL
 #define SDL_RenderViewportSet SDL_RenderViewportSet_REAL
 #define SDL_HasProperty SDL_HasProperty_REAL
+#define SDL_GetNumCameraDrivers SDL_GetNumCameraDrivers_REAL
+#define SDL_GetCameraDriver SDL_GetCameraDriver_REAL
+#define SDL_GetCurrentCameraDriver SDL_GetCurrentCameraDriver_REAL
 #define SDL_GetCameraDevices SDL_GetCameraDevices_REAL
-#define SDL_OpenCamera SDL_OpenCamera_REAL
-#define SDL_SetCameraSpec SDL_SetCameraSpec_REAL
-#define SDL_OpenCameraWithSpec SDL_OpenCameraWithSpec_REAL
+#define SDL_GetCameraDeviceSupportedSpecs SDL_GetCameraDeviceSupportedSpecs_REAL
 #define SDL_GetCameraDeviceName SDL_GetCameraDeviceName_REAL
+#define SDL_OpenCameraDevice SDL_OpenCameraDevice_REAL
+#define SDL_GetCameraInstanceID SDL_GetCameraInstanceID_REAL
+#define SDL_GetCameraProperties SDL_GetCameraProperties_REAL
 #define SDL_GetCameraSpec SDL_GetCameraSpec_REAL
-#define SDL_GetCameraFormat SDL_GetCameraFormat_REAL
-#define SDL_GetNumCameraFormats SDL_GetNumCameraFormats_REAL
-#define SDL_GetCameraFrameSize SDL_GetCameraFrameSize_REAL
-#define SDL_GetNumCameraFrameSizes SDL_GetNumCameraFrameSizes_REAL
-#define SDL_GetCameraStatus SDL_GetCameraStatus_REAL
-#define SDL_StartCamera SDL_StartCamera_REAL
 #define SDL_AcquireCameraFrame SDL_AcquireCameraFrame_REAL
 #define SDL_ReleaseCameraFrame SDL_ReleaseCameraFrame_REAL
-#define SDL_StopCamera SDL_StopCamera_REAL
 #define SDL_CloseCamera SDL_CloseCamera_REAL

+ 12 - 15
src/dynapi/SDL_dynapi_procs.h

@@ -1007,19 +1007,16 @@ SDL_DYNAPI_PROC(int,SDL_RenderGeometryRawFloat,(SDL_Renderer *a, SDL_Texture *b,
 SDL_DYNAPI_PROC(int,SDL_SetWindowShape,(SDL_Window *a, SDL_Surface *b),(a,b),return)
 SDL_DYNAPI_PROC(SDL_bool,SDL_RenderViewportSet,(SDL_Renderer *a),(a),return)
 SDL_DYNAPI_PROC(SDL_bool,SDL_HasProperty,(SDL_PropertiesID a, const char *b),(a,b),return)
+SDL_DYNAPI_PROC(int,SDL_GetNumCameraDrivers,(void),(),return)
+SDL_DYNAPI_PROC(const char*,SDL_GetCameraDriver,(int a),(a),return)
+SDL_DYNAPI_PROC(const char*,SDL_GetCurrentCameraDriver,(void),(),return)
 SDL_DYNAPI_PROC(SDL_CameraDeviceID*,SDL_GetCameraDevices,(int *a),(a),return)
-SDL_DYNAPI_PROC(SDL_CameraDevice*,SDL_OpenCamera,(SDL_CameraDeviceID a),(a),return)
-SDL_DYNAPI_PROC(int,SDL_SetCameraSpec,(SDL_CameraDevice *a, const SDL_CameraSpec *b, SDL_CameraSpec *c, int d),(a,b,c,d),return)
-SDL_DYNAPI_PROC(SDL_CameraDevice*,SDL_OpenCameraWithSpec,(SDL_CameraDeviceID a, const SDL_CameraSpec *b, SDL_CameraSpec *c, int d),(a,b,c,d),return)
-SDL_DYNAPI_PROC(const char*,SDL_GetCameraDeviceName,(SDL_CameraDeviceID a),(a),return)
-SDL_DYNAPI_PROC(int,SDL_GetCameraSpec,(SDL_CameraDevice *a, SDL_CameraSpec *b),(a,b),return)
-SDL_DYNAPI_PROC(int,SDL_GetCameraFormat,(SDL_CameraDevice *a, int b, Uint32 *c),(a,b,c),return)
-SDL_DYNAPI_PROC(int,SDL_GetNumCameraFormats,(SDL_CameraDevice *a),(a),return)
-SDL_DYNAPI_PROC(int,SDL_GetCameraFrameSize,(SDL_CameraDevice *a, Uint32 b, int c, int *d, int *e),(a,b,c,d,e),return)
-SDL_DYNAPI_PROC(int,SDL_GetNumCameraFrameSizes,(SDL_CameraDevice *a, Uint32 b),(a,b),return)
-SDL_DYNAPI_PROC(SDL_CameraStatus,SDL_GetCameraStatus,(SDL_CameraDevice *a),(a),return)
-SDL_DYNAPI_PROC(int,SDL_StartCamera,(SDL_CameraDevice *a),(a),return)
-SDL_DYNAPI_PROC(int,SDL_AcquireCameraFrame,(SDL_CameraDevice *a, SDL_CameraFrame *b),(a,b),return)
-SDL_DYNAPI_PROC(int,SDL_ReleaseCameraFrame,(SDL_CameraDevice *a, SDL_CameraFrame *b),(a,b),return)
-SDL_DYNAPI_PROC(int,SDL_StopCamera,(SDL_CameraDevice *a),(a),return)
-SDL_DYNAPI_PROC(void,SDL_CloseCamera,(SDL_CameraDevice *a),(a),)
+SDL_DYNAPI_PROC(SDL_CameraSpec*,SDL_GetCameraDeviceSupportedSpecs,(SDL_CameraDeviceID a, int *b),(a,b),return)
+SDL_DYNAPI_PROC(char*,SDL_GetCameraDeviceName,(SDL_CameraDeviceID a),(a),return)
+SDL_DYNAPI_PROC(SDL_Camera*,SDL_OpenCameraDevice,(SDL_CameraDeviceID a, const SDL_CameraSpec *b),(a,b),return)
+SDL_DYNAPI_PROC(SDL_CameraDeviceID,SDL_GetCameraInstanceID,(SDL_Camera *a),(a),return)
+SDL_DYNAPI_PROC(SDL_PropertiesID,SDL_GetCameraProperties,(SDL_Camera *a),(a),return)
+SDL_DYNAPI_PROC(int,SDL_GetCameraSpec,(SDL_Camera *a, SDL_CameraSpec *b),(a,b),return)
+SDL_DYNAPI_PROC(SDL_Surface*,SDL_AcquireCameraFrame,(SDL_Camera *a, Uint64 *b),(a,b),return)
+SDL_DYNAPI_PROC(int,SDL_ReleaseCameraFrame,(SDL_Camera *a, SDL_Surface *b),(a,b),return)
+SDL_DYNAPI_PROC(void,SDL_CloseCamera,(SDL_Camera *a),(a),)

+ 14 - 0
src/events/SDL_events.c

@@ -25,6 +25,7 @@
 #include "SDL_events_c.h"
 #include "../SDL_hints_c.h"
 #include "../audio/SDL_audio_c.h"
+#include "../camera/SDL_camera_c.h"
 #include "../timer/SDL_timer_c.h"
 #ifndef SDL_JOYSTICK_DISABLED
 #include "../joystick/SDL_joystick_c.h"
@@ -554,6 +555,15 @@ static void SDL_LogEvent(const SDL_Event *event)
         break;
 #undef PRINT_AUDIODEV_EVENT
 
+#define PRINT_CAMERADEV_EVENT(event) (void)SDL_snprintf(details, sizeof(details), " (timestamp=%u which=%u)", (uint)event->cdevice.timestamp, (uint)event->cdevice.which)
+        SDL_EVENT_CASE(SDL_EVENT_CAMERA_DEVICE_ADDED)
+        PRINT_CAMERADEV_EVENT(event);
+        break;
+        SDL_EVENT_CASE(SDL_EVENT_CAMERA_DEVICE_REMOVED)
+        PRINT_CAMERADEV_EVENT(event);
+        break;
+#undef PRINT_CAMERADEV_EVENT
+
         SDL_EVENT_CASE(SDL_EVENT_SENSOR_UPDATE)
         (void)SDL_snprintf(details, sizeof(details), " (timestamp=%u which=%d data[0]=%f data[1]=%f data[2]=%f data[3]=%f data[4]=%f data[5]=%f)",
                            (uint)event->sensor.timestamp, (int)event->sensor.which,
@@ -942,6 +952,10 @@ static void SDL_PumpEventsInternal(SDL_bool push_sentinel)
     SDL_UpdateAudio();
 #endif
 
+#ifndef SDL_CAMERA_DISABLED
+    SDL_UpdateCamera();
+#endif
+
 #ifndef SDL_SENSOR_DISABLED
     /* Check for sensor state change */
     if (SDL_update_sensors) {

+ 8 - 2
test/testcamera.c

@@ -18,8 +18,13 @@
 #include <emscripten/emscripten.h>
 #endif
 
-#include <stdio.h>
-
+#if 1
+int main(int argc, char **argv)
+{
+    SDL_Log("FIXME: update me");
+    return 0;
+}
+#else
 static const char *usage = "\
  \n\
  =========================================================================\n\
@@ -769,3 +774,4 @@ int main(int argc, char **argv)
 
     return 0;
 }
+#endif

+ 46 - 48
test/testcameraminimal.c

@@ -28,21 +28,14 @@ int main(int argc, char **argv)
     int quit = 0;
     SDLTest_CommonState  *state = NULL;
 
-    SDL_CameraDevice *device = NULL;
-    SDL_CameraSpec obtained;
-
-    SDL_CameraFrame frame_current;
+    SDL_Camera *camera = NULL;
+    SDL_CameraSpec spec;
     SDL_Texture *texture = NULL;
     int texture_updated = 0;
+    SDL_Surface *frame_current = NULL;
 
     SDL_zero(evt);
-    SDL_zero(obtained);
-    SDL_zero(frame_current);
-
-    /* Set 0 to disable TouchEvent to be duplicated as MouseEvent with SDL_TOUCH_MOUSEID */
-    SDL_SetHint(SDL_HINT_TOUCH_MOUSE_EVENTS, "0");
-    /* Set 0 to disable MouseEvent to be duplicated as TouchEvent with SDL_MOUSE_TOUCHID */
-    SDL_SetHint(SDL_HINT_MOUSE_TOUCH_EVENTS, "0");
+    SDL_zero(spec);
 
     /* Initialize test framework */
     state = SDLTest_CommonCreateState(argv, 0);
@@ -73,20 +66,42 @@ int main(int argc, char **argv)
         return 1;
     }
 
-    device = SDL_OpenCameraWithSpec(0, NULL, &obtained, SDL_CAMERA_ALLOW_ANY_CHANGE);
-    if (!device) {
-        SDL_Log("No camera? %s", SDL_GetError());
+    SDL_CameraDeviceID *devices = SDL_GetCameraDevices(NULL);
+    if (!devices) {
+        SDL_Log("SDL_GetCameraDevices failed: %s", SDL_GetError());
+        return 1;
+    }
+
+    const SDL_CameraDeviceID devid = devices[0];  /* just take the first one. */
+    SDL_free(devices);
+
+    if (!devid) {
+        SDL_Log("No cameras available? %s", SDL_GetError());
+        return 1;
+    }
+    
+    SDL_CameraSpec *pspec = NULL;
+    #if 0  /* just for edge-case testing purposes, ignore. */
+    pspec = &spec;
+    spec.width = 100 /*1280 * 2*/;
+    spec.height = 100 /*720 * 2*/;
+    spec.format = SDL_PIXELFORMAT_YUY2 /*SDL_PIXELFORMAT_RGBA8888*/;
+    #endif
+
+    camera = SDL_OpenCameraDevice(devid, pspec);
+    if (!camera) {
+        SDL_Log("Failed to open camera device: %s", SDL_GetError());
         return 1;
     }
 
-    if (SDL_StartCamera(device) < 0) {
-        SDL_Log("error SDL_StartCamera(): %s", SDL_GetError());
+   if (SDL_GetCameraSpec(camera, &spec) < 0) {
+        SDL_Log("Couldn't get camera spec: %s", SDL_GetError());
         return 1;
     }
 
     /* Create texture with appropriate format */
     if (texture == NULL) {
-        texture = SDL_CreateTexture(renderer, obtained.format, SDL_TEXTUREACCESS_STATIC, obtained.width, obtained.height);
+        texture = SDL_CreateTexture(renderer, spec.format, SDL_TEXTUREACCESS_STATIC, spec.width, spec.height);
         if (texture == NULL) {
             SDL_Log("Couldn't create texture: %s", SDL_GetError());
             return 1;
@@ -118,21 +133,18 @@ int main(int argc, char **argv)
         }
 
         {
-            SDL_CameraFrame frame_next;
-            SDL_zero(frame_next);
+            Uint64 timestampNS = 0;
+            SDL_Surface *frame_next = SDL_AcquireCameraFrame(camera, &timestampNS);
 
-            if (SDL_AcquireCameraFrame(device, &frame_next) < 0) {
-                SDL_Log("err SDL_AcquireCameraFrame: %s", SDL_GetError());
-            }
 #if 0
-            if (frame_next.num_planes) {
-                SDL_Log("frame: %p  at %" SDL_PRIu64, (void*)frame_next.data[0], frame_next.timestampNS);
+            if (frame_next) {
+                SDL_Log("frame: %p  at %" SDL_PRIu64, (void*)frame_next->pixels, timestampNS);
             }
 #endif
 
-            if (frame_next.num_planes) {
-                if (frame_current.num_planes) {
-                    if (SDL_ReleaseCameraFrame(device, &frame_current) < 0) {
+            if (frame_next) {
+                if (frame_current) {
+                    if (SDL_ReleaseCameraFrame(camera, frame_current) < 0) {
                         SDL_Log("err SDL_ReleaseCameraFrame: %s", SDL_GetError());
                     }
                 }
@@ -146,20 +158,8 @@ int main(int argc, char **argv)
         }
 
         /* Update SDL_Texture with last video frame (only once per new frame) */
-        if (frame_current.num_planes && texture_updated == 0) {
-            /* Use software data */
-            if (frame_current.num_planes == 1) {
-                SDL_UpdateTexture(texture, NULL,
-                        frame_current.data[0], frame_current.pitch[0]);
-            } else if (frame_current.num_planes == 2) {
-                SDL_UpdateNVTexture(texture, NULL,
-                        frame_current.data[0], frame_current.pitch[0],
-                        frame_current.data[1], frame_current.pitch[1]);
-            } else if (frame_current.num_planes == 3) {
-                SDL_UpdateYUVTexture(texture, NULL, frame_current.data[0], frame_current.pitch[0],
-                        frame_current.data[1], frame_current.pitch[1],
-                        frame_current.data[2], frame_current.pitch[2]);
-            }
+        if (frame_current && texture_updated == 0) {
+            SDL_UpdateTexture(texture, NULL, frame_current->pixels, frame_current->pitch);
             texture_updated = 1;
         }
 
@@ -177,7 +177,7 @@ int main(int argc, char **argv)
                 th = (int)((float) th * scale);
             }
             d.x = (float)(10 );
-            d.y = (float)(win_h - th);
+            d.y = ((float)(win_h - th)) / 2.0f;
             d.w = (float)tw;
             d.h = (float)(th - 10);
             SDL_RenderTexture(renderer, texture, NULL, &d);
@@ -186,13 +186,10 @@ int main(int argc, char **argv)
         SDL_RenderPresent(renderer);
     }
 
-    if (SDL_StopCamera(device) < 0) {
-        SDL_Log("error SDL_StopCamera(): %s", SDL_GetError());
-    }
-    if (frame_current.num_planes) {
-        SDL_ReleaseCameraFrame(device, &frame_current);
+    if (frame_current) {
+        SDL_ReleaseCameraFrame(camera, frame_current);
     }
-    SDL_CloseCamera(device);
+    SDL_CloseCamera(camera);
 
     if (texture) {
         SDL_DestroyTexture(texture);
@@ -205,3 +202,4 @@ int main(int argc, char **argv)
 
     return 0;
 }
+