Bladeren bron

camera: Rewrote Android support.

This does something a little weird, in that it doesn't care what
`__ANDROID_API__` is set to, but will attempt to dlopen the system
libraries, like we do for many other platform-specific pieces of SDL.

This allows us to a) not bump the minimum required Android version, which is
extremely ancient but otherwise still working, doing the right thing on old
and new hardware in the field, and b) not require the app to link against
more libraries than it previously did before the feature was available.

The downside is that it's a little messy, but it's okay for now, I think.
Ryan C. Gordon 1 jaar geleden
bovenliggende
commit
2613e3da24

+ 3 - 0
Android.mk

@@ -24,6 +24,9 @@ LOCAL_SRC_FILES := \
 	$(wildcard $(LOCAL_PATH)/src/audio/openslES/*.c) \
 	$(LOCAL_PATH)/src/atomic/SDL_atomic.c.arm \
 	$(LOCAL_PATH)/src/atomic/SDL_spinlock.c.arm \
+	$(wildcard $(LOCAL_PATH)/src/camera/*.c) \
+	$(wildcard $(LOCAL_PATH)/src/camera/android/*.c) \
+	$(wildcard $(LOCAL_PATH)/src/camera/dummy/*.c) \
 	$(wildcard $(LOCAL_PATH)/src/core/*.c) \
 	$(wildcard $(LOCAL_PATH)/src/core/android/*.c) \
 	$(wildcard $(LOCAL_PATH)/src/cpuinfo/*.c) \

+ 7 - 0
android-project/app/src/main/AndroidManifest.xml

@@ -37,6 +37,13 @@
         android:name="android.hardware.microphone"
         android:required="false" /> -->
 
+    <!-- Camera support -->
+    <!-- if you want to record video, uncomment this. -->
+    <!--
+    <uses-permission android:name="android.permission.CAMERA" />
+    <uses-feature android:name="android.hardware.camera" />
+    -->
+
     <!-- Allow downloading to the external storage on Android 5.1 and older -->
     <!-- <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="22" /> -->
 

+ 1 - 0
include/build_config/SDL_build_config_android.h

@@ -192,5 +192,6 @@
 
 /* Enable the camera driver */
 #define SDL_CAMERA_DRIVER_ANDROID 1
+#define SDL_CAMERA_DRIVER_DUMMY 1
 
 #endif /* SDL_build_config_android_h_ */

+ 4 - 6
src/camera/SDL_syscamera.h

@@ -27,12 +27,6 @@
 
 #define DEBUG_CAMERA 0
 
-
-// !!! FIXME: update this driver!
-#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
@@ -53,6 +47,10 @@ extern void SDL_CameraDevicePermissionOutcome(SDL_CameraDevice *device, SDL_bool
 // Backends can call this to get a standardized name for a thread to power a specific camera device.
 extern char *SDL_GetCameraThreadName(SDL_CameraDevice *device, char *buf, size_t buflen);
 
+// Backends can call these to change a device's refcount.
+extern void RefPhysicalCameraDevice(SDL_CameraDevice *device);
+extern void UnrefPhysicalCameraDevice(SDL_CameraDevice *device);
+
 // 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);

+ 650 - 471
src/camera/android/SDL_camera_android.c

@@ -25,32 +25,34 @@
 #include "../../video/SDL_pixels_c.h"
 #include "../../thread/SDL_systhread.h"
 
-#if defined(SDL_CAMERA_DRIVER_ANDROID)
-
-#if __ANDROID_API__ >= 24
+#ifdef SDL_CAMERA_DRIVER_ANDROID
 
 /*
- * APP_PLATFORM=android-24
- * minSdkVersion=24
- *
- * link with: -lcamera2ndk -lmediandk
- *
  * AndroidManifest.xml:
  *   <uses-permission android:name="android.permission.CAMERA"></uses-permission>
  *   <uses-feature android:name="android.hardware.camera" />
  *
- *
- * Add: #define SDL_CAMERA 1
- * in:  include/build_config/SDL_build_config_android.h
- *
- *
  * Very likely SDL must be build with YUV support (done by default)
  *
  * https://developer.android.com/reference/android/hardware/camera2/CameraManager
  * "All camera devices intended to be operated concurrently, must be opened using openCamera(String, CameraDevice.StateCallback, Handler),
- * before configuring sessions on any of the camera devices.  * "
+ * before configuring sessions on any of the camera devices."
  */
 
+// this is kinda gross, but on older NDK headers all the camera stuff is
+//  gated behind __ANDROID_API__. We'll dlopen() it at runtime, so we'll do
+//  the right thing on pre-Android 7.0 devices, but we still
+//  need the struct declarations and such in those headers.
+// The other option is to make a massive jump in minimum Android version we
+//  support--going from ancient to merely really old--but this seems less
+//  distasteful and using dlopen matches practices on other SDL platforms.
+//  We'll see if it works out.
+#if __ANDROID_API__ < 24
+#undef __ANDROID_API__
+#define __ANDROID_API__ 24
+#endif
+
+#include <dlfcn.h>
 #include <camera/NdkCameraDevice.h>
 #include <camera/NdkCameraManager.h>
 #include <media/NdkImage.h>
@@ -58,87 +60,195 @@
 
 #include "../../core/android/SDL_android.h"
 
+static void *libcamera2ndk = NULL;
+typedef ACameraManager* (*pfnACameraManager_create)(void);
+typedef camera_status_t (*pfnACameraManager_registerAvailabilityCallback)(ACameraManager*, const ACameraManager_AvailabilityCallbacks*);
+typedef camera_status_t (*pfnACameraManager_unregisterAvailabilityCallback)(ACameraManager*, const ACameraManager_AvailabilityCallbacks*);
+typedef camera_status_t (*pfnACameraManager_getCameraIdList)(ACameraManager*, ACameraIdList**);
+typedef void (*pfnACameraManager_deleteCameraIdList)(ACameraIdList*);
+typedef void (*pfnACameraCaptureSession_close)(ACameraCaptureSession*);
+typedef void (*pfnACaptureRequest_free)(ACaptureRequest*);
+typedef void (*pfnACameraOutputTarget_free)(ACameraOutputTarget*);
+typedef camera_status_t (*pfnACameraDevice_close)(ACameraDevice*);
+typedef void (*pfnACameraManager_delete)(ACameraManager*);
+typedef void (*pfnACaptureSessionOutputContainer_free)(ACaptureSessionOutputContainer*);
+typedef void (*pfnACaptureSessionOutput_free)(ACaptureSessionOutput*);
+typedef camera_status_t (*pfnACameraManager_openCamera)(ACameraManager*, const char*, ACameraDevice_StateCallbacks*, ACameraDevice**);
+typedef camera_status_t (*pfnACameraDevice_createCaptureRequest)(const ACameraDevice*, ACameraDevice_request_template, ACaptureRequest**);
+typedef camera_status_t (*pfnACameraDevice_createCaptureSession)(ACameraDevice*, const ACaptureSessionOutputContainer*, const ACameraCaptureSession_stateCallbacks*,ACameraCaptureSession**);
+typedef camera_status_t (*pfnACameraManager_getCameraCharacteristics)(ACameraManager*, const char*, ACameraMetadata**);
+typedef void (*pfnACameraMetadata_free)(ACameraMetadata*);
+typedef camera_status_t (*pfnACameraMetadata_getConstEntry)(const ACameraMetadata*, uint32_t tag, ACameraMetadata_const_entry*);
+typedef camera_status_t (*pfnACameraCaptureSession_setRepeatingRequest)(ACameraCaptureSession*, ACameraCaptureSession_captureCallbacks*, int numRequests, ACaptureRequest**, int*);
+typedef camera_status_t (*pfnACameraOutputTarget_create)(ACameraWindowType*,ACameraOutputTarget**);
+typedef camera_status_t (*pfnACaptureRequest_addTarget)(ACaptureRequest*, const ACameraOutputTarget*);
+typedef camera_status_t (*pfnACaptureSessionOutputContainer_add)(ACaptureSessionOutputContainer*, const ACaptureSessionOutput*);
+typedef camera_status_t (*pfnACaptureSessionOutputContainer_create)(ACaptureSessionOutputContainer**);
+typedef camera_status_t (*pfnACaptureSessionOutput_create)(ACameraWindowType*, ACaptureSessionOutput**);
+static pfnACameraManager_create pACameraManager_create = NULL;
+static pfnACameraManager_registerAvailabilityCallback pACameraManager_registerAvailabilityCallback = NULL;
+static pfnACameraManager_unregisterAvailabilityCallback pACameraManager_unregisterAvailabilityCallback = NULL;
+static pfnACameraManager_getCameraIdList pACameraManager_getCameraIdList = NULL;
+static pfnACameraManager_deleteCameraIdList pACameraManager_deleteCameraIdList = NULL;
+static pfnACameraCaptureSession_close pACameraCaptureSession_close = NULL;
+static pfnACaptureRequest_free pACaptureRequest_free = NULL;
+static pfnACameraOutputTarget_free pACameraOutputTarget_free = NULL;
+static pfnACameraDevice_close pACameraDevice_close = NULL;
+static pfnACameraManager_delete pACameraManager_delete = NULL;
+static pfnACaptureSessionOutputContainer_free pACaptureSessionOutputContainer_free = NULL;
+static pfnACaptureSessionOutput_free pACaptureSessionOutput_free = NULL;
+static pfnACameraManager_openCamera pACameraManager_openCamera = NULL;
+static pfnACameraDevice_createCaptureRequest pACameraDevice_createCaptureRequest = NULL;
+static pfnACameraDevice_createCaptureSession pACameraDevice_createCaptureSession = NULL;
+static pfnACameraManager_getCameraCharacteristics pACameraManager_getCameraCharacteristics = NULL;
+static pfnACameraMetadata_free pACameraMetadata_free = NULL;
+static pfnACameraMetadata_getConstEntry pACameraMetadata_getConstEntry = NULL;
+static pfnACameraCaptureSession_setRepeatingRequest pACameraCaptureSession_setRepeatingRequest = NULL;
+static pfnACameraOutputTarget_create pACameraOutputTarget_create = NULL;
+static pfnACaptureRequest_addTarget pACaptureRequest_addTarget = NULL;
+static pfnACaptureSessionOutputContainer_add pACaptureSessionOutputContainer_add = NULL;
+static pfnACaptureSessionOutputContainer_create pACaptureSessionOutputContainer_create = NULL;
+static pfnACaptureSessionOutput_create pACaptureSessionOutput_create = NULL;
+
+static void *libmediandk = NULL;
+typedef void (*pfnAImage_delete)(AImage*);
+typedef media_status_t (*pfnAImage_getTimestamp)(const AImage*, int64_t*);
+typedef media_status_t (*pfnAImage_getNumberOfPlanes)(const AImage*, int32_t*);
+typedef media_status_t (*pfnAImage_getPlaneRowStride)(const AImage*, int, int32_t*);
+typedef media_status_t (*pfnAImage_getPlaneData)(const AImage*, int, uint8_t**, int*);
+typedef media_status_t (*pfnAImageReader_acquireNextImage)(AImageReader*, AImage**);
+typedef void (*pfnAImageReader_delete)(AImageReader*);
+typedef media_status_t (*pfnAImageReader_setImageListener)(AImageReader*, AImageReader_ImageListener*);
+typedef media_status_t (*pfnAImageReader_getWindow)(AImageReader*, ANativeWindow**);
+typedef media_status_t (*pfnAImageReader_new)(int32_t, int32_t, int32_t, int32_t, AImageReader**);
+static pfnAImage_delete pAImage_delete = NULL;
+static pfnAImage_getTimestamp pAImage_getTimestamp = NULL;
+static pfnAImage_getNumberOfPlanes pAImage_getNumberOfPlanes = NULL;
+static pfnAImage_getPlaneRowStride pAImage_getPlaneRowStride = NULL;
+static pfnAImage_getPlaneData pAImage_getPlaneData = NULL;
+static pfnAImageReader_acquireNextImage pAImageReader_acquireNextImage = NULL;
+static pfnAImageReader_delete pAImageReader_delete = NULL;
+static pfnAImageReader_setImageListener pAImageReader_setImageListener = NULL;
+static pfnAImageReader_getWindow pAImageReader_getWindow = NULL;
+static pfnAImageReader_new pAImageReader_new = NULL;
+
+typedef media_status_t (*pfnAImage_getWidth)(const AImage*, int32_t*);
+typedef media_status_t (*pfnAImage_getHeight)(const AImage*, int32_t*);
+static pfnAImage_getWidth pAImage_getWidth = NULL;
+static pfnAImage_getHeight pAImage_getHeight = NULL;
 
-static ACameraManager *cameraMgr = NULL;
-static ACameraIdList *cameraIdList = NULL;
+struct SDL_PrivateCameraData
+{
+    ACameraDevice *device;
+    AImageReader *reader;
+    ANativeWindow *window;
+    ACaptureSessionOutput *sessionOutput;
+    ACaptureSessionOutputContainer *sessionOutputContainer;
+    ACameraOutputTarget *outputTarget;
+    ACaptureRequest *request;
+    ACameraCaptureSession *session;
+    SDL_CameraSpec requested_spec;
+};
 
-static int CreateCameraManager(void)
+static int SetErrorStr(const char *what, const char *errstr, const int rc)
 {
-    if (cameraMgr == NULL) {
-        #if 0  // !!! FIXME: this is getting replaced in a different branch.
-        if (!Android_JNI_RequestPermission("android.permission.CAMERA")) {
-            SDL_SetError("This app doesn't have CAMERA permission");
-            return;
-        }
-        #endif
-        cameraMgr = ACameraManager_create();
-        if (cameraMgr == NULL) {
-            SDL_Log("Error creating ACameraManager");
-        } else {
-            SDL_Log("Create ACameraManager");
-        }
+    char errbuf[128];
+    if (!errstr) {
+        SDL_snprintf(errbuf, sizeof (errbuf), "Unknown error #%d", rc);
+        errstr = errbuf;
     }
+    return SDL_SetError("%s: %s", what, errstr);
+}
 
-    cameraMgr = ACameraManager_create();
-
-    return cameraMgr ? 0 : SDL_SetError("Error creating ACameraManager");
+static const char *CameraStatusStr(const camera_status_t rc)
+{
+    switch (rc) {
+        case ACAMERA_OK: return "no error";
+        case ACAMERA_ERROR_UNKNOWN: return "unknown error";
+        case ACAMERA_ERROR_INVALID_PARAMETER: return "invalid parameter";
+        case ACAMERA_ERROR_CAMERA_DISCONNECTED: return "camera disconnected";
+        case ACAMERA_ERROR_NOT_ENOUGH_MEMORY: return "not enough memory";
+        case ACAMERA_ERROR_METADATA_NOT_FOUND: return "metadata not found";
+        case ACAMERA_ERROR_CAMERA_DEVICE: return "camera device error";
+        case ACAMERA_ERROR_CAMERA_SERVICE: return "camera service error";
+        case ACAMERA_ERROR_SESSION_CLOSED: return "session closed";
+        case ACAMERA_ERROR_INVALID_OPERATION: return "invalid operation";
+        case ACAMERA_ERROR_STREAM_CONFIGURE_FAIL: return "configure failure";
+        case ACAMERA_ERROR_CAMERA_IN_USE: return "camera in use";
+        case ACAMERA_ERROR_MAX_CAMERA_IN_USE: return "max cameras in use";
+        case ACAMERA_ERROR_CAMERA_DISABLED: return "camera disabled";
+        case ACAMERA_ERROR_PERMISSION_DENIED: return "permission denied";
+        case ACAMERA_ERROR_UNSUPPORTED_OPERATION: return "unsupported operation";
+        default: break;
+    }
+
+    return NULL;  // unknown error
 }
 
-static void DestroyCameraManager(void)
+static int SetCameraError(const char *what, const camera_status_t rc)
 {
-    if (cameraIdList) {
-        ACameraManager_deleteCameraIdList(cameraIdList);
-        cameraIdList = NULL;
-    }
+    return SetErrorStr(what, CameraStatusStr(rc), (int) rc);
+}
 
-    if (cameraMgr) {
-        ACameraManager_delete(cameraMgr);
-        cameraMgr = NULL;
-    }
+static const char *MediaStatusStr(const media_status_t rc)
+{
+    switch (rc) {
+        case AMEDIA_OK: return "no error";
+        case AMEDIACODEC_ERROR_INSUFFICIENT_RESOURCE: return "insuffient resources";
+        case AMEDIACODEC_ERROR_RECLAIMED: return "reclaimed";
+        case AMEDIA_ERROR_UNKNOWN: return "unknown error";
+        case AMEDIA_ERROR_MALFORMED: return "malformed";
+        case AMEDIA_ERROR_UNSUPPORTED: return "unsupported";
+        case AMEDIA_ERROR_INVALID_OBJECT: return "invalid object";
+        case AMEDIA_ERROR_INVALID_PARAMETER: return "invalid parameter";
+        case AMEDIA_ERROR_INVALID_OPERATION: return "invalid operation";
+        case AMEDIA_ERROR_END_OF_STREAM: return "end of stream";
+        case AMEDIA_ERROR_IO: return "i/o error";
+        case AMEDIA_ERROR_WOULD_BLOCK: return "operation would block";
+        case AMEDIA_DRM_NOT_PROVISIONED: return "DRM not provisioned";
+        case AMEDIA_DRM_RESOURCE_BUSY: return "DRM resource busy";
+        case AMEDIA_DRM_DEVICE_REVOKED: return "DRM device revoked";
+        case AMEDIA_DRM_SHORT_BUFFER: return "DRM short buffer";
+        case AMEDIA_DRM_SESSION_NOT_OPENED: return "DRM session not opened";
+        case AMEDIA_DRM_TAMPER_DETECTED: return "DRM tampering detected";
+        case AMEDIA_DRM_VERIFY_FAILED: return "DRM verify failed";
+        case AMEDIA_DRM_NEED_KEY: return "DRM need key";
+        case AMEDIA_DRM_LICENSE_EXPIRED: return "DRM license expired";
+        case AMEDIA_IMGREADER_NO_BUFFER_AVAILABLE: return "no buffer available";
+        case AMEDIA_IMGREADER_MAX_IMAGES_ACQUIRED: return "maximum images acquired";
+        case AMEDIA_IMGREADER_CANNOT_LOCK_IMAGE: return "cannot lock image";
+        case AMEDIA_IMGREADER_CANNOT_UNLOCK_IMAGE: return "cannot unlock image";
+        case AMEDIA_IMGREADER_IMAGE_NOT_LOCKED: return "image not locked";
+        default: break;
+    }
+
+    return NULL;  // unknown error
 }
 
-struct SDL_PrivateCameraData
+static int SetMediaError(const char *what, const media_status_t rc)
 {
-    ACameraDevice *device;
-    ACameraCaptureSession *session;
-    ACameraDevice_StateCallbacks dev_callbacks;
-    ACameraCaptureSession_stateCallbacks capture_callbacks;
-    ACaptureSessionOutputContainer *sessionOutputContainer;
-    AImageReader *reader;
-    int num_formats;
-    int count_formats[6]; // see format_to_id
-};
+    return SetErrorStr(what, MediaStatusStr(rc), (int) rc);
+}
 
 
-#define FORMAT_SDL SDL_PIXELFORMAT_NV12
+static ACameraManager *cameraMgr = NULL;
 
-static int format_to_id(int fmt) {
-     switch (fmt) {
-        #define CASE(x, y)  case x: return y
-        CASE(FORMAT_SDL, 0);
-        CASE(SDL_PIXELFORMAT_RGB565, 1);
-        CASE(SDL_PIXELFORMAT_XRGB8888, 2);
-        CASE(SDL_PIXELFORMAT_RGBA8888, 3);
-        CASE(SDL_PIXELFORMAT_RGBX8888, 4);
-        CASE(SDL_PIXELFORMAT_UNKNOWN, 5);
-        #undef CASE
-        default:
-            return 5;
+static int CreateCameraManager(void)
+{
+    SDL_assert(cameraMgr == NULL);
+
+    cameraMgr = pACameraManager_create();
+    if (!cameraMgr) {
+        return SDL_SetError("Error creating ACameraManager");
     }
+    return 0;
 }
 
-static int id_to_format(int fmt) {
-     switch (fmt) {
-        #define CASE(x, y)  case y: return x
-        CASE(FORMAT_SDL, 0);
-        CASE(SDL_PIXELFORMAT_RGB565, 1);
-        CASE(SDL_PIXELFORMAT_XRGB8888, 2);
-        CASE(SDL_PIXELFORMAT_RGBA8888, 3);
-        CASE(SDL_PIXELFORMAT_RGBX8888, 4);
-        CASE(SDL_PIXELFORMAT_UNKNOWN, 5);
-        #undef CASE
-        default:
-            return SDL_PIXELFORMAT_UNKNOWN;
+static void DestroyCameraManager(void)
+{
+    if (cameraMgr) {
+        pACameraManager_delete(cameraMgr);
+        cameraMgr = NULL;
     }
 }
 
@@ -146,27 +256,30 @@ static Uint32 format_android_to_sdl(Uint32 fmt)
 {
     switch (fmt) {
         #define CASE(x, y)  case x: return y
-        CASE(AIMAGE_FORMAT_YUV_420_888, FORMAT_SDL);
+        CASE(AIMAGE_FORMAT_YUV_420_888, SDL_PIXELFORMAT_NV12);
         CASE(AIMAGE_FORMAT_RGB_565,     SDL_PIXELFORMAT_RGB565);
         CASE(AIMAGE_FORMAT_RGB_888,     SDL_PIXELFORMAT_XRGB8888);
         CASE(AIMAGE_FORMAT_RGBA_8888,   SDL_PIXELFORMAT_RGBA8888);
         CASE(AIMAGE_FORMAT_RGBX_8888,   SDL_PIXELFORMAT_RGBX8888);
-
-        CASE(AIMAGE_FORMAT_RGBA_FP16,   SDL_PIXELFORMAT_UNKNOWN); // 64bits
-        CASE(AIMAGE_FORMAT_RAW_PRIVATE, SDL_PIXELFORMAT_UNKNOWN);
-        CASE(AIMAGE_FORMAT_JPEG,        SDL_PIXELFORMAT_UNKNOWN);
+        //CASE(AIMAGE_FORMAT_RGBA_FP16,   SDL_PIXELFORMAT_UNKNOWN); // 64bits
+        //CASE(AIMAGE_FORMAT_RAW_PRIVATE, SDL_PIXELFORMAT_UNKNOWN);
+        //CASE(AIMAGE_FORMAT_JPEG,        SDL_PIXELFORMAT_UNKNOWN);
         #undef CASE
-        default:
-            SDL_Log("Unknown format AIMAGE_FORMAT '%d'", fmt);
-            return SDL_PIXELFORMAT_UNKNOWN;
+        default: break;
     }
+
+    #if DEBUG_CAMERA
+    //SDL_Log("Unknown format AIMAGE_FORMAT '%d'", fmt);
+    #endif
+
+    return SDL_PIXELFORMAT_UNKNOWN;
 }
 
 static Uint32 format_sdl_to_android(Uint32 fmt)
 {
     switch (fmt) {
         #define CASE(x, y)  case y: return x
-        CASE(AIMAGE_FORMAT_YUV_420_888, FORMAT_SDL);
+        CASE(AIMAGE_FORMAT_YUV_420_888, SDL_PIXELFORMAT_NV12);
         CASE(AIMAGE_FORMAT_RGB_565,     SDL_PIXELFORMAT_RGB565);
         CASE(AIMAGE_FORMAT_RGB_888,     SDL_PIXELFORMAT_XRGB8888);
         CASE(AIMAGE_FORMAT_RGBA_8888,   SDL_PIXELFORMAT_RGBA8888);
@@ -177,12 +290,95 @@ static Uint32 format_sdl_to_android(Uint32 fmt)
     }
 }
 
+static int ANDROIDCAMERA_WaitDevice(SDL_CameraDevice *device)
+{
+    return 0;  // this isn't used atm, since we run our own thread via onImageAvailable callbacks.
+}
+
+static int ANDROIDCAMERA_AcquireFrame(SDL_CameraDevice *device, SDL_Surface *frame, Uint64 *timestampNS)
+{
+    int retval = 1;
+    media_status_t res;
+    AImage *image = NULL;
+
+    res = pAImageReader_acquireNextImage(device->hidden->reader, &image);
+    // We could also use this one:
+    //res = AImageReader_acquireLatestImage(device->hidden->reader, &image);
+
+    SDL_assert(res != AMEDIA_IMGREADER_NO_BUFFER_AVAILABLE);  // we should only be here if onImageAvailable was called.
+
+    if (res != AMEDIA_OK) {
+        return SetMediaError("Error AImageReader_acquireNextImage", res);
+    }
+
+    int64_t atimestamp = 0;
+    if (pAImage_getTimestamp(image, &atimestamp) == AMEDIA_OK) {
+        *timestampNS = (Uint64) atimestamp;
+    } else {
+        *timestampNS = 0;
+    }
+
+    // !!! FIXME: this currently copies the data to the surface (see FIXME about non-contiguous planar surfaces, but in theory we could just keep this locked until ReleaseFrame...
+    int32_t num_planes = 0;
+    pAImage_getNumberOfPlanes(image, &num_planes);
+
+    if ((num_planes == 3) && (device->spec.format == SDL_PIXELFORMAT_NV12)) {
+        num_planes--;   // treat the interleaved planes as one.
+    }
+
+    // !!! FIXME: we have an open issue in SDL3 to allow SDL_Surface to support non-contiguous planar data, but we don't have it yet.
+    size_t buflen = 0;
+    for (int i = 0; (i < num_planes) && (i < 3); i++) {
+        uint8_t *data = NULL;
+        int32_t datalen = 0;
+        pAImage_getPlaneData(image, i, &data, &datalen);
+        buflen += (int) datalen;
+    }
+
+    frame->pixels = SDL_aligned_alloc(SDL_SIMDGetAlignment(), buflen);
+    if (frame->pixels == NULL) {
+        retval = -1;
+    } else {
+        int32_t row_stride = 0;
+        Uint8 *dst = frame->pixels;
+        pAImage_getPlaneRowStride(image, 0, &row_stride);
+        frame->pitch = (int) row_stride;  // this is what SDL3 currently expects, probably incorrectly.
+
+        for (int i = 0; (i < num_planes) && (i < 3); i++) {
+            uint8_t *data = NULL;
+            int32_t datalen = 0;
+            pAImage_getPlaneData(image, i, &data, &datalen);
+            const void *src = data;
+            SDL_memcpy(dst, src, datalen);
+            dst += datalen;
+        }
+    }
+
+    pAImage_delete(image);
+
+    return retval;
+}
+
+static void ANDROIDCAMERA_ReleaseFrame(SDL_CameraDevice *device, SDL_Surface *frame)
+{
+    // !!! FIXME: this currently copies the data to the surface, but in theory we could just keep the AImage until ReleaseFrame...
+    SDL_aligned_free(frame->pixels);
+}
+
+static void onImageAvailable(void *context, AImageReader *reader)
+{
+    #if DEBUG_CAMERA
+    SDL_Log("CAMERA: CB onImageAvailable");
+    #endif
+    SDL_CameraDevice *device = (SDL_CameraDevice *) context;
+    SDL_CameraThreadIterate(device);
+}
 
 static void onDisconnected(void *context, ACameraDevice *device)
 {
     // SDL_CameraDevice *_this = (SDL_CameraDevice *) context;
     #if DEBUG_CAMERA
-    SDL_Log("CB onDisconnected");
+    SDL_Log("CAMERA: CB onDisconnected");
     #endif
 }
 
@@ -190,16 +386,15 @@ static void onError(void *context, ACameraDevice *device, int error)
 {
     // SDL_CameraDevice *_this = (SDL_CameraDevice *) context;
     #if DEBUG_CAMERA
-    SDL_Log("CB onError");
+    SDL_Log("CAMERA: CB onError");
     #endif
 }
 
-
 static void onClosed(void* context, ACameraCaptureSession *session)
 {
     // SDL_CameraDevice *_this = (SDL_CameraDevice *) context;
     #if DEBUG_CAMERA
-    SDL_Log("CB onClosed");
+    SDL_Log("CAMERA: CB onClosed");
     #endif
 }
 
@@ -207,7 +402,7 @@ static void onReady(void* context, ACameraCaptureSession *session)
 {
     // SDL_CameraDevice *_this = (SDL_CameraDevice *) context;
     #if DEBUG_CAMERA
-    SDL_Log("CB onReady");
+    SDL_Log("CAMERA: CB onReady");
     #endif
 }
 
@@ -215,493 +410,478 @@ static void onActive(void* context, ACameraCaptureSession *session)
 {
     // SDL_CameraDevice *_this = (SDL_CameraDevice *) context;
     #if DEBUG_CAMERA
-    SDL_Log("CB onActive");
+    SDL_Log("CAMERA: CB onActive");
     #endif
 }
 
-static int ANDROIDCAMERA_OpenDevice(SDL_CameraDevice *_this)
+static void ANDROIDCAMERA_CloseDevice(SDL_CameraDevice *device)
 {
-    /* Cannot open a second camera, while the first one is opened.
-     * If you want to play several camera, they must all be opened first, then played.
-     *
-     * https://developer.android.com/reference/android/hardware/camera2/CameraManager
-     * "All camera devices intended to be operated concurrently, must be opened using openCamera(String, CameraDevice.StateCallback, Handler),
-     * before configuring sessions on any of the camera devices.  * "
-     *
-     */
-    if (CheckDevicePlaying()) {
-        return SDL_SetError("A camera is already playing");
-    }
+    if (device && device->hidden) {
+        struct SDL_PrivateCameraData *hidden = device->hidden;
+        device->hidden = NULL;
 
-    _this->hidden = (struct SDL_PrivateCameraData *) SDL_calloc(1, sizeof (struct SDL_PrivateCameraData));
-    if (_this->hidden == NULL) {
-        return -1;
-    }
-
-    CreateCameraManager();
-
-    _this->hidden->dev_callbacks.context = (void *) _this;
-    _this->hidden->dev_callbacks.onDisconnected = onDisconnected;
-    _this->hidden->dev_callbacks.onError = onError;
-
-    camera_status_t res = ACameraManager_openCamera(cameraMgr, _this->dev_name, &_this->hidden->dev_callbacks, &_this->hidden->device);
-    if (res != ACAMERA_OK) {
-        return SDL_SetError("Failed to open camera");
-    }
+        if (hidden->reader) {
+            pAImageReader_setImageListener(hidden->reader, NULL);
+        }
 
-    return 0;
-}
+        if (hidden->session) {
+            pACameraCaptureSession_close(hidden->session);
+        }
 
-static void ANDROIDCAMERA_CloseDevice(SDL_CameraDevice *_this)
-{
-    if (_this && _this->hidden) {
-        if (_this->hidden->session) {
-            ACameraCaptureSession_close(_this->hidden->session);
+        if (hidden->request) {
+            pACaptureRequest_free(hidden->request);
         }
 
-        if (_this->hidden->sessionOutputContainer) {
-            ACaptureSessionOutputContainer_free(_this->hidden->sessionOutputContainer);
+        if (hidden->outputTarget) {
+            pACameraOutputTarget_free(hidden->outputTarget);
         }
 
-        if (_this->hidden->reader) {
-            AImageReader_delete(_this->hidden->reader);
+        if (hidden->sessionOutputContainer) {
+            pACaptureSessionOutputContainer_free(hidden->sessionOutputContainer);
         }
 
-        if (_this->hidden->device) {
-            ACameraDevice_close(_this->hidden->device);
+        if (hidden->sessionOutput) {
+            pACaptureSessionOutput_free(hidden->sessionOutput);
         }
 
-        SDL_free(_this->hidden);
+        // we don't free hidden->window here, it'll be cleaned up by AImageReader_delete.
 
-        _this->hidden = NULL;
-    }
-}
+        if (hidden->reader) {
+            pAImageReader_delete(hidden->reader);
+        }
 
-static int ANDROIDCAMERA_InitDevice(SDL_CameraDevice *_this)
-{
-    size_t size, pitch;
-    SDL_CalculateSize(_this->spec.format, _this->spec.width, _this->spec.height, &size, &pitch, SDL_FALSE);
-    SDL_Log("Buffer size: %d x %d", _this->spec.width, _this->spec.height);
-    return 0;
-}
+        if (hidden->device) {
+            pACameraDevice_close(hidden->device);
+        }
 
-static int ANDROIDCAMERA_GetDeviceSpec(SDL_CameraDevice *_this, SDL_CameraSpec *spec)
-{
-    // !!! FIXME: catch NULLs at higher level
-    if (spec) {
-        SDL_copyp(spec, &_this->spec);
-        return 0;
+        SDL_free(hidden);
     }
-    return -1;
 }
 
-static int ANDROIDCAMERA_StartCamera(SDL_CameraDevice *_this)
+// this is where the "opening" of the camera happens, after permission is granted.
+static int PrepareCamera(SDL_CameraDevice *device)
 {
-    // !!! FIXME: maybe log the error code in SDL_SetError
+    SDL_assert(device->hidden != NULL);
+
     camera_status_t res;
     media_status_t res2;
-    ANativeWindow *window = NULL;
-    ACaptureSessionOutput *sessionOutput;
-    ACameraOutputTarget *outputTarget;
-    ACaptureRequest *request;
-
-    res2 = AImageReader_new(_this->spec.width, _this->spec.height, format_sdl_to_android(_this->spec.format), 10 /* nb buffers */, &_this->hidden->reader);
-    if (res2 != AMEDIA_OK) {
-        SDL_SetError("Error AImageReader_new");
-        goto error;
-    }
-    res2 = AImageReader_getWindow(_this->hidden->reader, &window);
-    if (res2 != AMEDIA_OK) {
-        SDL_SetError("Error AImageReader_new");
-        goto error;
-    }
-
-    res = ACaptureSessionOutput_create(window, &sessionOutput);
-    if (res != ACAMERA_OK) {
-        SDL_SetError("Error ACaptureSessionOutput_create");
-        goto error;
-    }
-    res = ACaptureSessionOutputContainer_create(&_this->hidden->sessionOutputContainer);
-    if (res != ACAMERA_OK) {
-        SDL_SetError("Error ACaptureSessionOutputContainer_create");
-        goto error;
-    }
-    res = ACaptureSessionOutputContainer_add(_this->hidden->sessionOutputContainer, sessionOutput);
-    if (res != ACAMERA_OK) {
-        SDL_SetError("Error ACaptureSessionOutputContainer_add");
-        goto error;
-    }
-
-    res = ACameraOutputTarget_create(window, &outputTarget);
-    if (res != ACAMERA_OK) {
-        SDL_SetError("Error ACameraOutputTarget_create");
-        goto error;
-    }
 
-    res = ACameraDevice_createCaptureRequest(_this->hidden->device, TEMPLATE_RECORD, &request);
-    if (res != ACAMERA_OK) {
-        SDL_SetError("Error ACameraDevice_createCaptureRequest");
-        goto error;
-    }
-
-    res = ACaptureRequest_addTarget(request, outputTarget);
-    if (res != ACAMERA_OK) {
-        SDL_SetError("Error ACaptureRequest_addTarget");
-        goto error;
-    }
-
-    _this->hidden->capture_callbacks.context = (void *) _this;
-    _this->hidden->capture_callbacks.onClosed = onClosed;
-    _this->hidden->capture_callbacks.onReady = onReady;
-    _this->hidden->capture_callbacks.onActive = onActive;
-
-    res = ACameraDevice_createCaptureSession(_this->hidden->device,
-            _this->hidden->sessionOutputContainer,
-            &_this->hidden->capture_callbacks,
-            &_this->hidden->session);
-    if (res != ACAMERA_OK) {
-        SDL_SetError("Error ACameraDevice_createCaptureSession");
-        goto error;
-    }
+    ACameraDevice_StateCallbacks dev_callbacks;
+    SDL_zero(dev_callbacks);
+    dev_callbacks.context = device;
+    dev_callbacks.onDisconnected = onDisconnected;
+    dev_callbacks.onError = onError;
 
-    res = ACameraCaptureSession_setRepeatingRequest(_this->hidden->session, NULL, 1, &request, NULL);
-    if (res != ACAMERA_OK) {
-        SDL_SetError("Error ACameraDevice_createCaptureSession");
-        goto error;
+    ACameraCaptureSession_stateCallbacks capture_callbacks;
+    SDL_zero(capture_callbacks);
+    capture_callbacks.context = device;
+    capture_callbacks.onClosed = onClosed;
+    capture_callbacks.onReady = onReady;
+    capture_callbacks.onActive = onActive;
+
+    AImageReader_ImageListener imglistener;
+    SDL_zero(imglistener);
+    imglistener.context = device;
+    imglistener.onImageAvailable = onImageAvailable;
+
+    // just in case SDL_OpenCameraDevice is overwriting device->spec as CameraPermissionCallback runs, we work from a different copy.
+    const SDL_CameraSpec *spec = &device->hidden->requested_spec;
+
+    if ((res = pACameraManager_openCamera(cameraMgr, (const char *) device->handle, &dev_callbacks, &device->hidden->device)) != ACAMERA_OK) {
+        return SetCameraError("Failed to open camera", res);
+    } else if ((res2 = pAImageReader_new(spec->width, spec->height, format_sdl_to_android(spec->format), 10 /* nb buffers */, &device->hidden->reader)) != AMEDIA_OK) {
+        return SetMediaError("Error AImageReader_new", res2);
+    } else if ((res2 = pAImageReader_getWindow(device->hidden->reader, &device->hidden->window)) != AMEDIA_OK) {
+        return SetMediaError("Error AImageReader_getWindow", res2);
+    } else if ((res = pACaptureSessionOutput_create(device->hidden->window, &device->hidden->sessionOutput)) != ACAMERA_OK) {
+        return SetCameraError("Error ACaptureSessionOutput_create", res);
+    } else if ((res = pACaptureSessionOutputContainer_create(&device->hidden->sessionOutputContainer)) != ACAMERA_OK) {
+        return SetCameraError("Error ACaptureSessionOutputContainer_create", res);
+    } else if ((res = pACaptureSessionOutputContainer_add(device->hidden->sessionOutputContainer, device->hidden->sessionOutput)) != ACAMERA_OK) {
+        return SetCameraError("Error ACaptureSessionOutputContainer_add", res);
+    } else if ((res = pACameraOutputTarget_create(device->hidden->window, &device->hidden->outputTarget)) != ACAMERA_OK) {
+        return SetCameraError("Error ACameraOutputTarget_create", res);
+    } else if ((res = pACameraDevice_createCaptureRequest(device->hidden->device, TEMPLATE_RECORD, &device->hidden->request)) != ACAMERA_OK) {
+        return SetCameraError("Error ACameraDevice_createCaptureRequest", res);
+    } else if ((res = pACaptureRequest_addTarget(device->hidden->request, device->hidden->outputTarget)) != ACAMERA_OK) {
+        return SetCameraError("Error ACaptureRequest_addTarget", res);
+    } else if ((res = pACameraDevice_createCaptureSession(device->hidden->device, device->hidden->sessionOutputContainer, &capture_callbacks, &device->hidden->session)) != ACAMERA_OK) {
+        return SetCameraError("Error ACameraDevice_createCaptureSession", res);
+    } else if ((res = pACameraCaptureSession_setRepeatingRequest(device->hidden->session, NULL, 1, &device->hidden->request, NULL)) != ACAMERA_OK) {
+        return SetCameraError("Error ACameraCaptureSession_setRepeatingRequest", res);
+    } else if ((res2 = pAImageReader_setImageListener(device->hidden->reader, &imglistener)) != AMEDIA_OK) {
+        return SetMediaError("Error AImageReader_setImageListener", res2);
     }
 
     return 0;
-
-error:
-    return -1;
-}
-
-static int ANDROIDCAMERA_StopCamera(SDL_CameraDevice *_this)
-{
-    ACameraCaptureSession_close(_this->hidden->session);
-    _this->hidden->session = NULL;
-    return 0;
 }
 
-static int ANDROIDCAMERA_AcquireFrame(SDL_CameraDevice *_this, SDL_CameraFrame *frame)
+static void SDLCALL CameraPermissionCallback(void *userdata, const char *permission, SDL_bool granted)
 {
-    media_status_t res;
-    AImage *image;
-    res = AImageReader_acquireNextImage(_this->hidden->reader, &image);
-    /* We could also use this one:
-    res = AImageReader_acquireLatestImage(_this->hidden->reader, &image);
-    */
-    if (res == AMEDIA_IMGREADER_NO_BUFFER_AVAILABLE ) {
-        SDL_Delay(20); // TODO fix some delay
-        #if DEBUG_CAMERA
-        //SDL_Log("AImageReader_acquireNextImage: AMEDIA_IMGREADER_NO_BUFFER_AVAILABLE");
-        #endif
-    } else if (res == AMEDIA_OK ) {
-        int32_t numPlanes = 0;
-        AImage_getNumberOfPlanes(image, &numPlanes);
-
-        frame->timestampNS = SDL_GetTicksNS();
-
-        for (int i = 0; i < numPlanes && i < 3; i++) {
-            int dataLength = 0;
-            int rowStride = 0;
-            uint8_t *data = NULL;
-            frame->num_planes += 1;
-            AImage_getPlaneRowStride(image, i, &rowStride);
-            res = AImage_getPlaneData(image, i, &data, &dataLength);
-            if (res == AMEDIA_OK) {
-                frame->data[i] = data;
-                frame->pitch[i] = rowStride;
-            }
-        }
-
-        if (frame->num_planes == 3) {
-            /* plane 2 and 3 are interleaved NV12. SDL only takes two planes for this format */
-            int pixelStride = 0;
-            AImage_getPlanePixelStride(image, 1, &pixelStride);
-            if (pixelStride == 2) {
-                frame->num_planes -= 1;
-            }
+    SDL_CameraDevice *device = (SDL_CameraDevice *) userdata;
+    if (device->hidden != NULL) {   // if device was already closed, don't send an event.
+        if (!granted) {
+            SDL_CameraDevicePermissionOutcome(device, SDL_FALSE);  // sorry, permission denied.
+        } else if (PrepareCamera(device) < 0) {  // permission given? Actually open the camera now.
+            // uhoh, setup failed; since the app thinks we already "opened" the device, mark it as disconnected and don't report the permission.
+            SDL_CameraDeviceDisconnected(device);
+        } else {
+            // okay! We have permission to use the camera _and_ opening the hardware worked out, report that the camera is usable!
+            SDL_CameraDevicePermissionOutcome(device, SDL_TRUE);  // go go go!
         }
-
-        frame->internal = (void*)image;
-    } else if (res == AMEDIA_IMGREADER_MAX_IMAGES_ACQUIRED) {
-        return SDL_SetError("AMEDIA_IMGREADER_MAX_IMAGES_ACQUIRED");
-    } else {
-        return SDL_SetError("AImageReader_acquireNextImage: %d", res);
     }
 
-    return 0;
+    UnrefPhysicalCameraDevice(device);   // we ref'd this in OpenDevice, release the extra reference.
 }
 
-static int ANDROIDCAMERA_ReleaseFrame(SDL_CameraDevice *_this, SDL_CameraFrame *frame)
-{
-    if (frame->internal){
-        AImage_delete((AImage *)frame->internal);
-    }
-    return 0;
-}
 
-static int ANDROIDCAMERA_GetNumFormats(SDL_CameraDevice *_this)
+static int ANDROIDCAMERA_OpenDevice(SDL_CameraDevice *device, const SDL_CameraSpec *spec)
 {
-    camera_status_t res;
-    SDL_bool unknown = SDL_FALSE;
-    ACameraMetadata *metadata;
-    ACameraMetadata_const_entry entry;
-
-    if (_this->hidden->num_formats != 0) {
-        return _this->hidden->num_formats;
-    }
-
-    res = ACameraManager_getCameraCharacteristics(cameraMgr, _this->dev_name, &metadata);
-    if (res != ACAMERA_OK) {
-        return -1;
+#if 0  // !!! FIXME: for now, we'll just let this fail if it is going to fail, without checking for this
+    /* Cannot open a second camera, while the first one is opened.
+     * If you want to play several camera, they must all be opened first, then played.
+     *
+     * https://developer.android.com/reference/android/hardware/camera2/CameraManager
+     * "All camera devices intended to be operated concurrently, must be opened using openCamera(String, CameraDevice.StateCallback, Handler),
+     * before configuring sessions on any of the camera devices.  * "
+     *
+     */
+    if (CheckDevicePlaying()) {
+        return SDL_SetError("A camera is already playing");
     }
+#endif
 
-    res = ACameraMetadata_getConstEntry(metadata, ACAMERA_SCALER_AVAILABLE_STREAM_CONFIGURATIONS, &entry);
-    if (res != ACAMERA_OK) {
+    device->hidden = (struct SDL_PrivateCameraData *) SDL_calloc(1, sizeof (struct SDL_PrivateCameraData));
+    if (device->hidden == NULL) {
         return -1;
     }
 
-    SDL_Log("got entry ACAMERA_SCALER_AVAILABLE_STREAM_CONFIGURATIONS");
-
-    for (int i = 0; i < entry.count; i += 4) {
-        const int32_t format = entry.data.i32[i + 0];
-        const int32_t type = entry.data.i32[i + 3];
-
-        if (type == ACAMERA_SCALER_AVAILABLE_STREAM_CONFIGURATIONS_INPUT) {
-            continue;
-        }
-
-        const Uint32 fmt = format_android_to_sdl(format);
-        _this->hidden->count_formats[format_to_id(fmt)] += 1;
+    RefPhysicalCameraDevice(device);  // ref'd until permission callback fires.
 
-        #if DEBUG_CAMERA
-        if (fmt != SDL_PIXELFORMAT_UNKNOWN) {
-            int w = entry.data.i32[i + 1];
-            int h = entry.data.i32[i + 2];
-            SDL_Log("Got format android 0x%08x -> %s %d x %d", format, SDL_GetPixelFormatName(fmt), w, h);
-        } else {
-            unknown = SDL_TRUE;
-        }
-        #endif
-    }
-
-    #if DEBUG_CAMERA
-    if (unknown) {
-        SDL_Log("Got unknown android");
-    }
-    #endif
-
-
-    if ( _this->hidden->count_formats[0]) _this->hidden->num_formats += 1;
-    if ( _this->hidden->count_formats[1]) _this->hidden->num_formats += 1;
-    if ( _this->hidden->count_formats[2]) _this->hidden->num_formats += 1;
-    if ( _this->hidden->count_formats[3]) _this->hidden->num_formats += 1;
-    if ( _this->hidden->count_formats[4]) _this->hidden->num_formats += 1;
-    if ( _this->hidden->count_formats[5]) _this->hidden->num_formats += 1;
-
-    return _this->hidden->num_formats;
-}
-
-static int ANDROIDCAMERA_GetFormat(SDL_CameraDevice *_this, int index, Uint32 *format)
-{
-    int i2 = 0;
-
-    if (_this->hidden->num_formats == 0) {
-        GetNumFormats(_this);
-    }
-
-    if (index < 0 || index >= _this->hidden->num_formats) {
-        // !!! FIXME: call SDL_SetError()?
+    // just in case SDL_OpenCameraDevice is overwriting device->spec as CameraPermissionCallback runs, we work from a different copy.
+    SDL_copyp(&device->hidden->requested_spec, spec);
+    if (SDL_AndroidRequestPermission("android.permission.CAMERA", CameraPermissionCallback, device) < 0) {
+        UnrefPhysicalCameraDevice(device);
         return -1;
     }
 
-    for (int i = 0; i < SDL_arraysize(_this->hidden->count_formats); i++) {
-        if (_this->hidden->count_formats[i] == 0) {
-            continue;
-        }
-
-        if (i2 == index) {
-            *format = id_to_format(i);
-        }
-
-        i2++;
-
-    }
-    return 0;
+    return 0;  // we don't open the camera until permission is granted, so always succeed for now.
 }
 
-static int ANDROIDCAMERA_GetNumFrameSizes(SDL_CameraDevice *_this, Uint32 format)
+static void ANDROIDCAMERA_FreeDeviceHandle(SDL_CameraDevice *device)
 {
-    // !!! FIXME: call SDL_SetError()?
-    if (_this->hidden->num_formats == 0) {
-        GetNumFormats(_this);
-    }
-
-    const int index = format_to_id(format);
-
-    int i2 = 0;
-    for (int i = 0; i < SDL_arraysize(_this->hidden->count_formats); i++) {
-        if (_this->hidden->count_formats[i] == 0) {
-            continue;
-        }
-
-        if (i2 == index) {
-            /* number of resolution for this format */
-            return _this->hidden->count_formats[i];
-        }
-
-        i2++;
+    if (device) {
+        SDL_free(device->handle);
     }
-
-    return -1;
 }
 
-static int ANDROIDCAMERA_GetFrameSize(SDL_CameraDevice *_this, Uint32 format, int index, int *width, int *height)
+static void GatherCameraSpecs(const char *devid, CameraFormatAddData *add_data, char **fullname, const char **posstr)
 {
-    // !!! FIXME: call SDL_SetError()?
-    camera_status_t res;
-    ACameraMetadata *metadata;
-    ACameraMetadata_const_entry entry;
-
-    if (_this->hidden->num_formats == 0) {
-        GetNumFormats(_this);
+    SDL_zerop(add_data);
+
+    ACameraMetadata *metadata = NULL;
+    ACameraMetadata_const_entry cfgentry;
+    ACameraMetadata_const_entry durentry;
+    ACameraMetadata_const_entry infoentry;
+
+    // This can fail with an "unknown error" (with `adb logcat` reporting "no such file or directory")
+    // for "LEGACY" level cameras. I saw this happen on a 30-dollar budget phone I have for testing
+    // (but a different brand budget phone worked, so it's not strictly the low-end of Android devices).
+    // LEGACY devices are seen by onCameraAvailable, but are not otherwise accessible through
+    // libcamera2ndk. The Java camera2 API apparently _can_ access these cameras, but we're going on
+    // without them here for now, in hopes that such hardware is a dying breed.
+    if (pACameraManager_getCameraCharacteristics(cameraMgr, devid, &metadata) != ACAMERA_OK) {
+        return;  // oh well.
+    } else if (pACameraMetadata_getConstEntry(metadata, ACAMERA_SCALER_AVAILABLE_STREAM_CONFIGURATIONS, &cfgentry) != ACAMERA_OK) {
+        pACameraMetadata_free(metadata);
+        return;  // oh well.
+    } else if (pACameraMetadata_getConstEntry(metadata, ACAMERA_SCALER_AVAILABLE_MIN_FRAME_DURATIONS, &durentry) != ACAMERA_OK) {
+        pACameraMetadata_free(metadata);
+        return;  // oh well.
+    }
+
+    *fullname = NULL;
+    if (pACameraMetadata_getConstEntry(metadata, ACAMERA_INFO_VERSION, &infoentry) == ACAMERA_OK) {
+        *fullname = (char *) SDL_malloc(infoentry.count + 1);
+        if (*fullname) {
+            SDL_strlcpy(*fullname, (const char *) infoentry.data.u8, infoentry.count + 1);
+        }
     }
 
-    res = ACameraManager_getCameraCharacteristics(cameraMgr, _this->dev_name, &metadata);
-    if (res != ACAMERA_OK) {
-        return -1;
+    *posstr = NULL;
+    ACameraMetadata_const_entry posentry;
+    if (pACameraMetadata_getConstEntry(metadata, ACAMERA_LENS_FACING, &posentry) == ACAMERA_OK) {  // ignore this if it fails.
+        if (*posentry.data.u8 == ACAMERA_LENS_FACING_FRONT) {
+            *posstr = "front";
+            if (!*fullname) {
+                *fullname = SDL_strdup("Front-facing camera");
+            }
+        } else if (*posentry.data.u8 == ACAMERA_LENS_FACING_BACK) {
+            *posstr = "back";
+            if (!*fullname) {
+                *fullname = SDL_strdup("Back-facing camera");
+            }
+        }
     }
 
-    res = ACameraMetadata_getConstEntry(metadata, ACAMERA_SCALER_AVAILABLE_STREAM_CONFIGURATIONS, &entry);
-    if (res != ACAMERA_OK) {
-        return -1;
+    if (!*fullname) {
+        *fullname = SDL_strdup("Generic camera");   // we tried.
     }
 
-    int i2 = 0;
-    for (int i = 0; i < entry.count; i += 4) {
-        int32_t f = entry.data.i32[i + 0];
-        const int w = entry.data.i32[i + 1];
-        const int h = entry.data.i32[i + 2];
-        int32_t type = entry.data.i32[i + 3];
+    const int32_t *i32ptr = cfgentry.data.i32;
+    for (int i = 0; i < cfgentry.count; i++, i32ptr += 4) {
+        const int32_t fmt = i32ptr[0];
+        const int w = (int) i32ptr[1];
+        const int h = (int) i32ptr[2];
+        const int32_t type = i32ptr[3];
+        Uint32 sdlfmt;
 
         if (type == ACAMERA_SCALER_AVAILABLE_STREAM_CONFIGURATIONS_INPUT) {
             continue;
-        }
-
-        Uint32 fmt = format_android_to_sdl(f);
-        if (fmt != format) {
+        } else if ((w <= 0) || (h <= 0)) {
+            continue;
+        } else if ((sdlfmt = format_android_to_sdl(fmt)) == SDL_PIXELFORMAT_UNKNOWN) {
             continue;
         }
 
-        if (i2 == index) {
-            *width = w;
-            *height = h;
-            return 0;
+#if 0 // !!! FIXME: these all come out with 0 durations on my test phone.  :(
+        const int64_t *i64ptr = durentry.data.i64;
+        for (int j = 0; j < durentry.count; j++, i64ptr += 4) {
+            const int32_t fpsfmt = (int32_t) i64ptr[0];
+            const int fpsw = (int) i64ptr[1];
+            const int fpsh = (int) i64ptr[2];
+            const long long duration = (long long) i64ptr[3];
+                SDL_Log("CAMERA: possible fps %s %dx%d duration=%lld", SDL_GetPixelFormatName(format_android_to_sdl(fpsfmt)), fpsw, fpsh, duration);
+            if ((duration > 0) && (fpsfmt == fmt) && (fpsw == w) && (fpsh == h)) {
+                SDL_AddCameraFormat(add_data, sdlfmt, w, h, duration, 1000000000);
+            }
         }
-
-        i2++;
+#else
+        SDL_AddCameraFormat(add_data, sdlfmt, w, h, 1, 30);
+#endif
     }
 
-    return -1;
+    pACameraMetadata_free(metadata);
 }
 
-static int ANDROIDCAMERA_GetNumDevices(void)
+static SDL_bool FindAndroidCameraDeviceByID(SDL_CameraDevice *device, void *userdata)
 {
-    camera_status_t res;
-    CreateCameraManager();
-
-    if (cameraIdList) {
-        ACameraManager_deleteCameraIdList(cameraIdList);
-        cameraIdList = NULL;
-    }
+    const char *devid = (const char *) userdata;
+    return (SDL_strcmp(devid, (const char *) device->handle) == 0);
+}
 
-    res = ACameraManager_getCameraIdList(cameraMgr, &cameraIdList);
+static void MaybeAddDevice(const char *devid)
+{
+    #if DEBUG_CAMERA
+    SDL_Log("CAMERA: MaybeAddDevice('%s')", devid);
+    #endif
 
-    if (res == ACAMERA_OK) {
-        if (cameraIdList) {
-            return cameraIdList->numCameras;
+    if (SDL_FindPhysicalCameraDeviceByCallback(FindAndroidCameraDeviceByID, (void *) devid)) {
+        return;  // already have this one.
+    }
+
+    const char *posstr = NULL;
+    char *fullname = NULL;
+    CameraFormatAddData add_data;
+    GatherCameraSpecs(devid, &add_data, &fullname, &posstr);
+    if (add_data.num_specs > 0) {
+        char *namecpy = SDL_strdup(devid);
+        if (namecpy) {
+            SDL_CameraDevice *device = SDL_AddCameraDevice(fullname, add_data.num_specs, add_data.specs, namecpy);
+            if (!device) {
+                SDL_free(namecpy);
+            } else if (device && posstr) {
+                SDL_Camera *camera = (SDL_Camera *) device;  // currently there's no separation between physical and logical device.
+                SDL_PropertiesID props = SDL_GetCameraProperties(camera);
+                if (props) {
+                    SDL_SetStringProperty(props, SDL_PROP_CAMERA_POSITION_STRING, posstr);
+                }
+            }
         }
     }
-    return -1;
-}
 
-static int ANDROIDCAMERA_GetDeviceName(SDL_CameraDeviceID instance_id, char *buf, int size)
-{
-    // !!! FIXME: call SDL_SetError()?
-    int index = instance_id - 1;
-    CreateCameraManager();
+    SDL_free(fullname);
+    SDL_free(add_data.specs);
+}
 
-    if (cameraIdList == NULL) {
-        GetNumDevices();
-    }
+// note that camera "availability" covers both hotplugging and whether another
+//  has the device opened, but for something like Android, it's probably fine
+//  to treat both unplugging and loss of access as disconnection events. When
+//  the other app closes the camera, we get an available event as if it was
+//  just plugged back in.
 
-    if (cameraIdList) {
-        if (index >= 0 && index < cameraIdList->numCameras) {
-            SDL_snprintf(buf, size, "%s", cameraIdList->cameraIds[index]);
-            return 0;
-        }
-    }
+static void onCameraAvailable(void *context, const char *cameraId)
+{
+    #if DEBUG_CAMERA
+    SDL_Log("CAMERA: CB onCameraAvailable('%s')", cameraId);
+    #endif
+    SDL_assert(cameraId != NULL);
+    MaybeAddDevice(cameraId);
+}
 
-    return -1;
+static void onCameraUnavailable(void *context, const char *cameraId)
+{
+    #if DEBUG_CAMERA
+    SDL_Log("CAMERA: CB onCameraUnvailable('%s')", cameraId);
+    #endif
+    SDL_assert(cameraId != NULL);
+    SDL_CameraDeviceDisconnected(SDL_FindPhysicalCameraDeviceByCallback(FindAndroidCameraDeviceByID, (void *) cameraId));
 }
 
-static SDL_CameraDeviceID *ANDROIDCAMERA_GetDevices(int *count)
+static const ACameraManager_AvailabilityCallbacks camera_availability_listener = {
+    NULL,
+    onCameraAvailable,
+    onCameraUnavailable
+};
+
+static void ANDROIDCAMERA_DetectDevices(void)
 {
-    // hard-coded list of ID
-    const int num = GetNumDevices();
-    SDL_CameraDeviceID *retval = (SDL_CameraDeviceID *)SDL_malloc((num + 1) * sizeof(*ret));
+    ACameraIdList *list = NULL;
+    camera_status_t res = pACameraManager_getCameraIdList(cameraMgr, &list);
 
-    if (retval == NULL) {
-        *count = 0;
-        return NULL;
-    }
+    if ((res == ACAMERA_OK) && list) {
+        const int total = list->numCameras;
+        for (int i = 0; i < total; i++) {
+            MaybeAddDevice(list->cameraIds[i]);
+        }
 
-    for (int i = 0; i < num; i++) {
-        retval[i] = i + 1;
+        pACameraManager_deleteCameraIdList(list);
     }
-    retval[num] = 0;
-    *count = num;
-    return retval;
+
+    pACameraManager_registerAvailabilityCallback(cameraMgr, &camera_availability_listener);
 }
 
 static void ANDROIDCAMERA_Deinitialize(void)
 {
+    pACameraManager_unregisterAvailabilityCallback(cameraMgr, &camera_availability_listener);
     DestroyCameraManager();
-}
-
-#endif  // __ANDROID_API__ >= 24
 
+    dlclose(libcamera2ndk);
+    libcamera2ndk = NULL;
+    pACameraManager_create = NULL;
+    pACameraManager_registerAvailabilityCallback = NULL;
+    pACameraManager_unregisterAvailabilityCallback = NULL;
+    pACameraManager_getCameraIdList = NULL;
+    pACameraManager_deleteCameraIdList = NULL;
+    pACameraCaptureSession_close = NULL;
+    pACaptureRequest_free = NULL;
+    pACameraOutputTarget_free = NULL;
+    pACameraDevice_close = NULL;
+    pACameraManager_delete = NULL;
+    pACaptureSessionOutputContainer_free = NULL;
+    pACaptureSessionOutput_free = NULL;
+    pACameraManager_openCamera = NULL;
+    pACameraDevice_createCaptureRequest = NULL;
+    pACameraDevice_createCaptureSession = NULL;
+    pACameraManager_getCameraCharacteristics = NULL;
+    pACameraMetadata_free = NULL;
+    pACameraMetadata_getConstEntry = NULL;
+    pACameraCaptureSession_setRepeatingRequest = NULL;
+    pACameraOutputTarget_create = NULL;
+    pACaptureRequest_addTarget = NULL;
+    pACaptureSessionOutputContainer_add = NULL;
+    pACaptureSessionOutputContainer_create = NULL;
+    pACaptureSessionOutput_create = NULL;
+
+    dlclose(libmediandk);
+    libmediandk = NULL;
+    pAImage_delete = NULL;
+    pAImage_getTimestamp = NULL;
+    pAImage_getNumberOfPlanes = NULL;
+    pAImage_getPlaneRowStride = NULL;
+    pAImage_getPlaneData = NULL;
+    pAImageReader_acquireNextImage = NULL;
+    pAImageReader_delete = NULL;
+    pAImageReader_setImageListener = NULL;
+    pAImageReader_getWindow = NULL;
+    pAImageReader_new = NULL;
+}
 
 static SDL_bool ANDROIDCAMERA_Init(SDL_CameraDriverImpl *impl)
 {
-#if __ANDROID_API__ < 24
-    return SDL_FALSE;
-#else
+    // !!! FIXME: slide this off into a subroutine
+    // system libraries are in android-24 and later; we currently target android-16 and later, so check if they exist at runtime.
+    void *libcamera2 = dlopen("libcamera2ndk.so", RTLD_NOW | RTLD_LOCAL);
+    if (!libcamera2) {
+        SDL_Log("CAMERA: libcamera2ndk.so can't be loaded: %s", dlerror());
+        return SDL_FALSE;
+    }
+
+    void *libmedia = dlopen("libmediandk.so", RTLD_NOW | RTLD_LOCAL);
+    if (!libmedia) {
+        SDL_Log("CAMERA: libmediandk.so can't be loaded: %s", dlerror());
+        dlclose(libcamera2);
+        return SDL_FALSE;
+    }
+
+    SDL_bool okay = SDL_TRUE;
+    #define LOADSYM(lib, fn) if (okay) { p##fn = (pfn##fn) dlsym(lib, #fn); if (!p##fn) { SDL_Log("CAMERA: symbol '%s' can't be found in %s: %s", #fn, #lib "ndk.so", dlerror()); okay = SDL_FALSE; } }
+    //#define LOADSYM(lib, fn) p##fn = (pfn##fn) fn
+    LOADSYM(libcamera2, ACameraManager_create);
+    LOADSYM(libcamera2, ACameraManager_registerAvailabilityCallback);
+    LOADSYM(libcamera2, ACameraManager_unregisterAvailabilityCallback);
+    LOADSYM(libcamera2, ACameraManager_getCameraIdList);
+    LOADSYM(libcamera2, ACameraManager_deleteCameraIdList);
+    LOADSYM(libcamera2, ACameraCaptureSession_close);
+    LOADSYM(libcamera2, ACaptureRequest_free);
+    LOADSYM(libcamera2, ACameraOutputTarget_free);
+    LOADSYM(libcamera2, ACameraDevice_close);
+    LOADSYM(libcamera2, ACameraManager_delete);
+    LOADSYM(libcamera2, ACaptureSessionOutputContainer_free);
+    LOADSYM(libcamera2, ACaptureSessionOutput_free);
+    LOADSYM(libcamera2, ACameraManager_openCamera);
+    LOADSYM(libcamera2, ACameraDevice_createCaptureRequest);
+    LOADSYM(libcamera2, ACameraDevice_createCaptureSession);
+    LOADSYM(libcamera2, ACameraManager_getCameraCharacteristics);
+    LOADSYM(libcamera2, ACameraMetadata_free);
+    LOADSYM(libcamera2, ACameraMetadata_getConstEntry);
+    LOADSYM(libcamera2, ACameraCaptureSession_setRepeatingRequest);
+    LOADSYM(libcamera2, ACameraOutputTarget_create);
+    LOADSYM(libcamera2, ACaptureRequest_addTarget);
+    LOADSYM(libcamera2, ACaptureSessionOutputContainer_add);
+    LOADSYM(libcamera2, ACaptureSessionOutputContainer_create);
+    LOADSYM(libcamera2, ACaptureSessionOutput_create);
+    LOADSYM(libmedia, AImage_delete);
+    LOADSYM(libmedia, AImage_getTimestamp);
+    LOADSYM(libmedia, AImage_getNumberOfPlanes);
+    LOADSYM(libmedia, AImage_getPlaneRowStride);
+    LOADSYM(libmedia, AImage_getPlaneData);
+    LOADSYM(libmedia, AImageReader_acquireNextImage);
+    LOADSYM(libmedia, AImageReader_delete);
+    LOADSYM(libmedia, AImageReader_setImageListener);
+    LOADSYM(libmedia, AImageReader_getWindow);
+    LOADSYM(libmedia, AImageReader_new);
+    LOADSYM(libmedia, AImage_getWidth);
+    LOADSYM(libmedia, AImage_getHeight);
+
+    #undef LOADSYM
+
+    if (!okay) {
+        dlclose(libmedia);
+        dlclose(libcamera2);
+    }
+
     if (CreateCameraManager() < 0) {
+        dlclose(libmedia);
+        dlclose(libcamera2);
         return SDL_FALSE;
     }
 
+    libcamera2ndk = libcamera2;
+    libmediandk = libmedia;
+
     impl->DetectDevices = ANDROIDCAMERA_DetectDevices;
     impl->OpenDevice = ANDROIDCAMERA_OpenDevice;
     impl->CloseDevice = ANDROIDCAMERA_CloseDevice;
-    impl->InitDevice = ANDROIDCAMERA_InitDevice;
-    impl->GetDeviceSpec = ANDROIDCAMERA_GetDeviceSpec;
-    impl->StartCamera = ANDROIDCAMERA_StartCamera;
-    impl->StopCamera = ANDROIDCAMERA_StopCamera;
+    impl->WaitDevice = ANDROIDCAMERA_WaitDevice;
     impl->AcquireFrame = ANDROIDCAMERA_AcquireFrame;
     impl->ReleaseFrame = ANDROIDCAMERA_ReleaseFrame;
-    impl->GetNumFormats = ANDROIDCAMERA_GetNumFormats;
-    impl->GetFormat = ANDROIDCAMERA_GetFormat;
-    impl->GetNumFrameSizes = ANDROIDCAMERA_GetNumFrameSizes;
-    impl->GetFrameSize = ANDROIDCAMERA_GetFrameSize;
-    impl->GetDeviceName = ANDROIDCAMERA_GetDeviceName;
-    impl->GetDevices = ANDROIDCAMERA_GetDevices;
+    impl->FreeDeviceHandle = ANDROIDCAMERA_FreeDeviceHandle;
     impl->Deinitialize = ANDROIDCAMERA_Deinitialize;
 
+    impl->ProvidesOwnCallbackThread = SDL_TRUE;
+
     return SDL_TRUE;
-#endif
 }
 
 CameraBootStrap ANDROIDCAMERA_bootstrap = {
@@ -709,4 +889,3 @@ CameraBootStrap ANDROIDCAMERA_bootstrap = {
 };
 
 #endif
-